Compare commits
59
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 | ||
|
|
24e86e2eb1 | ||
|
|
c96ef5e7cc | ||
|
|
db666190cf | ||
|
|
b11151c832 | ||
|
|
a18ed0aeca | ||
|
|
e0b56f42e9 | ||
|
|
fec60b057f | ||
|
|
4cba41c831 | ||
|
|
59fb7b5c29 | ||
|
|
54c25f28bf | ||
|
|
49382358dc | ||
|
|
3750659a3d | ||
|
|
9d75d2c39c | ||
|
|
97e49a5d98 | ||
|
|
ef6b32e133 | ||
|
|
73a28beacc | ||
|
|
2aac72616c | ||
|
|
f4c6623334 | ||
|
|
5fbc33001a |
@@ -2,6 +2,18 @@
|
|||||||
|
|
||||||
## Unreleased
|
## Unreleased
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- Cargo ESP local OTA firmware upload page at `/ota`.
|
||||||
|
- Cargo ESP OTA API endpoint at `POST /api/v1/system/ota`.
|
||||||
|
|
||||||
|
## Unreleased
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- Cargo ESP starts AP and Web UI/API before attempting STA WiFi reconnects.
|
||||||
|
- STA reconnect attempts now service the web server during short connection windows so AP mode remains usable when no saved WiFi is available.
|
||||||
|
|
||||||
|
## Unreleased
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
- Reworked ESP32-S3 dashboard page navigation to use screen-wide LVGL swipe gestures.
|
- Reworked ESP32-S3 dashboard page navigation to use screen-wide LVGL swipe gestures.
|
||||||
- Removed failed temporary double-tap and transparent hitbox navigation experiments.
|
- Removed failed temporary double-tap and transparent hitbox navigation experiments.
|
||||||
|
|||||||
@@ -239,3 +239,30 @@ Important guardrails:
|
|||||||
## M9N GPS Module
|
## 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.
|
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:
|
Example 2-channel profile:
|
||||||
|
|
||||||
{
|
{
|
||||||
"hardware_profile": "generic_esp32_2ch_relay",
|
"hardware_profile": "lilygo_t_relay_s3_6ch",
|
||||||
"outputs": [
|
"outputs": [
|
||||||
{"id": 1, "name": "Fridge", "role": "fridge", "hardware_channel": 1},
|
{"id": 1, "name": "Fridge", "role": "fridge", "hardware_channel": 1},
|
||||||
{"id": 2, "name": "Starlink", "role": "starlink", "hardware_channel": 2}
|
{"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:
|
The current active hardware profile remains:
|
||||||
|
|
||||||
generic_esp32_2ch_relay
|
lilygo_t_relay_s3_6ch
|
||||||
|
|
||||||
The API includes:
|
The API includes:
|
||||||
|
|
||||||
|
|||||||
@@ -311,3 +311,11 @@ The diagnostics overlay shows dashboard version, uptime, heap, WiFi RSSI/IP, Car
|
|||||||
- Swipe right from the main dashboard to open Battery Detail.
|
- Swipe right from the main dashboard to open Battery Detail.
|
||||||
- Swipe left from Battery Detail to return to the main dashboard.
|
- Swipe left from Battery Detail to return to the main dashboard.
|
||||||
- Diagnostics remains hidden behind the multi-tap system-card gesture.
|
- Diagnostics remains hidden behind the multi-tap system-card gesture.
|
||||||
|
|
||||||
|
|
||||||
|
## OBD-II Coolant Polling
|
||||||
|
- Dashboard ESP32-S3 Touch LCD 5B uses onboard CAN/TWAI via GPIO15 CANTX and GPIO16 CANRX.
|
||||||
|
- First implemented PID is Mode 01 PID 05 coolant temperature.
|
||||||
|
- Sends standard 11-bit functional request ID `0x7DF` and accepts ECU responses from `0x7E8` through `0x7EF`.
|
||||||
|
- Diagnostics page shows TWAI state, coolant value, TX/RX counts, and last OBD status.
|
||||||
|
- Overview coolant gauge shows `--°` until a valid OBD response is received.
|
||||||
|
|||||||
@@ -270,7 +270,7 @@ The Cargo ESP API now represents relays/outputs as a hardware-profile-aware list
|
|||||||
|
|
||||||
Current active hardware profile:
|
Current active hardware profile:
|
||||||
|
|
||||||
generic_esp32_2ch_relay
|
lilygo_t_relay_s3_6ch
|
||||||
|
|
||||||
Future planned profile:
|
Future planned profile:
|
||||||
|
|
||||||
|
|||||||
@@ -192,3 +192,34 @@ Next Dashboard Milestones:
|
|||||||
## M9N GPS Module
|
## 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.
|
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.
|
||||||
|
|
||||||
|
## Planned: WebSocket live dashboard updates
|
||||||
|
|
||||||
|
Add a WebSocket push channel alongside the existing `/api/v1` REST API. The REST API remains the stable snapshot/debug/control contract, while WebSockets provide lower-latency live status updates for the dashboard.
|
||||||
|
|
||||||
|
Planned behavior:
|
||||||
|
|
||||||
|
- Cargo ESP continues to expose `/api/v1/status` and existing `/api/v1/...` endpoints.
|
||||||
|
- Cargo ESP adds a WebSocket endpoint, likely `/ws`.
|
||||||
|
- Dashboard ESP connects once to `/ws` and receives pushed status updates.
|
||||||
|
- Relay commands may continue using REST initially, with pushed status updates confirming state changes.
|
||||||
|
- Future high-rate data such as CAN/OBD, GPS, inclinometer, and temperature updates can use the WebSocket path.
|
||||||
|
- If WebSocket disconnects, dashboard should fall back to the existing REST polling behavior.
|
||||||
|
|
||||||
|
Rationale:
|
||||||
|
|
||||||
|
- Keeps the current API architecture intact and easy to debug.
|
||||||
|
- Avoids ESP-NOW channel-management complexity for now.
|
||||||
|
- 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.
|
||||||
|
|||||||
@@ -1,20 +1,20 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
static const char *DASHBOARD_VERSION = "0.1.118-temp-alert-local-threshold";
|
static const char *DASHBOARD_VERSION = "0.1.126-restore-boot-screen";
|
||||||
|
|
||||||
// Update these if your Cargo ESP AP credentials are different.
|
// Update these if your Cargo ESP AP credentials are different.
|
||||||
//static const char *CARGO_WIFI_SSID = "OverlandController";
|
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_SSID = "WardAP";
|
||||||
static const char *CARGO_WIFI_PASSWORD = "Ward5213";
|
//static const char *CARGO_WIFI_PASSWORD = "Ward5213";
|
||||||
|
|
||||||
//static const char *CARGO_API_STATUS_URL = "http://192.168.4.1/api/v1/status";
|
static const char *CARGO_API_STATUS_URL = "http://192.168.4.1/api/v1/status";
|
||||||
//static const char *CARGO_API_FAST_STATUS_URL = "http://192.168.4.1/api/v1/status?fields=battery,temps,relays";
|
static const char *CARGO_API_FAST_STATUS_URL = "http://192.168.4.1/api/v1/status?fields=battery,temps,relays";
|
||||||
//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_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_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_STATUS_URL = "http://192.168.88.181/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_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.108/api/v1/status?fields=network,system,config";
|
//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.108/api/v1/relay/set";
|
//static const char *CARGO_API_RELAY_SET_URL = "http://192.168.88.181/api/v1/relay/set";
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
#include <HTTPClient.h>
|
#include <HTTPClient.h>
|
||||||
#include <ArduinoJson.h>
|
#include <ArduinoJson.h>
|
||||||
#include <esp_display_panel.hpp>
|
#include <esp_display_panel.hpp>
|
||||||
|
#include <driver/twai.h>
|
||||||
|
|
||||||
#include <lvgl.h>
|
#include <lvgl.h>
|
||||||
#include "lvgl_v8_port.h"
|
#include "lvgl_v8_port.h"
|
||||||
@@ -15,6 +16,24 @@ using namespace esp_panel::drivers;
|
|||||||
using namespace esp_panel::board;
|
using namespace esp_panel::board;
|
||||||
|
|
||||||
static DashboardStatus status_model;
|
static DashboardStatus status_model;
|
||||||
|
static constexpr gpio_num_t OBD_CAN_TX_PIN = GPIO_NUM_15;
|
||||||
|
static constexpr gpio_num_t OBD_CAN_RX_PIN = GPIO_NUM_16;
|
||||||
|
static constexpr uint32_t OBD_REQUEST_ID = 0x7DF;
|
||||||
|
static constexpr uint32_t OBD_RESPONSE_ID_MIN = 0x7E8;
|
||||||
|
static constexpr uint32_t OBD_RESPONSE_ID_MAX = 0x7EF;
|
||||||
|
static constexpr uint8_t OBD_PID_COOLANT_TEMP = 0x05;
|
||||||
|
|
||||||
|
static bool obd_can_ready = false;
|
||||||
|
static bool obd_can_started = false;
|
||||||
|
static unsigned long last_obd_poll_ms = 0;
|
||||||
|
static unsigned long last_obd_rx_ms = 0;
|
||||||
|
static unsigned long last_obd_tx_ms = 0;
|
||||||
|
static uint32_t obd_tx_count = 0;
|
||||||
|
static uint32_t obd_rx_count = 0;
|
||||||
|
static int obd_coolant_temp_f = -999;
|
||||||
|
static int last_rendered_obd_coolant_temp_f = -1000;
|
||||||
|
static String obd_last_error = "not initialized";
|
||||||
|
|
||||||
|
|
||||||
static lv_obj_t *battery_card_obj = nullptr;
|
static lv_obj_t *battery_card_obj = nullptr;
|
||||||
static lv_obj_t *battery_soc_arc = nullptr;
|
static lv_obj_t *battery_soc_arc = nullptr;
|
||||||
@@ -369,6 +388,149 @@ static String format_diag_uptime(unsigned long seconds)
|
|||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static void update_vehicle_coolant_widget()
|
||||||
|
{
|
||||||
|
if (vehicle_coolant_value_label == nullptr || vehicle_coolant_meter == nullptr || vehicle_coolant_needle == nullptr) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (obd_coolant_temp_f == last_rendered_obd_coolant_temp_f) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
last_rendered_obd_coolant_temp_f = obd_coolant_temp_f;
|
||||||
|
|
||||||
|
lvgl_port_lock(-1);
|
||||||
|
|
||||||
|
if (obd_coolant_temp_f > -100) {
|
||||||
|
int bounded_temp = clamp_int(obd_coolant_temp_f, 100, 260);
|
||||||
|
lv_meter_set_indicator_value(vehicle_coolant_meter, vehicle_coolant_needle, bounded_temp);
|
||||||
|
lv_label_set_text(vehicle_coolant_value_label, (String(obd_coolant_temp_f) + "°").c_str());
|
||||||
|
} else {
|
||||||
|
lv_meter_set_indicator_value(vehicle_coolant_meter, vehicle_coolant_needle, 100);
|
||||||
|
lv_label_set_text(vehicle_coolant_value_label, "--°");
|
||||||
|
}
|
||||||
|
|
||||||
|
lvgl_port_unlock();
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool init_obd_can()
|
||||||
|
{
|
||||||
|
if (obd_can_ready && obd_can_started) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
twai_general_config_t general_config = TWAI_GENERAL_CONFIG_DEFAULT(OBD_CAN_TX_PIN, OBD_CAN_RX_PIN, TWAI_MODE_NORMAL);
|
||||||
|
general_config.tx_queue_len = 5;
|
||||||
|
general_config.rx_queue_len = 20;
|
||||||
|
|
||||||
|
twai_timing_config_t timing_config = TWAI_TIMING_CONFIG_500KBITS();
|
||||||
|
twai_filter_config_t filter_config = TWAI_FILTER_CONFIG_ACCEPT_ALL();
|
||||||
|
|
||||||
|
if (!obd_can_ready) {
|
||||||
|
esp_err_t install_result = twai_driver_install(&general_config, &timing_config, &filter_config);
|
||||||
|
if (install_result != ESP_OK && install_result != ESP_ERR_INVALID_STATE) {
|
||||||
|
obd_last_error = "driver install failed: " + String((int)install_result);
|
||||||
|
Serial.println("OBD CAN: " + obd_last_error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
obd_can_ready = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
esp_err_t start_result = twai_start();
|
||||||
|
if (start_result != ESP_OK && start_result != ESP_ERR_INVALID_STATE) {
|
||||||
|
obd_last_error = "start failed: " + String((int)start_result);
|
||||||
|
Serial.println("OBD CAN: " + obd_last_error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
obd_can_started = true;
|
||||||
|
obd_last_error = "started";
|
||||||
|
Serial.println("OBD CAN: TWAI started on TX GPIO15 / RX GPIO16 at 500 kbps");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void send_obd_coolant_request()
|
||||||
|
{
|
||||||
|
twai_message_t message = {};
|
||||||
|
message.identifier = OBD_REQUEST_ID;
|
||||||
|
message.extd = 0;
|
||||||
|
message.rtr = 0;
|
||||||
|
message.data_length_code = 8;
|
||||||
|
message.data[0] = 0x02;
|
||||||
|
message.data[1] = 0x01;
|
||||||
|
message.data[2] = OBD_PID_COOLANT_TEMP;
|
||||||
|
message.data[3] = 0x55;
|
||||||
|
message.data[4] = 0x55;
|
||||||
|
message.data[5] = 0x55;
|
||||||
|
message.data[6] = 0x55;
|
||||||
|
message.data[7] = 0x55;
|
||||||
|
|
||||||
|
esp_err_t result = twai_transmit(&message, 0);
|
||||||
|
if (result == ESP_OK) {
|
||||||
|
obd_tx_count++;
|
||||||
|
last_obd_tx_ms = millis();
|
||||||
|
obd_last_error = "coolant request sent";
|
||||||
|
} else {
|
||||||
|
obd_last_error = "tx failed: " + String((int)result);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void handle_obd_can_message(const twai_message_t &message)
|
||||||
|
{
|
||||||
|
if (message.extd || message.rtr) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (message.identifier < OBD_RESPONSE_ID_MIN || message.identifier > OBD_RESPONSE_ID_MAX) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (message.data_length_code < 4) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Single-frame OBD-II response:
|
||||||
|
// byte0 length, byte1 0x41 response mode, byte2 PID, byte3 value A.
|
||||||
|
if (message.data[1] == 0x41 && message.data[2] == OBD_PID_COOLANT_TEMP) {
|
||||||
|
int coolant_c = (int)message.data[3] - 40;
|
||||||
|
obd_coolant_temp_f = (int)roundf((coolant_c * 9.0f / 5.0f) + 32.0f);
|
||||||
|
obd_rx_count++;
|
||||||
|
last_obd_rx_ms = millis();
|
||||||
|
obd_last_error = "coolant response OK";
|
||||||
|
|
||||||
|
Serial.print("OBD CAN: coolant=");
|
||||||
|
Serial.print(obd_coolant_temp_f);
|
||||||
|
Serial.println(" F");
|
||||||
|
|
||||||
|
update_vehicle_coolant_widget();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void process_obd_can()
|
||||||
|
{
|
||||||
|
if (!init_obd_can()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (millis() - last_obd_poll_ms >= 1000) {
|
||||||
|
last_obd_poll_ms = millis();
|
||||||
|
send_obd_coolant_request();
|
||||||
|
}
|
||||||
|
|
||||||
|
twai_message_t message = {};
|
||||||
|
while (twai_receive(&message, 0) == ESP_OK) {
|
||||||
|
handle_obd_can_message(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (millis() - last_obd_rx_ms > 5000 && obd_coolant_temp_f > -100) {
|
||||||
|
obd_coolant_temp_f = -999;
|
||||||
|
update_vehicle_coolant_widget();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
static void update_diagnostics_overlay()
|
static void update_diagnostics_overlay()
|
||||||
{
|
{
|
||||||
if (diagnostics_overlay == nullptr || diagnostics_label == nullptr) {
|
if (diagnostics_overlay == nullptr || diagnostics_label == nullptr) {
|
||||||
@@ -442,6 +604,21 @@ static void update_diagnostics_overlay()
|
|||||||
text += String(status_model.cell_delta_mv);
|
text += String(status_model.cell_delta_mv);
|
||||||
text += " mV";
|
text += " mV";
|
||||||
|
|
||||||
|
text += "\n\nOBD-II / CAN\n";
|
||||||
|
text += " TWAI: ";
|
||||||
|
text += obd_can_started ? "started" : "offline";
|
||||||
|
text += "\n Pins: TX GPIO15 / RX GPIO16";
|
||||||
|
text += "\n Coolant: ";
|
||||||
|
text += obd_coolant_temp_f > -100 ? String(obd_coolant_temp_f) + "°F" : String("--");
|
||||||
|
text += "\n TX/RX: ";
|
||||||
|
text += String(obd_tx_count);
|
||||||
|
text += "/";
|
||||||
|
text += String(obd_rx_count);
|
||||||
|
text += "\n Last RX age: ";
|
||||||
|
text += last_obd_rx_ms > 0 ? String((millis() - last_obd_rx_ms) / 1000) + "s" : String("--");
|
||||||
|
text += "\n Last status: ";
|
||||||
|
text += obd_last_error;
|
||||||
|
|
||||||
text += "\n\nControls\n";
|
text += "\n\nControls\n";
|
||||||
text += " Relays visible: ";
|
text += " Relays visible: ";
|
||||||
text += String(relay_count);
|
text += String(relay_count);
|
||||||
@@ -1288,14 +1465,10 @@ static void update_temp_tile_alert_style(int index, bool alert)
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (last_temp_tile_alert[index] == alert) {
|
// Always apply the visual style. The cache can get out of sync when pages
|
||||||
return;
|
// are hidden/shown, so it must not prevent repainting the tile.
|
||||||
}
|
|
||||||
|
|
||||||
last_temp_tile_alert[index] = alert;
|
last_temp_tile_alert[index] = alert;
|
||||||
|
|
||||||
lvgl_port_lock(-1);
|
|
||||||
|
|
||||||
if (alert) {
|
if (alert) {
|
||||||
lv_obj_set_style_bg_color(temp_tile_objs[index], lv_color_hex(0x7A271A), 0);
|
lv_obj_set_style_bg_color(temp_tile_objs[index], lv_color_hex(0x7A271A), 0);
|
||||||
lv_obj_set_style_border_color(temp_tile_objs[index], lv_color_hex(0xF97066), 0);
|
lv_obj_set_style_border_color(temp_tile_objs[index], lv_color_hex(0xF97066), 0);
|
||||||
@@ -1305,11 +1478,8 @@ static void update_temp_tile_alert_style(int index, bool alert)
|
|||||||
lv_obj_set_style_border_color(temp_tile_objs[index], lv_color_hex(0x2F3A46), 0);
|
lv_obj_set_style_border_color(temp_tile_objs[index], lv_color_hex(0x2F3A46), 0);
|
||||||
lv_obj_set_style_border_width(temp_tile_objs[index], 2, 0);
|
lv_obj_set_style_border_width(temp_tile_objs[index], 2, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
lvgl_port_unlock();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
static void update_battery_card_alert_style(bool alert)
|
static void update_battery_card_alert_style(bool alert)
|
||||||
{
|
{
|
||||||
if (battery_card_obj == nullptr) {
|
if (battery_card_obj == nullptr) {
|
||||||
@@ -1465,14 +1635,6 @@ static void parse_status_json(const String &body)
|
|||||||
|
|
||||||
float temp_f = (float)(temp["temperature_f"] | 0.0);
|
float temp_f = (float)(temp["temperature_f"] | 0.0);
|
||||||
bool high_alert = temp["high_alert"] | false;
|
bool high_alert = temp["high_alert"] | false;
|
||||||
|
|
||||||
bool high_alert_enabled = temp["high_alert_enabled"] | false;
|
|
||||||
float high_alert_f = (float)(temp["high_alert_f"] | 9999.0);
|
|
||||||
|
|
||||||
if (high_alert_enabled && temp_f >= high_alert_f) {
|
|
||||||
high_alert = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (weather) {
|
if (weather) {
|
||||||
outside_status = "OUT ";
|
outside_status = "OUT ";
|
||||||
outside_status += String((int)round(temp_f));
|
outside_status += String((int)round(temp_f));
|
||||||
@@ -1765,11 +1927,11 @@ current_dashboard_screen = SCREEN_OVERVIEW;
|
|||||||
lv_obj_t *system_card = create_card(screen, "", 960, 48, LV_ALIGN_TOP_MID, 0, 12);
|
lv_obj_t *system_card = create_card(screen, "", 960, 48, LV_ALIGN_TOP_MID, 0, 12);
|
||||||
lv_obj_add_flag(system_card, LV_OBJ_FLAG_CLICKABLE);
|
lv_obj_add_flag(system_card, LV_OBJ_FLAG_CLICKABLE);
|
||||||
lv_obj_add_event_cb(system_card, system_card_event_cb, LV_EVENT_CLICKED, nullptr);
|
lv_obj_add_event_cb(system_card, system_card_event_cb, LV_EVENT_CLICKED, nullptr);
|
||||||
lv_obj_t *battery_card = create_card(screen, "", 380, 310, LV_ALIGN_TOP_LEFT, 32, 68);
|
lv_obj_t *battery_card = create_card(screen, "", 330, 310, LV_ALIGN_TOP_LEFT, 32, 68);
|
||||||
battery_card_obj = battery_card;
|
battery_card_obj = battery_card;
|
||||||
add_battery_card_tap_target(battery_card);
|
add_battery_card_tap_target(battery_card);
|
||||||
lv_obj_t *temp_card = create_card(screen, "", 260, 310, LV_ALIGN_TOP_LEFT, 426, 68);
|
lv_obj_t *temp_card = create_card(screen, "", 260, 310, LV_ALIGN_TOP_LEFT, 382, 68);
|
||||||
lv_obj_t *vehicle_card = create_card(screen, "", 292, 310, LV_ALIGN_TOP_RIGHT, -32, 68);
|
lv_obj_t *vehicle_card = create_card(screen, "", 330, 310, LV_ALIGN_TOP_RIGHT, -32, 68);
|
||||||
lv_obj_t *relay_card = create_card(screen, "", 960, 205, LV_ALIGN_BOTTOM_MID, 0, -8);
|
lv_obj_t *relay_card = create_card(screen, "", 960, 205, LV_ALIGN_BOTTOM_MID, 0, -8);
|
||||||
|
|
||||||
battery_charge_pulse_arc = lv_arc_create(battery_card);
|
battery_charge_pulse_arc = lv_arc_create(battery_card);
|
||||||
@@ -1900,7 +2062,7 @@ lv_obj_t *system_card = create_card(screen, "", 960, 48, LV_ALIGN_TOP_MID, 0, 12
|
|||||||
lv_meter_set_indicator_end_value(vehicle_coolant_meter, hot_arc, 260);
|
lv_meter_set_indicator_end_value(vehicle_coolant_meter, hot_arc, 260);
|
||||||
|
|
||||||
vehicle_coolant_needle = lv_meter_add_needle_line(vehicle_coolant_meter, coolant_scale, 4, lv_color_hex(0xFFFFFF), -14);
|
vehicle_coolant_needle = lv_meter_add_needle_line(vehicle_coolant_meter, coolant_scale, 4, lv_color_hex(0xFFFFFF), -14);
|
||||||
lv_meter_set_indicator_value(vehicle_coolant_meter, vehicle_coolant_needle, 188);
|
lv_meter_set_indicator_value(vehicle_coolant_meter, vehicle_coolant_needle, 100);
|
||||||
|
|
||||||
lv_obj_t *vehicle_coolant_center_cap = lv_obj_create(vehicle_coolant_meter);
|
lv_obj_t *vehicle_coolant_center_cap = lv_obj_create(vehicle_coolant_meter);
|
||||||
lv_obj_set_size(vehicle_coolant_center_cap, 54, 54);
|
lv_obj_set_size(vehicle_coolant_center_cap, 54, 54);
|
||||||
@@ -1912,7 +2074,7 @@ lv_obj_t *system_card = create_card(screen, "", 960, 48, LV_ALIGN_TOP_MID, 0, 12
|
|||||||
#endif
|
#endif
|
||||||
|
|
||||||
vehicle_coolant_value_label = lv_label_create(vehicle_card);
|
vehicle_coolant_value_label = lv_label_create(vehicle_card);
|
||||||
lv_label_set_text(vehicle_coolant_value_label, "188°");
|
lv_label_set_text(vehicle_coolant_value_label, "--°");
|
||||||
lv_obj_set_style_text_color(vehicle_coolant_value_label, lv_color_hex(0xFFFFFF), 0);
|
lv_obj_set_style_text_color(vehicle_coolant_value_label, lv_color_hex(0xFFFFFF), 0);
|
||||||
lv_obj_set_style_text_font(vehicle_coolant_value_label, &lv_font_montserrat_40, 0);
|
lv_obj_set_style_text_font(vehicle_coolant_value_label, &lv_font_montserrat_40, 0);
|
||||||
lv_obj_set_width(vehicle_coolant_value_label, 120);
|
lv_obj_set_width(vehicle_coolant_value_label, 120);
|
||||||
@@ -2038,9 +2200,7 @@ void setup()
|
|||||||
lvgl_port_init(board->getLCD(), board->getTouch());
|
lvgl_port_init(board->getLCD(), board->getTouch());
|
||||||
|
|
||||||
lvgl_port_lock(-1);
|
lvgl_port_lock(-1);
|
||||||
// TEMP DEBUG: bypass blocking Cargo ESP WiFi screen while troubleshooting pages.
|
show_boot_screen();
|
||||||
create_overland_overview_screen();
|
|
||||||
overview_screen_created = true;
|
|
||||||
lvgl_port_unlock();
|
lvgl_port_unlock();
|
||||||
|
|
||||||
Serial.println("Dashboard boot screen ready");
|
Serial.println("Dashboard boot screen ready");
|
||||||
@@ -2052,7 +2212,13 @@ void loop()
|
|||||||
update_system_status_label();
|
update_system_status_label();
|
||||||
process_pending_relay_command();
|
process_pending_relay_command();
|
||||||
poll_status_api();
|
poll_status_api();
|
||||||
|
process_obd_can();
|
||||||
process_dashboard_page_switch();
|
process_dashboard_page_switch();
|
||||||
|
|
||||||
delay(50);
|
|
||||||
|
|
||||||
|
if (diagnostics_overlay != nullptr) {
|
||||||
|
update_diagnostics_overlay();
|
||||||
|
}
|
||||||
|
delay(50);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,28 +7,27 @@ AppConfig appConfig;
|
|||||||
|
|
||||||
static Preferences preferences;
|
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() {
|
void loadDefaultConfig() {
|
||||||
appConfig.deviceName = "Overland Controller";
|
appConfig.deviceName = "Overland Controller";
|
||||||
|
|
||||||
appConfig.relays[0] = {
|
appConfig.relays[0] = defaultRelayConfig(1, "Relay 1", "aux");
|
||||||
"relay_1",
|
appConfig.relays[1] = defaultRelayConfig(2, "Relay 2", "fridge");
|
||||||
"Relay 1",
|
appConfig.relays[2] = defaultRelayConfig(3, "Relay 3", "starlink");
|
||||||
"aux",
|
appConfig.relays[3] = defaultRelayConfig(4, "Relay 4", "aux");
|
||||||
1,
|
appConfig.relays[4] = defaultRelayConfig(5, "Relay 5", "aux");
|
||||||
RELAY_1_PIN,
|
appConfig.relays[5] = defaultRelayConfig(6, "Relay 6", "aux");
|
||||||
true,
|
|
||||||
true
|
|
||||||
};
|
|
||||||
|
|
||||||
appConfig.relays[1] = {
|
|
||||||
"relay_2",
|
|
||||||
"Relay 2",
|
|
||||||
"fridge",
|
|
||||||
2,
|
|
||||||
RELAY_2_PIN,
|
|
||||||
true,
|
|
||||||
true
|
|
||||||
};
|
|
||||||
|
|
||||||
appConfig.tempSensorCount = 4;
|
appConfig.tempSensorCount = 4;
|
||||||
|
|
||||||
@@ -194,7 +193,7 @@ void printConfig() {
|
|||||||
Serial.print(appConfig.relays[i].role);
|
Serial.print(appConfig.relays[i].role);
|
||||||
Serial.print(" / channel ");
|
Serial.print(" / channel ");
|
||||||
Serial.print(appConfig.relays[i].hardwareChannel);
|
Serial.print(appConfig.relays[i].hardwareChannel);
|
||||||
Serial.print(" / GPIO ");
|
Serial.print(" / channel ");
|
||||||
Serial.print(appConfig.relays[i].pin);
|
Serial.print(appConfig.relays[i].pin);
|
||||||
Serial.print(" / available ");
|
Serial.print(" / available ");
|
||||||
Serial.print(appConfig.relays[i].available ? "true" : "false");
|
Serial.print(appConfig.relays[i].available ? "true" : "false");
|
||||||
|
|||||||
@@ -2,9 +2,9 @@
|
|||||||
|
|
||||||
#include <Arduino.h>
|
#include <Arduino.h>
|
||||||
|
|
||||||
#define MAX_RELAYS 2
|
#define MAX_RELAYS 6
|
||||||
#define MAX_TEMP_SENSORS 4
|
#define MAX_TEMP_SENSORS 4
|
||||||
#define HARDWARE_PROFILE "generic_esp32_2ch_relay"
|
#define HARDWARE_PROFILE "lilygo_trelay_s3_6ch"
|
||||||
|
|
||||||
struct RelayConfig {
|
struct RelayConfig {
|
||||||
String id;
|
String id;
|
||||||
|
|||||||
@@ -26,6 +26,8 @@ static int bmsConnectFailCount = 0;
|
|||||||
static unsigned long bmsReconnectPausedUntil = 0;
|
static unsigned long bmsReconnectPausedUntil = 0;
|
||||||
static bool bmsSetupMode = false;
|
static bool bmsSetupMode = false;
|
||||||
static const unsigned long READ_INTERVAL_MS = 5000;
|
static const unsigned long READ_INTERVAL_MS = 5000;
|
||||||
|
static const unsigned long BMS_CONNECT_BACKOFF_INITIAL_MS = 30000;
|
||||||
|
static const unsigned long BMS_CONNECT_BACKOFF_MAX_MS = 300000;
|
||||||
|
|
||||||
BmsData bmsData;
|
BmsData bmsData;
|
||||||
|
|
||||||
@@ -288,7 +290,34 @@ int scanBleDevices(uint32_t scanSeconds) {
|
|||||||
return bleScanResultCount;
|
return bleScanResultCount;
|
||||||
}
|
}
|
||||||
|
|
||||||
static bool connectBms() {
|
static void scheduleBmsReconnectBackoff();
|
||||||
|
static void clearBmsReconnectBackoff();
|
||||||
|
|
||||||
|
static void scheduleBmsReconnectBackoff() {
|
||||||
|
bmsConnectFailCount++;
|
||||||
|
|
||||||
|
if (bmsConnectBackoffMs == 0) {
|
||||||
|
bmsConnectBackoffMs = BMS_CONNECT_BACKOFF_INITIAL_MS;
|
||||||
|
} else {
|
||||||
|
bmsConnectBackoffMs = min(bmsConnectBackoffMs * 2, BMS_CONNECT_BACKOFF_MAX_MS);
|
||||||
|
}
|
||||||
|
|
||||||
|
bmsReconnectPausedUntil = millis() + bmsConnectBackoffMs;
|
||||||
|
|
||||||
|
Serial.print("BMS: reconnect backoff ");
|
||||||
|
Serial.print(bmsConnectBackoffMs / 1000);
|
||||||
|
Serial.print("s after ");
|
||||||
|
Serial.print(bmsConnectFailCount);
|
||||||
|
Serial.println(" failed attempt(s)");
|
||||||
|
}
|
||||||
|
|
||||||
|
static void clearBmsReconnectBackoff() {
|
||||||
|
bmsConnectFailCount = 0;
|
||||||
|
bmsConnectBackoffMs = 0;
|
||||||
|
bmsReconnectPausedUntil = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool connectBms() {
|
||||||
if (client && client->isConnected()) {
|
if (client && client->isConnected()) {
|
||||||
bmsData.connected = true;
|
bmsData.connected = true;
|
||||||
return true;
|
return true;
|
||||||
@@ -307,6 +336,7 @@ static bool connectBms() {
|
|||||||
|
|
||||||
if (!appConfig.bms.enabled || appConfig.bms.address.length() == 0) {
|
if (!appConfig.bms.enabled || appConfig.bms.address.length() == 0) {
|
||||||
bmsData.connected = false;
|
bmsData.connected = false;
|
||||||
|
clearBmsReconnectBackoff();
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -322,6 +352,9 @@ static bool connectBms() {
|
|||||||
bmsData.connected = false;
|
bmsData.connected = false;
|
||||||
NimBLEDevice::deleteClient(client);
|
NimBLEDevice::deleteClient(client);
|
||||||
client = nullptr;
|
client = nullptr;
|
||||||
|
notifyChar = nullptr;
|
||||||
|
writeChar = nullptr;
|
||||||
|
scheduleBmsReconnectBackoff();
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -352,6 +385,7 @@ static bool connectBms() {
|
|||||||
|
|
||||||
Serial.println("BMS: Connected");
|
Serial.println("BMS: Connected");
|
||||||
bmsData.connected = true;
|
bmsData.connected = true;
|
||||||
|
clearBmsReconnectBackoff();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,24 +2,25 @@
|
|||||||
|
|
||||||
#define DEVICE_NAME "Overland Controller"
|
#define DEVICE_NAME "Overland Controller"
|
||||||
|
|
||||||
// Relay Outputs
|
// LilyGO T-Relay S3 6-relay board.
|
||||||
#define RELAY_1_PIN 16
|
// Relays are driven through onboard 74HC595 shift register.
|
||||||
#define RELAY_2_PIN 17
|
#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
|
// Compatibility placeholder only. Relays are not direct GPIO outputs.
|
||||||
#define ONEWIRE_PIN 4
|
#define RELAY_SHIFT_REGISTER_PIN 255
|
||||||
|
|
||||||
// Ignition Sense
|
// GPIO4 is used by the LilyGO relay shift-register OE pin.
|
||||||
#define IGNITION_PIN 34
|
#define ONEWIRE_PIN 15
|
||||||
|
|
||||||
|
#define IGNITION_PIN 39
|
||||||
|
|
||||||
// Cargo OLED service/setup display
|
|
||||||
#define OLED_I2C_SDA_PIN 21
|
#define OLED_I2C_SDA_PIN 21
|
||||||
#define OLED_I2C_SCL_PIN 22
|
#define OLED_I2C_SCL_PIN 22
|
||||||
#define OLED_SETUP_BUTTON_PIN 25
|
#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_ENABLED 0
|
||||||
#define DASHBOARD_UART_BAUD 115200
|
#define DASHBOARD_UART_BAUD 115200
|
||||||
|
|
||||||
// RS-485/MAX3485 is fallback only and not currently planned.
|
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
#include <WiFi.h>
|
#include <WiFi.h>
|
||||||
|
#include <esp_heap_caps.h>
|
||||||
#include <WebServer.h>
|
#include <WebServer.h>
|
||||||
#include <Preferences.h>
|
#include <Preferences.h>
|
||||||
#include <esp_wifi.h>
|
#include <esp_wifi.h>
|
||||||
#include <esp_system.h>
|
#include <esp_system.h>
|
||||||
|
#include <Update.h>
|
||||||
|
|
||||||
#define OLED_ENABLED 0
|
#define OLED_ENABLED 0
|
||||||
|
|
||||||
@@ -637,12 +639,15 @@ button[onclick^="scan"]{
|
|||||||
<input id="bmsAddressTypeInput" placeholder="Address type: public or random">
|
<input id="bmsAddressTypeInput" placeholder="Address type: public or random">
|
||||||
<div class="btnrow">
|
<div class="btnrow">
|
||||||
<button class="save" onclick="saveBmsFullConfig()">Save BMS</button>
|
<button class="save" onclick="saveBmsFullConfig()">Save BMS</button>
|
||||||
|
<button onclick="scanBmsDevices()">Scan BMS</button>
|
||||||
<button class="off" onclick="setBmsEnabled(false)">Disable BMS</button>
|
<button class="off" onclick="setBmsEnabled(false)">Disable BMS</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="btnrow">
|
<div class="btnrow">
|
||||||
<button onclick="setBmsEnabled(true)">Enable BMS</button>
|
<button onclick="setBmsEnabled(true)">Enable BMS</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="row"><span class="muted">BMS Polling</span><span class="v" id="bmsEnabledText">--</span></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>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
@@ -1044,12 +1049,12 @@ function render(data){
|
|||||||
|
|
||||||
function populateSetupFields(data){
|
function populateSetupFields(data){
|
||||||
const cfg=data.config||{};
|
const cfg=data.config||{};
|
||||||
if($("deviceNameInput")) $("deviceNameInput").value=cfg.device_name||"";
|
setConfigValue("deviceNameInput", cfg.device_name||"");
|
||||||
|
|
||||||
const bms=cfg.bms||{};
|
const bms=cfg.bms||{};
|
||||||
if($("bmsNameInput")) $("bmsNameInput").value=bms.name||"";
|
setConfigValue("bmsNameInput", bms.name||"");
|
||||||
if($("bmsAddressInput")) $("bmsAddressInput").value=bms.address||"";
|
setConfigValue("bmsAddressInput", bms.address||"");
|
||||||
if($("bmsAddressTypeInput")) $("bmsAddressTypeInput").value=bms.address_type||"public";
|
setConfigValue("bmsAddressTypeInput", bms.address_type||"public");
|
||||||
}
|
}
|
||||||
|
|
||||||
async function saveDeviceConfig(){
|
async function saveDeviceConfig(){
|
||||||
@@ -1068,6 +1073,46 @@ async function saveDeviceConfig(){
|
|||||||
clearConfigTouched(["deviceNameInput"]);
|
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(){
|
async function saveBmsFullConfig(){
|
||||||
const name=$("bmsNameInput")?.value.trim()||"BMS";
|
const name=$("bmsNameInput")?.value.trim()||"BMS";
|
||||||
const address=$("bmsAddressInput")?.value.trim()||"";
|
const address=$("bmsAddressInput")?.value.trim()||"";
|
||||||
@@ -1197,14 +1242,20 @@ function renderConfigControls(data){
|
|||||||
|
|
||||||
const relayBox=$("relayConfigList");
|
const relayBox=$("relayConfigList");
|
||||||
if(relayBox){
|
if(relayBox){
|
||||||
|
const signature=relays.map(r=>r.id).join("|");
|
||||||
|
if(relayBox.dataset.signature!==signature){
|
||||||
|
relayBox.dataset.signature=signature;
|
||||||
relayBox.innerHTML=relays.map((r,i)=>`
|
relayBox.innerHTML=relays.map((r,i)=>`
|
||||||
<div class="configrow">
|
<div class="configrow">
|
||||||
<label>${r.id}</label>
|
<label>${r.id}</label>
|
||||||
<input id="relayName${i}" value="${r.name||""}" placeholder="Relay name">
|
<input id="relayName${i}" placeholder="Relay name" oninput="markConfigFieldTouched('relayName${i}')">
|
||||||
<label>${r.enabled?"Enabled":"Disabled"}</label>
|
<label>${r.enabled?"Enabled":"Disabled"}</label>
|
||||||
</div>`).join("");
|
</div>`).join("");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
relays.forEach((r,i)=>setConfigValue("relayName"+i,r.name||""));
|
||||||
|
}
|
||||||
|
|
||||||
setConfigValue("tempEnabledCount", temps.filter(t=>t.enabled).length);
|
setConfigValue("tempEnabledCount", temps.filter(t=>t.enabled).length);
|
||||||
|
|
||||||
const tempBox=$("tempConfigList");
|
const tempBox=$("tempConfigList");
|
||||||
@@ -1458,6 +1509,9 @@ setInterval(load,3000);
|
|||||||
|
|
||||||
WebServer server(80);
|
WebServer server(80);
|
||||||
|
|
||||||
|
unsigned long lastSensorUpdate = 0;
|
||||||
|
const unsigned long SENSOR_INTERVAL = 5000;
|
||||||
|
|
||||||
#define API_V1(path) "/api/v1" path
|
#define API_V1(path) "/api/v1" path
|
||||||
|
|
||||||
int findTempConfigIndexById(const String& id) {
|
int findTempConfigIndexById(const String& id) {
|
||||||
@@ -1648,6 +1702,97 @@ void sendSimpleJsonError(int status, const char* error, const char* detail) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const char OTA_HTML[] PROGMEM = R"rawliteral(
|
||||||
|
<!doctype html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||||
|
<title>Cargo ESP OTA</title>
|
||||||
|
<style>
|
||||||
|
body{background:#0b0f14;color:#eef4ff;font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Arial,sans-serif;margin:0}
|
||||||
|
.wrap{max-width:620px;margin:0 auto;padding:22px}
|
||||||
|
.card{background:#141b24;border:1px solid #223044;border-radius:18px;padding:18px}
|
||||||
|
h1{margin-top:0}
|
||||||
|
input,button{width:100%;box-sizing:border-box;margin-top:12px}
|
||||||
|
input{border:1px solid #223044;border-radius:12px;padding:12px;background:#0f151d;color:#eef4ff}
|
||||||
|
button{border:0;border-radius:12px;padding:13px;background:#1d324f;color:#eef4ff;font-weight:800}
|
||||||
|
.warn{color:#ffd166;font-size:14px;line-height:1.4}
|
||||||
|
.small{color:#7f8da3;font-size:13px}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="wrap">
|
||||||
|
<div class="card">
|
||||||
|
<h1>Cargo ESP OTA</h1>
|
||||||
|
<p class="warn">Upload only firmware built for the Cargo ESP. The controller will reboot after a successful update.</p>
|
||||||
|
<form method="POST" action="/api/v1/system/ota" enctype="multipart/form-data">
|
||||||
|
<input type="file" name="firmware" accept=".bin" required>
|
||||||
|
<button type="submit">Upload Firmware</button>
|
||||||
|
</form>
|
||||||
|
<p class="small">Tip: Arduino IDE can create this with Sketch → Export Compiled Binary.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
)rawliteral";
|
||||||
|
|
||||||
|
static bool otaUploadHadError = false;
|
||||||
|
|
||||||
|
void handleOtaPage() {
|
||||||
|
server.send_P(200, "text/html", OTA_HTML);
|
||||||
|
}
|
||||||
|
|
||||||
|
void handleOtaUploadDone() {
|
||||||
|
DynamicJsonDocument doc(512);
|
||||||
|
doc["ok"] = !otaUploadHadError && !Update.hasError();
|
||||||
|
doc["firmware_name"] = FIRMWARE_NAME;
|
||||||
|
doc["message"] = doc["ok"].as<bool>() ? "OTA update complete. Rebooting." : "OTA update failed.";
|
||||||
|
|
||||||
|
String output;
|
||||||
|
serializeJson(doc, output);
|
||||||
|
|
||||||
|
if (doc["ok"].as<bool>()) {
|
||||||
|
server.send(200, "application/json", output);
|
||||||
|
delay(500);
|
||||||
|
ESP.restart();
|
||||||
|
} else {
|
||||||
|
server.send(500, "application/json", output);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void handleOtaUploadStream() {
|
||||||
|
HTTPUpload& upload = server.upload();
|
||||||
|
|
||||||
|
if (upload.status == UPLOAD_FILE_START) {
|
||||||
|
otaUploadHadError = false;
|
||||||
|
Serial.print("OTA: upload start: ");
|
||||||
|
Serial.println(upload.filename);
|
||||||
|
|
||||||
|
if (!Update.begin(UPDATE_SIZE_UNKNOWN)) {
|
||||||
|
otaUploadHadError = true;
|
||||||
|
Update.printError(Serial);
|
||||||
|
}
|
||||||
|
} else if (upload.status == UPLOAD_FILE_WRITE) {
|
||||||
|
if (!otaUploadHadError && Update.write(upload.buf, upload.currentSize) != upload.currentSize) {
|
||||||
|
otaUploadHadError = true;
|
||||||
|
Update.printError(Serial);
|
||||||
|
}
|
||||||
|
} else if (upload.status == UPLOAD_FILE_END) {
|
||||||
|
if (!otaUploadHadError && Update.end(true)) {
|
||||||
|
Serial.print("OTA: success, bytes=");
|
||||||
|
Serial.println(upload.totalSize);
|
||||||
|
} else {
|
||||||
|
otaUploadHadError = true;
|
||||||
|
Update.printError(Serial);
|
||||||
|
}
|
||||||
|
} else if (upload.status == UPLOAD_FILE_ABORTED) {
|
||||||
|
otaUploadHadError = true;
|
||||||
|
Update.abort();
|
||||||
|
Serial.println("OTA: upload aborted");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
void handleResetApConfig() {
|
void handleResetApConfig() {
|
||||||
resetApConfigToDefaults();
|
resetApConfigToDefaults();
|
||||||
restartAccessPoint();
|
restartAccessPoint();
|
||||||
@@ -1998,8 +2143,10 @@ void connectStaWifi() {
|
|||||||
WiFi.begin(staSsids[i].c_str(), staPasswords[i].c_str());
|
WiFi.begin(staSsids[i].c_str(), staPasswords[i].c_str());
|
||||||
|
|
||||||
unsigned long start = millis();
|
unsigned long start = millis();
|
||||||
while (WiFi.status() != WL_CONNECTED && millis() - start < 10000) {
|
while (WiFi.status() != WL_CONNECTED && millis() - start < 8000) {
|
||||||
delay(500);
|
server.handleClient();
|
||||||
|
oledLoop();
|
||||||
|
delay(100);
|
||||||
Serial.print(".");
|
Serial.print(".");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2333,9 +2480,6 @@ int findStatusFieldIndex(const String& field) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
void buildStatusDocument(JsonDocument& doc) {
|
void buildStatusDocument(JsonDocument& doc) {
|
||||||
doc["type"] = MSG_STATUS_RESPONSE;
|
doc["type"] = MSG_STATUS_RESPONSE;
|
||||||
doc["timestamp"] = millis();
|
doc["timestamp"] = millis();
|
||||||
@@ -3404,6 +3548,7 @@ void handleCapabilities() {
|
|||||||
endpoints.add("POST /api/v1/bms/setup/exit");
|
endpoints.add("POST /api/v1/bms/setup/exit");
|
||||||
endpoints.add("POST /api/v1/bms/scan");
|
endpoints.add("POST /api/v1/bms/scan");
|
||||||
endpoints.add("POST /api/v1/bms/select");
|
endpoints.add("POST /api/v1/bms/select");
|
||||||
|
endpoints.add("POST /api/v1/system/ota");
|
||||||
|
|
||||||
JsonObject limits = doc.createNestedObject("limits");
|
JsonObject limits = doc.createNestedObject("limits");
|
||||||
limits["relay_count"] = MAX_RELAYS;
|
limits["relay_count"] = MAX_RELAYS;
|
||||||
@@ -3560,7 +3705,6 @@ void handleFactoryResetConfig() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
void sendWifiConfigJson() {
|
void sendWifiConfigJson() {
|
||||||
DynamicJsonDocument doc(2048);
|
DynamicJsonDocument doc(2048);
|
||||||
|
|
||||||
@@ -3971,63 +4115,53 @@ void handleStatus() {
|
|||||||
server.send(200, "application/json", output);
|
server.send(200, "application/json", output);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
void setup() {
|
void setup() {
|
||||||
Serial.begin(115200);
|
Serial.begin(115200);
|
||||||
|
delay(2000);
|
||||||
#if DASHBOARD_UART_ENABLED
|
|
||||||
DashboardSerial.begin(
|
|
||||||
DASHBOARD_UART_BAUD,
|
|
||||||
SERIAL_8N1,
|
|
||||||
0,
|
|
||||||
0
|
|
||||||
);
|
|
||||||
Serial.println("Legacy dashboard UART enabled");
|
|
||||||
#endif
|
|
||||||
|
|
||||||
Serial.println();
|
Serial.println();
|
||||||
Serial.println("==================================");
|
Serial.println("Overland Controller LilyGO T-Relay S3 Booting");
|
||||||
Serial.println("Overland Controller Booting");
|
|
||||||
Serial.println("==================================");
|
|
||||||
|
|
||||||
oledBegin();
|
|
||||||
|
|
||||||
loadConfig();
|
loadConfig();
|
||||||
|
|
||||||
printConfig();
|
printConfig();
|
||||||
|
|
||||||
pinMode(IGNITION_PIN, INPUT);
|
pinMode(IGNITION_PIN, INPUT);
|
||||||
|
|
||||||
initRelays();
|
initRelays();
|
||||||
|
|
||||||
initSensors();
|
initSensors();
|
||||||
|
|
||||||
initBms();
|
initBms();
|
||||||
|
|
||||||
loadWifiConfig();
|
loadWifiConfig();
|
||||||
|
|
||||||
loadApConfig();
|
loadApConfig();
|
||||||
|
|
||||||
|
WiFi.persistent(false);
|
||||||
|
WiFi.setSleep(false);
|
||||||
WiFi.mode(WIFI_AP_STA);
|
WiFi.mode(WIFI_AP_STA);
|
||||||
startAccessPoint();
|
|
||||||
oledShowSetupPage();
|
|
||||||
|
|
||||||
connectStaWifi();
|
startAccessPoint();
|
||||||
|
|
||||||
|
|
||||||
server.on("/", HTTP_GET, []() {
|
server.on("/", HTTP_GET, []() {
|
||||||
server.send_P(200, "text/html", INDEX_HTML);
|
server.send_P(200, "text/html", INDEX_HTML);
|
||||||
});
|
});
|
||||||
|
|
||||||
server.on(API_V1("/status"), handleStatus);
|
|
||||||
server.on(API_V1("/health"), HTTP_GET, handleHealth);
|
server.on(API_V1("/health"), HTTP_GET, handleHealth);
|
||||||
server.on(API_V1("/capabilities"), HTTP_GET, handleCapabilities);
|
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("/relay/set"), HTTP_POST, handleSetRelayPost);
|
||||||
|
|
||||||
|
|
||||||
server.on(API_V1("/config"), HTTP_GET, handleGetConfig);
|
server.on(API_V1("/config"), HTTP_GET, handleGetConfig);
|
||||||
server.on(API_V1("/config/export"), HTTP_GET, handleExportConfig);
|
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/wifi"), HTTP_GET, handleGetWifiConfig);
|
||||||
server.on(API_V1("/config/ap"), HTTP_GET, handleGetApConfig);
|
server.on(API_V1("/config/ap"), HTTP_GET, handleGetApConfig);
|
||||||
server.on(API_V1("/config/ap"), HTTP_POST, handleUpdateApConfig);
|
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/device"), HTTP_POST, handleUpdateDeviceConfig);
|
||||||
server.on(API_V1("/config/relay"), HTTP_POST, handleUpdateRelayConfig);
|
server.on(API_V1("/config/relay"), HTTP_POST, handleUpdateRelayConfig);
|
||||||
@@ -4036,67 +4170,59 @@ void setup() {
|
|||||||
server.on(API_V1("/config/factory-reset"), HTTP_POST, handleFactoryResetConfig);
|
server.on(API_V1("/config/factory-reset"), HTTP_POST, handleFactoryResetConfig);
|
||||||
server.on(API_V1("/config/save"), HTTP_POST, handleSaveConfig);
|
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/scan"), HTTP_POST, handleTempScan);
|
||||||
server.on(API_V1("/temps/assign"), HTTP_POST, handleTempAssign);
|
server.on(API_V1("/temps/assign"), HTTP_POST, handleTempAssign);
|
||||||
server.on(API_V1("/temps/clear"), HTTP_POST, handleTempClear);
|
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/enter"), HTTP_POST, handleEnterBmsSetup);
|
||||||
server.on(API_V1("/bms/setup/exit"), HTTP_POST, handleExitBmsSetup);
|
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();
|
server.begin();
|
||||||
|
|
||||||
Serial.println("Web Server Started");
|
Serial.println("Web Server Started");
|
||||||
|
lastStaReconnectAttempt = millis() - STA_RECONNECT_INTERVAL_MS + 5000;
|
||||||
}
|
}
|
||||||
|
|
||||||
void loop() {
|
void loop() {
|
||||||
server.handleClient();
|
server.handleClient();
|
||||||
|
|
||||||
maintainStaWifi();
|
maintainStaWifi();
|
||||||
handleDebugSerial();
|
|
||||||
updateBms();
|
|
||||||
updateAlarms();
|
|
||||||
#if DASHBOARD_UART_ENABLED
|
|
||||||
pollDashboardUart();
|
|
||||||
#endif
|
|
||||||
oledLoop();
|
|
||||||
|
|
||||||
static unsigned long lastSensorUpdate = 0;
|
if (millis() - lastSensorUpdate > SENSOR_INTERVAL) {
|
||||||
|
|
||||||
if (millis() - lastSensorUpdate > 5000) {
|
|
||||||
updateSensors();
|
updateSensors();
|
||||||
lastSensorUpdate = millis();
|
lastSensorUpdate = millis();
|
||||||
logDebug("Sensor Update");
|
logDebug("Sensor Update");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (millis() > 30000) {
|
||||||
|
updateBms();
|
||||||
|
}
|
||||||
|
|
||||||
|
#if DASHBOARD_UART_ENABLED
|
||||||
|
pollDashboardUart();
|
||||||
|
#endif
|
||||||
|
|
||||||
|
delay(2);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,20 +5,55 @@
|
|||||||
|
|
||||||
RelayState relays;
|
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() {
|
void initRelays() {
|
||||||
for (int i = 0; i < MAX_RELAYS; i++) {
|
pinMode(LILYGO_T_RELAY_S3_DATA_PIN, OUTPUT);
|
||||||
if (appConfig.relays[i].available) {
|
pinMode(LILYGO_T_RELAY_S3_CLOCK_PIN, OUTPUT);
|
||||||
pinMode(appConfig.relays[i].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();
|
updateRelayOutputs();
|
||||||
}
|
}
|
||||||
|
|
||||||
void updateRelayOutputs() {
|
void updateRelayOutputs() {
|
||||||
|
uint8_t nextState = 0;
|
||||||
|
|
||||||
for (int i = 0; i < MAX_RELAYS; i++) {
|
for (int i = 0; i < MAX_RELAYS; i++) {
|
||||||
if (appConfig.relays[i].available) {
|
if (!appConfig.relays[i].available) {
|
||||||
digitalWrite(appConfig.relays[i].pin, relays.state[i]);
|
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();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#define FIRMWARE_VERSION "0.5.0"
|
#define FIRMWARE_VERSION "0.5.5-wifi-connect-window-8s"
|
||||||
#define FIRMWARE_NAME "overland-controller"
|
#define FIRMWARE_NAME "overland-controller"
|
||||||
|
|||||||
+4
-4
@@ -8,7 +8,7 @@
|
|||||||
"enabled": true,
|
"enabled": true,
|
||||||
"role": "aux",
|
"role": "aux",
|
||||||
"hardware_channel": 1,
|
"hardware_channel": 1,
|
||||||
"hardware_profile": "generic_esp32_2ch_relay",
|
"hardware_profile": "lilygo_t_relay_s3_6ch",
|
||||||
"available": true
|
"available": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -18,7 +18,7 @@
|
|||||||
"enabled": true,
|
"enabled": true,
|
||||||
"role": "fridge",
|
"role": "fridge",
|
||||||
"hardware_channel": 2,
|
"hardware_channel": 2,
|
||||||
"hardware_profile": "generic_esp32_2ch_relay",
|
"hardware_profile": "lilygo_t_relay_s3_6ch",
|
||||||
"available": true
|
"available": true
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
@@ -38,6 +38,6 @@
|
|||||||
"weather": false
|
"weather": false
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"hardware_profile": "generic_esp32_2ch_relay",
|
"hardware_profile": "lilygo_t_relay_s3_6ch",
|
||||||
"output_count": 2
|
"output_count": 6
|
||||||
}
|
}
|
||||||
|
|||||||
+6
-6
@@ -44,7 +44,7 @@
|
|||||||
"state": false,
|
"state": false,
|
||||||
"role": "aux",
|
"role": "aux",
|
||||||
"hardware_channel": 1,
|
"hardware_channel": 1,
|
||||||
"hardware_profile": "generic_esp32_2ch_relay",
|
"hardware_profile": "lilygo_t_relay_s3_6ch",
|
||||||
"available": true
|
"available": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -55,7 +55,7 @@
|
|||||||
"state": true,
|
"state": true,
|
||||||
"role": "fridge",
|
"role": "fridge",
|
||||||
"hardware_channel": 2,
|
"hardware_channel": 2,
|
||||||
"hardware_profile": "generic_esp32_2ch_relay",
|
"hardware_profile": "lilygo_t_relay_s3_6ch",
|
||||||
"available": true
|
"available": true
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
@@ -106,7 +106,7 @@
|
|||||||
"enabled": true,
|
"enabled": true,
|
||||||
"role": "aux",
|
"role": "aux",
|
||||||
"hardware_channel": 1,
|
"hardware_channel": 1,
|
||||||
"hardware_profile": "generic_esp32_2ch_relay",
|
"hardware_profile": "lilygo_t_relay_s3_6ch",
|
||||||
"available": true
|
"available": true
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
@@ -126,9 +126,9 @@
|
|||||||
"weather": false
|
"weather": false
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"hardware_profile": "generic_esp32_2ch_relay",
|
"hardware_profile": "lilygo_t_relay_s3_6ch",
|
||||||
"output_count": 2
|
"output_count": 2
|
||||||
},
|
},
|
||||||
"hardware_profile": "generic_esp32_2ch_relay",
|
"hardware_profile": "lilygo_t_relay_s3_6ch",
|
||||||
"output_count": 2
|
"output_count": 6
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -57,10 +57,23 @@ def test_firmware_registers_current_http_contract_routes():
|
|||||||
assert expected_routes <= routes
|
assert expected_routes <= routes
|
||||||
|
|
||||||
|
|
||||||
def test_firmware_keeps_root_compatibility_aliases():
|
def test_firmware_uses_v1_routes_without_legacy_aliases():
|
||||||
routes = registered_routes()
|
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"),
|
("/status", "ANY"),
|
||||||
("/config", "HTTP_GET"),
|
("/config", "HTTP_GET"),
|
||||||
("/relay/set", "HTTP_POST"),
|
("/relay/set", "HTTP_POST"),
|
||||||
@@ -73,7 +86,8 @@ def test_firmware_keeps_root_compatibility_aliases():
|
|||||||
("/temps/clear", "HTTP_POST"),
|
("/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():
|
def test_embedded_webui_uses_versioned_api_routes():
|
||||||
@@ -134,8 +148,8 @@ def test_status_fixture_matches_dashboard_contract_shape():
|
|||||||
"online",
|
"online",
|
||||||
"temperature_f",
|
"temperature_f",
|
||||||
])
|
])
|
||||||
assert payload["hardware_profile"] == "generic_esp32_2ch_relay"
|
assert payload["hardware_profile"] == "lilygo_t_relay_s3_6ch"
|
||||||
assert payload["output_count"] == 2
|
assert payload["output_count"] == 6
|
||||||
assert_keys(payload["relays"][0], [
|
assert_keys(payload["relays"][0], [
|
||||||
"id",
|
"id",
|
||||||
"name",
|
"name",
|
||||||
@@ -180,8 +194,8 @@ def test_config_fixture_matches_current_config_response_shape():
|
|||||||
"temperature_sensor_count",
|
"temperature_sensor_count",
|
||||||
"temperature_sensors",
|
"temperature_sensors",
|
||||||
])
|
])
|
||||||
assert payload["hardware_profile"] == "generic_esp32_2ch_relay"
|
assert payload["hardware_profile"] == "lilygo_t_relay_s3_6ch"
|
||||||
assert payload["output_count"] == 2
|
assert payload["output_count"] == 6
|
||||||
assert_keys(payload["relays"][0], [
|
assert_keys(payload["relays"][0], [
|
||||||
"id",
|
"id",
|
||||||
"name",
|
"name",
|
||||||
@@ -461,6 +475,9 @@ def test_cargo_oled_shows_setup_and_service_pages():
|
|||||||
assert 'oledClear("Relays")' in source
|
assert 'oledClear("Relays")' in source
|
||||||
assert 'oledClear("Network")' in source
|
assert 'oledClear("Network")' in source
|
||||||
assert 'oledClear("Alarms")' in source
|
assert 'oledClear("Alarms")' 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 "oledShowSetupPage();" in source
|
||||||
assert "oledLoop();" in source
|
assert "oledLoop();" in source
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user