In this sixth part of our IoT project series, we explore automated water level control with ESP32-based sensing and pump management. This project demonstrates industrial-grade liquid level monitoring, automated fill/drain control, and advanced dashboard visualization with real-time plotting and dual-mode operation.
Project OverviewThis ESP32 project implements a comprehensive water level control system with MQTT communication and intelligent automation. It features an HC-SR04 ultrasonic sensor for precise distance measurement, dual relay modules for fill/drain valve control, and real-time monitoring with hysteresis-based control logic. The Wokwi simulation shows a professional industrial setup:
- HC-SR04 Ultrasonic Sensor: Connected to GPIO 22 (TRIG) and GPIO 23 (ECHO) for distance measurement
- Relay Module 1 (FILL): Connected to GPIO 17 for fill valve control
- Relay Module 2 (DRAIN): Connected to GPIO 16 for drain valve control
- Power Distribution: 5V supply for relays, 3.3V for ultrasonic sensor, common ground configuration
- Safety: Interlocked relay operation prevents simultaneous fill/drain activation
The firmware uses a state-machine approach with four core components. Connection Management handles WiFi with retry logic, MQTT with authentication, and automatic reconnection. Sensor Processing provides ultrasonic distance measurement with pulse timing, percentage conversion (400cm=0%, 0cm=100%), and 500ms update intervals. Relay Control implements safety interlocks, individual valve control functions, and fail-safe startup configuration. System Activation features MQTT-based enable/disable, status reporting, and controlled operation modes.
MQTT Communication Protocol
The system uses structured MQTT topics for comprehensive control:
arduino/sensor
- Water level data publishingarduino/sensor_Control
- Valve control commandsmqtt/request
- System activation and status requestsmqtt/response
- Device status and health reports
Message Formats:
Sensor Data: "Water Level: 45"
Control Commands: "FILL_ON DRAIN_OFF", "FILL_OFF DRAIN_ON", "FILL_DRAIN_OFF"
Status Response: "Board : ESP32 Status : Connected"
Status Request: "status_request"
Key Firmware Features
Ultrasonic Distance Measurement:
void measureAndPublishWaterLevel() {
// Trigger ultrasonic pulse
digitalWrite(trigPin, LOW);
delayMicroseconds(2);
digitalWrite(trigPin, HIGH);
delayMicroseconds(10);
digitalWrite(trigPin, LOW);
// Measure echo duration
long duration = pulseIn(echoPin, HIGH);
// Calculate distance and convert to percentage
int distance = duration * 0.034 / 2;
int waterLevelPercent = constrain(100 - (distance * 100 / 400), 0, 100);
char sensorMessage[50];
snprintf(sensorMessage, sizeof(sensorMessage), "Water Level: %d", waterLevelPercent);
mqttClient.publish(mqttTopicSensor, sensorMessage);
}
Safety-Interlocked Valve Control:
void Fill() {
digitalWrite(relayIn, HIGH); // Turn on fill valve
digitalWrite(relayOut, LOW); // Turn off drain valve
}
void Drain() {
digitalWrite(relayIn, LOW); // Turn off fill valve
digitalWrite(relayOut, HIGH); // Turn on drain valve
}
void FillDrain_OFF() {
digitalWrite(relayIn, LOW); // Turn off fill valve
digitalWrite(relayOut, LOW); // Turn off drain valve
}
Command Processing with System Control:
void handleMQTTCallback(char* topic, byte* payload, unsigned int length) {
if (systemActive && topicStr == mqttTopicSensorControl) {
if (message == "FILL_ON DRAIN_OFF") {
Fill();
} else if (message == "FILL_OFF DRAIN_ON") {
Drain();
} else if (message == "FILL_DRAIN_OFF") {
FillDrain_OFF();
}
}
}
PyQt5 Dashboard IntegrationThe dashboard interface provides comprehensive control through six main sections:
- MQTT Status Panel - Connection monitoring with LED indicators
- Water Level Display - Real-time percentage with progress bar visualization
- Mode Control - Toggle switch for Automatic/Manual operation modes
- Manual Controls - Fill/Drain buttons for manual valve operation
- Target Level Configuration - Slider-based setpoint adjustment
- History Plot - Real-time PyQtGraph visualization with target level overlay
Signal-Based Architecture
Signal-slot mechanism for thread-safe updates:
class WaterLevelControllerWindow(QObject):
# Signal definitions for thread-safe UI updates
water_level_changed = pyqtSignal(float)
status_update = pyqtSignal(str, str)
update_plot_signal = pyqtSignal()
board_connected_signal = pyqtSignal(bool)
log_message_signal = pyqtSignal(str)
Thread-Safe UI Updates:
@pyqtSlot(float)
def update_water_level_ui(self, level):
if hasattr(self.ui, 'tankProgressBar'):
self.ui.tankProgressBar.setValue(int(level))
if hasattr(self.ui, 'levelDisplay'):
self.ui.levelDisplay.setText(f"{level:.1f}%")
@pyqtSlot()
def update_history_plot(self):
if len(self.history_timestamps) > 0:
self.history_curve.setData(list(self.history_timestamps), list(self.history_levels))
self.target_line.setData(
[min(self.history_timestamps), max(self.history_timestamps)],
[self._target_level, self._target_level]
)
MQTT Message Handling
Water Level Processing:
def handle_sensor_message(self, topic, payload):
try:
if payload.startswith("Water Level:"):
level_str = payload.split(':')[1].strip().split('%')[0].strip()
new_level = float(level_str)
self._water_level = new_level
self.add_to_history(new_level)
self.water_level_changed.emit(new_level)
except Exception as e:
print(f"Sensor message error: {e}")
Status Monitoring with Flag Control:
def handle_status_message3(self, topic, payload):
if not self._waiting_for_status:
return
try:
if "Board :" in payload and "Status :" in payload:
status = status_part.strip().lower()
self.status_received = True
if status == "connected":
self._board_connected = True
self.board_connected_signal.emit(True)
except Exception as e:
print(f"Status message error: {e}")
Control Flow Analysis
Automatic Mode Operation:
- Level Monitoring: Continuous water level measurement from ultrasonic sensor
- Target Comparison: Dashboard compares current level against target setpoint
- Control Decision: Automatic logic determines fill/drain action based on hysteresis
- MQTT Command: Appropriate control command sent to ESP32
- Valve Action: ESP32 activates corresponding relay for fill or drain operation
Manual Mode Operation:
- Mode Switch: User toggles switch to disable automatic control
- Button Control: Fill/Drain buttons become active for manual operation
- Direct Command: Button press sends immediate MQTT control command
- Hardware Response: ESP32 activates valves based on manual commands
- Status Feedback: Real-time status updates reflect manual operations
Advanced Features
Automatic Control Logic with Hysteresis:
def update_control_simple(self):
if not self._board_connected or not self._auto_mode:
return
current = self._water_level
target = self._target_level
if target > current:
self.log_message(f"Target {target:.1f}% > Level {current:.1f}% — FILLING")
self.mqtt_client.publish(MQTT_TOPIC_CONTROL, "FILL_ON DRAIN_OFF")
elif target < current:
self.log_message(f"Target {target:.1f}% < Level {current:.1f}% — DRAINING")
self.mqtt_client.publish(MQTT_TOPIC_CONTROL, "FILL_OFF DRAIN_ON")
else:
self.mqtt_client.publish(MQTT_TOPIC_CONTROL, "FILL_DRAIN_OFF")
Real-Time History Plotting:
def setup_history_plot(self):
plot = self.ui.Wate_Level_History_Plot
plot.setBackground('transparent')
plot.setLabel('left', 'Water Level', units='%')
plot.setLabel('bottom', 'Time', units='s')
plot.showGrid(x=True, y=True, alpha=0.3)
pen = pg.mkPen(color='#2196F3', width=2)
self.history_curve = plot.plot(pen=pen, name='Water Level')
target_pen = pg.mkPen(color='#FF5722', width=2, style=pg.QtCore.Qt.DashLine)
self.target_line = plot.plot(pen=target_pen, name='Target Level')
Mode-Specific Control Management:
def handle_switch(self, state):
self._auto_mode = not state
if self._auto_mode:
self.control_timer.start(500)
self.ui.fillButton.setEnabled(False)
self.ui.drainButton.setEnabled(False)
self.log_message("Switched to Automatic mode")
else:
self.control_timer.stop()
self.ui.fillButton.setEnabled(True)
self.ui.drainButton.setEnabled(True)
self.log_message("Switched to Manual mode")
System Deactivation with Safety:
def deactivate(self):
# Send safety commands first
self.mqtt_client.publish(MQTT_TOPIC_CONTROL, "FILL_DRAIN_OFF")
self.mqtt_client.publish(MQTT_TOPIC_MQTT_Rq, "TurnOFF")
# Stop timers and update UI
if hasattr(self, 'control_timer'):
self.control_timer.stop()
if hasattr(self, 'plot_timer'):
self.plot_timer.stop()
# Unsubscribe from MQTT topics
self.mqtt_client.unsubscribe_from_topic(MQTT_TOPIC_SENSOR)
self.mqtt_client.unsubscribe_from_topic(MQTT_TOPIC_MQTT_Rs)
ConclusionThis ESP32 water level control system demonstrates advanced IoT automation with industrial-grade safety features, dual-mode operation, and comprehensive real-time visualization. The system provides reliable liquid level management with fail-safe interlocks, intelligent control algorithms, and professional dashboard monitoring.
In the next part of this series, IoT Projects Part 7: Load Cell Weight Measurement, we'll explore precision weight sensing and calibration systems with real-time data acquisition and advanced signal processing capabilities.
Comments