Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,22 @@
# Changelog
All notable changes to this project will be documented in this file.

## [Unreleased] 3.0.0-dev

## Fixed
- `NimBLEUUID` construction from C strings now initializes the UUID type and parses 16-, 32-, and 128-bit UUID strings correctly.
- Converting `NimBLEAttValue` to Arduino `String` now preserves the full value length, including embedded null bytes.

## Changed
- **C++20 is now required.** Support for C++11/14/17 has been removed; builds must use `-std=gnu++20` or newer.
- The `Has_data_size`, `Has_c_str_length` and `Has_value_type` SFINAE traits and the pre-C++17 `enable_if` overloads were replaced with C++20 concepts (`NimBLEDataContainer`, `NimBLEStringLike`, `NimBLECharArray`, `NimBLEByteValue`) and a single `NimBLEAsBytes` helper.
- `NimBLEAddress(std::string_view, uint8_t)` and the advertising/HID string setters (`setName`, `setShortName`, `setURI`, `setManufacturerData`, `setServiceData`, `addData`, `setManufacturer`) now take `std::string_view`.
- `NimBLEAdvertisedDevice::getManufacturerData<T>`/`getServiceData<T>` are now restricted to trivially copyable types and copy the bytes instead of casting the buffer.

## Added
- `NimBLEAttValue` can be constructed from and converted to `std::string_view`.
- `std::string_view` and `std::span` can be passed to the `setValue`, `writeValue`, `notify` and `indicate` templates.

## [2.5.0] 2026-04-01

