Compare commits

...

10 commits

Author SHA1 Message Date
Jonas Rabenstein
e797cdb349 wip 2026-01-04 19:19:00 +01:00
Jonas Rabenstein
b83f7b8e16 fixup! use esphome::crc16 2025-12-27 20:52:21 +01:00
Jonas Rabenstein
19587a0b73 zero fill is required 2025-12-27 20:48:27 +01:00
Jonas Rabenstein
820ce79b2f drop whitening warmup 0x25->0x7B 2025-12-27 20:43:46 +01:00
Jonas Rabenstein
869cf85ff9 fixup! working 2025-12-27 20:39:13 +01:00
Jonas Rabenstein
1adda1e65e fixup! working 2025-12-27 20:39:13 +01:00
Jonas Rabenstein
207e5d88a2 fixup! wip 2025-12-27 20:36:41 +01:00
Jonas Rabenstein
98e5dd0a5c use esphome::crc16 2025-12-23 10:26:24 +01:00
Jonas Rabenstein
d10ec6546d cleanup 2025-12-23 10:10:00 +01:00
Jonas Rabenstein
6dee644a12 working 2025-12-23 10:03:07 +01:00
37 changed files with 621 additions and 2642 deletions

View file

@ -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"]

View file

@ -0,0 +1,35 @@
#pragma once
#include <algorithm>
#include <array>
#include <span>
#include "esphome/core/helpers.h"
namespace esphome {
namespace brmesh {
class Address : public std::array<uint8_t, 3> {
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 <size_t size>
std::span<uint8_t> set(const std::span<uint8_t, size> &dst) const {
return set(std::span<uint8_t>(dst));
}
std::span<uint8_t> set(const std::span<uint8_t> &dst) const {
if (dst.size() < size())
return {};
std::copy(cbegin(), cend(), &dst[0]);
return dst.subspan(0, size());
}
};
} // namespace brmesh
} // namespace esphome

View file

@ -1,67 +1,88 @@
import esphome.codegen as cg import esphome.codegen as cg
import esphome.config_validation as cv import esphome.config_validation as cv
from esphome.const import CONF_ID from esphome.const import CONF_ID, CONF_TX_POWER
from esphome.core import HexInt from esphome.core import HexInt, TimePeriod
from esphome.components import esp32_ble 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" CONFIG_SCHEMA = cv.Schema([
CONF_ADV = "adv" {
CONF_INTERVAL = "interval" cv.GenerateID(): cv.declare_id(Network),
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),
cv.GenerateID(esp32_ble.CONF_BLE_ID): cv.use_id(esp32_ble.ESP32BLE), cv.GenerateID(esp32_ble.CONF_BLE_ID): cv.use_id(esp32_ble.ESP32BLE),
cv.Required(CONF_KEY): validate_key, 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] adv = config[CONF_ADV]
interval = adv[CONF_INTERVAL] interval = adv[CONF_INTERVAL]
if interval[CONF_MAX] < interval[CONF_MIN]: if interval[CONF_MAX] < interval[CONF_MIN]:
raise cv.Invalid( raise cv.Invalid(
f"{CONF_ADV}.{CONF_INTERVAL}.{CONF_MIN} ({interval[CONF_MIN]}) must be <= " f"{CONF_ADV}.{CONF_INTERVAL}.{CONF_MIN} ({interval[CONF_MIN]}) must be <= "
f"{CONF_ADV}.{CONF_INTERVLA}.{CONF_MAX} ({interval[CONF_MAX]})" f"{CONF_ADV}.{CONF_INTERVLA}.{CONF_MAX} ({interval[CONF_MAX]})"
) )
ble = await cg.get_variable(config[esp32_ble.CONF_BLE_ID]) 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) await cg.register_component(var, config)

View file

