Project Wire Routing Help

Yes, reconnect those missing ends to the correct nets.
I’ll reconnect those missing passive ends to 3V3 and the buck output now, then verify the same parts again.
Done — I reconnected the missing ends.
Fixed connections:
  • L1 P2 → 3V3
  • C7/C8/C11 P1 → 3V3
  • R3/R4/R6/R7/R12/R13 P1 → 3V3
Verified:
  • C7, C8, C11 now go between 3V3 and GND
  • L1 now goes between BUCK_SW and 3V3
  • The listed pull-up resistors now go between 3V3 and their signal nets
There are still other schematic warnings unrelated to that exact list, including U1:3V3, OLED1:3V3, U2:FB, and R5:P1, so the schematic still needs another cleanup pass before routing/manufacturing.
Fix remaining warnings
Explain warnings
Run full review
Fix schematic first
Check buck circuit
OLED1
J6
U3
U1
Manufacturer Part Number
ESP32-S3-WROOM-1-N16R8
U2 BST - C10 P1
J6 CC1 - R10 P1
J5 P1 - D11 C
J3 P1 - D9 C
J2 P2 - D12 A
U1 EN - R3 P2
U1 IO0 - R4 P2
U2 SW - L1 P1
U1 IO9 - OLED1 SPI_SCLK/I2C_SLK
J6 CC1 - R10 P1
J6 D- - U1 IO19
U1 IO13 - R8 P1
U2 SW - L1 P1
J6 CC2 - R11 P1
U1 IO0 - R4 P2
L1 P2 - C7 P1
U1 IO9 - OLED1 SPI_SCLK/I2C_SLK
J6 CC2 - R11 P1
U2 EN - C6 P1
U1 IO8 - OLED1 SPI_MOSI/I2C_SDA
U2 EN - C6 P1
J3 P1 - D9 C
U1 IO5 - SW2 1-1
R8 P2 - R9 P1
U1 IO4 - SW1 1-1
J1 P1 - U2 VIN
U2 SW - L1 P1
J3 P1 - D9 C
J1 P1 - U2 VIN
U1 IO0 - R4 P2
J2 P2 - D12 A
J4 P1 - D10 C
U1 IO9 - OLED1 SPI_SCLK/I2C_SLK
U1 IO4 - SW1 1-1
J4 P1 - D10 C
U1 IO5 - SW2 1-1
L1 P2 - C7 P1
U1 IO8 - OLED1 SPI_MOSI/I2C_SDA
R8 P2 - R9 P1
U2 EN - C6 P1
U1 IO8 - OLED1 SPI_MOSI/I2C_SDA
L1 P2 - C7 P1
L1 P2 - C7 P1
J3 P1 - D9 C
U1 IO13 - R8 P1
R8 P2 - R9 P1
U1 EN - R3 P2
J6 D+ - U1 IO20
L1 P2 - C7 P1
J5 P1 - D11 C
J6 D- - U1 IO19
J2 P2 - D12 A
U1 IO6 - SW3 1-1
L1 P2 - C7 P1
L1 P2 - C7 P1
J4 P1 - D10 C
L1 P2 - C7 P1
U1 EN - R3 P2
U2 BST - C10 P1
J6 D+ - U1 IO20
L1 P2 - C7 P1
J5 P1 - D11 C
U2 EN - C6 P1
J4 P1 - D10 C
J5 P1 - D11 C
L1 P2 - C7 P1
U1 EN - R3 P2
U2 EN - C6 P1
U1 IO6 - SW3 1-1
U1 GND_10 - U1 GND_11
GND
J3 P2 - D9 A
J2
U1 GND_10 - U1 GND_11
R9 P2 - U3 GND
GND
U1 GND_3 - U1 GND_4
U1 GND_7 - U1 GND_8
SW2 2-2 - SW3 2-1
SW4 2-2 - SW5 2-1
SW5 2-2 - OLED1 GND
SW5 2-2 - OLED1 GND
GND
SW1 2-2 - SW2 2-1
SW4 2-2 - SW5 2-1
J3 P2 - D9 A
J5
GND
C12 P2 - SW4 2-1
J3 P2 - D9 A
R9 P2 - U3 GND
GND
U1 GND_3 - U1 GND_4
R9 P2 - U3 GND
J6 GND - R10 P2
J6 GND - R10 P2
J3 P2 - D9 A
SW1 2-2 - SW2 2-1
J4
U1 GND_7 - U1 GND_8
SW2 2-2 - SW3 2-1
U1 GND_1 - U1 GND_2
GND
J6 GND - R10 P2
J3 P2 - D9 A
C12 P2 - SW4 2-1
U1 GND_1 - U1 GND_2
GND
J1
GND
J3
J3 P2 - D9 A
U1 GND_3 - U1 GND_4
J3 P2 - D9 A
R5
Resistance
4.7 kΩ
R13
Resistance
10 kΩ
C11
Capacitance
100 nF
C8
Capacitance
22 µF
C7
Capacitance
22 µF
D8
R10
Resistance
5.1 kΩ
C10
Capacitance
100 nF
SW5
R4
Resistance
10 kΩ
D10
R7
Resistance
10 kΩ
R9
Resistance
100 kΩ
R3
Resistance
10 kΩ
SW2
R6
Resistance
10 kΩ
R11
Resistance
5.1 kΩ
R8
Resistance
100 Ω
D11
C6
Capacitance
10 µF
SW1
SW3
C12
Capacitance
100 nF
R12
Resistance
4.7 kΩ
SW4
D9
D12
U2
L1
Inductance
3.9 µH

