Merge branch 'main' into feature/advanced-battery-webpage

This commit is contained in:
Daniel Öster 2024-10-11 12:37:00 +03:00
commit 742f51c658
92 changed files with 5121 additions and 1402 deletions

View file

@ -19,6 +19,10 @@ To update the software over the air:
- In your webbrowser, go to the url consisting of the IP address, followed by `/update`, for instance `http://192.168.0.224/update`.
- In the webbrowser, follow the steps to select the `.bin` file and to upload the file to the board.
Security Concerns
(https://randomnerdtutorials.com/esp32-esp8266-web-server-http-authentication/)
Authentication implemented here is meant to be used in your local network to protect from anyone just typing the ESP IP address and accessing the web server (like unauthorized family member or friend).
## Future work
This section lists a number of features that can be implemented as part of the webserver in the future.

View file

@ -1,17 +1,17 @@
#include "events_html.h"
#include <Arduino.h>
#include "../utils/events.h"
const char EVENTS_HTML_START[] = R"=====(
<style>body{background-color:#000;color:#fff}.event-log{display:flex;flex-direction:column}.event{display:flex;flex-wrap:wrap;border:1px solid #fff;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:25px"><div class="event-log"><div class="event" style="background-color:#1e2c33;font-weight:700"><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='home()'>Back to main page</button>
<style>.event:nth-child(even){background-color:#455a64}.event:nth-child(odd){background-color:#394b52}</style>
<script>function showEvent(){document.querySelectorAll(".event").forEach(function(e){var n=e.querySelector(".sec-ago");n&&(n.innerText=new Date(new Date().getTime()-1e3*parseInt(n.innerText,10)).toLocaleString())})}function home(){window.location.href="/"}window.onload=function(){showEvent()}</script>
<button onclick="askClear()">Clear all events</button>
<button onclick="home()">Back to main page</button>
<style>.event:nth-child(even){background-color:#455a64}.event:nth-child(odd){background-color:#394b52}</style><script>function showEvent(){document.querySelectorAll(".event").forEach(function(e){var n=e.querySelector(".sec-ago");n&&(n.innerText=new Date(Date.now()-(4294967296*+n.innerText.split(";")[0]+ +n.innerText.split(";")[1])).toLocaleString())})}function askClear(){window.confirm("Are you sure you want to clear all events?")&&(window.location.href="/clearevents")}function home(){window.location.href="/"}window.onload=function(){showEvent()}</script>
)=====";
static std::vector<EventData> order_events;
String events_processor(const String& var) {
if (var == "X") {
String content = "";
@ -20,65 +20,76 @@ String events_processor(const String& var) {
content.concat(FPSTR(EVENTS_HTML_START));
const EVENTS_STRUCT_TYPE* event_pointer;
unsigned long timestamp_now = get_current_event_time_secs();
//clear the vector
order_events.clear();
// Collect all events
for (int i = 0; i < EVENT_NOF_EVENTS; i++) {
event_pointer = get_event_pointer((EVENTS_ENUM_TYPE)i);
EVENTS_ENUM_TYPE event_handle = static_cast<EVENTS_ENUM_TYPE>(i);
if (event_pointer->occurences > 0) {
order_events.push_back({static_cast<EVENTS_ENUM_TYPE>(i), event_pointer});
}
}
// Sort events by timestamp
std::sort(order_events.begin(), order_events.end(), compareEventsByTimestampDesc);
unsigned long timestamp_now = millis();
// Generate HTML and debug output
for (const auto& event : order_events) {
EVENTS_ENUM_TYPE event_handle = event.event_handle;
event_pointer = event.event_pointer;
#ifdef DEBUG_VIA_USB
Serial.println("Event: " + String(get_event_enum_string(event_handle)) +
" count: " + String(event_pointer->occurences) + " seconds: " + String(event_pointer->timestamp) +
" data: " + String(event_pointer->data) +
" level: " + String(get_event_level_string(event_handle)));
#endif
if (event_pointer->occurences > 0) {
content.concat("<div class='event'>");
content.concat("<div>" + String(get_event_enum_string(event_handle)) + "</div>");
content.concat("<div>" + String(get_event_level_string(event_handle)) + "</div>");
content.concat("<div class='sec-ago'>" + String(timestamp_now - event_pointer->timestamp) + "</div>");
content.concat("<div>" + String(event_pointer->occurences) + "</div>");
content.concat("<div>" + String(event_pointer->data) + "</div>");
content.concat("<div>" + String(get_event_message_string(event_handle)) + "</div>");
content.concat("</div>"); // End of event row
}
content.concat("<div class='event'>");
content.concat("<div>" + String(get_event_enum_string(event_handle)) + "</div>");
content.concat("<div>" + String(get_event_level_string(event_handle)) + "</div>");
content.concat("<div class='sec-ago'>" + String(millisrolloverCount) + ";" +
String(timestamp_now - event_pointer->timestamp) + "</div>");
content.concat("<div>" + String(event_pointer->occurences) + "</div>");
content.concat("<div>" + String(event_pointer->data) + "</div>");
content.concat("<div>" + String(get_event_message_string(event_handle)) + "</div>");
content.concat("</div>"); // End of event row
}
//clear the vector
order_events.clear();
content.concat(FPSTR(EVENTS_HTML_END));
return content;
return String();
}
return String();
}
/* Script for displaying event log before it gets minified
<button onclick="askClear()">Clear all events</button>
<button onclick="home()">Back to main page</button>
<style>
.event:nth-child(even) {
background-color: #455a64;
}
.event:nth-child(odd) {
background-color: #394b52;
}
</style>
<script>
function showEvent() {
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('.sec-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 showEvent function when the page is loaded
window.onload = function() {
showEvent();
};
function home() {
window.location.href = '/';
}
function showEvent() {
document.querySelectorAll(".event").forEach(function (e) {
var n = e.querySelector(".sec-ago");
n && (n.innerText = new Date(Date.now() - (+n.innerText.split(";")[0] * 4294967296 + +n.innerText.split(";")[1])).toLocaleString());
});
}
function askClear() {
if (window.confirm('Are you sure you want to clear all events?')) {
window.location.href = '/clearevents';
}
}
function home() {
window.location.href = "/";
}
window.onload = function () {
showEvent();
};
</script>
*/

View file

@ -2,6 +2,9 @@
#define EVENTS_H
#include <Arduino.h>
#include <algorithm>
#include <vector>
#include "../utils/events.h"
/**
* @brief Replaces placeholder with content section in web page

View file

@ -1,6 +1,7 @@
#include "webserver.h"
#include <Preferences.h>
#include "../../datalayer/datalayer.h"
#include "../../lib/bblanchon-ArduinoJson/ArduinoJson.h"
#include "../utils/events.h"
#include "../utils/led_handler.h"
#include "../utils/timer.h"
@ -17,48 +18,37 @@ unsigned long ota_progress_millis = 0;
#include "index_html.cpp"
#include "settings_html.h"
enum WifiState {
INIT, //before connecting first time
RECONNECTING, //we've connected before, but lost connection
CONNECTED //we are connected
};
WifiState wifi_state = INIT;
MyTimer ota_timeout_timer = MyTimer(15000);
bool ota_active = false;
unsigned const long WIFI_MONITOR_INTERVAL_TIME = 15000;
unsigned const long INIT_WIFI_CONNECT_TIMEOUT = 8000; // Timeout for initial WiFi connect in milliseconds
unsigned const long DEFAULT_WIFI_RECONNECT_INTERVAL = 1000; // Default WiFi reconnect interval in ms
unsigned const long MAX_WIFI_RETRY_INTERVAL = 90000; // Maximum wifi retry interval in ms
unsigned long last_wifi_monitor_time = millis(); //init millis so wifi monitor doesn't run immediately
unsigned long wifi_reconnect_interval = DEFAULT_WIFI_RECONNECT_INTERVAL;
unsigned long last_wifi_attempt_time = millis(); //init millis so wifi monitor doesn't run immediately
const char get_firmware_info_html[] = R"rawliteral(%X%)rawliteral";
void init_webserver() {
// Configure WiFi
#ifdef WIFIAP
if (AccessPointEnabled) {
WiFi.mode(WIFI_AP_STA); // Simultaneous WiFi AP and Router connection
init_WiFi_AP();
} else {
WiFi.mode(WIFI_STA); // Only Router connection
}
#else
WiFi.mode(WIFI_STA); // Only Router connection
#endif // WIFIAP
init_WiFi_STA(ssid.c_str(), password.c_str(), wifi_channel);
String content = index_html;
server.on("/logout", HTTP_GET, [](AsyncWebServerRequest* request) { request->send(401); });
// Route for firmware info from ota update page
server.on("/GetFirmwareInfo", HTTP_GET, [](AsyncWebServerRequest* request) {
if (WEBSERVER_AUTH_REQUIRED && !request->authenticate(http_username, http_password))
return request->requestAuthentication();
request->send_P(200, "application/json", get_firmware_info_html, get_firmware_info_processor);
});
// Route for root / web page
server.on("/", HTTP_GET,
[](AsyncWebServerRequest* request) { request->send_P(200, "text/html", index_html, processor); });
server.on("/", HTTP_GET, [](AsyncWebServerRequest* request) {
if (WEBSERVER_AUTH_REQUIRED && !request->authenticate(http_username, http_password))
return request->requestAuthentication();
request->send_P(200, "text/html", index_html, processor);
});
// Route for going to settings web page
server.on("/settings", HTTP_GET,
[](AsyncWebServerRequest* request) { request->send_P(200, "text/html", index_html, settings_processor); });
server.on("/settings", HTTP_GET, [](AsyncWebServerRequest* request) {
if (WEBSERVER_AUTH_REQUIRED && !request->authenticate(http_username, http_password))
return request->requestAuthentication();
request->send_P(200, "text/html", index_html, settings_processor);
});
// Route for going to advanced battery info web page
server.on("/advanced", HTTP_GET, [](AsyncWebServerRequest* request) {
@ -67,15 +57,35 @@ void init_webserver() {
// Route for going to cellmonitor web page
server.on("/cellmonitor", HTTP_GET, [](AsyncWebServerRequest* request) {
if (WEBSERVER_AUTH_REQUIRED && !request->authenticate(http_username, http_password))
return request->requestAuthentication();
request->send_P(200, "text/html", index_html, cellmonitor_processor);
});
// 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); });
server.on("/events", HTTP_GET, [](AsyncWebServerRequest* request) {
if (WEBSERVER_AUTH_REQUIRED && !request->authenticate(http_username, http_password))
return request->requestAuthentication();
request->send_P(200, "text/html", index_html, events_processor);
});
// Route for clearing all events
server.on("/clearevents", HTTP_GET, [](AsyncWebServerRequest* request) {
if (WEBSERVER_AUTH_REQUIRED && !request->authenticate(http_username, http_password))
return request->requestAuthentication();
reset_all_events();
// Send back a response that includes an instant redirect to /events
String response = "<html><body>";
response += "<script>window.location.href = '/events';</script>"; // Instant redirect
response += "</body></html>";
request->send(200, "text/html", response);
});
// Route for editing SSID
server.on("/updateSSID", HTTP_GET, [](AsyncWebServerRequest* request) {
if (WEBSERVER_AUTH_REQUIRED && !request->authenticate(http_username, http_password))
return request->requestAuthentication();
if (request->hasParam("value")) {
String value = request->getParam("value")->value();
if (value.length() <= 63) { // Check if SSID is within the allowable length
@ -91,6 +101,8 @@ void init_webserver() {
});
// Route for editing Password
server.on("/updatePassword", HTTP_GET, [](AsyncWebServerRequest* request) {
if (WEBSERVER_AUTH_REQUIRED && !request->authenticate(http_username, http_password))
return request->requestAuthentication();
if (request->hasParam("value")) {
String value = request->getParam("value")->value();
if (value.length() > 8) { // Check if password is within the allowable length
@ -107,6 +119,8 @@ void init_webserver() {
// Route for editing Wh
server.on("/updateBatterySize", HTTP_GET, [](AsyncWebServerRequest* request) {
if (WEBSERVER_AUTH_REQUIRED && !request->authenticate(http_username, http_password))
return request->requestAuthentication();
if (request->hasParam("value")) {
String value = request->getParam("value")->value();
datalayer.battery.info.total_capacity_Wh = value.toInt();
@ -119,6 +133,8 @@ void init_webserver() {
// Route for editing USE_SCALED_SOC
server.on("/updateUseScaledSOC", HTTP_GET, [](AsyncWebServerRequest* request) {
if (WEBSERVER_AUTH_REQUIRED && !request->authenticate(http_username, http_password))
return request->requestAuthentication();
if (request->hasParam("value")) {
String value = request->getParam("value")->value();
datalayer.battery.settings.soc_scaling_active = value.toInt();
@ -131,6 +147,8 @@ void init_webserver() {
// Route for editing SOCMax
server.on("/updateSocMax", HTTP_GET, [](AsyncWebServerRequest* request) {
if (WEBSERVER_AUTH_REQUIRED && !request->authenticate(http_username, http_password))
return request->requestAuthentication();
if (request->hasParam("value")) {
String value = request->getParam("value")->value();
datalayer.battery.settings.max_percentage = static_cast<uint16_t>(value.toFloat() * 100);
@ -141,8 +159,40 @@ void init_webserver() {
}
});
// Route for pause/resume Battery emulator
server.on("/pause", HTTP_GET, [](AsyncWebServerRequest* request) {
if (WEBSERVER_AUTH_REQUIRED && !request->authenticate(http_username, http_password))
return request->requestAuthentication();
if (request->hasParam("p")) {
String valueStr = request->getParam("p")->value();
setBatteryPause(valueStr == "true" || valueStr == "1", false);
request->send(200, "text/plain", "Updated successfully");
} else {
request->send(400, "text/plain", "Bad Request");
}
});
// Route for equipment stop/resume
server.on("/equipmentStop", HTTP_GET, [](AsyncWebServerRequest* request) {
if (WEBSERVER_AUTH_REQUIRED && !request->authenticate(http_username, http_password))
return request->requestAuthentication();
if (request->hasParam("stop")) {
String valueStr = request->getParam("stop")->value();
if (valueStr == "true" || valueStr == "1") {
setBatteryPause(true, true, true);
} else {
setBatteryPause(false, false, false);
}
request->send(200, "text/plain", "Updated successfully");
} else {
request->send(400, "text/plain", "Bad Request");
}
});
// Route for editing SOCMin
server.on("/updateSocMin", HTTP_GET, [](AsyncWebServerRequest* request) {
if (WEBSERVER_AUTH_REQUIRED && !request->authenticate(http_username, http_password))
return request->requestAuthentication();
if (request->hasParam("value")) {
String value = request->getParam("value")->value();
datalayer.battery.settings.min_percentage = static_cast<uint16_t>(value.toFloat() * 100);
@ -155,6 +205,8 @@ void init_webserver() {
// Route for editing MaxChargeA
server.on("/updateMaxChargeA", HTTP_GET, [](AsyncWebServerRequest* request) {
if (WEBSERVER_AUTH_REQUIRED && !request->authenticate(http_username, http_password))
return request->requestAuthentication();
if (request->hasParam("value")) {
String value = request->getParam("value")->value();
datalayer.battery.info.max_charge_amp_dA = static_cast<uint16_t>(value.toFloat() * 10);
@ -167,6 +219,8 @@ void init_webserver() {
// Route for editing MaxDischargeA
server.on("/updateMaxDischargeA", HTTP_GET, [](AsyncWebServerRequest* request) {
if (WEBSERVER_AUTH_REQUIRED && !request->authenticate(http_username, http_password))
return request->requestAuthentication();
if (request->hasParam("value")) {
String value = request->getParam("value")->value();
datalayer.battery.info.max_discharge_amp_dA = static_cast<uint16_t>(value.toFloat() * 10);
@ -180,6 +234,8 @@ void init_webserver() {
#ifdef TEST_FAKE_BATTERY
// Route for editing FakeBatteryVoltage
server.on("/updateFakeBatteryVoltage", HTTP_GET, [](AsyncWebServerRequest* request) {
if (WEBSERVER_AUTH_REQUIRED && !request->authenticate(http_username, http_password))
return request->requestAuthentication();
if (!request->hasParam("value")) {
request->send(400, "text/plain", "Bad Request");
}
@ -196,6 +252,8 @@ void init_webserver() {
#if defined CHEVYVOLT_CHARGER || defined NISSANLEAF_CHARGER
// Route for editing ChargerTargetV
server.on("/updateChargeSetpointV", HTTP_GET, [](AsyncWebServerRequest* request) {
if (WEBSERVER_AUTH_REQUIRED && !request->authenticate(http_username, http_password))
return request->requestAuthentication();
if (!request->hasParam("value")) {
request->send(400, "text/plain", "Bad Request");
}
@ -218,6 +276,8 @@ void init_webserver() {
// Route for editing ChargerTargetA
server.on("/updateChargeSetpointA", HTTP_GET, [](AsyncWebServerRequest* request) {
if (WEBSERVER_AUTH_REQUIRED && !request->authenticate(http_username, http_password))
return request->requestAuthentication();
if (!request->hasParam("value")) {
request->send(400, "text/plain", "Bad Request");
}
@ -240,6 +300,8 @@ void init_webserver() {
// Route for editing ChargerEndA
server.on("/updateChargeEndA", HTTP_GET, [](AsyncWebServerRequest* request) {
if (WEBSERVER_AUTH_REQUIRED && !request->authenticate(http_username, http_password))
return request->requestAuthentication();
if (request->hasParam("value")) {
String value = request->getParam("value")->value();
charger_setpoint_HV_IDC_END = value.toFloat();
@ -251,6 +313,8 @@ void init_webserver() {
// Route for enabling/disabling HV charger
server.on("/updateChargerHvEnabled", HTTP_GET, [](AsyncWebServerRequest* request) {
if (WEBSERVER_AUTH_REQUIRED && !request->authenticate(http_username, http_password))
return request->requestAuthentication();
if (request->hasParam("value")) {
String value = request->getParam("value")->value();
charger_HV_enabled = (bool)value.toInt();
@ -262,6 +326,8 @@ void init_webserver() {
// Route for enabling/disabling aux12v charger
server.on("/updateChargerAux12vEnabled", HTTP_GET, [](AsyncWebServerRequest* request) {
if (WEBSERVER_AUTH_REQUIRED && !request->authenticate(http_username, http_password))
return request->requestAuthentication();
if (request->hasParam("value")) {
String value = request->getParam("value")->value();
charger_aux12V_enabled = (bool)value.toInt();
@ -273,13 +339,21 @@ void init_webserver() {
#endif // defined CHEVYVOLT_CHARGER || defined NISSANLEAF_CHARGER
// Send a GET request to <ESP_IP>/update
server.on("/debug", HTTP_GET,
[](AsyncWebServerRequest* request) { request->send(200, "text/plain", "Debug: all OK."); });
server.on("/debug", HTTP_GET, [](AsyncWebServerRequest* request) {
if (WEBSERVER_AUTH_REQUIRED && !request->authenticate(http_username, http_password))
return request->requestAuthentication();
request->send(200, "text/plain", "Debug: all OK.");
});
// Route to handle reboot command
server.on("/reboot", HTTP_GET, [](AsyncWebServerRequest* request) {
if (WEBSERVER_AUTH_REQUIRED && !request->authenticate(http_username, http_password))
return request->requestAuthentication();
request->send(200, "text/plain", "Rebooting server...");
//TODO: Should we handle contactors gracefully? Ifdef CONTACTOR_CONTROL then what?
//Equipment STOP without persisting the equipment state before restart
// Max Charge/Discharge = 0; CAN = stop; contactors = open
setBatteryPause(true, true, true, false);
delay(1000);
ESP.restart();
});
@ -289,29 +363,8 @@ void init_webserver() {
// Start server
server.begin();
#ifdef MQTT
// Init MQTT
init_mqtt();
#endif // MQTT
}
#ifdef WIFIAP
void init_WiFi_AP() {
#ifdef DEBUG_VIA_USB
Serial.println("Creating Access Point: " + String(ssidAP));
Serial.println("With password: " + String(passwordAP));
#endif // DEBUG_VIA_USB
WiFi.softAP(ssidAP, passwordAP);
IPAddress IP = WiFi.softAPIP();
#ifdef DEBUG_VIA_USB
Serial.println("Access Point created.");
Serial.print("IP address: ");
Serial.println(IP);
#endif // DEBUG_VIA_USB
}
#endif // WIFIAP
String getConnectResultString(wl_status_t status) {
switch (status) {
case WL_CONNECTED:
@ -335,62 +388,11 @@ String getConnectResultString(wl_status_t status) {
}
}
void wifi_monitor() {
unsigned long currentMillis = millis();
if (currentMillis - last_wifi_monitor_time > WIFI_MONITOR_INTERVAL_TIME) {
last_wifi_monitor_time = currentMillis;
wl_status_t status = WiFi.status();
if (status != WL_CONNECTED && status != WL_IDLE_STATUS) {
#ifdef DEBUG_VIA_USB
Serial.println(getConnectResultString(status));
#endif // DEBUG_VIA_USB
if (wifi_state == INIT) { //we haven't been connected yet, try the init logic
init_WiFi_STA(ssid.c_str(), password.c_str(), wifi_channel);
} else { //we were connected before, try the reconnect logic
if (currentMillis - last_wifi_attempt_time > wifi_reconnect_interval) {
last_wifi_attempt_time = currentMillis;
#ifdef DEBUG_VIA_USB
Serial.println("WiFi not connected, trying to reconnect...");
#endif // DEBUG_VIA_USB
wifi_state = RECONNECTING;
WiFi.reconnect();
wifi_reconnect_interval = min(wifi_reconnect_interval * 2, MAX_WIFI_RETRY_INTERVAL);
}
}
} else if (status == WL_CONNECTED && wifi_state != CONNECTED) {
wifi_state = CONNECTED;
wifi_reconnect_interval = DEFAULT_WIFI_RECONNECT_INTERVAL;
// Print local IP address and start web server
#ifdef DEBUG_VIA_USB
Serial.print("Connected to WiFi network: " + String(ssid.c_str()));
Serial.print(" IP address: " + WiFi.localIP().toString());
Serial.print(" Signal Strength: " + String(WiFi.RSSI()) + " dBm");
Serial.println(" Channel: " + String(WiFi.channel()));
Serial.println(" Hostname: " + String(WiFi.getHostname()));
#endif // DEBUG_VIA_USB
}
}
void ota_monitor() {
if (ota_active && ota_timeout_timer.elapsed()) {
// OTA timeout, try to restore can and clear the update event
ESP32Can.CANInit();
clear_event(EVENT_OTA_UPDATE);
set_event(EVENT_OTA_UPDATE_TIMEOUT, 0);
ota_active = false;
}
}
void init_WiFi_STA(const char* ssid, const char* password, const uint8_t wifi_channel) {
// Connect to Wi-Fi network with SSID and password
#ifdef DEBUG_VIA_USB
Serial.print("Connecting to ");
Serial.println(ssid);
#endif // DEBUG_VIA_USB
WiFi.begin(ssid, password, wifi_channel);
WiFi.setAutoReconnect(true); // Enable auto reconnect
wl_status_t result = static_cast<wl_status_t>(WiFi.waitForConnectResult(INIT_WIFI_CONNECT_TIMEOUT));
if (result) {
//TODO: Add event or serial print?
onOTAEnd(false);
}
}
@ -403,6 +405,24 @@ void init_ElegantOTA() {
ElegantOTA.onEnd(onOTAEnd);
}
String get_firmware_info_processor(const String& var) {
if (var == "X") {
String content = "";
static JsonDocument doc;
#ifdef HW_LILYGO
doc["hardware"] = "LilyGo T-CAN485";
#endif // HW_LILYGO
#ifdef HW_STARK
doc["hardware"] = "Stark CMR Module";
#endif // HW_STARK
doc["firmware"] = String(version_number);
serializeJson(doc, content);
return content;
}
return String();
}
String processor(const String& var) {
if (var == "X") {
String content = "";
@ -477,9 +497,9 @@ String processor(const String& var) {
#ifdef BYD_MODBUS
content += "BYD 11kWh HVM battery over Modbus RTU";
#endif // BYD_MODBUS
#ifdef LUNA2000_MODBUS
content += "Luna2000 battery over Modbus RTU";
#endif // LUNA2000_MODBUS
#ifdef FOXESS_CAN
content += "FoxESS compatible HV2600/ECS4100 battery";
#endif // FOXESS_CAN
#ifdef PYLON_CAN
content += "Pylontech battery over CAN bus";
#endif // PYLON_CAN
@ -528,6 +548,12 @@ String processor(const String& var) {
#ifdef NISSAN_LEAF_BATTERY
content += "Nissan LEAF";
#endif // NISSAN_LEAF_BATTERY
#ifdef PYLON_BATTERY
content += "Pylon compatible battery";
#endif // PYLON_BATTERY
#ifdef RJXZS_BMS
content += "RJXZS BMS, DIY battery";
#endif // RJXZS_BMS
#ifdef RENAULT_KANGOO_BATTERY
content += "Renault Kangoo";
#endif // RENAULT_KANGOO_BATTERY
@ -543,9 +569,12 @@ String processor(const String& var) {
#ifdef SERIAL_LINK_RECEIVER
content += "Serial link to another LilyGo board";
#endif // SERIAL_LINK_RECEIVER
#ifdef TESLA_MODEL_3_BATTERY
content += "Tesla Model S/3/X/Y";
#endif // TESLA_MODEL_3_BATTERY
#ifdef TESLA_MODEL_SX_BATTERY
content += "Tesla Model S/X";
#endif // TESLA_MODEL_SX_BATTERY
#ifdef TESLA_MODEL_3Y_BATTERY
content += "Tesla Model 3/Y";
#endif // TESLA_MODEL_3Y_BATTERY
#ifdef VOLVO_SPA_BATTERY
content += "Volvo / Polestar 78kWh battery";
#endif // VOLVO_SPA_BATTERY
@ -554,6 +583,9 @@ String processor(const String& var) {
#endif // TEST_FAKE_BATTERY
#ifdef DOUBLE_BATTERY
content += " (Double battery)";
if (datalayer.battery.info.chemistry == battery_chemistry_enum::LFP) {
content += " (LFP)";
}
#endif // DOUBLE_BATTERY
content += "</h4>";
@ -616,6 +648,8 @@ String processor(const String& var) {
float powerFloat = static_cast<float>(datalayer.battery.status.active_power_W); // Convert to float
float tempMaxFloat = static_cast<float>(datalayer.battery.status.temperature_max_dC) / 10.0; // Convert to float
float tempMinFloat = static_cast<float>(datalayer.battery.status.temperature_min_dC) / 10.0; // Convert to float
uint16_t cell_delta_mv =
datalayer.battery.status.cell_max_voltage_mV - datalayer.battery.status.cell_min_voltage_mV;
content += "<h4 style='color: white;'>Real SOC: " + String(socRealFloat, 2) + "</h4>";
content += "<h4 style='color: white;'>Scaled SOC: " + String(socScaledFloat, 2) + "</h4>";
@ -625,10 +659,22 @@ String processor(const String& var) {
content += formatPowerValue("Power", powerFloat, "", 1);
content += formatPowerValue("Total capacity", datalayer.battery.info.total_capacity_Wh, "h", 0);
content += formatPowerValue("Remaining capacity", datalayer.battery.status.remaining_capacity_Wh, "h", 1);
content += formatPowerValue("Max discharge power", datalayer.battery.status.max_discharge_power_W, "", 1);
content += formatPowerValue("Max charge power", datalayer.battery.status.max_charge_power_W, "", 1);
if (emulator_pause_status == NORMAL) {
content += formatPowerValue("Max discharge power", datalayer.battery.status.max_discharge_power_W, "", 1);
content += formatPowerValue("Max charge power", datalayer.battery.status.max_charge_power_W, "", 1);
} else {
content += formatPowerValue("Max discharge power", datalayer.battery.status.max_discharge_power_W, "", 1, "red");
content += formatPowerValue("Max charge power", datalayer.battery.status.max_charge_power_W, "", 1, "red");
}
content += "<h4>Cell max: " + String(datalayer.battery.status.cell_max_voltage_mV) + " mV</h4>";
content += "<h4>Cell min: " + String(datalayer.battery.status.cell_min_voltage_mV) + " mV</h4>";
if (cell_delta_mv > datalayer.battery.info.max_cell_voltage_deviation_mV) {
content += "<h4 style='color: red;'>Cell delta: " + String(cell_delta_mv) + " mV</h4>";
} else {
content += "<h4>Cell delta: " + String(cell_delta_mv) + " mV</h4>";
}
content += "<h4>Temperature max: " + String(tempMaxFloat, 1) + " C</h4>";
content += "<h4>Temperature min: " + String(tempMinFloat, 1) + " C</h4>";
if (datalayer.battery.status.bms_status == ACTIVE) {
@ -660,6 +706,20 @@ String processor(const String& var) {
} else {
content += "<span style='color: red;'>&#10005;</span></h4>";
}
if (emulator_pause_status == NORMAL)
content += "<h4>Power status: " + String(get_emulator_pause_status().c_str()) + " </h4>";
else
content += "<h4 style='color: red;'>Power status: " + String(get_emulator_pause_status().c_str()) + " </h4>";
#ifdef CONTACTOR_CONTROL
content += "<h4>Contactors controlled by Battery-Emulator: ";
if (datalayer.system.status.contactor_control_closed) {
content += "<span style='color: green;'>ON</span>";
} else {
content += "<span style='color: red;'>OFF</span>";
}
content += "</h4>";
#endif
// Close the block
content += "</div>";
@ -692,6 +752,7 @@ String processor(const String& var) {
powerFloat = static_cast<float>(datalayer.battery2.status.active_power_W); // Convert to float
tempMaxFloat = static_cast<float>(datalayer.battery2.status.temperature_max_dC) / 10.0; // Convert to float
tempMinFloat = static_cast<float>(datalayer.battery2.status.temperature_min_dC) / 10.0; // Convert to float
cell_delta_mv = datalayer.battery2.status.cell_max_voltage_mV - datalayer.battery2.status.cell_min_voltage_mV;
content += "<h4 style='color: white;'>Real SOC: " + String(socRealFloat, 2) + "</h4>";
content += "<h4 style='color: white;'>Scaled SOC: " + String(socScaledFloat, 2) + "</h4>";
@ -705,6 +766,11 @@ String processor(const String& var) {
content += formatPowerValue("Max charge power", datalayer.battery2.status.max_charge_power_W, "", 1);
content += "<h4>Cell max: " + String(datalayer.battery2.status.cell_max_voltage_mV) + " mV</h4>";
content += "<h4>Cell min: " + String(datalayer.battery2.status.cell_min_voltage_mV) + " mV</h4>";
if (cell_delta_mv > datalayer.battery2.info.max_cell_voltage_deviation_mV) {
content += "<h4 style='color: red;'>Cell delta: " + String(cell_delta_mv) + " mV</h4>";
} else {
content += "<h4>Cell delta: " + String(cell_delta_mv) + " mV</h4>";
}
content += "<h4>Temperature max: " + String(tempMaxFloat, 1) + " C</h4>";
content += "<h4>Temperature min: " + String(tempMinFloat, 1) + " C</h4>";
if (datalayer.battery.status.bms_status == ACTIVE) {
@ -737,6 +803,21 @@ String processor(const String& var) {
content += "<span style='color: red;'>&#10005;</span></h4>";
}
#ifdef CONTACTOR_CONTROL
content += "<h4>Contactors controlled by Battery-Emulator: ";
if (datalayer.system.status.contactor_control_closed) {
content += "<span style='color: green;'>ON</span>";
} else {
content += "<span style='color: red;'>OFF</span>";
}
content += "</h4>";
#endif
if (emulator_pause_status == NORMAL)
content += "<h4>Pause status: " + String(get_emulator_pause_status().c_str()) + " </h4>";
else
content += "<h4 style='color: red;'>Pause status: " + String(get_emulator_pause_status().c_str()) + " </h4>";
content += "</div>";
content += "</div>";
#endif // DOUBLE_BATTERY
@ -797,6 +878,39 @@ String processor(const String& var) {
content += "</div>";
#endif // defined CHEVYVOLT_CHARGER || defined NISSANLEAF_CHARGER
if (emulator_pause_request_ON)
content += "<button onclick='PauseBattery(false)'>Resume charge/discharge</button>";
else
content +=
"<button onclick=\"if(confirm('Are you sure you want to pause charging and discharging? This will set the "
"maximum charge and discharge values to zero, preventing any further power flow.')) { PauseBattery(true); "
"}\">Pause charge/discharge</button>";
content += " ";
content += "<button onclick='OTA()'>Perform OTA update</button>";
content += " ";
content += "<button onclick='Settings()'>Change Settings</button>";
content += " ";
content += "<button onclick='Cellmon()'>Cellmonitor</button>";
content += " ";
content += "<button onclick='Events()'>Events</button>";
content += " ";
content += "<button onclick='askReboot()'>Reboot Emulator</button>";
if (WEBSERVER_AUTH_REQUIRED)
content += "<button onclick='logout()'>Logout</button>";
if (!datalayer.system.settings.equipment_stop_active)
content +=
"<br/><br/><button style=\"background:red;color:white;cursor:pointer;\""
" onclick=\""
"if(confirm('This action will open contactors on the battery and stop all CAN communications. Are you "
"sure?')) { estop(true); }\""
">Open Contactors</button><br/>";
else
content +=
"<br/><br/><button style=\"background:green;color:white;cursor:pointer;\""
"20px;font-size:16px;font-weight:bold;cursor:pointer;border-radius:5px; margin:10px;"
" onclick=\""
"if(confirm('This action will restore the battery state. Are you sure?')) { estop(false); }\""
">Close Contactors</button><br/>";
content += "<script>";
content += "function OTA() { window.location.href = '/update'; }";
content += "function Cellmon() { window.location.href = '/cellmonitor'; }";
@ -812,6 +926,26 @@ String processor(const String& var) {
content += " xhr.open('GET', '/reboot', true);";
content += " xhr.send();";
content += "}";
if (WEBSERVER_AUTH_REQUIRED) {
content += "function logout() {";
content += " var xhr = new XMLHttpRequest();";
content += " xhr.open('GET', '/logout', true);";
content += " xhr.send();";
content += " setTimeout(function(){ window.open(\"/\",\"_self\"); }, 1000);";
content += "}";
}
content += "function PauseBattery(pause){";
content +=
"var xhr=new "
"XMLHttpRequest();xhr.onload=function() { "
"window.location.reload();};xhr.open('GET','/pause?p='+pause,true);xhr.send();";
content += "}";
content += "function estop(stop){";
content +=
"var xhr=new "
"XMLHttpRequest();xhr.onload=function() { "
"window.location.reload();};xhr.open('GET','/equipmentStop?stop='+stop,true);xhr.send();";
content += "}";
content += "</script>";
//Script for refreshing page
@ -825,13 +959,16 @@ String processor(const String& var) {
}
void onOTAStart() {
//try to Pause the battery
setBatteryPause(true, true);
// Log when OTA has started
ESP32Can.CANStop();
set_event(EVENT_OTA_UPDATE, 0);
// If already set, make a new attempt
clear_event(EVENT_OTA_UPDATE_TIMEOUT);
ota_active = true;
ota_timeout_timer.reset();
}
@ -848,8 +985,16 @@ void onOTAProgress(size_t current, size_t final) {
}
void onOTAEnd(bool success) {
ota_active = false;
clear_event(EVENT_OTA_UPDATE);
// Log when OTA has finished
if (success) {
//Equipment STOP without persisting the equipment state before restart
// Max Charge/Discharge = 0; CAN = stop; contactors = open
setBatteryPause(true, true, true, false);
// a reboot will be done by the OTA library. no need to do anything here
#ifdef DEBUG_VIA_USB
Serial.println("OTA update finished successfully!");
#endif // DEBUG_VIA_USB
@ -857,17 +1002,14 @@ void onOTAEnd(bool success) {
#ifdef DEBUG_VIA_USB
Serial.println("There was an error during OTA update!");
#endif // DEBUG_VIA_USB
// If we fail without a timeout, try to restore CAN
ESP32Can.CANInit();
//try to Resume the battery pause and CAN communication
setBatteryPause(false, false);
}
ota_active = false;
clear_event(EVENT_OTA_UPDATE);
}
template <typename T> // This function makes power values appear as W when under 1000, and kW when over
String formatPowerValue(String label, T value, String unit, int precision) {
String result = "<h4 style='color: white;'>" + label + ": ";
String formatPowerValue(String label, T value, String unit, int precision, String color) {
String result = "<h4 style='color: " + color + ";'>" + label + ": ";
if (std::is_same<T, float>::value || std::is_same<T, uint16_t>::value || std::is_same<T, uint32_t>::value) {
float convertedValue = static_cast<float>(value);

View file

@ -6,24 +6,17 @@
#include "../../include.h"
#include "../../lib/YiannisBourkelis-Uptime-Library/src/uptime_formatter.h"
#include "../../lib/ayushsharma82-ElegantOTA/src/ElegantOTA.h"
#ifdef MQTT
#include "../../lib/knolleary-pubsubclient/PubSubClient.h"
#endif
#include "../../lib/me-no-dev-AsyncTCP/src/AsyncTCP.h"
#include "../../lib/me-no-dev-ESPAsyncWebServer/src/ESPAsyncWebServer.h"
#include "../../lib/miwagner-ESP32-Arduino-CAN/ESP32CAN.h"
#ifdef MQTT
#include "../mqtt/mqtt.h"
#endif
extern const char* version_number; // The current software version, shown on webserver
#include <string>
extern std::string ssid;
extern std::string password;
extern const uint8_t wifi_channel;
extern const char* http_username;
extern const char* http_password;
extern const char* ssidAP;
extern const char* passwordAP;
// Common charger parameters
extern float charger_stat_HVcur;
@ -45,36 +38,6 @@ extern uint16_t OBC_Charge_Power;
*/
void init_webserver();
/**
* @brief Monitoring loop for WiFi. Will attempt to reconnect to access point if the connection goes down.
*
* @param[in] void
*
* @return void
*/
void wifi_monitor();
#ifdef WIFIAP
/**
* @brief Initialization function that creates a WiFi Access Point.
*
* @param[in] void
*
* @return void
*/
void init_WiFi_AP();
#endif // WIFIAP
/**
* @brief Initialization function that connects to an existing network.
*
* @param[in] ssid WiFi network name
* @param[in] password WiFi network password
*
* @return void
*/
void init_WiFi_STA(const char* ssid, const char* password, const uint8_t channel);
// /**
// * @brief Function to handle WiFi reconnection.
// *
@ -101,6 +64,7 @@ void init_ElegantOTA();
* @return String
*/
String processor(const String& var);
String get_firmware_info_processor(const String& var);
/**
* @brief Executes on OTA start
@ -138,8 +102,10 @@ void onOTAEnd(bool success);
* @return string: values
*/
template <typename T>
String formatPowerValue(String label, T value, String unit, int precision);
String formatPowerValue(String label, T value, String unit, int precision, String color = "white");
extern void storeSettings();
void ota_monitor();
#endif