diff --git a/LICENSE b/LICENSE index 2d5fb65..0863f87 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2022 Brown University Scientists for a Sustainable World +Copyright (c) 2023 Brown University Scientists for a Sustainable World Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/aaaaaaaaaaaa/Rovertest/RenogyRover.cpp b/aaaaaaaaaaaa/Rovertest/RenogyRover.cpp new file mode 100644 index 0000000..b608d07 --- /dev/null +++ b/aaaaaaaaaaaa/Rovertest/RenogyRover.cpp @@ -0,0 +1,289 @@ +/* + RenogyRover.cpp - Library for monitoring Renogy Rover 20/40 AMP MPPT controller + Created by hirschi-dev, November 28, 2020 + Released into the public domain +*/ + +#include "RenogyRover.h" + +RenogyRover::RenogyRover() { + _client = ModbusMaster(); + _modbusId = 1; +} + +RenogyRover::RenogyRover(int modbusId) { + _client = ModbusMaster(); + RenogyRover::_modbusId = modbusId; +} + +ModbusMaster RenogyRover::getModbusClient() { + return _client; +} + +void RenogyRover::begin(Stream& serial) { + _client.begin(_modbusId, serial); +} + +const char* RenogyRover::getLastModbusError() { + switch(_lastError) { + case _client.ku8MBIllegalDataAddress: + return "Illegal data address"; + case _client.ku8MBIllegalDataValue: + return "Illegal data value"; + case _client.ku8MBIllegalFunction: + return "Illegal function"; + case _client.ku8MBSlaveDeviceFailure: + return "Slave device failure"; + case _client.ku8MBSuccess: + return "Success"; + case _client.ku8MBInvalidSlaveID: + return "Invalid slave ID: The slave ID in the response does not match that of the request."; + case _client.ku8MBInvalidFunction: + return "Invalid function: The function code in the response does not match that of the request."; + case _client.ku8MBResponseTimedOut: + return "Response timed out"; + case _client.ku8MBInvalidCRC: + return "InvalidCRC"; + default: + return "Unknown error"; + } +} + +int RenogyRover::getProductModel(char*& productModel) { + int registerBase = 0x000C; + int registerLength = 8; + + uint16_t* values = new uint16_t[registerLength]; + productModel = new char[registerLength * 2 + 1]; + + if (!_readHoldingRegisters(registerBase, registerLength, values)) { + delete [] values; + return 0; + } else { + // convert uint16_t to int8_t, + // higher and lower byte need to be switched + int j = 0; + for (int i = 0; i < registerLength; i++) { + productModel[j++] = values[i] >> 8; + productModel[j++] = values[i]; + } + // append null terminator and slice first two chars as they are spaces + productModel[16] = '\0'; + productModel = &productModel[2]; + } + + delete [] values; + return 1; +} + +int RenogyRover::getPanelState(PanelState* state) { + state->chargingPower = 0; + state->current = 0; + state->voltage = 0; + + int registerBase = 0x0107; + int registerLength = 3; + + uint16_t* values = new uint16_t[registerLength]; + + if (!_readHoldingRegisters(registerBase, registerLength, values)) { + delete [] values; + return 0; + } else { + state->voltage = (int16_t) values[0] * 0.1f; + state->current = (int16_t) values[1] * 0.01f; + state->chargingPower = (int16_t) values[2]; + } + + delete [] values; + return 1; +} + +int RenogyRover::getBatteryState(BatteryState* state) { + state->batteryTemperature = 0; + state->chargingCurrent = 0; + state->controllerTemperature = 0; + state->stateOfCharge = 0; + state->batteryVoltage = 0; + + int registerBase = 0x0100; + int registerLength = 4; + + uint16_t* values = new uint16_t[registerLength]; + + if (!_readHoldingRegisters(registerBase, registerLength, values)) { + delete [] values; + return 0; + } else { + state->stateOfCharge = (int16_t) values[0]; + state->batteryVoltage = (int16_t) values[1] * 0.1f; + state->chargingCurrent = (int16_t) values[2] * 0.01f; + + // temperatures are in signed magnitude notation + state->batteryTemperature = _convertSignedMagnitude(values[3]); + state->controllerTemperature = _convertSignedMagnitude(values[3] >> 8); + } + + delete [] values; + return 1; +} + +int RenogyRover::getDayStatistics(DayStatistics* params) { + params->batteryVoltageMaxForDay = 0; + params->batteryVoltageMinForDay = 0; + params->chargingAmpHoursForDay = 0; + params->maxChargeCurrentForDay = 0; + params->maxChargePowerForDay = 0; + params->dischargingAmpHoursForDay = 0; + params->maxDischargeCurrentForDay = 0; + params->maxDischargePowerForDay = 0; + params->powerGenerationForDay = 0; + params->powerConsumptionForDay = 0; + + int registerBase = 0x010B; + int registerLength = 10; + + uint16_t* values = new uint16_t[registerLength]; + + if (!_readHoldingRegisters(registerBase, registerLength, values)) { + delete [] values; + return 0; + } else { + params->batteryVoltageMinForDay = (int16_t) values[0] * 0.1f; + params->batteryVoltageMaxForDay = (int16_t) values[1] * 0.1f; + params->maxChargeCurrentForDay = (int16_t) values[2] * 0.01f; + params->maxDischargeCurrentForDay = (int16_t) values[3] * 0.01f; + params->maxChargePowerForDay = (int16_t) values[4]; + params->maxDischargePowerForDay = (int16_t) values[5]; + params->chargingAmpHoursForDay= (int16_t) values[6]; + params->dischargingAmpHoursForDay = (int16_t) values[7]; + params->powerGenerationForDay = (int16_t) values[8]; + params->powerConsumptionForDay = (int16_t) values[9]; + } + + delete [] values; + return 1; +} + +int RenogyRover::getHistoricalStatistics(HistStatistics* stats) { + stats->batChargingAmpHours = 0; + stats->batDischargingAmpHours = 0; + stats->batFullCharges = 0; + stats->batOverDischarges = 0; + stats->operatingDays = 0; + stats->powerConsumed = 0; + stats->powerGenerated = 0; + + int registerBase = 0x0115; + int registerLength = 3; + + uint16_t* values = new uint16_t[registerLength]; + + if (!_readHoldingRegisters(registerBase, registerLength, values)) { + delete [] values; + return 0; + } else { + stats->operatingDays = (int16_t) values[0]; + stats->batOverDischarges = (int16_t) values[1]; + stats->batFullCharges = (int16_t) values[2]; + } + + registerBase = 0x118; + registerLength = 8; + + delete [] values; + + values = new uint16_t[registerLength]; + uint32_t* integers = new uint32_t[registerLength / 2]; + + if (!_readHoldingRegisters(registerBase, registerLength, values)) { + delete [] values; + delete [] integers; + return 0; + } else { + int j = 0; + for (int i = 0; i < registerLength / 2; i++) { + integers[i] = ((int32_t) values[j++]) << 8; + integers[i] = integers[i] | ((int32_t) values[j++]); + } + + stats->batChargingAmpHours = integers[0]; + stats->batDischargingAmpHours = integers[1]; + stats->powerGenerated = integers[2] / 10000.0f; + stats->powerConsumed = integers[3] / 10000.0f; + } + + delete [] values; + delete [] integers; + return 1; +}; + +int RenogyRover::getChargingState(ChargingState* state) { + state->chargingMode = ChargingMode::UNDEFINED; + state->streetLightBrightness = 0; + state->streetLightState = 0; + + int registerBase = 0x0120; + int registerLength = 1; + + uint16_t* values = new uint16_t; + + if (!_readHoldingRegisters(registerBase, registerLength, values)) { + delete [] values; + return 0; + } else { + state->streetLightState = (*values >> 15) & 1U; + state->streetLightBrightness = (*values >> 8) & ~(1U << 7); + state->chargingMode = ChargingMode((uint8_t) *values); + } + + delete [] values; + return 1; +} + +int RenogyRover::getErrors(int& errors) { + int registerBase = 0x0121; + int registerLength = 2; + + uint16_t* values = new uint16_t[registerLength]; + + if (!_readHoldingRegisters(registerBase, registerLength, values)) { + delete [] values; + return 0; + } + + // 16 lower bits are reserved + // highest bit is reserved + errors = (values[0] << 1U) >> 1U; + + delete [] values; + return 1; +} + +int RenogyRover::setStreetLight(int state) { + if (state > 1 || state < 0) { + return 0; + } + + _lastError = _client.writeSingleRegister(0x010A, (uint16_t) state); + return _lastError == _client.ku8MBSuccess; +} + +int RenogyRover::_readHoldingRegisters(int base, int length, uint16_t*& values) { + _lastError = _client.readHoldingRegisters(base, length); + if(_lastError != _client.ku8MBSuccess) { + return 0; + } else { + for(uint8_t i = 0x00; i < (uint16_t) length; i++){ + values[i] = _client.getResponseBuffer(i); + } + } + return 1; +} + +int8_t RenogyRover::_convertSignedMagnitude(uint8_t val) { + if (val & 0x80) { + return -(val & 0x7F); + } + return val; +} diff --git a/aaaaaaaaaaaa/Rovertest/RenogyRover.h b/aaaaaaaaaaaa/Rovertest/RenogyRover.h new file mode 100644 index 0000000..81d37fb --- /dev/null +++ b/aaaaaaaaaaaa/Rovertest/RenogyRover.h @@ -0,0 +1,110 @@ +/* + RenogyRover.h - Library for monitoring Renogy Rover 20/40 AMP MPPT controller + Created by hirschi-dev, November 28, 2020 + Released into the public domain +*/ + +#ifndef RenogyRover_h +#define RenogyRover_h + +#include +#include + +enum ChargingMode { + UNDEFINED = -1, + DEACTIVATED = 0, + ACTIVATED = 1, + MPPT = 2, + EQUALIZING = 3, + BOOST = 4, + FLOATING = 5, + OVERPOWER = 6 +}; + +enum FaultCode { + BAT_OVER_DISCHARGE = 1, + BAT_OVER_VOLTAGE = 2, + BAT_UNDER_VOLTAGE_WARNING = 4, + LOAD_SHORT = 8, + LOAD_OVERPOWER = 16, + CONTROLLER_TEMP_HIGH = 32, + AMBIENT_TEMP_HIGH = 64, + PV_OVERPOWER = 128, + PV_SHORT = 256, + PV_OVER_VOLTAGE = 512, + PV_COUNTER_CURRENT = 1024, + PV_WP_OVER_VOLTAGE = 2048, + PV_REVERSE_CONNECTED = 4096, + ANTI_REVERSE_MOS_SHORT = 8192, + CHARGE_MOS_SHORT = 16384 +}; + +struct PanelState { + float voltage; + float current; + float chargingPower; +}; + +struct BatteryState { + int stateOfCharge; + float batteryVoltage; + float chargingCurrent; + float controllerTemperature; + float batteryTemperature; +}; + +struct DayStatistics { + float batteryVoltageMinForDay; + float batteryVoltageMaxForDay; + float maxChargeCurrentForDay; + float maxDischargeCurrentForDay; + float maxChargePowerForDay; + float maxDischargePowerForDay; + float chargingAmpHoursForDay; + float dischargingAmpHoursForDay; + float powerGenerationForDay; + float powerConsumptionForDay; +}; + +struct HistStatistics { + int operatingDays; + int batOverDischarges; + int batFullCharges; + int batChargingAmpHours; + int batDischargingAmpHours; + float powerGenerated; + float powerConsumed; +}; + +struct ChargingState { + int streetLightState; + int streetLightBrightness; + ChargingMode chargingMode; +}; + +class RenogyRover { + public: + RenogyRover(); + RenogyRover(int modbusId); + ModbusMaster getModbusClient(); + void begin(Stream& serial); + const char* getLastModbusError(); + + int getProductModel(char*& productModel); + int getPanelState(PanelState* state); + int getBatteryState(BatteryState* state); + int getDayStatistics(DayStatistics* dayStats); + int getHistoricalStatistics(HistStatistics* histStats); + int getChargingState(ChargingState* chargingState); + int getErrors(int& errors); + + int setStreetLight(int state); + private: + ModbusMaster _client; + int _modbusId; + uint8_t _lastError; + int _readHoldingRegisters(int base, int length, uint16_t*& values); + int8_t _convertSignedMagnitude(uint8_t val); +}; + +#endif \ No newline at end of file diff --git a/src/Rovertest.ino b/aaaaaaaaaaaa/Rovertest/Rovertest.ino similarity index 100% rename from src/Rovertest.ino rename to aaaaaaaaaaaa/Rovertest/Rovertest.ino diff --git a/aaaaaaaaaaaa/a.ino b/aaaaaaaaaaaa/a.ino index 0aecc8c..b2ad258 100644 --- a/aaaaaaaaaaaa/a.ino +++ b/aaaaaaaaaaaa/a.ino @@ -28,7 +28,7 @@ void setup() Serial.println("WiFi connected."); // Init and get the time - configTime(gmtOffset_sec, 3600, ntpServer); + configTime(gmtOffset_sec, 0, ntpServer); } void loop() diff --git a/aaaaaaaaaaaa/aaaa/main.cpp b/aaaaaaaaaaaa/aaaa/main.cpp new file mode 100644 index 0000000..e69de29 diff --git a/aaaaaaaaaaaa/aaaa/main.h b/aaaaaaaaaaaa/aaaa/main.h new file mode 100644 index 0000000..343b1b6 --- /dev/null +++ b/aaaaaaaaaaaa/aaaa/main.h @@ -0,0 +1,16 @@ +#ifndef MAIN_H +#define MAIN_H +#include +#include +#include +FirebaseData fbdo; +FirebaseAuth auth; +FirebaseConfig config; + +void setup() +{ + Serial.begin(115200); + WiFi.begin(wifi_ssid); +} + +#endif // MAIN_H diff --git a/aaaaaaaaaaaa/scmc/dashboard.ino b/aaaaaaaaaaaa/scmc/dashboard.ino new file mode 100644 index 0000000..64c5ad5 --- /dev/null +++ b/aaaaaaaaaaaa/scmc/dashboard.ino @@ -0,0 +1,44 @@ +#include +#include // Enable this line if using Arduino Uno, Mega, etc. +#include +#include "Adafruit_LEDBackpack.h" + +Adafruit_7segment matrix = Adafruit_7segment(); +const byte lct = 12; //LedCounT +const byte neopixelPin = 23; +CRGB leds[lct]; +void setupDashboard() { + // matrix.begin(0x70); + setupDashboardLEDS(); +} +void setupDashboardLEDS() { + FastLED.addLeds(leds, lct); +} +void runDashboard() { + displayDashboardBatIndicator(liveBatPer); + // printToDisplay(liveUseW); +} + +void printToDisplay(int wattage) { + matrix.print(wattage, DEC); + matrix.writeDisplay(); +} + +void displayDashboardBatIndicator(int percent) { + memset(leds, CRGB(0, 0, 0), sizeof(leds)); + percent = remapPercent(percent, BatteryDischargeFloor); + int nb = round((float) percent / 100.0 * (float) lct); + for (int i = 0; i < nb; i++) { + if (percent <= LowBatteryThreshold) { + leds[i] = CRGB(255, 0, 0); + } else if (percent <= MidBatteryThreshold) { + leds[i] = CRGB(255, 255, 0); + } else { + leds[i] = CRGB(0, 255, 0); + } + } + FastLED.show(); +} +int remapPercent(int input, int percentFloor) { + return map(input, percentFloor, 100, 0, 100); +} diff --git a/aaaaaaaaaaaa/scmc/email.ino b/aaaaaaaaaaaa/scmc/email.ino new file mode 100644 index 0000000..8379d4c --- /dev/null +++ b/aaaaaaaaaaaa/scmc/email.ino @@ -0,0 +1,29 @@ +SMTPSession smtp; +boolean sendEmail(const char* subject, const char* text) { + ESP_Mail_Session session; + session.server.host_name = "smtp.gmail.com"; + session.server.port = 465; + session.login.email = email_address; + session.login.password = email_password; + + SMTP_Message message; + message.enable.chunking = true; + message.sender.name = "Solar Charger"; + message.subject = subject; + message.sender.email = email_address; + + message.addRecipient("", email_address); //send to self + + message.text.content = text; + message.priority = esp_mail_smtp_priority::esp_mail_smtp_priority_normal; + + if (!smtp.connect(&session)) { + Serial.println("error connecting to server with session config"); + return false; + } + if (!MailClient.sendMail(&smtp, &message)) { + Serial.println("Error sending Email, " + smtp.errorReason()); + return false; + } + return true; +} diff --git a/aaaaaaaaaaaa/scmc/firebase.ino b/aaaaaaaaaaaa/scmc/firebase.ino new file mode 100644 index 0000000..76f91fb --- /dev/null +++ b/aaaaaaaaaaaa/scmc/firebase.ino @@ -0,0 +1,204 @@ +FirebaseData fbdo; //firebase data object +FirebaseAuth fauth; +FirebaseConfig fconfig; +FirebaseJson fjson; +FirebaseJsonData jsonData; + +void connectFirebase() { + fconfig.host = FIREBASE_URL; + fconfig.api_key = FIREBASE_API_KEY; + fauth.user.email = FIREBASE_EMAIL; + fauth.user.password = FIREBASE_PASSWORD; + Firebase.begin(&fconfig, &fauth); + fbdo.setResponseSize(4096); + Firebase.RTDB.setReadTimeout(&fbdo, 1000 * 4); + Firebase.RTDB.setwriteSizeLimit(&fbdo, "medium"); + Firebase.RTDB.setMaxRetry(&fbdo, 1); + Firebase.setFloatDigits(3); +} +boolean firebaseGetSettings() { + if (Firebase.RTDB.getJSON(&fbdo, "/settings")) { + fjson = fbdo.jsonObject(); + + + fjson.get(jsonData, "/BatteryDischargeFloor"); + if (jsonData.type == "int") { + BatteryDischargeFloor = jsonData.intValue; + } else { + Serial.println("ERROR! Firebase (settings) BatteryDischargeFloor type wrong"); + } + + fjson.get(jsonData, "/LowBatteryThreshold"); + if (jsonData.type == "int") { + LowBatteryThreshold = jsonData.intValue; + } else { + Serial.println("ERROR! Firebase (settings) LowBatteryThreshold type wrong"); + } + + fjson.get(jsonData, "/MidBatteryThreshold"); + if (jsonData.type == "int") { + MidBatteryThreshold = jsonData.intValue; + } else { + Serial.println("ERROR! Firebase (settings) MidBatteryThreshold type wrong"); + } + + fjson.get(jsonData, "/liveDataUpdateIntervalMillis"); + if (jsonData.type == "int") { + liveDataUpdateMillisInterval = jsonData.intValue; + } else { + Serial.println("ERROR! Firebase (settings) liveDataUpdateIntervalMillis type wrong"); + } + + fjson.clear(); + return true; + } else { + Serial.println("ERROR! Firebase (settings)"); + Serial.println(fbdo.errorReason()); + return false; + } +} +boolean firebaseSendDebug() { + fjson.clear(); + fjson.set("local ip", WiFi.localIP().toString()); + fjson.set("mac address", WiFi.macAddress()); + fjson.set("OTAEnabled (read)", otaEnable); + fjson.set("time", int(timestampEpoch)); + fjson.set("minAfterMidnight", minutesAfterMidnight()); + fjson.set("sunrise", sunriseTime()); + fjson.set("sunset", sunsetTime()); + fjson.set("liveDataUpdateMillisInterval (read)", int(liveDataUpdateMillisInterval)); + fjson.set("seconds since boot", int(millis() / 1000)); + + if (Firebase.RTDB.updateNodeSilent(&fbdo, "/debug", &fjson)) { + return true; + } else { + Serial.println("ERROR! Firebase (sendDebug)"); + Serial.println(fbdo.errorReason()); + return false; + } +} +boolean firebaseRecvDebug() { + if (Firebase.RTDB.getJSON(&fbdo, "/debug")) { + fjson = fbdo.jsonObject(); + + fjson.get(jsonData, "/EnableOTA"); + if (jsonData.typeNum == FirebaseJson::JSON_BOOL) { + otaEnable = jsonData.boolValue; + } else { + Serial.println("ERROR! Firebase (RecvDebug) EnableOTA type wrong"); + } + + //add debug data to recieve + + fjson.get(jsonData, "/REBOOT"); + if (jsonData.typeNum == FirebaseJson::JSON_BOOL) { + if (jsonData.boolValue) { + fjson.set("/REBOOT", false); + Firebase.RTDB.updateNodeSilent(&fbdo, "/debug", &fjson); + Serial.println("REBOOTING! (settings/REBOOT equaled true in firebase)"); + rebootESP32(); + } + } else { + Serial.println("ERROR! Firebase (RecvDebug) REBOOT type wrong"); + } + + } else { + Serial.println("ERROR! Firebase (get debug)"); + return false; + } + return true; +} +boolean firebaseSendLiveData() { + fjson.clear(); + fjson.set("Available", available); + fjson.set("WGen", liveGenW); + fjson.set("WUse", liveUseW); + fjson.set("bat%", liveBatPer); + fjson.set("cumulativeWhGen", cumulativeWhGen); + fjson.set("time", int(timestampEpoch)); + + if (Firebase.RTDB.setJSON(&fbdo, "/data/liveData", &fjson)) { + return true; + } + else { + Serial.println("ERROR! Firebase (live)"); + Serial.println(fbdo.errorReason()); + return false; + } +} + +boolean firebaseDeleteOldData(String path, unsigned long interval, byte num) { + boolean report = true; + for (byte i_i = 0; i_i < num; i_i++) {//run multiple times so the number of entries can be reduced not just mantained + if (timeAvailable) { //if there's an error with getting the time, don't do anything + QueryFilter fquery; + fquery.orderBy("$key"); + fquery.limitToFirst(1); + + if (Firebase.RTDB.getJSON(&fbdo, path.c_str(), &fquery)) { + fjson = fbdo.jsonObject(); + + fjson.iteratorBegin(); + String key; String value; int type; + fjson.iteratorGet(0, type, key, value); + + FirebaseJsonData timeJsonData; + fjson.get(timeJsonData, key + "/time"); + + if (!timeJsonData.success || timeJsonData.intValue < timestampEpoch - interval) { //old data + String nodeToDelete = path + key; + if (!Firebase.RTDB.deleteNode(&fbdo, nodeToDelete.c_str())) { + Serial.println("ERROR! Firebase (delete delete)"); + Serial.println(fbdo.errorReason()); + report = false; + } + } + fjson.iteratorEnd(); + fjson.clear(); + + } + else { + //Failed to get JSON data at defined node, print out the error reason + Serial.println("ERROR! Firebase (delete)"); + Serial.println(fbdo.errorReason()); + report = false; + } + fquery.clear(); + } + } + return report; +} + +boolean firebaseSendDayData() { + fjson.clear(); + fjson.set("WGen", liveGenW); + fjson.set("WUse", liveUseW); + fjson.set("bat%", liveBatPer); + fjson.set("time", int(timestampEpoch)); + + if (Firebase.RTDB.pushJSON(&fbdo, "/data/dayData", &fjson)) { + return true; + } + else { + Serial.println("ERROR! Firebase (day)"); + Serial.println(fbdo.errorReason()); + return false; + } +} + +boolean firebaseSendMonthData() { + fjson.clear(); + fjson.set("WhGen", dayGenWh); + fjson.set("WhUse", dayUseWh); + fjson.set("hUsed", dayHoursUsed); + fjson.set("time", int(timestampEpoch)); + + if (Firebase.RTDB.pushJSON(&fbdo, "/data/monthData", &fjson)) { + return true; + } + else { + Serial.println("ERROR! Firebase (month)"); + Serial.println(fbdo.errorReason()); + return false; + } +} diff --git a/aaaaaaaaaaaa/scmc/ota.ino b/aaaaaaaaaaaa/scmc/ota.ino new file mode 100644 index 0000000..a702626 --- /dev/null +++ b/aaaaaaaaaaaa/scmc/ota.ino @@ -0,0 +1,34 @@ +#include +#include //This makes it possible to upload new code over wifi +void runOTA() { + if (otaEnable) { + ArduinoOTA.handle(); + } +} +void setupOTA() { + // ArduinoOTA.setPort(3232); + ArduinoOTA.setHostname(OTA_hostname); + ArduinoOTA.setPassword(OTA_password); + + ArduinoOTA + .onStart([]() { + Serial.println("OTA starting"); + setSafe(); + }) + .onEnd([]() { + rebootESP32(); + //Serial.println("\n OTA ENDED!"); + }) + .onProgress([](unsigned int progress, unsigned int total) { + // Serial.printf("Progress: %u%%\r", (progress / (total / 100))); + }) + .onError([](ota_error_t error) { + Serial.printf("Error[%u]: ", error); + if (error == OTA_AUTH_ERROR) Serial.println("Auth Failed"); + else if (error == OTA_BEGIN_ERROR) Serial.println("Begin Failed"); + else if (error == OTA_CONNECT_ERROR) Serial.println("Connect Failed"); + else if (error == OTA_RECEIVE_ERROR) Serial.println("Receive Failed"); + else if (error == OTA_END_ERROR) Serial.println("End Failed"); + }); + ArduinoOTA.begin(); +} diff --git a/aaaaaaaaaaaa/scmc/scmc.ino b/aaaaaaaaaaaa/scmc/scmc.ino new file mode 100644 index 0000000..e6ae87a --- /dev/null +++ b/aaaaaaaaaaaa/scmc/scmc.ino @@ -0,0 +1,232 @@ +/** + Solar Charger Monitoring Code + This program monitors a solar power system and runs a physical and online dashboard. + microcontroller: esp32 + charge controller: Renogy ROVER ELITE 40A MPPT + + Using library WiFi at version 1.0 in folder: C:\Users\Joshua\AppData\Local\Arduino15\packages\esp32\hardware\esp32\1.0.6\libraries\WiFi + Using library ESP_Mail_Client at version 1.6.4 in folder: C:\Users\Joshua\Documents\Arduino\libraries\ESP_Mail_Client + Using library SD at version 1.0.5 in folder: C:\Users\Joshua\AppData\Local\Arduino15\packages\esp32\hardware\esp32\1.0.6\libraries\SD + Using library FS at version 1.0 in folder: C:\Users\Joshua\AppData\Local\Arduino15\packages\esp32\hardware\esp32\1.0.6\libraries\FS + Using library SPI at version 1.0 in folder: C:\Users\Joshua\AppData\Local\Arduino15\packages\esp32\hardware\esp32\1.0.6\libraries\SPI + Using library SPIFFS at version 1.0 in folder: C:\Users\Joshua\AppData\Local\Arduino15\packages\esp32\hardware\esp32\1.0.6\libraries\SPIFFS + Using library Dusk2Dawn at version 1.0.1 in folder: C:\Users\Joshua\Documents\Arduino\libraries\Dusk2Dawn + Using library Firebase_Arduino_Client_Library_for_ESP8266_and_ESP32 at version 2.6.7 in folder: C:\Users\Joshua\Documents\Arduino\libraries\Firebase_Arduino_Client_Library_for_ESP8266_and_ESP32 + Using library WiFiClientSecure at version 1.0 in folder: C:\Users\Joshua\AppData\Local\Arduino15\packages\esp32\hardware\esp32\1.0.6\libraries\WiFiClientSecure + Using library FastLED at version 3.5.0 in folder: C:\Users\Joshua\Documents\Arduino\libraries\FastLED + Using library Wire at version 1.0.1 in folder: C:\Users\Joshua\AppData\Local\Arduino15\packages\esp32\hardware\esp32\1.0.6\libraries\Wire + Using library Adafruit_GFX_Library at version 1.10.12 in folder: C:\Users\Joshua\Documents\Arduino\libraries\Adafruit_GFX_Library + Using library Adafruit_LED_Backpack_Library at version 1.3.2 in folder: C:\Users\Joshua\Documents\Arduino\libraries\Adafruit_LED_Backpack_Library + Using library Adafruit_BusIO at version 1.10.1 in folder: C:\Users\Joshua\Documents\Arduino\libraries\Adafruit_BusIO + Using library ESPmDNS at version 1.0 in folder: C:\Users\Joshua\AppData\Local\Arduino15\packages\esp32\hardware\esp32\1.0.6\libraries\ESPmDNS + Using library ArduinoOTA at version 1.0 in folder: C:\Users\Joshua\AppData\Local\Arduino15\packages\esp32\hardware\esp32\1.0.6\libraries\ArduinoOTA + Using library Update at version 1.0 in folder: C:\Users\Joshua\AppData\Local\Arduino15\packages\esp32\hardware\esp32\1.0.6\libraries\Update + + + Made by members of Brown University club "Scientists for a Sustainable World" (s4sw@brown.edu) 2021- + https://github.com/brown-SSW/brown-solar-charger +*/ +#include +#include //used for sending stuff over the wifi radio within the esp32 +#include //library for sending email https://github.com/mobizt/ESP-Mail-Client +#include //sunrise sunset lookup https://github.com/dmkishi/Dusk2Dawn DOWNLOAD ZIP OF MASTER AND INSTALL MANUALLY SINCE THE RELEASE (1.0.1) IS OUTDATED AND DOESN'T COMPILE! +#include //firebase library https://github.com/mobizt/Firebase-ESP-Client + +#include "secret.h" //passwords and things we don't want public can be kept in this file since it doesn't upload to github (in gitignore) The file should be kept somewhat secure. +const byte LED_BUILTIN = 2; //esp32s have a blue light on pin 2, can be nice for status and debugging + +boolean wifiAvailable = false; +boolean timeAvailable = false; +boolean firebaseAvailable = false; +boolean firebaseStarted = false; + +boolean firebaseAvailSettings = true; +boolean firebaseAvailSendDebug = true; +boolean firebaseAvailRecvDebug = true; +boolean firebaseAvailSendLive = true; +boolean firebaseAvailDeleteDay = true; +boolean firebaseAvailDay = true; +boolean firebaseAvailDeleteMonth = true; +boolean firebaseAvailMonth = true; + + +boolean muteAllAlerts = true; +boolean firebaseAvailableAlerted = true; + +boolean firebaseRanSomething = false; + +boolean otaEnable = true; + +const int8_t UTC_offset = -5;//EST (not daylight time) +Dusk2Dawn sunTime(41.82, -71.40, UTC_offset);//latitude,longitude of Brown +struct tm timeClock; //used for keeping track of what time it is (in EST not toggling daylight savings) +time_t timestampEpoch; //internet time +boolean timeConfiged = false; + +unsigned long wifiCheckUpdateMillis = 0; + +//settings (from database) +int BatteryDischargeFloor = 50; +int LowBatteryThreshold = 20; +int MidBatteryThreshold = 40; + +//live data +float liveGenW = 0.0; +float liveUseW = 0.0; +float liveBatPer = 0.0; +boolean available = false; +int cumulativeWhGen = 0; +float cumuWhGenHelper = 0.0; +//day cumulative +float dayUseWh = 0.0; +float dayGenWh = 0.0; +float dayHoursUsed = 0; + +unsigned long lastLiveUpdateMillis = 0; +long liveDataUpdateMillisInterval = 30000; + +unsigned long lastDayDataUpdateMillis = 0; +long dayDataUpdateMillisInterval = 30000; + +unsigned long lastMonthDataUpdateMillis = 0; +long monthDataUpdateMillisInterval = 100000; //change to daily timer + +unsigned long lastCalcUpdateMillis = 0; +long calcUpdateMillisInterval = 1000; + +unsigned long lastLoadSettingsMillis = 0; +long loadSettingsMillisInterval = 60000; + +long wifiCheckIntervalMillis = 5000; + +void setup() { + pinMode(LED_BUILTIN, OUTPUT); + digitalWrite(LED_BUILTIN, HIGH); + Serial.begin(115200); + + //set up pin modes and hardware connections. + setSafe();//everything should be in the safest state until the code has fully started + setupWifi(); + setupOTA(); + setupDashboard(); + Serial.println("STARTING Loop!"); + digitalWrite(LED_BUILTIN, LOW); //one time setup finished +} + +void loop() { + //main loop code! runs continuously + wifiAvailable = checkWifiConnection(); + timeAvailable = updateTimeClock(); + + runCalc(); + + if (!firebaseStarted && wifiAvailable) { + connectFirebase(); + firebaseStarted = true; + } + runLiveUpdate(); + runDayDataUpdate(); + runMonthDataUpdate(); + runSettingsDebugUpdate(); + if (firebaseRanSomething && firebaseStarted) { + firebaseAvailable = + firebaseAvailSettings && + firebaseAvailSendDebug && + firebaseAvailRecvDebug && + firebaseAvailSendLive && + firebaseAvailDeleteDay && + firebaseAvailDay && + firebaseAvailDeleteMonth && + firebaseAvailMonth; + } + + if (firebaseAvailable) { + firebaseAvailableAlerted = false; + } else { + if (!firebaseAvailableAlerted) { + if (!muteAllAlerts) { + sendEmail("[ALERT] solar charging station error", "Firebase not available"); + } + Serial.println("[ALERT]: Firebase not available"); + firebaseAvailableAlerted = true; + } + } + runDashboard(); + runOTA(); + vTaskDelay(20); +} + +void setSafe() { + //put everything in the safest possible state (when booting, in detected error state, or reprogramming) + Serial.println("setting safe"); +} + +void runCalc() { + if (millis() - lastCalcUpdateMillis > calcUpdateMillisInterval) { + cumuWhGenHelper += 1.0 * liveGenW * (millis() - lastCalcUpdateMillis) / (1000 * 60 * 60); + cumulativeWhGen += long(cumuWhGenHelper); + cumuWhGenHelper -= long(cumuWhGenHelper); + lastCalcUpdateMillis = millis(); + } +} + +void runLiveUpdate() { + if (millis() - lastLiveUpdateMillis > liveDataUpdateMillisInterval) { + lastLiveUpdateMillis = millis(); + firebaseRanSomething = true; + if (timeAvailable) { // don't want to give inaccurate timestamps + firebaseAvailSendLive = firebaseSendLiveData(); + } + } +} + +void runSettingsDebugUpdate() { + if (millis() - lastLoadSettingsMillis > loadSettingsMillisInterval) { + lastLoadSettingsMillis = millis(); + firebaseRanSomething = true; + firebaseAvailSettings = firebaseGetSettings(); + firebaseAvailRecvDebug = firebaseRecvDebug(); + firebaseAvailSendDebug = firebaseSendDebug(); + } +} + +void runDayDataUpdate() { + if (millis() - lastDayDataUpdateMillis > dayDataUpdateMillisInterval) { + lastDayDataUpdateMillis = millis(); + firebaseRanSomething = true; + if (timeAvailable) { + firebaseAvailDay = firebaseSendDayData(); + firebaseAvailDeleteDay = firebaseDeleteOldData("/data/dayData/", 24 * 60 * 60, 2); + } + + liveGenW = 10000; + liveUseW = random(5, 500); + liveBatPer = random(50, 100); + } +} + +void runMonthDataUpdate() { + + if (millis() - lastMonthDataUpdateMillis > monthDataUpdateMillisInterval) { + lastMonthDataUpdateMillis = millis(); + firebaseRanSomething = true; + if (timeAvailable) { + firebaseAvailMonth = firebaseSendMonthData(); + firebaseAvailDeleteMonth = firebaseDeleteOldData("/data/monthData/", 31 * 24 * 60 * 60, 2); + } + //after testing, these variables should actually be reset to 0 for the next day + dayGenWh = random(1400, 2000); + dayUseWh = random(500, 3000); + dayHoursUsed = random(0, 60) / 10.0; + + } +} + +void rebootESP32() { + setSafe(); + Serial.flush(); + WiFi.disconnect(true); + WiFi.mode(WIFI_OFF); + delay(1000); + ESP.restart(); +} diff --git a/aaaaaaaaaaaa/scmc/sunTime.ino b/aaaaaaaaaaaa/scmc/sunTime.ino new file mode 100644 index 0000000..f57589a --- /dev/null +++ b/aaaaaaaaaaaa/scmc/sunTime.ino @@ -0,0 +1,34 @@ +int minutesAfterMidnight() { + //note that daylight savings time is not going to be toggled + return timeClock.tm_hour * 60 + timeClock.tm_min; +} +int sunriseTime() { + //timeClock is zero indexed, sunTime needs standard month and day ranges + return sunTime.sunrise(timeClock.tm_year + 1900, timeClock.tm_mon + 1, timeClock.tm_mday + 1, false); +} +int sunsetTime() { + return sunTime.sunset(timeClock.tm_year + 1900, timeClock.tm_mon + 1, timeClock.tm_mday + 1, false); +} + +boolean updateTimeClock() { + boolean report = false; + if (!timeConfiged && wifiAvailable) { + Serial.println("#############CONFIGURING TIME##############"); + configTime(3600 * UTC_offset, 0, "time.google.com"); + getLocalTime(&timeClock); + timeConfiged = true; + delay(1000); + } + if (timeConfiged) { + if (!getLocalTime(&timeClock)) { + report = false; + Serial.println("Failed to obtain time!"); + } else { + time(×tampEpoch); + Serial.print("time= "); + Serial.println(timestampEpoch); + report = true; + } + } + return report; +} diff --git a/aaaaaaaaaaaa/scmc/wifi.ino b/aaaaaaaaaaaa/scmc/wifi.ino new file mode 100644 index 0000000..ed6fb58 --- /dev/null +++ b/aaaaaaaaaaaa/scmc/wifi.ino @@ -0,0 +1,33 @@ +void setupWifi() { + WiFi.mode(WIFI_STA); + if (0 == strcmp(wifi_password, "None")) { + Serial.print("wifi setup with open network named: "); + Serial.println(wifi_ssid); + WiFi.begin(wifi_ssid); //connect to wifi network, remove password for open networks + } else {//wifi requires password + Serial.print("wifi setup with network named: "); + Serial.print(wifi_ssid); + Serial.print(" and password: "); + Serial.println(wifi_password); + WiFi.begin(wifi_ssid, wifi_password); //connect to wifi network, remove password for open networks + } +} + +boolean checkWifiConnection() { + if (millis() - wifiCheckUpdateMillis > wifiCheckIntervalMillis) { + wifiCheckUpdateMillis = millis(); + if (WiFi.status() != WL_CONNECTED) { + Serial.println("reconnecting to wifi"); + WiFi.reconnect(); + if (WiFi.status() != WL_CONNECTED) { + return false; + } + } + if (wifiAvailable == false) { + Serial.println("wifi reconnected"); + } + return true; + } else { + return wifiAvailable; + } +} diff --git a/platformio.ini b/platformio.ini index ecb7a9b..d4744f1 100644 --- a/platformio.ini +++ b/platformio.ini @@ -15,10 +15,13 @@ src_dir = src [env:esp32dev] framework = arduino -platform = espressif32@5.2.0 ; https://registry.platformio.org/platforms/platformio/espressif32/versions +platform = espressif32 ; @6.1.0 ; https://registry.platformio.org/platforms/platformio/espressif32/versions board = esp32dev build_flags = -DCORE_DEBUG_LEVEL=0 monitor_speed = 115200 lib_deps = - https://github.com/4-20ma/ModbusMaster@2.0.1 - https://github.com/hirschi-dev/renogy-rover-arduino.git#9a4d7d2c08fb6309400972dcd470782349c7e1a \ No newline at end of file + mobizt/Firebase Arduino Client Library for ESP8266 and ESP32 + https://github.com/4-20ma/ModbusMaster + ; @2.0.1 ; https://github.com/4-20ma/ModbusMaster + https://github.com/hirschi-dev/renogy-rover-arduino + ; #145c4181d6befc9531253c0999d6fce46d6ca0ca ; https://github.com/hirschi-dev/renogy-rover-arduino diff --git a/src/main.ino b/src/main.ino new file mode 100644 index 0000000..21f9fea --- /dev/null +++ b/src/main.ino @@ -0,0 +1,223 @@ +#include +#include + +#include + +// Provide the token generation process info. +#include + +// Provide the RTDB payload printing info and other helper functions. +#include + +#include "secret.h" + +// Define Firebase Data object +FirebaseData fbdo; +FirebaseAuth auth; +FirebaseConfig config; + +struct DayData { + int WGen; + int WUse; + int batPercent; + time_t time; +}; +DayData dayData = { 0, 0, 0, 0 }; + +struct LiveData { + boolean available; + int WGen; + int WUse; + int batPercent; + int cumulativeWhGen; + time_t time; +}; +LiveData liveData = { false, 0, 0, 0, 0, 0 }; + +struct MonthData { + int WhGen; + int WhUse; + float hUsed; + time_t time; +}; +MonthData monthData = { 0, 0, 0.0, 0 }; + +unsigned long sendLiveDataPrevMillis = 0; +unsigned long sendDayDataPrevMillis = 0; +unsigned long sendMonthDataPrevMillis = 0; +unsigned long timeUpdateMillis = 0; + +#include "sntp.h" +#include +const char* ntpServer = "pool.ntp.org"; +const long gmtOffset_sec = -3600 * 5; +boolean timeAvailable = false; + +time_t utcTime; +int localHour; +int localMinute; + +boolean dailyMessageFlag = false; + +void setup() +{ + + Serial.begin(115200); + + // WiFi.begin(wifi_ssid); + WiFi.begin("router", "password"); // TODO: + config.api_key = FIREBASE_API_KEY; + auth.user.email = email_address; + auth.user.password = firebase_password; + config.database_url = FIREBASE_URL; + config.token_status_callback = tokenStatusCallback; // see addons/TokenHelper.h, it Serial prints information + + fbdo.setResponseSize(2048); + + Firebase.begin(&config, &auth); + + Firebase.reconnectWiFi(true); + + Firebase.setDoubleDigits(5); + configTime(gmtOffset_sec, 3600, ntpServer); +} + +void loop() +{ + ///////////////// get time /////////////// + if (millis() - timeUpdateMillis > 1000) { + timeUpdateMillis = millis(); + time_t potentialUtcTime; + time(&potentialUtcTime); + if (potentialUtcTime > 1679089322) { // if not connected to ntp time() returns seconds since boot, not a valid time in the future of when this code was written. + utcTime = potentialUtcTime; + timeAvailable = (utcTime != 0); + } + localHour = localtime(&utcTime)->tm_hour; + localMinute = localtime(&utcTime)->tm_min; + Serial.println(localHour); + Serial.println(localMinute); + } + ///////////////////////////////////////// + + if (timeAvailable && Firebase.ready() && (millis() - sendDayDataPrevMillis > 15000 || sendDayDataPrevMillis == 0)) { + sendDayDataPrevMillis = millis(); + + dayData.time = utcTime; + + sendFirebaseDayData(dayData); + firebaseDeleteOldData("/data/dayData", 60, 2); + } + if (timeAvailable && Firebase.ready() && (millis() - sendMonthDataPrevMillis > 15000 || sendMonthDataPrevMillis == 0)) { + sendMonthDataPrevMillis = millis(); + + monthData.time = utcTime; + + sendFirebaseMonthData(monthData); + firebaseDeleteOldData("/data/monthData", 60, 2); + } + if (timeAvailable && Firebase.ready() && (millis() - sendLiveDataPrevMillis > 15000 || sendLiveDataPrevMillis == 0)) { + sendLiveDataPrevMillis = millis(); + + liveData.time = utcTime; + + sendFirebaseLiveData(liveData); + firebaseDeleteOldData("/data/liveData", 60, 2); + } +} + +boolean sendFirebaseLiveData(LiveData liveData) +{ + FirebaseJson json; + + json.set(F("available"), liveData.available); + json.set(F("WGen"), liveData.WGen); + json.set(F("WUse"), liveData.WUse); + json.set(F("batPercent"), liveData.batPercent); + json.set(F("cumulativeWhGen"), liveData.cumulativeWhGen); + json.set(F("time"), liveData.time); + + if (!Firebase.RTDB.setJSON(&fbdo, F("/data/liveData"), &json)) { + Serial.println("ERROR! Firebase (send liveData)"); + Serial.println(fbdo.errorReason().c_str()); + return false; + } + return true; +} + +boolean sendFirebaseDayData(DayData dayData) +{ + FirebaseJson json; + + json.set(F("WGen"), dayData.WGen); + json.set(F("WUse"), dayData.WUse); + json.set(F("batPercent"), dayData.batPercent); + json.set(F("time"), dayData.time); + if (!Firebase.RTDB.pushJSON(&fbdo, F("/data/dayData"), &json)) { + Serial.println("ERROR! Firebase (send dayData)"); + Serial.println(fbdo.errorReason().c_str()); + return false; + } + return true; +} + +boolean sendFirebaseMonthData(MonthData monthData) +{ + FirebaseJson json; + + json.set(F("hUsed"), monthData.hUsed); + json.set(F("time"), monthData.time); + json.set(F("WhGen"), monthData.WhGen); + json.set(F("WhUse"), monthData.WhUse); + if (!Firebase.RTDB.pushJSON(&fbdo, F("/data/monthData"), &json)) { + Serial.println("ERROR! Firebase (send monthData)"); + Serial.println(fbdo.errorReason().c_str()); + return false; + } + return true; +} + +boolean firebaseDeleteOldData(String path, unsigned long interval, byte num) +{ + boolean report = true; + + for (byte i_i = 0; i_i < num; i_i++) { // run multiple times so the number of entries can be reduced not just maintained + FirebaseJson fjson; + QueryFilter fquery; + fquery.orderBy("$key"); + fquery.limitToFirst(1); + + if (Firebase.RTDB.getJSON(&fbdo, path.c_str(), &fquery)) { + fjson = fbdo.jsonObject(); + + fjson.iteratorBegin(); + String key; + String value; + int type; + + fjson.iteratorGet(0, type, key, value); + + FirebaseJsonData timeJsonData; + fjson.get(timeJsonData, key + "/time"); + if (!timeJsonData.success || timeJsonData.intValue < utcTime - interval) { // old data + String nodeToDelete = path + "/" + key; + if (!Firebase.RTDB.deleteNode(&fbdo, nodeToDelete.c_str())) { + Serial.println("ERROR! Firebase (delete delete)"); + Serial.println(fbdo.errorReason()); + report = false; + } + } + fjson.iteratorEnd(); + fjson.clear(); + + } else { + // Failed to get JSON data at defined node, print out the error reason + Serial.println("ERROR! Firebase (delete)"); + Serial.println(fbdo.errorReason()); + report = false; + } + fquery.clear(); + } + + return report; +} diff --git a/src/secret.h b/src/secret.h new file mode 100644 index 0000000..54c288c --- /dev/null +++ b/src/secret.h @@ -0,0 +1,13 @@ +#ifndef SECRET_H +#define SECRET_H +const char* wifi_ssid = "Brown-Guest"; //name of the wifi network you want to connect to (e.g. Brown-Guest) + +const char* email_address = "sfsw.brown.solar.charger@gmail.com"; +const char* email_password = "Bruno2Bluno"; +const char* firebase_password="S$SWBrownSun"; // for Firebase User +const char* FIREBASE_URL = "brown-solar-charger-default-rtdb.firebaseio.com"; +const char* FIREBASE_API_KEY = "AIzaSyAqapK9e-NDJ1bAmbaggqasmJZvcd6V6z4"; +const char* FIREBASE_EMAIL = "sfsw.brown.solar.charger@gmail.com"; +const char* FIREBASE_PASSWORD = "nLxpFKf3VQC6bGfG2KhyhZr"; + +#endif // SECRET_H