/* ESP32 Bluetooth Low Energy presence detection, for use with MQTT. Version 0.0.5 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; 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"); mqttClient.setCredentials(mqttUser, mqttPassword); mqttClient.setClientId(hostname); mqttClient.connect(); } void WiFiEvent(WiFiEvent_t event) { Serial.printf("[WiFi-event] event: %x", 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(); break; case SYSTEM_EVENT_STA_DISCONNECTED: digitalWrite(LED_BUILTIN, LED_ON); Serial.println("WiFi lost connection"); mqttClient.disconnect(); xTimerStop(mqttReconnectTimer, 0); // ensure we don't reconnect to MQTT while reconnecting to Wi-Fi xTimerStart(wifiReconnectTimer, 0); break; case SYSTEM_EVENT_WIFI_READY: Serial.println("Wifi Ready"); break; case SYSTEM_EVENT_STA_START: Serial.println("STA Start"); tcpip_adapter_set_hostname(TCPIP_ADAPTER_IF_STA, hostname); break; case SYSTEM_EVENT_STA_STOP: Serial.println("STA Stop"); break; } } void onMqttConnect(bool sessionPresent) { Serial.println("Connected to MQTT."); 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."); if (WiFi.isConnected() && !updateInProgress) { Serial.println("Starting reconnect timer"); xTimerStart(mqttReconnectTimer, 0); } } 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= maxDistance) { Serial.printf("%s exceeded distance threshold %f\n\r", mac_address.c_str(), distance); } else { Serial.println("MQTT disconnected."); connectToMqtt(); } return false; } class MyAdvertisedDeviceCallbacks: public BLEAdvertisedDeviceCallbacks { void onResult(BLEAdvertisedDevice advertisedDevice) { digitalWrite(LED_BUILTIN, LED_ON); // Serial.printf("Advertised Device: %s \n", advertisedDevice.toString().c_str()); vTaskDelay(10 / portTICK_PERIOD_MS); digitalWrite(LED_BUILTIN, !LED_ON); } }; unsigned long last = 0; TaskHandle_t BLEScan; void scanForDevices(void * parameter) { while(1) { if (!updateInProgress && WiFi.isConnected() && (millis() - last > (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"); } 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.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(); }