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
+37 -199
View File
@@ -6,79 +6,29 @@ Base URL while connected to the ESP32 access point:
http://192.168.4.1 http://192.168.4.1
---
## Status ## Status
### GET /status ### GET /status
Returns full controller status. Returns full controller status, including battery/BMS telemetry, relay states, temperature sensor states, alarms, system info, and current configuration.
Includes: Relay states use generic IDs:
- Battery/BMS telemetry
- Cell voltages
- Temperature readings
- Sensor health
- Relay states
- Vehicle state
- Network state
- Alarm state
- System info
- Current configuration
Relay states use generic relay IDs:
relay_1 relay_1
relay_2 relay_2
Temperature sensors use generic sensor IDs: Temperature sensors use generic IDs:
temp_1 temp_1 through temp_8
temp_2
temp_3
temp_4
temp_5
temp_6
temp_7
temp_8
---
## Relay Control API ## Relay Control API
### GET /relay/relay_1/on GET /relay/relay_1/on
GET /relay/relay_1/off
GET /relay/relay_2/on
GET /relay/relay_2/off
Turns relay 1 on. Relay display names are configurable. Firmware and API clients should not assume what a relay controls.
### GET /relay/relay_1/off
Turns relay 1 off.
### GET /relay/relay_2/on
Turns relay 2 on.
### GET /relay/relay_2/off
Turns relay 2 off.
Relay display names are configurable. The firmware should not assume what a relay controls.
---
## Deprecated Relay Aliases
These legacy routes may still exist for compatibility during development, but should not be used by new clients:
/relay/starlink/on
/relay/starlink/off
/relay/fridge/on
/relay/fridge/off
Future work should remove these aliases after the Pico/dashboard clients use generic relay IDs.
---
## Configuration API ## Configuration API
@@ -86,13 +36,9 @@ Future work should remove these aliases after the Pico/dashboard clients use gen
Returns saved configuration. Returns saved configuration.
---
### POST /config/relay ### POST /config/relay
Updates relay config. Example:
Example body:
{ {
"id": "relay_1", "id": "relay_1",
@@ -100,18 +46,9 @@ Example body:
"enabled": true "enabled": true
} }
Valid IDs:
- relay_1
- relay_2
---
### POST /config/bms ### POST /config/bms
Updates BMS config. Example:
Example body:
{ {
"enabled": true, "enabled": true,
@@ -120,13 +57,9 @@ Example body:
"address_type": "public" "address_type": "public"
} }
---
### POST /config/temp ### POST /config/temp
Updates temperature sensor config. Example:
Example body:
{ {
"id": "temp_1", "id": "temp_1",
@@ -135,24 +68,9 @@ Example body:
"enabled": true "enabled": true
} }
Valid IDs:
- temp_1
- temp_2
- temp_3
- temp_4
- temp_5
- temp_6
- temp_7
- temp_8
---
### POST /config/factory-reset ### POST /config/factory-reset
Clears saved config and restores generic defaults. Clears saved configuration and restores generic defaults.
---
## Serial Monitor ## Serial Monitor
@@ -164,135 +82,59 @@ Line ending:
Newline Newline
---
## Status and Config Commands ## Status and Config Commands
### status status
config
Prints full JSON status to Serial. save
factory reset
### config
Prints current configuration to Serial.
### save
Saves current in-memory configuration to NVS.
### factory reset
Clears saved config and restores defaults.
---
## Log Level Commands ## Log Level Commands
### log quiet log quiet
log info
Minimizes serial output. log debug
### log info
Normal serial output.
### log debug
Enables recurring debug output.
---
## Relay Control Commands ## Relay Control Commands
Legacy relay control commands may still exist during development: relay 1 on
relay 1 off
relay starlink on relay 2 on
relay starlink off relay 2 off
relay fridge on
relay fridge off
Future work should replace these with generic serial relay commands.
---
## Relay Configuration Commands ## Relay Configuration Commands
### relayname 1 Aux Power relayname 1 Aux Power
relayname 2 Accessory Power
Sets relay 1 display name.
### relayname 2 Accessory Power
Sets relay 2 display name.
Run `save` after changing names. Run `save` after changing names.
---
## Temperature Sensor Configuration Commands ## Temperature Sensor Configuration Commands
### tempname 1 Cabin tempname 1 Cabin
tempname 2 Outside Air
Sets temp sensor 1 display name.
### tempname 2 Outside Air
Sets temp sensor 2 display name.
Run `save` after changing names. Run `save` after changing names.
---
## BMS Configuration Commands ## BMS Configuration Commands
### bmsname House Battery bmsname House Battery
bmsaddr aa:bb:cc:dd:ee:ff
Sets BMS display name.
### bmsaddr aa:bb:cc:dd:ee:ff
Sets BMS BLE address.
Run `save` after changing BMS config manually. Run `save` after changing BMS config manually.
---
## BMS Setup Mode Commands ## BMS Setup Mode Commands
### enter setup enter setup
scan ble
Enters BMS setup mode. select bms 1
exit setup
Effects:
- Disconnects BMS client
- Pauses BMS reconnect
- Allows repeated BLE scans
### scan ble
Scans for BLE devices.
Notes: Notes:
- Some BMS modules advertise intermittently. - Some BMS modules advertise intermittently.
- Multiple scans may be required. - Multiple BLE scans may be required.
- Once a BMS address is configured, BMS telemetry reconnects directly using the saved address.
### select bms 1
Selects BLE scan result 1 as the configured BMS.
This saves:
- BMS name
- BMS address
- BMS enabled state
### exit setup
Leaves BMS setup mode and allows BMS reconnect.
---
## Typical First-Time Setup Flow ## Typical First-Time Setup Flow
@@ -308,11 +150,7 @@ Leaves BMS setup mode and allows BMS reconnect.
save save
config config
---
## Known Notes ## Known Notes
- BMS telemetry is reliable once a BMS address is configured.
- BLE discovery depends on how often the BMS advertises.
- DS18B20 sensors require a 4.7kΩ pull-up resistor on the 1-Wire data bus. - DS18B20 sensors require a 4.7kΩ pull-up resistor on the 1-Wire data bus.
- Some legacy relay names still exist internally and should be removed in a future cleanup. - The firmware uses generic relay and temperature IDs. Installation-specific names belong in saved configuration only.
+2 -237
View File
@@ -1,238 +1,3 @@
# Xterra Overland Dashboard API Specification # Overland Controller API
## Overview See `docs/api-and-serial-commands.md` for the current HTTP API and Serial command reference.
The ESP32 acts as the system controller and provides status and control endpoints.
Primary communication is UART over CAT5. WiFi HTTP API is the backup communication path.
WiFi HTTP API is provided for:
* Web dashboard
* Mobile access
* Configuration
* Future Home Assistant integration
---
# GET /status
Returns complete current system status.
Example:
```json
{
"timestamp": 1748910000,
"battery": {
"soc": 82,
"voltage": 13.2,
"current": -6.4,
"remaining_ah": 82.5,
"runtime_hours": 11.2,
"temperature_f": 77.4
},
"temps": {
"fridge_zone_1": 35.8,
"fridge_zone_2": 12.1,
"rear_seat": 78.0,
"outside": 88.4
},
"relays": {
"starlink": false,
"fridge": true
},
"network": {
"wifi_enabled": false,
"uart_connected": true
}
}
```
---
# GET /battery
Returns detailed battery information.
Example:
```json
{
"soc": 82,
"voltage": 13.2,
"current": -6.4,
"power_w": -84.5,
"remaining_ah": 82.5,
"runtime_hours": 11.2,
"temperature_f": 77.4,
"cells": [
3.299,
3.301,
3.300,
3.298
],
"cell_delta_mv": 3
}
```
---
# GET /temps
Returns all temperature sensors.
Example:
```json
{
"fridge_zone_1": 35.8,
"fridge_zone_2": 12.1,
"rear_seat": 78.0,
"outside": 88.4
}
```
---
# GET /relays
Returns relay states.
Example:
```json
{
"starlink": false,
"fridge": true
}
```
---
# POST /relay/starlink
Request:
```json
{
"state": true
}
```
Response:
```json
{
"success": true,
"starlink": true
}
```
---
# POST /relay/fridge
Request:
```json
{
"state": true
}
```
Response:
```json
{
"success": true,
"fridge": true
}
```
---
# GET /network
Returns network status.
Example:
```json
{
"wifi_enabled": false,
"uart_connected": true,
"starlink_enabled": false
}
```
---
# POST /network/wifi
Enable WiFi temporarily.
Request:
```json
{
"minutes": 10
}
```
Response:
```json
{
"success": true,
"expires_minutes": 10
}
```
---
# Future Endpoints
## Vehicle Data
GET /vehicle
```json
{
"speed_mph": 62,
"rpm": 2200,
"coolant_temp_f": 192,
"fuel_percent": 48
}
```
## GPS
GET /gps
```json
{
"lat": 43.6629,
"lon": -116.6874,
"speed_mph": 0,
"heading": 0
}
```
## Historical Data
GET /history
GET /history/temps
GET /history/battery
## Home Assistant
MQTT integration planned.
@@ -13,14 +13,14 @@ void loadDefaultConfig() {
appConfig.relays[0] = { appConfig.relays[0] = {
"relay_1", "relay_1",
"Relay 1", "Relay 1",
RELAY_STARLINK_PIN, RELAY_1_PIN,
true true
}; };
appConfig.relays[1] = { appConfig.relays[1] = {
"relay_2", "relay_2",
"Relay 2", "Relay 2",
RELAY_FRIDGE_PIN, RELAY_2_PIN,
true true
}; };
@@ -44,7 +44,7 @@ void loadDefaultConfig() {
void loadConfig() { void loadConfig() {
loadDefaultConfig(); loadDefaultConfig();
preferences.begin("xterra", true); preferences.begin("overland", true);
bool configured = preferences.getBool("configured", false); bool configured = preferences.getBool("configured", false);
@@ -106,7 +106,7 @@ void loadConfig() {
} }
void saveConfig() { void saveConfig() {
preferences.begin("xterra", false); preferences.begin("overland", false);
preferences.putBool("configured", true); preferences.putBool("configured", true);
preferences.putString("device", appConfig.deviceName); preferences.putString("device", appConfig.deviceName);
@@ -135,7 +135,7 @@ void saveConfig() {
} }
void factoryResetConfig() { void factoryResetConfig() {
preferences.begin("xterra", false); preferences.begin("overland", false);
preferences.clear(); preferences.clear();
preferences.end(); preferences.end();
+1 -1
View File
@@ -497,7 +497,7 @@ static bool parseCellVoltages() {
} }
void initBms() { void initBms() {
NimBLEDevice::init("XterraESP32"); NimBLEDevice::init("OverlandController");
NimBLEDevice::setPower(ESP_PWR_LVL_P9); NimBLEDevice::setPower(ESP_PWR_LVL_P9);
resetResponseBuffer(); resetResponseBuffer();
} }
+2 -2
View File
@@ -3,8 +3,8 @@
#define DEVICE_NAME "Overland Controller" #define DEVICE_NAME "Overland Controller"
// Relay Outputs // Relay Outputs
#define RELAY_STARLINK_PIN 16 #define RELAY_1_PIN 16
#define RELAY_FRIDGE_PIN 17 #define RELAY_2_PIN 17
// DS18B20 Bus // DS18B20 Bus
#define ONEWIRE_PIN 4 #define ONEWIRE_PIN 4
@@ -18,15 +18,9 @@ HardwareSerial DashboardSerial(2);
String uartLineBuffer; String uartLineBuffer;
float calculateRuntimeHours() { float calculateRuntimeHours() {
if (!bmsData.valid || bmsData.current >= 0) { if (!bmsData.valid || bmsData.current >= 0) return 0;
return 0;
}
float dischargeAmps = -bmsData.current; float dischargeAmps = -bmsData.current;
if (dischargeAmps <= 0.1) { if (dischargeAmps <= 0.1) return 0;
return 0;
}
return bmsData.remainingAh / dischargeAmps; return bmsData.remainingAh / dischargeAmps;
} }
@@ -35,7 +29,6 @@ void buildStatusDocument(JsonDocument& doc) {
doc["timestamp"] = millis(); doc["timestamp"] = millis();
JsonObject battery = doc.createNestedObject("battery"); JsonObject battery = doc.createNestedObject("battery");
if (bmsData.valid) { if (bmsData.valid) {
battery["source"] = "jbd_bms"; battery["source"] = "jbd_bms";
battery["connected"] = bmsData.connected; battery["connected"] = bmsData.connected;
@@ -52,9 +45,7 @@ void buildStatusDocument(JsonDocument& doc) {
JsonArray cells = battery.createNestedArray("cell_voltages"); JsonArray cells = battery.createNestedArray("cell_voltages");
if (bmsData.cellsValid) { if (bmsData.cellsValid) {
for (int i = 0; i < bmsData.cellCount; i++) { for (int i = 0; i < bmsData.cellCount; i++) cells.add(bmsData.cellVoltages[i]);
cells.add(bmsData.cellVoltages[i]);
}
} }
battery["cell_min_voltage"] = bmsData.cellMinVoltage; battery["cell_min_voltage"] = bmsData.cellMinVoltage;
@@ -62,55 +53,52 @@ void buildStatusDocument(JsonDocument& doc) {
battery["cell_delta_mv"] = bmsData.cellDeltaMv; battery["cell_delta_mv"] = bmsData.cellDeltaMv;
battery["cells_valid"] = bmsData.cellsValid; battery["cells_valid"] = bmsData.cellsValid;
} else { } else {
battery["source"] = "placeholder"; battery["source"] = "unconfigured";
battery["connected"] = false; battery["connected"] = false;
battery["soc"] = 0; battery["soc"] = 0;
battery["voltage"] = 0; battery["voltage"] = 0;
battery["current"] = 0; battery["current"] = 0;
battery["remaining_ah"] = 0; battery["remaining_ah"] = 0;
battery["capacity_ah"] = 150; battery["capacity_ah"] = 0;
battery["runtime_hours"] = 0; battery["runtime_hours"] = 0;
battery["temperature_f"] = 0; battery["temperature_f"] = 0;
battery["cycle_count"] = 0; battery["cycle_count"] = 0;
battery["cell_count"] = 0; battery["cell_count"] = 0;
battery["ntc_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) { JsonArray temps = doc.createNestedArray("temps");
temps["fridge_zone_1"] = sensors.temp1; int tempCount = appConfig.tempSensorCount > 4 ? 4 : appConfig.tempSensorCount;
} else {
temps["fridge_zone_1"] = nullptr; 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) { JsonArray relayStates = doc.createNestedArray("relays");
temps["fridge_zone_2"] = sensors.temp2;
} else {
temps["fridge_zone_2"] = nullptr;
}
if (sensors.temp3Online) { JsonObject relay1 = relayStates.createNestedObject();
temps["rear_seat"] = sensors.temp3; relay1["id"] = appConfig.relays[0].id;
} else { relay1["name"] = appConfig.relays[0].name;
temps["rear_seat"] = nullptr; relay1["pin"] = appConfig.relays[0].pin;
} relay1["enabled"] = appConfig.relays[0].enabled;
relay1["state"] = relays.relay1;
if (sensors.temp4Online) { JsonObject relay2 = relayStates.createNestedObject();
temps["outside"] = sensors.temp4; relay2["id"] = appConfig.relays[1].id;
} else { relay2["name"] = appConfig.relays[1].name;
temps["outside"] = nullptr; relay2["pin"] = appConfig.relays[1].pin;
} relay2["enabled"] = appConfig.relays[1].enabled;
relay2["state"] = relays.relay2;
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 vehicle = doc.createNestedObject("vehicle"); JsonObject vehicle = doc.createNestedObject("vehicle");
vehicle["ignition_on"] = digitalRead(IGNITION_PIN); vehicle["ignition_on"] = digitalRead(IGNITION_PIN);
@@ -163,15 +151,10 @@ void buildStatusDocument(JsonDocument& doc) {
} }
void sendStatus(Stream& output, bool pretty = false) { void sendStatus(Stream& output, bool pretty = false) {
DynamicJsonDocument doc(2048); DynamicJsonDocument doc(4096);
buildStatusDocument(doc); buildStatusDocument(doc);
if (pretty) serializeJsonPretty(doc, output);
if (pretty) { else serializeJson(doc, output);
serializeJsonPretty(doc, output);
} else {
serializeJson(doc, output);
}
output.println(); output.println();
} }
@@ -183,23 +166,19 @@ void sendError(Stream& output, const char* message) {
output.println(); output.println();
} }
bool setRelayByName(const char* relayName, bool enabled) { bool setRelayById(const String& relayId, bool enabled) {
if (strcmp(relayName, "starlink") == 0) { if (relayId == "relay_1") relays.relay1 = enabled;
relays.relay1 = enabled; else if (relayId == "relay_2") relays.relay2 = enabled;
} else if (strcmp(relayName, "fridge") == 0) { else return false;
relays.relay2 = enabled;
} else {
return false;
}
updateRelayOutputs(); updateRelayOutputs();
return true; return true;
} }
void sendRelayResponse(Stream& output, const char* relayName, bool enabled) { void sendRelayResponse(Stream& output, const String& relayId, bool enabled) {
DynamicJsonDocument doc(256); DynamicJsonDocument doc(256);
doc["type"] = MSG_RELAY_RESPONSE; doc["type"] = MSG_RELAY_RESPONSE;
doc["relay"] = relayName; doc["relay"] = relayId;
doc["enabled"] = enabled; doc["enabled"] = enabled;
doc["ok"] = true; doc["ok"] = true;
serializeJson(doc, output); serializeJson(doc, output);
@@ -223,15 +202,15 @@ void handleUartMessage(const String& line) {
} }
if (strcmp(type, MSG_SET_RELAY) == 0) { if (strcmp(type, MSG_SET_RELAY) == 0) {
const char* relayName = doc["relay"] | ""; String relayId = doc["relay"] | "";
bool enabled = doc["enabled"] | false; bool enabled = doc["enabled"] | false;
if (!setRelayByName(relayName, enabled)) { if (!setRelayById(relayId, enabled)) {
sendError(DashboardSerial, "unknown_relay"); sendError(DashboardSerial, "unknown_relay");
return; return;
} }
sendRelayResponse(DashboardSerial, relayName, enabled); sendRelayResponse(DashboardSerial, relayId, enabled);
return; return;
} }
@@ -244,15 +223,10 @@ void pollDashboardUart() {
if (c == '\n') { if (c == '\n') {
uartLineBuffer.trim(); uartLineBuffer.trim();
if (uartLineBuffer.length() > 0) handleUartMessage(uartLineBuffer);
if (uartLineBuffer.length() > 0) {
handleUartMessage(uartLineBuffer);
}
uartLineBuffer = ""; uartLineBuffer = "";
} else if (c != '\r') { } else if (c != '\r') {
uartLineBuffer += c; uartLineBuffer += c;
if (uartLineBuffer.length() > 512) { if (uartLineBuffer.length() > 512) {
uartLineBuffer = ""; uartLineBuffer = "";
sendError(DashboardSerial, "message_too_long"); sendError(DashboardSerial, "message_too_long");
@@ -261,11 +235,8 @@ void pollDashboardUart() {
} }
} }
void handleDebugSerial() { void handleDebugSerial() {
if (!Serial.available()) { if (!Serial.available()) return;
return;
}
String command = Serial.readStringUntil('\n'); String command = Serial.readStringUntil('\n');
command.trim(); command.trim();
@@ -275,30 +246,6 @@ void handleDebugSerial() {
return; 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") { if (command == "config") {
printConfig(); printConfig();
return; return;
@@ -328,17 +275,44 @@ void handleDebugSerial() {
return; return;
} }
if (command.startsWith("relayname ")) { if (command == "save") {
saveConfig();
Serial.println("OK config saved");
return;
}
if (command.startsWith("relay ")) {
int firstSpace = command.indexOf(' '); int firstSpace = command.indexOf(' ');
int secondSpace = command.indexOf(' ', firstSpace + 1); int secondSpace = command.indexOf(' ', firstSpace + 1);
if (secondSpace > 0) { if (secondSpace > 0) {
int relayNumber = command.substring(firstSpace + 1, secondSpace).toInt(); 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) { if (relayNumber >= 1 && relayNumber <= MAX_RELAYS) {
appConfig.relays[relayNumber - 1].name = appConfig.relays[relayNumber - 1].name = command.substring(secondSpace + 1);
command.substring(secondSpace + 1);
Serial.println("OK relay name updated"); Serial.println("OK relay name updated");
return; return;
} }
@@ -348,14 +322,10 @@ void handleDebugSerial() {
if (command.startsWith("tempname ")) { if (command.startsWith("tempname ")) {
int firstSpace = command.indexOf(' '); int firstSpace = command.indexOf(' ');
int secondSpace = command.indexOf(' ', firstSpace + 1); int secondSpace = command.indexOf(' ', firstSpace + 1);
if (secondSpace > 0) { if (secondSpace > 0) {
int sensorNumber = command.substring(firstSpace + 1, secondSpace).toInt(); int sensorNumber = command.substring(firstSpace + 1, secondSpace).toInt();
if (sensorNumber >= 1 && sensorNumber <= MAX_TEMP_SENSORS) { if (sensorNumber >= 1 && sensorNumber <= MAX_TEMP_SENSORS) {
appConfig.tempSensors[sensorNumber - 1].name = appConfig.tempSensors[sensorNumber - 1].name = command.substring(secondSpace + 1);
command.substring(secondSpace + 1);
Serial.println("OK temp sensor name updated"); Serial.println("OK temp sensor name updated");
return; return;
} }
@@ -370,6 +340,7 @@ void handleDebugSerial() {
if (command.startsWith("bmsaddr ")) { if (command.startsWith("bmsaddr ")) {
appConfig.bms.address = command.substring(8); appConfig.bms.address = command.substring(8);
appConfig.bms.enabled = appConfig.bms.address.length() > 0;
Serial.println("OK bms address updated"); Serial.println("OK bms address updated");
return; return;
} }
@@ -398,7 +369,6 @@ void handleDebugSerial() {
} }
const BleScanResult* result = getBleScanResult(selected - 1); const BleScanResult* result = getBleScanResult(selected - 1);
if (!result) { if (!result) {
Serial.println("Invalid BMS selection. Run: scan ble"); Serial.println("Invalid BMS selection. Run: scan ble");
return; return;
@@ -407,12 +377,7 @@ void handleDebugSerial() {
appConfig.bms.enabled = true; appConfig.bms.enabled = true;
appConfig.bms.address = result->address; appConfig.bms.address = result->address;
appConfig.bms.addressType = "public"; appConfig.bms.addressType = "public";
appConfig.bms.name = result->name.length() > 0 ? result->name : "BMS";
if (result->name.length() > 0) {
appConfig.bms.name = result->name;
} else {
appConfig.bms.name = "BMS";
}
saveConfig(); saveConfig();
@@ -428,42 +393,16 @@ void handleDebugSerial() {
if (command.length() > 0) { if (command.length() > 0) {
Serial.print("Unknown command: "); Serial.print("Unknown command: ");
Serial.println(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() { void sendConfigJson() {
DynamicJsonDocument doc(4096); DynamicJsonDocument doc(4096);
addConfigStatus(doc);
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; String output;
serializeJson(doc, output); serializeJson(doc["config"], output);
server.send(200, "application/json", output); server.send(200, "application/json", output);
} }
@@ -474,18 +413,14 @@ void sendOkConfig() {
int findRelayConfigIndex(const String& id) { int findRelayConfigIndex(const String& id) {
for (int i = 0; i < MAX_RELAYS; i++) { for (int i = 0; i < MAX_RELAYS; i++) {
if (appConfig.relays[i].id == id) { if (appConfig.relays[i].id == id) return i;
return i;
}
} }
return -1; return -1;
} }
int findTempSensorConfigIndex(const String& id) { int findTempSensorConfigIndex(const String& id) {
for (int i = 0; i < MAX_TEMP_SENSORS; i++) { for (int i = 0; i < MAX_TEMP_SENSORS; i++) {
if (appConfig.tempSensors[i].id == id) { if (appConfig.tempSensors[i].id == id) return i;
return i;
}
} }
return -1; return -1;
} }
@@ -497,7 +432,6 @@ void handleGetConfig() {
void handleUpdateRelayConfig() { void handleUpdateRelayConfig() {
DynamicJsonDocument doc(1024); DynamicJsonDocument doc(1024);
DeserializationError error = deserializeJson(doc, server.arg("plain")); DeserializationError error = deserializeJson(doc, server.arg("plain"));
if (error) { if (error) {
server.send(400, "application/json", "{\"ok\":false,\"error\":\"invalid_json\"}"); server.send(400, "application/json", "{\"ok\":false,\"error\":\"invalid_json\"}");
return; return;
@@ -505,19 +439,13 @@ void handleUpdateRelayConfig() {
String id = doc["id"] | ""; String id = doc["id"] | "";
int index = findRelayConfigIndex(id); int index = findRelayConfigIndex(id);
if (index < 0) { if (index < 0) {
server.send(404, "application/json", "{\"ok\":false,\"error\":\"unknown_relay\"}"); server.send(404, "application/json", "{\"ok\":false,\"error\":\"unknown_relay\"}");
return; return;
} }
if (doc["name"].is<String>()) { if (doc["name"].is<String>()) appConfig.relays[index].name = doc["name"].as<String>();
appConfig.relays[index].name = doc["name"].as<String>(); if (doc["enabled"].is<bool>()) appConfig.relays[index].enabled = doc["enabled"].as<bool>();
}
if (doc["enabled"].is<bool>()) {
appConfig.relays[index].enabled = doc["enabled"].as<bool>();
}
sendOkConfig(); sendOkConfig();
} }
@@ -525,27 +453,15 @@ void handleUpdateRelayConfig() {
void handleUpdateBmsConfig() { void handleUpdateBmsConfig() {
DynamicJsonDocument doc(1024); DynamicJsonDocument doc(1024);
DeserializationError error = deserializeJson(doc, server.arg("plain")); DeserializationError error = deserializeJson(doc, server.arg("plain"));
if (error) { if (error) {
server.send(400, "application/json", "{\"ok\":false,\"error\":\"invalid_json\"}"); server.send(400, "application/json", "{\"ok\":false,\"error\":\"invalid_json\"}");
return; return;
} }
if (doc["enabled"].is<bool>()) { if (doc["enabled"].is<bool>()) appConfig.bms.enabled = doc["enabled"].as<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["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(); sendOkConfig();
} }
@@ -553,7 +469,6 @@ void handleUpdateBmsConfig() {
void handleUpdateTempSensorConfig() { void handleUpdateTempSensorConfig() {
DynamicJsonDocument doc(1024); DynamicJsonDocument doc(1024);
DeserializationError error = deserializeJson(doc, server.arg("plain")); DeserializationError error = deserializeJson(doc, server.arg("plain"));
if (error) { if (error) {
server.send(400, "application/json", "{\"ok\":false,\"error\":\"invalid_json\"}"); server.send(400, "application/json", "{\"ok\":false,\"error\":\"invalid_json\"}");
return; return;
@@ -561,23 +476,14 @@ void handleUpdateTempSensorConfig() {
String id = doc["id"] | ""; String id = doc["id"] | "";
int index = findTempSensorConfigIndex(id); int index = findTempSensorConfigIndex(id);
if (index < 0) { if (index < 0) {
server.send(404, "application/json", "{\"ok\":false,\"error\":\"unknown_temp_sensor\"}"); server.send(404, "application/json", "{\"ok\":false,\"error\":\"unknown_temp_sensor\"}");
return; return;
} }
if (doc["name"].is<String>()) { if (doc["name"].is<String>()) appConfig.tempSensors[index].name = doc["name"].as<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["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(); sendOkConfig();
} }
@@ -587,17 +493,10 @@ void handleFactoryResetConfig() {
sendConfigJson(); sendConfigJson();
} }
void handleGenericRelayRoute() { void handleGenericRelayRoute() {
String uri = server.uri(); 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/"; String prefix = "/relay/";
if (!uri.startsWith(prefix)) { if (!uri.startsWith(prefix)) {
server.send(404, "application/json", "{\"ok\":false,\"error\":\"invalid_relay_route\"}"); server.send(404, "application/json", "{\"ok\":false,\"error\":\"invalid_relay_route\"}");
return; return;
@@ -616,26 +515,18 @@ void handleGenericRelayRoute() {
bool enabled; bool enabled;
if (action == "on") { if (action == "on") enabled = true;
enabled = true; else if (action == "off") enabled = false;
} else if (action == "off") { else {
enabled = false;
} else {
server.send(400, "application/json", "{\"ok\":false,\"error\":\"invalid_relay_action\"}"); server.send(400, "application/json", "{\"ok\":false,\"error\":\"invalid_relay_action\"}");
return; return;
} }
if (relayId == "relay_1") { if (!setRelayById(relayId, enabled)) {
relays.relay1 = enabled;
} else if (relayId == "relay_2") {
relays.relay2 = enabled;
} else {
server.send(404, "application/json", "{\"ok\":false,\"error\":\"unknown_relay\"}"); server.send(404, "application/json", "{\"ok\":false,\"error\":\"unknown_relay\"}");
return; return;
} }
updateRelayOutputs();
DynamicJsonDocument doc(512); DynamicJsonDocument doc(512);
doc["ok"] = true; doc["ok"] = true;
doc["id"] = relayId; doc["id"] = relayId;
@@ -647,7 +538,7 @@ void handleGenericRelayRoute() {
} }
void handleStatus() { void handleStatus() {
DynamicJsonDocument doc(2048); DynamicJsonDocument doc(4096);
buildStatusDocument(doc); buildStatusDocument(doc);
String output; String output;
@@ -656,40 +547,6 @@ void handleStatus() {
server.send(200, "application/json", 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() { void setup() {
Serial.begin(115200); Serial.begin(115200);
@@ -702,9 +559,6 @@ void setup() {
Serial.println(); Serial.println();
Serial.println("=================================="); Serial.println("==================================");
loadConfig();
printConfig();
Serial.println("Overland Controller Booting"); Serial.println("Overland Controller Booting");
Serial.println("=================================="); Serial.println("==================================");
@@ -718,7 +572,6 @@ void setup() {
initBms(); initBms();
WiFi.mode(WIFI_AP); WiFi.mode(WIFI_AP);
bool apResult = WiFi.softAP("OverlandController"); bool apResult = WiFi.softAP("OverlandController");
if (apResult) { if (apResult) {
@@ -733,20 +586,12 @@ void setup() {
server.on("/relay/relay_2/on", HTTP_GET, handleGenericRelayRoute); server.on("/relay/relay_2/on", HTTP_GET, handleGenericRelayRoute);
server.on("/relay/relay_2/off", HTTP_GET, handleGenericRelayRoute); server.on("/relay/relay_2/off", HTTP_GET, handleGenericRelayRoute);
server.on("/config", HTTP_GET, handleGetConfig); server.on("/config", HTTP_GET, handleGetConfig);
server.on("/config/relay", HTTP_POST, handleUpdateRelayConfig); server.on("/config/relay", HTTP_POST, handleUpdateRelayConfig);
server.on("/config/bms", HTTP_POST, handleUpdateBmsConfig); server.on("/config/bms", HTTP_POST, handleUpdateBmsConfig);
server.on("/config/temp", HTTP_POST, handleUpdateTempSensorConfig); server.on("/config/temp", HTTP_POST, handleUpdateTempSensorConfig);
server.on("/config/factory-reset", HTTP_POST, handleFactoryResetConfig); 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(); server.begin();
Serial.println("Web Server Started"); Serial.println("Web Server Started");
@@ -5,13 +5,13 @@
RelayState relays; RelayState relays;
void initRelays() { void initRelays() {
pinMode(RELAY_STARLINK_PIN, OUTPUT); pinMode(RELAY_1_PIN, OUTPUT);
pinMode(RELAY_FRIDGE_PIN, OUTPUT); pinMode(RELAY_2_PIN, OUTPUT);
updateRelayOutputs(); updateRelayOutputs();
} }
void updateRelayOutputs() { void updateRelayOutputs() {
digitalWrite(RELAY_STARLINK_PIN, relays.relay1); digitalWrite(RELAY_1_PIN, relays.relay1);
digitalWrite(RELAY_FRIDGE_PIN, relays.relay2); digitalWrite(RELAY_2_PIN, relays.relay2);
} }
+8 -8
View File
@@ -10,20 +10,20 @@ echo "1. Status:"
curl -s "$BASE_URL/status" | python3 -m json.tool curl -s "$BASE_URL/status" | python3 -m json.tool
echo echo
echo "2. Starlink ON:" echo "2. Relay 1 ON:"
curl -s "$BASE_URL/relay/starlink/on" | python3 -m json.tool curl -s "$BASE_URL/relay/relay_1/on" | python3 -m json.tool
echo echo
echo "3. Starlink OFF:" echo "3. Relay 1 OFF:"
curl -s "$BASE_URL/relay/starlink/off" | python3 -m json.tool curl -s "$BASE_URL/relay/relay_1/off" | python3 -m json.tool
echo echo
echo "4. Fridge ON:" echo "4. Relay 2 ON:"
curl -s "$BASE_URL/relay/fridge/on" | python3 -m json.tool curl -s "$BASE_URL/relay/relay_2/on" | python3 -m json.tool
echo echo
echo "5. Fridge OFF:" echo "5. Relay 2 OFF:"
curl -s "$BASE_URL/relay/fridge/off" | python3 -m json.tool curl -s "$BASE_URL/relay/relay_2/off" | python3 -m json.tool
echo echo
echo "ESP32 HTTP bring-up test complete." echo "ESP32 HTTP bring-up test complete."