/* 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("BLE: 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("BLE: Delete device %s(%s) by age lastseen %u + maxage %u < now %u."), addr, alias, lastseenS, ageS, nowS); } else { AddLog_P(LOG_LEVEL_INFO,PSTR("BLE: 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("BLE: 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("BLE: 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("BLE: 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("BLE: 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("BLE: 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("BLE: 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("BLE: scancompleteCallbacks %d %d"), i, callbackres); #endif } catch(const std::exception& e){ #ifdef BLE_ESP32_DEBUG AddLog_P(LOG_LEVEL_ERROR,PSTR("BLE: 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("BLE: Notify: no remote char!!??")); #endif return; } #ifdef BLE_ESP32_DEBUG if (BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG,PSTR("BLE: 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("BLE: Notify: no remote service found")); #endif return; } pRClient = pSvc->getClient(); if (!pRClient){ #ifdef BLE_ESP32_DEBUG AddLog_P(LOG_LEVEL_ERROR,PSTR("BLE: 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("BLE: 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("BLE: 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; } 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 } // 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("BLE: %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("BLE: Task:Stopping NimBLE")); (*ppClient)->setClientCallbacks(nullptr, false); try { if ((*ppClient)->isConnected()){ #ifdef BLE_ESP32_DEBUG AddLog_P(LOG_LEVEL_INFO,PSTR("BLE: disconnecting connected client")); #endif (*ppClient)->disconnect(); } NimBLEDevice::deleteClient((*ppClient)); (*ppClient) = nullptr; #ifdef BLE_ESP32_DEBUG AddLog_P(LOG_LEVEL_INFO,PSTR("BLE: deleted client")); #endif } catch(const std::exception& e){ #ifdef BLE_ESP32_DEBUG AddLog_P(LOG_LEVEL_ERROR,PSTR("BLE: 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("BLE: 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("BLE: 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("BLE: 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("BLE: 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("BLE: 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("BLE: 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("BLE: 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("BLE: 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("BLE: 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("BLE: 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("BLE: 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("BLE: 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("BLE: 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("BLE: 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("BLE: 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("BLE: 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("BLE: characteristic can't notify")); #endif } } } else { newstate = GEN_STATE_FAILED_NONOTIFYCHAR; #ifdef BLE_ESP32_DEBUG AddLog_P(LOG_LEVEL_ERROR,PSTR("BLE: 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("BLE: 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("BLE: read characteristic with readmodifywritecallback")); #endif pFn(op); } else { #ifdef BLE_ESP32_DEBUG if (BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG,PSTR("BLE: 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("BLE: characteristic write fail")); #endif } else { if (!waitNotify) newstate = GEN_STATE_WRITEDONE; #ifdef BLE_ESP32_DEBUG if (BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG,PSTR("BLE: 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("BLE: 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("BLE: 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("BLE: 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("BLE: 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("BLE: 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("BLE: 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("BLE: >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("BLE: 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("BLE: 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("BLE: 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("BLE: 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("BLE: 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("BLE: 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("BLE: StartBLE - wait as BLEStop==1")); return 0; } static int StopBLE(void){ if (BLERunning){ if (BLEStop != 1){ BLEStop = 1; AddLog_P(LOG_LEVEL_INFO,PSTR("BLE: 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("BLE: StopBLE - wait as BLEStop==1")); return 0; } else { AddLog_P(LOG_LEVEL_ERROR,PSTR("BLE: 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("BLE: 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("BLE: 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("BLE: 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("BLE: Added %d Aliases"), added); BLEAliasListResp(); } else { BLEAliasListResp(); } return; } break; case 2:{ // clear if (BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG,PSTR("BLE: Alias 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("BLE: Can't get a newOperation")); ResponseCmndChar(PSTR("FAIL")); return; } else { if (BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG,PSTR("BLE: 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("BLE: 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("BLE: read name")); op->readlen = 1; } res = BLE_ESP32::extQueueOperation(&op); if (BLEDebugMode > 0) AddLog_P(LOG_LEVEL_DEBUG,PSTR("BLE: 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("BLE: 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("BLE: 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("BLE: 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("BLE: 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("BLE: 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("BLE: 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("BLE: 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("BLE: 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("BLE: 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("BLE: prep sent %s"), out.c_str()); #endif } if (queuedOperations.size() && !onlycompleted){ #ifdef BLE_ESP32_DEBUG if (BLEDebugMode > 0) AddLog_P(LOG_LEVEL_INFO,PSTR("BLE: 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("BLE: 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("BLE: 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("BLE: 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("BLE: 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("BLE: 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("BLE: op->completecallback %d"), callbackres); #endif } catch(const std::exception& e){ #ifdef BLE_ESP32_DEBUG AddLog_P(LOG_LEVEL_ERROR,PSTR("BLE: 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("BLE: 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("BLE: 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("BLE: 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("BLE: 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("BLE: HandleBleConfiguration")); #endif if (!HttpCheckPriviledgedAccess()) { #ifdef BLE_ESP32_DEBUG AddLog_P(LOG_LEVEL_DEBUG, PSTR("BLE: !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("BLE: 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("BLE: !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("BLE: Stopping if active")); 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("BLE: 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("BLE: 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("BLE: 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("BLE: Failed to queue new operation - deleted")); return; } #endif } #endif #endif // ESP32