RKCRobotics, Kinematics & Control
FUMES: Autonomous Gas Leak Detection Robot
A mobile robotic platform for inspecting oil & gas infrastructure — it patrols a facility, fuses MQ-series VOC gas sensors with MLX90640 thermal imaging to spot the cold spots left by expanding leaking gas, and syncs stationary + mobile ESP32 nodes over MQTT.
Hardware & Architecture
- Mobile AGV chassis handling facility navigation, obstacle avoidance, and payload transport.
- ESP32 microcontrollers as both fixed facility sensors and the robot's onboard gas-acquisition nodes (dual-core, native Wi-Fi/BT).
- Calibrated MQ-series gas sensors (MQ-4 methane/CNG, MQ-2 combustibles) read through the ESP32 ADC.
- MLX90640 (or FLIR Lepton) I²C thermal camera to detect the localized temperature drop of rapidly expanding compressed gas.
- MQTT as the low-latency messaging bus between stationary nodes, the AGV, and the central facility dashboard.
Key Highlights
- Hybrid architecture bridges distributed stationary IoT nodes with an autonomous mobile edge-compute platform.
- ESP32 firmware oversamples the analog gas sensor, applies a moving average, and publishes PPM-proxy telemetry over MQTT.
- Onboard Python engine reads the thermal matrix for cold-spot anomalies and fuses it with incoming gas alerts to trigger an isolation protocol.
Images

Code
fumes_gas_node.inoESP32 Gas Sensor Nodecpp
#include <WiFi.h>
#include <PubSubClient.h>
// --- Network & MQTT Configuration ---
const char* ssid = "FACILITY_WIFI";
const char* password = "SECURE_PASSWORD";
const char* mqtt_server = "192.168.1.50"; // Central Broker IP
const char* node_id = "FUMES_MOBILE_NODE_1";
const char* topic_gas = "fumes/telemetry/gas";
// --- Hardware Pins ---
const int gasSensorPin = 34; // ESP32 ADC1 pin
// --- Thresholds ---
const int GAS_ALARM_THRESHOLD = 1500; // Calibrated ADC threshold for hazardous levels
WiFiClient espClient;
PubSubClient client(espClient);
void setup_wifi() {
delay(10);
Serial.print("Connecting to ");
Serial.println(ssid);
WiFi.begin(ssid, password);
while (WiFi.status() != WL_CONNECTED) {
delay(500);
Serial.print(".");
}
Serial.println("\nWiFi connected");
}
void reconnect() {
while (!client.connected()) {
Serial.print("Attempting MQTT connection...");
if (client.connect(node_id)) {
Serial.println("connected");
} else {
Serial.print("failed, rc=");
Serial.print(client.state());
Serial.println(" try again in 5 seconds");
delay(5000);
}
}
}
void setup() {
Serial.begin(115200);
setup_wifi();
client.setServer(mqtt_server, 1883);
pinMode(gasSensorPin, INPUT);
}
void loop() {
if (!client.connected()) {
reconnect();
}
client.loop();
// Oversampling for noise reduction
long sum = 0;
for (int i = 0; i < 10; i++) {
sum += analogRead(gasSensorPin);
delay(10);
}
int averageGasLevel = sum / 10;
// Construct JSON Payload
String payload = "{\"node\":\"" + String(node_id) + "\", \"gas_level\":" + String(averageGasLevel) + "}";
Serial.println("Publishing: " + payload);
client.publish(topic_gas, payload.c_str());
// Trigger local interrupt/alarm if highly hazardous
if (averageGasLevel > GAS_ALARM_THRESHOLD) {
Serial.println("CRITICAL: High Gas Concentration Detected!");
client.publish("fumes/alerts", "{\"alert\":\"CRITICAL_GAS\", \"node\":\"FUMES_MOBILE_NODE_1\"}");
}
delay(2000); // 0.5 Hz publishing rate
}fumes_thermal_fusion.pyThermal Processing & Fusionpython
import time
import json
import paho.mqtt.client as mqtt
import numpy as np
# Note: In a real deployment, adafruit_mlx90640 or cv2 would be used for thermal mapping
# import board
# import busio
# import adafruit_mlx90640
# --- Configuration ---
MQTT_BROKER = "192.168.1.50"
GAS_TOPIC = "fumes/telemetry/gas"
ALERT_TOPIC = "fumes/alerts"
# Thermal threshold for expanding compressed gas (e.g., rapid cooling anomaly)
THERMAL_ANOMALY_TEMP_C = 5.0
def on_connect(client, userdata, flags, rc):
print(f"[SYSTEM] Connected to MQTT Broker with result code {rc}")
client.subscribe(GAS_TOPIC)
client.subscribe(ALERT_TOPIC)
def on_message(client, userdata, msg):
payload = json.loads(msg.payload.decode())
if msg.topic == ALERT_TOPIC:
print(f"\n[URGENT ALARM] Leak detected by {payload.get('node')}!")
trigger_isolation_protocol()
def trigger_isolation_protocol():
"""Commands the AGV to halt, sound alarms, and map the hazard zone."""
print("[ACTION] Halting AGV navigation. Isolating zone.")
print("[ACTION] Transmitting precise coordinates to central facility control.")
def read_thermal_matrix():
"""
Simulates reading a 32x24 thermal imaging array (like the MLX90640).
In a live environment, this captures ambient facility temperatures.
"""
# Simulating a normal ambient frame around 22C
frame = np.random.normal(22.0, 1.5, (24, 32))
# Simulating a localized leak (cold spot) 10% of the time
if np.random.rand() > 0.9:
frame[10:15, 10:15] = 2.0 # Sudden drop to 2C
return frame
def process_thermal_vision():
frame = read_thermal_matrix()
min_temp = np.min(frame)
if min_temp <= THERMAL_ANOMALY_TEMP_C:
print(f"[VISION ALERT] Thermal anomaly detected! Min Temp: {min_temp:.1f}C. Possible high-pressure leak.")
return True
return False
def main():
print("[SYSTEM] Booting FUMES Sensor Fusion Engine...")
client = mqtt.Client()
client.on_connect = on_connect
client.on_message = on_message
client.connect(MQTT_BROKER, 1883, 60)
client.loop_start()
try:
while True:
leak_visible = process_thermal_vision()
if leak_visible:
# Correlate vision with sensor data by publishing an alert
alert_payload = json.dumps({"alert": "THERMAL_ANOMALY", "node": "AGV_VISION_SYS"})
client.publish(ALERT_TOPIC, alert_payload)
time.sleep(1) # 1 FPS thermal processing
except KeyboardInterrupt:
print("\n[SYSTEM] Shutting down FUMES vision system.")
client.loop_stop()
client.disconnect()
if __name__ == '__main__':
main()