Refine this doc
Ask about this doc
Firmware Starter — ESP32-S3 Universal Well/Tank Node
Platform & Toolchain
  • MCU: ESP32-S3-WROOM-1-N16R8
  • Framework: Arduino for ESP32
  • Build system: PlatformIO
  • Purpose: Starter firmware for the universal WiFi node PCB. The same PCB can be configured as a Well 1 node, Well 2 node, or Tank/controller node.
Schematic Pin Mapping

Table


FunctionESP32-S3 GPIONetDirectionNotes
OLED SDAGPIO8I2C_SDAI/OSSD1306 I2C data with 4.7 kΩ pull-up
OLED SCLGPIO9I2C_SCLOutputSSD1306 I2C clock with 4.7 kΩ pull-up
Flow pulse inputGPIO10FLOW_PULSEInputDry-contact/open-collector input, active LOW
Tank float inputGPIO11TANK_FLOAT_NOInputNormally-open tank-full float, active LOW when closed
Auxiliary inputGPIO12AUX_INPUTInputDry-run, disable, or outlet meter input, active LOW
Contactor outputGPIO13CONTACTOR_GPIOOutputDrives external 24 VDC contactor coil driver through R8
Select buttonGPIO4BTN_SELECTInputLocal UI button, active LOW
Up buttonGPIO5BTN_UPInputLocal UI button, active LOW
Down buttonGPIO6BTN_DOWNInputLocal UI button, active LOW
USB D-GPIO19USB_DNUSBNative USB programming/debug
USB D+GPIO20USB_DPUSBNative USB programming/debug
EN resetENESP_ENInput10 kΩ pull-up, 100 nF filter, reset button to GND
BOOTGPIO0ESP_BOOTInput10 kΩ pull-up, boot button to GND
platformio.ini

Ini


[env:esp32-s3-devkitc-1]
platform = espressif32
board = esp32-s3-devkitc-1
framework = arduino
monitor_speed = 115200
upload_speed = 921600
lib_deps =
    adafruit/Adafruit SSD1306@^2.5.13
    adafruit/Adafruit GFX Library@^1.11.11
Complete Starter Source — src/main.cpp

Cpp


#include <Arduino.h>
#include <WiFi.h>
#include <WebServer.h>
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
#include <Preferences.h>

