diff --git a/Software/Software.ino b/Software/Software.ino index df20927a..c038af2f 100644 --- a/Software/Software.ino +++ b/Software/Software.ino @@ -130,9 +130,7 @@ void setup() { init_webserver(); #endif -#ifdef EVENTLOGGING init_events(); -#endif init_CAN(); diff --git a/Software/USER_SETTINGS.cpp b/Software/USER_SETTINGS.cpp index 9ef1f9d5..bcc2a677 100644 --- a/Software/USER_SETTINGS.cpp +++ b/Software/USER_SETTINGS.cpp @@ -16,11 +16,6 @@ volatile uint16_t MAXDISCHARGEAMP = 300; //30.0A , BYD CAN specific setting, Max discharge speed in Amp (Some inverters needs to be artificially limited) /* Charger settings (Optional, when generator charging) */ -// MQTT -#ifdef MQTT -const char* mqtt_user = "REDACTED"; -const char* mqtt_password = "REDACTED"; -#endif // USE_MQTT /* Charger settings */ volatile float CHARGER_SET_HV = 384; // Reasonably appropriate 4.0v per cell charging of a 96s pack volatile float CHARGER_MAX_HV = 420; // Max permissible output (VDC) of charger @@ -37,4 +32,11 @@ const char* password = "REPLACE_WITH_YOUR_PASSWORD"; // Minimum of 8 characters const char* ssidAP = "Battery Emulator"; // Maximum of 63 characters; const char* passwordAP = "123456789"; // Minimum of 8 characters; set to NULL if you want the access point to be open const uint8_t wifi_channel = 0; // set to 0 for automatic channel selection + +// MQTT +#ifdef MQTT +const char* mqtt_user = "REDACTED"; +const char* mqtt_password = "REDACTED"; +#endif // USE_MQTT + #endif diff --git a/Software/USER_SETTINGS.h b/Software/USER_SETTINGS.h index 4b69ef06..4ba402a6 100644 --- a/Software/USER_SETTINGS.h +++ b/Software/USER_SETTINGS.h @@ -37,7 +37,7 @@ //#define SERIAL_LINK_RECEIVER //Enable this line to receive battery data over RS485 pins from another Lilygo (This LilyGo interfaces with inverter) //#define SERIAL_LINK_TRANSMITTER //Enable this line to send battery data over RS485 pins to another Lilygo (This LilyGo interfaces with battery) #define WEBSERVER //Enable this line to enable WiFi, and to run the webserver. See USER_SETTINGS.cpp for the Wifi settings. -//#define LOAD_SAVED_SETTINGS_ON_BOOT //Enable this line to read settings stored via the webserver on boot +#define LOAD_SAVED_SETTINGS_ON_BOOT //Enable this line to read settings stored via the webserver on boot /* MQTT options */ //#define MQTT // Enable this line to enable MQTT @@ -47,7 +47,6 @@ #define MQTT_PORT 1883 /* Event options*/ -#define EVENTLOGGING //Enable this line to log events to the event log #define DUMMY_EVENT_ENABLED false //Enable this line to have a dummy event that gets logged to test the interface /* Select charger used (Optional) */ diff --git a/Software/src/devboard/utils/events.cpp b/Software/src/devboard/utils/events.cpp index ee2d79f5..3bf9035b 100644 --- a/Software/src/devboard/utils/events.cpp +++ b/Software/src/devboard/utils/events.cpp @@ -19,10 +19,10 @@ void init_events(void) { entries[i].timestamp = 0; entries[i].data = 0; entries[i].occurences = 0; - entries[i].led_color = RED; // Most events are RED + entries[i].led_color = RED; // Most events are RED, critical errors } - // YELLOW events below + // YELLOW warning events below entries[EVENT_12V_LOW].led_color = YELLOW; entries[EVENT_CAN_WARNING].led_color = YELLOW; entries[EVENT_CELL_DEVIATION_HIGH].led_color = YELLOW; @@ -30,7 +30,6 @@ void init_events(void) { } void set_event(EVENTS_ENUM_TYPE event, uint8_t data) { -#ifdef EVENTLOGGING if (event >= EVENT_NOF_EVENTS) { event = EVENT_UNKNOWN_EVENT_SET; } @@ -42,7 +41,6 @@ void set_event(EVENTS_ENUM_TYPE event, uint8_t data) { Serial.println("Set event: " + String(get_event_enum_string(event)) + ". Has occured " + String(entries[event].occurences) + " times"); #endif -#endif } void update_event_timestamps(void) { @@ -61,13 +59,13 @@ static void update_led_color(EVENTS_ENUM_TYPE event) { const char* get_led_color_display_text(u_int8_t led_color) { switch (led_color) { case RED: - return "RED"; + return "Error"; case YELLOW: - return "YELLOW"; + return "Warning"; case GREEN: - return "GREEN"; + return "Info"; case BLUE: - return "BLUE"; + return "Debug"; default: return "UNKNOWN"; } diff --git a/Software/src/devboard/webserver/README.md b/Software/src/devboard/webserver/README.md index 4965a784..b3027538 100644 --- a/Software/src/devboard/webserver/README.md +++ b/Software/src/devboard/webserver/README.md @@ -24,10 +24,6 @@ This section lists a number of features that can be implemented as part of the w - TODO: list all available ssids: scan WiFi Networks https://randomnerdtutorials.com/esp32-useful-wi-fi-functions-arduino/ - TODO: add option to add/change ssid and password and save, connect to the new ssid (Option: save ssid and password using Preferences.h library https://randomnerdtutorials.com/esp32-save-data-permanently-preferences/) -- TODO: display WiFi connection strength (https://randomnerdtutorials.com/esp32-useful-wi-fi-functions-arduino/) -- TODO: display CAN state (indicate if there is a communication error) -- TODO: display battery errors in battery diagnosis tab -- TODO: display inverter errors in battery diagnosis tab - TODO: add functionality to turn WiFi AP off - TODO: fix IP address on home network (https://randomnerdtutorials.com/esp32-static-fixed-ip-address-arduino-ide/) - TODO: set hostname (https://randomnerdtutorials.com/esp32-set-custom-hostname-arduino/) diff --git a/Software/src/devboard/webserver/cellmonitor_html.cpp b/Software/src/devboard/webserver/cellmonitor_html.cpp new file mode 100644 index 00000000..1051e87c --- /dev/null +++ b/Software/src/devboard/webserver/cellmonitor_html.cpp @@ -0,0 +1,56 @@ +#include "cellmonitor_html.h" +#include <Arduino.h> + +String cellmonitor_processor(const String& var) { + if (var == "PLACEHOLDER") { + String content = ""; + // Page format + content += "<style>"; + content += "body { background-color: black; color: white; }"; + content += ".container { display: flex; flex-wrap: wrap; justify-content: space-around; }"; + content += ".cell { width: 48%; margin: 1%; padding: 10px; border: 1px solid white; text-align: center; }"; + content += ".low-voltage { color: red; }"; // Style for low voltage text + content += ".voltage-values { margin-bottom: 10px; }"; // Style for voltage values section + content += "</style>"; + + // Start a new block with a specific background color + content += "<div style='background-color: #303E47; padding: 10px; margin-bottom: 10px; border-radius: 50px'>"; + + // Display max, min, and deviation voltage values + content += "<div class='voltage-values'>"; + content += "Max Voltage: " + String(cell_max_voltage) + " mV<br>"; + content += "Min Voltage: " + String(cell_min_voltage) + " mV<br>"; + int deviation = cell_max_voltage - cell_min_voltage; + content += "Voltage Deviation: " + String(deviation) + " mV"; + content += "</div>"; + + // Visualize the populated cells in forward order using flexbox with conditional text color + content += "<div class='container'>"; + for (int i = 0; i < 120; ++i) { + // Skip empty values + if (cellvoltages[i] == 0) { + continue; + } + + String cellContent = "Cell " + String(i + 1) + "<br>" + String(cellvoltages[i]) + " mV"; + + // Check if the cell voltage is below 3000, apply red color + if (cellvoltages[i] < 3000) { + cellContent = "<span class='low-voltage'>" + cellContent + "</span>"; + } + + content += "<div class='cell'>" + cellContent + "</div>"; + } + content += "</div>"; + + // Close the block + content += "</div>"; + + content += "<button onclick='goToMainPage()'>Back to main page</button>"; + content += "<script>"; + content += "function goToMainPage() { window.location.href = '/'; }"; + content += "</script>"; + return content; + } + return String(); +} diff --git a/Software/src/devboard/webserver/cellmonitor_html.h b/Software/src/devboard/webserver/cellmonitor_html.h new file mode 100644 index 00000000..0a3d4aa9 --- /dev/null +++ b/Software/src/devboard/webserver/cellmonitor_html.h @@ -0,0 +1,20 @@ +#ifndef CELLMONITOR_H +#define CELLMONITOR_H + +#include <Arduino.h> +#include <stdint.h> + +extern uint16_t cell_max_voltage; //mV, 0-4350 +extern uint16_t cell_min_voltage; //mV, 0-4350 +extern uint16_t cellvoltages[120]; //mV 0-4350 per cell + +/** + * @brief Replaces placeholder with content section in web page + * + * @param[in] var + * + * @return String + */ +String cellmonitor_processor(const String& var); + +#endif diff --git a/Software/src/devboard/webserver/events_html.cpp b/Software/src/devboard/webserver/events_html.cpp new file mode 100644 index 00000000..597245e0 --- /dev/null +++ b/Software/src/devboard/webserver/events_html.cpp @@ -0,0 +1,91 @@ +#include "events_html.h" +#include <Arduino.h> + +const char EVENTS_HTML_START[] = R"=====( +<style> + body { background-color: black; color: white; } + .event-log { display: flex; flex-direction: column; } + .event { display: flex; flex-wrap: wrap; border: 1px solid white; padding: 10px; } + .event > div { flex: 1; min-width: 100px; max-width: 90%; word-break: break-word; } +</style> +<div style='background-color: #303E47; padding: 10px; margin-bottom: 10px;border-radius: 50px'> +<div class="event-log"> +<div class="event"> +<div>Event Type</div><div>Severity</div><div>Last Event</div><div>Count</div><div>Data</div><div>Message</div> +</div> +)====="; +const char EVENTS_HTML_END[] = R"=====( +</div></div> +<button onclick='goToMainPage()'>Back to main page</button> +<script>function displayEventLog(){document.querySelector(".event-log");var i=(new Date).getTime()/1e3;document.querySelectorAll(".event").forEach(function(e){var n=e.querySelector(".last-event-seconds-ago"),t=e.querySelector(".timestamp");if(n&&t){var o=parseInt(n.innerText,10),a=parseFloat(t.innerText),r=new Date(1e3*(i-a+o)).toLocaleString();n.innerText=r}})}function goToMainPage(){window.location.href="/"}window.onload=function(){displayEventLog()}</script> +)====="; + +/* The above <script> section is minified to save storage and increase performance, here is the full function: +<script> +function displayEventLog() { + var eventLogElement = document.querySelector('.event-log'); + + // Get the current time on the client side + var currentTime = new Date().getTime() / 1000; // Convert milliseconds to seconds + + // Loop through the events and update the "Last Event" column + var events = document.querySelectorAll('.event'); + events.forEach(function(event) { + var secondsAgoElement = event.querySelector('.last-event-seconds-ago'); + var timestampElement = event.querySelector('.timestamp'); + + if (secondsAgoElement && timestampElement) { + var secondsAgo = parseInt(secondsAgoElement.innerText, 10); + var uptimeTimestamp = parseFloat(timestampElement.innerText); // Parse as float to handle seconds with decimal parts + + // Calculate the actual system time based on the client-side current time + var actualTime = new Date((currentTime - uptimeTimestamp + secondsAgo) * 1000); + + // Format the date and time + var formattedTime = actualTime.toLocaleString(); + + // Update the "Last Event" column with the formatted time + secondsAgoElement.innerText = formattedTime; + } + }); +} + +// Call the displayEventLog function when the page is loaded +window.onload = function() { + displayEventLog(); +}; + +function goToMainPage() { + window.location.href = '/'; +} +</script> +*/ + +String events_processor(const String& var) { + if (var == "PLACEHOLDER") { + String content = ""; + content.reserve(5000); + // Page format + content.concat(FPSTR(EVENTS_HTML_START)); + for (int i = 0; i < EVENT_NOF_EVENTS; i++) { + Serial.println("Event: " + String(get_event_enum_string(static_cast<EVENTS_ENUM_TYPE>(i))) + + " count: " + String(entries[i].occurences) + " seconds: " + String(entries[i].timestamp) + + " data: " + String(entries[i].data)); + if (entries[i].occurences > 0) { + content.concat("<div class='event'>"); + content.concat("<div>" + String(get_event_enum_string(static_cast<EVENTS_ENUM_TYPE>(i))) + "</div>"); + content.concat("<div>" + String(get_led_color_display_text(entries[i].led_color)) + "</div>"); + content.concat("<div class='last-event-seconds-ago'>" + String((millis() / 1000) - entries[i].timestamp) + + "</div>"); + content.concat("<div>" + String(entries[i].occurences) + "</div>"); + content.concat("<div>" + String(entries[i].data) + "</div>"); + content.concat("<div>" + String(get_event_message(static_cast<EVENTS_ENUM_TYPE>(i))) + "</div>"); + content.concat("<div class='timestamp'>" + String(entries[i].timestamp) + "</div>"); + content.concat("</div>"); // End of event row + } + } + content.concat(FPSTR(EVENTS_HTML_END)); + return content; + } + return String(); +} diff --git a/Software/src/devboard/webserver/events_html.h b/Software/src/devboard/webserver/events_html.h new file mode 100644 index 00000000..a13f9b0c --- /dev/null +++ b/Software/src/devboard/webserver/events_html.h @@ -0,0 +1,17 @@ +#ifndef EVENTS_H +#define EVENTS_H + +#include "../utils/events.h" + +extern EVENTS_STRUCT_TYPE entries[EVENT_NOF_EVENTS]; + +/** + * @brief Replaces placeholder with content section in web page + * + * @param[in] var + * + * @return String + */ +String events_processor(const String& var); + +#endif diff --git a/Software/src/devboard/webserver/index_html.cpp b/Software/src/devboard/webserver/index_html.cpp new file mode 100644 index 00000000..c7a11e90 --- /dev/null +++ b/Software/src/devboard/webserver/index_html.cpp @@ -0,0 +1,29 @@ +const char index_html[] = R"rawliteral( +<!DOCTYPE HTML><html><head><title>Battery Emulator</title><meta name="viewport" content="width=device-width,initial-scale=1"><style>html{font-family:Arial;display:inline-block;text-align:center}h2{font-size:3rem}p{font-size:3rem}body{max-width:600px;margin:0 auto;padding-bottom:25px}.switch{position:relative;display:inline-block;width:120px;height:68px}.switch input{display:none}.slider{position:absolute;top:0;left:0;right:0;bottom:0;background-color:#ccc;border-radius:6px}.slider:before{position:absolute;content:"";height:52px;width:52px;left:8px;bottom:8px;background-color:#fff;-webkit-transition:.4s;transition:.4s;border-radius:3px}input:checked+.slider{background-color:#b30000}input:checked+.slider:before{-webkit-transform:translateX(52px);-ms-transform:translateX(52px);transform:translateX(52px)}</style></head><body><h2>Battery Emulator</h2>%PLACEHOLDER%</body></html> +)rawliteral"; + +/* The above code is minified to increase performance. Here is the full HTML function: +<!DOCTYPE HTML><html> +<head> + <title>Battery Emulator</title> + <meta name="viewport" content="width=device-width, initial-scale=1"> + <style> + html {font-family: Arial; display: inline-block; text-align: center;} + h2 {font-size: 3.0rem;} + p {font-size: 3.0rem;} + body {max-width: 600px; margin:0px auto; padding-bottom: 25px;} + .switch {position: relative; display: inline-block; width: 120px; height: 68px} + .switch input {display: none} + .slider {position: absolute; top: 0; left: 0; right: 0; bottom: 0; background-color: #ccc; border-radius: 6px} + .slider:before {position: absolute; content: ""; height: 52px; width: 52px; left: 8px; bottom: 8px; background-color: #fff; -webkit-transition: .4s; transition: .4s; border-radius: 3px} + input:checked+.slider {background-color: #b30000} + input:checked+.slider:before {-webkit-transform: translateX(52px); -ms-transform: translateX(52px); transform: translateX(52px)} + </style> +</head> +<body> + <h2>Battery Emulator</h2> + %PLACEHOLDER% +</script> +</body> +</html> +*/ diff --git a/Software/src/devboard/webserver/settings_html.cpp b/Software/src/devboard/webserver/settings_html.cpp new file mode 100644 index 00000000..35cbd113 --- /dev/null +++ b/Software/src/devboard/webserver/settings_html.cpp @@ -0,0 +1,238 @@ +#include "settings_html.h" +#include <Arduino.h> + +String settings_processor(const String& var) { + if (var == "PLACEHOLDER") { + String content = ""; + //Page format + content += "<style>"; + content += "body { background-color: black; color: white; }"; + content += "</style>"; + + // Start a new block with a specific background color + content += "<div style='background-color: #303E47; padding: 10px; margin-bottom: 10px;border-radius: 50px'>"; + + // Show current settings with edit buttons and input fields + content += "<h4 style='color: white;'>Battery capacity: <span id='BATTERY_WH_MAX'>" + String(BATTERY_WH_MAX) + + " Wh </span> <button onclick='editWh()'>Edit</button></h4>"; + content += "<h4 style='color: white;'>SOC max percentage: " + String(MAXPERCENTAGE / 10.0, 1) + + " </span> <button onclick='editSocMax()'>Edit</button></h4>"; + content += "<h4 style='color: white;'>SOC min percentage: " + String(MINPERCENTAGE / 10.0, 1) + + " </span> <button onclick='editSocMin()'>Edit</button></h4>"; + content += "<h4 style='color: white;'>Max charge speed: " + String(MAXCHARGEAMP / 10.0, 1) + + " A </span> <button onclick='editMaxChargeA()'>Edit</button></h4>"; + content += "<h4 style='color: white;'>Max discharge speed: " + String(MAXDISCHARGEAMP / 10.0, 1) + + " A </span> <button onclick='editMaxDischargeA()'>Edit</button></h4>"; + // Close the block + content += "</div>"; + +#ifdef TEST_FAKE_BATTERY + // Start a new block with blue background color + content += "<div style='background-color: #2E37AD; padding: 10px; margin-bottom: 10px;border-radius: 50px'>"; + float voltageFloat = static_cast<float>(battery_voltage) / 10.0; // Convert to float and divide by 10 + content += "<h4 style='color: white;'>Fake battery voltage: " + String(voltageFloat, 1) + + " V </span> <button onclick='editFakeBatteryVoltage()'>Edit</button></h4>"; + + // Close the block + content += "</div>"; +#endif + +#if defined CHEVYVOLT_CHARGER || defined NISSANLEAF_CHARGER + + // Start a new block with orange background color + content += "<div style='background-color: #FF6E00; padding: 10px; margin-bottom: 10px;border-radius: 50px'>"; + + content += "<h4 style='color: white;'>Charger HVDC Enabled: "; + if (charger_HV_enabled) { + content += "<span>✓</span>"; + } else { + content += "<span style='color: red;'>✕</span>"; + } + content += " <button onclick='editChargerHVDCEnabled()'>Edit</button></h4>"; + + content += "<h4 style='color: white;'>Charger Aux12VDC Enabled: "; + if (charger_aux12V_enabled) { + content += "<span>✓</span>"; + } else { + content += "<span style='color: red;'>✕</span>"; + } + content += " <button onclick='editChargerAux12vEnabled()'>Edit</button></h4>"; + + content += "<h4 style='color: white;'>Charger Voltage Setpoint: " + String(charger_setpoint_HV_VDC, 1) + + " V </span> <button onclick='editChargerSetpointVDC()'>Edit</button></h4>"; + content += "<h4 style='color: white;'>Charger Current Setpoint: " + String(charger_setpoint_HV_IDC, 1) + + " A </span> <button onclick='editChargerSetpointIDC()'>Edit</button></h4>"; + + // Close the block + content += "</div>"; +#endif + + content += "<script>"; + content += "function editWh() {"; + content += "var value = prompt('How much energy the battery can store. Enter new Wh value (1-65000):');"; + content += "if (value !== null) {"; + content += " if (value >= 1 && value <= 65000) {"; + content += " var xhr = new XMLHttpRequest();"; + content += " xhr.open('GET', '/updateBatterySize?value=' + value, true);"; + content += " xhr.send();"; + content += " } else {"; + content += " alert('Invalid value. Please enter a value between 1 and 65000.');"; + content += " }"; + content += "}"; + content += "}"; + content += "function editSocMax() {"; + content += + "var value = prompt('Inverter will see fully charged (100pct)SOC when this value is reached. Enter new maximum " + "SOC value that battery will charge to (50.0-100.0):');"; + content += "if (value !== null) {"; + content += " if (value >= 50 && value <= 100) {"; + content += " var xhr = new XMLHttpRequest();"; + content += " xhr.open('GET', '/updateSocMax?value=' + value, true);"; + content += " xhr.send();"; + content += " } else {"; + content += " alert('Invalid value. Please enter a value between 50.0 and 100.0');"; + content += " }"; + content += "}"; + content += "}"; + content += "function editSocMin() {"; + content += + "var value = prompt('Inverter will see completely discharged (0pct)SOC when this value is reached. Enter new " + "minimum SOC value that battery will discharge to (0-50.0):');"; + content += "if (value !== null) {"; + content += " if (value >= 0 && value <= 50) {"; + content += " var xhr = new XMLHttpRequest();"; + content += " xhr.open('GET', '/updateSocMin?value=' + value, true);"; + content += " xhr.send();"; + content += " } else {"; + content += " alert('Invalid value. Please enter a value between 0 and 50.0');"; + content += " }"; + content += "}"; + content += "}"; + content += "function editMaxChargeA() {"; + content += + "var value = prompt('BYD CAN specific setting, some inverters needs to be artificially limited. Enter new " + "maximum charge current in A (0-1000.0):');"; + content += "if (value !== null) {"; + content += " if (value >= 0 && value <= 1000) {"; + content += " var xhr = new XMLHttpRequest();"; + content += " xhr.open('GET', '/updateMaxChargeA?value=' + value, true);"; + content += " xhr.send();"; + content += " } else {"; + content += " alert('Invalid value. Please enter a value between 0 and 1000.0');"; + content += " }"; + content += "}"; + content += "}"; + content += "function editMaxDischargeA() {"; + content += + "var value = prompt('BYD CAN specific setting, some inverters needs to be artificially limited. Enter new " + "maximum discharge current in A (0-1000.0):');"; + content += "if (value !== null) {"; + content += " if (value >= 0 && value <= 1000) {"; + content += " var xhr = new XMLHttpRequest();"; + content += " xhr.open('GET', '/updateMaxDischargeA?value=' + value, true);"; + content += " xhr.send();"; + content += " } else {"; + content += " alert('Invalid value. Please enter a value between 0 and 1000.0');"; + content += " }"; + content += "}"; + content += "}"; + +#ifdef TEST_FAKE_BATTERY + content += "function editFakeBatteryVoltage() {"; + content += " var value = prompt('Enter new fake battery voltage');"; + content += "if (value !== null) {"; + content += " if (value >= 0 && value <= 5000) {"; + content += " var xhr = new XMLHttpRequest();"; + content += " xhr.open('GET', '/updateFakeBatteryVoltage?value=' + value, true);"; + content += " xhr.send();"; + content += " } else {"; + content += " alert('Invalid value. Please enter a value between 0 and 1000');"; + content += " }"; + content += "}"; + content += "}"; +#endif + +#if defined CHEVYVOLT_CHARGER || defined NISSANLEAF_CHARGER + content += "function editChargerHVDCEnabled() {"; + content += " var value = prompt('Enable or disable HV DC output. Enter 1 for enabled, 0 for disabled');"; + content += " if (value !== null) {"; + content += " if (value == 0 || value == 1) {"; + content += " var xhr = new XMLHttpRequest();"; + content += " xhr.open('GET', '/updateChargerHvEnabled?value=' + value, true);"; + content += " xhr.send();"; + content += " }"; + content += " } else {"; + content += " alert('Invalid value. Please enter 1 or 0');"; + content += " }"; + content += "}"; + + content += "function editChargerAux12vEnabled() {"; + content += + "var value = prompt('Enable or disable low voltage 12v auxiliary DC output. Enter 1 for enabled, 0 for " + "disabled');"; + content += "if (value !== null) {"; + content += " if (value == 0 || value == 1) {"; + content += " var xhr = new XMLHttpRequest();"; + content += " xhr.open('GET', '/updateChargerAux12vEnabled?value=' + value, true);"; + content += " xhr.send();"; + content += " } else {"; + content += " alert('Invalid value. Please enter 1 or 0');"; + content += " }"; + content += "}"; + content += "}"; + + content += "function editChargerSetpointVDC() {"; + content += + "var value = prompt('Set charging voltage. Input will be validated against inverter and/or charger " + "configuration parameters, but use sensible values like 200 to 420.');"; + content += "if (value !== null) {"; + content += " if (value >= 0 && value <= 1000) {"; + content += " var xhr = new XMLHttpRequest();"; + content += " xhr.open('GET', '/updateChargeSetpointV?value=' + value, true);"; + content += " xhr.send();"; + content += " } else {"; + content += " alert('Invalid value. Please enter a value between 0 and 1000');"; + content += " }"; + content += "}"; + content += "}"; + + content += "function editChargerSetpointIDC() {"; + content += + "var value = prompt('Set charging amperage. Input will be validated against inverter and/or charger " + "configuration parameters, but use sensible values like 6 to 48.');"; + content += "if (value !== null) {"; + content += " if (value >= 0 && value <= 1000) {"; + content += " var xhr = new XMLHttpRequest();"; + content += " xhr.open('GET', '/updateChargeSetpointA?value=' + value, true);"; + content += " xhr.send();"; + content += " } else {"; + content += " alert('Invalid value. Please enter a value between 0 and 100');"; + content += " }"; + content += "}"; + content += "}"; + + content += "function editChargerSetpointEndI() {"; + content += + "var value = prompt('Set amperage that terminates charge as being sufficiently complete. Input will be " + "validated against inverter and/or charger configuration parameters, but use sensible values like 1-5.');"; + content += "if (value !== null) {"; + content += " if (value >= 0 && value <= 1000) {"; + content += " var xhr = new XMLHttpRequest();"; + content += " xhr.open('GET', '/updateChargeEndA?value=' + value, true);"; + content += " xhr.send();"; + content += " } else {"; + content += " alert('Invalid value. Please enter a value between 0 and 100');"; + content += " }"; + content += "}"; + content += "}"; +#endif + content += "</script>"; + + content += "<button onclick='goToMainPage()'>Back to main page</button>"; + content += "<script>"; + content += "function goToMainPage() { window.location.href = '/'; }"; + content += "</script>"; + return content; + } + return String(); +} diff --git a/Software/src/devboard/webserver/settings_html.h b/Software/src/devboard/webserver/settings_html.h new file mode 100644 index 00000000..88e7f47a --- /dev/null +++ b/Software/src/devboard/webserver/settings_html.h @@ -0,0 +1,18 @@ +#ifndef SETTINGS_H +#define SETTINGS_H + +#include <Arduino.h> + +#include "../../../USER_SETTINGS.h" // Needed for WiFi ssid and password +extern uint16_t battery_voltage; //V+1, 0-500.0 (0-5000) + +/** + * @brief Replaces placeholder with content section in web page + * + * @param[in] var + * + * @return String + */ +String settings_processor(const String& var); + +#endif diff --git a/Software/src/devboard/webserver/webserver.cpp b/Software/src/devboard/webserver/webserver.cpp index 8d7aa1ab..16bd556b 100644 --- a/Software/src/devboard/webserver/webserver.cpp +++ b/Software/src/devboard/webserver/webserver.cpp @@ -1,40 +1,16 @@ #include "webserver.h" #include <Preferences.h> -Preferences preferences3; - // Create AsyncWebServer object on port 80 AsyncWebServer server(80); // Measure OTA progress unsigned long ota_progress_millis = 0; -const char index_html[] PROGMEM = R"rawliteral( -<!DOCTYPE HTML><html> -<head> - <title>Battery Emulator</title> - <meta name="viewport" content="width=device-width, initial-scale=1"> - <link rel="icon" type="image/png" href="favicon.png"> - <style> - html {font-family: Arial; display: inline-block; text-align: center;} - h2 {font-size: 3.0rem;} - p {font-size: 3.0rem;} - body {max-width: 600px; margin:0px auto; padding-bottom: 25px;} - .switch {position: relative; display: inline-block; width: 120px; height: 68px} - .switch input {display: none} - .slider {position: absolute; top: 0; left: 0; right: 0; bottom: 0; background-color: #ccc; border-radius: 6px} - .slider:before {position: absolute; content: ""; height: 52px; width: 52px; left: 8px; bottom: 8px; background-color: #fff; -webkit-transition: .4s; transition: .4s; border-radius: 3px} - input:checked+.slider {background-color: #b30000} - input:checked+.slider:before {-webkit-transform: translateX(52px); -ms-transform: translateX(52px); transform: translateX(52px)} - </style> -</head> -<body> - <h2>Battery Emulator</h2> - %PLACEHOLDER% -</script> -</body> -</html> -)rawliteral"; +#include "cellmonitor_html.h" +#include "events_html.h" +#include "index_html.cpp" +#include "settings_html.h" enum WifiState { INIT, //before connecting first time @@ -62,6 +38,8 @@ void init_webserver() { } init_WiFi_STA(ssid, password, wifi_channel); + String content = index_html; + // Route for root / web page server.on("/", HTTP_GET, [](AsyncWebServerRequest* request) { request->send_P(200, "text/html", index_html, processor); }); @@ -75,10 +53,9 @@ void init_webserver() { request->send_P(200, "text/html", index_html, cellmonitor_processor); }); -#ifdef EVENTLOGGING + // Route for going to event log web page server.on("/events", HTTP_GET, [](AsyncWebServerRequest* request) { request->send_P(200, "text/html", index_html, events_processor); }); -#endif // Route for editing Wh server.on("/updateBatterySize", HTTP_GET, [](AsyncWebServerRequest* request) { @@ -629,9 +606,7 @@ String processor(const String& var) { content += "function goToUpdatePage() { window.location.href = '/update'; }"; content += "function goToCellmonitorPage() { window.location.href = '/cellmonitor'; }"; content += "function goToSettingsPage() { window.location.href = '/settings'; }"; -#ifdef EVENTLOGGING content += "function goToEventsPage() { window.location.href = '/events'; }"; -#endif content += "function promptToReboot() { if (window.confirm('Are you sure you want to reboot the emulator? NOTE: If " "emulator is handling contactors, they will open during reboot!')) { " @@ -653,348 +628,6 @@ String processor(const String& var) { return String(); } -String settings_processor(const String& var) { - if (var == "PLACEHOLDER") { - String content = ""; - //Page format - content += "<style>"; - content += "body { background-color: black; color: white; }"; - content += "</style>"; - - // Start a new block with a specific background color - content += "<div style='background-color: #303E47; padding: 10px; margin-bottom: 10px;border-radius: 50px'>"; - - // Show current settings with edit buttons and input fields - content += "<h4 style='color: white;'>Battery capacity: <span id='BATTERY_WH_MAX'>" + String(BATTERY_WH_MAX) + - " Wh </span> <button onclick='editWh()'>Edit</button></h4>"; - content += "<h4 style='color: white;'>SOC max percentage: " + String(MAXPERCENTAGE / 10.0, 1) + - " </span> <button onclick='editSocMax()'>Edit</button></h4>"; - content += "<h4 style='color: white;'>SOC min percentage: " + String(MINPERCENTAGE / 10.0, 1) + - " </span> <button onclick='editSocMin()'>Edit</button></h4>"; - content += "<h4 style='color: white;'>Max charge speed: " + String(MAXCHARGEAMP / 10.0, 1) + - " A </span> <button onclick='editMaxChargeA()'>Edit</button></h4>"; - content += "<h4 style='color: white;'>Max discharge speed: " + String(MAXDISCHARGEAMP / 10.0, 1) + - " A </span> <button onclick='editMaxDischargeA()'>Edit</button></h4>"; - // Close the block - content += "</div>"; - -#ifdef TEST_FAKE_BATTERY - // Start a new block with blue background color - content += "<div style='background-color: #2E37AD; padding: 10px; margin-bottom: 10px;border-radius: 50px'>"; - float voltageFloat = static_cast<float>(battery_voltage) / 10.0; // Convert to float and divide by 10 - content += "<h4 style='color: white;'>Fake battery voltage: " + String(voltageFloat, 1) + - " V </span> <button onclick='editFakeBatteryVoltage()'>Edit</button></h4>"; - - // Close the block - content += "</div>"; -#endif - -#if defined CHEVYVOLT_CHARGER || defined NISSANLEAF_CHARGER - - // Start a new block with orange background color - content += "<div style='background-color: #FF6E00; padding: 10px; margin-bottom: 10px;border-radius: 50px'>"; - - content += "<h4 style='color: white;'>Charger HVDC Enabled: "; - if (charger_HV_enabled) { - content += "<span>✓</span>"; - } else { - content += "<span style='color: red;'>✕</span>"; - } - content += " <button onclick='editChargerHVDCEnabled()'>Edit</button></h4>"; - - content += "<h4 style='color: white;'>Charger Aux12VDC Enabled: "; - if (charger_aux12V_enabled) { - content += "<span>✓</span>"; - } else { - content += "<span style='color: red;'>✕</span>"; - } - content += " <button onclick='editChargerAux12vEnabled()'>Edit</button></h4>"; - - content += "<h4 style='color: white;'>Charger Voltage Setpoint: " + String(charger_setpoint_HV_VDC, 1) + - " V </span> <button onclick='editChargerSetpointVDC()'>Edit</button></h4>"; - content += "<h4 style='color: white;'>Charger Current Setpoint: " + String(charger_setpoint_HV_IDC, 1) + - " A </span> <button onclick='editChargerSetpointIDC()'>Edit</button></h4>"; - - // Close the block - content += "</div>"; -#endif - - content += "<script>"; - content += "function editWh() {"; - content += "var value = prompt('How much energy the battery can store. Enter new Wh value (1-65000):');"; - content += "if (value !== null) {"; - content += " if (value >= 1 && value <= 65000) {"; - content += " var xhr = new XMLHttpRequest();"; - content += " xhr.open('GET', '/updateBatterySize?value=' + value, true);"; - content += " xhr.send();"; - content += " } else {"; - content += " alert('Invalid value. Please enter a value between 1 and 65000.');"; - content += " }"; - content += "}"; - content += "}"; - content += "function editSocMax() {"; - content += - "var value = prompt('Inverter will see fully charged (100pct)SOC when this value is reached. Enter new maximum " - "SOC value that battery will charge to (50.0-100.0):');"; - content += "if (value !== null) {"; - content += " if (value >= 50 && value <= 100) {"; - content += " var xhr = new XMLHttpRequest();"; - content += " xhr.open('GET', '/updateSocMax?value=' + value, true);"; - content += " xhr.send();"; - content += " } else {"; - content += " alert('Invalid value. Please enter a value between 50.0 and 100.0');"; - content += " }"; - content += "}"; - content += "}"; - content += "function editSocMin() {"; - content += - "var value = prompt('Inverter will see completely discharged (0pct)SOC when this value is reached. Enter new " - "minimum SOC value that battery will discharge to (0-50.0):');"; - content += "if (value !== null) {"; - content += " if (value >= 0 && value <= 50) {"; - content += " var xhr = new XMLHttpRequest();"; - content += " xhr.open('GET', '/updateSocMin?value=' + value, true);"; - content += " xhr.send();"; - content += " } else {"; - content += " alert('Invalid value. Please enter a value between 0 and 50.0');"; - content += " }"; - content += "}"; - content += "}"; - content += "function editMaxChargeA() {"; - content += - "var value = prompt('BYD CAN specific setting, some inverters needs to be artificially limited. Enter new " - "maximum charge current in A (0-1000.0):');"; - content += "if (value !== null) {"; - content += " if (value >= 0 && value <= 1000) {"; - content += " var xhr = new XMLHttpRequest();"; - content += " xhr.open('GET', '/updateMaxChargeA?value=' + value, true);"; - content += " xhr.send();"; - content += " } else {"; - content += " alert('Invalid value. Please enter a value between 0 and 1000.0');"; - content += " }"; - content += "}"; - content += "}"; - content += "function editMaxDischargeA() {"; - content += - "var value = prompt('BYD CAN specific setting, some inverters needs to be artificially limited. Enter new " - "maximum discharge current in A (0-1000.0):');"; - content += "if (value !== null) {"; - content += " if (value >= 0 && value <= 1000) {"; - content += " var xhr = new XMLHttpRequest();"; - content += " xhr.open('GET', '/updateMaxDischargeA?value=' + value, true);"; - content += " xhr.send();"; - content += " } else {"; - content += " alert('Invalid value. Please enter a value between 0 and 1000.0');"; - content += " }"; - content += "}"; - content += "}"; - -#ifdef TEST_FAKE_BATTERY - content += "function editFakeBatteryVoltage() {"; - content += " var value = prompt('Enter new fake battery voltage');"; - content += "if (value !== null) {"; - content += " if (value >= 0 && value <= 5000) {"; - content += " var xhr = new XMLHttpRequest();"; - content += " xhr.open('GET', '/updateFakeBatteryVoltage?value=' + value, true);"; - content += " xhr.send();"; - content += " } else {"; - content += " alert('Invalid value. Please enter a value between 0 and 1000');"; - content += " }"; - content += "}"; - content += "}"; -#endif - -#if defined CHEVYVOLT_CHARGER || defined NISSANLEAF_CHARGER - content += "function editChargerHVDCEnabled() {"; - content += " var value = prompt('Enable or disable HV DC output. Enter 1 for enabled, 0 for disabled');"; - content += " if (value !== null) {"; - content += " if (value == 0 || value == 1) {"; - content += " var xhr = new XMLHttpRequest();"; - content += " xhr.open('GET', '/updateChargerHvEnabled?value=' + value, true);"; - content += " xhr.send();"; - content += " }"; - content += " } else {"; - content += " alert('Invalid value. Please enter 1 or 0');"; - content += " }"; - content += "}"; - - content += "function editChargerAux12vEnabled() {"; - content += - "var value = prompt('Enable or disable low voltage 12v auxiliary DC output. Enter 1 for enabled, 0 for " - "disabled');"; - content += "if (value !== null) {"; - content += " if (value == 0 || value == 1) {"; - content += " var xhr = new XMLHttpRequest();"; - content += " xhr.open('GET', '/updateChargerAux12vEnabled?value=' + value, true);"; - content += " xhr.send();"; - content += " } else {"; - content += " alert('Invalid value. Please enter 1 or 0');"; - content += " }"; - content += "}"; - content += "}"; - - content += "function editChargerSetpointVDC() {"; - content += - "var value = prompt('Set charging voltage. Input will be validated against inverter and/or charger " - "configuration parameters, but use sensible values like 200 to 420.');"; - content += "if (value !== null) {"; - content += " if (value >= 0 && value <= 1000) {"; - content += " var xhr = new XMLHttpRequest();"; - content += " xhr.open('GET', '/updateChargeSetpointV?value=' + value, true);"; - content += " xhr.send();"; - content += " } else {"; - content += " alert('Invalid value. Please enter a value between 0 and 1000');"; - content += " }"; - content += "}"; - content += "}"; - - content += "function editChargerSetpointIDC() {"; - content += - "var value = prompt('Set charging amperage. Input will be validated against inverter and/or charger " - "configuration parameters, but use sensible values like 6 to 48.');"; - content += "if (value !== null) {"; - content += " if (value >= 0 && value <= 1000) {"; - content += " var xhr = new XMLHttpRequest();"; - content += " xhr.open('GET', '/updateChargeSetpointA?value=' + value, true);"; - content += " xhr.send();"; - content += " } else {"; - content += " alert('Invalid value. Please enter a value between 0 and 100');"; - content += " }"; - content += "}"; - content += "}"; - - content += "function editChargerSetpointEndI() {"; - content += - "var value = prompt('Set amperage that terminates charge as being sufficiently complete. Input will be " - "validated against inverter and/or charger configuration parameters, but use sensible values like 1-5.');"; - content += "if (value !== null) {"; - content += " if (value >= 0 && value <= 1000) {"; - content += " var xhr = new XMLHttpRequest();"; - content += " xhr.open('GET', '/updateChargeEndA?value=' + value, true);"; - content += " xhr.send();"; - content += " } else {"; - content += " alert('Invalid value. Please enter a value between 0 and 100');"; - content += " }"; - content += "}"; - content += "}"; -#endif - content += "</script>"; - - content += "<button onclick='goToMainPage()'>Back to main page</button>"; - content += "<script>"; - content += "function goToMainPage() { window.location.href = '/'; }"; - content += "</script>"; - return content; - } - return String(); -} - -String cellmonitor_processor(const String& var) { - if (var == "PLACEHOLDER") { - String content = ""; - // Page format - content += "<style>"; - content += "body { background-color: black; color: white; }"; - content += ".container { display: flex; flex-wrap: wrap; justify-content: space-around; }"; - content += ".cell { width: 48%; margin: 1%; padding: 10px; border: 1px solid white; text-align: center; }"; - content += ".low-voltage { color: red; }"; // Style for low voltage text - content += ".voltage-values { margin-bottom: 10px; }"; // Style for voltage values section - content += "</style>"; - - // Start a new block with a specific background color - content += "<div style='background-color: #303E47; padding: 10px; margin-bottom: 10px; border-radius: 50px'>"; - - // Display max, min, and deviation voltage values - content += "<div class='voltage-values'>"; - content += "Max Voltage: " + String(cell_max_voltage) + " mV<br>"; - content += "Min Voltage: " + String(cell_min_voltage) + " mV<br>"; - int deviation = cell_max_voltage - cell_min_voltage; - content += "Voltage Deviation: " + String(deviation) + " mV"; - content += "</div>"; - - // Visualize the populated cells in forward order using flexbox with conditional text color - content += "<div class='container'>"; - for (int i = 0; i < 120; ++i) { - // Skip empty values - if (cellvoltages[i] == 0) { - continue; - } - - String cellContent = "Cell " + String(i + 1) + "<br>" + String(cellvoltages[i]) + " mV"; - - // Check if the cell voltage is below 3000, apply red color - if (cellvoltages[i] < 3000) { - cellContent = "<span class='low-voltage'>" + cellContent + "</span>"; - } - - content += "<div class='cell'>" + cellContent + "</div>"; - } - content += "</div>"; - - // Close the block - content += "</div>"; - - content += "<button onclick='goToMainPage()'>Back to main page</button>"; - content += "<script>"; - content += "function goToMainPage() { window.location.href = '/'; }"; - content += "</script>"; - return content; - } - return String(); -} - -#ifdef EVENTLOGGING -const char EVENTS_HTML_START[] PROGMEM = R"=====( -<style> - body { background-color: black; color: white; } - .event-log { display: flex; flex-direction: column; } - .event { display: flex; flex-wrap: wrap; border: 1px solid white; padding: 10px; } - .event > div { flex: 1; min-width: 100px; max-width: 90%; word-break: break-word; } -</style> -<div style='background-color: #303E47; padding: 10px; margin-bottom: 10px;border-radius: 50px'> -<h4 style='color: white;'>Event log:</h4> -<div class="event-log"> -<div class="event"> -<div>Event Type</div><div>LED Color</div><div>Last Event (seconds ago)</div><div>Count</div><div>Data</div><div>Message</div> -</div> -)====="; -const char EVENTS_HTML_END[] PROGMEM = R"=====( -</div></div> -<button onclick='goToMainPage()'>Back to main page</button> -<script> -function goToMainPage() { - window.location.href = '/'; -} -</script> -)====="; - -String events_processor(const String& var) { - if (var == "PLACEHOLDER") { - String content = ""; - content.reserve(5000); - // Page format - content.concat(FPSTR(EVENTS_HTML_START)); - for (int i = 0; i < EVENT_NOF_EVENTS; i++) { - Serial.println("Event: " + String(get_event_enum_string(static_cast<EVENTS_ENUM_TYPE>(i))) + - " count: " + String(entries[i].occurences) + " seconds: " + String(entries[i].timestamp) + - " data: " + String(entries[i].data)); - if (entries[i].occurences > 0) { - content.concat("<div class='event'>"); - content.concat("<div>" + String(get_event_enum_string(static_cast<EVENTS_ENUM_TYPE>(i))) + "</div>"); - content.concat("<div>" + String(get_led_color_display_text(entries[i].led_color)) + "</div>"); - content.concat("<div>" + String((millis() / 1000) - entries[i].timestamp) + "</div>"); - content.concat("<div>" + String(entries[i].occurences) + "</div>"); - content.concat("<div>" + String(entries[i].data) + "</div>"); - content.concat("<div>" + String(get_event_message(static_cast<EVENTS_ENUM_TYPE>(i))) + "</div>"); - content.concat("</div>"); // End of event row - } - } - content.concat(FPSTR(EVENTS_HTML_END)); - return content; - } - return String(); -} -#endif void onOTAStart() { // Log when OTA has started Serial.println("OTA update started!"); diff --git a/Software/src/devboard/webserver/webserver.h b/Software/src/devboard/webserver/webserver.h index c874b728..23b65278 100644 --- a/Software/src/devboard/webserver/webserver.h +++ b/Software/src/devboard/webserver/webserver.h @@ -12,7 +12,6 @@ #include "../../lib/me-no-dev-ESPAsyncWebServer/src/ESPAsyncWebServer.h" #include "../../lib/miwagner-ESP32-Arduino-CAN/ESP32CAN.h" #include "../config.h" // Needed for LED defines -#include "../utils/events.h" #ifdef MQTT #include "../mqtt/mqtt.h" #endif @@ -37,7 +36,6 @@ extern uint16_t cellvoltages[120]; //mV 0-4350 per cell extern uint8_t LEDcolor; //Enum, 0-10 extern bool batteryAllowsContactorClosing; //Bool, 1=true, 0=false extern bool inverterAllowsContactorClosing; //Bool, 1=true, 0=false -extern EVENTS_STRUCT_TYPE entries[EVENT_NOF_EVENTS]; extern const char* ssid; extern const char* password; @@ -120,33 +118,6 @@ void init_ElegantOTA(); */ String processor(const String& var); -/** - * @brief Replaces placeholder with content section in web page - * - * @param[in] var - * - * @return String - */ -String settings_processor(const String& var); - -/** - * @brief Replaces placeholder with content section in web page - * - * @param[in] var - * - * @return String - */ -String cellmonitor_processor(const String& var); - -/** - * @brief Replaces placeholder with content section in web page - * - * @param[in] var - * - * @return String - */ -String events_processor(const String& var); - /** * @brief Executes on OTA start *