diff --git a/esphome/components/ble_device_base/ble_gatt_client.h b/esphome/components/ble_device_base/ble_gatt_client.h index 3b8c362d93..6208831fd4 100644 --- a/esphome/components/ble_device_base/ble_gatt_client.h +++ b/esphome/components/ble_device_base/ble_gatt_client.h @@ -8,9 +8,9 @@ // A consumer - a streaming consumer that forwards the raw database (the hub // BluetoothConnection wrapper) or a direct consumer owning a dedicated // backend and resolving handles by UUID - drives it and receives -// completions through a GattEventSink — a pointer-sized-entry function table -// rather than a concrete consumer type, because one build can hold several -// consumer types while the backend stays a single non-virtual class. All sink +// completions through the GattClientListener interface (one build can hold +// several consumer types while the backend stays a single non-virtual +// class). All listener // calls are delivered on the ESPHome main loop; borrowed data pointers are // valid only for the duration of the call. // @@ -82,90 +82,31 @@ struct GattServiceTable { uint16_t descriptor_count{0}; }; -// The event sink the backend calls directly, asserted where each consumer is -// defined: on_connection_state carries the negotiated MTU and an HCI -// status/disconnect reason. The requirements check call validity, not exact -// parameter types; keep sink parameters at the documented widths (uint16_t -// handles and lengths). -template -concept GattClientEventSinkContract = requires(S sink, const uint8_t *data) { - { sink.on_connection_state(true, uint16_t{}, int{}) } -> std::same_as; - { sink.on_service_discovery_done(int{}) } -> std::same_as; - { sink.on_read_result(uint16_t{}, data, uint16_t{}, int{}) } -> std::same_as; - { sink.on_write_result(uint16_t{}, int{}) } -> std::same_as; - { sink.on_notify_state(uint16_t{}, true, int{}) } -> std::same_as; - { sink.on_notify_data(uint16_t{}, data, uint16_t{}) } -> std::same_as; - { sink.on_pairing_result(int{}) } -> std::same_as; -}; - -/// One trampoline per event, shared by every instance of a consumer type. -struct GattEventVTable { - void (*connection_state)(void *, bool, uint16_t, int); - void (*service_discovery_done)(void *, int); - void (*read_result)(void *, uint16_t, const uint8_t *, uint16_t, int); - void (*write_result)(void *, uint16_t, int); - void (*notify_state)(void *, uint16_t, bool, int); - void (*notify_data)(void *, uint16_t, const uint8_t *, uint16_t); - void (*pairing_result)(void *, int); -}; - -// The per-consumer-type table lives in flash (constexpr), so a sink costs -// two pointers of RAM regardless of how many events the surface carries. -template -inline constexpr GattEventVTable GATT_EVENT_VTABLE{ - [](void *p, bool connected, uint16_t mtu, int error) { - static_cast(p)->on_connection_state(connected, mtu, error); - }, - [](void *p, int error) { static_cast(p)->on_service_discovery_done(error); }, - [](void *p, uint16_t handle, const uint8_t *data, uint16_t len, int error) { - static_cast(p)->on_read_result(handle, data, len, error); - }, - [](void *p, uint16_t handle, int error) { static_cast(p)->on_write_result(handle, error); }, - [](void *p, uint16_t handle, bool enabled, int error) { - static_cast(p)->on_notify_state(handle, enabled, error); - }, - [](void *p, uint16_t handle, const uint8_t *data, uint16_t len) { - static_cast(p)->on_notify_data(handle, data, len); - }, - [](void *p, int status) { static_cast(p)->on_pairing_result(status); }, -}; - -/// Type-erased consumer handle a backend delivers events through: an -/// instance pointer plus the consumer type's trampoline table. Built with -/// make_gatt_sink() from any type satisfying GattClientEventSinkContract; -/// call sites read the same as a direct listener call. No virtuals, no heap — -/// the cost of supporting several consumer types in one build is one -/// indirect call per event. Codegen wires the sink before setup(), so +/// The event surface a backend delivers completions through. Genuine runtime +/// polymorphism lives here - one build can hold several consumer types (the +/// proxy's connection wrapper, dedicated-backend components) against the one +/// non-virtual backend class - so this is a plain virtual interface: every +/// method defaults to a no-op, consumers override what they consume (override +/// makes a misspelled name a compile error), and adding an event touches no +/// existing consumer. No destructor: components are never destroyed, and +/// nothing deletes through this base. +/// on_connection_state carries the negotiated MTU and an HCI +/// status/disconnect reason. Codegen wires the listener before setup(), so /// backends may call without a null check. -struct GattEventSink { - void *instance{nullptr}; - const GattEventVTable *vtable{nullptr}; - - void on_connection_state(bool connected, uint16_t mtu, int error) const { - this->vtable->connection_state(this->instance, connected, mtu, error); - } - void on_service_discovery_done(int error) const { this->vtable->service_discovery_done(this->instance, error); } - void on_read_result(uint16_t handle, const uint8_t *data, uint16_t len, int error) const { - this->vtable->read_result(this->instance, handle, data, len, error); - } - void on_write_result(uint16_t handle, int error) const { this->vtable->write_result(this->instance, handle, error); } - void on_notify_state(uint16_t handle, bool enabled, int error) const { - this->vtable->notify_state(this->instance, handle, enabled, error); - } - void on_notify_data(uint16_t handle, const uint8_t *data, uint16_t len) const { - this->vtable->notify_data(this->instance, handle, data, len); - } - void on_pairing_result(int status) const { this->vtable->pairing_result(this->instance, status); } +class GattClientListener { + public: + virtual void on_connection_state(bool connected, uint16_t mtu, int error) {} + virtual void on_service_discovery_done(int error) {} + virtual void on_read_result(uint16_t handle, const uint8_t *data, uint16_t len, int error) {} + virtual void on_write_result(uint16_t handle, int error) {} + virtual void on_notify_state(uint16_t handle, bool enabled, int error) {} + virtual void on_notify_data(uint16_t handle, const uint8_t *data, uint16_t len) {} + virtual void on_pairing_result(int status) {} }; -template GattEventSink make_gatt_sink(T *consumer) { - static_assert(GattClientEventSinkContract, "the consumer is missing part of the event-sink surface"); - return {consumer, &GATT_EVENT_VTABLE}; -} - // The BLEGattConnection op surface, asserted where the alias binds // (bluetooth_connection_gatt_backend.h). Operations return 0 when accepted (completion arrives -// through the sink) or a synchronous error (busy, not connected, stack +// through the listener) or a synchronous error (busy, not connected, stack // rejection); one operation may be outstanding at a time. Semantics beyond // the signatures: // - connect: addr_type is a BLE_ADDR_TYPE_* constant (ble_device.h). @@ -184,8 +125,8 @@ template GattEventSink make_gatt_sink(T *consumer) { // on_notify_state, characteristic writes with response and descriptor // writes in on_write_result. template -concept BLEGattConnectionContract = requires(T conn, GattEventSink sink, const uint8_t *data) { - conn.set_sink(sink); +concept BLEGattConnectionContract = requires(T conn, GattClientListener *listener, const uint8_t *data) { + conn.set_listener(listener); { conn.connect(uint64_t{}, uint8_t{}) } -> std::same_as; { conn.disconnect() } -> std::same_as; { conn.discover_services() } -> std::same_as; diff --git a/esphome/components/bluetooth_connection/bluetooth_connection_bluedroid.cpp b/esphome/components/bluetooth_connection/bluetooth_connection_bluedroid.cpp index b8a23b6b93..8b1cd7fc36 100644 --- a/esphome/components/bluetooth_connection/bluetooth_connection_bluedroid.cpp +++ b/esphome/components/bluetooth_connection/bluetooth_connection_bluedroid.cpp @@ -437,7 +437,7 @@ void BluedroidGattClient::set_disconnecting_() { } void BluedroidGattClient::report_connection_state_(bool connected, int error) { - this->sink_.on_connection_state(connected, this->mtu_, error); + this->listener_->on_connection_state(connected, this->mtu_, error); } esp_err_t BluedroidGattClient::update_conn_params_(uint16_t min_interval, uint16_t max_interval, uint16_t latency, @@ -479,11 +479,11 @@ void BluedroidGattClient::handle_search_cmpl_() { // clients cache the streamed result permanently. auto status = primary_status != ESP_GATT_OK ? primary_status : secondary_status; this->log_gattc_warning_("esp_ble_gattc_get_attr_count", status); - this->sink_.on_service_discovery_done(status); + this->listener_->on_service_discovery_done(status); return; } this->service_total_ = primary + secondary; - this->sink_.on_service_discovery_done(0); + this->listener_->on_service_discovery_done(0); } #ifdef BLUETOOTH_CONNECTION_SERVES_PROXY @@ -766,32 +766,34 @@ bool BluedroidGattClient::handle_gattc_event_(esp_gattc_cb_event_t event, esp_ga if (this->conn_id_ != param->read.conn_id) return false; bool ok = param->read.status == ESP_GATT_OK; - this->sink_.on_read_result(param->read.handle, ok ? param->read.value : nullptr, ok ? param->read.value_len : 0, - ok ? 0 : param->read.status); + this->listener_->on_read_result(param->read.handle, ok ? param->read.value : nullptr, + ok ? param->read.value_len : 0, ok ? 0 : param->read.status); break; } case ESP_GATTC_WRITE_CHAR_EVT: case ESP_GATTC_WRITE_DESCR_EVT: { if (this->conn_id_ != param->write.conn_id) return false; - this->sink_.on_write_result(param->write.handle, param->write.status == ESP_GATT_OK ? 0 : param->write.status); + this->listener_->on_write_result(param->write.handle, + param->write.status == ESP_GATT_OK ? 0 : param->write.status); break; } case ESP_GATTC_REG_FOR_NOTIFY_EVT: { - this->sink_.on_notify_state(param->reg_for_notify.handle, true, - param->reg_for_notify.status == ESP_GATT_OK ? 0 : param->reg_for_notify.status); + this->listener_->on_notify_state(param->reg_for_notify.handle, true, + param->reg_for_notify.status == ESP_GATT_OK ? 0 : param->reg_for_notify.status); break; } case ESP_GATTC_UNREG_FOR_NOTIFY_EVT: { - this->sink_.on_notify_state(param->unreg_for_notify.handle, false, - param->unreg_for_notify.status == ESP_GATT_OK ? 0 : param->unreg_for_notify.status); + this->listener_->on_notify_state( + param->unreg_for_notify.handle, false, + param->unreg_for_notify.status == ESP_GATT_OK ? 0 : param->unreg_for_notify.status); break; } case ESP_GATTC_NOTIFY_EVT: { if (this->conn_id_ != param->notify.conn_id) return false; ESP_LOGV(TAG, "[%d] NOTIFY_EVT handle=0x%2X", this->connection_index_, param->notify.handle); - this->sink_.on_notify_data(param->notify.handle, param->notify.value, param->notify.value_len); + this->listener_->on_notify_data(param->notify.handle, param->notify.value, param->notify.value_len); break; } default: @@ -812,8 +814,8 @@ void BluedroidGattClient::handle_gap_event_(esp_gap_ble_cb_event_t event, esp_bl case ESP_GAP_BLE_AUTH_CMPL_EVT: { if (!this->check_addr_(param->ble_security.auth_cmpl.bd_addr)) break; - this->sink_.on_pairing_result(param->ble_security.auth_cmpl.success ? 0 - : param->ble_security.auth_cmpl.fail_reason); + this->listener_->on_pairing_result( + param->ble_security.auth_cmpl.success ? 0 : param->ble_security.auth_cmpl.fail_reason); break; } default: diff --git a/esphome/components/bluetooth_connection/bluetooth_connection_bluedroid.h b/esphome/components/bluetooth_connection/bluetooth_connection_bluedroid.h index f100c5068e..8b9546a6b0 100644 --- a/esphome/components/bluetooth_connection/bluetooth_connection_bluedroid.h +++ b/esphome/components/bluetooth_connection/bluetooth_connection_bluedroid.h @@ -57,7 +57,7 @@ class BluedroidGattClient final : public Component { float get_setup_priority() const override { return setup_priority::AFTER_BLUETOOTH; } // Wired by codegen before setup and invariant for the device lifetime. - void set_sink(ble_device_base::GattEventSink sink) { this->sink_ = sink; } + void set_listener(ble_device_base::GattClientListener *listener) { this->listener_ = listener; } esp32_ble_tracker::ESPBTClient *tracker_client() { return &this->shim_; } // ---- ble_device_base::BLEGattConnection contract ---- @@ -123,7 +123,7 @@ class BluedroidGattClient final : public Component { // Group 1: pointers / composed objects BluedroidTrackerShim shim_{this}; - ble_device_base::GattEventSink sink_; + ble_device_base::GattClientListener *listener_{nullptr}; #ifdef USE_BLE_GATT_SERVICE_TABLE // One exact-size block carved into the table's three arrays; owned here, // freed by release_services(). Null when no table is materialized. The diff --git a/esphome/components/bluetooth_connection/bluetooth_connection_gatt_backend.h b/esphome/components/bluetooth_connection/bluetooth_connection_gatt_backend.h index ff7ff0eeca..d0ff875c7e 100644 --- a/esphome/components/bluetooth_connection/bluetooth_connection_gatt_backend.h +++ b/esphome/components/bluetooth_connection/bluetooth_connection_gatt_backend.h @@ -26,7 +26,7 @@ namespace esphome::bluetooth_connection { class StubGattBackend { public: - void set_sink(ble_device_base::GattEventSink sink) {} + void set_listener(ble_device_base::GattClientListener *listener) {} int connect(uint64_t address, uint8_t addr_type) { return ble_device_base::GATT_ERR_NOT_CONNECTED; } int disconnect() { return ble_device_base::GATT_ERR_NOT_CONNECTED; } int discover_services() { return ble_device_base::GATT_ERR_NOT_CONNECTED; } diff --git a/esphome/components/bluetooth_connection/bluetooth_connection_hub.cpp b/esphome/components/bluetooth_connection/bluetooth_connection_hub.cpp index d1719a0617..cb398193d4 100644 --- a/esphome/components/bluetooth_connection/bluetooth_connection_hub.cpp +++ b/esphome/components/bluetooth_connection/bluetooth_connection_hub.cpp @@ -96,7 +96,7 @@ void BluetoothConnection::reset_connection_(conn_err_t reason) { this->proxy_->reset_connection_slot_(this, reason); } -// ---- backend event sink ---- +// ---- backend event listener ---- void BluetoothConnection::on_connection_state(bool connected, uint16_t mtu, int error) { if (connected && this->address_ == 0) { diff --git a/esphome/components/bluetooth_connection/bluetooth_connection_hub.h b/esphome/components/bluetooth_connection/bluetooth_connection_hub.h index 291391d575..cb8e2a1b1e 100644 --- a/esphome/components/bluetooth_connection/bluetooth_connection_hub.h +++ b/esphome/components/bluetooth_connection/bluetooth_connection_hub.h @@ -25,12 +25,12 @@ namespace esphome::bluetooth_connection { using ClientState = ble_device_base::ClientState; using ConnectionType = ble_device_base::ConnectionType; -class BluetoothConnection final { +class BluetoothConnection final : public ble_device_base::GattClientListener { public: /// Wire the platform backend. Called from codegen before setup. void set_backend(ble_device_base::BLEGattConnection *backend) { this->backend_ = backend; - backend->set_sink(ble_device_base::make_gatt_sink(this)); + backend->set_listener(this); } // ---- proxy dispatch surface ---- @@ -87,14 +87,14 @@ class BluetoothConnection final { } } - // ---- backend event sink (called directly by the backend, main loop) ---- - void on_connection_state(bool connected, uint16_t mtu, int error); - void on_service_discovery_done(int error); - void on_read_result(uint16_t handle, const uint8_t *data, uint16_t len, int error); - void on_write_result(uint16_t handle, int error); - void on_notify_state(uint16_t handle, bool enabled, int error); - void on_notify_data(uint16_t handle, const uint8_t *data, uint16_t len); - void on_pairing_result(int status); + // ---- backend event listener (called directly by the backend, main loop) ---- + void on_connection_state(bool connected, uint16_t mtu, int error) override; + void on_service_discovery_done(int error) override; + 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; + void on_pairing_result(int status) override; protected: friend class bluetooth_proxy::BluetoothProxy; @@ -145,9 +145,6 @@ class BluetoothConnection final { bool services_discovered_{false}; }; -static_assert(ble_device_base::GattClientEventSinkContract, - "The hub wrapper is missing part of the event-sink surface (ble_gatt_client.h)"); - } // namespace esphome::bluetooth_connection #endif // BLUETOOTH_CONNECTION_SERVES_PROXY diff --git a/esphome/components/bluetooth_connection/bluetooth_connection_rp2.cpp b/esphome/components/bluetooth_connection/bluetooth_connection_rp2.cpp index 33181ed748..0bd097dc6d 100644 --- a/esphome/components/bluetooth_connection/bluetooth_connection_rp2.cpp +++ b/esphome/components/bluetooth_connection/bluetooth_connection_rp2.cpp @@ -383,7 +383,7 @@ void RP2GattClient::loop() { RP2GattNotifyEvent *notify; while ((notify = this->notify_queue_.pop()) != nullptr) { if (this->notify_subscribed_(notify->handle)) { - this->sink_.on_notify_data(notify->handle, notify->data, notify->len); + this->listener_->on_notify_data(notify->handle, notify->data, notify->len); } this->notify_pool_.release(notify); } @@ -446,7 +446,7 @@ void RP2GattClient::loop() { } if (timed_out) { ESP_LOGW(TAG, "Deferred write timeout, handle=0x%04x", this->op_handle_); - this->sink_.on_write_result(this->op_handle_, GATT_CLIENT_BUSY); + this->listener_->on_write_result(this->op_handle_, GATT_CLIENT_BUSY); } } else if (this->state_ == EngineState::IDLE || (this->state_ == EngineState::READY && !this->op_in_flight_() && this->event_queue_.empty() && this->notify_queue_.empty())) { @@ -470,7 +470,7 @@ void RP2GattClient::handle_event_(const RP2GattEvent &event) { this->state_ = EngineState::READY; // Scanning resumes and runs alongside the established connection. this->release_scan_inhibit_(); - this->sink_.on_connection_state(true, this->mtu_, 0); + this->listener_->on_connection_state(true, this->mtu_, 0); } break; case RP2GattEvent::QUERY_COMPLETE: @@ -480,7 +480,7 @@ void RP2GattClient::handle_event_(const RP2GattEvent &event) { this->finish_write_no_rsp_(event.status); break; case RP2GattEvent::PAIRING_RESULT: - this->sink_.on_pairing_result(event.status); + this->listener_->on_pairing_result(event.status); break; } } @@ -506,7 +506,7 @@ void RP2GattClient::finish_write_no_rsp_(uint8_t status) { return; } this->op_type_ = OpType::NONE; - this->sink_.on_write_result(this->op_handle_, status); + this->listener_->on_write_result(this->op_handle_, status); } void RP2GattClient::handle_connected_(uint8_t status, uint16_t con_handle) { @@ -566,7 +566,7 @@ void RP2GattClient::fail_connection_(uint8_t reason) { this->cleanup_link_state_(); this->release_scan_inhibit_(); this->state_ = EngineState::IDLE; - this->sink_.on_connection_state(false, 0, reason); + this->listener_->on_connection_state(false, 0, reason); } void RP2GattClient::cleanup_link_state_() { @@ -619,11 +619,12 @@ void RP2GattClient::handle_query_complete_(uint8_t att_status) { this->op_len_ > 0) { att_status = 0; } - this->sink_.on_read_result(this->op_handle_, this->op_buffer_, att_status == 0 ? this->op_len_ : 0, att_status); + this->listener_->on_read_result(this->op_handle_, this->op_buffer_, att_status == 0 ? this->op_len_ : 0, + att_status); break; case OpType::WRITE_CHAR: case OpType::WRITE_DESC: - this->sink_.on_write_result(this->op_handle_, att_status); + this->listener_->on_write_result(this->op_handle_, att_status); break; default: break; @@ -780,7 +781,7 @@ void RP2GattClient::finish_discovery_(int error) { if (error != 0) { this->release_services(); } - this->sink_.on_service_discovery_done(error); + this->listener_->on_service_discovery_done(error); } ble_device_base::GattServiceTable RP2GattClient::get_service_table() { @@ -973,7 +974,7 @@ int RP2GattClient::write_characteristic(uint16_t handle, const uint8_t *data, ui } } if (status == 0) { - this->sink_.on_write_result(handle, 0); + this->listener_->on_write_result(handle, 0); } return status; } @@ -1074,7 +1075,7 @@ int RP2GattClient::notify_characteristic(uint16_t handle, bool enable) { } } } - this->sink_.on_notify_state(handle, enable, 0); + this->listener_->on_notify_state(handle, enable, 0); return 0; } diff --git a/esphome/components/bluetooth_connection/bluetooth_connection_rp2.h b/esphome/components/bluetooth_connection/bluetooth_connection_rp2.h index 89aa31ba5f..727952c66e 100644 --- a/esphome/components/bluetooth_connection/bluetooth_connection_rp2.h +++ b/esphome/components/bluetooth_connection/bluetooth_connection_rp2.h @@ -78,7 +78,7 @@ class RP2GattClient final : public Component, public Parentedsink_ = sink; } + void set_listener(ble_device_base::GattClientListener *listener) { this->listener_ = listener; } // ---- ble_device_base::BLEGattConnection contract ---- int connect(uint64_t address, uint8_t addr_type); @@ -150,7 +150,7 @@ class RP2GattClient final : public Component, public Parented event_queue_; esphome::EventPool event_pool_; diff --git a/tests/components/ble_device_base/test_gatt_client_contract.cpp b/tests/components/ble_device_base/test_gatt_client_contract.cpp index 6afa1d41e8..ec3756157d 100644 --- a/tests/components/ble_device_base/test_gatt_client_contract.cpp +++ b/tests/components/ble_device_base/test_gatt_client_contract.cpp @@ -2,7 +2,7 @@ // configured; this TU pins it on the host so the header cannot rot unseen. // The contract is a concept (BLEGattConnection is a per-platform alias), so // the minimal backend here proves the concept stays satisfiable and routes -// events through the type-erased GattEventSink the way a real backend does. +// events through the GattClientListener interface the way a real backend does. #define USE_BLE_GATT_CLIENT #include "esphome/components/ble_device_base/ble_gatt_client.h" @@ -11,37 +11,34 @@ namespace esphome::ble_device_base::testing { -struct RecordingSink { - void on_connection_state(bool connected, uint16_t mtu, int error) { this->connected_ = connected; } - void on_service_discovery_done(int error) { this->discovery_error_ = error; } - void on_read_result(uint16_t handle, const uint8_t *data, uint16_t len, int error) {} - void on_write_result(uint16_t handle, int error) { this->write_handle_ = handle; } - void on_notify_state(uint16_t handle, bool enabled, int error) {} - void on_notify_data(uint16_t handle, const uint8_t *data, uint16_t len) {} - void on_pairing_result(int status) {} +// Overrides only what it records; the interface's defaults cover the rest. +class RecordingListener : public GattClientListener { + 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_write_result(uint16_t handle, int error) override { this->write_handle_ = handle; } + bool connected_{false}; int discovery_error_{0}; uint16_t write_handle_{0}; }; -static_assert(GattClientEventSinkContract, "the recording sink must cover the full event-sink surface"); - class MinimalConnection { public: - void set_sink(GattEventSink sink) { this->sink_ = sink; } + void set_listener(GattClientListener *listener) { this->listener_ = listener; } int connect(uint64_t address, uint8_t addr_type) { - this->sink_.on_connection_state(true, 517, 0); + this->listener_->on_connection_state(true, 517, 0); return 0; } int disconnect() { return 0; } int discover_services() { - this->sink_.on_service_discovery_done(0); + this->listener_->on_service_discovery_done(0); return 0; } int read_characteristic(uint16_t handle) { return GATT_ERR_NOT_CONNECTED; } int write_characteristic(uint16_t handle, const uint8_t *data, uint16_t len, bool response) { - this->sink_.on_write_result(handle, 0); + this->listener_->on_write_result(handle, 0); return 0; } int read_descriptor(uint16_t handle) { return 0; } @@ -56,7 +53,7 @@ class MinimalConnection { void set_connection_type(ConnectionType ct) {} protected: - GattEventSink sink_; + GattClientListener *listener_{nullptr}; }; static_assert(BLEGattConnectionContract, @@ -64,8 +61,8 @@ static_assert(BLEGattConnectionContract, TEST(BleGattClientContract, MinimalImplementerCompilesAndRoutesEvents) { MinimalConnection connection; - RecordingSink listener; - connection.set_sink(make_gatt_sink(&listener)); + RecordingListener listener; + connection.set_listener(&listener); EXPECT_EQ(connection.connect(0xAABBCCDDEEFFULL, 0), 0); EXPECT_TRUE(listener.connected_); EXPECT_EQ(connection.discover_services(), 0);