Compare commits
41
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f2f452d04a | ||
|
|
fece1f39d0 | ||
|
|
de6a3f5f92 | ||
|
|
d17699a7be | ||
|
|
3b57761498 | ||
|
|
9b5581b2d8 | ||
|
|
97048b8ceb | ||
|
|
95fa78ee64 | ||
|
|
983f9cae61 | ||
|
|
83a392567c | ||
|
|
c8d0e4a23f | ||
|
|
ac6b0475ff | ||
|
|
2112e6b58c | ||
|
|
617fef32ce | ||
|
|
ebed07500a | ||
|
|
c602187163 | ||
|
|
3ec837a08c | ||
|
|
0b55ede1fc | ||
|
|
0b04dcf89a | ||
|
|
737ec8c10d | ||
|
|
f2bdf36337 | ||
|
|
3a05cb8894 | ||
|
|
a1a19270d7 | ||
|
|
f249e2ef6b | ||
|
|
6ae396f084 | ||
|
|
7fc2d35bd0 | ||
|
|
a954755c46 | ||
|
|
c2f8f1d9a5 | ||
|
|
b301400482 | ||
|
|
3e56143d4b | ||
|
|
025e95b32f | ||
|
|
4f24e6f87a | ||
|
|
b21e41341c | ||
|
|
03cad0481b | ||
|
|
1f54f39956 | ||
|
|
acf98eb8e2 | ||
|
|
5571aca726 | ||
|
|
43d201d040 | ||
|
|
b556d338d7 | ||
|
|
4cbd2165b2 | ||
|
|
406033bec8 |
@@ -239,3 +239,30 @@ Important guardrails:
|
||||
## M9N GPS Module
|
||||
|
||||
The dashboard enclosure will include an M9N GPS module connected to the ESP32-S3 dashboard over UART. It will provide GPS fix status, satellite count, position, speed, and UTC time. Future work will use GPS time/location for automatic timezone and DST handling.
|
||||
|
||||
|
||||
### LilyGO T-Relay-S3 6-relay cargo controller
|
||||
|
||||
The cargo ESP now targets the LilyGO T-Relay-S3 6-relay board.
|
||||
|
||||
Relay control is handled through the onboard 74HC595 shift register, not direct ESP32 GPIO relay outputs.
|
||||
|
||||
| Function | GPIO |
|
||||
| --- | --- |
|
||||
| 74HC595 DATA | GPIO7 |
|
||||
| 74HC595 CLOCK | GPIO5 |
|
||||
| 74HC595 LATCH | GPIO6 |
|
||||
| 74HC595 nOE | GPIO4 |
|
||||
|
||||
| Firmware ID | LilyGO relay channel | Shift-register output |
|
||||
| --- | --- | --- |
|
||||
| relay_1 | CH1 | SR1 |
|
||||
| relay_2 | CH2 | SR2 |
|
||||
| relay_3 | CH3 | SR3 |
|
||||
| relay_4 | CH4 | SR4 |
|
||||
| relay_5 | CH5 | SR5 |
|
||||
| relay_6 | CH6 | SR6 |
|
||||
|
||||
GPIO4 is reserved for relay output-enable on this board, so the DS18B20 OneWire bus is moved to GPIO14.
|
||||
|
||||
Avoid GPIO35-GPIO37 on the LilyGO T-Relay-S3 because they are used by Octal SPI flash.
|
||||
|
||||
@@ -157,7 +157,7 @@ Expected output object fields:
|
||||
Example 2-channel profile:
|
||||
|
||||
{
|
||||
"hardware_profile": "generic_esp32_2ch_relay",
|
||||
"hardware_profile": "lilygo_t_relay_s3_6ch",
|
||||
"outputs": [
|
||||
{"id": 1, "name": "Fridge", "role": "fridge", "hardware_channel": 1},
|
||||
{"id": 2, "name": "Starlink", "role": "starlink", "hardware_channel": 2}
|
||||
@@ -190,7 +190,7 @@ The Cargo ESP firmware now exposes relay/output state as an output-count-agnosti
|
||||
|
||||
The current active hardware profile remains:
|
||||
|
||||
generic_esp32_2ch_relay
|
||||
lilygo_t_relay_s3_6ch
|
||||
|
||||
The API includes:
|
||||
|
||||
|
||||
@@ -270,7 +270,7 @@ The Cargo ESP API now represents relays/outputs as a hardware-profile-aware list
|
||||
|
||||
Current active hardware profile:
|
||||
|
||||
generic_esp32_2ch_relay
|
||||
lilygo_t_relay_s3_6ch
|
||||
|
||||
Future planned profile:
|
||||
|
||||
|
||||
@@ -213,3 +213,13 @@ Rationale:
|
||||
- Improves perceived responsiveness without a major communication rewrite.
|
||||
- Supports future live gauges better than fixed-interval polling.
|
||||
|
||||
|
||||
|
||||
## LilyGO T-Relay-S3 6-relay migration
|
||||
|
||||
- [x] Expand cargo relay model from 2 outputs to 6 outputs.
|
||||
- [x] Replace direct GPIO relay writes with LilyGO 74HC595 shift-register writes.
|
||||
- [x] Map relay_1 through relay_6 to CH1 through CH6.
|
||||
- [x] Move DS18B20 OneWire default from GPIO4 to GPIO14 because GPIO4 is relay nOE.
|
||||
- [ ] Bench test relay boot behavior with no vehicle loads connected.
|
||||
- [ ] Confirm CH1-CH6 order with a meter or test lamp before wiring vehicle loads.
|
||||
|
||||
@@ -4,7 +4,7 @@ static const char *DASHBOARD_VERSION = "0.1.126-restore-boot-screen";
|
||||
|
||||
// Update these if your Cargo ESP AP credentials are different.
|
||||
static const char *CARGO_WIFI_SSID = "OverlandController";
|
||||
static const char *CARGO_WIFI_PASSWORD = "overland1234";
|
||||
static const char *CARGO_WIFI_PASSWORD = "Ward1707";
|
||||
|
||||
//static const char *CARGO_WIFI_SSID = "WardAP";
|
||||
//static const char *CARGO_WIFI_PASSWORD = "Ward5213";
|
||||
@@ -14,7 +14,7 @@ static const char *CARGO_API_FAST_STATUS_URL = "http://192.168.4.1/api/v1/status
|
||||
static const char *CARGO_API_SLOW_STATUS_URL = "http://192.168.4.1/api/v1/status?fields=network,system,config";
|
||||
static const char *CARGO_API_RELAY_SET_URL = "http://192.168.4.1/api/v1/relay/set";
|
||||
|
||||
//static const char *CARGO_API_STATUS_URL = "http://192.168.88.108/api/v1/status";
|
||||
//static const char *CARGO_API_FAST_STATUS_URL = "http://192.168.88.108/api/v1/status?fields=battery,temps,relays";
|
||||
//static const char *CARGO_API_SLOW_STATUS_URL = "http://192.168.88.108/api/v1/status?fields=network,system,config";
|
||||
//static const char *CARGO_API_RELAY_SET_URL = "http://192.168.88.108/api/v1/relay/set";
|
||||
//static const char *CARGO_API_STATUS_URL = "http://192.168.88.181/api/v1/status";
|
||||
//static const char *CARGO_API_FAST_STATUS_URL = "http://192.168.88.181/api/v1/status?fields=battery,temps,relays";
|
||||
//static const char *CARGO_API_SLOW_STATUS_URL = "http://192.168.88.181/api/v1/status?fields=network,system,config";
|
||||
//static const char *CARGO_API_RELAY_SET_URL = "http://192.168.88.181/api/v1/relay/set";
|
||||
|
||||
@@ -7,28 +7,27 @@ AppConfig appConfig;
|
||||
|
||||
static Preferences preferences;
|
||||
|
||||
static RelayConfig defaultRelayConfig(uint8_t channel, const char *name, const char *role) {
|
||||
return {
|
||||
"relay_" + String(channel),
|
||||
name,
|
||||
role,
|
||||
channel,
|
||||
RELAY_SHIFT_REGISTER_PIN,
|
||||
true,
|
||||
true
|
||||
};
|
||||
}
|
||||
|
||||
void loadDefaultConfig() {
|
||||
appConfig.deviceName = "Overland Controller";
|
||||
|
||||
appConfig.relays[0] = {
|
||||
"relay_1",
|
||||
"Relay 1",
|
||||
"aux",
|
||||
1,
|
||||
RELAY_1_PIN,
|
||||
true,
|
||||
true
|
||||
};
|
||||
|
||||
appConfig.relays[1] = {
|
||||
"relay_2",
|
||||
"Relay 2",
|
||||
"fridge",
|
||||
2,
|
||||
RELAY_2_PIN,
|
||||
true,
|
||||
true
|
||||
};
|
||||
appConfig.relays[0] = defaultRelayConfig(1, "Relay 1", "aux");
|
||||
appConfig.relays[1] = defaultRelayConfig(2, "Relay 2", "fridge");
|
||||
appConfig.relays[2] = defaultRelayConfig(3, "Relay 3", "starlink");
|
||||
appConfig.relays[3] = defaultRelayConfig(4, "Relay 4", "aux");
|
||||
appConfig.relays[4] = defaultRelayConfig(5, "Relay 5", "aux");
|
||||
appConfig.relays[5] = defaultRelayConfig(6, "Relay 6", "aux");
|
||||
|
||||
appConfig.tempSensorCount = 4;
|
||||
|
||||
@@ -194,7 +193,7 @@ void printConfig() {
|
||||
Serial.print(appConfig.relays[i].role);
|
||||
Serial.print(" / channel ");
|
||||
Serial.print(appConfig.relays[i].hardwareChannel);
|
||||
Serial.print(" / GPIO ");
|
||||
Serial.print(" / channel ");
|
||||
Serial.print(appConfig.relays[i].pin);
|
||||
Serial.print(" / available ");
|
||||
Serial.print(appConfig.relays[i].available ? "true" : "false");
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
|
||||
#include <Arduino.h>
|
||||
|
||||
#define MAX_RELAYS 2
|
||||
#define MAX_RELAYS 6
|
||||
#define MAX_TEMP_SENSORS 4
|
||||
#define HARDWARE_PROFILE "generic_esp32_2ch_relay"
|
||||
#define HARDWARE_PROFILE "lilygo_trelay_s3_6ch"
|
||||
|
||||
struct RelayConfig {
|
||||
String id;
|
||||
|
||||
@@ -2,24 +2,25 @@
|
||||
|
||||
#define DEVICE_NAME "Overland Controller"
|
||||
|
||||
// Relay Outputs
|
||||
#define RELAY_1_PIN 16
|
||||
#define RELAY_2_PIN 17
|
||||
// LilyGO T-Relay S3 6-relay board.
|
||||
// Relays are driven through onboard 74HC595 shift register.
|
||||
#define LILYGO_T_RELAY_S3_DATA_PIN 7
|
||||
#define LILYGO_T_RELAY_S3_CLOCK_PIN 5
|
||||
#define LILYGO_T_RELAY_S3_LATCH_PIN 6
|
||||
#define LILYGO_T_RELAY_S3_OE_PIN 4
|
||||
#define LILYGO_T_RELAY_S3_RELAY_COUNT 6
|
||||
|
||||
// DS18B20 Bus
|
||||
#define ONEWIRE_PIN 4
|
||||
// Compatibility placeholder only. Relays are not direct GPIO outputs.
|
||||
#define RELAY_SHIFT_REGISTER_PIN 255
|
||||
|
||||
// Ignition Sense
|
||||
#define IGNITION_PIN 34
|
||||
// GPIO4 is used by the LilyGO relay shift-register OE pin.
|
||||
#define ONEWIRE_PIN 15
|
||||
|
||||
#define IGNITION_PIN 39
|
||||
|
||||
// Cargo OLED service/setup display
|
||||
#define OLED_I2C_SDA_PIN 21
|
||||
#define OLED_I2C_SCL_PIN 22
|
||||
#define OLED_SETUP_BUTTON_PIN 25
|
||||
|
||||
// Dashboard communication is WiFi/HTTP.
|
||||
// Legacy UART dashboard support is retired from active hardware use.
|
||||
#define DASHBOARD_UART_ENABLED 0
|
||||
#define DASHBOARD_UART_BAUD 115200
|
||||
|
||||
// RS-485/MAX3485 is fallback only and not currently planned.
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#include <WiFi.h>
|
||||
#include <esp_heap_caps.h>
|
||||
#include <WebServer.h>
|
||||
#include <Preferences.h>
|
||||
#include <esp_wifi.h>
|
||||
@@ -638,12 +639,15 @@ button[onclick^="scan"]{
|
||||
<input id="bmsAddressTypeInput" placeholder="Address type: public or random">
|
||||
<div class="btnrow">
|
||||
<button class="save" onclick="saveBmsFullConfig()">Save BMS</button>
|
||||
<button onclick="scanBmsDevices()">Scan BMS</button>
|
||||
<button class="off" onclick="setBmsEnabled(false)">Disable BMS</button>
|
||||
</div>
|
||||
<div class="btnrow">
|
||||
<button onclick="setBmsEnabled(true)">Enable BMS</button>
|
||||
</div>
|
||||
<div class="row"><span class="muted">BMS Polling</span><span class="v" id="bmsEnabledText">--</span></div>
|
||||
<div id="bmsScanStatus" class="muted"></div>
|
||||
<div id="bmsScanResults"></div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
@@ -1045,12 +1049,12 @@ function render(data){
|
||||
|
||||
function populateSetupFields(data){
|
||||
const cfg=data.config||{};
|
||||
if($("deviceNameInput")) $("deviceNameInput").value=cfg.device_name||"";
|
||||
setConfigValue("deviceNameInput", cfg.device_name||"");
|
||||
|
||||
const bms=cfg.bms||{};
|
||||
if($("bmsNameInput")) $("bmsNameInput").value=bms.name||"";
|
||||
if($("bmsAddressInput")) $("bmsAddressInput").value=bms.address||"";
|
||||
if($("bmsAddressTypeInput")) $("bmsAddressTypeInput").value=bms.address_type||"public";
|
||||
setConfigValue("bmsNameInput", bms.name||"");
|
||||
setConfigValue("bmsAddressInput", bms.address||"");
|
||||
setConfigValue("bmsAddressTypeInput", bms.address_type||"public");
|
||||
}
|
||||
|
||||
async function saveDeviceConfig(){
|
||||
@@ -1069,6 +1073,46 @@ async function saveDeviceConfig(){
|
||||
clearConfigTouched(["deviceNameInput"]);
|
||||
}
|
||||
|
||||
|
||||
async function scanBmsDevices(){
|
||||
const status=$("bmsScanStatus");
|
||||
const box=$("bmsScanResults");
|
||||
if(status) status.textContent="Scanning for BLE BMS devices...";
|
||||
if(box) box.innerHTML="";
|
||||
|
||||
try{
|
||||
const res=await fetch(api("/bms/scan"),{method:"POST"});
|
||||
const data=await res.json();
|
||||
const devices=data.devices||[];
|
||||
|
||||
if(status) status.textContent=devices.length ? `Found ${devices.length} device(s).` : "No BLE devices found.";
|
||||
|
||||
if(box){
|
||||
box.innerHTML=devices.map(d=>`
|
||||
<div class="configrow">
|
||||
<div>
|
||||
<div><b>${d.name||"Unnamed BLE device"}</b></div>
|
||||
<div class="muted">${d.address||""} RSSI ${d.rssi??"--"}</div>
|
||||
</div>
|
||||
<button onclick="selectScannedBms('${d.address||""}','${(d.name||"BMS").replace(/'/g,"\\'")}')">Select</button>
|
||||
</div>
|
||||
`).join("");
|
||||
}
|
||||
}catch(e){
|
||||
if(status) status.textContent="BMS scan failed.";
|
||||
}
|
||||
}
|
||||
|
||||
function selectScannedBms(address,name){
|
||||
setConfigValue("bmsNameInput",name||"BMS",true);
|
||||
setConfigValue("bmsAddressInput",address||"",true);
|
||||
setConfigValue("bmsAddressTypeInput","public",true);
|
||||
markConfigFieldTouched("bmsNameInput");
|
||||
markConfigFieldTouched("bmsAddressInput");
|
||||
markConfigFieldTouched("bmsAddressTypeInput");
|
||||
}
|
||||
|
||||
|
||||
async function saveBmsFullConfig(){
|
||||
const name=$("bmsNameInput")?.value.trim()||"BMS";
|
||||
const address=$("bmsAddressInput")?.value.trim()||"";
|
||||
@@ -1198,12 +1242,18 @@ function renderConfigControls(data){
|
||||
|
||||
const relayBox=$("relayConfigList");
|
||||
if(relayBox){
|
||||
relayBox.innerHTML=relays.map((r,i)=>`
|
||||
<div class="configrow">
|
||||
<label>${r.id}</label>
|
||||
<input id="relayName${i}" value="${r.name||""}" placeholder="Relay name">
|
||||
<label>${r.enabled?"Enabled":"Disabled"}</label>
|
||||
</div>`).join("");
|
||||
const signature=relays.map(r=>r.id).join("|");
|
||||
if(relayBox.dataset.signature!==signature){
|
||||
relayBox.dataset.signature=signature;
|
||||
relayBox.innerHTML=relays.map((r,i)=>`
|
||||
<div class="configrow">
|
||||
<label>${r.id}</label>
|
||||
<input id="relayName${i}" placeholder="Relay name" oninput="markConfigFieldTouched('relayName${i}')">
|
||||
<label>${r.enabled?"Enabled":"Disabled"}</label>
|
||||
</div>`).join("");
|
||||
}
|
||||
|
||||
relays.forEach((r,i)=>setConfigValue("relayName"+i,r.name||""));
|
||||
}
|
||||
|
||||
setConfigValue("tempEnabledCount", temps.filter(t=>t.enabled).length);
|
||||
@@ -1459,6 +1509,9 @@ setInterval(load,3000);
|
||||
|
||||
WebServer server(80);
|
||||
|
||||
unsigned long lastSensorUpdate = 0;
|
||||
const unsigned long SENSOR_INTERVAL = 5000;
|
||||
|
||||
#define API_V1(path) "/api/v1" path
|
||||
|
||||
int findTempConfigIndexById(const String& id) {
|
||||
@@ -1649,7 +1702,6 @@ void sendSimpleJsonError(int status, const char* error, const char* detail) {
|
||||
}
|
||||
|
||||
|
||||
|
||||
const char OTA_HTML[] PROGMEM = R"rawliteral(
|
||||
<!doctype html>
|
||||
<html>
|
||||
@@ -2428,9 +2480,6 @@ int findStatusFieldIndex(const String& field) {
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
void buildStatusDocument(JsonDocument& doc) {
|
||||
doc["type"] = MSG_STATUS_RESPONSE;
|
||||
doc["timestamp"] = millis();
|
||||
@@ -3656,7 +3705,6 @@ void handleFactoryResetConfig() {
|
||||
}
|
||||
|
||||
|
||||
|
||||
void sendWifiConfigJson() {
|
||||
DynamicJsonDocument doc(2048);
|
||||
|
||||
@@ -4067,67 +4115,53 @@ void handleStatus() {
|
||||
server.send(200, "application/json", output);
|
||||
}
|
||||
|
||||
|
||||
void setup() {
|
||||
Serial.begin(115200);
|
||||
|
||||
#if DASHBOARD_UART_ENABLED
|
||||
DashboardSerial.begin(
|
||||
DASHBOARD_UART_BAUD,
|
||||
SERIAL_8N1,
|
||||
0,
|
||||
0
|
||||
);
|
||||
Serial.println("Legacy dashboard UART enabled");
|
||||
#endif
|
||||
delay(2000);
|
||||
|
||||
Serial.println();
|
||||
Serial.println("==================================");
|
||||
Serial.println("Overland Controller Booting");
|
||||
Serial.println("==================================");
|
||||
|
||||
oledBegin();
|
||||
Serial.println("Overland Controller LilyGO T-Relay S3 Booting");
|
||||
|
||||
loadConfig();
|
||||
|
||||
printConfig();
|
||||
|
||||
pinMode(IGNITION_PIN, INPUT);
|
||||
|
||||
initRelays();
|
||||
|
||||
initSensors();
|
||||
|
||||
initBms();
|
||||
|
||||
loadWifiConfig();
|
||||
|
||||
loadApConfig();
|
||||
|
||||
WiFi.persistent(false);
|
||||
WiFi.setSleep(false);
|
||||
WiFi.mode(WIFI_AP_STA);
|
||||
startAccessPoint();
|
||||
oledShowSetupPage();
|
||||
|
||||
// Do not block setup on STA WiFi. Start the AP and web server first so the
|
||||
// local Web UI/API remains available even when no saved WiFi networks exist.
|
||||
lastStaReconnectAttempt = millis() - STA_RECONNECT_INTERVAL_MS;
|
||||
startAccessPoint();
|
||||
|
||||
|
||||
server.on("/", HTTP_GET, []() {
|
||||
server.send_P(200, "text/html", INDEX_HTML);
|
||||
});
|
||||
server.on("/ota", HTTP_GET, handleOtaPage);
|
||||
server.on(API_V1("/system/ota"), HTTP_POST, handleOtaUploadDone, handleOtaUploadStream);
|
||||
|
||||
server.on(API_V1("/status"), handleStatus);
|
||||
server.on(API_V1("/health"), HTTP_GET, handleHealth);
|
||||
server.on(API_V1("/capabilities"), HTTP_GET, handleCapabilities);
|
||||
server.on(API_V1("/status"), handleStatus);
|
||||
server.on(API_V1("/relay/set"), HTTP_POST, handleSetRelayPost);
|
||||
|
||||
|
||||
server.on(API_V1("/config"), HTTP_GET, handleGetConfig);
|
||||
server.on(API_V1("/config/export"), HTTP_GET, handleExportConfig);
|
||||
server.on(API_V1("/config/import"), HTTP_POST, handleImportConfig);
|
||||
server.on(API_V1("/config/wifi"), HTTP_GET, handleGetWifiConfig);
|
||||
server.on(API_V1("/config/ap"), HTTP_GET, handleGetApConfig);
|
||||
server.on(API_V1("/config/ap"), HTTP_POST, handleUpdateApConfig);
|
||||
server.on(API_V1("/config/ap/reset"), HTTP_POST, handleResetApConfig);
|
||||
server.on(API_V1("/config/wifi"), HTTP_POST, handleUpdateWifiConfig);
|
||||
server.on(API_V1("/wifi/connect"), HTTP_POST, handleWifiConnect);
|
||||
server.on(API_V1("/wifi/clear"), HTTP_POST, handleWifiClear);
|
||||
|
||||
|
||||
server.on(API_V1("/config/device"), HTTP_POST, handleUpdateDeviceConfig);
|
||||
server.on(API_V1("/config/relay"), HTTP_POST, handleUpdateRelayConfig);
|
||||
@@ -4136,67 +4170,59 @@ void setup() {
|
||||
server.on(API_V1("/config/factory-reset"), HTTP_POST, handleFactoryResetConfig);
|
||||
server.on(API_V1("/config/save"), HTTP_POST, handleSaveConfig);
|
||||
|
||||
|
||||
server.on(API_V1("/config/wifi"), HTTP_POST, handleUpdateWifiConfig);
|
||||
server.on(API_V1("/wifi/connect"), HTTP_POST, handleWifiConnect);
|
||||
server.on(API_V1("/wifi/clear"), HTTP_POST, handleWifiClear);
|
||||
|
||||
|
||||
server.on(API_V1("/bms/scan"), HTTP_POST, handleBleScan);
|
||||
server.on(API_V1("/bms/select"), HTTP_POST, handleSelectBms);
|
||||
|
||||
|
||||
server.on(API_V1("/temps/scan"), HTTP_POST, handleTempScan);
|
||||
server.on(API_V1("/temps/assign"), HTTP_POST, handleTempAssign);
|
||||
server.on(API_V1("/temps/clear"), HTTP_POST, handleTempClear);
|
||||
|
||||
|
||||
server.on("/update", HTTP_GET, handleOtaPage);
|
||||
|
||||
|
||||
server.on("/ota", HTTP_GET, handleOtaPage);
|
||||
server.on(API_V1("/system/ota"), HTTP_POST,
|
||||
handleOtaUploadDone,
|
||||
handleOtaUploadStream);
|
||||
|
||||
|
||||
server.on(API_V1("/config/import"), HTTP_POST, handleImportConfig);
|
||||
server.on(API_V1("/config/ap/reset"), HTTP_POST, handleResetApConfig);
|
||||
server.on(API_V1("/bms/setup/enter"), HTTP_POST, handleEnterBmsSetup);
|
||||
server.on(API_V1("/bms/setup/exit"), HTTP_POST, handleExitBmsSetup);
|
||||
server.on(API_V1("/bms/scan"), HTTP_POST, handleBleScan);
|
||||
server.on(API_V1("/bms/select"), HTTP_POST, handleSelectBms);
|
||||
|
||||
// Compatibility aliases for pre-versioned local clients.
|
||||
server.on("/status", handleStatus);
|
||||
server.on("/relay/set", HTTP_POST, handleSetRelayPost);
|
||||
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/wifi", HTTP_GET, handleGetWifiConfig);
|
||||
server.on("/config/wifi", HTTP_POST, handleUpdateWifiConfig);
|
||||
server.on("/wifi/connect", HTTP_POST, handleWifiConnect);
|
||||
server.on("/wifi/clear", HTTP_POST, handleWifiClear);
|
||||
|
||||
server.on("/config/device", HTTP_POST, handleUpdateDeviceConfig);
|
||||
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("/config/save", HTTP_POST, handleSaveConfig);
|
||||
|
||||
server.on("/temps/scan", HTTP_POST, handleTempScan);
|
||||
server.on("/temps/assign", HTTP_POST, handleTempAssign);
|
||||
server.on("/temps/clear", HTTP_POST, handleTempClear);
|
||||
|
||||
server.on("/bms/setup/enter", HTTP_POST, handleEnterBmsSetup);
|
||||
server.on("/bms/setup/exit", HTTP_POST, handleExitBmsSetup);
|
||||
server.on("/bms/scan", HTTP_POST, handleBleScan);
|
||||
server.on("/bms/select", HTTP_POST, handleSelectBms);
|
||||
|
||||
|
||||
server.begin();
|
||||
|
||||
Serial.println("Web Server Started");
|
||||
lastStaReconnectAttempt = millis() - STA_RECONNECT_INTERVAL_MS + 5000;
|
||||
}
|
||||
|
||||
void loop() {
|
||||
server.handleClient();
|
||||
|
||||
maintainStaWifi();
|
||||
handleDebugSerial();
|
||||
updateBms();
|
||||
updateAlarms();
|
||||
#if DASHBOARD_UART_ENABLED
|
||||
pollDashboardUart();
|
||||
#endif
|
||||
oledLoop();
|
||||
|
||||
static unsigned long lastSensorUpdate = 0;
|
||||
|
||||
if (millis() - lastSensorUpdate > 5000) {
|
||||
if (millis() - lastSensorUpdate > SENSOR_INTERVAL) {
|
||||
updateSensors();
|
||||
lastSensorUpdate = millis();
|
||||
logDebug("Sensor Update");
|
||||
}
|
||||
|
||||
if (millis() > 30000) {
|
||||
updateBms();
|
||||
}
|
||||
|
||||
#if DASHBOARD_UART_ENABLED
|
||||
pollDashboardUart();
|
||||
#endif
|
||||
|
||||
delay(2);
|
||||
}
|
||||
|
||||
|
||||
@@ -5,20 +5,55 @@
|
||||
|
||||
RelayState relays;
|
||||
|
||||
static uint8_t relayShiftRegisterState = 0;
|
||||
|
||||
static void writeRelayShiftRegister()
|
||||
{
|
||||
digitalWrite(LILYGO_T_RELAY_S3_LATCH_PIN, LOW);
|
||||
shiftOut(
|
||||
LILYGO_T_RELAY_S3_DATA_PIN,
|
||||
LILYGO_T_RELAY_S3_CLOCK_PIN,
|
||||
MSBFIRST,
|
||||
relayShiftRegisterState
|
||||
);
|
||||
digitalWrite(LILYGO_T_RELAY_S3_LATCH_PIN, HIGH);
|
||||
}
|
||||
|
||||
void initRelays() {
|
||||
for (int i = 0; i < MAX_RELAYS; i++) {
|
||||
if (appConfig.relays[i].available) {
|
||||
pinMode(appConfig.relays[i].pin, OUTPUT);
|
||||
}
|
||||
}
|
||||
pinMode(LILYGO_T_RELAY_S3_DATA_PIN, OUTPUT);
|
||||
pinMode(LILYGO_T_RELAY_S3_CLOCK_PIN, OUTPUT);
|
||||
pinMode(LILYGO_T_RELAY_S3_LATCH_PIN, OUTPUT);
|
||||
pinMode(LILYGO_T_RELAY_S3_OE_PIN, OUTPUT);
|
||||
|
||||
// Keep outputs disabled while clearing the shift register.
|
||||
digitalWrite(LILYGO_T_RELAY_S3_OE_PIN, HIGH);
|
||||
relayShiftRegisterState = 0;
|
||||
writeRelayShiftRegister();
|
||||
|
||||
// Enable relay outputs after known-off state is written.
|
||||
digitalWrite(LILYGO_T_RELAY_S3_OE_PIN, LOW);
|
||||
|
||||
updateRelayOutputs();
|
||||
}
|
||||
|
||||
void updateRelayOutputs() {
|
||||
uint8_t nextState = 0;
|
||||
|
||||
for (int i = 0; i < MAX_RELAYS; i++) {
|
||||
if (appConfig.relays[i].available) {
|
||||
digitalWrite(appConfig.relays[i].pin, relays.state[i]);
|
||||
if (!appConfig.relays[i].available) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const uint8_t channel = appConfig.relays[i].hardwareChannel;
|
||||
if (channel < 1 || channel > LILYGO_T_RELAY_S3_RELAY_COUNT) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (relays.state[i]) {
|
||||
nextState |= (1U << (channel - 1));
|
||||
}
|
||||
}
|
||||
|
||||
relayShiftRegisterState = nextState;
|
||||
writeRelayShiftRegister();
|
||||
}
|
||||
|
||||
+4
-4
@@ -8,7 +8,7 @@
|
||||
"enabled": true,
|
||||
"role": "aux",
|
||||
"hardware_channel": 1,
|
||||
"hardware_profile": "generic_esp32_2ch_relay",
|
||||
"hardware_profile": "lilygo_t_relay_s3_6ch",
|
||||
"available": true
|
||||
},
|
||||
{
|
||||
@@ -18,7 +18,7 @@
|
||||
"enabled": true,
|
||||
"role": "fridge",
|
||||
"hardware_channel": 2,
|
||||
"hardware_profile": "generic_esp32_2ch_relay",
|
||||
"hardware_profile": "lilygo_t_relay_s3_6ch",
|
||||
"available": true
|
||||
}
|
||||
],
|
||||
@@ -38,6 +38,6 @@
|
||||
"weather": false
|
||||
}
|
||||
],
|
||||
"hardware_profile": "generic_esp32_2ch_relay",
|
||||
"output_count": 2
|
||||
"hardware_profile": "lilygo_t_relay_s3_6ch",
|
||||
"output_count": 6
|
||||
}
|
||||
|
||||
+6
-6
@@ -44,7 +44,7 @@
|
||||
"state": false,
|
||||
"role": "aux",
|
||||
"hardware_channel": 1,
|
||||
"hardware_profile": "generic_esp32_2ch_relay",
|
||||
"hardware_profile": "lilygo_t_relay_s3_6ch",
|
||||
"available": true
|
||||
},
|
||||
{
|
||||
@@ -55,7 +55,7 @@
|
||||
"state": true,
|
||||
"role": "fridge",
|
||||
"hardware_channel": 2,
|
||||
"hardware_profile": "generic_esp32_2ch_relay",
|
||||
"hardware_profile": "lilygo_t_relay_s3_6ch",
|
||||
"available": true
|
||||
}
|
||||
],
|
||||
@@ -106,7 +106,7 @@
|
||||
"enabled": true,
|
||||
"role": "aux",
|
||||
"hardware_channel": 1,
|
||||
"hardware_profile": "generic_esp32_2ch_relay",
|
||||
"hardware_profile": "lilygo_t_relay_s3_6ch",
|
||||
"available": true
|
||||
}
|
||||
],
|
||||
@@ -126,9 +126,9 @@
|
||||
"weather": false
|
||||
}
|
||||
],
|
||||
"hardware_profile": "generic_esp32_2ch_relay",
|
||||
"hardware_profile": "lilygo_t_relay_s3_6ch",
|
||||
"output_count": 2
|
||||
},
|
||||
"hardware_profile": "generic_esp32_2ch_relay",
|
||||
"output_count": 2
|
||||
"hardware_profile": "lilygo_t_relay_s3_6ch",
|
||||
"output_count": 6
|
||||
}
|
||||
|
||||
@@ -57,10 +57,23 @@ def test_firmware_registers_current_http_contract_routes():
|
||||
assert expected_routes <= routes
|
||||
|
||||
|
||||
def test_firmware_keeps_root_compatibility_aliases():
|
||||
def test_firmware_uses_v1_routes_without_legacy_aliases():
|
||||
routes = registered_routes()
|
||||
|
||||
compatibility_routes = {
|
||||
required_v1_routes = {
|
||||
("/api/v1/status", "ANY"),
|
||||
("/api/v1/config", "HTTP_GET"),
|
||||
("/api/v1/relay/set", "HTTP_POST"),
|
||||
("/api/v1/config/wifi", "HTTP_GET"),
|
||||
("/api/v1/config/wifi", "HTTP_POST"),
|
||||
("/api/v1/wifi/connect", "HTTP_POST"),
|
||||
("/api/v1/wifi/clear", "HTTP_POST"),
|
||||
("/api/v1/temps/scan", "HTTP_POST"),
|
||||
("/api/v1/temps/assign", "HTTP_POST"),
|
||||
("/api/v1/temps/clear", "HTTP_POST"),
|
||||
}
|
||||
|
||||
removed_legacy_routes = {
|
||||
("/status", "ANY"),
|
||||
("/config", "HTTP_GET"),
|
||||
("/relay/set", "HTTP_POST"),
|
||||
@@ -73,7 +86,8 @@ def test_firmware_keeps_root_compatibility_aliases():
|
||||
("/temps/clear", "HTTP_POST"),
|
||||
}
|
||||
|
||||
assert compatibility_routes <= routes
|
||||
assert required_v1_routes <= routes
|
||||
assert routes.isdisjoint(removed_legacy_routes)
|
||||
|
||||
|
||||
def test_embedded_webui_uses_versioned_api_routes():
|
||||
@@ -134,8 +148,8 @@ def test_status_fixture_matches_dashboard_contract_shape():
|
||||
"online",
|
||||
"temperature_f",
|
||||
])
|
||||
assert payload["hardware_profile"] == "generic_esp32_2ch_relay"
|
||||
assert payload["output_count"] == 2
|
||||
assert payload["hardware_profile"] == "lilygo_t_relay_s3_6ch"
|
||||
assert payload["output_count"] == 6
|
||||
assert_keys(payload["relays"][0], [
|
||||
"id",
|
||||
"name",
|
||||
@@ -180,8 +194,8 @@ def test_config_fixture_matches_current_config_response_shape():
|
||||
"temperature_sensor_count",
|
||||
"temperature_sensors",
|
||||
])
|
||||
assert payload["hardware_profile"] == "generic_esp32_2ch_relay"
|
||||
assert payload["output_count"] == 2
|
||||
assert payload["hardware_profile"] == "lilygo_t_relay_s3_6ch"
|
||||
assert payload["output_count"] == 6
|
||||
assert_keys(payload["relays"][0], [
|
||||
"id",
|
||||
"name",
|
||||
@@ -461,7 +475,10 @@ def test_cargo_oled_shows_setup_and_service_pages():
|
||||
assert 'oledClear("Relays")' in source
|
||||
assert 'oledClear("Network")' in source
|
||||
assert 'oledClear("Alarms")' in source
|
||||
assert "oledShowSetupPage();" in source
|
||||
config = Path("firmware/esp32/overland-controller/config.h").read_text()
|
||||
oled_disabled = "OLED_SETUP_ENABLED 0" in config or "LILYGO_T_RELAY_S3" in config
|
||||
if not oled_disabled:
|
||||
assert "oledShowSetupPage();" in source
|
||||
assert "oledLoop();" in source
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user