Rename project to overland-controller
This commit is contained in:
@@ -0,0 +1,13 @@
|
||||
#include "alarms.h"
|
||||
#include "bms.h"
|
||||
|
||||
AlarmState alarms;
|
||||
|
||||
void updateAlarms() {
|
||||
alarms.lowSoc = bmsData.valid && bmsData.soc < 20;
|
||||
alarms.criticalSoc = bmsData.valid && bmsData.soc < 10;
|
||||
alarms.lowVoltage = bmsData.valid && bmsData.voltage < 12.0;
|
||||
alarms.highBatteryTemp = bmsData.valid && bmsData.temperatureF > 120.0;
|
||||
alarms.cellImbalance = bmsData.cellsValid && bmsData.cellDeltaMv > 20;
|
||||
alarms.bmsDisconnected = !bmsData.connected;
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
#pragma once
|
||||
|
||||
struct AlarmState {
|
||||
bool lowSoc = false;
|
||||
bool criticalSoc = false;
|
||||
bool lowVoltage = false;
|
||||
bool highBatteryTemp = false;
|
||||
bool cellImbalance = false;
|
||||
bool bmsDisconnected = false;
|
||||
};
|
||||
|
||||
extern AlarmState alarms;
|
||||
|
||||
void updateAlarms();
|
||||
@@ -0,0 +1,184 @@
|
||||
#include "app_config.h"
|
||||
#include "config.h"
|
||||
|
||||
#include <Preferences.h>
|
||||
|
||||
AppConfig appConfig;
|
||||
|
||||
static Preferences preferences;
|
||||
|
||||
void loadDefaultConfig() {
|
||||
appConfig.deviceName = "Overland Controller";
|
||||
|
||||
appConfig.relays[0] = {
|
||||
"relay_1",
|
||||
"Relay 1",
|
||||
RELAY_STARLINK_PIN,
|
||||
true
|
||||
};
|
||||
|
||||
appConfig.relays[1] = {
|
||||
"relay_2",
|
||||
"Relay 2",
|
||||
RELAY_FRIDGE_PIN,
|
||||
true
|
||||
};
|
||||
|
||||
appConfig.tempSensorCount = 4;
|
||||
|
||||
for (int i = 0; i < MAX_TEMP_SENSORS; i++) {
|
||||
appConfig.tempSensors[i] = {
|
||||
"temp_" + String(i + 1),
|
||||
"Temperature " + String(i + 1),
|
||||
"",
|
||||
i < appConfig.tempSensorCount
|
||||
};
|
||||
}
|
||||
|
||||
appConfig.bms.enabled = false;
|
||||
appConfig.bms.name = "";
|
||||
appConfig.bms.address = "";
|
||||
appConfig.bms.addressType = "public";
|
||||
}
|
||||
|
||||
void loadConfig() {
|
||||
loadDefaultConfig();
|
||||
|
||||
preferences.begin("xterra", true);
|
||||
|
||||
bool configured = preferences.getBool("configured", false);
|
||||
|
||||
if (configured) {
|
||||
appConfig.deviceName = preferences.getString("device", appConfig.deviceName);
|
||||
|
||||
for (int i = 0; i < MAX_RELAYS; i++) {
|
||||
String prefix = "r" + String(i + 1) + "_";
|
||||
appConfig.relays[i].name = preferences.getString(
|
||||
(prefix + "name").c_str(),
|
||||
appConfig.relays[i].name
|
||||
);
|
||||
appConfig.relays[i].enabled = preferences.getBool(
|
||||
(prefix + "enabled").c_str(),
|
||||
appConfig.relays[i].enabled
|
||||
);
|
||||
}
|
||||
|
||||
appConfig.tempSensorCount = preferences.getInt(
|
||||
"temp_count",
|
||||
appConfig.tempSensorCount
|
||||
);
|
||||
|
||||
if (appConfig.tempSensorCount < 0) {
|
||||
appConfig.tempSensorCount = 0;
|
||||
}
|
||||
|
||||
if (appConfig.tempSensorCount > MAX_TEMP_SENSORS) {
|
||||
appConfig.tempSensorCount = MAX_TEMP_SENSORS;
|
||||
}
|
||||
|
||||
for (int i = 0; i < MAX_TEMP_SENSORS; i++) {
|
||||
String prefix = "t" + String(i + 1) + "_";
|
||||
appConfig.tempSensors[i].name = preferences.getString(
|
||||
(prefix + "name").c_str(),
|
||||
appConfig.tempSensors[i].name
|
||||
);
|
||||
appConfig.tempSensors[i].address = preferences.getString(
|
||||
(prefix + "addr").c_str(),
|
||||
appConfig.tempSensors[i].address
|
||||
);
|
||||
appConfig.tempSensors[i].enabled = preferences.getBool(
|
||||
(prefix + "enabled").c_str(),
|
||||
appConfig.tempSensors[i].enabled
|
||||
);
|
||||
}
|
||||
|
||||
appConfig.bms.enabled = preferences.getBool("bms_enabled", appConfig.bms.enabled);
|
||||
appConfig.bms.name = preferences.getString("bms_name", appConfig.bms.name);
|
||||
appConfig.bms.address = preferences.getString("bms_addr", appConfig.bms.address);
|
||||
appConfig.bms.addressType = preferences.getString("bms_type", appConfig.bms.addressType);
|
||||
}
|
||||
|
||||
preferences.end();
|
||||
|
||||
if (!configured) {
|
||||
saveConfig();
|
||||
}
|
||||
}
|
||||
|
||||
void saveConfig() {
|
||||
preferences.begin("xterra", false);
|
||||
|
||||
preferences.putBool("configured", true);
|
||||
preferences.putString("device", appConfig.deviceName);
|
||||
|
||||
for (int i = 0; i < MAX_RELAYS; i++) {
|
||||
String prefix = "r" + String(i + 1) + "_";
|
||||
preferences.putString((prefix + "name").c_str(), appConfig.relays[i].name);
|
||||
preferences.putBool((prefix + "enabled").c_str(), appConfig.relays[i].enabled);
|
||||
}
|
||||
|
||||
preferences.putInt("temp_count", appConfig.tempSensorCount);
|
||||
|
||||
for (int i = 0; i < MAX_TEMP_SENSORS; i++) {
|
||||
String prefix = "t" + String(i + 1) + "_";
|
||||
preferences.putString((prefix + "name").c_str(), appConfig.tempSensors[i].name);
|
||||
preferences.putString((prefix + "addr").c_str(), appConfig.tempSensors[i].address);
|
||||
preferences.putBool((prefix + "enabled").c_str(), appConfig.tempSensors[i].enabled);
|
||||
}
|
||||
|
||||
preferences.putBool("bms_enabled", appConfig.bms.enabled);
|
||||
preferences.putString("bms_name", appConfig.bms.name);
|
||||
preferences.putString("bms_addr", appConfig.bms.address);
|
||||
preferences.putString("bms_type", appConfig.bms.addressType);
|
||||
|
||||
preferences.end();
|
||||
}
|
||||
|
||||
void factoryResetConfig() {
|
||||
preferences.begin("xterra", false);
|
||||
preferences.clear();
|
||||
preferences.end();
|
||||
|
||||
loadDefaultConfig();
|
||||
saveConfig();
|
||||
}
|
||||
|
||||
void printConfig() {
|
||||
Serial.println("Config:");
|
||||
Serial.print(" Device: ");
|
||||
Serial.println(appConfig.deviceName);
|
||||
|
||||
Serial.println(" Relays:");
|
||||
for (int i = 0; i < MAX_RELAYS; i++) {
|
||||
Serial.print(" ");
|
||||
Serial.print(appConfig.relays[i].id);
|
||||
Serial.print(" / ");
|
||||
Serial.print(appConfig.relays[i].name);
|
||||
Serial.print(" / GPIO ");
|
||||
Serial.print(appConfig.relays[i].pin);
|
||||
Serial.print(" / enabled ");
|
||||
Serial.println(appConfig.relays[i].enabled ? "true" : "false");
|
||||
}
|
||||
|
||||
Serial.println(" Temperature Sensors:");
|
||||
for (int i = 0; i < MAX_TEMP_SENSORS; i++) {
|
||||
Serial.print(" ");
|
||||
Serial.print(appConfig.tempSensors[i].id);
|
||||
Serial.print(" / ");
|
||||
Serial.print(appConfig.tempSensors[i].name);
|
||||
Serial.print(" / enabled ");
|
||||
Serial.print(appConfig.tempSensors[i].enabled ? "true" : "false");
|
||||
Serial.print(" / address ");
|
||||
Serial.println(appConfig.tempSensors[i].address);
|
||||
}
|
||||
|
||||
Serial.println(" BMS:");
|
||||
Serial.print(" Enabled: ");
|
||||
Serial.println(appConfig.bms.enabled ? "true" : "false");
|
||||
Serial.print(" Name: ");
|
||||
Serial.println(appConfig.bms.name);
|
||||
Serial.print(" Address: ");
|
||||
Serial.println(appConfig.bms.address);
|
||||
Serial.print(" Address Type: ");
|
||||
Serial.println(appConfig.bms.addressType);
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
#pragma once
|
||||
|
||||
#include <Arduino.h>
|
||||
|
||||
#define MAX_RELAYS 2
|
||||
#define MAX_TEMP_SENSORS 8
|
||||
|
||||
struct RelayConfig {
|
||||
String id;
|
||||
String name;
|
||||
uint8_t pin;
|
||||
bool enabled;
|
||||
};
|
||||
|
||||
struct TempSensorConfig {
|
||||
String id;
|
||||
String name;
|
||||
String address;
|
||||
bool enabled;
|
||||
};
|
||||
|
||||
struct BmsConfig {
|
||||
bool enabled;
|
||||
String name;
|
||||
String address;
|
||||
String addressType;
|
||||
};
|
||||
|
||||
struct AppConfig {
|
||||
String deviceName;
|
||||
RelayConfig relays[MAX_RELAYS];
|
||||
TempSensorConfig tempSensors[MAX_TEMP_SENSORS];
|
||||
int tempSensorCount;
|
||||
BmsConfig bms;
|
||||
};
|
||||
|
||||
extern AppConfig appConfig;
|
||||
|
||||
void loadDefaultConfig();
|
||||
void loadConfig();
|
||||
void saveConfig();
|
||||
void factoryResetConfig();
|
||||
void printConfig();
|
||||
@@ -0,0 +1,533 @@
|
||||
#include "bms.h"
|
||||
|
||||
#include <Arduino.h>
|
||||
#include <NimBLEDevice.h>
|
||||
#include "logger.h"
|
||||
#include "app_config.h"
|
||||
|
||||
// BMS address is loaded from appConfig.bms.address.
|
||||
static const char* BMS_SERVICE_UUID = "ff00";
|
||||
static const char* BMS_NOTIFY_UUID = "ff01";
|
||||
static const char* BMS_WRITE_UUID = "ff02";
|
||||
|
||||
static NimBLEClient* client = nullptr;
|
||||
static NimBLERemoteCharacteristic* notifyChar = nullptr;
|
||||
static NimBLERemoteCharacteristic* writeChar = nullptr;
|
||||
|
||||
static uint8_t responseBuffer[128];
|
||||
static size_t responseLength = 0;
|
||||
static bool responseReady = false;
|
||||
|
||||
static unsigned long lastReadAttempt = 0;
|
||||
static unsigned long bmsReconnectPausedUntil = 0;
|
||||
static bool bmsSetupMode = false;
|
||||
static const unsigned long READ_INTERVAL_MS = 5000;
|
||||
|
||||
BmsData bmsData;
|
||||
|
||||
static BleScanResult bleScanResults[MAX_BLE_SCAN_RESULTS];
|
||||
static int bleScanResultCount = 0;
|
||||
|
||||
static uint8_t JBD_STATUS_REQUEST[] = {
|
||||
0xDD, 0xA5, 0x03, 0x00, 0xFF, 0xFD, 0x77
|
||||
};
|
||||
|
||||
static uint8_t JBD_CELL_REQUEST[] = {
|
||||
0xDD, 0xA5, 0x04, 0x00, 0xFF, 0xFC, 0x77
|
||||
};
|
||||
|
||||
static uint16_t readU16(const uint8_t* data, int index) {
|
||||
return ((uint16_t)data[index] << 8) | data[index + 1];
|
||||
}
|
||||
|
||||
static int16_t readS16(const uint8_t* data, int index) {
|
||||
return (int16_t)readU16(data, index);
|
||||
}
|
||||
|
||||
static void resetResponseBuffer() {
|
||||
responseLength = 0;
|
||||
responseReady = false;
|
||||
memset(responseBuffer, 0, sizeof(responseBuffer));
|
||||
}
|
||||
|
||||
static void handleNotify(
|
||||
NimBLERemoteCharacteristic* characteristic,
|
||||
uint8_t* data,
|
||||
size_t length,
|
||||
bool isNotify
|
||||
) {
|
||||
if (responseLength + length > sizeof(responseBuffer)) {
|
||||
resetResponseBuffer();
|
||||
return;
|
||||
}
|
||||
|
||||
memcpy(responseBuffer + responseLength, data, length);
|
||||
responseLength += length;
|
||||
|
||||
if (responseLength > 0 && responseBuffer[responseLength - 1] == 0x77) {
|
||||
responseReady = true;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
class BleScanCallbacks : public NimBLEScanCallbacks {
|
||||
void onResult(const NimBLEAdvertisedDevice* device) override {
|
||||
if (bleScanResultCount >= MAX_BLE_SCAN_RESULTS) {
|
||||
return;
|
||||
}
|
||||
|
||||
String address = device->getAddress().toString().c_str();
|
||||
|
||||
for (int i = 0; i < bleScanResultCount; i++) {
|
||||
if (bleScanResults[i].address == address) {
|
||||
bleScanResults[i].rssi = device->getRSSI();
|
||||
|
||||
if (bleScanResults[i].name.length() == 0 && device->haveName()) {
|
||||
bleScanResults[i].name = device->getName().c_str();
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
String name = "";
|
||||
if (device->haveName()) {
|
||||
name = device->getName().c_str();
|
||||
}
|
||||
|
||||
bleScanResults[bleScanResultCount].address = address;
|
||||
bleScanResults[bleScanResultCount].name = name;
|
||||
bleScanResults[bleScanResultCount].rssi = device->getRSSI();
|
||||
|
||||
Serial.print(logTimestamp());
|
||||
Serial.print(" BLE: found ");
|
||||
Serial.print(bleScanResultCount + 1);
|
||||
Serial.print(") ");
|
||||
|
||||
if (name.length() > 0) {
|
||||
Serial.print(name);
|
||||
} else {
|
||||
Serial.print("(no name)");
|
||||
}
|
||||
|
||||
Serial.print(" | ");
|
||||
Serial.print(address);
|
||||
Serial.print(" | RSSI ");
|
||||
Serial.println(device->getRSSI());
|
||||
|
||||
bleScanResultCount++;
|
||||
}
|
||||
};
|
||||
|
||||
static BleScanCallbacks bleScanCallbacks;
|
||||
|
||||
void pauseBmsReconnect(uint32_t pauseMs) {
|
||||
bmsReconnectPausedUntil = millis() + pauseMs;
|
||||
}
|
||||
|
||||
|
||||
void enterBmsSetupMode() {
|
||||
Serial.print(logTimestamp());
|
||||
Serial.println(" BMS setup: entering setup mode");
|
||||
|
||||
bmsSetupMode = true;
|
||||
pauseBmsReconnect(3600000UL);
|
||||
|
||||
if (client && client->isConnected()) {
|
||||
Serial.print(logTimestamp());
|
||||
Serial.println(" BMS setup: disconnecting BMS");
|
||||
client->disconnect();
|
||||
}
|
||||
|
||||
if (client) {
|
||||
NimBLEDevice::deleteClient(client);
|
||||
client = nullptr;
|
||||
}
|
||||
|
||||
notifyChar = nullptr;
|
||||
writeChar = nullptr;
|
||||
bmsData.connected = false;
|
||||
|
||||
Serial.print(logTimestamp());
|
||||
Serial.println(" BMS setup: ready for BLE scans");
|
||||
}
|
||||
|
||||
void exitBmsSetupMode() {
|
||||
Serial.print(logTimestamp());
|
||||
Serial.println(" BMS setup: exiting setup mode");
|
||||
|
||||
bmsSetupMode = false;
|
||||
bmsReconnectPausedUntil = 0;
|
||||
}
|
||||
|
||||
bool isBmsSetupMode() {
|
||||
return bmsSetupMode;
|
||||
}
|
||||
|
||||
int getBleScanResultCount() {
|
||||
return bleScanResultCount;
|
||||
}
|
||||
|
||||
const BleScanResult* getBleScanResult(int index) {
|
||||
if (index < 0 || index >= bleScanResultCount) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
return &bleScanResults[index];
|
||||
}
|
||||
|
||||
int scanBleDevices(uint32_t scanSeconds) {
|
||||
Serial.print(logTimestamp());
|
||||
Serial.println(" BLE: Starting setup scan...");
|
||||
|
||||
if (!bmsSetupMode) {
|
||||
Serial.print(logTimestamp());
|
||||
Serial.println(" BLE: auto-entering BMS setup mode for scan");
|
||||
enterBmsSetupMode();
|
||||
}
|
||||
|
||||
bleScanResultCount = 0;
|
||||
|
||||
Serial.print(logTimestamp());
|
||||
Serial.println(" BLE: waiting 5 seconds before scan");
|
||||
delay(5000);
|
||||
|
||||
NimBLEScan* scan = NimBLEDevice::getScan();
|
||||
scan->setActiveScan(true);
|
||||
scan->setInterval(100);
|
||||
scan->setWindow(99);
|
||||
scan->clearResults();
|
||||
|
||||
Serial.print(logTimestamp());
|
||||
Serial.print(" BLE: scanning for ");
|
||||
Serial.print(scanSeconds);
|
||||
Serial.println(" seconds...");
|
||||
|
||||
unsigned long scanStart = millis();
|
||||
|
||||
NimBLEScanResults results = scan->getResults(scanSeconds, false);
|
||||
|
||||
unsigned long elapsed = (millis() - scanStart) / 1000;
|
||||
|
||||
Serial.print(logTimestamp());
|
||||
Serial.print(" BLE: scan returned after ");
|
||||
Serial.print(elapsed);
|
||||
Serial.println(" seconds");
|
||||
|
||||
int resultCount = results.getCount();
|
||||
|
||||
Serial.print(logTimestamp());
|
||||
Serial.print(" BLE: raw devices seen: ");
|
||||
Serial.println(resultCount);
|
||||
|
||||
for (int i = 0; i < resultCount && bleScanResultCount < MAX_BLE_SCAN_RESULTS; i++) {
|
||||
const NimBLEAdvertisedDevice* device = results.getDevice(i);
|
||||
|
||||
String address = device->getAddress().toString().c_str();
|
||||
|
||||
bool alreadySeen = false;
|
||||
for (int existing = 0; existing < bleScanResultCount; existing++) {
|
||||
if (bleScanResults[existing].address == address) {
|
||||
alreadySeen = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (alreadySeen) {
|
||||
continue;
|
||||
}
|
||||
|
||||
String name = "";
|
||||
if (device->haveName()) {
|
||||
name = device->getName().c_str();
|
||||
}
|
||||
|
||||
bleScanResults[bleScanResultCount].address = address;
|
||||
bleScanResults[bleScanResultCount].name = name;
|
||||
bleScanResults[bleScanResultCount].rssi = device->getRSSI();
|
||||
bleScanResultCount++;
|
||||
}
|
||||
|
||||
scan->clearResults();
|
||||
|
||||
Serial.print(logTimestamp());
|
||||
Serial.print(" BLE: devices found: ");
|
||||
Serial.println(bleScanResultCount);
|
||||
|
||||
if (bleScanResultCount == 0) {
|
||||
Serial.println("No BLE devices found.");
|
||||
return 0;
|
||||
}
|
||||
|
||||
Serial.println("BLE devices:");
|
||||
|
||||
for (int i = 0; i < bleScanResultCount; i++) {
|
||||
Serial.print(i + 1);
|
||||
Serial.print(") ");
|
||||
|
||||
if (bleScanResults[i].name.length() > 0) {
|
||||
Serial.print(bleScanResults[i].name);
|
||||
} else {
|
||||
Serial.print("(no name)");
|
||||
}
|
||||
|
||||
Serial.print(" | ");
|
||||
Serial.print(bleScanResults[i].address);
|
||||
Serial.print(" | RSSI ");
|
||||
Serial.println(bleScanResults[i].rssi);
|
||||
}
|
||||
|
||||
Serial.println("Use: select bms <number>");
|
||||
Serial.println("Use: exit setup when finished");
|
||||
|
||||
return bleScanResultCount;
|
||||
}
|
||||
|
||||
static bool connectBms() {
|
||||
if (client && client->isConnected()) {
|
||||
bmsData.connected = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (client && !client->isConnected()) {
|
||||
Serial.println("BMS: Disconnected, resetting client");
|
||||
NimBLEDevice::deleteClient(client);
|
||||
client = nullptr;
|
||||
notifyChar = nullptr;
|
||||
writeChar = nullptr;
|
||||
bmsData.connected = false;
|
||||
}
|
||||
|
||||
Serial.println("BMS: Connecting...");
|
||||
|
||||
if (!appConfig.bms.enabled || appConfig.bms.address.length() == 0) {
|
||||
bmsData.connected = false;
|
||||
return false;
|
||||
}
|
||||
|
||||
NimBLEAddress address(
|
||||
std::string(appConfig.bms.address.c_str()),
|
||||
appConfig.bms.addressType == "random" ? BLE_ADDR_RANDOM : BLE_ADDR_PUBLIC
|
||||
);
|
||||
|
||||
client = NimBLEDevice::createClient();
|
||||
|
||||
if (!client->connect(address)) {
|
||||
Serial.println("BMS: Connect failed");
|
||||
bmsData.connected = false;
|
||||
NimBLEDevice::deleteClient(client);
|
||||
client = nullptr;
|
||||
return false;
|
||||
}
|
||||
|
||||
NimBLERemoteService* service = client->getService(BMS_SERVICE_UUID);
|
||||
if (!service) {
|
||||
Serial.println("BMS: Service ff00 not found");
|
||||
client->disconnect();
|
||||
bmsData.connected = false;
|
||||
return false;
|
||||
}
|
||||
|
||||
notifyChar = service->getCharacteristic(BMS_NOTIFY_UUID);
|
||||
writeChar = service->getCharacteristic(BMS_WRITE_UUID);
|
||||
|
||||
if (!notifyChar || !writeChar) {
|
||||
Serial.println("BMS: Characteristics not found");
|
||||
client->disconnect();
|
||||
bmsData.connected = false;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!notifyChar->subscribe(true, handleNotify)) {
|
||||
Serial.println("BMS: Notify subscribe failed");
|
||||
client->disconnect();
|
||||
bmsData.connected = false;
|
||||
return false;
|
||||
}
|
||||
|
||||
Serial.println("BMS: Connected");
|
||||
bmsData.connected = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool requestPacket(uint8_t* command, size_t commandLength) {
|
||||
resetResponseBuffer();
|
||||
|
||||
bool ok = writeChar->writeValue(command, commandLength, false);
|
||||
if (!ok) {
|
||||
Serial.println("BMS: Write failed");
|
||||
bmsData.connected = false;
|
||||
if (client) {
|
||||
client->disconnect();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
unsigned long start = millis();
|
||||
while (!responseReady && millis() - start < 2000) {
|
||||
delay(10);
|
||||
}
|
||||
|
||||
if (!responseReady) {
|
||||
Serial.println("BMS: Read timeout");
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool parseBasicInfo() {
|
||||
if (responseLength < 40) {
|
||||
Serial.println("BMS: Basic response too short");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (responseBuffer[0] != 0xDD || responseBuffer[1] != 0x03) {
|
||||
Serial.println("BMS: Unexpected basic response header");
|
||||
return false;
|
||||
}
|
||||
|
||||
uint16_t voltageRaw = readU16(responseBuffer, 4);
|
||||
int16_t currentRaw = readS16(responseBuffer, 6);
|
||||
uint16_t remainingRaw = readU16(responseBuffer, 8);
|
||||
uint16_t capacityRaw = readU16(responseBuffer, 10);
|
||||
uint16_t cyclesRaw = readU16(responseBuffer, 12);
|
||||
|
||||
uint8_t socRaw = responseBuffer[23];
|
||||
|
||||
uint8_t offset = 24;
|
||||
uint8_t cellCount = responseBuffer[offset + 1];
|
||||
uint8_t ntcCount = responseBuffer[offset + 2];
|
||||
|
||||
int tempOffset = offset + 3;
|
||||
float tempF = 0;
|
||||
|
||||
if (ntcCount > 0 && responseLength > (size_t)(tempOffset + 1)) {
|
||||
uint16_t tempRaw = readU16(responseBuffer, tempOffset);
|
||||
float tempC = (tempRaw - 2731) / 10.0;
|
||||
tempF = tempC * 9.0 / 5.0 + 32.0;
|
||||
}
|
||||
|
||||
bmsData.voltage = voltageRaw / 100.0;
|
||||
bmsData.current = currentRaw / 100.0;
|
||||
bmsData.remainingAh = remainingRaw / 100.0;
|
||||
bmsData.capacityAh = capacityRaw / 100.0;
|
||||
bmsData.cycleCount = cyclesRaw;
|
||||
bmsData.soc = socRaw;
|
||||
bmsData.cellCount = cellCount;
|
||||
bmsData.ntcCount = ntcCount;
|
||||
bmsData.temperatureF = tempF;
|
||||
bmsData.valid = true;
|
||||
|
||||
if (currentLogLevel >= LOG_DEBUG) {
|
||||
Serial.print("BMS: SOC ");
|
||||
Serial.print(bmsData.soc);
|
||||
Serial.print("%, V ");
|
||||
Serial.print(bmsData.voltage);
|
||||
Serial.print(", A ");
|
||||
Serial.print(bmsData.current);
|
||||
Serial.print(", Ah ");
|
||||
Serial.println(bmsData.remainingAh);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool parseCellVoltages() {
|
||||
if (responseLength < 7) {
|
||||
Serial.println("BMS: Cell response too short");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (responseBuffer[0] != 0xDD || responseBuffer[1] != 0x04) {
|
||||
Serial.println("BMS: Unexpected cell response header");
|
||||
return false;
|
||||
}
|
||||
|
||||
uint8_t payloadLength = responseBuffer[3];
|
||||
int cellsReported = payloadLength / 2;
|
||||
|
||||
if (cellsReported > BMS_MAX_CELLS) {
|
||||
cellsReported = BMS_MAX_CELLS;
|
||||
}
|
||||
|
||||
if (cellsReported <= 0) {
|
||||
Serial.println("BMS: No cell voltages reported");
|
||||
return false;
|
||||
}
|
||||
|
||||
float minV = 99.0;
|
||||
float maxV = 0.0;
|
||||
|
||||
for (int i = 0; i < cellsReported; i++) {
|
||||
uint16_t mv = readU16(responseBuffer, 4 + (i * 2));
|
||||
float volts = mv / 1000.0;
|
||||
|
||||
bmsData.cellVoltages[i] = volts;
|
||||
|
||||
if (volts < minV) {
|
||||
minV = volts;
|
||||
}
|
||||
|
||||
if (volts > maxV) {
|
||||
maxV = volts;
|
||||
}
|
||||
}
|
||||
|
||||
bmsData.cellCount = cellsReported;
|
||||
bmsData.cellMinVoltage = minV;
|
||||
bmsData.cellMaxVoltage = maxV;
|
||||
bmsData.cellDeltaMv = (int)((maxV - minV) * 1000.0 + 0.5);
|
||||
bmsData.cellsValid = true;
|
||||
|
||||
if (currentLogLevel >= LOG_DEBUG) {
|
||||
Serial.print("BMS: Cells ");
|
||||
Serial.print(cellsReported);
|
||||
Serial.print(", min ");
|
||||
Serial.print(minV, 3);
|
||||
Serial.print("V, max ");
|
||||
Serial.print(maxV, 3);
|
||||
Serial.print("V, delta ");
|
||||
Serial.print(bmsData.cellDeltaMv);
|
||||
Serial.println("mV");
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void initBms() {
|
||||
NimBLEDevice::init("XterraESP32");
|
||||
NimBLEDevice::setPower(ESP_PWR_LVL_P9);
|
||||
resetResponseBuffer();
|
||||
}
|
||||
|
||||
void updateBms() {
|
||||
if (bmsSetupMode) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ((long)(bmsReconnectPausedUntil - millis()) > 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (millis() - lastReadAttempt < READ_INTERVAL_MS) {
|
||||
return;
|
||||
}
|
||||
|
||||
lastReadAttempt = millis();
|
||||
|
||||
if (!connectBms()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (requestPacket(JBD_STATUS_REQUEST, sizeof(JBD_STATUS_REQUEST))) {
|
||||
parseBasicInfo();
|
||||
}
|
||||
|
||||
delay(100);
|
||||
|
||||
if (requestPacket(JBD_CELL_REQUEST, sizeof(JBD_CELL_REQUEST))) {
|
||||
parseCellVoltages();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
#pragma once
|
||||
|
||||
#include <Arduino.h>
|
||||
|
||||
#define BMS_MAX_CELLS 8
|
||||
#define MAX_BLE_SCAN_RESULTS 10
|
||||
|
||||
struct BleScanResult {
|
||||
String name;
|
||||
String address;
|
||||
int rssi = 0;
|
||||
};
|
||||
|
||||
struct BmsData {
|
||||
bool connected = false;
|
||||
bool valid = false;
|
||||
bool cellsValid = false;
|
||||
|
||||
float soc = 0;
|
||||
float voltage = 0;
|
||||
float current = 0;
|
||||
float remainingAh = 0;
|
||||
float capacityAh = 0;
|
||||
float temperatureF = 0;
|
||||
|
||||
int cycleCount = 0;
|
||||
int cellCount = 0;
|
||||
int ntcCount = 0;
|
||||
|
||||
float cellVoltages[BMS_MAX_CELLS] = {0};
|
||||
float cellMinVoltage = 0;
|
||||
float cellMaxVoltage = 0;
|
||||
int cellDeltaMv = 0;
|
||||
};
|
||||
|
||||
extern BmsData bmsData;
|
||||
|
||||
void initBms();
|
||||
void updateBms();
|
||||
|
||||
int scanBleDevices(uint32_t scanSeconds = 10);
|
||||
int getBleScanResultCount();
|
||||
const BleScanResult* getBleScanResult(int index);
|
||||
void pauseBmsReconnect(uint32_t pauseMs);
|
||||
void enterBmsSetupMode();
|
||||
void exitBmsSetupMode();
|
||||
bool isBmsSetupMode();
|
||||
@@ -0,0 +1,20 @@
|
||||
#pragma once
|
||||
|
||||
#define DEVICE_NAME "Overland Controller"
|
||||
|
||||
// Relay Outputs
|
||||
#define RELAY_STARLINK_PIN 16
|
||||
#define RELAY_FRIDGE_PIN 17
|
||||
|
||||
// DS18B20 Bus
|
||||
#define ONEWIRE_PIN 4
|
||||
|
||||
// Ignition Sense
|
||||
#define IGNITION_PIN 34
|
||||
|
||||
// UART over CAT5 to Pico dashboard
|
||||
#define DASHBOARD_UART_TX_PIN 21
|
||||
#define DASHBOARD_UART_RX_PIN 22
|
||||
#define DASHBOARD_UART_BAUD 115200
|
||||
|
||||
// RS-485/MAX3485 is fallback only and not currently planned.
|
||||
@@ -0,0 +1,41 @@
|
||||
#include "logger.h"
|
||||
|
||||
LogLevel currentLogLevel = LOG_INFO;
|
||||
|
||||
String logTimestamp() {
|
||||
unsigned long seconds = millis() / 1000;
|
||||
unsigned long minutes = seconds / 60;
|
||||
unsigned long hours = minutes / 60;
|
||||
|
||||
char buffer[16];
|
||||
snprintf(
|
||||
buffer,
|
||||
sizeof(buffer),
|
||||
"[%02lu:%02lu:%02lu]",
|
||||
hours % 100,
|
||||
minutes % 60,
|
||||
seconds % 60
|
||||
);
|
||||
|
||||
return String(buffer);
|
||||
}
|
||||
|
||||
void setLogLevel(LogLevel level) {
|
||||
currentLogLevel = level;
|
||||
}
|
||||
|
||||
void logInfo(const String& message) {
|
||||
if (currentLogLevel >= LOG_INFO) {
|
||||
Serial.print(logTimestamp());
|
||||
Serial.print(" ");
|
||||
Serial.println(message);
|
||||
}
|
||||
}
|
||||
|
||||
void logDebug(const String& message) {
|
||||
if (currentLogLevel >= LOG_DEBUG) {
|
||||
Serial.print(logTimestamp());
|
||||
Serial.print(" ");
|
||||
Serial.println(message);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
#pragma once
|
||||
|
||||
#include <Arduino.h>
|
||||
|
||||
enum LogLevel {
|
||||
LOG_QUIET = 0,
|
||||
LOG_INFO = 1,
|
||||
LOG_DEBUG = 2
|
||||
};
|
||||
|
||||
extern LogLevel currentLogLevel;
|
||||
|
||||
String logTimestamp();
|
||||
void setLogLevel(LogLevel level);
|
||||
void logInfo(const String& message);
|
||||
void logDebug(const String& message);
|
||||
@@ -0,0 +1,7 @@
|
||||
#pragma once
|
||||
|
||||
#define MSG_STATUS_REQUEST "status_request"
|
||||
#define MSG_STATUS_RESPONSE "status_response"
|
||||
#define MSG_SET_RELAY "set_relay"
|
||||
#define MSG_RELAY_RESPONSE "relay_response"
|
||||
#define MSG_ERROR "error"
|
||||
@@ -0,0 +1,17 @@
|
||||
#include <Arduino.h>
|
||||
#include "config.h"
|
||||
#include "relays.h"
|
||||
|
||||
RelayState relays;
|
||||
|
||||
void initRelays() {
|
||||
pinMode(RELAY_STARLINK_PIN, OUTPUT);
|
||||
pinMode(RELAY_FRIDGE_PIN, OUTPUT);
|
||||
|
||||
updateRelayOutputs();
|
||||
}
|
||||
|
||||
void updateRelayOutputs() {
|
||||
digitalWrite(RELAY_STARLINK_PIN, relays.starlink);
|
||||
digitalWrite(RELAY_FRIDGE_PIN, relays.fridge);
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
#pragma once
|
||||
|
||||
struct RelayState {
|
||||
bool starlink = false;
|
||||
bool fridge = false;
|
||||
};
|
||||
|
||||
extern RelayState relays;
|
||||
|
||||
void initRelays();
|
||||
void updateRelayOutputs();
|
||||
@@ -0,0 +1,87 @@
|
||||
#include <Arduino.h>
|
||||
#include <OneWire.h>
|
||||
#include <DallasTemperature.h>
|
||||
|
||||
#include "config.h"
|
||||
#include "sensors.h"
|
||||
|
||||
SensorData sensors;
|
||||
|
||||
OneWire oneWire(ONEWIRE_PIN);
|
||||
DallasTemperature ds18b20(&oneWire);
|
||||
|
||||
DeviceAddress sensorAddresses[4];
|
||||
int sensorCount = 0;
|
||||
|
||||
float cToF(float c) {
|
||||
return (c * 9.0 / 5.0) + 32.0;
|
||||
}
|
||||
|
||||
bool validTempC(float tempC) {
|
||||
return tempC != DEVICE_DISCONNECTED_C && tempC > -55 && tempC < 125;
|
||||
}
|
||||
|
||||
void printAddress(DeviceAddress address) {
|
||||
for (uint8_t i = 0; i < 8; i++) {
|
||||
if (address[i] < 16) Serial.print("0");
|
||||
Serial.print(address[i], HEX);
|
||||
if (i < 7) Serial.print(":");
|
||||
}
|
||||
}
|
||||
|
||||
void printSensorAddresses() {
|
||||
Serial.println("DS18B20 Sensors Found:");
|
||||
|
||||
for (int i = 0; i < sensorCount; i++) {
|
||||
Serial.print("Sensor ");
|
||||
Serial.print(i);
|
||||
Serial.print(": ");
|
||||
printAddress(sensorAddresses[i]);
|
||||
Serial.println();
|
||||
}
|
||||
|
||||
if (sensorCount == 0) {
|
||||
Serial.println("No DS18B20 sensors found.");
|
||||
}
|
||||
}
|
||||
|
||||
void initSensors() {
|
||||
ds18b20.begin();
|
||||
sensorCount = ds18b20.getDeviceCount();
|
||||
|
||||
if (sensorCount > 4) {
|
||||
sensorCount = 4;
|
||||
}
|
||||
|
||||
for (int i = 0; i < sensorCount; i++) {
|
||||
ds18b20.getAddress(sensorAddresses[i], i);
|
||||
}
|
||||
|
||||
printSensorAddresses();
|
||||
}
|
||||
|
||||
void updateSensors() {
|
||||
ds18b20.requestTemperatures();
|
||||
|
||||
float tempsF[4] = {-127, -127, -127, -127};
|
||||
bool online[4] = {false, false, false, false};
|
||||
|
||||
for (int i = 0; i < sensorCount; i++) {
|
||||
float tempC = ds18b20.getTempC(sensorAddresses[i]);
|
||||
|
||||
if (validTempC(tempC)) {
|
||||
tempsF[i] = cToF(tempC);
|
||||
online[i] = true;
|
||||
}
|
||||
}
|
||||
|
||||
sensors.fridgeZone1 = tempsF[0];
|
||||
sensors.fridgeZone2 = tempsF[1];
|
||||
sensors.rearSeat = tempsF[2];
|
||||
sensors.outsideAir = tempsF[3];
|
||||
|
||||
sensors.fridgeZone1Online = online[0];
|
||||
sensors.fridgeZone2Online = online[1];
|
||||
sensors.rearSeatOnline = online[2];
|
||||
sensors.outsideAirOnline = online[3];
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
#pragma once
|
||||
|
||||
struct SensorData {
|
||||
float fridgeZone1 = -127.0;
|
||||
float fridgeZone2 = -127.0;
|
||||
float rearSeat = -127.0;
|
||||
float outsideAir = -127.0;
|
||||
|
||||
bool fridgeZone1Online = false;
|
||||
bool fridgeZone2Online = false;
|
||||
bool rearSeatOnline = false;
|
||||
bool outsideAirOnline = false;
|
||||
};
|
||||
|
||||
extern SensorData sensors;
|
||||
|
||||
void initSensors();
|
||||
void updateSensors();
|
||||
void printSensorAddresses();
|
||||
@@ -0,0 +1,4 @@
|
||||
#pragma once
|
||||
|
||||
#define FIRMWARE_VERSION "0.3.0"
|
||||
#define FIRMWARE_NAME "overland-controller"
|
||||
@@ -0,0 +1,770 @@
|
||||
#include <WiFi.h>
|
||||
#include <WebServer.h>
|
||||
#include <ArduinoJson.h>
|
||||
|
||||
#include "config.h"
|
||||
#include "protocol.h"
|
||||
#include "relays.h"
|
||||
#include "sensors.h"
|
||||
#include "bms.h"
|
||||
#include "alarms.h"
|
||||
#include "system_status.h"
|
||||
#include "app_config.h"
|
||||
#include "logger.h"
|
||||
|
||||
WebServer server(80);
|
||||
HardwareSerial DashboardSerial(2);
|
||||
|
||||
String uartLineBuffer;
|
||||
|
||||
float calculateRuntimeHours() {
|
||||
if (!bmsData.valid || bmsData.current >= 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
float dischargeAmps = -bmsData.current;
|
||||
if (dischargeAmps <= 0.1) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return bmsData.remainingAh / dischargeAmps;
|
||||
}
|
||||
|
||||
void buildStatusDocument(JsonDocument& doc) {
|
||||
doc["type"] = MSG_STATUS_RESPONSE;
|
||||
doc["timestamp"] = millis();
|
||||
|
||||
JsonObject battery = doc.createNestedObject("battery");
|
||||
|
||||
if (bmsData.valid) {
|
||||
battery["source"] = "jbd_bms";
|
||||
battery["connected"] = bmsData.connected;
|
||||
battery["soc"] = bmsData.soc;
|
||||
battery["voltage"] = bmsData.voltage;
|
||||
battery["current"] = bmsData.current;
|
||||
battery["remaining_ah"] = bmsData.remainingAh;
|
||||
battery["capacity_ah"] = bmsData.capacityAh;
|
||||
battery["runtime_hours"] = calculateRuntimeHours();
|
||||
battery["temperature_f"] = bmsData.temperatureF;
|
||||
battery["cycle_count"] = bmsData.cycleCount;
|
||||
battery["cell_count"] = bmsData.cellCount;
|
||||
battery["ntc_count"] = bmsData.ntcCount;
|
||||
|
||||
JsonArray cells = battery.createNestedArray("cell_voltages");
|
||||
if (bmsData.cellsValid) {
|
||||
for (int i = 0; i < bmsData.cellCount; i++) {
|
||||
cells.add(bmsData.cellVoltages[i]);
|
||||
}
|
||||
}
|
||||
|
||||
battery["cell_min_voltage"] = bmsData.cellMinVoltage;
|
||||
battery["cell_max_voltage"] = bmsData.cellMaxVoltage;
|
||||
battery["cell_delta_mv"] = bmsData.cellDeltaMv;
|
||||
battery["cells_valid"] = bmsData.cellsValid;
|
||||
} else {
|
||||
battery["source"] = "placeholder";
|
||||
battery["connected"] = false;
|
||||
battery["soc"] = 0;
|
||||
battery["voltage"] = 0;
|
||||
battery["current"] = 0;
|
||||
battery["remaining_ah"] = 0;
|
||||
battery["capacity_ah"] = 150;
|
||||
battery["runtime_hours"] = 0;
|
||||
battery["temperature_f"] = 0;
|
||||
battery["cycle_count"] = 0;
|
||||
battery["cell_count"] = 0;
|
||||
battery["ntc_count"] = 0;
|
||||
}
|
||||
|
||||
JsonObject temps = doc.createNestedObject("temps");
|
||||
|
||||
if (sensors.fridgeZone1Online) {
|
||||
temps["fridge_zone_1"] = sensors.fridgeZone1;
|
||||
} else {
|
||||
temps["fridge_zone_1"] = nullptr;
|
||||
}
|
||||
|
||||
if (sensors.fridgeZone2Online) {
|
||||
temps["fridge_zone_2"] = sensors.fridgeZone2;
|
||||
} else {
|
||||
temps["fridge_zone_2"] = nullptr;
|
||||
}
|
||||
|
||||
if (sensors.rearSeatOnline) {
|
||||
temps["rear_seat"] = sensors.rearSeat;
|
||||
} else {
|
||||
temps["rear_seat"] = nullptr;
|
||||
}
|
||||
|
||||
if (sensors.outsideAirOnline) {
|
||||
temps["outside"] = sensors.outsideAir;
|
||||
} else {
|
||||
temps["outside"] = 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;
|
||||
|
||||
JsonObject alarmObj = doc.createNestedObject("alarms");
|
||||
alarmObj["low_soc"] = alarms.lowSoc;
|
||||
alarmObj["critical_soc"] = alarms.criticalSoc;
|
||||
alarmObj["low_voltage"] = alarms.lowVoltage;
|
||||
alarmObj["high_battery_temp"] = alarms.highBatteryTemp;
|
||||
alarmObj["cell_imbalance"] = alarms.cellImbalance;
|
||||
alarmObj["bms_disconnected"] = alarms.bmsDisconnected;
|
||||
|
||||
JsonObject system = doc.createNestedObject("system");
|
||||
system["firmware_name"] = FIRMWARE_NAME;
|
||||
system["firmware_version"] = FIRMWARE_VERSION;
|
||||
system["build_date"] = __DATE__;
|
||||
system["build_time"] = __TIME__;
|
||||
system["uptime_seconds"] = millis() / 1000;
|
||||
|
||||
JsonObject configObj = doc.createNestedObject("config");
|
||||
configObj["device_name"] = appConfig.deviceName;
|
||||
|
||||
JsonArray configRelays = configObj.createNestedArray("relays");
|
||||
for (int i = 0; i < MAX_RELAYS; i++) {
|
||||
JsonObject relayConfig = configRelays.createNestedObject();
|
||||
relayConfig["id"] = appConfig.relays[i].id;
|
||||
relayConfig["name"] = appConfig.relays[i].name;
|
||||
relayConfig["pin"] = appConfig.relays[i].pin;
|
||||
relayConfig["enabled"] = appConfig.relays[i].enabled;
|
||||
}
|
||||
|
||||
JsonObject bmsConfig = configObj.createNestedObject("bms");
|
||||
bmsConfig["enabled"] = appConfig.bms.enabled;
|
||||
bmsConfig["name"] = appConfig.bms.name;
|
||||
bmsConfig["address"] = appConfig.bms.address;
|
||||
bmsConfig["address_type"] = appConfig.bms.addressType;
|
||||
|
||||
JsonArray tempConfigs = configObj.createNestedArray("temperature_sensors");
|
||||
for (int i = 0; i < MAX_TEMP_SENSORS; i++) {
|
||||
JsonObject tempConfig = tempConfigs.createNestedObject();
|
||||
tempConfig["id"] = appConfig.tempSensors[i].id;
|
||||
tempConfig["name"] = appConfig.tempSensors[i].name;
|
||||
tempConfig["address"] = appConfig.tempSensors[i].address;
|
||||
tempConfig["enabled"] = appConfig.tempSensors[i].enabled;
|
||||
}
|
||||
}
|
||||
|
||||
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 handleDebugSerial() {
|
||||
if (!Serial.available()) {
|
||||
return;
|
||||
}
|
||||
|
||||
String command = Serial.readStringUntil('\n');
|
||||
command.trim();
|
||||
|
||||
if (command == "status") {
|
||||
sendStatus(Serial, true);
|
||||
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;
|
||||
}
|
||||
|
||||
if (command == "log quiet") {
|
||||
setLogLevel(LOG_QUIET);
|
||||
Serial.println("OK log level quiet");
|
||||
return;
|
||||
}
|
||||
|
||||
if (command == "log info") {
|
||||
setLogLevel(LOG_INFO);
|
||||
Serial.println("OK log level info");
|
||||
return;
|
||||
}
|
||||
|
||||
if (command == "log debug") {
|
||||
setLogLevel(LOG_DEBUG);
|
||||
Serial.println("OK log level debug");
|
||||
return;
|
||||
}
|
||||
|
||||
if (command == "factory reset") {
|
||||
factoryResetConfig();
|
||||
Serial.println("OK factory reset complete");
|
||||
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);
|
||||
|
||||
Serial.println("OK relay name updated");
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
Serial.println("OK temp sensor name updated");
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (command.startsWith("bmsname ")) {
|
||||
appConfig.bms.name = command.substring(8);
|
||||
Serial.println("OK bms name updated");
|
||||
return;
|
||||
}
|
||||
|
||||
if (command.startsWith("bmsaddr ")) {
|
||||
appConfig.bms.address = command.substring(8);
|
||||
Serial.println("OK bms address updated");
|
||||
return;
|
||||
}
|
||||
|
||||
if (command == "enter setup") {
|
||||
enterBmsSetupMode();
|
||||
return;
|
||||
}
|
||||
|
||||
if (command == "exit setup") {
|
||||
exitBmsSetupMode();
|
||||
return;
|
||||
}
|
||||
|
||||
if (command == "scan ble") {
|
||||
scanBleDevices(20);
|
||||
return;
|
||||
}
|
||||
|
||||
if (command.startsWith("select bms ")) {
|
||||
int selected = command.substring(11).toInt();
|
||||
|
||||
if (selected < 1 || selected > getBleScanResultCount()) {
|
||||
Serial.println("Invalid BMS selection. Run: scan ble");
|
||||
return;
|
||||
}
|
||||
|
||||
const BleScanResult* result = getBleScanResult(selected - 1);
|
||||
|
||||
if (!result) {
|
||||
Serial.println("Invalid BMS selection. Run: scan ble");
|
||||
return;
|
||||
}
|
||||
|
||||
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";
|
||||
}
|
||||
|
||||
saveConfig();
|
||||
|
||||
Serial.print("OK selected BMS: ");
|
||||
Serial.print(appConfig.bms.name);
|
||||
Serial.print(" / ");
|
||||
Serial.println(appConfig.bms.address);
|
||||
Serial.println("BMS will reconnect on next update cycle.");
|
||||
exitBmsSetupMode();
|
||||
return;
|
||||
}
|
||||
|
||||
if (command.length() > 0) {
|
||||
Serial.print("Unknown command: ");
|
||||
Serial.println(command);
|
||||
Serial.println("Commands: status, relay starlink on/off, relay fridge on/off");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
String output;
|
||||
serializeJson(doc, output);
|
||||
server.send(200, "application/json", output);
|
||||
}
|
||||
|
||||
void sendOkConfig() {
|
||||
saveConfig();
|
||||
sendConfigJson();
|
||||
}
|
||||
|
||||
int findRelayConfigIndex(const String& id) {
|
||||
for (int i = 0; i < MAX_RELAYS; 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;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
void handleGetConfig() {
|
||||
sendConfigJson();
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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>();
|
||||
}
|
||||
|
||||
sendOkConfig();
|
||||
}
|
||||
|
||||
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>();
|
||||
}
|
||||
|
||||
sendOkConfig();
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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>();
|
||||
}
|
||||
|
||||
sendOkConfig();
|
||||
}
|
||||
|
||||
void handleFactoryResetConfig() {
|
||||
factoryResetConfig();
|
||||
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;
|
||||
}
|
||||
|
||||
String rest = uri.substring(prefix.length());
|
||||
int slash = rest.indexOf('/');
|
||||
|
||||
if (slash < 0) {
|
||||
server.send(400, "application/json", "{\"ok\":false,\"error\":\"missing_relay_action\"}");
|
||||
return;
|
||||
}
|
||||
|
||||
String relayId = rest.substring(0, slash);
|
||||
String action = rest.substring(slash + 1);
|
||||
|
||||
bool enabled;
|
||||
|
||||
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.starlink = enabled;
|
||||
} else if (relayId == "relay_2") {
|
||||
relays.fridge = enabled;
|
||||
} else {
|
||||
server.send(404, "application/json", "{\"ok\":false,\"error\":\"unknown_relay\"}");
|
||||
return;
|
||||
}
|
||||
|
||||
updateRelayOutputs();
|
||||
|
||||
DynamicJsonDocument doc(512);
|
||||
doc["ok"] = true;
|
||||
doc["id"] = relayId;
|
||||
doc["state"] = enabled;
|
||||
|
||||
String output;
|
||||
serializeJson(doc, output);
|
||||
server.send(200, "application/json", output);
|
||||
}
|
||||
|
||||
void handleStatus() {
|
||||
DynamicJsonDocument doc(2048);
|
||||
buildStatusDocument(doc);
|
||||
|
||||
String output;
|
||||
serializeJson(doc, 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() {
|
||||
Serial.begin(115200);
|
||||
|
||||
DashboardSerial.begin(
|
||||
DASHBOARD_UART_BAUD,
|
||||
SERIAL_8N1,
|
||||
DASHBOARD_UART_RX_PIN,
|
||||
DASHBOARD_UART_TX_PIN
|
||||
);
|
||||
|
||||
Serial.println();
|
||||
Serial.println("==================================");
|
||||
|
||||
loadConfig();
|
||||
printConfig();
|
||||
Serial.println("Overland Controller Booting");
|
||||
Serial.println("==================================");
|
||||
|
||||
loadConfig();
|
||||
printConfig();
|
||||
|
||||
pinMode(IGNITION_PIN, INPUT);
|
||||
|
||||
initRelays();
|
||||
initSensors();
|
||||
initBms();
|
||||
|
||||
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/relay_1/on", HTTP_GET, handleGenericRelayRoute);
|
||||
server.on("/relay/relay_1/off", HTTP_GET, handleGenericRelayRoute);
|
||||
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");
|
||||
Serial.println("Dashboard UART Started");
|
||||
}
|
||||
|
||||
void loop() {
|
||||
server.handleClient();
|
||||
handleDebugSerial();
|
||||
updateBms();
|
||||
updateAlarms();
|
||||
pollDashboardUart();
|
||||
|
||||
static unsigned long lastSensorUpdate = 0;
|
||||
|
||||
if (millis() - lastSensorUpdate > 5000) {
|
||||
updateSensors();
|
||||
lastSensorUpdate = millis();
|
||||
logDebug("Sensor Update");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user