From 6dee644a125643e3b4eb2bfcf486b3ed993aa9e3 Mon Sep 17 00:00:00 2001 From: Jonas Rabenstein Date: Tue, 23 Dec 2025 10:03:07 +0100 Subject: [PATCH 01/10] 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; } From d10ec6546d81206df7880bd3115c29654122bba5 Mon Sep 17 00:00:00 2001 From: Jonas Rabenstein Date: Tue, 23 Dec 2025 10:10:00 +0100 Subject: [PATCH 02/10] cleanup --- components/brmesh/controller.cpp | 275 ------------------------------- components/brmesh/controller.h | 78 --------- components/brmesh/debug.h | 43 ----- components/brmesh/light.cpp | 1 - components/brmesh/network.cpp | 1 - components/brmesh/protocol.cpp | 64 ------- components/brmesh/protocol.h | 18 -- components/brmesh/q.h | 72 -------- components/brmesh/utils.cpp | 126 -------------- components/brmesh/utils.h | 36 ---- components/brmesh/whitening.cpp | 1 - 11 files changed, 715 deletions(-) delete mode 100644 components/brmesh/controller.cpp delete mode 100644 components/brmesh/controller.h delete mode 100644 components/brmesh/debug.h delete mode 100644 components/brmesh/protocol.cpp delete mode 100644 components/brmesh/protocol.h delete mode 100644 components/brmesh/q.h delete mode 100644 components/brmesh/utils.cpp delete mode 100644 components/brmesh/utils.h diff --git a/components/brmesh/controller.cpp b/components/brmesh/controller.cpp deleted file mode 100644 index e62fe98..0000000 --- a/components/brmesh/controller.cpp +++ /dev/null @@ -1,275 +0,0 @@ -#if 0 -#include "esphome/core/component_iterator.h" -#include "esphome/core/log.h" -#include "esphome/components/esp32_ble/ble.h" -#include "controller.h" -#include "protocol.h" - -namespace { -const char *const TAG = "brmesh.controller"; - -template -std::enable_if_t<(addr_size == 3 || addr_size == std::dynamic_extent), uint16_t> -crc16(const std::span &addr, const std::span &data, const uint16_t init = 0xFFFF) -{ - constexpr auto step = [](uint16_t result, uint8_t byte)->uint16_t{ - result ^= static_cast(byte) << 8; - for (int j = 0; j < 4; j++) { - uint16_t tmp = result << 1; - if (result & 0x8000) - tmp ^= 0x1021; - result = tmp << 1; - if (tmp & 0x8000) - result ^= 0x1021; - } - return result; - }; - - auto result = init; - - for (const auto &c:addr) - result = step(result, c); - - for (const uint8_t &c:data) - result = step(result, esphome::reverse_bits(c)); - - return ~esphome::reverse_bits(result); -} - -} - - -namespace esphome { -namespace brmesh { -Controller::Controller(esp32_ble::ESP32BLE &ble, - 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) - : ble_(ble), key_(key), - adv_interval_min_(adv_interval_min), - adv_interval_max_(adv_interval_max), - adv_duration_(adv_duration), - adv_gap_(adv_duration) -{ -} - -void Controller::setup() -{ - ESP_LOGCONFIG(TAG, "BRmesh Controller:\n" - " key: %02hx:%02hx:%02hx:%02hx\n" - " adv interval: %hd-%hd\n" - " adv duration: %hdms\n" - " adv gap: %hdms", - key_[0], key_[1], key_[2], key_[3], - adv_interval_min_, adv_interval_max_, - adv_duration_, - adv_gap_); - ble_.register_gap_event_handler(this); -} - -void Controller::loop() -{ - auto *light = q_.peek(); - disable_loop(); - if (light == nullptr) - light = q_.peek(); - if (light == nullptr) - return; -} - -void Controller::advertise(Light &light) -{ - 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, - }; - - std::array adv_data_raw[31]; - 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 - const auto cmd_data_len = adv_data_len++; - 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; - - const auto cmd = command(std::span(adv_data_raw).subspan(adv_data_len), light); - adv_data_raw[cmd_data_len] = cmd.size_bytes(); - adv_data_len += cmd.size_bytes(); - - 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; - } - - ESP_LOGD(TAG, "advertise id=%hd", light.id()); -} - -void -Controller::gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) -{ - switch (event) { - case ESP_GAP_BLE_ADV_START_COMPLETE_EVT: - ESP_LOD(TAG, "advertise: %hdms\n", adv_duration_); - set_timeout(adv_duration_, esp_ble_gap_stop_advertising); - return; - - case ESP_GAP_BLE_ADV_STOP_COMPLETE_EVT: - ESP_LOGD(TAG, "advertise gap: %hdms\n", adv_gap_); - set_timeout(adv_gap_, [this](){ - q_.dequeue(); - auto *light = q_.peek(); - if (light == nullptr) - advertise(*light); - }); - return; - - default: - return; - } -} - -void -Controller::enqueue(Light &light) -{ - if (!q_.enqueue(light)) - return; - defer([this](){ advertise(*q_.peek()); }); -} - -std::vector Controller::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 Controller::generate_command(uint8_t n, uint32_t light_id_, const std::vector &data, bool forward) -{ - uint8_t& sequence = seq_; - - // 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->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->key_[i & 3] ^ body[4 + i]; - } - - // Prepare the final payload with RF protocol formatting - std::vector addr = {DEFAULT_BLE_ADDRESS.begin(), DEFAULT_BLE_ADDRESS.end()}; - return prepare_payload(addr, body); -} - -std::span Controller::payload(std::span &&span, const Light &light) -{ - const auto header = span.subspan(0, 3); - header[0] = 0x71; - header[1] = 0x0f; - header[2] = 0x55; - - const auto address = span.subspan(3, 3); - std::reverse_copy(address.begin(), address.end(), - DEFAULT_BLE_ADDRESS.cbegin(), - DEFAULT_BLE_ADDRESS.cend()); - - const auto data = command(span.subspan(6, 10), light); - const auto crc = span.subspan(6 + data.size(), 2); - const auto crc16 = ::crc16(address, data); - crc[0] = static_cast(crc16); - crc[1] = static_cast(crc16 >> 8); - - auto result = span.subspan(0, header.size() + address.size() + data.size() + crc.size()); - - // TODO: adjust seed to skip zero byte warmup - Whitening whitening(0x25); - for (size_t i = 0; i< 16; ++i) - (void) whitening.encode(0x00); - - whitening.encode(result); - - return result; -} - -std::span command(std::span &&span, const Light &light) -{ - static constexpr uint8_t n = 5; // TODO - static constexpr bool forward = true; // TODO - - const auto inner = span.subpsna(4, light.command(span.subspan(4, 6))); - - auto header = span.subspan(0, 4); - header[0] = (i2 & 0b1111) | ((n & 0b111) << 4) | (forward ? 0x80 : 0); - header[1] = seq_++; - header[2] = key_[3]; // safe key - header[3] = crc(inner); - - DEFAULT_ENCRYPT_KEY(header); - key_(inner); - - //prepare(DEFAULT_BLE_ADDRESS, span.subspan(4 + inner.size())); - return span.subspan(0, 4 + inner.size()); -} - -} // namespace brmesh -} // namespace esphome -#endif diff --git a/components/brmesh/controller.h b/components/brmesh/controller.h deleted file mode 100644 index 159e7b3..0000000 --- a/components/brmesh/controller.h +++ /dev/null @@ -1,78 +0,0 @@ -#pragma once - -#include -#include -#include -#include "esphome/core/component.h" -#include "esphome/components/esp32_ble/ble.h" - -#include "q.h" -#include "key.h" - -namespace esphome { -namespace brmesh { - -class Light; - -class Controller : public Component, public esp32_ble::GAPEventHandler -{ -public: - Controller(esp32_ble::ESP32BLE *ble, const Key &key, uint16_t adv_interval_min, uint16_t adv_interval_max, uint16_t adv_duration, uint16_t adv_gap) - : Controller(*ble, key, adv_interval_min, adv_interval_max, adv_duration, adv_gap) {} - - Controller(esp32_ble::ESP32BLE &ble, const Key &key, uint16_t adv_interval_min, uint16_t adv_interval_max, uint16_t adv_duration, uint16_t adv_gap); - - std::vector single_control(uint32_t addr, const std::vector &light_data); - void enqueue(Light &); - - // interface: Component - void setup() override; - void loop() override; - - // interface: GAPEventHandler - void gap_event_handler(esp_gap_ble_cb_event_t, esp_ble_gap_cb_param_t *); - -protected: - struct Command - { - std::vector data; - uint32_t timestamp; - uint8_t retries{0}; - static constexpr uint8_t MAX_RETRIES = 3; - }; - - 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); - - static const uint16_t MANUFACTURER_DATA_ID = 0xfff0; -private: - esp32_ble::ESP32BLE &ble_; - Key key_; - - uint16_t adv_interval_min_; - uint16_t adv_interval_max_; - uint16_t adv_duration_; - uint16_t adv_gap_; - - Q q_ = {}; - uint8_t seq_ = 0; - - void advertise(Light &); - - std::span payload(std::span &span, const Light &light); - - std::span command(std::span &&span, const Light &light); -}; - -} // namespace brmesh -} // namespace esphome diff --git a/components/brmesh/debug.h b/components/brmesh/debug.h deleted file mode 100644 index 6b7ab59..0000000 --- a/components/brmesh/debug.h +++ /dev/null @@ -1,43 +0,0 @@ -#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 { -namespace brmesh { - -namespace { -template -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()); -} - -template -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 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/light.cpp b/components/brmesh/light.cpp index 23eb261..1dbf674 100644 --- a/components/brmesh/light.cpp +++ b/components/brmesh/light.cpp @@ -1,6 +1,5 @@ #include "light.h" -#include "debug.h" #include "network.h" #include "whitening.h" diff --git a/components/brmesh/network.cpp b/components/brmesh/network.cpp index d915023..68f7c26 100644 --- a/components/brmesh/network.cpp +++ b/components/brmesh/network.cpp @@ -1,6 +1,5 @@ #include "network.h" -#include "debug.h" #include "light.h" #include "whitening.h" diff --git a/components/brmesh/protocol.cpp b/components/brmesh/protocol.cpp deleted file mode 100644 index 2b8aba4..0000000 --- a/components/brmesh/protocol.cpp +++ /dev/null @@ -1,64 +0,0 @@ -#include -#include "protocol.h" -#include "debug.h" - -namespace { -const char *TAG = "fastcon.protocol"; -} -namespace esphome -{ - namespace brmesh - { - std::vector get_rf_payload(const std::vector &addr, const std::vector &data) - { - const size_t data_offset = 0x12; - const size_t inverse_offset = 0x0f; - const size_t result_data_size = data_offset + addr.size() + data.size(); - - // Create result buffer including space for checksum - std::vector resultbuf(result_data_size + 2, 0); - - // Set hardcoded values - resultbuf[0x0f] = 0x71; - resultbuf[0x10] = 0x0f; - resultbuf[0x11] = 0x55; - - // Copy address in reverse - for (size_t i = 0; i < addr.size(); i++) - { - resultbuf[data_offset + addr.size() - i - 1] = addr[i]; - } - - // Copy data - std::copy(data.begin(), data.end(), resultbuf.begin() + data_offset + addr.size()); - - // Reverse bytes in specified range - for (size_t i = inverse_offset; i < inverse_offset + addr.size() + 3; i++) - { - resultbuf[i] = reverse_8(resultbuf[i]); - } - - // Add CRC - uint16_t crc = crc16(addr, data); - resultbuf[result_data_size] = crc & 0xFF; - resultbuf[result_data_size + 1] = (crc >> 8) & 0xFF; - - return resultbuf; - } - - std::vector prepare_payload(const std::vector &addr, const std::vector &data) - { - auto payload = get_rf_payload(addr, data); - - // Initialize whitening - WhiteningContext context; - whitening_init(0x25, context); - - // Apply whitening to the payload - whitening_encode(payload, context); - - // Return only the portion after 0xf bytes - return std::vector(payload.begin() + 0xf, payload.end()); - } - } // namespace brmesh -} // namespace esphome diff --git a/components/brmesh/protocol.h b/components/brmesh/protocol.h deleted file mode 100644 index ee25c20..0000000 --- a/components/brmesh/protocol.h +++ /dev/null @@ -1,18 +0,0 @@ -#pragma once - -#include -#include -#include -#include "utils.h" - -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}; - - 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/q.h b/components/brmesh/q.h deleted file mode 100644 index aedc24f..0000000 --- a/components/brmesh/q.h +++ /dev/null @@ -1,72 +0,0 @@ -#pragma once - -namespace esphome { -namespace brmesh { - -template -class Q { -public: - class Node; - - constexpr Q() = default; - - bool enqueue(T &); - T *dequeue(); - T *peek(); -private: - Node *head_ = nullptr; - Node **tail_ = &head_; -}; - -template -class Q::Node { -public: - constexpr Node() = default; - - bool enqueue(Node *&head, Node **&tail); - static Node *dequeue(Node *&head, Node **&tail); -private: - Node *next_ = nullptr; -}; - -template -bool Q::enqueue(T &node) { - return static_cast(node).enqueue(head_, tail_); -} - -template -T *Q::dequeue() { - auto *node = Node::dequeue(head_, tail_); - return dynamic_cast(node); -} - -template -T *Q::peek() { - auto *node = head_; - return dynamic_cast(node); -} - - -template -bool Q::Node::enqueue(Node *&head, Node **&tail) { - const bool first = tail == &head; - *tail = this; - tail = &next_; - return first; -} - -template -Q::Node *Q::Node::dequeue(Node *&head, Node **&tail) { - auto *node = head; - if (node == nullptr) - return nullptr; - - head = node->next_; - if (head == nullptr) - tail = &head; - - return node; -} - -} // namespace brmesh -} // namespace esphome diff --git a/components/brmesh/utils.cpp b/components/brmesh/utils.cpp deleted file mode 100644 index f3a8eb3..0000000 --- a/components/brmesh/utils.cpp +++ /dev/null @@ -1,126 +0,0 @@ -#include -#include -#include "esphome/core/log.h" -#include "utils.h" - -namespace esphome -{ - namespace brmesh - { - uint8_t reverse_8(uint8_t d) - { - uint8_t result = 0; - for (int i = 0; i < 8; i++) - { - result |= ((d >> i) & 1) << (7 - i); - } - return result; - } - - uint16_t reverse_16(uint16_t d) - { - uint16_t result = 0; - for (int i = 0; i < 16; i++) - { - result |= ((d >> i) & 1) << (15 - i); - } - return result; - } - - uint16_t crc16(const std::vector &addr, const std::vector &data) - { - uint16_t crc = 0xffff; - - // Process address in reverse - for (auto it = addr.rbegin(); it != addr.rend(); ++it) - { - crc ^= (static_cast(*it) << 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; - } - } - } - - // Process data - for (size_t i = 0; i < data.size(); i++) - { - crc ^= (static_cast(reverse_8(data[i])) << 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; - } - } - } - - crc = ~reverse_16(crc); - return crc; - } - - void whitening_init(uint32_t val, WhiteningContext &ctx) - { - uint32_t v0[] = {(val >> 5), (val >> 4), (val >> 3), (val >> 2)}; - - ctx.f_0x0 = 1; - ctx.f_0x4 = v0[0] & 1; - ctx.f_0x8 = v0[1] & 1; - ctx.f_0xc = v0[2] & 1; - ctx.f_0x10 = v0[3] & 1; - ctx.f_0x14 = (val >> 1) & 1; - ctx.f_0x18 = val & 1; - } - - void whitening_encode(std::vector &data, WhiteningContext &ctx) - { - for (size_t i = 0; i < data.size(); i++) - { - uint32_t varC = ctx.f_0xc; - uint32_t var14 = ctx.f_0x14; - uint32_t var18 = ctx.f_0x18; - uint32_t var10 = ctx.f_0x10; - uint32_t var8 = var14 ^ ctx.f_0x8; - uint32_t var4 = var10 ^ ctx.f_0x4; - uint32_t _var = var18 ^ varC; - uint32_t var0 = _var ^ ctx.f_0x0; - - uint8_t c = data[i]; - data[i] = ((c & 0x80) ^ ((var8 ^ var18) << 7)) + ((c & 0x40) ^ (var0 << 6)) + ((c & 0x20) ^ (var4 << 5)) + ((c & 0x10) ^ (var8 << 4)) + ((c & 0x08) ^ (_var << 3)) + ((c & 0x04) ^ (var10 << 2)) + ((c & 0x02) ^ (var14 << 1)) + ((c & 0x01) ^ (var18 << 0)); - - ctx.f_0x8 = var4; - ctx.f_0xc = var8; - ctx.f_0x10 = var8 ^ varC; - ctx.f_0x14 = var0 ^ var10; - ctx.f_0x18 = var4 ^ var14; - ctx.f_0x0 = var8 ^ var18; - ctx.f_0x4 = var0; - } - } - - std::vector vector_to_hex_string(std::vector &data) - { - std::vector hex_str(data.size() * 2 + 1); // Allocate the vector with the required size - for (size_t i = 0; i < data.size(); i++) - { - sprintf(hex_str.data() + (i * 2), "%02X", data[i]); - } - hex_str[data.size() * 2] = '\0'; // Ensure null termination - return hex_str; - } - } // namespace brmesh -} // namespace esphome \ No newline at end of file diff --git a/components/brmesh/utils.h b/components/brmesh/utils.h deleted file mode 100644 index 9783096..0000000 --- a/components/brmesh/utils.h +++ /dev/null @@ -1,36 +0,0 @@ -#pragma once - -#include -#include - -namespace esphome -{ - namespace brmesh - { - // Bit manipulation utilities - uint8_t reverse_8(uint8_t d); - uint16_t reverse_16(uint16_t d); - - // CRC calculation - uint16_t crc16(const std::vector &addr, const std::vector &data); - - // Whitening context and functions - struct WhiteningContext - { - uint32_t f_0x0; - uint32_t f_0x4; - uint32_t f_0x8; - uint32_t f_0xc; - uint32_t f_0x10; - uint32_t f_0x14; - uint32_t f_0x18; - - WhiteningContext() : f_0x0(0), f_0x4(0), f_0x8(0), f_0xc(0), f_0x10(0), f_0x14(0), f_0x18(0) {} - }; - - void whitening_init(uint32_t val, WhiteningContext &ctx); - void whitening_encode(std::vector &data, WhiteningContext &ctx); - - std::vector vector_to_hex_string(std::vector &data); - } // namespace brmesh -} // namespace esphome \ No newline at end of file diff --git a/components/brmesh/whitening.cpp b/components/brmesh/whitening.cpp index eee79ae..6a44c7a 100644 --- a/components/brmesh/whitening.cpp +++ b/components/brmesh/whitening.cpp @@ -1,5 +1,4 @@ #include "whitening.h" -#include "debug.h" #include "esphome/core/log.h" #include "esphome/core/helpers.h" From 98e5dd0a5ca31188637fa772bcd00678897c630a Mon Sep 17 00:00:00 2001 From: Jonas Rabenstein Date: Tue, 23 Dec 2025 10:26:24 +0100 Subject: [PATCH 03/10] use esphome::crc16 --- components/brmesh/network.cpp | 41 ++++++++++------------------------- 1 file changed, 12 insertions(+), 29 deletions(-) diff --git a/components/brmesh/network.cpp b/components/brmesh/network.cpp index 68f7c26..a521317 100644 --- a/components/brmesh/network.cpp +++ b/components/brmesh/network.cpp @@ -65,29 +65,15 @@ uint8_t crc(const std::span &bytes, uint8_t init = 0x00) { 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); +uint16_t crc16(const std::span &data, const uint16_t seed = 0xFFFF) { + return ~esphome::crc16( + data.data(), + data.size(), + seed, + 0x1021, + true, // refin + true // refout + ); } std::span inner(const std::span &buffer, Light::Id id, const std::span &light_data) { @@ -158,7 +144,7 @@ std::span command(const std::span &buffer, const Key &key, Lig } 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 ADDRESS = { 0xC3, 0x43, 0x83 }; constexpr std::array PREFIX = { 0x71, 0x0F, 0x55 }; const auto prefix = subspan(buffer, 0, PREFIX.size()); @@ -188,10 +174,9 @@ std::span payload(const std::span &buffer, const Key &key, Lig std::copy(PREFIX.cbegin(), PREFIX.cend(), prefix.begin()); // copy - std::reverse_copy(ADDRESS.cbegin(), ADDRESS.cend(), address.begin()); + std::copy(ADDRESS.cbegin(), ADDRESS.cend(), address.begin()); - - uint16_t crc_value = crc16(ADDRESS, command); + uint16_t crc_value = crc16(buffer.subspan(prefix.size(), address.size() + command.size())); crc[0] = crc_value; crc[1] = crc_value >> 8; @@ -200,8 +185,6 @@ std::span payload(const std::span &buffer, const Key &key, Lig 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()); From 207e5d88a2f887cd045a1f18c4a5b56f0b2af2e3 Mon Sep 17 00:00:00 2001 From: Jonas Rabenstein Date: Sat, 27 Dec 2025 20:36:41 +0100 Subject: [PATCH 04/10] fixup! wip --- components/fastcon/__init__.py | 3 - components/fastcon/fastcon_controller.cpp | 275 ---------------------- components/fastcon/fastcon_controller.h | 90 ------- components/fastcon/fastcon_controller.py | 78 ------ components/fastcon/fastcon_light.cpp | 71 ------ components/fastcon/fastcon_light.h | 35 --- components/fastcon/light.py | 37 --- components/fastcon/protocol.cpp | 60 ----- components/fastcon/protocol.h | 18 -- components/fastcon/utils.cpp | 126 ---------- components/fastcon/utils.h | 36 --- 11 files changed, 829 deletions(-) delete mode 100644 components/fastcon/__init__.py delete mode 100644 components/fastcon/fastcon_controller.cpp delete mode 100644 components/fastcon/fastcon_controller.h delete mode 100644 components/fastcon/fastcon_controller.py delete mode 100644 components/fastcon/fastcon_light.cpp delete mode 100644 components/fastcon/fastcon_light.h delete mode 100644 components/fastcon/light.py delete mode 100644 components/fastcon/protocol.cpp delete mode 100644 components/fastcon/protocol.h delete mode 100644 components/fastcon/utils.cpp delete mode 100644 components/fastcon/utils.h diff --git a/components/fastcon/__init__.py b/components/fastcon/__init__.py deleted file mode 100644 index 9b07c9a..0000000 --- a/components/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/fastcon/fastcon_controller.cpp b/components/fastcon/fastcon_controller.cpp deleted file mode 100644 index 4a0180e..0000000 --- a/components/fastcon/fastcon_controller.cpp +++ /dev/null @@ -1,275 +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; - - const auto str = format_hex_pretty(cmd.data); - ESP_LOGI(TAG, "cmd: %s", str.c_str()); - - memcpy(&adv_data_raw[adv_data_len], cmd.data.data(), cmd.data.size()); - adv_data_len += cmd.data.size(); - - const auto adv = format_hex_pretty(adv_data_raw); - ESP_LOGI(TAG, "adv: %s", adv.c_str()); - 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/fastcon/fastcon_controller.h b/components/fastcon/fastcon_controller.h deleted file mode 100644 index cf78107..0000000 --- a/components/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/ble.h" - -namespace esphome -{ - namespace fastcon - { - - class FastconController : public Component, public esp32_ble::GAPEventHandler - { - 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/fastcon/fastcon_controller.py b/components/fastcon/fastcon_controller.py deleted file mode 100644 index a3361e1..0000000 --- a/components/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/fastcon/fastcon_light.cpp b/components/fastcon/fastcon_light.cpp deleted file mode 100644 index af7a983..0000000 --- a/components/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/fastcon/fastcon_light.h b/components/fastcon/fastcon_light.h deleted file mode 100644 index bf583e2..0000000 --- a/components/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/fastcon/light.py b/components/fastcon/light.py deleted file mode 100644 index 3ff7da4..0000000 --- a/components/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/fastcon/protocol.cpp b/components/fastcon/protocol.cpp deleted file mode 100644 index 549b4be..0000000 --- a/components/fastcon/protocol.cpp +++ /dev/null @@ -1,60 +0,0 @@ -#include -#include "protocol.h" - -namespace esphome -{ - namespace fastcon - { - std::vector get_rf_payload(const std::vector &addr, const std::vector &data) - { - const size_t data_offset = 0x12; - const size_t inverse_offset = 0x0f; - const size_t result_data_size = data_offset + addr.size() + data.size(); - - // Create result buffer including space for checksum - std::vector resultbuf(result_data_size + 2, 0); - - // Set hardcoded values - resultbuf[0x0f] = 0x71; - resultbuf[0x10] = 0x0f; - resultbuf[0x11] = 0x55; - - // Copy address in reverse - for (size_t i = 0; i < addr.size(); i++) - { - resultbuf[data_offset + addr.size() - i - 1] = addr[i]; - } - - // Copy data - std::copy(data.begin(), data.end(), resultbuf.begin() + data_offset + addr.size()); - - // Reverse bytes in specified range - for (size_t i = inverse_offset; i < inverse_offset + addr.size() + 3; i++) - { - resultbuf[i] = reverse_8(resultbuf[i]); - } - - // Add CRC - uint16_t crc = crc16(addr, data); - resultbuf[result_data_size] = crc & 0xFF; - resultbuf[result_data_size + 1] = (crc >> 8) & 0xFF; - - return resultbuf; - } - - std::vector prepare_payload(const std::vector &addr, const std::vector &data) - { - auto payload = get_rf_payload(addr, data); - - // Initialize whitening - WhiteningContext context; - whitening_init(0x25, context); - - // Apply whitening to the payload - whitening_encode(payload, context); - - // 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 diff --git a/components/fastcon/protocol.h b/components/fastcon/protocol.h deleted file mode 100644 index 930acab..0000000 --- a/components/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/fastcon/utils.cpp b/components/fastcon/utils.cpp deleted file mode 100644 index 371ae09..0000000 --- a/components/fastcon/utils.cpp +++ /dev/null @@ -1,126 +0,0 @@ -#include -#include -#include "esphome/core/log.h" -#include "utils.h" - -namespace esphome -{ - namespace fastcon - { - uint8_t reverse_8(uint8_t d) - { - uint8_t result = 0; - for (int i = 0; i < 8; i++) - { - result |= ((d >> i) & 1) << (7 - i); - } - return result; - } - - uint16_t reverse_16(uint16_t d) - { - uint16_t result = 0; - for (int i = 0; i < 16; i++) - { - result |= ((d >> i) & 1) << (15 - i); - } - return result; - } - - uint16_t crc16(const std::vector &addr, const std::vector &data) - { - uint16_t crc = 0xffff; - - // Process address in reverse - for (auto it = addr.rbegin(); it != addr.rend(); ++it) - { - crc ^= (static_cast(*it) << 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; - } - } - } - - // Process data - for (size_t i = 0; i < data.size(); i++) - { - crc ^= (static_cast(reverse_8(data[i])) << 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; - } - } - } - - crc = ~reverse_16(crc); - return crc; - } - - void whitening_init(uint32_t val, WhiteningContext &ctx) - { - uint32_t v0[] = {(val >> 5), (val >> 4), (val >> 3), (val >> 2)}; - - ctx.f_0x0 = 1; - ctx.f_0x4 = v0[0] & 1; - ctx.f_0x8 = v0[1] & 1; - ctx.f_0xc = v0[2] & 1; - ctx.f_0x10 = v0[3] & 1; - ctx.f_0x14 = (val >> 1) & 1; - ctx.f_0x18 = val & 1; - } - - void whitening_encode(std::vector &data, WhiteningContext &ctx) - { - for (size_t i = 0; i < data.size(); i++) - { - uint32_t varC = ctx.f_0xc; - uint32_t var14 = ctx.f_0x14; - uint32_t var18 = ctx.f_0x18; - uint32_t var10 = ctx.f_0x10; - uint32_t var8 = var14 ^ ctx.f_0x8; - uint32_t var4 = var10 ^ ctx.f_0x4; - uint32_t _var = var18 ^ varC; - uint32_t var0 = _var ^ ctx.f_0x0; - - uint8_t c = data[i]; - data[i] = ((c & 0x80) ^ ((var8 ^ var18) << 7)) + ((c & 0x40) ^ (var0 << 6)) + ((c & 0x20) ^ (var4 << 5)) + ((c & 0x10) ^ (var8 << 4)) + ((c & 0x08) ^ (_var << 3)) + ((c & 0x04) ^ (var10 << 2)) + ((c & 0x02) ^ (var14 << 1)) + ((c & 0x01) ^ (var18 << 0)); - - ctx.f_0x8 = var4; - ctx.f_0xc = var8; - ctx.f_0x10 = var8 ^ varC; - ctx.f_0x14 = var0 ^ var10; - ctx.f_0x18 = var4 ^ var14; - ctx.f_0x0 = var8 ^ var18; - ctx.f_0x4 = var0; - } - } - - std::vector vector_to_hex_string(std::vector &data) - { - std::vector hex_str(data.size() * 2 + 1); // Allocate the vector with the required size - for (size_t i = 0; i < data.size(); i++) - { - sprintf(hex_str.data() + (i * 2), "%02X", data[i]); - } - hex_str[data.size() * 2] = '\0'; // Ensure null termination - return hex_str; - } - } // namespace fastcon -} // namespace esphome \ No newline at end of file diff --git a/components/fastcon/utils.h b/components/fastcon/utils.h deleted file mode 100644 index c243d9c..0000000 --- a/components/fastcon/utils.h +++ /dev/null @@ -1,36 +0,0 @@ -#pragma once - -#include -#include - -namespace esphome -{ - namespace fastcon - { - // Bit manipulation utilities - uint8_t reverse_8(uint8_t d); - uint16_t reverse_16(uint16_t d); - - // CRC calculation - uint16_t crc16(const std::vector &addr, const std::vector &data); - - // Whitening context and functions - struct WhiteningContext - { - uint32_t f_0x0; - uint32_t f_0x4; - uint32_t f_0x8; - uint32_t f_0xc; - uint32_t f_0x10; - uint32_t f_0x14; - uint32_t f_0x18; - - WhiteningContext() : f_0x0(0), f_0x4(0), f_0x8(0), f_0xc(0), f_0x10(0), f_0x14(0), f_0x18(0) {} - }; - - void whitening_init(uint32_t val, WhiteningContext &ctx); - void whitening_encode(std::vector &data, WhiteningContext &ctx); - - std::vector vector_to_hex_string(std::vector &data); - } // namespace fastcon -} // namespace esphome \ No newline at end of file From 1adda1e65e82f3615ad45e57845ac85747a866cf Mon Sep 17 00:00:00 2001 From: Jonas Rabenstein Date: Sat, 27 Dec 2025 20:39:13 +0100 Subject: [PATCH 05/10] fixup! working --- components/brmesh/whitening.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/brmesh/whitening.cpp b/components/brmesh/whitening.cpp index 6a44c7a..767d788 100644 --- a/components/brmesh/whitening.cpp +++ b/components/brmesh/whitening.cpp @@ -15,7 +15,7 @@ Whitening::use(bool encode, const LUT &lut, State state, std::span byte const bool log = (bytes.size() > 1 || bytes[0] != 0x00); if (log) - ESP_LOGV(TAG, "%s: input: %s", encode ? "encode" : "decode", + ESP_LOGW(TAG, "%s: input: (%02hhX) %s", encode ? "encode" : "decode", state, format_hex_pretty(&bytes[0], bytes.size()).c_str()); else ESP_LOGVV(TAG, "%s: input: %s", encode ? "encode" : "decode", From 869cf85ff9c8e5f7fffbae8a266f62ea1885e1d7 Mon Sep 17 00:00:00 2001 From: Jonas Rabenstein Date: Sat, 27 Dec 2025 20:39:13 +0100 Subject: [PATCH 06/10] fixup! working --- components/brmesh/whitening.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/brmesh/whitening.cpp b/components/brmesh/whitening.cpp index 767d788..3658638 100644 --- a/components/brmesh/whitening.cpp +++ b/components/brmesh/whitening.cpp @@ -28,7 +28,7 @@ Whitening::use(bool encode, const LUT &lut, State state, std::span byte } if (log) - ESP_LOGV(TAG, "%s: output: %s", encode ? "encode" : "decode", + ESP_LOGV(TAG, "%s: output: (%02hhX) %s", encode ? "encode" : "decode", state, format_hex_pretty(&bytes[0], bytes.size()).c_str()); else ESP_LOGVV(TAG, "%s: output: %s", encode ? "encode" : "decode", From 820ce79b2f26c9d620c22e1ca7c86ec3e2ea710e Mon Sep 17 00:00:00 2001 From: Jonas Rabenstein Date: Sat, 27 Dec 2025 20:43:46 +0100 Subject: [PATCH 07/10] drop whitening warmup 0x25->0x7B --- components/brmesh/network.cpp | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/components/brmesh/network.cpp b/components/brmesh/network.cpp index a521317..ba3458f 100644 --- a/components/brmesh/network.cpp +++ b/components/brmesh/network.cpp @@ -189,11 +189,7 @@ std::span payload(const std::span &buffer, const Key &key, Lig //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); - + auto whitening = Whitening::from_val(0x7B); whitening.encode(result); return result; From 19587a0b73635a2964192ad81a88be0d28945bdf Mon Sep 17 00:00:00 2001 From: Jonas Rabenstein Date: Sat, 27 Dec 2025 20:48:27 +0100 Subject: [PATCH 08/10] zero fill is required --- components/brmesh/network.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/components/brmesh/network.cpp b/components/brmesh/network.cpp index ba3458f..b0e68f1 100644 --- a/components/brmesh/network.cpp +++ b/components/brmesh/network.cpp @@ -94,7 +94,6 @@ std::span inner(const std::span &buffer, Light::Id id, const s 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(), From b83f7b8e16e3e88bb182e39579272d094f7be67b Mon Sep 17 00:00:00 2001 From: Jonas Rabenstein Date: Sat, 27 Dec 2025 20:52:21 +0100 Subject: [PATCH 09/10] fixup! use esphome::crc16 --- components/brmesh/network.cpp | 17 +++++------------ 1 file changed, 5 insertions(+), 12 deletions(-) diff --git a/components/brmesh/network.cpp b/components/brmesh/network.cpp index b0e68f1..9f39e33 100644 --- a/components/brmesh/network.cpp +++ b/components/brmesh/network.cpp @@ -65,17 +65,6 @@ uint8_t crc(const std::span &bytes, uint8_t init = 0x00) { return init; } -uint16_t crc16(const std::span &data, const uint16_t seed = 0xFFFF) { - return ~esphome::crc16( - data.data(), - data.size(), - seed, - 0x1021, - true, // refin - true // refout - ); -} - std::span inner(const std::span &buffer, Light::Id id, const std::span &light_data) { if (id.kind() != 'L') return {}; @@ -169,13 +158,17 @@ std::span payload(const std::span &buffer, const Key &key, Lig if (result.empty()) return {}; + const auto crc_guarded = subspan(buffer, prefix.size(), address.size() + command.size()); + if (crc_guarded.empty()) + return {}; + // static values std::copy(PREFIX.cbegin(), PREFIX.cend(), prefix.begin()); // copy std::copy(ADDRESS.cbegin(), ADDRESS.cend(), address.begin()); - uint16_t crc_value = crc16(buffer.subspan(prefix.size(), address.size() + command.size())); + uint16_t crc_value = ~crc16(&crc_guarded[0], crc_guarded.size(), 0xFFFF, true, true); crc[0] = crc_value; crc[1] = crc_value >> 8; From e797cdb34970edd8bf9d17205414a38b85babec5 Mon Sep 17 00:00:00 2001 From: Jonas Rabenstein Date: Sun, 4 Jan 2026 19:19:00 +0100 Subject: [PATCH 10/10] wip --- components/brmesh/address.h | 35 +++++++++++++++++++++++++++++++++++ components/brmesh/light.cpp | 20 +++++++++++++++++++- components/brmesh/light.h | 29 ++++++----------------------- components/brmesh/light.py | 6 +++--- components/brmesh/network.cpp | 12 ++++++------ components/brmesh/output.h | 35 +++++++++++++++++++++++++++++++++++ 6 files changed, 104 insertions(+), 33 deletions(-) create mode 100644 components/brmesh/address.h create mode 100644 components/brmesh/output.h diff --git a/components/brmesh/address.h b/components/brmesh/address.h new file mode 100644 index 0000000..ba3a1b0 --- /dev/null +++ b/components/brmesh/address.h @@ -0,0 +1,35 @@ +#pragma once +#include +#include +#include + +#include "esphome/core/helpers.h" + +namespace esphome { +namespace brmesh { + +class Address : public std::array { + static constexpr uint8_t reverse(uint8_t x) { + x = ((x & 0xAA) >> 1) | ((x & 0x55) << 1); + x = ((x & 0xCC) >> 2) | ((x & 0x33) << 2); + x = ((x & 0xF0) >> 4) | ((x & 0x0F) << 4); + return x; + } +public: + constexpr Address(const uint8_t a, const uint8_t b, const uint8_t c) + : array{reverse(c), reverse(b), reverse(a)} {} + + template + std::span set(const std::span &dst) const { + return set(std::span(dst)); + } + + std::span set(const std::span &dst) const { + if (dst.size() < size()) + return {}; + std::copy(cbegin(), cend(), &dst[0]); + return dst.subspan(0, size()); + } +}; +} // namespace brmesh +} // namespace esphome diff --git a/components/brmesh/light.cpp b/components/brmesh/light.cpp index 1dbf674..0f0bb8a 100644 --- a/components/brmesh/light.cpp +++ b/components/brmesh/light.cpp @@ -188,6 +188,24 @@ Light::advertise(bool advertise) { network_.advertise(id_, {}); } +std::span +Light::command(const std::span &output) const +{ + const auto hdr = output.subspan(0, 2); + const auto dst = output.subspan(hdr.size()); + const auto src = std::span(payload_.cbegin(), payload_length_); + + if (output.size() < hdr.size() + src.size()) + return {}; + + hdr[0] = 2 | (((0xfffffff & (src.size() + 1)) << 4)); + hdr[1] = id_.value(); + std::copy(src.begin(), src.end(), dst.begin()); + std::ranges::fill(dst.subspan(src.size()), 0); + + return output; +} + void Light::loop() { disable_loop(); @@ -247,7 +265,7 @@ Light::write_state(light::LightState *state) { count_ = 0; //ESP_LOGD(TAG, "%c%02hhX: state: %s", id_.kind(), id_.value(), // format_hex_pretty(&payload[0], payload.size()).c_str()); - enable_loop(); + //enable_loop(); } } // namespace brmesh diff --git a/components/brmesh/light.h b/components/brmesh/light.h index cb20922..dda261d 100644 --- a/components/brmesh/light.h +++ b/components/brmesh/light.h @@ -23,26 +23,24 @@ public: class Id : public std::variant { public: using variant::variant; + constexpr Id(uint8_t value) : variant(Single{value}) {} static constexpr char kind(Single) { return 'L'; } static constexpr char kind(Group) { return 'G'; } - constexpr char kind() { + constexpr char kind() const { return std::visit([](auto &&value) { return Id::kind(value); }, *this); } static constexpr uint8_t value(Single value) { return static_cast(value); } static constexpr uint8_t value(Group value) { return static_cast(value); } - constexpr uint8_t value() { + constexpr uint8_t value() const { return std::visit([](auto &&value) { return Id::value(value); }, *this); } }; Light(Id id, Network *network) : id_(id), network_(*network) {} + Light(Single id, Network *network) : Light(Id{id}, network) {} -// 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 { @@ -59,31 +57,16 @@ public: // callback void advertise(bool advertise); + + std::span command(const std::span &output) const; private: 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_; - 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 - }; } // namespace brmesh diff --git a/components/brmesh/light.py b/components/brmesh/light.py index f9954b2..03f9a38 100644 --- a/components/brmesh/light.py +++ b/components/brmesh/light.py @@ -3,7 +3,7 @@ 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.const import CONF_LIGHT_ID, CONF_OUTPUT_ID, CONF_REPEAT from esphome.core import HexInt from esphome.components import light, esp32_ble @@ -15,14 +15,14 @@ AUTO_LOAD = ["light"] CONF_NETWORK_ID = "network" Light = brmesh_ns.class_("Light", light.LightOutput, cg.Component) -Single = Light.enum("Single", True) -Group = Light.enum("Group", True); +Single = Light.enum('Single', is_class=True) CONFIG_SCHEMA = light.light_schema(Light, light.LightType.RGB).extend({ cv.Required(CONF_NETWORK_ID): cv.use_id(Network), cv.Required(CONF_LIGHT_ID): cv.int_range(min=1, max=255), # TODO: use CONF_BLE_ID from CONF_NETWORK_ID... cv.GenerateID(esp32_ble.CONF_BLE_ID): cv.use_id(esp32_ble.ESP32BLE), + cv.Optional(CONF_REPEAT, default=5): cv.positive_not_null_int, }) diff --git a/components/brmesh/network.cpp b/components/brmesh/network.cpp index 9f39e33..a67c981 100644 --- a/components/brmesh/network.cpp +++ b/components/brmesh/network.cpp @@ -1,5 +1,6 @@ #include "network.h" +#include "address.h" #include "light.h" #include "whitening.h" @@ -132,8 +133,9 @@ std::span command(const std::span &buffer, const Key &key, Lig } 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 = { 0xC3, 0x43, 0x83 }; - constexpr std::array PREFIX = { 0x71, 0x0F, 0x55 }; + constexpr Address ADDRESS = { 0xC1, 0xC2, 0xC3 }; + //constexpr std::array PREFIX = { 0x71, 0x0F, 0x55 }; + constexpr std::array PREFIX = { 0x8E, 0xF0, 0xAA }; const auto prefix = subspan(buffer, 0, PREFIX.size()); if (prefix.empty()) @@ -175,8 +177,8 @@ std::span payload(const std::span &buffer, const Key &key, Lig //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:prefix) + // byte = reverse_bits(byte); //ESP_LOGV(TAG, "%c%02hhX: reverse: %s", id.kind(), id.value(), // format_hex_pretty(&result[0], result.size()).c_str()); @@ -254,11 +256,9 @@ Network::advertise(Light::Id id, const std::span &payload) { 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()); diff --git a/components/brmesh/output.h b/components/brmesh/output.h new file mode 100644 index 0000000..51c8e31 --- /dev/null +++ b/components/brmesh/output.h @@ -0,0 +1,35 @@ +#pragma once + +namespace esphome { +namespace brmesh { + +class Output : public Component, public light::LightOutput +{ +public: + virtual const char *kind() const = 0; + virtual uint8_t id() const = 0; + + virtual std::span command(const std::span &output) const = 0; + + virtual void advertise(bool advertise) const; + + // interface: Component + void dump_config() override; + float get_setup_priority() const override { + return setup_priority::AFTER_BLUETOOTH; + } + void setup() override; + void loop() override; + + // interface: LightOutput + light::LightTraits get_traits() override; + void setup_state(light::LightState *state) override; + void update_state(light::LightState *state) override; + void write_state(light::LightState *state) override; + +private: + Network &network_; +}; + +} // namespace brmesh +} // namespace esphome