// ─── Pin Definitions from schematic ──────────────────────
#define PIN_I2C_SDA        8
#define PIN_I2C_SCL        9
#define PIN_FLOW_PULSE    10
#define PIN_TANK_FLOAT    11
#define PIN_AUX_INPUT     12
#define PIN_CONTACTOR     13
#define PIN_BTN_SELECT     4
#define PIN_BTN_UP         5
#define PIN_BTN_DOWN       6

// Native USB is on GPIO19/GPIO20 internally on ESP32-S3.

// ─── Display ─────────────────────────────────────────────
#define SCREEN_WIDTH 128
#define SCREEN_HEIGHT 64
#define OLED_RESET -1
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);

// ─── WiFi / Web UI ───────────────────────────────────────
const char* WIFI_SSID = "YOUR_WIFI_SSID";
const char* WIFI_PASSWORD = "YOUR_WIFI_PASSWORD";
WebServer server(80);

// ─── Nonvolatile settings ────────────────────────────────
Preferences prefs;

// Node role can be: "well1", "well2", or "tank".
String nodeRole = "well1";
float targetGallonsPerHour = 7.0f;
float tankCapacityGallons = 1000.0f;
float meterPulsesPerGallon = 1.0f;  // update after final meter selection/calibration
bool pumpEnabled = true;
bool tankFull = false;

// ─── Runtime counters ────────────────────────────────────
volatile uint32_t flowPulseCount = 0;
volatile uint32_t auxPulseCount = 0;
uint32_t lastFlowPulseCount = 0;
uint32_t lastAuxPulseCount = 0;
unsigned long lastDisplayMs = 0;
unsigned long pumpStartMs = 0;
bool pumpRunning = false;

// Default simple schedule: 7 gallons/hour at 0.13 GPM ≈ 53.8 min ON per hour.
float assumedWellGpm = 0.13f;

void IRAM_ATTR onFlowPulse() {
  flowPulseCount++;
}

void IRAM_ATTR onAuxPulse() {
  auxPulseCount++;
}

float runtimeMinutesForTarget() {
  if (assumedWellGpm <= 0.0f) return 0.0f;
  return targetGallonsPerHour / assumedWellGpm;
}

void setPump(bool on) {
  if (!pumpEnabled || tankFull) on = false;
  digitalWrite(PIN_CONTACTOR, on ? HIGH : LOW);
  if (on && !pumpRunning) {
    pumpStartMs = millis();
  }
  pumpRunning = on;
}

void loadSettings() {
  prefs.begin("wellnode", false);
  nodeRole = prefs.getString("role", "well1");
  targetGallonsPerHour = prefs.getFloat("gph", 7.0f);
  tankCapacityGallons = prefs.getFloat("tankgal", 1000.0f);
  meterPulsesPerGallon = prefs.getFloat("ppg", 1.0f);
  assumedWellGpm = prefs.getFloat("wellgpm", 0.13f);
  pumpEnabled = prefs.getBool("enabled", true);
}

void saveSettings() {
  prefs.putString("role", nodeRole);
  prefs.putFloat("gph", targetGallonsPerHour);
  prefs.putFloat("tankgal", tankCapacityGallons);
  prefs.putFloat("ppg", meterPulsesPerGallon);
  prefs.putFloat("wellgpm", assumedWellGpm);
  prefs.putBool("enabled", pumpEnabled);
}

String htmlPage() {
  String s;
  s += "<!doctype html><html><head><meta name='viewport' content='width=device-width,initial-scale=1'>";
  s += "<title>Well Node</title></head><body>";
  s += "<h2>Dual Well Pump Controller Node</h2>";
  s += "<p>Role: " + nodeRole + "</p>";
  s += "<p>Flow pulses: " + String((uint32_t)flowPulseCount) + "</p>";
  s += "<p>Aux pulses: " + String((uint32_t)auxPulseCount) + "</p>";
  s += "<p>Tank float/full input: " + String(tankFull ? "FULL/ACTIVE" : "not full") + "</p>";
  s += "<p>Pump output: " + String(pumpRunning ? "ON" : "OFF") + "</p>";
  s += "<form action='/save' method='get'>";
  s += "Role <select name='role'><option>well1</option><option>well2</option><option>tank</option></select><br>";
  s += "Target gal/hour <input name='gph' value='" + String(targetGallonsPerHour, 2) + "'><br>";
  s += "Assumed well GPM <input name='wellgpm' value='" + String(assumedWellGpm, 3) + "'><br>";
  s += "Tank capacity gallons <input name='tankgal' value='" + String(tankCapacityGallons, 0) + "'><br>";
  s += "Meter pulses/gallon <input name='ppg' value='" + String(meterPulsesPerGallon, 3) + "'><br>";
  s += "<input type='submit' value='Save'>";
  s += "</form>";
  s += "<p><a href='/pump/on'>Pump ON</a> | <a href='/pump/off'>Pump OFF</a></p>";
  s += "</body></html>";
  return s;
}

