From 33c08812ccc34515f5fb163bbd937ecdce772971 Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Wed, 4 Dec 2019 17:12:26 +0100 Subject: [PATCH] Update ESP32 BLE ADV parse to match BLE spec (#904) * Update ESP32 BLE ADV parse to match BLE spec * Update xiaomi * Update ruuvi * Format * Update esp32_ble_tracker.cpp * Fix log * Format * Update xiaomi_ble.cpp --- .../esp32_ble_tracker/esp32_ble_tracker.cpp | 130 ++++++++++++------ .../esp32_ble_tracker/esp32_ble_tracker.h | 56 +++++--- esphome/components/ruuvi_ble/ruuvi_ble.cpp | 35 +++-- esphome/components/xiaomi_ble/xiaomi_ble.cpp | 31 +++-- esphome/core/helpers.h | 1 + 5 files changed, 158 insertions(+), 95 deletions(-) diff --git a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp index 53c6de62cc..ab6bfa681c 100644 --- a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp +++ b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp @@ -203,10 +203,6 @@ void ESP32BLETracker::gap_scan_result(const esp_ble_gap_cb_param_t::ble_scan_res } } -std::string hexencode_string(const std::string &raw_data) { - return hexencode(reinterpret_cast(raw_data.c_str()), raw_data.size()); -} - ESPBTUUID::ESPBTUUID() : uuid_() {} ESPBTUUID ESPBTUUID::from_uint16(uint16_t uuid) { ESPBTUUID ret; @@ -267,13 +263,13 @@ std::string ESPBTUUID::to_string() { } ESPBLEiBeacon::ESPBLEiBeacon(const uint8_t *data) { memcpy(&this->beacon_data_, data, sizeof(beacon_data_)); } -optional ESPBLEiBeacon::from_manufacturer_data(const std::string &data) { - if (data.size() != 25) - return {}; - if (data[0] != 0x4C || data[1] != 0x00) +optional ESPBLEiBeacon::from_manufacturer_data(const ServiceData &data) { + if (!data.uuid.contains(0x4C, 0x00)) return {}; - return ESPBLEiBeacon(reinterpret_cast(data.data())); + if (data.data.size() != 23) + return {}; + return ESPBLEiBeacon(data.data.data()); } void ESPBTDevice::parse_scan_rst(const esp_ble_gap_cb_param_t::ble_scan_result_evt_param ¶m) { @@ -305,8 +301,8 @@ void ESPBTDevice::parse_scan_rst(const esp_ble_gap_cb_param_t::ble_scan_result_e ESP_LOGVV(TAG, " RSSI: %d", this->rssi_); ESP_LOGVV(TAG, " Name: '%s'", this->name_.c_str()); - if (this->tx_power_.has_value()) { - ESP_LOGVV(TAG, " TX Power: %d", *this->tx_power_); + for (auto &it : this->tx_powers_) { + ESP_LOGVV(TAG, " TX Power: %d", it); } if (this->appearance_.has_value()) { ESP_LOGVV(TAG, " Appearance: %u", *this->appearance_); @@ -314,20 +310,19 @@ void ESPBTDevice::parse_scan_rst(const esp_ble_gap_cb_param_t::ble_scan_result_e if (this->ad_flag_.has_value()) { ESP_LOGVV(TAG, " Ad Flag: %u", *this->ad_flag_); } - for (auto uuid : this->service_uuids_) { + for (auto &uuid : this->service_uuids_) { ESP_LOGVV(TAG, " Service UUID: %s", uuid.to_string().c_str()); } - ESP_LOGVV(TAG, " Manufacturer data: %s", hexencode_string(this->manufacturer_data_).c_str()); - ESP_LOGVV(TAG, " Service data: %s", hexencode_string(this->service_data_).c_str()); - - if (this->service_data_uuid_.has_value()) { - ESP_LOGVV(TAG, " Service Data UUID: %s", this->service_data_uuid_->to_string().c_str()); + for (auto &data : this->manufacturer_datas_) { + ESP_LOGVV(TAG, " Manufacturer data: %s", hexencode(data.data).c_str()); + } + for (auto &data : this->service_datas_) { + ESP_LOGVV(TAG, " Service data:"); + ESP_LOGVV(TAG, " UUID: %s", data.uuid.to_string().c_str()); + ESP_LOGVV(TAG, " Data: %s", hexencode(data.data).c_str()); } - ESP_LOGVV(TAG, "Adv data: %s", - hexencode_string( - std::string(reinterpret_cast(param.ble_adv), param.adv_data_len + param.scan_rsp_len)) - .c_str()); + ESP_LOGVV(TAG, "Adv data: %s", hexencode(param.ble_adv, param.adv_data_len + param.scan_rsp_len).c_str()); #endif } void ESPBTDevice::parse_adv_(const esp_ble_gap_cb_param_t::ble_scan_result_evt_param ¶m) { @@ -346,25 +341,52 @@ void ESPBTDevice::parse_adv_(const esp_ble_gap_cb_param_t::ble_scan_result_evt_p const uint8_t record_length = field_length - 1; offset += record_length; + // See also Generic Access Profile Assigned Numbers: + // https://www.bluetooth.com/specifications/assigned-numbers/generic-access-profile/ See also ADVERTISING AND SCAN + // RESPONSE DATA FORMAT: https://www.bluetooth.com/specifications/bluetooth-core-specification/ (vol 3, part C, 11) + // See also Core Specification Supplement: https://www.bluetooth.com/specifications/bluetooth-core-specification/ + // (called CSS here) + switch (record_type) { case ESP_BLE_AD_TYPE_NAME_CMPL: { + // CSS 1.2 LOCAL NAME + // "The Local Name data type shall be the same as, or a shortened version of, the local name assigned to the + // device." CSS 1: Optional in this context; shall not appear more than once in a block. this->name_ = std::string(reinterpret_cast(record), record_length); break; } case ESP_BLE_AD_TYPE_TX_PWR: { - this->tx_power_ = *payload; + // CSS 1.5 TX POWER LEVEL + // "The TX Power Level data type indicates the transmitted power level of the packet containing the data type." + // CSS 1: Optional in this context (may appear more than once in a block). + this->tx_powers_.push_back(*payload); break; } case ESP_BLE_AD_TYPE_APPEARANCE: { + // CSS 1.12 APPEARANCE + // "The Appearance data type defines the external appearance of the device." + // See also https://www.bluetooth.com/specifications/gatt/characteristics/ + // CSS 1: Optional in this context; shall not appear more than once in a block and shall not appear in both + // the AD and SRD of the same extended advertising interval. this->appearance_ = *reinterpret_cast(record); break; } case ESP_BLE_AD_TYPE_FLAG: { + // CSS 1.3 FLAGS + // "The Flags data type contains one bit Boolean flags. The Flags data type shall be included when any of the + // Flag bits are non-zero and the advertising packet is connectable, otherwise the Flags data type may be + // omitted." + // CSS 1: Optional in this context; shall not appear more than once in a block. this->ad_flag_ = *record; break; } + // CSS 1.1 SERVICE UUID + // The Service UUID data type is used to include a list of Service or Service Class UUIDs. + // There are six data types defined for the three sizes of Service UUIDs that may be returned: + // CSS 1: Optional in this context (may appear more than once in a block). case ESP_BLE_AD_TYPE_16SRV_CMPL: case ESP_BLE_AD_TYPE_16SRV_PART: { + // • 16-bit Bluetooth Service UUIDs for (uint8_t i = 0; i < record_length / 2; i++) { this->service_uuids_.push_back(ESPBTUUID::from_uint16(*reinterpret_cast(record + 2 * i))); } @@ -372,6 +394,7 @@ void ESPBTDevice::parse_adv_(const esp_ble_gap_cb_param_t::ble_scan_result_evt_p } case ESP_BLE_AD_TYPE_32SRV_CMPL: case ESP_BLE_AD_TYPE_32SRV_PART: { + // • 32-bit Bluetooth Service UUIDs for (uint8_t i = 0; i < record_length / 4; i++) { this->service_uuids_.push_back(ESPBTUUID::from_uint32(*reinterpret_cast(record + 4 * i))); } @@ -379,41 +402,70 @@ void ESPBTDevice::parse_adv_(const esp_ble_gap_cb_param_t::ble_scan_result_evt_p } case ESP_BLE_AD_TYPE_128SRV_CMPL: case ESP_BLE_AD_TYPE_128SRV_PART: { + // • Global 128-bit Service UUIDs this->service_uuids_.push_back(ESPBTUUID::from_raw(record)); break; } case ESP_BLE_AD_MANUFACTURER_SPECIFIC_TYPE: { - this->manufacturer_data_ = std::string(reinterpret_cast(record), record_length); + // CSS 1.4 MANUFACTURER SPECIFIC DATA + // "The Manufacturer Specific data type is used for manufacturer specific data. The first two data octets shall + // contain a company identifier from Assigned Numbers. The interpretation of any other octets within the data + // shall be defined by the manufacturer specified by the company identifier." + // CSS 1: Optional in this context (may appear more than once in a block). + if (record_length < 2) { + ESP_LOGV(TAG, "Record length too small for ESP_BLE_AD_MANUFACTURER_SPECIFIC_TYPE"); + break; + } + ServiceData data{}; + data.uuid = ESPBTUUID::from_uint16(*reinterpret_cast(record)); + data.data.assign(record + 2UL, record + record_length); + this->manufacturer_datas_.push_back(data); break; } + + // CSS 1.11 SERVICE DATA + // "The Service Data data type consists of a service UUID with the data associated with that service." + // CSS 1: Optional in this context (may appear more than once in a block). case ESP_BLE_AD_TYPE_SERVICE_DATA: { + // «Service Data - 16 bit UUID» + // Size: 2 or more octets + // The first 2 octets contain the 16 bit Service UUID fol- lowed by additional service data if (record_length < 2) { ESP_LOGV(TAG, "Record length too small for ESP_BLE_AD_TYPE_SERVICE_DATA"); break; } - this->service_data_uuid_ = ESPBTUUID::from_uint16(*reinterpret_cast(record)); - if (record_length > 2) - this->service_data_ = std::string(reinterpret_cast(record + 2), record_length - 2UL); + ServiceData data{}; + data.uuid = ESPBTUUID::from_uint16(*reinterpret_cast(record)); + data.data.assign(record + 2UL, record + record_length); + this->service_datas_.push_back(data); break; } case ESP_BLE_AD_TYPE_32SERVICE_DATA: { + // «Service Data - 32 bit UUID» + // Size: 4 or more octets + // The first 4 octets contain the 32 bit Service UUID fol- lowed by additional service data if (record_length < 4) { ESP_LOGV(TAG, "Record length too small for ESP_BLE_AD_TYPE_32SERVICE_DATA"); break; } - this->service_data_uuid_ = ESPBTUUID::from_uint32(*reinterpret_cast(record)); - if (record_length > 4) - this->service_data_ = std::string(reinterpret_cast(record + 4), record_length - 4UL); + ServiceData data{}; + data.uuid = ESPBTUUID::from_uint32(*reinterpret_cast(record)); + data.data.assign(record + 4UL, record + record_length); + this->service_datas_.push_back(data); break; } case ESP_BLE_AD_TYPE_128SERVICE_DATA: { + // «Service Data - 128 bit UUID» + // Size: 16 or more octets + // The first 16 octets contain the 128 bit Service UUID followed by additional service data if (record_length < 16) { ESP_LOGV(TAG, "Record length too small for ESP_BLE_AD_TYPE_128SERVICE_DATA"); break; } - this->service_data_uuid_ = ESPBTUUID::from_raw(record); - if (record_length > 16) - this->service_data_ = std::string(reinterpret_cast(record + 16), record_length - 16UL); + ServiceData data{}; + data.uuid = ESPBTUUID::from_raw(record); + data.data.assign(record + 16UL, record + record_length); + this->service_datas_.push_back(data); break; } default: { @@ -430,16 +482,6 @@ std::string ESPBTDevice::address_str() const { return mac; } uint64_t ESPBTDevice::address_uint64() const { return ble_addr_to_uint64(this->address_); } -esp_ble_addr_type_t ESPBTDevice::get_address_type() const { return this->address_type_; } -int ESPBTDevice::get_rssi() const { return this->rssi_; } -const std::string &ESPBTDevice::get_name() const { return this->name_; } -const optional &ESPBTDevice::get_tx_power() const { return this->tx_power_; } -const optional &ESPBTDevice::get_appearance() const { return this->appearance_; } -const optional &ESPBTDevice::get_ad_flag() const { return this->ad_flag_; } -const std::vector &ESPBTDevice::get_service_uuids() const { return this->service_uuids_; } -const std::string &ESPBTDevice::get_manufacturer_data() const { return this->manufacturer_data_; } -const std::string &ESPBTDevice::get_service_data() const { return this->service_data_; } -const optional &ESPBTDevice::get_service_data_uuid() const { return this->service_data_uuid_; } void ESP32BLETracker::dump_config() { ESP_LOGCONFIG(TAG, "BLE Tracker:"); @@ -480,8 +522,8 @@ void ESP32BLETracker::print_bt_device_info(const ESPBTDevice &device) { ESP_LOGD(TAG, " Address Type: %s", address_type_s); if (!device.get_name().empty()) ESP_LOGD(TAG, " Name: '%s'", device.get_name().c_str()); - if (device.get_tx_power().has_value()) { - ESP_LOGD(TAG, " TX Power: %d", *device.get_tx_power()); + for (auto &tx_power : device.get_tx_powers()) { + ESP_LOGD(TAG, " TX Power: %d", tx_power); } } diff --git a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h index 280c3fc45f..74bb7e5d10 100644 --- a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h +++ b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h @@ -33,11 +33,18 @@ class ESPBTUUID { esp_bt_uuid_t uuid_; }; +using adv_data_t = std::vector; + +struct ServiceData { + ESPBTUUID uuid; + adv_data_t data; +}; + class ESPBLEiBeacon { public: ESPBLEiBeacon() { memset(&this->beacon_data_, 0, sizeof(this->beacon_data_)); } ESPBLEiBeacon(const uint8_t *data); - static optional from_manufacturer_data(const std::string &data); + static optional from_manufacturer_data(const ServiceData &data); uint16_t get_major() { return reverse_bits_16(this->beacon_data_.major); } uint16_t get_minor() { return reverse_bits_16(this->beacon_data_.minor); } @@ -46,7 +53,6 @@ class ESPBLEiBeacon { protected: struct { - uint16_t manufacturer_id; uint8_t sub_type; uint8_t proximity_uuid[16]; uint16_t major; @@ -63,18 +69,33 @@ class ESPBTDevice { uint64_t address_uint64() const; - esp_ble_addr_type_t get_address_type() const; - int get_rssi() const; - const std::string &get_name() const; - const optional &get_tx_power() const; - const optional &get_appearance() const; - const optional &get_ad_flag() const; - const std::vector &get_service_uuids() const; - const std::string &get_manufacturer_data() const; - const std::string &get_service_data() const; - const optional &get_service_data_uuid() const; - const optional get_ibeacon() const { - return ESPBLEiBeacon::from_manufacturer_data(this->manufacturer_data_); + esp_ble_addr_type_t get_address_type() const { return this->address_type_; } + int get_rssi() const { return rssi_; } + const std::string &get_name() const { return this->name_; } + + ESPDEPRECATED("Use get_tx_powers() instead") + optional get_tx_power() const { + if (this->tx_powers_.empty()) + return {}; + return this->tx_powers_[0]; + } + const std::vector &get_tx_powers() const { return tx_powers_; } + + const optional &get_appearance() const { return appearance_; } + const optional &get_ad_flag() const { return ad_flag_; } + const std::vector &get_service_uuids() const { return service_uuids_; } + + const std::vector &get_manufacturer_datas() const { return manufacturer_datas_; } + + const std::vector &get_service_datas() const { return service_datas_; } + + optional get_ibeacon() const { + for (auto &it : this->manufacturer_datas_) { + auto res = ESPBLEiBeacon::from_manufacturer_data(it); + if (res.has_value()) + return *res; + } + return {}; } protected: @@ -86,13 +107,12 @@ class ESPBTDevice { esp_ble_addr_type_t address_type_{BLE_ADDR_TYPE_PUBLIC}; int rssi_{0}; std::string name_{}; - optional tx_power_{}; + std::vector tx_powers_{}; optional appearance_{}; optional ad_flag_{}; std::vector service_uuids_; - std::string manufacturer_data_{}; - std::string service_data_{}; - optional service_data_uuid_{}; + std::vector manufacturer_datas_{}; + std::vector service_datas_{}; }; class ESP32BLETracker; diff --git a/esphome/components/ruuvi_ble/ruuvi_ble.cpp b/esphome/components/ruuvi_ble/ruuvi_ble.cpp index 28a689fee4..7e13140e55 100644 --- a/esphome/components/ruuvi_ble/ruuvi_ble.cpp +++ b/esphome/components/ruuvi_ble/ruuvi_ble.cpp @@ -8,10 +8,12 @@ namespace ruuvi_ble { static const char *TAG = "ruuvi_ble"; -bool parse_ruuvi_data_byte(uint8_t data_type, uint8_t data_length, const uint8_t *data, RuuviParseResult &result) { +bool parse_ruuvi_data_byte(const esp32_ble_tracker::adv_data_t &adv_data, RuuviParseResult &result) { + const uint8_t data_type = adv_data[0]; + const auto *data = &adv_data[1]; switch (data_type) { case 0x03: { // RAWv1 - if (data_length != 16) + if (adv_data.size() != 14) return false; const uint8_t temp_sign = (data[1] >> 7) & 1; @@ -32,13 +34,13 @@ bool parse_ruuvi_data_byte(uint8_t data_type, uint8_t data_length, const uint8_t result.acceleration_y = acceleration_y; result.acceleration_z = acceleration_z; result.acceleration = - sqrt(acceleration_x * acceleration_x + acceleration_y * acceleration_y + acceleration_z * acceleration_z); + sqrtf(acceleration_x * acceleration_x + acceleration_y * acceleration_y + acceleration_z * acceleration_z); result.battery_voltage = battery_voltage; return true; } case 0x05: { // RAWv2 - if (data_length != 26) + if (adv_data.size() != 24) return false; const float temperature = (int16_t(data[0] << 8) + int16_t(data[1])) * 0.005f; @@ -63,8 +65,8 @@ bool parse_ruuvi_data_byte(uint8_t data_type, uint8_t data_length, const uint8_t result.acceleration_z = data[10] == 0xFF && data[11] == 0xFF ? NAN : acceleration_z; result.acceleration = result.acceleration_x == NAN || result.acceleration_y == NAN || result.acceleration_z == NAN ? NAN - : sqrt(acceleration_x * acceleration_x + acceleration_y * acceleration_y + - acceleration_z * acceleration_z); + : sqrtf(acceleration_x * acceleration_x + acceleration_y * acceleration_y + + acceleration_z * acceleration_z); result.battery_voltage = (power_info >> 5) == 0x7FF ? NAN : battery_voltage; result.tx_power = (power_info & 0x1F) == 0x1F ? NAN : tx_power; result.movement_counter = movement_counter; @@ -77,21 +79,16 @@ bool parse_ruuvi_data_byte(uint8_t data_type, uint8_t data_length, const uint8_t } } optional parse_ruuvi(const esp32_ble_tracker::ESPBTDevice &device) { - const auto *raw = reinterpret_cast(device.get_manufacturer_data().data()); + bool success = false; + RuuviParseResult result{}; + for (auto &it : device.get_manufacturer_datas()) { + bool is_ruuvi = it.uuid.contains(0x99, 0x04); + if (!is_ruuvi) + continue; - bool is_ruuvi = raw[0] == 0x99 && raw[1] == 0x04; - - if (!is_ruuvi) { - return {}; + if (parse_ruuvi_data_byte(it.data, result)) + success = true; } - - const uint8_t data_length = device.get_manufacturer_data().size(); - const uint8_t format = raw[2]; - const uint8_t *data = &raw[3]; - - RuuviParseResult result; - - bool success = parse_ruuvi_data_byte(format, data_length, data, result); if (!success) return {}; return result; diff --git a/esphome/components/xiaomi_ble/xiaomi_ble.cpp b/esphome/components/xiaomi_ble/xiaomi_ble.cpp index e6884e5ea6..18eaffed06 100644 --- a/esphome/components/xiaomi_ble/xiaomi_ble.cpp +++ b/esphome/components/xiaomi_ble/xiaomi_ble.cpp @@ -63,22 +63,17 @@ bool parse_xiaomi_data_byte(uint8_t data_type, const uint8_t *data, uint8_t data return false; } } -optional parse_xiaomi(const esp32_ble_tracker::ESPBTDevice &device) { - if (!device.get_service_data_uuid().has_value()) { - // ESP_LOGVV(TAG, "Xiaomi no service data"); - return {}; - } - - if (!device.get_service_data_uuid()->contains(0x95, 0xFE)) { +bool parse_xiaomi_service_data(XiaomiParseResult &result, const esp32_ble_tracker::ServiceData &service_data) { + if (!service_data.uuid.contains(0x95, 0xFE)) { // ESP_LOGVV(TAG, "Xiaomi no service data UUID magic bytes"); - return {}; + return false; } - const auto *raw = reinterpret_cast(device.get_service_data().data()); + const auto raw = service_data.data; - if (device.get_service_data().size() < 14) { + if (raw.size() < 14) { // ESP_LOGVV(TAG, "Xiaomi service data too short!"); - return {}; + return false; } bool is_lywsdcgq = (raw[1] & 0x20) == 0x20 && raw[2] == 0xAA && raw[3] == 0x01; @@ -88,10 +83,9 @@ optional parse_xiaomi(const esp32_ble_tracker::ESPBTDevice &d if (!is_lywsdcgq && !is_hhccjcy01 && !is_lywsd02 && !is_cgg1) { // ESP_LOGVV(TAG, "Xiaomi no magic bytes"); - return {}; + return false; } - XiaomiParseResult result; result.type = XiaomiParseResult::TYPE_HHCCJCY01; if (is_lywsdcgq) { result.type = XiaomiParseResult::TYPE_LYWSDCGQ; @@ -111,7 +105,7 @@ optional parse_xiaomi(const esp32_ble_tracker::ESPBTDevice &d const uint8_t *raw_data = &raw[raw_offset]; uint8_t data_offset = 0; - uint8_t data_length = device.get_service_data().size() - raw_offset; + uint8_t data_length = raw.size() - raw_offset; bool success = false; while (true) { @@ -136,6 +130,15 @@ optional parse_xiaomi(const esp32_ble_tracker::ESPBTDevice &d data_offset += 3 + datapoint_length; } + return success; +} +optional parse_xiaomi(const esp32_ble_tracker::ESPBTDevice &device) { + XiaomiParseResult result; + bool success = false; + for (auto &service_data : device.get_service_datas()) { + if (parse_xiaomi_service_data(result, service_data)) + success = true; + } if (!success) return {}; return result; diff --git a/esphome/core/helpers.h b/esphome/core/helpers.h index 88f0d587e5..91b78b73a0 100644 --- a/esphome/core/helpers.h +++ b/esphome/core/helpers.h @@ -158,6 +158,7 @@ ParseOnOffState parse_on_off(const char *str, const char *on = nullptr, const ch // Encode raw data to a human-readable string (for debugging) std::string hexencode(const uint8_t *data, uint32_t len); +template std::string hexencode(const T &data) { return hexencode(data.data(), data.size()); } // https://stackoverflow.com/questions/7858817/unpacking-a-tuple-to-call-a-matching-function-pointer/7858971#7858971 template struct seq {}; // NOLINT