// ============================================================
// IFIIS — Intelligent Fuel Identification and Indication System
// ESP32 Firmware — Main Sketch
//
// All pin numbers, MQTT credentials, and timing constants
// are defined in ifiis_config.h
//
// Libraries required:
// MFRC522, LiquidCrystal I2C, PubSubClient, ArduinoJson
// ============================================================
#include <Arduino.h>
#include <SPI.h>
#include <Wire.h>
#include <WiFi.h>
#include <WiFiClientSecure.h>
#include <MFRC522.h>
#include <LiquidCrystal_I2C.h>
#include <PubSubClient.h>
#include <ArduinoJson.h>
#include "ifiis_config.h"
// ── Object instances ──────────────────────────────────────────
MFRC522 rfid(RFID_SS_PIN, RFID_RST_PIN);
LiquidCrystal_I2C lcd(LCD_I2C_ADDR, LCD_COLS, LCD_ROWS);
WiFiClientSecure wifiClient;
PubSubClient mqttClient(wifiClient);
// ── Runtime state ──────────────────────────────────────────────
SystemState currentState = STATE_IDLE;
NozzleType nozzleLifted = NOZZLE_NONE;
String lastUID = "";
String authFuelType = "";
String authReason = "";
bool authReceived = false;
bool authResult = false;
unsigned long rfidScanStart = 0;
unsigned long authWaitStart = 0;
unsigned long valveOpenStart = 0;
unsigned long lastReconnect = 0;
// ── Forward declarations ──────────────────────────────────────
void wifiConnect();
void mqttConnect();
void mqttLoop();
void onMqttMessage(char* topic, byte* payload, unsigned int len);
bool rfidRead(String& uid);
void lcdPrint(const char* line1, const char* line2);
void setLED(bool redOn, bool greenOn);
void buzzerBeep(int count);
void valveOpen();
void valveClose();
void publishRFIDScan(const String& uid);
void publishALPRTrigger();
void publishStatus(const char* status);
void processAuthResponse();
void handleError(const char* detail);
void runStateMachine();
void readNozzle();
// ============================================================
// SETUP
// ============================================================
void setup() {
Serial.begin(SERIAL_BAUD);
delay(500);
DBG("=== IFIIS Firmware Starting ===");
// ── GPIO initialisation ──────────────────────────────────
pinMode(LED_RED_PIN, OUTPUT);
pinMode(LED_GREEN_PIN, OUTPUT);
pinMode(BUZZER_PIN, OUTPUT);
pinMode(RELAY_PIN, OUTPUT);
pinMode(BTN_DIESEL_PIN, INPUT_PULLUP);
pinMode(BTN_PETROL_PIN, INPUT_PULLUP);
// Safe defaults — valve locked, all outputs OFF
digitalWrite(LED_RED_PIN, LOW);
digitalWrite(LED_GREEN_PIN, LOW);
digitalWrite(BUZZER_PIN, LOW);
digitalWrite(RELAY_PIN, LOW);
// ── I2C / LCD ────────────────────────────────────────────
Wire.begin(LCD_SDA_PIN, LCD_SCL_PIN);
lcd.init();
lcd.backlight();
lcdPrint(" IFIIS v1.0 ", " Initialising...");
DBG("LCD initialised");
// ── SPI / RFID ────────────────────────────────────────────
// Default VSPI: SCK=GPIO18, MISO=GPIO19, MOSI=GPIO23, SS=RFID_SS_PIN
SPI.begin();
rfid.PCD_Init();
rfid.PCD_DumpVersionToSerial();
DBG("MFRC522 initialised");
// ── Wi-Fi ─────────────────────────────────────────────────
lcdPrint(" Connecting to ", " Wi-Fi... ");
wifiConnect();
// ── MQTT ──────────────────────────────────────────────────
lcdPrint(" Connecting to ", " MQTT... ");
wifiClient.setInsecure();
mqttClient.setServer(MQTT_BROKER, MQTT_PORT);
mqttClient.setCallback(onMqttMessage);
mqttClient.setBufferSize(512);
mqttConnect();
// ── Ready ──────────────────────────────────────────────────
lcdPrint(" SYSTEM READY - ", " AWAITING VEHICLE");
setLED(false, false);
publishStatus("IDLE");
DBG("System ready — awaiting vehicle");
}
// ============================================================
// LOOP
// ============================================================
void loop() {
mqttLoop();
runStateMachine();
delay(10);
}
// ============================================================
// MODULE A — Wi-Fi & MQTT
// ============================================================
void wifiConnect() {
DBG2("Connecting to SSID", WIFI_SSID);
WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
int tries = 0;
while (WiFi.status() != WL_CONNECTED && tries < 20) {
delay(500);
Serial.print(".");
tries++;
}
Serial.println();
if (WiFi.status() == WL_CONNECTED) {
DBG("Wi-Fi connected");
DBG2("IP address", WiFi.localIP().toString());
} else {
DBG("Wi-Fi failed — continuing in offline mode");
}
}
void mqttConnect() {
int tries = 0;
while (!mqttClient.connected() && tries < 5) {
DBG2("MQTT connecting to", MQTT_BROKER);
if (mqttClient.connect(MQTT_CLIENT_ID, MQTT_USER, MQTT_PASS)) {
DBG("MQTT connected");
if (mqttClient.subscribe(TOPIC_AUTH_RESP, 1)) {
DBG2("Subscribed to", TOPIC_AUTH_RESP);
} else {
DBG("Subscribe FAILED");
}
} else {
DBG2("MQTT connect failed, state", mqttClient.state());
delay(3000);
}
tries++;
}
if (!mqttClient.connected()) {
DBG("MQTT unavailable — offline mode active");
}
}
void mqttLoop() {
if (!mqttClient.connected()) {
unsigned long now = millis();
if (now - lastReconnect > RECONNECT_DELAY_MS) {
lastReconnect = now;
DBG("MQTT reconnecting...");
mqttConnect();
}
}
mqttClient.loop();
}
void onMqttMessage(char* topic, byte* payload, unsigned int len) {
char buf[512];
len = min(len, (unsigned int)(sizeof(buf) - 1));
memcpy(buf, payload, len);
buf[len] = '\0';
DBG2("MQTT received on", topic);
DBG2("Payload", buf);
if (String(topic) == String(TOPIC_AUTH_RESP)) {
StaticJsonDocument<512> doc;
DeserializationError err = deserializeJson(doc, buf);
if (err) {
DBG2("JSON parse error", err.c_str());
return;
}
authResult = doc["authorised"] | false;
authReason = String(doc["reason"] | "UNKNOWN");
authFuelType = String(doc["fuel_type"] | "UNKNOWN");
authReceived = true;
DBG2("Auth decision", authResult ? "AUTHORISED" : "LOCKED");
DBG2("Reason", authReason);
DBG2("Fuel type", authFuelType);
}
}
// ============================================================
// MODULE B — RFID Reader
// ============================================================
bool rfidRead(String& uid) {
if (!rfid.PICC_IsNewCardPresent()) return false;
if (!rfid.PICC_ReadCardSerial()) return false;
uid = "";
for (byte i = 0; i < rfid.uid.size; i++) {
if (i > 0) uid += ":";
if (rfid.uid.uidByte[i] < 0x10) uid += "0";
uid += String(rfid.uid.uidByte[i], HEX);
}
uid.toUpperCase();
DBG2("RFID card detected, UID", uid);
rfid.PICC_HaltA();
rfid.PCD_StopCrypto1();
return true;
}
// ============================================================
// MODULE C — Output Actuation
// ============================================================
void lcdPrint(const char* line1, const char* line2) {
lcd.clear();
lcd.setCursor(0, 0);
lcd.print(line1);
lcd.setCursor(0, 1);
lcd.print(line2);
}
void setLED(bool redOn, bool greenOn) {
digitalWrite(LED_RED_PIN, redOn ? HIGH : LOW);
digitalWrite(LED_GREEN_PIN, greenOn ? HIGH : LOW);
}
void buzzerBeep(int count) {
for (int i = 0; i < count; i++) {
tone(BUZZER_PIN, 1000, BUZZER_BEEP_MS);
delay(BUZZER_BEEP_MS + BUZZER_GAP_MS);
}
noTone(BUZZER_PIN);
}
void valveOpen() {
digitalWrite(RELAY_PIN, HIGH);
valveOpenStart = millis();
DBG("VALVE OPENED — fuel flow authorised");
}
void valveClose() {
digitalWrite(RELAY_PIN, LOW);
DBG("VALVE CLOSED — fuel flow blocked");
}
// ============================================================
// MODULE D — MQTT Publishing & State Machine
// ============================================================
void publishRFIDScan(const String& uid) {
if (!mqttClient.connected()) { DBG("Publish skipped — not connected"); return; }
StaticJsonDocument<JSON_BUFFER_SIZE> doc;
doc["uid"] = uid;
doc["nozzle"] = (nozzleLifted == NOZZLE_DIESEL) ? FUEL_DIESEL_STR : FUEL_PETROL_STR;
doc["pump_id"] = PUMP_ID;
char buf[JSON_BUFFER_SIZE];
serializeJson(doc, buf);
bool ok = mqttClient.publish(TOPIC_RFID_SCAN, buf, true);
DBG2("RFID scan published", ok ? "OK" : "FAIL");
DBG2("Payload", buf);
}
void publishALPRTrigger() {
if (!mqttClient.connected()) return;
StaticJsonDocument<JSON_BUFFER_SIZE> doc;
doc["status"] = "ALPR_TRIGGER";
doc["pump_id"] = PUMP_ID;
doc["nozzle"] = (nozzleLifted == NOZZLE_DIESEL) ? FUEL_DIESEL_STR : FUEL_PETROL_STR;
char buf[JSON_BUFFER_SIZE];
serializeJson(doc, buf);
mqttClient.publish(TOPIC_STATUS, buf, true);
DBG("ALPR trigger published");
}
void publishStatus(const char* status) {
if (!mqttClient.connected()) return;
StaticJsonDocument<128> doc;
doc["status"] = status;
doc["pump_id"] = PUMP_ID;
char buf[128];
serializeJson(doc, buf);
mqttClient.publish(TOPIC_STATUS, buf, true);
}
void processAuthResponse() {
authReceived = false;
if (authResult) {
// ── AUTHORISED ──────────────────────────────────────────
DBG("Decision: AUTHORISED — opening valve");
valveOpen();
setLED(false, true);
if (authFuelType == "Diesel") {
lcdPrint(" ** AUTHORISED **", "DIESEL - PUMP ON");
} else {
lcdPrint(" ** AUTHORISED **", "PETROL - PUMP ON");
}
publishStatus("AUTHORISED");
currentState = STATE_AUTHORISED;
} else if (authReason == REASON_MISMATCH) {
// ── MISMATCH ────────────────────────────────────────────
DBG2("Decision: MISMATCH — expected", authFuelType);
valveClose();
setLED(true, false);
buzzerBeep(3);
char l2[17];
snprintf(l2, sizeof(l2), "%s ONLY! ", authFuelType.c_str());
lcdPrint(" !! MISMATCH !! ", l2);
publishStatus("MISMATCH");
currentState = STATE_MISMATCH;
} else if (authReason == REASON_UNKNOWN_VEH) {
// ── UNKNOWN VEHICLE ─────────────────────────────────────
DBG("Decision: VEHICLE UNKNOWN");
valveClose();
setLED(true, false);
buzzerBeep(2);
lcdPrint(" VEHICLE UNKNOWN", " SEE ATTENDANT ");
publishStatus("UNKNOWN");
currentState = STATE_UNKNOWN;
} else if (authReason == REASON_ALPR_LOW) {
// ── LOW ALPR CONFIDENCE ──────────────────────────────────
DBG("Decision: ALPR LOW CONFIDENCE");
valveClose();
setLED(true, false);
buzzerBeep(1);
lcdPrint(" LOW CONFIDENCE ", " SEE ATTENDANT ");
publishStatus("ALPR_LOW_CONF");
currentState = STATE_UNKNOWN;
} else {
handleError(authReason.c_str());
}
}
void handleError(const char* detail) {
DBG2("SYSTEM ERROR", detail);
valveClose();
setLED(true, false);
buzzerBeep(1);
lcdPrint(" SYSTEM ERROR ", " CONTACT STAFF ");
publishStatus("ERROR");
currentState = STATE_ERROR;
}
void runStateMachine() {
switch (currentState) {
// ──────────────────────────────────────────────────────────
case STATE_IDLE:
readNozzle();
if (nozzleLifted != NOZZLE_NONE) {
DBG("IDLE → RFID_SCAN");
if (nozzleLifted == NOZZLE_DIESEL) {
lcdPrint(" DIESEL NOZZLE ", " SCANNING... ");
} else {
lcdPrint(" PETROL NOZZLE ", " SCANNING... ");
}
rfidScanStart = millis();
currentState = STATE_RFID_SCAN;
publishStatus("RFID_SCAN");
}
break;
// ──────────────────────────────────────────────────────────
case STATE_RFID_SCAN:
{
String uid = "";
if (rfidRead(uid)) {
lastUID = uid;
authReceived = false;
lcdPrint(" VERIFYING... ", " ");
publishRFIDScan(uid);
authWaitStart = millis();
currentState = STATE_AUTH_WAIT;
publishStatus("AUTH_WAIT");
DBG2("RFID_SCAN → AUTH_WAIT | UID", uid);
} else if (millis() - rfidScanStart > RFID_TIMEOUT_MS) {
DBG("RFID timeout → ALPR_AWAIT");
lcdPrint(" ALPR MODE ", " CAMERA ACTIVE ");
publishALPRTrigger();
authReceived = false;
authWaitStart = millis();
currentState = STATE_ALPR_AWAIT;
publishStatus("ALPR_AWAIT");
}
}
break;
// ──────────────────────────────────────────────────────────
case STATE_ALPR_AWAIT:
if (authReceived) {
processAuthResponse();
} else if (millis() - authWaitStart > AUTH_WAIT_TIMEOUT_MS) {
DBG("ALPR response timeout → ERROR");
handleError("ALPR_TIMEOUT");
}
break;
// ──────────────────────────────────────────────────────────
case STATE_AUTH_WAIT:
if (authReceived) {
processAuthResponse();
} else if (millis() - authWaitStart > AUTH_WAIT_TIMEOUT_MS) {
DBG("Auth response timeout → ERROR");
handleError("AUTH_TIMEOUT");
}
break;
// ──────────────────────────────────────────────────────────
case STATE_AUTHORISED:
{
bool nozzleReturned = (digitalRead(BTN_DIESEL_PIN) == HIGH) &&
(digitalRead(BTN_PETROL_PIN) == HIGH);
bool timedOut = millis() - valveOpenStart > VALVE_OPEN_DURATION_MS;
if (nozzleReturned || timedOut) {
DBG("Nozzle returned or timeout → IDLE");
valveClose();
delay(5000);
setLED(false, false);
lcdPrint(" SYSTEM READY- ", " AWAITING VEHICLE ");
nozzleLifted = NOZZLE_NONE;
currentState = STATE_IDLE;
publishStatus("IDLE");
}
}
break;
// ──────────────────────────────────────────────────────────
case STATE_MISMATCH:
delay(5000);
setLED(false, false);
lcdPrint(" SYSTEM READY- ", " AWAITING VEHICLE ");
nozzleLifted = NOZZLE_NONE;
currentState = STATE_IDLE;
publishStatus("IDLE");
break;
// ──────────────────────────────────────────────────────────
case STATE_UNKNOWN:
delay(5000);
setLED(false, false);
lcdPrint(" SYSTEM READY- ", " AWAITING VEHICLE ");
nozzleLifted = NOZZLE_NONE;
currentState = STATE_IDLE;
publishStatus("IDLE");
break;
// ──────────────────────────────────────────────────────────
case STATE_ERROR:
delay(5000);
setLED(false, false);
lcdPrint(" SYSTEM READY- ", " AWAITING VEHICLE ");
nozzleLifted = NOZZLE_NONE;
currentState = STATE_IDLE;
publishStatus("IDLE");
break;
default:
currentState = STATE_IDLE;
break;
}
}
// ============================================================
// MODULE E — Nozzle Input (Pushbuttons)
// ============================================================
void readNozzle() {
if (digitalRead(BTN_DIESEL_PIN) == LOW) {
nozzleLifted = NOZZLE_DIESEL;
} else if (digitalRead(BTN_PETROL_PIN) == LOW) {
nozzleLifted = NOZZLE_PETROL;
} else {
nozzleLifted = NOZZLE_NONE;
}
}
[ MFRC522 RFID READER ]
Diesel Nozzle
Petrol Nozzle
[ SOLENOID VALVE (RELAY) ]
STATUS LED (R/G)
ALARM BUZZER
VALVE
LCD DISPLAY
ESP32 BOARD