void setupWeb() {
  server.on("/", []() { server.send(200, "text/html", htmlPage()); });

  server.on("/save", []() {
    if (server.hasArg("role")) nodeRole = server.arg("role");
    if (server.hasArg("gph")) targetGallonsPerHour = server.arg("gph").toFloat();
    if (server.hasArg("wellgpm")) assumedWellGpm = server.arg("wellgpm").toFloat();
    if (server.hasArg("tankgal")) tankCapacityGallons = server.arg("tankgal").toFloat();
    if (server.hasArg("ppg")) meterPulsesPerGallon = server.arg("ppg").toFloat();
    saveSettings();
    server.sendHeader("Location", "/");
    server.send(302, "text/plain", "Saved");
  });

  server.on("/pump/on", []() {
    setPump(true);
    server.sendHeader("Location", "/");
    server.send(302, "text/plain", "Pump on");
  });

  server.on("/pump/off", []() {
    setPump(false);
    server.sendHeader("Location", "/");
    server.send(302, "text/plain", "Pump off");
  });

  server.begin();
}

void connectWiFi() {
  WiFi.mode(WIFI_STA);
  WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
  Serial.print("Connecting to WiFi");
  for (int i = 0; i < 30 && WiFi.status() != WL_CONNECTED; i++) {
    delay(500);
    Serial.print('.');
  }
  Serial.println();
  if (WiFi.status() == WL_CONNECTED) {
    Serial.print("IP address: ");
    Serial.println(WiFi.localIP());
  } else {
    Serial.println("WiFi not connected; local controls still work.");
  }
}

void updateDisplay() {
  display.clearDisplay();
  display.setTextSize(1);
  display.setTextColor(SSD1306_WHITE);
  display.setCursor(0, 0);
  display.println("Well/Tank Node");
  display.print("Role: "); display.println(nodeRole);
  display.print("GPH target: "); display.println(targetGallonsPerHour, 1);
  display.print("Float: "); display.println(tankFull ? "FULL" : "OK");
  display.print("Pump: "); display.println(pumpRunning ? "ON" : "OFF");
  display.print("Flow pulses: "); display.println((uint32_t)flowPulseCount);
  display.print("IP: ");
  display.println(WiFi.status() == WL_CONNECTED ? WiFi.localIP().toString() : "offline");
  display.display();
}

void handleButtons() {
  static bool lastSelect = HIGH;
  static bool lastUp = HIGH;
  static bool lastDown = HIGH;

  bool sel = digitalRead(PIN_BTN_SELECT);
  bool up = digitalRead(PIN_BTN_UP);
  bool down = digitalRead(PIN_BTN_DOWN);

  if (lastUp == HIGH && up == LOW) {
    targetGallonsPerHour += 0.5f;
    saveSettings();
  }
  if (lastDown == HIGH && down == LOW) {
    targetGallonsPerHour -= 0.5f;
    if (targetGallonsPerHour < 0.0f) targetGallonsPerHour = 0.0f;
    saveSettings();
  }
  if (lastSelect == HIGH && sel == LOW) {
    pumpEnabled = !pumpEnabled;
    if (!pumpEnabled) setPump(false);
    saveSettings();
  }

  lastSelect = sel;
  lastUp = up;
  lastDown = down;
}

