Project Start
Firmware Starter — WeLift Gate Controller
Board Bring-Up Plan — WeLift Gate Controller
FCC/CE TCF Draft and RF Integration Notes — WeLift Gate Controller
Preliminary Safety Analysis and FMEA — WeLift Gate Controller
Table
| Function | U3 pin / GPIO | Live net | Destination / behavior |
|---|---|---|---|
| OPEN relay output | IO4 / GPIO4 | RELAY1_CMD | K1 driver; active HIGH; hardware gate pull-down defaults OFF |
| CLOSE relay output | IO5 / GPIO5 | RELAY2_CMD | K2 driver; active HIGH; hardware gate pull-down defaults OFF |
| Status LED | IO6 / GPIO6 | STATUS_LED | R12/LED2; active HIGH |
| Isolated OPEN position | IO7 / GPIO7 | OPEN_LIMIT_N | U5 collector, 10 kΩ pull-up and 10 nF filter; asserted LOW |
| Isolated CLOSED position | IO8 / GPIO8 | CLOSED_LIMIT_N | U6 collector, 10 kΩ pull-up and 10 nF filter; asserted LOW |
| Cellular rail enable | IO9 / GPIO9 | CELL_PWR_CMD | R23 → CELL_PWR_EN/U4; active HIGH, hardware pull-down defaults OFF |
| Modem UART RTS | IO15 / GPIO15 | CELL_UART_RTS | J7 pin 8 |
| Modem UART CTS | IO16 / GPIO16 | CELL_UART_CTS | J7 pin 7 |
| Modem enable control | IO17 / GPIO17 | CELL_MODEM_EN | J7 pin 9; module-dependent polarity/sequence |
| Modem reset | IO18 / GPIO18 | CELL_MODEM_RESET_N | J7 pin 10; nominal active LOW, module timing is module-dependent |
| Native USB D− | IO19 / GPIO19 | USB_D_N | J6 through R26/D4 |
| Native USB D+ | IO20 / GPIO20 | USB_D_P | J6 through R27/D4 |
| Modem UART TX | TXD0 / GPIO43 | CELL_UART_TX | U3 → J7 pin 6 |
| Modem UART RX | RXD0 / GPIO44 | CELL_UART_RX | J7 pin 5 → U3 |
| Boot strap | IO0 / GPIO0 | ESP_BOOT | 10 kΩ pull-up; SW2 pulls LOW for download boot |
| Reset/chip enable | EN | ESP_EN | 10 kΩ pull-up, 1 µF, SW1 pulls LOW; not a GPIO |
Text
welift-fw/ ├── platformio.ini └── src/ └── main.cpp
platformio.iniIni
[env:esp32-s3-wroom-1u] platform = espressif32 board = esp32-s3-devkitc-1 framework = arduino monitor_speed = 115200 board_build.flash_mode = qio board_build.arduino.memory_type = qio_opi board_upload.flash_size = 16MB board_build.partitions = default_16MB.csv build_flags = -DARDUINO_USB_MODE=1 -DARDUINO_USB_CDC_ON_BOOT=1 -DCORE_DEBUG_LEVEL=1
src/main.cppCpp
#include <Arduino.h> #include <WiFi.h> #include "driver/uart.h" namespace Pin { constexpr uint8_t RELAY_OPEN=4, RELAY_CLOSE=5, LED_STATUS=6; constexpr uint8_t LIMIT_OPEN_N=7, LIMIT_CLOSED_N=8; constexpr uint8_t CELL_POWER=9, CELL_RTS=15, CELL_CTS=16; constexpr uint8_t CELL_ENABLE=17, CELL_RESET_N=18; constexpr uint8_t CELL_TX=43, CELL_RX=44; } // Provision these outside source control for production. #ifndef WELIFT_WIFI_SSID #define WELIFT_WIFI_SSID "" #endif #ifndef WELIFT_WIFI_PASSWORD #define WELIFT_WIFI_PASSWORD "" #endif #ifndef WELIFT_COMMAND_TOKEN #define WELIFT_COMMAND_TOKEN "CHANGE-ME" #endif constexpr uint32_t DEBOUNCE_MS=40; constexpr uint32_t DEFAULT_PULSE_MS=750; constexpr uint32_t MAX_PULSE_MS=2000; constexpr uint32_t ABSOLUTE_RELAY_ON_MS=2500; constexpr uint32_t RELAY_LOCKOUT_MS=500; constexpr uint32_t WIFI_RETRY_MS=10000; constexpr uint32_t MODEM_BOOT_WAIT_MS=3000; struct DebouncedInput { uint8_t pin; bool raw=false, stable=false; uint32_t changedAt=0; void begin(){ pinMode(pin, INPUT_PULLUP); raw=stable=(digitalRead(pin)==LOW); changedAt=millis(); } void update(){ bool now=(digitalRead(pin)==LOW); if(now!=raw){raw=now;changedAt=millis();} if((millis()-changedAt)>=DEBOUNCE_MS) stable=raw; } }; DebouncedInput openLimit{Pin::LIMIT_OPEN_N}, closedLimit{Pin::LIMIT_CLOSED_N}; struct RelayPulse { uint8_t pin; bool active=false; uint32_t started=0, stopAt=0; }; RelayPulse openRelay{Pin::RELAY_OPEN}, closeRelay{Pin::RELAY_CLOSE}; uint32_t lastRelayOff=0, lastWifiAttempt=0; String commandLine; void allRelaysOff(){ digitalWrite(Pin::RELAY_OPEN, LOW); digitalWrite(Pin::RELAY_CLOSE, LOW); openRelay.active=closeRelay.active=false; lastRelayOff=millis(); } bool pulseRelay(RelayPulse &requested, RelayPulse &other, uint32_t durationMs){ if(other.active || requested.active) return false; if(millis()-lastRelayOff < RELAY_LOCKOUT_MS) return false; durationMs=constrain(durationMs, 100UL, MAX_PULSE_MS); allRelaysOff(); requested.active=true; requested.started=millis(); requested.stopAt=requested.started+durationMs; digitalWrite(requested.pin, HIGH); return true; } void serviceRelays(){ uint32_t now=millis(); for(RelayPulse *r : {&openRelay,&closeRelay}){ if(r->active && ((int32_t)(now-r->stopAt)>=0 || now-r->started>=ABSOLUTE_RELAY_ON_MS)) allRelaysOff(); } // Impossible command overlap is always forced safe. if(digitalRead(Pin::RELAY_OPEN) && digitalRead(Pin::RELAY_CLOSE)) allRelaysOff(); } void serviceWiFi(){ if(WiFi.status()==WL_CONNECTED) return; if(millis()-lastWifiAttempt < WIFI_RETRY_MS) return; lastWifiAttempt=millis(); WiFi.disconnect(); if(strlen(WELIFT_WIFI_SSID)) WiFi.begin(WELIFT_WIFI_SSID, WELIFT_WIFI_PASSWORD); } void initModemInterface(){ // Safe GPIO state first: switched 5 V OFF, module enable inactive, reset asserted. pinMode(Pin::CELL_POWER,OUTPUT); pinMode(Pin::CELL_ENABLE,OUTPUT); pinMode(Pin::CELL_RESET_N,OUTPUT); digitalWrite(Pin::CELL_POWER,LOW); digitalWrite(Pin::CELL_ENABLE,LOW); digitalWrite(Pin::CELL_RESET_N,LOW); Serial1.begin(115200,SERIAL_8N1,Pin::CELL_RX,Pin::CELL_TX); uart_set_pin(UART_NUM_1,Pin::CELL_TX,Pin::CELL_RX,Pin::CELL_RTS,Pin::CELL_CTS); uart_set_hw_flow_ctrl(UART_NUM_1,UART_HW_FLOWCTRL_CTS_RTS,64); digitalWrite(Pin::CELL_POWER,HIGH); delay(50); digitalWrite(Pin::CELL_RESET_N,HIGH); delay(MODEM_BOOT_WAIT_MS); digitalWrite(Pin::CELL_ENABLE,HIGH); // TODO(module-dependent): verify CELL_MODEM_EN polarity, RESET_N pulse width, // UART baud, power-key sequence and AT commands against the selected certified modem. // No generic AT command is sent here because J7 is a project-defined interface. } bool authenticated(const String &line){ return line.startsWith(String(WELIFT_COMMAND_TOKEN)+" "); } void handleCommand(String line){ line.trim(); if(!authenticated(line)){ Serial.println("ERR auth"); allRelaysOff(); return; } String cmd=line.substring(strlen(WELIFT_COMMAND_TOKEN)+1); cmd.toUpperCase(); // Minimal placeholder only: local USB serial transport. Production must use TLS, // replay protection, authorization, audit logging and signed OTA. if(cmd=="STATUS"){ Serial.printf("OK open=%d closed=%d wifi=%d relays=%d/%d\n",openLimit.stable,closedLimit.stable,WiFi.status()==WL_CONNECTED,openRelay.active,closeRelay.active); } else if(cmd=="OPEN"){ Serial.println(pulseRelay(openRelay,closeRelay,DEFAULT_PULSE_MS)?"OK open pulse":"ERR interlock"); } else if(cmd=="CLOSE"){ // This remains only a dry-contact request to the certified operator. Serial.println(pulseRelay(closeRelay,openRelay,DEFAULT_PULSE_MS)?"OK close pulse":"ERR interlock"); } else if(cmd=="STOP" || cmd=="OFF") { allRelaysOff(); Serial.println("OK off"); } else { allRelaysOff(); Serial.println("ERR command"); } } void setup(){ // First executable action: outputs OFF before network, modem or command parsing. pinMode(Pin::RELAY_OPEN,OUTPUT); pinMode(Pin::RELAY_CLOSE,OUTPUT); allRelaysOff(); pinMode(Pin::LED_STATUS,OUTPUT); digitalWrite(Pin::LED_STATUS,LOW); openLimit.begin(); closedLimit.begin(); Serial.begin(115200); delay(50); WiFi.mode(WIFI_STA); WiFi.setAutoReconnect(false); serviceWiFi(); initModemInterface(); } void loop(){ openLimit.update(); closedLimit.update(); serviceRelays(); serviceWiFi(); digitalWrite(Pin::LED_STATUS, WiFi.status()==WL_CONNECTED ? HIGH : ((millis()/500)&1)); while(Serial.available()){ char c=(char)Serial.read(); if(c=='\n'){ handleCommand(commandLine); commandLine=""; } else if(c!='\r' && commandLine.length()<160) commandLine+=c; } delay(2); }
Bash
python -m pip install -U platformio pio run # Hold BOOT (SW2), tap RESET (SW1), release BOOT if automatic USB download does not start. pio run -t upload --upload-port /dev/ttyACM0 pio device monitor -b 115200 --port /dev/ttyACM0
COMx; macOS typically uses /dev/cu.usbmodem*. Native USB is on GPIO19/20 and USB VBUS does not power this board, so apply normal board input power while servicing USB.Scope and safety boundary
Exact live GPIO mapping
PlatformIO layout
`platformio.ini`
`src/main.cpp`
Build, flash, monitor
Production TODOs