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
---
## 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:
- 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 states use generic IDs:
relay_1
relay_2
Temperature sensors use generic sensor IDs:
Temperature sensors use generic IDs:
temp_1
temp_2
temp_3
temp_4
temp_5
temp_6
temp_7
temp_8
---
temp_1 through temp_8
## 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.
### 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.
---
Relay display names are configurable. Firmware and API clients should not assume what a relay controls.
## Configuration API
@@ -86,13 +36,9 @@ Future work should remove these aliases after the Pico/dashboard clients use gen
Returns saved configuration.
---
### POST /config/relay
Updates relay config.
Example body:
Example:
{
"id": "relay_1",
@@ -100,18 +46,9 @@ Example body:
"enabled": true
}
Valid IDs:
- relay_1
- relay_2
---
### POST /config/bms
Updates BMS config.
Example body:
Example:
{
"enabled": true,
@@ -120,13 +57,9 @@ Example body:
"address_type": "public"
}
---
### POST /config/temp
Updates temperature sensor config.
Example body:
Example:
{
"id": "temp_1",
@@ -135,24 +68,9 @@ Example body:
"enabled": true
}
Valid IDs:
- temp_1
- temp_2
- temp_3
- temp_4
- temp_5
- temp_6
- temp_7
- temp_8
---
### POST /config/factory-reset
Clears saved config and restores generic defaults.
---
Clears saved configuration and restores generic defaults.
## Serial Monitor
@@ -164,135 +82,59 @@ Line ending:
Newline
---
## Status and Config Commands
### status
Prints full JSON status to Serial.
### config
Prints current configuration to Serial.
### save
Saves current in-memory configuration to NVS.
### factory reset
Clears saved config and restores defaults.
---
status
config
save
factory reset
## Log Level Commands
### log quiet
Minimizes serial output.
### log info
Normal serial output.
### log debug
Enables recurring debug output.
---
log quiet
log info
log debug
## Relay Control Commands
Legacy relay control commands may still exist during development:
relay starlink on
relay starlink off
relay fridge on
relay fridge off
Future work should replace these with generic serial relay commands.
---
relay 1 on
relay 1 off
relay 2 on
relay 2 off
## Relay Configuration Commands
### relayname 1 Aux Power
Sets relay 1 display name.
### relayname 2 Accessory Power
Sets relay 2 display name.
relayname 1 Aux Power
relayname 2 Accessory Power
Run `save` after changing names.
---
## Temperature Sensor Configuration Commands
### tempname 1 Cabin
Sets temp sensor 1 display name.
### tempname 2 Outside Air
Sets temp sensor 2 display name.
tempname 1 Cabin
tempname 2 Outside Air
Run `save` after changing names.
---
## BMS Configuration Commands
### bmsname House Battery
Sets BMS display name.
### bmsaddr aa:bb:cc:dd:ee:ff
Sets BMS BLE address.
bmsname House Battery
bmsaddr aa:bb:cc:dd:ee:ff
Run `save` after changing BMS config manually.
---
## BMS Setup Mode Commands
### enter setup
Enters BMS setup mode.
Effects:
- Disconnects BMS client
- Pauses BMS reconnect
- Allows repeated BLE scans
### scan ble
Scans for BLE devices.
enter setup
scan ble
select bms 1
exit setup
Notes:
- Some BMS modules advertise intermittently.
- Multiple scans may be required.
### 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.
---
- Multiple BLE scans may be required.
- Once a BMS address is configured, BMS telemetry reconnects directly using the saved address.
## Typical First-Time Setup Flow
@@ -308,11 +150,7 @@ Leaves BMS setup mode and allows BMS reconnect.
save
config
---
## 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.
- 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
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.
See `docs/api-and-serial-commands.md` for the current HTTP API and Serial command reference.
@@ -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);
}
+8 -8
View File
@@ -10,20 +10,20 @@ echo "1. Status:"
curl -s "$BASE_URL/status" | python3 -m json.tool
echo
echo "2. Starlink ON:"
curl -s "$BASE_URL/relay/starlink/on" | python3 -m json.tool
echo "2. Relay 1 ON:"
curl -s "$BASE_URL/relay/relay_1/on" | python3 -m json.tool
echo
echo "3. Starlink OFF:"
curl -s "$BASE_URL/relay/starlink/off" | python3 -m json.tool
echo "3. Relay 1 OFF:"
curl -s "$BASE_URL/relay/relay_1/off" | python3 -m json.tool
echo
echo "4. Fridge ON:"
curl -s "$BASE_URL/relay/fridge/on" | python3 -m json.tool
echo "4. Relay 2 ON:"
curl -s "$BASE_URL/relay/relay_2/on" | python3 -m json.tool
echo
echo "5. Fridge OFF:"
curl -s "$BASE_URL/relay/fridge/off" | python3 -m json.tool
echo "5. Relay 2 OFF:"
curl -s "$BASE_URL/relay/relay_2/off" | python3 -m json.tool
echo
echo "ESP32 HTTP bring-up test complete."