## Fixed
Expand Down
4 changes: 2 additions & 2 deletions src/NimBLEAddress.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ NimBLEAddress::NimBLEAddress(ble_addr_t address) : ble_addr_t{address} {}
* * BLE_ADDR_PUBLIC (0)
* * BLE_ADDR_RANDOM (1)
*/
NimBLEAddress::NimBLEAddress(const std::string& addr, uint8_t type) {
NimBLEAddress::NimBLEAddress(std::string_view addr, uint8_t type) {
this->type = type;

if (addr.length() == BLE_DEV_ADDR_LEN) {
Expand All @@ -93,7 +93,7 @@ NimBLEAddress::NimBLEAddress(const std::string& addr, uint8_t type) {
}

*this = NimBLEAddress{};
NIMBLE_LOGE(LOG_TAG, "Invalid address '%s'", addr.c_str());
NIMBLE_LOGE(LOG_TAG, "Invalid address '%.*s'", static_cast<int>(addr.length()), addr.data());
} // NimBLEAddress

/**
Expand Down
4 changes: 2 additions & 2 deletions src/NimBLEAddress.h
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
/**************************/

# include <string>

# include <string_view>
/**
* @brief A BLE device address.
*
Expand All @@ -47,7 +47,7 @@ class NimBLEAddress : private ble_addr_t {
NimBLEAddress() = default;
NimBLEAddress(const ble_addr_t address);
NimBLEAddress(const uint8_t address[BLE_DEV_ADDR_LEN], uint8_t type);
NimBLEAddress(const std::string& stringAddress, uint8_t type);
NimBLEAddress(std::string_view stringAddress, uint8_t type);
NimBLEAddress(const uint64_t& address, uint8_t type);

bool isRpa() const;
Expand Down
33 changes: 21 additions & 12 deletions src/NimBLEAdvertisedDevice.h
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,14 @@
# endif

# include <vector>
# include <string>
# include <string_view>
# include <algorithm>
# include <cstring>
# include <type_traits>

class NimBLEScan;

/**
* @brief A representation of a %BLE advertised device found by a scan.
*
Expand Down Expand Up @@ -109,11 +115,9 @@ class NimBLEAdvertisedDevice {
* @details <b>Use:</b> <tt>getManufacturerData<type>(skipSizeCheck);</tt>
*/
template <typename T>
requires std::is_trivially_copyable_v<T>
T getManufacturerData(bool skipSizeCheck = false) const {
std::string data = getManufacturerData();
if (!skipSizeCheck && data.size() < sizeof(T)) return T();
const char* pData = data.data();
return *((T*)pData);
return fromBytes<T>(getManufacturerData(), skipSizeCheck);
}

/**
Expand All @@ -126,11 +130,9 @@ class NimBLEAdvertisedDevice {
* @details <b>Use:</b> <tt>getServiceData<type>(skipSizeCheck);</tt>
*/
template <typename T>
requires std::is_trivially_copyable_v<T>
T getServiceData(uint8_t index = 0, bool skipSizeCheck = false) const {
std::string data = getServiceData(index);
if (!skipSizeCheck && data.size() < sizeof(T)) return T();
const char* pData = data.data();
return *((T*)pData);
return fromBytes<T>(getServiceData(index), skipSizeCheck);
}

/**
Expand All @@ -143,16 +145,23 @@ class NimBLEAdvertisedDevice {
* @details <b>Use:</b> <tt>getServiceData<type>(skipSizeCheck);</tt>
*/
template <typename T>
requires std::is_trivially_copyable_v<T>
T getServiceData(const NimBLEUUID& uuid, bool skipSizeCheck = false) const {
std::string data = getServiceData(uuid);
if (!skipSizeCheck && data.size() < sizeof(T)) return T();
const char* pData = data.data();
return *((T*)pData);
return fromBytes<T>(getServiceData(uuid), skipSizeCheck);
}

private:
friend class NimBLEScan;

// Copies the bytes into a T instead of reinterpreting the buffer, avoiding unaligned/aliased access.
template <typename T>
static T fromBytes(std::string_view data, bool skipSizeCheck) {
T value{};
if (!skipSizeCheck && data.size() < sizeof(T)) return value;
std::memcpy(&value, data.data(), std::min(data.size(), sizeof(T)));
return value;
}

NimBLEAdvertisedDevice(const ble_gap_event* event, uint8_t eventType);
void update(const ble_gap_event* event, uint8_t eventType);
uint8_t findAdvField(uint8_t type, uint8_t index = 0, size_t* data_loc = nullptr) const;
Expand Down
14 changes: 7 additions & 7 deletions src/NimBLEAdvertisementData.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -284,7 +284,7 @@ bool NimBLEAdvertisementData::setManufacturerData(const uint8_t* data, size_t le
* @param [in] data The manufacturer data to advertise.
* @return True if successful.
*/
bool NimBLEAdvertisementData::setManufacturerData(const std::string& data) {
bool NimBLEAdvertisementData::setManufacturerData(std::string_view data) {
return setManufacturerData(reinterpret_cast<const uint8_t*>(data.data()), data.length());
} // setManufacturerData

Expand All @@ -302,7 +302,7 @@ bool NimBLEAdvertisementData::setManufacturerData(const std::vector<uint8_t>& da
* @param [in] uri The uri to advertise.
* @return True if successful.
*/
bool NimBLEAdvertisementData::setURI(const std::string& uri) {
bool NimBLEAdvertisementData::setURI(std::string_view uri) {
if (uri.length() > BLE_HS_ADV_MAX_FIELD_SZ) {
NIMBLE_LOGE(LOG_TAG, "URI too long");
return false;
Expand All @@ -312,7 +312,7 @@ bool NimBLEAdvertisementData::setURI(const std::string& uri) {
uint8_t length = 2 + uri.length();
data[0] = length - 1;
data[1] = BLE_HS_ADV_TYPE_URI;
memcpy(&data[2], uri.c_str(), uri.length());
memcpy(&data[2], uri.data(), uri.length());
return addData(data, length);
} // setURI

Expand All @@ -324,7 +324,7 @@ bool NimBLEAdvertisementData::setURI(const std::string& uri) {
* and the data type will be set to incomplete name.
* @return True if successful.
*/
bool NimBLEAdvertisementData::setName(const std::string& name, bool isComplete) {
bool NimBLEAdvertisementData::setName(std::string_view name, bool isComplete) {
if (name.length() > BLE_HS_ADV_MAX_FIELD_SZ) {
NIMBLE_LOGE(LOG_TAG, "Name too long - truncating");
isComplete = false;
Expand All @@ -334,7 +334,7 @@ bool NimBLEAdvertisementData::setName(const std::string& name, bool isComplete)
uint8_t length = 2 + std::min<uint8_t>(name.length(), BLE_HS_ADV_MAX_FIELD_SZ);
data[0] = length - 1;
data[1] = isComplete ? BLE_HS_ADV_TYPE_COMP_NAME : BLE_HS_ADV_TYPE_INCOMP_NAME;
memcpy(&data[2], name.c_str(), std::min<uint8_t>(name.length(), BLE_HS_ADV_MAX_FIELD_SZ));
memcpy(&data[2], name.data(), std::min<uint8_t>(name.length(), BLE_HS_ADV_MAX_FIELD_SZ));
return addData(data, length);
} // setName

Expand All @@ -343,7 +343,7 @@ bool NimBLEAdvertisementData::setName(const std::string& name, bool isComplete)
* @param [in] name The short name of the device.
* @return True if successful.
*/
bool NimBLEAdvertisementData::setShortName(const std::string& name) {
bool NimBLEAdvertisementData::setShortName(std::string_view name) {
return setName(name, false);
} // setShortName

Expand Down Expand Up @@ -501,7 +501,7 @@ bool NimBLEAdvertisementData::setServiceData(const NimBLEUUID& uuid, const uint8
* @return True if the service data was set successfully.
* @note If data length is 0 the service data will not be advertised.
*/
bool NimBLEAdvertisementData::setServiceData(const NimBLEUUID& uuid, const std::string& data) {
bool NimBLEAdvertisementData::setServiceData(const NimBLEUUID& uuid, std::string_view data) {
return setServiceData(uuid, reinterpret_cast<const uint8_t*>(data.data()), data.length());
} // setServiceData

Expand Down
11 changes: 6 additions & 5 deletions src/NimBLEAdvertisementData.h
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@

# include <cstdint>
# include <string>
# include <string_view>
# include <vector>

class NimBLEUUID;
Expand All @@ -46,19 +47,19 @@ class NimBLEAdvertisementData {
bool removeServiceUUID(const char* serviceUUID);
bool removeServices();
bool setManufacturerData(const uint8_t* data, size_t length);
bool setManufacturerData(const std::string& data);
bool setManufacturerData(std::string_view data);
bool setManufacturerData(const std::vector<uint8_t>& data);
bool setURI(const std::string& uri);
bool setName(const std::string& name, bool isComplete = true);
bool setShortName(const std::string& name);
bool setURI(std::string_view uri);
bool setName(std::string_view name, bool isComplete = true);
bool setShortName(std::string_view name);
bool setCompleteServices(const NimBLEUUID& uuid);
bool setCompleteServices16(const std::vector<NimBLEUUID>& uuids);
bool setCompleteServices32(const std::vector<NimBLEUUID>& uuids);
bool setPartialServices(const NimBLEUUID& uuid);
bool setPartialServices16(const std::vector<NimBLEUUID>& uuids);
bool setPartialServices32(const std::vector<NimBLEUUID>& uuids);
bool setServiceData(const NimBLEUUID& uuid, const uint8_t* data, size_t length);
bool setServiceData(const NimBLEUUID& uuid, const std::string& data);
bool setServiceData(const NimBLEUUID& uuid, std::string_view data);
bool setServiceData(const NimBLEUUID& uuid, const std::vector<uint8_t>& data);
bool removeData(uint8_t type);
void clearData();
Expand Down
8 changes: 4 additions & 4 deletions src/NimBLEAdvertising.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -516,7 +516,7 @@ bool NimBLEAdvertising::addTxPower() {
* @note If the name is too long it will be truncated.
* @details If scan response is enabled the name will be set in the scan response data.
*/
bool NimBLEAdvertising::setName(const std::string& name) {
bool NimBLEAdvertising::setName(std::string_view name) {
if (m_scanResp && m_scanData.setName(name)) {
m_advDataSet = false;
return true;
Expand Down Expand Up @@ -552,7 +552,7 @@ bool NimBLEAdvertising::setManufacturerData(const uint8_t* data, size_t length)
* @param [in] data The data to advertise.
* @return True if the manufacturer data was set successfully.
*/
bool NimBLEAdvertising::setManufacturerData(const std::string& data) {
bool NimBLEAdvertising::setManufacturerData(std::string_view data) {
return setManufacturerData(reinterpret_cast<const uint8_t*>(data.data()), data.length());
} // setManufacturerData

Expand All @@ -570,7 +570,7 @@ bool NimBLEAdvertising::setManufacturerData(const std::vector<uint8_t>& data) {
* @param [in] uri The URI to advertise.
* @return True if the URI was set successfully.
*/
bool NimBLEAdvertising::setURI(const std::string& uri) {
bool NimBLEAdvertising::setURI(std::string_view uri) {
if (!m_advData.setURI(uri)) {
if (!m_scanData.setURI(uri)) {
return false;
Expand Down Expand Up @@ -618,7 +618,7 @@ bool NimBLEAdvertising::setServiceData(const NimBLEUUID& uuid, const std::vector
* @return True if the service data was set successfully.
* @note If data length is 0 the service data will not be advertised.
*/
bool NimBLEAdvertising::setServiceData(const NimBLEUUID& uuid, const std::string& data) {
bool NimBLEAdvertising::setServiceData(const NimBLEUUID& uuid, std::string_view data) {
return setServiceData(uuid, reinterpret_cast<const uint8_t*>(data.data()), data.length());
} // setServiceData

Expand Down
9 changes: 5 additions & 4 deletions src/NimBLEAdvertising.h
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@

# include <functional>
# include <string>
# include <string_view>
# include <vector>

class NimBLEAdvertising;
Expand Down Expand Up @@ -79,13 +80,13 @@ class NimBLEAdvertising {
bool setAppearance(uint16_t appearance);
bool setPreferredParams(uint16_t minInterval, uint16_t maxInterval);
bool addTxPower();
bool setName(const std::string& name);
bool setName(std::string_view name);
bool setManufacturerData(const uint8_t* data, size_t length);
bool setManufacturerData(const std::string& data);
bool setManufacturerData(std::string_view data);
bool setManufacturerData(const std::vector<uint8_t>& data);
bool setURI(const std::string& uri);
bool setURI(std::string_view uri);
bool setServiceData(const NimBLEUUID& uuid, const uint8_t* data, size_t length);
bool setServiceData(const NimBLEUUID& uuid, const std::string& data);
bool setServiceData(const NimBLEUUID& uuid, std::string_view data);
bool setServiceData(const NimBLEUUID& uuid, const std::vector<uint8_t>& data);

private:
Expand Down
47 changes: 29 additions & 18 deletions src/NimBLEAttValue.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -58,13 +58,10 @@ unsigned char __atomic_exchange_1(volatile void* ptr, unsigned char val, int) {
# endif

static const char* LOG_TAG = "NimBLEAttValue";
ble_npl_mutex NimBLEAttValue::Lock::m_mutex{};
std::atomic<bool> NimBLEAttValue::Lock::m_initialized{false};
std::atomic<int> NimBLEAttValue::Lock::m_activeCount{0};

// Default constructor implementation.
NimBLEAttValue::NimBLEAttValue(uint16_t init_len, uint16_t max_len)
: m_attr_value{static_cast<uint8_t*>(calloc(init_len + 1, 1))},
: m_attr_value{init_len > 0 ? static_cast<uint8_t*>(calloc(init_len + 1, 1)) : &EMPTY_VALUE},
m_attr_max_len{std::min<uint16_t>(BLE_ATT_ATTR_MAX_LEN, max_len)},
m_attr_len{},
m_capacity{init_len}
Expand All @@ -75,39 +72,47 @@ NimBLEAttValue::NimBLEAttValue(uint16_t init_len, uint16_t max_len)
{
NIMBLE_CPP_DEBUG_ASSERT(m_attr_value);
if (m_attr_value == nullptr) {
// Keep max_len so the object can still allocate later; callers detect the failure by size() != requested len.
NIMBLE_LOGE(LOG_TAG, "Failed to allocate value buffer");
m_attr_len = 0;
m_capacity = 0;
m_attr_max_len = 0;
m_attr_value = &EMPTY_VALUE;
m_capacity = 0;
}
}

// Value constructor implementation.
NimBLEAttValue::NimBLEAttValue(const uint8_t* value, uint16_t len, uint16_t max_len) : NimBLEAttValue(len, max_len) {
if (m_attr_value != nullptr && len > 0) {
NimBLEAttValue::NimBLEAttValue(const uint8_t* value, uint16_t len, uint16_t max_len)
: NimBLEAttValue(len > std::min<uint16_t>(max_len, BLE_ATT_ATTR_MAX_LEN) ? 0 : len, max_len) {
// Same behavior as append(): a value larger than max_len is rejected, leaving the value empty.
if (len > m_attr_max_len) {
NIMBLE_LOGE(LOG_TAG, "val > max, len=%u, max=%u", len, m_attr_max_len);
return;
}

if (len > 0 && m_capacity >= len) {
memcpy(m_attr_value, value, len);
m_attr_len = len;
}
}

// Destructor implementation.
NimBLEAttValue::~NimBLEAttValue() {
if (m_attr_value != nullptr) {
free(m_attr_value);
}
free(ownedBuffer());
}

// Move assignment operator implementation.
NimBLEAttValue& NimBLEAttValue::operator=(NimBLEAttValue&& source) {
if (this != &source) {
Lock lock;
free(m_attr_value);
free(ownedBuffer());
m_attr_value = source.m_attr_value;
m_attr_max_len = source.m_attr_max_len;
m_attr_len = source.m_attr_len;
m_capacity = source.m_capacity;
setTimeStamp(source.getTimeStamp());
source.m_attr_value = nullptr;
source.m_attr_value = &EMPTY_VALUE;
source.m_attr_len = 0;
source.m_capacity = 0;
source.setTimeStamp(0);
}

return *this;
Expand All @@ -123,8 +128,12 @@ NimBLEAttValue& NimBLEAttValue::operator=(const NimBLEAttValue& source) {

// Copy all the data from the source object to this object, including allocated space.
bool NimBLEAttValue::deepCopy(const NimBLEAttValue& source) {
if (this == &source) {
return true; // Self-copy; reallocating would leave an aliased, uninitialized buffer when source is EMPTY_VALUE.
}

Lock lock;
uint8_t* res = static_cast<uint8_t*>(realloc(m_attr_value, source.m_capacity + 1));
uint8_t* res = static_cast<uint8_t*>(realloc(ownedBuffer(), source.m_capacity + 1));
NIMBLE_CPP_DEBUG_ASSERT(res);
if (res == nullptr) {
NIMBLE_LOGE(LOG_TAG, "Failed to realloc deepCopy");
Expand All @@ -144,9 +153,11 @@ bool NimBLEAttValue::deepCopy(const NimBLEAttValue& source) {
bool NimBLEAttValue::setValue(const uint8_t* value, uint16_t len) {
Lock lock;
m_attr_len = 0; // Just set the value length to 0 and append instead of repeating code.
m_attr_value[0] = '\0'; // Set the first byte to 0 incase the len of the new value is 0.
if (m_attr_value != &EMPTY_VALUE) {
m_attr_value[0] = '\0'; // Set the first byte to 0 incase the len of the new value is 0.
}
append(value, len);
return memcmp(m_attr_value, value, len) == 0 && m_attr_len == len;
return m_attr_len == len && memcmp(m_attr_value, value, len) == 0;
}

// Append the new data, allocate as necessary.
Expand All @@ -165,7 +176,7 @@ NimBLEAttValue& NimBLEAttValue::append(const uint8_t* value, uint16_t len) {
uint16_t new_len = m_attr_len + len;
bool grow = new_len > m_capacity;
if (grow) {
res = static_cast<uint8_t*>(realloc(m_attr_value, (new_len + 1)));
res = static_cast<uint8_t*>(realloc(ownedBuffer(), (new_len + 1)));
}
NIMBLE_CPP_DEBUG_ASSERT(res);
if (res == nullptr) {
Expand Down
Loading
Loading