diff --git a/tasmota/my_user_config.h b/tasmota/my_user_config.h index 8f3fa5a89..8cf0316f8 100644 --- a/tasmota/my_user_config.h +++ b/tasmota/my_user_config.h @@ -861,6 +861,9 @@ //#define USE_SPI // Add support for hardware SPI #define USE_MI_ESP32 // Add support for ESP32 as a BLE-bridge (+9k2 mem, +292k flash) +//#define USE_BLE_ESP32 // Add support for ESP32 as a BLE-bridge (+9k2? mem, +292k? flash) +//#define USE_IBEACON // Add support for bluetooth LE passive scan of ibeacon devices (uses HM17 module) +//#define USE_IBEACON_ESP32 //#define USE_WEBCAM // Add support for webcam #endif // ESP32 diff --git a/tasmota/support_command.ino b/tasmota/support_command.ino index 1c5e32474..32180783d 100644 --- a/tasmota/support_command.ino +++ b/tasmota/support_command.ino @@ -690,6 +690,12 @@ void CmndSleep(void) } +#ifdef USE_BLE_ESP32 + // declare the fn + int ExtStopBLE(); +#endif + + void CmndUpgrade(void) { // Check if the payload is numerically 1, and had no trailing chars. @@ -700,6 +706,11 @@ void CmndUpgrade(void) TasmotaGlobal.ota_state_flag = 3; char stemp1[TOPSZ]; Response_P(PSTR("{\"%s\":\"" D_JSON_VERSION " %s " D_JSON_FROM " %s\"}"), XdrvMailbox.command, TasmotaGlobal.version, GetOtaUrl(stemp1, sizeof(stemp1))); + +#ifdef USE_BLE_ESP32 + ExtStopBLE(); +#endif + } else { Response_P(PSTR("{\"%s\":\"" D_JSON_ONE_OR_GT "\"}"), XdrvMailbox.command, TasmotaGlobal.version); } diff --git a/tasmota/xdrv_01_webserver.ino b/tasmota/xdrv_01_webserver.ino index d68cf9cd0..f148a5a6e 100644 --- a/tasmota/xdrv_01_webserver.ino +++ b/tasmota/xdrv_01_webserver.ino @@ -2374,6 +2374,11 @@ void UploadServices(uint32_t start_service) { } } +#ifdef USE_BLE_ESP32 + // declare the fn + int ExtStopBLE(); +#endif + void HandleUploadLoop(void) { // Based on ESP8266HTTPUpdateServer.cpp uses ESP8266WebServer Parsing.cpp and Cores Updater.cpp (Update) static uint32_t upload_size; @@ -2409,6 +2414,11 @@ void HandleUploadLoop(void) { } SettingsSave(1); // Free flash for upload +#ifdef USE_BLE_ESP32 + ExtStopBLE(); +#endif + + AddLog_P(LOG_LEVEL_INFO, PSTR(D_LOG_UPLOAD D_FILE " %s"), upload.filename.c_str()); if (UPL_SETTINGS == Web.upload_file_type) { @@ -2632,7 +2642,9 @@ void HandleUploadLoop(void) { Web.upload_error = 7; // Upload aborted if (UPL_TASMOTA == Web.upload_file_type) { Update.end(); } } - delay(0); + // do actually wait a little to allow ESP32 tasks to tick + // fixes task timeout in ESP32Solo1 style unicore code. + delay(10); OsWatchLoop(); // Scheduler(); // Feed OsWatch timer to prevent restart on long uploads } diff --git a/tasmota/xdrv_52_BLE_ESP32.ino b/tasmota/xdrv_52_BLE_ESP32.ino new file mode 100644 index 000000000..90739757d --- /dev/null +++ b/tasmota/xdrv_52_BLE_ESP32.ino @@ -0,0 +1,3613 @@ +/* + xdrv_52_BLE_ESP32.ino - BLE via ESP32 support for Tasmota + + Copyright (C) 2020 Christian Baars and Theo Arends and Simon Hailes + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + + + -------------------------------------------------------------------------------------------- + Version yyyymmdd Action Description + -------------------------------------------------------------------------------------------- +*/ + +/* + xdrv_52: + This driver uses the ESP32 BLE functionality to hopefully provide enough + BLE functionality to implement specific drivers on top of it. + + As a generic driver, it can: + Be asked to + connect/write to a MAC/Service/Characteristic + connect/read from a MAC/Service/Characteristic + connect/write/awaitnotify from a MAC/Service/Characteristic/NotifyCharacteristic + connect/read/awaitnotify from a MAC/Service/Characteristic/NotifyCharacteristic + + Cmnds: + BLEOp0 - requests status of operations + BLEOp1 MAC - create an operation in preparation, and populate it's MAC address + BLEOp2 Service - add a serviceUUID to the operation in preparation + BLEOp3 Characteristic - add a CharacteristicUUID to the operation in preparation for read/write + BLEOp4 writedata - optional:add data to write to the operation in preparation - hex string + BLEOp5 - optional:signify that a read should be done + BLEOp6 NotifyCharacteristic - optional:add a NotifyCharacteristicUUID to the operation in preparation to wait for a notify + BLEOp9 - publish the 'operation in preparation' to MQTT. + BLEOp10 - add the 'operation in preparation' to the queue of operations to perform. + + Other drivers can add callbacks to receive advertisements + Other drivers can add 'operations' to be performed and receive callbacks from the operation's success or failure + +Example: +Write and request next notify: +backlog BLEOp1 001A22092EE0; BLEOp2 3e135142-654f-9090-134a-a6ff5bb77046; BLEOp3 3fa4585a-ce4a-3bad-db4b-b8df8179ea09; BLEOp4 03; BLEOp6 d0e8434d-cd29-0996-af41-6c90f4e0eb2a; +BLEOp10 -> +19:25:08 RSL: tele/tasmota_E89E98/SENSOR = {"BLEOperation":{"opid":"3,"state":"1,"MAC":"001A22092EE0","svc":"3e135142-654f-9090-134a-a6ff5bb77046","char":"3fa4585a-ce4a-3bad-db4b-b8df8179ea09","wrote":"03}} +19:25:08 queued 0 sent {"BLEOperation":{"opid":"3,"state":"1,"MAC":"001A22092EE0","svc":"3e135142-654f-9090-134a-a6ff5bb77046","char":"3fa4585a-ce4a-3bad-db4b-b8df8179ea09","wrote":"03}} +19:25:08 RSL: stat/tasmota_E89E98/RESULT = {"BLEOp":"Done"} +..... +19:25:11 RSL: tele/tasmota_E89E98/SENSOR = {"BLEOperation":{"opid":"3,"state":"11,"MAC":"001A22092EE0","svc":"3e135142-654f-9090-134a-a6ff5bb77046","char":"3fa4585a-ce4a-3bad-db4b-b8df8179ea09","wrote":"03","notify":"020109000428}} + +state: 1 -> starting, +7 -> read complete +8 -> write complete +11 -> notify complete +-ve + -> failure (see GEN_STATE_FAILED_XXXX constants below.) + + +The driver can also be used by other drivers, using the functions: + +void registerForAdvertismentCallbacks(char *loggingtag, ADVERTISMENT_CALLBACK* pFn); +void registerForOpCallbacks(char *loggingtag, OPCOMPLETE_CALLBACK* pFn); +bool extQueueOperation(generic_sensor_t** op); + +These allow other code to + receive advertisements + receive operation callbacks. + create and start an operation, and get a callback when done/failed. + +i.e. the Bluetooth of the ESP can be shared without conflict. + +*/ + + +// TEMPORARILY define ESP32 and USE_BLE_ESP32 so VSCODE shows highlighting.... +//#define VSCODE_DEV + +#ifdef VSCODE_DEV +#define ESP32 +#define USE_BLE_ESP32 +#endif + +#ifdef ESP32 // ESP32 only. Use define USE_HM10 for ESP8266 support + +#ifdef USE_BLE_ESP32 + +#define BLE_ESP32_ALIASES + +// uncomment for more diagnostic/information messages - + more flash use. +//#define BLE_ESP32_DEBUG + + + +#define XDRV_52 52 +#define USE_MI_DECRYPTION + +#include +#include +#include +#include +#ifdef USE_MI_DECRYPTION +#include +#endif //USE_MI_DECRYPTION + +#include +#include +#include "NimBLEEddystoneURL.h" +#include "NimBLEEddystoneTLM.h" +#include "NimBLEBeacon.h" + +// from ble_gap.c +extern "C" void ble_gap_conn_broken(uint16_t conn_handle, int reason); + +void installExamples(); +void sendExample(); + + +namespace BLE_ESP32 { + + +// generic sensor type used as during +// connect/read/wrtie/notify operations +// only one operation will happen at a time + +#pragma pack( push, 0 ) // aligned structures for speed. but be sepcific + +///////////////////////////////////////////////////// +// states for runTaskDoneOperation +#define GEN_STATE_IDLE 0 +#define GEN_STATE_START 1 +#define GEN_STATE_STARTED 2 + +#define GEN_STATE_READDONE 3 +#define GEN_STATE_WRITEDONE 4 +#define GEN_STATE_WAITNOTIFY 5 +#define GEN_STATE_WAITINDICATE 6 + +#define GEN_STATE_NOTIFIED 7 + + +// Errors are all base on 0x100 +#define GEN_STATE_FAILED -1 +#define GEN_STATE_FAILED_CANTNOTIFYORINDICATE -2 +#define GEN_STATE_FAILED_CANTREAD -3 +#define GEN_STATE_FAILED_CANTWRITE -4 +#define GEN_STATE_FAILED_NOSERVICE -5 +#define GEN_STATE_FAILED_NO_RW_CHAR -6 +#define GEN_STATE_FAILED_NONOTIFYCHAR -7 +#define GEN_STATE_FAILED_NOTIFYTIMEOUT -8 +#define GEN_STATE_FAILED_READ -9 +#define GEN_STATE_FAILED_WRITE -10 +#define GEN_STATE_FAILED_CONNECT -11 +#define GEN_STATE_FAILED_NOTIFY -12 +#define GEN_STATE_FAILED_INDICATE -13 +#define GEN_STATE_FAILED_NODEVICE -14 +#define GEN_STATE_FAILED_NOREADWRITE -15 +#define GEN_STATE_FAILED_CANCEL -16 +// +///////////////////////////////////////////////////// + +#define BLE_ESP32_MAXNAMELEN 32 +#define BLE_ESP32_MAXALIASLEN 20 + + +#define MAX_BLE_DATA_LEN 100 +struct generic_sensor_t { + int16_t state; + uint32_t opid; // incrementing id so we can find them + uint64_t notifytimer; + + // uint8_t cancel; + // uint8_t requestType; + NimBLEAddress addr; + NimBLEUUID serviceUUID; + NimBLEUUID characteristicUUID; + NimBLEUUID notificationCharacteristicUUID; + uint8_t dataToWrite[MAX_BLE_DATA_LEN]; + uint8_t writelen; + uint8_t dataRead[MAX_BLE_DATA_LEN]; + uint8_t readlen; + uint8_t readtruncated; + uint8_t dataNotify[MAX_BLE_DATA_LEN]; + uint8_t notifylen; + uint8_t notifytruncated; + + // NOTE!!!: this callback is called DIRECTLY from the operation task, so be careful about cross-thread access of data + // if is called after read, so that you can do a read/modify/write operation on a characteristic. + // i.e. modify dataToWrite and writelen according to what you see in readData and readlen. + // for a normal read, please use the OPCOMPLETE_CALLBACK 'completecallback' + // normally null + void *readmodifywritecallback; // READ_CALLBACK function, used by external drivers + + void *completecallback; // OPCOMPLETE_CALLBACK function, used by external drivers + void *context; // opaque context, used by external drivers, or can be set to a long for MQTT +}; + + +//////////////////////////////////////////////////////////////// +// structure for callbacks from other drivers from advertisements. +struct ble_advertisment_t { + BLEAdvertisedDevice *advertisedDevice; // the full NimBLE advertisment, in case people need MORE info. + uint32_t totalCount; + + uint8_t addr[6]; + uint8_t addrtype; + int8_t RSSI; + char name[BLE_ESP32_MAXNAMELEN+1]; +}; + +struct ble_alias_t { + uint8_t addr[6]; + char name[BLE_ESP32_MAXALIASLEN+1]; +}; + +/* +This is probabyl what you are looking for: +ble_gap_addr_t gap_addr; +gap_addr.addr_type = BLE_GAP_ADDR_TYPE_PUBLIC; //Public address 0x00 +gap_addr.addr_type = BLE_GAP_ADDR_TYPE_RANDOM_STATIC; //Random static address 0x01 +gap_addr.addr_type = BLE_GAP_ADDR_TYPE_RANDOM_PRIVATE_RESOLVABLE; //Random private resolvable address 0x02 +gap_addr.addr_type = BLE_GAP_ADDR_TYPE_RANDOM_PRIVATE_NON_RESOLVABLE; //Random private non-resolvable address 0x03 +*/ + +#pragma pack( pop ) // byte-aligned structures to read the sensor data + +//////////////////////////////////////////////////////////////// + +/////////////////////////////////////////////////////////////////////// +// External interface to this driver for use by others. +// +// callback types to be used by external drivers +// +// returns - +// 0 = let others see this, +// 1 = I processed this, no need to give it to the next callback +// 2 = I want this device erased from the scan +typedef int ADVERTISMENT_CALLBACK(BLE_ESP32::ble_advertisment_t *pStruct); +// returns - 0 = let others see this, 1 = I processed this, no need to give it to the next callback, or post on MQTT +typedef int OPCOMPLETE_CALLBACK(BLE_ESP32::generic_sensor_t *pStruct); + +// NOTE!!!: this callback is called DIRECTLY from the operation task, so be careful about cross-thread access of data +// if is called after read, so that you can do a read/modify/write operation on a characteristic. +typedef int READ_CALLBACK(BLE_ESP32::generic_sensor_t *pStruct); + +typedef int SCANCOMPLETE_CALLBACK(NimBLEScanResults results); + +// tag is just a name for logging +void registerForAdvertismentCallbacks(const char *tag, BLE_ESP32::ADVERTISMENT_CALLBACK* pFn); +void registerForOpCallbacks(const char *tag, BLE_ESP32::OPCOMPLETE_CALLBACK* pFn); +void registerForScanCallbacks(const char *tag, BLE_ESP32::SCANCOMPLETE_CALLBACK* pFn); + +//////////////////////////////////////////////////// +// BLE operations: these are currently 'new'ed and 'delete'ed. +// in the future, they may be allocated from some constant menory store to avoid fragmentation. +// so PLEASE don't create or destroy them yourselves. +// create a new BLE operation. +int newOperation(BLE_ESP32::generic_sensor_t** op); +// free a BLE operation - this should be done if you did not call extQueueOperation for some reason +int freeOperation(BLE_ESP32::generic_sensor_t** op); +// queue a BLE operation - it will happen some time in the future. +// Note: you do not need to free an operation once it have been queued. it will be freed by the driver. +int extQueueOperation(BLE_ESP32::generic_sensor_t** op); +const char * getStateString(int state); +/////////////////////////////////////////////////////////////////////// + +#define USE_NATIVE_LOGGING + + +// a temporay safe logging mechanism. This has a max of 40 chars, and a max of 15 slots per 50ms +//int SafeAddLog_P(uint32_t loglevel, PGM_P formatP, ...); + +static void BLEDiag(); +const char *getAlias(uint8_t *addr); +//void BLEAliasMqttList(); +void BLEAliasListResp(); +//////////////////////////////////////////////////////////////////////// +// utilities +// dump a binary to hex +char * dump(char *dest, int maxchars, const uint8_t *src, int len); + + + + +struct BLE_simple_device_t { + uint8_t mac[6]; + uint8_t addrtype; + char name[BLE_ESP32_MAXNAMELEN+1]; + int8_t RSSI; + uint64_t lastseen; // last seen us + uint16_t maxAge; // maximum observed age of this device +}; + + + +// this protects our queues, which can be accessed by multiple tasks +SemaphoreHandle_t BLEOperationsRecursiveMutex; +SemaphoreHandle_t BLEDevicesMutex; + + +// only run from main thread, because it deletes things that were newed there... +static void mainThreadOpCallbacks(); +static void mainThreadBLETimeouts(); + +int addOperation(std::deque *ops, BLE_ESP32::generic_sensor_t** op); +BLE_ESP32::generic_sensor_t* nextOperation(std::deque *ops); +std::string BLETriggerResponse(BLE_ESP32::generic_sensor_t *toSend); +static void BLEscanEndedCB(NimBLEScanResults results); +static void BLEGenNotifyCB(NimBLERemoteCharacteristic* pRemoteCharacteristic, uint8_t* pData, size_t length, bool isNotify); + +// this is called from the advert callback, be careful +void BLEPostAdvert(ble_advertisment_t *Advertisment); +static void BLEPostMQTTSeenDevices(int type); + +static void BLEShow(bool json); +static void BLEPostMQTT(bool json); +static void BLEStartOperationTask(); + +// these are only run from the run task +static void BLETaskRunCurrentOperation(BLE_ESP32::generic_sensor_t** pCurrentOperation, NimBLEClient **ppClient); +static void BLETaskRunTaskDoneOperation(BLE_ESP32::generic_sensor_t** op, NimBLEClient **ppClient); +int BLETaskStartScan(int time); + + +// these are run from main thread +static int StartBLE(void); +static int StopBLE(void); + +// called from advert callback +void setDetails(ble_advertisment_t *ad); + +#undef EXAMPLE_ADVERTISMENT_CALLBACK +#undef EXAMPLE_OPERATION_CALLBACK + +#ifdef EXAMPLE_ADVERTISMENT_CALLBACK +int myAdvertCallback(BLE_ESP32::ble_advertisment_t *pStruct); +#endif +#ifdef EXAMPLE_OPERATION_CALLBACK +int myOpCallback(BLE_ESP32::generic_sensor_t *pStruct); +int myOpCallback2(BLE_ESP32::generic_sensor_t *pStruct); +#endif + + +// single storage for advert callbacks.... +static ble_advertisment_t BLEAdvertisment; + + +////////////////////////////////////////////////// +// general variables for running the driver +TaskHandle_t TasmotaMainTask; + + +static int BLEMasterEnable = 0; +static int BLEInitState = 0; +static int BLERunningScan = 0; +static uint32_t BLEScanCount = 0; +static uint8_t BLEScanActiveMode = 0; +static uint32_t BLELoopCount = 0; +static uint32_t BLEOpCount = 0; + +static int BLEPublishDevices = 0; // causes MQTT publish of device list (each scan end) +static BLEScan* ble32Scan = nullptr; +bool BLERunning = false; +// time we last started a scan in uS using esp_timer_get_time(); +// used to setect a scan which did not call back? +uint64_t BLEScanStartedAt = 0; +uint64_t BLEScanToEndBefore = 0; +uint8_t BLEStopScan = 0; +uint8_t BLEOtaStallBLE = 0; +uint8_t BLEDebugMode = 0; +int BLEMaxTaskLoopTime = 120; // we expect the task to NOT take > 120s per loop!!! +uint64_t BLELastLoopTime = 0; +int BLEScanTimeS = 20; // scan duraiton in S +int BLEMaxTimeBetweenAdverts = 120; // we expect an advert at least this frequently, else restart BLE (in S) +uint64_t BLEScanLastAdvertismentAt = 0; +uint32_t lastopid = 0; // incrementing uinique opid +uint32_t BLEResets = 0; +// controls request of details about one device +uint8_t BLEDetailsRequest = 0; +uint8_t BLEDetailsMac[6]; +uint8_t BLEAliasListTrigger = 0; +// triggers send for ALL operations known about +uint8_t BLEPostMQTTTrigger = 0; +int BLEMaxAge = 60*10; // 10 minutes +int BLEAddressFilter = 3; + + +////////////////////////////////////////////////// + + +// operation being prepared through commands +BLE_ESP32::generic_sensor_t* prepOperation = nullptr; + +// operations which have been queued +std::deque queuedOperations; +// operations in progress (at the moment, only one) +std::deque currentOperations; +// operations which have completed or failed, ready to send to MQTT +std::deque completedOperations; + +// seen devices +#define MAX_BLE_DEVICES_LOGGED 80 +std::deque seenDevices; +std::deque freeDevices; + + + +// list of registered callbacks for advertisements +// register using void registerForAdvertismentCallbacks(const char *somename ADVERTISMENT_CALLBACK* pFN); +std::deque advertismentCallbacks; + +std::deque operationsCallbacks; + +std::deque scancompleteCallbacks; + + +#ifdef BLE_ESP32_ALIASES +std::deque aliases; +#endif + + +/*********************************************************************************************\ + * constants +\*********************************************************************************************/ + +#define D_CMND_BLE "BLE" + +const char kBLE_Commands[] PROGMEM = D_CMND_BLE "|" + "Period|Adv|Op|Mode|Details|Scan|Alias|Name|Debug|Devices|MaxAge|AddrFilter"; + +static void CmndBLEPeriod(void); +static void CmndBLEAdv(void); +static void CmndBLEOperation(void); +static void CmndBLEMode(void); +static void CmndBLEDetails(void); +static void CmndBLEScan(void); +static void CmndBLEAlias(void); +static void CmndBLEName(void); +static void CmndBLEDebug(void); +static void CmndBLEDevices(void); +static void CmndBLEMaxAge(void); +static void CmndBLEAddrFilter(void); + +void (*const BLE_Commands[])(void) PROGMEM = { + &BLE_ESP32::CmndBLEPeriod, + &BLE_ESP32::CmndBLEAdv, + &BLE_ESP32::CmndBLEOperation, + &BLE_ESP32::CmndBLEMode, + &BLE_ESP32::CmndBLEDetails, + &BLE_ESP32::CmndBLEScan, + &BLE_ESP32::CmndBLEAlias, + &BLE_ESP32::CmndBLEName, + &BLE_ESP32::CmndBLEDebug, + &BLE_ESP32::CmndBLEDevices, + &BLE_ESP32::CmndBLEMaxAge, + &BLE_ESP32::CmndBLEAddrFilter +}; + +const char *successStates[] PROGMEM = { + PSTR("IDLE"), // 0 + PSTR("START"), + PSTR("STARTED"), + PSTR("DONEREAD"), + PSTR("DONEWRITE"), + PSTR("WAITNOTIFY"), + PSTR("WAITINDICATE"), + PSTR("DONENOTIFIED") // 7 +}; + +const char *failStates[] PROGMEM = { + PSTR("IDLE"), //0 + PSTR("FAILED"), //-1 + PSTR("FAILCANTNOTIFYORINDICATE"), + PSTR("FAILCANTREAD"), + PSTR("FAILCANTWRITE"), + PSTR("FAILNOSERVICE"), + PSTR("FAILNORWCHAR"), //-6 + PSTR("FAILNONOTIFYCHAR"), + PSTR("FAILNOTIFYTIMEOUT"), + PSTR("FAILEREAD"), + PSTR("FAILWRITE"), + PSTR("FAILCONNECT"), + PSTR("FAILNOTIFY"), + PSTR("FAILINDICATE"), + PSTR("FAILNODEVICE"), + PSTR("FAILNOREADWRITE"), + PSTR("FAILCANCEL")// -16 +}; + +const char * getStateString(int state){ + if ((state >= 0) && (state < sizeof(successStates)/sizeof(*successStates))){ + return successStates[state]; + } + + state = -state; + if ((state >= 0) && (state < sizeof(failStates)/sizeof(*failStates))){ + return failStates[state]; + } + + return PSTR("STATEINVALID"); +} + +/*********************************************************************************************\ + * enumerations +\*********************************************************************************************/ + +enum BLE_Commands { // commands useable in console or rules + CMND_BLE_PERIOD, // set period like TELE-period in seconds between read-cycles + CMND_BLE_ADV, // change advertisment options at runtime + CMND_BLE_OP, // connect/read/write/notify operations + CMND_BLE_MODE, // change mode of ble - BLE_MODES + CMND_BLE_DETAILS, // get details for one device's adverts + CMND_BLE_SCAN // Scan control + }; + +enum { + BLEModeDisabled = 0, // BLE is disabled + BLEModeScanByCommand = 1, // BLE is activeated by commands only + BLEModeRegularScan = 2, // BLE is scanning all the time +} BLE_SCAN_MODES; + +// values of BLEAdvertMode +enum { + BLE_NO_ADV_SEND = 0, // driver is silent on MQTT regarding adverts + BLE_ADV_TELE = 1, // driver sends a summary at tele period + //BLE_ADV_ALL = 2, // driver sends every advert with full data to MQTT +} BLEADVERTMODE; + + +uint8_t BLEMode = BLEModeRegularScan; +//uint8_t BLEMode = BLEModeScanByCommand; +uint8_t BLETriggerScan = 0; +uint8_t BLEAdvertMode = BLE_ADV_TELE; +uint8_t BLEdeviceLimitReached = 0; + +uint8_t BLEStop = 0; +uint64_t BLEStopAt = 0; + +uint8_t BLERestartTasmota = 0; +uint8_t BLERestartNimBLE = 0; +const char *BLE_RESTART_TEAMOTA_REASON_UNKNOWN = PSTR("unknown"); +const char *BLE_RESTART_TEAMOTA_REASON_RESTARTING_BLE_TIMEOUT = PSTR("restarting BLE took > 5s"); +const char *BLE_RESTART_TEAMOTA_REASON_BLE_LOOP_STALLED = PSTR("BLE loop stalled > 120s"); +const char *BLE_RESTART_TEAMOTA_REASON_BLE_DISCONNECT_FAIL = PSTR("BLE disconnect taking > 60s"); +const char *BLERestartTasmotaReason = BLE_RESTART_TEAMOTA_REASON_UNKNOWN; + +const char *BLE_RESTART_BLE_REASON_UNKNOWN = PSTR("unknown"); +const char *BLE_RESTART_BLE_REASON_ADVERT_BLE_TIMEOUT = PSTR("no adverts in 120s"); +const char *BLE_RESTART_BLE_REASON_CONN_LIMIT = PSTR("connect failed with connection limit reached"); +const char *BLE_RESTART_BLE_REASON_CONN_EXISTS = PSTR("connect failed with connection exists"); +const char *BLERestartBLEReason = nullptr; + + +/*********************************************************************************************\ + * log of all devices present +\*********************************************************************************************/ + +void initSeenDevices(){ + /* added dynamically below, but never removed. + for (int i = 0; i < MAX_BLE_DEVICES_LOGGED; i++){ + BLE_ESP32::BLE_simple_device_t* dev = new BLE_ESP32::BLE_simple_device_t; + freeDevices.push_back(dev); + } + */ + return; +} + +int addSeenDevice(const uint8_t *mac, uint8_t addrtype, const char *name, int8_t RSSI){ + int res = 0; + uint64_t now = esp_timer_get_time(); + TasAutoMutex localmutex(&BLEDevicesMutex, "BLEAdd"); + + int devicefound = 0; + // do we already know this device? + for (int i = 0; i < seenDevices.size(); i++){ + if (!memcmp(seenDevices[i]->mac, mac, 6)){ + seenDevices[i]->lastseen = now; + seenDevices[i]->addrtype = addrtype; + seenDevices[i]->RSSI = RSSI; + if ((!seenDevices[i]->name[0]) && name[0]){ + strncpy(seenDevices[i]->name, name, sizeof(seenDevices[i]->name)); + seenDevices[i]->name[sizeof(seenDevices[i]->name)-1] = 0; + } + devicefound = 1; + break; + } + } + if (!devicefound){ + // if no free slots, add one if we have not reached our limit + if (!freeDevices.size()){ + int total = seenDevices.size(); + if (total < MAX_BLE_DEVICES_LOGGED){ +#ifdef BLE_ESP32_DEBUG + if (BLEDebugMode > 0) AddLog_P(LOG_LEVEL_INFO,PSTR("new seendev slot %d"), total); +#endif + BLE_ESP32::BLE_simple_device_t* dev = new BLE_ESP32::BLE_simple_device_t; + freeDevices.push_back(dev); + } else { + // flag we hit the limit + BLEdeviceLimitReached ++; + if (BLEdeviceLimitReached >= 254){ + BLEdeviceLimitReached = 254; + } + } + } + + // get a new device from the free list + if (freeDevices.size()){ + BLE_ESP32::BLE_simple_device_t* dev = freeDevices[0]; + freeDevices.erase(freeDevices.begin()); + memcpy(dev->mac, mac, 6); + strncpy(dev->name, name, sizeof(dev->name)); + dev->name[sizeof(dev->name)-1] = 0; + dev->lastseen = now; + dev->addrtype = addrtype; + dev->RSSI = RSSI; + dev->maxAge = 1; + seenDevices.push_back(dev); + res = 2; // added + } + } else { + res = 1; // already there + } + return res; +} + +// remove devices from the seen list by age, and add them to the free list +// set ageS to 0 to delete all... +int deleteSeenDevices(int ageS = 0){ + int res = 0; + uint64_t now = esp_timer_get_time(); + now = now/1000L; + now = now/1000L; + uint32_t nowS = (uint32_t)now; + uint32_t mintime = nowS - ageS; + + { + TasAutoMutex localmutex(&BLEDevicesMutex, "BLEDel"); + + for (int i = seenDevices.size()-1; i >= 0; i--){ + BLE_ESP32::BLE_simple_device_t* dev = seenDevices[i]; + uint64_t lastseen = dev->lastseen/1000L; + lastseen = lastseen/1000L; + uint32_t lastseenS = (uint32_t) lastseen; + uint32_t del_at = lastseenS + ageS; + uint32_t devAge = nowS - lastseenS; + if (dev->maxAge < devAge){ + dev->maxAge = devAge; + } + + uint8_t filter = 0; + if (dev->addrtype > BLEAddressFilter){ + filter = 1; + } + + if ((del_at < nowS) || (ageS == 0) || filter){ +#ifdef BLE_ESP32_DEBUG + char addr[20]; + dump(addr, 20, dev->mac, 6); + const char *alias = getAlias(dev->mac); + if (!filter){ + AddLog_P(LOG_LEVEL_INFO,PSTR("delete device %s(%s) by age lastseen %u + maxage %u < now %u."), + addr, alias, lastseenS, ageS, nowS); + } else { + AddLog_P(LOG_LEVEL_INFO,PSTR("delete device %s(%s) by addrtype filter %d > %d."), + addr, alias, dev->addrtype, BLEAddressFilter); + } +#endif + seenDevices.erase(seenDevices.begin()+i); + freeDevices.push_back(dev); + res++; + } + } + } + if (res){ +#ifdef BLE_ESP32_DEBUG + AddLog_P(LOG_LEVEL_INFO,PSTR("BLE deleted %d devices"), res); +#endif + } + return res; +} + +int deleteSeenDevice(uint8_t *mac){ + int res = 0; + TasAutoMutex localmutex(&BLEDevicesMutex, "BLEDel2"); + for (int i = 0; i < seenDevices.size(); i++){ + if (!memcmp(seenDevices[i]->mac, mac, 6)){ + BLE_ESP32::BLE_simple_device_t* dev = seenDevices[i]; + seenDevices.erase(seenDevices.begin()+i); + freeDevices.push_back(dev); + res = 1; + break; + } + } + return res; +} + + +void checkDeviceTimouts(){ + if (BLEMaxAge){ + deleteSeenDevices(BLEMaxAge); + } +} + + +/////////////////////////////////////////////////////// +// returns age of device or 0. if age IS0, returns 1s +uint32_t devicePresent(uint8_t *mac){ + int res = 0; + uint64_t now = esp_timer_get_time(); + now = now/1000L; + now = now/1000L; + uint32_t nowS = (uint32_t)now; + + TasAutoMutex localmutex(&BLEDevicesMutex, "BLEPRes"); + for (int i = 0; i < seenDevices.size(); i++){ + if (!memcmp(seenDevices[i]->mac, mac, 6)){ + uint64_t lastseen = seenDevices[i]->lastseen/1000L; + lastseen = lastseen/1000L; + uint32_t lastseenS = (uint32_t) lastseen; + uint32_t ageS = nowS-lastseenS; + if (!ageS) ageS++; + res = ageS; + break; + } + } + return res; +} + + +// the MAX we could expect. +#define MAX_DEV_JSON_NAME_LEN BLE_ESP32_MAXNAMELEN +#define MAX_DEV_JSON_RSSI_LEN 3 +#define MAX_DEV_JSON_INDEX_LEN 3 +#define MAX_DEV_JSON_ALIAS_LEN BLE_ESP32_MAXALIASLEN +// "001122334455":{"i":123,"n":"01234567890123456789","r":-77}\0 +#define MIN_REQUIRED_DEVJSON_LEN \ + (1+12+1 + 1 + 1 + \ + +4 + MAX_DEV_JSON_INDEX_LEN \ + +1 + 4 + MAX_DEV_JSON_NAME_LEN + 2 \ + +1 + 4 + MAX_DEV_JSON_RSSI_LEN + 2 \ + +1 + 4 + MAX_DEV_JSON_ALIAS_LEN + 2 \ + +1 +1 \ + ) +int getSeenDeviceToJson(int index, BLE_ESP32::BLE_simple_device_t* dev, char **dest, int *maxlen){ + char *p = *dest; + // add 20 to be sure + if (*maxlen < MIN_REQUIRED_DEVJSON_LEN+20){ + return 0; + } + // add mac as key + *((*dest)++) = '"'; + dump((*dest), 20, dev->mac, 6); + (*dest) += 12; + *((*dest)++) = '"'; + *((*dest)++) = ':'; + + // add a structure, so we COULD add more than name later + *((*dest)++) = '{'; + *((*dest)++) = '"'; + *((*dest)++) = 'i'; // index + *((*dest)++) = '"'; + *((*dest)++) = ':'; + sprintf((*dest), "%d", index); + (*dest) += strlen((*dest)); + + if (dev->name[0]){ + *((*dest)++) = ','; + *((*dest)++) = '"'; + *((*dest)++) = 'n'; + *((*dest)++) = '"'; + *((*dest)++) = ':'; + *((*dest)++) = '"'; + *(*dest) = 0; // must term, else it adds to the *end* of old data! + strncat((*dest), dev->name, MAX_DEV_JSON_NAME_LEN); + (*dest) += strlen((*dest)); + *((*dest)++) = '"'; + } + *((*dest)++) = ','; + *((*dest)++) = '"'; + *((*dest)++) = 'r'; + *((*dest)++) = '"'; + *((*dest)++) = ':'; + sprintf((*dest), "%d", dev->RSSI); + (*dest) += strlen((*dest)); + + const char *alias = getAlias(dev->mac); + if (alias && alias[0]){ + *((*dest)++) = ','; + *((*dest)++) = '"'; + *((*dest)++) = 'a'; + *((*dest)++) = '"'; + *((*dest)++) = ':'; + *((*dest)++) = '"'; + sprintf((*dest), "%s", alias); + (*dest) += strlen((*dest)); + *((*dest)++) = '"'; + } + + *((*dest)++) = '}'; + *maxlen -= (*dest - p); + return 1; +} + + +int nextSeenDev = 0; + +int getSeenDevicesToJson(char *dest, int maxlen){ + + if ((nextSeenDev == 0) || (nextSeenDev >= seenDevices.size())){ + nextSeenDev = 0; + } + + // deliberate test of SafeAddLog_P from main thread... + //AddLog_P(LOG_LEVEL_INFO,PSTR("getSeen %d"), seenDevices.size()); + + + int len; + if (!maxlen) return 0; + strcpy((dest), ",\"BLEDevices\":{"); + len = strlen(dest); + dest += len; + maxlen -= len; + + int added = 0; + TasAutoMutex localmutex(&BLEDevicesMutex, "BLEGet"); + + snprintf((dest), maxlen-5, "\"total\":%d", seenDevices.size()); + len = strlen(dest); + dest += len; + maxlen -= len; + added = 1; // trigger ',' + + for (; nextSeenDev < seenDevices.size(); nextSeenDev++){ + if (maxlen > MIN_REQUIRED_DEVJSON_LEN + 3){ + if (added){ + *(dest++) = ','; + maxlen--; + } + int res = getSeenDeviceToJson(nextSeenDev, seenDevices[nextSeenDev], &dest, &maxlen); + if (res) { + added++; + } else { + if (added){ + dest--; // reverse out comma it the string did not get added + maxlen++; + break; + } + } + } else { + break; + } + } + *(dest++) = '}'; + *(dest++) = '}'; + *(dest++) = 0; + int remains = (seenDevices.size() - nextSeenDev); + return remains; +} + + + + +/*********************************************************************************************\ + * Mutex protected logging - max 5 logs of 40 chars +\*********************************************************************************************/ + +/* +#ifdef BLE_ESP32_DEBUG + #define MAX_SAFELOG_LEN 40 + #define MAX_SAFELOG_COUNT 25 +#else + #define MAX_SAFELOG_LEN 20 + #define MAX_SAFELOG_COUNT 5 +#endif + +struct safelogdata { + int level; + char log_data[MAX_SAFELOG_LEN]; +}; + +std::deque freelogs; +std::deque filledlogs; +uint8_t filledlogsOverflows = 0; +SemaphoreHandle_t SafeLogMutex; + + +void initSafeLog(){ + TasmotaMainTask = xTaskGetCurrentTaskHandle(); + SafeLogMutex = xSemaphoreCreateMutex(); + + for (int i = 0; i < MAX_SAFELOG_COUNT; i++){ + BLE_ESP32::safelogdata* logdata = new BLE_ESP32::safelogdata; + freelogs.push_back(logdata); + } +} + +int SafeAddLog_P(uint32_t loglevel, PGM_P formatP, ...) { + TaskHandle_t thistask = xTaskGetCurrentTaskHandle(); + int added = 0; + + // if the log would not be output do nothing here. + if ((loglevel > Settings.weblog_level) && + (loglevel > TasmotaGlobal.seriallog_level) && + (loglevel > Settings.mqttlog_level) && + (loglevel > TasmotaGlobal.syslog_level)){ + return added; + } + + char BLE_temp_log_data[MAX_SAFELOG_LEN]; + // as these are'expensive', let's not bother unless they are lower than the serial log level +#ifndef USE_NATIVE_LOGGING + xSemaphoreTake(SafeLogMutex, portMAX_DELAY); +#endif + int maxlen = sizeof(BLE_temp_log_data)-3; + if (thistask == TasmotaMainTask){ + maxlen -= 13; // room for "-!MAINTHREAD!" + } + // assume this is thread safe - it may not be + va_list arg; + va_start(arg, formatP); + vsnprintf_P(BLE_temp_log_data, maxlen, formatP, arg); + va_end(arg); +#ifdef USE_NATIVE_LOGGING + AddLog_P(loglevel, PSTR("%s"), BLE_temp_log_data); + return 1; +#else + if (thistask == TasmotaMainTask){ + loglevel = LOG_LEVEL_ERROR; + snprintf(BLE_temp_log_data + strlen(BLE_temp_log_data), 13, "-!MAINTHREAD!"); + xSemaphoreGive(SafeLogMutex); // release mutex + AddLog_P(loglevel, PSTR("%s"), BLE_temp_log_data); + return 0; + } + + if (freelogs.size()){ + BLE_ESP32::safelogdata* logdata = (freelogs)[0]; + freelogs.pop_front(); + logdata->level = loglevel; + memcpy(logdata->log_data, BLE_temp_log_data, sizeof(logdata->log_data)); + filledlogs.push_back(logdata); + added = 1; + } else { + // can't log it? + filledlogsOverflows++; + } + xSemaphoreGive(SafeLogMutex); // release mutex + return added; +#endif +} + +BLE_ESP32::safelogdata* GetSafeLog() { + xSemaphoreTake(SafeLogMutex, portMAX_DELAY); + if (filledlogs.size()){ + BLE_ESP32::safelogdata* logdata = (filledlogs)[0]; + filledlogs.pop_front(); + xSemaphoreGive(SafeLogMutex); // release mutex + return logdata; + } + xSemaphoreGive(SafeLogMutex); // release mutex + return nullptr; +} + +void ReleaseSafeLog(BLE_ESP32::safelogdata* logdata){ + xSemaphoreTake(SafeLogMutex, portMAX_DELAY); + freelogs.push_back(logdata); + xSemaphoreGive(SafeLogMutex); // release mutex +} +*/ + +/*********************************************************************************************\ + * Helper functions +\*********************************************************************************************/ + +/** + * @brief Simple pair of functions to dump to a hex string. + * + */ +static const char h[] PROGMEM = "0123456789ABCDEF"; +void hex(char *dest, uint8_t v){ + *(dest++) = h[(v>>4)&0xf]; + *(dest++) = h[v&0xf]; + *(dest) = 0; +} + +// convert from binary to hex. +// add a '+' on the end if not enough room. +char * dump(char *dest, int maxchars, const uint8_t *src, int len){ + int lenmax = (maxchars-1)/2; + int actuallen = 0; + for (actuallen = 0; actuallen < lenmax && actuallen < len; actuallen++){ + if (actuallen < lenmax){ + hex(dest+actuallen*2, src[actuallen]); + } + } + if (actuallen != len){ + *(dest+(actuallen*2)) = '+'; + *(dest+(actuallen*2)+1) = 0; + } + return dest; +} + +// convert from a hex string to binary +int fromHex(uint8_t *dest, const char *src, int maxlen){ + int srclen = strlen(src)/2; + if (srclen > maxlen){ + return 0; + } + + for (int i = 0; i < srclen; i++){ + char t[3]; + if (!isalnum(src[i*2])){ + return 0; + } + if (!isalnum(src[i*2 + 1])){ + return 0; + } + + t[0] = src[i*2]; + t[1] = src[i*2 + 1]; + t[2] = 0; + + int byte = strtol(t, NULL, 16); + *dest++ = byte; + } + return srclen; +} + + +/** + * @brief Reverse an array of 6 bytes + * + * @param _mac a byte array of size 6 (typicalliy representing a MAC address) + */ +void ReverseMAC(uint8_t _mac[]){ + uint8_t _reversedMAC[6]; + for (uint8_t i=0; i<6; i++){ + _reversedMAC[5-i] = _mac[i]; + } + memcpy(_mac,_reversedMAC, sizeof(_reversedMAC)); +} + + + + +/*********************************************************************************************\ + * Advertisment details +\*********************************************************************************************/ + +//ble_advertisment_t BLEAdvertismentDetails; +#define MAX_ADVERT_DETAILS 200 +char BLEAdvertismentDetailsJson[MAX_ADVERT_DETAILS]; +uint8_t BLEAdvertismentDetailsJsonSet = 0; +uint8_t BLEAdvertismentDetailsJsonLost = 0; + + +void setDetails(ble_advertisment_t *ad){ + TasAutoMutex localmutex(&BLEOperationsRecursiveMutex, "BLESetDet"); + if (BLEAdvertismentDetailsJsonSet){ + BLEAdvertismentDetailsJsonLost = 1; + return; + } + char *p = BLEAdvertismentDetailsJson; + int maxlen = sizeof(BLEAdvertismentDetailsJson); + // just in case someone tries to read whilst we are writing + BLEAdvertismentDetailsJson[sizeof(BLEAdvertismentDetailsJson)-1] = 0; + + *(p++) = '{'; + maxlen--; + strcpy(p, "\"details\":{"); + int len = strlen(p); + p += len; + maxlen -= len; + + strcpy(p, "\"mac\":\""); + len = strlen(p); + p += len; + maxlen -= len; + dump(p, 14, ad->addr, 6); + len = strlen(p); + p += len; + maxlen -= len; + *(p++) = '\"'; maxlen--; + + if (BLEAdvertismentDetailsJsonLost){ + BLEAdvertismentDetailsJsonLost = 0; + strcpy(p, ",\"lost\":true"); + len = strlen(p); + p += len; + maxlen -= len; + } + + BLEAdvertisedDevice *advertisedDevice = ad->advertisedDevice; + + uint8_t* payload = advertisedDevice->getPayload(); + size_t payloadlen = advertisedDevice->getPayloadLength(); + if (payloadlen && (maxlen > 30)){ // will truncate if not enough space + strcpy(p, ",\"p\":\""); + p += 6; + maxlen -= 6; + dump(p, maxlen-10, payload, payloadlen); + int len = strlen(p); + p += len; + maxlen -= len; + *(p++) = '\"'; maxlen--; + } + + int svcdataCount = advertisedDevice->getServiceDataCount(); + if (svcdataCount){ + for (int i = 0; i < svcdataCount; i++){ + NimBLEUUID UUID = advertisedDevice->getServiceDataUUID(i);//.getNative()->u16.value; + std::string ServiceData = advertisedDevice->getServiceData(i); + + size_t ServiceDataLength = ServiceData.length(); + const uint8_t *serviceData = (const uint8_t *)ServiceData.data(); + + //char svcuuidstr[20]; + std::string strUUID = UUID; + + int svclen = strUUID.length(); + svclen++; // , + svclen += 3; // "": + svclen += ServiceDataLength*2; + svclen += 3; // ""} + + if (maxlen -10 > svclen){ + *(p++) = ','; + *(p++) = '\"'; + strcpy(p, strUUID.c_str()); + p += strUUID.length(); + *(p++) = '\"'; + *(p++) = ':'; + *(p++) = '\"'; + dump(p, ServiceDataLength*2+2, (uint8_t*)serviceData, ServiceDataLength); + int len = strlen(p); + p += len; + *(p++) = '\"'; + maxlen -= len; + } + } + } + + *(p++) = '}'; maxlen--; + *(p++) = '}'; maxlen--; + *(p++) = 0; maxlen--; + + BLEAdvertismentDetailsJsonSet = 1; +} + + +// call from main thread only! +// post advertisment detail if available, then clear. +void postAdvertismentDetails(){ +// if (TasmotaGlobal.ota_state_flag) return; + + TasAutoMutex localmutex(&BLEOperationsRecursiveMutex, "BLEPostAdd"); + if (BLEAdvertismentDetailsJsonSet){ + strncpy(TasmotaGlobal.mqtt_data, BLEAdvertismentDetailsJson, sizeof(TasmotaGlobal.mqtt_data)); + TasmotaGlobal.mqtt_data[sizeof(TasmotaGlobal.mqtt_data)-1] = 0; + BLEAdvertismentDetailsJsonSet = 0; + // we got the data, give before MQTT call. + localmutex.give(); + // no retain - this is present devices, not historic + MqttPublishPrefixTopic_P(TELE, PSTR("BLE"), 0); + } else { + } +} + + + +/*********************************************************************************************\ + * Classes +\*********************************************************************************************/ + +// does not really take any action +class BLESensorCallback : public NimBLEClientCallbacks { + void onConnect(NimBLEClient* pClient) { +#ifdef BLE_ESP32_DEBUG + if (BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG,PSTR("onConnect %s"), ((std::string)pClient->getPeerAddress()).c_str()); +#endif + } + void onDisconnect(NimBLEClient* pClient) { +#ifdef BLE_ESP32_DEBUG + if (BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG,PSTR("onDisconnect %s"), ((std::string)pClient->getPeerAddress()).c_str()); +#endif + } + bool onConnParamsUpdateRequest(NimBLEClient* pClient, const ble_gap_upd_params* params) { +#ifdef BLE_ESP32_DEBUG + if (BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG,PSTR("onConnParamsUpdateRequest %s"), ((std::string)pClient->getPeerAddress()).c_str()); +#endif + +// if(params->itvl_min < 24) { /** 1.25ms units */ +// return false; +// } else if(params->itvl_max > 300) { /** 1.25ms units */ +// return false; +// } else if(params->latency > 2) { /** Number of intervals allowed to skip */ +// return false; +// } else if(params->supervision_timeout > 6000) { /** 10ms units */ +// return false; +// } + +/* + if(params->itvl_min < 24) { // 1.25ms units + return false; + } else if(params->itvl_max > 40) { // 1.25ms units + return false; + } else if(params->latency > 2) { // Number of intervals allowed to skip + return false; + } else if(params->supervision_timeout > 200) { /// 10ms units + return false; + } + + return true; +*/ + // just always reject thiers, and use ours. + return false; + + } +}; + +static BLESensorCallback clientCB; + + +class BLEAdvCallbacks: public NimBLEAdvertisedDeviceCallbacks { + void onResult(NimBLEAdvertisedDevice* advertisedDevice) { + TasAutoMutex localmutex(&BLEOperationsRecursiveMutex, "BLEAddCB"); + uint64_t now = esp_timer_get_time(); + BLEScanLastAdvertismentAt = now; // note the time of the last advertisment + + uint32_t totalCount = BLEAdvertisment.totalCount; + memset(&BLEAdvertisment, 0, sizeof(BLEAdvertisment)); + BLEAdvertisment.totalCount = totalCount+1; + + BLEAdvertisment.advertisedDevice = advertisedDevice; + + // keep sign - char seems unsigned + int8_t RSSI = (char)advertisedDevice->getRSSI(); + NimBLEAddress address = advertisedDevice->getAddress(); + + BLEAdvertisment.addrtype = address.getType(); + + memcpy(BLEAdvertisment.addr, address.getNative(), 6); + ReverseMAC(BLEAdvertisment.addr); + + BLEAdvertisment.RSSI = RSSI; + + char addrstr[20]; + dump(addrstr, 20, BLEAdvertisment.addr, 6); + + // this mjust survive the scope of the callbacks + std::string name = ""; + const char *namestr = name.c_str(); + if (advertisedDevice->haveName()){ + name = advertisedDevice->getName(); + namestr = name.c_str(); + strncpy(BLEAdvertisment.name, namestr, sizeof(BLEAdvertisment.name)-1); + BLEAdvertisment.name[sizeof(BLEAdvertisment.name)-1] = 0; + } + + + // log this device safely + if (BLEAdvertisment.addrtype <= BLEAddressFilter){ + addSeenDevice(BLEAdvertisment.addr, BLEAdvertisment.addrtype, BLEAdvertisment.name, BLEAdvertisment.RSSI); + } + + if (BLEDetailsRequest){ + switch (BLEDetailsRequest){ + case 1:{ // one advert for one device + BLEDetailsRequest = 0; // only one requested if 2, it's a request all + if (!memcmp(BLEDetailsMac, BLEAdvertisment.addr, 6)){ + setDetails(&BLEAdvertisment); + } + } break; + case 2:{ // all adverts for one device - may not get them all + if (!memcmp(BLEDetailsMac, BLEAdvertisment.addr, 6)){ + setDetails(&BLEAdvertisment); + } + } break; + case 3:{ // all adverts for ALL DEVICES - may not get them all + // ignore from here on if filtered on addrtype + if (BLEAdvertisment.addrtype > BLEAddressFilter){ + return; + } + setDetails(&BLEAdvertisment); + } break; + } + } + + // ignore from here on if filtered on addrtype + if (BLEAdvertisment.addrtype > BLEAddressFilter){ + return; + } + + // call anyone who asked about advertisements + for (int i = 0; i < advertismentCallbacks.size(); i++) { + try { + ADVERTISMENT_CALLBACK* pFN; + pFN = advertismentCallbacks[i]; + int res = pFN(&BLEAdvertisment); + + // if this callback wants to stop here, then do so. + if (1 == res) break; + + // if this callback wants to kill this device + if (2 == res) { + //BLEScan->erase(address); + } + } catch(const std::exception& e){ +#ifdef BLE_ESP32_DEBUG + AddLog_P(LOG_LEVEL_ERROR,PSTR("exception in advertismentCallbacks")); +#endif + } + } + + } +}; + + +static BLEAdvCallbacks BLEScanCallbacks; +static BLESensorCallback BLESensorCB; + +/*********************************************************************************************\ + * BLE callback functions +\*********************************************************************************************/ + +static void BLEscanEndedCB(NimBLEScanResults results){ + +#ifdef BLE_ESP32_DEBUG + if (BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG,PSTR("Scan ended")); +#endif + for (int i = 0; i < scancompleteCallbacks.size(); i++){ + try { + SCANCOMPLETE_CALLBACK *pFn = scancompleteCallbacks[i]; + int callbackres = pFn(results); +#ifdef BLE_ESP32_DEBUG + if (BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG,PSTR("scancompleteCallbacks %d %d"), i, callbackres); +#endif + } catch(const std::exception& e){ +#ifdef BLE_ESP32_DEBUG + AddLog_P(LOG_LEVEL_ERROR,PSTR("exception in operationsCallbacks")); +#endif + } + } + + BLERunningScan = 2; + BLEScanToEndBefore = 0L; + BLEScanCount++; +} + + +/////////////////////////////////////////////////////////////////////// +// !!!!!!!!!!@@@@@@@@@@@@@@@@ +// NOTE: this can callback BEFORE the write is completed. +// so we should not do any actions against the device if we can help it +// this COULD be the reason for the BLE stack hanging up.... +/////////////////////////////////////////////////////////////////////// +static void BLEGenNotifyCB(NimBLERemoteCharacteristic* pRemoteCharacteristic, uint8_t* pData, size_t length, bool isNotify){ + NimBLEClient *pRClient; + + if (!pRemoteCharacteristic){ +#ifdef BLE_ESP32_DEBUG + AddLog_P(LOG_LEVEL_DEBUG,PSTR("Notify: no remote char!!??")); +#endif + return; + } + + +#ifdef BLE_ESP32_DEBUG + if (BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG,PSTR("Notified length: %u"),length); +#endif + // find the operation this is associated with + NimBLERemoteService *pSvc = pRemoteCharacteristic->getRemoteService(); + + if (!pSvc){ +#ifdef BLE_ESP32_DEBUG + AddLog_P(LOG_LEVEL_ERROR,PSTR("Notify: no remote service found")); +#endif + return; + } + + pRClient = pSvc->getClient(); + if (!pRClient){ +#ifdef BLE_ESP32_DEBUG + AddLog_P(LOG_LEVEL_ERROR,PSTR("Notify: no remote client!!??")); +#endif + return; + } + NimBLEAddress devaddr = pRClient->getPeerAddress(); + + generic_sensor_t *thisop = nullptr; + { + // make sure we are not disturbed + TasAutoMutex localmutex(&BLEOperationsRecursiveMutex, "BLENotif"); + + for (int i = 0; i < currentOperations.size(); i++){ + generic_sensor_t *op = currentOperations[i]; + if (!op){ +#ifdef BLE_ESP32_DEBUG + AddLog_P(LOG_LEVEL_ERROR,PSTR("Notify: null op in currentOperations!!??")); +#endif + } else { + if (devaddr == op->addr){ + thisop = op; + break; + } + } + } + } + + // we'll try without + //pRemoteCharacteristic->unsubscribe(); + + if (!thisop){ +#ifdef BLE_ESP32_DEBUG + AddLog_P(LOG_LEVEL_DEBUG,PSTR("no op for notify")); +#endif + return; + } + + for (int i = 0; i < length && i < sizeof(thisop->dataNotify); i++){ + thisop->dataNotify[i] = pData[i]; + } + thisop->notifylen = length; + if (length > sizeof(thisop->dataNotify)){ + thisop->notifytruncated = 1; + } else { + thisop->notifytruncated = 0; + } + // we will NOT change the state here... + // rely on thisop->notifylen as a flag notify is complete + //thisop->state = GEN_STATE_NOTIFIED; + + // this triggers our notify complete, either at the end of read/write, or next 1s cycle. + thisop->notifytimer = 0; + +} + + + + +/*********************************************************************************************\ + * functions for registering callbacks against the driver +\*********************************************************************************************/ + +void registerForAdvertismentCallbacks(const char *tag, BLE_ESP32::ADVERTISMENT_CALLBACK* pFn){ +#ifdef BLE_ESP32_DEBUG + AddLog_P(LOG_LEVEL_INFO,PSTR("BLE: registerForAdvertismentCallbacks %s:%x"), tag, (uint32_t) pFn); +#endif + advertismentCallbacks.push_back(pFn); +} + +void registerForOpCallbacks(const char *tag, BLE_ESP32::OPCOMPLETE_CALLBACK* pFn){ +#ifdef BLE_ESP32_DEBUG + AddLog_P(LOG_LEVEL_INFO,PSTR("BLE: registerForOpCallbacks %s:%x"), tag, (uint32_t) pFn); +#endif + operationsCallbacks.push_back(pFn); +} + +void registerForScanCallbacks(const char *tag, BLE_ESP32::SCANCOMPLETE_CALLBACK* pFn){ +#ifdef BLE_ESP32_DEBUG + AddLog_P(LOG_LEVEL_INFO,PSTR("BLE: registerForScnCallbacks %s:%x"), tag, (uint32_t) pFn); +#endif + scancompleteCallbacks.push_back(pFn); +} + + +/*********************************************************************************************\ + * init NimBLE +\*********************************************************************************************/ +static void BLEPreInit(void) { + BLEInitState = 0; + prepOperation = nullptr; +} + + +static void BLEInit(void) { + if (BLEMode == BLEModeDisabled) return; + + if (BLEInitState) { return; } + + if (TasmotaGlobal.global_state.wifi_down) { return; } + if (WiFi.getSleep() == false) { +#ifdef BLE_ESP32_DEBUG + AddLog_P(LOG_LEVEL_DEBUG,PSTR("BLE: WiFi modem not in sleep mode, BLE cannot start yet")); +#endif + if (0 == Settings.flag3.sleep_normal) { + AddLog_P(LOG_LEVEL_DEBUG,PSTR("BLE: About to restart to put WiFi modem in sleep mode")); + Settings.flag3.sleep_normal = 1; // SetOption60 - Enable normal sleep instead of dynamic sleep + TasmotaGlobal.restart_flag = 2; + } + return; + } + + + // this is only for testing, does nothin if examples are undefed + installExamples(); + //initSafeLog(); + initSeenDevices(); + + uint64_t now = esp_timer_get_time(); + BLEScanLastAdvertismentAt = now; // initialise the time of the last advertisment + BLELastLoopTime = now; + + BLEInitState = 1; + + // dont start of disabled + BLEMasterEnable = Settings.flag5.mi32_enable; + if (!BLEMasterEnable) return; + + + StartBLE(); + + return; +} + +/*********************************************************************************************\ + * Task section +\*********************************************************************************************/ + +static void BLEOperationTask(void *pvParameters); + +static void BLEStartOperationTask(){ + if (BLERunning == false){ +#ifdef BLE_ESP32_DEBUG + AddLog_P(LOG_LEVEL_DEBUG,PSTR("%s: Start operations"),D_CMND_BLE); +#endif + BLERunning = true; + + xTaskCreatePinnedToCore( + BLE_ESP32::BLEOperationTask, /* Function to implement the task */ + "BLEOperationTask", /* Name of the task */ + 4096, /* Stack size in bytes */ + NULL, /* Task input parameter */ + 0, /* Priority of the task */ + NULL, /* Task handle. */ +#ifdef CONFIG_FREERTOS_UNICORE + 0); /* Core where the task should run */ +#else + 1); /* Core where the task should run */ +#endif + } +} + + +static void BLETaskStopStartNimBLE(NimBLEClient **ppClient, bool start = true){ + + if (*ppClient){ + AddLog_P(LOG_LEVEL_ERROR,PSTR("BLETask:Stopping NimBLE")); + + (*ppClient)->setClientCallbacks(nullptr, false); + + try { + if ((*ppClient)->isConnected()){ +#ifdef BLE_ESP32_DEBUG + AddLog_P(LOG_LEVEL_INFO,PSTR("disconnecting connected client")); +#endif + (*ppClient)->disconnect(); + } + NimBLEDevice::deleteClient((*ppClient)); + (*ppClient) = nullptr; +#ifdef BLE_ESP32_DEBUG + AddLog_P(LOG_LEVEL_INFO,PSTR("deleted client")); +#endif + } catch(const std::exception& e){ +#ifdef BLE_ESP32_DEBUG + AddLog_P(LOG_LEVEL_ERROR,PSTR("Stopping NimBLE:exception in delete client")); +#endif + } + + if (ble32Scan){ + ble32Scan->setAdvertisedDeviceCallbacks(nullptr,true); + ble32Scan->stop(); + ble32Scan = nullptr; + } + + // wait second + vTaskDelay(100/ portTICK_PERIOD_MS); + NimBLEDevice::deinit(true); + } + BLERunningScan = 0; + + if (start){ + AddLog_P(LOG_LEVEL_INFO,PSTR("BLETask:Starting NimBLE")); + NimBLEDevice::init("BLE_ESP32"); + + *ppClient = NimBLEDevice::createClient(); + (*ppClient)->setClientCallbacks(&clientCB, false); + /** Set initial connection parameters: These settings are 15ms interval, 0 latency, 120ms timout. + * These settings are safe for 3 clients to connect reliably, can go faster if you have less + * connections. Timeout should be a multiple of the interval, minimum is 100ms. + * Min interval: 12 * 1.25ms = 15, Max interval: 12 * 1.25ms = 15, 0 latency, 51 * 10ms = 510ms timeout + */ + (*ppClient)->setConnectionParams(12,12,0,51); + /** Set how long we are willing to wait for the connection to complete (seconds), default is 30. */ + (*ppClient)->setConnectTimeout(15); + } + + uint64_t now = esp_timer_get_time(); + + // don't restart because of these for a while + BLELastLoopTime = now; // initialise the time of the last advertisment + BLEScanLastAdvertismentAt = now; // initialise the time of the last advertisment + +} + +int BLETaskStartScan(int time){ + if (!ble32Scan) return -1; + if (BLEMode == BLEModeDisabled) return -4; + // don't scan whilst OTA in progress + if (BLEOtaStallBLE) return -5; + if (currentOperations.size()) return -3; + + if (BLERunningScan) { + // if we hit 2, wait one more time before starting + if (BLERunningScan == 2){ + // wait 100ms + vTaskDelay(100/ portTICK_PERIOD_MS); + BLERunningScan = 0; + } + return -2; + } + +#ifdef BLE_ESP32_DEBUG + if (BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG,PSTR("BLETask: Startscan")); +#endif + //vTaskDelay(500/ portTICK_PERIOD_MS); + ble32Scan->setActiveScan(BLEScanActiveMode ? 1: 0); + + + // seems we could get the callback within the start call.... + // so set these before starting + BLERunningScan = 1; + BLEScanStartedAt = esp_timer_get_time(); + if (BLETriggerScan){ + time = BLETriggerScan; + BLETriggerScan = 0; + } + ble32Scan->start(time, BLEscanEndedCB, (BLEScanActiveMode == 2)); // 20s scans, restarted when then finish + //vTaskDelay(500/ portTICK_PERIOD_MS); + return 0; +} + +// this runs one operation +// if the passed pointer is empty, it tries to get a next one. +static void BLETaskRunCurrentOperation(BLE_ESP32::generic_sensor_t** pCurrentOperation, NimBLEClient **ppClient){ + if (!pCurrentOperation) return; + + NimBLEClient *pClient = *ppClient; + if (!*pCurrentOperation) { + *pCurrentOperation = nextOperation(&queuedOperations); + if (*pCurrentOperation){ +#ifdef BLE_ESP32_DEBUG + if (BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG,PSTR("BLETask: new currentOperation")); +#endif + BLEOpCount++; + generic_sensor_t* temp = *pCurrentOperation; + //this will null it out, so save and restore. + addOperation(¤tOperations, pCurrentOperation); + *pCurrentOperation = temp; + } + } + if (!*pCurrentOperation) return; + + + + // if awaiting notification + if ((*pCurrentOperation)->notifytimer){ + // if it took too long, then disconnect + uint64_t now = esp_timer_get_time(); + uint64_t diff = now - (*pCurrentOperation)->notifytimer; + diff = diff/1000; + if (diff > 20000){ // 20s +#ifdef BLE_ESP32_DEBUG + AddLog_P(LOG_LEVEL_DEBUG,PSTR("BLETask: notify timeout")); +#endif + (*pCurrentOperation)->state = GEN_STATE_FAILED_NOTIFYTIMEOUT; + (*pCurrentOperation)->notifytimer = 0; + } + // we can't process any further, because op will be at state readdone or writedone + return; + } + + + switch((*pCurrentOperation)->state){ + case GEN_STATE_WAITINDICATE: + case GEN_STATE_WAITNOTIFY: + //(*pCurrentOperation)->notifytimer == 0 at this point, so must be done + (*pCurrentOperation)->state = GEN_STATE_NOTIFIED; + // just stay here until this is removed by the main thread +#ifdef BLE_ESP32_DEBUG + if (BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG,PSTR("BLETask: notify operation complete")); +#endif + BLE_ESP32::BLETaskRunTaskDoneOperation(pCurrentOperation, ppClient); + pClient = *ppClient; + return; + break; + case GEN_STATE_READDONE: + case GEN_STATE_WRITEDONE: + case GEN_STATE_NOTIFIED: // - may have completed DURING our read/write to get here + // just stay here until this is removed by the main thread +#ifdef BLE_ESP32_DEBUG + if (BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG,PSTR("BLETask: operation complete")); +#endif + BLE_ESP32::BLETaskRunTaskDoneOperation(pCurrentOperation, ppClient); + pClient = *ppClient; + return; + break; + + case GEN_STATE_START: + // continue to start the process here. + break; + + default: + break; + } + + + if (!*pCurrentOperation) return; + + if ((*pCurrentOperation)->state <= GEN_STATE_FAILED){ +#ifdef BLE_ESP32_DEBUG + AddLog_P(LOG_LEVEL_ERROR,PSTR("BLETask: op failed %d"), (*pCurrentOperation)->state); +#endif + BLE_ESP32::BLETaskRunTaskDoneOperation(pCurrentOperation, ppClient); + pClient = *ppClient; + return; + } + + if ((*pCurrentOperation)->state != GEN_STATE_START){ + return; + } + + if (pClient->isConnected()){ + // don't do anything if we are still connected +#ifdef BLE_ESP32_DEBUG + AddLog_P(LOG_LEVEL_DEBUG,PSTR("BLETask: still connected")); +#endif + return; + } + + + // if we managed to run operations back to back with long connection timeouts, + // then we may NOT see advertisements. + // so to prevent triggering of the advert timeout restart mechanism, + // set the last advert time each time we start an operation + uint64_t now = esp_timer_get_time(); + BLEScanLastAdvertismentAt = now; // initialise the time of the last advertisment + + + generic_sensor_t* op = *pCurrentOperation; + + int newstate = GEN_STATE_STARTED; + op->state = GEN_STATE_STARTED; + +#ifdef BLE_ESP32_DEBUG + if (BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG,PSTR("BLETask: attempt connect %s"), ((std::string)op->addr).c_str()); +#endif + + if (!op->serviceUUID.bitSize()){ + op->state = GEN_STATE_FAILED_NOSERVICE; + return; + } + + if (pClient->connect(op->addr, true)) { + +#ifdef BLE_ESP32_DEBUG + if (BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG,PSTR("connected %s -> getservice"), ((std::string)op->addr).c_str()); +#endif + NimBLERemoteService *pService = pClient->getService(op->serviceUUID); + int waitNotify = false; + int notifystate = 0; + op->notifytimer = 0L; + + if (pService != nullptr) { +#ifdef BLE_ESP32_DEBUG + if (BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG,PSTR("got service")); +#endif + // pre-set to fail if no operations requested + //newstate = GEN_STATE_FAILED_NOREADWRITE; + + /////////////////////////////////////////////////////////////////////// + // !!!!!!!!!!@@@@@@@@@@@@@@@@ + // NOTE: Notify callback can happen BEFORE the read/write is completed. + // this COULD be the reason for the BLE stack hanging up.... + /////////////////////////////////////////////////////////////////////// + + // if we have been asked to get a notification + if (op->notificationCharacteristicUUID.bitSize()) { + NimBLERemoteCharacteristic *pNCharacteristic = + pService->getCharacteristic(op->notificationCharacteristicUUID); + if (pNCharacteristic != nullptr) { +#ifdef BLE_ESP32_DEBUG + if (BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG,PSTR("got notify characteristic")); +#endif + op->notifylen = 0; + if(pNCharacteristic->canNotify()) { + if(pNCharacteristic->subscribe(true, BLE_ESP32::BLEGenNotifyCB)) { +#ifdef BLE_ESP32_DEBUG + if (BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG,PSTR("subscribe for notify")); +#endif + uint64_t now = esp_timer_get_time(); + op->notifytimer = now; + // this will get changed to read or write, + // but here in case it's notify only (can that happen?) + notifystate = GEN_STATE_WAITNOTIFY; + waitNotify = true; + } else { +#ifdef BLE_ESP32_DEBUG + AddLog_P(LOG_LEVEL_ERROR,PSTR("failed subscribe for notify")); +#endif + newstate = GEN_STATE_FAILED_NOTIFY; + } + } else { + if(pNCharacteristic->canIndicate()) { + if(pNCharacteristic->subscribe(false, BLE_ESP32::BLEGenNotifyCB)) { +#ifdef BLE_ESP32_DEBUG + AddLog_P(LOG_LEVEL_DEBUG,PSTR("subscribe for indicate")); +#endif + notifystate = GEN_STATE_WAITINDICATE; + uint64_t now = esp_timer_get_time(); + op->notifytimer = now; + waitNotify = true; + } else { +#ifdef BLE_ESP32_DEBUG + AddLog_P(LOG_LEVEL_ERROR,PSTR("failed subscribe for indicate")); +#endif + newstate = GEN_STATE_FAILED_INDICATE; + } + } else { + newstate = GEN_STATE_FAILED_CANTNOTIFYORINDICATE; +#ifdef BLE_ESP32_DEBUG + AddLog_P(LOG_LEVEL_ERROR,PSTR("characteristic can't notify")); +#endif + } + } + } else { + newstate = GEN_STATE_FAILED_NONOTIFYCHAR; +#ifdef BLE_ESP32_DEBUG + AddLog_P(LOG_LEVEL_ERROR,PSTR("notify characteristic not found")); +#endif + } + + // force the 'error' of the notify coming in before the read/write for testing + //vTaskDelay(1000/ portTICK_PERIOD_MS); + } // no supplied notify char is ok + + // this will only happen if you ask for a notify char which is not there? + if (!(newstate <= GEN_STATE_FAILED)){ + if (op->characteristicUUID.bitSize()) { + // read or write characteristic - we always need this? + NimBLERemoteCharacteristic *pCharacteristic = nullptr; + + pCharacteristic = pService->getCharacteristic(op->characteristicUUID); + if (pCharacteristic != nullptr) { +#ifdef BLE_ESP32_DEBUG + if (BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG,PSTR("got read/write characteristic")); +#endif + newstate = GEN_STATE_FAILED_NOREADWRITE; // overwritten on failure + + if (op->readlen){ + if(pCharacteristic->canRead()) { + std::string value = pCharacteristic->readValue(); + op->readlen = value.length(); + memcpy(op->dataRead, value.data(), + (op->readlen > sizeof(op->dataRead))? + sizeof(op->dataRead): + op->readlen); + if (op->readlen > sizeof(op->dataRead)){ + op->readtruncated = 1; + } else { + op->readtruncated = 0; + } + if (op->readmodifywritecallback){ + READ_CALLBACK *pFn = (READ_CALLBACK *)op->readmodifywritecallback; +#ifdef BLE_ESP32_DEBUG + if (BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG,PSTR("read characteristic with readmodifywritecallback")); +#endif + pFn(op); + } else { +#ifdef BLE_ESP32_DEBUG + if (BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG,PSTR("read characteristic")); +#endif + } + + // only change it to a 'finished' state if we really are + if (!waitNotify && !op->writelen) newstate = GEN_STATE_READDONE; + + } else { + newstate = GEN_STATE_FAILED_CANTREAD; + } + } + if (op->writelen){ + if(pCharacteristic->canWrite()) { + if (!pCharacteristic->writeValue(op->dataToWrite, op->writelen, true)){ + newstate = GEN_STATE_FAILED_WRITE; +#ifdef BLE_ESP32_DEBUG + AddLog_P(LOG_LEVEL_DEBUG,PSTR("characteristic write fail")); +#endif + } else { + if (!waitNotify) newstate = GEN_STATE_WRITEDONE; +#ifdef BLE_ESP32_DEBUG + if (BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG,PSTR("write characteristic")); +#endif + } + } else { + newstate = GEN_STATE_FAILED_CANTWRITE; + } + } + // print or do whatever you need with the value + + } else { + newstate = GEN_STATE_FAILED_NO_RW_CHAR; +#ifdef BLE_ESP32_DEBUG + AddLog_P(LOG_LEVEL_DEBUG,PSTR("r/w characteristic not found")); +#endif + } + } + } + + + // disconnect if not waiting for notify, + if (!op->notifytimer){ + if (waitNotify){ + vTaskDelay(50/ portTICK_PERIOD_MS); + // must have completed during our read/write operation + newstate = GEN_STATE_NOTIFIED; + } + } else { + newstate = notifystate; + } + } else { + newstate = GEN_STATE_FAILED_NOSERVICE; + // failed to get a service +#ifdef BLE_ESP32_DEBUG + AddLog_P(LOG_LEVEL_DEBUG,PSTR("failed - svc not on device?")); +#endif + } + + } else { // connect itself failed + newstate = GEN_STATE_FAILED_CONNECT; +#ifdef NIMBLE_CLIENT_HAS_GETRESULT + int rc = pClient->getResult(); + + switch (rc){ + case (0x0200+BLE_ERR_CONN_LIMIT ): +#ifdef BLE_ESP32_DEBUG + AddLog_P(LOG_LEVEL_ERROR,PSTR("Hit connection limit? - restarting NimBLE")); +#endif + BLERestartNimBLE = 1; + BLERestartBLEReason = BLE_RESTART_BLE_REASON_CONN_LIMIT; + break; + case (0x0200+BLE_ERR_ACL_CONN_EXISTS): +#ifdef BLE_ESP32_DEBUG + AddLog_P(LOG_LEVEL_ERROR,PSTR("Connection exists? - restarting NimBLE")); +#endif + BLERestartNimBLE = 1; + BLERestartBLEReason = BLE_RESTART_BLE_REASON_CONN_EXISTS; + break; + } +#endif + + // failed to connect +#ifdef BLE_ESP32_DEBUG + AddLog_P(LOG_LEVEL_DEBUG,PSTR("failed to connect to device %d"), rc); +#endif + } + op->state = newstate; +} + + + +// this disconnects from a device if necessary, and then +// moves the operation from 'currentOperations' to 'completedOperations'. + +// for safety's sake, only call from the run task +static void BLETaskRunTaskDoneOperation(BLE_ESP32::generic_sensor_t** op, NimBLEClient **ppClient){ + try { + if ((*ppClient)->isConnected()){ +#ifdef BLE_ESP32_DEBUG + if (BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG,PSTR("runTaskDoneOperation: disconnecting connected client")); +#endif + (*ppClient)->disconnect(); + // wait for 1/2 second after disconnect + int waits = 0; + do { + vTaskDelay(500/ portTICK_PERIOD_MS); + if (waits) { + //(*ppClient)->disconnect(); + // we will stall here forever!!! - as testing +#ifdef BLE_ESP32_DEBUG + AddLog_P(LOG_LEVEL_ERROR,PSTR("BLE wait discon%d"), waits); +#endif + vTaskDelay(500/ portTICK_PERIOD_MS); + } + waits++; + if (waits == 5){ + int conn_id = (*ppClient)->getConnId(); + ble_gap_conn_broken(conn_id, -1); +#ifdef BLE_ESP32_DEBUG + AddLog_P(LOG_LEVEL_ERROR,PSTR("BLE wait discon%d - kill connection"), waits); +#endif + } + if (waits == 60){ + AddLog_P(LOG_LEVEL_ERROR,PSTR(">60s waiting -> BLE Failed, restart Tasmota %d"), waits); + BLEStop = 1; + BLEStopAt = esp_timer_get_time(); + + BLERestartTasmota = 10; + BLERestartTasmotaReason = BLE_RESTART_TEAMOTA_REASON_BLE_DISCONNECT_FAIL; + break; + } + } while ((*ppClient)->isConnected()); + } + } catch(const std::exception& e){ +#ifdef BLE_ESP32_DEBUG + AddLog_P(LOG_LEVEL_ERROR,PSTR("runTaskDoneOperation: exception in disconnect")); +#endif + } + + + { + TasAutoMutex localmutex(&BLEOperationsRecursiveMutex, "BLEDoneOp"); + + // find this operation in currentOperations, and remove it. + for (int i = 0; i < currentOperations.size(); i++){ + if (currentOperations[i]->opid == (*op)->opid){ + currentOperations.erase(currentOperations.begin() + i); + break; + } + } + } + + + // by adding it to this list, this will cause it to be sent to MQTT +#ifdef BLE_ESP32_DEBUG + if (BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG,PSTR("runTaskDoneOperation: add to completedOperations")); +#endif + addOperation(&completedOperations, op); + return; +} + + + + +// this IS as task. +// we MAY be able to run a few of these simultaneously, but this is not yet tested. +// and probably not required. But everything is there to do so.... +static void BLEOperationTask(void *pvParameters){ + + BLELoopCount = 0; + BLEOpCount = 0;; + + uint32_t timer = 0; + // operation which is currently in progress in THIS TASK + generic_sensor_t* currentOperation = nullptr; + + NimBLEClient *pClient = nullptr; + BLE_ESP32::BLETaskStopStartNimBLE(&pClient); + + for(;;){ + BLELastLoopTime = esp_timer_get_time(); + BLELoopCount++; + + BLE_ESP32::BLETaskRunCurrentOperation(¤tOperation, &pClient); + + // start a scan if possible + if ((BLEMode == BLEModeRegularScan) || (BLETriggerScan)){ + BLEScan* lastScan = ble32Scan; + ble32Scan = NimBLEDevice::getScan(); + if (lastScan != ble32Scan){ + //ble32Scan->setInterval(70); + //ble32Scan->setWindow(50); + ble32Scan->setInterval(0x40); + ble32Scan->setWindow(0x20); + ble32Scan->setAdvertisedDeviceCallbacks(&BLEScanCallbacks,true); + } + + BLE_ESP32::BLETaskStartScan(20); + } + + if (BLEStopScan){ + ble32Scan->stop(); + BLEStopScan = 0; + } + + // come around every 1/10s + vTaskDelay(100/ portTICK_PERIOD_MS); + + if (BLEStop == 1){ + break; + } + + if (BLERestartNimBLE){ + BLERestartNimBLE = 0; + BLERestartTasmota = 10; + BLERestartTasmotaReason = BLE_RESTART_TEAMOTA_REASON_RESTARTING_BLE_TIMEOUT; + AddLog_P(LOG_LEVEL_ERROR,PSTR("BLETask: Restart NimBLE - restart Tasmota in 10 if not complt")); + BLE_ESP32::BLETaskStopStartNimBLE(&pClient); + BLERestartTasmotaReason = BLE_RESTART_TEAMOTA_REASON_UNKNOWN; + BLERestartTasmota = 0; + BLEResets ++; + } + } + + BLE_ESP32::BLETaskStopStartNimBLE(&pClient, false); + + // wait 1/10 second + vTaskDelay(100/ portTICK_PERIOD_MS); + +#ifdef BLE_ESP32_DEBUG + AddLog_P(LOG_LEVEL_DEBUG,PSTR("BLEOperationTask: Left task")); +#endif + deleteSeenDevices(); + + BLEStop = 2; + BLERunning = false; + vTaskDelete( NULL ); +} + + + + + +/***********************************************************************\ + * Regular Tasmota called functions + * +\***********************************************************************/ +void BLEEvery50mSecond(){ +/* if (BLEAliasListTrigger){ + BLEAliasListTrigger = 0; + BLEAliasMqttList(); + }*/ + postAdvertismentDetails(); +} + + + +/** + * @brief Main loop of the driver, "high level"-loop + * + */ + +static void BLEEverySecond(bool restart){ + + BLEDiag(); + + checkDeviceTimouts(); + + + if (Settings.flag5.mi32_enable != BLEMasterEnable){ + if (Settings.flag5.mi32_enable){ + if (StartBLE()){ + BLEMasterEnable = Settings.flag5.mi32_enable; + } + } else { + if (StopBLE()){ + BLEMasterEnable = Settings.flag5.mi32_enable; + } + } + AddLog_P(LOG_LEVEL_INFO,PSTR("BLE: MasterEnable->%d"), BLEMasterEnable); + } + + + // check for application callbacks here. + // this may remove complete items. + BLE_ESP32::mainThreadOpCallbacks(); + + // post any MQTT data if we completed anything in the last second + if (completedOperations.size()){ + BLE_ESP32::BLEPostMQTT(true); // send only completed + } + + // request send of ALL oeprations prepped, queued, in progress - + // in separate MQTT messages. + if (BLEPostMQTTTrigger){ + BLEPostMQTTTrigger = 0; + BLE_ESP32::BLEPostMQTT(false); // show all operations, not just completed + } + + if (BLEPublishDevices){ + BLEPostMQTTSeenDevices(BLEPublishDevices); + BLEPublishDevices = 0; + } + + // we have been asked to restart in this many seconds.... + if (BLERestartTasmota){ + BLERestartTasmota--; + // 2 seconds to go, post to BLE topic on MQTT our reason + if (BLERestartTasmota == 2){ + if (!BLERestartTasmotaReason) BLERestartTasmotaReason = BLE_RESTART_TEAMOTA_REASON_UNKNOWN; + snprintf_P(TasmotaGlobal.mqtt_data, sizeof(TasmotaGlobal.mqtt_data), PSTR("{\"reboot\":\"%s\"}"), BLERestartTasmotaReason); + MqttPublishPrefixTopic_P(TELE, PSTR("BLE"), Settings.flag.mqtt_sensor_retain); + AddLog_P(LOG_LEVEL_ERROR,PSTR("BLE Failure! Restarting Tasmota in %d seconds because %s"), BLERestartTasmota, BLERestartTasmotaReason); + } + + if (!BLERestartTasmota){ + AddLog_P(LOG_LEVEL_ERROR,PSTR("BLE Failure! Restarting Tasmota because %s"), BLERestartTasmotaReason); + // just a normal restart + TasmotaGlobal.restart_flag = 1; + } + } + + if (BLERestartBLEReason){ // just use the ptr as the trigger to send MQTT + snprintf_P(TasmotaGlobal.mqtt_data, sizeof(TasmotaGlobal.mqtt_data), PSTR("{\"blerestart\":\"%s\"}"), BLERestartBLEReason); + MqttPublishPrefixTopic_P(TELE, PSTR("BLE"), Settings.flag.mqtt_sensor_retain); + AddLog_P(LOG_LEVEL_ERROR,PSTR("BLE Failure! Restarting BLE Stack because %s"), BLERestartBLEReason); + BLERestartBLEReason = nullptr; + } + + + BLE_ESP32::mainThreadBLETimeouts(); + if (!BLEMasterEnable){ + return; + } + +} + + + + + +/*********************************************************************************************\ + * Operations queue functions - all to do with read/write and notify for a device +\*********************************************************************************************/ + +// this retrievs the next operation from the passed list, and removes it from the list. +// or returns null if none. +generic_sensor_t* nextOperation(std::deque *ops){ + generic_sensor_t* op = nullptr; + if (ops->size()){ + TasAutoMutex localmutex(&BLEOperationsRecursiveMutex, "BLENExtOp"); + op = (*ops)[0]; + ops->pop_front(); + } + return op; +} + +// this adds an operation to the end of passed list. +// it also sets the op pointer passed to null. +int addOperation(std::deque *ops, generic_sensor_t** op){ + int res = 0; + { + TasAutoMutex localmutex(&BLEOperationsRecursiveMutex, "BLEAddOp"); + if (ops->size() < 10){ + ops->push_back(*op); + *op = nullptr; + res = 1; + } + } + if (res){ + //AddLog_P(LOG_LEVEL_DEBUG,PSTR("added operation")); + } else { + AddLog_P(LOG_LEVEL_ERROR,PSTR("BLE op - no room")); + } + return res; +} + + +int newOperation(BLE_ESP32::generic_sensor_t** op){ + if (!op) { + AddLog_P(LOG_LEVEL_ERROR,PSTR("BLE op inv in newOperation")); + return 0; + } + + BLE_ESP32::generic_sensor_t *o = new BLE_ESP32::generic_sensor_t; + + // clear to zeros, but not the NimBLE classes + o->state = 0; + o->opid = 0; // incrementing id so we can find them + o->notifytimer = 0L; + //uint8_t writeRead[MAX_BLE_DATA_LEN]; + o->writelen = 0; + //uint8_t dataRead[MAX_BLE_DATA_LEN]; + o->readlen = 0; + o->readtruncated = 0; + //uint8_t dataNotify[MAX_BLE_DATA_LEN]; + o->notifylen = 0; + o->notifytruncated = 0; + o->readmodifywritecallback = nullptr; // READ_CALLBACK function, used by external drivers + o->completecallback = nullptr; // OPCOMPLETE_CALLBACK function, used by external drivers + o->context = nullptr; // opaque context, used by external drivers, or can be set to a long for MQTT + + (*op) = o; + return 1; +} + +int freeOperation(BLE_ESP32::generic_sensor_t** op){ + if (!op) { + return 0; + } + + delete (*op); + (*op) = nullptr; + return 1; +} + + +int extQueueOperation(BLE_ESP32::generic_sensor_t** op){ + if (!op || !(*op)) { + AddLog_P(LOG_LEVEL_ERROR,PSTR("BLE: op invalid")); + return 0; + } + (*op)->state = GEN_STATE_START; // trigger request later + (*op)->opid = lastopid++; + + int res = addOperation(&queuedOperations, op); + if (!res){ + AddLog_P(LOG_LEVEL_ERROR,PSTR("extQueueOperation: op added id %d failed"), (lastopid-1)); + } + return res; +} + + +/*********************************************************************************************\ + * BLE Name alisaes +\*********************************************************************************************/ +#ifdef BLE_ESP32_ALIASES +int addAlias( uint8_t *addr, char *name){ + if (!addr || !name){ + return 0; + } + + int count = aliases.size(); + // replace name for existing address + for (int i = 0; i < count; i++){ + if (!memcmp(aliases[i]->addr, addr, 6)){ + strncpy(aliases[i]->name, name, sizeof(aliases[i]->name)); + aliases[i]->name[sizeof(aliases[i]->name)-1] = 0; + return 2; + } + } + + // replace addr for existing name + for (int i = 0; i < count; i++){ + if (!strcmp(aliases[i]->name, name)){ + memcpy(aliases[i]->addr, addr, 6); + return 2; + } + } + + BLE_ESP32::ble_alias_t *alias = new BLE_ESP32::ble_alias_t; + memcpy(alias->addr, addr, 6); + strncpy(alias->name, name, sizeof(alias->name)); + alias->name[sizeof(alias->name)-1] = 0; + aliases.push_back(alias); + return 1; +} +#endif + +/** + * @brief Remove all colons from null terminated char array + * + * @param _string Typically representing a MAC-address like AA:BB:CC:DD:EE:FF + */ +void stripColon(char* _string){ + uint32_t _length = strlen(_string); + uint32_t _index = 0; + while (_index < _length) { + char c = _string[_index]; + if(c==':'){ + memmove(_string+_index,_string+_index+1,_length-_index); + } + _index++; + } + _string[_index] = 0; +} + + +////////////////////////////////////////////////// +// use this for address interpretaton from string +// it looks for aliases, and converts AABBCCDDEEFF and AA:BB:CC:DD:EE:FF +int getAddr(uint8_t *dest, char *src){ + if (!dest || !src){ + return 0; + } +#ifdef BLE_ESP32_ALIASES + for (int i = 0; i < aliases.size(); i++){ + if (!strcmp(aliases[i]->name, src)){ + memcpy(dest, aliases[i]->addr, 6); + return 2; //was an alias + } + } +#endif + + char tmp[12+5+1]; + if (strlen(src) == 12+5){ + strcpy(tmp, src); + stripColon(tmp); + src = tmp; + } + + int len = fromHex(dest, src, 6); + if (len == 6){ + return 1; + } + // not found + return 0; +} + +static const char *noAlias = PSTR(""); + +//////////////////////////////////////////// +// use to display the alias name if required +const char *getAlias(uint8_t *addr){ + if (!addr){ + return noAlias; + } +#ifdef BLE_ESP32_ALIASES + for (int i = 0; i < aliases.size(); i++){ + if (!memcmp(aliases[i]->addr, addr, 6)){ + return aliases[i]->name; //was an alias + } + } +#endif + return noAlias; +} + + +/*********************************************************************************************\ + * Highest level BLE task control functions +\*********************************************************************************************/ + +static int StartBLE(void) { + if (BLEStop != 1){ + BLE_ESP32::BLEStartOperationTask(); + return 1; + } + AddLog_P(LOG_LEVEL_ERROR,PSTR("StartBLE - wait as BLEStop==1")); + + return 0; +} + +static int StopBLE(void){ + if (BLERunning){ + if (BLEStop != 1){ + BLEStop = 1; + AddLog_P(LOG_LEVEL_INFO,PSTR("StopBLE - BLEStop->1")); + BLEStopAt = esp_timer_get_time(); + // give a little time for it to stop. + vTaskDelay(1000/ portTICK_PERIOD_MS); + return 1; + } + AddLog_P(LOG_LEVEL_ERROR,PSTR("StopBLE - wait as BLEStop==1")); + return 0; + } else { + AddLog_P(LOG_LEVEL_ERROR,PSTR("StopBLE - was not running")); + return 1; + } +} + + +/*********************************************************************************************\ + * Commands +\*********************************************************************************************/ + +static void CmndBLEPeriod(void) { + //ResponseCmndNumber(BLE.period); + ResponseCmndDone(); +} + + +////////////////////////////////////////////////////////////// +// Determine what to do with advertismaents +// BLEAdv0 -> suppress MQTT about devices found +// BLEAdv1 -> send MQTT about devices found after each scan +void CmndBLEAdv(void){ + switch(XdrvMailbox.index){ + case 0: + BLEAdvertMode = BLE_ESP32::BLE_NO_ADV_SEND; + break; + case 1: + BLEAdvertMode = BLE_ESP32::BLE_ADV_TELE; + break; + /*case 2: + BLEAdvertMode = BLE_ADV_ALL; + break;*/ + case 3: + break; + } + + ResponseCmndNumber(BLEAdvertMode); +} + + +////////////////////////////////////////////////////////////// +// Determine what to do with advertismaents +// BLEAdv0 -> suppress MQTT about devices found +// BLEAdv1 -> send MQTT about devices found after each scan +void CmndBLEDebug(void){ + BLEDebugMode = XdrvMailbox.index; + ResponseCmndNumber(BLEDebugMode); +} + +void CmndBLEDevices(void){ + switch(XdrvMailbox.index){ + case 0:{ + // clear devices delete + deleteSeenDevices(); + } break; + case 1:{ + BLEPublishDevices = 2; // mqtt publish as 'STAT' + } break; + } + ResponseCmndDone(); +} + +void CmndBLEMaxAge(void){ + switch(XdrvMailbox.index){ + case 1:{ + if (XdrvMailbox.data_len > 0) { + BLEMaxAge = XdrvMailbox.payload; + } + } break; + } + ResponseCmndIdxNumber(BLEMaxAge); + if (BLEMaxAge) deleteSeenDevices(BLEMaxAge); +} + +void CmndBLEAddrFilter(void){ + switch(XdrvMailbox.index){ + case 1:{ + if (XdrvMailbox.data_len > 0) { + BLEAddressFilter = XdrvMailbox.payload; + } + } break; + } + ResponseCmndIdxNumber(BLEAddressFilter); +} + + +////////////////////////////////////////////////////////////// +// Scan options +// BLEScan0 -> do a scan now if BLEMode == BLEModeScanByCommand +// BLEScan0 -> do a scan now if BLEMode == BLEModeScanByCommand for timesec seconds +// BLEScan1 0 -> Scans are passive +// BLEScan1 1 -> Scans are active +// more options could be added... +void CmndBLEScan(void){ + switch(XdrvMailbox.index){ + case 0:{ + if (XdrvMailbox.data_len > 0) { + BLEScanActiveMode = XdrvMailbox.payload; + ResponseCmndNumber(BLEScanActiveMode); + } else { + ResponseCmndChar("Invalid"); + } + } break; + + case 1: // do a manual scan now + switch (BLEMode){ + case BLEModeScanByCommand: { + int time = 20; + if (XdrvMailbox.data_len > 0) { + time = XdrvMailbox.payload; + if (time < 2) time = 2; + if (time > 40) time = 40; + } + BLETriggerScan = time; + ResponseCmndNumber(time); // -ve for fail for a few reasons + } break; + case BLEModeDisabled: + ResponseCmndChar("BLEDisabled"); + break; + case BLEModeRegularScan: + ResponseCmndChar("BLEActive"); + break; + } + break; + default: + ResponseCmndChar("Invalid"); + break; + } +} + + +////////////////////////////////////////////////////////////// +// Determine what to do with advertismaents +// BLEMode0 -> kill BLE completely +// BLEMode1 -> start BLE, scan by command +// BLEMode2 -> start BLE, regular scan +void CmndBLEMode(void){ + int val = XdrvMailbox.index; + if (XdrvMailbox.data_len > 0) { + val = XdrvMailbox.payload; + } + + switch(val){ + case BLEModeDisabled:{ + if (BLEMode != BLEModeDisabled){ + BLEMode = BLEModeDisabled; + StopBLE(); + ResponseCmndChar("StoppingBLE"); + } else { + ResponseCmndChar("Disabled"); + } + } break; + case BLEModeScanByCommand:{ + uint64_t now = esp_timer_get_time(); + switch(BLEMode){ + // when changing from regular to by command, + // stop the scan next loop + case BLEModeRegularScan: { + BLEMode = BLEModeScanByCommand; + BLEStopScan = 1; + ResponseCmndChar("BLEStopScan"); + } break; + case BLEModeDisabled: { + BLEMode = BLEModeScanByCommand; + StartBLE(); + ResponseCmndChar("StartingBLE"); + } break; + case BLEModeScanByCommand:{ + ResponseCmndChar("BLERunning"); + } break; + } + BLEScanLastAdvertismentAt = now; // note the time of the last advertisment + } break; + case BLEModeRegularScan:{ + uint64_t now = esp_timer_get_time(); + switch(BLEMode){ + case BLEModeDisabled: { + BLEMode = BLEModeRegularScan; + StartBLE(); + ResponseCmndChar("StartingBLE"); + } break; + case BLEModeScanByCommand:{ + BLEMode = BLEModeRegularScan; + ResponseCmndChar("BLEEnableScan"); + } break; + case BLEModeRegularScan:{ + BLEMode = BLEModeRegularScan; + ResponseCmndChar("BLERunning"); + } break; + } + BLEScanLastAdvertismentAt = now; // note the time of the last advertisment + } break; + default: + ResponseCmndChar("InvalidIndex"); + break; + } +} + + +////////////////////////////////////////// +// get more drtails for a single MAC address +// BLEDetails0 -> don;t send me anything +// BLEDetails1 -> send me details for once +// BLEDetails2 -> send me details for every advert if possible +// example: BLEDetails1 001A22092C9A +// details look like: +// MQT: tele/tasmota_esp32/BLE = {"details":{"mac":"001A22092C9A","p":"0C0943432D52542D4D2D424C450CFF0000000000000000000000"}} +// and incliude mac, complete advert payload, plus optional ,"lost":true if an advert was not captured because MQTT we already +// had one waiting to be sent +void CmndBLEDetails(void){ + switch(XdrvMailbox.index){ + case 0: + BLEDetailsRequest = 0; + ResponseCmndNumber(BLEDetailsRequest); + break; + + case 1: + case 2:{ + BLEDetailsRequest = 0; + if (getAddr(BLEDetailsMac, XdrvMailbox.data)){ + BLEDetailsRequest = XdrvMailbox.index; + ResponseCmndIdxChar(XdrvMailbox.data); + } else { + ResponseCmndChar("InvalidMac"); + } + } break; + + case 3:{ + BLEDetailsRequest = XdrvMailbox.index; + ResponseCmndNumber(BLEDetailsRequest); + } break; + + default: + ResponseCmndChar("InvalidIndex"); + break; + } +} + + +void CmndBLEAlias(void){ +#ifdef BLE_ESP32_ALIASES + int op = XdrvMailbox.index; + if (BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG,PSTR("Alias %d %s"), op, XdrvMailbox.data); + + int res = -1; + switch(op){ + case 0: + case 1:{ + char *p = strtok(XdrvMailbox.data, " ,="); + bool trigger = false; + int added = 0; + + do { + if (!p || !(*p)){ + break; + } + + uint8_t addr[6]; + char *mac = p; + int len = fromHex(addr, p, sizeof(addr)); + if (len != 6){ + AddLog_P(LOG_LEVEL_ERROR,PSTR("Alias invalid mac %s"), p); + ResponseCmndChar("invalidmac"); + return; + } + + p = strtok(nullptr, " ,="); + char *name = p; + if (!p || !(*p)){ + int i = 0; + for (i = 0; i < aliases.size(); i++){ + BLE_ESP32::ble_alias_t *alias = aliases[i]; + if (!memcmp(alias->addr, addr, 6)){ + aliases.erase(aliases.begin() + i); + BLEAliasListResp(); + return; + } + } + ResponseCmndChar("invalidmac"); + return; + } + + AddLog_P(LOG_LEVEL_ERROR,PSTR("Add Alias mac %s = name %s"), mac, p); + if (addAlias( addr, name )){ + added++; + } + p = strtok(nullptr, " ,="); + } while (p); + + if (added){ + if (BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG,PSTR("Added %d Aliases"), added); + BLEAliasListResp(); + } else { + BLEAliasListResp(); + } + return; + } break; + case 2:{ // clear + if (BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG,PSTR("BLEAlias clearing %d"), aliases.size()); + for (int i = aliases.size()-1; i >= 0; i--){ + BLE_ESP32::ble_alias_t *alias = aliases[i]; + aliases.pop_back(); + delete alias; + } + BLEAliasListResp(); + return; + } break; + } + ResponseCmndChar("invalididx"); +#endif +} + + +// SET the BLE name for a device - +// uses s:1800 c:2a00 and writes name to DEVICE +void CmndBLEName(void) { + char *p = strtok(XdrvMailbox.data, " "); + + if (!p || !(*p)){ + ResponseCmndIdxChar(PSTR("invalid")); + return; + } + + uint8_t addrbin[6]; + int addrres = BLE_ESP32::getAddr(addrbin, p); + NimBLEAddress addr(addrbin); + + if (addrres){ + if (addrres == 2){ + if (BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG,PSTR("BLE addr used alias: %s"), p); + } + +//#ifdef EQ3_DEBUG + if (BLEDebugMode > 0) AddLog_P(LOG_LEVEL_INFO,PSTR("BLE cmd addr: %s -> %s"), p, addr.toString().c_str()); +//#endif + } else { + AddLog_P(LOG_LEVEL_ERROR,PSTR("BLE addr invalid: %s"), p); + ResponseCmndIdxChar(PSTR("invalidaddr")); + return; + } + + BLE_ESP32::generic_sensor_t *op = nullptr; + // ALWAYS use this function to create a new one. + int res = BLE_ESP32::newOperation(&op); + if (!res){ + AddLog_P(LOG_LEVEL_ERROR,PSTR("Can't get a newOperation")); + ResponseCmndChar(PSTR("FAIL")); + return; + } else { + if (BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG,PSTR("got a newOperation from BLE")); + } + + op->addr = addr; + op->serviceUUID = NimBLEUUID("1800"); + op->characteristicUUID = NimBLEUUID("2A00"); + + // get next part of cmd + char *name = strtok(nullptr, " "); + bool write = false; + if (name && *name){ + if (BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG,PSTR("write name %s"), name); + op->writelen = strlen(name); + memcpy(op->dataToWrite, name, op->writelen); + write = true; + } else { + if (BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG,PSTR("read name")); + op->readlen = 1; + } + + res = BLE_ESP32::extQueueOperation(&op); + if (BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG,PSTR("queue res %d"), res); + if (!res){ + // if it fails to add to the queue, do please delete it + BLE_ESP32::freeOperation(&op); + AddLog_P(LOG_LEVEL_ERROR,PSTR("Failed to queue new operation - deleted")); + ResponseCmndChar(PSTR("QUEUEFAIL")); + return; + } + + if (write){ + if (BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG, PSTR("BLE: will write name")); + } else { + if (BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG, PSTR("BLE: will read name")); + } + ResponseCmndDone(); + return; +} + + + +////////////////////////////////////////////////////////////////////////// +// Command to cause BLE read/write/notify operations to be run. +////////////////////////////////////////////////////////////////////////// + +// we expect BLEOp0 - poll state +// we expect BLEOp1 m:MAC s:svc +// we expect BLEOp2 trigger queue of op. return is opid + +// returns: Done|FailCreate|FailNoOp|FailQueue|InvalidIndex| + +// BLEop0/1/2 will cause an MQTT send of ops currently known. +// on op complete/op fail, a MQTT send is triggered of all known ops, and the completed/failed op removed. + +// example: +// BLEOp1 M:001A22092CDB s:3e135142-654f-9090-134a-a6ff5bb77046 c:3fa4585a-ce4a-3bad-db4b-b8df8179ea09 w:03 n:d0e8434d-cd29-0996-af41-6c90f4e0eb2a go +// requests write of 03, and request wait for notify. + +// You may queue up operations. they are currently processed serially. +void CmndBLEOperation(void){ + + int op = XdrvMailbox.index; + + //AddLog_P(LOG_LEVEL_INFO,PSTR("op %d"), op); + + int res = -1; + + // if in progress, only op 0 or 11 are allowed + switch(op) { + case 0: +#ifdef BLE_ESP32_DEBUG + if (BLEDebugMode > 0) AddLog_P(LOG_LEVEL_INFO,PSTR("preview")); +#endif + BLEPostMQTTTrigger = 1; + break; + case 1: { + if (prepOperation){ + BLE_ESP32::freeOperation(&prepOperation); + } + int opres = BLE_ESP32::newOperation(&prepOperation); + if (!opres){ +#ifdef BLE_ESP32_DEBUG + AddLog_P(LOG_LEVEL_ERROR,PSTR("Could not create new operation")); +#endif + ResponseCmndChar("FailCreate"); + return; + } + // expect m:MAC s:svc + // < > are optional + char *p = strtok(XdrvMailbox.data, " ,"); + bool trigger = false; + + while (p){ + switch(*p | 0x20){ + case 'm':{ + uint8_t addr[6]; + if (getAddr(addr, p+2)){ + prepOperation->addr = NimBLEAddress(addr); + } else { + prepOperation->addr = NimBLEAddress(); + } + } break; + case 's':{ + prepOperation->serviceUUID = NimBLEUUID(p+2); + } break; + case 'c': + prepOperation->characteristicUUID = NimBLEUUID(p+2); + //strncpy(prepOperation->characteristicStr, p+2, sizeof(prepOperation->characteristicStr)-1); + break; + case 'n': + prepOperation->notificationCharacteristicUUID = NimBLEUUID(p+2); + //strncpy(prepOperation->notificationCharacteristicStr, p+2, sizeof(prepOperation->notificationCharacteristicStr)-1); + break; + case 'w': + prepOperation->writelen = fromHex(prepOperation->dataToWrite, p+2, sizeof(prepOperation->dataToWrite)); + break; + case 'u': // 'unique' context for this request + prepOperation->context = (void *)atoi(p+2); + break; + case 'r': + prepOperation->readlen = 1; + break; + case 'g': + if ((*(p+1))|0x20 == 'o'){ + trigger = true; + } + break; + } + + p = strtok(nullptr, " ,"); + } + + if (trigger){ + int u = (int)prepOperation->context; + int opres = BLE_ESP32::extQueueOperation(&prepOperation); + if (!opres){ + // NOTE: prepOperation will NOT have been deleted. + // this means you could retry with another BLEOp10. + // it WOULD be deleted if you sent another BELOP1 +#ifdef BLE_ESP32_DEBUG + AddLog_P(LOG_LEVEL_ERROR,PSTR("Could not queue new operation")); +#endif + ResponseCmndChar("FailQueue"); + return; + } else { + // NOTE: prepOperation has been set to null if we queued sucessfully. +#ifdef BLE_ESP32_DEBUG + if (BLEDebugMode > 0) AddLog_P(LOG_LEVEL_INFO,PSTR("Operations queued:%d"), queuedOperations.size()); +#endif + char temp[40]; + sprintf(temp, "{\"opid\":%d,\"u\":%d}", lastopid-1, u); + Response_P(S_JSON_COMMAND_XVALUE, XdrvMailbox.command, temp); + // don't do this here... overwrites response + //BLE_ESP32::BLEPostMQTT(false); + return; + } + } else { + ResponseCmndChar("Prepared"); + //BLE_ESP32::BLEPostMQTT(false); + return; + } + } break; + + case 2: { + if (!prepOperation) { + ResponseCmndChar("FailNoOp"); + return; + } + //prepOperation->requestType = atoi(XdrvMailbox.data); + int u = (int)prepOperation->context; + int opres = BLE_ESP32::extQueueOperation(&prepOperation); + if (!opres){ + // NOTE: prepOperation will NOT have been deleted. + // this means you could retry with another BLEOp10. + // it WOULD be deleted if you sent another BELOP1 +#ifdef BLE_ESP32_DEBUG + AddLog_P(LOG_LEVEL_ERROR,PSTR("Could not queue new operation")); +#endif + ResponseCmndChar("FailQueue"); + } else { + // NOTE: prepOperation has been set to null if we queued sucessfully. +#ifdef BLE_ESP32_DEBUG + if (BLEDebugMode > 0) AddLog_P(LOG_LEVEL_INFO,PSTR("Operations queued:%d"), queuedOperations.size()); +#endif + char temp[40]; + sprintf(temp, "{\"opid\":%d,\"u\":%d}", lastopid-1, u); + Response_P(S_JSON_COMMAND_XVALUE, XdrvMailbox.command, temp); + } + return; + } break; + + default: + ResponseCmndChar("InvalidIndex"); + return; + } + + ResponseCmndDone(); + return; +} + + +/*********************************************************************************************\ + * Presentation +\*********************************************************************************************/ +static void BLEPostMQTTSeenDevices(int type) { + int remains = 0; + nextSeenDev = 0; + + memset(TasmotaGlobal.mqtt_data, 0, sizeof(TasmotaGlobal.mqtt_data)); + ResponseTime_P(PSTR("")); + int timelen = strlen(TasmotaGlobal.mqtt_data); + char *dest = TasmotaGlobal.mqtt_data + timelen; + int maxlen = (sizeof(TasmotaGlobal.mqtt_data)-20) - timelen; + +// if (!TasmotaGlobal.ota_state_flag){ + do { + remains = getSeenDevicesToJson(dest, maxlen); + // no retain - this is present devices, not historic + if (type == 1){ + MqttPublishPrefixTopic_P(TELE, PSTR("BLE"), 0); + } else { + MqttPublishPrefixTopic_P(STAT, PSTR("BLE"), 0); + } + } while (remains); +// } +} + +static void BLEPostMQTT(bool onlycompleted) { +// if (TasmotaGlobal.ota_state_flag) return; + + + if (prepOperation || completedOperations.size() || queuedOperations.size() || currentOperations.size()){ +#ifdef BLE_ESP32_DEBUG + if (BLEDebugMode > 0) AddLog_P(LOG_LEVEL_INFO,PSTR("some to show")); +#endif + if (prepOperation && !onlycompleted){ + std::string out = BLETriggerResponse(prepOperation); + snprintf_P(TasmotaGlobal.mqtt_data, sizeof(TasmotaGlobal.mqtt_data), PSTR("%s"), out.c_str()); + MqttPublishPrefixTopic_P(TELE, PSTR("BLE"), Settings.flag.mqtt_sensor_retain); +#ifdef BLE_ESP32_DEBUG + if (BLEDebugMode > 0) AddLog_P(LOG_LEVEL_INFO,PSTR("prep sent %s"), out.c_str()); +#endif + } + + if (queuedOperations.size() && !onlycompleted){ +#ifdef BLE_ESP32_DEBUG + if (BLEDebugMode > 0) AddLog_P(LOG_LEVEL_INFO,PSTR("queued %d"), queuedOperations.size()); +#endif + for (int i = 0; i < queuedOperations.size(); i++){ + TasAutoMutex localmutex(&BLEOperationsRecursiveMutex, "BLEPost1"); + + generic_sensor_t *toSend = queuedOperations[i]; + if (!toSend) { + break; + } else { + std::string out = BLETriggerResponse(toSend); + localmutex.give(); + snprintf_P(TasmotaGlobal.mqtt_data, sizeof(TasmotaGlobal.mqtt_data), PSTR("%s"), out.c_str()); + MqttPublishPrefixTopic_P(TELE, PSTR("BLE"), Settings.flag.mqtt_sensor_retain); +#ifdef BLE_ESP32_DEBUG + if (BLEDebugMode > 0) AddLog_P(LOG_LEVEL_INFO,PSTR("queued %d sent %s"), i, out.c_str()); +#endif + //break; + } + } + } + + if (currentOperations.size() && !onlycompleted){ +#ifdef BLE_ESP32_DEBUG + if (BLEDebugMode > 0) AddLog_P(LOG_LEVEL_INFO,PSTR("current %d"), currentOperations.size()); +#endif + for (int i = 0; i < currentOperations.size(); i++){ + TasAutoMutex localmutex(&BLEOperationsRecursiveMutex, "BLEPost2"); + generic_sensor_t *toSend = currentOperations[i]; + if (!toSend) { + break; + } else { + std::string out = BLETriggerResponse(toSend); + localmutex.give(); + snprintf_P(TasmotaGlobal.mqtt_data, sizeof(TasmotaGlobal.mqtt_data), PSTR("%s"), out.c_str()); + MqttPublishPrefixTopic_P(TELE, PSTR("BLE"), Settings.flag.mqtt_sensor_retain); +#ifdef BLE_ESP32_DEBUG + if (BLEDebugMode > 0) AddLog_P(LOG_LEVEL_INFO,PSTR("curr %d sent %s"), i, out.c_str()); +#endif + //break; + } + } + } + + if (completedOperations.size()){ +#ifdef BLE_ESP32_DEBUG + if (BLEDebugMode > 0) AddLog_P(LOG_LEVEL_INFO,PSTR("completed %d"), completedOperations.size()); +#endif + do { + generic_sensor_t *toSend = nextOperation(&completedOperations); + if (!toSend) { + break; // break from while loop + } else { +#ifdef BLE_ESP32_DEBUG + if (BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG,PSTR("BLE:completedOperation removed")); +#endif + std::string out = BLETriggerResponse(toSend); + snprintf_P(TasmotaGlobal.mqtt_data, sizeof(TasmotaGlobal.mqtt_data), PSTR("%s"), out.c_str()); + MqttPublishPrefixTopic_P(TELE, PSTR("BLE"), Settings.flag.mqtt_sensor_retain); + // we alreayd removed this from the queues, so now delete + delete toSend; + //break; + } + //break; + } while (1); + } + } else { + snprintf_P(TasmotaGlobal.mqtt_data, sizeof(TasmotaGlobal.mqtt_data), PSTR("{\"BLEOperation\":{}}")); + MqttPublishPrefixTopic_P(TELE, PSTR("BLE"), Settings.flag.mqtt_sensor_retain); + } +} + +static void mainThreadBLETimeouts() { + uint64_t now = esp_timer_get_time(); + + if (!BLERunning){ + BLELastLoopTime = now; // initialise the time of the last advertisment + BLEScanLastAdvertismentAt = now; // initialise the time of the last advertisment + return; + } + + if (BLEStop == 1){ + if (BLEStopAt + 30L*1000L*1000L < now){ // if asked to stop > 30s ago... + AddLog_P(LOG_LEVEL_ERROR,PSTR("BLE: Stop Timeout - restart Tasmota")); + BLERestartTasmota = 2; + BLEStopAt = now; + } + AddLog_P(LOG_LEVEL_ERROR,PSTR("BLE: Awaiting BLEStop")); + return; + } + + // if no adverts for 120s, and BLE is running, retsart NimBLE. + // belt and braces.... + uint64_t adTimeout = ((uint64_t)BLEMaxTimeBetweenAdverts)*1000L*1000L; + if (BLEScanLastAdvertismentAt + adTimeout < now){ + BLEScanLastAdvertismentAt = now; // initialise the time of the last advertisment + BLERestartNimBLE = 1; + AddLog_P(LOG_LEVEL_ERROR,PSTR("BLE: scan stall? no adverts > 120s, restart BLE")); + + BLERestartBLEReason = BLE_RESTART_BLE_REASON_ADVERT_BLE_TIMEOUT; + } + + // if stuck and have not done task for 120s, something is seriously wrong. + // restart Tasmota completely. (belt and braces) + uint64_t bleLoopTimeout = ((uint64_t)BLEMaxTaskLoopTime)*1000L*1000L; + if (BLELastLoopTime + bleLoopTimeout < now){ + BLELastLoopTime = now; // initialise the time of the last advertisment + BLERestartTasmota = 10; + AddLog_P(LOG_LEVEL_DEBUG,PSTR("BLE: BLETask stall > 120s, restart Tasmota in 10s")); + BLERestartTasmotaReason = BLE_RESTART_TEAMOTA_REASON_BLE_LOOP_STALLED; + } +} + + +static void mainThreadOpCallbacks() { + if (completedOperations.size()){ + //AddLog_P(LOG_LEVEL_INFO,PSTR("completed %d"), completedOperations.size()); + TasAutoMutex localmutex(&BLEOperationsRecursiveMutex, "BLEMainCB"); + + // find this operation in currentOperations, and remove it. + // in reverse so we can erase them safely. + for (int i = completedOperations.size()-1; i >= 0 ; i--){ + generic_sensor_t *op = completedOperations[i]; + + bool callbackres = false; + + if (op->completecallback){ + try { + OPCOMPLETE_CALLBACK *pFn = (OPCOMPLETE_CALLBACK *)(op->completecallback); + callbackres = pFn(op); +#ifdef BLE_ESP32_DEBUG + if (BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG,PSTR("op->completecallback %d"), callbackres); +#endif + } catch(const std::exception& e){ +#ifdef BLE_ESP32_DEBUG + AddLog_P(LOG_LEVEL_ERROR,PSTR("exception in op->completecallback")); +#endif + } + } + + if (!callbackres){ + for (int i = 0; i < operationsCallbacks.size(); i++){ + try { + OPCOMPLETE_CALLBACK *pFn = operationsCallbacks[i]; + callbackres = pFn(op); +#ifdef BLE_ESP32_DEBUG + if (BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG,PSTR("operationsCallbacks %d %d"), i, callbackres); +#endif + if (callbackres){ + break; // this callback ate the op. + } + } catch(const std::exception& e){ +#ifdef BLE_ESP32_DEBUG + AddLog_P(LOG_LEVEL_ERROR,PSTR("exception in operationsCallbacks")); +#endif + } + } + } + + // if some callback told us not to send on MQTT, then remove from completed and delete the data + if (callbackres){ +#ifdef BLE_ESP32_DEBUG + if (BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG,PSTR("callbackres true -> delete op")); +#endif + completedOperations.erase(completedOperations.begin() + i); + delete op; + } + } + } +} + + +static void BLEShow(bool json) +{ + if (json){ +#ifdef BLE_ESP32_DEBUG + if (BLEDebugMode > 0) AddLog_P(LOG_LEVEL_INFO,PSTR("show json %d"),json); +#endif + uint32_t totalCount = BLEAdvertisment.totalCount; + uint32_t deviceCount = seenDevices.size(); + + ResponseAppend_P(PSTR(",\"BLE\":{\"scans\":%u,\"adverts\":%u,\"devices\":%u,\"resets\":%u}"), BLEScanCount, totalCount, deviceCount, BLEResets); + } +#ifdef USE_WEBSERVER + else { + //WSContentSend_PD(HTTP_MI32, i+1,stemp,MIBLEsensors.size()); + } +#endif // USE_WEBSERVER + +} + +/*void BLEAliasMqttList(){ + ResponseTime_P(PSTR(",\"BLEAlias\":[")); + for (int i = 0; i < aliases.size(); i++){ + if (i){ + ResponseAppend_P(PSTR(",")); + } + char tmp[20]; + ToHex_P(aliases[i]->addr,6,tmp,20,0); + ResponseAppend_P(PSTR("{\"%s\":\"%s\"}"), tmp, aliases[i]->name); + } + ResponseAppend_P(PSTR("]}")); + MqttPublishPrefixTopic_P(TELE, PSTR("BLE"), Settings.flag.mqtt_sensor_retain); +}*/ + +void BLEAliasListResp(){ + Response_P(PSTR("{\"BLEAlias\":{")); + for (int i = 0; i < aliases.size(); i++){ + if (i){ + ResponseAppend_P(PSTR(",")); + } + char tmp[20]; + ToHex_P(aliases[i]->addr,6,tmp,20,0); + ResponseAppend_P(PSTR("\"%s\":\"%s\""), tmp, aliases[i]->name); + } + ResponseAppend_P(PSTR("}}")); +} + + +static void BLEDiag() +{ + uint32_t totalCount = BLEAdvertisment.totalCount; + uint32_t deviceCount = seenDevices.size(); +#ifdef BLE_ESP32_DEBUG + if (BLEDebugMode > 0) AddLog_P(LOG_LEVEL_INFO,PSTR("BLE:scans:%u,advertisements:%u,devices:%u,resets:%u,BLEStop:%d,BLERunning:%d,BLERunningScan:%d,BLELoopCount:%u,BLEOpCount:%u"), BLEScanCount, totalCount, deviceCount, BLEResets, BLEStop, BLERunning, BLERunningScan, BLELoopCount, BLEOpCount); +#endif +} + +/** + * @brief creates a JSON representing a single operation. + * + */ +std::string BLETriggerResponse(generic_sensor_t *toSend){ + char temp[100]; + if (!toSend) return ""; + std::string out = "{\"BLEOperation\":{\"opid\":\""; + sprintf(temp, "%d", toSend->opid); // note only 10 long! + out = out + temp; +/* out = out + "\",\"state\":\""; + sprintf(t, "%d", toSend->state); + out = out + t;*/ + out = out + "\",\"stat\":\""; + sprintf(temp, "%d", toSend->state); + out = out + temp; + out = out + "\",\"state\":\""; + out = out + getStateString(toSend->state); + + if (toSend->addr != NimBLEAddress()){ + out = out + "\",\"MAC\":\""; + uint8_t addrrev[6]; + memcpy(addrrev, toSend->addr.getNative(), 6); + ReverseMAC(addrrev); + dump(temp, 13, addrrev, 6); + out = out + temp; + } + if (toSend->context){ + out = out + "\",\"u\":\""; + sprintf(temp, "%d", (int32_t)toSend->context); + out = out + temp; + } + if (toSend->serviceUUID.bitSize()){ + out = out + "\",\"svc\":\""; + out = out + toSend->serviceUUID.toString(); + } + if (toSend->characteristicUUID.bitSize()){ + out = out + "\",\"char\":\""; + out = out + toSend->characteristicUUID.toString(); + } + if (toSend->notificationCharacteristicUUID.bitSize()){ + out = out + "\",\"notifychar\":\""; + out = out + toSend->notificationCharacteristicUUID.toString(); + } + out = out + "\""; + if (toSend->readlen){ + dump(temp, 99, toSend->dataRead, toSend->readlen); + if (toSend->readtruncated){ + strcat(temp, "+"); + } + out = out + ",\"read\":\""; + out = out + temp; + out = out + "\""; + } + if (toSend->writelen){ + dump(temp, 99, toSend->dataToWrite, toSend->writelen); + out = out + ",\"write\":\""; + out = out + temp; + out = out + "\""; + } + if (toSend->notifylen){ + dump(temp, 99, toSend->dataNotify, toSend->notifylen); + if (toSend->notifytruncated){ + strcat(temp, "+"); + } + out = out + ",\"notify\":\""; + out = out + temp; + out = out + "\""; + } + out = out + "}}"; + return out; +} + +#ifdef USE_WEBSERVER + +#define WEB_HANDLE_BLE "ble" +#define D_CONFIGURE_BLE "Configure BLE" +#define D_BLE_PARAMETERS "Bluetooth Settings" +#define D_MQTT_BLE_ENABLE "Enable Bluetooth" +#define D_MQTT_BLE_ACTIVESCAN "Enable Active Scan(*)" +#define D_BLE_DEVICES "Devices Seen" + +const char HTTP_BTN_MENU_BLE[] PROGMEM = + "

