From a70a205ace089c30c0ffcf38b9793d7181595b04 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Tue, 8 Jun 2021 11:56:21 +1200 Subject: [PATCH] Improv - BLE WiFi provisioning (#1807) Co-authored-by: Paulus Schoutsen --- .github/workflows/ci.yml | 1 + .github/workflows/release-dev.yml | 1 + .github/workflows/release.yml | 1 + CODEOWNERS | 3 + .../captive_portal/captive_portal.cpp | 25 +- .../captive_portal/captive_portal.h | 8 - esphome/components/esp32_ble/__init__.py | 44 +++ esphome/components/esp32_ble/ble.cpp | 178 +++++++++++ esphome/components/esp32_ble/ble.h | 62 ++++ esphome/components/esp32_ble/ble_2901.cpp | 18 ++ esphome/components/esp32_ble/ble_2901.h | 19 ++ esphome/components/esp32_ble/ble_2902.cpp | 18 ++ esphome/components/esp32_ble/ble_2902.h | 18 ++ .../components/esp32_ble/ble_advertising.cpp | 97 ++++++ .../components/esp32_ble/ble_advertising.h | 37 +++ .../esp32_ble/ble_characteristic.cpp | 280 ++++++++++++++++++ .../components/esp32_ble/ble_characteristic.h | 85 ++++++ .../components/esp32_ble/ble_descriptor.cpp | 82 +++++ esphome/components/esp32_ble/ble_descriptor.h | 40 +++ esphome/components/esp32_ble/ble_server.cpp | 158 ++++++++++ esphome/components/esp32_ble/ble_server.h | 84 ++++++ esphome/components/esp32_ble/ble_service.cpp | 129 ++++++++ esphome/components/esp32_ble/ble_service.h | 61 ++++ esphome/components/esp32_ble/ble_uuid.cpp | 184 ++++++++++++ esphome/components/esp32_ble/ble_uuid.h | 45 +++ esphome/components/esp32_ble/queue.h | 114 +++++++ esphome/components/esp32_improv/__init__.py | 69 +++++ .../esp32_improv/esp32_improv_component.cpp | 275 +++++++++++++++++ .../esp32_improv/esp32_improv_component.h | 74 +++++ esphome/components/improv/__init__.py | 1 + esphome/components/improv/improv.cpp | 66 +++++ esphome/components/improv/improv.h | 53 ++++ esphome/components/wifi/wifi_component.cpp | 60 +++- esphome/components/wifi/wifi_component.h | 10 + esphome/core/component.cpp | 2 + esphome/core/component.h | 2 + esphome/core/helpers.h | 10 +- script/test | 1 + tests/README.md | 13 +- tests/test5.yaml | 36 +++ 40 files changed, 2422 insertions(+), 42 deletions(-) create mode 100644 esphome/components/esp32_ble/__init__.py create mode 100644 esphome/components/esp32_ble/ble.cpp create mode 100644 esphome/components/esp32_ble/ble.h create mode 100644 esphome/components/esp32_ble/ble_2901.cpp create mode 100644 esphome/components/esp32_ble/ble_2901.h create mode 100644 esphome/components/esp32_ble/ble_2902.cpp create mode 100644 esphome/components/esp32_ble/ble_2902.h create mode 100644 esphome/components/esp32_ble/ble_advertising.cpp create mode 100644 esphome/components/esp32_ble/ble_advertising.h create mode 100644 esphome/components/esp32_ble/ble_characteristic.cpp create mode 100644 esphome/components/esp32_ble/ble_characteristic.h create mode 100644 esphome/components/esp32_ble/ble_descriptor.cpp create mode 100644 esphome/components/esp32_ble/ble_descriptor.h create mode 100644 esphome/components/esp32_ble/ble_server.cpp create mode 100644 esphome/components/esp32_ble/ble_server.h create mode 100644 esphome/components/esp32_ble/ble_service.cpp create mode 100644 esphome/components/esp32_ble/ble_service.h create mode 100644 esphome/components/esp32_ble/ble_uuid.cpp create mode 100644 esphome/components/esp32_ble/ble_uuid.h create mode 100644 esphome/components/esp32_ble/queue.h create mode 100644 esphome/components/esp32_improv/__init__.py create mode 100644 esphome/components/esp32_improv/esp32_improv_component.cpp create mode 100644 esphome/components/esp32_improv/esp32_improv_component.h create mode 100644 esphome/components/improv/__init__.py create mode 100644 esphome/components/improv/improv.cpp create mode 100644 esphome/components/improv/improv.h create mode 100644 tests/test5.yaml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 68e308d2bb..56e94cb32c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -97,6 +97,7 @@ jobs: - test2 - test3 - test4 + - test5 steps: - uses: actions/checkout@v2 - name: Set up Python diff --git a/.github/workflows/release-dev.yml b/.github/workflows/release-dev.yml index 51606ecad0..9361ab4b73 100644 --- a/.github/workflows/release-dev.yml +++ b/.github/workflows/release-dev.yml @@ -94,6 +94,7 @@ jobs: - test2 - test3 - test4 + - test5 steps: - uses: actions/checkout@v2 - name: Set up Python diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 2891959d35..39cc7c37ce 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -93,6 +93,7 @@ jobs: - test2 - test3 - test4 + - test5 steps: - uses: actions/checkout@v2 - name: Set up Python diff --git a/CODEOWNERS b/CODEOWNERS index 576af1459e..3f1abae5df 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -35,6 +35,8 @@ esphome/components/debug/* @OttoWinter esphome/components/dfplayer/* @glmnet esphome/components/dht/* @OttoWinter esphome/components/ds1307/* @badbadc0ffee +esphome/components/esp32_ble/* @jesserockz +esphome/components/esp32_improv/* @jesserockz esphome/components/exposure_notifications/* @OttoWinter esphome/components/ezo/* @ssieb esphome/components/fastled_base/* @OttoWinter @@ -44,6 +46,7 @@ esphome/components/gpio/* @esphome/core esphome/components/gps/* @coogle esphome/components/homeassistant/* @OttoWinter esphome/components/i2c/* @esphome/core +esphome/components/improv/* @jesserockz esphome/components/inkbird_ibsth1_mini/* @fkirill esphome/components/inkplate6/* @jesserockz esphome/components/integration/* @OttoWinter diff --git a/esphome/components/captive_portal/captive_portal.cpp b/esphome/components/captive_portal/captive_portal.cpp index 83f85d354c..0e0e08fdea 100644 --- a/esphome/components/captive_portal/captive_portal.cpp +++ b/esphome/components/captive_portal/captive_portal.cpp @@ -64,32 +64,11 @@ void CaptivePortal::handle_wifisave(AsyncWebServerRequest *request) { ESP_LOGI(TAG, "Captive Portal Requested WiFi Settings Change:"); ESP_LOGI(TAG, " SSID='%s'", ssid.c_str()); ESP_LOGI(TAG, " Password=" LOG_SECRET("'%s'"), psk.c_str()); - this->override_sta_(ssid, psk); + wifi::global_wifi_component->save_wifi_sta(ssid, psk); request->redirect("/?save=true"); } -void CaptivePortal::override_sta_(const std::string &ssid, const std::string &password) { - CaptivePortalSettings save{}; - strcpy(save.ssid, ssid.c_str()); - strcpy(save.password, password.c_str()); - this->pref_.save(&save); - wifi::WiFiAP sta{}; - sta.set_ssid(ssid); - sta.set_password(password); - wifi::global_wifi_component->set_sta(sta); -} - -void CaptivePortal::setup() { - // Hash with compilation time - // This ensures the AP override is not applied for OTA - uint32_t hash = fnv1_hash(App.get_compilation_time()); - this->pref_ = global_preferences.make_preference(hash, true); - - CaptivePortalSettings save{}; - if (this->pref_.load(&save)) { - this->override_sta_(save.ssid, save.password); - } -} +void CaptivePortal::setup() {} void CaptivePortal::start() { this->base_->init(); if (!this->initialized_) { diff --git a/esphome/components/captive_portal/captive_portal.h b/esphome/components/captive_portal/captive_portal.h index 3af47546cf..afd4ff9dc5 100644 --- a/esphome/components/captive_portal/captive_portal.h +++ b/esphome/components/captive_portal/captive_portal.h @@ -10,11 +10,6 @@ namespace esphome { namespace captive_portal { -struct CaptivePortalSettings { - char ssid[33]; - char password[65]; -} PACKED; // NOLINT - class CaptivePortal : public AsyncWebHandler, public Component { public: CaptivePortal(web_server_base::WebServerBase *base); @@ -67,12 +62,9 @@ class CaptivePortal : public AsyncWebHandler, public Component { void handleRequest(AsyncWebServerRequest *req) override; protected: - void override_sta_(const std::string &ssid, const std::string &password); - web_server_base::WebServerBase *base_; bool initialized_{false}; bool active_{false}; - ESPPreferenceObject pref_; DNSServer *dns_server_{nullptr}; }; diff --git a/esphome/components/esp32_ble/__init__.py b/esphome/components/esp32_ble/__init__.py new file mode 100644 index 0000000000..6437216f69 --- /dev/null +++ b/esphome/components/esp32_ble/__init__.py @@ -0,0 +1,44 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.const import CONF_ID, CONF_MODEL, ESP_PLATFORM_ESP32 + +ESP_PLATFORMS = [ESP_PLATFORM_ESP32] +CODEOWNERS = ["@jesserockz"] + +CONF_MANUFACTURER = "manufacturer" +CONF_SERVER = "server" + +esp32_ble_ns = cg.esphome_ns.namespace("esp32_ble") +ESP32BLE = esp32_ble_ns.class_("ESP32BLE", cg.Component) +BLEServer = esp32_ble_ns.class_("BLEServer", cg.Component) + +BLEServiceComponent = esp32_ble_ns.class_("BLEServiceComponent") + + +CONFIG_SCHEMA = cv.Schema( + { + cv.GenerateID(): cv.declare_id(ESP32BLE), + cv.Optional(CONF_SERVER): cv.Schema( + { + cv.GenerateID(): cv.declare_id(BLEServer), + cv.Optional(CONF_MANUFACTURER, default="ESPHome"): cv.string, + cv.Optional(CONF_MODEL): cv.string, + } + ), + } +).extend(cv.COMPONENT_SCHEMA) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + + if CONF_SERVER in config: + conf = config[CONF_SERVER] + server = cg.new_Pvariable(conf[CONF_ID]) + await cg.register_component(server, conf) + cg.add(server.set_manufacturer(conf[CONF_MANUFACTURER])) + if CONF_MODEL in conf: + cg.add(server.set_model(conf[CONF_MODEL])) + cg.add_define("USE_ESP32_BLE_SERVER") + cg.add(var.set_server(server)) diff --git a/esphome/components/esp32_ble/ble.cpp b/esphome/components/esp32_ble/ble.cpp new file mode 100644 index 0000000000..9ce9eaec7e --- /dev/null +++ b/esphome/components/esp32_ble/ble.cpp @@ -0,0 +1,178 @@ +#include "ble.h" +#include "esphome/core/application.h" +#include "esphome/core/log.h" + +#ifdef ARDUINO_ARCH_ESP32 + +#include +#include +#include +#include +#include +#include + +namespace esphome { +namespace esp32_ble { + +static const char *TAG = "esp32_ble"; + +void ESP32BLE::setup() { + global_ble = this; + ESP_LOGCONFIG(TAG, "Setting up BLE..."); + + xTaskCreatePinnedToCore(ESP32BLE::ble_core_task_, + "ble_task", // name + 10000, // stack size + nullptr, // input params + 1, // priority + nullptr, // handle, not needed + 0 // core + ); +} + +void ESP32BLE::mark_failed() { + Component::mark_failed(); + if (this->server_ != nullptr) { + this->server_->mark_failed(); + } +} + +bool ESP32BLE::can_proceed() { return this->ready_; } + +void ESP32BLE::ble_core_task_(void *params) { + if (!ble_setup_()) { + ESP_LOGE(TAG, "BLE could not be set up"); + global_ble->mark_failed(); + return; + } + + global_ble->ready_ = true; + ESP_LOGD(TAG, "BLE Setup complete"); + + while (true) { + vTaskDelay(1000 / portTICK_PERIOD_MS); + } +} + +bool ESP32BLE::ble_setup_() { + esp_err_t err = nvs_flash_init(); + if (err != ESP_OK) { + ESP_LOGE(TAG, "nvs_flash_init failed: %d", err); + return false; + } + + if (!btStart()) { + ESP_LOGE(TAG, "btStart failed: %d", esp_bt_controller_get_status()); + return false; + } + + esp_bt_controller_mem_release(ESP_BT_MODE_CLASSIC_BT); + + err = esp_bluedroid_init(); + if (err != ESP_OK) { + ESP_LOGE(TAG, "esp_bluedroid_init failed: %d", err); + return false; + } + err = esp_bluedroid_enable(); + if (err != ESP_OK) { + ESP_LOGE(TAG, "esp_bluedroid_enable failed: %d", err); + return false; + } + err = esp_ble_gap_register_callback(ESP32BLE::gap_event_handler); + if (err != ESP_OK) { + ESP_LOGE(TAG, "esp_ble_gap_register_callback failed: %d", err); + return false; + } + + if (global_ble->has_server()) { + err = esp_ble_gatts_register_callback(ESP32BLE::gatts_event_handler); + if (err != ESP_OK) { + ESP_LOGE(TAG, "esp_ble_gatts_register_callback failed: %d", err); + return false; + } + } + + if (global_ble->has_client()) { + err = esp_ble_gattc_register_callback(ESP32BLE::gattc_event_handler); + if (err != ESP_OK) { + ESP_LOGE(TAG, "esp_ble_gattc_register_callback failed: %d", err); + return false; + } + } + + err = esp_ble_gap_set_device_name(App.get_name().c_str()); + if (err != ESP_OK) { + ESP_LOGE(TAG, "esp_ble_gap_set_device_name failed: %d", err); + return false; + } + + esp_ble_io_cap_t iocap = ESP_IO_CAP_NONE; + err = esp_ble_gap_set_security_param(ESP_BLE_SM_IOCAP_MODE, &iocap, sizeof(uint8_t)); + if (err != ESP_OK) { + ESP_LOGE(TAG, "esp_ble_gap_set_security_param failed: %d", err); + return false; + } + + // BLE takes some time to be fully set up, 200ms should be more than enough + delay(200); // NOLINT + + return true; +} + +void ESP32BLE::loop() { + BLEEvent *ble_event = this->ble_events_.pop(); + while (ble_event != nullptr) { + switch (ble_event->type_) { + case ble_event->GATTS: + this->real_gatts_event_handler_(ble_event->event_.gatts.gatts_event, ble_event->event_.gatts.gatts_if, + &ble_event->event_.gatts.gatts_param); + break; + case ble_event->GAP: + this->real_gap_event_handler_(ble_event->event_.gap.gap_event, &ble_event->event_.gap.gap_param); + break; + default: + break; + } + delete ble_event; + ble_event = this->ble_events_.pop(); + } +} + +void ESP32BLE::gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) { + global_ble->real_gap_event_handler_(event, param); +} + +void ESP32BLE::real_gap_event_handler_(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) { + ESP_LOGV(TAG, "(BLE) gap_event_handler - %d", event); + switch (event) { + default: + break; + } +} + +void ESP32BLE::gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_t gatts_if, + esp_ble_gatts_cb_param_t *param) { + global_ble->real_gatts_event_handler_(event, gatts_if, param); +} + +void ESP32BLE::real_gatts_event_handler_(esp_gatts_cb_event_t event, esp_gatt_if_t gatts_if, + esp_ble_gatts_cb_param_t *param) { + ESP_LOGV(TAG, "(BLE) gatts_event [esp_gatt_if: %d] - %d", gatts_if, event); + this->server_->gatts_event_handler(event, gatts_if, param); +} + +void ESP32BLE::real_gattc_event_handler_(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, + esp_ble_gattc_cb_param_t *param) { + // this->client_->gattc_event_handler(event, gattc_if, param); +} + +float ESP32BLE::get_setup_priority() const { return setup_priority::BLUETOOTH; } + +void ESP32BLE::dump_config() { ESP_LOGCONFIG(TAG, "ESP32 BLE:"); } + +ESP32BLE *global_ble = nullptr; + +} // namespace esp32_ble +} // namespace esphome + +#endif diff --git a/esphome/components/esp32_ble/ble.h b/esphome/components/esp32_ble/ble.h new file mode 100644 index 0000000000..8eaa7068fd --- /dev/null +++ b/esphome/components/esp32_ble/ble.h @@ -0,0 +1,62 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/core/helpers.h" +#include "ble_server.h" +#include "queue.h" + +#ifdef ARDUINO_ARCH_ESP32 + +#include +#include +#include + +namespace esphome { +namespace esp32_ble { + +typedef struct { + void *peer_device; + bool connected; + uint16_t mtu; +} conn_status_t; + +class ESP32BLE : public Component { + public: + void setup() override; + void loop() override; + void dump_config() override; + float get_setup_priority() const override; + void mark_failed() override; + bool can_proceed() override; + + bool has_server() { return this->server_ != nullptr; } + bool has_client() { return false; } + + bool is_ready() { return this->ready_; } + + void set_server(BLEServer *server) { this->server_ = server; } + + protected: + static void gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_t gatts_if, esp_ble_gatts_cb_param_t *param); + static void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, esp_ble_gattc_cb_param_t *param); + static void gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param); + + void real_gatts_event_handler_(esp_gatts_cb_event_t event, esp_gatt_if_t gatts_if, esp_ble_gatts_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); + void real_gap_event_handler_(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param); + + static void ble_core_task_(void *params); + static bool ble_setup_(); + + bool ready_{false}; + + BLEServer *server_{nullptr}; + Queue ble_events_; +}; + +extern ESP32BLE *global_ble; + +} // namespace esp32_ble +} // namespace esphome + +#endif diff --git a/esphome/components/esp32_ble/ble_2901.cpp b/esphome/components/esp32_ble/ble_2901.cpp new file mode 100644 index 0000000000..d62c67c4cf --- /dev/null +++ b/esphome/components/esp32_ble/ble_2901.cpp @@ -0,0 +1,18 @@ +#include "ble_2901.h" +#include "ble_uuid.h" + +#ifdef ARDUINO_ARCH_ESP32 + +namespace esphome { +namespace esp32_ble { + +BLE2901::BLE2901(const std::string value) : BLE2901((uint8_t *) value.data(), value.length()) {} +BLE2901::BLE2901(uint8_t *data, size_t length) : BLEDescriptor(ESPBTUUID::from_uint16(0x2901)) { + this->set_value(data, length); + this->permissions_ = ESP_GATT_PERM_READ; +} + +} // namespace esp32_ble +} // namespace esphome + +#endif diff --git a/esphome/components/esp32_ble/ble_2901.h b/esphome/components/esp32_ble/ble_2901.h new file mode 100644 index 0000000000..6f72f38fd0 --- /dev/null +++ b/esphome/components/esp32_ble/ble_2901.h @@ -0,0 +1,19 @@ +#pragma once + +#include "ble_descriptor.h" + +#ifdef ARDUINO_ARCH_ESP32 + +namespace esphome { +namespace esp32_ble { + +class BLE2901 : public BLEDescriptor { + public: + BLE2901(const std::string value); + BLE2901(uint8_t *data, size_t length); +}; + +} // namespace esp32_ble +} // namespace esphome + +#endif diff --git a/esphome/components/esp32_ble/ble_2902.cpp b/esphome/components/esp32_ble/ble_2902.cpp new file mode 100644 index 0000000000..164c8f40ff --- /dev/null +++ b/esphome/components/esp32_ble/ble_2902.cpp @@ -0,0 +1,18 @@ +#include "ble_2902.h" +#include "ble_uuid.h" + +#ifdef ARDUINO_ARCH_ESP32 + +namespace esphome { +namespace esp32_ble { + +BLE2902::BLE2902() : BLEDescriptor(ESPBTUUID::from_uint16(0x2902)) { + this->value_.attr_len = 2; + uint8_t data[2] = {0, 0}; + memcpy(this->value_.attr_value, data, 2); +} + +} // namespace esp32_ble +} // namespace esphome + +#endif diff --git a/esphome/components/esp32_ble/ble_2902.h b/esphome/components/esp32_ble/ble_2902.h new file mode 100644 index 0000000000..356a0bb107 --- /dev/null +++ b/esphome/components/esp32_ble/ble_2902.h @@ -0,0 +1,18 @@ +#pragma once + +#include "ble_descriptor.h" + +#ifdef ARDUINO_ARCH_ESP32 + +namespace esphome { +namespace esp32_ble { + +class BLE2902 : public BLEDescriptor { + public: + BLE2902(); +}; + +} // namespace esp32_ble +} // namespace esphome + +#endif diff --git a/esphome/components/esp32_ble/ble_advertising.cpp b/esphome/components/esp32_ble/ble_advertising.cpp new file mode 100644 index 0000000000..6162bf3a40 --- /dev/null +++ b/esphome/components/esp32_ble/ble_advertising.cpp @@ -0,0 +1,97 @@ +#include "ble_advertising.h" + +#ifdef ARDUINO_ARCH_ESP32 + +#include "ble_uuid.h" + +namespace esphome { +namespace esp32_ble { + +BLEAdvertising::BLEAdvertising() { + this->advertising_data_.set_scan_rsp = false; + this->advertising_data_.include_name = true; + this->advertising_data_.include_txpower = true; + this->advertising_data_.min_interval = 0x20; + this->advertising_data_.max_interval = 0x40; + this->advertising_data_.appearance = 0x00; + this->advertising_data_.manufacturer_len = 0; + this->advertising_data_.p_manufacturer_data = nullptr; + this->advertising_data_.service_data_len = 0; + this->advertising_data_.p_service_data = nullptr; + this->advertising_data_.service_uuid_len = 0; + this->advertising_data_.p_service_uuid = nullptr; + this->advertising_data_.flag = (ESP_BLE_ADV_FLAG_GEN_DISC | ESP_BLE_ADV_FLAG_BREDR_NOT_SPT); + + this->advertising_params_.adv_int_min = 0x20; + this->advertising_params_.adv_int_max = 0x40; + this->advertising_params_.adv_type = ADV_TYPE_IND; + this->advertising_params_.own_addr_type = BLE_ADDR_TYPE_PUBLIC; + this->advertising_params_.channel_map = ADV_CHNL_ALL; + this->advertising_params_.adv_filter_policy = ADV_FILTER_ALLOW_SCAN_ANY_CON_ANY; + this->advertising_params_.peer_addr_type = BLE_ADDR_TYPE_PUBLIC; +} + +void BLEAdvertising::add_service_uuid(ESPBTUUID uuid) { this->advertising_uuids_.push_back(uuid); } + +void BLEAdvertising::start() { + int num_services = this->advertising_uuids_.size(); + if (num_services == 0) { + this->advertising_data_.service_uuid_len = 0; + } else { + this->advertising_data_.service_uuid_len = 16 * num_services; + this->advertising_data_.p_service_uuid = new uint8_t[this->advertising_data_.service_uuid_len]; + uint8_t *p = this->advertising_data_.p_service_uuid; + for (int i = 0; i < num_services; i++) { + ESPBTUUID uuid = this->advertising_uuids_[i]; + memcpy(p, uuid.as_128bit().get_uuid().uuid.uuid128, 16); + p += 16; + } + } + + esp_err_t err; + + this->advertising_data_.set_scan_rsp = false; + this->advertising_data_.include_name = !this->scan_response_; + this->advertising_data_.include_txpower = !this->scan_response_; + err = esp_ble_gap_config_adv_data(&this->advertising_data_); + if (err != ESP_OK) { + ESP_LOGE(TAG, "esp_ble_gap_config_adv_data failed (Advertising): %d", err); + return; + } + + memcpy(&this->scan_response_data_, &this->advertising_data_, sizeof(esp_ble_adv_data_t)); + this->scan_response_data_.set_scan_rsp = true; + this->scan_response_data_.include_name = true; + this->scan_response_data_.include_txpower = true; + this->scan_response_data_.appearance = 0; + this->scan_response_data_.flag = 0; + err = esp_ble_gap_config_adv_data(&this->scan_response_data_); + if (err != ESP_OK) { + ESP_LOGE(TAG, "esp_ble_gap_config_adv_data failed (Scan response): %d", err); + return; + } + + if (this->advertising_data_.service_uuid_len > 0) { + delete[] this->advertising_data_.p_service_uuid; + this->advertising_data_.p_service_uuid = nullptr; + } + + err = esp_ble_gap_start_advertising(&this->advertising_params_); + if (err != ESP_OK) { + ESP_LOGE(TAG, "esp_ble_gap_start_advertising failed: %d", err); + return; + } +} + +void BLEAdvertising::stop() { + esp_err_t err = esp_ble_gap_stop_advertising(); + if (err != ESP_OK) { + ESP_LOGE(TAG, "esp_ble_gap_stop_advertising failed: %d", err); + return; + } +} + +} // namespace esp32_ble +} // namespace esphome + +#endif diff --git a/esphome/components/esp32_ble/ble_advertising.h b/esphome/components/esp32_ble/ble_advertising.h new file mode 100644 index 0000000000..405edbf482 --- /dev/null +++ b/esphome/components/esp32_ble/ble_advertising.h @@ -0,0 +1,37 @@ +#pragma once + +#include + +#ifdef ARDUINO_ARCH_ESP32 + +#include +#include + +namespace esphome { +namespace esp32_ble { + +class ESPBTUUID; + +class BLEAdvertising { + public: + BLEAdvertising(); + + void add_service_uuid(ESPBTUUID uuid); + void set_scan_response(bool scan_response) { this->scan_response_ = scan_response; } + void set_min_preferred_interval(uint16_t interval) { this->advertising_data_.min_interval = interval; } + + void start(); + void stop(); + + protected: + bool scan_response_; + esp_ble_adv_data_t advertising_data_; + esp_ble_adv_data_t scan_response_data_; + esp_ble_adv_params_t advertising_params_; + std::vector advertising_uuids_; +}; + +} // namespace esp32_ble +} // namespace esphome + +#endif diff --git a/esphome/components/esp32_ble/ble_characteristic.cpp b/esphome/components/esp32_ble/ble_characteristic.cpp new file mode 100644 index 0000000000..cee15ce6a8 --- /dev/null +++ b/esphome/components/esp32_ble/ble_characteristic.cpp @@ -0,0 +1,280 @@ +#include "ble_characteristic.h" +#include "ble_server.h" +#include "ble_service.h" + +#include "esphome/core/log.h" + +#ifdef ARDUINO_ARCH_ESP32 + +namespace esphome { +namespace esp32_ble { + +static const char *TAG = "esp32_ble.characteristic"; + +BLECharacteristic::BLECharacteristic(const ESPBTUUID uuid, uint32_t properties) : uuid_(uuid) { + this->set_value_lock_ = xSemaphoreCreateBinary(); + this->create_lock_ = xSemaphoreCreateBinary(); + + xSemaphoreGive(this->set_value_lock_); + xSemaphoreGive(this->create_lock_); + this->properties_ = (esp_gatt_char_prop_t) 0; + + this->set_broadcast_property((properties & PROPERTY_BROADCAST) != 0); + this->set_indicate_property((properties & PROPERTY_INDICATE) != 0); + this->set_notify_property((properties & PROPERTY_NOTIFY) != 0); + this->set_read_property((properties & PROPERTY_READ) != 0); + this->set_write_property((properties & PROPERTY_WRITE) != 0); + this->set_write_no_response_property((properties & PROPERTY_WRITE_NR) != 0); +} + +void BLECharacteristic::set_value(std::vector value) { + xSemaphoreTake(this->set_value_lock_, 0L); + this->value_ = value; + xSemaphoreGive(this->set_value_lock_); +} +void BLECharacteristic::set_value(std::string value) { + this->set_value(std::vector(value.begin(), value.end())); +} +void BLECharacteristic::set_value(uint8_t *data, size_t length) { + this->set_value(std::vector(data, data + length)); +} +void BLECharacteristic::set_value(uint8_t &data) { + uint8_t temp[1]; + temp[0] = data; + this->set_value(temp, 1); +} +void BLECharacteristic::set_value(uint16_t &data) { + uint8_t temp[2]; + temp[0] = data; + temp[1] = data >> 8; + this->set_value(temp, 2); +} +void BLECharacteristic::set_value(uint32_t &data) { + uint8_t temp[4]; + temp[0] = data; + temp[1] = data >> 8; + temp[2] = data >> 16; + temp[3] = data >> 24; + this->set_value(temp, 4); +} +void BLECharacteristic::set_value(int &data) { + uint8_t temp[4]; + temp[0] = data; + temp[1] = data >> 8; + temp[2] = data >> 16; + temp[3] = data >> 24; + this->set_value(temp, 4); +} +void BLECharacteristic::set_value(float &data) { + float temp = data; + this->set_value((uint8_t *) &temp, 4); +} +void BLECharacteristic::set_value(double &data) { + double temp = data; + this->set_value((uint8_t *) &temp, 8); +} +void BLECharacteristic::set_value(bool &data) { + uint8_t temp[1]; + temp[0] = data; + this->set_value(temp, 1); +} + +void BLECharacteristic::notify(bool notification) { + if (!notification) { + ESP_LOGW(TAG, "notification=false is not yet supported"); + // TODO: Handle when notification=false + } + if (this->service_->get_server()->get_connected_client_count() == 0) + return; + + for (auto &client : this->service_->get_server()->get_clients()) { + size_t length = this->value_.size(); + esp_err_t err = esp_ble_gatts_send_indicate(this->service_->get_server()->get_gatts_if(), client.first, + this->handle_, length, this->value_.data(), false); + if (err != ESP_OK) { + ESP_LOGE(TAG, "esp_ble_gatts_send_indicate failed %d", err); + return; + } + } +} + +void BLECharacteristic::add_descriptor(BLEDescriptor *descriptor) { this->descriptors_.push_back(descriptor); } + +bool BLECharacteristic::do_create(BLEService *service) { + this->service_ = service; + esp_attr_control_t control; + control.auto_rsp = ESP_GATT_RSP_BY_APP; + + xSemaphoreTake(this->create_lock_, SEMAPHORE_MAX_DELAY); + ESP_LOGV(TAG, "Creating characteristic - %s", this->uuid_.to_string().c_str()); + + esp_bt_uuid_t uuid = this->uuid_.get_uuid(); + esp_err_t err = esp_ble_gatts_add_char(service->get_handle(), &uuid, static_cast(this->permissions_), + this->properties_, nullptr, &control); + + if (err != ESP_OK) { + ESP_LOGE(TAG, "esp_ble_gatts_add_char failed: %d", err); + return false; + } + + xSemaphoreWait(this->create_lock_, SEMAPHORE_MAX_DELAY); + + for (auto *descriptor : this->descriptors_) { + descriptor->do_create(this); + } + return true; +} + +void BLECharacteristic::set_broadcast_property(bool value) { + if (value) + this->properties_ = (esp_gatt_char_prop_t)(this->properties_ | ESP_GATT_CHAR_PROP_BIT_BROADCAST); + else + this->properties_ = (esp_gatt_char_prop_t)(this->properties_ & ~ESP_GATT_CHAR_PROP_BIT_BROADCAST); +} +void BLECharacteristic::set_indicate_property(bool value) { + if (value) + this->properties_ = (esp_gatt_char_prop_t)(this->properties_ | ESP_GATT_CHAR_PROP_BIT_INDICATE); + else + this->properties_ = (esp_gatt_char_prop_t)(this->properties_ & ~ESP_GATT_CHAR_PROP_BIT_INDICATE); +} +void BLECharacteristic::set_notify_property(bool value) { + if (value) + this->properties_ = (esp_gatt_char_prop_t)(this->properties_ | ESP_GATT_CHAR_PROP_BIT_NOTIFY); + else + this->properties_ = (esp_gatt_char_prop_t)(this->properties_ & ~ESP_GATT_CHAR_PROP_BIT_NOTIFY); +} +void BLECharacteristic::set_read_property(bool value) { + if (value) + this->properties_ = (esp_gatt_char_prop_t)(this->properties_ | ESP_GATT_CHAR_PROP_BIT_READ); + else + this->properties_ = (esp_gatt_char_prop_t)(this->properties_ & ~ESP_GATT_CHAR_PROP_BIT_READ); +} +void BLECharacteristic::set_write_property(bool value) { + if (value) + this->properties_ = (esp_gatt_char_prop_t)(this->properties_ | ESP_GATT_CHAR_PROP_BIT_WRITE); + else + this->properties_ = (esp_gatt_char_prop_t)(this->properties_ & ~ESP_GATT_CHAR_PROP_BIT_WRITE); +} +void BLECharacteristic::set_write_no_response_property(bool value) { + if (value) + this->properties_ = (esp_gatt_char_prop_t)(this->properties_ | ESP_GATT_CHAR_PROP_BIT_WRITE_NR); + else + this->properties_ = (esp_gatt_char_prop_t)(this->properties_ & ~ESP_GATT_CHAR_PROP_BIT_WRITE_NR); +} + +void BLECharacteristic::gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_t gatts_if, + esp_ble_gatts_cb_param_t *param) { + switch (event) { + case ESP_GATTS_ADD_CHAR_EVT: { + if (this->uuid_ == ESPBTUUID::from_uuid(param->add_char.char_uuid)) { + this->handle_ = param->add_char.attr_handle; + xSemaphoreGive(this->create_lock_); + } + break; + } + case ESP_GATTS_READ_EVT: { + if (param->read.handle != this->handle_) + break; // Not this characteristic + + if (!param->read.need_rsp) + break; // For some reason you can request a read but not want a response + + uint16_t max_offset = 22; + + esp_gatt_rsp_t response; + if (param->read.is_long) { + if (this->value_.size() - this->value_read_offset_ < max_offset) { + // Last message in the chain + response.attr_value.len = this->value_.size() - this->value_read_offset_; + response.attr_value.offset = this->value_read_offset_; + memcpy(response.attr_value.value, this->value_.data() + response.attr_value.offset, response.attr_value.len); + this->value_read_offset_ = 0; + } else { + response.attr_value.len = max_offset; + response.attr_value.offset = this->value_read_offset_; + memcpy(response.attr_value.value, this->value_.data() + response.attr_value.offset, response.attr_value.len); + this->value_read_offset_ += max_offset; + } + } else { + response.attr_value.offset = 0; + if (this->value_.size() + 1 > max_offset) { + response.attr_value.len = max_offset; + this->value_read_offset_ = max_offset; + } else { + response.attr_value.len = this->value_.size(); + } + memcpy(response.attr_value.value, this->value_.data(), response.attr_value.len); + } + + response.attr_value.handle = this->handle_; + response.attr_value.auth_req = ESP_GATT_AUTH_REQ_NONE; + + esp_err_t err = + esp_ble_gatts_send_response(gatts_if, param->read.conn_id, param->read.trans_id, ESP_GATT_OK, &response); + if (err != ESP_OK) { + ESP_LOGE(TAG, "esp_ble_gatts_send_response failed: %d", err); + } + break; + } + case ESP_GATTS_WRITE_EVT: { + if (this->handle_ != param->write.handle) + return; + + if (param->write.is_prep) { + this->value_.insert(this->value_.end(), param->write.value, param->write.value + param->write.len); + this->write_event_ = true; + } else { + this->set_value(param->write.value, param->write.len); + } + + if (param->write.need_rsp) { + esp_gatt_rsp_t response; + + response.attr_value.len = param->write.len; + response.attr_value.handle = this->handle_; + response.attr_value.offset = param->write.offset; + response.attr_value.auth_req = ESP_GATT_AUTH_REQ_NONE; + memcpy(response.attr_value.value, param->write.value, param->write.len); + + esp_err_t err = + esp_ble_gatts_send_response(gatts_if, param->write.conn_id, param->write.trans_id, ESP_GATT_OK, &response); + + if (err != ESP_OK) { + ESP_LOGE(TAG, "esp_ble_gatts_send_response failed: %d", err); + } + } + + if (!param->write.is_prep) { + this->on_write_(this->value_); + } + + break; + } + + case ESP_GATTS_EXEC_WRITE_EVT: { + if (!this->write_event_) + break; + this->write_event_ = false; + if (param->exec_write.exec_write_flag == ESP_GATT_PREP_WRITE_EXEC) { + this->on_write_(this->value_); + } + esp_err_t err = + esp_ble_gatts_send_response(gatts_if, param->write.conn_id, param->write.trans_id, ESP_GATT_OK, nullptr); + if (err != ESP_OK) { + ESP_LOGE(TAG, "esp_ble_gatts_send_response failed: %d", err); + } + break; + } + default: + break; + } + + for (auto *descriptor : this->descriptors_) { + descriptor->gatts_event_handler(event, gatts_if, param); + } +} + +} // namespace esp32_ble +} // namespace esphome + +#endif diff --git a/esphome/components/esp32_ble/ble_characteristic.h b/esphome/components/esp32_ble/ble_characteristic.h new file mode 100644 index 0000000000..b181b98e8c --- /dev/null +++ b/esphome/components/esp32_ble/ble_characteristic.h @@ -0,0 +1,85 @@ +#pragma once + +#include + +#include "ble_uuid.h" +#include "ble_descriptor.h" + +#ifdef ARDUINO_ARCH_ESP32 + +#include +#include +#include +#include +#include + +namespace esphome { +namespace esp32_ble { + +class BLEService; + +class BLECharacteristic { + public: + BLECharacteristic(const ESPBTUUID uuid, uint32_t properties); + + void set_value(uint8_t *data, size_t length); + void set_value(std::vector value); + void set_value(std::string value); + void set_value(uint8_t &data); + void set_value(uint16_t &data); + void set_value(uint32_t &data); + void set_value(int &data); + void set_value(float &data); + void set_value(double &data); + void set_value(bool &data); + + void set_broadcast_property(bool value); + void set_indicate_property(bool value); + void set_notify_property(bool value); + void set_read_property(bool value); + void set_write_property(bool value); + void set_write_no_response_property(bool value); + + void notify(bool notification = true); + + bool do_create(BLEService *service); + void gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_t gatts_if, esp_ble_gatts_cb_param_t *param); + + void on_write(const std::function &)> &func) { this->on_write_ = func; } + + void add_descriptor(BLEDescriptor *descriptor); + + BLEService *get_service() { return this->service_; } + ESPBTUUID get_uuid() { return this->uuid_; } + std::vector &get_value() { return this->value_; } + + static const uint32_t PROPERTY_READ = 1 << 0; + static const uint32_t PROPERTY_WRITE = 1 << 1; + static const uint32_t PROPERTY_NOTIFY = 1 << 2; + static const uint32_t PROPERTY_BROADCAST = 1 << 3; + static const uint32_t PROPERTY_INDICATE = 1 << 4; + static const uint32_t PROPERTY_WRITE_NR = 1 << 5; + + protected: + bool write_event_{false}; + BLEService *service_; + ESPBTUUID uuid_; + esp_gatt_char_prop_t properties_; + uint16_t handle_{0xFFFF}; + + uint16_t value_read_offset_{0}; + std::vector value_; + SemaphoreHandle_t set_value_lock_; + SemaphoreHandle_t create_lock_; + + std::vector descriptors_; + + std::function &)> on_write_; + + esp_gatt_perm_t permissions_ = ESP_GATT_PERM_READ | ESP_GATT_PERM_WRITE; +}; + +} // namespace esp32_ble +} // namespace esphome + +#endif diff --git a/esphome/components/esp32_ble/ble_descriptor.cpp b/esphome/components/esp32_ble/ble_descriptor.cpp new file mode 100644 index 0000000000..a6cf4c707e --- /dev/null +++ b/esphome/components/esp32_ble/ble_descriptor.cpp @@ -0,0 +1,82 @@ +#include "ble_descriptor.h" +#include "ble_characteristic.h" +#include "ble_service.h" + +#include "esphome/core/log.h" + +#ifdef ARDUINO_ARCH_ESP32 + +namespace esphome { +namespace esp32_ble { + +static const char *TAG = "esp32_ble.descriptor"; + +BLEDescriptor::BLEDescriptor(ESPBTUUID uuid, uint16_t max_len) { + this->uuid_ = uuid; + this->value_.attr_len = 0; + this->value_.attr_max_len = max_len; + this->value_.attr_value = (uint8_t *) malloc(max_len); + + this->create_lock_ = xSemaphoreCreateBinary(); + xSemaphoreGive(this->create_lock_); +} + +BLEDescriptor::~BLEDescriptor() { free(this->value_.attr_value); } + +bool BLEDescriptor::do_create(BLECharacteristic *characteristic) { + this->characteristic_ = characteristic; + esp_attr_control_t control; + control.auto_rsp = ESP_GATT_AUTO_RSP; + + xSemaphoreTake(this->create_lock_, SEMAPHORE_MAX_DELAY); + ESP_LOGV(TAG, "Creating descriptor - %s", this->uuid_.to_string().c_str()); + esp_bt_uuid_t uuid = this->uuid_.get_uuid(); + esp_err_t err = esp_ble_gatts_add_char_descr(this->characteristic_->get_service()->get_handle(), &uuid, + this->permissions_, &this->value_, &control); + if (err != ESP_OK) { + ESP_LOGE(TAG, "esp_ble_gatts_add_char_descr failed: %d", err); + return false; + } + xSemaphoreWait(this->create_lock_, SEMAPHORE_MAX_DELAY); + + return true; +} + +void BLEDescriptor::set_value(const std::string value) { this->set_value((uint8_t *) value.data(), value.length()); } +void BLEDescriptor::set_value(uint8_t *data, size_t length) { + if (length > this->value_.attr_max_len) { + ESP_LOGE(TAG, "Size %d too large, must be no bigger than %d", length, this->value_.attr_max_len); + return; + } + this->value_.attr_len = length; + memcpy(this->value_.attr_value, data, length); +} + +void BLEDescriptor::gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_t gatts_if, + esp_ble_gatts_cb_param_t *param) { + switch (event) { + case ESP_GATTS_ADD_CHAR_DESCR_EVT: { + if (this->characteristic_ != nullptr && this->uuid_ == ESPBTUUID::from_uuid(param->add_char_descr.descr_uuid) && + this->characteristic_->get_service()->get_handle() == param->add_char_descr.service_handle && + this->characteristic_ == this->characteristic_->get_service()->get_last_created_characteristic()) { + this->handle_ = param->add_char_descr.attr_handle; + xSemaphoreGive(this->create_lock_); + } + break; + } + case ESP_GATTS_WRITE_EVT: { + if (this->handle_ == param->write.handle) { + this->value_.attr_len = param->write.len; + memcpy(this->value_.attr_value, param->write.value, param->write.len); + } + break; + } + default: + break; + } +} + +} // namespace esp32_ble +} // namespace esphome + +#endif diff --git a/esphome/components/esp32_ble/ble_descriptor.h b/esphome/components/esp32_ble/ble_descriptor.h new file mode 100644 index 0000000000..a0f92dc929 --- /dev/null +++ b/esphome/components/esp32_ble/ble_descriptor.h @@ -0,0 +1,40 @@ +#pragma once + +#include "ble_uuid.h" + +#ifdef ARDUINO_ARCH_ESP32 + +#include +#include + +namespace esphome { +namespace esp32_ble { + +class BLECharacteristic; + +class BLEDescriptor { + public: + BLEDescriptor(ESPBTUUID uuid, uint16_t max_len = 100); + virtual ~BLEDescriptor(); + bool do_create(BLECharacteristic *characteristic); + + void set_value(const std::string value); + void set_value(uint8_t *data, size_t length); + + void gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_t gatts_if, esp_ble_gatts_cb_param_t *param); + + protected: + BLECharacteristic *characteristic_{nullptr}; + ESPBTUUID uuid_; + uint16_t handle_{0xFFFF}; + SemaphoreHandle_t create_lock_; + + esp_attr_value_t value_; + + esp_gatt_perm_t permissions_ = ESP_GATT_PERM_READ | ESP_GATT_PERM_WRITE; +}; + +} // namespace esp32_ble +} // namespace esphome + +#endif diff --git a/esphome/components/esp32_ble/ble_server.cpp b/esphome/components/esp32_ble/ble_server.cpp new file mode 100644 index 0000000000..0b0105cd4d --- /dev/null +++ b/esphome/components/esp32_ble/ble_server.cpp @@ -0,0 +1,158 @@ +#include "ble_server.h" +#include "ble.h" +#include "esphome/core/log.h" +#include "esphome/core/application.h" +#include "esphome/core/version.h" + +#ifdef ARDUINO_ARCH_ESP32 + +#include +#include +#include +#include +#include +#include + +namespace esphome { +namespace esp32_ble { + +static const char *TAG = "esp32_ble.server"; + +static const uint16_t DEVICE_INFORMATION_SERVICE_UUID = 0x180A; +static const uint16_t MODEL_UUID = 0x2A24; +static const uint16_t VERSION_UUID = 0x2A26; +static const uint16_t MANUFACTURER_UUID = 0x2A29; + +void BLEServer::setup() { + if (this->is_failed()) { + ESP_LOGE(TAG, "BLE Server was marked failed by ESP32BLE"); + return; + } + + ESP_LOGD(TAG, "Setting up BLE Server..."); + + global_ble_server = this; + this->register_lock_ = xSemaphoreCreateBinary(); + xSemaphoreGive(this->register_lock_); + this->advertising_ = new BLEAdvertising(); + + this->setup_server_(); + + for (auto *component : this->service_components_) { + component->setup_service(); + } + + ESP_LOGD(TAG, "BLE Server set up complete..."); +} + +void BLEServer::setup_server_() { + xSemaphoreTake(this->register_lock_, SEMAPHORE_MAX_DELAY); + esp_err_t err = esp_ble_gatts_app_register(0); + if (err != ESP_OK) { + ESP_LOGE(TAG, "esp_ble_gatts_app_register failed: %d", err); + this->mark_failed(); + return; + } + xSemaphoreWait(this->register_lock_, SEMAPHORE_MAX_DELAY); + + this->device_information_service = this->create_service(DEVICE_INFORMATION_SERVICE_UUID); + + this->create_device_characteristics_(); + + this->advertising_->set_scan_response(true); + this->advertising_->set_min_preferred_interval(0x06); + this->advertising_->start(); + + this->device_information_service->start(); +} + +bool BLEServer::create_device_characteristics_() { + if (this->model_.has_value()) { + BLECharacteristic *model = + this->device_information_service->create_characteristic(MODEL_UUID, BLECharacteristic::PROPERTY_READ); + model->set_value(this->model_.value()); + } else { +#ifdef ARDUINO_BOARD + BLECharacteristic *model = + this->device_information_service->create_characteristic(MODEL_UUID, BLECharacteristic::PROPERTY_READ); + model->set_value(ARDUINO_BOARD); +#endif + } + + BLECharacteristic *version = + this->device_information_service->create_characteristic(VERSION_UUID, BLECharacteristic::PROPERTY_READ); + version->set_value("ESPHome " ESPHOME_VERSION); + + BLECharacteristic *manufacturer = + this->device_information_service->create_characteristic(MANUFACTURER_UUID, BLECharacteristic::PROPERTY_READ); + manufacturer->set_value(this->manufacturer_); + + return true; +} + +BLEService *BLEServer::create_service(const uint8_t *uuid, bool advertise) { + return this->create_service(ESPBTUUID::from_raw(uuid), advertise); +} +BLEService *BLEServer::create_service(uint16_t uuid, bool advertise) { + return this->create_service(ESPBTUUID::from_uint16(uuid), advertise); +} +BLEService *BLEServer::create_service(const std::string uuid, bool advertise) { + return this->create_service(ESPBTUUID::from_raw(uuid), advertise); +} +BLEService *BLEServer::create_service(ESPBTUUID uuid, bool advertise, uint16_t num_handles, uint8_t inst_id) { + ESP_LOGV(TAG, "Creating service - %s", uuid.to_string().c_str()); + BLEService *service = new BLEService(uuid, num_handles, inst_id); + this->services_.push_back(service); + if (advertise) { + this->advertising_->add_service_uuid(uuid); + } + service->do_create(this); + return service; +} + +void BLEServer::gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_t gatts_if, + esp_ble_gatts_cb_param_t *param) { + switch (event) { + case ESP_GATTS_CONNECT_EVT: { + ESP_LOGD(TAG, "BLE Client connected"); + this->add_client_(param->connect.conn_id, (void *) this); + this->connected_clients_++; + for (auto *component : this->service_components_) { + component->on_client_connect(); + } + break; + } + case ESP_GATTS_DISCONNECT_EVT: { + ESP_LOGD(TAG, "BLE Client disconnected"); + if (this->remove_client_(param->disconnect.conn_id)) + this->connected_clients_--; + this->advertising_->start(); + for (auto *component : this->service_components_) { + component->on_client_disconnect(); + } + break; + } + case ESP_GATTS_REG_EVT: { + this->gatts_if_ = gatts_if; + xSemaphoreGive(this->register_lock_); + break; + } + default: + break; + } + + for (auto *service : this->services_) { + service->gatts_event_handler(event, gatts_if, param); + } +} + +float BLEServer::get_setup_priority() const { return setup_priority::BLUETOOTH - 10; } + +void BLEServer::dump_config() { ESP_LOGCONFIG(TAG, "ESP32 BLE Server:"); } + +BLEServer *global_ble_server = nullptr; + +} // namespace esp32_ble +} // namespace esphome + +#endif diff --git a/esphome/components/esp32_ble/ble_server.h b/esphome/components/esp32_ble/ble_server.h new file mode 100644 index 0000000000..b6e3f203ed --- /dev/null +++ b/esphome/components/esp32_ble/ble_server.h @@ -0,0 +1,84 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/core/helpers.h" +#include "esphome/core/preferences.h" +#include "ble_service.h" +#include "ble_characteristic.h" +#include "ble_uuid.h" +#include "ble_advertising.h" +#include + +#include "queue.h" + +#ifdef ARDUINO_ARCH_ESP32 + +#include +#include + +namespace esphome { +namespace esp32_ble { + +class BLEServiceComponent { + public: + virtual void setup_service(); + virtual void on_client_connect(){}; + virtual void on_client_disconnect(){}; +}; + +class BLEServer : public Component { + public: + void setup() override; + void dump_config() override; + float get_setup_priority() const override; + + void teardown(); + + void set_manufacturer(const std::string manufacturer) { this->manufacturer_ = manufacturer; } + void set_model(const std::string model) { this->model_ = model; } + + BLEService *create_service(const uint8_t *uuid, bool advertise = false); + BLEService *create_service(uint16_t uuid, bool advertise = false); + BLEService *create_service(const std::string uuid, bool advertise = false); + BLEService *create_service(ESPBTUUID uuid, bool advertise = false, uint16_t num_handles = 15, uint8_t inst_id = 0); + + esp_gatt_if_t get_gatts_if() { return this->gatts_if_; } + uint32_t get_connected_client_count() { return this->connected_clients_; } + std::map get_clients() { return this->clients_; } + BLEAdvertising *get_advertising() { return this->advertising_; } + + void gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_t gatts_if, esp_ble_gatts_cb_param_t *param); + + void register_service_component(BLEServiceComponent *component) { this->service_components_.push_back(component); } + + protected: + bool create_device_characteristics_(); + void setup_server_(); + + void add_client_(uint16_t conn_id, void *client) { + this->clients_.insert(std::pair(conn_id, client)); + } + bool remove_client_(uint16_t conn_id) { return this->clients_.erase(conn_id) > 0; } + + std::string manufacturer_; + optional model_; + esp_gatt_if_t gatts_if_{0}; + BLEAdvertising *advertising_; + + uint32_t connected_clients_{0}; + std::map clients_; + + std::vector services_; + BLEService *device_information_service; + + std::vector service_components_; + + SemaphoreHandle_t register_lock_; +}; + +extern BLEServer *global_ble_server; + +} // namespace esp32_ble +} // namespace esphome + +#endif diff --git a/esphome/components/esp32_ble/ble_service.cpp b/esphome/components/esp32_ble/ble_service.cpp new file mode 100644 index 0000000000..9f837175ed --- /dev/null +++ b/esphome/components/esp32_ble/ble_service.cpp @@ -0,0 +1,129 @@ +#include "ble_service.h" +#include "ble_server.h" +#include "esphome/core/log.h" + +#ifdef ARDUINO_ARCH_ESP32 + +namespace esphome { +namespace esp32_ble { + +static const char *TAG = "esp32_ble.service"; + +BLEService::BLEService(ESPBTUUID uuid, uint16_t num_handles, uint8_t inst_id) + : uuid_(uuid), num_handles_(num_handles), inst_id_(inst_id) { + this->create_lock_ = xSemaphoreCreateBinary(); + this->start_lock_ = xSemaphoreCreateBinary(); + this->stop_lock_ = xSemaphoreCreateBinary(); + + xSemaphoreGive(this->create_lock_); + xSemaphoreGive(this->start_lock_); + xSemaphoreGive(this->stop_lock_); +} + +BLEService::~BLEService() { + for (auto &chr : this->characteristics_) + delete chr; +} + +BLECharacteristic *BLEService::get_characteristic(ESPBTUUID uuid) { + for (auto *chr : this->characteristics_) + if (chr->get_uuid() == uuid) + return chr; + return nullptr; +} + +BLECharacteristic *BLEService::get_characteristic(uint16_t uuid) { + return this->get_characteristic(ESPBTUUID::from_uint16(uuid)); +} +BLECharacteristic *BLEService::create_characteristic(uint16_t uuid, esp_gatt_char_prop_t properties) { + return create_characteristic(ESPBTUUID::from_uint16(uuid), properties); +} +BLECharacteristic *BLEService::create_characteristic(const std::string uuid, esp_gatt_char_prop_t properties) { + return create_characteristic(ESPBTUUID::from_raw(uuid), properties); +} +BLECharacteristic *BLEService::create_characteristic(ESPBTUUID uuid, esp_gatt_char_prop_t properties) { + BLECharacteristic *characteristic = new BLECharacteristic(uuid, properties); + this->characteristics_.push_back(characteristic); + return characteristic; +} + +bool BLEService::do_create(BLEServer *server) { + this->server_ = server; + + xSemaphoreTake(this->create_lock_, SEMAPHORE_MAX_DELAY); + esp_gatt_srvc_id_t srvc_id; + srvc_id.is_primary = true; + srvc_id.id.inst_id = this->inst_id_; + srvc_id.id.uuid = this->uuid_.get_uuid(); + + esp_err_t err = esp_ble_gatts_create_service(server->get_gatts_if(), &srvc_id, this->num_handles_); + if (err != ESP_OK) { + ESP_LOGE(TAG, "esp_ble_gatts_create_service failed: %d", err); + return false; + } + xSemaphoreWait(this->create_lock_, SEMAPHORE_MAX_DELAY); + + return true; +} + +void BLEService::start() { + for (auto *characteristic : this->characteristics_) { + this->last_created_characteristic_ = characteristic; + characteristic->do_create(this); + } + + xSemaphoreTake(this->start_lock_, SEMAPHORE_MAX_DELAY); + esp_err_t err = esp_ble_gatts_start_service(this->handle_); + if (err != ESP_OK) { + ESP_LOGE(TAG, "esp_ble_gatts_start_service failed: %d", err); + return; + } + xSemaphoreWait(this->start_lock_, SEMAPHORE_MAX_DELAY); +} + +void BLEService::stop() { + xSemaphoreTake(this->stop_lock_, SEMAPHORE_MAX_DELAY); + esp_err_t err = esp_ble_gatts_stop_service(this->handle_); + if (err != ESP_OK) { + ESP_LOGE(TAG, "esp_ble_gatts_stop_service failed: %d", err); + return; + } + xSemaphoreWait(this->stop_lock_, SEMAPHORE_MAX_DELAY); +} + +void BLEService::gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_t gatts_if, + esp_ble_gatts_cb_param_t *param) { + switch (event) { + case ESP_GATTS_CREATE_EVT: { + if (this->uuid_ == ESPBTUUID::from_uuid(param->create.service_id.id.uuid) && + this->inst_id_ == param->create.service_id.id.inst_id) { + this->handle_ = param->create.service_handle; + xSemaphoreGive(this->create_lock_); + } + break; + } + case ESP_GATTS_START_EVT: { + if (param->start.service_handle == this->handle_) { + xSemaphoreGive(this->start_lock_); + } + break; + } + case ESP_GATTS_STOP_EVT: { + if (param->start.service_handle == this->handle_) { + xSemaphoreGive(this->stop_lock_); + } + break; + } + default: + break; + } + + for (auto *characteristic : this->characteristics_) { + characteristic->gatts_event_handler(event, gatts_if, param); + } +} + +} // namespace esp32_ble +} // namespace esphome + +#endif diff --git a/esphome/components/esp32_ble/ble_service.h b/esphome/components/esp32_ble/ble_service.h new file mode 100644 index 0000000000..c87415be13 --- /dev/null +++ b/esphome/components/esp32_ble/ble_service.h @@ -0,0 +1,61 @@ +#pragma once + +#include "ble_uuid.h" +#include "ble_characteristic.h" + +#ifdef ARDUINO_ARCH_ESP32 + +#include +#include +#include +#include +#include + +namespace esphome { +namespace esp32_ble { + +class BLEServer; + +class BLEService { + public: + BLEService(ESPBTUUID uuid, uint16_t num_handles, uint8_t inst_id); + ~BLEService(); + BLECharacteristic *get_characteristic(ESPBTUUID uuid); + BLECharacteristic *get_characteristic(uint16_t uuid); + + BLECharacteristic *create_characteristic(const std::string uuid, esp_gatt_char_prop_t properties); + BLECharacteristic *create_characteristic(uint16_t uuid, esp_gatt_char_prop_t properties); + BLECharacteristic *create_characteristic(ESPBTUUID uuid, esp_gatt_char_prop_t properties); + + ESPBTUUID get_uuid() { return this->uuid_; } + BLECharacteristic *get_last_created_characteristic() { return this->last_created_characteristic_; } + uint16_t get_handle() { return this->handle_; } + + BLEServer *get_server() { return this->server_; } + + bool do_create(BLEServer *server); + void gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_t gatts_if, esp_ble_gatts_cb_param_t *param); + + void start(); + void stop(); + + protected: + bool errored_{false}; + + std::vector characteristics_; + BLECharacteristic *last_created_characteristic_{nullptr}; + BLEServer *server_; + ESPBTUUID uuid_; + uint16_t num_handles_; + uint16_t handle_{0xFFFF}; + uint8_t inst_id_; + + SemaphoreHandle_t create_lock_; + SemaphoreHandle_t start_lock_; + SemaphoreHandle_t stop_lock_; +}; + +} // namespace esp32_ble +} // namespace esphome + +#endif diff --git a/esphome/components/esp32_ble/ble_uuid.cpp b/esphome/components/esp32_ble/ble_uuid.cpp new file mode 100644 index 0000000000..63633e2c43 --- /dev/null +++ b/esphome/components/esp32_ble/ble_uuid.cpp @@ -0,0 +1,184 @@ +#include "ble_uuid.h" + +#ifdef ARDUINO_ARCH_ESP32 + +namespace esphome { +namespace esp32_ble { + +ESPBTUUID::ESPBTUUID() : uuid_() {} +ESPBTUUID ESPBTUUID::from_uint16(uint16_t uuid) { + ESPBTUUID ret; + ret.uuid_.len = ESP_UUID_LEN_16; + ret.uuid_.uuid.uuid16 = uuid; + return ret; +} +ESPBTUUID ESPBTUUID::from_uint32(uint32_t uuid) { + ESPBTUUID ret; + ret.uuid_.len = ESP_UUID_LEN_32; + ret.uuid_.uuid.uuid32 = uuid; + return ret; +} +ESPBTUUID ESPBTUUID::from_raw(const uint8_t *data) { + ESPBTUUID ret; + ret.uuid_.len = ESP_UUID_LEN_128; + for (size_t i = 0; i < ESP_UUID_LEN_128; i++) + ret.uuid_.uuid.uuid128[i] = data[i]; + return ret; +} +ESPBTUUID ESPBTUUID::from_raw(const std::string data) { + ESPBTUUID ret; + if (data.length() == 4) { + ret.uuid_.len = ESP_UUID_LEN_16; + ret.uuid_.uuid.uuid16 = 0; + for (int i = 0; i < data.length();) { + uint8_t MSB = data.c_str()[i]; + uint8_t LSB = data.c_str()[i + 1]; + + if (MSB > '9') + MSB -= 7; + if (LSB > '9') + LSB -= 7; + ret.uuid_.uuid.uuid16 += (((MSB & 0x0F) << 4) | (LSB & 0x0F)) << (2 - i) * 4; + i += 2; + } + } else if (data.length() == 8) { + ret.uuid_.len = ESP_UUID_LEN_32; + ret.uuid_.uuid.uuid32 = 0; + for (int i = 0; i < data.length();) { + uint8_t MSB = data.c_str()[i]; + uint8_t LSB = data.c_str()[i + 1]; + + if (MSB > '9') + MSB -= 7; + if (LSB > '9') + LSB -= 7; + ret.uuid_.uuid.uuid32 += (((MSB & 0x0F) << 4) | (LSB & 0x0F)) << (6 - i) * 4; + i += 2; + } + } else if (data.length() == 16) { // how we can have 16 byte length string reprezenting 128 bit uuid??? needs to be + // investigated (lack of time) + ret.uuid_.len = ESP_UUID_LEN_128; + memcpy(ret.uuid_.uuid.uuid128, (uint8_t *) data.data(), 16); + } else if (data.length() == 36) { + // If the length of the string is 36 bytes then we will assume it is a long hex string in + // UUID format. + ret.uuid_.len = ESP_UUID_LEN_128; + int n = 0; + for (int i = 0; i < data.length();) { + if (data.c_str()[i] == '-') + i++; + uint8_t MSB = data.c_str()[i]; + uint8_t LSB = data.c_str()[i + 1]; + + if (MSB > '9') + MSB -= 7; + if (LSB > '9') + LSB -= 7; + ret.uuid_.uuid.uuid128[15 - n++] = ((MSB & 0x0F) << 4) | (LSB & 0x0F); + i += 2; + } + } else { + ESP_LOGE(TAG, "ERROR: UUID value not 2, 4, 16 or 36 bytes - %s", data.c_str()); + } + 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; + } + uint8_t data[] = {0xFB, 0x34, 0x9B, 0x5F, 0x80, 0x00, 0x00, 0x80, 0x00, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + uint32_t uuid32; + if (this->uuid_.len == ESP_UUID_LEN_32) { + uuid32 = this->uuid_.uuid.uuid32; + } else { + uuid32 = this->uuid_.uuid.uuid16; + } + for (uint8_t i = 0; i < this->uuid_.len; i++) { + data[12 + i] = ((uuid32 >> i * 8) & 0xFF); + } + return ESPBTUUID::from_raw(data); +} +bool ESPBTUUID::contains(uint8_t data1, uint8_t data2) const { + if (this->uuid_.len == ESP_UUID_LEN_16) { + return (this->uuid_.uuid.uuid16 >> 8) == data2 && (this->uuid_.uuid.uuid16 & 0xFF) == data1; + } else if (this->uuid_.len == ESP_UUID_LEN_32) { + for (uint8_t i = 0; i < 3; i++) { + bool a = ((this->uuid_.uuid.uuid32 >> i * 8) & 0xFF) == data1; + bool b = ((this->uuid_.uuid.uuid32 >> (i + 1) * 8) & 0xFF) == data2; + if (a && b) + return true; + } + } else { + for (uint8_t i = 0; i < 15; i++) { + if (this->uuid_.uuid.uuid128[i] == data1 && this->uuid_.uuid.uuid128[i + 1] == data2) + return true; + } + } + return false; +} +bool ESPBTUUID::operator==(const ESPBTUUID &uuid) const { + if (this->uuid_.len == uuid.uuid_.len) { + switch (this->uuid_.len) { + case ESP_UUID_LEN_16: + if (uuid.uuid_.uuid.uuid16 == this->uuid_.uuid.uuid16) { + return true; + } + break; + case ESP_UUID_LEN_32: + if (uuid.uuid_.uuid.uuid32 == this->uuid_.uuid.uuid32) { + return true; + } + break; + case ESP_UUID_LEN_128: + for (int i = 0; i < ESP_UUID_LEN_128; i++) { + if (uuid.uuid_.uuid.uuid128[i] != this->uuid_.uuid.uuid128[i]) { + return false; + } + } + return true; + break; + } + } else { + return this->as_128bit() == uuid.as_128bit(); + } + return false; +} +esp_bt_uuid_t ESPBTUUID::get_uuid() { return this->uuid_; } +std::string ESPBTUUID::to_string() { + char sbuf[64]; + switch (this->uuid_.len) { + case ESP_UUID_LEN_16: + sprintf(sbuf, "0x%02X%02X", this->uuid_.uuid.uuid16 >> 8, this->uuid_.uuid.uuid16 & 0xff); + break; + case ESP_UUID_LEN_32: + 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: + char *bpos = sbuf; + for (int8_t i = 15; i >= 0; i--) { + sprintf(bpos, "%02X", this->uuid_.uuid.uuid128[i]); + bpos += 2; + if (i == 6 || i == 8 || i == 10 || i == 12) + sprintf(bpos++, "-"); + } + sbuf[47] = '\0'; + break; + } + return sbuf; +} + +} // namespace esp32_ble +} // namespace esphome + +#endif diff --git a/esphome/components/esp32_ble/ble_uuid.h b/esphome/components/esp32_ble/ble_uuid.h new file mode 100644 index 0000000000..96e9b126d4 --- /dev/null +++ b/esphome/components/esp32_ble/ble_uuid.h @@ -0,0 +1,45 @@ +#pragma once + +#include "esphome/core/helpers.h" + +#ifdef ARDUINO_ARCH_ESP32 + +#include +#include + +namespace esphome { +namespace esp32_ble { + +class ESPBTUUID { + public: + ESPBTUUID(); + + static ESPBTUUID from_uint16(uint16_t uuid); + + static ESPBTUUID from_uint32(uint32_t uuid); + + static ESPBTUUID from_raw(const uint8_t *data); + + static ESPBTUUID from_raw(const std::string data); + + static ESPBTUUID from_uuid(esp_bt_uuid_t uuid); + + ESPBTUUID as_128bit() const; + + bool contains(uint8_t data1, uint8_t data2) const; + + bool operator==(const ESPBTUUID &uuid) const; + bool operator!=(const ESPBTUUID &uuid) const { return !(*this == uuid); } + + esp_bt_uuid_t get_uuid(); + + std::string to_string(); + + protected: + esp_bt_uuid_t uuid_; +}; + +} // namespace esp32_ble +} // namespace esphome + +#endif diff --git a/esphome/components/esp32_ble/queue.h b/esphome/components/esp32_ble/queue.h new file mode 100644 index 0000000000..f5c861a917 --- /dev/null +++ b/esphome/components/esp32_ble/queue.h @@ -0,0 +1,114 @@ +#pragma once +#include "esphome/core/component.h" +#include "esphome/core/helpers.h" + +#include +#include + +#ifdef ARDUINO_ARCH_ESP32 + +#include +#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 { + +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_ = GAP; + }; + + 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_ = GATTC; + }; + + BLEEvent(esp_gatts_cb_event_t e, esp_gatt_if_t i, esp_ble_gatts_cb_param_t *p) { + this->event_.gatts.gatts_event = e; + this->event_.gatts.gatts_if = i; + memcpy(&this->event_.gatts.gatts_param, p, sizeof(esp_ble_gatts_cb_param_t)); + this->type_ = GATTS; + }; + + 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; + + struct gatts_event { + esp_gatts_cb_event_t gatts_event; + esp_gatt_if_t gatts_if; + esp_ble_gatts_cb_param_t gatts_param; + } gatts; + } event_; + enum ble_event_t : uint8_t { + GAP, + GATTC, + GATTS, + } type_; +}; + +} // namespace esp32_ble +} // namespace esphome + +#endif diff --git a/esphome/components/esp32_improv/__init__.py b/esphome/components/esp32_improv/__init__.py new file mode 100644 index 0000000000..8b91e9151d --- /dev/null +++ b/esphome/components/esp32_improv/__init__.py @@ -0,0 +1,69 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import binary_sensor, output, esp32_ble +from esphome.const import CONF_ID, ESP_PLATFORM_ESP32 + + +AUTO_LOAD = ["binary_sensor", "output", "improv"] +CODEOWNERS = ["@jesserockz"] +DEPENDENCIES = ["esp32_ble", "wifi"] +ESP_PLATFORMS = [ESP_PLATFORM_ESP32] + +CONF_AUTHORIZED_DURATION = "authorized_duration" +CONF_AUTHORIZER = "authorizer" +CONF_BLE_SERVER_ID = "ble_server_id" +CONF_IDENTIFY_DURATION = "identify_duration" +CONF_STATUS_INDICATOR = "status_indicator" +CONF_WIFI_TIMEOUT = "wifi_timeout" + +esp32_improv_ns = cg.esphome_ns.namespace("esp32_improv") +ESP32ImprovComponent = esp32_improv_ns.class_( + "ESP32ImprovComponent", cg.Component, esp32_ble.BLEServiceComponent +) + + +def validate_none_(value): + if value in ("none", "None"): + return None + if cv.boolean(value) is False: + return None + raise cv.Invalid("Must be none") + + +CONFIG_SCHEMA = cv.Schema( + { + cv.GenerateID(): cv.declare_id(ESP32ImprovComponent), + cv.GenerateID(CONF_BLE_SERVER_ID): cv.use_id(esp32_ble.BLEServer), + cv.Required(CONF_AUTHORIZER): cv.Any( + validate_none_, cv.use_id(binary_sensor.BinarySensor) + ), + cv.Optional(CONF_STATUS_INDICATOR): cv.use_id(output.BinaryOutput), + cv.Optional( + CONF_IDENTIFY_DURATION, default="10s" + ): cv.positive_time_period_milliseconds, + cv.Optional( + CONF_AUTHORIZED_DURATION, default="1min" + ): cv.positive_time_period_milliseconds, + } +).extend(cv.COMPONENT_SCHEMA) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + + ble_server = await cg.get_variable(config[CONF_BLE_SERVER_ID]) + cg.add(ble_server.register_service_component(var)) + + cg.add_define("USE_IMPROV") + + cg.add(var.set_identify_duration(config[CONF_IDENTIFY_DURATION])) + cg.add(var.set_authorized_duration(config[CONF_AUTHORIZED_DURATION])) + + if CONF_AUTHORIZER in config and config[CONF_AUTHORIZER] is not None: + activator = await cg.get_variable(config[CONF_AUTHORIZER]) + cg.add(var.set_authorizer(activator)) + + if CONF_STATUS_INDICATOR in config: + status_indicator = await cg.get_variable(config[CONF_STATUS_INDICATOR]) + cg.add(var.set_status_indicator(status_indicator)) diff --git a/esphome/components/esp32_improv/esp32_improv_component.cpp b/esphome/components/esp32_improv/esp32_improv_component.cpp new file mode 100644 index 0000000000..3fc5e6acf2 --- /dev/null +++ b/esphome/components/esp32_improv/esp32_improv_component.cpp @@ -0,0 +1,275 @@ +#include "esp32_improv_component.h" +#include "esphome/core/log.h" +#include "esphome/core/application.h" +#include "esphome/components/esp32_ble/ble_2902.h" + +#ifdef ARDUINO_ARCH_ESP32 + +namespace esphome { +namespace esp32_improv { + +static const char *TAG = "esp32_improv.component"; + +ESP32ImprovComponent::ESP32ImprovComponent() { global_improv_component = this; } + +void ESP32ImprovComponent::setup_service() { + this->service_ = esp32_ble::global_ble_server->create_service(improv::SERVICE_UUID, true); + + this->status_ = this->service_->create_characteristic( + improv::STATUS_UUID, esp32_ble::BLECharacteristic::PROPERTY_READ | esp32_ble::BLECharacteristic::PROPERTY_NOTIFY); + esp32_ble::BLEDescriptor *status_descriptor = new esp32_ble::BLE2902(); + this->status_->add_descriptor(status_descriptor); + + this->error_ = this->service_->create_characteristic( + improv::ERROR_UUID, esp32_ble::BLECharacteristic::PROPERTY_READ | esp32_ble::BLECharacteristic::PROPERTY_NOTIFY); + esp32_ble::BLEDescriptor *error_descriptor = new esp32_ble::BLE2902(); + this->error_->add_descriptor(error_descriptor); + + this->rpc_ = + this->service_->create_characteristic(improv::RPC_COMMAND_UUID, esp32_ble::BLECharacteristic::PROPERTY_WRITE); + this->rpc_->on_write([this](std::vector &data) { + if (data.size() > 0) { + this->incoming_data_.insert(this->incoming_data_.end(), data.begin(), data.end()); + } + }); + esp32_ble::BLEDescriptor *rpc_descriptor = new esp32_ble::BLE2902(); + this->rpc_->add_descriptor(rpc_descriptor); + + this->rpc_response_ = + this->service_->create_characteristic(improv::RPC_RESULT_UUID, esp32_ble::BLECharacteristic::PROPERTY_READ | + esp32_ble::BLECharacteristic::PROPERTY_NOTIFY); + esp32_ble::BLEDescriptor *rpc_response_descriptor = new esp32_ble::BLE2902(); + this->rpc_response_->add_descriptor(rpc_response_descriptor); + + this->capabilities_ = + this->service_->create_characteristic(improv::CAPABILITIES_UUID, esp32_ble::BLECharacteristic::PROPERTY_READ); + esp32_ble::BLEDescriptor *capabilities_descriptor = new esp32_ble::BLE2902(); + this->capabilities_->add_descriptor(capabilities_descriptor); + uint8_t capabilities = 0x00; + if (this->status_indicator_ != nullptr) + capabilities |= improv::CAPABILITY_IDENTIFY; + this->capabilities_->set_value(capabilities); + this->setup_complete_ = true; +} + +void ESP32ImprovComponent::loop() { + if (this->incoming_data_.size() > 0) + this->process_incoming_data_(); + uint32_t now = millis(); + + switch (this->state_) { + case improv::STATE_STOPPED: + if (this->status_indicator_ != nullptr) + this->status_indicator_->turn_off(); + + if (this->should_start_ && this->setup_complete_) { + ESP_LOGD(TAG, "Starting Improv service..."); + + this->service_->start(); + this->service_->get_server()->get_advertising()->start(); + + this->set_state_(improv::STATE_AWAITING_AUTHORIZATION); + this->set_error_(improv::ERROR_NONE); + this->should_start_ = false; + ESP_LOGD(TAG, "Service started!"); + } + break; + case improv::STATE_AWAITING_AUTHORIZATION: { + if (this->authorizer_ == nullptr || this->authorizer_->state) { + this->set_state_(improv::STATE_AUTHORIZED); + this->authorized_start_ = now; + } else { + if (this->status_indicator_ != nullptr) { + if (!this->check_identify_()) + this->status_indicator_->turn_on(); + } + } + break; + } + case improv::STATE_AUTHORIZED: { + if (this->authorizer_ != nullptr) { + if (now - this->authorized_start_ > this->authorized_duration_) { + ESP_LOGD(TAG, "Authorization timeout"); + this->set_state_(improv::STATE_AWAITING_AUTHORIZATION); + return; + } + } + if (this->status_indicator_ != nullptr) { + if (!this->check_identify_()) { + if ((now % 1000) < 500) { + this->status_indicator_->turn_on(); + } else { + this->status_indicator_->turn_off(); + } + } + } + break; + } + case improv::STATE_PROVISIONING: { + if (this->status_indicator_ != nullptr) { + if ((now % 200) < 100) { + this->status_indicator_->turn_on(); + } else { + this->status_indicator_->turn_off(); + } + } + if (wifi::global_wifi_component->is_connected()) { + wifi::global_wifi_component->save_wifi_sta(this->connecting_sta_.get_ssid(), + this->connecting_sta_.get_password()); + this->connecting_sta_ = {}; + this->cancel_timeout("wifi-connect-timeout"); + this->set_state_(improv::STATE_PROVISIONED); + + std::string url = "https://my.home-assistant.io/redirect/config_flow_start?domain=esphome"; + std::vector data = improv::build_rpc_response(improv::WIFI_SETTINGS, {url}); + this->send_response(data); + } + break; + } + case improv::STATE_PROVISIONED: { + this->incoming_data_.clear(); + if (this->status_indicator_ != nullptr) + this->status_indicator_->turn_on(); + break; + } + } +} + +bool ESP32ImprovComponent::check_identify_() { + uint32_t now = millis(); + + bool identify = this->identify_start_ != 0 && now - this->identify_start_ <= this->identify_duration_; + + if (identify) { + uint32_t time = now % 1000; + if (time < 600 && time % 200 < 100) { + this->status_indicator_->turn_on(); + } else { + this->status_indicator_->turn_off(); + } + } + return identify; +} + +void ESP32ImprovComponent::set_state_(improv::State state) { + ESP_LOGV(TAG, "Setting state: %d", state); + this->state_ = state; + if (this->status_->get_value().size() == 0 || this->status_->get_value()[0] != state) { + uint8_t data[1]{state}; + this->status_->set_value(data, 1); + if (state != improv::STATE_STOPPED) + this->status_->notify(); + } +} + +void ESP32ImprovComponent::set_error_(improv::Error error) { + if (error != improv::ERROR_NONE) + ESP_LOGE(TAG, "Error: %d", error); + if (this->error_->get_value().size() == 0 || this->error_->get_value()[0] != error) { + uint8_t data[1]{error}; + this->error_->set_value(data, 1); + if (this->state_ != improv::STATE_STOPPED) + this->error_->notify(); + } +} + +void ESP32ImprovComponent::send_response(std::vector &response) { + this->rpc_response_->set_value(response); + if (this->state_ != improv::STATE_STOPPED) + this->rpc_response_->notify(); +} + +void ESP32ImprovComponent::start() { + if (this->state_ != improv::STATE_STOPPED) + return; + + ESP_LOGD(TAG, "Setting Improv to start"); + this->should_start_ = true; +} + +void ESP32ImprovComponent::end() { + this->set_state_(improv::STATE_STOPPED); + this->service_->stop(); +} + +float ESP32ImprovComponent::get_setup_priority() const { + // Before WiFi + return setup_priority::AFTER_BLUETOOTH; +} + +void ESP32ImprovComponent::dump_config() { + ESP_LOGCONFIG(TAG, "ESP32 Improv:"); + LOG_BINARY_SENSOR(" ", "Authorizer", this->authorizer_); + ESP_LOGCONFIG(TAG, " Status Indicator: '%s'", YESNO(this->status_indicator_ != nullptr)); +} + +void ESP32ImprovComponent::process_incoming_data_() { + uint8_t length = this->incoming_data_[1]; + + ESP_LOGD(TAG, "Processing bytes - %s", hexencode(this->incoming_data_).c_str()); + if (this->incoming_data_.size() - 3 == length) { + this->set_error_(improv::ERROR_NONE); + improv::ImprovCommand command = improv::parse_improv_data(this->incoming_data_); + switch (command.command) { + case improv::BAD_CHECKSUM: + ESP_LOGW(TAG, "Error decoding Improv payload"); + this->set_error_(improv::ERROR_INVALID_RPC); + this->incoming_data_.clear(); + break; + case improv::WIFI_SETTINGS: { + if (this->state_ != improv::STATE_AUTHORIZED) { + ESP_LOGW(TAG, "Settings received, but not authorized"); + this->set_error_(improv::ERROR_NOT_AUTHORIZED); + this->incoming_data_.clear(); + return; + } + wifi::WiFiAP sta{}; + sta.set_ssid(command.ssid); + sta.set_password(command.password); + this->connecting_sta_ = sta; + + wifi::global_wifi_component->set_sta(sta); + wifi::global_wifi_component->start_scanning(); + this->set_state_(improv::STATE_PROVISIONING); + ESP_LOGD(TAG, "Received Improv wifi settings ssid=%s, password=" LOG_SECRET("%s"), command.ssid.c_str(), + command.password.c_str()); + + auto f = std::bind(&ESP32ImprovComponent::on_wifi_connect_timeout_, this); + this->set_timeout("wifi-connect-timeout", 30000, f); + this->incoming_data_.clear(); + break; + } + case improv::IDENTIFY: + this->incoming_data_.clear(); + this->identify_start_ = millis(); + break; + default: + ESP_LOGW(TAG, "Unknown Improv payload"); + this->set_error_(improv::ERROR_UNKNOWN_RPC); + this->incoming_data_.clear(); + } + } else if (this->incoming_data_.size() - 2 > length) { + ESP_LOGV(TAG, "Too much data came in, or malformed resetting buffer..."); + this->incoming_data_.clear(); + } else { + ESP_LOGV(TAG, "Waiting for split data packets..."); + } +} + +void ESP32ImprovComponent::on_wifi_connect_timeout_() { + this->set_error_(improv::ERROR_UNABLE_TO_CONNECT); + this->set_state_(improv::STATE_AUTHORIZED); + if (this->authorizer_ != nullptr) + this->authorized_start_ = millis(); + ESP_LOGW(TAG, "Timed out trying to connect to given WiFi network"); + wifi::global_wifi_component->clear_sta(); +} + +void ESP32ImprovComponent::on_client_disconnect() { this->set_error_(improv::ERROR_NONE); }; + +ESP32ImprovComponent *global_improv_component = nullptr; + +} // namespace esp32_improv +} // namespace esphome + +#endif diff --git a/esphome/components/esp32_improv/esp32_improv_component.h b/esphome/components/esp32_improv/esp32_improv_component.h new file mode 100644 index 0000000000..820951f18d --- /dev/null +++ b/esphome/components/esp32_improv/esp32_improv_component.h @@ -0,0 +1,74 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/core/helpers.h" +#include "esphome/core/preferences.h" +#include "esphome/components/binary_sensor/binary_sensor.h" +#include "esphome/components/esp32_ble/ble_server.h" +#include "esphome/components/esp32_ble/ble_characteristic.h" +#include "esphome/components/output/binary_output.h" +#include "esphome/components/wifi/wifi_component.h" +#include "esphome/components/improv/improv.h" + +#ifdef ARDUINO_ARCH_ESP32 + +namespace esphome { +namespace esp32_improv { + +class ESP32ImprovComponent : public Component, public esp32_ble::BLEServiceComponent { + public: + ESP32ImprovComponent(); + void dump_config() override; + void loop() override; + void setup_service() override; + void on_client_disconnect() override; + + float get_setup_priority() const override; + void start(); + void end(); + bool is_active() const { return this->state_ == improv::STATE_AUTHORIZED; } + + void set_authorizer(binary_sensor::BinarySensor *authorizer) { this->authorizer_ = authorizer; } + void set_status_indicator(output::BinaryOutput *status_indicator) { this->status_indicator_ = status_indicator; } + void set_identify_duration(uint32_t identify_duration) { this->identify_duration_ = identify_duration; } + void set_authorized_duration(uint32_t authorized_duration) { this->authorized_duration_ = authorized_duration; } + + protected: + bool should_start_{false}; + bool setup_complete_{false}; + + uint32_t identify_start_{0}; + uint32_t identify_duration_; + uint32_t authorized_start_{0}; + uint32_t authorized_duration_; + + std::vector incoming_data_; + wifi::WiFiAP connecting_sta_; + + esp32_ble::BLEService *service_; + esp32_ble::BLECharacteristic *status_; + esp32_ble::BLECharacteristic *error_; + esp32_ble::BLECharacteristic *rpc_; + esp32_ble::BLECharacteristic *rpc_response_; + esp32_ble::BLECharacteristic *capabilities_; + + binary_sensor::BinarySensor *authorizer_{nullptr}; + output::BinaryOutput *status_indicator_{nullptr}; + + improv::State state_{improv::STATE_STOPPED}; + improv::Error error_state_{improv::ERROR_NONE}; + + void set_state_(improv::State state); + void set_error_(improv::Error error); + void send_response(std::vector &response); + void process_incoming_data_(); + void on_wifi_connect_timeout_(); + bool check_identify_(); +}; + +extern ESP32ImprovComponent *global_improv_component; + +} // namespace esp32_improv +} // namespace esphome + +#endif diff --git a/esphome/components/improv/__init__.py b/esphome/components/improv/__init__.py new file mode 100644 index 0000000000..b1de57df8f --- /dev/null +++ b/esphome/components/improv/__init__.py @@ -0,0 +1 @@ +CODEOWNERS = ["@jesserockz"] diff --git a/esphome/components/improv/improv.cpp b/esphome/components/improv/improv.cpp new file mode 100644 index 0000000000..6bda79a2c1 --- /dev/null +++ b/esphome/components/improv/improv.cpp @@ -0,0 +1,66 @@ +#include "improv.h" + +namespace improv { + +ImprovCommand parse_improv_data(std::vector &data) { return parse_improv_data(data.data(), data.size()); } + +ImprovCommand parse_improv_data(const uint8_t *data, size_t length) { + Command command = (Command) data[0]; + uint8_t data_length = data[1]; + + if (data_length != length - 3) { + return {.command = UNKNOWN}; + } + + uint8_t checksum = data[length - 1]; + + uint32_t calculated_checksum = 0; + for (uint8_t i = 0; i < length - 1; i++) { + calculated_checksum += data[i]; + } + + if ((uint8_t) calculated_checksum != checksum) { + return {.command = BAD_CHECKSUM}; + } + + if (command == WIFI_SETTINGS) { + uint8_t ssid_length = data[2]; + uint8_t ssid_start = 3; + size_t ssid_end = ssid_start + ssid_length; + + uint8_t pass_length = data[ssid_end]; + size_t pass_start = ssid_end + 1; + size_t pass_end = pass_start + pass_length; + + std::string ssid(data + ssid_start, data + ssid_end); + std::string password(data + pass_start, data + pass_end); + return {.command = command, .ssid = ssid, .password = password}; + } + + return { + .command = command, + }; +} + +std::vector build_rpc_response(Command command, std::vector datum) { + std::vector out; + uint32_t length = 0; + out.push_back(command); + for (auto str : datum) { + uint8_t len = str.length(); + length += len; + out.push_back(len); + out.insert(out.end(), str.begin(), str.end()); + } + out.insert(out.begin() + 1, length); + + uint32_t calculated_checksum = 0; + + for (uint8_t byte : out) { + calculated_checksum += byte; + } + out.push_back(calculated_checksum); + return out; +} + +} // namespace improv diff --git a/esphome/components/improv/improv.h b/esphome/components/improv/improv.h new file mode 100644 index 0000000000..c13c68c90e --- /dev/null +++ b/esphome/components/improv/improv.h @@ -0,0 +1,53 @@ +#pragma once + +#include +#include +#include + +namespace improv { + +static const char *SERVICE_UUID = "00467768-6228-2272-4663-277478268000"; +static const char *STATUS_UUID = "00467768-6228-2272-4663-277478268001"; +static const char *ERROR_UUID = "00467768-6228-2272-4663-277478268002"; +static const char *RPC_COMMAND_UUID = "00467768-6228-2272-4663-277478268003"; +static const char *RPC_RESULT_UUID = "00467768-6228-2272-4663-277478268004"; +static const char *CAPABILITIES_UUID = "00467768-6228-2272-4663-277478268005"; + +enum Error : uint8_t { + ERROR_NONE = 0x00, + ERROR_INVALID_RPC = 0x01, + ERROR_UNKNOWN_RPC = 0x02, + ERROR_UNABLE_TO_CONNECT = 0x03, + ERROR_NOT_AUTHORIZED = 0x04, + ERROR_UNKNOWN = 0xFF, +}; + +enum State : uint8_t { + STATE_STOPPED = 0x00, + STATE_AWAITING_AUTHORIZATION = 0x01, + STATE_AUTHORIZED = 0x02, + STATE_PROVISIONING = 0x03, + STATE_PROVISIONED = 0x04, +}; + +enum Command : uint8_t { + UNKNOWN = 0x00, + WIFI_SETTINGS = 0x01, + IDENTIFY = 0x02, + BAD_CHECKSUM = 0xFF, +}; + +static const uint8_t CAPABILITY_IDENTIFY = 0x01; + +struct ImprovCommand { + Command command; + std::string ssid; + std::string password; +}; + +ImprovCommand parse_improv_data(std::vector &data); +ImprovCommand parse_improv_data(const uint8_t *data, size_t length); + +std::vector build_rpc_response(Command command, std::vector datum); + +} // namespace improv diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index 0a6607852d..da1f6f85a7 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -22,6 +22,10 @@ #include "esphome/components/captive_portal/captive_portal.h" #endif +#ifdef USE_IMPROV +#include "esphome/components/esp32_improv/esp32_improv_component.h" +#endif + namespace esphome { namespace wifi { @@ -34,6 +38,19 @@ void WiFiComponent::setup() { this->last_connected_ = millis(); this->wifi_pre_setup_(); + uint32_t hash = fnv1_hash(App.get_compilation_time()); + this->pref_ = global_preferences.make_preference(hash, true); + + SavedWifiSettings save{}; + if (this->pref_.load(&save)) { + ESP_LOGD(TAG, "Loaded saved wifi settings: %s", save.ssid); + + WiFiAP sta{}; + sta.set_ssid(save.ssid); + sta.set_password(save.password); + this->set_sta(sta); + } + if (this->has_sta()) { this->wifi_sta_pre_setup_(); if (this->output_power_.has_value() && !this->wifi_apply_output_power_(*this->output_power_)) { @@ -60,7 +77,10 @@ void WiFiComponent::setup() { captive_portal::global_captive_portal->start(); #endif } - +#ifdef USE_IMPROV + if (esp32_improv::global_improv_component != nullptr) + esp32_improv::global_improv_component->start(); +#endif this->wifi_apply_hostname_(); #if defined(ARDUINO_ARCH_ESP32) && defined(USE_MDNS) network_setup_mdns(); @@ -122,6 +142,14 @@ void WiFiComponent::loop() { } } +#ifdef USE_IMPROV + if (esp32_improv::global_improv_component != nullptr) { + if (!this->is_connected()) { + esp32_improv::global_improv_component->start(); + } + } +#endif + if (!this->has_ap() && this->reboot_timeout_ != 0) { if (now - this->last_connected_ > this->reboot_timeout_) { ESP_LOGE(TAG, "Can't connect to WiFi, rebooting..."); @@ -186,9 +214,21 @@ float WiFiComponent::get_loop_priority() const { void WiFiComponent::set_ap(const WiFiAP &ap) { this->ap_ = ap; } void WiFiComponent::add_sta(const WiFiAP &ap) { this->sta_.push_back(ap); } void WiFiComponent::set_sta(const WiFiAP &ap) { - this->sta_.clear(); + this->clear_sta(); this->add_sta(ap); } +void WiFiComponent::clear_sta() { this->sta_.clear(); } +void WiFiComponent::save_wifi_sta(const std::string &ssid, const std::string &password) { + SavedWifiSettings save{}; + strcpy(save.ssid, ssid.c_str()); + strcpy(save.password, password.c_str()); + this->pref_.save(&save); + + WiFiAP sta{}; + sta.set_ssid(ssid); + sta.set_password(password); + this->set_sta(sta); +} void WiFiComponent::start_connecting(const WiFiAP &ap, bool two) { ESP_LOGI(TAG, "WiFi Connecting to '%s'...", ap.get_ssid().c_str()); @@ -466,6 +506,12 @@ void WiFiComponent::check_connecting_finished() { ESP_LOGD(TAG, "Disabling AP..."); this->wifi_mode_({}, false); } +#ifdef USE_IMPROV + if (this->is_esp32_improv_active_()) { + esp32_improv::global_improv_component->end(); + } +#endif + #if defined(ARDUINO_ARCH_ESP8266) && defined(USE_MDNS) network_setup_mdns(this->wifi_sta_ip_(), 0); #endif @@ -517,7 +563,8 @@ void WiFiComponent::retry_connect() { } delay(10); - if (!this->is_captive_portal_active_() && (this->num_retried_ > 5 || this->error_from_callback_)) { + if (!this->is_captive_portal_active_() && !this->is_esp32_improv_active_() && + (this->num_retried_ > 5 || this->error_from_callback_)) { // If retry failed for more than 5 times, let's restart STA ESP_LOGW(TAG, "Restarting WiFi adapter..."); this->wifi_mode_(false, {}); @@ -563,6 +610,13 @@ bool WiFiComponent::is_captive_portal_active_() { return false; #endif } +bool WiFiComponent::is_esp32_improv_active_() { +#ifdef USE_IMPROV + return esp32_improv::global_improv_component != nullptr && esp32_improv::global_improv_component->is_active(); +#else + return false; +#endif +} void WiFiAP::set_ssid(const std::string &ssid) { this->ssid_ = ssid; } void WiFiAP::set_bssid(bssid_t bssid) { this->bssid_ = bssid; } diff --git a/esphome/components/wifi/wifi_component.h b/esphome/components/wifi/wifi_component.h index ee8fd208b2..753455ddb1 100644 --- a/esphome/components/wifi/wifi_component.h +++ b/esphome/components/wifi/wifi_component.h @@ -27,6 +27,11 @@ extern "C" { namespace esphome { namespace wifi { +struct SavedWifiSettings { + char ssid[33]; + char password[65]; +} PACKED; // NOLINT + enum WiFiComponentState { /** Nothing has been initialized yet. Internal AP, if configured, is disabled at this point. */ WIFI_COMPONENT_STATE_OFF = 0, @@ -156,6 +161,7 @@ class WiFiComponent : public Component { void set_sta(const WiFiAP &ap); void add_sta(const WiFiAP &ap); + void clear_sta(); /** Setup an Access Point that should be created if no connection to a station can be made. * @@ -185,6 +191,7 @@ class WiFiComponent : public Component { void set_power_save_mode(WiFiPowerSaveMode power_save); void set_output_power(float output_power) { output_power_ = output_power; } + void save_wifi_sta(const std::string &ssid, const std::string &password); // ========== INTERNAL METHODS ========== // (In most use cases you won't need these) /// Setup WiFi interface. @@ -253,6 +260,7 @@ class WiFiComponent : public Component { bool wifi_disconnect_(); bool is_captive_portal_active_(); + bool is_esp32_improv_active_(); #ifdef ARDUINO_ARCH_ESP8266 static void wifi_event_callback(System_Event_t *event); @@ -288,6 +296,8 @@ class WiFiComponent : public Component { bool scan_done_{false}; bool ap_setup_{false}; optional output_power_; + ESPPreferenceObject pref_; + bool has_saved_wifi_settings_{false}; }; extern WiFiComponent *global_wifi_component; diff --git a/esphome/core/component.cpp b/esphome/core/component.cpp index d2e9607e28..c571e5e23f 100644 --- a/esphome/core/component.cpp +++ b/esphome/core/component.cpp @@ -15,6 +15,8 @@ const float IO = 900.0f; const float HARDWARE = 800.0f; const float DATA = 600.0f; const float PROCESSOR = 400.0; +const float BLUETOOTH = 350.0f; +const float AFTER_BLUETOOTH = 300.0f; const float WIFI = 250.0f; const float AFTER_WIFI = 200.0f; const float AFTER_CONNECTION = 100.0f; diff --git a/esphome/core/component.h b/esphome/core/component.h index e3f9a51f25..ad3619386b 100644 --- a/esphome/core/component.h +++ b/esphome/core/component.h @@ -26,6 +26,8 @@ extern const float DATA; extern const float HARDWARE_LATE; /// For components that use data from sensors like displays extern const float PROCESSOR; +extern const float BLUETOOTH; +extern const float AFTER_BLUETOOTH; extern const float WIFI; /// For components that should be initialized after WiFi is connected. extern const float AFTER_WIFI; diff --git a/esphome/core/helpers.h b/esphome/core/helpers.h index 5f9ab1fdd1..d4ee109126 100644 --- a/esphome/core/helpers.h +++ b/esphome/core/helpers.h @@ -21,8 +21,14 @@ #define ALWAYS_INLINE __attribute__((always_inline)) #define PACKED __attribute__((packed)) +#define xSemaphoreWait(semaphore, wait_time) \ + xSemaphoreTake(semaphore, wait_time); \ + xSemaphoreGive(semaphore); + namespace esphome { +static const uint32_t SEMAPHORE_MAX_DELAY = 4294967295UL; + /// The characters that are allowed in a hostname. extern const char *HOSTNAME_CHARACTER_ALLOWLIST; @@ -318,8 +324,6 @@ template class Parented { uint32_t fnv1_hash(const std::string &str); -} // namespace esphome - template T *new_buffer(size_t length) { T *buffer; #ifdef ARDUINO_ARCH_ESP32 @@ -333,4 +337,6 @@ template T *new_buffer(size_t length) { #endif return buffer; +} + } // namespace esphome diff --git a/script/test b/script/test index 0327e08ca8..9f5dca65fa 100755 --- a/script/test +++ b/script/test @@ -10,3 +10,4 @@ esphome compile tests/test1.yaml esphome compile tests/test2.yaml esphome compile tests/test3.yaml esphome compile tests/test4.yaml +esphome compile tests/test5.yaml diff --git a/tests/README.md b/tests/README.md index c8962dedc1..546025526f 100644 --- a/tests/README.md +++ b/tests/README.md @@ -17,9 +17,10 @@ be tested on the same device. Current test_.yaml file contents. -| Test name | Platform | Network | -|-|-|-| -| test1.yaml | ESP32 | wifi | -| test2.yaml | ESP32 | ethernet | -| test3.yaml | ESP8266 | wifi | -| test4.yaml | ESP32 | ethernet | +| Test name | Platform | Network | BLE | +|-|-|-|-| +| test1.yaml | ESP32 | wifi | None +| test2.yaml | ESP32 | ethernet | esp32_ble_tracker +| test3.yaml | ESP8266 | wifi | N/A +| test4.yaml | ESP32 | ethernet | None +| test5.yaml | ESP32 | wifi | ble_server diff --git a/tests/test5.yaml b/tests/test5.yaml new file mode 100644 index 0000000000..993c421bdd --- /dev/null +++ b/tests/test5.yaml @@ -0,0 +1,36 @@ +esphome: + name: test5 + platform: ESP32 + board: nodemcu-32s + build_path: build/test5 + +wifi: + networks: + - ssid: 'MySSID' + password: 'password1' + +api: + +ota: + +logger: + +binary_sensor: + - platform: gpio + pin: GPIO0 + id: io0_button + +output: + - platform: gpio + pin: GPIO2 + id: built_in_led + +esp32_ble: + server: + manufacturer: "ESPHome" + model: "Test5" + +esp32_improv: + authorizer: io0_button + authorized_duration: 1min + status_indicator: built_in_led