Hi everyone,
I am a beginner in industrial electronics, so please go easy on me. I’ve built an automated liquid filling machine. It works perfectly for a short time, but then completely loses its accuracy. I suspect a hardware/power/thermal issue and really need your advice before I tear the whole panel apart.
The Hardware Setup:
- MCU: Dual-Core ESP32.
- ADC: HX711 (set to 80Hz mode) reading a load cell.
- Pump: 220V AC high-flow pump controlled by a VFD. It fills 5 Liters in about 15 seconds (~333g/sec).
- Power Supply: A cheap 5V / 0.6A Switch-Mode Power Supply (SMPS). It powers the ESP32, the optocouplers, the HX711, and 3 reed switches.
Outputs (Galvanically Isolated):
I have two types of outputs:
- Pump control: A simple optocoupler that closes the VFD contacts to turn the pump on/off.
- Pneumatic valves (220V AC): Controlled via triacs. The circuit is: ESP32 pin $\rightarrow$ 150 Ohm resistor $\rightarrow$ MOC3052 opto-triac $\rightarrow$ BT131 triac $\rightarrow$ output terminal.
HX711 Protection & Shielding (What I've already done):
To protect the ADC from the VFD noise, I moved the HX711 to a separate small top box on its own perfboard.
- I added a 470uF capacitor specifically on the HX711 power input.
- I carefully wrapped the contacts with electrical tape.
- I built a Faraday cage by hot-gluing an aluminum foil shield around the HX711 board.
- I grounded this foil shield, AND the shield of the cable running from the ESP32 to the HX711, directly to the SMPS Ground.
The Physical Layout (The "Spaghetti" Panel):
Everything else is crammed into one plastic enclosure. My ESP32 perfboard is mounted just a few inches away from the VFD. The 220V AC wires and the low-voltage wires are somewhat separated but not perfectly isolated in dedicated cable ducts. Currently, there are NO bypass capacitors on the 5V line right at the ESP32 itself.
The Symptoms:
When the machine is "cold" (e.g., after sitting off for a day), it works flawlessly. It perfectly fills 20 consecutive 5-Liter bottles with an amazing accuracy of $\pm 5$ to $8$ grams.
However, right after the 20th bottle (about 10 minutes of continuous work), the weight readings start jumping randomly. The scatter goes up to $\pm 30$g or worse. If I let it cool down, it works perfectly again.
My Hypotheses & Questions for the pros:
- Power Supply Overheating? Since my SMPS is only 0.6A, and the ESP32 (running a WiFi AP) + optocouplers draw near 400-500mA peaks, is the SMPS overheating after 10 mins and sending heavy voltage ripples to the ESP32, causing it to drop ADC ticks?
- Capacitors on ESP32: I have 470uF on the HX711, but should I absolutely add a 1000uF electrolytic + a 0.1uF ceramic capacitor directly to the 5V/GND pins of the ESP32 to buffer the WiFi power spikes?
- Software Issue? I'm using FreeRTOS to separate WiFi/Web (Core 1) from the HX711 reading (Core 0). Could my code be causing this drift over time?
Here is my code:
#include <Arduino.h>
#include <WiFi.h>
#include <esp_wifi.h>
#include <AsyncTCP.h>
#include <ESPAsyncWebServer.h>
#include <ArduinoJson.h>
#include <Preferences.h>
#include <freertos/FreeRTOS.h>
#include <freertos/task.h>
#include <atomic>
Preferences preferences;
#define PIN_IN_TRAY 16
#define PIN_IN_HEAD_UP 17
#define PIN_IN_HEAD_DOWN 21
#define HX_COMMON_SCK 18
#define HX_DT_PLATFORM 23
#define PIN_PUMP_MAIN 4
#define PIN_PUMP_AUX 5
#define PIN_NOZZLE 13
#define PIN_WATER 14
#define PIN_AIR 27
#define PIN_PRODUCT 32
#define PIN_HEAD 25
#define PIN_DRIP 33
#define PIN_SPARE_1 26
#define PIN_SPARE_2 15
#define EMERGENCY_VALVE_SHUTOFF() { \
GPIO.out_w1tc = (1 << PIN_PUMP_MAIN) | (1 << PIN_NOZZLE) | (1 << PIN_HEAD); \
GPIO.out1_w1tc.val = (1 << (PIN_PRODUCT - 32)); \
}
AsyncWebServer server(80);
AsyncWebSocket ws("/ws");
std::atomic<float> atomicTargetWeight{5000.0f};
std::atomic<float> atomicWeightOffset{0.0f};
std::atomic<float> weightRealDisplay{0.0f};
std::atomic<int> currentStateAtomic{0};
std::atomic<bool> triggerFillStop{false};
std::atomic<bool> requestSingleTare{false};
volatile float expectedTare = 100.0f;
volatile float tareTolerance = 30.0f;
volatile float calibrationFactor = 107.16f;
int washSec = 5;
int blowSec = 3;
float pauseSec = 2.0f;
int totalBottles = 0;
volatile long tareBase = 0;
portMUX_TYPE mux = portMUX_INITIALIZER_UNLOCKED;
bool isAutoArmed = false;
bool isBottlePresent = false;
unsigned long stableTimer = 0;
unsigned long stateTimer = 0;
char machineStatus[32] = "IDLE";
bool isAutoMode = false;
enum MachineState {
STATE_IDLE = 0,
STATE_TRAY_MOVING,
STATE_HEAD_MOVING,
STATE_FILLING,
STATE_HEAD_LIFTING,
STATE_TRAY_RETURN,
STATE_EMERGENCY_LIFT,
STATE_WASH_PREPARE,
STATE_WASHING,
STATE_WASH_FINISH,
STATE_BLOW_PREPARE,
STATE_BLOWING_PROCESS,
STATE_BLOW_FINISH,
STATE_WAIT_REMOVAL
};
MachineState currentState = STATE_IDLE;
bool isSensorActive(int pin) { return digitalRead(pin) == HIGH; }
uint8_t targetMac[] = {0x68, 0xFE, 0x71, 0x0C, 0x7E, 0x50};
void checkSecurity() {
uint8_t chipMac[6];
WiFi.macAddress(chipMac);
if (memcmp(chipMac, targetMac, sizeof(chipMac)) != 0) {
while (true) { digitalWrite(PIN_PUMP_MAIN, LOW); delay(100); }
}
}
void changeState(MachineState newState) {
currentState = newState;
currentStateAtomic.store((int)newState, std::memory_order_relaxed);
stateTimer = millis();
}
void stopAll() {
EMERGENCY_VALVE_SHUTOFF();
digitalWrite(PIN_WATER, LOW);
digitalWrite(PIN_AIR, LOW);
digitalWrite(PIN_PUMP_AUX, LOW);
digitalWrite(PIN_DRIP, HIGH);
strlcpy(machineStatus, "STOP / RETURN", sizeof(machineStatus));
isAutoMode = false;
isAutoArmed = false;
isBottlePresent = false;
stableTimer = 0;
atomicWeightOffset.store(0.0f, std::memory_order_relaxed);
changeState(STATE_EMERGENCY_LIFT);
}
void startCycle() {
isAutoMode = true;
atomicWeightOffset.store(weightRealDisplay.load(std::memory_order_relaxed), std::memory_order_relaxed);
strlcpy(machineStatus, "START: TRAY", sizeof(machineStatus));
digitalWrite(PIN_DRIP, HIGH);
changeState(STATE_TRAY_MOVING);
}
void checkStartCondition() {
if (!isSensorActive(PIN_IN_HEAD_UP)) {
strlcpy(machineStatus, "ERR: HEAD NOT UP", sizeof(machineStatus));
isAutoArmed = false;
return;
}
if (isSensorActive(PIN_IN_TRAY)) {
strlcpy(machineStatus, "ERR: TRAY IS IN", sizeof(machineStatus));
isAutoArmed = false;
return;
}
startCycle();
}
void startWash() {
strlcpy(machineStatus, "WASH: START", sizeof(machineStatus));
digitalWrite(PIN_DRIP, HIGH);
changeState(STATE_WASH_PREPARE);
}
void startBlow() {
strlcpy(machineStatus, "BLOW: START", sizeof(machineStatus));
digitalWrite(PIN_DRIP, HIGH);
changeState(STATE_BLOW_PREPARE);
}
void togglePin(int pin) {
if (isAutoMode) return;
digitalWrite(pin, !digitalRead(pin));
}
void weightTaskCore(void * pvParameters) {
float localSmoothedWeight = 0.0f;
for(;;) {
unsigned long waitTime = micros();
bool timeout = false;
while(digitalRead(HX_DT_PLATFORM) == HIGH) {
if (micros() - waitTime > 50000) { timeout = true; break; }
}
if (timeout) {
vTaskDelay(pdMS_TO_TICKS(2));
continue;
}
long count = 0;
portENTER_CRITICAL(&mux);
for (int i = 0; i < 24; i++) {
GPIO.out_w1ts = (1 << HX_COMMON_SCK); delayMicroseconds(1);
count = count << 1;
if (GPIO.in & (1 << HX_DT_PLATFORM)) count++;
GPIO.out_w1tc = (1 << HX_COMMON_SCK); delayMicroseconds(1);
}
GPIO.out_w1ts = (1 << HX_COMMON_SCK); delayMicroseconds(1);
GPIO.out_w1tc = (1 << HX_COMMON_SCK);
portEXIT_CRITICAL(&mux);
count ^= 0x800000;
vTaskDelay(pdMS_TO_TICKS(2));
if (requestSingleTare.load(std::memory_order_relaxed)) {
tareBase = count;
localSmoothedWeight = 0.0f;
requestSingleTare.store(false, std::memory_order_relaxed);
}
double currentRawDouble = (double)(count - tareBase) / (double)calibrationFactor;
int currentStateCore1 = currentStateAtomic.load(std::memory_order_relaxed);
if (currentStateCore1 == STATE_HEAD_MOVING) {
continue;
}
float alpha = 0.05f;
if (localSmoothedWeight == 0.0f || (currentStateCore1 == STATE_IDLE && abs(currentRawDouble - localSmoothedWeight) > 100.0)) {
localSmoothedWeight = (float)currentRawDouble;
} else {
localSmoothedWeight = (alpha * currentRawDouble) + ((1.0f - alpha) * localSmoothedWeight);
}
weightRealDisplay.store(localSmoothedWeight, std::memory_order_relaxed);
if (currentStateCore1 == STATE_FILLING) {
float target = atomicTargetWeight.load(std::memory_order_relaxed);
float offset = atomicWeightOffset.load(std::memory_order_relaxed);
float liquidWeight = localSmoothedWeight - offset;
if (liquidWeight >= target) {
EMERGENCY_VALVE_SHUTOFF();
currentStateAtomic.store(STATE_HEAD_LIFTING, std::memory_order_relaxed);
triggerFillStop.store(true, std::memory_order_relaxed);
}
}
}
}
const char index_html[] PROGMEM = R"rawliteral(
<!DOCTYPE HTML><html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
<title>REAKTOR HMI</title>
<style>
* { box-sizing: border-box; -webkit-tap-highlight-color: transparent; }
body { background-color: #121212; color: white; font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; margin: 0; padding: 0; width: 100vh; height: 100vw; transform: rotate(90deg); transform-origin: top left; position: absolute; top: 0; left: 100%; overflow: hidden; user-select: none; }
.container { display: grid; grid-template-columns: 1fr 1fr; gap: 1vh; height: 100%; padding: 1vh 1vw; }
.col { display: flex; flex-direction: column; gap: 1vh; height: 100%; }
.box { background: #1e1e2e; border-radius: 10px; padding: 1vh; border: 1px solid #444; }
.box-green { border-color: #2e7d32; background: #1b3a1b; }
.box-red { border-color: #c62828; background: #2b0b0b; margin-top: 2vh; }
.row { display: flex; justify-content: space-between; align-items: center; margin-bottom: 0.5vh; }
.label { font-size: 2.2vh; font-weight: bold; color: #ddd; }
input[type="number"] { background: #333; border: 1px solid #555; color: #fff; font-size: 3vh; height: 5vh; width: 15vw; text-align: center; border-radius: 5px; padding: 0; outline: none;}
.big-val { font-size: 6vh; font-weight: bold; color: #fff; background: #303050; padding: 0 15px; border-radius: 5px; min-width: 120px; text-align: right; line-height: 1.2;}
.btn { border: none; border-radius: 5px; cursor: pointer; color: white; font-weight: bold; text-transform: uppercase; padding: 0; text-align: center; display: flex; align-items: center; justify-content: center;}
.btn:active { transform: scale(0.98); opacity: 0.8; }
.btn-sq { width: 6vh; height: 6vh; font-size: 2vh; background: #444; border: 2px solid #888; }
.btn-sq.active { background: #00e676; border-color: #fff; color:black;}
.btn-big { width: 100%; height: 12vh; font-size: 4vh; margin-top: 1vh; background: #2e7d32; }
.btn-stop { width: 100%; height: 12vh; font-size: 4vh; background: #d32f2f; border: 2px solid #ff5252; }
.led { width: 3vh; height: 3vh; border-radius: 50%; background: #333; border: 2px solid #555; display:inline-block;}
.led.on { background: #00bcd4; border-color: #fff; box-shadow: 0 0 10px #00bcd4; }
.grid-manual { display: grid; grid-template-columns: repeat(4, 1fr); gap: 1vh; }
.man-btn { width: 100%; height: 7vh; background: #555; border: 1px solid #777; border-radius: 5px; font-size: 1.8vh; color:white; }
.man-btn.active { background: #ff9800; color: black; border-color: white; }
</style>
</head>
<body>
<div class="container">
<div class="col">
<div class="box">
<div class="row">
<div class="big-val" id="val_weight">0</div>
<div style="text-align:right">
<div style="font-size:2vh; color:#aaa">РАЗОМ</div>
<div style="font-size:4vh; font-weight:bold" id="val_total">0</div>
</div>
</div>
<div class="row">
<div class="label">ЗАВДАННЯ</div>
<input type="number" id="inp_target" value="5000" onchange="sendCfg()">
<button class="btn btn-sq" onclick="send('RST_CNT')">000</button>
</div>
</div>
<div class="box">
<div class="row">
<div class="label">ТАРА (ГР)</div>
<input type="number" id="inp_tare_val" value="100" onchange="sendCfg()">
<button class="btn btn-sq" onclick="send('TARE')">TARA</button>
</div>
<div class="row">
<div class="label">ПАУЗА (СЕК)</div>
<input type="number" id="inp_pause" value="2" step="0.1" onchange="sendCfg()">
</div>
<div class="row">
<div class="label">ПРОДУВ (СЕК)</div>
<input type="number" id="inp_blow" value="3" onchange="sendCfg()">
</div>
<div class="row">
<div class="label">ПРОМИВ (СЕК)</div>
<input type="number" id="inp_wash" value="5" onchange="sendCfg()">
</div>
<div class="row">
<div class="label">КОЕФ.</div>
<input type="number" id="inp_coef" value="107.16" onchange="sendCfg()">
</div>
</div>
<div class="box">
<div style="font-size:2vh; color:#888;">СТАТУС:</div>
<div style="font-size:3vh; font-weight:bold; color:#00e676" id="status_text">---</div>
<div class="row" style="margin-top:10px">
<div><span class="led" id="led_up"></span> ВЕРХ</div>
<div><span class="led" id="led_down"></span> НИЗ</div>
<div><span class="led" id="led_tray"></span> ЛОТОК</div>
</div>
</div>
</div>
<div class="col">
<div class="box box-green">
<div class="label" style="text-align:center">АВТОМАТ</div>
<button class="btn btn-big" id="btn_auto" onclick="send('START')">СТАРТ ЦИКЛУ</button>
<div class="row" style="margin-top:10px">
<button class="btn" style="background:#0288d1; flex:1; margin-right:5px; height:8vh; font-size:2.5vh" onclick="send('BLOW_START')">ПРОДУВКА</button>
<button class="btn" style="background:#7b1fa2; flex:1; height:8vh; font-size:2.5vh" onclick="send('WASH_START')">ПРОМИВКА</button>
</div>
</div>
<div class="box">
<div class="label" style="text-align:center">РУЧНЕ КЕРУВАННЯ</div>
<div class="grid-manual">
<button class="man-btn" id="m_head" onclick="send('M_HEAD')">ГОЛОВА</button>
<button class="man-btn" id="m_tray" onclick="send('M_TRAY')">ЛОТОК</button>
<button class="man-btn" id="m_air" onclick="send('M_AIR')">ПОВІТРЯ</button>
<button class="man-btn" id="m_water" onclick="send('M_WATER')">ВОДА</button>
<button class="man-btn" id="m_prod" onclick="send('M_PROD')">СИРОВ</button>
<button class="man-btn" id="m_noz" onclick="send('M_NOZ')">СОПЛО</button>
<button class="man-btn" id="m_pump" onclick="send('M_PUMP')">НАСОС</button>
<button class="man-btn" style="background:#555" onclick="send('REBOOT')">REBOOT</button>
</div>
</div>
<div class="box box-red">
<button class="btn btn-stop" onclick="send('STOP')">СТОП / АВАРІЯ</button>
</div>
</div>
</div>
<script>
var ws = new WebSocket(`ws://${location.hostname}/ws`);
ws.onmessage = function(e) {
var d = JSON.parse(e.data);
document.getElementById('val_weight').innerText = d.w;
document.getElementById('val_total').innerText = d.cnt;
document.getElementById('status_text').innerText = d.st;
setLed('led_up', d.s[0]);
setLed('led_tray', d.s[1]);
setLed('led_down', d.s[2]);
setMan('m_tray', d.o[0]);
setMan('m_head', d.o[1]);
setMan('m_noz', d.o[2]);
setMan('m_pump', d.o[3]);
setMan('m_prod', d.o[4]);
setMan('m_air', d.o[5]);
setMan('m_water', d.o[6]);
if(d.auto) document.getElementById('btn_auto').style.background = "#00e676";
else document.getElementById('btn_auto').style.background = "#2e7d32";
if(d.cfg && !window.cfgLoaded) {
document.getElementById('inp_target').value = d.cfg.t;
document.getElementById('inp_tare_val').value = d.cfg.tar;
document.getElementById('inp_pause').value = d.cfg.p;
document.getElementById('inp_blow').value = d.cfg.b;
document.getElementById('inp_wash').value = d.cfg.w;
document.getElementById('inp_coef').value = d.cfg.k;
window.cfgLoaded = true;
}
};
function setLed(id, s) { var el = document.getElementById(id); if(s == '1') el.classList.add('on'); else el.classList.remove('on'); }
function setMan(id, s) { var el = document.getElementById(id); if(s == '1') el.classList.add('active'); else el.classList.remove('active'); }
function send(cmd) { ws.send(JSON.stringify({c: cmd})); }
function sendCfg() {
var t = document.getElementById('inp_target').value;
var tar = document.getElementById('inp_tare_val').value;
var k = document.getElementById('inp_coef').value;
var p = document.getElementById('inp_pause').value;
var b = document.getElementById('inp_blow').value;
var w = document.getElementById('inp_wash').value;
ws.send(JSON.stringify({ c: 'CFG', t: t, tar: tar, k: k, p: p, b: b, w: w }));
}
</script>
</body></html>
)rawliteral";
void handleWS(void *arg, uint8_t *data, size_t len) {
AwsFrameInfo *info = (AwsFrameInfo*)arg;
if (info->final && info->index == 0 && info->len == len && info->opcode == WS_TEXT) {
JsonDocument doc;
DeserializationError error = deserializeJson(doc, data, len);
if (error) return;
const char* cmd = doc["c"];
if (strcmp(cmd, "START") == 0) {
isAutoArmed = true;
strlcpy(machineStatus, "ARMED: WAIT TARE", sizeof(machineStatus));
}
else if (strcmp(cmd, "STOP") == 0) stopAll();
else if (strcmp(cmd, "TARE") == 0) { requestSingleTare.store(true, std::memory_order_relaxed); }
else if (strcmp(cmd, "RST_CNT") == 0) totalBottles = 0;
else if (strcmp(cmd, "REBOOT") == 0) ESP.restart();
else if (strcmp(cmd, "WASH_START") == 0) { startWash(); }
else if (strcmp(cmd, "BLOW_START") == 0) { startBlow(); }
else if (strcmp(cmd, "M_HEAD") == 0) {
if (digitalRead(PIN_HEAD) == HIGH) { digitalWrite(PIN_HEAD, LOW); }
else {
if (isSensorActive(PIN_IN_TRAY)) { digitalWrite(PIN_HEAD, HIGH); }
else { strlcpy(machineStatus, "ERR: TRAY IS OUT!", sizeof(machineStatus)); }
}
}
else if (strcmp(cmd, "M_TRAY") == 0) {
if (digitalRead(PIN_DRIP) == HIGH) {
if (isSensorActive(PIN_IN_HEAD_UP)) { digitalWrite(PIN_DRIP, LOW); }
else { strlcpy(machineStatus, "ERR: HEAD IS DOWN!", sizeof(machineStatus)); }
} else { digitalWrite(PIN_DRIP, HIGH); }
}
else if (strcmp(cmd, "M_NOZ") == 0) togglePin(PIN_NOZZLE);
else if (strcmp(cmd, "M_PUMP") == 0) togglePin(PIN_PUMP_MAIN);
else if (strcmp(cmd, "M_PROD") == 0) togglePin(PIN_PRODUCT);
else if (strcmp(cmd, "M_AIR") == 0) togglePin(PIN_AIR);
else if (strcmp(cmd, "M_WATER") == 0) togglePin(PIN_WATER);
else if (strcmp(cmd, "CFG") == 0) {
if(doc.containsKey("t")) { atomicTargetWeight.store(doc["t"].as<float>(), std::memory_order_relaxed); preferences.putFloat("tW", doc["t"].as<float>()); }
if(doc.containsKey("tar")) { expectedTare = doc["tar"].as<float>(); preferences.putFloat("tar", expectedTare); }
if(doc.containsKey("p")) { pauseSec = doc["p"].as<float>(); preferences.putFloat("p", pauseSec); }
if(doc.containsKey("b")) { blowSec = doc["b"].as<int>(); preferences.putInt("b", blowSec); }
if(doc.containsKey("w")) { washSec = doc["w"].as<int>(); preferences.putInt("w", washSec); }
if(doc.containsKey("k")) {
calibrationFactor = doc["k"].as<float>();
preferences.putFloat("calFactor", calibrationFactor);
}
}
}
}
void onEvent(AsyncWebSocket *server, AsyncWebSocketClient *client, AwsEventType type, void *arg, uint8_t *data, size_t len) {
if (type == WS_EVT_DATA) handleWS(arg, data, len);
}
void setup() {
Serial.begin(115200);
preferences.begin("reaktor", false);
calibrationFactor = preferences.getFloat("calFactor", 107.16);
atomicTargetWeight.store(preferences.getFloat("tW", 5000.0), std::memory_order_relaxed);
expectedTare = preferences.getFloat("tar", 100.0);
pauseSec = preferences.getFloat("p", 2.0);
blowSec = preferences.getInt("b", 3);
washSec = preferences.getInt("w", 5);
checkSecurity();
pinMode(PIN_PUMP_MAIN, OUTPUT); digitalWrite(PIN_PUMP_MAIN, LOW);
pinMode(PIN_PUMP_AUX, OUTPUT); digitalWrite(PIN_PUMP_AUX, LOW);
pinMode(PIN_NOZZLE, OUTPUT); digitalWrite(PIN_NOZZLE, LOW);
pinMode(PIN_WATER, OUTPUT); digitalWrite(PIN_WATER, LOW);
pinMode(PIN_PRODUCT, OUTPUT); digitalWrite(PIN_PRODUCT, LOW);
pinMode(PIN_AIR, OUTPUT); digitalWrite(PIN_AIR, LOW);
pinMode(PIN_HEAD, OUTPUT); digitalWrite(PIN_HEAD, LOW);
pinMode(PIN_DRIP, OUTPUT); digitalWrite(PIN_DRIP, LOW);
pinMode(PIN_IN_TRAY, INPUT_PULLDOWN);
pinMode(PIN_IN_HEAD_UP, INPUT_PULLDOWN);
pinMode(PIN_IN_HEAD_DOWN, INPUT_PULLDOWN);
pinMode(HX_DT_PLATFORM, INPUT);
pinMode(HX_COMMON_SCK, OUTPUT);
digitalWrite(HX_COMMON_SCK, LOW);
xTaskCreatePinnedToCore(
weightTaskCore,
"WeightTask",
4096,
NULL,
configMAX_PRIORITIES - 1,
NULL,
0);
WiFi.mode(WIFI_MODE_AP);
WiFi.softAP("REAKTOR_PANEL", "88888888");
ws.onEvent(onEvent);
server.addHandler(&ws);
server.on("/", HTTP_GET, [](AsyncWebServerRequest *req){ req->send_P(200, "text/html", index_html); });
server.begin();
requestSingleTare.store(true, std::memory_order_relaxed);
}
void loop() {
ws.cleanupClients();
if (triggerFillStop.load(std::memory_order_relaxed)) {
triggerFillStop.store(false, std::memory_order_relaxed);
changeState(STATE_HEAD_LIFTING);
strlcpy(machineStatus, "HEAD UP...", sizeof(machineStatus));
}
switch (currentState) {
case STATE_IDLE:
if (isAutoArmed) {
float displayReal = weightRealDisplay.load(std::memory_order_relaxed);
if (abs(displayReal - expectedTare) <= tareTolerance) {
if (!isBottlePresent) {
stableTimer = millis();
isBottlePresent = true;
strlcpy(machineStatus, "TARE OK. PAUSE...", sizeof(machineStatus));
} else {
if (millis() - stableTimer > (pauseSec * 1000)) {
checkStartCondition();
}
}
} else {
isBottlePresent = false;
if (isAutoArmed) strlcpy(machineStatus, "ARMED: WAIT TARE", sizeof(machineStatus));
}
}
break;
case STATE_EMERGENCY_LIFT:
if (isSensorActive(PIN_IN_HEAD_UP)) {
strlcpy(machineStatus, "STOP: OK (MANUAL TRAY)", sizeof(machineStatus));
changeState(STATE_IDLE);
}
break;
case STATE_TRAY_MOVING:
if (millis() - stateTimer > 5000) { stopAll(); strlcpy(machineStatus, "ERR: TRAY TIMEOUT", sizeof(machineStatus)); break; }
if (isSensorActive(PIN_IN_TRAY)) {
digitalWrite(PIN_HEAD, HIGH);
strlcpy(machineStatus, "HEAD DOWN...", sizeof(machineStatus));
changeState(STATE_HEAD_MOVING);
}
break;
case STATE_HEAD_MOVING:
if (millis() - stateTimer > 5000) { stopAll(); strlcpy(machineStatus, "ERR: HEAD TIMEOUT", sizeof(machineStatus)); break; }
if (isSensorActive(PIN_IN_HEAD_DOWN)) {
if (millis() - stateTimer > 150) {
digitalWrite(PIN_NOZZLE, HIGH);
digitalWrite(PIN_PRODUCT, HIGH);
digitalWrite(PIN_PUMP_MAIN, HIGH);
strlcpy(machineStatus, "FILLING...", sizeof(machineStatus));
changeState(STATE_FILLING);
}
} else {
stateTimer = millis();
}
break;
case STATE_FILLING:
break;
case STATE_HEAD_LIFTING:
if (millis() - stateTimer < 500) break;
if (isSensorActive(PIN_IN_HEAD_UP)) {
delay(100);
if (isSensorActive(PIN_IN_HEAD_UP)) {
digitalWrite(PIN_DRIP, LOW);
strlcpy(machineStatus, "TRAY OUT...", sizeof(machineStatus));
changeState(STATE_TRAY_RETURN);
}
}
break;
case STATE_TRAY_RETURN:
if (millis() - stateTimer > 1000) {
totalBottles++;
strlcpy(machineStatus, "REMOVE BOTTLE", sizeof(machineStatus));
isAutoMode = false;
changeState(STATE_WAIT_REMOVAL);
}
break;
case STATE_WAIT_REMOVAL:
if (weightRealDisplay.load(std::memory_order_relaxed) < 20.0) {
if (millis() - stateTimer > 1000) {
strlcpy(machineStatus, "IDLE", sizeof(machineStatus));
isBottlePresent = false;
changeState(STATE_IDLE);
}
} else {
stateTimer = millis();
}
break;
case STATE_WASH_PREPARE:
if (millis() - stateTimer > 5000) { stopAll(); strlcpy(machineStatus, "ERR: TRAY TIMEOUT", sizeof(machineStatus)); break; }
if (isSensorActive(PIN_IN_TRAY)) {
digitalWrite(PIN_HEAD, HIGH);
changeState(STATE_WASHING);
}
break;
case STATE_WASHING:
if (isSensorActive(PIN_IN_HEAD_DOWN)) {
digitalWrite(PIN_WATER, HIGH);
digitalWrite(PIN_PRODUCT, HIGH);
digitalWrite(PIN_NOZZLE, HIGH);
digitalWrite(PIN_PUMP_MAIN, HIGH);
if (millis() - stateTimer > (washSec * 1000)) stopAll();
}
break;
case STATE_WASH_FINISH: break;
case STATE_BLOW_PREPARE:
if (millis() - stateTimer > 5000) { stopAll(); strlcpy(machineStatus, "ERR: TRAY TIMEOUT", sizeof(machineStatus)); break; }
if (isSensorActive(PIN_IN_TRAY)) {
digitalWrite(PIN_HEAD, HIGH);
changeState(STATE_BLOWING_PROCESS);
}
break;
case STATE_BLOWING_PROCESS:
if (isSensorActive(PIN_IN_HEAD_DOWN)) {
digitalWrite(PIN_AIR, HIGH);
if (millis() - stateTimer > (blowSec * 1000)) stopAll();
}
break;
case STATE_BLOW_FINISH: break;
}
static unsigned long t = 0;
if (millis() - t > 400) {
char jsonBuf[384];
float displayW;
float currentReal = weightRealDisplay.load(std::memory_order_relaxed);
if (currentState == STATE_FILLING) {
float offset = atomicWeightOffset.load(std::memory_order_relaxed);
displayW = currentReal - offset;
} else {
displayW = currentReal;
}
snprintf(jsonBuf, sizeof(jsonBuf),
"{\"w\":\"%.0f\",\"cnt\":\"%d\",\"st\":\"%s\",\"s\":\"%d%d%d\",\"o\":\"%d%d%d%d%d%d%d\",\"auto\":%s,\"cfg\":{\"t\":%.0f,\"tar\":%.0f,\"p\":%.1f,\"b\":%d,\"w\":%d,\"k\":%.2f}}",
displayW, totalBottles, machineStatus,
isSensorActive(PIN_IN_HEAD_UP), isSensorActive(PIN_IN_TRAY), isSensorActive(PIN_IN_HEAD_DOWN),
digitalRead(PIN_DRIP), digitalRead(PIN_HEAD), digitalRead(PIN_NOZZLE), digitalRead(PIN_PUMP_MAIN),
digitalRead(PIN_PRODUCT), digitalRead(PIN_AIR), digitalRead(PIN_WATER),
isAutoArmed ? "true" : "false",
atomicTargetWeight.load(std::memory_order_relaxed), expectedTare, pauseSec, blowSec, washSec, calibrationFactor
);
ws.textAll(jsonBuf);
t = millis();
}
}
(Special thanks @PaulRB and @build_1971 for the tips on how to make this code better)
I know the physical wiring is a bit of a mess, but considering it works perfectly for the first 20 cycles, I want to know if soldering the capacitors to the ESP32 and/or upgrading the PSU to 2A is the most logical first step to fix this thermal/time-based drift.
Any advice, harsh truths, or tips for a newbie are highly appreciated! Thank you!


