HCesp/BLEScan.cpp
2026-04-17 11:34:49 +09:00

443 lines
17 KiB
C++

// ====================================================================
//
// Tuya 1 MAC: 38:1F:8D:77:73:34
// Tuya 1 MAC: 38:1F:8D:CB:ED:78
// Xiami MAC: A4:C1:38:E7:58:CF
//
// ====================================================================
#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((uint16_t *) &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
}