#include "webserver.h"
// 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(
Battery Emulator
Battery Emulator
%PLACEHOLDER%
)rawliteral";
String wifi_state;
bool wifi_connected;
// Wifi connect time declarations and definition
unsigned long wifi_connect_start_time;
unsigned long wifi_connect_current_time;
const long wifi_connect_timeout = 5000; // Timeout for WiFi connect in milliseconds
void init_webserver() {
// Configure WiFi
if (AccessPointEnabled) {
WiFi.mode(WIFI_AP_STA); // Simultaneous WiFi AP and Router connection
init_WiFi_AP();
init_WiFi_STA(ssid, password);
} else {
WiFi.mode(WIFI_STA); // Only Router connection
init_WiFi_STA(ssid, password);
}
// Route for root / web page
server.on("/", HTTP_GET,
[](AsyncWebServerRequest* request) { 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); });
// Route for editing Wh
server.on("/updateBatterySize", HTTP_GET, [](AsyncWebServerRequest* request) {
if (request->hasParam("value")) {
String value = request->getParam("value")->value();
BATTERY_WH_MAX = value.toInt();
request->send(200, "text/plain", "Updated successfully");
} else {
request->send(400, "text/plain", "Bad Request");
}
});
// Route for editing SOCMax
server.on("/updateSocMax", HTTP_GET, [](AsyncWebServerRequest* request) {
if (request->hasParam("value")) {
String value = request->getParam("value")->value();
MAXPERCENTAGE = value.toInt() * 10;
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 (request->hasParam("value")) {
String value = request->getParam("value")->value();
MINPERCENTAGE = value.toInt() * 10;
request->send(200, "text/plain", "Updated successfully");
} else {
request->send(400, "text/plain", "Bad Request");
}
});
// Route for editing MaxChargeA
server.on("/updateMaxChargeA", HTTP_GET, [](AsyncWebServerRequest* request) {
if (request->hasParam("value")) {
String value = request->getParam("value")->value();
MAXCHARGEAMP = value.toInt() * 10;
request->send(200, "text/plain", "Updated successfully");
} else {
request->send(400, "text/plain", "Bad Request");
}
});
// Route for editing MaxDischargeA
server.on("/updateMaxDischargeA", HTTP_GET, [](AsyncWebServerRequest* request) {
if (request->hasParam("value")) {
String value = request->getParam("value")->value();
MAXDISCHARGEAMP = value.toInt() * 10;
request->send(200, "text/plain", "Updated successfully");
} else {
request->send(400, "text/plain", "Bad Request");
}
});
#ifdef TEST_FAKE_BATTERY
// Route for editing FakeBatteryVoltage
server.on("/updateFakeBatteryVoltage", HTTP_GET, [](AsyncWebServerRequest* request) {
if (!request->hasParam("value")) {
request->send(400, "text/plain", "Bad Request");
}
String value = request->getParam("value")->value();
float val = value.toFloat();
battery_voltage = val*10;
request->send(200, "text/plain", "Updated successfully");
});
#endif
#if defined CHEVYVOLT_CHARGER || defined NISSANLEAF_CHARGER
// Route for editing ChargerTargetV
server.on("/updateChargeSetpointV", HTTP_GET, [](AsyncWebServerRequest* request) {
if (!request->hasParam("value")) {
request->send(400, "text/plain", "Bad Request");
}
String value = request->getParam("value")->value();
float val = value.toFloat();
if (!(val <= CHARGER_MAX_HV && val >= CHARGER_MIN_HV)) {
request->send(400, "text/plain", "Bad Request");
}
if (!(val * charger_setpoint_HV_IDC <= CHARGER_MAX_POWER)) {
request->send(400, "text/plain", "Bad Request");
}
charger_setpoint_HV_VDC = val;
request->send(200, "text/plain", "Updated successfully");
});
// Route for editing ChargerTargetA
server.on("/updateChargeSetpointA", HTTP_GET, [](AsyncWebServerRequest* request) {
if (!request->hasParam("value")) {
request->send(400, "text/plain", "Bad Request");
}
String value = request->getParam("value")->value();
float val = value.toFloat();
if (!(val <= MAXCHARGEAMP && val <= CHARGER_MAX_A)) {
request->send(400, "text/plain", "Bad Request");
}
if (!(val * charger_setpoint_HV_VDC <= CHARGER_MAX_POWER)) {
request->send(400, "text/plain", "Bad Request");
}
charger_setpoint_HV_IDC = value.toFloat();
request->send(200, "text/plain", "Updated successfully");
});
// Route for editing ChargerEndA
server.on("/updateChargeEndA", HTTP_GET, [](AsyncWebServerRequest* request) {
if (request->hasParam("value")) {
String value = request->getParam("value")->value();
charger_setpoint_HV_IDC_END = value.toFloat();
request->send(200, "text/plain", "Updated successfully");
} else {
request->send(400, "text/plain", "Bad Request");
}
});
// Route for enabling/disabling HV charger
server.on("/updateChargerHvEnabled", HTTP_GET, [](AsyncWebServerRequest* request) {
if (request->hasParam("value")) {
String value = request->getParam("value")->value();
charger_HV_enabled = (bool)value.toInt();
request->send(200, "text/plain", "Updated successfully");
} else {
request->send(400, "text/plain", "Bad Request");
}
});
// Route for enabling/disabling aux12v charger
server.on("/updateChargerAux12vEnabled", HTTP_GET, [](AsyncWebServerRequest* request) {
if (request->hasParam("value")) {
String value = request->getParam("value")->value();
charger_aux12V_enabled = (bool)value.toInt();
request->send(200, "text/plain", "Updated successfully");
} else {
request->send(400, "text/plain", "Bad Request");
}
});
#endif
// Send a GET request to /update
server.on("/debug", HTTP_GET,
[](AsyncWebServerRequest* request) { request->send(200, "text/plain", "Debug: all OK."); });
// Route to handle reboot command
server.on("/reboot", HTTP_GET, [](AsyncWebServerRequest* request) {
request->send(200, "text/plain", "Rebooting server...");
//TODO: Should we handle contactors gracefully? Ifdef CONTACTOR_CONTROL then what?
delay(1000);
ESP.restart();
});
// Initialize ElegantOTA
init_ElegantOTA();
// Start server
server.begin();
}
void init_WiFi_AP() {
Serial.print("Creating Access Point: ");
Serial.println(ssidAP);
Serial.print("With password: ");
Serial.println(passwordAP);
WiFi.softAP(ssidAP, passwordAP);
IPAddress IP = WiFi.softAPIP();
Serial.println("Access Point created.");
Serial.print("IP address: ");
Serial.println(IP);
}
void init_WiFi_STA(const char* ssid, const char* password) {
// Connect to Wi-Fi network with SSID and password
Serial.print("Connecting to ");
Serial.println(ssid);
WiFi.begin(ssid, password);
wifi_connect_start_time = millis();
wifi_connect_current_time = wifi_connect_start_time;
while ((wifi_connect_current_time - wifi_connect_start_time) <= wifi_connect_timeout &&
WiFi.status() != WL_CONNECTED) { // do this loop for up to 5000ms
// to break the loop when the connection is not established (wrong ssid or password).
delay(500);
Serial.print(".");
wifi_connect_current_time = millis();
}
if (WiFi.status() == WL_CONNECTED) { // WL_CONNECTED is assigned when connected to a WiFi network
wifi_connected = true;
wifi_state = "Connected";
// Print local IP address and start web server
Serial.println("");
Serial.print("Connected to WiFi network: ");
Serial.println(ssid);
Serial.print("IP address: ");
Serial.println(WiFi.localIP());
} else {
wifi_connected = false;
wifi_state = "Not connected";
Serial.print("Not connected to WiFi network: ");
Serial.println(ssid);
Serial.println("Please check WiFi network name and password, and if WiFi network is available.");
}
}
void init_ElegantOTA() {
ElegantOTA.begin(&server); // Start ElegantOTA
// ElegantOTA callbacks
ElegantOTA.onStart(onOTAStart);
ElegantOTA.onProgress(onOTAProgress);
ElegantOTA.onEnd(onOTAEnd);
}
String processor(const String& var) {
if (var == "PLACEHOLDER") {
String content = "";
//Page format
content += "";
// Start a new block with a specific background color
content += "
";
// Show version number
content += "
Software version: " + String(versionNumber) + "
";
// Display LED color
content += "
LED color: ";
switch (LEDcolor) {
case GREEN:
content += "GREEN
";
break;
case YELLOW:
content += "YELLOW";
break;
case BLUE:
content += "BLUE";
break;
case RED:
content += "RED";
break;
case TEST_ALL_COLORS:
content += "RGB Testing loop";
break;
default:
break;
}
// Display ssid of network connected to and, if connected to the WiFi, its own IP
content += "
SSID: " + String(ssid) + "
";
content += "
Wifi status: " + wifi_state + "
";
if (wifi_connected == true) {
content += "
IP: " + WiFi.localIP().toString() + "
";
// Get and display the signal strength (RSSI)
content += "
Signal Strength: " + String(WiFi.RSSI()) + " dBm
";
}
// Close the block
content += "
";
// Start a new block with a specific background color
content += "
";
// Display which components are used
content += "
Inverter protocol: ";
#ifdef BYD_CAN
content += "BYD Battery-Box Premium HVS over CAN Bus";
#endif
#ifdef BYD_MODBUS
content += "BYD 11kWh HVM battery over Modbus RTU";
#endif
#ifdef LUNA2000_MODBUS
content += "Luna2000 battery over Modbus RTU";
#endif
#ifdef PYLON_CAN
content += "Pylontech battery over CAN bus";
#endif
#ifdef SERIAL_LINK_TRANSMITTER
content += "Serial link to another LilyGo board";
#endif
#ifdef SMA_CAN
content += "BYD Battery-Box H 8.9kWh, 7 mod over CAN bus";
#endif
#ifdef SOFAR_CAN
content += "Sofar Energy Storage Inverter High Voltage BMS General Protocol (Extended Frame) over CAN bus";
#endif
#ifdef SOLAX_CAN
content += "SolaX Triple Power LFP over CAN bus";
#endif
content += "
Charger Voltage Setpoint: " + String(charger_setpoint_HV_VDC, 1) +
" V
";
content += "
Charger Current Setpoint: " + String(charger_setpoint_HV_IDC, 1) +
" A
";
// Close the block
content += "
";
#endif
content += "";
content += "";
content += "";
return content;
}
return String();
}
void onOTAStart() {
// Log when OTA has started
Serial.println("OTA update started!");
ESP32Can.CANStop();
bms_status = UPDATING; //Inform inverter that we are updating
LEDcolor = BLUE;
}
void onOTAProgress(size_t current, size_t final) {
bms_status = UPDATING; //Inform inverter that we are updating
LEDcolor = BLUE;
// Log every 1 second
if (millis() - ota_progress_millis > 1000) {
ota_progress_millis = millis();
Serial.printf("OTA Progress Current: %u bytes, Final: %u bytes\n", current, final);
}
}
void onOTAEnd(bool success) {
// Log when OTA has finished
if (success) {
Serial.println("OTA update finished successfully!");
} else {
Serial.println("There was an error during OTA update!");
}
bms_status = UPDATING; //Inform inverter that we are updating
LEDcolor = BLUE;
}
template // 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 = "
" + label + ": ";
if (std::is_same::value || std::is_same::value) {
float convertedValue = static_cast(value);
if (convertedValue >= 1000.0 || convertedValue <= -1000.0) {
result += String(convertedValue / 1000.0, precision) + " kW";
} else {
result += String(convertedValue, 0) + " W";
}
}
result += unit + "