diff --git a/firmware/esp32/xterra-controller/config.h b/firmware/esp32/xterra-controller/config.h index bf287dd..08a3a35 100644 --- a/firmware/esp32/xterra-controller/config.h +++ b/firmware/esp32/xterra-controller/config.h @@ -13,7 +13,8 @@ #define IGNITION_PIN 34 // UART over CAT5 to Pico dashboard -#define UART_TX_PIN 21 -#define UART_RX_PIN 22 +#define DASHBOARD_UART_TX_PIN 21 +#define DASHBOARD_UART_RX_PIN 22 +#define DASHBOARD_UART_BAUD 115200 -// RS-485/MAX3485 fallback only; not currently planned +// RS-485/MAX3485 is fallback only and not currently planned. diff --git a/firmware/esp32/xterra-controller/protocol.h b/firmware/esp32/xterra-controller/protocol.h index cb86ede..e5fcebc 100644 --- a/firmware/esp32/xterra-controller/protocol.h +++ b/firmware/esp32/xterra-controller/protocol.h @@ -4,3 +4,4 @@ #define MSG_STATUS_RESPONSE "status_response" #define MSG_SET_RELAY "set_relay" #define MSG_RELAY_RESPONSE "relay_response" +#define MSG_ERROR "error" diff --git a/firmware/esp32/xterra-controller/xterra-controller.ino b/firmware/esp32/xterra-controller/xterra-controller.ino index 1a602e7..e6bed14 100644 --- a/firmware/esp32/xterra-controller/xterra-controller.ino +++ b/firmware/esp32/xterra-controller/xterra-controller.ino @@ -3,10 +3,14 @@ #include #include "config.h" +#include "protocol.h" #include "relays.h" #include "sensors.h" WebServer server(80); +HardwareSerial DashboardSerial(2); + +String uartLineBuffer; float batterySOC = 82.0; float batteryVoltage = 13.2; @@ -15,10 +19,8 @@ float batteryRemainingAh = 82.0; float batteryRuntimeHours = 12.0; float batteryTemp = 76.0; -void handleStatus() { - - DynamicJsonDocument doc(2048); - +void buildStatusDocument(JsonDocument& doc) { + doc["type"] = MSG_STATUS_RESPONSE; doc["timestamp"] = millis(); JsonObject battery = doc.createNestedObject("battery"); @@ -51,41 +53,157 @@ void handleStatus() { JsonObject network = doc.createNestedObject("network"); network["wifi_enabled"] = true; network["uart_connected"] = true; +} + +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 handleStatus() { + server.setContentLength(CONTENT_LENGTH_UNKNOWN); + server.send(200, "application/json", ""); + sendStatus(server.client(), true); +} + +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; - serializeJsonPretty(doc, output); + serializeJson(doc, output); server.send(200, "application/json", output); } void handleStarlinkOn() { - relays.starlink = true; - updateRelayOutputs(); - server.send(200, "text/plain", "OK"); + handleRelayHttp("starlink", true); } void handleStarlinkOff() { - relays.starlink = false; - updateRelayOutputs(); - server.send(200, "text/plain", "OK"); + handleRelayHttp("starlink", false); } void handleFridgeOn() { - relays.fridge = true; - updateRelayOutputs(); - server.send(200, "text/plain", "OK"); + handleRelayHttp("fridge", true); } void handleFridgeOff() { - relays.fridge = false; - updateRelayOutputs(); - server.send(200, "text/plain", "OK"); + 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("=================================="); Serial.println("Xterra Controller Booting"); @@ -117,20 +235,18 @@ void setup() { server.begin(); Serial.println("Web Server Started"); + Serial.println("Dashboard UART Started"); } void loop() { - server.handleClient(); + pollDashboardUart(); static unsigned long lastSensorUpdate = 0; if (millis() - lastSensorUpdate > 5000) { - updateSensors(); - lastSensorUpdate = millis(); - Serial.println("Sensor Update"); } }