void simpleHourlySchedule() {
  if (nodeRole == "tank") {
    setPump(false);
    return;
  }

  unsigned long now = millis();
  unsigned long hourMs = 3600000UL;
  unsigned long onMs = (unsigned long)(runtimeMinutesForTarget() * 60000.0f);
  if (onMs > hourMs) onMs = hourMs;

  unsigned long phase = now % hourMs;
  bool shouldRun = phase < onMs;
  setPump(shouldRun);
}

void setup() {
  Serial.begin(115200);
  delay(200);
  Serial.println("Dual Well Pump Controller node starting");

  loadSettings();

  pinMode(PIN_CONTACTOR, OUTPUT);
  digitalWrite(PIN_CONTACTOR, LOW);

  pinMode(PIN_FLOW_PULSE, INPUT_PULLUP);
  pinMode(PIN_TANK_FLOAT, INPUT_PULLUP);
  pinMode(PIN_AUX_INPUT, INPUT_PULLUP);
  pinMode(PIN_BTN_SELECT, INPUT_PULLUP);
  pinMode(PIN_BTN_UP, INPUT_PULLUP);
  pinMode(PIN_BTN_DOWN, INPUT_PULLUP);

  attachInterrupt(digitalPinToInterrupt(PIN_FLOW_PULSE), onFlowPulse, FALLING);
  attachInterrupt(digitalPinToInterrupt(PIN_AUX_INPUT), onAuxPulse, FALLING);

  Wire.begin(PIN_I2C_SDA, PIN_I2C_SCL);
  if (!display.begin(SSD1306_SWITCHCAPVCC, 0x3C)) {
    Serial.println("SSD1306 display not found at 0x3C");
  } else {
    updateDisplay();
  }

  connectWiFi();
  setupWeb();
}

void loop() {
  server.handleClient();

  tankFull = (digitalRead(PIN_TANK_FLOAT) == LOW);  // normally-open contact closed = full/active
  handleButtons();
  simpleHourlySchedule();

  if (millis() - lastDisplayMs > 1000) {
    lastDisplayMs = millis();
    updateDisplay();

    uint32_t fp = flowPulseCount;
    if (fp != lastFlowPulseCount) {
      Serial.printf("Flow pulses: %lu\n", (unsigned long)fp);
      lastFlowPulseCount = fp;
    }
  }

  if (WiFi.status() != WL_CONNECTED) {
    // Current starter behavior: keep local schedule working.
    // Final distributed firmware should fail safe: inhibit pump starts if tank/controller heartbeat is lost.
  }
}
Build & Flash

Bash


pio run
pio run -t upload
pio device monitor
To force USB download mode: hold BOOT, tap RESET, then release BOOT. The schematic provides BOOT on GPIO0 and RESET on EN.
Important Firmware Safety TODOs Before Field Use
  • Add authenticated configuration pages; the starter web UI has no password.
  • Add heartbeat/command protocol between Tank node and Well nodes.
  • Well nodes should fail safe: stop/inhibit pumping if tank/controller heartbeat is lost.
  • Add no-flow/underflow alarm logic using expected pulse count during a pump window.
  • Add nonvolatile totalizer storage with wear-conscious update intervals.
  • Calibrate meterPulsesPerGallon after final pulse-output water meter selection.
  • Platform & Toolchain

  • Schematic Pin Mapping

  • `platformio.ini`

  • Complete Starter Source — `src/main.cpp`

  • Build & Flash

  • Important Firmware Safety TODOs Before Field Use

Dual Well Pump Controller thumbnail
Controller for two low-producing water wells that meters pump runtime/volume into an above-ground tank and inhibits pumping when an external tank-full float switch is active.

Properties

Properties describe core aspects of the project.

Pricing & Availability

Distributor

Qty 1

Arrow

$1.00–$1.71

Digi-Key

$7.38–$7.95

HQonline

$2.02

LCSC

$8.74

Mouser

$15.91

TME

$1.94

Verical

$1.73–$202.90

Controls