"; + +const char HTTP_FORM_BLE[] PROGMEM = + "
 " D_BLE_PARAMETERS " " + "
" + "

" + "

" + "

items marked (*) are not stored in config

"; + + +const char HTTP_BLE_DEV_STYLE[] PROGMEM = "th, td { padding-left:5px; }"; +const char HTTP_BLE_DEV_START[] PROGMEM = + "
 " D_BLE_DEVICES " " + ""; +const char HTTP_BLE_DEV[] PROGMEM = + ""; +const char HTTP_BLE_DEV_END[] PROGMEM = + "
"; + +void HandleBleConfiguration(void) +{ + +#ifdef BLE_ESP32_DEBUG + AddLog_P(LOG_LEVEL_DEBUG, PSTR("HandleBleConfiguration")); +#endif + + if (!HttpCheckPriviledgedAccess()) { +#ifdef BLE_ESP32_DEBUG + AddLog_P(LOG_LEVEL_DEBUG, PSTR("!HttpCheckPriviledgedAccess()")); +#endif + return; + } + +#ifdef BLE_ESP32_DEBUG + AddLog_P(LOG_LEVEL_DEBUG, PSTR(D_LOG_HTTP D_CONFIGURE_BLE)); +#endif + + char tmp[20]; + WebGetArg("en", tmp, sizeof(tmp)); + +#ifdef BLE_ESP32_DEBUG + if (BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG, PSTR("arg en is %s"), tmp); +#endif + + if (Webserver->hasArg("save")) { +#ifdef BLE_ESP32_DEBUG + AddLog_P(LOG_LEVEL_DEBUG, PSTR("BLE SETTINGS SAVE")); +#endif + Settings.flag5.mi32_enable = Webserver->hasArg("e0"); // + BLEScanActiveMode = (Webserver->hasArg("e1")?1:0); // + + SettingsSaveAll(); + HandleConfiguration(); + return; + } +#ifdef BLE_ESP32_DEBUG + AddLog_P(LOG_LEVEL_DEBUG, PSTR("!SAVE")); +#endif + char str[TOPSZ]; + + WSContentStart_P(PSTR(D_CONFIGURE_BLE)); + WSContentSendStyle_P(HTTP_BLE_DEV_STYLE); + //WSContentSendStyle(); + WSContentSend_P(HTTP_FORM_BLE, + (Settings.flag5.mi32_enable) ? " checked" : "", + (BLEScanActiveMode) ? " checked" : "" + ); + WSContentSend_P(HTTP_FORM_END); + + + { + //TasAutoMutex localmutex(&BLEOperationsRecursiveMutex, "BLEConf"); + int number = seenDevices.size(); + if (number){ + WSContentSend_P(HTTP_BLE_DEV_START); + uint64_t now = esp_timer_get_time(); + now = now/1000L; + now = now/1000L; + uint32_t nowS = (uint32_t)now; + + for (int i = 0; i < number; i++){ + BLE_ESP32::BLE_simple_device_t* dev = seenDevices[i]; + char addr[20]; + dump(addr, 20, dev->mac, 6); + uint8_t addrtype = dev->addrtype; + const char *alias = getAlias(dev->mac); + uint64_t lastseen = dev->lastseen/1000L; + lastseen = lastseen/1000L; + uint32_t lastseenS = (uint32_t) lastseen; + uint32_t ageS = nowS-lastseenS; + + WSContentSend_P(HTTP_BLE_DEV, addr, addrtype, alias, dev->name, dev->RSSI, ageS, dev->maxAge); + } + WSContentSend_P(HTTP_BLE_DEV_END); + } + } + + WSContentSpaceButton(BUTTON_CONFIGURATION); + WSContentStop(); + +} +#endif + + +} // end namespace BLE_ESP32 + +/*********************************************************************************************\ + * Interface +\*********************************************************************************************/ + +int ExtStopBLE(){ + AddLog_P(LOG_LEVEL_INFO, PSTR("Stopping BLE if active - upgrade starting?")); + BLE_ESP32::BLEMode = BLE_ESP32::BLEModeDisabled; + BLE_ESP32::StopBLE(); + return 0; +} + +bool Xdrv52(uint8_t function) +{ + //if (!Settings.flag5.mi32_enable) { return false; } // SetOption115 - Enable ESP32 BLE BLE + + bool result = false; + + if (FUNC_INIT == function){ + BLE_ESP32::BLEPreInit(); + } + + if (!BLE_ESP32::BLEInitState) { + if (function == FUNC_EVERY_250_MSECOND) { + BLE_ESP32::BLEInit(); + } + return result; + } + switch (function) { + case FUNC_EVERY_50_MSECOND: + BLE_ESP32::BLEEvery50mSecond(); + //############################# DEBUG + TasmotaGlobal.seriallog_timer = 0; + break; + case FUNC_EVERY_SECOND: + BLE_ESP32::BLEEverySecond(false); + break; + case FUNC_COMMAND: + result = DecodeCommand(BLE_ESP32::kBLE_Commands, BLE_ESP32::BLE_Commands); + break; + case FUNC_JSON_APPEND: + BLE_ESP32::BLEShow(1); + break; + + // next second, we will publish to our MQTT topic. + case FUNC_AFTER_TELEPERIOD: + BLE_ESP32::BLEPublishDevices = 1; // mqtt publish as 'TELE' + break; + +#ifdef USE_WEBSERVER + case FUNC_WEB_ADD_BUTTON: + WSContentSend_P(BLE_ESP32::HTTP_BTN_MENU_BLE); + break; + case FUNC_WEB_ADD_HANDLER: + WebServer_on(PSTR("/" WEB_HANDLE_BLE), BLE_ESP32::HandleBleConfiguration); + break; + + case FUNC_WEB_SENSOR: + BLE_ESP32::BLEShow(0); + break; +#endif // USE_WEBSERVER + } + return result; +} + + + +/*********************************************************************************************\ + * Example Advertisment callback +\*********************************************************************************************/ + +#ifdef EXAMPLE_ADVERTISMENT_CALLBACK + +// match ADVERTISMENT_CALLBACK +int myAdvertCallback(BLE_ESP32::ble_advertisment_t *pStruct) { + + // indicate others can also hear this + // to say 'I want this exclusively', return true. + return 0; + +} +#endif +/*********************************************************************************************\ + * End of Example Advertisment callback +\*********************************************************************************************/ + + +/*********************************************************************************************\ + * Example Operations callbacks +\*********************************************************************************************/ +#ifdef EXAMPLE_OPERATION_CALLBACK + +// this one is used to demonstrate processing ALL operations +int myOpCallback(BLE_ESP32::generic_sensor_t *pStruct){ + AddLog_P(LOG_LEVEL_INFO,PSTR("myOpCallback")); + return 0; // return true to block MQTT broadcast +} + +// this one is used to demonstrate processing of ONE specific operation +int myOpCallback2(BLE_ESP32::generic_sensor_t *pStruct){ + AddLog_P(LOG_LEVEL_INFO,PSTR("myOpCallback2")); + return 1; // return true to block MQTT broadcast +} +#endif +/*********************************************************************************************\ + * End of Example Operations callbacks +\*********************************************************************************************/ + +void installExamples(){ +#ifdef EXAMPLE_ADVERTISMENT_CALLBACK + BLE_ESP32::registerForAdvertismentCallbacks((const char *)"test myOpCallback", &myAdvertCallback); +#endif + +#ifdef EXAMPLE_OPERATION_CALLBACK + BLE_ESP32:registerForOpCallbacks((const char *)"test myOpCallback", &myOpCallback); +#endif +} + +void sendExample(){ +#ifdef EXAMPLE_OPERATION_CALLBACK + BLE_ESP32::generic_sensor_t *op = nullptr; + int res = BLE_ESP32::newOperation(&op); + if (!res){ + AddLog_P(LOG_LEVEL_ERROR,PSTR("Could not create new operation")); + return; + } + strncpy(op->MAC, "001A22092EE0", sizeof(op->MAC)); + strncpy(op->serviceStr, "3e135142-654f-9090-134a-a6ff5bb77046", sizeof(op->serviceStr)); + strncpy(op->characteristicStr, "3fa4585a-ce4a-3bad-db4b-b8df8179ea09", sizeof(op->characteristicStr)); + strncpy(op->notificationCharacteristicStr, "d0e8434d-cd29-0996-af41-6c90f4e0eb2a", sizeof(op->notificationCharacteristicStr)); + op->writelen = BLE_ESP32::fromHex(op->dataToWrite, (char *)"4040", sizeof(op->dataToWrite)); + + // this op will call us back on complete or failure. + op->completecallback = (void *)myOpCallback2; + res = BLE_ESP32::extQueueOperation(&op); + if (!res){ + // if it fails to add to the queue, do please delete it + BLE_ESP32::freeOperation(&op); + AddLog_P(LOG_LEVEL_ERROR,PSTR("Failed to queue new operation - deleted")); + return; + } + +#endif +} + + + +#endif +#endif // ESP32 + + diff --git a/tasmota/xsns_52_ibeacon.ino b/tasmota/xsns_52_ibeacon.ino index 723446700..8fb924d4e 100755 --- a/tasmota/xsns_52_ibeacon.ino +++ b/tasmota/xsns_52_ibeacon.ino @@ -17,6 +17,10 @@ along with this program. If not, see . */ +// for testing of BLE_ESP32, we remove this completely, and instead add the modified xsns_52_ibeacon_BLE_ESP32.ino +// in the future this may be more fine-grained, e.g. to allow hm17 for this, and BLE-ESP32 for other +#ifndef USE_BLE_ESP32 + #ifdef USE_IBEACON #define XSNS_52 52 @@ -279,13 +283,16 @@ void ESP32Init() { if (TasmotaGlobal.global_state.wifi_down) { return; } - TasmotaGlobal.wifi_stay_asleep = true; if (WiFi.getSleep() == false) { - AddLog_P(LOG_LEVEL_DEBUG,PSTR("%s: Put WiFi modem in sleep mode"),"BLE"); - WiFi.setSleep(true); // Sleep + if (0 == Settings.flag3.sleep_normal) { + AddLog_P(LOG_LEVEL_DEBUG,PSTR("%s: About to restart to put WiFi modem in sleep mode"),"BLE"); + Settings.flag3.sleep_normal = 1; // SetOption60 - Enable normal sleep instead of dynamic sleep + TasmotaGlobal.restart_flag = 2; + } + return; } - AddLog_P(LOG_LEVEL_DEBUG,PSTR("%s: Initializing Bluetooth..."),"BLE"); + AddLog_P(LOG_LEVEL_DEBUG,PSTR("%s: Initializing Blueetooth..."),"BLE"); if (!ESP32BLE.mode.init) { NimBLEDevice::init(""); @@ -1012,3 +1019,5 @@ bool Xsns52(byte function) } #endif // USE_IBEACON + +#endif \ No newline at end of file diff --git a/tasmota/xsns_52_ibeacon_BLE_ESP32.ino b/tasmota/xsns_52_ibeacon_BLE_ESP32.ino new file mode 100644 index 000000000..687bf05cf --- /dev/null +++ b/tasmota/xsns_52_ibeacon_BLE_ESP32.ino @@ -0,0 +1,952 @@ +/* + xsns_52_ibeacon.ino - Support for HM17 BLE Module + ibeacon reader on Tasmota + + Copyright (C) 2020 Gerhard Mutz and Theo Arends + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ + +// for testing of BLE_ESP32, we remove xsns_52_ibeacon.ino completely, and instead add this modified xsns_52_ibeacon_BLE_ESP32.ino +// in the future this may be more fine-grained, e.g. to allow hm17 for this, and BLE-ESP32 for other +#ifdef USE_BLE_ESP32 + +#ifdef USE_IBEACON_ESP32 + +#ifdef USE_IBEACON + +#define XSNS_52 52 + +// keyfob expires after N seconds +#define IB_TIMEOUT_INTERVAL 30 +// does a passive scan every N seconds +#define IB_UPDATE_TIME_INTERVAL 10 + +// should be in Settings +#if 1 + uint8_t ib_upd_interval,ib_tout_interval; + #define IB_UPDATE_TIME ib_upd_interval + #define IB_TIMEOUT_TIME ib_tout_interval +#else + #undef IB_UPDATE_TIME + #undef IB_TIMEOUT_TIME + #define IB_UPDATE_TIME Settings.ib_upd_interval + #define IB_TIMEOUT_TIME Settings.ib_tout_interval +#endif + +char ib_mac[14]; + + + struct { + union { + struct { + uint32_t init:1; + }; + uint32_t all = 0; + } mode; + } ESP32BLE; + + void *beaconmutex = nullptr; + + #define ENDIAN_CHANGE_U16(x) ((((x)&0xFF00) >> 8) + (((x)&0xFF) << 8)) + +#else + + #include + + #define TMSBSIZ52 512 + + #define HM17_BAUDRATE 9600 + + #define IBEACON_DEBUG + + // use this for Version 110 + #define HM17_V110 + + TasmotaSerial *IBEACON_Serial = nullptr; + + uint8_t hm17_found,hm17_cmd,hm17_flag; + + #ifdef IBEACON_DEBUG + uint8_t hm17_debug=0; + #endif + + // 78 is max serial response + #define HM17_BSIZ 128 + char hm17_sbuffer[HM17_BSIZ]; + uint8_t hm17_sindex,hm17_result,hm17_scanning,hm17_connecting; + uint32_t hm17_lastms; + + enum {HM17_TEST,HM17_ROLE,HM17_IMME,HM17_DISI,HM17_IBEA,HM17_SCAN,HM17_DISC,HM17_RESET,HM17_RENEW,HM17_CON}; + #define HM17_SUCESS 99 + +#endif + +struct IBEACON { + char FACID[8]; + char UID[32]; + char MAJOR[4]; + char MINOR[4]; + char PWR[2]; + char MAC[12]; + char RSSI[4]; +#ifdef USE_IBEACON_ESP32 + char NAME[16]; +#endif +}; + +#ifdef USE_IBEACON_ESP32 + #define MAX_IBEACONS 32 +#else + #define MAX_IBEACONS 16 +#endif + +struct IBEACON_UID { + char MAC[12]; + char RSSI[4]; + char UID[32]; + char MAJOR[4]; + char MINOR[4]; + uint8_t FLAGS; + uint8_t TIME; +#ifdef USE_IBEACON_ESP32 + uint8_t REPORTED; + uint8_t REPTIME; + char NAME[16]; +#endif +} ibeacons[MAX_IBEACONS]; + +#ifdef USE_IBEACON_ESP32 + +uint32_t ibeacon_add(struct IBEACON *ib); + +void ESP32BLE_ReverseStr(uint8_t _mac[], uint8_t len=6){ + uint8_t _reversedMAC[len]; + for (uint8_t i=0; i>4) & 0xF]; + pout[1] = hex[ pgm_read_byte(pin) & 0xF]; + } +} + +int advertismentCallback(BLE_ESP32::ble_advertisment_t *pStruct) +{ + struct IBEACON ib; + BLEAdvertisedDevice *advertisedDevice = pStruct->advertisedDevice; + + char sRSSI[6]; + itoa(pStruct->RSSI,sRSSI,10); + const uint8_t *MAC = pStruct->addr; + + int manufacturerDataLen = 0; + std::string data; + if (advertisedDevice->haveManufacturerData()){ + data = advertisedDevice->getManufacturerData(); + manufacturerDataLen = data.length(); + } + if (manufacturerDataLen){ + const uint8_t *manufacturerData = (const uint8_t *)data.data(); + DumpHex(manufacturerData, 2, ib.FACID); + if (manufacturerDataLen == 25 && + manufacturerData[0] == 0x4C && + manufacturerData[1] == 0x00) + { + BLEBeacon oBeacon = BLEBeacon(); + oBeacon.setData(std::string((char *)manufacturerData, manufacturerDataLen)); + uint8_t UUID[16]; + memcpy(UUID,oBeacon.getProximityUUID().getNative()->u128.value,16); + ESP32BLE_ReverseStr(UUID,16); + + uint16_t Major = ENDIAN_CHANGE_U16(oBeacon.getMajor()); + uint16_t Minor = ENDIAN_CHANGE_U16(oBeacon.getMinor()); + uint8_t PWR = oBeacon.getSignalPower(); + + DumpHex((const unsigned char*)&UUID,16,ib.UID); + DumpHex((const unsigned char*)&Major,2,ib.MAJOR); + DumpHex((const unsigned char*)&Minor,2,ib.MINOR); + DumpHex((const unsigned char*)&PWR,1,ib.PWR); + DumpHex((const unsigned char*)MAC,6,ib.MAC); + memcpy(ib.RSSI,sRSSI,4); + memset(ib.NAME,0x0,16); + + // if we added it + if (ibeacon_add(&ib) == 1){ + AddLog_P(LOG_LEVEL_DEBUG, PSTR("%s: MAC: %s Major: %d Minor: %d UUID: %s Power: %d RSSI: %d"), + "iBeacon", + advertisedDevice->getAddress().toString().c_str(), + Major, Minor, + oBeacon.getProximityUUID().toString().c_str(), + PWR, pStruct->RSSI); + } + return 0; + } + } + + // no manufacturer data, or not recognised. + // still have an RSSi.... + memset(ib.UID,'0',32); + memset(ib.MAJOR,'0',4); + memset(ib.MINOR,'0',4); + memset(ib.PWR,'0',2); + DumpHex((const unsigned char*)MAC,6,ib.MAC); + memcpy(ib.RSSI,sRSSI,4); + + if (advertisedDevice->haveName()) { + strncpy(ib.NAME,advertisedDevice->getName().c_str(),16); + } else { + memset(ib.NAME,0x0,16); + } + + ibeacon_add(&ib); + return 0; +} + +void ESP32Init() { + + if (!ESP32BLE.mode.init) { + ESP32BLE.mode.init = 1; + IB_UPDATE_TIME=IB_UPDATE_TIME_INTERVAL; + IB_TIMEOUT_TIME=IB_TIMEOUT_INTERVAL; + } +} + + +#endif + +void IBEACON_Init() { + + +#ifdef USE_IBEACON_ESP32 + BLE_ESP32::registerForAdvertismentCallbacks((const char *)"iBeacon", advertismentCallback); +#else + + hm17_found=0; + +// actually doesnt work reliably with software serial + if (PinUsed(GPIO_IBEACON_RX) && PinUsed(GPIO_IBEACON_TX)) { + IBEACON_Serial = new TasmotaSerial(Pin(GPIO_IBEACON_RX), Pin(GPIO_IBEACON_TX),1,0,TMSBSIZ52); + if (IBEACON_Serial->begin(HM17_BAUDRATE)) { + if (IBEACON_Serial->hardwareSerial()) { + ClaimSerial(); + } + hm17_sendcmd(HM17_TEST); + hm17_lastms=millis(); + // in case of using Settings this has to be moved + IB_UPDATE_TIME=IB_UPDATE_TIME_INTERVAL; + IB_TIMEOUT_TIME=IB_TIMEOUT_INTERVAL; + } + } + +#endif + +} + +#ifdef USE_IBEACON_ESP32 + +void esp32_every_second(void) { + for (uint32_t cnt=0; cnt < MAX_IBEACONS; cnt++) { + if (ibeacons[cnt].FLAGS) { + uint8_t mac[6]; + char tmp[13]; + memcpy(tmp, ibeacons[cnt].MAC, 12); + tmp[12] = 0; + BLE_ESP32::fromHex(mac, tmp, 6); + // use global device timeouts from BLE_ESP32. + + uint32_t ageS = BLE_ESP32::devicePresent(mac); + + // if device not present at all. + if (!ageS){ + //AddLog_P(LOG_LEVEL_INFO, PSTR("iBeacon no device %s %02x%02x%02x%02x%02x%02x"),tmp, mac[0],mac[1], mac[2],mac[3], mac[4],mac[5]); + ibeacons[cnt].FLAGS=0; + ibeacon_mqtt(ibeacons[cnt].MAC,"0000",ibeacons[cnt].UID,ibeacons[cnt].MAJOR,ibeacons[cnt].MINOR,ibeacons[cnt].NAME); + } else { + //AddLog_P(LOG_LEVEL_INFO, PSTR("iBeacon device %s %02x%02x%02x%02x%02x%02x"),tmp, mac[0],mac[1], mac[2],mac[3], mac[4],mac[5]); + } + //ibeacons[cnt].TIME++; + ibeacons[cnt].REPTIME++; // counter used to send mqtt for a dev regularly + } + } +} + +#else + +void hm17_every_second(void) { + if (!IBEACON_Serial) return; + + if (hm17_found) { + if (IB_UPDATE_TIME && (TasmotaGlobal.uptime%IB_UPDATE_TIME==0)) { + if (hm17_cmd!=99) { + if (hm17_flag&2) { + ib_sendbeep(); + } else { + if (!hm17_connecting) { + hm17_sendcmd(HM17_DISI); + } + } + } + } + for (uint32_t cnt=0;cntIB_TIMEOUT_TIME) { + ibeacons[cnt].FLAGS=0; + ibeacon_mqtt(ibeacons[cnt].MAC,"0000",ibeacons[cnt].UID,ibeacons[cnt].MAJOR,ibeacons[cnt].MINOR); + } + } + } + } else { + if (TasmotaGlobal.uptime%20==0) { + hm17_sendcmd(HM17_TEST); + } + } +} + +void hm17_sbclr(void) { + memset(hm17_sbuffer,0,HM17_BSIZ); + hm17_sindex=0; + //IBEACON_Serial->flush(); +} + +void hm17_sendcmd(uint8_t cmd) { + hm17_sbclr(); + hm17_cmd=cmd; +#ifdef IBEACON_DEBUG + if (hm17_debug) AddLog_P(LOG_LEVEL_INFO, PSTR("hm17cmd %d"),cmd); +#endif + switch (cmd) { + case HM17_TEST: + IBEACON_Serial->write("AT"); + break; + case HM17_ROLE: + IBEACON_Serial->write("AT+ROLE1"); + break; + case HM17_IMME: + IBEACON_Serial->write("AT+IMME1"); + break; + case HM17_DISI: + IBEACON_Serial->write("AT+DISI?"); + hm17_scanning=1; + break; + case HM17_IBEA: + IBEACON_Serial->write("AT+IBEA1"); + break; + case HM17_RESET: + IBEACON_Serial->write("AT+RESET"); + break; + case HM17_RENEW: + IBEACON_Serial->write("AT+RENEW"); + break; + case HM17_SCAN: + IBEACON_Serial->write("AT+SCAN5"); + break; + case HM17_DISC: + IBEACON_Serial->write("AT+DISC?"); + hm17_scanning=1; + break; + case HM17_CON: + IBEACON_Serial->write((const uint8_t*)"AT+CON",6); + IBEACON_Serial->write((const uint8_t*)ib_mac,12); + hm17_connecting=1; + break; + } +} + +#endif + +uint32_t ibeacon_add(struct IBEACON *ib) { +/* if (!strncmp(ib->MAJOR,"4B1C",4)) { + return 0; + } + */ + if (!strncmp(ib->RSSI,"0",1)) { + return 0; + } + + // don't bother protecting this. + //TasAutoMutex localmutex(&beaconmutex, "iBeacAdd"); + + // keyfob starts with ffff, ibeacon has valid facid + if (!strncmp(ib->MAC,"FFFF",4) || strncmp(ib->FACID,"00000000",8)) { + for (uint32_t cnt=0;cntUID,PSTR("00000000000000000000000000000000"),32)) { + if (!strncmp(ibeacons[cnt].MAC,ib->MAC,12)) { + // exists + memcpy(ibeacons[cnt].RSSI,ib->RSSI,4); + ibeacons[cnt].TIME=0; +#ifdef USE_IBEACON_ESP32 + if (ibeacons[cnt].REPTIME >= IB_UPDATE_TIME) { + ibeacons[cnt].REPTIME = 0; + ibeacons[cnt].REPORTED = 0; + } +#endif + return 2; + } + } else { + if (!strncmp(ibeacons[cnt].UID,ib->UID,32)) { + // exists + memcpy(ibeacons[cnt].RSSI,ib->RSSI,4); + ibeacons[cnt].TIME=0; +#ifdef USE_IBEACON_ESP32 + if (ibeacons[cnt].REPTIME >= IB_UPDATE_TIME) { + ibeacons[cnt].REPTIME = 0; + ibeacons[cnt].REPORTED = 0; + } +#endif + return 2; + } + } + } + } + for (uint32_t cnt=0;cntMAC,12); + memcpy(ibeacons[cnt].RSSI,ib->RSSI,4); + memcpy(ibeacons[cnt].UID,ib->UID,32); + memcpy(ibeacons[cnt].MAJOR,ib->MAJOR,4); + memcpy(ibeacons[cnt].MINOR,ib->MINOR,4); + ibeacons[cnt].FLAGS=1; + ibeacons[cnt].TIME=0; +#ifdef USE_IBEACON_ESP32 + memcpy(ibeacons[cnt].NAME,ib->NAME,16); + ibeacons[cnt].REPTIME = 0; + ibeacons[cnt].REPORTED = 0; +#endif + return 1; + } + } + } + return 0; +} + +#ifndef USE_IBEACON_ESP32 + +void hm17_decode(void) { + struct IBEACON ib; + switch (hm17_cmd) { + case HM17_TEST: + if (!strncmp(hm17_sbuffer,"OK",2)) { +#ifdef IBEACON_DEBUG + if (hm17_debug) AddLog_P(LOG_LEVEL_INFO, PSTR("AT OK")); +#endif + hm17_sbclr(); + hm17_result=HM17_SUCESS; + hm17_found=1; + } + break; + case HM17_ROLE: + if (!strncmp(hm17_sbuffer,"OK+Set:1",8)) { +#ifdef IBEACON_DEBUG + if (hm17_debug) AddLog_P(LOG_LEVEL_INFO, PSTR("ROLE OK")); +#endif + hm17_sbclr(); + hm17_result=HM17_SUCESS; + } + break; + case HM17_IMME: + if (!strncmp(hm17_sbuffer,"OK+Set:1",8)) { +#ifdef IBEACON_DEBUG + if (hm17_debug) AddLog_P(LOG_LEVEL_INFO, PSTR("IMME OK")); +#endif + hm17_sbclr(); + hm17_result=HM17_SUCESS; + } + break; + case HM17_IBEA: + if (!strncmp(hm17_sbuffer,"OK+Set:1",8)) { +#ifdef IBEACON_DEBUG + if (hm17_debug) AddLog_P(LOG_LEVEL_INFO, PSTR("IBEA OK")); +#endif + hm17_sbclr(); + hm17_result=HM17_SUCESS; + } + break; + case HM17_SCAN: + if (!strncmp(hm17_sbuffer,"OK+Set:5",8)) { +#ifdef IBEACON_DEBUG + if (hm17_debug) AddLog_P(LOG_LEVEL_INFO, PSTR("SCAN OK")); +#endif + hm17_sbclr(); + hm17_result=HM17_SUCESS; + } + break; + case HM17_RESET: + if (!strncmp(hm17_sbuffer,"OK+RESET",8)) { +#ifdef IBEACON_DEBUG + if (hm17_debug) AddLog_P(LOG_LEVEL_INFO, PSTR("RESET OK")); +#endif + hm17_sbclr(); + hm17_result=HM17_SUCESS; + } + break; + case HM17_RENEW: + if (!strncmp(hm17_sbuffer,"OK+RENEW",8)) { +#ifdef IBEACON_DEBUG + if (hm17_debug) AddLog_P(LOG_LEVEL_INFO, PSTR("RENEW OK")); +#endif + hm17_sbclr(); + hm17_result=HM17_SUCESS; + } + break; + case HM17_CON: + if (!strncmp(hm17_sbuffer,"OK+CONNA",8)) { + hm17_sbclr(); +#ifdef IBEACON_DEBUG + if (hm17_debug) AddLog_P(LOG_LEVEL_INFO, PSTR("CONNA OK")); +#endif + hm17_connecting=2; + break; + } + if (!strncmp(hm17_sbuffer,"OK+CONNE",8)) { + hm17_sbclr(); +#ifdef IBEACON_DEBUG + if (hm17_debug) AddLog_P(LOG_LEVEL_INFO, PSTR("CONNE ERROR")); +#endif + break; + } + if (!strncmp(hm17_sbuffer,"OK+CONNF",8)) { + hm17_sbclr(); +#ifdef IBEACON_DEBUG + if (hm17_debug) AddLog_P(LOG_LEVEL_INFO, PSTR("CONNF ERROR")); +#endif + break; + } + if (hm17_connecting==2 && !strncmp(hm17_sbuffer,"OK+CONN",7)) { + hm17_sbclr(); +#ifdef IBEACON_DEBUG + if (hm17_debug) AddLog_P(LOG_LEVEL_INFO, PSTR("CONN OK")); +#endif + hm17_connecting=3; + hm17_sendcmd(HM17_TEST); + hm17_connecting=0; + break; + } + break; + + case HM17_DISI: + case HM17_DISC: + if (!strncmp(hm17_sbuffer,"OK+DISCS",8)) { + hm17_sbclr(); + hm17_result=1; +#ifdef IBEACON_DEBUG + if (hm17_debug) AddLog_P(LOG_LEVEL_INFO, PSTR("DISCS OK")); +#endif + break; + } + if (!strncmp(hm17_sbuffer,"OK+DISIS",8)) { + hm17_sbclr(); + hm17_result=1; +#ifdef IBEACON_DEBUG + if (hm17_debug) AddLog_P(LOG_LEVEL_INFO, PSTR("DISIS OK")); +#endif + break; + } + if (!strncmp(hm17_sbuffer,"OK+DISCE",8)) { + hm17_sbclr(); + hm17_result=HM17_SUCESS; +#ifdef IBEACON_DEBUG + if (hm17_debug) AddLog_P(LOG_LEVEL_INFO, PSTR("DISCE OK")); +#endif + hm17_scanning=0; + break; + } + if (!strncmp(hm17_sbuffer,"OK+NAME:",8)) { + if (hm17_sbuffer[hm17_sindex-1]=='\n') { + hm17_result=HM17_SUCESS; +#ifdef IBEACON_DEBUG + if (hm17_debug) { + AddLog_P(LOG_LEVEL_INFO, PSTR("NAME OK")); + AddLog_P(LOG_LEVEL_INFO, PSTR(">>%s"),&hm17_sbuffer[8]); + } +#endif + hm17_sbclr(); + } + break; + } + if (!strncmp(hm17_sbuffer,"OK+DIS0:",8)) { + if (hm17_cmd==HM17_DISI) { +#ifdef HM17_V110 + goto hm17_v110; +#endif + } else { + if (hm17_sindex==20) { + hm17_result=HM17_SUCESS; +#ifdef IBEACON_DEBUG + if (hm17_debug) { + AddLog_P(LOG_LEVEL_INFO, PSTR("DIS0 OK")); + AddLog_P(LOG_LEVEL_INFO, PSTR(">>%s"),&hm17_sbuffer[8]); + } +#endif + hm17_sbclr(); + } + } + break; + } + if (!strncmp(hm17_sbuffer,"OK+DISC:",8)) { +hm17_v110: + if (hm17_cmd==HM17_DISI) { + if (hm17_sindex==78) { +#ifdef IBEACON_DEBUG + if (hm17_debug) { + AddLog_P(LOG_LEVEL_INFO, PSTR("DISC: OK")); + //OK+DISC:4C 000C0E:003 A9144081A8 3B16849611 862EC1005: 0B1CE7485D :4DB4E940F C0E:-078 + AddLog_P(LOG_LEVEL_INFO, PSTR(">>%s"),&hm17_sbuffer[8]); + } +#endif + memcpy(ib.FACID,&hm17_sbuffer[8],8); + memcpy(ib.UID,&hm17_sbuffer[8+8+1],32); + memcpy(ib.MAJOR,&hm17_sbuffer[8+8+1+32+1],4); + memcpy(ib.MINOR,&hm17_sbuffer[8+8+1+32+1+4],4); + memcpy(ib.PWR,&hm17_sbuffer[8+8+1+32+1+4+4],2); + memcpy(ib.MAC,&hm17_sbuffer[8+8+1+32+1+4+4+2+1],12); + memcpy(ib.RSSI,&hm17_sbuffer[8+8+1+32+1+4+4+2+1+12+1],4); + + if (ibeacon_add(&ib)) { + ibeacon_mqtt(ib.MAC,ib.RSSI,ib.UID,ib.MAJOR,ib.MINOR); + } + hm17_sbclr(); + hm17_result=1; + } + } else { +#ifdef IBEACON_DEBUG + if (hm17_debug) AddLog_P(LOG_LEVEL_INFO, PSTR(">->%s"),&hm17_sbuffer[8]); +#endif + } + break; + } + } +} + +#endif + +void IBEACON_loop() { + +#ifdef USE_IBEACON_ESP32 + //TasAutoMutex localmutex(&beaconmutex, "iBeacLoop"); + for (uint32_t cnt=0;cntavailable()) { + hm17_lastms=millis(); + // shift in + if (hm17_sindexread(); + hm17_sindex++; + hm17_decode(); + } else { + hm17_sindex=0; + break; + } + } + + if (hm17_cmd==99) { + if (hm17_sindex>=HM17_BSIZ-2 || (hm17_sindex && (difftime>100))) { + AddLog_P(LOG_LEVEL_INFO, PSTR("%s"),hm17_sbuffer); + hm17_sbclr(); + } + } + +#endif + +} + +#ifdef USE_WEBSERVER +const char HTTP_IBEACON_HL[] PROGMEM = "{s}
{m}
{e}"; +const char HTTP_IBEACON_mac[] PROGMEM = + "{s}IBEACON-MAC : %s" " {m} RSSI : %s" "{e}"; +const char HTTP_IBEACON_uid[] PROGMEM = + "{s}IBEACON-UID : %s" " {m} RSSI : %s" "{e}"; +#ifdef USE_IBEACON_ESP32 +const char HTTP_IBEACON_name[] PROGMEM = + "{s}IBEACON-NAME : %s (%s)" " {m} RSSI : %s" "{e}"; +#endif +void IBEACON_Show(void) { + char mac[14]; + char rssi[6]; + char uid[34]; +#ifdef USE_IBEACON_ESP32 + char name[18]; + //TasAutoMutex localmutex(&beaconmutex, "iBeacShow"); +#endif + int total = 0; + + for (uint32_t cnt=0;cnt 0) { + char *cp=XdrvMailbox.data; + if (*cp=='u') { + cp++; + if (*cp) IB_UPDATE_TIME=atoi(cp); + Response_P(S_JSON_IBEACON, XSNS_52,"uintv",IB_UPDATE_TIME); + } else if (*cp=='t') { + cp++; + if (*cp) IB_TIMEOUT_TIME=atoi(cp); + Response_P(S_JSON_IBEACON, XSNS_52,"lintv",IB_TIMEOUT_TIME); + } else if (*cp=='c') { + for (uint32_t cnt=0;cnt='0' && *cp<='8') { + hm17_sendcmd(*cp&7); + Response_P(S_JSON_IBEACON, XSNS_52,"hm17cmd",*cp&7); + } else if (*cp=='s') { + cp++; + len--; + while (*cp==' ') { + len--; + cp++; + } + IBEACON_Serial->write((uint8_t*)cp,len); + hm17_cmd=99; + Response_P(S_JSON_IBEACON1, XSNS_52,"hm17cmd",cp); + } +#endif +#ifdef IBEACON_DEBUG + else if (*cp=='d') { + cp++; + hm17_debug=atoi(cp); + Response_P(S_JSON_IBEACON, XSNS_52,"debug",hm17_debug); + } +#endif + } else { + serviced=false; + } + return serviced; +} + +#define D_CMND_IBEACON "IBEACON" + +#ifndef USE_IBEACON_ESP32 +//"IBEACON_FFFF3D1B1E9D_RSSI", Data "99" causes TAG to beep +bool ibeacon_cmd(void) { + ib_mac[0]=0; + int16_t rssi=0; + const char S_JSON_IBEACON[] = "{\"" D_CMND_IBEACON "_%s_RSSI\":%d}"; + uint8_t cmd_len = strlen(D_CMND_IBEACON); + if (!strncasecmp_P(XdrvMailbox.topic, PSTR(D_CMND_IBEACON), cmd_len)) { + // IBEACON prefix + rssi = XdrvMailbox.payload; + if (rssi==99) { + memcpy(ib_mac,XdrvMailbox.topic+cmd_len+1,12); + ib_mac[12]=0; + if (hm17_scanning) { + // postpone sendbeep + hm17_flag|=2; + } else { + ib_sendbeep(); + } + } + Response_P(S_JSON_IBEACON,ib_mac,rssi); + return true; + } + return false; +} + +void ib_sendbeep(void) { + hm17_flag=0; + hm17_sendcmd(HM17_CON); +} + +#endif + +#ifdef USE_IBEACON_ESP32 +void ibeacon_mqtt(const char *mac,const char *rssi,const char *uid,const char *major,const char *minor, const char *name) { +#else +void ibeacon_mqtt(const char *mac,const char *rssi,const char *uid,const char *major,const char *minor) { +#endif + char s_mac[14]; + char s_uid[34]; + char s_major[6]; + char s_minor[6]; + char s_rssi[6]; +#ifdef USE_IBEACON_ESP32 + char *s_state; +#endif + char s_name[18]; + memcpy(s_mac,mac,12); + s_mac[12]=0; + memcpy(s_uid,uid,32); + s_uid[32]=0; + memcpy(s_major,major,4); + s_major[4]=0; + memcpy(s_minor,minor,4); + s_minor[4]=0; + memcpy(s_rssi,rssi,4); + s_rssi[4]=0; + int16_t n_rssi=atoi(s_rssi); +#ifdef USE_IBEACON_ESP32 + if (n_rssi) { + s_state=(char *)"ON"; + } else { + s_state=(char *)"OFF"; + } +#endif + // if uid == all zeros, take mac + if (!strncmp_P(s_uid,PSTR("00000000000000000000000000000000"),32)) { +#ifdef USE_IBEACON_ESP32 + if (name[0]) { + memcpy(s_name,name,16); + s_name[16]=0; + ResponseTime_P(PSTR(",\"" D_CMND_IBEACON "\":{\"MAC\":\"%s\",\"NAME\":\"%s\",\"RSSI\":%d,\"STATE\":\"%s\"}}"),s_mac,s_name,n_rssi,s_state); + } else { + ResponseTime_P(PSTR(",\"" D_CMND_IBEACON "\":{\"MAC\":\"%s\",\"RSSI\":%d,\"STATE\":\"%s\"}}"),s_mac,n_rssi,s_state); + } +#else + ResponseTime_P(PSTR(",\"" D_CMND_IBEACON "\":{\"MAC\":\"%s\",\"UID\":\"%s\",\"MAJOR\":\"%s\",\"MINOR\":\"%s\",\"RSSI\":%d}}"),s_mac,s_uid,s_major,s_minor,n_rssi); +#endif + } else { +#ifdef USE_IBEACON_ESP32 + ResponseTime_P(PSTR(",\"" D_CMND_IBEACON "\":{\"UID\":\"%s\",\"MAJOR\":\"%s\",\"MINOR\":\"%s\",\"MAC\":\"%s\",\"RSSI\":%d,\"STATE\":\"%s\"}}"),s_uid,s_major,s_minor,s_mac,n_rssi,s_state); +#else + ResponseTime_P(PSTR(",\"" D_CMND_IBEACON "\":{\"UID\":\"%s\",\"MAJOR\":\"%s\",\"MINOR\":\"%s\",\"MAC\":\"%s\",\"RSSI\":%d}}"),s_uid,s_major,s_minor,s_mac,n_rssi); +#endif + } + + MqttPublishTeleSensor(); +} + + +/*********************************************************************************************\ + * Interface +\*********************************************************************************************/ + +bool Xsns52(byte function) +{ + bool result = false; + + switch (function) { + case FUNC_INIT: + IBEACON_Init(); + break; +#ifdef USE_IBEACON_ESP32 + case FUNC_EVERY_250_MSECOND: + if (!ESP32BLE.mode.init) { + ESP32Init(); + } + break; +#endif + case FUNC_LOOP: + IBEACON_loop(); + break; + case FUNC_EVERY_SECOND: +#ifdef USE_IBEACON_ESP32 + esp32_every_second(); +#else + hm17_every_second(); +#endif + break; + case FUNC_COMMAND_SENSOR: + if (XSNS_52 == XdrvMailbox.index) { + result = xsns52_cmd(); + } + break; +#ifndef USE_IBEACON_ESP32 + case FUNC_COMMAND: + result=ibeacon_cmd(); + break; +#endif +#ifdef USE_WEBSERVER + case FUNC_WEB_SENSOR: +#ifndef USE_IBEACON_ESP32 + if (hm17_found) IBEACON_Show(); +#else + IBEACON_Show(); +#endif + break; +#endif // USE_WEBSERVER + } + return result; +} + +#endif // USE_IBEACON + +#endif \ No newline at end of file diff --git a/tasmota/xsns_62_MI_ESP32.ino b/tasmota/xsns_62_MI_ESP32.ino index 689b5a72a..af24ec083 100644 --- a/tasmota/xsns_62_MI_ESP32.ino +++ b/tasmota/xsns_62_MI_ESP32.ino @@ -45,6 +45,7 @@ forked - from arendst/tasmota - https://github.com/arendst/Tasmota */ +#ifndef USE_BLE_ESP32 #ifdef ESP32 // ESP32 only. Use define USE_HM10 for ESP8266 support #ifdef USE_MI_ESP32 @@ -589,17 +590,17 @@ int MI32_decryptPacket(char *_buf, uint16_t _bufSize, uint32_t _type){ MI32_ReverseMAC(packet->MAC); uint8_t _bindkey[16] = {0x0}; bool foundNoKey = true; - AddLog_P(LOG_LEVEL_DEBUG,PSTR("M32: Search key for MAC: %02x %02x %02x %02x %02x %02x"), packet->MAC[0], packet->MAC[1], packet->MAC[2], packet->MAC[3], packet->MAC[4], packet->MAC[5]); + AddLog_P(LOG_LEVEL_DEBUG,PSTR("MI32: search key for MAC: %02x %02x %02x %02x %02x %02x"), packet->MAC[0], packet->MAC[1], packet->MAC[2], packet->MAC[3], packet->MAC[4], packet->MAC[5]); for(uint32_t i=0; iMAC,MIBLEbindKeys[i].MAC,sizeof(packet->MAC))==0){ memcpy(_bindkey,MIBLEbindKeys[i].key,sizeof(_bindkey)); - AddLog_P(LOG_LEVEL_DEBUG,PSTR("M32: Decryption Key found")); + AddLog_P(LOG_LEVEL_DEBUG,PSTR("MI32: decryption Key found")); foundNoKey = false; break; } } if(foundNoKey){ - AddLog_P(LOG_LEVEL_DEBUG,PSTR("M32: No Key found !!")); + AddLog_P(LOG_LEVEL_DEBUG,PSTR("MI32: no Key found !!")); return -2; } @@ -619,7 +620,7 @@ int MI32_decryptPacket(char *_buf, uint16_t _bufSize, uint32_t _type){ ret = br_ccm_check_tag(&ctx, &tag); - AddLog_P(LOG_LEVEL_DEBUG,PSTR("M32: Err:%i, Decrypted : %02x %02x %02x %02x %02x "), ret, packet->payload[1],packet->payload[2],packet->payload[3],packet->payload[4],packet->payload[5]); + AddLog_P(LOG_LEVEL_DEBUG,PSTR("MI32: Err:%i, Decrypted : %02x %02x %02x %02x %02x "), ret, packet->payload[1],packet->payload[2],packet->payload[3],packet->payload[4],packet->payload[5]); return ret-1; } #endif // USE_MI_DECRYPTION @@ -660,7 +661,7 @@ uint32_t MIBLEgetSensorSlot(uint8_t (&_MAC)[6], uint16_t _type, uint8_t counter) bool _success = false; for (uint32_t i=0;i19) { - AddLog_P(LOG_LEVEL_INFO,PSTR("M32: Scan buffer full")); + AddLog_P(LOG_LEVEL_INFO,PSTR("MI32: Scan buffer full")); MI32.state.beaconScanCounter = 1; return; } for(auto _scanResult : MIBLEscanResult){ if(memcmp(addr,_scanResult.MAC,6)==0){ - // AddLog_P(LOG_LEVEL_INFO,PSTR("M32: known device")); + // AddLog_P(LOG_LEVEL_INFO,PSTR("MI32: known device")); return; } } @@ -1582,12 +1586,12 @@ void MI32addBeacon(uint8_t index, char* data){ _new.time = 0; if(memcmp(_empty,_new.MAC,6) == 0){ _new.active = false; - AddLog_P(LOG_LEVEL_INFO,PSTR("M32: Beacon%u deactivated"), index); + AddLog_P(LOG_LEVEL_INFO,PSTR("MI32: beacon%u deactivated"), index); } else{ _new.active = true; MI32.mode.activeBeacon = 1; - AddLog_P(LOG_LEVEL_INFO,PSTR("M32: Beacon added with MAC: %s"), _MAC); + AddLog_P(LOG_LEVEL_INFO,PSTR("MI32: beacon added with MAC: %s"), _MAC); } } @@ -1842,7 +1846,7 @@ void CmndMi32Time(void) { if (XdrvMailbox.data_len > 0) { if (MIBLEsensors.size() > XdrvMailbox.payload) { if ((LYWSD02 == MIBLEsensors[XdrvMailbox.payload].type) || (MHOC303 == MIBLEsensors[XdrvMailbox.payload].type)) { - AddLog_P(LOG_LEVEL_DEBUG, PSTR("M32: Will set Time")); + AddLog_P(LOG_LEVEL_DEBUG, PSTR("MI32: will set Time")); MI32.state.sensor = XdrvMailbox.payload; MI32.mode.canScan = 0; MI32.mode.canConnect = 0; @@ -1872,7 +1876,7 @@ void CmndMi32Unit(void) { if (XdrvMailbox.data_len > 0) { if (MIBLEsensors.size() > XdrvMailbox.payload) { if ((LYWSD02 == MIBLEsensors[XdrvMailbox.payload].type) || (MHOC303 == MIBLEsensors[XdrvMailbox.payload].type)) { - AddLog_P(LOG_LEVEL_DEBUG,PSTR("M32: Will set Unit")); + AddLog_P(LOG_LEVEL_DEBUG,PSTR("MI32: will set Unit")); MI32.state.sensor = XdrvMailbox.payload; MI32.mode.canScan = 0; MI32.mode.canConnect = 0; @@ -1922,11 +1926,11 @@ void CmndMi32Block(void){ switch (XdrvMailbox.index) { case 0: MIBLEBlockList.clear(); - // AddLog_P(LOG_LEVEL_INFO,PSTR("M32: Size of ilist: %u"), MIBLEBlockList.size()); - ResponseCmndIdxChar(PSTR("Block list cleared")); + // AddLog_P(LOG_LEVEL_INFO,PSTR("MI32: size of ilist: %u"), MIBLEBlockList.size()); + ResponseCmndIdxChar(PSTR("block list cleared")); break; case 1: - ResponseCmndIdxChar(PSTR("Show block list")); + ResponseCmndIdxChar(PSTR("show block list")); break; } } @@ -1952,7 +1956,7 @@ void CmndMi32Block(void){ ResponseCmndIdxChar(XdrvMailbox.data); MI32removeMIBLEsensor(_MACasBytes.buf); } - // AddLog_P(LOG_LEVEL_INFO,PSTR("M32: Size of ilist: %u"), MIBLEBlockList.size()); + // AddLog_P(LOG_LEVEL_INFO,PSTR("MI32: size of ilist: %u"), MIBLEBlockList.size()); break; } } @@ -2319,3 +2323,4 @@ bool Xsns62(uint8_t function) } #endif // USE_MI_ESP32 #endif // ESP32 +#endif \ No newline at end of file diff --git a/tasmota/xsns_62_MI_ESP32_BLE_ESP32.ino b/tasmota/xsns_62_MI_ESP32_BLE_ESP32.ino new file mode 100644 index 000000000..832aae2e7 --- /dev/null +++ b/tasmota/xsns_62_MI_ESP32_BLE_ESP32.ino @@ -0,0 +1,2742 @@ +/* + xsns_62_MI_ESP32.ino - MI-BLE-sensors via ESP32 support for Tasmota + + Copyright (C) 2020 Christian Baars and Theo Arends + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + + + -------------------------------------------------------------------------------------------- + Version yyyymmdd Action Description + -------------------------------------------------------------------------------------------- + 0.9.1.9 20201226 changed - All change now. + ------- + 0.9.1.7 20201116 changed - small bugfixes, add BLOCK and OPTION command, send BLE scan via MQTT + ------- + 0.9.1.6 20201022 changed - Beacon support, RSSI at TELEPERIOD, refactoring + ------- + 0.9.1.5 20201021 changed - HASS related ('null', hold back discovery), number of found sensors for RULES + ------- + 0.9.1.4 20201020 changed - use BearSSL for decryption, revert to old TELEPERIOD-cycle as default + ------- + 0.9.1.3 20200926 changed - Improve HA discovery, make key+MAC case insensitive + ------- + 0.9.1.3 20200916 changed - add ATC (custom FW for LYWSD03MMC), API adaption for NimBLE-Arduino 1.0.2 + ------- + 0.9.1.2 20200802 changed - add MHO-C303 + ------- + 0.9.1.1 20200715 changed - add MHO-C401, refactoring + ------- + 0.9.1.0 20200712 changed - add lights and yeerc, add pure passive mode with decryption, + lots of refactoring + ------- + 0.9.0.1 20200706 changed - adapt to new NimBLE-API, tweak scan process + ------- + 0.9.0.0 20200413 started - initial development by Christian Baars + forked - from arendst/tasmota - https://github.com/arendst/Tasmota + +*/ +//#define VSCODE_DEV + +/* +#ifdef VSCODE_DEV +#define ESP32 +#define USE_BLE_ESP32 +#define USE_MI_ESP32 +#endif +*/ +//#undef USE_MI_ESP32 + +// for testing of BLE_ESP32, we remove xsns_62_MI_ESP32.ino completely, and instead add this modified xsns_52_ibeacon_BLE_ESP32.ino +#ifdef USE_BLE_ESP32 + +#ifdef ESP32 // ESP32 only. Use define USE_HM10 for ESP8266 support + +#ifdef USE_MI_ESP32 + +#define XSNS_62 62 +#define USE_MI_DECRYPTION + +#include +#ifdef USE_MI_DECRYPTION +#include +#endif //USE_MI_DECRYPTION + +void MI32scanEndedCB(NimBLEScanResults results); +void MI32notifyCB(NimBLERemoteCharacteristic* pRemoteCharacteristic, uint8_t* pData, size_t length, bool isNotify); + +struct { + uint16_t perPage = 4; + uint8_t mqttCurrentSlot = 0; + uint32_t period; // set manually in addition to TELE-period, is set to TELE-period after start + int secondsCounter = 0; // counts up in MI32EverySecond to period + int secondsCounter2 = 0; // counts up in MI32EverySecond to period + union { + struct { + uint32_t init:1; + uint32_t shallClearResults:1; // BLE scan results + uint32_t shallShowStatusInfo:1; // react to amount of found sensors via RULES + uint32_t firstAutodiscoveryDone:1; + uint32_t shallTriggerTele:1; + uint32_t triggeredTele:1; + }; + uint32_t all = 0; + } mode; + + struct { + // the slot currently having it's battery read + // set to 0 to start a battery read cycle + uint8_t slot = 255; + uint8_t active = 0; + } batteryreader; + + struct { + // the slot currently having it's battery read + // set to 0 to start a battery read cycle + uint8_t slot = 255; + uint8_t active = 0; + } sensorreader; + + struct { + uint32_t allwaysAggregate:1; // always show all known values of one sensor in brdigemode + uint32_t noSummary:1; // no sensor values at TELE-period + uint32_t directBridgeMode:1; // send every received BLE-packet as a MQTT-message in real-time + uint32_t holdBackFirstAutodiscovery:1; // allows to trigger it later + uint32_t showRSSI:1; + uint32_t ignoreBogusBattery:1; + uint32_t minimalSummary:1; // DEPRECATED!! + } option; +} MI32; + +#pragma pack(1) // byte-aligned structures to read the sensor data + + struct { + int16_t temp; + uint8_t hum; + uint16_t volt; // LYWSD03 only + } LYWSD0x_HT; + struct { + uint8_t spare; + int16_t temp; + uint16_t hum; + } CGD1_HT; + struct { + int16_t temp; + uint8_t spare; + uint32_t lux; + uint8_t moist; + uint16_t fert; + } Flora_TLMF; // temperature, lux, moisture, fertility + + +//////////////////////////////////////////////////////////// +// from https://github.com/Magalex2x14/LYWSD03MMC-info +struct mi_beacon_frame_data_t{ + // data from byte 0 - e.g. 30 + uint8_t meshflag; //Byte 0: x....... + uint8_t dataflag; //Byte 0: .x...... + uint8_t compatibilityflag; //Byte 0: ..x..... - indicates compatibility data present + uint8_t MACFlag; //Byte 0: ...x.... + uint8_t isencrypted; //Byte 0: ....x... + uint8_t reserved; //Byte 0: .....xxx + + // data from byte 1 - e.g. 58 + uint8_t version; //Byte 0: xxxx.... + uint8_t authMode; //Byte 0: ....xx.. // e.g. 2 + uint8_t bindingvalidreq; //Byte 0: ......x. + uint8_t registeredflag; //Byte 0: .......x +}; + +struct mi_beacon_compatibility_data_t{ // e.g. 28/08 + uint8_t reserved; //Byte 0: xx...... + uint8_t IOcap; //Byte 0: ..x..... + uint8_t bondability; //Byte 0: ...xx... + uint8_t unused; //Byte 0: .....xxx + uint16_t IOCapability; // bytes 1-2, e.g. 01 00 -> 0001 +}; +struct mi_beacon_mac_data_t{ // e.g. 28/08 + uint8_t mac[6]; +}; +struct mi_beacon_payload_data_t{ // + uint8_t type; + uint8_t ten; + uint8_t size; + uint8_t data[16]; +}; + +struct mi_beacon_data_t { // + mi_beacon_frame_data_t framedata; + uint16_t devicetype; + uint8_t framecnt; + mi_beacon_mac_data_t macdata; + mi_beacon_compatibility_data_t compatibility; + uint8_t payloadpresent; + uint8_t needkey; // we need a (new) encryption key? + + mi_beacon_payload_data_t payload; +}; + +struct mi_beacon_data_payload_data_t { // + union { + struct{ //01 + uint16_t num; + uint8_t longPress; + } Btn; + + int16_t temp; //04 + uint16_t hum; //06 + uint32_t lux; //07 + uint8_t moist; //08 + uint16_t fert; //09 + uint8_t bat; //0a + struct{ //0d + int16_t temp; + uint16_t hum; + } HT; + uint32_t NMT; //17 + }; +}; + + + +/////////////////////////////////////////////////////////// + + + +union mi_bindKey_t{ + struct{ + uint8_t key[16]; + uint8_t MAC[6]; + }; + uint8_t buf[22]; +}; + +struct ATCPacket_t{ + //uint8_t size; // = 16? + //uint8_t uid; // = 0x16, 16-bit UUID + //uint16_t UUID; // = 0x181A, GATT Service 0x181A Environmental Sensing + uint8_t MAC[6]; // [0] - hi, .. [6] - lo digits + uint16_t temp; //sadly this is in wrong endianess + uint8_t hum; + uint8_t batPer; + uint16_t batMV; + uint8_t frameCnt; +}; + +// GATT Service 0x181A Environmental Sensing +// All data little-endian +struct PVVXPacket_t { + //uint8_t size; // = 19 + //uint8_t uid; // = 0x16, 16-bit UUID + //uint16_t UUID; // = 0x181A, GATT Service 0x181A Environmental Sensing + uint8_t MAC[6]; // [0] - lo, .. [6] - hi digits + int16_t temperature; // x 0.1 degree + uint16_t humidity; // x 0.01 % + uint16_t battery_mv; // mV + uint8_t battery_level; // 0..100 % + uint8_t counter; // measurement count + uint8_t flags; +}; + +#pragma pack(0) + +struct mi_sensor_t{ + uint8_t type; //MI_Flora = 1; MI_MI-HT_V1=2; MI_LYWSD02=3; MI_LYWSD03=4; MI_CGG1=5; MI_CGD1=6 + uint8_t needkey; // tells http to display needkey message with link + uint8_t lastCnt; //device generated counter of the packet + uint8_t shallSendMQTT; + uint8_t MAC[6]; + union { + struct { + uint32_t temp:1; + uint32_t hum:1; + uint32_t tempHum:1; //every hum sensor has temp too, easier to use Tasmota dew point functions + uint32_t lux:1; + uint32_t moist:1; + uint32_t fert:1; + uint32_t bat:1; + uint32_t NMT:1; + uint32_t PIR:1; + uint32_t Btn:1; + }; + uint32_t raw; + } feature; + union { + struct { + uint32_t temp:1; + uint32_t hum:1; + uint32_t tempHum:1; //can be combined from the sensor + uint32_t lux:1; + uint32_t moist:1; + uint32_t fert:1; + uint32_t bat:1; + uint32_t NMT:1; + uint32_t motion:1; + uint32_t noMotion:1; + uint32_t Btn:1; + uint32_t PairBtn:1; + }; + uint32_t raw; + } eventType; + + int RSSI; + uint8_t pairing; + uint32_t lastTime; + uint32_t lux; + float temp; //Flora, MJ_HT_V1, LYWSD0x, CGx + union { + struct { + uint8_t moisture; + uint16_t fertility; + char firmware[6]; // actually only for FLORA but hopefully we can add for more devices + }; // Flora + struct { + float hum; + }; // MJ_HT_V1, LYWSD0x + struct { + uint16_t events; //"alarms" since boot + uint32_t NMT; // no motion time in seconds for the MJYD2S + }; + uint16_t Btn; + }; + union { + uint8_t bat; // many values seem to be hard-coded garbage (LYWSD0x, GCD1) + }; +}; + +struct MAC_t { + uint8_t buf[6]; +}; + +std::vector MIBLEsensors; +std::vector MIBLEbindKeys; +std::vector MIBLEBlockList; + +void *slotmutex = nullptr; + +/*********************************************************************************************\ + * constants +\*********************************************************************************************/ + +#define D_CMND_MI32 "MI32" + +const char kMI32_Commands[] PROGMEM = D_CMND_MI32 "|" +#ifdef USE_MI_DECRYPTION + "Key|" + "Keys|" +#endif // USE_MI_DECRYPTION + "Period|Time|Page|Battery|Unit|Block|Option"; + +void (*const MI32_Commands[])(void) PROGMEM = { +#ifdef USE_MI_DECRYPTION + &CmndMi32Key, + &CmndMi32Keys, +#endif // USE_MI_DECRYPTION + &CmndMi32Period, &CmndMi32Time, &CmndMi32Page, &CmndMi32Battery, &CmndMi32Unit, &CmndMi32Block, &CmndMi32Option }; + + +#define MI_UNKOWN 1 +#define MI_FLORA 2 +#define MI_MJ_HT_V1 3 +#define MI_LYWSD02 4 +#define MI_LYWSD03MMC 5 +#define MI_CGG1 6 +#define MI_CGD1 7 +#define MI_NLIGHT 8 +#define MI_MJYD2S 9 +#define MI_YEERC 10 +#define MI_MHOC401 11 +#define MI_MHOC303 12 +#define MI_ATC 13 + +#define MI_MI32_TYPES 13 //count this manually + +const uint16_t kMI32DeviceID[MI_MI32_TYPES]={ + 0x0000, // Unkown + 0x0098, // Flora + 0x01aa, // MJ_HT_V1 + 0x045b, // LYWSD02 + 0x055b, // LYWSD03 + 0x0347, // CGG1 + 0x0576, // CGD1 + 0x03dd, // NLIGHT + 0x07f6, // MJYD2S + 0x0153, // yee-rc + 0x0387, // MHO-C401 + 0x06d3, // MHO-C303 + 0x0a1c // ATC -> this is a fake ID +}; + +const char kMI32DeviceType0[] PROGMEM = "Unknown"; +const char kMI32DeviceType1[] PROGMEM = "Flora"; +const char kMI32DeviceType2[] PROGMEM = "MJ_HT_V1"; +const char kMI32DeviceType3[] PROGMEM = "LYWSD02"; +const char kMI32DeviceType4[] PROGMEM = "LYWSD03"; +const char kMI32DeviceType5[] PROGMEM = "CGG1"; +const char kMI32DeviceType6[] PROGMEM = "CGD1"; +const char kMI32DeviceType7[] PROGMEM = "NLIGHT"; +const char kMI32DeviceType8[] PROGMEM = "MJYD2S"; +const char kMI32DeviceType9[] PROGMEM = "YEERC"; +const char kMI32DeviceType10[] PROGMEM ="MHOC401"; +const char kMI32DeviceType11[] PROGMEM ="MHOC303"; +const char kMI32DeviceType12[] PROGMEM ="ATC"; +const char * kMI32DeviceType[] PROGMEM = {kMI32DeviceType0,kMI32DeviceType1,kMI32DeviceType2,kMI32DeviceType3,kMI32DeviceType4,kMI32DeviceType5,kMI32DeviceType6,kMI32DeviceType7,kMI32DeviceType8,kMI32DeviceType9,kMI32DeviceType10,kMI32DeviceType11,kMI32DeviceType12}; + +typedef int BATREAD_FUNCTION(int slot); +typedef int UNITWRITE_FUNCTION(int slot, int unit); +typedef int TIMEWRITE_FUNCTION(int slot); + +int genericOpCompleteFn(BLE_ESP32::generic_sensor_t *pStruct); +int genericBatReadFn(int slot); +int genericUnitWriteFn(int slot, int unit); +int genericTimeWriteFn(int slot); +int MI32scanCompleteCallback(NimBLEScanResults results); + +const char LYWSD02_Svc[] PROGMEM = "EBE0CCB0-7A0A-4B0C-8A1A-6FF2997DA3A6"; +const char LYWSD02_BattChar[] PROGMEM = "EBE0CCC4-7A0A-4B0C-8A1A-6FF2997DA3A6"; +const char LYWSD02_UnitChar[] PROGMEM = "EBE0CCBE-7A0A-4B0C-8A1A-6FF2997DA3A6"; +const char LYWSD02_TimeChar[] PROGMEM = "EBE0CCB7-7A0A-4B0C-8A1A-6FF2997DA3A6"; +const char LYWSD02_BattNotifyChar[] PROGMEM = "EBE0CCC1-7A0A-4B0C-8A1A-6FF2997DA3A6"; + +const char *LYWSD03_Svc = LYWSD02_Svc; +const char *LYWSD03_BattNotifyChar = LYWSD02_BattNotifyChar; + +const char *MHOC303_Svc = LYWSD02_Svc; +const char *MHOC303_UnitChar = LYWSD02_UnitChar; +const char *MHOC303_TimeChar = LYWSD02_TimeChar; + +const char *MHOC401_Svc = LYWSD02_Svc; +const char *MHOC401_BattNotifyChar = LYWSD02_BattNotifyChar; + +const char CGD1_Svc[] PROGMEM = "180F"; +const char CGD1_BattChar[] PROGMEM = "2A19"; + +const char FLORA_Svc[] PROGMEM = "00001204-0000-1000-8000-00805F9B34FB"; +const char FLORA_BattChar[] PROGMEM = "00001A02-0000-1000-8000-00805F9B34FB"; + + + +/*********************************************************************************************\ + * enumerations +\*********************************************************************************************/ + +// types of operation performed, included in context +enum MI32_MI_OP_TYPES { + OP_TIME_WRITE = 0, + OP_BATT_READ = 1, + OP_UNIT_WRITE = 2, + OP_UNIT_READ = 3, + OP_UNIT_TOGGLE = 4, + OP_READ_HT_LY = 5, +}; + + +enum MI32_MI_KEY_REQ { + KEY_REQUIREMENT_UNKNOWN = 0, // we don't know if a key is needed + KEY_NOT_REQUIRED = 1, // we got an unencrypted payload + KEY_REQUIRED_BUT_NOT_FOUND = 2, // we got an encrypted packet, but had not key + KEY_REQUIRED_AND_FOUND = 3, // we got an encrypted packet, and could decrypt + KEY_REQUIRED_AND_INVALID = 4, // we got an encrypted packet, and could not decrypt +}; + +/*********************************************************************************************\ + * Classes +\*********************************************************************************************/ + + +// fn type READ_CALLBACK +// NOTE!!!: this callback is called DIRECTLY from the operation task, so be careful about cross-thread access of data +// if is called after read, so that you can do a read/modify/write operation on a characteristic. +int toggleUnit(BLE_ESP32::generic_sensor_t *op){ + uint32_t context = (uint32_t) op->context; + int opType = context >> 24; + // we only need to op type + int devType = (context >> 16) & 0xff; + int slot = (context) & 0xff; + switch (opType){ + case OP_UNIT_TOGGLE:{ + uint8_t curUnit = 0; + if( op->dataRead[0] != 0 && op->dataRead[0] < 101 ){ + curUnit = op->dataRead[0]; + } + + curUnit = curUnit == 0x01?0xFF:0x01; // C/F + // copy in ALL of the data, because we don't know how long this is from the existing src code. + memcpy(op->dataToWrite, op->dataRead, op->readlen); + op->writelen = op->readlen; + op->dataToWrite[0] = curUnit; + } break; + case OP_UNIT_WRITE:{ + uint8_t curUnit = op->dataToWrite[0]; + // copy in ALL of the data, because we don't know how long this is from the existing src code. + memcpy(op->dataToWrite, op->dataRead, op->readlen); + op->writelen = op->readlen; + op->dataToWrite[0] = curUnit; + } break; + } + return 0; +} + +bool MI32Operation(int slot, int optype, const char *svc, const char *charactistic, const char *notifychar = nullptr, const uint8_t *data = nullptr, int datalen = 0, uint8_t *addr = nullptr ) { + if (!svc || !svc[0]){ + AddLog_P(LOG_LEVEL_ERROR,PSTR("MI32Op: inv svc")); + return 0; + } + + BLE_ESP32::generic_sensor_t *op = nullptr; + + // ALWAYS use this function to create a new one. + int res = BLE_ESP32::newOperation(&op); + if (!res){ + AddLog_P(LOG_LEVEL_ERROR,PSTR("Can't get a newOperation from BLE")); + return 0; + } else { + if (BLE_ESP32::BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG,PSTR("got a newOperation from BLE")); + } + + if (slot >= 0){ + op->addr = NimBLEAddress(MIBLEsensors[slot].MAC); + } else { + if (!addr){ + AddLog_P(LOG_LEVEL_ERROR,PSTR("no addr")); + BLE_ESP32::freeOperation(&op); + return 0; + } + op->addr = NimBLEAddress(addr); + } + + bool havechar = false; + op->serviceUUID = NimBLEUUID(svc); + + if (!op->serviceUUID.bitSize()){ + BLE_ESP32::freeOperation(&op); + AddLog_P(LOG_LEVEL_ERROR,PSTR("MI: Bad service string %s"), svc); + return 0; + } + + + if (charactistic && charactistic[0]){ + havechar = true; + op->characteristicUUID = NimBLEUUID(charactistic); + if (!op->characteristicUUID.bitSize()){ + BLE_ESP32::freeOperation(&op); + AddLog_P(LOG_LEVEL_ERROR,PSTR("MI: Bad characteristic string %s"), charactistic); + return 0; + } + } + if (notifychar && notifychar[0]){ + op->notificationCharacteristicUUID = NimBLEUUID(notifychar); + if (!op->notificationCharacteristicUUID.bitSize()){ + BLE_ESP32::freeOperation(&op); + AddLog_P(LOG_LEVEL_ERROR,PSTR("MI: Bad notifycharacteristic string %s"), notifychar); + return 0; + } + } + + if (data && datalen) { + op->writelen = datalen; + memcpy(op->dataToWrite, data, datalen); + } else { + if (!datalen && havechar){ + op->readlen = 1; // if we don't set readlen, then it won't read + } + } + + // the only times we intercept between read abnd write + if ((optype == OP_UNIT_WRITE) || (optype == OP_UNIT_TOGGLE)){ + op->readlen = 1; // if we don't set readlen, then it won't read + op->readmodifywritecallback = (void *)toggleUnit; + } + + // this op will call us back on complete or failure. + op->completecallback = (void *)genericOpCompleteFn; + uint32_t context = (optype << 24) | (MIBLEsensors[slot].type << 16) | slot; + op->context = (void *)context; + + if (BLE_ESP32::BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG,PSTR("MI s:%d op:%s"), slot, BLE_ESP32::BLETriggerResponse(op).c_str()); + + res = BLE_ESP32::extQueueOperation(&op); + if (!res){ + // if it fails to add to the queue, do please delete it + BLE_ESP32::freeOperation(&op); + AddLog_P(LOG_LEVEL_ERROR,PSTR("Failed to queue new operation - deleted")); + } + + return res; +} + + + +int genericBatReadFn(int slot){ + int res = 0; + + switch(MIBLEsensors[slot].type) { + // these use notify for battery read, and it comes in the temp packet + case MI_LYWSD03MMC: + res = MI32Operation(slot, OP_BATT_READ, LYWSD03_Svc, nullptr, LYWSD03_BattNotifyChar); + break; + case MI_MHOC401: + res = MI32Operation(slot, OP_BATT_READ, MHOC401_Svc, nullptr, MHOC401_BattNotifyChar); + break; + + // these read a characteristic + case MI_FLORA: + res = MI32Operation(slot, OP_BATT_READ, FLORA_Svc, FLORA_BattChar); + break; + case MI_LYWSD02: + res = MI32Operation(slot, OP_BATT_READ, LYWSD02_Svc, LYWSD02_BattChar); + break; + case MI_CGD1: + res = MI32Operation(slot, OP_BATT_READ, CGD1_Svc, CGD1_BattChar); + break; + +// this was for testing only - it does work, but no need to read as we get good bat in advert +// case MI_MJ_HT_V1: +// res = MI32Operation(slot, OP_BATT_READ, CGD1_Svc, CGD1_BattChar); +// break; + + default: + res = -10; // no need to read + break; + } + if (res > 0){ + if (BLE_ESP32::BLEDebugMode > 0) AddLog_P(LOG_LEVEL_INFO,PSTR("Req batt read slot %d type %d queued"), slot, MIBLEsensors[slot].type); + } else { + if (BLE_ESP32::BLEDebugMode > 0) AddLog_P(LOG_LEVEL_INFO,PSTR("Req batt read slot %d type %d non-queued res %d"), slot, MIBLEsensors[slot].type, res); + } + return res; +} + + + +int genericSensorReadFn(int slot, int force){ + int res = 0; + switch(MIBLEsensors[slot].type) { +/* seen notify timeout consistently with MI_LYWSD02, + so although the characteristic seems to exist, it does not work? + further dev required with sensor to hand. + case MI_LYWSD02: + // don't read if key present and we've decoded at least one advert + if (MIBLEsensors[slot].needkey == KEY_REQUIRED_AND_FOUND) return -2; + res = MI32Operation(slot, OP_READ_HT_LY, LYWSD02_Svc, nullptr, LYWSD02_BattNotifyChar); + break;*/ + case MI_LYWSD03MMC: + // don't read if key present and we've decoded at least one advert + if (MIBLEsensors[slot].needkey == KEY_REQUIRED_AND_FOUND && !force) return -2; + res = MI32Operation(slot, OP_READ_HT_LY, LYWSD03_Svc, nullptr, LYWSD03_BattNotifyChar); + break; + case MI_MHOC401: + // don't read if key present and we've decoded at least one advert + if (MIBLEsensors[slot].needkey == KEY_REQUIRED_AND_FOUND && !force) return -2; + res = MI32Operation(slot, OP_READ_HT_LY, MHOC401_Svc, nullptr, MHOC401_BattNotifyChar); + break; + + default: + res = -1; + break; + } + return res; +} + + +// called once per second +int readOneSensor(){ + if (MI32.sensorreader.active){ + AddLog_P(LOG_LEVEL_DEBUG,PSTR("readOneSensor - already active reading %d"), MI32.sensorreader.slot-1); + return 0; + } + + // loop if the sensor at the slot does not need to be read + // i.e. drop out of loop when we start a read, or hit the end + int res = -1; + do { + // MI32.sensorreader.slot is reset to zero to trigger a read sequence + if (MI32.sensorreader.slot >= MIBLEsensors.size()){ + //AddLog_P(LOG_LEVEL_DEBUG,PSTR("readOneSensor past end of slots - %d > %d"), MI32.sensorreader.slot, MIBLEsensors.size()); + return 0; + } + + res = genericSensorReadFn(MI32.sensorreader.slot, 0); + if (BLE_ESP32::BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG,PSTR("genericSensorReadFn slot %d res %d"), MI32.sensorreader.slot, res); + + // if this sensor in this slot does not need to be read via notify, just move on top the next one + if (res < 0){ + MI32.sensorreader.slot++; + } else { + break; + } + } while (1); + + if (res == 0){ + // can't read at the moment (no operations available?) + AddLog_P(LOG_LEVEL_DEBUG,PSTR("readOneSensor no ops available slot %d res %d"), MI32.sensorreader.slot, res); + return 0; + } + + // setup next slot to read + MI32.sensorreader.slot++; + // and make it wait until the read/notify is complete + // this is cleared in the response callback. + MI32.sensorreader.active = 1; + if (BLE_ESP32::BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG,PSTR("readOneSensor reading for slot %d res %d"), MI32.sensorreader.slot-1, res); + + // started one + return 1; +} + + + +// called once per second +int readOneBat(){ + if (MI32.batteryreader.active){ + return 0; + } + + //MI32.batteryreader.slot is rest to zero to trigger a read... + if (MI32.batteryreader.slot >= MIBLEsensors.size()){ + return 0; + } + + int res = genericBatReadFn(MI32.batteryreader.slot); + + // if this sensor in this slot does not support battery read, just move on top the next one + if (res < 0){ + MI32.batteryreader.slot++; + if (MI32.batteryreader.slot >= MIBLEsensors.size()){ + if (BLE_ESP32::BLEDebugMode > 0) AddLog_P(LOG_LEVEL_INFO,PSTR("Batt loop complete at %d"), MI32.batteryreader.slot); + } + return 0; + } + + if (res == 0){ + // can't read at the moment (no operations available?) + return 0; + } + + // setup next slot to read + MI32.batteryreader.slot++; + // and make it wait until the read/notify is complete + // this is cleared in the response callback. + MI32.batteryreader.active = 1; + if (MI32.batteryreader.slot >= MIBLEsensors.size()){ + if (BLE_ESP32::BLEDebugMode > 0) AddLog_P(LOG_LEVEL_INFO,PSTR("Batt loop will complete at %d"), MI32.batteryreader.slot); + } + // started one + return 1; +} + + + +///////////////////////////////////////////////////// +// change the unit of measurement? +// call with unit == -1 to cause the unit to be toggled. +int genericUnitWriteFn(int slot, int unit){ + int res = 0; + int op = OP_UNIT_WRITE; + if (unit == -1){ + op = OP_UNIT_TOGGLE; + } + uint8_t writeData[1]; + writeData[0] = unit; + switch (MIBLEsensors[slot].type){ + case MI_LYWSD02: + res = MI32Operation(slot, op, LYWSD02_Svc, LYWSD02_UnitChar, nullptr, writeData, 1); + break; + case MI_MHOC303: // actually, EXACTLY the same as above, including the sevice and characteristic... + res = MI32Operation(slot, op, MHOC303_Svc, MHOC303_UnitChar, nullptr, writeData, 1); + break; + default: + res = -1; + break; + } + return res; +} + +///////////////////////////////////////////////////// +// read the unit of measurement. genericOpCompleteFn +int genericUnitReadFn(int slot){ + int res = 0; + switch (MIBLEsensors[slot].type){ + case MI_LYWSD02: + res = MI32Operation(slot, OP_UNIT_READ, LYWSD02_Svc, LYWSD02_UnitChar); + break; + case MI_MHOC303: // actually, EXACTLY the same as above, including the sevice and characteristic... + res = MI32Operation(slot, OP_UNIT_READ, MHOC303_Svc, MHOC303_UnitChar); + break; + default: + res = -1; + break; + } + return res; +} + + +///////////////////////////////////////////////////// +// write time to a device. genericOpCompleteFn +int genericTimeWriteFn(int slot){ + int res = 0; + switch (MIBLEsensors[slot].type){ + case MI_LYWSD02: { + union { + uint8_t buf[5]; + uint32_t time; + } _utc; + _utc.time = Rtc.utc_time; + _utc.buf[4] = Rtc.time_timezone / 60; + res = MI32Operation(slot, OP_TIME_WRITE, LYWSD02_Svc, LYWSD02_TimeChar, nullptr, _utc.buf, sizeof(_utc.buf)); + } break; + case MI_MHOC303: // actually, EXACTLY the same as above, including the sevice and characteristic... + union { + uint8_t buf[5]; + uint32_t time; + } _utc; + _utc.time = Rtc.utc_time; + _utc.buf[4] = Rtc.time_timezone / 60; + res = MI32Operation(slot, OP_TIME_WRITE, MHOC303_Svc, MHOC303_TimeChar, nullptr, _utc.buf, sizeof(_utc.buf)); + break; + default: + res = -1; + break; + } + return res; +} + + +int genericOpCompleteFn(BLE_ESP32::generic_sensor_t *op){ + uint32_t context = (uint32_t) op->context; + + if (BLE_ESP32::BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG,PSTR("MI op complete context %x"), context); + + int opType = context >> 24; + int devType = (context >> 16) & 0xff; + int slot = (context) & 0xff; + + char slotMAC[13]; + BLE_ESP32::dump(slotMAC, sizeof(slotMAC), MIBLEsensors[slot].MAC, 6) ; + uint8_t addrrev[6]; + memcpy(addrrev, MIBLEsensors[slot].MAC, 6); + //BLE_ESP32::ReverseMAC(addrrev); + NimBLEAddress addr(addrrev); + + bool fail = false; + if (op->addr != addr){ + // slot changed during operation? + AddLog_P(LOG_LEVEL_ERROR,PSTR("Slot mac changed during an operation")); + fail = true; + } + + if (op->state <= GEN_STATE_FAILED){ + AddLog_P(LOG_LEVEL_ERROR,PSTR("operation failed %d for %s"), op->state, slotMAC); + fail = true; + } + + if (fail){ + switch(opType){ + case OP_BATT_READ:{ + // allow another... + MI32.batteryreader.active = 0; + } break; + case OP_READ_HT_LY: { + // allow another... + MI32.sensorreader.active = 0; + } break; + } + return 0; + } + + switch(opType){ + case OP_TIME_WRITE: + AddLog_P(LOG_LEVEL_DEBUG,PSTR("Time write for %s complete"), slotMAC); + return 0; // nothing to do + case OP_BATT_READ:{ + uint8_t *data = nullptr; + int len = 0; + if (op->notifylen){ + data = op->dataNotify; + len = op->notifylen; + // note: the only thingas that have battery in notify FOR THE MOMENT read it like this. + MI32notifyHT_LY(slot, (char*)op->dataNotify, op->notifylen); + } + if (op->readlen){ + data = op->dataRead; + len = op->readlen; + MIParseBatt(slot, data, len); + } + + // allow another... + MI32.batteryreader.active = 0; + AddLog_P(LOG_LEVEL_INFO,PSTR("batt read slot %d done state %x"), slot, op->state); + + } return 0; + + case OP_UNIT_WRITE: // nothing more to do? + AddLog_P(LOG_LEVEL_DEBUG,PSTR("Unit write for %s complete"), slotMAC); + return 0; + + case OP_UNIT_READ: { + uint8_t currUnit = op->dataRead[0]; + AddLog_P(LOG_LEVEL_DEBUG,PSTR("Unit read for %s complete %d"), slotMAC, currUnit); + } return 0; + + case OP_UNIT_TOGGLE: { + uint8_t currUnit = op->dataToWrite[0]; + AddLog_P(LOG_LEVEL_DEBUG,PSTR("Unit toggle for %s complete %d->%d; datasize was %d"), slotMAC, op->dataRead[0], op->dataToWrite[0], op->readlen); + } return 0; + + case OP_READ_HT_LY: { + // allow another... + MI32.sensorreader.active = 0; + MI32notifyHT_LY(slot, (char*)op->dataNotify, op->notifylen); + if (BLE_ESP32::BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG,PSTR("HT_LY notify for %s complete"), slotMAC); + } return 0; + + default: + AddLog_P(LOG_LEVEL_ERROR,PSTR("OpType %d not recognised?"), opType); + return 0; + } + + return 0; +} + +int MI32advertismentCallback(BLE_ESP32::ble_advertisment_t *pStruct) +{ + // we will try not to use this... + BLEAdvertisedDevice *advertisedDevice = pStruct->advertisedDevice; + + // AddLog_P(LOG_LEVEL_DEBUG,PSTR("Advertised Device: %s Buffer: %u"),advertisedDevice->getAddress().toString().c_str(),advertisedDevice->getServiceData(0).length()); + int RSSI = pStruct->RSSI; + const uint8_t *addr = pStruct->addr; + if(MI32isInBlockList(addr) == true) return 0; + + int svcdataCount = advertisedDevice->getServiceDataCount(); + + if (svcdataCount == 0) { + return 0; + } + + NimBLEUUID UUIDBig = advertisedDevice->getServiceDataUUID(0);//.getNative()->u16.value; + + const ble_uuid_any_t* native = UUIDBig.getNative(); + if (native->u.type != 16){ + //not interested in 128 bit; + return 0; + } + uint16_t UUID = native->u16.value; + + char temp[60]; + BLE_ESP32::dump(temp, 13, addr, 6); + + if (BLE_ESP32::BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG_MORE,PSTR("MI:%s svc[0] UUID (%x)"), temp, UUID); + std::string ServiceDataStr = advertisedDevice->getServiceData(0); + + uint32_t ServiceDataLength = ServiceDataStr.length(); + const uint8_t *ServiceData = (const uint8_t *)ServiceDataStr.data(); + BLE_ESP32::dump(temp, 60, ServiceData, ServiceDataLength); + if (BLE_ESP32::BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG_MORE,PSTR("MI:%s"), temp); + + + if (UUID){ + // this will take and keep the mutex until the function is over + TasAutoMutex localmutex(&slotmutex, "Mi32AdCB2"); + switch(UUID){ + case 0xfe95: // std MI? + case 0xfdcd: // CGD1? + { + MI32ParseResponse(ServiceData, ServiceDataLength, addr, RSSI); + } break; + case 0x181a: { //ATC + MI32ParseATCPacket(ServiceData, ServiceDataLength, addr, RSSI); + } break; + + default:{ + } break; + } + } + return 0; +} + + +/*********************************************************************************************\ + * Helper functions +\*********************************************************************************************/ + +/** + * @brief Remove all colons from null terminated char array + * + * @param _string Typically representing a MAC-address like AA:BB:CC:DD:EE:FF + */ +void MI32stripColon(char* _string){ + uint32_t _length = strlen(_string); + uint32_t _index = 0; + while (_index < _length) { + char c = _string[_index]; + if(c==':'){ + memmove(_string+_index,_string+_index+1,_length-_index); + } + _index++; + } + _string[_index] = 0; +} + +/** + * @brief Convert string that repesents a hexadecimal number to a byte array + * + * @param _string input string in format: AABBCCDDEEFF or AA:BB:CC:DD:EE:FF, caseinsensitive + * @param _mac target byte array must match the correct size (i.e. AA:BB -> uint8_t bytes[2]) + */ + +void MI32HexStringToBytes(char* _string, uint8_t* _byteArray) { + MI32stripColon(_string); + UpperCase(_string,_string); + uint32_t index = 0; + uint32_t _end = strlen(_string); + memset(_byteArray,0,_end/2); + while (index < _end) { + char c = _string[index]; + uint8_t value = 0; + if(c >= '0' && c <= '9') + value = (c - '0'); + else if (c >= 'A' && c <= 'F') + value = (10 + (c - 'A')); + _byteArray[(index/2)] += value << (((index + 1) % 2) * 4); + index++; + } +} + +/** + * @brief Reverse an array of 6 bytes + * + * @param _mac a byte array of size 6 (typicalliy representing a MAC address) + */ +void MI32_ReverseMAC(uint8_t _mac[]){ + uint8_t _reversedMAC[6]; + for (uint8_t i=0; i<6; i++){ + _reversedMAC[5-i] = _mac[i]; + } + memcpy(_mac,_reversedMAC, sizeof(_reversedMAC)); +} + +#ifdef USE_MI_DECRYPTION +int MI32AddKey(char* payload, char* key = nullptr){ + mi_bindKey_t keyMAC; + + if (!key){ + MI32HexStringToBytes(payload,keyMAC.buf); + } else { + MI32HexStringToBytes(payload,keyMAC.MAC); + MI32HexStringToBytes(key,keyMAC.key); + } + + bool unknownKey = true; + for(uint32_t i=0; i 0) AddLog_P(LOG_LEVEL_DEBUG,PSTR("MI32: search key for MAC: %02x%02x%02x%02x%02x%02x"), mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]); + for(uint32_t i=0; i 0) AddLog_P(LOG_LEVEL_DEBUG,PSTR("MI32: decryption Key found")); + foundNoKey = false; + break; + } + } + if(foundNoKey){ + AddLog_P(LOG_LEVEL_DEBUG,PSTR("MI32: no Key found !!")); + return -2; // indicates needs key + } + + br_aes_small_ctrcbc_keys keyCtx; + br_aes_small_ctrcbc_init(&keyCtx, _bindkey, 16); + + br_ccm_context ctx; + br_ccm_init(&ctx, &keyCtx.vtable); + br_ccm_reset(&ctx, nonce, 12, 1, len, 4); + br_ccm_aad_inject(&ctx, authData, 1); + br_ccm_flip(&ctx); + + memcpy(payload, data, len); //we want to be sure about 4-byte alignement + br_ccm_run(&ctx, 0, payload, len); + memcpy(data, payload, len); //back to the packet + + + // crashed in here - why?, so give it more space to work with? + // returns 1 if matched, else 0 + int ret = br_ccm_check_tag(&ctx, &tag); + + if (BLE_ESP32::BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG,PSTR("MI32: Err:%i, Decrypted : %02x %02x %02x %02x %02x"), ret, payload[1],payload[2],payload[3],payload[4],payload[5]); + return ret-1; // -> -1=fail, 0=success +} + +#endif // USE_MI_DECRYPTION + + +// packet examples: +// MJ_HT_V1 +// 5020 AA01 41 3AF4DAA8654C 0A100109 +// 5020 AA01 43 3AF4DAA8654C 061002E901 +// 5020 AA01 48 3AF4DAA8654C 041002BF00 +// 5020 AA01 4A 3AF4DAA8654C 0D1004BF00E901 +// 7122 AA01 15 3AF4DAA8654C 0D 0200020D10 + +// LYWSD03 encrypted data: +// 5858 5B05 2F B3E30838C1A4 [69A9FBDF67] ,060000 0791C39A - 23bytes +// 23-9 = 14 +// -> nonce B3E30838C1A4|5B02|2F|060000 +// 23-6 = 17 +// -> tag 0791C39A +// datalen = 23 - 9 - 4 - 3 - 1 - 1 = 5 + +// CGD1 reconstructed from src: (svcdata on fdcd) +// xxyy FFEEDDCCBBAA MMMM TTTTHHHH|BB +// xxyy FFEEDDCCBBAA 0104 TTTTHHHH +// xxyy FFEEDDCCBBAA 0201 BB + + +int MIParsePacket(const uint8_t* slotmac, struct mi_beacon_data_t *parsed, const uint8_t *datain, int len){ + uint8_t data[32]; + memcpy(data, datain, len); + if (!parsed){ + return 0; + } + if (len < 5){ + return 0; + } + + int byteindex = 0; + + // 58 58 = 0x5858 = data|comp|mac|enc, v5|auth2 + // 30 58 = 0x5830 = comp|mac, v5|auth2 + // 30 50 = 0x5030 = comp|mac, v5|auth0 + // 48 59 = 0x5948 = data|enc, v5|auth2|registered + // 10 59 = 0x5910 = mac, v5|auth2|registered + // 71 22 = 0x2271 = data|comp|mac v2|bind + // 50 20 = 0x2050 = data|mac v2 - MJ_HT_V1 data + // 71 22 = 0x2271 = data|comp|mac|reserved1 v2|bind - MJ_HT_V1 pair + + // data from byte 0 - e.g. 30 + parsed->framedata.meshflag = (data[byteindex] & 0x80)>>7; //Byte 0: x....... + parsed->framedata.dataflag = (data[byteindex] & 0x40)>>6; //Byte 0: .x...... + parsed->framedata.compatibilityflag = (data[byteindex] & 0x20)>>5; //Byte 0: ..x..... - indicates compatibility data present + parsed->framedata.MACFlag = (data[byteindex] & 0x10)>>4; //Byte 0: ...x.... + parsed->framedata.isencrypted = (data[byteindex] & 0x08)>>3; //Byte 0: ....x... + parsed->framedata.reserved = (data[byteindex] & 0x03)>>6; //Byte 0: .....xxx + + // data from byte 1 - e.g. 58 + byteindex++; + parsed->framedata.version = (data[byteindex] & 0xf0)>>4; //Byte 0: xxxx.... + parsed->framedata.authMode = (data[byteindex] & 0x0C)>>6; //Byte 0: ....xx.. // e.g. 2 + parsed->framedata.bindingvalidreq = (data[byteindex] & 0x02)>>1; //Byte 0: ......x. + parsed->framedata.registeredflag = (data[byteindex] & 0x01); //Byte 0: .......x + + byteindex++; + + parsed->devicetype = *((uint16_t *)(data + byteindex)); + byteindex += 2; + parsed->framecnt = data[byteindex]; + //if (BLE_ESP32::BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG,PSTR("MI frame %d"), parsed->framecnt); + byteindex++; + + + if (parsed->framedata.version <= 3){ + // e.g. MJ_HT_V1 + } + + if (parsed->framedata.MACFlag){ + if (len < byteindex + 6){ + return 0; + } + memcpy(parsed->macdata.mac, &data[byteindex], 6); + byteindex += 6; + } + + int decres = 1; + // everything after MAC is encrypted if specified? + if (parsed->framedata.isencrypted){ + if (len < byteindex + 3+4+1){ + return 0; + } + const uint8_t* mac = slotmac; + if (parsed->framedata.MACFlag){ + mac = parsed->macdata.mac; + } + uint8_t nonce[12]; + uint8_t *p = nonce; + memcpy(p, mac, 6); + p += 6; + memcpy(p, &parsed->devicetype, 2); + p += 2; + *(p++) = parsed->framecnt; + uint8_t *extCnt = data +(len-7); + memcpy(p, extCnt, 3); + p += 3; + uint32_t tag = *(uint32_t *)(data + (len-4)); + + // decrypt the data in place + decres = MIDecryptPayload(mac, nonce, tag, data + byteindex, len - byteindex - 7); + // no longer need the nonce data. + len -= 7; + } + + switch(decres){ + case 1: // decrypt not requested + break; + case 0: // suceeded + parsed->needkey = KEY_REQUIRED_AND_FOUND; + if (BLE_ESP32::BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG,PSTR("MI payload decrypted")); + break; + case -1: // key failed to work + parsed->needkey = KEY_REQUIRED_AND_INVALID; + AddLog_P(LOG_LEVEL_ERROR,PSTR("MI payload decrypt failed")); + parsed->payloadpresent = 0; + return 0; + break; + case -2: // key not present + parsed->needkey = KEY_REQUIRED_BUT_NOT_FOUND; + AddLog_P(LOG_LEVEL_ERROR,PSTR("MI payload encrypted but no key")); + parsed->payloadpresent = 0; + return 0; + break; + } + + // if set, there could be 1 or 3 bytes here. + if (parsed->framedata.compatibilityflag) { + if (len < byteindex + 1){ + return 0; + } + // e.g. in pair: 7122 AA01 15 3AF4DAA8654C [0D] 0200020D10 -> bond|unused2 + parsed->compatibility.reserved = (data[byteindex] & 0xc0) >> 6; //Byte 0: xx...... + parsed->compatibility.IOcap = (data[byteindex] & 0x20) >> 5; //Byte 0: ..x..... + parsed->compatibility.bondability = (data[byteindex] & 0x18) >> 3; //Byte 0: ...xx... + parsed->compatibility.unused = (data[byteindex] & 0x07) >> 0; //Byte 0: .....xxx + byteindex ++; + + if (parsed->compatibility.IOcap) { + if (len < byteindex + 2){ + return 0; + } + parsed->compatibility.IOCapability = *((uint16_t *)(data + byteindex)); // bytes 1-2, e.g. 01 00 -> 0001 + byteindex += 2; + } + } + + // rest is payload + int rem = (len - byteindex); + if (rem > sizeof(parsed->payload)){ + rem = sizeof(parsed->payload); + return 0; + } + + if ((len - byteindex) == 0){ + if (BLE_ESP32::BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG,PSTR("MI no payload")); + parsed->payload.size = 0; + parsed->payloadpresent = 0; + return 0; + } + + // we have payload which did not need decrypt. + if (decres == 1){ + parsed->needkey = KEY_NOT_REQUIRED; + if (BLE_ESP32::BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG,PSTR("MI payload unencrypted")); + } + + // already decrypted if required + parsed->payloadpresent = 1; + memcpy(&parsed->payload, (data + byteindex), (len - byteindex)); + if (parsed->payload.size != (len - byteindex) - 3){ + AddLog_P(LOG_LEVEL_DEBUG,PSTR("MI payload length mismatch")); + } + + return 1; +} + + + + +#ifdef USE_HOME_ASSISTANT +/** + * @brief For HASS only, changes last entry of JSON in mqtt_data to 'null' + */ + +void MI32nullifyEndOfMQTT_DATA(){ + char *p = TasmotaGlobal.mqtt_data + strlen(TasmotaGlobal.mqtt_data); + while(true){ + *p--; + if(p[0]==':'){ + p[1] = 0; + break; + } + } + ResponseAppend_P(PSTR("null")); +} +#endif // USE_HOME_ASSISTANT + +/*********************************************************************************************\ + * common functions +\*********************************************************************************************/ + + +/** + * @brief Return the slot number of a known sensor or return create new sensor slot + * + * @param _MAC BLE address of the sensor + * @param _type Type number of the sensor + * @return uint32_t Known or new slot in the sensors-vector + */ +uint32_t MIBLEgetSensorSlot(const uint8_t *mac, uint16_t _type, uint8_t counter){ + + //AddLog_P(LOG_LEVEL_DEBUG_MORE,PSTR("%s: will test ID-type: %x"),D_CMND_MI32, _type); + bool _success = false; + for (uint32_t i=0; i < MI_MI32_TYPES; i++){ // i < sizeof(kMI32DeviceID) gives compiler warning + if(_type == kMI32DeviceID[i]){ + _type = i+1; + _success = true; + break; + } + else { + //AddLog_P(LOG_LEVEL_DEBUG_MORE,PSTR("%s: ID-type is not: %x"),D_CMND_MI32,kMI32DeviceID[i]); + } + } + if(!_success) { + _type = 1; // unknown + } + + //AddLog_P(LOG_LEVEL_DEBUG_MORE,PSTR("%s: vector size %u"),D_CMND_MI32, MIBLEsensors.size()); + for(uint32_t i=0; i 0) AddLog_P(LOG_LEVEL_DEBUG_MORE,PSTR("%s: slot: %u/%u - ign repeat"),D_CMND_MI32, i, MIBLEsensors.size()); + //return 0xff; // packet received before, stop here + } + if (BLE_ESP32::BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG,PSTR("MI frame %d, last %d"), counter, MIBLEsensors[i].lastCnt); + MIBLEsensors[i].lastCnt = counter; + if (BLE_ESP32::BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG_MORE,PSTR("%s: slot: %u/%u"),D_CMND_MI32, i, MIBLEsensors.size()); + + if (MIBLEsensors[i].type != _type){ + // this happens on incorrectly configured pvvx ATC firmware + AddLog_P(LOG_LEVEL_ERROR,PSTR("%s: slot: %u - device type 0x%04x(%s) -> 0x%04x(%s) - check device is only sending one type of advert."),D_CMND_MI32, i, + kMI32DeviceID[MIBLEsensors[i].type-1], kMI32DeviceType[MIBLEsensors[i].type-1], kMI32DeviceID[_type-1], kMI32DeviceType[_type-1]); + MIBLEsensors[i].type = _type; + } + + return i; + } + //AddLog_P(LOG_LEVEL_DEBUG_MORE,PSTR("%s: i: %x %x %x %x %x %x"),D_CMND_MI32, MIBLEsensors[i].MAC[5], MIBLEsensors[i].MAC[4],MIBLEsensors[i].MAC[3],MIBLEsensors[i].MAC[2],MIBLEsensors[i].MAC[1],MIBLEsensors[i].MAC[0]); + //AddLog_P(LOG_LEVEL_DEBUG_MORE,PSTR("%s: n: %x %x %x %x %x %x"),D_CMND_MI32, mac[5], mac[4], mac[3],mac[2],mac[1],mac[0]); + } + //AddLog_P(LOG_LEVEL_DEBUG_MORE,PSTR("%s: new sensor -> slot: %u"),D_CMND_MI32, MIBLEsensors.size()); + //AddLog_P(LOG_LEVEL_DEBUG_MORE,PSTR("%s: found new sensor"),D_CMND_MI32); + mi_sensor_t _newSensor; + memset(&_newSensor, 0 , sizeof(_newSensor)); + memcpy(_newSensor.MAC, mac, 6); + _newSensor.type = _type; + _newSensor.eventType.raw = 0; + _newSensor.feature.raw = 0; + _newSensor.temp = NAN; + _newSensor.needkey = KEY_REQUIREMENT_UNKNOWN; + _newSensor.bat = 0x00; + _newSensor.RSSI = 0xffff; + _newSensor.lux = 0x00ffffff; + + switch (_type) + { + case MI_FLORA: + _newSensor.moisture =0xff; + _newSensor.fertility =0xffff; + _newSensor.firmware[0]='\0'; + _newSensor.feature.temp=1; + _newSensor.feature.moist=1; + _newSensor.feature.fert=1; + _newSensor.feature.lux=1; + _newSensor.feature.bat=1; + break; + case MI_NLIGHT: + _newSensor.events=0x00; + _newSensor.feature.PIR=1; + _newSensor.feature.NMT=1; + break; + case MI_MJYD2S: + _newSensor.NMT=0; + _newSensor.events=0x00; + _newSensor.feature.PIR=1; + _newSensor.feature.NMT=1; + _newSensor.feature.lux=1; + _newSensor.feature.bat=1; + break; + case MI_YEERC: + _newSensor.feature.Btn=1; + break; + default: + _newSensor.hum=NAN; + _newSensor.feature.temp=1; + _newSensor.feature.hum=1; + _newSensor.feature.tempHum=1; + _newSensor.feature.bat=1; + break; + } + MIBLEsensors.push_back(_newSensor); + AddLog_P(LOG_LEVEL_DEBUG,PSTR("%s: new %s at slot: %u"),D_CMND_MI32, kMI32DeviceType[_type-1],MIBLEsensors.size()-1); + MI32.mode.shallShowStatusInfo = 1; + return MIBLEsensors.size()-1; +}; + +/** + * @brief trigger real-time message for PIR or RC + * + */ +void MI32triggerTele(void){ + MI32.mode.triggeredTele = 1; + MI32ShowTriggeredSensors(); + MI32.mode.triggeredTele = 0; +} + +/** + * @brief Is called after every finding of new BLE sensor + * + */ +void MI32StatusInfo() { + MI32.mode.shallShowStatusInfo = 0; + Response_P(PSTR("{\"%s\":{\"found\":%u}}"), D_CMND_MI32, MIBLEsensors.size()); + XdrvRulesProcess(); +} + +/*********************************************************************************************\ + * BLE callbacks section + * These are called from main thread only. +\*********************************************************************************************/ + + +int MI32scanCompleteCallback(NimBLEScanResults results){ + // we actually don't need to do anything here.... + if (BLE_ESP32::BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG,PSTR("MI32: scancomplete")); + return 0; +} + + +/*********************************************************************************************\ + * init BLE_32 +\*********************************************************************************************/ + + +void MI32Init(void) { + MIBLEsensors.reserve(10); + MIBLEbindKeys.reserve(10); + MI32.mode.init = false; + + //test section for options + MI32.option.allwaysAggregate = 1; + MI32.option.noSummary = 0; + MI32.option.minimalSummary = 0; + MI32.option.directBridgeMode = 0; + MI32.option.showRSSI = 1; + MI32.option.ignoreBogusBattery = 1; // from advertisements + MI32.option.holdBackFirstAutodiscovery = 1; + + BLE_ESP32::registerForAdvertismentCallbacks((const char *)"MI32", MI32advertismentCallback); + BLE_ESP32::registerForScanCallbacks((const char *)"MI32", MI32scanCompleteCallback); + // note: for operations, we will set individual callbacks in the operations we request + //void registerForOpCallbacks(const char *tag, BLE_ESP32::OPCOMPLETE_CALLBACK* pFn); + + AddLog_P(LOG_LEVEL_INFO,PSTR("MI32: init: request callbacks")); + MI32.period = Settings.tele_period; + MI32.mode.init = 1; + return; +} + + +/*********************************************************************************************\ + * Task section +\*********************************************************************************************/ + + + + +int MIParseBatt(int slot, uint8_t *data, int len){ + int value = data[0]; + char slotMAC[13]; + BLE_ESP32::dump(slotMAC, sizeof(slotMAC), MIBLEsensors[slot].MAC, 6) ; + + if ((value != 0) && (value < 101)){ + MIBLEsensors[slot].bat = value; + if(MIBLEsensors[slot].type==MI_FLORA){ + if (len < 7){ + AddLog_P(LOG_LEVEL_ERROR,PSTR("FLORA: not enough bytes read for firmware?")); + } else { + memcpy(MIBLEsensors[slot].firmware, data+2, 5); + MIBLEsensors[slot].firmware[5] = '\0'; + AddLog_P(LOG_LEVEL_DEBUG,PSTR("%s: FLORA Firmware: %s"),D_CMND_MI32,MIBLEsensors[slot].firmware); + } + } + MIBLEsensors[slot].eventType.bat = 1; + MIBLEsensors[slot].shallSendMQTT = 1; + MI32.mode.shallTriggerTele = 1; + if (BLE_ESP32::BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG,PSTR("Batt read for %s complete %d"), slotMAC, value); + } else { + AddLog_P(LOG_LEVEL_ERROR,PSTR("Batt read for %s complete but out of range 1-101 (%d)"), slotMAC, value); + } + + return 0; +} + +/*********************************************************************************************\ + * parse the response from advertisements +\*********************************************************************************************/ + + +void MI32ParseATCPacket(const uint8_t * _buf, uint32_t length, const uint8_t *addr, int RSSI){ + ATCPacket_t *_packet = (ATCPacket_t*)_buf; + PVVXPacket_t *ppv_packet = (PVVXPacket_t*)_buf; + + + if (length == 15){ // 19-1-1-2 + uint8_t addrrev[6]; + memcpy(addrrev, addr, 6); + MI32_ReverseMAC(addrrev); + if (!memcmp(addrrev, ppv_packet->MAC, 6)){ + //int16_t temperature; // x 0.1 degree + //uint16_t humidity; // x 0.01 % + //uint16_t battery_mv; // mV + //uint8_t battery_level; // 0..100 % + //uint8_t counter; // measurement count + //uint8_t flags; + + uint32_t _slot = MIBLEgetSensorSlot(addr, 0x0a1c, ppv_packet->counter); // This must be a hard-coded fake ID + if(_slot==0xff) return; + + if ((_slot >= 0) && (_slot < MIBLEsensors.size())){ + if (BLE_ESP32::BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG,PSTR("%s:pvvx at slot %u"), kMI32DeviceType[MIBLEsensors[_slot].type-1],_slot); + MIBLEsensors[_slot].RSSI=RSSI; + MIBLEsensors[_slot].needkey=KEY_NOT_REQUIRED; + + MIBLEsensors[_slot].temp = (float)(ppv_packet->temperature)/100.0f; + MIBLEsensors[_slot].hum = (float)(ppv_packet->humidity)/100.0f; + MIBLEsensors[_slot].eventType.tempHum = 1; + MIBLEsensors[_slot].bat = ppv_packet->battery_level; + MIBLEsensors[_slot].eventType.bat = 1; + + if(MI32.option.directBridgeMode) { + MIBLEsensors[_slot].shallSendMQTT = 1; + MI32.mode.shallTriggerTele = 1; + } + } + return; + } else { + AddLog_P(LOG_LEVEL_ERROR, PSTR("PVVX packet mac mismatch - ignored?")); + return; + } + } + + + uint8_t addrrev[6]; + memcpy(addrrev, addr, 6); + //MI32_ReverseMAC(addrrev); + + // if packet tell a different address to origin, use the different address + if (memcmp(addrrev, _packet->MAC, 6)){ + MI32_ReverseMAC(_packet->MAC); + if (!memcmp(addrrev, _packet->MAC, 6)){ + AddLog_P(LOG_LEVEL_ERROR, PSTR("ATC packet with reversed MAC addr?")); + } else { + AddLog_P(LOG_LEVEL_ERROR, PSTR("ATC packet with MAC addr mismatch - is this mesh?")); + memcpy(addrrev, _packet->MAC, 6); + } + addr = addrrev; + } + + uint32_t _slot = MIBLEgetSensorSlot(addr, 0x0a1c, _packet->frameCnt); // This must be a hard-coded fake ID + + if(_slot==0xff) return; + + if ((_slot >= 0) && (_slot < MIBLEsensors.size())){ + if (BLE_ESP32::BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG,PSTR("%s at slot %u"), kMI32DeviceType[MIBLEsensors[_slot].type-1],_slot); + MIBLEsensors[_slot].RSSI=RSSI; + MIBLEsensors[_slot].needkey=KEY_NOT_REQUIRED; + + MIBLEsensors[_slot].temp = (float)(int16_t(__builtin_bswap16(_packet->temp)))/10.0f; + MIBLEsensors[_slot].hum = (float)_packet->hum; + MIBLEsensors[_slot].eventType.tempHum = 1; + MIBLEsensors[_slot].bat = _packet->batPer; + MIBLEsensors[_slot].eventType.bat = 1; + + if(MI32.option.directBridgeMode) { + MIBLEsensors[_slot].shallSendMQTT = 1; + MI32.mode.shallTriggerTele = 1; + } + } else { + + } +} + +//////////////////////////////////////////////////////////// +// this SHOULD parse any MI payload. +int MI32parseMiPayload(int _slot, struct mi_beacon_data_t *parsed){ + struct mi_beacon_data_payload_data_t *pld = + (struct mi_beacon_data_payload_data_t *) &parsed->payload.data; + int res = 1; + + if (!parsed->payloadpresent){ + return 0; + } + + char tmp[20]; + BLE_ESP32::dump(tmp, 20, (uint8_t*)&(parsed->payload), parsed->payload.size+3); + if (BLE_ESP32::BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG_MORE,PSTR("MI%d payload %s"), _slot, tmp); + + switch(parsed->payload.type){ + case 0x01: // button press + MIBLEsensors[_slot].Btn = pld->Btn.num + (pld->Btn.longPress/2)*6; + MIBLEsensors[_slot].eventType.Btn = 1; + MI32.mode.shallTriggerTele = 1; + break; + case 0x02: + res = 0; + break; + case 0x03: {// motion? 1 byte + uint8_t motion = parsed->payload.data[0]; + res = 0; + }break; + case 0x04:{ + float _tempFloat=(float)(pld->temp)/10.0f; + if(_tempFloat<60){ + MIBLEsensors[_slot].temp=_tempFloat; + MIBLEsensors[_slot].eventType.temp = 1; + if (BLE_ESP32::BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG_MORE,PSTR("Mode 4: temp updated")); + } else { + if (BLE_ESP32::BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG_MORE,PSTR("Mode 4: temp ignored > 60 (%f)"), _tempFloat); + } + // AddLog_P(LOG_LEVEL_DEBUG,PSTR("Mode 4: U16: %u Temp"), _beacon.temp ); + } break; + case 0x06: { + float _tempFloat=(float)(pld->hum)/10.0f; + if(_tempFloat<101){ + MIBLEsensors[_slot].hum=_tempFloat; + MIBLEsensors[_slot].eventType.hum = 1; + if (BLE_ESP32::BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG_MORE,PSTR("Mode 6: hum updated")); + } else { + if (BLE_ESP32::BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG_MORE,PSTR("Mode 6: hum ignored > 101 (%f)"), _tempFloat); + } + // AddLog_P(LOG_LEVEL_DEBUG,PSTR("Mode 6: U16: %u Hum"), _beacon.hum); + } break; + case 0x07: + MIBLEsensors[_slot].lux=pld->lux & 0x00ffffff; + if(MIBLEsensors[_slot].type==MI_MJYD2S){ + MIBLEsensors[_slot].eventType.noMotion = 1; + } + MIBLEsensors[_slot].eventType.lux = 1; + // AddLog_P(LOG_LEVEL_DEBUG,PSTR("Mode 7: U24: %u Lux"), _beacon.lux & 0x00ffffff); + break; + case 0x08: + MIBLEsensors[_slot].moisture=pld->moist; + MIBLEsensors[_slot].eventType.moist = 1; + if (BLE_ESP32::BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG_MORE,PSTR("Mode 8: moisture updated")); + // AddLog_P(LOG_LEVEL_DEBUG,PSTR("Mode 8: U8: %u Moisture"), _beacon.moist); + break; + case 0x09: // 'conductivity' + MIBLEsensors[_slot].fertility=pld->fert; + MIBLEsensors[_slot].eventType.fert = 1; + if (BLE_ESP32::BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG_MORE,PSTR("Mode 9: fertility updated")); + // AddLog_P(LOG_LEVEL_DEBUG,PSTR("Mode 9: U16: %u Fertility"), _beacon.fert); + break; + case 0x0a: + if(MI32.option.ignoreBogusBattery){ + if(MIBLEsensors[_slot].type==MI_LYWSD03MMC || MIBLEsensors[_slot].type==MI_MHOC401){ + res = 0; + break; + } + } + if(pld->bat<101){ + MIBLEsensors[_slot].bat = pld->bat; + MIBLEsensors[_slot].eventType.bat = 1; + if (BLE_ESP32::BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG_MORE,PSTR("Mode a: bat updated")); + } else { + MIBLEsensors[_slot].bat = 100; + MIBLEsensors[_slot].eventType.bat = 1; + if (BLE_ESP32::BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG_MORE,PSTR("Mode a: bat > 100 (%d)"), pld->bat); + } + // AddLog_P(LOG_LEVEL_DEBUG,PSTR("Mode a: U8: %u %%"), _beacon.bat); + break; + case 0x0d:{ + float _tempFloat=(float)(pld->HT.temp)/10.0f; + if(_tempFloat < 60){ + MIBLEsensors[_slot].temp = _tempFloat; + if (BLE_ESP32::BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG_MORE,PSTR("Mode d: temp updated")); + } else { + if (BLE_ESP32::BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG_MORE,PSTR("Mode d: temp ignored > 60 (%f)"), _tempFloat); + } + _tempFloat=(float)(pld->HT.hum)/10.0f; + if(_tempFloat < 100){ + MIBLEsensors[_slot].hum = _tempFloat; + if (BLE_ESP32::BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG_MORE,PSTR("Mode d: hum updated")); + } else { + if (BLE_ESP32::BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG_MORE,PSTR("Mode d: hum ignored > 100 (%f)"), _tempFloat); + } + MIBLEsensors[_slot].eventType.tempHum = 1; + // AddLog_P(LOG_LEVEL_DEBUG,PSTR("Mode d: U16: %x Temp U16: %x Hum"), _beacon.HT.temp, _beacon.HT.hum); + } break; + case 0x0f: + if (parsed->payload.ten != 0) break; + MIBLEsensors[_slot].eventType.motion = 1; + MIBLEsensors[_slot].lastTime = millis(); + MIBLEsensors[_slot].events++; + MIBLEsensors[_slot].lux = pld->lux; + MIBLEsensors[_slot].eventType.lux = 1; + MIBLEsensors[_slot].NMT = 0; + MI32.mode.shallTriggerTele = 1; + // AddLog_P(LOG_LEVEL_DEBUG,PSTR("PIR: primary"),MIBLEsensors[_slot].lux ); + break; + case 0x10:{ // 'formaldehide' + const uint16_t f = uint16_t(parsed->payload.data[0]) | (uint16_t(parsed->payload.data[1]) << 8); + float formaldehyde = (float)f / 100.0f; + res = 0; + } break; + case 0x12:{ // 'active' + int active = parsed->payload.data[0]; + res = 0; + } break; + case 0x13:{ //mosquito tablet + int tablet = parsed->payload.data[0]; + res = 0; + } break; + case 0x17:{ + const uint32_t idle_time = + uint32_t(parsed->payload.data[0]) | (uint32_t(parsed->payload.data[1]) << 8) | (uint32_t(parsed->payload.data[2]) << 16) | (uint32_t(parsed->payload.data[2]) << 24); + float idlemins = (float)idle_time / 60.0f; + int has_motion = (idle_time) ? 0 : 0; + + MIBLEsensors[_slot].NMT = pld->NMT; + MIBLEsensors[_slot].eventType.NMT = 1; + MI32.mode.shallTriggerTele = 1; + // AddLog_P(LOG_LEVEL_DEBUG,PSTR("Mode 17: NMT: %u seconds"), _beacon.NMT); + } break; + + default: { + AddLog_P(LOG_LEVEL_DEBUG,PSTR("Unknown MI pld")); + res = 0; + } break; + } + + if(res && MI32.option.directBridgeMode) { + MIBLEsensors[_slot].shallSendMQTT = 1; + MI32.mode.shallTriggerTele = 1; + } + + return res; +} + +//////////////////////////////////////////////////////////// +// this SHOULD parse any MI packet, including encrypted. +void MI32ParseResponse(const uint8_t *buf, uint16_t bufsize, const uint8_t* addr, int RSSI) { + struct mi_beacon_data_t parsed; + memset(&parsed, 0, sizeof(parsed)); + int res = MIParsePacket(addr, &parsed, buf, bufsize); + + uint8_t addrrev[6]; + memcpy(addrrev, addr, 6); + MI32_ReverseMAC(addrrev); + + if (memcmp(addrrev, parsed.macdata.mac, 6)){ + AddLog_P(LOG_LEVEL_ERROR, PSTR("MI packet with MAC addr mismatch - is this mesh?")); + memcpy(addrrev, parsed.macdata.mac, 6); + MI32_ReverseMAC(addrrev); + addr = addrrev; + } + + uint16_t _slot = MIBLEgetSensorSlot( addr, parsed.devicetype, parsed.framecnt ); + if(_slot==0xff) return; + if ((_slot >= 0) && (_slot < MIBLEsensors.size())){ + if (parsed.needkey != KEY_REQUIREMENT_UNKNOWN){ + MIBLEsensors[_slot].needkey = parsed.needkey; + } + MIBLEsensors[_slot].RSSI=RSSI; + if (!res){ // - if the payload is not valid + if (BLE_ESP32::BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG, PSTR("MIParsePacket returned %d"), res); + return; + } else { + } + MI32parseMiPayload(_slot, &parsed); + } +} + +bool MI32isInBlockList(const uint8_t* MAC){ + bool isBlocked = false; + for(auto &_blockedMAC : MIBLEBlockList){ + if(memcmp(_blockedMAC.buf,MAC,6) == 0) isBlocked = true; + } + return isBlocked; +} + +void MI32removeMIBLEsensor(uint8_t* MAC){ + // this will take and keep the mutex until the function is over + TasAutoMutex localmutex(&slotmutex, "Mi32Rem"); + + MIBLEsensors.erase( std::remove_if( MIBLEsensors.begin() , MIBLEsensors.end(), [MAC]( mi_sensor_t _sensor )->bool + { return (memcmp(_sensor.MAC,MAC,6) == 0); } + ), end( MIBLEsensors ) ); +} +/***********************************************************************\ + * Read data from connections +\***********************************************************************/ + +void MI32notifyHT_LY(int slot, char *_buf, int len){ + if (BLE_ESP32::BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG_MORE,PSTR("%s: raw data: %x%x%x%x%x%x%x"),D_CMND_MI32,_buf[0],_buf[1],_buf[2],_buf[3],_buf[4],_buf[5],_buf[6]); + // the value 0b00 is 28.16 C? + if(_buf[0] != 0 || _buf[1] != 0){ + memcpy(&LYWSD0x_HT,(void *)_buf,sizeof(LYWSD0x_HT)); + if (BLE_ESP32::BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG, PSTR("%s: T * 100: %u, H: %u, V: %u"),D_CMND_MI32,LYWSD0x_HT.temp,LYWSD0x_HT.hum, LYWSD0x_HT.volt); + uint32_t _slot = slot; + + if (BLE_ESP32::BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG_MORE,PSTR("MIBLE: Sensor slot: %u"), _slot); + static float _tempFloat; + _tempFloat=(float)(LYWSD0x_HT.temp)/100.0f; + if(_tempFloat<60){ + MIBLEsensors[_slot].temp=_tempFloat; + // MIBLEsensors[_slot].showedUp=255; // this sensor is real + } + _tempFloat=(float)LYWSD0x_HT.hum; + if(_tempFloat<100){ + MIBLEsensors[_slot].hum = _tempFloat; + if (BLE_ESP32::BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG_MORE,PSTR("LYWSD0x: hum updated")); + } + MIBLEsensors[_slot].eventType.tempHum = 1; + if (MIBLEsensors[_slot].type == MI_LYWSD03MMC || MIBLEsensors[_slot].type == MI_MHOC401){ + // ok, so CR2032 is 3.0v, but drops immediately to ~2.9. + // so we'll go with the 2.1 min, 2.95 max. + float minVolts = 2100.0; + //float maxVolts = 2950.0; + //float range = maxVolts - minVolts; + //float divisor = range/100; // = 8.5 + float percent = (((float)LYWSD0x_HT.volt) - minVolts)/ 8.5; //divisor; + if (percent > 100) percent = 100; + + MIBLEsensors[_slot].bat = (int) percent; + MIBLEsensors[_slot].eventType.bat = 1; + } + if(MI32.option.directBridgeMode) { + MIBLEsensors[_slot].shallSendMQTT = 1; + MI32.mode.shallTriggerTele = 1; + } + } +} + + +/** + * @brief Launch functions from Core 1 to make race conditions less likely + * + */ + +void MI32Every50mSecond(){ + + if(MI32.mode.shallTriggerTele){ + MI32.mode.shallTriggerTele = 0; + MI32triggerTele(); + } +} + +/** + * @brief Main loop of the driver, "high level"-loop + * + */ + +void MI32EverySecond(bool restart){ + +// AddLog_P(LOG_LEVEL_DEBUG_MORE, PSTR("MI32: onesec")); + MI32TimeoutSensors(); + + MI32ShowSomeSensors(); + + // read a battery if + // MI32.batteryreader.slot < filled and !MI32.batteryreader.active + readOneBat(); + + + // read a sensor if + // MI32.sensorreader.slot < filled and !MI32.sensorreader.active + // for sensors which need to get data through notify... + readOneSensor(); + + if (MI32.secondsCounter >= MI32.period){ + // only if we finished the last read + if (MI32.sensorreader.slot >= MIBLEsensors.size()){ + AddLog_P(LOG_LEVEL_DEBUG,PSTR("kick off readOneSensor")); + // kick off notification sensor reading every period. + MI32.sensorreader.slot = 0; + MI32.secondsCounter = 0; + } + } + MI32.secondsCounter ++; + + if (MI32.secondsCounter2 >= MI32.period){ + if (MI32.mqttCurrentSlot >= MIBLEsensors.size()){ + AddLog_P(LOG_LEVEL_DEBUG,PSTR("kick off tele sending")); + MI32.mqttCurrentSlot = 0; + MI32.secondsCounter2 = 0; + } else { + AddLog_P(LOG_LEVEL_DEBUG,PSTR("hit tele time, restarted but not finished last - lost from slot %d")+MI32.mqttCurrentSlot); + MI32.mqttCurrentSlot = 0; + MI32.secondsCounter2 = 0; + } + } + MI32.secondsCounter2++; + + static uint32_t _counter = MI32.period - 15; + static uint32_t _nextSensorSlot = 0; + uint32_t _idx = 0; + + int numsensors = MIBLEsensors.size(); + for (uint32_t i = 0; i < numsensors; i++) { + if(MIBLEsensors[i].type==MI_NLIGHT || MIBLEsensors[i].type==MI_MJYD2S){ + MIBLEsensors[i].NMT++; + } + } + + if(MI32.mode.shallShowStatusInfo == 1){ + MI32StatusInfo(); + } +} + +/*********************************************************************************************\ + * Commands +\*********************************************************************************************/ + +void CmndMi32Period(void) { + if (XdrvMailbox.data_len > 0) { + if (1 == XdrvMailbox.payload) { + MI32EverySecond(true); + } else { + MI32.period = XdrvMailbox.payload; + } + } + ResponseCmndNumber(MI32.period); +} + +int findSlot(char *addrOrAlias){ + uint8_t mac[6]; + int res = BLE_ESP32::getAddr(mac, addrOrAlias); + if (!res) return -1; + + for (int i = MIBLEsensors.size()-1; i >= 0 ; i--) { + if (!memcmp(MIBLEsensors[i].MAC, mac, 6)){ + return i; + } + } + return -1; +} + + +void CmndMi32Time(void) { + if (XdrvMailbox.data_len > 0) { + int slot = findSlot(XdrvMailbox.data); + if (slot < 0) { + slot = XdrvMailbox.payload; + } + if (MIBLEsensors.size() > slot) { + int res = genericTimeWriteFn(slot); + if (res > 0){ + if (BLE_ESP32::BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG, PSTR("MI32: will set Time")); + ResponseCmndNumber(slot); + return; + } + if (res < 0) { + AddLog_P(LOG_LEVEL_ERROR, PSTR("MI32: cannot set Time on sensor type")); + } + if (res == 0) { + AddLog_P(LOG_LEVEL_ERROR, PSTR("MI32: cannot set Time right now")); + } + } + } + ResponseCmndChar_P("fail"); +} + +void CmndMi32Page(void) { + if (XdrvMailbox.payload > 0) { + MI32.perPage = XdrvMailbox.payload; + } + ResponseCmndNumber(MI32.perPage); +} + +// read ALL battery values where we can? +void CmndMi32Battery(void) { + // trigger a read cycle + MI32.batteryreader.slot = 0; + ResponseCmndDone(); +} + + +void CmndMi32Unit(void) { + if (XdrvMailbox.data_len > 0) { + int slot = findSlot(XdrvMailbox.data); + if (slot < 0) { + slot = XdrvMailbox.payload; + } + + if (MIBLEsensors.size() > slot) { + // TOGGLE unit? + int res = genericUnitWriteFn(slot, -1); + if (res > 0){ + if (BLE_ESP32::BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG, PSTR("MI32: will toggle Unit")); + ResponseCmndNumber(slot); + return; + } + if (res < 0) { + AddLog_P(LOG_LEVEL_ERROR, PSTR("MI32: cannot toggle Unit on sensor type")); + } + if (res == 0) { + AddLog_P(LOG_LEVEL_ERROR, PSTR("MI32: cannot toggle Unit right now")); + } + } + } + ResponseCmndIdxChar(PSTR("Invalid")); +} + +#ifdef USE_MI_DECRYPTION +void CmndMi32Key(void) { + if (44 == XdrvMailbox.data_len) { // a KEY-MAC-string + MI32AddKey(XdrvMailbox.data, nullptr); + MI32KeyListResp(); + } else { + ResponseCmndIdxChar(PSTR("Invalid")); + } +} +#endif // USE_MI_DECRYPTION + +void MI32BlockListResp(){ + Response_P(PSTR("{\"MI32Block\":{")); + for (int i = 0; i < MIBLEBlockList.size(); i++){ + if (i){ + ResponseAppend_P(PSTR(",")); + } + char tmp[20]; + ToHex_P(MIBLEBlockList[i].buf,6,tmp,20,0); + ResponseAppend_P(PSTR("\"%s\":1"), tmp); + } + ResponseAppend_P(PSTR("}}")); +} + + +void CmndMi32Block(void){ + if (XdrvMailbox.data_len == 0) { + switch (XdrvMailbox.index) { + case 0: { + //TasAutoMutex localmutex(&slotmutex, "Mi32Block1"); + MIBLEBlockList.clear(); + } break; + default: + case 1: + break; + } + MI32BlockListResp(); + return; + } + + MAC_t _MACasBytes; + int res = BLE_ESP32::getAddr(_MACasBytes.buf, XdrvMailbox.data); + if (!res){ + ResponseCmndIdxChar(PSTR("Addr invalid")); + return; + } + + //MI32HexStringToBytes(XdrvMailbox.data,_MACasBytes.buf); + switch (XdrvMailbox.index) { + case 0: { + //TasAutoMutex localmutex(&slotmutex, "Mi32Block2"); + MIBLEBlockList.erase( std::remove_if( begin( MIBLEBlockList ), end( MIBLEBlockList ), [_MACasBytes]( MAC_t& _entry )->bool + { return (memcmp(_entry.buf,_MACasBytes.buf,6) == 0); } + ), end( MIBLEBlockList ) ); + } break; + case 1: { + //TasAutoMutex localmutex(&slotmutex, "Mi32Block3"); + bool _notYetInList = true; + for (auto &_entry : MIBLEBlockList) { + if (memcmp(_entry.buf,_MACasBytes.buf,6) == 0){ + _notYetInList = false; + } + } + if (_notYetInList) { + MIBLEBlockList.push_back(_MACasBytes); + MI32removeMIBLEsensor(_MACasBytes.buf); + } + // AddLog_P(LOG_LEVEL_INFO,PSTR("MI32: size of ilist: %u"), MIBLEBlockList.size()); + } break; + } + MI32BlockListResp(); +} + +void CmndMi32Option(void){ + bool onOff = atoi(XdrvMailbox.data); + switch(XdrvMailbox.index) { + case 0: + MI32.option.allwaysAggregate = onOff; + break; + case 1: + MI32.option.noSummary = onOff; + break; + case 2: + MI32.option.directBridgeMode = onOff; + break; + case 4:{ + MI32.option.ignoreBogusBattery = onOff; + } break; + } + ResponseCmndDone(); +} + +void MI32KeyListResp(){ + Response_P(PSTR("{\"MIKeys\":{")); + for (int i = 0; i < MIBLEbindKeys.size(); i++){ + if (i){ + ResponseAppend_P(PSTR(",")); + } + char tmp[20]; + ToHex_P(MIBLEbindKeys[i].MAC,6,tmp,20,0); + char key[16*2+1]; + ToHex_P(MIBLEbindKeys[i].key,16,key,33,0); + + ResponseAppend_P(PSTR("\"%s\":\"%s\""), tmp, key); + } + ResponseAppend_P(PSTR("}}")); +} + + +void CmndMi32Keys(void){ +#ifdef BLE_ESP32_ALIASES + int op = XdrvMailbox.index; + if (BLE_ESP32::BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG,PSTR("key %d %s"), op, XdrvMailbox.data); + + int res = -1; + switch(op){ + case 0: + case 1:{ + char *p = strtok(XdrvMailbox.data, " ,="); + bool trigger = false; + int added = 0; + + do { + if (!p || !(*p)){ + break; + } + + uint8_t addr[6]; + char *mac = p; + int addrres = BLE_ESP32::getAddr(addr, p); + if (!addrres){ + ResponseCmndChar("invalidmac"); + return; + } + + p = strtok(nullptr, " ,="); + char *key = p; + if (!p || !(*p)){ + int i = 0; + for (i = 0; i < MIBLEbindKeys.size(); i++){ + mi_bindKey_t *key = &MIBLEbindKeys[i]; + if (!memcmp(key->MAC, addr, 6)){ + MIBLEbindKeys.erase(MIBLEbindKeys.begin() + i); + MI32KeyListResp(); + return; + } + } + ResponseCmndChar("invalidmac"); + return; + } + + AddLog_P(LOG_LEVEL_ERROR,PSTR("Add key mac %s = key %s"), mac, key); + char tmp[20]; + // convert mac back to string + ToHex_P(addr,6,tmp,20,0); + if (MI32AddKey(tmp, key)){ + added++; + } + p = strtok(nullptr, " ,="); + } while (p); + + if (added){ + if (BLE_ESP32::BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG,PSTR("Added %d Keys"), added); + MI32KeyListResp(); + } else { + MI32KeyListResp(); + } + return; + } break; + case 2:{ // clear + if (BLE_ESP32::BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG,PSTR("MI32Keys clearing %d"), MIBLEbindKeys.size()); + for (int i = MIBLEbindKeys.size()-1; i >= 0; i--){ + MIBLEbindKeys.pop_back(); + } + MI32KeyListResp(); + return; + } break; + } + ResponseCmndChar("invalididx"); +#endif +} + + +/*********************************************************************************************\ + * Presentation +\*********************************************************************************************/ + +const char HTTP_MI32[] PROGMEM = "{s}MI ESP32 v0918{m}%u%s / %u{e}"; +const char HTTP_MI32_ALIAS[] PROGMEM = "{s}%s Alias {m}%s{e}"; +const char HTTP_MI32_MAC[] PROGMEM = "{s}%s %s{m}%s{e}"; +const char HTTP_RSSI[] PROGMEM = "{s}%s " D_RSSI "{m}%d dBm{e}"; +const char HTTP_BATTERY[] PROGMEM = "{s}%s" " Battery" "{m}%u %%{e}"; +const char HTTP_LASTBUTTON[] PROGMEM = "{s}%s Last Button{m}%u {e}"; +const char HTTP_EVENTS[] PROGMEM = "{s}%s Events{m}%u {e}"; +const char HTTP_NMT[] PROGMEM = "{s}%s No motion{m}> %u seconds{e}"; +const char HTTP_MI32_FLORA_DATA[] PROGMEM = "{s}%s" " Fertility" "{m}%u us/cm{e}"; +const char HTTP_MI32_HL[] PROGMEM = "{s}
{m}
{e}"; + +//const char HTTP_NEEDKEY[] PROGMEM = "{s}%s %s{m} {e}"; + +//const char HTTP_NEEDKEY[] PROGMEM = "{s}%s %s{m} {e}"; +const char HTTP_NEEDKEY[] PROGMEM = "{s}%s %s{m} {e}"; + + +const char HTTP_PAIRING[] PROGMEM = "{s}%s Pair Button Pressed{m} {e}"; + + +const char HTTP_KEY_ERROR[] PROGMEM = "Key error %s"; +const char HTTP_MAC_ERROR[] PROGMEM = "MAC error %s"; +const char HTTP_KEY_ADDED[] PROGMEM = "Cmnd: MI32Keys %s=%s"; +const char HTTP_MI_KEY_STYLE[] PROGMEM = ""; + + +#define D_MI32_KEY "MI32 Set Key" + +void HandleMI32Key(){ + AddLog_P(LOG_LEVEL_DEBUG, PSTR("HandleMI32Key hit")); + if (!HttpCheckPriviledgedAccess()) { + AddLog_P(LOG_LEVEL_DEBUG, PSTR("!HttpCheckPriviledgedAccess()")); + return; + } + WSContentStart_P(PSTR(D_MI32_KEY)); + WSContentSendStyle_P(HTTP_MI_KEY_STYLE); + + char key[64] = {0}; + WebGetArg("key", key, sizeof(key)); + + if (strlen(key) != 16*2){ + WSContentSend_P(HTTP_KEY_ERROR, key); + WSContentStop(); + return; + } + + char mac[13] = {0}; + WebGetArg("mac", mac, sizeof(mac)); + if (strlen(mac) != 12){ + WSContentSend_P(HTTP_MAC_ERROR, mac); + WSContentStop(); + return; + } + + WSContentSend_P(HTTP_KEY_ADDED, mac, key); + + strncat(key, mac, sizeof(key)); + MI32AddKey(key, nullptr); + +// WSContentSpaceButton(BUTTON_CONFIGURATION); + WSContentStop(); +} + + +void MI32TimeoutSensors(){ + // whatever, this function access all the arrays.... + // so block for as long as it takes. + + // PROBLEM: when we take this, it hangs the BLE loop. + // BUT, devicePresent uses the + // remove devices for which the adverts have timed out + for (int i = MIBLEsensors.size()-1; i >= 0 ; i--) { + //if (MIBLEsensors[i].MAC[2] || MIBLEsensors[i].MAC[3] || MIBLEsensors[i].MAC[4] || MIBLEsensors[i].MAC[5]){ + if (!BLE_ESP32::devicePresent(MIBLEsensors[i].MAC)){ + uint8_t *mac = MIBLEsensors[i].MAC; + AddLog_P(LOG_LEVEL_DEBUG,PSTR("MI32: dev no longer present MAC: %02x%02x%02x%02x%02x%02x"), mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]); + TasAutoMutex localmutex(&slotmutex, "Mi32Timeout"); + MIBLEsensors.erase(MIBLEsensors.begin() + i); + } + //} + } +} + + +// this assumes that we're adding to a ResponseTime_P +void MI32GetOneSensorJson(int slot){ + mi_sensor_t *p; + p = &MIBLEsensors[slot]; + + ResponseAppend_P(PSTR(",\"%s-%02x%02x%02x\":{"), + kMI32DeviceType[p->type-1], + p->MAC[3], p->MAC[4], p->MAC[5]); + + ResponseAppend_P(PSTR("\"MAC\":\"%02x%02x%02x%02x%02x%02x\""), + p->MAC[0], p->MAC[1], p->MAC[2], + p->MAC[3], p->MAC[4], p->MAC[5]); + + if((!MI32.mode.triggeredTele && !MI32.option.minimalSummary)||MI32.mode.triggeredTele){ + bool tempHumSended = false; + if(p->feature.tempHum){ + if(p->eventType.tempHum || !MI32.mode.triggeredTele || MI32.option.allwaysAggregate){ + if (!isnan(p->hum) && !isnan(p->temp) +#ifdef USE_HOME_ASSISTANT + ||(hass_mode!=-1) +#endif //USE_HOME_ASSISTANT + ) { + ResponseAppend_P(PSTR(",")); + ResponseAppendTHD(p->temp, p->hum); + tempHumSended = true; + } + } + } + if(p->feature.temp && !tempHumSended){ + if(p->eventType.temp || !MI32.mode.triggeredTele || MI32.option.allwaysAggregate) { + if (!isnan(p->temp) +#ifdef USE_HOME_ASSISTANT + ||(hass_mode!=-1) +#endif //USE_HOME_ASSISTANT + ) { + char temperature[FLOATSZ]; + dtostrfd(p->temp, Settings.flag2.temperature_resolution, temperature); + ResponseAppend_P(PSTR(",\"" D_JSON_TEMPERATURE "\":%s"), temperature); + } + } + } + if(p->feature.hum && !tempHumSended){ + if(p->eventType.hum || !MI32.mode.triggeredTele || MI32.option.allwaysAggregate) { + if (!isnan(p->hum) +#ifdef USE_HOME_ASSISTANT + ||(hass_mode!=-1) +#endif //USE_HOME_ASSISTANT + ) { + char hum[FLOATSZ]; + dtostrfd(p->hum, Settings.flag2.humidity_resolution, hum); + ResponseAppend_P(PSTR(",\"" D_JSON_HUMIDITY "\":%s"), hum); + } + } + } + if (p->feature.lux){ + if(p->eventType.lux || !MI32.mode.triggeredTele || MI32.option.allwaysAggregate){ + if (p->lux!=0x0ffffff +#ifdef USE_HOME_ASSISTANT + ||(hass_mode!=-1) +#endif //USE_HOME_ASSISTANT + ) { // this is the error code -> no lux + ResponseAppend_P(PSTR(",\"" D_JSON_ILLUMINANCE "\":%u"), p->lux); +#ifdef USE_HOME_ASSISTANT + if (p->lux==0x0ffffff) MI32nullifyEndOfMQTT_DATA(); +#endif //USE_HOME_ASSISTANT + } + } + } + if (p->feature.moist){ + if(p->eventType.moist || !MI32.mode.triggeredTele || MI32.option.allwaysAggregate){ + if (p->moisture!=0xff +#ifdef USE_HOME_ASSISTANT + ||(hass_mode!=-1) +#endif //USE_HOME_ASSISTANT + ) { + ResponseAppend_P(PSTR(",\"" D_JSON_MOISTURE "\":%u"), p->moisture); +#ifdef USE_HOME_ASSISTANT + if (p->moisture==0xff) MI32nullifyEndOfMQTT_DATA(); +#endif //USE_HOME_ASSISTANT + } + } + } + if (p->feature.fert){ + if(p->eventType.fert || !MI32.mode.triggeredTele || MI32.option.allwaysAggregate){ + if (p->fertility!=0xffff +#ifdef USE_HOME_ASSISTANT + ||(hass_mode!=-1) +#endif //USE_HOME_ASSISTANT + ) { + ResponseAppend_P(PSTR(",\"Fertility\":%u"), p->fertility); +#ifdef USE_HOME_ASSISTANT + if (p->fertility==0xffff) MI32nullifyEndOfMQTT_DATA(); +#endif //USE_HOME_ASSISTANT + } + } + } + if (p->feature.Btn){ + if(p->eventType.Btn +#ifdef USE_HOME_ASSISTANT + ||(hass_mode==2) +#endif //USE_HOME_ASSISTANT + ){ + ResponseAppend_P(PSTR(",\"Btn\":%u"),p->Btn); + } + } + if(p->eventType.PairBtn && p->pairing){ + ResponseAppend_P(PSTR(",\"Pair\":%u"),p->pairing); + } + } // minimal summary + + + if (p->feature.PIR){ + if(p->eventType.motion || !MI32.mode.triggeredTele){ + if(MI32.mode.triggeredTele) ResponseAppend_P(PSTR(",\"PIR\":1")); // only real-time + ResponseAppend_P(PSTR(",\"Events\":%u"),p->events); + } + else if(p->eventType.noMotion && MI32.mode.triggeredTele){ + ResponseAppend_P(PSTR(",\"PIR\":0")); + } + } + + if (p->type == MI_FLORA && !MI32.mode.triggeredTele) { + if (p->firmware[0] != '\0') { // this is the error code -> no firmware + ResponseAppend_P(PSTR(",\"Firmware\":\"%s\""), p->firmware); + } + } + + if (p->feature.NMT || !MI32.mode.triggeredTele){ + if(p->eventType.NMT){ + ResponseAppend_P(PSTR(",\"NMT\":%u"), p->NMT); + } + } + if (p->feature.bat){ + if(p->eventType.bat || !MI32.mode.triggeredTele || MI32.option.allwaysAggregate){ + if (p->bat != 0x00 +#ifdef USE_HOME_ASSISTANT + ||(hass_mode!=-1) +#endif //USE_HOME_ASSISTANT + ) { // this is the error code -> no battery + ResponseAppend_P(PSTR(",\"Battery\":%u"), p->bat); +#ifdef USE_HOME_ASSISTANT + if (p->bat == 0x00) MI32nullifyEndOfMQTT_DATA(); +#endif //USE_HOME_ASSISTANT + } + } + } + if (MI32.option.showRSSI) ResponseAppend_P(PSTR(",\"RSSI\":%d"), p->RSSI); + + ResponseAppend_P(PSTR("}")); + p->eventType.raw = 0; + p->shallSendMQTT = 0; + +} + + +/////////////////////////////////////////////// +// starts a completely fresh MQTT message. +// sends up to 4 sensors +// triggered by setting MI32.mqttCurrentSlot = 0 +void MI32ShowSomeSensors(){ + // don't detect half-added ones here + int numsensors = MIBLEsensors.size(); + if (MI32.mqttCurrentSlot >= numsensors){ + // if we got to the end of the sensors, then don't send more + return; + } + +#ifdef USE_HOME_ASSISTANT + bool _noSummarySave = MI32.option.noSummary; + bool _minimalSummarySave = MI32.option.minimalSummary; + if(hass_mode==2){ + if(MI32.option.holdBackFirstAutodiscovery){ + if(!MI32.mode.firstAutodiscoveryDone){ + MI32.mode.firstAutodiscoveryDone = 1; + return; + } + } + MI32.option.noSummary = false; + MI32.option.minimalSummary = false; + } +#endif //USE_HOME_ASSISTANT + + ResponseTime_P(PSTR("")); + int cnt = 0; + for (; (MI32.mqttCurrentSlot < numsensors) && (cnt < 4); MI32.mqttCurrentSlot++, cnt++) { + MI32GetOneSensorJson(MI32.mqttCurrentSlot); + int mlen = strlen(TasmotaGlobal.mqtt_data); + + // if we ran out of room, leave here. + if (sizeof(TasmotaGlobal.mqtt_data) - mlen < 100){ + MI32.mqttCurrentSlot++; + break; + } + } + ResponseAppend_P(PSTR("}")); + MqttPublishPrefixTopic_P(TELE, PSTR(D_RSLT_SENSOR), Settings.flag.mqtt_sensor_retain); + //AddLog_P(LOG_LEVEL_DEBUG,PSTR("%s: show some %d %s"),D_CMND_MI32, MI32.mqttCurrentSlot, TasmotaGlobal.mqtt_data); + +#ifdef USE_RULES + RulesTeleperiod(); // Allow rule based HA messages +#endif // USE_RULES + +#ifdef USE_HOME_ASSISTANT + if(hass_mode==2){ + MI32.option.noSummary = _noSummarySave; + MI32.option.minimalSummary = _minimalSummarySave; + } +#endif //USE_HOME_ASSISTANT +} + +/////////////////////////////////////////////// +// starts a completely fresh MQTT message. +// sends up to 4 sensors pe5r msg +// sends only those which are raw and triggered. +// triggered by setting MI32.mode.triggeredTele = 1 +void MI32ShowTriggeredSensors(){ + if (!MI32.mode.triggeredTele) return; // none to show + MI32.mode.triggeredTele = 0; + + // don't detect half-added ones here + int numsensors = MIBLEsensors.size(); + + int sensor = 0; + + do { + ResponseTime_P(PSTR("")); + int cnt = 0; + for (; (sensor < numsensors) && (cnt < 4); sensor++) { + mi_sensor_t *p; + p = &MIBLEsensors[sensor]; + if(p->eventType.raw == 0) continue; + if(p->shallSendMQTT==0) continue; + + cnt++; + MI32GetOneSensorJson(sensor); + int mlen = strlen(TasmotaGlobal.mqtt_data); + + // if we ran out of room, leave here. + if (sizeof(TasmotaGlobal.mqtt_data) - mlen < 100){ + sensor++; + break; + } + } + if (cnt){ // if we got one, then publish + ResponseAppend_P(PSTR("}")); + MqttPublishPrefixTopic_P(STAT, PSTR(D_RSLT_SENSOR), Settings.flag.mqtt_sensor_retain); + AddLog_P(LOG_LEVEL_DEBUG,PSTR("%s: triggered %d %s"),D_CMND_MI32, sensor, TasmotaGlobal.mqtt_data); + +#ifdef USE_RULES + RulesTeleperiod(); // Allow rule based HA messages +#endif // USE_RULES + + } else { // else don't and clear + ResponseClear(); + } + } while (sensor < numsensors); +} + + +void MI32Show(bool json) +{ + // don't detect half-added ones here + int numsensors = MIBLEsensors.size(); + + if (json) { + // TELE JSON messages now do nothing here, apart from set MI32.mqttCurrentSlot + // which will trigger send next second of up to 4 sensors, then the next four in the next second, etc. + //MI32.mqttCurrentSlot = 0; + +#ifdef USE_WEBSERVER + } else { + static uint16_t _page = 0; + static uint16_t _counter = 0; + int32_t i = _page * MI32.perPage; + uint32_t j = i + MI32.perPage; + + if (j+1 > numsensors){ + j = numsensors; + } + char stemp[5] ={0}; + if (numsensors-(_page*MI32.perPage)>1 && MI32.perPage!=1) { + sprintf_P(stemp,"-%u",j); + } + if (numsensors==0) i=-1; // only for the GUI + + WSContentSend_PD(HTTP_MI32, i+1,stemp,numsensors); + for (i; itype-1]; + const char *alias = BLE_ESP32::getAlias(p->MAC); + if (alias && *alias){ + WSContentSend_PD(HTTP_MI32_ALIAS, typeName, alias); + } + char _MAC[18]; + ToHex_P(p->MAC,6,_MAC,18);//,':'); + WSContentSend_PD(HTTP_MI32_MAC, typeName, D_MAC_ADDRESS, _MAC); + WSContentSend_PD(HTTP_RSSI, typeName, p->RSSI); + + + // for some reason, display flora differently + switch(p->type){ + case MI_FLORA:{ + if (!isnan(p->temp)) { + char temperature[FLOATSZ]; + dtostrfd(p->temp, Settings.flag2.temperature_resolution, temperature); + WSContentSend_PD(HTTP_SNS_TEMP, typeName, temperature, TempUnit()); + } + if (p->moisture!=0xff) { + WSContentSend_PD(HTTP_SNS_MOISTURE, typeName, p->moisture); + } + if (p->fertility!=0xffff) { + WSContentSend_PD(HTTP_MI32_FLORA_DATA, typeName, p->fertility); + } + } break; + default:{ + if (!isnan(p->hum) && !isnan(p->temp)) { + WSContentSend_THD(typeName, p->temp, p->hum); + } + } + } + +#ifdef USE_MI_DECRYPTION + bool showkey = false; + char tmp[40]; + strcpy(tmp, PSTR("KeyRqd")); + switch(p->needkey) { + default:{ + snprintf(tmp, 39, PSTR("?%d?"), p->needkey ); + showkey = true; + } break; + case KEY_REQUIREMENT_UNKNOWN: { + strcpy(tmp, PSTR("WAIT")); + showkey = true; + } break; + case KEY_NOT_REQUIRED: { + strcpy(tmp, PSTR("NOTKEY")); + //showkey = true; + } break; + case KEY_REQUIRED_BUT_NOT_FOUND: { + strcpy(tmp, PSTR("NoKey")); + showkey = true; + } break; + case KEY_REQUIRED_AND_FOUND: { + strcpy(tmp, PSTR("KeyOk")); + showkey = true; + } break; + case KEY_REQUIRED_AND_INVALID: { + strcpy(tmp, PSTR("KeyInv")); + showkey = true; + } break; + } + + // adds the link to get the key. + // provides mac and callback address to receive the key, if we had a website which did this + // (future work) + if (showkey){ + BLE_ESP32::dump(_MAC, 13, p->MAC,6); + WSContentSend_PD(HTTP_NEEDKEY, typeName, _MAC, Webserver->client().localIP().toString().c_str(), tmp ); + } + + if (p->type==MI_NLIGHT || p->type==MI_MJYD2S) { +#else + if (p->type==MI_NLIGHT) { +#endif //USE_MI_DECRYPTION + WSContentSend_PD(HTTP_EVENTS, typeName, p->events); + if(p->NMT>0) WSContentSend_PD(HTTP_NMT, typeName, p->NMT); + } + + if (p->lux!=0x00ffffff) { // this is the error code -> no valid value + WSContentSend_PD(HTTP_SNS_ILLUMINANCE, typeName, p->lux); + } + if(p->bat!=0x00){ + WSContentSend_PD(HTTP_BATTERY, typeName, p->bat); + } + if (p->type==MI_YEERC){ + WSContentSend_PD(HTTP_LASTBUTTON, typeName, p->Btn); + } + if (p->pairing){ + WSContentSend_PD(HTTP_PAIRING, typeName); + } + } + _counter++; + if(_counter>3) { + _page++; + _counter=0; + } + if (MIBLEsensors.size()%MI32.perPage==0 && _page==MIBLEsensors.size()/MI32.perPage) { _page = 0; } + if (_page>MIBLEsensors.size()/MI32.perPage) { _page = 0; } +#endif // USE_WEBSERVER + } +} + +/*********************************************************************************************\ + * Interface +\*********************************************************************************************/ +#define WEB_HANDLE_MI32 "mikey" + +bool Xsns62(uint8_t function) +{ +// if (!Settings.flag5.mi32_enable) { return false; } // SetOption115 - Enable ESP32 MI32 BLE +// return false; + + bool result = false; + + switch (function) { + case FUNC_INIT: + MI32Init(); + break; + case FUNC_EVERY_50_MSECOND: + MI32Every50mSecond(); + break; + case FUNC_EVERY_SECOND: + MI32EverySecond(false); + break; + case FUNC_COMMAND: + result = DecodeCommand(kMI32_Commands, MI32_Commands); + break; + case FUNC_JSON_APPEND: + // we are not in control of when this is called... + //MI32Show(1); + break; +#ifdef USE_WEBSERVER + case FUNC_WEB_ADD_HANDLER: + WebServer_on(PSTR("/" WEB_HANDLE_MI32), HandleMI32Key); + break; + case FUNC_WEB_SENSOR: + MI32Show(0); + break; +#endif // USE_WEBSERVER + } + return result; +} +#endif // USE_MI_ESP32 +#endif // ESP32 + +#endif \ No newline at end of file