Project Start

Created 5 project docs

Firmware Starter — WeLift Gate Controller

View Document

Arduino/PlatformIO starter for U3 ESP32-S3-WROOM-1U-N16R8. The mapping below is grounded in the live U3 netlist; TXD0/RXD0 GPIO identities a...

Board Bring-Up Plan — WeLift Gate Controller

View Document

Use an isolated, current-limited bench source, DMM, oscilloscope with appropriate differential probing, electronic load, USB data isolator w...

FCC/CE TCF Draft and RF Integration Notes — WeLift Gate Controller

View Document

Host controller using U3 ESP32-S3-WROOM-1U-N16R8, a Wi-Fi/Bluetooth module with U.FL external-antenna interface, plus an optional plug-in LT...

Preliminary Safety Analysis and FMEA — WeLift Gate Controller

View Document

The product presents only isolated, normally-open dry contacts to command inputs of an independently certified gate operator. It does not sw...
Show 1 More
The schematic is complete and ERC-clean, with firmware, bring-up, compliance, safety, and manufacturing files created.
Open blockers
  • PCB stackup repeatedly reverts to two layers due to a platform persistence fault, so routing cannot safely proceed.
  • Cellular-area placement remains unroutable.
  • Production also requires the TVS, cellular connector, fuse-inrush, mounting-hole, and relay-surge items listed in the Manufacturing Readiness Checklist.
Completed files
The stackup fault has been reported internally; routing and manufacturing export remain intentionally unfinished rather than producing an unsafe board.
Fix blockers and review
Fix PCB blockers
Replace input TVS
Verify cellular connector
Review BOM cost

Refine this doc
Ask about this doc
Firmware Starter — WeLift Gate Controller
Scope and safety boundary
Arduino/PlatformIO starter for U3 ESP32-S3-WROOM-1U-N16R8. The mapping below is grounded in the live U3 netlist; TXD0/RXD0 GPIO identities are confirmed by the Espressif module datasheet. This starter only requests momentary dry-contact closures from a certified gate operator. It does not drive a motor, implement automatic closing, or bypass entrapment protection.
Exact live GPIO mapping

Table


FunctionU3 pin / GPIOLive netDestination / behavior
OPEN relay outputIO4 / GPIO4RELAY1_CMDK1 driver; active HIGH; hardware gate pull-down defaults OFF
CLOSE relay outputIO5 / GPIO5RELAY2_CMDK2 driver; active HIGH; hardware gate pull-down defaults OFF
Status LEDIO6 / GPIO6STATUS_LEDR12/LED2; active HIGH
Isolated OPEN positionIO7 / GPIO7OPEN_LIMIT_NU5 collector, 10 kΩ pull-up and 10 nF filter; asserted LOW
Isolated CLOSED positionIO8 / GPIO8CLOSED_LIMIT_NU6 collector, 10 kΩ pull-up and 10 nF filter; asserted LOW
Cellular rail enableIO9 / GPIO9CELL_PWR_CMDR23 → CELL_PWR_EN/U4; active HIGH, hardware pull-down defaults OFF
Modem UART RTSIO15 / GPIO15CELL_UART_RTSJ7 pin 8
Modem UART CTSIO16 / GPIO16CELL_UART_CTSJ7 pin 7
Modem enable controlIO17 / GPIO17CELL_MODEM_ENJ7 pin 9; module-dependent polarity/sequence
Modem resetIO18 / GPIO18CELL_MODEM_RESET_NJ7 pin 10; nominal active LOW, module timing is module-dependent
Native USB D−IO19 / GPIO19USB_D_NJ6 through R26/D4
Native USB D+IO20 / GPIO20USB_D_PJ6 through R27/D4
Modem UART TXTXD0 / GPIO43CELL_UART_TXU3 → J7 pin 6
Modem UART RXRXD0 / GPIO44CELL_UART_RXJ7 pin 5 → U3
Boot strapIO0 / GPIO0ESP_BOOT10 kΩ pull-up; SW2 pulls LOW for download boot
Reset/chip enableENESP_EN10 kΩ pull-up, 1 µF, SW1 pulls LOW; not a GPIO
J7 pins 1–2 are CELL_5V_SW, 3–4 GND. Pins 11–12 are intentionally unused/mechanical in the live schematic. Connector footprint/numbering is not yet production-verified.
PlatformIO layout

Text


welift-fw/
├── platformio.ini
└── src/
    └── main.cpp
platformio.ini

Ini


[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.cpp

Cpp


#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);
}
Build, flash, monitor

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
Windows ports use 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.
Production TODOs
  • Replace compile-time credentials with secure provisioning/NVS; enable secure boot, flash encryption, signed OTA and watchdog policy.
  • Select the LTE-M/NB-IoT module, then implement only its documented power sequence and AT command set.
  • Replace the local serial authenticated placeholder with mutually authenticated TLS plus nonce/replay protection and role authorization.
  • Confirm relay labels against each gate operator and keep pulse maximum/interlock non-bypassable.
  • Scope and safety boundary

  • Exact live GPIO mapping

  • PlatformIO layout

  • `platformio.ini`

  • `src/main.cpp`

  • Build, flash, monitor

  • Production TODOs

WeLift Gate Controller thumbnail
Outdoor gate-interface controller accepting 12–24 V AC/DC, with Wi‑Fi, plug-in LTE‑M/NB‑IoT backup, two isolated OPEN/CLOSE relay outputs, and two isolated gate-position inputs.

Properties

Properties describe core aspects of the project.

Pricing & Availability

Distributor

Qty 1

Arrow

$5.73–$9.67

Digi-Key

$0.74

HQonline

$1.55–$2.22

LCSC

$12.90–$16.50

Mouser

$20.11–$21.49

TME

$3.62–$5.01

Verical

$2.60–$20.28

Controls