A program áttekintése
A bemutatott program nem egy végleges, önálló alkalmazás, hanem egy stabil és továbbfejleszthető alapprogram. Biztosítja a vezeték nélküli hálózati kommunikációhoz, a távoli adatkapcsolathoz és a hozzáférési pontok közötti automatikus váltáshoz szükséges alapvető szolgáltatásokat. Erre a programstruktúrára később különböző célalkalmazások építhetők további funkciók hozzáadásával.
A fejlesztés célja egy olyan ESP32-alapú Wi-Fi-kliens megvalósítása, amely képes a háttérben, a főprogram futásának blokkolása nélkül feltérképezni az elérhető vezeték nélküli hálózatokat, majd ugyanazon SSID-hez tartozó hozzáférési pontok közül kiválasztani a kedvezőbb kapcsolatot biztosító AP-t. Ennek köszönhetően az eszköz mozgás közben is stabilabb és megbízhatóbb hálózati kapcsolatot tarthat fenn.

A program kezeli az ESP32 Wi-Fi-kapcsolatát, folyamatosan felügyeli annak állapotát, valamint megvalósítja az MQTT-alapú kommunikációt. Tartalmazza továbbá a hozzáférési pontok közötti automatikus váltás, vagyis a roaming alaplogikáját. A döntési folyamat során figyelembe veszi a mért jelerősséget, annak átlagolt értékét, a beállított hiszterézist, valamint az új hozzáférési pont stabilitását.
A fejlesztés során kiemelt szempont volt a megbízható, áttekinthető és könnyen bővíthető programstruktúra kialakítása. Ennek megfelelően a Wi-Fi-kapcsolat kezelése, a hálózatkeresés, a roaming, az MQTT-kommunikáció és az időszinkronizálás egymástól elkülönített függvényekben valósul meg. Ez a moduláris felépítés megkönnyíti a hibakeresést és a továbbfejlesztést, valamint lehetővé teszi, hogy a későbbi alkalmazások minimális módosítással használják fel a már elkészített alapfunkciókat.
A program időzítése elsősorban a millis() függvény által visszaadott futási időre épül. Ez lehetővé teszi a blokkoló várakozások elkerülését, így a rendszer folyamatosan képes felügyelni a hálózati kapcsolatokat és gyorsan reagálni az eseményekre.
A program részletes bemutatása
Felhasznált könyvtárak
A program működéséhez három könyvtár szükséges.
A WiFi.h könyvtár biztosítja az ESP32 vezeték nélküli hálózati funkcióit, például a csatlakozást, a hálózatok keresését és az RSSI értékek lekérdezését.
A PubSubClient.h könyvtár az MQTT kommunikáció megvalósításáért felel. Ennek segítségével az eszköz üzeneteket tud küldeni és fogadni egy MQTT brokeren keresztül.
A time.h könyvtár az internetes időszinkronizációt teszi lehetővé, amelyet a program az indulási idő meghatározására használ.
#include <WiFi.h>
#include <PubSubClient.h>
#include "time.h"
Konfiguráció és globális változók
A program elején találhatók azok a konstansok és globális változók, amelyek a hálózati kommunikációhoz és a program működéséhez szükséges alapvető beállításokat tartalmazzák. Itt kerül megadásra a Wi-Fi hálózat neve (SSID) és jelszava, az MQTT broker IP-címe és portszáma, valamint azok az MQTT témák (topikok), amelyeken keresztül az ESP32 kommunikál.
A program használatához ezeket az értékeket a saját hálózati környezetnek megfelelően kell beállítani. Ennek megfelelően meg kell adni a használni kívánt Wi-Fi hálózat csatlakozási adatait, valamint az MQTT broker IP-címét és a kommunikációhoz használt portszámot.
// Wi-Fi csatlakozási adatok
const char* ssid = "mySSID";
const char* password = "myPassword";
// MQTT szerver beállításai
const char* mqttServer = "192.168.1.2";
const uint16_t mqttPort = 1883;
Ezt követően létrejön egy Wi-Fi kliens és egy MQTT kliens objektum. Ezek felelnek a hálózati kapcsolat, illetve az MQTT kommunikáció kezeléséért.
A globális változók között találhatók továbbá az RSSI átlagolásához szükséges tömbök, a roaming döntési folyamatát vezérlő állapotjelzők, valamint az időzítések és az aktuális kapcsolat állapotát tároló jelzők.
A program működése néhány konstans módosításával egyszerűen testre szabható.
A wifiScanFreq konstans határozza meg, hogy milyen időközönként induljon új Wi-Fi hálózatkeresés. Az érték ezredmásodpercben van megadva.
// Hálózatkeresés periódusa ezredmásodpercben megadva
unsigned long wifiScanFreq = 15000; // 15 másodperc
Az RSSI átlagolásához használt minták száma. Az rssiSampleCount konstans adja meg, hogy a program hány egymást követő RSSI mérésből számítson átlagot. Nagyobb érték stabilabb átlagot eredményez, ugyanakkor lassabban követi a jelerősség változását.
// Ennyi mintából számol átlagot.
const int rssiSampleCount = 3;
A requiredStableScans konstans azt határozza meg, hogy az új hozzáférési pontnak hány egymást követő hálózatkeresés során kell a legjobb választásnak bizonyulnia, mielőtt a program végrehajtja a roamingot.
// Az új AP-nak legalább ennyi egymást követő keresés során
// is a legjobbnak kell lennie!
const int requiredStableScans = 2;
A rssiHysteresis konstans adja meg azt a minimális jelerősség-különbséget, amely szükséges ahhoz, hogy a program új hozzáférési pontra váltson. Minél kisebb ez az érték, annál érzékenyebben reagál a rendszer a jelerősség változására, míg nagyobb érték esetén stabilabban ragaszkodik a meglévő kapcsolathoz.
// Az új AP csak akkor választható, ha legalább
// ennyi dBm-mel jobb a jelenlegi kapcsolatnál
const int rssiHysteresis = 12; // 12 dBm
Eseményvezérelt Wi-Fi Kezelés
A program eseményvezérelt módon figyeli a Wi-Fi-kapcsolat állapotát. Az egyes hálózati eseményeket a WiFiEvent() callback függvény kezeli, amelyet az ESP32 Wi-Fi könyvtára automatikusan meghív, amikor változás történik a hálózati kapcsolat állapotában.
void WiFiEvent(WiFiEvent_t event, WiFiEventInfo_t info)
{
switch(event)
{
// Sikeres csatlakozás a hozzáférési ponthoz
case ARDUINO_EVENT_WIFI_STA_CONNECTED:
....
break;
// Az eszköz IP-címet kapott, a kapcsolat használatra kész
case ARDUINO_EVENT_WIFI_STA_GOT_IP:
....
break;
// Kapcsolat megszakadt, az állapotjelzők alaphelyzetbe állítása
case ARDUINO_EVENT_WIFI_STA_DISCONNECTED:
...
break;
// Egyéb Wi-Fi események kezelése nem szükséges
default:
break;
}
}
Amikor az ESP32 sikeresen csatlakozik egy hozzáférési ponthoz, a program ezt jelzi a soros monitoron. Ebben az állapotban a vezeték nélküli kapcsolat már létrejött, az eszköz azonban még az IP-cím kiosztására vár.
// Sikeres csatlakozás a hozzáférési ponthoz
case ARDUINO_EVENT_WIFI_STA_CONNECTED:
if(debug)
{
Serial.println();
Serial.print("Connected to the ");
Serial.println(WiFi.SSID().c_str());
Serial.println(" Waiting for IP address...");
}
break;
Miután IP-címet kap a hálózattól, beállít egy kapcsolati állapotjelzőt, eltárolja az aktuális BSSID-t, valamint kiírja az IP-címet, az aktuális RSSI értéket és az eszköz nevét.
Az eltárolt BSSID-re a későbbi hálózatkeresések során van szükség, mivel ennek segítségével a program meg tudja különböztetni az aktuálisan használt hozzáférési pontot a többi, azonos SSID-vel rendelkező AP-tól.
// Az eszköz IP-címet kapott, a kapcsolat használatra kész
case ARDUINO_EVENT_WIFI_STA_GOT_IP:
wifiConnected = true; // kapcsolati állapotjelző beállítása
currentBssid = WiFi.BSSIDstr();
if(debug)
{
Serial.println();
Serial.println("IP address received!");
Serial.print("IP: ");
Serial.println(WiFi.localIP());
Serial.print("BSSID: ");
Serial.println(currentBssid);
Serial.print("HostName: ");
Serial.println(WiFi.getHostname());
Serial.print("RSSI: ");
Serial.println(WiFi.RSSI());
Serial.println();
}
break;
Ha a Wi-Fi-kapcsolat megszakad, a program törli a kapcsolatot jelző állapotváltozót. Emellett alaphelyzetbe állítja a hálózatkeresés állapotát is, hogy a következő csatlakozás után új keresés indulhasson.
// Kapcsolat megszakadt, az állapotjelzők alaphelyzetbe állítása
case ARDUINO_EVENT_WIFI_STA_DISCONNECTED:
wifiConnected = false;
isScanning = false;
break;
Kapcsolódás a hálózathoz
A setupWifi() függvény valójában nemcsak inicializálja a Wi-Fi-t, hanem gondoskodik arról is, hogy az eszköz tiszta állapotból induljon. Bontja az esetleges korábbi kapcsolatot, kliens (STA) módba állítja az ESP32-t, alaphelyzetbe állítja a hálózatkeresés állapotát, majd megkísérli a csatlakozást a megadott vezeték nélküli hálózathoz.
void setupWifi()
{
// Korábbi Wi-Fi kapcsolat törlése
WiFi.disconnect(true);
// Az ESP32 kliens (Station) üzemmódba állítása
WiFi.mode(WIFI_STA);
// Hálózatkeresés állapotának alaphelyzetbe állítása
isScanning = false;
// Csatlakozási kísérlet a megadott Wi-Fi hálózathoz
if(!connectToWifi(ssid, password, 0, NULL))
{
...
}
}
A tényleges kapcsolódást a connectToWifi() függvény valósítja meg. A függvény kialakítása lehetővé teszi, hogy normál csatlakozás és roaming során is ugyanaz a programrész kezelje a kapcsolat felépítését.
Kezdeti csatlakozáskor elegendő a Wi-Fi hálózat neve (SSID) és a hozzá tartozó jelszó megadása, ilyenkor az ESP32 automatikusan kiválasztja a megfelelő hozzáférési pontot.
Wi-Fi szkennelés után, ha a feltételek teljesülnek a roaming során azonban a program már ismeri a kiválasztott hozzáférési pont BSSID-jét és működési csatornáját, ezért közvetlenül ehhez az AP-hoz csatlakozik. Ez lerövidíti a csatlakozási folyamatot, mivel nincs szükség újabb hálózatkeresésre.
A csatlakozás külön függvénybe szervezésének előnye, hogy ugyanaz a megvalósítás használható mind a kezdeti hálózati kapcsolódás, mind a roaming során végrehajtott AP-váltás esetén, így elkerülhető a kódismétlés.
bool connectToWifi(const char* targetSsid, const char* targetPass, int32_t channel, const uint8_t* bssid)
{
....
if(bssid != NULL) // Ha ismert a cél AP BSSID-je, akkor közvetlenül ahhoz csatlakozunk
{
WiFi.begin(targetSsid, targetPass, channel, bssid, true);
}
else // Egyébként hagyjuk, hogy az ESP32 válasszon hozzáférési pontot
{
WiFi.begin(targetSsid, targetPass);
}
....
Sikeres csatlakozás után a függvény inicializálja a roaming működéséhez szükséges változókat. Újraindítja az RSSI átlagolását, törli az esetleges korábbi roaming jelöltet, valamint alaphelyzetbe állítja a roaming állapotát. Ennek köszönhetően minden új kapcsolat tiszta állapotból indul, függetlenül attól, hogy kezdeti csatlakozásról vagy hozzáférési pontok közötti váltásról van szó.
....
// Sikertelen kapcsolódás esetén hamis értékkel tér vissza
if(!wifiConnected)
{
return false;
}
// Sikeres kapcsolódás után újraindítjuk az RSSI átlagolását
averagedRssi = -100;
updateAveragedRssi();
// Töröljük az előző roaming döntéshez tartozó adatokat
candidateBssid = "";
candidateCount = 0;
// Jelezzük, hogy a roaming művelet befejeződött
isRoaming = false;
return true;
}
Nem-blokkoló (Aszinkron) Wi-Fi Szkennelés
A hagyományos Wi-Fi szkennelési metódusok másodpercekre megállítják a program futását, ami egy IoT eszköznél adatvesztést okozhat. Az aszinkron Wi-Fi keresés lényege, hogy az ESP32 a háttérben keresi a környező hálózatokat és nem várja meg tétlenül a mérések végét. A háttérben folyó pásztázás alatt a főprogram folyamatosan, megszakítás nélkül futhat tovább.
A startAsynchronousScan() függvény elindítja ezt a keresést, de csak akkor, ha az eszköz már csatlakozott a hálózathoz, és éppen nincs folyamatban másik keresés.
void startAsynchronousScan()
{
// Ha már fut keresés, vagy nincs aktív Wi-Fi kapcsolat,
// akkor nincs szükség új keresés indítására
if(isScanning || !wifiConnected) return;
// Az aktuális kapcsolat jelerősségének frissítése
// a roaming döntés előkészítéséhez
updateAveragedRssi();
// Keresés állapotjelző beállítása
isScanning = true;
if(debug) Serial.println("Network scan started...");
// Aszinkron hálózatkeresés indítása
// A program futása a keresés ideje alatt nem áll meg
WiFi.scanNetworks(true, true, false);
}
Intelligens Roaming Algoritmus (Hiszterézissel)
A Wi-Fi jelerősség (RSSI) mérése a valóságban nagyon instabil. A falak, a tereptárgyak, de még az emberi test mozgása is pillanatnyi kilengéseket okozhat.
Az updateAveragedRssi() függvény RSSI mozgóátlag (Moving Average) szűrést végez. A rendszer nem egyetlen RSSI-mérés alapján hoz döntést, hanem az utolsó három mérési eredmény átlagát használja. Ez csökkenti a rövid idejű jelerősség-ingadozások hatását, így stabilabb roaming döntés hozható.
void updateAveragedRssi()
{
// Aktuális Wi-Fi jelerősség lekérdezése
int8_t currentRssi = WiFi.RSSI();
// Első mérés esetén minden mintát az aktuális értékkel töltünk fel,
// így az átlag azonnal használható értéket ad
if(averagedRssi == -100)
{
for(int i = 0; i < rssiSampleCount; i++)
{
rssiSamples[i] = currentRssi;
}
averagedRssi = currentRssi;
return;
}
// Az új mérési eredmény eltárolása a körkörös mintatárolóban
rssiSamples[rssiSampleIndex] = currentRssi;
// Következő minta helyének meghatározása
rssiSampleIndex = (rssiSampleIndex + 1) % rssiSampleCount;
// A tárolt RSSI értékek összegzése
int32_t sum = 0;
for(int i = 0; i < rssiSampleCount; i++)
{
sum += rssiSamples[i];
}
// Átlagos RSSI érték kiszámítása
averagedRssi = sum / rssiSampleCount;
}
A hiszterézis egy olyan biztonsági sáv, amely megakadályozza, hogy az ESP32 folyamatosan oda-vissza ugráljon két Wi-Fi hozzáférési pont (AP) között, ha azok jelerőssége közel azonos (ezt hívják ping-pong effektusnak). Jelenleg a programban alkalmazott hiszterézis értéke 12 dBm.
Mi történne hiszterézis nélkül? Ha az „A hozzáférési pont” jelerőssége -70 dBm, az „B hozzáférési pont” pedig épp felugrik -69 dBm-re, az ESP32 azonnal lecsatlakozna az A-ról és átváltana a B-re. Egy másodperccel később a B visszaesik -71 dBm-re, az ESP32 megint visszavált. Ez folyamatos hálózati szakadozást okozna.
Hogyan véd meg a 12 dBm-es hiszterézis? Az ESP32 csak akkor indítja el a roamingot, ha a talált új hálózat jelerőssége legalább 12 dBm-mel erősebb, mint a jelenlegi hálózat átlagolt jelerőssége.
A hiszterézis önmagában még nem elegendő annak biztosítására, hogy az új hozzáférési pont valóban tartósan jobb kapcsolatot biztosítson. Ezért a program időalapú megerősítést (debounce) is alkalmaz.
A jelenlegi beállítások mellett ez két egymást követő hálózatkeresést jelent, amely 15 másodperces keresési periódussal számolva összesen körülbelül 30 másodpercnek felel meg.
void checkScanResults()
{
....
// Csak akkor vizsgáljuk a roaming lehetőségét,
// ha az új AP jelerőssége meghaladja a hiszterézis küszöbét
if(bestNetworkIndex != -1 && bestScanRssi > averagedRssi + rssiHysteresis)
{
// Ellenőrizzük, hogy ugyanaz az AP több egymást követő
// keresés során is a legjobb választás-e
if(bestScanBssid == candidateBssid)
{
candidateCount++;
}
else
{
candidateBssid = bestScanBssid;
candidateCount = 1;
}
....
Ha a függvényben minden feltétel a fentiek szerint teljesül, indul a roaming. Az ESP32 bontja a meglévő kapcsolatot, megszakítja az MQTT kommunikációt, majd közvetlenül csatlakozik az új hozzáférési ponthoz.
Az MQTT kapcsolat bontása azért szükséges, mert a Wi-Fi kapcsolat megszakításával a TCP kapcsolat is érvénytelenné válik. Az új hozzáférési ponthoz történő csatlakozás után az MQTT kapcsolat automatikusan újraépül.
....
// Ha a roaming feltételei teljesülnek,
// átváltunk az új hozzáférési pontra
if(candidateCount >= requiredStableScans)
{
if(debug)
{
Serial.printf("Roaming from %s to %s\n", currentBssid.c_str(), candidateBssid.c_str());
Serial.println();
}
// beállítjuk a jelzőt, hogy folyamatban van a roamingolás
isRoaming = true;
int32_t channel = WiFi.channel(bestNetworkIndex);
const uint8_t* bssid = WiFi.BSSID(bestNetworkIndex);
// MQTT kapcsolat bontása roaming előtt
if(mqttClient.connected()) mqttClient.disconnect();
// Wi-Fi kapcsolat bontása
WiFi.disconnect();
// A keresési eredmények törlése, memória felszabadítása
WiFi.scanDelete();
isScanning = false;
// Csatlakozás a kiválasztott AP-hoz
connectToWifi(ssid, password, channel, bssid);
return;
}
....
Minden szkennelési ciklus után felszabadítja a memóriát a WiFi.scanDelete() függvény meghívásával és beállít egy jelzőt, hogy a hálózat keresése véget ért.
Ezzel lezárul az aktuális hálózatkeresési ciklus, majd a program a következő időzített keresésig a meglévő kapcsolaton folytatja a működést.
// A keresési eredmények törlése, memória felszabadítása
WiFi.scanDelete();
// Keresés befejezése jelző beállítása
isScanning = false;
....
}
MQTT Kapcsolat és Távvezérlés
Az MQTT kommunikáció során fontos követelmény, hogy a Wi-Fi hálózat pásztázása és a hozzáférési pontok közötti váltás (roaming) ne okozzon tartós kapcsolatvesztést. Amennyiben a roaming során az MQTT kapcsolat megszakad, a program automatikusan újraépíti azt, így a kommunikáció emberi beavatkozás nélkül helyreáll.
Az MQTT kapcsolat újraépítését a reconnectMqtt() függvény végzi. A hagyományos példaprogramokkal ellentétben a függvény nem használ blokkoló while ciklust. Ennek köszönhetően, ha az MQTT broker átmenetileg nem érhető el, a program nem áll meg várakozni, hanem folytatja a loop() ciklus végrehajtását. Ez biztosítja, hogy a Wi-Fi kapcsolat kezelése, a hálózatkeresés és a roaming folyamata zavartalanul működhessen.
void reconnectMqtt()
{
// Csak aktív Wi-Fi kapcsolat mellett és hálózatkeresés közben ne
// próbáljunk MQTT kapcsolatot létrehozni
if(wifiConnected && !mqttClient.connected() && !isScanning)
{
if(debug) Serial.println("Connecting to the mqtt server...");
// Kapcsolódás az MQTT brokerhez
if(mqttClient.connect(mqttClientId))
{
if(debug)
{
Serial.println("Mqtt server is connected!");
Serial.println();
}
....
A program feliratkozik a debug témára, így távolról, egyetlen MQTT üzenettel ki/be kapcsolható a soros porti hibakeresési (Log) mód. Bekapcsolt állapotban a program részletes információkat ír ki a soros monitorra, ami jelentősen megkönnyíti a működés ellenőrzését és a hibák felderítését.
A program bővítese során itt iratkozhatunk fel további témákra.
....
// Feliratkozás a szükséges topikokra
mqttClient.subscribe(debugTopic);
// feliratkozás további témákra...
}
else
{
if(debug)
{
Serial.print("Failed to connect to the mqtt server. | rc=");
Serial.println(mqttClient.state());
Serial.println();
}
}
}
}
A beérkező MQTT üzenetek feldolgozását az mqttCallback() függvény végzi. A függvény automatikusan meghívódik minden olyan esetben, amikor az MQTT broker egy feliratkozott témára új üzenetet továbbít.
A jelenlegi megvalósításban a callback függvény a hibakeresési mód távoli vezérlését valósítja meg. A debug témára küldött true vagy false értékekkel a soros porti naplózás futás közben be- vagy kikapcsolható, így nincs szükség a program újrafordítására vagy újraindítására.
void mqttCallback(char* topic, byte* payload, unsigned int length)
{
// A beérkezett topik és üzenet karakterlánccá alakítása
String strTopic = String(topic);
String strPayload = String((char*)payload, length);
// A hibakeresési mód távoli vezérlése MQTT üzenettel
if(strTopic == debugTopic)
{
if(strPayload == "true")
{
debug = true;
Serial.println("Debug enabled!");
Serial.println();
}
else if(strPayload == "false")
{
Serial.println("Debug disabled!");
Serial.println();
debug = false;
}
}
// További MQTT üzenetek fogadásának kezelése...
}
A callback függvény felépítése lehetővé teszi, hogy később további MQTT parancsok kezelése is egyszerűen hozzáadható legyen, így a rendszer könnyen bővíthető új funkciókkal.
Az ESP32 működése során folyamatosan közzéteszi a legfontosabb állapotinformációkat, például az aktuális IP-címet és az átlagolt RSSI értéket. Ezeket dedikált MQTT témákba publikálja, így egy külső alkalmazás vagy felügyeleti rendszer folyamatosan nyomon követheti az eszköz állapotát.
A publikálás a checkScanResults() függvény végén történik. Ennek az az előnye, hogy roaming után a Wi-Fi kapcsolat és az MQTT kommunikáció már helyreállt, így a közzétett adatok mindig az aktuális hálózati állapotot tükrözik.
void checkScanResults()
{
.....
// Az aktuális állapot továbbítása MQTT-n
if(mqttClient.connected())
{
mqttClient.publish(rssiTopic, String(averagedRssi).c_str());
mqttClient.publish(ipTopic, WiFi.localIP().toString().c_str());
if(debug)
{
Serial.print("Averaged RSSI: ");
Serial.println(averagedRssi);
Serial.print("IP: ");
Serial.println(WiFi.localIP());
Serial.println();
}
}
}
Indítási időbélyeg időzóna kezeléssel
Az első sikeres Wi-Fi és MQTT csatlakozást követően az eszköz lekéri a pontos dátumot és időt a pool.ntp.org NTP (Network Time Protocol) szerverről, majd beállítja a közép-európai időzónát (CET/CEST), amely automatikusan kezeli a nyári és téli időszámítás közötti átállást.
A lekért idő alapján a program egy formázott időbélyeget készít, amelyet MQTT üzenetként elküld a brokernek. Ez lehetővé teszi, hogy a szerver naplózza az eszköz indulásának pontos időpontját, ami megkönnyíti a rendszer működésének nyomon követését és az esetleges hibák utólagos elemzését.
void sendLocalTime()
{
// Aktuális idő lekérése az NTP szerverről
if(!getLocalTime(&timeinfo))
{
....
// Időbélyeg formázása
strftime(Msg_Timestamp, sizeof(Msg_Timestamp), "%Y.%m.%d-%H:%M:%S", &timeinfo);
...
// Az időbélyeg elküldése MQTT üzenetként
mqttClient.publish(startTimeTopic, formattedTime);
}
Az NTP-alapú időszinkronizálás használata előtt konfigurálni kell a használt időzónát és az NTP szervert. Ez a setup() függvényben a configTzTime() függvény meghívásával tehetjük meg.
void setup()
{
.....
// Időszinkronizálás konfigurálása NTP szerver használatával
configTzTime(timeZone, ntpServer);
}
Az időbélyeg elküldése csak egyszer, az első sikeres csatlakozást követően történik meg. A program ezt egy állapotjelző változó segítségével biztosítja, így az üzenet nem kerül ismételt elküldésre későbbi újracsatlakozások vagy roaming műveletek során.
void loop()
{
....
mqttClient.loop();
// Az indulási időbélyeg elküldése csak egyszer
if(first)
{
sendLocalTime();
first = false;
}
....
}
A rendszer inicializálása
A setup() függvény a program indulásakor egyszer fut le, és elvégzi a rendszer működéséhez szükséges inicializálási feladatokat. Ennek során beállítja a soros kommunikációt, regisztrálja a Wi-Fi és MQTT eseménykezelő függvényeket, inicializálja a hálózati kapcsolatot, valamint konfigurálja az NTP-alapú időszinkronizálást.
void setup()
{
// Soros kommunikáció inicializálása
Serial.begin(115200);
// Rövid várakozás a rendszer stabilizálódásához
delay(2000);
// Wi-Fi eseménykezelő regisztrálása
WiFi.onEvent(WiFiEvent);
// Wi-Fi kapcsolat inicializálása
setupWifi();
// MQTT szerver címének és portjának beállítása
mqttClient.setServer(mqttServer, mqttPort);
// MQTT callback függvény regisztrálása
mqttClient.setCallback(mqttCallback);
// Időszinkronizálás konfigurálása NTP szerver használatával
configTzTime(timeZone, ntpServer);
}
Ezzel minden szükséges szolgáltatás készen áll a főprogram működéséhez.
A főprogram működése
A loop() függvény a program fő vezérlőciklusa, amely folyamatosan ismétlődik a rendszer működése során. Feladata a Wi-Fi és az MQTT kapcsolat felügyelete, a hálózatkeresés időzítése, a roaming folyamat vezérlése, valamint az egyéb időzített feladatok végrehajtása.

A főprogram nem alkalmazza a delay() függvény által okozott blokkoló várakozásokat, így a különböző feladatok egymástól függetlenül, folyamatosan hajthatók végre. Az időzítések megvalósítása a millis() függvény által visszaadott futási idő alapján történik, amely lehetővé teszi a nem blokkoló működést és a rendszer gyors reagálását az eseményekre.
Első lépésként a program ellenőrzi, hogy az ESP32 rendelkezik-e aktív Wi-Fi kapcsolattal. Amennyiben a kapcsolat megszakadt, és azt nem roaming művelet okozta, automatikusan újrakezdi a csatlakozást.
Ha a Wi-Fi kapcsolat aktív, a program meghatározott időközönként aszinkron hálózatkeresést indít. A keresés befejezése után kiértékeli az eredményeket, majd szükség esetén végrehajtja a hozzáférési pontok közötti váltást (roaming).
Ezzel párhuzamosan folyamatosan felügyeli az MQTT kapcsolat állapotát. Szükség esetén automatikusan újraépíti a kapcsolatot a brokerrel, feldolgozza a beérkező MQTT üzeneteket, valamint az első sikeres kapcsolódást követően elküldi az indítási időbélyeget is.
void loop()
{
// Az aktuális futási idő lekérdezése
currentMillis = millis();
// Ha nincs Wi-Fi kapcsolat és nem roaming miatt szakadt meg,
// újrakezdjük a kapcsolódást
if(!wifiConnected && !isRoaming)
{
setupWifi();
return;
}
else if(wifiConnected)
{
// Meghatározott időközönként új hálózatkeresés indítása
if(currentMillis - prevScanMillis > wifiScanFreq)
{
prevScanMillis = currentMillis;
startAsynchronousScan();
}
// A hálózatkeresés eredményének kiértékelése
if(isScanning)
{
checkScanResults();
}
// MQTT kapcsolat helyreállítása, ha szükséges
if(!mqttClient.connected())
{
reconnectMqtt();
}
else
{
// MQTT kommunikáció fenntartása
mqttClient.loop();
// Az indulási időbélyeg elküldése csak egyszer
if(first)
{
sendLocalTime();
first = false;
}
// További MQTT üzenetek küldése ide...
}
}
}
Melléklet
A teljes, kommentelt forráskód:
#include <WiFi.h>
#include <PubSubClient.h>
#include "time.h"
// Wi-Fi csatlakozási adatok
const char* ssid = "mySSID";
const char* password = "myPassword";
// MQTT szerver beállításai
const char* mqttServer = "192.168.1.2";
const uint16_t mqttPort = 1883;
const char* mqttClientId = "ESP32_Mesh_Node";
// MQTT témák (topik)
const char* rssiTopic = "esp32/RSSI";
const char* debugTopic = "esp32/DebugEnabled";
const char* ipTopic = "esp32/IP";
const char* newstartTopic = "esp32/Newstart";
// TCP és MQTT kliens objektum létrehozása
WiFiClient espClient;
PubSubClient mqttClient(espClient);
// Hibakeresési mód engedélyezése
bool debug = true;
// Időzítéshez szükséges változók
unsigned long currentMillis = 0;
unsigned long wifiScanFreq = 15000; // Hálózatkeresés periódusa (15 sec)
unsigned long prevScanMillis = 0;
// Aktuális AP adatai
String currentBssid;
int8_t rssi;
bool isScanning = false; // Jelzi, hogy folyamatban van-e hálózatkeresés
// RSSI mozgóátlag számításához szükséges változók
const int rssiSampleCount = 3;
int8_t rssiSamples[rssiSampleCount] = {0};
int rssiSampleIndex = 0;
int8_t averagedRssi = -100;
// Roaming döntéshez használt változók
String candidateBssid = "";
int candidateCount = 0;
// Az új AP-nak legalább két egymást követő keresés során
// is a legjobbnak kell lennie!
const int requiredStableScans = 2;
// Az új AP csak akkor választható, ha legalább
// 12 dBm-mel jobb a jelenlegi kapcsolatnál
const int rssiHysteresis = 12;
// NTP szerver és időzóna beállításai
const char* ntpServer = "pool.ntp.org";
const char* timeZone = "CET-1CEST, M3.5.0, M10.5.0/3";
// Az indulási időbélyeg csak egyszer kerüljön elküldésre
bool first = true;
// Állapotjelző változók
bool wifiConnected = false;
bool isRoaming = false;
/*****************************************************************************/
// Wi-Fi eseménykezelő függvény.
// A rendszer automatikusan meghívja, amikor a Wi-Fi kapcsolat állapotában
// változás történik (pl. csatlakozás, IP-cím megszerzése vagy kapcsolatvesztés).
// Az esemény típusától függően frissíti a program állapotát és szükség esetén
// diagnosztikai információkat jelenít meg.
/*****************************************************************************/
void WiFiEvent(WiFiEvent_t event, WiFiEventInfo_t info)
{
switch(event)
{
// Sikeres csatlakozás a hozzáférési ponthoz
case ARDUINO_EVENT_WIFI_STA_CONNECTED:
if(debug)
{
Serial.println();
Serial.print("Connected to the ");
Serial.println(WiFi.SSID().c_str());
Serial.println(" Waiting for IP address...");
}
break;
// Az eszköz IP-címet kapott, a kapcsolat használatra kész
case ARDUINO_EVENT_WIFI_STA_GOT_IP:
wifiConnected = true;
currentBssid = WiFi.BSSIDstr();
if(debug)
{
Serial.println();
Serial.println("IP address received!");
Serial.print("IP: ");
Serial.println(WiFi.localIP());
Serial.print("BSSID: ");
Serial.println(currentBssid);
Serial.print("HostName: ");
Serial.println(WiFi.getHostname());
Serial.print("RSSI: ");
Serial.println(WiFi.RSSI());
Serial.println();
}
break;
// Kapcsolat megszakadt, az állapotjelzők alaphelyzetbe állítása
case ARDUINO_EVENT_WIFI_STA_DISCONNECTED:
wifiConnected = false;
isScanning = false;
break;
// Egyéb Wi-Fi események kezelése nem szükséges
default:
break;
}
}
/*****************************************************************************/
// Kapcsolódás a megadott Wi-Fi hálózathoz.
// Normál csatlakozás esetén elegendő az SSID és a jelszó megadása,
// roaming során azonban lehetőség van egy konkrét BSSID és csatorna
// megadására is, amely gyorsabb AP-váltást tesz lehetővé.
/*****************************************************************************/
bool connectToWifi(const char* targetSsid, const char* targetPass, int32_t channel, const uint8_t* bssid)
{
// Kapcsolati állapot alaphelyzetbe állítása
wifiConnected = false;
if(debug) Serial.print("Connecting to Wi-Fi...");
if(bssid != NULL) // Ha ismert a cél AP BSSID-je, akkor közvetlenül ahhoz csatlakozunk
{
WiFi.begin(targetSsid, targetPass, channel, bssid, true);
}
else // Egyébként hagyjuk, hogy az ESP32 válasszon hozzáférési pontot
{
WiFi.begin(targetSsid, targetPass);
}
// Várakozás a sikeres csatlakozásra (legfeljebb 15 másodperc)
int attempts = 0;
while(!wifiConnected && attempts < 30)
{
delay(500);
attempts++;
}
// Sikertelen kapcsolódás esetén hamis értékkel tér vissza
if(!wifiConnected)
{
return false;
}
// Sikeres kapcsolódás után újraindítjuk az RSSI átlagolását
averagedRssi = -100;
updateAveragedRssi();
// Töröljük az előző roaming döntéshez tartozó adatokat
candidateBssid = "";
candidateCount = 0;
// Jelezzük, hogy a roaming művelet befejeződött
isRoaming = false;
return true;
}
/*****************************************************************************/
// A Wi-Fi kapcsolat inicializálása.
// Törli az esetleges korábbi kapcsolatot, kliens (STA) módba állítja az ESP32-t,
// majd megkísérli a csatlakozást a megadott vezeték nélküli hálózathoz.
/*****************************************************************************/
void setupWifi()
{
// Korábbi Wi-Fi kapcsolat törlése
WiFi.disconnect(true);
// Az ESP32 kliens (Station) üzemmódba állítása
WiFi.mode(WIFI_STA);
// Hálózatkeresés állapotának alaphelyzetbe állítása
isScanning = false;
// Csatlakozási kísérlet a megadott Wi-Fi hálózathoz
if(!connectToWifi(ssid, password, 0, NULL))
{
if(debug)
{
Serial.println();
Serial.println("Initial connection failed. Waiting for event retries.");
}
}
}
/*****************************************************************************/
// Aszinkron Wi-Fi hálózatkeresés indítása.
// A függvény ellenőrzi, hogy indítható-e új keresés, majd a háttérben elindítja
// a környező hálózatok felderítését. A keresés nem blokkolja a program futását,
// így közben az egyéb feladatok (például MQTT kommunikáció) tovább működhetnek.
/*****************************************************************************/
void startAsynchronousScan()
{
// Ha már fut keresés, vagy nincs aktív Wi-Fi kapcsolat,
// akkor nincs szükség új keresés indítására
if(isScanning || !wifiConnected) return;
// Az aktuális kapcsolat jelerősségének frissítése
// a roaming döntés előkészítéséhez
updateAveragedRssi();
// Keresés állapotjelző beállítása
isScanning = true;
if(debug) Serial.println("Network scan started...");
// Aszinkron hálózatkeresés indítása
// A program futása a keresés ideje alatt nem áll meg
WiFi.scanNetworks(true, true, false);
}
/*****************************************************************************/
// Az aktuális RSSI érték frissítése és átlagolása.
// A vezeték nélküli jelerősség természetes ingadozása miatt a program több
// mérési eredményből számított átlagos értéket használ a roaming döntésekhez.
// Ez csökkenti a pillanatnyi jelerősség-változások hatását.
/*****************************************************************************/
void updateAveragedRssi()
{
// Aktuális Wi-Fi jelerősség lekérdezése
int8_t currentRssi = WiFi.RSSI();
// Első mérés esetén minden mintát az aktuális értékkel töltünk fel,
// így az átlag azonnal használható értéket ad
if(averagedRssi == -100)
{
for(int i = 0; i < rssiSampleCount; i++)
{
rssiSamples[i] = currentRssi;
}
averagedRssi = currentRssi;
return;
}
// Az új mérési eredmény eltárolása a körkörös mintatárolóban
rssiSamples[rssiSampleIndex] = currentRssi;
// Következő minta helyének meghatározása
rssiSampleIndex = (rssiSampleIndex + 1) % rssiSampleCount;
// A tárolt RSSI értékek összegzése
int32_t sum = 0;
for(int i = 0; i < rssiSampleCount; i++)
{
sum += rssiSamples[i];
}
// Átlagos RSSI érték kiszámítása
averagedRssi = sum / rssiSampleCount;
}
/*****************************************************************************/
// A hálózatkeresés eredményeinek kiértékelése.
// A függvény kiválasztja a legkedvezőbb hozzáférési pontot, majd hiszterézis
// és stabilitási feltételek alapján eldönti, hogy szükséges-e roamingot
// végrehajtani.
/*****************************************************************************/
void checkScanResults()
{
// Ellenőrizzük, hogy a hálózatkeresés befejeződött-e
int16_t networks = WiFi.scanComplete();
if(networks == WIFI_SCAN_RUNNING) return;
// Sikertelen keresés esetén töröljük a keresés állapotát
if(networks == WIFI_SCAN_FAILED)
{
if(debug)
{
Serial.println("Network scan failed!");
Serial.println();
}
isScanning = false;
return;
}
if(networks > 0)
{
// A keresés során talált legjobb AP adatainak tárolása
int bestNetworkIndex = -1;
int8_t bestScanRssi = -100;
String bestScanBssid = "";
if(debug)
{
Serial.print("Network scan completed. ");
Serial.print(networks);
Serial.println(" networks found:");
}
// Az azonos SSID-jű hálózatok közül kiválasztjuk
// a legerősebb, jelenleg nem használt hozzáférési pontot
for(int i = 0; i < networks; i++)
{
if(WiFi.SSID(i) == ssid)
{
int8_t scanRssi = WiFi.RSSI(i);
if(scanRssi > bestScanRssi && WiFi.BSSIDstr(i) != currentBssid)
{
bestScanRssi = scanRssi;
bestScanBssid = WiFi.BSSIDstr(i);
bestNetworkIndex = i;
}
}
if(debug)
{
Serial.print(i + 1);
Serial.print(". SSID: ");
Serial.print(WiFi.SSID(i));
Serial.print(" | RSSI: ");
Serial.print(WiFi.RSSI(i));
Serial.print(" | BSSID: ");
Serial.println(WiFi.BSSIDstr(i));
}
}
if(debug) Serial.println();
// Csak akkor vizsgáljuk a roaming lehetőségét,
// ha az új AP jelerőssége meghaladja a hiszterézis küszöbét
if(bestNetworkIndex != -1 && bestScanRssi > averagedRssi + rssiHysteresis)
{
// Ellenőrizzük, hogy ugyanaz az AP több egymást követő
// keresés során is a legjobb választás-e
if(bestScanBssid == candidateBssid)
{
candidateCount++;
}
else
{
candidateBssid = bestScanBssid;
candidateCount = 1;
}
// Ha a roaming feltételei teljesülnek,
// átváltunk az új hozzáférési pontra
if(candidateCount >= requiredStableScans)
{
if(debug)
{
Serial.printf("Roaming from %s to %s\n", currentBssid.c_str(), candidateBssid.c_str());
Serial.println();
}
// beállítjuk a jelzőt, hogy folyamatban van a roamingolás
isRoaming = true;
int32_t channel = WiFi.channel(bestNetworkIndex);
const uint8_t* bssid = WiFi.BSSID(bestNetworkIndex);
// MQTT kapcsolat bontása roaming előtt
if(mqttClient.connected()) mqttClient.disconnect();
// Wi-Fi kapcsolat bontása
WiFi.disconnect();
// A keresési eredmények törlése, memória felszabadítása
WiFi.scanDelete();
isScanning = false;
// Csatlakozás a kiválasztott AP-hoz
connectToWifi(ssid, password, channel, bssid);
return;
}
}
else
{
// Ha nincs megfelelő roaming célpont,
// töröljük az előző jelöltet
candidateBssid = "";
candidateCount = 0;
}
}
// A keresési eredmények törlése, memória felszabadítása
WiFi.scanDelete();
// Keresés befejezése jelző beállítása
isScanning = false;
// Az aktuális állapot továbbítása MQTT-n
if(mqttClient.connected())
{
mqttClient.publish(rssiTopic, String(averagedRssi).c_str());
mqttClient.publish(ipTopic, WiFi.localIP().toString().c_str());
if(debug)
{
Serial.print("Averaged RSSI: ");
Serial.println(averagedRssi);
Serial.print("IP: ");
Serial.println(WiFi.localIP());
Serial.println();
}
}
}
/*****************************************************************************/
// Az aktuális dátum és idő lekérése NTP szerverről.
// Sikeres időszinkronizálás után az időbélyeget formázza, majd MQTT üzenetként
// elküldi az eszköz indulási idejének naplózásához.
/*****************************************************************************/
void sendLocalTime()
{
char Msg_Timestamp[20];
struct tm timeinfo;
// Aktuális idő lekérése az NTP szerverről
if(!getLocalTime(&timeinfo))
{
if(debug)
{
Serial.println("Unable to retrieve the time from the NTP server!");
Serial.println();
}
return;
}
else
{
if(debug) Serial.println("Time retrieved!");
}
// Az idő formázása
strftime(Msg_Timestamp, sizeof(Msg_Timestamp), "%Y.%m.%d-%H:%M:%S", &timeinfo);
if(debug)
{
Serial.println(Msg_Timestamp);
Serial.println();
}
// Az időbélyeg elküldése MQTT üzenetként
mqttClient.publish(newstartTopic, Msg_Timestamp);
}
/*****************************************************************************/
// MQTT callback függvény.
// A függvényt az MQTT kliens automatikusan meghívja egy új üzenet érkezésekor.
// A beérkezett topik és üzenet tartalma alapján végrehajtja a szükséges
// műveleteket, jelenleg a hibakeresési mód távoli vezérlését valósítja meg.
// Később további funkciókhoz tartozó MQTT üzenetek feldolgozása is itt történik
/*****************************************************************************/
void mqttCallback(char* topic, byte* payload, unsigned int length)
{
// A beérkezett topik és üzenet karakterlánccá alakítása
String strTopic = String(topic);
String strPayload = String((char*)payload, length);
// A hibakeresési mód távoli vezérlése MQTT üzenettel
if(strTopic == debugTopic)
{
if(strPayload == "true")
{
debug = true;
Serial.println("Debug enabled!");
Serial.println();
}
else if(strPayload == "false")
{
Serial.println("Debug disabled!");
Serial.println();
debug = false;
}
}
// További MQTT üzenetek kezelése...
}
/*****************************************************************************/
// MQTT kapcsolat újraépítése.
// Ha a Wi-Fi kapcsolat aktív, de az MQTT kapcsolat megszakadt, a függvény
// megkísérli az újracsatlakozást a brokerhez, majd siker esetén feliratkozik
// a szükséges MQTT topikokra.
/*****************************************************************************/
void reconnectMqtt()
{
// Csak aktív Wi-Fi kapcsolat mellett és hálózatkeresés közben ne
// próbáljunk MQTT kapcsolatot létrehozni
if(wifiConnected && !mqttClient.connected() && !isScanning)
{
if(debug) Serial.println("Connecting to the mqtt server...");
// Kapcsolódás az MQTT brokerhez
if(mqttClient.connect(mqttClientId))
{
if(debug)
{
Serial.println("Mqtt server is connected!");
Serial.println();
}
// Feliratkozás a szükséges topikokra
mqttClient.subscribe(debugTopic);
}
else
{
if(debug)
{
Serial.print("Failed to connect to the mqtt server. | rc=");
Serial.println(mqttClient.state());
Serial.println();
}
}
}
}
/*****************************************************************************/
// A program inicializálása
/*****************************************************************************/
void setup()
{
// Soros kommunikáció inicializálása
Serial.begin(115200);
// Rövid várakozás a rendszer stabilizálódásához
delay(2000);
// Wi-Fi eseménykezelő regisztrálása
WiFi.onEvent(WiFiEvent);
// Wi-Fi kapcsolat inicializálása
setupWifi();
// MQTT szerver címének és portjának beállítása
mqttClient.setServer(mqttServer, mqttPort);
// MQTT callback függvény regisztrálása
mqttClient.setCallback(mqttCallback);
// Időszinkronizálás konfigurálása NTP szerver használatával
configTzTime(timeZone, ntpServer);
}
/*****************************************************************************/
// A program fő ciklusa.
// Folyamatosan felügyeli a Wi-Fi és az MQTT kapcsolat állapotát, időzítetten
// elindítja a hálózatkeresést, kiértékeli annak eredményét, valamint gondoskodik
// a roaming és az MQTT kommunikáció folyamatos működéséről.
/*****************************************************************************/
void loop()
{
// Az aktuális futási idő lekérdezése
currentMillis = millis();
// Ha nincs Wi-Fi kapcsolat és nem roaming miatt szakadt meg,
// újrakezdjük a kapcsolódást
if(!wifiConnected && !isRoaming)
{
setupWifi();
return;
}
else if(wifiConnected)
{
// Meghatározott időközönként új hálózatkeresés indítása
if(currentMillis - prevScanMillis > wifiScanFreq)
{
prevScanMillis = currentMillis;
startAsynchronousScan();
}
// A hálózatkeresés eredményének kiértékelése
if(isScanning)
{
checkScanResults();
}
// MQTT kapcsolat helyreállítása, ha szükséges
if(!mqttClient.connected())
{
reconnectMqtt();
}
else
{
// MQTT kommunikáció fenntartása
mqttClient.loop();
// Az indulási időbélyeg elküldése csak egyszer
if(first)
{
sendLocalTime();
first = false;
}
// További MQTT üzenetek küldése ide...
}
}
}