diff --git a/esphome/components/ble_client/sensor/ble_sensor.cpp b/esphome/components/ble_client/sensor/ble_sensor.cpp index 60992f282e4..5dbb7e42ed2 100644 --- a/esphome/components/ble_client/sensor/ble_sensor.cpp +++ b/esphome/components/ble_client/sensor/ble_sensor.cpp @@ -61,7 +61,7 @@ void BLESensor::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t ga break; } this->handle = chr->handle; - if (this->descr_uuid_.get_uuid().len > 0) { + if (this->descr_uuid_.is_set()) { auto *descr = chr->get_descriptor(this->descr_uuid_); if (descr == nullptr) { this->status_set_warning(); diff --git a/esphome/components/ble_client/text_sensor/ble_text_sensor.cpp b/esphome/components/ble_client/text_sensor/ble_text_sensor.cpp index 6f092819224..ed2b0a63a0e 100644 --- a/esphome/components/ble_client/text_sensor/ble_text_sensor.cpp +++ b/esphome/components/ble_client/text_sensor/ble_text_sensor.cpp @@ -61,7 +61,7 @@ void BLETextSensor::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_ break; } this->handle = chr->handle; - if (this->descr_uuid_.get_uuid().len > 0) { + if (this->descr_uuid_.is_set()) { auto *descr = chr->get_descriptor(this->descr_uuid_); if (descr == nullptr) { this->status_set_warning(); diff --git a/esphome/components/ble_device_base/ble_device.cpp b/esphome/components/ble_device_base/ble_device.cpp index 2235c085981..9256270b7aa 100644 --- a/esphome/components/ble_device_base/ble_device.cpp +++ b/esphome/components/ble_device_base/ble_device.cpp @@ -98,6 +98,8 @@ ESPBTUUID ESPBTUUID::from_raw(const char *data, size_t length) { #ifdef USE_ESP32 ESPBTUUID ESPBTUUID::from_uuid(esp_bt_uuid_t uuid) { + if (uuid.len == 0) // the unset sentinel get_uuid() emits + return {}; if (uuid.len == ESP_UUID_LEN_16) return ESPBTUUID::from_uint16(uuid.uuid.uuid16); if (uuid.len == ESP_UUID_LEN_32) @@ -108,6 +110,10 @@ ESPBTUUID ESPBTUUID::from_uuid(esp_bt_uuid_t uuid) { esp_bt_uuid_t ESPBTUUID::get_uuid() const { esp_bt_uuid_t ret; switch (this->type_) { + case Type::UNSET: + ret.len = 0; + memset(&ret.uuid, 0, sizeof(ret.uuid)); + break; case Type::UUID16: ret.len = ESP_UUID_LEN_16; ret.uuid.uuid16 = this->uuid_.uuid16; @@ -139,7 +145,8 @@ void ESPBTDevice::parse_scan_rst(const esp32_ble::BLEScanResult &scan_result) { #endif // USE_ESP32 ESPBTUUID ESPBTUUID::as_128bit() const { - if (this->type_ == Type::UUID128) + // Widening an unset UUID stays unset; expanding it would produce a set 0x0000 base UUID. + if (this->type_ == Type::UNSET || this->type_ == Type::UUID128) return *this; uint8_t data[16]; this->to_128bit_(data); @@ -149,6 +156,8 @@ ESPBTUUID ESPBTUUID::as_128bit() const { bool ESPBTUUID::contains(uint8_t data1, uint8_t data2) const { // Adjacent byte-pair search — identical semantics to esp32_ble::ESPBTUUID::contains. switch (this->type_) { + case Type::UNSET: + return false; case Type::UUID16: return (this->uuid_.uuid16 >> 8) == data2 && (this->uuid_.uuid16 & 0xFF) == data1; case Type::UUID32: @@ -173,6 +182,9 @@ const char *ESPBTUUID::to_str(char *buf) const { // Identical output format to esp32_ble::ESPBTUUID::to_str. char *pos = buf; switch (this->type_) { + case Type::UNSET: + memcpy(buf, "None", 5); + return buf; case Type::UUID16: *pos++ = '0'; *pos++ = 'x'; @@ -207,6 +219,7 @@ const char *ESPBTUUID::to_str(char *buf) const { void ESPBTUUID::to_128bit_(uint8_t out[16]) const { // Bluetooth Base UUID 00000000-0000-1000-8000-00805F9B34FB (LSB-first), with the 16/32-bit // value placed at bytes 12..; identical expansion to esp32_ble::ESPBTUUID::as_128bit(). + // Callers screen out UNSET first (operator==, as_128bit); it would expand like 0x0000. static const uint8_t BASE[16] = {0xFB, 0x34, 0x9B, 0x5F, 0x80, 0x00, 0x00, 0x80, 0x00, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; if (this->type_ == Type::UUID128) { @@ -223,6 +236,8 @@ void ESPBTUUID::to_128bit_(uint8_t out[16]) const { bool ESPBTUUID::operator==(const ESPBTUUID &other) const { if (this->type_ == other.type_) { switch (this->type_) { + case Type::UNSET: + return true; case Type::UUID16: return this->uuid_.uuid16 == other.uuid_.uuid16; case Type::UUID32: @@ -232,6 +247,9 @@ bool ESPBTUUID::operator==(const ESPBTUUID &other) const { } return false; } + // Unset never equals a set UUID; 0x0000 is a valid value, distinct from "not configured". + if (this->type_ == Type::UNSET || other.type_ == Type::UNSET) + return false; // Different widths: expand both to the 128-bit Bluetooth Base UUID form and compare, so a // configured 16/32-bit UUID matches the equivalent 128-bit advertisement (esp32 parity). uint8_t a[16]; diff --git a/esphome/components/ble_device_base/ble_device.h b/esphome/components/ble_device_base/ble_device.h index 716bc026f22..ee3256d11f2 100644 --- a/esphome/components/ble_device_base/ble_device.h +++ b/esphome/components/ble_device_base/ble_device.h @@ -91,8 +91,11 @@ class ESPBTUUID { #if defined(__cpp_lib_span) const char *to_str(std::span output) const { return this->to_str(output.data()); } #endif - enum class Type : uint8_t { UUID16, UUID32, UUID128 }; + // UNSET is the default-constructed state; get_uuid() reports it as len 0 (the historical sentinel). + enum class Type : uint8_t { UNSET, UUID16, UUID32, UUID128 }; Type type() const { return this->type_; } + /// True if a UUID has been configured (not default-constructed). + bool is_set() const { return this->type_ != Type::UNSET; } uint16_t uuid16() const { return this->uuid_.uuid16; } uint32_t uuid32() const { return this->uuid_.uuid32; } const uint8_t *uuid128() const { return this->uuid_.uuid128; } @@ -101,7 +104,7 @@ class ESPBTUUID { // Expand to the 128-bit Bluetooth Base UUID byte form (out is 16 bytes, little-endian). void to_128bit_(uint8_t out[16]) const; - Type type_{Type::UUID16}; + Type type_{Type::UNSET}; union { uint16_t uuid16; uint32_t uuid32; diff --git a/tests/components/ble_device_base/test_ble_uuid.cpp b/tests/components/ble_device_base/test_ble_uuid.cpp index 45abd484796..7e99ce8a954 100644 --- a/tests/components/ble_device_base/test_ble_uuid.cpp +++ b/tests/components/ble_device_base/test_ble_uuid.cpp @@ -27,6 +27,60 @@ TEST(BleDeviceUuid, ThirtyTwoBitMatchesEquivalentLongForm) { EXPECT_TRUE(u32 == u128); } +// A default-constructed UUID is UNSET, the historical "not configured" sentinel +// (len 0 through the esp32 get_uuid() adapter). +TEST(BleDeviceUuid, DefaultConstructedIsUnset) { + const ESPBTUUID unset; + EXPECT_EQ(unset.type(), ESPBTUUID::Type::UNSET); + EXPECT_TRUE(unset == ESPBTUUID()); + EXPECT_FALSE(unset == ESPBTUUID::from_uint16(0x1234)); + EXPECT_FALSE(unset.contains(0x00, 0x00)); +} + +// Every factory yields a non-UNSET UUID, even for 0x0000: only default construction and a +// failed text parse are unset, keeping type() != UNSET equivalent to the old len > 0 check. +TEST(BleDeviceUuid, AllFactoriesProduceSetUuids) { + const uint8_t raw[16] = {0xFB, 0x34, 0x9B, 0x5F, 0x80, 0x00, 0x00, 0x80, + 0x00, 0x10, 0x00, 0x00, 0x34, 0x12, 0x00, 0x00}; + EXPECT_NE(ESPBTUUID::from_uint16(0x0000).type(), ESPBTUUID::Type::UNSET); + EXPECT_NE(ESPBTUUID::from_uint32(0).type(), ESPBTUUID::Type::UNSET); + EXPECT_NE(ESPBTUUID::from_raw(raw).type(), ESPBTUUID::Type::UNSET); + EXPECT_NE(ESPBTUUID::from_raw_reversed(raw).type(), ESPBTUUID::Type::UNSET); + EXPECT_NE(ESPBTUUID::from_raw("180F", 4).type(), ESPBTUUID::Type::UNSET); + EXPECT_NE(ESPBTUUID::from_raw("0000180F", 8).type(), ESPBTUUID::Type::UNSET); + EXPECT_NE(ESPBTUUID::from_raw(reinterpret_cast(raw), 16).type(), ESPBTUUID::Type::UNSET); + EXPECT_NE(ESPBTUUID::from_raw("6E400001-B5A3-F393-E0A9-E50E24DCCA9E").type(), ESPBTUUID::Type::UNSET); +} + +// 0x0000 is a valid short UUID on real devices (esphome/aioesphomeapi#1742); an unset +// UUID must never compare equal to it. Unset equals only unset. +TEST(BleDeviceUuid, UnsetIsNotEqualToZeroUuid) { + EXPECT_FALSE(ESPBTUUID() == ESPBTUUID::from_uint16(0x0000)); + EXPECT_FALSE(ESPBTUUID::from_uint16(0x0000) == ESPBTUUID()); + const uint8_t base[16] = {0xFB, 0x34, 0x9B, 0x5F, 0x80, 0x00, 0x00, 0x80, + 0x00, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + EXPECT_FALSE(ESPBTUUID() == ESPBTUUID::from_raw(base)); + EXPECT_TRUE(ESPBTUUID() == ESPBTUUID()); + EXPECT_FALSE(ESPBTUUID().as_128bit().is_set()); // widening preserves the unset state + // A configured 0x0000 still matches its own 128-bit base UUID expansion. + EXPECT_TRUE(ESPBTUUID::from_uint16(0x0000) == ESPBTUUID::from_raw(base)); +} + +// is_set() is the sentinel check; an unset UUID prints as "None" instead of a +// valid-looking all-zero 128-bit UUID. +TEST(BleDeviceUuid, IsSetAndUnsetToStr) { + char buf[UUID_STR_LEN]; + EXPECT_FALSE(ESPBTUUID().is_set()); + EXPECT_STREQ(ESPBTUUID().to_str(buf), "None"); + EXPECT_TRUE(ESPBTUUID::from_uint16(0x0000).is_set()); + EXPECT_STREQ(ESPBTUUID::from_uint16(0x0000).to_str(buf), "0x0000"); +} + +// Text parsing of an invalid length historically produced a len-0 (unset) UUID. +TEST(BleDeviceUuid, InvalidTextFormParsesToUnset) { + EXPECT_EQ(ESPBTUUID::from_raw("nope", 3).type(), ESPBTUUID::Type::UNSET); +} + TEST(BleDeviceUuid, DifferentUuidsDoNotMatch) { EXPECT_FALSE(ESPBTUUID::from_uint16(0x1234) == ESPBTUUID::from_uint16(0x1235)); const uint8_t raw128[16] = {0xFB, 0x34, 0x9B, 0x5F, 0x80, 0x00, 0x00, 0x80,