Make ESP32 firmware API fully generic

This commit is contained in:
2026-06-04 03:33:36 -06:00
parent dc42284d87
commit cc1909faaf
8 changed files with 159 additions and 711 deletions
@@ -13,14 +13,14 @@ void loadDefaultConfig() {
appConfig.relays[0] = {
"relay_1",
"Relay 1",
RELAY_STARLINK_PIN,
RELAY_1_PIN,
true
};
appConfig.relays[1] = {
"relay_2",
"Relay 2",
RELAY_FRIDGE_PIN,
RELAY_2_PIN,
true
};
@@ -44,7 +44,7 @@ void loadDefaultConfig() {
void loadConfig() {
loadDefaultConfig();
preferences.begin("xterra", true);
preferences.begin("overland", true);
bool configured = preferences.getBool("configured", false);
@@ -106,7 +106,7 @@ void loadConfig() {
}
void saveConfig() {
preferences.begin("xterra", false);
preferences.begin("overland", false);
preferences.putBool("configured", true);
preferences.putString("device", appConfig.deviceName);
@@ -135,7 +135,7 @@ void saveConfig() {
}
void factoryResetConfig() {
preferences.begin("xterra", false);
preferences.begin("overland", false);
preferences.clear();
preferences.end();
+1 -1
View File
@@ -497,7 +497,7 @@ static bool parseCellVoltages() {
}
void initBms() {
NimBLEDevice::init("XterraESP32");
NimBLEDevice::init("OverlandController");
NimBLEDevice::setPower(ESP_PWR_LVL_P9);
resetResponseBuffer();
}
+2 -2
View File
@@ -3,8 +3,8 @@
#define DEVICE_NAME "Overland Controller"
// Relay Outputs
#define RELAY_STARLINK_PIN 16
#define RELAY_FRIDGE_PIN 17
#define RELAY_1_PIN 16
#define RELAY_2_PIN 17
// DS18B20 Bus
#define ONEWIRE_PIN 4
@@ -18,15 +18,9 @@ HardwareSerial DashboardSerial(2);
String uartLineBuffer;
float calculateRuntimeHours() {
if (!bmsData.valid || bmsData.current >= 0) {
return 0;
}
if (!bmsData.valid || bmsData.current >= 0) return 0;
float dischargeAmps = -bmsData.current;
if (dischargeAmps <= 0.1) {
return 0;
}
if (dischargeAmps <= 0.1) return 0;
return bmsData.remainingAh / dischargeAmps;
}
@@ -35,7 +29,6 @@ void buildStatusDocument(JsonDocument& doc) {
doc["timestamp"] = millis();
JsonObject battery = doc.createNestedObject("battery");
if (bmsData.valid) {
battery["source"] = "jbd_bms";
battery["connected"] = bmsData.connected;
@@ -52,9 +45,7 @@ void buildStatusDocument(JsonDocument& doc) {
JsonArray cells = battery.createNestedArray("cell_voltages");
if (bmsData.cellsValid) {
for (int i = 0; i < bmsData.cellCount; i++) {
cells.add(bmsData.cellVoltages[i]);
}
for (int i = 0; i < bmsData.cellCount; i++) cells.add(bmsData.cellVoltages[i]);
}
battery["cell_min_voltage"] = bmsData.cellMinVoltage;
@@ -62,55 +53,52 @@ void buildStatusDocument(JsonDocument& doc) {
battery["cell_delta_mv"] = bmsData.cellDeltaMv;
battery["cells_valid"] = bmsData.cellsValid;
} else {
battery["source"] = "placeholder";
battery["source"] = "unconfigured";
battery["connected"] = false;
battery["soc"] = 0;
battery["voltage"] = 0;
battery["current"] = 0;
battery["remaining_ah"] = 0;
battery["capacity_ah"] = 150;
battery["capacity_ah"] = 0;
battery["runtime_hours"] = 0;
battery["temperature_f"] = 0;
battery["cycle_count"] = 0;
battery["cell_count"] = 0;
battery["ntc_count"] = 0;
battery["cell_delta_mv"] = 0;
battery["cells_valid"] = false;
}
JsonObject temps = doc.createNestedObject("temps");
const float tempValues[4] = {sensors.temp1, sensors.temp2, sensors.temp3, sensors.temp4};
const bool tempOnline[4] = {sensors.temp1Online, sensors.temp2Online, sensors.temp3Online, sensors.temp4Online};
if (sensors.temp1Online) {
temps["fridge_zone_1"] = sensors.temp1;
} else {
temps["fridge_zone_1"] = nullptr;
JsonArray temps = doc.createNestedArray("temps");
int tempCount = appConfig.tempSensorCount > 4 ? 4 : appConfig.tempSensorCount;
for (int i = 0; i < tempCount; i++) {
JsonObject temp = temps.createNestedObject();
temp["id"] = appConfig.tempSensors[i].id;
temp["name"] = appConfig.tempSensors[i].name;
temp["enabled"] = appConfig.tempSensors[i].enabled;
temp["online"] = tempOnline[i];
temp["temperature_f"] = tempOnline[i] ? tempValues[i] : nullptr;
}
if (sensors.temp2Online) {
temps["fridge_zone_2"] = sensors.temp2;
} else {
temps["fridge_zone_2"] = nullptr;
}
JsonArray relayStates = doc.createNestedArray("relays");
if (sensors.temp3Online) {
temps["rear_seat"] = sensors.temp3;
} else {
temps["rear_seat"] = nullptr;
}
JsonObject relay1 = relayStates.createNestedObject();
relay1["id"] = appConfig.relays[0].id;
relay1["name"] = appConfig.relays[0].name;
relay1["pin"] = appConfig.relays[0].pin;
relay1["enabled"] = appConfig.relays[0].enabled;
relay1["state"] = relays.relay1;
if (sensors.temp4Online) {
temps["outside"] = sensors.temp4;
} else {
temps["outside"] = nullptr;
}
JsonObject sensorHealth = doc.createNestedObject("sensor_health");
sensorHealth["fridge_zone_1"] = sensors.temp1Online;
sensorHealth["fridge_zone_2"] = sensors.temp2Online;
sensorHealth["rear_seat"] = sensors.temp3Online;
sensorHealth["outside"] = sensors.temp4Online;
JsonObject relayObj = doc.createNestedObject("relays");
relayObj["starlink"] = relays.relay1;
relayObj["fridge"] = relays.relay2;
JsonObject relay2 = relayStates.createNestedObject();
relay2["id"] = appConfig.relays[1].id;
relay2["name"] = appConfig.relays[1].name;
relay2["pin"] = appConfig.relays[1].pin;
relay2["enabled"] = appConfig.relays[1].enabled;
relay2["state"] = relays.relay2;
JsonObject vehicle = doc.createNestedObject("vehicle");
vehicle["ignition_on"] = digitalRead(IGNITION_PIN);
@@ -163,15 +151,10 @@ void buildStatusDocument(JsonDocument& doc) {
}
void sendStatus(Stream& output, bool pretty = false) {
DynamicJsonDocument doc(2048);
DynamicJsonDocument doc(4096);
buildStatusDocument(doc);
if (pretty) {
serializeJsonPretty(doc, output);
} else {
serializeJson(doc, output);
}
if (pretty) serializeJsonPretty(doc, output);
else serializeJson(doc, output);
output.println();
}
@@ -183,23 +166,19 @@ void sendError(Stream& output, const char* message) {
output.println();
}
bool setRelayByName(const char* relayName, bool enabled) {
if (strcmp(relayName, "starlink") == 0) {
relays.relay1 = enabled;
} else if (strcmp(relayName, "fridge") == 0) {
relays.relay2 = enabled;
} else {
return false;
}
bool setRelayById(const String& relayId, bool enabled) {
if (relayId == "relay_1") relays.relay1 = enabled;
else if (relayId == "relay_2") relays.relay2 = enabled;
else return false;
updateRelayOutputs();
return true;
}
void sendRelayResponse(Stream& output, const char* relayName, bool enabled) {
void sendRelayResponse(Stream& output, const String& relayId, bool enabled) {
DynamicJsonDocument doc(256);
doc["type"] = MSG_RELAY_RESPONSE;
doc["relay"] = relayName;
doc["relay"] = relayId;
doc["enabled"] = enabled;
doc["ok"] = true;
serializeJson(doc, output);
@@ -223,15 +202,15 @@ void handleUartMessage(const String& line) {
}
if (strcmp(type, MSG_SET_RELAY) == 0) {
const char* relayName = doc["relay"] | "";
String relayId = doc["relay"] | "";
bool enabled = doc["enabled"] | false;
if (!setRelayByName(relayName, enabled)) {
if (!setRelayById(relayId, enabled)) {
sendError(DashboardSerial, "unknown_relay");
return;
}
sendRelayResponse(DashboardSerial, relayName, enabled);
sendRelayResponse(DashboardSerial, relayId, enabled);
return;
}
@@ -244,15 +223,10 @@ void pollDashboardUart() {
if (c == '\n') {
uartLineBuffer.trim();
if (uartLineBuffer.length() > 0) {
handleUartMessage(uartLineBuffer);
}
if (uartLineBuffer.length() > 0) handleUartMessage(uartLineBuffer);
uartLineBuffer = "";
} else if (c != '\r') {
uartLineBuffer += c;
if (uartLineBuffer.length() > 512) {
uartLineBuffer = "";
sendError(DashboardSerial, "message_too_long");
@@ -261,11 +235,8 @@ void pollDashboardUart() {
}
}
void handleDebugSerial() {
if (!Serial.available()) {
return;
}
if (!Serial.available()) return;
String command = Serial.readStringUntil('\n');
command.trim();
@@ -275,30 +246,6 @@ void handleDebugSerial() {
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;
@@ -328,17 +275,44 @@ void handleDebugSerial() {
return;
}
if (command.startsWith("relayname ")) {
if (command == "save") {
saveConfig();
Serial.println("OK config saved");
return;
}
if (command.startsWith("relay ")) {
int firstSpace = command.indexOf(' ');
int secondSpace = command.indexOf(' ', firstSpace + 1);
if (secondSpace > 0) {
int relayNumber = command.substring(firstSpace + 1, secondSpace).toInt();
String action = command.substring(secondSpace + 1);
String relayId = "relay_" + String(relayNumber);
if (relayNumber >= 1 && relayNumber <= MAX_RELAYS && (action == "on" || action == "off")) {
bool enabled = action == "on";
if (setRelayById(relayId, enabled)) {
Serial.print("OK ");
Serial.print(relayId);
Serial.print(" ");
Serial.println(action);
return;
}
}
}
Serial.println("Invalid relay command. Use: relay 1 on, relay 1 off, relay 2 on, relay 2 off");
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);
appConfig.relays[relayNumber - 1].name = command.substring(secondSpace + 1);
Serial.println("OK relay name updated");
return;
}
@@ -348,14 +322,10 @@ void handleDebugSerial() {
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);
appConfig.tempSensors[sensorNumber - 1].name = command.substring(secondSpace + 1);
Serial.println("OK temp sensor name updated");
return;
}
@@ -370,6 +340,7 @@ void handleDebugSerial() {
if (command.startsWith("bmsaddr ")) {
appConfig.bms.address = command.substring(8);
appConfig.bms.enabled = appConfig.bms.address.length() > 0;
Serial.println("OK bms address updated");
return;
}
@@ -398,7 +369,6 @@ void handleDebugSerial() {
}
const BleScanResult* result = getBleScanResult(selected - 1);
if (!result) {
Serial.println("Invalid BMS selection. Run: scan ble");
return;
@@ -407,12 +377,7 @@ void handleDebugSerial() {
appConfig.bms.enabled = true;
appConfig.bms.address = result->address;
appConfig.bms.addressType = "public";
if (result->name.length() > 0) {
appConfig.bms.name = result->name;
} else {
appConfig.bms.name = "BMS";
}
appConfig.bms.name = result->name.length() > 0 ? result->name : "BMS";
saveConfig();
@@ -428,42 +393,16 @@ void handleDebugSerial() {
if (command.length() > 0) {
Serial.print("Unknown command: ");
Serial.println(command);
Serial.println("Commands: status, relay starlink on/off, relay fridge on/off");
Serial.println("Commands: status, config, save, relay 1 on/off, relay 2 on/off, relayname N <name>, tempname N <name>, bmsname <name>, bmsaddr <addr>, enter setup, scan ble, select bms N, exit setup");
}
}
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;
}
addConfigStatus(doc);
String output;
serializeJson(doc, output);
serializeJson(doc["config"], output);
server.send(200, "application/json", output);
}
@@ -474,18 +413,14 @@ void sendOkConfig() {
int findRelayConfigIndex(const String& id) {
for (int i = 0; i < MAX_RELAYS; i++) {
if (appConfig.relays[i].id == id) {
return 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;
}
if (appConfig.tempSensors[i].id == id) return i;
}
return -1;
}
@@ -497,7 +432,6 @@ void handleGetConfig() {
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;
@@ -505,19 +439,13 @@ void handleUpdateRelayConfig() {
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>();
}
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();
}
@@ -525,27 +453,15 @@ void handleUpdateRelayConfig() {
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>();
}
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();
}
@@ -553,7 +469,6 @@ void handleUpdateBmsConfig() {
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;
@@ -561,23 +476,14 @@ void handleUpdateTempSensorConfig() {
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>();
}
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();
}
@@ -587,17 +493,10 @@ void handleFactoryResetConfig() {
sendConfigJson();
}
void handleGenericRelayRoute() {
String uri = server.uri();
// Expected format:
// /relay/relay_1/on
// /relay/relay_1/off
// /relay/relay_2/on
// /relay/relay_2/off
String prefix = "/relay/";
if (!uri.startsWith(prefix)) {
server.send(404, "application/json", "{\"ok\":false,\"error\":\"invalid_relay_route\"}");
return;
@@ -616,26 +515,18 @@ void handleGenericRelayRoute() {
bool enabled;
if (action == "on") {
enabled = true;
} else if (action == "off") {
enabled = false;
} else {
if (action == "on") enabled = true;
else if (action == "off") enabled = false;
else {
server.send(400, "application/json", "{\"ok\":false,\"error\":\"invalid_relay_action\"}");
return;
}
if (relayId == "relay_1") {
relays.relay1 = enabled;
} else if (relayId == "relay_2") {
relays.relay2 = enabled;
} else {
if (!setRelayById(relayId, enabled)) {
server.send(404, "application/json", "{\"ok\":false,\"error\":\"unknown_relay\"}");
return;
}
updateRelayOutputs();
DynamicJsonDocument doc(512);
doc["ok"] = true;
doc["id"] = relayId;
@@ -647,7 +538,7 @@ void handleGenericRelayRoute() {
}
void handleStatus() {
DynamicJsonDocument doc(2048);
DynamicJsonDocument doc(4096);
buildStatusDocument(doc);
String output;
@@ -656,40 +547,6 @@ void handleStatus() {
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);
@@ -702,9 +559,6 @@ void setup() {
Serial.println();
Serial.println("==================================");
loadConfig();
printConfig();
Serial.println("Overland Controller Booting");
Serial.println("==================================");
@@ -718,7 +572,6 @@ void setup() {
initBms();
WiFi.mode(WIFI_AP);
bool apResult = WiFi.softAP("OverlandController");
if (apResult) {
@@ -733,20 +586,12 @@ void setup() {
server.on("/relay/relay_2/on", HTTP_GET, handleGenericRelayRoute);
server.on("/relay/relay_2/off", HTTP_GET, handleGenericRelayRoute);
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");
@@ -5,13 +5,13 @@
RelayState relays;
void initRelays() {
pinMode(RELAY_STARLINK_PIN, OUTPUT);
pinMode(RELAY_FRIDGE_PIN, OUTPUT);
pinMode(RELAY_1_PIN, OUTPUT);
pinMode(RELAY_2_PIN, OUTPUT);
updateRelayOutputs();
}
void updateRelayOutputs() {
digitalWrite(RELAY_STARLINK_PIN, relays.relay1);
digitalWrite(RELAY_FRIDGE_PIN, relays.relay2);
digitalWrite(RELAY_1_PIN, relays.relay1);
digitalWrite(RELAY_2_PIN, relays.relay2);
}