diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5dac9f5 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +.pioenvs +.piolibdeps +.clang_complete +.gcc-flags.json diff --git a/data/index.html b/data/index.html new file mode 100644 index 0000000..793177d --- /dev/null +++ b/data/index.html @@ -0,0 +1,24 @@ + + + + Domofon + + + + + + + +
+

Domofon

+ + + +
+ + +
+
+ + + diff --git a/data/script.js b/data/script.js new file mode 100644 index 0000000..d39cc94 --- /dev/null +++ b/data/script.js @@ -0,0 +1,63 @@ +(function() { + var terminal = document.getElementById('terminal'), + clear = document.getElementById('clear'), + restart = document.getElementById('restart'), + ws = null, + reconnectTimeout = null, + prefix = ''; + + function _connect() { + if (!ws || ws.readyState === WebSocket.CLOSED) { + try { + ws = new WebSocket('ws://' + window.location.hostname + '/ws'); + ws.onopen = _onOpen; + ws.onmessage = _onMessage; + ws.onclose = _onClose; + } catch (e) { + _onClose(); + } + } + } + + function _onOpen() { + clearTimeout(reconnectTimeout); + } + + function _onClose(e) { + var code = e && e.code || 1012; + ws = null; + if (code > 1000) { + reconnectTimeout = setTimeout(_connect, 1000); + } + } + + function _onMessage(message) { + var data = message && message.data; + if (data) { + data = prefix + data; + + if (data.endsWith("\n")) { + prefix = "\n"; + data = data.substr(0, data.length - 1); + } else { + prefix = ''; + } + + terminal.value += data; + terminal.scrollTop = terminal.scrollHeight; + } + } + + clear.addEventListener('click', function(e) { + terminal.value = ''; + prefix = ''; + }); + + restart.addEventListener('click', function(e) { + var xhr = new XMLHttpRequest(); + xhr.open('POST', '/restart', true); + xhr.send(null); + }); + + _connect(); +})(); diff --git a/data/style.css b/data/style.css new file mode 100644 index 0000000..caa4d43 --- /dev/null +++ b/data/style.css @@ -0,0 +1,80 @@ +html, body { + margin: 0; + padding: 0; + width: 100%; + height: 100%; + font-family: sans-serif; +} + +.content { + line-height: 1.6em; + margin: 0 auto; + padding: 30px 0 50px; + max-width: 50pc; + padding: 0 2em; + overflow: hidden; +} + +h2 { + font-size: 3em; + font-weight: 300; + margin: .5em 0 .3em; + text-align: center; + line-height: 1em; +} + +input, textarea { + border-radius: 4px; + padding: .5em .6em; + box-sizing: border-box; + outline: none; +} + +input { + height: 36px; + width: 20%; + min-width: 140px; + font-family: sans-serif; + vertical-align: middle; + line-height: normal; + display: inline-block; + white-space: nowrap; + font-size: 100%; + cursor: pointer; + user-select: none; + color: #fff; + text-shadow: 0 1px 1px rgba(0,0,0,.2); + text-align: center; +} + +input:active { + box-shadow: 0 0 0 1px rgba(0,0,0,.15) inset, 0 0 6px rgba(0,0,0,.2) inset; +} + +.controls { + margin: .5em 0 1em; +} + +#clear { + background: #009a3e; +} + +#restart { + float: right; + background: #c01200; +} + +#terminal { + display: block; + width: 100%; + background: #222; + height: 25pc; + max-height: 60vh; + color: #c9ea7b; + font-family: Courier New, monospace; + font-size: 80%; + line-height: 110%; + resize: none; + box-shadow: inset 0 1px 3px #ddd; + border: 1px solid #ccc; +} diff --git a/lib/readme.txt b/lib/readme.txt new file mode 100644 index 0000000..cfa16df --- /dev/null +++ b/lib/readme.txt @@ -0,0 +1,41 @@ + +This directory is intended for project specific (private) libraries. +PlatformIO will compile them to static libraries and link them to executable files. + +The source code of each library should be placed in separate directories, like +"lib/private_lib/[here are source files]". + +For example, see the structure of the following two libraries `Foo` and `Bar`: + +|--lib +| | +| |--Bar +| | |--docs +| | |--examples +| | |--src +| | |- Bar.c +| | |- Bar.h +| | |- library.json (optional, custom build options, etc) https://docs.platformio.org/page/librarymanager/config.html +| | +| |--Foo +| | |- Foo.c +| | |- Foo.h +| | +| |- readme.txt --> THIS FILE +| +|- platformio.ini +|--src + |- main.c + +Then in `src/main.c` you should use: + +#include +#include + +// rest H/C/CPP code + +PlatformIO will find your libraries automatically, configure preprocessor's +include paths and build them. + +More information about PlatformIO Library Dependency Finder +- https://docs.platformio.org/page/librarymanager/ldf.html diff --git a/platformio.ini b/platformio.ini new file mode 100644 index 0000000..7bbfda2 --- /dev/null +++ b/platformio.ini @@ -0,0 +1,33 @@ +; PlatformIO Project Configuration File +; +; Build options: build flags, source filter +; Upload options: custom upload port, speed and extra flags +; Library options: dependencies, extra library storages +; Advanced options: extra scripting +; +; Please visit documentation for the other options and examples +; https://docs.platformio.org/page/projectconf.html + +[platformio] +env_default = nodemcuv2 +src_dir = src + +[env:nodemcuv2] +platform = espressif8266 +board = nodemcuv2 +framework = arduino + +lib_deps = + PubSubClient + Bounce2 + ESP Async WebServer + +; targets = upload, uploadfs + +upload_port = +upload_flags = --auth=domofon + +; upload_port = /dev/cu.wchusbserial1430 +; upload_speed = 115200 +; monitor_port = /dev/cu.wchusbserial1430 +; monitor_speed = 115200 diff --git a/src/config/hardware.h b/src/config/hardware.h new file mode 100644 index 0000000..fa9006a --- /dev/null +++ b/src/config/hardware.h @@ -0,0 +1,17 @@ +#ifndef HW_H_ +#define HW_H_ + +// Hardware configuration +#define PIN_RELAY_ANSWER 16 //D0 +#define PIN_RELAY_DOOR_OPEN 5 //D1 + +#define PIN_LED_RED 4 //D2 +#define PIN_LED_GREEN 0 //D3 +#define PIN_LED_BLUE 2 //D4 + +#define PIN_CALL_DETECT 14 //D5 + +#define PIN_BUTTON_GREEN 12 //D6 +#define PIN_BUTTON_RED 13 //D7 + +#endif // HW_H_ diff --git a/src/config/mqtt.h b/src/config/mqtt.h new file mode 100644 index 0000000..3cadd6c --- /dev/null +++ b/src/config/mqtt.h @@ -0,0 +1,19 @@ +#ifndef MQTT_H_ +#define MQTT_H_ + +// High level protocol messages +#define MSG_STATUS_READY "R" // ready; sent after successfull boot-up or after receiving of 'P' message +#define MSG_STATUS_LAST_WILL "L" // last will message; send when device goes offline + +#define MSG_OUT_CALL "C" // call; sent after detecting of incoming intercom call +#define MSG_OUT_HANGUP "H" // hangup; sent after detected incoming call finished +#define MSG_OUT_OPENED_BY_BUTTON "B" // button; sent when "door open" has been performed by green hw button press +#define MSG_OUT_REJECTED_BY_BUTTON "J" // reJected; sent when incoming call has been rejected by red hw button press +#define MSG_OUT_SUCCESS "S" // success; sent in response to 'O' or 'N' command +#define MSG_OUT_FAIL "F" // fail; sent in response to 'O' or 'N' command (this means that 'O' or 'N' command has been received but no incoming call detected) + +#define MSG_IN_OPEN 'O' // door open command +#define MSG_IN_REJECT 'N' // call reject command (door will not open) +#define MSG_IN_PING 'P' // ping command (answers with 'R') + +#endif // MQTT_H_ diff --git a/src/config/software.h b/src/config/software.h new file mode 100644 index 0000000..43081a1 --- /dev/null +++ b/src/config/software.h @@ -0,0 +1,24 @@ +#ifndef SW_H_ +#define SW_H_ + +// Software configuration +#define HOST_NAME "domofon" +#define HOST_PASSWORD "domofon" +#define OTA_PORT 8266 +#define WIFI_SSID "" +#define WIFI_PASSWORD "" + +#define MQTT_SERVER_ADDR "" +#define MQTT_SERVER_PORT 1883 +#define MQTT_USER_NAME "" +#define MQTT_USER_PASSWORD "" +#define MQTT_CLIENT_ID "domofon" +#define MQTT_TOPIC_IN "domofon/in" +#define MQTT_TOPIC_OUT "domofon/out" +#define MQTT_TOPIC_STATUS "domofon/status" + +#define CALL_HANGUP_DETECT_DELAY 3000 +#define RELAY_ANSWER_ON_TIME 1500 +#define RELAY_OPEN_ON_TIME 600 + +#endif // SW_H_ diff --git a/src/debug.ino b/src/debug.ino new file mode 100644 index 0000000..0d3668b --- /dev/null +++ b/src/debug.ino @@ -0,0 +1,12 @@ +#include "inc/include.h" + +void DEBUG_LN(const char *text) { + Serial.println(text); + webSocket().printfAll("%s\n", text); +} + +template +void DEBUG_F(const char *format, Args... args) { + Serial.printf(format, args...); + webSocket().printfAll(format, args...); +} diff --git a/src/domofon.ino b/src/domofon.ino new file mode 100644 index 0000000..b83e7bb --- /dev/null +++ b/src/domofon.ino @@ -0,0 +1,145 @@ +#include "inc/include.h" + +EState state = IDLE; +EAction action = NO_ACTION; + +Bounce debouncerBtnGreen = Bounce(); +Bounce debouncerBtnRed = Bounce(); + +unsigned long lastCallDetectedTime = 0; + +void callAnswer() { + DEBUG_LN("[HW] Call answer..."); + digitalWrite(PIN_RELAY_DOOR_OPEN, RELAY_OFF); + digitalWrite(PIN_RELAY_ANSWER, RELAY_ON); + DEBUG_LN("[HW] Done"); +} + +void callHangUp() { + DEBUG_LN("[HW] Hang up..."); + digitalWrite(PIN_RELAY_ANSWER, RELAY_OFF); + digitalWrite(PIN_RELAY_DOOR_OPEN, RELAY_OFF); + DEBUG_LN("[HW] Done"); +} + +void doorOpen() { + DEBUG_LN("[HW] Door open..."); + digitalWrite(PIN_RELAY_DOOR_OPEN, RELAY_ON); + delay(RELAY_OPEN_ON_TIME); + digitalWrite(PIN_RELAY_DOOR_OPEN, RELAY_OFF); + DEBUG_LN("[HW] Done"); +} + +void answerAndOpen() { + callAnswer(); + delay(RELAY_ANSWER_ON_TIME); + doorOpen(); + callHangUp(); +} + +void answerAndReject() { + callAnswer(); + delay(RELAY_ANSWER_ON_TIME); + callHangUp(); +} + +void handleIdle(EState oldState) { + if (oldState != IDLE) { + mqttSendCommand(MSG_OUT_HANGUP); + ledOff(); + DEBUG_LN("[HW] Current state: IDLE"); + } + + if (action != NO_ACTION) { + mqttSendCommand(MSG_OUT_FAIL); + action = NO_ACTION; + } + + if (debouncerBtnGreen.fell()) { + DEBUG_LN("[HW] Button click"); + ledBlink(PIN_LED_GREEN, 2); + } +} + +void handleCall(EState oldState) { + if (oldState != CALL) { + action = NO_ACTION; + mqttSendCommand(MSG_OUT_CALL); + ledOn(PIN_LED_RED); + DEBUG_LN("[HW] Current state: CALL"); + } + + if (action == NO_ACTION) { + if (debouncerBtnRed.fell()) { + action = REJECT_BY_BUTTON; + } else if (debouncerBtnGreen.fell()) { + action = OPEN_BY_BUTTON; + } + } + + switch (action) { + case OPEN_BY_BUTTON: + answerAndOpen(); + mqttSendCommand(MSG_OUT_OPENED_BY_BUTTON); + break; + + case REJECT_BY_BUTTON: + answerAndReject(); + mqttSendCommand(MSG_OUT_REJECTED_BY_BUTTON); + break; + + case OPEN: + answerAndOpen(); + mqttSendCommand(MSG_OUT_SUCCESS); + break; + + case REJECT: + answerAndReject(); + mqttSendCommand(MSG_OUT_SUCCESS); + break; + + default: + break; + } + + action = NO_ACTION; +} + +void setStateIdle() { + state = IDLE; +} + +void setStateCall() { + state = CALL; +} + +void domofonSetup() { + debouncerBtnGreen.attach(PIN_BUTTON_GREEN); + debouncerBtnGreen.interval(25); + debouncerBtnRed.attach(PIN_BUTTON_RED); + debouncerBtnRed.interval(25); +} + +void domofonLoop() { + debouncerBtnGreen.update(); + debouncerBtnRed.update(); + + EState oldState = state; + + if (digitalRead(PIN_CALL_DETECT) == LOW) { + setStateCall(); + lastCallDetectedTime = millis(); + } else if (millis() - lastCallDetectedTime > CALL_HANGUP_DETECT_DELAY) { + setStateIdle(); + } + + switch (state) { + case IDLE: + handleIdle(oldState); + break; + + case CALL: + handleCall(oldState); + break; + } +} diff --git a/src/hardware.ino b/src/hardware.ino new file mode 100644 index 0000000..80d0509 --- /dev/null +++ b/src/hardware.ino @@ -0,0 +1,28 @@ +#include "inc/include.h" + +Ticker _defer_restart; + +void hardwareSetup() { + pinMode(PIN_BUTTON_GREEN, INPUT_PULLUP); + pinMode(PIN_BUTTON_RED, INPUT_PULLUP); + pinMode(PIN_CALL_DETECT, INPUT_PULLUP); + pinMode(PIN_LED_RED, OUTPUT); + pinMode(PIN_LED_GREEN, OUTPUT); + pinMode(PIN_LED_BLUE, OUTPUT); + pinMode(PIN_RELAY_ANSWER, OUTPUT); + pinMode(PIN_RELAY_DOOR_OPEN, OUTPUT); + + digitalWrite(PIN_LED_RED, LED_OFF); + digitalWrite(PIN_LED_GREEN, LED_OFF); + digitalWrite(PIN_LED_BLUE, LED_OFF); + digitalWrite(PIN_RELAY_ANSWER, RELAY_OFF); + digitalWrite(PIN_RELAY_DOOR_OPEN, RELAY_OFF); +} + +void restart() { + ESP.reset(); +} + +void deferredRestart(unsigned long delay) { + _defer_restart.once_ms(delay, restart); +} diff --git a/src/inc/include.h b/src/inc/include.h new file mode 100644 index 0000000..667de89 --- /dev/null +++ b/src/inc/include.h @@ -0,0 +1,14 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "../config/hardware.h" +#include "../config/software.h" +#include "../config/mqtt.h" +#include "types.h" diff --git a/src/inc/types.h b/src/inc/types.h new file mode 100644 index 0000000..932b0e7 --- /dev/null +++ b/src/inc/types.h @@ -0,0 +1,22 @@ +#ifndef TYPES_H_ +#define TYPES_H_ + +#define LED_ON HIGH +#define LED_OFF LOW +#define RELAY_ON LOW +#define RELAY_OFF HIGH + +typedef enum { + IDLE, + CALL +} EState; + +typedef enum { + NO_ACTION, + OPEN, + OPEN_BY_BUTTON, + REJECT, + REJECT_BY_BUTTON +} EAction; + +#endif // TYPES_H_ diff --git a/src/led.ino b/src/led.ino new file mode 100644 index 0000000..a34bff4 --- /dev/null +++ b/src/led.ino @@ -0,0 +1,21 @@ +#include "inc/include.h" + +void ledBlink(int pin, int count) { + for (int i = 0; i < count; i++) { + digitalWrite(pin, LED_ON); + delay(125); + digitalWrite(pin, LED_OFF); + delay(125); + } +} + +void ledOff() { + digitalWrite(PIN_LED_GREEN, LED_OFF); + digitalWrite(PIN_LED_RED, LED_OFF); + digitalWrite(PIN_LED_BLUE, LED_OFF); +} + +void ledOn(int pin) { + ledOff(); + digitalWrite(pin, LED_ON); +} diff --git a/src/main.ino b/src/main.ino new file mode 100644 index 0000000..515b3cb --- /dev/null +++ b/src/main.ino @@ -0,0 +1,20 @@ +#include "inc/include.h" + +// Код частично заимствован и переделан +// @see https://github.com/Metori/mqtt_domofon + +void setup() { + hardwareSetup(); + wifiSetup(); + otaSetup(); + mqttSetup(); + webServerSetup(); + domofonSetup(); +} + +void loop() { + wifiLoop(); + otaLoop(); + mqttLoop(); + domofonLoop(); +} diff --git a/src/mqtt.ino b/src/mqtt.ino new file mode 100644 index 0000000..894516f --- /dev/null +++ b/src/mqtt.ino @@ -0,0 +1,89 @@ +#include "inc/include.h" + +WiFiClient espClient; +PubSubClient mqttClient(espClient); + +void mqttSendCommand(const char *msg) { + mqttClient.publish(MQTT_TOPIC_OUT, msg, 1); + DEBUG_F("[MQTT] Message sent: %s\n", msg); +} + +void mqttSendStatus(const char *msg) { + mqttClient.publish(MQTT_TOPIC_STATUS, msg, 1); + DEBUG_F("[MQTT] Status sent: %s\n", msg); +} + +void onMqttMsgReceived(char* topic, byte* payload, unsigned int len) { + if (len != 1) { + char* command = (char*)malloc(len + 2); + memcpy(command, payload, len); + command[len] = '\0'; + + DEBUG_F("[MQTT] Message received [%u]: %s\n", len, command); + return; + } + + char cmd = (char)payload[0]; + DEBUG_F("[MQTT] Command received: %c\n", cmd); + + switch (cmd) { + case MSG_IN_OPEN: + action = OPEN; + break; + + case MSG_IN_REJECT: + action = REJECT; + break; + + case MSG_IN_PING: + mqttSendStatus(MSG_STATUS_READY); + break; + + default: + DEBUG_LN("[MQTT] Unknown command"); + break; + } +} + +void mqttConnect() { + DEBUG_F("[MQTT] (Re)connecting to server on %s...\n", MQTT_SERVER_ADDR); + + for (int i = 0; !mqttClient.connected(); i++) { + // Если не получилось за 5 попыток - перезагружаемся + if (i >= 5) { + ESP.restart(); + return; + } + + if (!mqttClient.connect(MQTT_CLIENT_ID, MQTT_USER_NAME, MQTT_USER_PASSWORD, MQTT_TOPIC_STATUS, 0, 0, MSG_STATUS_LAST_WILL)) { + // Ждем 2 секунды + DEBUG_F("."); + ledBlink(PIN_LED_GREEN, 8); + } + } + + DEBUG_LN("\n[MQTT] Done"); + mqttSendStatus(MSG_STATUS_READY); + mqttClient.subscribe(MQTT_TOPIC_IN); + + setStateIdle(); + DEBUG_LN("[MQTT] Current state: IDLE"); +} + +void mqttStop() { + mqttClient.disconnect(); + ledOff(); +} + +void mqttSetup() { + mqttClient.setServer(MQTT_SERVER_ADDR, MQTT_SERVER_PORT); + mqttClient.setCallback(onMqttMsgReceived); + mqttConnect(); +} + +void mqttLoop() { + if (!mqttClient.connected()) { + mqttConnect(); + } + mqttClient.loop(); +} diff --git a/src/ota.ino b/src/ota.ino new file mode 100644 index 0000000..f709297 --- /dev/null +++ b/src/ota.ino @@ -0,0 +1,58 @@ +#include "inc/include.h" + +void otaSetup() { + ArduinoOTA.setHostname(HOST_NAME); + ArduinoOTA.setPort(OTA_PORT); + + ArduinoOTA.setPassword(HOST_PASSWORD); + + ArduinoOTA.onStart([]() { + DEBUG_LN("[OTA] Start update"); + webServerStop(); + mqttStop(); + }); + + ArduinoOTA.onEnd([]() { + DEBUG_LN("[OTA] End update"); + }); + + ArduinoOTA.onProgress([](unsigned int progress, unsigned int total) { + DEBUG_F("[OTA] Progress: %u%%\n", (progress / (total / 100))); + }); + + ArduinoOTA.onError([](ota_error_t error) { + String type; + switch (error) { + // "Ошибка при аутентификации" + case OTA_AUTH_ERROR: + type = "Auth Failed"; + break; + // "Ошибка при начале OTA-апдейта" + case OTA_BEGIN_ERROR: + type = "Begin Failed"; + break; + // "Ошибка при подключении" + case OTA_CONNECT_ERROR: + type = "Connect Failed"; + break; + // "Ошибка при получении данных" + case OTA_RECEIVE_ERROR: + type = "Receive Failed"; + break; + // "Ошибка при завершении OTA-апдейта" + case OTA_END_ERROR: + type = "End Failed"; + break; + default: + type = "Unknown"; + } + + DEBUG_F("[OTA] Error[%u]: %s\n", error, type.c_str()); + }); + + ArduinoOTA.begin(); +} + +void otaLoop() { + ArduinoOTA.handle(); +} diff --git a/src/server.ino b/src/server.ino new file mode 100644 index 0000000..b26a66f --- /dev/null +++ b/src/server.ino @@ -0,0 +1,72 @@ +#include "inc/include.h" + +AsyncWebServer _server(80); +AsyncWebSocket _ws("/ws"); + +AsyncWebSocket webSocket() { + return _ws; +} + +void _wsEvent(AsyncWebSocket * server, AsyncWebSocketClient * client, AwsEventType type, void * arg, uint8_t *data, size_t len) { + uint32_t id = client->id(); + + switch (type) { + case WS_EVT_CONNECT: + DEBUG_F("[WS] Client connected - ID: %u\n", id); + client->ping(); + break; + + case WS_EVT_DISCONNECT: + DEBUG_F("[WS] Client disconnected - ID: %u\n", id); + break; + + case WS_EVT_ERROR: + DEBUG_F("[WS] Error - ID: %u, code: %u, error: %s\n", id, *((uint16_t*)arg), (char*)data); + break; + + case WS_EVT_PONG: + DEBUG_F("[WS] Pong - ID: %u\n", id); + break; + + case WS_EVT_DATA: + AwsFrameInfo * info = (AwsFrameInfo*)arg; + if (info->final && info->index == 0 && info->len == len) { + if (info->opcode == WS_TEXT) { + DEBUG_F("[WS] Message received - ID: %u, message: %s\n", id, (char*)data); + } else { + DEBUG_F("[WS] Bynary messages not supported - ID: %u\n", id); + } + } else if (info->final && (info->index + len) == info->len) { + DEBUG_F("[WS] Message too long - ID: %u\n", id); + } + break; + } +} + +void webServerStop() { + DEBUG_LN("[WS] Server stopped"); + SPIFFS.end(); + _ws.enable(false); + _ws.closeAll(1012); +} + +void webServerSetup() { + SPIFFS.begin(); + + _server.rewrite("/", "/index.html"); + _server.onNotFound([](AsyncWebServerRequest *request) { + request->send(404, "text/plain", "404: Not Found"); + }); + _server.on("/restart", HTTP_POST, [](AsyncWebServerRequest *request) { + request->send(200); + DEBUG_LN("Restarting..."); + webServerStop(); + mqttStop(); + deferredRestart(200); + }); + _server.serveStatic("/", SPIFFS, "/"); + _server.begin(); + + _ws.onEvent(_wsEvent); + _server.addHandler(&_ws); +} diff --git a/src/wifi.ino b/src/wifi.ino new file mode 100644 index 0000000..a36c1c0 --- /dev/null +++ b/src/wifi.ino @@ -0,0 +1,44 @@ +#include "inc/include.h" + +void wifiDisconnect() { + WiFi.disconnect(); + ledOff(); +} + +void wifiConnect() { + DEBUG_F("[WIFI] (Re)connecting to \"%s\"\n", WIFI_SSID); + + WiFi.mode(WIFI_STA); + WiFi.hostname(HOST_NAME); + WiFi.begin(WIFI_SSID, WIFI_PASSWORD); + + for (int i = 0; WiFi.status() != WL_CONNECTED; i++) { + // Если не получилось за 5 попыток - перезагружаемся + if (i >= 5) { + ESP.restart(); + return; + } + + // Ждем 2 секунды + DEBUG_F("."); + ledBlink(PIN_LED_BLUE, 8); + } + + DEBUG_LN("\n[WIFI] Done"); + DEBUG_F("[WIFI] IP address: %s\n", WiFi.localIP().toString().c_str()); +} + +void wifiReconnect() { + wifiDisconnect(); + wifiConnect(); +} + +void wifiSetup() { + wifiConnect(); +} + +void wifiLoop() { + if (WiFi.status() != WL_CONNECTED) { + wifiReconnect(); + } +}