initial draft

This commit is contained in:
Jonas Rabenstein 2025-12-22 01:45:30 +01:00
commit 771c1f6e32
32 changed files with 2282 additions and 804 deletions

View file

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

View file

@ -0,0 +1,67 @@
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.const import CONF_ID
from esphome.core import HexInt
from esphome.components import esp32_ble
if False:
DEPENDENCIES = ["esp32_ble"]
CONF_KEY = "key"
CONF_ADV = "adv"
CONF_INTERVAL = "interval"
CONF_MIN = "min"
CONF_MAX = "max"
CONF_DURATION = "duration"
CONF_GAP = "gap"
def validate_key_string(value):
value = value.replace(" ", "")
if len(value) != 8:
raise cv.Invalid("Key must be exactly 4 bytes (8 hex characters)")
try:
return [HexInt(int(value[x:x+2], 16)) for x in range(0, len(value), 2)]
except ValueError as err:
raise cv.Invalid(f"Invalid hex value: {err}")
def validate_key(value):
if isinstance(value, str):
value = validate_key_string(value)
if not isinstance(value, list):
raise cv.Invalid("Key must be a list")
if len(value) != 4:
raise cv.Invalid("Key must have 4 bytes")
return [ cv.uint8_t(x) for x in value ]
brmesh_ns = cg.esphome_ns.namespace("brmesh")
Controller = brmesh_ns.class_("Controller", cg.Component, esp32_ble.GAPEventHandler)
CONFIG_SCHEMA = cv.COMPONENT_SCHEMA.extend({
cv.GenerateID(): cv.declare_id(Controller),
cv.GenerateID(esp32_ble.CONF_BLE_ID): cv.use_id(esp32_ble.ESP32BLE),
cv.Required(CONF_KEY): validate_key,
cv.Optional(CONF_ADV): ADV_SCHEMA,
})
async def to_code(config):
adv = config[CONF_ADV]
interval = adv[CONF_INTERVAL]
if interval[CONF_MAX] < interval[CONF_MIN]:
raise cv.Invalid(
f"{CONF_ADV}.{CONF_INTERVAL}.{CONF_MIN} ({interval[CONF_MIN]}) must be <= "
f"{CONF_ADV}.{CONF_INTERVLA}.{CONF_MAX} ({interval[CONF_MAX]})"
)
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)
await cg.register_component(var, config)

View file

@ -0,0 +1,275 @@
#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

@ -0,0 +1,78 @@
#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

31
components/brmesh/debug.h Normal file
View file

@ -0,0 +1,31 @@
#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

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

View file

