637 lines
17 KiB
Arduino
637 lines
17 KiB
Arduino
#include <WiFi.h>
|
|
#include <WebServer.h>
|
|
#include <ArduinoJson.h>
|
|
|
|
#include "config.h"
|
|
#include "protocol.h"
|
|
#include "relays.h"
|
|
#include "sensors.h"
|
|
#include "bms.h"
|
|
#include "alarms.h"
|
|
#include "system_status.h"
|
|
#include "app_config.h"
|
|
|
|
WebServer server(80);
|
|
HardwareSerial DashboardSerial(2);
|
|
|
|
String uartLineBuffer;
|
|
|
|
float calculateRuntimeHours() {
|
|
if (!bmsData.valid || bmsData.current >= 0) {
|
|
return 0;
|
|
}
|
|
|
|
float dischargeAmps = -bmsData.current;
|
|
if (dischargeAmps <= 0.1) {
|
|
return 0;
|
|
}
|
|
|
|
return bmsData.remainingAh / dischargeAmps;
|
|
}
|
|
|
|
void buildStatusDocument(JsonDocument& doc) {
|
|
doc["type"] = MSG_STATUS_RESPONSE;
|
|
doc["timestamp"] = millis();
|
|
|
|
JsonObject battery = doc.createNestedObject("battery");
|
|
|
|
if (bmsData.valid) {
|
|
battery["source"] = "jbd_bms";
|
|
battery["connected"] = bmsData.connected;
|
|
battery["soc"] = bmsData.soc;
|
|
battery["voltage"] = bmsData.voltage;
|
|
battery["current"] = bmsData.current;
|
|
battery["remaining_ah"] = bmsData.remainingAh;
|
|
battery["capacity_ah"] = bmsData.capacityAh;
|
|
battery["runtime_hours"] = calculateRuntimeHours();
|
|
battery["temperature_f"] = bmsData.temperatureF;
|
|
battery["cycle_count"] = bmsData.cycleCount;
|
|
battery["cell_count"] = bmsData.cellCount;
|
|
battery["ntc_count"] = bmsData.ntcCount;
|
|
|
|
JsonArray cells = battery.createNestedArray("cell_voltages");
|
|
if (bmsData.cellsValid) {
|
|
for (int i = 0; i < bmsData.cellCount; i++) {
|
|
cells.add(bmsData.cellVoltages[i]);
|
|
}
|
|
}
|
|
|
|
battery["cell_min_voltage"] = bmsData.cellMinVoltage;
|
|
battery["cell_max_voltage"] = bmsData.cellMaxVoltage;
|
|
battery["cell_delta_mv"] = bmsData.cellDeltaMv;
|
|
battery["cells_valid"] = bmsData.cellsValid;
|
|
} else {
|
|
battery["source"] = "placeholder";
|
|
battery["connected"] = false;
|
|
battery["soc"] = 0;
|
|
battery["voltage"] = 0;
|
|
battery["current"] = 0;
|
|
battery["remaining_ah"] = 0;
|
|
battery["capacity_ah"] = 150;
|
|
battery["runtime_hours"] = 0;
|
|
battery["temperature_f"] = 0;
|
|
battery["cycle_count"] = 0;
|
|
battery["cell_count"] = 0;
|
|
battery["ntc_count"] = 0;
|
|
}
|
|
|
|
JsonObject temps = doc.createNestedObject("temps");
|
|
|
|
if (sensors.fridgeZone1Online) {
|
|
temps["fridge_zone_1"] = sensors.fridgeZone1;
|
|
} else {
|
|
temps["fridge_zone_1"] = nullptr;
|
|
}
|
|
|
|
if (sensors.fridgeZone2Online) {
|
|
temps["fridge_zone_2"] = sensors.fridgeZone2;
|
|
} else {
|
|
temps["fridge_zone_2"] = nullptr;
|
|
}
|
|
|
|
if (sensors.rearSeatOnline) {
|
|
temps["rear_seat"] = sensors.rearSeat;
|
|
} else {
|
|
temps["rear_seat"] = nullptr;
|
|
}
|
|
|
|
if (sensors.outsideAirOnline) {
|
|
temps["outside"] = sensors.outsideAir;
|
|
} else {
|
|
temps["outside"] = nullptr;
|
|
}
|
|
|
|
JsonObject sensorHealth = doc.createNestedObject("sensor_health");
|
|
sensorHealth["fridge_zone_1"] = sensors.fridgeZone1Online;
|
|
sensorHealth["fridge_zone_2"] = sensors.fridgeZone2Online;
|
|
sensorHealth["rear_seat"] = sensors.rearSeatOnline;
|
|
sensorHealth["outside"] = sensors.outsideAirOnline;
|
|
|
|
JsonObject relayObj = doc.createNestedObject("relays");
|
|
relayObj["starlink"] = relays.starlink;
|
|
relayObj["fridge"] = relays.fridge;
|
|
|
|
JsonObject vehicle = doc.createNestedObject("vehicle");
|
|
vehicle["ignition_on"] = digitalRead(IGNITION_PIN);
|
|
|
|
JsonObject network = doc.createNestedObject("network");
|
|
network["wifi_enabled"] = true;
|
|
network["uart_connected"] = true;
|
|
|
|
JsonObject alarmObj = doc.createNestedObject("alarms");
|
|
alarmObj["low_soc"] = alarms.lowSoc;
|
|
alarmObj["critical_soc"] = alarms.criticalSoc;
|
|
alarmObj["low_voltage"] = alarms.lowVoltage;
|
|
alarmObj["high_battery_temp"] = alarms.highBatteryTemp;
|
|
alarmObj["cell_imbalance"] = alarms.cellImbalance;
|
|
alarmObj["bms_disconnected"] = alarms.bmsDisconnected;
|
|
|
|
JsonObject system = doc.createNestedObject("system");
|
|
system["firmware_name"] = FIRMWARE_NAME;
|
|
system["firmware_version"] = FIRMWARE_VERSION;
|
|
system["build_date"] = __DATE__;
|
|
system["build_time"] = __TIME__;
|
|
system["uptime_seconds"] = millis() / 1000;
|
|
|
|
JsonObject configObj = doc.createNestedObject("config");
|
|
configObj["device_name"] = appConfig.deviceName;
|
|
|
|
JsonArray configRelays = configObj.createNestedArray("relays");
|
|
for (int i = 0; i < MAX_RELAYS; i++) {
|
|
JsonObject relayConfig = configRelays.createNestedObject();
|
|
relayConfig["id"] = appConfig.relays[i].id;
|
|
relayConfig["name"] = appConfig.relays[i].name;
|
|
relayConfig["pin"] = appConfig.relays[i].pin;
|
|
relayConfig["enabled"] = appConfig.relays[i].enabled;
|
|
}
|
|
|
|
JsonObject bmsConfig = configObj.createNestedObject("bms");
|
|
bmsConfig["enabled"] = appConfig.bms.enabled;
|
|
bmsConfig["name"] = appConfig.bms.name;
|
|
bmsConfig["address"] = appConfig.bms.address;
|
|
bmsConfig["address_type"] = appConfig.bms.addressType;
|
|
|
|
JsonArray tempConfigs = configObj.createNestedArray("temperature_sensors");
|
|
for (int i = 0; i < MAX_TEMP_SENSORS; i++) {
|
|
JsonObject tempConfig = tempConfigs.createNestedObject();
|
|
tempConfig["id"] = appConfig.tempSensors[i].id;
|
|
tempConfig["name"] = appConfig.tempSensors[i].name;
|
|
tempConfig["address"] = appConfig.tempSensors[i].address;
|
|
tempConfig["enabled"] = appConfig.tempSensors[i].enabled;
|
|
}
|
|
}
|
|
|
|
void sendStatus(Stream& output, bool pretty = false) {
|
|
DynamicJsonDocument doc(2048);
|
|
buildStatusDocument(doc);
|
|
|
|
if (pretty) {
|
|
serializeJsonPretty(doc, output);
|
|
} else {
|
|
serializeJson(doc, output);
|
|
}
|
|
|
|
output.println();
|
|
}
|
|
|
|
void sendError(Stream& output, const char* message) {
|
|
DynamicJsonDocument doc(256);
|
|
doc["type"] = MSG_ERROR;
|
|
doc["message"] = message;
|
|
serializeJson(doc, output);
|
|
output.println();
|
|
}
|
|
|
|
bool setRelayByName(const char* relayName, bool enabled) {
|
|
if (strcmp(relayName, "starlink") == 0) {
|
|
relays.starlink = enabled;
|
|
} else if (strcmp(relayName, "fridge") == 0) {
|
|
relays.fridge = enabled;
|
|
} else {
|
|
return false;
|
|
}
|
|
|
|
updateRelayOutputs();
|
|
return true;
|
|
}
|
|
|
|
void sendRelayResponse(Stream& output, const char* relayName, bool enabled) {
|
|
DynamicJsonDocument doc(256);
|
|
doc["type"] = MSG_RELAY_RESPONSE;
|
|
doc["relay"] = relayName;
|
|
doc["enabled"] = enabled;
|
|
doc["ok"] = true;
|
|
serializeJson(doc, output);
|
|
output.println();
|
|
}
|
|
|
|
void handleUartMessage(const String& line) {
|
|
DynamicJsonDocument doc(512);
|
|
DeserializationError error = deserializeJson(doc, line);
|
|
|
|
if (error) {
|
|
sendError(DashboardSerial, "invalid_json");
|
|
return;
|
|
}
|
|
|
|
const char* type = doc["type"] | "";
|
|
|
|
if (strcmp(type, MSG_STATUS_REQUEST) == 0) {
|
|
sendStatus(DashboardSerial);
|
|
return;
|
|
}
|
|
|
|
if (strcmp(type, MSG_SET_RELAY) == 0) {
|
|
const char* relayName = doc["relay"] | "";
|
|
bool enabled = doc["enabled"] | false;
|
|
|
|
if (!setRelayByName(relayName, enabled)) {
|
|
sendError(DashboardSerial, "unknown_relay");
|
|
return;
|
|
}
|
|
|
|
sendRelayResponse(DashboardSerial, relayName, enabled);
|
|
return;
|
|
}
|
|
|
|
sendError(DashboardSerial, "unknown_message_type");
|
|
}
|
|
|
|
void pollDashboardUart() {
|
|
while (DashboardSerial.available()) {
|
|
char c = DashboardSerial.read();
|
|
|
|
if (c == '\n') {
|
|
uartLineBuffer.trim();
|
|
|
|
if (uartLineBuffer.length() > 0) {
|
|
handleUartMessage(uartLineBuffer);
|
|
}
|
|
|
|
uartLineBuffer = "";
|
|
} else if (c != '\r') {
|
|
uartLineBuffer += c;
|
|
|
|
if (uartLineBuffer.length() > 512) {
|
|
uartLineBuffer = "";
|
|
sendError(DashboardSerial, "message_too_long");
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
void handleDebugSerial() {
|
|
if (!Serial.available()) {
|
|
return;
|
|
}
|
|
|
|
String command = Serial.readStringUntil('\n');
|
|
command.trim();
|
|
|
|
if (command == "status") {
|
|
sendStatus(Serial, true);
|
|
return;
|
|
}
|
|
|
|
if (command == "relay starlink on") {
|
|
setRelayByName("starlink", true);
|
|
Serial.println("OK starlink on");
|
|
return;
|
|
}
|
|
|
|
if (command == "relay starlink off") {
|
|
setRelayByName("starlink", false);
|
|
Serial.println("OK starlink off");
|
|
return;
|
|
}
|
|
|
|
if (command == "relay fridge on") {
|
|
setRelayByName("fridge", true);
|
|
Serial.println("OK fridge on");
|
|
return;
|
|
}
|
|
|
|
if (command == "relay fridge off") {
|
|
setRelayByName("fridge", false);
|
|
Serial.println("OK fridge off");
|
|
return;
|
|
}
|
|
|
|
if (command == "config") {
|
|
printConfig();
|
|
return;
|
|
}
|
|
|
|
if (command == "factory reset") {
|
|
factoryResetConfig();
|
|
Serial.println("OK factory reset complete");
|
|
return;
|
|
}
|
|
|
|
if (command.startsWith("relayname ")) {
|
|
int firstSpace = command.indexOf(' ');
|
|
int secondSpace = command.indexOf(' ', firstSpace + 1);
|
|
|
|
if (secondSpace > 0) {
|
|
int relayNumber = command.substring(firstSpace + 1, secondSpace).toInt();
|
|
|
|
if (relayNumber >= 1 && relayNumber <= MAX_RELAYS) {
|
|
appConfig.relays[relayNumber - 1].name =
|
|
command.substring(secondSpace + 1);
|
|
|
|
Serial.println("OK relay name updated");
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (command.startsWith("tempname ")) {
|
|
int firstSpace = command.indexOf(' ');
|
|
int secondSpace = command.indexOf(' ', firstSpace + 1);
|
|
|
|
if (secondSpace > 0) {
|
|
int sensorNumber = command.substring(firstSpace + 1, secondSpace).toInt();
|
|
|
|
if (sensorNumber >= 1 && sensorNumber <= MAX_TEMP_SENSORS) {
|
|
appConfig.tempSensors[sensorNumber - 1].name =
|
|
command.substring(secondSpace + 1);
|
|
|
|
Serial.println("OK temp sensor name updated");
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (command.startsWith("bmsname ")) {
|
|
appConfig.bms.name = command.substring(8);
|
|
Serial.println("OK bms name updated");
|
|
return;
|
|
}
|
|
|
|
if (command.startsWith("bmsaddr ")) {
|
|
appConfig.bms.address = command.substring(8);
|
|
Serial.println("OK bms address updated");
|
|
return;
|
|
}
|
|
|
|
if (command.length() > 0) {
|
|
Serial.print("Unknown command: ");
|
|
Serial.println(command);
|
|
Serial.println("Commands: status, relay starlink on/off, relay fridge on/off");
|
|
}
|
|
}
|
|
|
|
|
|
void sendConfigJson() {
|
|
DynamicJsonDocument doc(4096);
|
|
|
|
doc["device_name"] = appConfig.deviceName;
|
|
|
|
JsonArray relaysArray = doc.createNestedArray("relays");
|
|
for (int i = 0; i < MAX_RELAYS; i++) {
|
|
JsonObject relay = relaysArray.createNestedObject();
|
|
relay["id"] = appConfig.relays[i].id;
|
|
relay["name"] = appConfig.relays[i].name;
|
|
relay["pin"] = appConfig.relays[i].pin;
|
|
relay["enabled"] = appConfig.relays[i].enabled;
|
|
}
|
|
|
|
JsonObject bms = doc.createNestedObject("bms");
|
|
bms["enabled"] = appConfig.bms.enabled;
|
|
bms["name"] = appConfig.bms.name;
|
|
bms["address"] = appConfig.bms.address;
|
|
bms["address_type"] = appConfig.bms.addressType;
|
|
|
|
JsonArray temps = doc.createNestedArray("temperature_sensors");
|
|
for (int i = 0; i < MAX_TEMP_SENSORS; i++) {
|
|
JsonObject temp = temps.createNestedObject();
|
|
temp["id"] = appConfig.tempSensors[i].id;
|
|
temp["name"] = appConfig.tempSensors[i].name;
|
|
temp["address"] = appConfig.tempSensors[i].address;
|
|
temp["enabled"] = appConfig.tempSensors[i].enabled;
|
|
}
|
|
|
|
String output;
|
|
serializeJson(doc, output);
|
|
server.send(200, "application/json", output);
|
|
}
|
|
|
|
void sendOkConfig() {
|
|
saveConfig();
|
|
sendConfigJson();
|
|
}
|
|
|
|
int findRelayConfigIndex(const String& id) {
|
|
for (int i = 0; i < MAX_RELAYS; i++) {
|
|
if (appConfig.relays[i].id == id) {
|
|
return i;
|
|
}
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
int findTempSensorConfigIndex(const String& id) {
|
|
for (int i = 0; i < MAX_TEMP_SENSORS; i++) {
|
|
if (appConfig.tempSensors[i].id == id) {
|
|
return i;
|
|
}
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
void handleGetConfig() {
|
|
sendConfigJson();
|
|
}
|
|
|
|
void handleUpdateRelayConfig() {
|
|
DynamicJsonDocument doc(1024);
|
|
DeserializationError error = deserializeJson(doc, server.arg("plain"));
|
|
|
|
if (error) {
|
|
server.send(400, "application/json", "{\"ok\":false,\"error\":\"invalid_json\"}");
|
|
return;
|
|
}
|
|
|
|
String id = doc["id"] | "";
|
|
int index = findRelayConfigIndex(id);
|
|
|
|
if (index < 0) {
|
|
server.send(404, "application/json", "{\"ok\":false,\"error\":\"unknown_relay\"}");
|
|
return;
|
|
}
|
|
|
|
if (doc["name"].is<String>()) {
|
|
appConfig.relays[index].name = doc["name"].as<String>();
|
|
}
|
|
|
|
if (doc["enabled"].is<bool>()) {
|
|
appConfig.relays[index].enabled = doc["enabled"].as<bool>();
|
|
}
|
|
|
|
sendOkConfig();
|
|
}
|
|
|
|
void handleUpdateBmsConfig() {
|
|
DynamicJsonDocument doc(1024);
|
|
DeserializationError error = deserializeJson(doc, server.arg("plain"));
|
|
|
|
if (error) {
|
|
server.send(400, "application/json", "{\"ok\":false,\"error\":\"invalid_json\"}");
|
|
return;
|
|
}
|
|
|
|
if (doc["enabled"].is<bool>()) {
|
|
appConfig.bms.enabled = doc["enabled"].as<bool>();
|
|
}
|
|
|
|
if (doc["name"].is<String>()) {
|
|
appConfig.bms.name = doc["name"].as<String>();
|
|
}
|
|
|
|
if (doc["address"].is<String>()) {
|
|
appConfig.bms.address = doc["address"].as<String>();
|
|
}
|
|
|
|
if (doc["address_type"].is<String>()) {
|
|
appConfig.bms.addressType = doc["address_type"].as<String>();
|
|
}
|
|
|
|
sendOkConfig();
|
|
}
|
|
|
|
void handleUpdateTempSensorConfig() {
|
|
DynamicJsonDocument doc(1024);
|
|
DeserializationError error = deserializeJson(doc, server.arg("plain"));
|
|
|
|
if (error) {
|
|
server.send(400, "application/json", "{\"ok\":false,\"error\":\"invalid_json\"}");
|
|
return;
|
|
}
|
|
|
|
String id = doc["id"] | "";
|
|
int index = findTempSensorConfigIndex(id);
|
|
|
|
if (index < 0) {
|
|
server.send(404, "application/json", "{\"ok\":false,\"error\":\"unknown_temp_sensor\"}");
|
|
return;
|
|
}
|
|
|
|
if (doc["name"].is<String>()) {
|
|
appConfig.tempSensors[index].name = doc["name"].as<String>();
|
|
}
|
|
|
|
if (doc["address"].is<String>()) {
|
|
appConfig.tempSensors[index].address = doc["address"].as<String>();
|
|
}
|
|
|
|
if (doc["enabled"].is<bool>()) {
|
|
appConfig.tempSensors[index].enabled = doc["enabled"].as<bool>();
|
|
}
|
|
|
|
sendOkConfig();
|
|
}
|
|
|
|
void handleFactoryResetConfig() {
|
|
factoryResetConfig();
|
|
sendConfigJson();
|
|
}
|
|
|
|
void handleStatus() {
|
|
DynamicJsonDocument doc(2048);
|
|
buildStatusDocument(doc);
|
|
|
|
String output;
|
|
serializeJson(doc, output);
|
|
|
|
server.send(200, "application/json", output);
|
|
}
|
|
|
|
void handleRelayHttp(const char* relayName, bool enabled) {
|
|
if (!setRelayByName(relayName, enabled)) {
|
|
server.send(404, "application/json", "{\"ok\":false,\"error\":\"unknown_relay\"}");
|
|
return;
|
|
}
|
|
|
|
DynamicJsonDocument doc(256);
|
|
doc["type"] = MSG_RELAY_RESPONSE;
|
|
doc["relay"] = relayName;
|
|
doc["enabled"] = enabled;
|
|
doc["ok"] = true;
|
|
|
|
String output;
|
|
serializeJson(doc, output);
|
|
|
|
server.send(200, "application/json", output);
|
|
}
|
|
|
|
void handleStarlinkOn() {
|
|
handleRelayHttp("starlink", true);
|
|
}
|
|
|
|
void handleStarlinkOff() {
|
|
handleRelayHttp("starlink", false);
|
|
}
|
|
|
|
void handleFridgeOn() {
|
|
handleRelayHttp("fridge", true);
|
|
}
|
|
|
|
void handleFridgeOff() {
|
|
handleRelayHttp("fridge", false);
|
|
}
|
|
|
|
void setup() {
|
|
Serial.begin(115200);
|
|
|
|
DashboardSerial.begin(
|
|
DASHBOARD_UART_BAUD,
|
|
SERIAL_8N1,
|
|
DASHBOARD_UART_RX_PIN,
|
|
DASHBOARD_UART_TX_PIN
|
|
);
|
|
|
|
Serial.println();
|
|
Serial.println("==================================");
|
|
|
|
loadConfig();
|
|
printConfig();
|
|
Serial.println("Xterra Controller Booting");
|
|
Serial.println("==================================");
|
|
|
|
loadConfig();
|
|
printConfig();
|
|
|
|
pinMode(IGNITION_PIN, INPUT);
|
|
|
|
initRelays();
|
|
initSensors();
|
|
initBms();
|
|
|
|
WiFi.mode(WIFI_AP);
|
|
|
|
bool apResult = WiFi.softAP("XterraController");
|
|
|
|
if (apResult) {
|
|
Serial.println("AP Started");
|
|
Serial.print("IP: ");
|
|
Serial.println(WiFi.softAPIP());
|
|
}
|
|
|
|
server.on("/status", handleStatus);
|
|
|
|
server.on("/config", HTTP_GET, handleGetConfig);
|
|
server.on("/config/relay", HTTP_POST, handleUpdateRelayConfig);
|
|
server.on("/config/bms", HTTP_POST, handleUpdateBmsConfig);
|
|
server.on("/config/temp", HTTP_POST, handleUpdateTempSensorConfig);
|
|
server.on("/config/factory-reset", HTTP_POST, handleFactoryResetConfig);
|
|
|
|
|
|
server.on("/relay/starlink/on", handleStarlinkOn);
|
|
server.on("/relay/starlink/off", handleStarlinkOff);
|
|
|
|
server.on("/relay/fridge/on", handleFridgeOn);
|
|
server.on("/relay/fridge/off", handleFridgeOff);
|
|
|
|
server.begin();
|
|
|
|
Serial.println("Web Server Started");
|
|
Serial.println("Dashboard UART Started");
|
|
}
|
|
|
|
void loop() {
|
|
server.handleClient();
|
|
handleDebugSerial();
|
|
updateBms();
|
|
updateAlarms();
|
|
pollDashboardUart();
|
|
|
|
static unsigned long lastSensorUpdate = 0;
|
|
|
|
if (millis() - lastSensorUpdate > 5000) {
|
|
updateSensors();
|
|
lastSensorUpdate = millis();
|
|
Serial.println("Sensor Update");
|
|
}
|
|
}
|