#include #include #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; float batteryCurrent = -6.4; float batteryRemainingAh = 82.0; float batteryRuntimeHours = 12.0; float batteryTemp = 76.0; void buildStatusDocument(JsonDocument& doc) { doc["type"] = MSG_STATUS_RESPONSE; doc["timestamp"] = millis(); JsonObject battery = doc.createNestedObject("battery"); battery["soc"] = batterySOC; battery["voltage"] = batteryVoltage; battery["current"] = batteryCurrent; battery["remaining_ah"] = batteryRemainingAh; battery["runtime_hours"] = batteryRuntimeHours; battery["temperature_f"] = batteryTemp; JsonObject temps = doc.createNestedObject("temps"); temps["fridge_zone_1"] = sensors.fridgeZone1Online ? sensors.fridgeZone1 : nullptr; temps["fridge_zone_2"] = sensors.fridgeZone2Online ? sensors.fridgeZone2 : nullptr; temps["rear_seat"] = sensors.rearSeatOnline ? sensors.rearSeat : nullptr; temps["outside"] = sensors.outsideAirOnline ? sensors.outsideAir : 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; } 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; 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("=================================="); Serial.println("Xterra Controller Booting"); Serial.println("=================================="); pinMode(IGNITION_PIN, INPUT); initRelays(); initSensors(); 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("/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(); pollDashboardUart(); static unsigned long lastSensorUpdate = 0; if (millis() - lastSensorUpdate > 5000) { updateSensors(); lastSensorUpdate = millis(); Serial.println("Sensor Update"); } }