mirror of
https://github.com/esphome/esphome.git
synced 2026-08-22 22:26:21 +00:00
[ble_device_base] Add platform-neutral GATT client contract (#18128)
This commit is contained in:
@@ -50,4 +50,18 @@ TEST(BleDeviceAddress, MacLsbFirstToUint64AgreesWithParsedDevice) {
|
||||
EXPECT_EQ(mac_lsb_first_to_uint64(MAC_LSB_FIRST), device.address_uint64());
|
||||
}
|
||||
|
||||
// uint64_to_mac_msb_first() is the inverse: unpacking the wire value yields
|
||||
// printable (MSB-first) order, and round-tripping through the LSB-first
|
||||
// packer restores the original value.
|
||||
TEST(BleDeviceAddress, Uint64ToMacMsbFirstRoundTrip) {
|
||||
uint8_t msb_first[6];
|
||||
uint64_to_mac_msb_first(0xAABBCCDDEEFFULL, msb_first);
|
||||
EXPECT_EQ(msb_first[0], 0xaa);
|
||||
EXPECT_EQ(msb_first[5], 0xff);
|
||||
uint8_t lsb_first[6];
|
||||
for (int i = 0; i < 6; i++)
|
||||
lsb_first[i] = msb_first[5 - i];
|
||||
EXPECT_EQ(mac_lsb_first_to_uint64(lsb_first), 0xAABBCCDDEEFFULL);
|
||||
}
|
||||
|
||||
} // namespace esphome::ble_device_base::testing
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
#include "esphome/components/ble_device_base/ble_device.h"
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <cstring>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace esphome::ble_device_base {
|
||||
namespace {
|
||||
|
||||
// AD types under test
|
||||
constexpr uint8_t AD_SHORT_NAME = 0x08;
|
||||
constexpr uint8_t AD_COMPLETE_NAME = 0x09;
|
||||
|
||||
void append_name(std::vector<uint8_t> &adv, uint8_t ad_type, const char *name) {
|
||||
size_t len = strlen(name);
|
||||
adv.push_back(static_cast<uint8_t>(len + 1));
|
||||
adv.push_back(ad_type);
|
||||
adv.insert(adv.end(), name, name + len);
|
||||
}
|
||||
|
||||
ESPBTDevice device_from(const std::vector<uint8_t> &adv) {
|
||||
const uint8_t mac[6] = {0x11, 0x22, 0x33, 0x44, 0x55, 0x66};
|
||||
ESPBTDevice device;
|
||||
device.from_scan_result(mac, -59, 0, adv.data(), static_cast<uint16_t>(adv.size()));
|
||||
return device;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
TEST(BleAdvName, ParsesACompleteName) {
|
||||
std::vector<uint8_t> adv;
|
||||
append_name(adv, AD_COMPLETE_NAME, "TP96");
|
||||
ESPBTDevice device = device_from(adv);
|
||||
EXPECT_EQ(device.get_name(), "TP96");
|
||||
// The backing buffer is NUL-terminated so c_str() is usable directly.
|
||||
EXPECT_STREQ(device.get_name().c_str(), "TP96");
|
||||
}
|
||||
|
||||
TEST(BleAdvName, LongestNameWinsShortenedThenComplete) {
|
||||
// A merged adv + scan-response frame can carry both forms; the shortened
|
||||
// one must never replace the complete one.
|
||||
std::vector<uint8_t> adv;
|
||||
append_name(adv, AD_SHORT_NAME, "Radon");
|
||||
append_name(adv, AD_COMPLETE_NAME, "RadonEye");
|
||||
EXPECT_EQ(device_from(adv).get_name(), "RadonEye");
|
||||
}
|
||||
|
||||
TEST(BleAdvName, LongestNameWinsCompleteThenShortened) {
|
||||
std::vector<uint8_t> adv;
|
||||
append_name(adv, AD_COMPLETE_NAME, "RadonEye");
|
||||
append_name(adv, AD_SHORT_NAME, "Radon");
|
||||
EXPECT_EQ(device_from(adv).get_name(), "RadonEye");
|
||||
}
|
||||
|
||||
TEST(BleAdvName, MaxLengthNameFitsAndTerminates) {
|
||||
// 29 bytes is the largest name a legacy AD element can carry and exactly
|
||||
// fills the fixed buffer.
|
||||
std::string max_name(29, 'a');
|
||||
std::vector<uint8_t> adv;
|
||||
append_name(adv, AD_COMPLETE_NAME, max_name.c_str());
|
||||
ESPBTDevice device = device_from(adv);
|
||||
EXPECT_EQ(device.get_name().size(), 29u);
|
||||
EXPECT_EQ(device.get_name(), max_name);
|
||||
EXPECT_STREQ(device.get_name().c_str(), max_name.c_str());
|
||||
}
|
||||
|
||||
TEST(BleAdvName, ReparseResetsThePreviousName) {
|
||||
const uint8_t mac[6] = {0x11, 0x22, 0x33, 0x44, 0x55, 0x66};
|
||||
std::vector<uint8_t> first;
|
||||
append_name(first, AD_COMPLETE_NAME, "RadonEye");
|
||||
std::vector<uint8_t> second;
|
||||
append_name(second, AD_COMPLETE_NAME, "TP96");
|
||||
|
||||
ESPBTDevice device;
|
||||
device.from_scan_result(mac, -59, 0, first.data(), static_cast<uint16_t>(first.size()));
|
||||
ASSERT_EQ(device.get_name(), "RadonEye");
|
||||
// A shorter name from a fresh report must fully replace the longer one:
|
||||
// the longest-name rule applies within one report, not across reports.
|
||||
device.from_scan_result(mac, -59, 0, second.data(), static_cast<uint16_t>(second.size()));
|
||||
EXPECT_EQ(device.get_name(), "TP96");
|
||||
EXPECT_STREQ(device.get_name().c_str(), "TP96");
|
||||
}
|
||||
|
||||
TEST(BleAdvName, NoNamePresentIsEmpty) {
|
||||
std::vector<uint8_t> adv = {0x02, 0x0A, 0x00}; // TX power only
|
||||
EXPECT_TRUE(device_from(adv).get_name().empty());
|
||||
}
|
||||
|
||||
} // namespace esphome::ble_device_base
|
||||
@@ -0,0 +1,66 @@
|
||||
// The GATT client contract compiles in no real build until a hub backend is
|
||||
// configured; this TU pins it on the host so the header cannot rot unseen.
|
||||
#define USE_BLE_GATT_CLIENT
|
||||
|
||||
#include "esphome/components/ble_device_base/ble_gatt_client.h"
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
namespace esphome::ble_device_base::testing {
|
||||
|
||||
class RecordingListener : public GattClientEventListener {
|
||||
public:
|
||||
void on_connection_state(bool connected, uint16_t mtu, int error) override { this->connected_ = connected; }
|
||||
void on_service_discovery_done(int error) override { this->discovery_error_ = error; }
|
||||
void on_read_result(uint16_t handle, const uint8_t *data, uint16_t len, int error) override {}
|
||||
void on_write_result(uint16_t handle, int error) override {}
|
||||
void on_notify_state(uint16_t handle, bool enabled, int error) override {}
|
||||
void on_notify_data(uint16_t handle, const uint8_t *data, uint16_t len) override {}
|
||||
bool connected_{false};
|
||||
int discovery_error_{0};
|
||||
};
|
||||
|
||||
class MinimalConnection : public BLEGattConnection {
|
||||
public:
|
||||
int connect(uint64_t address, uint8_t addr_type) override {
|
||||
if (this->listener_ != nullptr)
|
||||
this->listener_->on_connection_state(true, 517, 0);
|
||||
return 0;
|
||||
}
|
||||
int disconnect() override { return 0; }
|
||||
int discover_services() override {
|
||||
if (this->listener_ != nullptr)
|
||||
this->listener_->on_service_discovery_done(0);
|
||||
return 0;
|
||||
}
|
||||
int read_characteristic(uint16_t handle) override { return GATT_ERR_NOT_CONNECTED; }
|
||||
int write_characteristic(uint16_t handle, const uint8_t *data, uint16_t len, bool response) override { return 0; }
|
||||
int read_descriptor(uint16_t handle) override { return 0; }
|
||||
int write_descriptor(uint16_t handle, const uint8_t *data, uint16_t len) override { return 0; }
|
||||
int notify_characteristic(uint16_t handle, bool enable) override { return 0; }
|
||||
int update_connection_params(uint16_t min_interval, uint16_t max_interval, uint16_t latency,
|
||||
uint16_t timeout) override {
|
||||
return 0;
|
||||
}
|
||||
GattServiceTable get_service_table() override { return {}; }
|
||||
void release_services() override {}
|
||||
};
|
||||
|
||||
TEST(BleGattClientContract, MinimalImplementerCompilesAndRoutesEvents) {
|
||||
MinimalConnection connection;
|
||||
RecordingListener listener;
|
||||
connection.set_listener(&listener);
|
||||
EXPECT_EQ(connection.connect(0xAABBCCDDEEFFULL, 0), 0);
|
||||
EXPECT_TRUE(listener.connected_);
|
||||
EXPECT_EQ(connection.discover_services(), 0);
|
||||
EXPECT_EQ(listener.discovery_error_, 0);
|
||||
EXPECT_EQ(connection.read_characteristic(1), GATT_ERR_NOT_CONNECTED);
|
||||
|
||||
// A default table is empty and safe to walk.
|
||||
GattServiceTable table = connection.get_service_table();
|
||||
EXPECT_EQ(table.service_count, 0);
|
||||
EXPECT_EQ(table.characteristic_count, 0);
|
||||
EXPECT_EQ(table.descriptor_count, 0);
|
||||
}
|
||||
|
||||
} // namespace esphome::ble_device_base::testing
|
||||
Reference in New Issue
Block a user