/* ESP32 Bluetooth Low Energy presence detection, for use with MQTT. Version 0.0.6 Major thank you to the following contributors for their efforts: pcbreflux for the original version of this code, as well as the eddystone handlers. Andreis Speiss for his work on YouTube and his invaluable github at sensorsiot. Sidddy for the implementation of Mi Flora plant sensor support. https://github.com/sidddy/flora Based on Neil Kolban example for IDF: https://github.com/nkolban/esp32-snippets/blob/master/cpp_utils/tests/BLE%20Tests/SampleScan.cpp Ported to Arduino ESP32 by Evandro Copercini */ #include extern "C" { #include "freertos/FreeRTOS.h" #include "freertos/timers.h" } #include "soc/timer_group_struct.h" #include "soc/timer_group_reg.h" #include #include #include #include #include #include #include #include #include "BLEBeacon.h" #include "BLEEddystoneTLM.h" #include "BLEEddystoneURL.h" #include "Settings_local.h" BLEScan* pBLEScan; int scanTime = singleScanTime; //In seconds int waitTime = scanInterval; //In seconds bool updateInProgress = false; String localIp; uint16_t beaconUUID = 0xFEAA; #define ENDIAN_CHANGE_U16(x) ((((x)&0xFF00)>>8) + (((x)&0xFF)<<8)) WiFiClient espClient; AsyncMqttClient mqttClient; TimerHandle_t mqttReconnectTimer; TimerHandle_t wifiReconnectTimer; byte retryAttempts = 0; String getProximityUUIDString(BLEBeacon beacon) { std::string serviceData = beacon.getProximityUUID().toString().c_str(); int serviceDataLength = serviceData.length(); String returnedString = ""; int i = serviceDataLength; while (i > 0) { if (serviceData[i-1] == '-') { i--; } char a = serviceData[i-1]; char b = serviceData[i-2]; returnedString += b; returnedString += a; i -= 2; } return returnedString; } float calculateDistance(int rssi, int txPower) { float distFl; if (rssi == 0) { return -1.0; } if (!txPower) { // somewhat reasonable default value txPower = -59; } else if (txPower > 0) { txPower = txPower * -1; } const float ratio = rssi * 1.0 / txPower; if (ratio < 1.0) { distFl = pow(ratio, 10); } else { distFl = (0.89976) * pow(ratio, 7.7095) + 0.111; } return round(distFl * 100) / 100; } bool sendTelemetry(int deviceCount = -1, int reportCount = -1) { StaticJsonDocument<256> tele; tele["room"] = room; tele["ip"] = localIp; tele["hostname"] = WiFi.getHostname(); tele["scan_dur"] = scanTime; tele["wait_dur"] = waitTime; tele["max_dist"] = maxDistance; if (deviceCount > -1) { Serial.printf("devices_discovered: %d\n\r",deviceCount); tele["disc_ct"] = deviceCount; } if (reportCount > -1) { Serial.printf("devices_reported: %d\n\r",reportCount); tele["rept_ct"] = reportCount; } char teleMessageBuffer[258]; serializeJson(tele, teleMessageBuffer); if (mqttClient.publish(telemetryTopic, 0, 1, teleMessageBuffer) == true) { Serial.println("Telemetry sent"); return true; } else { Serial.println("Error sending telemetry"); return false; } } void connectToWifi() { Serial.println("Connecting to WiFi..."); WiFi.begin(ssid, password); WiFi.setHostname(hostname); } void connectToMqtt() { Serial.println("Connecting to MQTT"); if (WiFi.isConnected() && !updateInProgress) { mqttClient.setCredentials(mqttUser, mqttPassword); mqttClient.setClientId(hostname); mqttClient.connect(); } else { Serial.println("Cannot reconnect MQTT - WiFi error"); handleWifiDisconnect(); } } bool handleMqttDisconnect() { if (updateInProgress) { Serial.println("Not retrying MQTT connection - OTA update in progress"); return true; } if (retryAttempts > 10) { Serial.println("Too many retries. Restarting"); ESP.restart(); } else { retryAttempts++; } if (WiFi.isConnected() && !updateInProgress) { Serial.println("Starting MQTT reconnect timer"); if (xTimerReset(mqttReconnectTimer, 0) == pdFAIL) { Serial.println("failed to restart"); xTimerStart(mqttReconnectTimer, 0); } else { Serial.println("restarted"); } } else { Serial.print("Disconnected from WiFi; starting WiFi reconnect timiler\t"); handleWifiDisconnect(); } } bool handleWifiDisconnect() { if (WiFi.isConnected()) { Serial.println("WiFi appears to be connected. Not retrying."); return true; } if (retryAttempts > 10) { Serial.println("Too many retries. Restarting"); ESP.restart(); } else { retryAttempts++; } if (mqttClient.connected()) { mqttClient.disconnect(); } if (xTimerIsTimerActive(mqttReconnectTimer) != pdFALSE) { xTimerStop(mqttReconnectTimer, 0); // ensure we don't reconnect to MQTT while reconnecting to Wi-Fi } if (xTimerReset(wifiReconnectTimer, 0) == pdFAIL) { Serial.println("failed to restart"); xTimerStart(wifiReconnectTimer, 0); return false; } else { Serial.println("restarted"); return true; } } void WiFiEvent(WiFiEvent_t event) { Serial.printf("[WiFi-event] event: %x\n\r", event); switch(event) { case SYSTEM_EVENT_STA_GOT_IP: digitalWrite(LED_BUILTIN, !LED_ON); Serial.print("IP address: \t"); Serial.println(WiFi.localIP()); localIp = WiFi.localIP().toString().c_str(); Serial.print("Hostname: \t"); Serial.println(WiFi.getHostname()); connectToMqtt(); if (xTimerIsTimerActive(wifiReconnectTimer) != pdFALSE) { Serial.println("Stopping wifi reconnect timer"); xTimerStop(wifiReconnectTimer, 0); } retryAttempts = 0; break; case SYSTEM_EVENT_STA_DISCONNECTED: digitalWrite(LED_BUILTIN, LED_ON); Serial.println("WiFi lost connection, resetting timer\t"); handleWifiDisconnect(); break; case SYSTEM_EVENT_WIFI_READY: Serial.println("Wifi Ready"); handleWifiDisconnect(); break; case SYSTEM_EVENT_STA_START: Serial.println("STA Start"); tcpip_adapter_set_hostname(TCPIP_ADAPTER_IF_STA, hostname); if (xTimerIsTimerActive(wifiReconnectTimer) != pdFALSE) { TickType_t xRemainingTime = xTimerGetExpiryTime( wifiReconnectTimer ) - xTaskGetTickCount(); Serial.print("WiFi Time remaining: "); Serial.println(xRemainingTime); } else { Serial.println("WiFi Timer is inactive; resetting\t"); handleWifiDisconnect(); } break; case SYSTEM_EVENT_STA_STOP: Serial.println("STA Stop"); handleWifiDisconnect(); break; } } void onMqttConnect(bool sessionPresent) { Serial.println("Connected to MQTT."); retryAttempts = 0; if (mqttClient.publish(availabilityTopic, 0, 1, "CONNECTED") == true) { Serial.print("Success sending message to topic:\t"); Serial.println(availabilityTopic); } else { Serial.println("Error sending message"); } sendTelemetry(); } void onMqttDisconnect(AsyncMqttClientDisconnectReason reason) { Serial.println("Disconnected from MQTT."); handleMqttDisconnect(); } bool reportDevice(BLEAdvertisedDevice advertisedDevice) { // Serial.printf("\n\n"); StaticJsonDocument<500> doc; String mac_address = advertisedDevice.getAddress().toString().c_str(); mac_address.replace(":",""); mac_address.toLowerCase(); // Serial.print("mac:\t"); // Serial.println(mac_address); int rssi = advertisedDevice.getRSSI(); float distance; doc["id"] = mac_address; doc["uuid"] = mac_address; doc["rssi"] = rssi; if (advertisedDevice.haveName()){ String nameBLE = String(advertisedDevice.getName().c_str()); // Serial.print("Name: "); // Serial.println(nameBLE); doc["name"] = nameBLE; // } else { // doc["name"] = "unknown"; } // Serial.printf("\n\r"); // Serial.printf("Advertised Device: %s \n\r", advertisedDevice.toString().c_str()); std::string strServiceData = advertisedDevice.getServiceData(); uint8_t cServiceData[100]; strServiceData.copy((char *)cServiceData, strServiceData.length(), 0); if (advertisedDevice.getServiceDataUUID().equals(BLEUUID(beaconUUID))==true) { // found Eddystone UUID // Serial.printf("is Eddystone: %d %s length %d\n", advertisedDevice.getServiceDataUUID().bitSize(), advertisedDevice.getServiceDataUUID().toString().c_str(),strServiceData.length()); if (cServiceData[0]==0x10) { BLEEddystoneURL oBeacon = BLEEddystoneURL(); oBeacon.setData(strServiceData); // Serial.printf("Eddystone Frame Type (Eddystone-URL) "); // Serial.printf(oBeacon.getDecodedURL().c_str()); doc["url"] = oBeacon.getDecodedURL().c_str(); } else if (cServiceData[0]==0x20) { BLEEddystoneTLM oBeacon = BLEEddystoneTLM(); oBeacon.setData(strServiceData); // Serial.printf("Eddystone Frame Type (Unencrypted Eddystone-TLM) \n"); // Serial.printf(oBeacon.toString().c_str()); } else { // Serial.println("service data"); for (int i=0;i (waitTime * 1000) || last == 0)) { Serial.print("Scanning...\t"); BLEScanResults foundDevices = pBLEScan->start(scanTime); int devicesCount = foundDevices.getCount(); Serial.printf("Scan done! Devices found: %d\n\r",devicesCount); int devicesReported = 0; if (mqttClient.connected()) { for (uint32_t i = 0; i < devicesCount; i++) { bool included = reportDevice(foundDevices.getDevice(i)); if (included) { devicesReported++; } } sendTelemetry(devicesCount, devicesReported); pBLEScan->clearResults(); } else { Serial.println("Cannot report; mqtt disconnected"); if (xTimerIsTimerActive(mqttReconnectTimer) != pdFALSE) { TickType_t xRemainingTime = xTimerGetExpiryTime( mqttReconnectTimer ) - xTaskGetTickCount(); Serial.print("Time remaining: "); Serial.println(xRemainingTime); } else { handleMqttDisconnect(); } } last = millis(); } } } void configureOTA() { ArduinoOTA .onStart([]() { Serial.println("OTA Start"); pBLEScan->stop(); updateInProgress = true; mqttClient.disconnect(true); xTimerStop(mqttReconnectTimer, 0); // ensure we don't reconnect to MQTT while reconnecting to Wi-Fi }) .onEnd([]() { updateInProgress = false; digitalWrite(LED_BUILTIN, !LED_ON); Serial.println("\n\rEnd"); }) .onProgress([](unsigned int progress, unsigned int total) { byte percent = (progress / (total / 100)); Serial.printf("Progress: %u% \n\r", percent); digitalWrite(LED_BUILTIN, percent % 2); }) .onError([](ota_error_t error) { Serial.printf("Error[%u]: ", error); if (error == OTA_AUTH_ERROR) Serial.println("Auth Failed"); else if (error == OTA_BEGIN_ERROR) Serial.println("Begin Failed"); else if (error == OTA_CONNECT_ERROR) Serial.println("Connect Failed"); else if (error == OTA_RECEIVE_ERROR) Serial.println("Receive Failed"); else if (error == OTA_END_ERROR) Serial.println("End Failed"); ESP.restart(); }); ArduinoOTA.setHostname(hostname); ArduinoOTA.setPort(8266); ArduinoOTA.begin(); } void setup() { Serial.begin(115200); pinMode(LED_BUILTIN, OUTPUT); digitalWrite(LED_BUILTIN, LED_ON); mqttReconnectTimer = xTimerCreate("mqttTimer", pdMS_TO_TICKS(2000), pdFALSE, (void*)0, reinterpret_cast(connectToMqtt)); wifiReconnectTimer = xTimerCreate("wifiTimer", pdMS_TO_TICKS(2000), pdFALSE, (void*)0, reinterpret_cast(connectToWifi)); WiFi.onEvent(WiFiEvent); mqttClient.onConnect(onMqttConnect); mqttClient.onDisconnect(onMqttDisconnect); mqttClient.setServer(mqttHost, mqttPort); mqttClient.setWill(availabilityTopic, 0, 1, "DISCONNECTED"); mqttClient.setKeepAlive(60); connectToWifi(); configureOTA(); BLEDevice::init(""); pBLEScan = BLEDevice::getScan(); //create new scan pBLEScan->setAdvertisedDeviceCallbacks(new MyAdvertisedDeviceCallbacks()); pBLEScan->setActiveScan(activeScan); pBLEScan->setInterval(bleScanInterval); pBLEScan->setWindow(bleScanWindow); xTaskCreatePinnedToCore( scanForDevices, "BLE Scan", 4096, pBLEScan, 1, &BLEScan, 1); } void loop() { TIMERG0.wdt_wprotect=TIMG_WDT_WKEY_VALUE; TIMERG0.wdt_feed=1; TIMERG0.wdt_wprotect=0; ArduinoOTA.handle(); }