/* * BLEDevice.cpp * * Created on: Mar 22, 2017 * Author: kolban */ #include "sdkconfig.h" #if defined(CONFIG_BT_ENABLED) #include #include #include #include #include "BLEClient.h" #include "BLEUtils.h" #include "BLEService.h" #include "GeneralUtils.h" #include #include #include #include "BLEDevice.h" #include "esp32-hal-log.h" /* * Design * ------ * When we perform a searchService() requests, we are asking the BLE server to return each of the services * that it exposes. For each service, we received an ESP_GATTC_SEARCH_RES_EVT event which contains details * of the exposed service including its UUID. * * The objects we will invent for a BLEClient will be as follows: * * BLERemoteService - A model of a remote service. * * BLERemoteCharacteristic - A model of a remote characteristic * * BLERemoteDescriptor - A model of a remote descriptor. * * Since there is a hierarchical relationship here, we will have the idea that from a BLERemoteService will own * zero or more remote characteristics and a BLERemoteCharacteristic will own zero or more remote BLEDescriptors. * * We will assume that a BLERemoteService contains a map that maps BLEUUIDs to the set of owned characteristics * and that a BLECharacteristic contains a map that maps BLEUUIDs to the set of owned descriptors. * * */ BLEClient::BLEClient() { m_pClientCallbacks = nullptr; m_conn_id = ESP_GATT_IF_NONE; m_gattc_if = ESP_GATT_IF_NONE; m_haveServices = false; m_isConnected = false; // Initially, we are flagged as not connected. } // BLEClient /** * @brief Destructor. */ BLEClient::~BLEClient() { // We may have allocated service references associated with this client. Before we are finished // with the client, we must release resources. for (auto &myPair : m_servicesMap) { delete myPair.second; } m_servicesMap.clear(); } // ~BLEClient /** * @brief Clear any existing services. * */ void BLEClient::clearServices() { log_v(">> clearServices"); // Delete all the services. for (auto &myPair : m_servicesMap) { delete myPair.second; } m_servicesMap.clear(); m_haveServices = false; log_v("<< clearServices"); } // clearServices /** * Add overloaded function to ease connect to peer device with not public address */ bool BLEClient::connect(BLEAdvertisedDevice* device) { BLEAddress address = device->getAddress(); esp_ble_addr_type_t type = device->getAddressType(); return connect(address, type); } /** * @brief Connect to the partner (BLE Server). * @param [in] address The address of the partner. * @return True on success. */ bool BLEClient::connect(BLEAddress address, esp_ble_addr_type_t type) { log_v(">> connect(%s)", address.toString().c_str()); // We need the connection handle that we get from registering the application. We register the app // and then block on its completion. When the event has arrived, we will have the handle. m_appId = BLEDevice::m_appId++; BLEDevice::addPeerDevice(this, true, m_appId); m_semaphoreRegEvt.take("connect"); // clearServices(); // we dont need to delete services since every client is unique? esp_err_t errRc = ::esp_ble_gattc_app_register(m_appId); if (errRc != ESP_OK) { log_e("esp_ble_gattc_app_register: rc=%d %s", errRc, GeneralUtils::errorToString(errRc)); return false; } m_semaphoreRegEvt.wait("connect"); m_peerAddress = address; // Perform the open connection request against the target BLE Server. m_semaphoreOpenEvt.take("connect"); errRc = ::esp_ble_gattc_open( m_gattc_if, *getPeerAddress().getNative(), // address type, // Note: This was added on 2018-04-03 when the latest ESP-IDF was detected to have changed the signature. 1 // direct connection <-- maybe needs to be changed in case of direct indirect connection??? ); if (errRc != ESP_OK) { log_e("esp_ble_gattc_open: rc=%d %s", errRc, GeneralUtils::errorToString(errRc)); return false; } uint32_t rc = m_semaphoreOpenEvt.wait("connect"); // Wait for the connection to complete. log_v("<< connect(), rc=%d", rc==ESP_GATT_OK); return rc == ESP_GATT_OK; } // connect /** * @brief Disconnect from the peer. * @return N/A. */ void BLEClient::disconnect() { log_v(">> disconnect()"); esp_err_t errRc = ::esp_ble_gattc_close(getGattcIf(), getConnId()); if (errRc != ESP_OK) { log_e("esp_ble_gattc_close: rc=%d %s", errRc, GeneralUtils::errorToString(errRc)); return; } log_v("<< disconnect()"); } // disconnect /** * @brief Handle GATT Client events */ void BLEClient::gattClientEventHandler( esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, esp_ble_gattc_cb_param_t* evtParam) { log_d("gattClientEventHandler [esp_gatt_if: %d] ... %s", gattc_if, BLEUtils::gattClientEventTypeToString(event).c_str()); // Execute handler code based on the type of event received. switch(event) { case ESP_GATTC_SRVC_CHG_EVT: log_i("SERVICE CHANGED"); break; case ESP_GATTC_CLOSE_EVT: { // esp_ble_gattc_app_unregister(m_appId); // BLEDevice::removePeerDevice(m_gattc_if, true); break; } // // ESP_GATTC_DISCONNECT_EVT // // disconnect: // - esp_gatt_status_t status // - uint16_t conn_id // - esp_bd_addr_t remote_bda case ESP_GATTC_DISCONNECT_EVT: { if (evtParam->disconnect.conn_id != getConnId()) break; // If we receive a disconnect event, set the class flag that indicates that we are // no longer connected. m_isConnected = false; if (m_pClientCallbacks != nullptr) { m_pClientCallbacks->onDisconnect(this); } esp_ble_gattc_app_unregister(m_gattc_if); m_semaphoreOpenEvt.give(ESP_GATT_IF_NONE); m_semaphoreRssiCmplEvt.give(); m_semaphoreSearchCmplEvt.give(1); BLEDevice::removePeerDevice(m_appId, true); break; } // ESP_GATTC_DISCONNECT_EVT // // ESP_GATTC_OPEN_EVT // // open: // - esp_gatt_status_t status // - uint16_t conn_id // - esp_bd_addr_t remote_bda // case ESP_GATTC_OPEN_EVT: { m_conn_id = evtParam->open.conn_id; if (m_pClientCallbacks != nullptr) { m_pClientCallbacks->onConnect(this); } if (evtParam->open.status == ESP_GATT_OK) { m_isConnected = true; // Flag us as connected. } m_semaphoreOpenEvt.give(evtParam->open.status); break; } // ESP_GATTC_OPEN_EVT // // ESP_GATTC_REG_EVT // // reg: // esp_gatt_status_t status // uint16_t app_id // case ESP_GATTC_REG_EVT: { m_gattc_if = gattc_if; m_semaphoreRegEvt.give(); break; } // ESP_GATTC_REG_EVT case ESP_GATTC_CFG_MTU_EVT: if (evtParam->cfg_mtu.conn_id != getConnId()) break; if(evtParam->cfg_mtu.status != ESP_GATT_OK) { log_e("Config mtu failed"); } m_mtu = evtParam->cfg_mtu.mtu; break; case ESP_GATTC_CONNECT_EVT: { if (evtParam->connect.conn_id != getConnId()) break; BLEDevice::updatePeerDevice(this, true, m_appId); esp_err_t errRc = esp_ble_gattc_send_mtu_req(gattc_if, evtParam->connect.conn_id); if (errRc != ESP_OK) { log_e("esp_ble_gattc_send_mtu_req: rc=%d %s", errRc, GeneralUtils::errorToString(errRc)); } #ifdef CONFIG_BLE_SMP_ENABLE // Check that BLE SMP (security) is configured in make menuconfig if(BLEDevice::m_securityLevel){ esp_ble_set_encryption(evtParam->connect.remote_bda, BLEDevice::m_securityLevel); } #endif // CONFIG_BLE_SMP_ENABLE break; } // ESP_GATTC_CONNECT_EVT // // ESP_GATTC_SEARCH_CMPL_EVT // // search_cmpl: // - esp_gatt_status_t status // - uint16_t conn_id // case ESP_GATTC_SEARCH_CMPL_EVT: { if (evtParam->search_cmpl.conn_id != getConnId()) break; esp_ble_gattc_cb_param_t* p_data = (esp_ble_gattc_cb_param_t*)evtParam; if (p_data->search_cmpl.status != ESP_GATT_OK){ log_e("search service failed, error status = %x", p_data->search_cmpl.status); break; } #ifndef ARDUINO_ARCH_ESP32 // commented out just for now to keep backward compatibility // if(p_data->search_cmpl.searched_service_source == ESP_GATT_SERVICE_FROM_REMOTE_DEVICE) { // log_i("Get service information from remote device"); // } else if (p_data->search_cmpl.searched_service_source == ESP_GATT_SERVICE_FROM_NVS_FLASH) { // log_i("Get service information from flash"); // } else { // log_i("unknown service source"); // } #endif m_semaphoreSearchCmplEvt.give(0); break; } // ESP_GATTC_SEARCH_CMPL_EVT // // ESP_GATTC_SEARCH_RES_EVT // // search_res: // - uint16_t conn_id // - uint16_t start_handle // - uint16_t end_handle // - esp_gatt_id_t srvc_id // case ESP_GATTC_SEARCH_RES_EVT: { if (evtParam->search_res.conn_id != getConnId()) break; BLEUUID uuid = BLEUUID(evtParam->search_res.srvc_id); BLERemoteService* pRemoteService = new BLERemoteService( evtParam->search_res.srvc_id, this, evtParam->search_res.start_handle, evtParam->search_res.end_handle ); m_servicesMap.insert(std::pair(uuid.toString(), pRemoteService)); m_servicesMapByInstID.insert(std::pair(pRemoteService, evtParam->search_res.srvc_id.inst_id)); break; } // ESP_GATTC_SEARCH_RES_EVT default: { break; } } // Switch // Pass the request on to all services. for (auto &myPair : m_servicesMap) { myPair.second->gattClientEventHandler(event, gattc_if, evtParam); } } // gattClientEventHandler uint16_t BLEClient::getConnId() { return m_conn_id; } // getConnId esp_gatt_if_t BLEClient::getGattcIf() { return m_gattc_if; } // getGattcIf /** * @brief Retrieve the address of the peer. * * Returns the Bluetooth device address of the %BLE peer to which this client is connected. */ BLEAddress BLEClient::getPeerAddress() { return m_peerAddress; } // getAddress /** * @brief Ask the BLE server for the RSSI value. * @return The RSSI value. */ int BLEClient::getRssi() { log_v(">> getRssi()"); if (!isConnected()) { log_v("<< getRssi(): Not connected"); return 0; } // We make the API call to read the RSSI value which is an asynchronous operation. We expect to receive // an ESP_GAP_BLE_READ_RSSI_COMPLETE_EVT to indicate completion. // m_semaphoreRssiCmplEvt.take("getRssi"); esp_err_t rc = ::esp_ble_gap_read_rssi(*getPeerAddress().getNative()); if (rc != ESP_OK) { log_e("<< getRssi: esp_ble_gap_read_rssi: rc=%d %s", rc, GeneralUtils::errorToString(rc)); return 0; } int rssiValue = m_semaphoreRssiCmplEvt.wait("getRssi"); log_v("<< getRssi(): %d", rssiValue); return rssiValue; } // getRssi /** * @brief Get the service BLE Remote Service instance corresponding to the uuid. * @param [in] uuid The UUID of the service being sought. * @return A reference to the Service or nullptr if don't know about it. */ BLERemoteService* BLEClient::getService(const char* uuid) { return getService(BLEUUID(uuid)); } // getService /** * @brief Get the service object corresponding to the uuid. * @param [in] uuid The UUID of the service being sought. * @return A reference to the Service or nullptr if don't know about it. * @throws BLEUuidNotFound */ BLERemoteService* BLEClient::getService(BLEUUID uuid) { log_v(">> getService: uuid: %s", uuid.toString().c_str()); // Design // ------ // We wish to retrieve the service given its UUID. It is possible that we have not yet asked the // device what services it has in which case we have nothing to match against. If we have not // asked the device about its services, then we do that now. Once we get the results we can then // examine the services map to see if it has the service we are looking for. if (!m_haveServices) { getServices(); } std::string uuidStr = uuid.toString(); for (auto &myPair : m_servicesMap) { if (myPair.first == uuidStr) { log_v("<< getService: found the service with uuid: %s", uuid.toString().c_str()); return myPair.second; } } // End of each of the services. log_v("<< getService: not found"); return nullptr; } // getService /** * @brief Ask the remote %BLE server for its services. * A %BLE Server exposes a set of services for its partners. Here we ask the server for its set of * services and wait until we have received them all. * @return N/A */ std::map* BLEClient::getServices() { /* * Design * ------ * We invoke esp_ble_gattc_search_service. This will request a list of the service exposed by the * peer BLE partner to be returned as events. Each event will be an an instance of ESP_GATTC_SEARCH_RES_EVT * and will culminate with an ESP_GATTC_SEARCH_CMPL_EVT when all have been received. */ log_v(">> getServices"); // TODO implement retrieving services from cache clearServices(); // Clear any services that may exist. esp_err_t errRc = esp_ble_gattc_search_service( getGattcIf(), getConnId(), NULL // Filter UUID ); m_semaphoreSearchCmplEvt.take("getServices"); if (errRc != ESP_OK) { log_e("esp_ble_gattc_search_service: rc=%d %s", errRc, GeneralUtils::errorToString(errRc)); return &m_servicesMap; } // If sucessfull, remember that we now have services. m_haveServices = (m_semaphoreSearchCmplEvt.wait("getServices") == 0); log_v("<< getServices"); return &m_servicesMap; } // getServices /** * @brief Get the value of a specific characteristic associated with a specific service. * @param [in] serviceUUID The service that owns the characteristic. * @param [in] characteristicUUID The characteristic whose value we wish to read. * @throws BLEUuidNotFound */ std::string BLEClient::getValue(BLEUUID serviceUUID, BLEUUID characteristicUUID) { log_v(">> getValue: serviceUUID: %s, characteristicUUID: %s", serviceUUID.toString().c_str(), characteristicUUID.toString().c_str()); std::string ret = getService(serviceUUID)->getCharacteristic(characteristicUUID)->readValue(); log_v("<read_rssi_cmpl.rssi); break; } // ESP_GAP_BLE_READ_RSSI_COMPLETE_EVT default: break; } } // handleGAPEvent /** * @brief Are we connected to a partner? * @return True if we are connected and false if we are not connected. */ bool BLEClient::isConnected() { return m_isConnected; } // isConnected /** * @brief Set the callbacks that will be invoked. */ void BLEClient::setClientCallbacks(BLEClientCallbacks* pClientCallbacks) { m_pClientCallbacks = pClientCallbacks; } // setClientCallbacks /** * @brief Set the value of a specific characteristic associated with a specific service. * @param [in] serviceUUID The service that owns the characteristic. * @param [in] characteristicUUID The characteristic whose value we wish to write. * @throws BLEUuidNotFound */ void BLEClient::setValue(BLEUUID serviceUUID, BLEUUID characteristicUUID, std::string value) { log_v(">> setValue: serviceUUID: %s, characteristicUUID: %s", serviceUUID.toString().c_str(), characteristicUUID.toString().c_str()); getService(serviceUUID)->getCharacteristic(characteristicUUID)->writeValue(value); log_v("<< setValue"); } // setValue uint16_t BLEClient::getMTU() { return m_mtu; } /** * @brief Return a string representation of this client. * @return A string representation of this client. */ std::string BLEClient::toString() { std::string res = "peer address: " + m_peerAddress.toString(); res += "\nServices:\n"; for (auto &myPair : m_servicesMap) { res += myPair.second->toString() + "\n"; // myPair.second is the value } return res; } // toString #endif // CONFIG_BT_ENABLED