/* * Copyright 2020-2025 Ryan Powell and * esp-nimble-cpp, NimBLE-Arduino contributors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #include "NimBLECharacteristic.h" #if CONFIG_BT_NIMBLE_ENABLED && MYNEWT_VAL(BLE_ROLE_PERIPHERAL) # if defined(CONFIG_NIMBLE_CPP_IDF) # if !defined(ESP_IDF_VERSION_MAJOR) || ESP_IDF_VERSION_MAJOR < 5 # define ble_gatts_notify_custom ble_gattc_notify_custom # define ble_gatts_indicate_custom ble_gattc_indicate_custom # endif # endif # include "NimBLE2904.h" # include "NimBLEDevice.h" # include "NimBLELog.h" static NimBLECharacteristicCallbacks defaultCallback; static const char* LOG_TAG = "NimBLECharacteristic"; /** * @brief Construct a characteristic * @param [in] uuid - UUID (const char*) for the characteristic. * @param [in] properties - Properties for the characteristic. * @param [in] maxLen - The maximum length in bytes that the characteristic value can hold. (Default: 512 bytes for esp32, 20 for all others). * @param [in] pService - pointer to the service instance this characteristic belongs to. */ NimBLECharacteristic::NimBLECharacteristic(const char* uuid, uint16_t properties, uint16_t maxLen, NimBLEService* pService) : NimBLECharacteristic(NimBLEUUID(uuid), properties, maxLen, pService) {} /** * @brief Construct a characteristic * @param [in] uuid - UUID for the characteristic. * @param [in] properties - Properties for the characteristic. * @param [in] maxLen - The maximum length in bytes that the characteristic value can hold. (Default: 512 bytes for esp32, 20 for all others). * @param [in] pService - pointer to the service instance this characteristic belongs to. */ NimBLECharacteristic::NimBLECharacteristic(const NimBLEUUID& uuid, uint16_t properties, uint16_t maxLen, NimBLEService* pService) : NimBLELocalValueAttribute{uuid, 0, maxLen}, m_pCallbacks{&defaultCallback}, m_pService{pService} { setProperties(properties); } // NimBLECharacteristic /** * @brief Destructor. */ NimBLECharacteristic::~NimBLECharacteristic() { for (const auto& dsc : m_vDescriptors) { delete dsc; } } // ~NimBLECharacteristic /** * @brief Create a new BLE Descriptor associated with this characteristic. * @param [in] uuid - The UUID of the descriptor. * @param [in] properties - The properties of the descriptor. * @param [in] maxLen - The max length in bytes of the descriptor value. * @return The new BLE descriptor. */ NimBLEDescriptor* NimBLECharacteristic::createDescriptor(const char* uuid, uint32_t properties, uint16_t maxLen) { return createDescriptor(NimBLEUUID(uuid), properties, maxLen); } /** * @brief Create a new BLE Descriptor associated with this characteristic. * @param [in] uuid - The UUID of the descriptor. * @param [in] properties - The properties of the descriptor. * @param [in] maxLen - The max length in bytes of the descriptor value. * @return The new BLE descriptor. */ NimBLEDescriptor* NimBLECharacteristic::createDescriptor(const NimBLEUUID& uuid, uint32_t properties, uint16_t maxLen) { NimBLEDescriptor* pDescriptor = nullptr; if (uuid == NimBLEUUID(static_cast(0x2904))) { NIMBLE_LOGW(LOG_TAG, "0x2904 descriptor should be created with create2904()"); pDescriptor = create2904(); } else { pDescriptor = new NimBLEDescriptor(uuid, properties, maxLen, this); } addDescriptor(pDescriptor); return pDescriptor; } // createDescriptor /** * @brief Create a Characteristic Presentation Format Descriptor for this characteristic. * @return A pointer to a NimBLE2904 descriptor. */ NimBLE2904* NimBLECharacteristic::create2904() { NimBLE2904* pDescriptor = new NimBLE2904(this); addDescriptor(pDescriptor); return pDescriptor; } // create2904 /** * @brief Add a descriptor to the characteristic. * @param [in] pDescriptor A pointer to the descriptor to add. */ void NimBLECharacteristic::addDescriptor(NimBLEDescriptor* pDescriptor) { bool foundRemoved = false; if (pDescriptor->getRemoved() > 0) { for (const auto& dsc : m_vDescriptors) { if (dsc == pDescriptor) { foundRemoved = true; pDescriptor->setRemoved(0); } } } // Check if the descriptor is already in the vector and if so, return. for (const auto& dsc : m_vDescriptors) { if (dsc == pDescriptor) { pDescriptor->setCharacteristic(this); // Update the characteristic pointer in the descriptor. return; } } if (!foundRemoved) { m_vDescriptors.push_back(pDescriptor); } pDescriptor->setCharacteristic(this); NimBLEDevice::getServer()->serviceChanged(); } /** * @brief Remove a descriptor from the characteristic. * @param[in] pDescriptor A pointer to the descriptor instance to remove from the characteristic. * @param[in] deleteDsc If true it will delete the descriptor instance and free it's resources. */ void NimBLECharacteristic::removeDescriptor(NimBLEDescriptor* pDescriptor, bool deleteDsc) { // Check if the descriptor was already removed and if so, check if this // is being called to delete the object and do so if requested. // Otherwise, ignore the call and return. if (pDescriptor->getRemoved() > 0) { if (deleteDsc) { for (auto it = m_vDescriptors.begin(); it != m_vDescriptors.end(); ++it) { if ((*it) == pDescriptor) { delete (*it); m_vDescriptors.erase(it); break; } } } return; } pDescriptor->setRemoved(deleteDsc ? NIMBLE_ATT_REMOVE_DELETE : NIMBLE_ATT_REMOVE_HIDE); NimBLEDevice::getServer()->serviceChanged(); } // removeDescriptor /** * @brief Return the BLE Descriptor for the given UUID. * @param [in] uuid The UUID of the descriptor. * @return A pointer to the descriptor object or nullptr if not found. */ NimBLEDescriptor* NimBLECharacteristic::getDescriptorByUUID(const char* uuid) const { return getDescriptorByUUID(NimBLEUUID(uuid)); } // getDescriptorByUUID /** * @brief Return the BLE Descriptor for the given UUID. * @param [in] uuid The UUID of the descriptor. * @return A pointer to the descriptor object or nullptr if not found. */ NimBLEDescriptor* NimBLECharacteristic::getDescriptorByUUID(const NimBLEUUID& uuid) const { for (const auto& dsc : m_vDescriptors) { if (dsc->getUUID() == uuid) { return dsc; } } return nullptr; } // getDescriptorByUUID /** * @brief Return the BLE Descriptor for the given handle. * @param [in] handle The handle of the descriptor. * @return A pointer to the descriptor object or nullptr if not found. */ NimBLEDescriptor* NimBLECharacteristic::getDescriptorByHandle(uint16_t handle) const { for (const auto& dsc : m_vDescriptors) { if (dsc->getHandle() == handle) { return dsc; } } return nullptr; } // getDescriptorByHandle /** * @brief Get the properties of the characteristic. * @return The properties of the characteristic. */ uint16_t NimBLECharacteristic::getProperties() const { return m_properties; } // getProperties /** * @brief Get the service that owns this characteristic. */ NimBLEService* NimBLECharacteristic::getService() const { return m_pService; } // getService void NimBLECharacteristic::setService(NimBLEService* pService) { m_pService = pService; } // setService /** * @brief Send an indication. * @param[in] connHandle Connection handle to send an individual indication, or BLE_HS_CONN_HANDLE_NONE to send * the indication to all subscribed clients. * @return True if the indication was sent successfully, false otherwise. */ bool NimBLECharacteristic::indicate(uint16_t connHandle) const { auto value{m_value}; // make a copy to avoid issues if the value is changed while indicating return sendValue(value.data(), value.size(), false, connHandle); } // indicate /** * @brief Send an indication. * @param[in] value A pointer to the data to send. * @param[in] length The length of the data to send. * @param[in] connHandle Connection handle to send an individual indication, or BLE_HS_CONN_HANDLE_NONE to send * the indication to all subscribed clients. * @return True if the indication was sent successfully, false otherwise. */ bool NimBLECharacteristic::indicate(const uint8_t* value, size_t length, uint16_t connHandle) const { return sendValue(value, length, false, connHandle); } // indicate /** * @brief Send a notification. * @param[in] connHandle Connection handle to send an individual notification, or BLE_HS_CONN_HANDLE_NONE to send * the notification to all subscribed clients. * @return True if the notification was sent successfully, false otherwise. */ bool NimBLECharacteristic::notify(uint16_t connHandle) const { auto value{m_value}; // make a copy to avoid issues if the value is changed while notifying return sendValue(value.data(), value.size(), true, connHandle); } // notify /** * @brief Send a notification. * @param[in] value A pointer to the data to send. * @param[in] length The length of the data to send. * @param[in] connHandle Connection handle to send an individual notification, or BLE_HS_CONN_HANDLE_NONE to send * the notification to all subscribed clients. * @return True if the notification was sent successfully, false otherwise. */ bool NimBLECharacteristic::notify(const uint8_t* value, size_t length, uint16_t connHandle) const { return sendValue(value, length, true, connHandle); } // indicate /** * @brief Sends a notification or indication. * @param[in] value A pointer to the data to send. * @param[in] length The length of the data to send. * @param[in] isNotification if true sends a notification, false sends an indication. * @param[in] connHandle Connection handle to send to a specific peer. * @return True if the value was sent successfully, false otherwise. */ bool NimBLECharacteristic::sendValue(const uint8_t* value, size_t length, bool isNotification, uint16_t connHandle) const { ble_npl_hw_enter_critical(); const auto subs = getSubscribers(); // make a copy to avoid issues if subscribers change while sending ble_npl_hw_exit_critical(0); bool chSpecified = connHandle != BLE_HS_CONN_HANDLE_NONE; bool requireSecure = m_properties & (BLE_GATT_CHR_F_READ_ENC | BLE_GATT_CHR_F_READ_AUTHEN | BLE_GATT_CHR_F_READ_AUTHOR); int rc = chSpecified ? BLE_HS_ENOENT : 0; // if handle specified, assume not found until sent // Notify all connected peers unless a specific handle is provided for (const auto& entry : subs) { uint16_t ch = entry.getConnHandle(); if (ch == BLE_HS_CONN_HANDLE_NONE || (chSpecified && ch != connHandle)) { continue; } if (requireSecure && !entry.isSecured()) { NIMBLE_LOGW(LOG_TAG, "skipping notify/indicate to connHandle=%d, link not secured", entry.getConnHandle()); continue; } // Must re-create the data buffer on each iteration because it is freed by the calls below. uint8_t retries = 10; // wait up to 10ms for a free buffer os_mbuf* om = ble_hs_mbuf_from_flat(value, length); while (!om && --retries) { ble_npl_time_delay(ble_npl_time_ms_to_ticks32(1)); om = ble_hs_mbuf_from_flat(value, length); } if (!om) { rc = BLE_HS_ENOMEM; break; } if (isNotification) { rc = ble_gatts_notify_custom(ch, m_handle, om); } else { rc = ble_gatts_indicate_custom(ch, m_handle, om); } if (rc != 0 || chSpecified) { break; } } if (rc != 0) { NIMBLE_LOGE(LOG_TAG, "failed to send value, rc=%d %s", rc, NimBLEUtils::returnCodeToString(rc)); return false; } return true; } // sendValue /** * @brief Process a subscription or unsubscription request from a peer. * @param[in] connInfo A reference to the connection info of the peer. * @param[in] subVal The subscription value (bitmask). */ void NimBLECharacteristic::processSubRequest(NimBLEConnInfo& connInfo, uint8_t subVal) const { // Only allocate subscribers for characteristics that support notify or indicate. const uint16_t props = getProperties(); if (!(props & (BLE_GATT_CHR_F_NOTIFY | BLE_GATT_CHR_F_INDICATE))) { return; } auto found = false; auto firstFree = -1; for (auto& entry : m_subPeers) { if (entry.getConnHandle() == connInfo.getConnHandle()) { found = true; if (!subVal) { ble_npl_hw_enter_critical(); entry = SubPeerEntry{}; // unsubscribed, reset entry ble_npl_hw_exit_critical(0); } break; } if (firstFree == -1 && entry.getConnHandle() == BLE_HS_CONN_HANDLE_NONE) { firstFree = &entry - &m_subPeers[0]; } } if (!found && subVal) { if (firstFree >= 0) { ble_npl_hw_enter_critical(); m_subPeers[firstFree].setConnHandle(connInfo.getConnHandle()); m_subPeers[firstFree].setSubNotify(subVal & 0x1); m_subPeers[firstFree].setSubIndicate(subVal & 0x2); m_subPeers[firstFree].setSecured(connInfo.isEncrypted() || connInfo.isAuthenticated() || connInfo.isBonded()); if (m_properties & (BLE_GATT_CHR_F_READ_AUTHEN | BLE_GATT_CHR_F_READ_AUTHOR | BLE_GATT_CHR_F_READ_ENC)) { // characteristic requires security/authorization if (!m_subPeers[firstFree].isSecured()) { m_subPeers[firstFree].setAwaitingSecure(true); ble_npl_hw_exit_critical(0); NimBLEDevice::startSecurity(connInfo.getConnHandle()); NIMBLE_LOGD(LOG_TAG, "Subscription deferred until link is secured for connHandle=%d", connInfo.getConnHandle()); return; } } ble_npl_hw_exit_critical(0); } else { // should never happen, but log just in case NIMBLE_LOGE(LOG_TAG, "No free subscription slots"); return; } } m_pCallbacks->onSubscribe(const_cast(this), const_cast(connInfo), subVal); } /** * @brief Update the security status of a subscribed peer. * @param[in] peerInfo A reference to the connection info of the peer. */ void NimBLECharacteristic::updatePeerStatus(const NimBLEConnInfo& peerInfo) const { if (!(getProperties() & (NIMBLE_PROPERTY::NOTIFY | NIMBLE_PROPERTY::INDICATE))) { return; } ble_npl_hw_enter_critical(); for (auto& entry : m_subPeers) { if (entry.getConnHandle() == peerInfo.getConnHandle()) { entry.setSecured(peerInfo.isEncrypted() || peerInfo.isAuthenticated() || peerInfo.isBonded()); if (entry.isAwaitingSecure()) { entry.setAwaitingSecure(false); ble_npl_hw_exit_critical(0); m_pCallbacks->onSubscribe(const_cast(this), const_cast(peerInfo), entry.isSubNotify() | (entry.isSubIndicate() << 1)); return; } break; } } ble_npl_hw_exit_critical(0); } /** * @brief Handle a read event from a client. * @param [in] connInfo A reference to a NimBLEConnInfo instance containing the peer info. */ void NimBLECharacteristic::readEvent(NimBLEConnInfo& connInfo) { m_pCallbacks->onRead(this, connInfo); } // readEvent /** * @brief Handle a write event from a client. * @param [in] val A pointer to the data written by the client. * @param [in] len The length of the data written by the client. * @param [in] connInfo A reference to a NimBLEConnInfo instance containing the peer info. */ void NimBLECharacteristic::writeEvent(const uint8_t* val, uint16_t len, NimBLEConnInfo& connInfo) { setValue(val, len); m_pCallbacks->onWrite(this, connInfo); } // writeEvent /** * @brief Set the callback handlers for this characteristic. * @param [in] pCallbacks An instance of a NimBLECharacteristicCallbacks class\n * used to define any callbacks for the characteristic. */ void NimBLECharacteristic::setCallbacks(NimBLECharacteristicCallbacks* pCallbacks) { if (pCallbacks != nullptr) { m_pCallbacks = pCallbacks; } else { m_pCallbacks = &defaultCallback; } } // setCallbacks /** * @brief Get the callback handlers for this characteristic. */ NimBLECharacteristicCallbacks* NimBLECharacteristic::getCallbacks() const { return m_pCallbacks; } // getCallbacks /** * @brief Return a string representation of the characteristic. * @return A string representation of the characteristic. */ std::string NimBLECharacteristic::toString() const { std::string res = "UUID: " + m_uuid.toString() + ", handle : 0x"; char hex[5]; snprintf(hex, sizeof(hex), "%04x", getHandle()); res += hex; res += " "; if (m_properties & BLE_GATT_CHR_PROP_READ) res += "Read "; if (m_properties & BLE_GATT_CHR_PROP_WRITE) res += "Write "; if (m_properties & BLE_GATT_CHR_PROP_WRITE_NO_RSP) res += "WriteNoResponse "; if (m_properties & BLE_GATT_CHR_PROP_BROADCAST) res += "Broadcast "; if (m_properties & BLE_GATT_CHR_PROP_NOTIFY) res += "Notify "; if (m_properties & BLE_GATT_CHR_PROP_INDICATE) res += "Indicate "; return res; } // toString /** * @brief Callback function to support a read request. * @param [in] pCharacteristic The characteristic that is the source of the event. * @param [in] connInfo A reference to a NimBLEConnInfo instance containing the peer info. */ void NimBLECharacteristicCallbacks::onRead(NimBLECharacteristic* pCharacteristic, NimBLEConnInfo& connInfo) { NIMBLE_LOGD("NimBLECharacteristicCallbacks", "onRead: default"); } // onRead /** * @brief Callback function to support a write request. * @param [in] pCharacteristic The characteristic that is the source of the event. * @param [in] connInfo A reference to a NimBLEConnInfo instance containing the peer info. */ void NimBLECharacteristicCallbacks::onWrite(NimBLECharacteristic* pCharacteristic, NimBLEConnInfo& connInfo) { NIMBLE_LOGD("NimBLECharacteristicCallbacks", "onWrite: default"); } // onWrite /** * @brief Callback function to support a Notify/Indicate Status report. * @param [in] pCharacteristic The characteristic that is the source of the event. * @param [in] code Status return code from the NimBLE stack. * @details The status code for success is 0 for notifications and BLE_HS_EDONE for indications, * any other value is an error. */ void NimBLECharacteristicCallbacks::onStatus(NimBLECharacteristic* pCharacteristic, int code) { NIMBLE_LOGD("NimBLECharacteristicCallbacks", "onStatus: default"); } // onStatus /** * @brief Callback function to support a Notify/Indicate Status report. * @param [in] pCharacteristic The characteristic that is the source of the event. * @param [in] connInfo A reference to a NimBLEConnInfo instance containing the peer info. * @param [in] code Status return code from the NimBLE stack. * @details The status code for success is 0 for notifications and BLE_HS_EDONE for indications, * any other value is an error. */ void NimBLECharacteristicCallbacks::onStatus(NimBLECharacteristic* pCharacteristic, NimBLEConnInfo& connInfo, int code) { NIMBLE_LOGD("NimBLECharacteristicCallbacks", "onStatus: default"); } // onStatus /** * @brief Callback function called when a client changes subscription status. * @param [in] pCharacteristic The characteristic that is the source of the event. * @param [in] connInfo A reference to a NimBLEConnInfo instance containing the peer info. * @param [in] subValue The subscription status: * * 0 = Un-Subscribed * * 1 = Notifications * * 2 = Indications * * 3 = Notifications and Indications */ void NimBLECharacteristicCallbacks::onSubscribe(NimBLECharacteristic* pCharacteristic, NimBLEConnInfo& connInfo, uint16_t subValue) { NIMBLE_LOGD("NimBLECharacteristicCallbacks", "onSubscribe: default"); } #endif // CONFIG_BT_NIMBLE_ENABLED && MYNEWT_VAL(BLE_ROLE_PERIPHERAL)