@ -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 <size_t addr_size, size_t data_size>
std::enable_if_t<(addr_size == 3 || addr_size == std::dynamic_extent), uint16_t>
crc16(const std::span<const uint8_t, addr_size> &addr, const std::span<const uint8_t, data_size> &data, const uint16_t init = 0xFFFF)
{
constexpr auto step = [](uint16_t result, uint8_t byte)->uint16_t{
result ^= static_cast<uint16_t>(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<uint8_t, 31> 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<uint8_t> Controller::single_control(uint32_t light_id_, const std::vector<uint8_t> &light_data)
{
std::vector<uint8_t> 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<uint8_t> Controller::generate_command(uint8_t n, uint32_t light_id_, const std::vector<uint8_t> &data, bool forward)
{
uint8_t& sequence = seq_;
// Create command body with header
std::vector<uint8_t> 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<uint8_t> addr = {DEFAULT_BLE_ADDRESS.begin(), DEFAULT_BLE_ADDRESS.end()};
return prepare_payload(addr, body);
}
std::span<uint8_t> Controller::payload(std::span<uint8_t, 24> &&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<uint8_t>(crc16);
crc[1] = static_cast<uint8_t>(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<uint8_t> command(std::span<uint8_t, 10> &&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

View file

@ -1,78 +0,0 @@
#pragma once
#include <queue>
#include <mutex>
#include <vector>
#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<uint8_t> single_control(uint32_t addr, const std::vector<uint8_t> &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<uint8_t> 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<uint8_t> generate_command(uint8_t n, uint32_t light_id_, const std::vector<uint8_t> &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<Light> q_ = {};
uint8_t seq_ = 0;
void advertise(Light &);
std::span<uint8_t> payload(std::span<uint8_t, 24> &span, const Light &light);
std::span<uint8_t> command(std::span<uint8_t, 24> &&span, const Light &light);
};
} // namespace brmesh
} // namespace esphome

View file

@ -1,31 +0,0 @@
#pragma once
#include "esphome/core/log.h"
#include "esphome/core/helpers.h"
#include <span>
namespace esphome {
namespace brmesh {
namespace {
template <typename...types>
void hexdump(const char *TAG, 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<types>(args)..., hex.c_str());
}
template <typename...types>
void hexdump(const char *TAG, const void *data, size_t size, const char *fmt, types&&...args) {
hexdump(TAG, static_cast<const uint8_t *>(data), size, fmt, std::forward<types>(args)...);
}
template <typename...types, typename base_type, size_t size>
void hexdump(const char *TAG, const std::span<base_type, size> &span, const char *fmt, types&&...args) {
hexdump(TAG, static_cast<const void*>(&span.front()), span.size_bytes(), fmt, std::forward<types>(args)...);
}
} // namespace
} // namespace brmesh
} // namespace esphome

View file

@ -1,3 +0,0 @@
from .fastcon_controller import CONFIG_SCHEMA, DEPENDENCIES, FastconController, to_code
__all__ = ["CONFIG_SCHEMA", "DEPENDENCIES", "FastconController", "to_code"]

View file

@ -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<uint8_t> &data)
{
std::lock_guard<std::mutex> 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<std::mutex> lock(queue_mutex_);
std::queue<Command> 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<std::mutex> 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<uint8_t> FastconController::get_light_data(light::LightState *state)
{
std::vector<uint8_t> 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<uint8_t>({0x00});
}
auto color_mode = values.get_color_mode();
bool has_white = (static_cast<uint8_t>(color_mode) & static_cast<uint8_t>(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<uint8_t>(brightness);
if (has_white)
{
return std::vector<uint8_t>({static_cast<uint8_t>(brightness)});
// DEBUG: when changing to white mode, this should be the payload:
// ff0000007f7f
}
bool has_rgb = (static_cast<uint8_t>(color_mode) & static_cast<uint8_t>(light::ColorCapability::RGB)) != 0;
if (has_rgb)
{
light_data[1] = static_cast<uint8_t>(values.get_blue() * 255.0f);
light_data[2] = static_cast<uint8_t>(values.get_red() * 255.0f);
light_data[3] = static_cast<uint8_t>(values.get_green() * 255.0f);
}
bool has_cold_warm = (static_cast<uint8_t>(color_mode) & static_cast<uint8_t>(light::ColorCapability::COLD_WARM_WHITE)) != 0;
if (has_cold_warm)
{
light_data[4] = static_cast<uint8_t>(values.get_warm_white() * 255.0f);
light_data[5] = static_cast<uint8_t>(values.get_cold_white() * 255.0f);
}
// TODO figure out if we can use these, and how
bool has_temp = (static_cast<uint8_t>(color_mode) & static_cast<uint8_t>(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<uint8_t> FastconController::single_control(uint32_t light_id_, const std::vector<uint8_t> &light_data)
{
std::vector<uint8_t> 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<uint8_t> FastconController::generate_command(uint8_t n, uint32_t light_id_, const std::vector<uint8_t> &data, bool forward)
{
static uint8_t sequence = 0;
// Create command body with header
std::vector<uint8_t> 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<uint8_t> addr = {DEFAULT_BLE_FASTCON_ADDRESS.begin(), DEFAULT_BLE_FASTCON_ADDRESS.end()};
return prepare_payload(addr, body);
}
} // namespace fastcon
} // namespace esphome

View file

@ -1,90 +0,0 @@
#pragma once
#include <queue>
#include <mutex>
#include <vector>
#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<uint8_t> get_light_data(light::LightState *state);
std::vector<uint8_t> single_control(uint32_t addr, const std::vector<uint8_t> &light_data);
void queueCommand(uint32_t light_id_, const std::vector<uint8_t> &data);
void clear_queue();
bool is_queue_empty() const
{
std::lock_guard<std::mutex> lock(queue_mutex_);
return queue_.empty();
}
size_t get_queue_size() const
{
std::lock_guard<std::mutex> lock(queue_mutex_);
return queue_.size();
}
void set_max_queue_size(size_t size) { max_queue_size_ = size; }
void set_mesh_key(std::array<uint8_t, 4> 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<uint8_t> data;
uint32_t timestamp;
uint8_t retries{0};
static constexpr uint8_t MAX_RETRIES = 3;
};
std::queue<Command> 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<uint8_t> generate_command(uint8_t n, uint32_t light_id_, const std::vector<uint8_t> &data, bool forward = true);
std::array<uint8_t, 4> 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

View file

@ -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]))

View file

@ -1,71 +0,0 @@
#include <algorithm>
#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

View file

@ -1,35 +0,0 @@
#pragma once
#include <array>
#include <vector>
#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

View file

@ -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))

View file

@ -1,60 +0,0 @@
#include <algorithm>
#include "protocol.h"
namespace esphome
{
namespace fastcon
{
std::vector<uint8_t> get_rf_payload(const std::vector<uint8_t> &addr, const std::vector<uint8_t> &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<uint8_t> 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<uint8_t> prepare_payload(const std::vector<uint8_t> &addr, const std::vector<uint8_t> &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<uint8_t>(payload.begin() + 0xf, payload.end());
}
} // namespace fastcon
} // namespace esphome

View file

@ -1,18 +0,0 @@
#pragma once
#include <vector>
#include <array>
#include <cstdint>
#include "utils.h"
namespace esphome
{
namespace fastcon
{
static const std::array<uint8_t, 4> DEFAULT_ENCRYPT_KEY = {0x5e, 0x36, 0x7b, 0xc4};
static const std::array<uint8_t, 3> DEFAULT_BLE_FASTCON_ADDRESS = {0xC1, 0xC2, 0xC3};
std::vector<uint8_t> get_rf_payload(const std::vector<uint8_t> &addr, const std::vector<uint8_t> &data);
std::vector<uint8_t> prepare_payload(const std::vector<uint8_t> &addr, const std::vector<uint8_t> &data);
} // namespace fastcon
} // namespace esphome

View file

@ -1,126 +0,0 @@
#include <vector>
#include <cstdio>
#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<uint8_t> &addr, const std::vector<uint8_t> &data)
{
uint16_t crc = 0xffff;
// Process address in reverse
for (auto it = addr.rbegin(); it != addr.rend(); ++it)
{
crc ^= (static_cast<uint16_t>(*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<uint16_t>(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<uint8_t> &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<char> vector_to_hex_string(std::vector<uint8_t> &data)
{
std::vector<char> 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

View file

@ -1,36 +0,0 @@
#pragma once
#include <cstdint>
#include <vector>
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<uint8_t> &addr, const std::vector<uint8_t> &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<uint8_t> &data, WhiteningContext &ctx);
std::vector<char> vector_to_hex_string(std::vector<uint8_t> &data);
} // namespace fastcon
} // namespace esphome

View file

@ -1,13 +1,12 @@
#include "light.h" #include "light.h"
#include "network.h"
#include "whitening.h" #include "whitening.h"
#include "protocol.h"
#include "debug.h"
#include "esphome/core/log.h" #include "esphome/core/log.h"
namespace { namespace {
const char *const TAG = "brmesh.light"; const char *const TAG = "brmesh.light";
} // namespace } // namespace
namespace esphome { namespace esphome {
@ -24,22 +23,39 @@ uint8_t crc(const std::span<uint8_t, size> &bytes, uint8_t init = 0x00) {
uint16_t crc16(uint8_t byte, uint16_t crc) { uint16_t crc16(uint8_t byte, uint16_t crc) {
crc ^= static_cast<uint16_t>(byte) << 8; crc ^= static_cast<uint16_t>(byte) << 8;
for (int j=0; j<4; ++j) {
uint16_t tmp = crc << 1; auto a = [](uint16_t crc) {
if (crc & 0x8000) for (int j=0; j<4; ++j) {
tmp ^= 0x1021; uint16_t tmp = crc << 1;
crc = tmp << 1; if (crc & 0x8000)
if (tmp & 0x8000) tmp ^= 0x1021;
crc ^= 0x1021; crc = tmp << 1;
} if (tmp & 0x8000)
return crc; 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<uint8_t, 3> &addr, const std::span<uint8_t> &data, const uint16_t seed = 0xFFFF) { uint16_t crc16(const std::array<uint8_t, 3> &addr, const std::span<uint8_t> &data, const uint16_t seed = 0xFFFF) {
auto crc = seed; auto crc = seed;
for (const auto &byte:addr) for (auto it = addr.rbegin(); it != addr.rend(); ++it)
crc = crc16(byte, crc); crc = crc16(*it, crc);
for (const auto &byte:data) for (const auto &byte:data)
crc = crc16(reverse_bits(byte), crc); crc = crc16(reverse_bits(byte), crc);
@ -47,11 +63,6 @@ uint16_t crc16(const std::array<uint8_t, 3> &addr, const std::span<uint8_t> &dat
return ~reverse_bits(crc); return ~reverse_bits(crc);
} }
template <size_t size, size_t input_size, typename type=uint8_t>
std::span<type, size> subspan(const std::span<type, input_size> &buffer, size_t offset) {
return std::span<type, size>(buffer.subspan(offset, size));
}
std::span<uint8_t> single_light_data(const std::span<uint8_t, 6> &buffer, const light::LightState &state) { std::span<uint8_t> single_light_data(const std::span<uint8_t, 6> &buffer, const light::LightState &state) {
const auto &values = state.current_values; const auto &values = state.current_values;
// 0 - On/Off Bit + 7-bit Brightness // 0 - On/Off Bit + 7-bit Brightness
@ -126,139 +137,26 @@ std::span<uint8_t> single_light_data(const std::span<uint8_t, 6> &buffer, const
} }
std::span<uint8_t> command(const std::span<uint8_t, 16> &buffer, const Key &key, Light::Id id, uint8_t seq, light::LightState &state) { std::span<uint8_t>
static constexpr uint8_t n = 5; // TODO group_light_data(const std::span<uint8_t, 6> &buffer, const light::LightState &state) {
static constexpr bool forward = true; // TODO ESP_LOGE(TAG, "not implemented: group light data");
const uint8_t i2 = (id.value() / 256); return {};
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<uint8_t> payload(const std::span<uint8_t, 24> &buffer, const Key &key, Light::Id id, uint8_t seq, light::LightState &state) { std::span<uint8_t> light_data(Light::Single, const std::span<uint8_t, 6> &buffer, light::LightState &state) {
// static values return single_light_data(buffer, state);
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<uint8_t> adv_data(const std::span<uint8_t, 31> &buffer, const Key &key, Light::Id id, uint8_t seq, light::LightState &state) { std::span<uint8_t> light_data(Light::Group, const std::span<uint8_t, 6> &buffer, light::LightState &state) {
const auto flags = buffer.subspan(0, 3); return group_light_data(buffer, state);
const auto length = buffer.subspan(3, 1);
const auto manufacturer = buffer.subspan(4, 3);
const auto payload = brmesh::payload(std::span<uint8_t, 24>(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());
} }
} // namespace } // 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 &param) {
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 &param) {
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 // interface: esphome::Component
void void
Light::dump_config() { Light::dump_config() {
auto *ble = esphome::esp32_ble::global_ble; ESP_LOGCONFIG(TAG, "light %c%hhd", id_.kind(), id_.value());
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);
} }
void void
@ -271,55 +169,62 @@ Light::setup() {
} }
ble->advertising_register_raw_advertisement_callback([this](bool advertise) { ble->advertising_register_raw_advertisement_callback([this](bool advertise) {
const bool advertising = advertising_; this->advertise(advertise);
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();
}); });
} }
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_, {});
}
std::span<uint8_t>
Light::command(const std::span<uint8_t> &output) const
{
const auto hdr = output.subspan(0, 2);
const auto dst = output.subspan(hdr.size());
const auto src = std::span<const uint8_t>(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 void
Light::loop() { Light::loop() {
disable_loop(); disable_loop();
if (!advertising_) { 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; return;
} }
if (state_ == nullptr) { if (payload_length_ == 0 || count_ >= 5) {
ESP_LOGD(TAG, "%c%02hhX: sleep", id_.kind(), id_.value()); ESP_LOGD(TAG, "%c%02hhX: no update", id_.kind(), id_.value());
return; return;
} }
++count_; ++count_;
auto *const state = state_; auto payload = std::span(&payload_[0], payload_length_);
ESP_LOGV(TAG, "%c%02hhX: advertise: %s", id_.kind(), id_.value(),
const auto seq = seq_++; format_hex_pretty(&payload[0], payload.size()).c_str());
std::array<uint8_t, 31> buffer; network_.advertise(id_, payload);
auto data = std::visit([buffer=std::span<uint8_t, 31>(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));
} }
// interface: esphome::light::LightOutput // interface: esphome::light::LightOutput
@ -339,211 +244,29 @@ Light::get_traits() {
void void
Light::setup_state(light::LightState *state) { Light::setup_state(light::LightState *state) {
// nothing to do
(void) state;
} }
void void
Light::update_state(light::LightState *state) { Light::update_state(light::LightState *state) {
// nothing to do
(void) state;
} }
#include "./fastcon/fastcon_controller.h"
void void
Light::write_state(light::LightState *state) { 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<uint8_t, 6>(payload_), *state).size(); const auto payload = std::visit([this, state](auto && id) {
hexdump(TAG, &payload_.front(), payload_length_, "%c%02hhX: state: %s", id_.kind(), id_.value()); return light_data(id, std::span<uint8_t, 6>(payload_), *state);
state_ = state; }, id_);
payload_length_ = payload.size();
count_ = 0; count_ = 0;
//ESP_LOGD(TAG, "%c%02hhX: state: %s", id_.kind(), id_.value(),
#if 0 // format_hex_pretty(&payload[0], payload.size()).c_str());
switch (state->current_values.get_color_mode()) //enable_loop();
// 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;
}
} }
} // namespace brmesh } // namespace brmesh
} // namespace esphome } // namespace esphome
#if 0
#include <algorithm>
#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<uint8_t, 6> &&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<uint8_t>(brightness);
if (color_mode(light::ColorCapability::RGB)) {
data[1] = static_cast<uint8_t>(values.get_blue() * 255.0f);
data[2] = static_cast<uint8_t>(values.get_red() * 255.0f);
data[3] = static_cast<uint8_t>(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<uint8_t>(values.get_warm_white() * 255.0f);
data[5] = static_cast<uint8_t>(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

View file

@ -13,7 +13,9 @@
namespace esphome { namespace esphome {
namespace brmesh { namespace brmesh {
class Light : public Component, public light::LightOutput, public esp32_ble::GAPEventHandler class Network;
class Light : public Component, public light::LightOutput
{ {
public: public:
enum class Single : uint8_t {}; enum class Single : uint8_t {};
@ -21,27 +23,23 @@ public:
class Id : public std::variant<Single, Group> { class Id : public std::variant<Single, Group> {
public: public:
using variant::variant; using variant::variant;
constexpr Id(uint8_t value) : variant(Single{value}) {}
static constexpr char kind(Single) { return 'L'; } static constexpr char kind(Single) { return 'L'; }
static constexpr char kind(Group) { return 'G'; } static constexpr char kind(Group) { return 'G'; }
constexpr char kind() { constexpr char kind() const {
return std::visit([](auto &&value) { return Id::kind(value); }, *this); return std::visit([](auto &&value) { return Id::kind(value); }, *this);
} }
static constexpr uint8_t value(Single value) { return static_cast<uint8_t>(value); } static constexpr uint8_t value(Single value) { return static_cast<uint8_t>(value); }
static constexpr uint8_t value(Group value) { return static_cast<uint8_t>(value); } static constexpr uint8_t value(Group value) { return static_cast<uint8_t>(value); }
constexpr uint8_t value() { constexpr uint8_t value() const {
return std::visit([](auto &&value) { return Id::value(value); }, *this); return std::visit([](auto &&value) { return Id::value(value); }, *this);
} }
}; };
Light(Id id, Network *network) : id_(id), network_(*network) {}
Light(Id id, const Key &key, Light(Single id, Network *network) : Light(Id{id}, network) {}
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) {}
// interface: Component // interface: Component
void dump_config() override; void dump_config() override;
@ -57,31 +55,18 @@ public:
void update_state(light::LightState *state) override; void update_state(light::LightState *state) override;
void write_state(light::LightState *state) override; void write_state(light::LightState *state) override;
// interface: GAPEventHandler // callback
void gap_event_handler(esp_gap_ble_cb_event_t, esp_ble_gap_cb_param_t *) override; void advertise(bool advertise);
std::span<uint8_t> command(const std::span<uint8_t> &output) const;
private: private:
light::LightState *state_ = nullptr;
Id id_; Id id_;
const Key key_; Network &network_;
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;
uint8_t count_ = 0; uint8_t count_ = 0;
bool advertising_ = false; bool advertising_ = false;
std::array<uint8_t, 6> payload_ = {}; std::array<uint8_t, 6> payload_ = {};
size_t payload_length_ = 0; size_t payload_length_ = 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<uint8_t> adv_data_(const std::span<uint8_t, 31> &, uint8_t seq, light::LightState &);
}; };
} // namespace brmesh } // namespace brmesh

View file

@ -2,91 +2,40 @@
import esphome.codegen as cg import esphome.codegen as cg
import esphome.config_validation as cv import esphome.config_validation as cv
from esphome.const import CONF_LIGHT_ID, CONF_OUTPUT_ID from esphome.cpp_generator import LambdaExpression
from esphome.const import CONF_LIGHT_ID, CONF_OUTPUT_ID, CONF_REPEAT
from esphome.core import HexInt from esphome.core import HexInt
from esphome.components import light, esp32_ble from esphome.components import light, esp32_ble
#from .brmesh import Controller from .brmesh import Network, brmesh_ns
DEPENDENCIES = ["esp32_ble"] DEPENDENCIES = ["esp32_ble"]
AUTO_LOAD = ["light"] AUTO_LOAD = ["light"]
CONF_KEY = "key" CONF_NETWORK_ID = "network"
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")
Light = brmesh_ns.class_("Light", light.LightOutput, cg.Component) Light = brmesh_ns.class_("Light", light.LightOutput, cg.Component)
Single = Light.enum("Single", True) Single = Light.enum('Single', is_class=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({ 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_NETWORK_ID): cv.use_id(Network),
cv.Required(CONF_KEY): validate_key,
cv.Required(CONF_LIGHT_ID): cv.int_range(min=1, max=255), 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),
cv.Optional(CONF_REPEAT, default=5): cv.positive_not_null_int,
}) })
async def to_code(config): async def to_code(config):
adv = config[CONF_ADV] network = await cg.get_variable(config[CONF_NETWORK_ID])
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]) ble = await cg.get_variable(config[esp32_ble.CONF_BLE_ID])
target = Single(config[CONF_LIGHT_ID]) target = Single(config[CONF_LIGHT_ID])
var = cg.new_Pvariable(config[CONF_OUTPUT_ID], target, config[CONF_KEY], var = cg.new_Pvariable(config[CONF_OUTPUT_ID], target, network)
interval[CONF_MIN], interval[CONF_MAX], #config[CONF_KEY],
adv[CONF_DURATION].total_milliseconds, # interval[CONF_MIN], interval[CONF_MAX])
adv[CONF_GAP].total_milliseconds) #esp32_ble.register_gap_event_handler(ble, var)
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 cg.register_component(var, config)
await light.register_light(var, config) await light.register_light(var, config)
cg.add_define("USE_ESP32_BLE_ADVERTISING")
cg.add_define("USE_ESP32_BLE_UUID")

View file

@ -0,0 +1,300 @@
#include "network.h"
#include "address.h"
#include "light.h"
#include "whitening.h"
#include <algorithm>
namespace {
const char *const TAG = "brmesh.network";
} // namespace
namespace esphome {
namespace brmesh {
namespace {
template <size_t SIZE, typename type>
std::span<type> subspan(const std::span<type, SIZE> &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 <size_t SIZE, typename type>
std::span<type> subspan(const std::span<type, SIZE> &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 <size_t size>
uint8_t crc(const std::span<uint8_t, size> &bytes, uint8_t init = 0x00) {
for (const auto& byte:bytes)
init += byte;
return init;
}
std::span<uint8_t> inner(const std::span<uint8_t> &buffer, Light::Id id, const std::span<uint8_t> &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());
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<uint8_t> command(const std::span<uint8_t> &buffer, const Key &key, Light::Id id, uint8_t seq, const std::span<uint8_t> &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<uint8_t> payload(const std::span<uint8_t> &buffer, const Key &key, Light::Id id, uint8_t seq, const std::span<uint8_t> &light_data) {
constexpr Address ADDRESS = { 0xC1, 0xC2, 0xC3 };
//constexpr std::array<uint8_t, 3> PREFIX = { 0x71, 0x0F, 0x55 };
constexpr std::array<uint8_t, 3> PREFIX = { 0x8E, 0xF0, 0xAA };
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 {};
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(&crc_guarded[0], crc_guarded.size(), 0xFFFF, true, true);
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);
//ESP_LOGV(TAG, "%c%02hhX: reverse: %s", id.kind(), id.value(),
// format_hex_pretty(&result[0], result.size()).c_str());
auto whitening = Whitening::from_val(0x7B);
whitening.encode(result);
return result;
}
std::span<uint8_t> adv_data(const std::span<uint8_t> &buffer, const Key &key, Light::Id id, uint8_t seq, const std::span<uint8_t> &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<uint8_t> &payload) {
if (payload.size() == 0) {
advertise_= false;
// TODO: disable ble advertise
return;
}
std::array<uint8_t, 31> buffer;
const auto data = adv_data(std::span<uint8_t>(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;
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));
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

View file

@ -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 <cstdint>
#include <span>
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<uint8_t> &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

View file

@ -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<uint8_t> command(const std::span<uint8_t> &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

View file

@ -1,16 +0,0 @@
#pragma once
#include <vector>
#include <array>
#include <cstdint>
#include "key.h"
namespace esphome {
namespace brmesh {
static constexpr Key DEFAULT_ENCRYPT_KEY = { 0x5e, 0x36, 0x7b, 0xc4 };
static const std::array<uint8_t, 3> DEFAULT_BLE_ADDRESS = {0xC1, 0xC2, 0xC3};
} // namespace brmesh
} // namespace esphome

View file

@ -1,72 +0,0 @@
#pragma once
namespace esphome {
namespace brmesh {
template <typename T>
class Q {
public:
class Node;
constexpr Q() = default;
bool enqueue(T &);
T *dequeue();
T *peek();
private:
Node *head_ = nullptr;
Node **tail_ = &head_;
};
template <typename T>
class Q<T>::Node {
public:
constexpr Node() = default;
bool enqueue(Node *&head, Node **&tail);
static Node *dequeue(Node *&head, Node **&tail);
private:
Node *next_ = nullptr;
};
template <typename T>
bool Q<T>::enqueue(T &node) {
return static_cast<Node&>(node).enqueue(head_, tail_);
}
template <typename T>
T *Q<T>::dequeue() {
auto *node = Node::dequeue(head_, tail_);
return dynamic_cast<T*>(node);
}
template <typename T>
T *Q<T>::peek() {
auto *node = head_;
return dynamic_cast<T*>(node);
}
template <typename T>
bool Q<T>::Node::enqueue(Node *&head, Node **&tail) {
const bool first = tail == &head;
*tail = this;
tail = &next_;
return first;
}
template <typename T>
Q<T>::Node *Q<T>::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

View file

@ -1,5 +1,4 @@
#include "whitening.h" #include "whitening.h"
#include "debug.h"
#include "esphome/core/log.h" #include "esphome/core/log.h"
#include "esphome/core/helpers.h" #include "esphome/core/helpers.h"
@ -14,15 +13,26 @@ Whitening::State
Whitening::use(bool encode, const LUT &lut, State state, std::span<uint8_t> bytes) Whitening::use(bool encode, const LUT &lut, State state, std::span<uint8_t> bytes)
{ {
const bool log = (bytes.size() > 1 || bytes[0] != 0x00); const bool log = (bytes.size() > 1 || bytes[0] != 0x00);
if (log) if (log)
hexdump(TAG, bytes, "%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",
format_hex_pretty(&bytes[0], bytes.size()).c_str());
for (auto &byte:bytes) { for (auto &byte:bytes) {
auto &next = lut[state]; auto &next = lut[state];
byte = next.lut[byte]; byte = next.lut[byte];
state = next.state; state = next.state;
} }
if (log) if (log)
hexdump(TAG, bytes, "%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",
format_hex_pretty(&bytes[0], bytes.size()).c_str());
return state; return state;
} }

View file

@ -1,3 +0,0 @@
from .fastcon_controller import CONFIG_SCHEMA, DEPENDENCIES, FastconController, to_code
__all__ = ["CONFIG_SCHEMA", "DEPENDENCIES", "FastconController", "to_code"]

View file

@ -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<uint8_t> &data)
{
std::lock_guard<std::mutex> 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<std::mutex> lock(queue_mutex_);
std::queue<Command> 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<std::mutex> 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<uint8_t> FastconController::get_light_data(light::LightState *state)
{
std::vector<uint8_t> 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<uint8_t>({0x00});
}
auto color_mode = values.get_color_mode();
bool has_white = (static_cast<uint8_t>(color_mode) & static_cast<uint8_t>(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<uint8_t>(brightness);
if (has_white)
{
return std::vector<uint8_t>({static_cast<uint8_t>(brightness)});
// DEBUG: when changing to white mode, this should be the payload:
// ff0000007f7f
}
bool has_rgb = (static_cast<uint8_t>(color_mode) & static_cast<uint8_t>(light::ColorCapability::RGB)) != 0;
if (has_rgb)
{
light_data[1] = static_cast<uint8_t>(values.get_blue() * 255.0f);
light_data[2] = static_cast<uint8_t>(values.get_red() * 255.0f);
light_data[3] = static_cast<uint8_t>(values.get_green() * 255.0f);
}
bool has_cold_warm = (static_cast<uint8_t>(color_mode) & static_cast<uint8_t>(light::ColorCapability::COLD_WARM_WHITE)) != 0;
if (has_cold_warm)
{
light_data[4] = static_cast<uint8_t>(values.get_warm_white() * 255.0f);
light_data[5] = static_cast<uint8_t>(values.get_cold_white() * 255.0f);
}
// TODO figure out if we can use these, and how
bool has_temp = (static_cast<uint8_t>(color_mode) & static_cast<uint8_t>(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<uint8_t> FastconController::single_control(uint32_t light_id_, const std::vector<uint8_t> &light_data)
{
std::vector<uint8_t> 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<uint8_t> FastconController::generate_command(uint8_t n, uint32_t light_id_, const std::vector<uint8_t> &data, bool forward)
{
static uint8_t sequence = 0;
// Create command body with header
std::vector<uint8_t> 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<uint8_t> addr = {DEFAULT_BLE_FASTCON_ADDRESS.begin(), DEFAULT_BLE_FASTCON_ADDRESS.end()};
return prepare_payload(addr, body);
}
} // namespace fastcon
} // namespace esphome

View file

@ -1,90 +0,0 @@
#pragma once
#include <queue>
#include <mutex>
#include <vector>
#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<uint8_t> get_light_data(light::LightState *state);
std::vector<uint8_t> single_control(uint32_t addr, const std::vector<uint8_t> &light_data);
void queueCommand(uint32_t light_id_, const std::vector<uint8_t> &data);
void clear_queue();
bool is_queue_empty() const
{
std::lock_guard<std::mutex> lock(queue_mutex_);
return queue_.empty();
}
size_t get_queue_size() const
{
std::lock_guard<std::mutex> lock(queue_mutex_);
return queue_.size();
}
void set_max_queue_size(size_t size) { max_queue_size_ = size; }
void set_mesh_key(std::array<uint8_t, 4> 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<uint8_t> data;
uint32_t timestamp;
uint8_t retries{0};
static constexpr uint8_t MAX_RETRIES = 3;
};
std::queue<Command> 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<uint8_t> generate_command(uint8_t n, uint32_t light_id_, const std::vector<uint8_t> &data, bool forward = true);
std::array<uint8_t, 4> 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

View file

@ -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]))

View file

@ -1,71 +0,0 @@
#include <algorithm>
#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

View file

@ -1,35 +0,0 @@
#pragma once
#include <array>
#include <vector>
#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

View file

@ -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))

View file

@ -1,60 +0,0 @@
#include <algorithm>
#include "protocol.h"
namespace esphome
{
namespace fastcon
{
std::vector<uint8_t> get_rf_payload(const std::vector<uint8_t> &addr, const std::vector<uint8_t> &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<uint8_t> 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<uint8_t> prepare_payload(const std::vector<uint8_t> &addr, const std::vector<uint8_t> &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<uint8_t>(payload.begin() + 0xf, payload.end());
}
} // namespace fastcon
} // namespace esphome

View file

@ -1,18 +0,0 @@
#pragma once
#include <vector>
#include <array>
#include <cstdint>
#include "utils.h"
namespace esphome
{
namespace fastcon
{
static const std::array<uint8_t, 4> DEFAULT_ENCRYPT_KEY = {0x5e, 0x36, 0x7b, 0xc4};
static const std::array<uint8_t, 3> DEFAULT_BLE_FASTCON_ADDRESS = {0xC1, 0xC2, 0xC3};
std::vector<uint8_t> get_rf_payload(const std::vector<uint8_t> &addr, const std::vector<uint8_t> &data);
std::vector<uint8_t> prepare_payload(const std::vector<uint8_t> &addr, const std::vector<uint8_t> &data);
} // namespace fastcon
} // namespace esphome

View file

@ -1,126 +0,0 @@
#include <vector>
#include <cstdio>
#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<uint8_t> &addr, const std::vector<uint8_t> &data)
{
uint16_t crc = 0xffff;
// Process address in reverse
for (auto it = addr.rbegin(); it != addr.rend(); ++it)
{
crc ^= (static_cast<uint16_t>(*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<uint16_t>(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<uint8_t> &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<char> vector_to_hex_string(std::vector<uint8_t> &data)
{
std::vector<char> 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

View file

@ -1,36 +0,0 @@
#pragma once
#include <cstdint>
#include <vector>
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<uint8_t> &addr, const std::vector<uint8_t> &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<uint8_t> &data, WhiteningContext &ctx);
std::vector<char> vector_to_hex_string(std::vector<uint8_t> &data);
} // namespace fastcon
} // namespace esphome