From 6dee644a125643e3b4eb2bfcf486b3ed993aa9e3 Mon Sep 17 00:00:00 2001 From: Jonas Rabenstein Date: Tue, 23 Dec 2025 10:03:07 +0100 Subject: [PATCH] working --- components/brmesh/__init__.py | 6 +- components/brmesh/brmesh.py | 121 +++-- components/brmesh/debug.h | 26 +- components/brmesh/fastcon/__init__.py | 3 - .../brmesh/fastcon/fastcon_controller.cpp | 269 ----------- .../brmesh/fastcon/fastcon_controller.h | 90 ---- .../brmesh/fastcon/fastcon_controller.py | 78 --- components/brmesh/fastcon/fastcon_light.cpp | 71 --- components/brmesh/fastcon/fastcon_light.h | 35 -- components/brmesh/fastcon/light.py | 37 -- components/brmesh/fastcon/protocol.h | 18 - components/brmesh/light.cpp | 444 +++--------------- components/brmesh/light.h | 30 +- components/brmesh/light.py | 77 +-- components/brmesh/network.cpp | 330 +++++++++++++ components/brmesh/network.h | 43 ++ components/brmesh/{fastcon => }/protocol.cpp | 10 +- components/brmesh/protocol.h | 20 +- components/brmesh/{fastcon => }/utils.cpp | 4 +- components/brmesh/{fastcon => }/utils.h | 4 +- components/brmesh/whitening.cpp | 15 +- 21 files changed, 606 insertions(+), 1125 deletions(-) delete mode 100644 components/brmesh/fastcon/__init__.py delete mode 100644 components/brmesh/fastcon/fastcon_controller.cpp delete mode 100644 components/brmesh/fastcon/fastcon_controller.h delete mode 100644 components/brmesh/fastcon/fastcon_controller.py delete mode 100644 components/brmesh/fastcon/fastcon_light.cpp delete mode 100644 components/brmesh/fastcon/fastcon_light.h delete mode 100644 components/brmesh/fastcon/light.py delete mode 100644 components/brmesh/fastcon/protocol.h create mode 100644 components/brmesh/network.cpp create mode 100644 components/brmesh/network.h rename components/brmesh/{fastcon => }/protocol.cpp (93%) rename components/brmesh/{fastcon => }/utils.cpp (98%) rename components/brmesh/{fastcon => }/utils.h (95%) diff --git a/components/brmesh/__init__.py b/components/brmesh/__init__.py index eac3d21..ae9493b 100644 --- a/components/brmesh/__init__.py +++ b/components/brmesh/__init__.py @@ -1,3 +1,5 @@ -#from .brmesh import CONFIG_SCHEMA, DEPENDENCIES, to_code +from .brmesh import CONFIG_SCHEMA, DEPENDENCIES, to_code -#__all__ = ["CONFIG_SCHEMA", "DEPENDENCIES", "to_code"] +AUTO_LOAD = [ "light" ] + +__all__ = ["CONFIG_SCHEMA", "DEPENDENCIES", "to_code"] diff --git a/components/brmesh/brmesh.py b/components/brmesh/brmesh.py index 0376382..0f5e386 100644 --- a/components/brmesh/brmesh.py +++ b/components/brmesh/brmesh.py @@ -1,67 +1,88 @@ import esphome.codegen as cg import esphome.config_validation as cv -from esphome.const import CONF_ID -from esphome.core import HexInt +from esphome.const import CONF_ID, CONF_TX_POWER +from esphome.core import HexInt, TimePeriod from esphome.components import esp32_ble -if False: - DEPENDENCIES = ["esp32_ble"] +DEPENDENCIES = ["esp32_ble"] + +CONF_KEY = "key" +CONF_ADV = "advertise" +CONF_INTERVAL = "interval" +CONF_MIN = "min" +CONF_MAX = "max" + +brmesh_ns = cg.esphome_ns.namespace("brmesh") +Network = brmesh_ns.class_("Network", cg.Component) + +def validate_key_string(value): + value = value.replace(" ", "") + if len(value) != 8: + raise cv.Invalid("Key must be exactly 4 bytes (8 hex characters)") + + try: + return [HexInt(int(value[x:x+2], 16)) for x in range(0, len(value), 2)] + except ValueError as err: + raise cv.Invalid(f"Invalid hex value: {err}") + + +def validate_key(value): + if isinstance(value, str): + value = validate_key_string(value) + + if not isinstance(value, list): + raise cv.Invalid("Key must be a list") + + if len(value) != 4: + raise cv.Invalid("Key must have 4 bytes") + + return [ cv.uint8_t(x) for x in value ] + +INTERVAL_RANGE_SCHEMA = cv.All( + cv.positive_time_period_milliseconds, + cv.Range( + min=TimePeriod(milliseconds=20), + max=TimePeriod(milliseconds=10240) + ), +) + +INTERVAL_SCHEMA = cv.Schema({ + cv.Optional(CONF_MIN, default="20ms"): INTERVAL_RANGE_SCHEMA, + cv.Optional(CONF_MAX, default="40ms"): INTERVAL_RANGE_SCHEMA, +}) + +ADV_SCHEMA = cv.Schema({ + cv.Optional(CONF_INTERVAL, default=INTERVAL_SCHEMA({})): INTERVAL_SCHEMA, + cv.Optional(CONF_TX_POWER, default="3dBm"): cv.All( + cv.decibel, cv.enum(esp32_ble.TX_POWER_LEVELS, int=True) + ), +}) - CONF_KEY = "key" - CONF_ADV = "adv" - CONF_INTERVAL = "interval" - CONF_MIN = "min" - CONF_MAX = "max" - CONF_DURATION = "duration" - CONF_GAP = "gap" - - - def validate_key_string(value): - value = value.replace(" ", "") - if len(value) != 8: - raise cv.Invalid("Key must be exactly 4 bytes (8 hex characters)") - - try: - return [HexInt(int(value[x:x+2], 16)) for x in range(0, len(value), 2)] - except ValueError as err: - raise cv.Invalid(f"Invalid hex value: {err}") - - - def validate_key(value): - if isinstance(value, str): - value = validate_key_string(value) - - if not isinstance(value, list): - raise cv.Invalid("Key must be a list") - - if len(value) != 4: - raise cv.Invalid("Key must have 4 bytes") - - return [ cv.uint8_t(x) for x in value ] - - - brmesh_ns = cg.esphome_ns.namespace("brmesh") - Controller = brmesh_ns.class_("Controller", cg.Component, esp32_ble.GAPEventHandler) - - - CONFIG_SCHEMA = cv.COMPONENT_SCHEMA.extend({ - cv.GenerateID(): cv.declare_id(Controller), +CONFIG_SCHEMA = cv.Schema([ + { + cv.GenerateID(): cv.declare_id(Network), cv.GenerateID(esp32_ble.CONF_BLE_ID): cv.use_id(esp32_ble.ESP32BLE), cv.Required(CONF_KEY): validate_key, - cv.Optional(CONF_ADV): ADV_SCHEMA, - }) + cv.Optional(CONF_ADV, default=ADV_SCHEMA({})): ADV_SCHEMA, + } +]) - - async def to_code(config): +async def to_code(config): + cg.add_define("USE_ESP32_BLE_ADVERTISING") + cg.add_define("USE_ESP32_BLE_UUID") + for config in config: adv = config[CONF_ADV] interval = adv[CONF_INTERVAL] - + if interval[CONF_MAX] < interval[CONF_MIN]: raise cv.Invalid( f"{CONF_ADV}.{CONF_INTERVAL}.{CONF_MIN} ({interval[CONF_MIN]}) must be <= " f"{CONF_ADV}.{CONF_INTERVLA}.{CONF_MAX} ({interval[CONF_MAX]})" ) ble = await cg.get_variable(config[esp32_ble.CONF_BLE_ID]) - - var = cg.new_Pvariable(config[CONF_ID], ble, config[CONF_KEY], interval[CONF_MIN], interval[CONF_MAX], adv[CONF_DURATION].total_milliseconds, adv[CONF_GAP].total_milliseconds) + + var = cg.new_Pvariable(config[CONF_ID], config[CONF_KEY], + interval[CONF_MIN], interval[CONF_MAX], + adv[CONF_TX_POWER]); + esp32_ble.register_gap_event_handler(ble, var) await cg.register_component(var, config) diff --git a/components/brmesh/debug.h b/components/brmesh/debug.h index 9e6e4d6..6b7ab59 100644 --- a/components/brmesh/debug.h +++ b/components/brmesh/debug.h @@ -1,8 +1,13 @@ #pragma once - +#if 0 #include "esphome/core/log.h" +#include "esphome/core/defines.h" #include "esphome/core/helpers.h" +#ifdef USE_LOGGER +#include "esphome/components/logger/logger.h" +#endif + #include namespace esphome { @@ -10,22 +15,29 @@ namespace brmesh { namespace { template -void hexdump(const char *TAG, const uint8_t *data, size_t size, const char *fmt, types&&...args) { +void hexdump(const unsigned level, const char *tag, unsigned line, const uint8_t *data, size_t size, const char *fmt, types&&...args) { // TODO: conditional format_hex_pretty const auto hex = format_hex_pretty(data, size); - ESP_LOGD(TAG, fmt, std::forward(args)..., hex.c_str()); + ESP_LOGD(tag, fmt, std::forward(args)..., hex.c_str()); } template -void hexdump(const char *TAG, const void *data, size_t size, const char *fmt, types&&...args) { - hexdump(TAG, static_cast(data), size, fmt, std::forward(args)...); +void hexdump(const unsigned level, const char *tag, const void *data, size_t size, const char *fmt, types&&...args) { + hexdump(level, tag, static_cast(data), size, fmt, std::forward(args)...); } template -void hexdump(const char *TAG, const std::span &span, const char *fmt, types&&...args) { - hexdump(TAG, static_cast(&span.front()), span.size_bytes(), fmt, std::forward(args)...); +void hexdump(const unsigned level, const char *tag, const std::span &span, const char *fmt, types&&...args) { + hexdump(level, tag, static_cast(&span.front()), span.size_bytes(), fmt, std::forward(args)...); +} + +template +void hexdump(const char *tag, types&&...args) +{ + return hexdump(ESPHOME_LOG_LEVEL_DEBUG, tag, std::forward(args)...); } } // namespace } // namespace brmesh } // namespace esphome +#endif diff --git a/components/brmesh/fastcon/__init__.py b/components/brmesh/fastcon/__init__.py deleted file mode 100644 index 9b07c9a..0000000 --- a/components/brmesh/fastcon/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -from .fastcon_controller import CONFIG_SCHEMA, DEPENDENCIES, FastconController, to_code - -__all__ = ["CONFIG_SCHEMA", "DEPENDENCIES", "FastconController", "to_code"] diff --git a/components/brmesh/fastcon/fastcon_controller.cpp b/components/brmesh/fastcon/fastcon_controller.cpp deleted file mode 100644 index 993c079..0000000 --- a/components/brmesh/fastcon/fastcon_controller.cpp +++ /dev/null @@ -1,269 +0,0 @@ -#include "esphome/core/component_iterator.h" -#include "esphome/core/log.h" -#include "esphome/components/light/color_mode.h" -#include "fastcon_controller.h" -#include "protocol.h" - -namespace esphome -{ - namespace fastcon - { - static const char *const TAG = "fastcon.controller"; - - void FastconController::queueCommand(uint32_t light_id_, const std::vector &data) - { - std::lock_guard lock(queue_mutex_); - if (queue_.size() >= max_queue_size_) - { - ESP_LOGW(TAG, "Command queue full (size=%d), dropping command for light %d", - queue_.size(), light_id_); - return; - } - - Command cmd; - cmd.data = data; - cmd.timestamp = millis(); - cmd.retries = 0; - - queue_.push(cmd); - ESP_LOGV(TAG, "Command queued, queue size: %d", queue_.size()); - } - - void FastconController::clear_queue() - { - std::lock_guard lock(queue_mutex_); - std::queue empty; - std::swap(queue_, empty); - } - - void FastconController::setup() - { - ESP_LOGCONFIG(TAG, "Setting up Fastcon BLE Controller..."); - ESP_LOGCONFIG(TAG, " Advertisement interval: %d-%d", this->adv_interval_min_, this->adv_interval_max_); - ESP_LOGCONFIG(TAG, " Advertisement duration: %dms", this->adv_duration_); - ESP_LOGCONFIG(TAG, " Advertisement gap: %dms", this->adv_gap_); - } - - void FastconController::loop() - { - const uint32_t now = millis(); - - switch (adv_state_) - { - case AdvertiseState::IDLE: - { - std::lock_guard lock(queue_mutex_); - if (queue_.empty()) - return; - - Command cmd = queue_.front(); - queue_.pop(); - - esp_ble_adv_params_t adv_params = { - .adv_int_min = adv_interval_min_, - .adv_int_max = adv_interval_max_, - .adv_type = ADV_TYPE_NONCONN_IND, - .own_addr_type = BLE_ADDR_TYPE_PUBLIC, - .peer_addr = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, - .peer_addr_type = BLE_ADDR_TYPE_PUBLIC, - .channel_map = ADV_CHNL_ALL, - .adv_filter_policy = ADV_FILTER_ALLOW_SCAN_ANY_CON_ANY, - }; - - uint8_t adv_data_raw[31] = {0}; - uint8_t adv_data_len = 0; - - // Add flags - adv_data_raw[adv_data_len++] = 2; - adv_data_raw[adv_data_len++] = ESP_BLE_AD_TYPE_FLAG; - adv_data_raw[adv_data_len++] = ESP_BLE_ADV_FLAG_BREDR_NOT_SPT | ESP_BLE_ADV_FLAG_GEN_DISC; - - // Add manufacturer data - adv_data_raw[adv_data_len++] = cmd.data.size() + 2; - adv_data_raw[adv_data_len++] = ESP_BLE_AD_MANUFACTURER_SPECIFIC_TYPE; - adv_data_raw[adv_data_len++] = MANUFACTURER_DATA_ID & 0xFF; - adv_data_raw[adv_data_len++] = (MANUFACTURER_DATA_ID >> 8) & 0xFF; - - memcpy(&adv_data_raw[adv_data_len], cmd.data.data(), cmd.data.size()); - adv_data_len += cmd.data.size(); - - esp_err_t err = esp_ble_gap_config_adv_data_raw(adv_data_raw, adv_data_len); - if (err != ESP_OK) - { - ESP_LOGW(TAG, "Error setting raw advertisement data (err=%d): %s", err, esp_err_to_name(err)); - return; - } - - err = esp_ble_gap_start_advertising(&adv_params); - if (err != ESP_OK) - { - ESP_LOGW(TAG, "Error starting advertisement (err=%d): %s", err, esp_err_to_name(err)); - return; - } - - adv_state_ = AdvertiseState::ADVERTISING; - state_start_time_ = now; - ESP_LOGV(TAG, "Started advertising"); - break; - } - - case AdvertiseState::ADVERTISING: - { - if (now - state_start_time_ >= adv_duration_) - { - esp_ble_gap_stop_advertising(); - adv_state_ = AdvertiseState::GAP; - state_start_time_ = now; - ESP_LOGV(TAG, "Stopped advertising, entering gap period"); - } - break; - } - - case AdvertiseState::GAP: - { - if (now - state_start_time_ >= adv_gap_) - { - adv_state_ = AdvertiseState::IDLE; - ESP_LOGV(TAG, "Gap period complete"); - } - break; - } - } - } - - std::vector FastconController::get_light_data(light::LightState *state) - { - std::vector light_data = { - 0, // 0 - On/Off Bit + 7-bit Brightness - 0, // 1 - Blue byte - 0, // 2 - Red byte - 0, // 3 - Green byte - 0, // 4 - Warm byte - 0 // 5 - Cold byte - }; - - // TODO: need to figure out when esphome is changing to white vs setting brightness - - auto values = state->current_values; - - bool is_on = values.is_on(); - if (!is_on) - { - return std::vector({0x00}); - } - - auto color_mode = values.get_color_mode(); - bool has_white = (static_cast(color_mode) & static_cast(light::ColorCapability::WHITE)) != 0; - float brightness = std::min(values.get_brightness() * 127.0f, 127.0f); // clamp the value to at most 127 - light_data[0] = 0x80 + static_cast(brightness); - - if (has_white) - { - return std::vector({static_cast(brightness)}); - // DEBUG: when changing to white mode, this should be the payload: - // ff0000007f7f - } - - bool has_rgb = (static_cast(color_mode) & static_cast(light::ColorCapability::RGB)) != 0; - if (has_rgb) - { - light_data[1] = static_cast(values.get_blue() * 255.0f); - light_data[2] = static_cast(values.get_red() * 255.0f); - light_data[3] = static_cast(values.get_green() * 255.0f); - } - - bool has_cold_warm = (static_cast(color_mode) & static_cast(light::ColorCapability::COLD_WARM_WHITE)) != 0; - if (has_cold_warm) - { - light_data[4] = static_cast(values.get_warm_white() * 255.0f); - light_data[5] = static_cast(values.get_cold_white() * 255.0f); - } - - // TODO figure out if we can use these, and how - bool has_temp = (static_cast(color_mode) & static_cast(light::ColorCapability::COLOR_TEMPERATURE)) != 0; - if (has_temp) - { - float temperature = values.get_color_temperature(); - if (temperature < 153) - { - light_data[4] = 0xff; - light_data[5] = 0x00; - } - else if (temperature > 500) - { - light_data[4] = 0x00; - light_data[5] = 0xff; - } - else - { - // Linear interpolation between (153, 0xff) and (500, 0x00) - light_data[4] = (uint8_t)(((500 - temperature) * 255.0f + (temperature - 153) * 0x00) / (500 - 153)); - light_data[5] = (uint8_t)(((temperature - 153) * 255.0f + (500 - temperature) * 0x00) / (500 - 153)); - } - } - - return light_data; - } - - std::vector FastconController::single_control(uint32_t light_id_, const std::vector &light_data) - { - std::vector result_data(12); - - result_data[0] = 2 | (((0xfffffff & (light_data.size() + 1)) << 4)); - result_data[1] = light_id_; - std::copy(light_data.begin(), light_data.end(), result_data.begin() + 2); - - // Debug output - print payload as hex - auto hex_str = vector_to_hex_string(result_data).data(); - ESP_LOGD(TAG, "Inner Payload (%d bytes): %s", result_data.size(), hex_str); - - return this->generate_command(5, light_id_, result_data, true); - } - - std::vector FastconController::generate_command(uint8_t n, uint32_t light_id_, const std::vector &data, bool forward) - { - static uint8_t sequence = 0; - - // Create command body with header - std::vector body(data.size() + 4); - uint8_t i2 = (light_id_ / 256); - - // Construct header - body[0] = (i2 & 0b1111) | ((n & 0b111) << 4) | (forward ? 0x80 : 0); - body[1] = sequence++; // Use and increment sequence number - if (sequence >= 255) - sequence = 1; - - body[2] = this->mesh_key_[3]; // Safe key - - // Copy data - std::copy(data.begin(), data.end(), body.begin() + 4); - - // Calculate checksum - uint8_t checksum = 0; - for (size_t i = 0; i < body.size(); i++) - { - if (i != 3) - { - checksum = checksum + body[i]; - } - } - body[3] = checksum; - - // Encrypt header and data - for (size_t i = 0; i < 4; i++) - { - body[i] = DEFAULT_ENCRYPT_KEY[i & 3] ^ body[i]; - } - - for (size_t i = 0; i < data.size(); i++) - { - body[4 + i] = this->mesh_key_[i & 3] ^ body[4 + i]; - } - - // Prepare the final payload with RF protocol formatting - std::vector addr = {DEFAULT_BLE_FASTCON_ADDRESS.begin(), DEFAULT_BLE_FASTCON_ADDRESS.end()}; - return prepare_payload(addr, body); - } - } // namespace fastcon -} // namespace esphome diff --git a/components/brmesh/fastcon/fastcon_controller.h b/components/brmesh/fastcon/fastcon_controller.h deleted file mode 100644 index cdc7808..0000000 --- a/components/brmesh/fastcon/fastcon_controller.h +++ /dev/null @@ -1,90 +0,0 @@ -#pragma once - -#include -#include -#include -#include "esphome/core/component.h" -#include "esphome/components/esp32_ble_server/ble_server.h" - -namespace esphome -{ - namespace fastcon - { - - class FastconController : public Component - { - public: - FastconController() = default; - - void setup() override; - void loop() override; - - std::vector get_light_data(light::LightState *state); - std::vector single_control(uint32_t addr, const std::vector &light_data); - - void queueCommand(uint32_t light_id_, const std::vector &data); - - void clear_queue(); - bool is_queue_empty() const - { - std::lock_guard lock(queue_mutex_); - return queue_.empty(); - } - size_t get_queue_size() const - { - std::lock_guard lock(queue_mutex_); - return queue_.size(); - } - void set_max_queue_size(size_t size) { max_queue_size_ = size; } - - void set_mesh_key(std::array key) { mesh_key_ = key; } - void set_adv_interval_min(uint16_t val) { adv_interval_min_ = val; } - void set_adv_interval_max(uint16_t val) - { - adv_interval_max_ = val; - if (adv_interval_max_ < adv_interval_min_) - { - adv_interval_max_ = adv_interval_min_; - } - } - void set_adv_duration(uint16_t val) { adv_duration_ = val; } - void set_adv_gap(uint16_t val) { adv_gap_ = val; } - - protected: - struct Command - { - std::vector data; - uint32_t timestamp; - uint8_t retries{0}; - static constexpr uint8_t MAX_RETRIES = 3; - }; - - std::queue queue_; - mutable std::mutex queue_mutex_; - size_t max_queue_size_{100}; - - enum class AdvertiseState - { - IDLE, - ADVERTISING, - GAP - }; - - AdvertiseState adv_state_{AdvertiseState::IDLE}; - uint32_t state_start_time_{0}; - - // Protocol implementation - std::vector generate_command(uint8_t n, uint32_t light_id_, const std::vector &data, bool forward = true); - - std::array mesh_key_{}; - - uint16_t adv_interval_min_{0x20}; - uint16_t adv_interval_max_{0x40}; - uint16_t adv_duration_{50}; - uint16_t adv_gap_{10}; - - static const uint16_t MANUFACTURER_DATA_ID = 0xfff0; - }; - - } // namespace fastcon -} // namespace esphome diff --git a/components/brmesh/fastcon/fastcon_controller.py b/components/brmesh/fastcon/fastcon_controller.py deleted file mode 100644 index a3361e1..0000000 --- a/components/brmesh/fastcon/fastcon_controller.py +++ /dev/null @@ -1,78 +0,0 @@ -import esphome.codegen as cg -import esphome.config_validation as cv -from esphome.const import CONF_ID -from esphome.core import HexInt - -DEPENDENCIES = ["esp32_ble"] - -CONF_MESH_KEY = "mesh_key" -CONF_ADV_INTERVAL_MIN = "adv_interval_min" -CONF_ADV_INTERVAL_MAX = "adv_interval_max" -CONF_ADV_DURATION = "adv_duration" -CONF_ADV_GAP = "adv_gap" -CONF_MAX_QUEUE_SIZE = "max_queue_size" - -DEFAULT_ADV_INTERVAL_MIN = 0x20 -DEFAULT_ADV_INTERVAL_MAX = 0x40 -DEFAULT_ADV_DURATION = 50 -DEFAULT_ADV_GAP = 10 -DEFAULT_MAX_QUEUE_SIZE = 100 - - -def validate_hex_bytes(value): - if isinstance(value, str): - value = value.replace(" ", "") - if len(value) != 8: - raise cv.Invalid("Mesh key must be exactly 4 bytes (8 hex characters)") - - try: - return HexInt(int(value, 16)) - except ValueError as err: - raise cv.Invalid(f"Invalid hex value: {err}") - raise cv.Invalid("Mesh key must be a string") - - -fastcon_ns = cg.esphome_ns.namespace("fastcon") -FastconController = fastcon_ns.class_("FastconController", cg.Component) - -CONFIG_SCHEMA = cv.Schema( - { - cv.Optional(CONF_ID, default="fastcon_controller"): cv.declare_id( - FastconController - ), - cv.Required(CONF_MESH_KEY): validate_hex_bytes, - cv.Optional( - CONF_ADV_INTERVAL_MIN, default=DEFAULT_ADV_INTERVAL_MIN - ): cv.uint16_t, - cv.Optional( - CONF_ADV_INTERVAL_MAX, default=DEFAULT_ADV_INTERVAL_MAX - ): cv.uint16_t, - cv.Optional(CONF_ADV_DURATION, default=DEFAULT_ADV_DURATION): cv.uint16_t, - cv.Optional(CONF_ADV_GAP, default=DEFAULT_ADV_GAP): cv.uint16_t, - cv.Optional( - CONF_MAX_QUEUE_SIZE, default=DEFAULT_MAX_QUEUE_SIZE - ): cv.positive_int, - } -).extend(cv.COMPONENT_SCHEMA) - - -async def to_code(config): - var = cg.new_Pvariable(config[CONF_ID]) - await cg.register_component(var, config) - - if CONF_MESH_KEY in config: - mesh_key = config[CONF_MESH_KEY] - key_bytes = [(mesh_key >> (i * 8)) & 0xFF for i in range(3, -1, -1)] - cg.add(var.set_mesh_key(key_bytes)) - - if config[CONF_ADV_INTERVAL_MAX] < config[CONF_ADV_INTERVAL_MIN]: - raise cv.Invalid( - f"adv_interval_max ({config[CONF_ADV_INTERVAL_MAX]}) must be >= " - f"adv_interval_min ({config[CONF_ADV_INTERVAL_MIN]})" - ) - - cg.add(var.set_adv_interval_min(config[CONF_ADV_INTERVAL_MIN])) - cg.add(var.set_adv_interval_max(config[CONF_ADV_INTERVAL_MAX])) - cg.add(var.set_adv_duration(config[CONF_ADV_DURATION])) - cg.add(var.set_adv_gap(config[CONF_ADV_GAP])) - cg.add(var.set_max_queue_size(config[CONF_MAX_QUEUE_SIZE])) diff --git a/components/brmesh/fastcon/fastcon_light.cpp b/components/brmesh/fastcon/fastcon_light.cpp deleted file mode 100644 index af7a983..0000000 --- a/components/brmesh/fastcon/fastcon_light.cpp +++ /dev/null @@ -1,71 +0,0 @@ -#include -#include "esphome/core/log.h" -#include "fastcon_light.h" -#include "fastcon_controller.h" -#include "utils.h" - -namespace esphome -{ - namespace fastcon - { - static const char *const TAG = "fastcon.light"; - - void FastconLight::setup() - { - if (this->controller_ == nullptr) - { - ESP_LOGE(TAG, "Controller not set for light %d!", this->light_id_); - this->mark_failed(); - return; - } - ESP_LOGCONFIG(TAG, "Setting up Fastcon BLE light (ID: %d)...", this->light_id_); - } - - void FastconLight::set_controller(FastconController *controller) - { - this->controller_ = controller; - } - - light::LightTraits FastconLight::get_traits() - { - auto traits = light::LightTraits(); - traits.set_supported_color_modes({light::ColorMode::RGB, light::ColorMode::WHITE, light::ColorMode::BRIGHTNESS, light::ColorMode::COLD_WARM_WHITE}); - traits.set_min_mireds(153); - traits.set_max_mireds(500); - return traits; - } - - void FastconLight::write_state(light::LightState *state) - { - // Get the light data bits from the state - auto light_data = this->controller_->get_light_data(state); - - // Debug output - print the light state values - bool is_on = (light_data[0] & 0x80) != 0; - float brightness = ((light_data[0] & 0x7F) / 127.0f) * 100.0f; - if (light_data.size() == 1) - { - ESP_LOGD(TAG, "Writing state: light_id=%d, on=%d, brightness=%.1f%%", light_id_, is_on, brightness); - } - else - { - auto r = light_data[2]; - auto g = light_data[3]; - auto b = light_data[1]; - auto warm = light_data[4]; - auto cold = light_data[5]; - ESP_LOGD(TAG, "Writing state: light_id=%d, on=%d, brightness=%.1f%%, rgb=(%d,%d,%d), warm=%d, cold=%d", light_id_, is_on, brightness, r, g, b, warm, cold); - } - - // Generate the advertisement payload - auto adv_data = this->controller_->single_control(this->light_id_, light_data); - - // Debug output - print payload as hex - auto hex_str = vector_to_hex_string(adv_data).data(); - ESP_LOGD(TAG, "Advertisement Payload (%d bytes): %s", adv_data.size(), hex_str); - - // Send the advertisement - this->controller_->queueCommand(this->light_id_, adv_data); - } - } // namespace fastcon -} // namespace esphome diff --git a/components/brmesh/fastcon/fastcon_light.h b/components/brmesh/fastcon/fastcon_light.h deleted file mode 100644 index bf583e2..0000000 --- a/components/brmesh/fastcon/fastcon_light.h +++ /dev/null @@ -1,35 +0,0 @@ -#pragma once - -#include -#include -#include "esphome/core/component.h" -#include "esphome/components/light/light_output.h" -#include "fastcon_controller.h" - -namespace esphome -{ - namespace fastcon - { - enum class LightState - { - OFF, - WARM_WHITE, - RGB - }; - - class FastconLight : public Component, public light::LightOutput - { - public: - FastconLight(uint8_t light_id) : light_id_(light_id) {} - - void setup() override; - light::LightTraits get_traits() override; - void write_state(light::LightState *state) override; - void set_controller(FastconController *controller); - - protected: - FastconController *controller_{nullptr}; - uint8_t light_id_; - }; - } // namespace fastcon -} // namespace esphome diff --git a/components/brmesh/fastcon/light.py b/components/brmesh/fastcon/light.py deleted file mode 100644 index 3ff7da4..0000000 --- a/components/brmesh/fastcon/light.py +++ /dev/null @@ -1,37 +0,0 @@ -"""Light platform for Fastcon BLE lights.""" - -import esphome.codegen as cg -import esphome.config_validation as cv -from esphome.components import light -from esphome.const import CONF_LIGHT_ID, CONF_OUTPUT_ID - -from .fastcon_controller import FastconController - -DEPENDENCIES = ["esp32_ble"] -AUTO_LOAD = ["light"] - -CONF_CONTROLLER_ID = "controller_id" - -fastcon_ns = cg.esphome_ns.namespace("fastcon") -FastconLight = fastcon_ns.class_("FastconLight", light.LightOutput, cg.Component) - -CONFIG_SCHEMA = cv.All( - light.BRIGHTNESS_ONLY_LIGHT_SCHEMA.extend( - { - cv.GenerateID(CONF_OUTPUT_ID): cv.declare_id(FastconLight), - cv.Required(CONF_LIGHT_ID): cv.int_range(min=1, max=255), - cv.Optional(CONF_CONTROLLER_ID, default="fastcon_controller"): cv.use_id( - FastconController - ), - } - ).extend(cv.COMPONENT_SCHEMA) -) - - -async def to_code(config): - var = cg.new_Pvariable(config[CONF_OUTPUT_ID], config[CONF_LIGHT_ID]) - await cg.register_component(var, config) - await light.register_light(var, config) - - controller = await cg.get_variable(config[CONF_CONTROLLER_ID]) - cg.add(var.set_controller(controller)) diff --git a/components/brmesh/fastcon/protocol.h b/components/brmesh/fastcon/protocol.h deleted file mode 100644 index 930acab..0000000 --- a/components/brmesh/fastcon/protocol.h +++ /dev/null @@ -1,18 +0,0 @@ -#pragma once - -#include -#include -#include -#include "utils.h" - -namespace esphome -{ - namespace fastcon - { - static const std::array DEFAULT_ENCRYPT_KEY = {0x5e, 0x36, 0x7b, 0xc4}; - static const std::array DEFAULT_BLE_FASTCON_ADDRESS = {0xC1, 0xC2, 0xC3}; - - std::vector get_rf_payload(const std::vector &addr, const std::vector &data); - std::vector prepare_payload(const std::vector &addr, const std::vector &data); - } // namespace fastcon -} // namespace esphome \ No newline at end of file diff --git a/components/brmesh/light.cpp b/components/brmesh/light.cpp index ad15a1e..23eb261 100644 --- a/components/brmesh/light.cpp +++ b/components/brmesh/light.cpp @@ -1,13 +1,13 @@ #include "light.h" -#include "whitening.h" -#include "protocol.h" + #include "debug.h" +#include "network.h" +#include "whitening.h" #include "esphome/core/log.h" namespace { const char *const TAG = "brmesh.light"; - } // namespace namespace esphome { @@ -24,22 +24,39 @@ uint8_t crc(const std::span &bytes, uint8_t init = 0x00) { uint16_t crc16(uint8_t byte, uint16_t crc) { crc ^= static_cast(byte) << 8; - for (int j=0; j<4; ++j) { - uint16_t tmp = crc << 1; - if (crc & 0x8000) - tmp ^= 0x1021; - crc = tmp << 1; - if (tmp & 0x8000) - crc ^= 0x1021; - } - return crc; + + auto a = [](uint16_t crc) { + for (int j=0; j<4; ++j) { + uint16_t tmp = crc << 1; + if (crc & 0x8000) + tmp ^= 0x1021; + crc = tmp << 1; + if (tmp & 0x8000) + crc ^= 0x1021; + } + return crc; + }(crc); + auto b = [](uint16_t crc) { + for (int i = 0; i < 8; i++) { + if (crc & 0x8000) + crc = (crc << 1) ^ 0x1021; + else + crc <<= 1; + } + return crc; + }(crc); + + if (a != b) + ESP_LOGE(TAG, "crc16 missmatch: %04hX*%02hhX => %04hX|%04hX", + crc, byte, a, b); + return a; } uint16_t crc16(const std::array &addr, const std::span &data, const uint16_t seed = 0xFFFF) { auto crc = seed; - for (const auto &byte:addr) - crc = crc16(byte, crc); + for (auto it = addr.rbegin(); it != addr.rend(); ++it) + crc = crc16(*it, crc); for (const auto &byte:data) crc = crc16(reverse_bits(byte), crc); @@ -47,11 +64,6 @@ uint16_t crc16(const std::array &addr, const std::span &dat return ~reverse_bits(crc); } -template -std::span subspan(const std::span &buffer, size_t offset) { - return std::span(buffer.subspan(offset, size)); -} - std::span single_light_data(const std::span &buffer, const light::LightState &state) { const auto &values = state.current_values; // 0 - On/Off Bit + 7-bit Brightness @@ -126,139 +138,26 @@ std::span single_light_data(const std::span &buffer, const } -std::span command(const std::span &buffer, const Key &key, Light::Id id, uint8_t seq, light::LightState &state) { - static constexpr uint8_t n = 5; // TODO - static constexpr bool forward = true; // TODO - const uint8_t i2 = (id.value() / 256); - - if (id.kind() != 'L') - return {}; - - const auto header = buffer.subspan(0, 4); - const auto body = single_light_data(subspan<6>(buffer, 4), state); - - hexdump(TAG, body, "%c%02hhX: body: %s", id.kind(), id.value()); - - header[0] = (i2 & 0b1111) | ((n & 0b111) << 4) | (forward ? 0x80 : 0); - header[1] = seq; - header[2] = key[3]; - header[3] = crc(body); - - const auto result = buffer.subspan(0, 4 + body.size()); - - hexdump(TAG, result, "%c%02hhX: plain: %s", id.kind(), id.value()); - - DEFAULT_ENCRYPT_KEY(header); - key(body); - - hexdump(TAG, result, "%c%02hhX: cipher: %s", id.kind(), id.value()); - - return result; +std::span +group_light_data(const std::span &buffer, const light::LightState &state) { + ESP_LOGE(TAG, "not implemented: group light data"); + return {}; } -std::span payload(const std::span &buffer, const Key &key, Light::Id id, uint8_t seq, light::LightState &state) { - // static values - buffer[0] = 0x71; - buffer[1] = 0x0F; - buffer[2] = 0x55; - - const auto address = buffer.subspan(3, 3); - std::reverse_copy(DEFAULT_BLE_ADDRESS.cbegin(), DEFAULT_BLE_ADDRESS.cend(), address.begin()); - - const auto command = brmesh::command(subspan<16>(buffer, 4), key, id, seq, state); - hexdump(TAG, command, "L%hhd: command: %s", id); - - const auto result = buffer.subspan(0, 4 + command.size() + 2); - - uint16_t crc_value = crc16(DEFAULT_BLE_ADDRESS, command); - const auto crc = buffer.subspan(4+command.size(), 2); - crc[0] = crc_value; - crc[1] = crc_value >> 8; - - for (auto &byte:buffer.subspan(0, 6)) - byte = reverse_bits(byte); - - hexdump(TAG, result, "L%hhd: raw: %s", id); - - // TODO: shift seed to drop warmup - Whitening whitening(0x25); - for (size_t i=0; i<0x10; ++i) - whitening.encode(0); - - return whitening.encode(result); +std::span light_data(Light::Single, const std::span &buffer, light::LightState &state) { + return single_light_data(buffer, state); } -std::span adv_data(const std::span &buffer, const Key &key, Light::Id id, uint8_t seq, light::LightState &state) { - const auto flags = buffer.subspan(0, 3); - const auto length = buffer.subspan(3, 1); - const auto manufacturer = buffer.subspan(4, 3); - const auto payload = brmesh::payload(std::span(buffer.subspan(7)), key, id, seq, state); - - hexdump(TAG, payload, "L%hhd: payload: %s", id); - - flags[0] = 2; - flags[1] = ESP_BLE_AD_TYPE_FLAG; - flags[2] = ESP_BLE_ADV_FLAG_BREDR_NOT_SPT | ESP_BLE_ADV_FLAG_GEN_DISC; - - length[0] = payload.size(); - - manufacturer[0] = ESP_BLE_AD_MANUFACTURER_SPECIFIC_TYPE; - manufacturer[1] = 0xF0; - manufacturer[2] = 0xFF; - - return buffer.subspan(0, 7 + payload.size()); +std::span light_data(Light::Group, const std::span &buffer, light::LightState &state) { + return group_light_data(buffer, state); } } // namespace -void -Light::adv_event_(esp_ble_gap_cb_param_t::ble_adv_data_raw_cmpl_evt_param &) { - esp_ble_adv_params_t adv_params = { - .adv_int_min = adv_interval_min_, - .adv_int_max = adv_interval_max_, - .adv_type = ADV_TYPE_NONCONN_IND, - .own_addr_type = BLE_ADDR_TYPE_PUBLIC, - .peer_addr = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, - .peer_addr_type = BLE_ADDR_TYPE_PUBLIC, - .channel_map = ADV_CHNL_ALL, - .adv_filter_policy = ADV_FILTER_ALLOW_SCAN_ANY_CON_ANY, - }; - - const auto err = esp_ble_gap_start_advertising(&adv_params); - if (err != ESP_OK) - ESP_LOGE(TAG, "%c%02hhX: start advertising: %s", id_.kind(), id_.value(), esp_err_to_name(err)); - else - ESP_LOGD(TAG, "%c%02hhX: start advertising %hhd", id_.kind(), id_.value(), count_); -} - -void -Light::adv_event_(esp_ble_gap_cb_param_t::ble_adv_start_cmpl_evt_param ¶m) { - if (param.status != ESP_BT_STATUS_SUCCESS) - // TODO: is param.status a valid esp_err_to_name parameter? - ESP_LOGE(TAG, "adv start: %s", esp_err_to_name(param.status)); - else - ESP_LOGD(TAG, "%c%02hhX: advertise started", id_.kind(), id_.value()); -} - -void -Light::adv_event_(esp_ble_gap_cb_param_t::ble_adv_stop_cmpl_evt_param ¶m) { - if (param.status != ESP_BT_STATUS_SUCCESS) - // TODO: is param.status a valid esp_err_to_name parameter? - ESP_LOGE(TAG, "%c%02hhX: adv stop: %s", id_.kind(), id_.value(), esp_err_to_name(param.status)); - else - ESP_LOGD(TAG, "%c%02hhX: advertise stop", id_.kind(), id_.value()); -} - // interface: esphome::Component void Light::dump_config() { - auto *ble = esphome::esp32_ble::global_ble; - ESP_LOGCONFIG(TAG, - "light %hhd\n" - " key: %02hhx %02hhx %02hhx %02hhx\n" - " seq: %02hhx\n" - " ble: %p", - id_, key_[0], key_[1], key_[2], key_[3], seq_, ble); + ESP_LOGCONFIG(TAG, "light %c%hhd", id_.kind(), id_.value()); } void @@ -271,55 +170,44 @@ Light::setup() { } ble->advertising_register_raw_advertisement_callback([this](bool advertise) { - const bool advertising = advertising_; - ESP_LOGD(TAG, "%c%02hhX: adv callback %s", id_.kind(), id_.value(), advertise ? "on" : "off"); - - if (advertise == advertising) - return; - - advertising_ = advertise; - if (!advertise) - return; - - enable_loop(); + this->advertise(advertise); }); } +void +Light::advertise(bool advertise) { + const bool advertising = advertising_; + ESP_LOGV(TAG, "%c%02hhX: adv callback %s", id_.kind(), id_.value(), advertise ? "on" : "off"); + + if (advertise == advertising) + return; + + advertising_ = advertise; + if (advertise) + enable_loop(); + else + network_.advertise(id_, {}); +} + void Light::loop() { disable_loop(); if (!advertising_) { - ESP_LOGE(TAG, "%c%02hhX: loop while not advertising", id_.kind(), id_.value()); + ESP_LOGD(TAG, "%c%02hhX: not advertising", id_.kind(), id_.value()); return; } - if (state_ == nullptr) { - ESP_LOGD(TAG, "%c%02hhX: sleep", id_.kind(), id_.value()); + if (payload_length_ == 0 || count_ >= 5) { + ESP_LOGD(TAG, "%c%02hhX: no update", id_.kind(), id_.value()); return; } ++count_; - auto *const state = state_; - - const auto seq = seq_++; - std::array buffer; - - auto data = std::visit([buffer=std::span(buffer), this, seq, state](auto &&id) { - return adv_data(buffer, key_, id, seq, *state); - }, id_); - hexdump(TAG, data, "%c%02hhX: advertise: %s", id_.kind(), id_.value()); - - if (data.size() == 0) - return; - - auto err = esp_ble_tx_power_set(ESP_BLE_PWR_TYPE_ADV, tx_power_); - if (err != ESP_OK) - ESP_LOGW(TAG, "%c%02hhX: tx power set: %s", id_.kind(), id_.value(), esp_err_to_name(err)); - - err = esp_ble_gap_config_adv_data_raw(&data.front(), data.size()); - if (err != ESP_OK) - ESP_LOGE(TAG, "%c%02hhX: config adv: %s", id_.kind(), id_.value(), esp_err_to_name(err)); + auto payload = std::span(&payload_[0], payload_length_); + ESP_LOGV(TAG, "%c%02hhX: advertise: %s", id_.kind(), id_.value(), + format_hex_pretty(&payload[0], payload.size()).c_str()); + network_.advertise(id_, payload); } // interface: esphome::light::LightOutput @@ -339,211 +227,29 @@ Light::get_traits() { void Light::setup_state(light::LightState *state) { + // nothing to do + (void) state; } void Light::update_state(light::LightState *state) { + // nothing to do + (void) state; } -#include "./fastcon/fastcon_controller.h" - void Light::write_state(light::LightState *state) { - ESP_LOGV(TAG, "%c%02hhX: state %02hx", id_.kind(), id_.value(), state->current_values.get_color_mode()); + //ESP_LOGV(TAG, "%c%02hhX: state %02hx", id_.kind(), id_.value(), state->current_values.get_color_mode()); - payload_length_ = single_light_data(std::span(payload_), *state).size(); - hexdump(TAG, &payload_.front(), payload_length_, "%c%02hhX: state: %s", id_.kind(), id_.value()); - state_ = state; + const auto payload = std::visit([this, state](auto && id) { + return light_data(id, std::span(payload_), *state); + }, id_); + payload_length_ = payload.size(); count_ = 0; - -#if 0 - switch (state->current_values.get_color_mode()) - // handled in update_state - float red; - float green; - float blue; - float temperatur; - float brightness; - state->current_values_as_rgbct(&red, &green, &blue, &temperatur, &brightness); -#else - static fastcon::FastconController controller = [this]() { - fastcon::FastconController controller; - controller.set_mesh_key(key_); - }(); - - const auto fastcon_data = controller.get_light_data(state); - hexdump(TAG, &fastcon_data.front(), fastcon_data.size(), "%c%02hhX: fastcon: %s", id_.kind(), id_.value()); - - -#endif -} - -// interface: esphome::esp32_ble::GAPEventHandler -void -Light::gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) { - if (!advertising_) - return; - - esp_err_t err; - switch (event) { - case ESP_GAP_BLE_ADV_DATA_RAW_SET_COMPLETE_EVT: - adv_event_(param->adv_data_raw_cmpl); - return; - - case ESP_GAP_BLE_ADV_START_COMPLETE_EVT: - adv_event_(param->adv_start_cmpl); - return; - - case ESP_GAP_BLE_ADV_STOP_COMPLETE_EVT: - adv_event_(param->adv_stop_cmpl); - return; - } + //ESP_LOGD(TAG, "%c%02hhX: state: %s", id_.kind(), id_.value(), + // format_hex_pretty(&payload[0], payload.size()).c_str()); + enable_loop(); } } // namespace brmesh } // namespace esphome - -#if 0 -#include -#include "esphome/core/log.h" -#include "esphome/components/light/light_color_values.h" -#include "light.h" -#include "controller.h" -#include "utils.h" - -namespace { -const char *const TAG = "fastcon.light"; - -const esphome::light::ColorModeMask color_modes[] = { - esphome::light::ColorMode::RGB, - esphome::light::ColorMode::WHITE, - esphome::light::ColorMode::BRIGHTNESS, - esphome::light::ColorMode::COLD_WARM_WHITE, -}; - -} // namespace - -namespace esphome { -namespace brmesh { - -void Light::setup() -{ - ESP_LOGCONFIG(TAG, "id=%hhd", light_id_); -} - -light::LightTraits Light::get_traits() -{ - auto traits = light::LightTraits(); - traits.set_supported_color_modes(color_modes); - traits.set_min_mireds(153); - traits.set_max_mireds(500); - return traits; -} - -void Light::setup_state(light::LightState *state) -{ - state_ = state; -} - -void Light::update_state(light::LightState *state) -{ - state_ = state; - controller_.enqueue(*this); -} - -void Light::write_state(light::LightState *state) -{ - // already queued from update_state -} - -size_t Light::command(std::span &&data) const -{ - if (light_state_ == nullptr) - return 0; - - const auto &values = light_state_->current_values; - // 0 - On/Off Bit + 7-bit Brightness - // 1 - Blue byte - // 2 - Red byte - // 3 - Green byte - // 4 - Warm byte - // 5 - Cold byte - - bool is_on = values.is_on(); - if (!is_on) - { - data[0] = 0x00; - return 1; - } - - auto color_mode = [mode=light::ColorModeMask(values.get_color_mode())](light::ColorCapability cap){ - return light::has_capability(mode, cap); - }; - float brightness = std::min(values.get_brightness() * 127.0f, 127.0f); // clamp the value to at most 127 - - if (color_mode(light::ColorCapability::WHITE)) - { - // TODO: need to figure out when esphome is changing to white vs setting brightness - // when changing to white mode, this should be the payload: - // ff0000007f7f - - data[0] = brightness; - return 1; - } - - data[0] = 0x80 + static_cast(brightness); - if (color_mode(light::ColorCapability::RGB)) { - data[1] = static_cast(values.get_blue() * 255.0f); - data[2] = static_cast(values.get_red() * 255.0f); - data[3] = static_cast(values.get_green() * 255.0f); - } else { - data[1] = 0; - data[2] = 0; - data[3] = 0; - } - - if (color_mode(light::ColorCapability::COLD_WARM_WHITE)) { - data[4] = static_cast(values.get_warm_white() * 255.0f); - data[5] = static_cast(values.get_cold_white() * 255.0f); - } else if (color_mode(light::ColorCapability::COLOR_TEMPERATURE)) { - // TODO figure out if we can use these, and how - float temperature = values.get_color_temperature(); - if (temperature < 153) - { - data[4] = 0xff; - data[5] = 0x00; - } - else if (temperature > 500) - { - data[4] = 0x00; - data[5] = 0xff; - } - else - { - // Linear interpolation between (153, 0xff) and (500, 0x00) - data[4] = (uint8_t)(((500 - temperature) * 255.0f + (temperature - 153) * 0x00) / (500 - 153)); - data[5] = (uint8_t)(((temperature - 153) * 255.0f + (500 - temperature) * 0x00) / (500 - 153)); - } - } else { - data[4] = 0; - data[5] = 0; - } - - return 6; -} -#if 0 - // Generate the advertisement payload - auto adv_data = controller_.single_control(this->light_id_, light_data); - - // Debug output - print payload as hex - auto hex_str = vector_to_hex_string(adv_data).data(); - ESP_LOGD(TAG, "Advertisement Payload (%d bytes): %s", adv_data.size(), hex_str); - - // Send the advertisement - controller_.queueCommand(this->light_id_, adv_data); -} -#endif - -} // namespace brmesh -} // namespace esphome -#endif diff --git a/components/brmesh/light.h b/components/brmesh/light.h index abed4db..cb20922 100644 --- a/components/brmesh/light.h +++ b/components/brmesh/light.h @@ -13,7 +13,9 @@ namespace esphome { namespace brmesh { -class Light : public Component, public light::LightOutput, public esp32_ble::GAPEventHandler +class Network; + +class Light : public Component, public light::LightOutput { public: enum class Single : uint8_t {}; @@ -35,14 +37,12 @@ public: } }; + Light(Id id, Network *network) : id_(id), network_(*network) {} - Light(Id id, const Key &key, - uint16_t adv_interval_min, uint16_t adv_interval_max, - uint16_t adv_duration, uint16_t adv_gap) - : id_(id), key_(key), - adv_interval_min_(adv_interval_min), adv_interval_max_(adv_interval_max), - adv_duration_(adv_duration), adv_gap_(adv_gap) {} - +// Light(Id id, const Key &key, uint16_t adv_interval_min, uint16_t adv_interval_max) +// : id_(id), key_(key), +// adv_interval_min_(adv_interval_min / 0.625f), adv_interval_max_(adv_interval_max / 0.625f) {} +// // interface: Component void dump_config() override; float get_setup_priority() const override { @@ -57,30 +57,32 @@ public: void update_state(light::LightState *state) override; void write_state(light::LightState *state) override; - // interface: GAPEventHandler - void gap_event_handler(esp_gap_ble_cb_event_t, esp_ble_gap_cb_param_t *) override; - + // callback + void advertise(bool advertise); private: - light::LightState *state_ = nullptr; Id id_; + Network &network_; +#if 0 + light::LightState *state_ = nullptr; const Key key_; const uint16_t adv_interval_min_; const uint16_t adv_interval_max_; - const uint16_t adv_duration_; - const uint16_t adv_gap_; static constexpr esp_power_level_t tx_power_{}; uint8_t seq_ = 0; +#endif uint8_t count_ = 0; bool advertising_ = false; std::array payload_ = {}; size_t payload_length_ = 0; +#if 0 void adv_event_(esp_ble_gap_cb_param_t::ble_adv_data_raw_cmpl_evt_param &); void adv_event_(esp_ble_gap_cb_param_t::ble_adv_start_cmpl_evt_param &); void adv_event_(esp_ble_gap_cb_param_t::ble_adv_stop_cmpl_evt_param &); static std::span adv_data_(const std::span &, uint8_t seq, light::LightState &); +#endif }; diff --git a/components/brmesh/light.py b/components/brmesh/light.py index 4c7d79f..f9954b2 100644 --- a/components/brmesh/light.py +++ b/components/brmesh/light.py @@ -2,91 +2,40 @@ import esphome.codegen as cg import esphome.config_validation as cv +from esphome.cpp_generator import LambdaExpression from esphome.const import CONF_LIGHT_ID, CONF_OUTPUT_ID from esphome.core import HexInt from esphome.components import light, esp32_ble -#from .brmesh import Controller +from .brmesh import Network, brmesh_ns DEPENDENCIES = ["esp32_ble"] AUTO_LOAD = ["light"] -CONF_KEY = "key" -CONF_ADV = "adv" -CONF_INTERVAL = "interval" -CONF_MIN = "min" -CONF_MAX = "max" -CONF_DURATION = "duration" -CONF_GAP = "gap" +CONF_NETWORK_ID = "network" - -def validate_key_string(value): - value = value.replace(" ", "") - if len(value) != 8: - raise cv.Invalid("Key must be exactly 4 bytes (8 hex characters)") - - try: - return [HexInt(int(value[x:x+2], 16)) for x in range(0, len(value), 2)] - except ValueError as err: - raise cv.Invalid(f"Invalid hex value: {err}") - - -def validate_key(value): - if isinstance(value, str): - value = validate_key_string(value) - - if not isinstance(value, list): - raise cv.Invalid("Key must be a list") - - if len(value) != 4: - raise cv.Invalid("Key must have 4 bytes") - - return [ cv.uint8_t(x) for x in value ] - - -brmesh_ns = cg.esphome_ns.namespace("brmesh") Light = brmesh_ns.class_("Light", light.LightOutput, cg.Component) Single = Light.enum("Single", True) Group = Light.enum("Group", True); -ADV_INTERVAL_SCHEMA = cv.Schema({ - cv.Optional(CONF_MIN, default=0x20): cv.uint16_t, - cv.Optional(CONF_MAX, default=0x80): cv.uint16_t, -}) - -ADV_SCHEMA = cv.Schema({ - cv.Optional(CONF_INTERVAL, default=ADV_INTERVAL_SCHEMA({})): ADV_INTERVAL_SCHEMA, - cv.Optional(CONF_DURATION, default="50ms"): cv.positive_time_period, - cv.Optional(CONF_GAP, default="10ms"): cv.positive_time_period, -}) - CONFIG_SCHEMA = light.light_schema(Light, light.LightType.RGB).extend({ - cv.GenerateID(esp32_ble.CONF_BLE_ID): cv.use_id(esp32_ble.ESP32BLE), - cv.Required(CONF_KEY): validate_key, + cv.Required(CONF_NETWORK_ID): cv.use_id(Network), cv.Required(CONF_LIGHT_ID): cv.int_range(min=1, max=255), - cv.Optional(CONF_ADV, default=ADV_SCHEMA({})): ADV_SCHEMA, + # TODO: use CONF_BLE_ID from CONF_NETWORK_ID... + cv.GenerateID(esp32_ble.CONF_BLE_ID): cv.use_id(esp32_ble.ESP32BLE), }) async def to_code(config): - adv = config[CONF_ADV] - interval = adv[CONF_INTERVAL] - - if interval[CONF_MAX] < interval[CONF_MIN]: - raise cv.Invalid( - f"{CONF_ADV}.{CONF_INTERVAL}.{CONF_MIN} ({interval[CONF_MIN]}) must be <= " - f"{CONF_ADV}.{CONF_INTERVLA}.{CONF_MAX} ({interval[CONF_MAX]})" - ) + network = await cg.get_variable(config[CONF_NETWORK_ID]) ble = await cg.get_variable(config[esp32_ble.CONF_BLE_ID]) target = Single(config[CONF_LIGHT_ID]) - var = cg.new_Pvariable(config[CONF_OUTPUT_ID], target, config[CONF_KEY], - interval[CONF_MIN], interval[CONF_MAX], - adv[CONF_DURATION].total_milliseconds, - adv[CONF_GAP].total_milliseconds) - esp32_ble.register_gap_event_handler(ble, var) + var = cg.new_Pvariable(config[CONF_OUTPUT_ID], target, network) + #config[CONF_KEY], + # interval[CONF_MIN], interval[CONF_MAX]) + #esp32_ble.register_gap_event_handler(ble, var) + callback = LambdaExpression(f"{var}->advertise(advertise);", [ (bool, "advertise") ], "") + cg.add(ble.advertising_register_raw_advertisement_callback(callback)) await cg.register_component(var, config) await light.register_light(var, config) - - cg.add_define("USE_ESP32_BLE_ADVERTISING") - cg.add_define("USE_ESP32_BLE_UUID") diff --git a/components/brmesh/network.cpp b/components/brmesh/network.cpp new file mode 100644 index 0000000..d915023 --- /dev/null +++ b/components/brmesh/network.cpp @@ -0,0 +1,330 @@ +#include "network.h" + +#include "debug.h" +#include "light.h" +#include "whitening.h" + +#include + +namespace { +const char *const TAG = "brmesh.network"; +} // namespace + +namespace esphome { +namespace brmesh { + +namespace { + +template +std::span subspan(const std::span &input, size_t offset, ssize_t size) { + if (input.size() < offset) + return {}; + + const auto remain = input.size() - offset; + if (size < 0) { + size += remain; + if (size < 0) + return {}; + } + if (remain < size) + return {}; + + return input.subspan(offset, size); +} + +template +std::span subspan(const std::span &input, size_t offset) { + if (input.size() < offset) + return {}; + return subspan(input, offset, input.size() - offset); +} + +void +adv_start(uint16_t adv_int_min, uint16_t adv_int_max) { + esp_ble_adv_params_t adv_params = { + .adv_int_min = adv_int_min, + .adv_int_max = adv_int_max, + .adv_type = ADV_TYPE_NONCONN_IND, + .own_addr_type = BLE_ADDR_TYPE_PUBLIC, + .peer_addr = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, + .peer_addr_type = BLE_ADDR_TYPE_PUBLIC, + .channel_map = ADV_CHNL_ALL, + .adv_filter_policy = ADV_FILTER_ALLOW_SCAN_ANY_CON_ANY, + }; + + const auto err = esp_ble_gap_start_advertising(&adv_params); + if (err != ESP_OK) + ESP_LOGE(TAG, "start advertising: %s", esp_err_to_name(err)); + else + ESP_LOGD(TAG, "start advertising"); +} + +template +uint8_t crc(const std::span &bytes, uint8_t init = 0x00) { + for (const auto& byte:bytes) + init += byte; + return init; +} + +uint16_t crc16(uint8_t byte, uint16_t crc) { + crc ^= static_cast(byte) << 8; + for (int j=0; j<4; ++j) { + uint16_t tmp = crc << 1; + if (crc & 0x8000) + tmp ^= 0x1021; + crc = tmp << 1; + if (tmp & 0x8000) + crc ^= 0x1021; + } + return crc; +} + +uint16_t crc16(const std::array &addr, const std::span &data, const uint16_t seed = 0xFFFF) { + auto crc = seed; + + for (auto it = addr.rbegin(); it != addr.rend(); ++it) + crc = crc16(*it, crc); + + for (const auto &byte:data) + crc = crc16(reverse_bits(byte), crc); + + return ~reverse_bits(crc); +} + +std::span inner(const std::span &buffer, Light::Id id, const std::span &light_data) { + if (id.kind() != 'L') + return {}; + + const auto src = light_data; + const auto hdr = subspan(buffer, 0, 2); + const auto dst = subspan(buffer, hdr.size()); + + if (hdr.size() == 0) + return {}; + + if (dst.size() < src.size()) + return {}; + + hdr[0] = 2 | (((0xfffffff & (light_data.size() + 1)) << 4)); + hdr[1] = id.value(); + std::copy(src.begin(), src.end(), dst.begin()); + + // TODO: required? + std::ranges::fill(subspan(dst, src.size()), 0); + + //ESP_LOGV(TAG, "%c%02hhX: inner: %s", id.kind(), id.value(), + // format_hex_pretty(&buffer[0], buffer.size()).c_str()); + + return buffer; +} + +std::span command(const std::span &buffer, const Key &key, Light::Id id, uint8_t seq, const std::span &light_data) { + if (buffer.empty()) + return {}; + + static constexpr Key DEFAULT_ENCRYPT_KEY = { 0x5e, 0x36, 0x7b, 0xc4 }; + + static constexpr uint8_t n = 5; // TODO + static constexpr bool forward = true; // TODO + + const auto header = subspan(buffer, 0, 4); + if (header.empty()) + return {}; + + const auto body = inner(subspan(buffer, header.size()), id, light_data); + if (body.empty()) + return {}; + + const auto result = subspan(buffer, 0, header.size() + body.size()); + if (result.empty()) + return {}; + + const uint8_t i2 = (id.value() / 256); + header[0] = (i2 & 0b1111) | ((n & 0b111) << 4) | (forward ? 0x80 : 0); + header[1] = seq; + header[2] = key[3]; + header[3] = header[0] + header[1] + header[2] + crc(body); + + //ESP_LOGV(TAG, "%c%02hhX: plain: %s", id.kind(), id.value(), + // format_hex_pretty(&result[0], result.size()).c_str()); + + DEFAULT_ENCRYPT_KEY(header); + key(body); + + //ESP_LOGV(TAG, "%c%02hhX: crypt: %s", id.kind(), id.value(), + // format_hex_pretty(&result[0], result.size()).c_str()); + + + return result; +} + +std::span payload(const std::span &buffer, const Key &key, Light::Id id, uint8_t seq, const std::span &light_data) { + constexpr std::array ADDRESS = {0xC1, 0xC2, 0xC3}; + constexpr std::array PREFIX = { 0x71, 0x0F, 0x55 }; + + const auto prefix = subspan(buffer, 0, PREFIX.size()); + if (prefix.empty()) + return {}; + + const auto address = subspan(buffer, prefix.size(), ADDRESS.size()); + if (address.empty()) + return {}; + + const auto command = brmesh::command(subspan(buffer, prefix.size() + address.size(), -2), key, id, seq, light_data); + if (command.empty()) + return {}; + + const auto crc = subspan(buffer, prefix.size() + address.size() + command.size(), 2); + if (crc.empty()) + return {}; + + //ESP_LOGD(TAG, "%c%02hhX: command: %s", id.kind(), id.value(), + // format_hex_pretty(&command[0], command.size()).c_str()); + + const auto result = subspan(buffer, 0, prefix.size() + address.size() + command.size() + crc.size()); + if (result.empty()) + return {}; + + // static values + std::copy(PREFIX.cbegin(), PREFIX.cend(), prefix.begin()); + + // copy + std::reverse_copy(ADDRESS.cbegin(), ADDRESS.cend(), address.begin()); + + + uint16_t crc_value = crc16(ADDRESS, command); + crc[0] = crc_value; + crc[1] = crc_value >> 8; + + //ESP_LOGV(TAG, "%c%02hhX: raw: %s", id.kind(), id.value(), + // format_hex_pretty(&result[0], result.size()).c_str()); + + for (auto &byte:prefix) + byte = reverse_bits(byte); + for (auto &byte:address) + byte = reverse_bits(byte); + + //ESP_LOGV(TAG, "%c%02hhX: reverse: %s", id.kind(), id.value(), + // format_hex_pretty(&result[0], result.size()).c_str()); + + // TODO: shift seed to drop warmup + auto whitening = Whitening::from_val(0x25); + for (size_t i=0; i<0xf; ++i) + whitening.encode(0); + + whitening.encode(result); + + return result; +} + +std::span adv_data(const std::span &buffer, const Key &key, Light::Id id, uint8_t seq, const std::span &light_data) { + const auto flags = subspan(buffer, 0, 3); + if (flags.empty()) + return {}; + const auto length = subspan(buffer, flags.size(), 1); + if (length.empty()) + return {}; + const auto manufacturer = subspan(buffer, flags.size() + length.size(), 3); + + const auto payload = brmesh::payload(subspan(buffer, flags.size() + length.size() + manufacturer.size()), key, id, seq, light_data); + + if (payload.empty()) + return {}; + const auto result = subspan(buffer, 0, flags.size() + length.size() + manufacturer.size() + payload.size()); + + ESP_LOGD(TAG, "%c%02hhX: payload: %s", id.kind(), id.value(), + format_hex_pretty(&payload[0], payload.size()).c_str()); + + flags[0] = 2; + flags[1] = ESP_BLE_AD_TYPE_FLAG; + flags[2] = ESP_BLE_ADV_FLAG_BREDR_NOT_SPT | ESP_BLE_ADV_FLAG_GEN_DISC; + + length[0] = payload.size(); + + manufacturer[0] = ESP_BLE_AD_MANUFACTURER_SPECIFIC_TYPE; + manufacturer[1] = 0xF0; + manufacturer[2] = 0xFF; + + return result; +} + +} // namespace + +// interface: esphome::Component +void +Network::dump_config() { + ESP_LOGCONFIG(TAG, "%02hhx %02hhx %02hhx %02hhx", + key_[0], key_[1], key_[2], key_[3]); +} + +void +Network::setup() { + disable_loop(); +} + +void +Network::loop() { +} + +void +Network::advertise(Light::Id id, const std::span &payload) { + if (payload.size() == 0) { + advertise_= false; + // TODO: disable ble advertise + return; + } + + std::array buffer; + const auto data = adv_data(std::span(buffer), key_, id, seq_++, payload); + + ESP_LOGD(TAG, "%c%02hhX: advertise: %s", id.kind(), id.value(), + format_hex_pretty(&data[0], data.size()).c_str()); + + if (data.size() == 0) + return; + + auto err = ESP_OK; +#if 0 + err = esp_ble_tx_power_set(ESP_BLE_PWR_TYPE_ADV, tx_power_); + if (err != ESP_OK) + ESP_LOGW(TAG, "%c%02hhX: tx power set: %s", id.kind(), id.value(), esp_err_to_name(err)); +#endif + + advertise_ = true; + err = esp_ble_gap_config_adv_data_raw(&data.front(), data.size()); + if (err != ESP_OK) + ESP_LOGE(TAG, "%c%02hhX: config adv: %s", id.kind(), id.value(), esp_err_to_name(err)); + else + ESP_LOGD(TAG, "%c%02hhX: config adv", id.kind(), id.value()); +} + +// interface: esphome::esp32_ble::GAPEventHandler +void +Network::gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) { + if (!advertise_) + return; + + esp_err_t err; + switch (event) { + case ESP_GAP_BLE_ADV_DATA_RAW_SET_COMPLETE_EVT: + ESP_LOGD(TAG, "adv data raw set"); + adv_start(adv_interval_min_, adv_interval_max_); + break; + + case ESP_GAP_BLE_ADV_START_COMPLETE_EVT: + err = param->adv_start_cmpl.status; + if (err != ESP_BT_STATUS_SUCCESS) + // TODO: is param.status a valid esp_err_to_name parameter? + ESP_LOGE(TAG, "adv start: %s", esp_err_to_name(err)); + else + ESP_LOGD(TAG, "adv started"); + break; + + case ESP_GAP_BLE_ADV_STOP_COMPLETE_EVT: + ESP_LOGD(TAG, "adv stop"); + break; + } +} + +} // namespace brmesh +} // namespace esphome diff --git a/components/brmesh/network.h b/components/brmesh/network.h new file mode 100644 index 0000000..6d01831 --- /dev/null +++ b/components/brmesh/network.h @@ -0,0 +1,43 @@ +#pragma once +#include "key.h" +#include "light.h" + +#include "esphome/core/component.h" +#include "esphome/components/esp32_ble/ble.h" + +#include +#include + +namespace esphome { +namespace brmesh { + +class Network : public Component, public esp32_ble::GAPEventHandler { +public: + constexpr Network(const Key &key, uint16_t adv_interval_min, uint16_t adv_interval_max, esp_power_level_t tx_power) + : key_(key), adv_interval_min_(adv_interval_min / 0.625f), adv_interval_max_(adv_interval_max / 0.625f), tx_power_(tx_power) {} + + void advertise(Light::Id id, const std::span &payload); + + // interface: Component + void dump_config() override; + float get_setup_priority() const override { + return setup_priority::AFTER_BLUETOOTH; + } + void setup() override; + void loop() override; + + // interface: GAPEventHandler + void gap_event_handler(esp_gap_ble_cb_event_t, esp_ble_gap_cb_param_t *) override; + +private: + const Key key_; + const uint16_t adv_interval_min_; + const uint16_t adv_interval_max_; + const esp_power_level_t tx_power_{}; + uint8_t seq_ = 0; + + bool advertise_ = false; +}; + +} // namespace brmesh +} // namespace esphome diff --git a/components/brmesh/fastcon/protocol.cpp b/components/brmesh/protocol.cpp similarity index 93% rename from components/brmesh/fastcon/protocol.cpp rename to components/brmesh/protocol.cpp index 549b4be..2b8aba4 100644 --- a/components/brmesh/fastcon/protocol.cpp +++ b/components/brmesh/protocol.cpp @@ -1,9 +1,13 @@ #include #include "protocol.h" +#include "debug.h" +namespace { +const char *TAG = "fastcon.protocol"; +} namespace esphome { - namespace fastcon + namespace brmesh { std::vector get_rf_payload(const std::vector &addr, const std::vector &data) { @@ -56,5 +60,5 @@ namespace esphome // Return only the portion after 0xf bytes return std::vector(payload.begin() + 0xf, payload.end()); } - } // namespace fastcon -} // namespace esphome \ No newline at end of file + } // namespace brmesh +} // namespace esphome diff --git a/components/brmesh/protocol.h b/components/brmesh/protocol.h index aaee4c0..ee25c20 100644 --- a/components/brmesh/protocol.h +++ b/components/brmesh/protocol.h @@ -3,14 +3,16 @@ #include #include #include -#include "key.h" +#include "utils.h" -namespace esphome { -namespace brmesh { +namespace esphome +{ + namespace brmesh + { + static const std::array DEFAULT_ENCRYPT_KEY = {0x5e, 0x36, 0x7b, 0xc4}; + static const std::array DEFAULT_BLE_FASTCON_ADDRESS = {0xC1, 0xC2, 0xC3}; -static constexpr Key DEFAULT_ENCRYPT_KEY = { 0x5e, 0x36, 0x7b, 0xc4 }; - -static const std::array DEFAULT_BLE_ADDRESS = {0xC1, 0xC2, 0xC3}; - -} // namespace brmesh -} // namespace esphome + std::vector get_rf_payload(const std::vector &addr, const std::vector &data); + std::vector prepare_payload(const std::vector &addr, const std::vector &data); + } // namespace brmesh +} // namespace esphome \ No newline at end of file diff --git a/components/brmesh/fastcon/utils.cpp b/components/brmesh/utils.cpp similarity index 98% rename from components/brmesh/fastcon/utils.cpp rename to components/brmesh/utils.cpp index 371ae09..f3a8eb3 100644 --- a/components/brmesh/fastcon/utils.cpp +++ b/components/brmesh/utils.cpp @@ -5,7 +5,7 @@ namespace esphome { - namespace fastcon + namespace brmesh { uint8_t reverse_8(uint8_t d) { @@ -122,5 +122,5 @@ namespace esphome hex_str[data.size() * 2] = '\0'; // Ensure null termination return hex_str; } - } // namespace fastcon + } // namespace brmesh } // namespace esphome \ No newline at end of file diff --git a/components/brmesh/fastcon/utils.h b/components/brmesh/utils.h similarity index 95% rename from components/brmesh/fastcon/utils.h rename to components/brmesh/utils.h index c243d9c..9783096 100644 --- a/components/brmesh/fastcon/utils.h +++ b/components/brmesh/utils.h @@ -5,7 +5,7 @@ namespace esphome { - namespace fastcon + namespace brmesh { // Bit manipulation utilities uint8_t reverse_8(uint8_t d); @@ -32,5 +32,5 @@ namespace esphome void whitening_encode(std::vector &data, WhiteningContext &ctx); std::vector vector_to_hex_string(std::vector &data); - } // namespace fastcon + } // namespace brmesh } // namespace esphome \ No newline at end of file diff --git a/components/brmesh/whitening.cpp b/components/brmesh/whitening.cpp index f174fd4..eee79ae 100644 --- a/components/brmesh/whitening.cpp +++ b/components/brmesh/whitening.cpp @@ -14,15 +14,26 @@ Whitening::State Whitening::use(bool encode, const LUT &lut, State state, std::span bytes) { const bool log = (bytes.size() > 1 || bytes[0] != 0x00); + if (log) - hexdump(TAG, bytes, "%s: input: %s", encode ? "encode" : "decode"); + ESP_LOGV(TAG, "%s: input: %s", encode ? "encode" : "decode", + format_hex_pretty(&bytes[0], bytes.size()).c_str()); + else + ESP_LOGVV(TAG, "%s: input: %s", encode ? "encode" : "decode", + format_hex_pretty(&bytes[0], bytes.size()).c_str()); + for (auto &byte:bytes) { auto &next = lut[state]; byte = next.lut[byte]; state = next.state; } + if (log) - hexdump(TAG, bytes, "%s: output: %s", encode ? "encode" : "decode"); + ESP_LOGV(TAG, "%s: output: %s", encode ? "encode" : "decode", + format_hex_pretty(&bytes[0], bytes.size()).c_str()); + else + ESP_LOGVV(TAG, "%s: output: %s", encode ? "encode" : "decode", + format_hex_pretty(&bytes[0], bytes.size()).c_str()); return state; }