434 lines
17 KiB
C++
434 lines
17 KiB
C++
#include <NimBLEDevice.h>
|
|
#include <BLEScan.h>
|
|
#include <WiFi.h>
|
|
#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<const uint8_t*>(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
|
|
}
|