← All projects

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.

  • AGVs
  • Sensor Fusion
  • Thermal Imaging
  • Distributed IoT
  • Industrial Safety

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()