From 02aa75f68c149df50b2d3cdd23c539ccd715e10b Mon Sep 17 00:00:00 2001 From: buxtronix Date: Mon, 3 May 2021 09:10:50 +1000 Subject: [PATCH] BLE client support on ESP32 (#1177) Co-authored-by: Ben Buxton Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- CODEOWNERS | 1 + esphome/components/ble_client/__init__.py | 87 ++++ esphome/components/ble_client/automation.h | 37 ++ esphome/components/ble_client/ble_client.cpp | 380 ++++++++++++++++++ esphome/components/ble_client/ble_client.h | 140 +++++++ .../components/ble_client/sensor/__init__.py | 115 ++++++ .../components/ble_client/sensor/automation.h | 37 ++ .../ble_client/sensor/ble_sensor.cpp | 129 ++++++ .../components/ble_client/sensor/ble_sensor.h | 46 +++ .../components/ble_client/switch/__init__.py | 30 ++ .../ble_client/switch/ble_switch.cpp | 39 ++ .../components/ble_client/switch/ble_switch.h | 30 ++ .../components/esp32_ble_tracker/__init__.py | 8 + .../esp32_ble_tracker/esp32_ble_tracker.cpp | 86 +++- .../esp32_ble_tracker/esp32_ble_tracker.h | 44 ++ esphome/components/esp32_ble_tracker/queue.h | 96 +++++ esphome/config.py | 13 +- esphome/const.py | 3 + tests/test1.yaml | 29 ++ 19 files changed, 1342 insertions(+), 8 deletions(-) create mode 100644 esphome/components/ble_client/__init__.py create mode 100644 esphome/components/ble_client/automation.h create mode 100644 esphome/components/ble_client/ble_client.cpp create mode 100644 esphome/components/ble_client/ble_client.h create mode 100644 esphome/components/ble_client/sensor/__init__.py create mode 100644 esphome/components/ble_client/sensor/automation.h create mode 100644 esphome/components/ble_client/sensor/ble_sensor.cpp create mode 100644 esphome/components/ble_client/sensor/ble_sensor.h create mode 100644 esphome/components/ble_client/switch/__init__.py create mode 100644 esphome/components/ble_client/switch/ble_switch.cpp create mode 100644 esphome/components/ble_client/switch/ble_switch.h create mode 100644 esphome/components/esp32_ble_tracker/queue.h diff --git a/CODEOWNERS b/CODEOWNERS index 891b24f179..e15de721b9 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -21,6 +21,7 @@ esphome/components/atc_mithermometer/* @ahpohl esphome/components/b_parasite/* @rbaron esphome/components/bang_bang/* @OttoWinter esphome/components/binary_sensor/* @esphome/core +esphome/components/ble_client/* @buxtronix esphome/components/bme680_bsec/* @trvrnrth esphome/components/canbus/* @danielschramm @mvturnho esphome/components/captive_portal/* @OttoWinter diff --git a/esphome/components/ble_client/__init__.py b/esphome/components/ble_client/__init__.py new file mode 100644 index 0000000000..d3b287574b --- /dev/null +++ b/esphome/components/ble_client/__init__.py @@ -0,0 +1,87 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import esp32_ble_tracker +from esphome.const import ( + CONF_ID, + CONF_MAC_ADDRESS, + CONF_NAME, + CONF_ON_CONNECT, + CONF_ON_DISCONNECT, + CONF_TRIGGER_ID, +) +from esphome.core import coroutine +from esphome import automation + +CODEOWNERS = ["@buxtronix"] +DEPENDENCIES = ["esp32_ble_tracker"] + +ble_client_ns = cg.esphome_ns.namespace("ble_client") +BLEClient = ble_client_ns.class_( + "BLEClient", cg.Component, esp32_ble_tracker.ESPBTClient +) +BLEClientNode = ble_client_ns.class_("BLEClientNode") +BLEClientNodeConstRef = BLEClientNode.operator("ref").operator("const") +# Triggers +BLEClientConnectTrigger = ble_client_ns.class_( + "BLEClientConnectTrigger", automation.Trigger.template(BLEClientNodeConstRef) +) +BLEClientDisconnectTrigger = ble_client_ns.class_( + "BLEClientDisconnectTrigger", automation.Trigger.template(BLEClientNodeConstRef) +) + +# Espressif platformio framework is built with MAX_BLE_CONN to 3, so +# enforce this in yaml checks. +MULTI_CONF = 3 + +CONFIG_SCHEMA = ( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(BLEClient), + cv.Required(CONF_MAC_ADDRESS): cv.mac_address, + cv.Optional(CONF_NAME): cv.string, + cv.Optional(CONF_ON_CONNECT): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( + BLEClientConnectTrigger + ), + } + ), + cv.Optional(CONF_ON_DISCONNECT): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( + BLEClientDisconnectTrigger + ), + } + ), + } + ) + .extend(cv.COMPONENT_SCHEMA) + .extend(esp32_ble_tracker.ESP_BLE_DEVICE_SCHEMA) +) + +CONF_BLE_CLIENT_ID = "ble_client_id" + +BLE_CLIENT_SCHEMA = cv.Schema( + { + cv.Required(CONF_BLE_CLIENT_ID): cv.use_id(BLEClient), + } +) + + +@coroutine +def register_ble_node(var, config): + parent = yield cg.get_variable(config[CONF_BLE_CLIENT_ID]) + cg.add(parent.register_ble_node(var)) + + +def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + yield cg.register_component(var, config) + yield esp32_ble_tracker.register_client(var, config) + cg.add(var.set_address(config[CONF_MAC_ADDRESS].as_hex)) + for conf in config.get(CONF_ON_CONNECT, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) + yield automation.build_automation(trigger, [], conf) + for conf in config.get(CONF_ON_DISCONNECT, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) + yield automation.build_automation(trigger, [], conf) diff --git a/esphome/components/ble_client/automation.h b/esphome/components/ble_client/automation.h new file mode 100644 index 0000000000..2db609de55 --- /dev/null +++ b/esphome/components/ble_client/automation.h @@ -0,0 +1,37 @@ +#pragma once + +#include "esphome/core/automation.h" +#include "esphome/components/ble_client/ble_client.h" + +#ifdef ARDUINO_ARCH_ESP32 + +namespace esphome { +namespace ble_client { +class BLEClientConnectTrigger : public Trigger<>, public BLEClientNode { + public: + explicit BLEClientConnectTrigger(BLEClient *parent) { parent->register_ble_node(this); } + void loop() override {} + void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, esp_ble_gattc_cb_param_t *param) { + if (event == ESP_GATTC_OPEN_EVT && param->open.status == ESP_GATT_OK) + this->trigger(); + if (event == ESP_GATTC_SEARCH_CMPL_EVT) + this->node_state = espbt::ClientState::Established; + } +}; + +class BLEClientDisconnectTrigger : public Trigger<>, public BLEClientNode { + public: + explicit BLEClientDisconnectTrigger(BLEClient *parent) { parent->register_ble_node(this); } + void loop() override {} + void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, esp_ble_gattc_cb_param_t *param) { + if (event == ESP_GATTC_DISCONNECT_EVT && memcmp(param->disconnect.remote_bda, this->parent_->remote_bda, 6) == 0) + this->trigger(); + if (event == ESP_GATTC_SEARCH_CMPL_EVT) + this->node_state = espbt::ClientState::Established; + } +}; + +} // namespace ble_client +} // namespace esphome + +#endif diff --git a/esphome/components/ble_client/ble_client.cpp b/esphome/components/ble_client/ble_client.cpp new file mode 100644 index 0000000000..391f6425e2 --- /dev/null +++ b/esphome/components/ble_client/ble_client.cpp @@ -0,0 +1,380 @@ +#include "esphome/core/log.h" +#include "esphome/core/application.h" +#include "esphome/core/helpers.h" +#include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h" +#include "ble_client.h" + +#ifdef ARDUINO_ARCH_ESP32 + +namespace esphome { +namespace ble_client { + +static const char *TAG = "ble_client"; + +void BLEClient::setup() { + auto ret = esp_ble_gattc_app_register(this->app_id); + if (ret) { + ESP_LOGE(TAG, "gattc app register failed. app_id=%d code=%d", this->app_id, ret); + this->mark_failed(); + } + this->set_states(espbt::ClientState::Idle); + this->enabled = true; +} + +void BLEClient::loop() { + if (this->state() == espbt::ClientState::Discovered) { + this->connect(); + } + for (auto *node : this->nodes_) + node->loop(); +} + +void BLEClient::dump_config() { + ESP_LOGCONFIG(TAG, "BLE Client:"); + ESP_LOGCONFIG(TAG, " Address: %s", this->address_str().c_str()); +} + +bool BLEClient::parse_device(const espbt::ESPBTDevice &device) { + if (!this->enabled) + return false; + if (device.address_uint64() != this->address) + return false; + if (this->state() != espbt::ClientState::Idle) + return false; + + ESP_LOGD(TAG, "Found device at MAC address [%s]", device.address_str().c_str()); + this->set_states(espbt::ClientState::Discovered); + + auto addr = device.address_uint64(); + this->remote_bda[0] = (addr >> 40) & 0xFF; + this->remote_bda[1] = (addr >> 32) & 0xFF; + this->remote_bda[2] = (addr >> 24) & 0xFF; + this->remote_bda[3] = (addr >> 16) & 0xFF; + this->remote_bda[4] = (addr >> 8) & 0xFF; + this->remote_bda[5] = (addr >> 0) & 0xFF; + return true; +} + +std::string BLEClient::address_str() const { + char buf[20]; + sprintf(buf, "%02x:%02x:%02x:%02x:%02x:%02x", (uint8_t)(this->address >> 40) & 0xff, + (uint8_t)(this->address >> 32) & 0xff, (uint8_t)(this->address >> 24) & 0xff, + (uint8_t)(this->address >> 16) & 0xff, (uint8_t)(this->address >> 8) & 0xff, + (uint8_t)(this->address >> 0) & 0xff); + std::string ret; + ret = buf; + return ret; +} + +void BLEClient::set_enabled(bool enabled) { + if (enabled == this->enabled) + return; + if (!enabled && this->state() != espbt::ClientState::Idle) { + ESP_LOGI(TAG, "[%s] Disabling BLE client.", this->address_str().c_str()); + auto ret = esp_ble_gattc_close(this->gattc_if, this->conn_id); + if (ret) { + ESP_LOGW(TAG, "esp_ble_gattc_close error, address=%s status=%d", this->address_str().c_str(), ret); + } + } + this->enabled = enabled; +} + +void BLEClient::connect() { + ESP_LOGI(TAG, "Attempting BLE connection to %s", this->address_str().c_str()); + auto ret = esp_ble_gattc_open(this->gattc_if, this->remote_bda, BLE_ADDR_TYPE_PUBLIC, true); + if (ret) { + ESP_LOGW(TAG, "esp_ble_gattc_open error, address=%s status=%d", this->address_str().c_str(), ret); + this->set_states(espbt::ClientState::Idle); + } else { + this->set_states(espbt::ClientState::Connecting); + } +} + +void BLEClient::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t esp_gattc_if, + esp_ble_gattc_cb_param_t *param) { + if (event == ESP_GATTC_REG_EVT && this->app_id != param->reg.app_id) + return; + if (event != ESP_GATTC_REG_EVT && esp_gattc_if != ESP_GATT_IF_NONE && gattc_if != this->gattc_if) + return; + + bool all_established = this->all_nodes_established(); + + switch (event) { + case ESP_GATTC_REG_EVT: { + if (param->reg.status == ESP_GATT_OK) { + ESP_LOGI(TAG, "gattc registered app id %d", this->app_id); + this->gattc_if = esp_gattc_if; + } else { + ESP_LOGE(TAG, "gattc app registration failed id=%d code=%d", param->reg.app_id, param->reg.status); + } + break; + } + case ESP_GATTC_OPEN_EVT: { + ESP_LOGI(TAG, "[%s] ESP_GATTC_OPEN_EVT", this->address_str().c_str()); + if (param->open.status != ESP_GATT_OK) { + ESP_LOGW(TAG, "connect to %s failed, status=%d", this->address_str().c_str(), param->open.status); + this->set_states(espbt::ClientState::Idle); + break; + } + this->conn_id = param->open.conn_id; + auto ret = esp_ble_gattc_send_mtu_req(this->gattc_if, param->open.conn_id); + if (ret) { + ESP_LOGW(TAG, "esp_ble_gattc_send_mtu_req failed, status=%d", ret); + } + break; + } + case ESP_GATTC_CFG_MTU_EVT: { + if (param->cfg_mtu.status != ESP_GATT_OK) { + ESP_LOGW(TAG, "cfg_mtu to %s failed, status %d", this->address_str().c_str(), param->cfg_mtu.status); + this->set_states(espbt::ClientState::Idle); + break; + } + ESP_LOGV(TAG, "cfg_mtu status %d, mtu %d", param->cfg_mtu.status, param->cfg_mtu.mtu); + esp_ble_gattc_search_service(esp_gattc_if, param->cfg_mtu.conn_id, NULL); + break; + } + case ESP_GATTC_DISCONNECT_EVT: { + if (memcmp(param->disconnect.remote_bda, this->remote_bda, 6) != 0) { + return; + } + ESP_LOGI(TAG, "[%s] ESP_GATTC_DISCONNECT_EVT", this->address_str().c_str()); + for (auto &svc : this->services_) + delete svc; + this->services_.clear(); + this->set_states(espbt::ClientState::Idle); + break; + } + case ESP_GATTC_SEARCH_RES_EVT: { + BLEService *ble_service = new BLEService(); + ble_service->uuid = espbt::ESPBTUUID::from_uuid(param->search_res.srvc_id.uuid); + ble_service->start_handle = param->search_res.start_handle; + ble_service->end_handle = param->search_res.end_handle; + ble_service->client = this; + this->services_.push_back(ble_service); + break; + } + case ESP_GATTC_SEARCH_CMPL_EVT: { + ESP_LOGV(TAG, "[%s] ESP_GATTC_SEARCH_CMPL_EVT", this->address_str().c_str()); + for (auto &svc : this->services_) { + ESP_LOGI(TAG, "Service UUID: %s", svc->uuid.to_string().c_str()); + ESP_LOGI(TAG, " start_handle: 0x%x end_handle: 0x%x", svc->start_handle, svc->end_handle); + svc->parse_characteristics(); + } + this->set_states(espbt::ClientState::Connected); + this->set_state(espbt::ClientState::Established); + break; + } + case ESP_GATTC_REG_FOR_NOTIFY_EVT: { + auto descr = this->get_config_descriptor(param->reg_for_notify.handle); + if (descr == nullptr) { + ESP_LOGW(TAG, "No descriptor found for notify of handle 0x%x", param->reg_for_notify.handle); + break; + } + if (descr->uuid.get_uuid().len != ESP_UUID_LEN_16 || + descr->uuid.get_uuid().uuid.uuid16 != ESP_GATT_UUID_CHAR_CLIENT_CONFIG) { + ESP_LOGW(TAG, "Handle 0x%x (uuid %s) is not a client config char uuid", param->reg_for_notify.handle, + descr->uuid.to_string().c_str()); + break; + } + uint8_t notify_en = 1; + auto status = esp_ble_gattc_write_char_descr(this->gattc_if, this->conn_id, descr->handle, sizeof(notify_en), + ¬ify_en, ESP_GATT_WRITE_TYPE_RSP, ESP_GATT_AUTH_REQ_NONE); + if (status) { + ESP_LOGW(TAG, "esp_ble_gattc_write_char_descr error, status=%d", status); + } + break; + } + + default: + break; + } + for (auto *node : this->nodes_) + node->gattc_event_handler(event, esp_gattc_if, param); + + // Delete characteristics after clients have used them to save RAM. + if (!all_established && this->all_nodes_established()) { + for (auto &svc : this->services_) + delete svc; + this->services_.clear(); + } +} + +// Parse GATT values into a float for a sensor. +// Ref: https://www.bluetooth.com/specifications/assigned-numbers/format-types/ +float BLEClient::parse_char_value(uint8_t *value, uint16_t length) { + // A length of one means a single octet value. + if (length == 0) + return 0; + if (length == 1) + return (float) ((uint8_t) value[0]); + + switch (value[0]) { + case 0x1: // boolean. + case 0x2: // 2bit. + case 0x3: // nibble. + case 0x4: // uint8. + return (float) ((uint8_t) value[1]); + case 0x5: // uint12. + case 0x6: // uint16. + return (float) ((uint16_t)(value[1] << 8) + (uint16_t) value[2]); + case 0x7: // uint24. + return (float) ((uint32_t)(value[1] << 16) + (uint32_t)(value[2] << 8) + (uint32_t)(value[3])); + case 0x8: // uint32. + return (float) ((uint32_t)(value[1] << 24) + (uint32_t)(value[2] << 16) + (uint32_t)(value[3] << 8) + + (uint32_t)(value[4])); + case 0xC: // int8. + return (float) ((int8_t) value[1]); + case 0xD: // int12. + case 0xE: // int16. + return (float) ((int16_t)(value[1] << 8) + (int16_t) value[2]); + case 0xF: // int24. + return (float) ((int32_t)(value[1] << 16) + (int32_t)(value[2] << 8) + (int32_t)(value[3])); + case 0x10: // int32. + return (float) ((int32_t)(value[1] << 24) + (int32_t)(value[2] << 16) + (int32_t)(value[3] << 8) + + (int32_t)(value[4])); + } + ESP_LOGW(TAG, "Cannot parse characteristic value of type 0x%x", value[0]); + return NAN; +} + +BLEService *BLEClient::get_service(espbt::ESPBTUUID uuid) { + for (auto svc : this->services_) + if (svc->uuid == uuid) + return svc; + return nullptr; +} + +BLEService *BLEClient::get_service(uint16_t uuid) { return this->get_service(espbt::ESPBTUUID::from_uint16(uuid)); } + +BLECharacteristic *BLEClient::get_characteristic(espbt::ESPBTUUID service, espbt::ESPBTUUID chr) { + auto svc = this->get_service(service); + if (svc == nullptr) + return nullptr; + return svc->get_characteristic(chr); +} + +BLECharacteristic *BLEClient::get_characteristic(uint16_t service, uint16_t chr) { + return this->get_characteristic(espbt::ESPBTUUID::from_uint16(service), espbt::ESPBTUUID::from_uint16(chr)); +} + +BLEDescriptor *BLEClient::get_config_descriptor(uint16_t handle) { + for (auto &svc : this->services_) + for (auto &chr : svc->characteristics) + if (chr->handle == handle) + for (auto &desc : chr->descriptors) + if (desc->uuid == espbt::ESPBTUUID::from_uint16(0x2902)) + return desc; + return nullptr; +} + +BLECharacteristic *BLEService::get_characteristic(espbt::ESPBTUUID uuid) { + for (auto &chr : this->characteristics) + if (chr->uuid == uuid) + return chr; + return nullptr; +} + +BLECharacteristic *BLEService::get_characteristic(uint16_t uuid) { + return this->get_characteristic(espbt::ESPBTUUID::from_uint16(uuid)); +} + +BLEDescriptor *BLEClient::get_descriptor(espbt::ESPBTUUID service, espbt::ESPBTUUID chr, espbt::ESPBTUUID descr) { + auto svc = this->get_service(service); + if (svc == nullptr) + return nullptr; + auto ch = svc->get_characteristic(chr); + if (ch == nullptr) + return nullptr; + return ch->get_descriptor(descr); +} + +BLEDescriptor *BLEClient::get_descriptor(uint16_t service, uint16_t chr, uint16_t descr) { + return this->get_descriptor(espbt::ESPBTUUID::from_uint16(service), espbt::ESPBTUUID::from_uint16(chr), + espbt::ESPBTUUID::from_uint16(descr)); +} + +BLEService::~BLEService() { + for (auto &chr : this->characteristics) + delete chr; +} + +void BLEService::parse_characteristics() { + uint16_t offset = 0; + esp_gattc_char_elem_t result; + + while (true) { + uint16_t count = 1; + esp_gatt_status_t status = esp_ble_gattc_get_all_char( + this->client->gattc_if, this->client->conn_id, this->start_handle, this->end_handle, &result, &count, offset); + if (status == ESP_GATT_INVALID_OFFSET || status == ESP_GATT_NOT_FOUND) { + break; + } + if (status != ESP_GATT_OK) { + ESP_LOGW(TAG, "esp_ble_gattc_get_all_char error, status=%d", status); + break; + } + if (count == 0) { + break; + } + + BLECharacteristic *characteristic = new BLECharacteristic(); + characteristic->uuid = espbt::ESPBTUUID::from_uuid(result.uuid); + characteristic->properties = result.properties; + characteristic->handle = result.char_handle; + characteristic->service = this; + this->characteristics.push_back(characteristic); + ESP_LOGI(TAG, " characteristic %s, handle 0x%x, properties 0x%x", characteristic->uuid.to_string().c_str(), + characteristic->handle, characteristic->properties); + characteristic->parse_descriptors(); + offset++; + } +} + +BLECharacteristic::~BLECharacteristic() { + for (auto &desc : this->descriptors) + delete desc; +} + +void BLECharacteristic::parse_descriptors() { + uint16_t offset = 0; + esp_gattc_descr_elem_t result; + + while (true) { + uint16_t count = 1; + esp_gatt_status_t status = esp_ble_gattc_get_all_descr( + this->service->client->gattc_if, this->service->client->conn_id, this->handle, &result, &count, offset); + if (status == ESP_GATT_INVALID_OFFSET || status == ESP_GATT_NOT_FOUND) { + break; + } + if (status != ESP_GATT_OK) { + ESP_LOGW(TAG, "esp_ble_gattc_get_all_descr error, status=%d", status); + break; + } + if (count == 0) { + break; + } + + BLEDescriptor *desc = new BLEDescriptor(); + desc->uuid = espbt::ESPBTUUID::from_uuid(result.uuid); + desc->handle = result.handle; + desc->characteristic = this; + this->descriptors.push_back(desc); + ESP_LOGI(TAG, " descriptor %s, handle 0x%x", desc->uuid.to_string().c_str(), desc->handle); + offset++; + } +} + +BLEDescriptor *BLECharacteristic::get_descriptor(espbt::ESPBTUUID uuid) { + for (auto &desc : this->descriptors) + if (desc->uuid == uuid) + return desc; + return nullptr; +} +BLEDescriptor *BLECharacteristic::get_descriptor(uint16_t uuid) { + return this->get_descriptor(espbt::ESPBTUUID::from_uint16(uuid)); +} + +} // namespace ble_client +} // namespace esphome + +#endif diff --git a/esphome/components/ble_client/ble_client.h b/esphome/components/ble_client/ble_client.h new file mode 100644 index 0000000000..d99753e9da --- /dev/null +++ b/esphome/components/ble_client/ble_client.h @@ -0,0 +1,140 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/core/helpers.h" +#include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h" + +#ifdef ARDUINO_ARCH_ESP32 + +#include +#include +#include +#include +#include + +namespace espbt = esphome::esp32_ble_tracker; + +namespace esphome { +namespace ble_client { + +class BLEClient; +class BLEService; +class BLECharacteristic; + +class BLEClientNode { + public: + virtual void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, + esp_ble_gattc_cb_param_t *param) = 0; + virtual void loop() = 0; + void set_address(uint64_t address) { address_ = address; } + espbt::ESPBTClient *client; + // This should be transitioned to Established once the node no longer needs + // the services/descriptors/characteristics of the parent client. This will + // allow some memory to be freed. + espbt::ClientState node_state; + + BLEClient *parent() { return this->parent_; } + void set_ble_client_parent(BLEClient *parent) { this->parent_ = parent; } + + protected: + BLEClient *parent_; + uint64_t address_; +}; + +class BLEDescriptor { + public: + espbt::ESPBTUUID uuid; + uint16_t handle; + + BLECharacteristic *characteristic; +}; + +class BLECharacteristic { + public: + ~BLECharacteristic(); + espbt::ESPBTUUID uuid; + uint16_t handle; + esp_gatt_char_prop_t properties; + std::vector descriptors; + void parse_descriptors(); + BLEDescriptor *get_descriptor(espbt::ESPBTUUID uuid); + BLEDescriptor *get_descriptor(uint16_t uuid); + + BLEService *service; +}; + +class BLEService { + public: + ~BLEService(); + espbt::ESPBTUUID uuid; + uint16_t start_handle; + uint16_t end_handle; + std::vector characteristics; + BLEClient *client; + void parse_characteristics(); + BLECharacteristic *get_characteristic(espbt::ESPBTUUID uuid); + BLECharacteristic *get_characteristic(uint16_t uuid); +}; + +class BLEClient : public espbt::ESPBTClient, public Component { + public: + void setup() override; + void dump_config() override; + void loop() override; + + void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, esp_ble_gattc_cb_param_t *param); + bool parse_device(const espbt::ESPBTDevice &device) override; + void on_scan_end() override {} + void connect(); + + void set_address(uint64_t address) { this->address = address; } + + void set_enabled(bool enabled); + + void register_ble_node(BLEClientNode *node) { + node->client = this; + node->set_ble_client_parent(this); + this->nodes_.push_back(node); + } + + BLEService *get_service(espbt::ESPBTUUID uuid); + BLEService *get_service(uint16_t uuid); + BLECharacteristic *get_characteristic(espbt::ESPBTUUID service, espbt::ESPBTUUID chr); + BLECharacteristic *get_characteristic(uint16_t service, uint16_t chr); + BLEDescriptor *get_descriptor(espbt::ESPBTUUID service, espbt::ESPBTUUID chr, espbt::ESPBTUUID descr); + BLEDescriptor *get_descriptor(uint16_t service, uint16_t chr, uint16_t descr); + // Get the configuration descriptor for the given characteristic handle. + BLEDescriptor *get_config_descriptor(uint16_t handle); + + float parse_char_value(uint8_t *value, uint16_t length); + + int gattc_if; + esp_bd_addr_t remote_bda; + uint16_t conn_id; + uint64_t address; + bool enabled; + std::string address_str() const; + + protected: + void set_states(espbt::ClientState st) { + this->set_state(st); + for (auto &node : nodes_) + node->node_state = st; + } + bool all_nodes_established() { + if (this->state() != espbt::ClientState::Established) + return false; + for (auto &node : nodes_) + if (node->node_state != espbt::ClientState::Established) + return false; + return true; + } + + std::vector nodes_; + std::vector services_; +}; + +} // namespace ble_client +} // namespace esphome + +#endif diff --git a/esphome/components/ble_client/sensor/__init__.py b/esphome/components/ble_client/sensor/__init__.py new file mode 100644 index 0000000000..27eca87a37 --- /dev/null +++ b/esphome/components/ble_client/sensor/__init__.py @@ -0,0 +1,115 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import sensor, ble_client, esp32_ble_tracker +from esphome.const import ( + DEVICE_CLASS_EMPTY, + CONF_ID, + UNIT_EMPTY, + ICON_EMPTY, + CONF_TRIGGER_ID, + CONF_SERVICE_UUID, +) +from esphome import automation +from .. import ble_client_ns + +DEPENDENCIES = ["ble_client"] + +CONF_CHARACTERISTIC_UUID = "characteristic_uuid" +CONF_DESCRIPTOR_UUID = "descriptor_uuid" + +CONF_NOTIFY = "notify" +CONF_ON_NOTIFY = "on_notify" + +BLESensor = ble_client_ns.class_( + "BLESensor", sensor.Sensor, cg.PollingComponent, ble_client.BLEClientNode +) +BLESensorNotifyTrigger = ble_client_ns.class_( + "BLESensorNotifyTrigger", automation.Trigger.template(cg.float_) +) + +CONFIG_SCHEMA = cv.All( + sensor.sensor_schema(UNIT_EMPTY, ICON_EMPTY, 0, DEVICE_CLASS_EMPTY) + .extend( + { + cv.GenerateID(): cv.declare_id(BLESensor), + cv.Required(CONF_SERVICE_UUID): esp32_ble_tracker.bt_uuid, + cv.Required(CONF_CHARACTERISTIC_UUID): esp32_ble_tracker.bt_uuid, + cv.Optional(CONF_DESCRIPTOR_UUID): esp32_ble_tracker.bt_uuid, + cv.Optional(CONF_NOTIFY, default=False): cv.boolean, + cv.Optional(CONF_ON_NOTIFY): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( + BLESensorNotifyTrigger + ), + } + ), + } + ) + .extend(cv.polling_component_schema("60s")) + .extend(ble_client.BLE_CLIENT_SCHEMA) +) + + +def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + if len(config[CONF_SERVICE_UUID]) == len(esp32_ble_tracker.bt_uuid16_format): + cg.add( + var.set_service_uuid16(esp32_ble_tracker.as_hex(config[CONF_SERVICE_UUID])) + ) + elif len(config[CONF_SERVICE_UUID]) == len(esp32_ble_tracker.bt_uuid32_format): + cg.add( + var.set_service_uuid32(esp32_ble_tracker.as_hex(config[CONF_SERVICE_UUID])) + ) + elif len(config[CONF_SERVICE_UUID]) == len(esp32_ble_tracker.bt_uuid128_format): + uuid128 = esp32_ble_tracker.as_hex_array(config[CONF_SERVICE_UUID]) + cg.add(var.set_service_uuid128(uuid128)) + + if len(config[CONF_CHARACTERISTIC_UUID]) == len(esp32_ble_tracker.bt_uuid16_format): + cg.add( + var.set_char_uuid16( + esp32_ble_tracker.as_hex(config[CONF_CHARACTERISTIC_UUID]) + ) + ) + elif len(config[CONF_CHARACTERISTIC_UUID]) == len( + esp32_ble_tracker.bt_uuid32_format + ): + cg.add( + var.set_char_uuid32( + esp32_ble_tracker.as_hex(config[CONF_CHARACTERISTIC_UUID]) + ) + ) + elif len(config[CONF_CHARACTERISTIC_UUID]) == len( + esp32_ble_tracker.bt_uuid128_format + ): + uuid128 = esp32_ble_tracker.as_hex_array(config[CONF_CHARACTERISTIC_UUID]) + cg.add(var.set_char_uuid128(uuid128)) + + if CONF_DESCRIPTOR_UUID in config: + if len(config[CONF_DESCRIPTOR_UUID]) == len(esp32_ble_tracker.bt_uuid16_format): + cg.add( + var.set_descr_uuid16( + esp32_ble_tracker.as_hex(config[CONF_DESCRIPTOR_UUID]) + ) + ) + elif len(config[CONF_DESCRIPTOR_UUID]) == len( + esp32_ble_tracker.bt_uuid32_format + ): + cg.add( + var.set_descr_uuid32( + esp32_ble_tracker.as_hex(config[CONF_DESCRIPTOR_UUID]) + ) + ) + elif len(config[CONF_DESCRIPTOR_UUID]) == len( + esp32_ble_tracker.bt_uuid128_format + ): + uuid128 = esp32_ble_tracker.as_hex_array(config[CONF_DESCRIPTOR_UUID]) + cg.add(var.set_descr_uuid128(uuid128)) + + yield cg.register_component(var, config) + yield ble_client.register_ble_node(var, config) + cg.add(var.set_enable_notify(config[CONF_NOTIFY])) + yield sensor.register_sensor(var, config) + for conf in config.get(CONF_ON_NOTIFY, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) + yield ble_client.register_ble_node(trigger, config) + yield automation.build_automation(trigger, [(float, "x")], conf) diff --git a/esphome/components/ble_client/sensor/automation.h b/esphome/components/ble_client/sensor/automation.h new file mode 100644 index 0000000000..a528493947 --- /dev/null +++ b/esphome/components/ble_client/sensor/automation.h @@ -0,0 +1,37 @@ +#pragma once + +#include "esphome/core/automation.h" +#include "esphome/components/ble_client/sensor/ble_sensor.h" + +#ifdef ARDUINO_ARCH_ESP32 + +namespace esphome { +namespace ble_client { + +class BLESensorNotifyTrigger : public Trigger, public BLESensor { + public: + explicit BLESensorNotifyTrigger(BLESensor *sensor) { sensor_ = sensor; } + void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, esp_ble_gattc_cb_param_t *param) { + switch (event) { + case ESP_GATTC_SEARCH_CMPL_EVT: { + this->sensor_->node_state = espbt::ClientState::Established; + break; + } + case ESP_GATTC_NOTIFY_EVT: { + if (param->notify.conn_id != this->sensor_->parent()->conn_id || param->notify.handle != this->sensor_->handle) + break; + this->trigger(this->sensor_->parent()->parse_char_value(param->notify.value, param->notify.value_len)); + } + default: + break; + } + } + + protected: + BLESensor *sensor_; +}; + +} // namespace ble_client +} // namespace esphome + +#endif diff --git a/esphome/components/ble_client/sensor/ble_sensor.cpp b/esphome/components/ble_client/sensor/ble_sensor.cpp new file mode 100644 index 0000000000..d1b4801021 --- /dev/null +++ b/esphome/components/ble_client/sensor/ble_sensor.cpp @@ -0,0 +1,129 @@ +#include "ble_sensor.h" +#include "esphome/core/log.h" +#include "esphome/core/application.h" +#include "esphome/core/helpers.h" +#include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h" + +#ifdef ARDUINO_ARCH_ESP32 + +namespace esphome { +namespace ble_client { + +static const char *TAG = "ble_sensor"; + +uint32_t BLESensor::hash_base() { return 343459825UL; } + +void BLESensor::loop() {} + +void BLESensor::dump_config() { + LOG_SENSOR("", "BLE Sensor", this); + ESP_LOGCONFIG(TAG, " MAC address : %s", this->parent()->address_str().c_str()); + ESP_LOGCONFIG(TAG, " Service UUID : %s", this->service_uuid_.to_string().c_str()); + ESP_LOGCONFIG(TAG, " Characteristic UUID: %s", this->char_uuid_.to_string().c_str()); + ESP_LOGCONFIG(TAG, " Descriptor UUID : %s", this->descr_uuid_.to_string().c_str()); + ESP_LOGCONFIG(TAG, " Notifications : %s", YESNO(this->notify_)); + LOG_UPDATE_INTERVAL(this); +} + +void BLESensor::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, + esp_ble_gattc_cb_param_t *param) { + switch (event) { + case ESP_GATTC_OPEN_EVT: { + if (param->open.status == ESP_GATT_OK) { + ESP_LOGW(TAG, "[%s] Connected successfully!", this->get_name().c_str()); + break; + } + break; + } + case ESP_GATTC_DISCONNECT_EVT: { + ESP_LOGW(TAG, "[%s] Disconnected!", this->get_name().c_str()); + this->status_set_warning(); + this->publish_state(NAN); + break; + } + case ESP_GATTC_SEARCH_CMPL_EVT: { + this->handle = 0; + auto chr = this->parent()->get_characteristic(this->service_uuid_, this->char_uuid_); + if (chr == nullptr) { + this->status_set_warning(); + this->publish_state(NAN); + ESP_LOGW(TAG, "No sensor characteristic found at service %s char %s", this->service_uuid_.to_string().c_str(), + this->char_uuid_.to_string().c_str()); + break; + } + this->handle = chr->handle; + if (this->descr_uuid_.get_uuid().len > 0) { + auto descr = chr->get_descriptor(this->descr_uuid_); + if (descr == nullptr) { + this->status_set_warning(); + this->publish_state(NAN); + ESP_LOGW(TAG, "No sensor descriptor found at service %s char %s descr %s", + this->service_uuid_.to_string().c_str(), this->char_uuid_.to_string().c_str(), + this->descr_uuid_.to_string().c_str()); + break; + } + this->handle = descr->handle; + } + if (this->notify_) { + auto status = + esp_ble_gattc_register_for_notify(this->parent()->gattc_if, this->parent()->remote_bda, chr->handle); + if (status) { + ESP_LOGW(TAG, "esp_ble_gattc_register_for_notify failed, status=%d", status); + } + } else { + this->node_state = espbt::ClientState::Established; + } + break; + } + case ESP_GATTC_READ_CHAR_EVT: { + if (param->read.conn_id != this->parent()->conn_id) + break; + if (param->read.status != ESP_GATT_OK) { + ESP_LOGW(TAG, "Error reading char at handle %d, status=%d", param->read.handle, param->read.status); + break; + } + if (param->read.handle == this->handle) { + this->status_clear_warning(); + this->publish_state((float) param->read.value[0]); + } + break; + } + case ESP_GATTC_NOTIFY_EVT: { + if (param->notify.conn_id != this->parent()->conn_id || param->notify.handle != this->handle) + break; + ESP_LOGI(TAG, "[%s] ESP_GATTC_NOTIFY_EVT: handle=0x%x, value=0x%x", this->get_name().c_str(), + param->notify.handle, param->notify.value[0]); + this->publish_state((float) param->notify.value[0]); + break; + } + case ESP_GATTC_REG_FOR_NOTIFY_EVT: { + this->node_state = espbt::ClientState::Established; + break; + } + default: + break; + } +} + +void BLESensor::update() { + if (this->node_state != espbt::ClientState::Established) { + ESP_LOGW(TAG, "[%s] Cannot poll, not connected", this->get_name().c_str()); + return; + } + if (this->handle == 0) { + ESP_LOGW(TAG, "[%s] Cannot poll, no service or characteristic found", this->get_name().c_str()); + return; + } + + auto status = + esp_ble_gattc_read_char(this->parent()->gattc_if, this->parent()->conn_id, this->handle, ESP_GATT_AUTH_REQ_NONE); + if (status) { + this->status_set_warning(); + this->publish_state(NAN); + ESP_LOGW(TAG, "[%s] Error sending read request for sensor, status=%d", this->get_name().c_str(), status); + } +} + +} // namespace ble_client +} // namespace esphome +#endif diff --git a/esphome/components/ble_client/sensor/ble_sensor.h b/esphome/components/ble_client/sensor/ble_sensor.h new file mode 100644 index 0000000000..e3a8a8ea25 --- /dev/null +++ b/esphome/components/ble_client/sensor/ble_sensor.h @@ -0,0 +1,46 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/ble_client/ble_client.h" +#include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h" +#include "esphome/components/sensor/sensor.h" + +#ifdef ARDUINO_ARCH_ESP32 +#include + +namespace esphome { +namespace ble_client { + +namespace espbt = esphome::esp32_ble_tracker; + +class BLESensor : public sensor::Sensor, public PollingComponent, public BLEClientNode { + public: + void loop() override; + void update() override; + void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, + esp_ble_gattc_cb_param_t *param) override; + void dump_config() override; + float get_setup_priority() const override { return setup_priority::DATA; } + void set_service_uuid16(uint16_t uuid) { this->service_uuid_ = espbt::ESPBTUUID::from_uint16(uuid); } + void set_service_uuid32(uint32_t uuid) { this->service_uuid_ = espbt::ESPBTUUID::from_uint32(uuid); } + void set_service_uuid128(uint8_t *uuid) { this->service_uuid_ = espbt::ESPBTUUID::from_raw(uuid); } + void set_char_uuid16(uint16_t uuid) { this->char_uuid_ = espbt::ESPBTUUID::from_uint16(uuid); } + void set_char_uuid32(uint32_t uuid) { this->char_uuid_ = espbt::ESPBTUUID::from_uint32(uuid); } + void set_char_uuid128(uint8_t *uuid) { this->char_uuid_ = espbt::ESPBTUUID::from_raw(uuid); } + void set_descr_uuid16(uint16_t uuid) { this->descr_uuid_ = espbt::ESPBTUUID::from_uint16(uuid); } + void set_descr_uuid32(uint32_t uuid) { this->descr_uuid_ = espbt::ESPBTUUID::from_uint32(uuid); } + void set_descr_uuid128(uint8_t *uuid) { this->descr_uuid_ = espbt::ESPBTUUID::from_raw(uuid); } + void set_enable_notify(bool notify) { this->notify_ = notify; } + uint16_t handle; + + protected: + uint32_t hash_base() override; + bool notify_; + espbt::ESPBTUUID service_uuid_; + espbt::ESPBTUUID char_uuid_; + espbt::ESPBTUUID descr_uuid_; +}; + +} // namespace ble_client +} // namespace esphome +#endif diff --git a/esphome/components/ble_client/switch/__init__.py b/esphome/components/ble_client/switch/__init__.py new file mode 100644 index 0000000000..acc8683407 --- /dev/null +++ b/esphome/components/ble_client/switch/__init__.py @@ -0,0 +1,30 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import switch, ble_client +from esphome.const import CONF_ICON, CONF_ID, CONF_INVERTED, ICON_BLUETOOTH +from .. import ble_client_ns + +BLEClientSwitch = ble_client_ns.class_( + "BLEClientSwitch", switch.Switch, cg.Component, ble_client.BLEClientNode +) + +CONFIG_SCHEMA = ( + switch.SWITCH_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(BLEClientSwitch), + cv.Optional(CONF_INVERTED): cv.invalid( + "BLE client switches do not support inverted mode!" + ), + cv.Optional(CONF_ICON, default=ICON_BLUETOOTH): switch.icon, + } + ) + .extend(ble_client.BLE_CLIENT_SCHEMA) + .extend(cv.COMPONENT_SCHEMA) +) + + +def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + yield cg.register_component(var, config) + yield switch.register_switch(var, config) + yield ble_client.register_ble_node(var, config) diff --git a/esphome/components/ble_client/switch/ble_switch.cpp b/esphome/components/ble_client/switch/ble_switch.cpp new file mode 100644 index 0000000000..ef7b64be66 --- /dev/null +++ b/esphome/components/ble_client/switch/ble_switch.cpp @@ -0,0 +1,39 @@ +#include "ble_switch.h" +#include "esphome/core/log.h" +#include "esphome/core/application.h" + +#ifdef ARDUINO_ARCH_ESP32 + +namespace esphome { +namespace ble_client { + +static const char *TAG = "ble_switch"; + +void BLEClientSwitch::write_state(bool state) { + this->parent_->set_enabled(state); + this->publish_state(state); +} + +void BLEClientSwitch::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, + esp_ble_gattc_cb_param_t *param) { + switch (event) { + case ESP_GATTC_REG_EVT: + this->publish_state(this->parent_->enabled); + break; + case ESP_GATTC_OPEN_EVT: + this->node_state = espbt::ClientState::Established; + break; + case ESP_GATTC_DISCONNECT_EVT: + this->node_state = espbt::ClientState::Idle; + this->publish_state(this->parent_->enabled); + break; + default: + break; + } +} + +void BLEClientSwitch::dump_config() { LOG_SWITCH("", "BLE Client Switch", this); } + +} // namespace ble_client +} // namespace esphome +#endif diff --git a/esphome/components/ble_client/switch/ble_switch.h b/esphome/components/ble_client/switch/ble_switch.h new file mode 100644 index 0000000000..f91af533f1 --- /dev/null +++ b/esphome/components/ble_client/switch/ble_switch.h @@ -0,0 +1,30 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/ble_client/ble_client.h" +#include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h" +#include "esphome/components/switch/switch.h" + +#ifdef ARDUINO_ARCH_ESP32 +#include + +namespace esphome { +namespace ble_client { + +namespace espbt = esphome::esp32_ble_tracker; + +class BLEClientSwitch : public switch_::Switch, public Component, public BLEClientNode { + public: + void dump_config() override; + void loop() override {} + void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, + esp_ble_gattc_cb_param_t *param) override; + float get_setup_priority() const override { return setup_priority::DATA; } + + protected: + void write_state(bool state) override; +}; + +} // namespace ble_client +} // namespace esphome +#endif diff --git a/esphome/components/esp32_ble_tracker/__init__.py b/esphome/components/esp32_ble_tracker/__init__.py index 2e567b49bc..8726ab3e8d 100644 --- a/esphome/components/esp32_ble_tracker/__init__.py +++ b/esphome/components/esp32_ble_tracker/__init__.py @@ -26,6 +26,7 @@ CONF_WINDOW = "window" CONF_ACTIVE = "active" esp32_ble_tracker_ns = cg.esphome_ns.namespace("esp32_ble_tracker") ESP32BLETracker = esp32_ble_tracker_ns.class_("ESP32BLETracker", cg.Component) +ESPBTClient = esp32_ble_tracker_ns.class_("ESPBTClient") ESPBTDeviceListener = esp32_ble_tracker_ns.class_("ESPBTDeviceListener") ESPBTDevice = esp32_ble_tracker_ns.class_("ESPBTDevice") ESPBTDeviceConstRef = ESPBTDevice.operator("ref").operator("const") @@ -220,3 +221,10 @@ def register_ble_device(var, config): paren = yield cg.get_variable(config[CONF_ESP32_BLE_ID]) cg.add(paren.register_listener(var)) yield var + + +@coroutine +def register_client(var, config): + paren = yield cg.get_variable(config[CONF_ESP32_BLE_ID]) + cg.add(paren.register_client(var)) + yield var diff --git a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp index b2d303da15..7a5b023387 100644 --- a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp +++ b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp @@ -48,7 +48,23 @@ void ESP32BLETracker::setup() { } void ESP32BLETracker::loop() { - if (xSemaphoreTake(this->scan_end_lock_, 0L)) { + BLEEvent *ble_event = this->ble_events_.pop(); + while (ble_event != nullptr) { + if (ble_event->type_) + this->real_gattc_event_handler(ble_event->event_.gattc.gattc_event, ble_event->event_.gattc.gattc_if, + &ble_event->event_.gattc.gattc_param); + else + this->real_gap_event_handler(ble_event->event_.gap.gap_event, &ble_event->event_.gap.gap_param); + delete ble_event; + ble_event = this->ble_events_.pop(); + } + + bool connecting = false; + for (auto *client : this->clients_) { + if (client->state() == ClientState::Connecting || client->state() == ClientState::Discovered) + connecting = true; + } + if (!connecting && xSemaphoreTake(this->scan_end_lock_, 0L)) { xSemaphoreGive(this->scan_end_lock_); global_esp32_ble_tracker->start_scan(false); } @@ -69,6 +85,17 @@ void ESP32BLETracker::loop() { if (listener->parse_device(device)) found = true; + for (auto *client : this->clients_) + if (client->parse_device(device)) { + found = true; + if (client->state() == ClientState::Discovered) { + esp_ble_gap_stop_scanning(); + if (xSemaphoreTake(this->scan_end_lock_, 10L / portTICK_PERIOD_MS)) { + xSemaphoreGive(this->scan_end_lock_); + } + } + } + if (!found) { this->print_bt_device_info(device); } @@ -122,6 +149,11 @@ bool ESP32BLETracker::ble_setup() { ESP_LOGE(TAG, "esp_ble_gap_register_callback failed: %d", err); return false; } + err = esp_ble_gattc_register_callback(ESP32BLETracker::gattc_event_handler); + if (err != ESP_OK) { + ESP_LOGE(TAG, "esp_ble_gattc_register_callback failed: %d", err); + return false; + } // Empty name esp_ble_gap_set_device_name(""); @@ -166,7 +198,17 @@ void ESP32BLETracker::start_scan(bool first) { }); } +void ESP32BLETracker::register_client(ESPBTClient *client) { + client->app_id = ++this->app_id_; + this->clients_.push_back(client); +} + void ESP32BLETracker::gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) { + BLEEvent *gap_event = new BLEEvent(event, param); + global_esp32_ble_tracker->ble_events_.push(gap_event); +} + +void ESP32BLETracker::real_gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) { switch (event) { case ESP_GAP_BLE_SCAN_RESULT_EVT: global_esp32_ble_tracker->gap_scan_result(param->scan_rst); @@ -177,6 +219,9 @@ void ESP32BLETracker::gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_ga case ESP_GAP_BLE_SCAN_START_COMPLETE_EVT: global_esp32_ble_tracker->gap_scan_start_complete(param->scan_start_cmpl); break; + case ESP_GAP_BLE_SCAN_STOP_COMPLETE_EVT: + global_esp32_ble_tracker->gap_scan_stop_complete(param->scan_stop_cmpl); + break; default: break; } @@ -190,6 +235,10 @@ void ESP32BLETracker::gap_scan_start_complete(const esp_ble_gap_cb_param_t::ble_ this->scan_start_failed_ = param.status; } +void ESP32BLETracker::gap_scan_stop_complete(const esp_ble_gap_cb_param_t::ble_scan_stop_cmpl_evt_param ¶m) { + xSemaphoreGive(this->scan_end_lock_); +} + void ESP32BLETracker::gap_scan_result(const esp_ble_gap_cb_param_t::ble_scan_result_evt_param ¶m) { if (param.search_evt == ESP_GAP_SEARCH_INQ_RES_EVT) { if (xSemaphoreTake(this->scan_result_lock_, 0L)) { @@ -203,6 +252,19 @@ void ESP32BLETracker::gap_scan_result(const esp_ble_gap_cb_param_t::ble_scan_res } } +void ESP32BLETracker::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, + esp_ble_gattc_cb_param_t *param) { + BLEEvent *gattc_event = new BLEEvent(event, gattc_if, param); + global_esp32_ble_tracker->ble_events_.push(gattc_event); +} + +void ESP32BLETracker::real_gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, + esp_ble_gattc_cb_param_t *param) { + for (auto *client : global_esp32_ble_tracker->clients_) { + client->gattc_event_handler(event, gattc_if, param); + } +} + ESPBTUUID::ESPBTUUID() : uuid_() {} ESPBTUUID ESPBTUUID::from_uint16(uint16_t uuid) { ESPBTUUID ret; @@ -223,6 +285,15 @@ ESPBTUUID ESPBTUUID::from_raw(const uint8_t *data) { ret.uuid_.uuid.uuid128[i] = data[i]; return ret; } +ESPBTUUID ESPBTUUID::from_uuid(esp_bt_uuid_t uuid) { + ESPBTUUID ret; + ret.uuid_.len = uuid.len; + ret.uuid_.uuid.uuid16 = uuid.uuid.uuid16; + ret.uuid_.uuid.uuid32 = uuid.uuid.uuid32; + for (size_t i = 0; i < ESP_UUID_LEN_128; i++) + ret.uuid_.uuid.uuid128[i] = uuid.uuid.uuid128[i]; + return ret; +} ESPBTUUID ESPBTUUID::as_128bit() const { if (this->uuid_.len == ESP_UUID_LEN_128) { return *this; @@ -289,16 +360,21 @@ std::string ESPBTUUID::to_string() { char sbuf[64]; switch (this->uuid_.len) { case ESP_UUID_LEN_16: - sprintf(sbuf, "%02X:%02X", this->uuid_.uuid.uuid16 >> 8, this->uuid_.uuid.uuid16 & 0xff); + sprintf(sbuf, "0x%02X%02X", this->uuid_.uuid.uuid16 >> 8, this->uuid_.uuid.uuid16 & 0xff); break; case ESP_UUID_LEN_32: - sprintf(sbuf, "%02X:%02X:%02X:%02X", this->uuid_.uuid.uuid32 >> 24, (this->uuid_.uuid.uuid32 >> 16 & 0xff), + sprintf(sbuf, "0x%02X%02X%02X%02X", this->uuid_.uuid.uuid32 >> 24, (this->uuid_.uuid.uuid32 >> 16 & 0xff), (this->uuid_.uuid.uuid32 >> 8 & 0xff), this->uuid_.uuid.uuid32 & 0xff); break; default: case ESP_UUID_LEN_128: - for (uint8_t i = 0; i < 16; i++) - sprintf(sbuf + i * 3, "%02X:", this->uuid_.uuid.uuid128[i]); + char *bpos = sbuf; + for (int8_t i = 15; i >= 0; i--) { + sprintf(bpos, "%02X", this->uuid_.uuid.uuid128[i]); + bpos += 2; + if (i == 3 || i == 5 || i == 7 || i == 9) + sprintf(bpos++, "-"); + } sbuf[47] = '\0'; break; } diff --git a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h index eef7930b78..6f0c28a73c 100644 --- a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h +++ b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h @@ -2,12 +2,14 @@ #include "esphome/core/component.h" #include "esphome/core/helpers.h" +#include "queue.h" #ifdef ARDUINO_ARCH_ESP32 #include #include #include +#include #include namespace esphome { @@ -23,6 +25,8 @@ class ESPBTUUID { static ESPBTUUID from_raw(const uint8_t *data); + static ESPBTUUID from_uuid(esp_bt_uuid_t uuid); + ESPBTUUID as_128bit() const; bool contains(uint8_t data1, uint8_t data2) const; @@ -135,6 +139,32 @@ class ESPBTDeviceListener { ESP32BLETracker *parent_{nullptr}; }; +enum class ClientState { + // Connection is idle, no device detected. + Idle, + // Device advertisement found. + Discovered, + // Connection in progress. + Connecting, + // Initial connection established. + Connected, + // The client and sub-clients have completed setup. + Established, +}; + +class ESPBTClient : public ESPBTDeviceListener { + public: + virtual void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, + esp_ble_gattc_cb_param_t *param) = 0; + virtual void connect() = 0; + void set_state(ClientState st) { this->state_ = st; } + ClientState state() const { return state_; } + int app_id; + + protected: + ClientState state_; +}; + class ESP32BLETracker : public Component { public: void set_scan_duration(uint32_t scan_duration) { scan_duration_ = scan_duration; } @@ -153,6 +183,8 @@ class ESP32BLETracker : public Component { this->listeners_.push_back(listener); } + void register_client(ESPBTClient *client); + void print_bt_device_info(const ESPBTDevice &device); protected: @@ -162,16 +194,26 @@ class ESP32BLETracker : public Component { void start_scan(bool first); /// Callback that will handle all GAP events and redistribute them to other callbacks. static void gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param); + void real_gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param); /// Called when a `ESP_GAP_BLE_SCAN_RESULT_EVT` event is received. void gap_scan_result(const esp_ble_gap_cb_param_t::ble_scan_result_evt_param ¶m); /// Called when a `ESP_GAP_BLE_SCAN_PARAM_SET_COMPLETE_EVT` event is received. void gap_scan_set_param_complete(const esp_ble_gap_cb_param_t::ble_scan_param_cmpl_evt_param ¶m); /// Called when a `ESP_GAP_BLE_SCAN_START_COMPLETE_EVT` event is received. void gap_scan_start_complete(const esp_ble_gap_cb_param_t::ble_scan_start_cmpl_evt_param ¶m); + /// Called when a `ESP_GAP_BLE_SCAN_STOP_COMPLETE_EVT` event is received. + void gap_scan_stop_complete(const esp_ble_gap_cb_param_t::ble_scan_stop_cmpl_evt_param ¶m); + + int app_id_; + /// Callback that will handle all GATTC events and redistribute them to other callbacks. + static void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, esp_ble_gattc_cb_param_t *param); + void real_gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, esp_ble_gattc_cb_param_t *param); /// Vector of addresses that have already been printed in print_bt_device_info std::vector already_discovered_; std::vector listeners_; + /// Client parameters. + std::vector clients_; /// A structure holding the ESP BLE scan parameters. esp_ble_scan_params_t scan_params_; /// The interval in seconds to perform scans. @@ -185,6 +227,8 @@ class ESP32BLETracker : public Component { esp_ble_gap_cb_param_t::ble_scan_result_evt_param scan_result_buffer_[16]; esp_bt_status_t scan_start_failed_{ESP_BT_STATUS_SUCCESS}; esp_bt_status_t scan_set_param_failed_{ESP_BT_STATUS_SUCCESS}; + + Queue ble_events_; }; extern ESP32BLETracker *global_esp32_ble_tracker; diff --git a/esphome/components/esp32_ble_tracker/queue.h b/esphome/components/esp32_ble_tracker/queue.h new file mode 100644 index 0000000000..6f36cf874d --- /dev/null +++ b/esphome/components/esp32_ble_tracker/queue.h @@ -0,0 +1,96 @@ +#pragma once +#include "esphome/core/component.h" +#include "esphome/core/helpers.h" + +#include +#include + +#ifdef ARDUINO_ARCH_ESP32 + +#include +#include + +/* + * BLE events come in from a separate Task (thread) in the ESP32 stack. Rather + * than trying to deal wth various locking strategies, all incoming GAP and GATT + * events will simply be placed on a semaphore guarded queue. The next time the + * component runs loop(), these events are popped off the queue and handed at + * this safer time. + */ + +namespace esphome { +namespace esp32_ble_tracker { + +template class Queue { + public: + Queue() { m = xSemaphoreCreateMutex(); } + + void push(T *element) { + if (element == nullptr) + return; + if (xSemaphoreTake(m, 5L / portTICK_PERIOD_MS)) { + q.push(element); + xSemaphoreGive(m); + } + } + + T *pop() { + T *element = nullptr; + + if (xSemaphoreTake(m, 5L / portTICK_PERIOD_MS)) { + if (!q.empty()) { + element = q.front(); + q.pop(); + } + xSemaphoreGive(m); + } + return element; + } + + protected: + std::queue q; + SemaphoreHandle_t m; +}; + +// Received GAP and GATTC events are only queued, and get processed in the main loop(). +// This class stores each event in a single type. +class BLEEvent { + public: + BLEEvent(esp_gap_ble_cb_event_t e, esp_ble_gap_cb_param_t *p) { + this->event_.gap.gap_event = e; + memcpy(&this->event_.gap.gap_param, p, sizeof(esp_ble_gap_cb_param_t)); + this->type_ = 0; + }; + + BLEEvent(esp_gattc_cb_event_t e, esp_gatt_if_t i, esp_ble_gattc_cb_param_t *p) { + this->event_.gattc.gattc_event = e; + this->event_.gattc.gattc_if = i; + memcpy(&this->event_.gattc.gattc_param, p, sizeof(esp_ble_gattc_cb_param_t)); + // Need to also make a copy of notify event data. + if (e == ESP_GATTC_NOTIFY_EVT) { + memcpy(this->event_.gattc.notify_data, p->notify.value, p->notify.value_len); + this->event_.gattc.gattc_param.notify.value = this->event_.gattc.notify_data; + } + this->type_ = 1; + }; + + union { + struct gap_event { + esp_gap_ble_cb_event_t gap_event; + esp_ble_gap_cb_param_t gap_param; + } gap; + + struct gattc_event { + esp_gattc_cb_event_t gattc_event; + esp_gatt_if_t gattc_if; + esp_ble_gattc_cb_param_t gattc_param; + uint8_t notify_data[64]; + } gattc; + } event_; + uint8_t type_; // 0=gap 1=gattc +}; + +} // namespace esp32_ble_tracker +} // namespace esphome + +#endif diff --git a/esphome/config.py b/esphome/config.py index 0c8e51fdce..5a135b3e37 100644 --- a/esphome/config.py +++ b/esphome/config.py @@ -49,7 +49,7 @@ class ComponentManifest: return getattr(self.module, "CONFIG_SCHEMA", None) @property - def is_multi_conf(self): + def multi_conf(self): return getattr(self.module, "MULTI_CONF", False) @property @@ -194,7 +194,7 @@ _COMPONENT_CACHE["esphome"] = ComponentManifest( def iter_components(config): for domain, conf in config.items(): component = get_component(domain) - if component.is_multi_conf: + if component.multi_conf: for conf_ in conf: yield domain, component, conf_ else: @@ -651,9 +651,16 @@ def validate_config(config, command_line_substitutions): ) continue - if comp.is_multi_conf: + if comp.multi_conf: if not isinstance(conf, list): result[domain] = conf = [conf] + if not isinstance(comp.multi_conf, bool) and len(conf) > comp.multi_conf: + result.add_str_error( + "Component {} supports a maximum of {} " + "entries ({} found).".format(domain, comp.multi_conf, len(conf)), + path, + ) + continue for i, part_conf in enumerate(conf): validate_queue.append((path + [i], part_conf, comp)) continue diff --git a/esphome/const.py b/esphome/const.py index eef9252a5c..1d992d12e9 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -358,6 +358,8 @@ CONF_ON_BLE_MANUFACTURER_DATA_ADVERTISE = "on_ble_manufacturer_data_advertise" CONF_ON_BLE_SERVICE_DATA_ADVERTISE = "on_ble_service_data_advertise" CONF_ON_BOOT = "on_boot" CONF_ON_CLICK = "on_click" +CONF_ON_CONNECT = "on_connect" +CONF_ON_DISCONNECT = "on_disconnect" CONF_ON_DOUBLE_CLICK = "on_double_click" CONF_ON_ENROLLMENT_DONE = "on_enrollment_done" CONF_ON_ENROLLMENT_FAILED = "on_enrollment_failed" @@ -620,6 +622,7 @@ ICON_ACCOUNT = "mdi:account" ICON_ACCOUNT_CHECK = "mdi:account-check" ICON_ARROW_EXPAND_VERTICAL = "mdi:arrow-expand-vertical" ICON_BATTERY = "mdi:battery" +ICON_BLUETOOTH = "mdi:bluetooth" ICON_BRIEFCASE_DOWNLOAD = "mdi:briefcase-download" ICON_BRIGHTNESS_5 = "mdi:brightness-5" ICON_BUG = "mdi:bug" diff --git a/tests/test1.yaml b/tests/test1.yaml index 8a93de09b1..167c0b4902 100644 --- a/tests/test1.yaml +++ b/tests/test1.yaml @@ -9,6 +9,8 @@ esphome: name_add_mac_suffix: true platform: ESP32 board: nodemcu-32s + platformio_options: + board_build.partitions: huge_app.csv on_boot: priority: 150.0 then: @@ -235,6 +237,19 @@ wled: adalight: +esp32_ble_tracker: + +ble_client: + - mac_address: AA:BB:CC:DD:EE:FF + id: ble_foo + - mac_address: 11:22:33:44:55:66 + id: ble_blah + on_connect: + then: + - switch.turn_on: ble1_status + on_disconnect: + then: + - switch.turn_on: ble1_status mcp23s08: - id: 'mcp23s08_hub' cs_pin: GPIO12 @@ -246,6 +261,18 @@ mcp23s17: deviceaddress: 1 sensor: + - platform: ble_client + ble_client_id: ble_foo + name: "Green iTag btn" + service_uuid: 'ffe0' + characteristic_uuid: 'ffe1' + descriptor_uuid: 'ffe2' + notify: true + update_interval: never + on_notify: + then: + - lambda: |- + ESP_LOGD("green_btn", "Button was pressed, val%f", x); - platform: adc pin: A0 name: 'Living Room Brightness' @@ -1722,6 +1749,8 @@ switch: # Use pin number 0 number: 0 inverted: False + - platform: template + id: ble1_status fan: - platform: binary