#include #include #include #include "Config.h" #include "HermitCrab.h" #include "ConnectWiFi.h" CBLEScan ble; std::string serviceData; char BLE_SSID[32]; char BLE_PW[32]; #define TAG_BLE "BLE_SCAN" #define ADVERTISE_INTERVAL_MS 5000 // Expected advertisement interval #define SCAN_SHORT_MS 300 // Short scan time #define SCAN_LONG_MS 12000 // Longer scan if missed // Xiaomi - The remote service and characteristic UUIDs static BLEUUID serviceUUID("ebe0ccb0-7a0a-4b0c-8a1a-6ff2997da3a6"); static BLEUUID charUUID("ebe0ccc1-7a0a-4b0c-8a1a-6ff2997da3a6"); //#define TUYA_BLE_NAME "THB1-xxxxxx" // sensor name //#define SENSOR_BLE_NAME //#define SENSOR_BLE_MAC "38:1F:8D:77:73:34" // Tuya sensor MAC address #define WIFI_BLE_NAME "HC_" // Name of PC/Android BLE advertiser // UUIDs for environmental sensing and characteristics #define ENV_SENSING_UUID "181A" // Environmental Sensing UUID #define TEMP_UUID "2A6E" // Temperature UUID #define HUMIDITY_UUID "2A6F" // Humidity UUID // Tuya sensor advertisement data structure (in network order) typedef struct __attribute__((packed)) _bthome_t { //uint8_t flag[3]; // Advertise type flags uint8_t info; // = 0x40 BtHomeID_Info (not encrypted) uint8_t p_id; // = BtHomeID_PacketId uint8_t pid; // PacketId (measurement count) uint8_t b_id; // = BtHomeID_battery uint8_t battery_level; // 0..100 % uint8_t t_id; // = BtHomeID_temperature int16_t temp; // x 0.001 degree uint8_t h_id; // = BtHomeID_humidity uint16_t humid; // x 0.01 % uint8_t v_id; // = BtHomeID_voltage uint16_t battery_mv; // x 0.001 V } bthome_t; // Callback when a device is found during scan class HCScanCallbacks : public NimBLEScanCallbacks { /** * @brief Called when a new device is discovered, before the scan result is received (if applicable). * @param [in] advertisedDevice The device which was discovered. */ void onDiscovered(const NimBLEAdvertisedDevice* advertisedDevice) override { static uint8_t flag = 0; static char ssid[32]; static char pw[32]; if (isWiFiConnected()) return; // Retrieve manufacturer data (SSID or password) const std::string& manufacturerData = advertisedDevice->getManufacturerData(); // Check for specific manufacturer data signatures (SIGNATURE1 for SSID, SIGNATURE2 for password) uint16_t manufacturerID = * (uint16_t *) &manufacturerData[0]; if (manufacturerID == SIGNATURE2 - 1) { // For SSID (SIGNATURE1) strncpy(ssid, (char *) &manufacturerData[2], std::min(sizeof(ssid) - 1, manufacturerData.length() - 1)); ssid[sizeof(ssid) - 1] = '\0'; // Ensure null termination flag |= 1; //DPRINTF("BLE Scan - SSID(\"%s\")\n", (char *) &manufacturerData[2]); } else if (manufacturerID == SIGNATURE2) { // For password (SIGNATURE2) strncpy(pw, (char *)&manufacturerData[2], std::min(sizeof(pw) - 1, manufacturerData.length() - 1)); pw[sizeof(pw) - 1] = '\0'; // Ensure null termination flag |= 2; //DPRINTF("BLE Scan - PW(\"%s\")\n", (char *) &manufacturerData[2]); } else { flag = 0; } if (flag == 3) { strncpy(BLE_SSID, ssid, sizeof(BLE_SSID) - 1); BLE_SSID[sizeof(BLE_SSID) - 1] = 0; strncpy(BLE_PW, pw, sizeof(BLE_PW) - 1); BLE_PW[sizeof(BLE_PW) - 1] = 0; flag = 0; } }; /** * @brief Called when a new scan result is complete, including scan response data (if applicable). * @param [in] advertisedDevice The device for which the complete result is available. */ void onResult(const NimBLEAdvertisedDevice* advertisedDevice) override { uint16_t len; bthome_t *pTuyaData = (bthome_t *) advertisedDevice->getServiceData(&len); // Tuya if (len == sizeof(bthome_t) && pTuyaData->info == 0x40) { if (config.nTemp1SensorType == TEMP_SENSOR_TYPE::BLE_TUYA) { // Look for the custom Tuya sensor data structure ble.setData(pTuyaData->temp, pTuyaData->humid, pTuyaData->battery_level); } else if (config.nTemp2SensorType == TEMP_SENSOR_TYPE::BLE_TUYA) { // Look for the custom Tuya sensor data structure ble.setData2(pTuyaData->temp, pTuyaData->humid, pTuyaData->battery_level); } } else // InkBird if (config.nTemp1SensorType == TEMP_SENSOR_TYPE::BLE_INKBIRD || config.nTemp2SensorType == TEMP_SENSOR_TYPE::BLE_INKBIRD) { // Process Inkbird Sensors std::string manufacturerData = advertisedDevice->getManufacturerData(); if (manufacturerData.length() >= 7) { const uint8_t* data = reinterpret_cast(manufacturerData.data()); // Inkbird data format: [unknown][unknown][temp_LSB][temp_MSB][humidity][voltage_LSB][voltage_MSB] int16_t temp = (data[2] | (data[3] << 8)); // Little-endian, scale by 0.01 uint16_t humi = data[4]; // Humidity byte (0-100%) uint16_t batteryLevel = map(data[5] | (data[6] << 8), 2300, 3200, 0, 100); // Convert voltage to battery % if (config.nTemp1SensorType == TEMP_SENSOR_TYPE::BLE_INKBIRD) { ble.setData(temp, humi, batteryLevel); } else if (config.nTemp2SensorType == TEMP_SENSOR_TYPE::BLE_INKBIRD) { ble.setData2(temp, humi, batteryLevel); } } } }; /** * @brief Called when a scan operation ends. * @param [in] scanResults The results of the scan that ended. * @param [in] reason The reason code for why the scan ended. */ void onScanEnd(const NimBLEScanResults& scanResults, int reason) override {}; } hcScanCallbacks; // Connection based Notification from Xiaomi devices static void notifyCallback( BLERemoteCharacteristic* pBLERemoteCharacteristic, uint8_t* pData, size_t length, bool isNotify) { uint16_t voltage; if (config.nTemp1SensorType == TEMP_SENSOR_TYPE::BLE_XIAOMI_MIJIA) { voltage = pData[3] | (pData[4] << 8); // little endian ble.setData(pData[0] | (pData[1] << 8), pData[2] * 100, map(voltage, 2300, 3200, 0, 100)); } else if (config.nTemp2SensorType == TEMP_SENSOR_TYPE::BLE_XIAOMI_MIJIA) { voltage = pData[3] | (pData[4] << 8); // little endian ble.setData2(pData[0] | (pData[1] << 8), pData[2] * 100, map(voltage, 2300, 3200, 0, 100)); } } // Function to setup BLE void CBLEScan::setupConnect(uint64_t addr, uint64_t addr2) { m_nAddr = addr; m_nAddr2 = addr2; m_nLatestBLEAdvertise = m_nLatestBLEAdvertise2 = millis(); m_nLastReceive = m_nLastReceive2 = m_nLatestBLEAdvertise; m_bDataReceived = m_bDataReceived2 = false; NimBLEDevice::init(""); status.nFlags &= ~FLAG_BLE_BATT; status.nFlags &= ~FLAG_BLE_NODATA; status.nFlags &= ~FLAG_BLE_LOST; m_nTemp = -9999; m_nHumid = -9999; m_nBatteryLevel = 0; m_nTemp2 = -9999; m_nHumid2 = -9999; m_nBatteryLevel2 = 0; m_bConnected = false; m_bConnectionInProgress = false; pClient = nullptr; if ( config.nTemp1SensorType == TEMP_SENSOR_TYPE::BLE_XIAOMI_MIJIA) { connect(NimBLEAddress(m_nAddr, BLE_ADDR_PUBLIC), false); } else if ( config.nTemp2SensorType == TEMP_SENSOR_TYPE::BLE_XIAOMI_MIJIA) { connect(NimBLEAddress(m_nAddr2, BLE_ADDR_PUBLIC), false); } } void CBLEScan::setupScan() { NimBLEAddress cTargetAddress; if (config.nBLEScanInterval == 143 || config.nBLEScanInterval == 77 || config.nBLEScanInterval == 139 || config.nBLEScanInterval == 91) m_nInterval = config.nBLEScanInterval; else m_nInterval = 143; serviceData.reserve(256); NimBLEScan* pBLEScan = NimBLEDevice::getScan(); pBLEScan->setScanCallbacks((NimBLEScanCallbacks *) &hcScanCallbacks, true); pBLEScan->setActiveScan(false); pBLEScan->setDuplicateFilter(false); pBLEScan->setMaxResults(0); m_bContinuousScan = true; m_bScanStarted = false; if (config.nTemp1SensorType == TEMP_SENSOR_TYPE::BLE_TUYA || config.nTemp1SensorType == TEMP_SENSOR_TYPE::BLE_INKBIRD) { cTargetAddress = NimBLEAddress(m_nAddr, BLE_ADDR_PUBLIC); pBLEScan->setTargetAddress(cTargetAddress); startScan(); } else if (config.nTemp2SensorType == TEMP_SENSOR_TYPE::BLE_TUYA || config.nTemp2SensorType == TEMP_SENSOR_TYPE::BLE_INKBIRD) { cTargetAddress = NimBLEAddress(m_nAddr2, BLE_ADDR_PUBLIC); pBLEScan->setTargetAddress(cTargetAddress); startScan(); } else if (!isWiFiConnected()) { startScan(); } } void CBLEScan::startScan() { DPRINTF("Starting BLE scan... %d(%d)\n", m_nInterval, 5000 % m_nInterval); NimBLEScan* pBLEScan = NimBLEDevice::getScan(); pBLEScan->setInterval(m_nInterval); // Interval between scan windows in ms pBLEScan->setWindow(m_nInterval - 3); // Length of time the scanner listens in ms if (m_bContinuousScan) { // 85(15), 77(5), 82(2), 61(2) 122(2), 139(4), 91(5), 143(5), 135(-5) pBLEScan->start(0, false, false); // Continuous scan } else { pBLEScan->start(12000, false, true); // Continuous scan } m_bScanStarted = true; } bool CBLEScan::connect(NimBLEAddress pAddress, bool bAsync) { DPRINTLN("BLE Connect: Connecting..."); if (pClient == nullptr) { pClient = NimBLEDevice::createClient(); pClient->setConnectTimeout(30000); } m_bConnectionInProgress = false; bool ret = pClient->connect(pAddress, true, bAsync, true); if (bAsync) { m_bConnectionInProgress = true; m_bConnected = false; } else { m_bConnected = ret; } if (!bAsync && m_bConnected) { NimBLERemoteService* pRemoteService = pClient->getService(serviceUUID); if (!pRemoteService) { DPRINTLN("BLE Connect: Service not found"); return false; } pRemoteCharacteristic = pRemoteService->getCharacteristic(charUUID); if (!pRemoteCharacteristic) { DPRINTLN("BLE Connect: Characteristic not found"); return false; } pRemoteCharacteristic->subscribe(true, notifyCallback); DPRINTF("BLE Connect: Connected to %s\n", pAddress.toString().c_str()); } return m_bConnected; } void CBLEScan::loop(unsigned long clock) { unsigned long gap; if (config.nTemp1SensorType == TEMP_SENSOR_TYPE::BLE_XIAOMI_MIJIA || config.nTemp2SensorType == TEMP_SENSOR_TYPE::BLE_XIAOMI_MIJIA) { if (!m_bConnected) { if (pClient->isConnected()) { m_bConnectionInProgress = false; NimBLERemoteService* pRemoteService = pClient->getService(serviceUUID); if (!pRemoteService) { DPRINTLN("BLE Connect: Service not found"); return; } pRemoteCharacteristic = pRemoteService->getCharacteristic(charUUID); if (!pRemoteCharacteristic) { DPRINTLN("BLE Connect: Characteristic not found"); return; } pRemoteCharacteristic->subscribe(true, notifyCallback); DPRINTLN("BLE Connect: Connected within the Loop"); m_bConnected = true; } else if (!m_bConnectionInProgress) { if ( config.nTemp1SensorType == TEMP_SENSOR_TYPE::BLE_XIAOMI_MIJIA) { connect(NimBLEAddress(m_nAddr, BLE_ADDR_PUBLIC), true); } else if ( config.nTemp2SensorType == TEMP_SENSOR_TYPE::BLE_XIAOMI_MIJIA) { connect(NimBLEAddress(m_nAddr2, BLE_ADDR_PUBLIC), true); } } } } else { if (m_bConnected) { pClient->disconnect(); m_bConnected = false; m_bConnectionInProgress = false; } } if (config.nTemp1SensorType == TEMP_SENSOR_TYPE::BLE_TUYA || config.nTemp1SensorType == TEMP_SENSOR_TYPE::BLE_INKBIRD) { if (m_bScanStarted) { gap = clock - m_nLatestBLEAdvertise; if (gap < 6050) { status.nFlags &= ~FLAG_BLE_NODATA; status.nFlags &= ~FLAG_BLE_LOST; } else if (gap < 8150) { status.nFlags |= FLAG_BLE_NODATA; status.nFlags &= ~FLAG_BLE_LOST; if (config.bBLETest) m_nHumid = 0; } else if (gap > 60100 && gap < 63200) { //DPRINTF("GAP Reached... %d", gap); status.nFlags |= (FLAG_BLE_NODATA | FLAG_BLE_LOST); m_nTemp = -9999; m_nHumid = -9999; m_nBatteryLevel = 0; } } else { startScan(); } } else if (config.nTemp2SensorType == TEMP_SENSOR_TYPE::BLE_TUYA || config.nTemp2SensorType == TEMP_SENSOR_TYPE::BLE_INKBIRD) { if (m_bScanStarted) { gap = clock - m_nLatestBLEAdvertise2; if (gap > 6050 && gap < 8150) { if (config.bBLETest) m_nHumid2 = 0; } else if (gap > 60100 && gap < 63200) { m_nTemp2 = -9999; m_nHumid2 = -9999; m_nBatteryLevel2 = 0; } } else { startScan(); } } #ifdef BLE_DEBUG { static int col = 0; static int hit = 0; static int miss = 0; static int hit_total = 0; static int miss_total = 0; if (m_bDataReceived) { gap = m_nLatestBLEAdvertise - m_nLastReceive; int cnt = (gap + 2500)/5000 - 1; for (int i = 0; i < cnt; i++) { Serial.print(cnt); miss++; if (++col >=60) { hit_total += hit; miss_total += miss; DPRINTF(" Hit: %2d, Miss: %2d Total(Hit: %3d, Miss: %3d, Ratio %.1f%%) Mem: %d\n", hit, miss, hit_total, miss_total, (float)hit_total * 100.0f / (hit_total + miss_total), ESP.getFreeHeap()); col = 0; hit = 0; miss = 0; } } Serial.print('.'); hit++; if (++col >=60) { hit_total += hit; miss_total += miss; DPRINTF(" Hit: %d, Miss: %d Total(Hit: %d, Miss: %d, Ratio %.1f%%) Mem: %d\n", hit, miss, hit_total, miss_total, (float)hit_total * 100.0f / (hit_total + miss_total), ESP.getFreeHeap()); col = 0; hit = 0; miss = 0; } m_bDataReceived = false; status.nFlags &= ~FLAG_BLE_NODATA; } /* // Interval Scan // { // NimBLEScan* pScan = NimBLEDevice::getScan(); // if (g_bBLEDataReceived) { // Serial.printf("BLE(%d) - Temp: %.2f°C Hum: %.2f%% Batt: %d Mem: %d (Stop)\n", // tickMillis - lastReceived, // g_nBLETemp / 100.0f, // g_nBLEHumid / 100.0f, // g_nBLEBatt, // ESP.getFreeHeap() ); // status.nFlags &= ~FLAG_BLE_NODATA; // g_bBLEDataReceived = false; // if (pScan->isScanning()) { // pScan->stop(); // pScan->clearResults(); // } // lastErrorMessage = tickMillis; // lastReceived = lastTickMillis; // } else { // unsigned long gap = tickMillis - g_lastBLEAdvertise; // if (!pScan->isScanning()) { // if (gap > 4910 && gap < 4960) { // pScan->start(200, false, true); // Serial.printf("Starting Scan for 200ms at %d\n", tickMillis - g_lastBLEAdvertise); // } else if (gap > 9500) { // pScan->start(5200, false, true); // Serial.printf("Starting Scan for 5200ms at %d\n", tickMillis - g_lastBLEAdvertise); // } // } // if (gap > 10050 && tickMillis - lastErrorMessage > 10050) { // Serial.printf("No BLE Data for % seconds\n", tickMillis - lastErrorMessage); // status.nFlags |= FLAG_BLE_NODATA; // lastErrorMessage = tickMillis; // } // } // } */ } #endif }