@ -4,7 +4,6 @@
#include "fastcon_controller.h" #include "fastcon_controller.h"
#include "protocol.h" #include "protocol.h"
namespace esphome namespace esphome
{ {
namespace fastcon namespace fastcon
@ -85,14 +84,9 @@ namespace esphome
adv_data_raw[adv_data_len++] = MANUFACTURER_DATA_ID & 0xFF; adv_data_raw[adv_data_len++] = MANUFACTURER_DATA_ID & 0xFF;
adv_data_raw[adv_data_len++] = (MANUFACTURER_DATA_ID >> 8) & 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()); memcpy(&adv_data_raw[adv_data_len], cmd.data.data(), cmd.data.size());
adv_data_len += 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); esp_err_t err = esp_ble_gap_config_adv_data_raw(adv_data_raw, adv_data_len);
if (err != ESP_OK) if (err != ESP_OK)
{ {

View file

@ -4,14 +4,14 @@
#include <mutex> #include <mutex>
#include <vector> #include <vector>
#include "esphome/core/component.h" #include "esphome/core/component.h"
#include "esphome/components/esp32_ble/ble.h" #include "esphome/components/esp32_ble_server/ble_server.h"
namespace esphome namespace esphome
{ {
namespace fastcon namespace fastcon
{ {
class FastconController : public Component, public esp32_ble::GAPEventHandler class FastconController : public Component
{ {
public: public:
FastconController() = default; FastconController() = default;

View file

@ -0,0 +1,37 @@
"""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

@ -0,0 +1,18 @@
#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

28
components/brmesh/key.h Normal file
View file

@ -0,0 +1,28 @@
#pragma once
#include <cstdint>
#include <span>
namespace esphome {
namespace brmesh {
class Key {
public:
constexpr Key(uint8_t a, uint8_t b, uint8_t c, uint8_t d) : data_{a,b,c,d} {}
template <size_t size>
constexpr std::span<uint8_t, size> operator()(const std::span<uint8_t, size> &span, size_t offset=0) const {
for (size_t i=0; i<span.size(); ++i)
span[i] ^= operator[](offset + i);
return span;
}
constexpr uint8_t operator[](size_t idx) const {
return data_[idx % data_.size()];
}
private:
const std::array<uint8_t, 4> data_;
};
} // namespace brmesh
} // namespace esphome

549
components/brmesh/light.cpp Normal file
View file

@ -0,0 +1,549 @@
#include "light.h"
#include "whitening.h"
#include "protocol.h"
#include "debug.h"
#include "esphome/core/log.h"
namespace {
const char *const TAG = "brmesh.light";
} // namespace
namespace esphome {
namespace brmesh {
namespace {
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;
}
uint16_t crc16(uint8_t byte, uint16_t crc) {
crc ^= static_cast<uint16_t>(byte) << 8;
for (int j=0; j<4; ++j) {
uint16_t tmp = crc << 1;
if (crc & 0x8000)
tmp ^= 0x1021;
crc = tmp << 1;
if (tmp & 0x8000)
crc ^= 0x1021;
}
return crc;
}
uint16_t crc16(const std::array<uint8_t, 3> &addr, const std::span<uint8_t> &data, const uint16_t seed = 0xFFFF) {
auto crc = seed;
for (const auto &byte:addr)
crc = crc16(byte, crc);
for (const auto &byte:data)
crc = crc16(reverse_bits(byte), 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) {
const auto &values = 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
if (!values.is_on()) {
const auto data = buffer.subspan(0, 1);
data[0] = 0x00;
return data;
}
auto color_mode = [mode=values.get_color_mode()](light::ColorMode wanted) {
const auto raw = static_cast<uint8_t>(wanted);
return raw == (static_cast<uint8_t>(mode) & raw);
};
float brightness = std::min(values.get_brightness() * 127.0f, 127.0f); // clamp the value to at most 127
if (color_mode(light::ColorMode::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
const auto data = buffer.subspan(0, 1);
data[0] = brightness;
return data;
}
const auto data = buffer;
data[0] = 0x80 + static_cast<uint8_t>(brightness);
if (color_mode(light::ColorMode::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::ColorMode::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::ColorMode::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 data;
}
std::span<uint8_t> command(const std::span<uint8_t, 16> &buffer, const Key &key, Light::Id id, uint8_t seq, light::LightState &state) {
static constexpr uint8_t n = 5; // TODO
static constexpr bool forward = true; // TODO
const uint8_t i2 = (id.value() / 256);
if (id.kind() != 'L')
return {};
const auto header = buffer.subspan(0, 4);
const auto body = single_light_data(subspan<6>(buffer, 4), state);
hexdump(TAG, body, "%c%02hhX: body: %s", id.kind(), id.value());
header[0] = (i2 & 0b1111) | ((n & 0b111) << 4) | (forward ? 0x80 : 0);
header[1] = seq;
header[2] = key[3];
header[3] = crc(body);
const auto result = buffer.subspan(0, 4 + body.size());
hexdump(TAG, result, "%c%02hhX: plain: %s", id.kind(), id.value());
DEFAULT_ENCRYPT_KEY(header);
key(body);
hexdump(TAG, result, "%c%02hhX: cipher: %s", id.kind(), id.value());
return result;
}
std::span<uint8_t> payload(const std::span<uint8_t, 24> &buffer, const Key &key, Light::Id id, uint8_t seq, light::LightState &state) {
// static values
buffer[0] = 0x71;
buffer[1] = 0x0F;
buffer[2] = 0x55;
const auto address = buffer.subspan(3, 3);
std::reverse_copy(DEFAULT_BLE_ADDRESS.cbegin(), DEFAULT_BLE_ADDRESS.cend(), address.begin());
const auto command = brmesh::command(subspan<16>(buffer, 4), key, id, seq, state);
hexdump(TAG, command, "L%hhd: command: %s", id);
const auto result = buffer.subspan(0, 4 + command.size() + 2);
uint16_t crc_value = crc16(DEFAULT_BLE_ADDRESS, command);
const auto crc = buffer.subspan(4+command.size(), 2);
crc[0] = crc_value;
crc[1] = crc_value >> 8;
for (auto &byte:buffer.subspan(0, 6))
byte = reverse_bits(byte);
hexdump(TAG, result, "L%hhd: raw: %s", id);
// TODO: shift seed to drop warmup
Whitening whitening(0x25);
for (size_t i=0; i<0x10; ++i)
whitening.encode(0);
return whitening.encode(result);
}
std::span<uint8_t> adv_data(const std::span<uint8_t, 31> &buffer, const Key &key, Light::Id id, uint8_t seq, light::LightState &state) {
const auto flags = buffer.subspan(0, 3);
const auto length = buffer.subspan(3, 1);
const auto manufacturer = buffer.subspan(4, 3);
const auto payload = brmesh::payload(std::span<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
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
void
Light::dump_config() {
auto *ble = esphome::esp32_ble::global_ble;
ESP_LOGCONFIG(TAG,
"light %hhd\n"
" key: %02hhx %02hhx %02hhx %02hhx\n"
" seq: %02hhx\n"
" ble: %p",
id_, key_[0], key_[1], key_[2], key_[3], seq_, ble);
}
void
Light::setup() {
auto *ble = esphome::esp32_ble::global_ble;
if (!ble) {
ESP_LOGE(TAG, "%c%02hhX: no ble device", id_.kind(), id_.value());
return;
}
ble->advertising_register_raw_advertisement_callback([this](bool advertise) {
const bool advertising = advertising_;
ESP_LOGD(TAG, "%c%02hhX: adv callback %s", id_.kind(), id_.value(), advertise ? "on" : "off");
if (advertise == advertising)
return;
advertising_ = advertise;
if (!advertise)
return;
enable_loop();
});
}
void
Light::loop() {
disable_loop();
if (!advertising_) {
ESP_LOGE(TAG, "%c%02hhX: loop while not advertising", id_.kind(), id_.value());
return;
}
if (state_ == nullptr) {
ESP_LOGD(TAG, "%c%02hhX: sleep", id_.kind(), id_.value());
return;
}
++count_;
auto *const state = state_;
const auto seq = seq_++;
std::array<uint8_t, 31> buffer;
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
light::LightTraits
Light::get_traits() {
auto traits = light::LightTraits();
traits.set_supported_color_modes({
esphome::light::ColorMode::RGB,
esphome::light::ColorMode::WHITE,
esphome::light::ColorMode::BRIGHTNESS,
esphome::light::ColorMode::COLD_WARM_WHITE,
});
traits.set_min_mireds(153);
traits.set_max_mireds(500);
return traits;
}
void
Light::setup_state(light::LightState *state) {
}
void
Light::update_state(light::LightState *state) {
}
#include "./fastcon/fastcon_controller.h"
void
Light::write_state(light::LightState *state) {
ESP_LOGV(TAG, "%c%02hhX: state %02hx", id_.kind(), id_.value(), state->current_values.get_color_mode());
payload_length_ = single_light_data(std::span<uint8_t, 6>(payload_), *state).size();
hexdump(TAG, &payload_.front(), payload_length_, "%c%02hhX: state: %s", id_.kind(), id_.value());
state_ = state;
count_ = 0;
#if 0
switch (state->current_values.get_color_mode())
// handled in update_state
float red;
float green;
float blue;
float temperatur;
float brightness;
state->current_values_as_rgbct(&red, &green, &blue, &temperatur, &brightness);
#else
static fastcon::FastconController controller = [this]() {
fastcon::FastconController controller;
controller.set_mesh_key(key_);
}();
const auto fastcon_data = controller.get_light_data(state);
hexdump(TAG, &fastcon_data.front(), fastcon_data.size(), "%c%02hhX: fastcon: %s", id_.kind(), id_.value());
#endif
}
// interface: esphome::esp32_ble::GAPEventHandler
void
Light::gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) {
if (!advertising_)
return;
esp_err_t err;
switch (event) {
case ESP_GAP_BLE_ADV_DATA_RAW_SET_COMPLETE_EVT:
adv_event_(param->adv_data_raw_cmpl);
return;
case ESP_GAP_BLE_ADV_START_COMPLETE_EVT:
adv_event_(param->adv_start_cmpl);
return;
case ESP_GAP_BLE_ADV_STOP_COMPLETE_EVT:
adv_event_(param->adv_stop_cmpl);
return;
}
}
} // namespace brmesh
} // 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

88
components/brmesh/light.h Normal file
View file

@ -0,0 +1,88 @@
#pragma once
#include <array>
#include <variant>
#include <vector>
#include <span>
#include "esphome/core/component.h"
#include "esphome/components/light/light_output.h"
#include "esphome/components/esp32_ble/ble.h"
#include "key.h"
namespace esphome {
namespace brmesh {
class Light : public Component, public light::LightOutput, public esp32_ble::GAPEventHandler
{
public:
enum class Single : uint8_t {};
enum class Group : uint8_t {};
class Id : public std::variant<Single, Group> {
public:
using variant::variant;
static constexpr char kind(Single) { return 'L'; }
static constexpr char kind(Group) { return 'G'; }
constexpr char kind() {
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(Group value) { return static_cast<uint8_t>(value); }
constexpr uint8_t value() {
return std::visit([](auto &&value) { return Id::value(value); }, *this);
}
};
Light(Id id, const Key &key,
uint16_t adv_interval_min, uint16_t adv_interval_max,
uint16_t adv_duration, uint16_t adv_gap)
: id_(id), key_(key),
adv_interval_min_(adv_interval_min), adv_interval_max_(adv_interval_max),
adv_duration_(adv_duration), adv_gap_(adv_gap) {}
// 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;
// interface: GAPEventHandler
void gap_event_handler(esp_gap_ble_cb_event_t, esp_ble_gap_cb_param_t *) override;
private:
light::LightState *state_ = nullptr;
Id id_;
const Key key_;
const uint16_t adv_interval_min_;
const uint16_t adv_interval_max_;
const uint16_t adv_duration_;
const uint16_t adv_gap_;
static constexpr esp_power_level_t tx_power_{};
uint8_t seq_ = 0;
uint8_t count_ = 0;
bool advertising_ = false;
std::array<uint8_t, 6> payload_ = {};
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 esphome

View file

@ -1,37 +1,92 @@
"""Light platform for Fastcon BLE lights.""" """Light platform for BRmesh BLE lights."""
import esphome.codegen as cg import esphome.codegen as cg
import esphome.config_validation as cv import esphome.config_validation as cv
from esphome.components import light
from esphome.const import CONF_LIGHT_ID, CONF_OUTPUT_ID from esphome.const import CONF_LIGHT_ID, CONF_OUTPUT_ID
from esphome.core import HexInt
from esphome.components import light, esp32_ble
from .fastcon_controller import FastconController #from .brmesh import Controller
DEPENDENCIES = ["esp32_ble"] DEPENDENCIES = ["esp32_ble"]
AUTO_LOAD = ["light"] AUTO_LOAD = ["light"]
CONF_CONTROLLER_ID = "controller_id" CONF_KEY = "key"
CONF_ADV = "adv"
CONF_INTERVAL = "interval"
CONF_MIN = "min"
CONF_MAX = "max"
CONF_DURATION = "duration"
CONF_GAP = "gap"
fastcon_ns = cg.esphome_ns.namespace("fastcon")
FastconLight = fastcon_ns.class_("FastconLight", light.LightOutput, cg.Component)
CONFIG_SCHEMA = cv.All( def validate_key_string(value):
light.BRIGHTNESS_ONLY_LIGHT_SCHEMA.extend( value = value.replace(" ", "")
{ if len(value) != 8:
cv.GenerateID(CONF_OUTPUT_ID): cv.declare_id(FastconLight), raise cv.Invalid("Key must be exactly 4 bytes (8 hex characters)")
cv.Required(CONF_LIGHT_ID): cv.int_range(min=1, max=255),
cv.Optional(CONF_CONTROLLER_ID, default="fastcon_controller"): cv.use_id( try:
FastconController 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}")
).extend(cv.COMPONENT_SCHEMA)
)
def validate_key(value):
if isinstance(value, str):
value = validate_key_string(value)
if not isinstance(value, list):
raise cv.Invalid("Key must be a list")
if len(value) != 4:
raise cv.Invalid("Key must have 4 bytes")
return [ cv.uint8_t(x) for x in value ]
brmesh_ns = cg.esphome_ns.namespace("brmesh")
Light = brmesh_ns.class_("Light", light.LightOutput, cg.Component)
Single = Light.enum("Single", True)
Group = Light.enum("Group", True);
ADV_INTERVAL_SCHEMA = cv.Schema({
cv.Optional(CONF_MIN, default=0x20): cv.uint16_t,
cv.Optional(CONF_MAX, default=0x80): cv.uint16_t,
})
ADV_SCHEMA = cv.Schema({
cv.Optional(CONF_INTERVAL, default=ADV_INTERVAL_SCHEMA({})): ADV_INTERVAL_SCHEMA,
cv.Optional(CONF_DURATION, default="50ms"): cv.positive_time_period,
cv.Optional(CONF_GAP, default="10ms"): cv.positive_time_period,
})
CONFIG_SCHEMA = light.light_schema(Light, light.LightType.RGB).extend({
cv.GenerateID(esp32_ble.CONF_BLE_ID): cv.use_id(esp32_ble.ESP32BLE),
cv.Required(CONF_KEY): validate_key,
cv.Required(CONF_LIGHT_ID): cv.int_range(min=1, max=255),
cv.Optional(CONF_ADV, default=ADV_SCHEMA({})): ADV_SCHEMA,
})
async def to_code(config): async def to_code(config):
var = cg.new_Pvariable(config[CONF_OUTPUT_ID], config[CONF_LIGHT_ID]) adv = config[CONF_ADV]
interval = adv[CONF_INTERVAL]
if interval[CONF_MAX] < interval[CONF_MIN]:
raise cv.Invalid(
f"{CONF_ADV}.{CONF_INTERVAL}.{CONF_MIN} ({interval[CONF_MIN]}) must be <= "
f"{CONF_ADV}.{CONF_INTERVLA}.{CONF_MAX} ({interval[CONF_MAX]})"
)
ble = await cg.get_variable(config[esp32_ble.CONF_BLE_ID])
target = Single(config[CONF_LIGHT_ID])
var = cg.new_Pvariable(config[CONF_OUTPUT_ID], target, config[CONF_KEY],
interval[CONF_MIN], interval[CONF_MAX],
adv[CONF_DURATION].total_milliseconds,
adv[CONF_GAP].total_milliseconds)
esp32_ble.register_gap_event_handler(ble, var)
await cg.register_component(var, config) await cg.register_component(var, config)
await light.register_light(var, config) await light.register_light(var, config)
controller = await cg.get_variable(config[CONF_CONTROLLER_ID]) cg.add_define("USE_ESP32_BLE_ADVERTISING")
cg.add(var.set_controller(controller)) cg.add_define("USE_ESP32_BLE_UUID")

View file

@ -3,16 +3,14 @@
#include <vector> #include <vector>
#include <array> #include <array>
#include <cstdint> #include <cstdint>
#include "utils.h" #include "key.h"
namespace esphome namespace esphome {
{ namespace brmesh {
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); static constexpr Key DEFAULT_ENCRYPT_KEY = { 0x5e, 0x36, 0x7b, 0xc4 };
std::vector<uint8_t> prepare_payload(const std::vector<uint8_t> &addr, const std::vector<uint8_t> &data);
} // namespace fastcon static const std::array<uint8_t, 3> DEFAULT_BLE_ADDRESS = {0xC1, 0xC2, 0xC3};
} // namespace brmesh
} // namespace esphome } // namespace esphome

72
components/brmesh/q.h Normal file
View file

@ -0,0 +1,72 @@
#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

@ -0,0 +1,31 @@
#include "whitening.h"
#include "debug.h"
#include "esphome/core/log.h"
#include "esphome/core/helpers.h"
namespace {
constexpr auto TAG = "brmesh.whitening";
} //
namespace esphome {
namespace brmesh {
Whitening::State
Whitening::use(bool encode, const LUT &lut, State state, std::span<uint8_t> bytes)
{
const bool log = (bytes.size() > 1 || bytes[0] != 0x00);
if (log)
hexdump(TAG, bytes, "%s: input: %s", encode ? "encode" : "decode");
for (auto &byte:bytes) {
auto &next = lut[state];
byte = next.lut[byte];
state = next.state;
}
if (log)
hexdump(TAG, bytes, "%s: output: %s", encode ? "encode" : "decode");
return state;
}
} // brmesh
} // esphome

View file

@ -0,0 +1,154 @@
#pragma once
#include <array>
#include <cstdint>
#include <span>
#include "esphome/core/log.h"
namespace esphome {
namespace brmesh {
class Whitening {
public:
using State = uint8_t;
explicit Whitening(State s = 0) : m_state(s & 0x7F) {}
// Factory reproduces whitening_init
static Whitening from_val(uint32_t val) {
Context ctx{};
uint32_t v0[] = { (val >> 5), (val >> 4), (val >> 3), (val >> 2) };
ctx.f0 = 1;
ctx.f4 = v0[0] & 1;
ctx.f8 = v0[1] & 1;
ctx.fc = v0[2] & 1;
ctx.f10 = v0[3] & 1;
ctx.f14 = (val >> 1) & 1;
ctx.f18 = val & 1;
return Whitening(context_to_index(ctx));
}
template <size_t size>
std::span<uint8_t, size> encode(const std::span<uint8_t, size> &input) {
m_state = use<true>(m_state, input);
return input;
}
uint8_t encode(uint8_t input) {
return encode(std::span<uint8_t>(&input, 1))[0];
}
template <size_t size>
std::span<uint8_t, size> decode(const std::span<uint8_t, size> &input) {
m_state = use<false>(m_state, input);
return input;
}
uint8_t decode(uint8_t input) {
return decode(std::span<uint8_t>(&input, 1))[0];
}
private:
struct Context { uint8_t f0,f4,f8,fc,f10,f14,f18; };
struct Entry { uint8_t value; State state; };
static constexpr int STATE_SIZE = 128;
static constexpr int BYTE_SIZE = 256;
struct Next { State state; std::array<uint8_t, BYTE_SIZE> lut; };
using LUT = std::array<Next, STATE_SIZE>;
State m_state;
static State use(bool encode, const LUT &lut, State state, std::span<uint8_t>(bytes));
template <bool encode, size_t size>
static State use(State state, std::span<uint8_t, size> bytes) {
return use(encode, lut<encode>, state, std::span<uint8_t>(bytes));
}
public:
static constexpr State context_to_index(const Context &ctx) {
return (ctx.f0 << 6) | (ctx.f4 << 5) | (ctx.f8 << 4)
| (ctx.fc << 3) | (ctx.f10 << 2) | (ctx.f14 << 1) | ctx.f18;
}
static constexpr Context index_to_context(State idx) {
return Context{
uint8_t((idx >> 6) & 1),
uint8_t((idx >> 5) & 1),
uint8_t((idx >> 4) & 1),
uint8_t((idx >> 3) & 1),
uint8_t((idx >> 2) & 1),
uint8_t((idx >> 1) & 1),
uint8_t(idx & 1)
};
}
static constexpr uint8_t byte(uint8_t input, const Context &ctx) {
uint32_t varC = ctx.fc;
uint32_t var14 = ctx.f14;
uint32_t var18 = ctx.f18;
uint32_t var10 = ctx.f10;
uint32_t var8 = var14 ^ ctx.f8;
uint32_t var4 = var10 ^ ctx.f4;
uint32_t _var = var18 ^ varC;
uint32_t var0 = _var ^ ctx.f0;
return ((input & 0x80) ^ ((var8 ^ var18) << 7))
| ((input & 0x40) ^ (var0 << 6))
| ((input & 0x20) ^ (var4 << 5))
| ((input & 0x10) ^ (var8 << 4))
| ((input & 0x08) ^ (_var << 3))
| ((input & 0x04) ^ (var10 << 2))
| ((input & 0x02) ^ (var14 << 1))
| ((input & 0x01) ^ var18);
}
static constexpr Context next(const Context &ctx) {
uint32_t varC = ctx.fc;
uint32_t var14 = ctx.f14;
uint32_t var18 = ctx.f18;
uint32_t var10 = ctx.f10;
uint32_t var8 = var14 ^ ctx.f8;
uint32_t var4 = var10 ^ ctx.f4;
uint32_t _var = var18 ^ varC;
uint32_t var0 = _var ^ ctx.f0;
Context next{};
next.f8 = var4;
next.fc = var8;
next.f10 = var8 ^ varC;
next.f14 = var0 ^ var10;
next.f18 = var4 ^ var14;
next.f0 = var8 ^ var18;
next.f4 = var0;
return next;
}
private:
static constexpr LUT create_lut(bool forward) {
LUT lut{};
for (uint16_t s = 0; s < lut.size(); ++s) {
auto ctx = Whitening::index_to_context(uint8_t(s));
lut[s].state = Whitening::context_to_index(Whitening::next(ctx));
for (uint16_t b = 0; b < lut[s].lut.size(); ++b) {
uint8_t enc = Whitening::byte(uint8_t(b), ctx);
const auto key = forward ? uint8_t(b) : enc;
const auto value = forward ? enc : uint8_t(b);
lut[s].lut[key] = value;
}
}
return lut;
}
template <bool forward>
static const LUT lut;
};
template <bool forward>
const Whitening::LUT Whitening::lut = Whitening::create_lut(forward);
} // namespace: brmesh
} // namespace: esphome