diff --git a/docs/HARDWARE.md b/docs/HARDWARE.md index 7276475..0a09877 100644 --- a/docs/HARDWARE.md +++ b/docs/HARDWARE.md @@ -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. diff --git a/docs/cargo-api-contract.md b/docs/cargo-api-contract.md index 5e3177b..e204627 100644 --- a/docs/cargo-api-contract.md +++ b/docs/cargo-api-contract.md @@ -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: diff --git a/docs/project-state.md b/docs/project-state.md index b204366..ec2f786 100644 --- a/docs/project-state.md +++ b/docs/project-state.md @@ -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: diff --git a/docs/roadmap.md b/docs/roadmap.md index 3680543..7952395 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -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. diff --git a/firmware/esp32/overland-controller/app_config.cpp b/firmware/esp32/overland-controller/app_config.cpp index 27a1519..acbc75f 100644 --- a/firmware/esp32/overland-controller/app_config.cpp +++ b/firmware/esp32/overland-controller/app_config.cpp @@ -7,68 +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[2] = { - "relay_3", - "Relay 3", - "starlink", - 3, - 0, - true, - true - }; - - appConfig.relays[3] = { - "relay_4", - "Relay 4", - "aux", - 4, - 0, - true, - true - }; - - appConfig.relays[4] = { - "relay_5", - "Relay 5", - "aux", - 5, - 0, - true, - true - }; - - appConfig.relays[5] = { - "relay_6", - "Relay 6", - "aux", - 6, - 0, - 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; @@ -234,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"); diff --git a/firmware/esp32/overland-controller/config.h b/firmware/esp32/overland-controller/config.h index a7d9620..0255226 100644 --- a/firmware/esp32/overland-controller/config.h +++ b/firmware/esp32/overland-controller/config.h @@ -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 + +// GPIO4 is used by the LilyGO relay shift-register OE pin. +#define ONEWIRE_PIN 14 -// Ignition Sense #define IGNITION_PIN 34 -// 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. diff --git a/firmware/esp32/overland-controller/relays.cpp b/firmware/esp32/overland-controller/relays.cpp index 0c5381a..89d38c5 100644 --- a/firmware/esp32/overland-controller/relays.cpp +++ b/firmware/esp32/overland-controller/relays.cpp @@ -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(); } diff --git a/tests/fixtures/http_api/config_response.json b/tests/fixtures/http_api/config_response.json index 8e04bf1..396697a 100644 --- a/tests/fixtures/http_api/config_response.json +++ b/tests/fixtures/http_api/config_response.json @@ -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 } diff --git a/tests/fixtures/http_api/status_response.json b/tests/fixtures/http_api/status_response.json index 0040ebb..f02d042 100644 --- a/tests/fixtures/http_api/status_response.json +++ b/tests/fixtures/http_api/status_response.json @@ -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 } diff --git a/tests/test_http_api_contract.py b/tests/test_http_api_contract.py index 014510c..d82444a 100644 --- a/tests/test_http_api_contract.py +++ b/tests/test_http_api_contract.py @@ -134,8 +134,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 +180,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",