/*
ESP32 FreeRTOS multi-core sketch
Core 0: WiFi + WebServer + LCD + Proximity + Conveyor
Core 1: Button + Flow aggregation + Pump control
*/
#include <ArduinoJson.h>
#include <WiFi.h>
#include <WebServer.h>
#include <Preferences.h>
#include <LiquidCrystal_I2C.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/semphr.h"
// ========== HARDWARE PINS ==========
#define RELAY_PIN 13 // Pump / valve relay (activated by button/pump task)
#define BUTTON_PIN 19 // Physical start button (INPUT_PULLUP)
#define FLOW_SENSOR_PIN 35 // Flow sensor (ISR increments pulse count)
#define CONVEYOR_PIN 32 // Conveyor relay (ON/OFF via proximity on core0)
#define PROX_PIN 33 // Proximity sensor (INPUT_PULLUP; LOW = detected)
#define I2C_ADDR 0x27 // LCD I2C address
// ========== GLOBALS & PERFS ==========
LiquidCrystal_I2C lcd(I2C_ADDR, 16, 2);
Preferences prefs;
WebServer server(80);
const char* ssid = "ESP32_Auto";
const char* password = "12345678";
IPAddress local_IP(192, 168, 10, 1);
IPAddress gateway(192, 168, 10, 1);
IPAddress subnet(255, 255, 255, 0);
#define PREF_NAMESPACE "esp32auto"
#define KEY_MODE "mode"
#define KEY_TIME "time"
#define KEY_LITER "liter"
#define KEY_PPL "ppl"
#define KEY_CONV "conv"
// defaults
volatile int modeSetting = 1;
volatile unsigned long mode1Seconds = 5;
volatile float mode2Liters = 10.0;
volatile float pulsesPerLiter = 12; // our value 71.5125
volatile unsigned long conveyorSeconds = 3;
// flow variables
volatile unsigned long pulseCount = 0; // incremented in ISR
float totalLiters = 0.0; // aggregated once a second (protected by mutex)
unsigned long lastFlowCalc = 0;
// mutex to protect totalLiters
SemaphoreHandle_t xLitMutex = NULL;
// pump/sequence flags (Core1)
volatile bool pumpRunning = false;
// conveyor state (Core0)
volatile bool conveyorRunning = false;
unsigned long conveyorStartMillis = 0;
unsigned long conveyorDurationMs = 0;
// button debounce state (Core1)
const unsigned long BUTTON_DEBOUNCE_MS = 50;
unsigned long lastButtonDebounceMillis = 0;
int lastButtonStable = HIGH;
// proximity debounce (Core0)
const unsigned long PROX_DEBOUNCE_MS = 50;
unsigned long lastProxDebounceMillis = 0;
int lastProxStable = HIGH;
// HTML page (same UI as earlier)
const char* htmlPage = R"rawliteral(
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Brine Automation Dashboard</title>
<style>
body { font-family: Arial, sans-serif; background: #f4f7f9; margin: 0; padding: 0; }
.container { max-width: 480px; width: 90%; margin: 40px auto; background: #fff; padding: 20px; border-radius: 12px; box-shadow: 0 4px 12px rgba(0,0,0,0.1); }
h2 { text-align: center; color: #0077cc; margin-bottom: 20px; }
label { font-weight: bold; display: block; margin-top: 10px; }
input, select, button { width: 100%; padding: 10px; margin-top: 6px; font-size: 16px; border-radius: 6px; border: 1px solid #ccc; box-sizing: border-box; }
button { background: #0077cc; color: white; border: none; margin-top: 15px; cursor: pointer; transition: 0.3s; }
button:hover { background: #005fa3; }
#msg { text-align: center; margin-top: 10px; font-weight: bold; color: green; }
</style>
</head>
<body>
<div class="container">
<h2>ESP32 Automation</h2>
<p>Current Mode: <span id="curMode">Loading...</span></p>
<label>Select Mode:</label>
<select id="mode">
<option value="1">Mode 1 - Time</option>
<option value="2">Mode 2 - Volume</option>
<option value="3">Mode 3 - Disabled</option>
</select>
<div id="mode1Box">
<label>Seconds:</label>
<input id="time" type="number" min="1" value="5">
</div>
<div id="mode2Box" style="display:none;">
<label>Liters:</label>
<input id="lit" type="number" min="1" value="10">
</div>
<label>Conveyor Time (sec):</label>
<input id="conv" type="number" min="1" value="3">
<button onclick="save()">Save</button>
<p id="msg"></p>
</div>
<script>
async function loadStatus(){
let r = await fetch('/status');
if(r.ok){
let d = await r.json();
document.getElementById('curMode').innerText = d.mode;
document.getElementById('mode').value = d.mode;
document.getElementById('time').value = d.time;
document.getElementById('lit').value = d.liter;
document.getElementById('conv').value = d.conv;
switchModeUI();
}
}
async function save(){
let data = {
mode: parseInt(document.getElementById('mode').value),
time: parseInt(document.getElementById('time').value),
liter: parseFloat(document.getElementById('lit').value),
conv: parseInt(document.getElementById('conv').value)
};
let r = await fetch('/save', {
method:'POST',
headers:{'Content-Type':'application/json'},
body:JSON.stringify(data)
});
if(r.ok){
document.getElementById('msg').innerText = '✅ Settings Saved!';
loadStatus();
} else {
document.getElementById('msg').innerText = '❌ Save failed';
}
}
document.getElementById('mode').addEventListener('change', switchModeUI);
function switchModeUI(){
let m = document.getElementById('mode').value;
document.getElementById('mode1Box').style.display = (m=='1')?'block':'none';
document.getElementById('mode2Box').style.display = (m=='2')?'block':'none';
}
loadStatus();
</script>
</body>
</html>
)rawliteral";
// ========== PREFERENCES ==========
void saveSettings() {
prefs.begin(PREF_NAMESPACE, false);
prefs.putInt(KEY_MODE, (int)modeSetting);
prefs.putULong(KEY_TIME, mode1Seconds);
prefs.putFloat(KEY_LITER, mode2Liters);
prefs.putFloat(KEY_PPL, pulsesPerLiter);
prefs.putULong(KEY_CONV, conveyorSeconds);
prefs.end();
Serial.printf("Saved settings: mode=%d time=%lu liter=%.2f conv=%lu ppl=%.2f\n",
(int)modeSetting, mode1Seconds, mode2Liters, conveyorSeconds, pulsesPerLiter);
}
void loadSettings() {
prefs.begin(PREF_NAMESPACE, true);
modeSetting = prefs.getInt(KEY_MODE, 1);
mode1Seconds = prefs.getULong(KEY_TIME, 5);
mode2Liters = prefs.getFloat(KEY_LITER, 10.0);
pulsesPerLiter = prefs.getFloat(KEY_PPL, 71.5125);
conveyorSeconds = prefs.getULong(KEY_CONV, 3);
prefs.end();
Serial.printf("Loaded settings: mode=%d time=%lu liter=%.2f conv=%lu ppl=%.2f\n",
(int)modeSetting, mode1Seconds, mode2Liters, conveyorSeconds, pulsesPerLiter);
}
// ========== FLOW ISR & AGGREGATION ==========
void IRAM_ATTR flowISR() {
pulseCount++;
}
// Called periodically on Core1 to aggregate pulses into liters into totalLiters
void aggregateFlow() {
unsigned long now = millis();
if (now - lastFlowCalc >= 1000UL) { // every 1 second
noInterrupts();
unsigned long pulses = pulseCount;
pulseCount = 0;
interrupts();
float litersThisSec = (float)pulses / pulsesPerLiter;
// protect totalLiters
if (xLitMutex != NULL && xSemaphoreTake(xLitMutex, (TickType_t)10) == pdTRUE) {
totalLiters += litersThisSec;
xSemaphoreGive(xLitMutex);
} else {
// fallback (shouldn't happen often)
totalLiters += litersThisSec;
}
Serial.printf("Flow agg: pulses=%lu +%.4f L total=%.4f\n", pulses, litersThisSec, totalLiters);
lastFlowCalc = now;
}
}
// ========== WEB HANDLERS ==========
void handleRoot() {
server.send(200, "text/html", htmlPage);
}
void handleStatus() {
float safeLit = 0.0;
if (xLitMutex != NULL && xSemaphoreTake(xLitMutex, (TickType_t)10) == pdTRUE) {
safeLit = totalLiters;
xSemaphoreGive(xLitMutex);
} else {
safeLit = totalLiters;
}
String json = "{\"mode\":" + String((int)modeSetting)
+ ",\"time\":" + String(mode1Seconds)
+ ",\"liter\":" + String(mode2Liters)
+ ",\"conv\":" + String(conveyorSeconds)
+ ",\"flow\":" + String(safeLit)
+ "}";
server.send(200, "application/json", json);
}
void handleSave() {
String body = server.arg("plain");
DynamicJsonDocument doc(512);
DeserializationError err = deserializeJson(doc, body);
if (err) {
server.send(400, "application/json", "{\"ok\":false, \"error\":\"bad json\"}");
return;
}
if (doc.containsKey("mode")) modeSetting = doc["mode"].as<int>();
if (doc.containsKey("time")) mode1Seconds = doc["time"].as<unsigned long>();
if (doc.containsKey("liter")) mode2Liters = doc["liter"].as<float>();
if (doc.containsKey("conv")) conveyorSeconds = doc["conv"].as<unsigned long>();
saveSettings();
server.send(200, "application/json", "{\"ok\":true}");
}
// ========== TASKS ==========
// Core0: Web + LCD + Proximity & Conveyor
void TaskCore0(void* pv) {
(void)pv;
Serial.println("TaskCore0 starting on core " + String(xPortGetCoreID()));
// setup AP and server
WiFi.softAP(ssid, password);
WiFi.softAPConfig(local_IP, gateway, subnet);
Serial.print("AP IP: ");
Serial.println(WiFi.softAPIP());
#if defined(WEB_SERVER_ENABLE_CORS)
server.enableCORS(true);
#endif
server.on("/", handleRoot);
server.on("/status", handleStatus);
server.on("/save", HTTP_POST, handleSave);
server.begin();
Serial.println("Web server started (Core0).");
unsigned long lastLCD = 0;
const unsigned long LCD_INTERVAL = 700;
for (;;) {
// handle web clients frequently
server.handleClient();
// Proximity debounce & control conveyor (INPUT_PULLUP; LOW = detected)
int prox = digitalRead(PROX_PIN);
if (prox != lastProxStable) {
lastProxDebounceMillis = millis();
lastProxStable = prox;
} else {
if ((millis() - lastProxDebounceMillis) > PROX_DEBOUNCE_MS) {
// stable
if (prox == LOW) { // detection
if (!conveyorRunning) {
conveyorRunning = true;
conveyorStartMillis = millis();
conveyorDurationMs = conveyorSeconds * 1000UL;
digitalWrite(CONVEYOR_PIN, HIGH);
Serial.printf("Proximity detected -> Conveyor ON for %lu ms\n", conveyorDurationMs);
}
}
// If prox is HIGH we do nothing special: conveyor will auto-stop after duration
}
}
// conveyor timing (non-blocking)
if (conveyorRunning) {
if (millis() - conveyorStartMillis >= conveyorDurationMs) {
conveyorRunning = false;
digitalWrite(CONVEYOR_PIN, LOW);
Serial.println("Conveyor auto-stopped (Core0).");
}
}
// LCD update: read totalLiters safely
if (millis() - lastLCD >= LCD_INTERVAL) {
float safeLit = 0.0;
if (xLitMutex != NULL && xSemaphoreTake(xLitMutex, (TickType_t)10) == pdTRUE) {
safeLit = totalLiters;
xSemaphoreGive(xLitMutex);
} else {
safeLit = totalLiters;
}
// Update LCD content
lcd.clear();
switch (modeSetting) {
case 1:
lcd.print("Mode1: Time ");
lcd.setCursor(0, 1);
lcd.print("Time:");
lcd.print(mode1Seconds);
lcd.print("s ");
break;
case 2:
lcd.print("Mode2: Volume");
lcd.setCursor(0, 1);
lcd.print(safeLit, 2);
lcd.print("/");
lcd.print(mode2Liters);
lcd.print("L");
break;
default:
lcd.print("Mode3 Disabled");
lcd.setCursor(0, 1);
lcd.print("Conv:");
lcd.print(conveyorSeconds);
break;
}
lastLCD = millis();
}
vTaskDelay(pdMS_TO_TICKS(40)); // yield
}
}
// Core1: Flow aggregation and Pump control + Button handling
TaskHandle_t pumpTaskHandle = NULL;
void PumpTask(void* pv) {
(void)pv;
Serial.println("PumpTask starting on core " + String(xPortGetCoreID()));
for (;;) {
// Wait to be notified by the Button task when a start is requested
// ulTaskNotifyTake blocks until a notification arrives (or timeout)
uint32_t notified = ulTaskNotifyTake(pdTRUE, portMAX_DELAY); // clear on receive
if (notified > 0) {
// If mode disabled ignore
if (modeSetting == 3) {
Serial.println("PumpTask: mode disabled -> ignoring start");
continue;
}
if (pumpRunning) {
Serial.println("PumpTask: pump already running -> ignoring");
continue;
}
// Start pump according to mode
if (modeSetting == 1) {
// Time based
pumpRunning = true;
digitalWrite(RELAY_PIN, HIGH);
Serial.printf("PumpTask: time-based run for %lu s\n", mode1Seconds);
unsigned long start = millis();
while (millis() - start < mode1Seconds * 1000UL) {
aggregateFlow(); // keep aggregating flow during pump
vTaskDelay(pdMS_TO_TICKS(200));
}
digitalWrite(RELAY_PIN, LOW);
pumpRunning = false;
Serial.println("PumpTask: time run finished.");
} else if (modeSetting == 2) {
// Volume based
pumpRunning = true;
// Reset flow counters safely
if (xLitMutex != NULL && xSemaphoreTake(xLitMutex, (TickType_t)10) == pdTRUE) {
totalLiters = 0.0;
xSemaphoreGive(xLitMutex);
} else {
totalLiters = 0.0;
}
noInterrupts();
pulseCount = 0;
interrupts();
digitalWrite(RELAY_PIN, HIGH);
unsigned long startMillis = millis();
Serial.printf("PumpTask: volume-based target %.2f L\n", mode2Liters);
while (true) {
aggregateFlow();
float safeLit = 0.0;
if (xLitMutex != NULL && xSemaphoreTake(xLitMutex, (TickType_t)10) == pdTRUE) {
safeLit = totalLiters;
xSemaphoreGive(xLitMutex);
} else {
safeLit = totalLiters;
}
if (safeLit >= mode2Liters) {
Serial.printf("PumpTask: target reached %.3f L\n", safeLit);
break;
}
// safety timeout (10 min)
if (millis() - startMillis > 10UL * 60UL * 1000UL) {
Serial.println("PumpTask: timeout waiting for volume -> abort");
break;
}
vTaskDelay(pdMS_TO_TICKS(250));
}
digitalWrite(RELAY_PIN, LOW);
pumpRunning = false;
Serial.println("PumpTask: volume run finished.");
} // modes
}
}
}
void ButtonTask(void* pv) {
(void)pv;
Serial.println("ButtonTask starting on core " + String(xPortGetCoreID()));
int lastStable = HIGH;
lastButtonStable = HIGH;
for (;;) {
int b = digitalRead(BUTTON_PIN);
if (b != lastStable) {
lastButtonDebounceMillis = millis();
lastStable = b;
} else {
if ((millis() - lastButtonDebounceMillis) > BUTTON_DEBOUNCE_MS) {
// falling edge detection
static int prevState = HIGH;
if (b == LOW && prevState == HIGH) {
Serial.println("ButtonTask: button pressed -> notify pump");
// Notify (unblock) PumpTask
if (pumpTaskHandle != NULL) {
xTaskNotifyGive(pumpTaskHandle);
}
}
prevState = b;
}
}
// also aggregate flow here regularly
aggregateFlow();
vTaskDelay(pdMS_TO_TICKS(40));
}
}
// ========== SETUP & LOOP ==========
void setup() {
Serial.begin(115200);
delay(50);
// pins
pinMode(RELAY_PIN, OUTPUT);
pinMode(CONVEYOR_PIN, OUTPUT);
pinMode(BUTTON_PIN, INPUT_PULLUP);
pinMode(FLOW_SENSOR_PIN, INPUT_PULLUP);
pinMode(PROX_PIN, INPUT_PULLUP);
digitalWrite(RELAY_PIN, LOW);
digitalWrite(CONVEYOR_PIN, LOW);
attachInterrupt(digitalPinToInterrupt(FLOW_SENSOR_PIN), flowISR, RISING);
// lcd
lcd.init();
lcd.backlight();
// mutex
xLitMutex = xSemaphoreCreateMutex();
// load settings
loadSettings();
// create tasks pinned to cores
BaseType_t r0 = xTaskCreatePinnedToCore(TaskCore0, "TaskCore0", 8192, NULL, 1, NULL, 0); // Core 0
BaseType_t rPump = xTaskCreatePinnedToCore(PumpTask, "PumpTask", 8192, NULL, 2, &pumpTaskHandle, 1); // Core 1
BaseType_t rButton = xTaskCreatePinnedToCore(ButtonTask, "ButtonTask", 4096, NULL, 2, NULL, 1); // Core 1
if (r0 != pdPASS || rPump != pdPASS || rButton != pdPASS) {
Serial.println("Task creation failed!");
} else {
Serial.println("Tasks created successfully.");
}
}
void loop() {
// Empty - tasks do the work
vTaskDelay(pdMS_TO_TICKS(1000));
}