initial commit
This commit is contained in:
parent
f60dfdd52e
commit
4417c77875
18 changed files with 3522 additions and 0 deletions
177
.gitignore
vendored
Normal file
177
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,177 @@
|
||||||
|
# Byte-compiled / optimized / DLL files
|
||||||
|
__pycache__/
|
||||||
|
*.py[cod]
|
||||||
|
*$py.class
|
||||||
|
|
||||||
|
# C extensions
|
||||||
|
*.so
|
||||||
|
|
||||||
|
# Distribution / packaging
|
||||||
|
.Python
|
||||||
|
build/
|
||||||
|
develop-eggs/
|
||||||
|
dist/
|
||||||
|
downloads/
|
||||||
|
eggs/
|
||||||
|
.eggs/
|
||||||
|
lib/
|
||||||
|
lib64/
|
||||||
|
parts/
|
||||||
|
sdist/
|
||||||
|
var/
|
||||||
|
wheels/
|
||||||
|
share/python-wheels/
|
||||||
|
*.egg-info/
|
||||||
|
.installed.cfg
|
||||||
|
*.egg
|
||||||
|
MANIFEST
|
||||||
|
|
||||||
|
# PyInstaller
|
||||||
|
# Usually these files are written by a python script from a template
|
||||||
|
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
||||||
|
*.manifest
|
||||||
|
*.spec
|
||||||
|
|
||||||
|
# Installer logs
|
||||||
|
pip-log.txt
|
||||||
|
pip-delete-this-directory.txt
|
||||||
|
|
||||||
|
# Unit test / coverage reports
|
||||||
|
htmlcov/
|
||||||
|
.tox/
|
||||||
|
.nox/
|
||||||
|
.coverage
|
||||||
|
.coverage.*
|
||||||
|
.cache
|
||||||
|
nosetests.xml
|
||||||
|
coverage.xml
|
||||||
|
*.cover
|
||||||
|
*.py,cover
|
||||||
|
.hypothesis/
|
||||||
|
.pytest_cache/
|
||||||
|
cover/
|
||||||
|
|
||||||
|
# Translations
|
||||||
|
*.mo
|
||||||
|
*.pot
|
||||||
|
|
||||||
|
# Django stuff:
|
||||||
|
*.log
|
||||||
|
local_settings.py
|
||||||
|
db.sqlite3
|
||||||
|
db.sqlite3-journal
|
||||||
|
|
||||||
|
# Flask stuff:
|
||||||
|
instance/
|
||||||
|
.webassets-cache
|
||||||
|
|
||||||
|
# Scrapy stuff:
|
||||||
|
.scrapy
|
||||||
|
|
||||||
|
# Sphinx documentation
|
||||||
|
docs/_build/
|
||||||
|
|
||||||
|
# PyBuilder
|
||||||
|
.pybuilder/
|
||||||
|
target/
|
||||||
|
|
||||||
|
# Jupyter Notebook
|
||||||
|
.ipynb_checkpoints
|
||||||
|
|
||||||
|
# IPython
|
||||||
|
profile_default/
|
||||||
|
ipython_config.py
|
||||||
|
|
||||||
|
# pyenv
|
||||||
|
# For a library or package, you might want to ignore these files since the code is
|
||||||
|
# intended to run in multiple environments; otherwise, check them in:
|
||||||
|
# .python-version
|
||||||
|
|
||||||
|
# pipenv
|
||||||
|
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
||||||
|
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
||||||
|
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
||||||
|
# install all needed dependencies.
|
||||||
|
#Pipfile.lock
|
||||||
|
|
||||||
|
# UV
|
||||||
|
# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
|
||||||
|
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
||||||
|
# commonly ignored for libraries.
|
||||||
|
#uv.lock
|
||||||
|
|
||||||
|
# poetry
|
||||||
|
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
|
||||||
|
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
||||||
|
# commonly ignored for libraries.
|
||||||
|
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
|
||||||
|
#poetry.lock
|
||||||
|
|
||||||
|
# pdm
|
||||||
|
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
|
||||||
|
#pdm.lock
|
||||||
|
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
|
||||||
|
# in version control.
|
||||||
|
# https://pdm.fming.dev/latest/usage/project/#working-with-version-control
|
||||||
|
.pdm.toml
|
||||||
|
.pdm-python
|
||||||
|
.pdm-build/
|
||||||
|
|
||||||
|
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
|
||||||
|
__pypackages__/
|
||||||
|
|
||||||
|
# Celery stuff
|
||||||
|
celerybeat-schedule
|
||||||
|
celerybeat.pid
|
||||||
|
|
||||||
|
# SageMath parsed files
|
||||||
|
*.sage.py
|
||||||
|
|
||||||
|
# Environments
|
||||||
|
.env
|
||||||
|
.venv
|
||||||
|
env/
|
||||||
|
venv/
|
||||||
|
ENV/
|
||||||
|
env.bak/
|
||||||
|
venv.bak/
|
||||||
|
|
||||||
|
# Spyder project settings
|
||||||
|
.spyderproject
|
||||||
|
.spyproject
|
||||||
|
|
||||||
|
# Rope project settings
|
||||||
|
.ropeproject
|
||||||
|
|
||||||
|
# mkdocs documentation
|
||||||
|
/site
|
||||||
|
|
||||||
|
# mypy
|
||||||
|
.mypy_cache/
|
||||||
|
.dmypy.json
|
||||||
|
dmypy.json
|
||||||
|
|
||||||
|
# Pyre type checker
|
||||||
|
.pyre/
|
||||||
|
|
||||||
|
# pytype static type analyzer
|
||||||
|
.pytype/
|
||||||
|
|
||||||
|
# Cython debug symbols
|
||||||
|
cython_debug/
|
||||||
|
|
||||||
|
# PyCharm
|
||||||
|
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
|
||||||
|
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
||||||
|
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
||||||
|
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
||||||
|
#.idea/
|
||||||
|
|
||||||
|
# PyPI configuration file
|
||||||
|
.pypirc
|
||||||
|
|
||||||
|
.esphome
|
||||||
|
.pio
|
||||||
|
.vscode/c_cpp_properties.json
|
||||||
|
.vscode/launch.json
|
||||||
|
secrets.yaml
|
||||||
11
.vscode/extensions.json
vendored
Normal file
11
.vscode/extensions.json
vendored
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
{
|
||||||
|
// See http://go.microsoft.com/fwlink/?LinkId=827846
|
||||||
|
// for the documentation about the extensions.json format
|
||||||
|
"recommendations": [
|
||||||
|
"charliermarsh.ruff",
|
||||||
|
"platformio.platformio-ide"
|
||||||
|
],
|
||||||
|
"unwantedRecommendations": [
|
||||||
|
"ms-vscode.cpptools-extension-pack"
|
||||||
|
]
|
||||||
|
}
|
||||||
71
.vscode/settings.json
vendored
Normal file
71
.vscode/settings.json
vendored
Normal file
|
|
@ -0,0 +1,71 @@
|
||||||
|
{
|
||||||
|
"[python]": {
|
||||||
|
"editor.defaultFormatter": "charliermarsh.ruff",
|
||||||
|
"editor.formatOnSave": true,
|
||||||
|
"editor.codeActionsOnSave": {
|
||||||
|
"source.organizeImports": "explicit"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ruff.nativeServer": true,
|
||||||
|
"files.associations": {
|
||||||
|
"*.css": "tailwindcss",
|
||||||
|
"*.pgsql": "postgres",
|
||||||
|
"array": "cpp",
|
||||||
|
"bitset": "cpp",
|
||||||
|
"string_view": "cpp",
|
||||||
|
"initializer_list": "cpp",
|
||||||
|
"utility": "cpp",
|
||||||
|
"string": "cpp",
|
||||||
|
"atomic": "cpp",
|
||||||
|
"*.tcc": "cpp",
|
||||||
|
"cctype": "cpp",
|
||||||
|
"chrono": "cpp",
|
||||||
|
"cinttypes": "cpp",
|
||||||
|
"clocale": "cpp",
|
||||||
|
"cmath": "cpp",
|
||||||
|
"condition_variable": "cpp",
|
||||||
|
"cstdarg": "cpp",
|
||||||
|
"cstddef": "cpp",
|
||||||
|
"cstdint": "cpp",
|
||||||
|
"cstdio": "cpp",
|
||||||
|
"cstdlib": "cpp",
|
||||||
|
"cstring": "cpp",
|
||||||
|
"ctime": "cpp",
|
||||||
|
"cwchar": "cpp",
|
||||||
|
"cwctype": "cpp",
|
||||||
|
"deque": "cpp",
|
||||||
|
"unordered_map": "cpp",
|
||||||
|
"unordered_set": "cpp",
|
||||||
|
"vector": "cpp",
|
||||||
|
"exception": "cpp",
|
||||||
|
"algorithm": "cpp",
|
||||||
|
"functional": "cpp",
|
||||||
|
"iterator": "cpp",
|
||||||
|
"map": "cpp",
|
||||||
|
"memory": "cpp",
|
||||||
|
"memory_resource": "cpp",
|
||||||
|
"numeric": "cpp",
|
||||||
|
"optional": "cpp",
|
||||||
|
"random": "cpp",
|
||||||
|
"ratio": "cpp",
|
||||||
|
"regex": "cpp",
|
||||||
|
"set": "cpp",
|
||||||
|
"system_error": "cpp",
|
||||||
|
"tuple": "cpp",
|
||||||
|
"type_traits": "cpp",
|
||||||
|
"fstream": "cpp",
|
||||||
|
"iomanip": "cpp",
|
||||||
|
"iosfwd": "cpp",
|
||||||
|
"iostream": "cpp",
|
||||||
|
"istream": "cpp",
|
||||||
|
"limits": "cpp",
|
||||||
|
"mutex": "cpp",
|
||||||
|
"new": "cpp",
|
||||||
|
"ostream": "cpp",
|
||||||
|
"sstream": "cpp",
|
||||||
|
"stdexcept": "cpp",
|
||||||
|
"streambuf": "cpp",
|
||||||
|
"thread": "cpp",
|
||||||
|
"typeinfo": "cpp"
|
||||||
|
}
|
||||||
|
}
|
||||||
107
README.md
Normal file
107
README.md
Normal file
|
|
@ -0,0 +1,107 @@
|
||||||
|
# ESPHome Fastcon BLE Light Component
|
||||||
|
|
||||||
|
This is a custom component for ESPHome that allows you to control Broadlink Fastcon BLE lights, also known as brMesh. It should work with any light that can be controlled by brMesh or Broadlink BLE mobile apps.
|
||||||
|
|
||||||
|
Be warned - there is also a brLight app, which might look like brMesh, but the protocol is different.
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
- ESP32 board
|
||||||
|
- ESPHome 2023.12.0 or newer
|
||||||
|
|
||||||
|
## Supported Features
|
||||||
|
|
||||||
|
- On/Off control
|
||||||
|
- Brightness control
|
||||||
|
- RGB color control
|
||||||
|
- White mode
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
Add the following to your ESPHome configuration:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# ESP32 is required
|
||||||
|
esp32:
|
||||||
|
board: esp32-s3-devkitc-1
|
||||||
|
framework:
|
||||||
|
type: arduino
|
||||||
|
|
||||||
|
esp32_ble_tracker:
|
||||||
|
esp32_ble_server:
|
||||||
|
|
||||||
|
# Source configuration
|
||||||
|
external_components:
|
||||||
|
- source: github://dennispg/esphome-fastcon@main
|
||||||
|
|
||||||
|
# Controller configuration
|
||||||
|
fastcon:
|
||||||
|
mesh_key: "12345678" # Your mesh key in hex format
|
||||||
|
|
||||||
|
# Optional parameters to control the advertisdement protocol with their defaults:
|
||||||
|
adv_interval_min: 0x20 # Minimum advertisement interval
|
||||||
|
adv_interval_max: 0x40 # Maximum advertisement interval
|
||||||
|
adv_duration: 50 # Advertisement duration in milliseconds
|
||||||
|
adv_gap: 10 # Gap between advertisements in milliseconds
|
||||||
|
max_queue_size: 100 # Maximum number of queued commands
|
||||||
|
|
||||||
|
# Light configuration (add an entry for each light)
|
||||||
|
light:
|
||||||
|
- platform: fastcon
|
||||||
|
id: living_room_light
|
||||||
|
name: "Living Room Light"
|
||||||
|
light_id: 1 # ID of the light (1-255)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Configuration Variables
|
||||||
|
|
||||||
|
#### Fastcon Controller
|
||||||
|
|
||||||
|
- **mesh_key** (*Required*, string): The mesh key for your Fastcon lights in hexadecimal format (8 characters/4 bytes)
|
||||||
|
- **id** (*Optional*, ID): The ID to use for this controller component. Defaults to "fastcon_controller"
|
||||||
|
- **adv_interval_min** (*Optional*, int): Minimum advertisement interval. Defaults to 0x20
|
||||||
|
- **adv_interval_max** (*Optional*, int): Maximum advertisement interval. Defaults to 0x40
|
||||||
|
- **adv_duration** (*Optional*, int): Duration of each advertisement in milliseconds. Defaults to 50
|
||||||
|
- **adv_gap** (*Optional*, int): Gap between advertisements in milliseconds. Defaults to 10
|
||||||
|
- **max_queue_size** (*Optional*, int): Maximum number of commands that can be queued. Defaults to 100
|
||||||
|
|
||||||
|
#### Fastcon Light
|
||||||
|
|
||||||
|
- **light_id** (*Required*, int): The ID of the light (1-255)
|
||||||
|
- **name** (*Required*, string): The name for the light entity
|
||||||
|
- **id** (*Optional*, ID): The ID to use for this light component
|
||||||
|
- **controller_id** (*Optional*, ID): The ID of the controller to use. Defaults to "fastcon_controller"
|
||||||
|
|
||||||
|
## Finding Your Mesh Key
|
||||||
|
|
||||||
|
The mesh key is crucial for controlling your Fastcon BLE lights. To find your light's mesh key, you first need to setup your devices using an Android device. The app generates a unique mesh key that will be used with all lights that are set up in the app.
|
||||||
|
|
||||||
|
Once the lights are setup, you can use ADB to connect to your phone and you may use the following command to extract the mesh key.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
adb logcat | { grep -m 1 -o 'jyq_helper: .* payload:.\{24\},[[:space:]]*key:[[:space:]]*.\{8\}' | awk '{print $NF}'; kill -2 $(pgrep -P $$ adb); }
|
||||||
|
```
|
||||||
|
|
||||||
|
While running the above, open the app and toggle a light on and off. The command should then output your mesh key.
|
||||||
|
|
||||||
|
## Acknowledgments
|
||||||
|
|
||||||
|
This component builds upon the reverse engineering and hard work of several others who must be acknowledged and thanked:
|
||||||
|
|
||||||
|
### Protocol Reverse Engineering
|
||||||
|
|
||||||
|
The foundational protocol reverse engineering work was done by [Mooody](https://mooody.me/posts/2023-04/reverse-the-fastcon-ble-protocol/), who provided detailed analysis of the Fastcon BLE protocol, including packet structure and encryption methods. https://mooody.me/posts/2023-04/reverse-the-fastcon-ble-protocol/
|
||||||
|
|
||||||
|
### Implementation References
|
||||||
|
|
||||||
|
- [ArcadeMachinist's brMeshMQTT](https://github.com/ArcadeMachinist/brMeshMQTT) - This work was crucial in helping me understand the practical implementation details of the protocol. https://github.com/ArcadeMachinist/brMeshMQTT
|
||||||
|
|
||||||
|
### Community Resources
|
||||||
|
|
||||||
|
- [Home Assistant Community Thread](https://community.home-assistant.io/t/brmesh-app-bluetooth-lights/473486/102)
|
||||||
|
|
||||||
|
This ESPHome component adapts and/or takes heavy inspiration from all of these works to run directly on ESP32 devices, allowing for native integration with Home Assistant without requiring additional bridges or MQTT brokers. A huge thank you to all those who contributed to my understanding of the Fastcon BLE protocol.
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
This project is licensed under the MIT License - see the LICENSE file for details.
|
||||||
3
components/fastcon/__init__.py
Normal file
3
components/fastcon/__init__.py
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
from .fastcon_controller import CONFIG_SCHEMA, DEPENDENCIES, FastconController, to_code
|
||||||
|
|
||||||
|
__all__ = ["CONFIG_SCHEMA", "DEPENDENCIES", "FastconController", "to_code"]
|
||||||
228
components/fastcon/fastcon_controller.cpp
Normal file
228
components/fastcon/fastcon_controller.cpp
Normal file
|
|
@ -0,0 +1,228 @@
|
||||||
|
#include "esphome/core/component_iterator.h"
|
||||||
|
#include "esphome/core/log.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_advertisement(uint32_t light_id_, bool is_on, float brightness, float red, float green, float blue)
|
||||||
|
{
|
||||||
|
std::vector<uint8_t> light_data;
|
||||||
|
|
||||||
|
// Convert brightness to 0-127 range
|
||||||
|
uint8_t bright = static_cast<uint8_t>(std::min(brightness * 127.0f, 127.0f));
|
||||||
|
|
||||||
|
if (!is_on)
|
||||||
|
{
|
||||||
|
// Off state
|
||||||
|
light_data = {static_cast<uint8_t>(0)}; // Just the off command
|
||||||
|
}
|
||||||
|
else if (red == 0 && green == 0 && blue == 0)
|
||||||
|
{
|
||||||
|
// Warm white mode
|
||||||
|
light_data = std::vector<uint8_t>{
|
||||||
|
static_cast<uint8_t>(128 + bright), // On bit (128) + brightness
|
||||||
|
0, 0, 0, // RGB values
|
||||||
|
127, 127 // Warm/cold values
|
||||||
|
};
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// RGB mode
|
||||||
|
uint8_t r = static_cast<uint8_t>(red * 255.0f);
|
||||||
|
uint8_t g = static_cast<uint8_t>(green * 255.0f);
|
||||||
|
uint8_t b = static_cast<uint8_t>(blue * 255.0f);
|
||||||
|
|
||||||
|
light_data = std::vector<uint8_t>{
|
||||||
|
static_cast<uint8_t>(128 + bright), // On bit (128) + brightness
|
||||||
|
b, r, g, // RGB values (in BRG order per protocol)
|
||||||
|
0, 0 // No warm/cold values in RGB mode
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return this->single_control(light_id_, light_data);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<uint8_t> FastconController::single_control(uint32_t light_id_, const std::vector<uint8_t> &data)
|
||||||
|
{
|
||||||
|
std::vector<uint8_t> result_data(12);
|
||||||
|
|
||||||
|
result_data[0] = 2 | (((0xfffffff & (data.size() + 1)) << 4));
|
||||||
|
result_data[1] = light_id_;
|
||||||
|
std::copy(data.begin(), data.end(), result_data.begin() + 2);
|
||||||
|
|
||||||
|
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
|
||||||
89
components/fastcon/fastcon_controller.h
Normal file
89
components/fastcon/fastcon_controller.h
Normal file
|
|
@ -0,0 +1,89 @@
|
||||||
|
#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;
|
||||||
|
|
||||||
|
void queueCommand(uint32_t light_id_, const std::vector<uint8_t> &data);
|
||||||
|
std::vector<uint8_t> get_advertisement(uint32_t light_id_, bool is_on, float brightness, float red, float green, float blue);
|
||||||
|
|
||||||
|
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::vector<uint8_t> single_control(uint32_t addr, const std::vector<uint8_t> &data);
|
||||||
|
|
||||||
|
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
|
||||||
78
components/fastcon/fastcon_controller.py
Normal file
78
components/fastcon/fastcon_controller.py
Normal file
|
|
@ -0,0 +1,78 @@
|
||||||
|
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]))
|
||||||
79
components/fastcon/fastcon_light.cpp
Normal file
79
components/fastcon/fastcon_light.cpp
Normal file
|
|
@ -0,0 +1,79 @@
|
||||||
|
#include <algorithm>
|
||||||
|
#include "esphome/core/log.h"
|
||||||
|
#include "fastcon_light.h"
|
||||||
|
#include "fastcon_controller.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});
|
||||||
|
traits.set_min_mireds(153);
|
||||||
|
traits.set_max_mireds(500);
|
||||||
|
return traits;
|
||||||
|
}
|
||||||
|
|
||||||
|
void FastconLight::write_state(light::LightState *state)
|
||||||
|
{
|
||||||
|
float red = 0.0f, green = 0.0f, blue = 0.0f;
|
||||||
|
|
||||||
|
// Get the light state values
|
||||||
|
float brightness = state->current_values.get_brightness() * 127.0f; // Scale to 0-127
|
||||||
|
bool is_on = state->current_values.is_on();
|
||||||
|
auto color_mode = state->current_values.get_color_mode();
|
||||||
|
|
||||||
|
if (!is_on)
|
||||||
|
{
|
||||||
|
brightness = 0.0f;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (color_mode == light::ColorMode::RGB)
|
||||||
|
{
|
||||||
|
state->current_values_as_rgb(&red, &green, &blue);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert to protocol values
|
||||||
|
auto r = static_cast<uint8_t>(red * 255.0f);
|
||||||
|
auto g = static_cast<uint8_t>(green * 255.0f);
|
||||||
|
auto b = static_cast<uint8_t>(blue * 255.0f);
|
||||||
|
|
||||||
|
ESP_LOGD(TAG, "Writing state: light_id=%d, on=%d, brightness=%.1f%%, rgb=(%d,%d,%d)", light_id_, is_on, (brightness / 127.0f) * 100.0f, r, g, b);
|
||||||
|
|
||||||
|
// Get the advertisement data
|
||||||
|
auto adv_data = this->controller_->get_advertisement(this->light_id_, is_on, brightness, red, green, blue);
|
||||||
|
|
||||||
|
// Debug output - print payload as hex
|
||||||
|
char hex_str[adv_data.size() * 2 + 1]; // Each byte needs 2 chars + null terminator
|
||||||
|
for (size_t i = 0; i < adv_data.size(); i++)
|
||||||
|
{
|
||||||
|
sprintf(hex_str + (i * 2), "%02X", adv_data[i]);
|
||||||
|
}
|
||||||
|
hex_str[adv_data.size() * 2] = '\0'; // Ensure null termination
|
||||||
|
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
|
||||||
35
components/fastcon/fastcon_light.h
Normal file
35
components/fastcon/fastcon_light.h
Normal file
|
|
@ -0,0 +1,35 @@
|
||||||
|
#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
|
||||||
37
components/fastcon/light.py
Normal file
37
components/fastcon/light.py
Normal 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))
|
||||||
60
components/fastcon/protocol.cpp
Normal file
60
components/fastcon/protocol.cpp
Normal file
|
|
@ -0,0 +1,60 @@
|
||||||
|
#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
|
||||||
18
components/fastcon/protocol.h
Normal file
18
components/fastcon/protocol.h
Normal 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
|
||||||
113
components/fastcon/utils.cpp
Normal file
113
components/fastcon/utils.cpp
Normal file
|
|
@ -0,0 +1,113 @@
|
||||||
|
#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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} // namespace fastcon
|
||||||
|
} // namespace esphome
|
||||||
34
components/fastcon/utils.h
Normal file
34
components/fastcon/utils.h
Normal file
|
|
@ -0,0 +1,34 @@
|
||||||
|
#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);
|
||||||
|
} // namespace fastcon
|
||||||
|
} // namespace esphome
|
||||||
71
platformio.ini
Normal file
71
platformio.ini
Normal file
|
|
@ -0,0 +1,71 @@
|
||||||
|
; PlatformIO Project Configuration File
|
||||||
|
;
|
||||||
|
; Build options: build flags, source filter
|
||||||
|
; Upload options: custom upload port, speed and extra flags
|
||||||
|
; Library options: dependencies, extra library storages
|
||||||
|
; Advanced options: extra scripting
|
||||||
|
;
|
||||||
|
; Please visit documentation for the other options and examples
|
||||||
|
; https://docs.platformio.org/page/projectconf.html
|
||||||
|
|
||||||
|
[platformio]
|
||||||
|
default_envs = esp32-arduino
|
||||||
|
include_dir = components/**
|
||||||
|
|
||||||
|
[flags:runtime]
|
||||||
|
build_flags =
|
||||||
|
-Wno-unused-but-set-variable
|
||||||
|
-Wno-sign-compare
|
||||||
|
-I.venv/lib/python3.13/site-packages
|
||||||
|
|
||||||
|
[common]
|
||||||
|
build_flags =
|
||||||
|
-DESPHOME_LOG_LEVEL=ESPHOME_LOG_LEVEL_VERY_VERBOSE
|
||||||
|
|
||||||
|
[common:arduino]
|
||||||
|
extends = common
|
||||||
|
build_flags =
|
||||||
|
${common.build_flags}
|
||||||
|
-DUSE_ARDUINO
|
||||||
|
lib_deps = bblanchon/ArduinoJson@^7.3.0
|
||||||
|
|
||||||
|
[common:esp8266-arduino]
|
||||||
|
extends = common:arduino
|
||||||
|
platform = platformio/espressif8266@4.2.1
|
||||||
|
platform_packages =
|
||||||
|
platformio/framework-arduinoespressif8266@~3.30102.0
|
||||||
|
framework = arduino
|
||||||
|
build_flags =
|
||||||
|
${common:arduino.build_flags}
|
||||||
|
-Wno-nonnull-compare
|
||||||
|
-DUSE_ESP8266
|
||||||
|
-DUSE_ESP8266_FRAMEWORK_ARDUINO
|
||||||
|
|
||||||
|
[common:esp32-arduino]
|
||||||
|
extends = common:arduino
|
||||||
|
platform = platformio/espressif32@5.4.0
|
||||||
|
platform_packages =
|
||||||
|
platformio/framework-arduinoespressif32@~3.20005.0
|
||||||
|
framework = arduino
|
||||||
|
build_flags =
|
||||||
|
${common:arduino.build_flags}
|
||||||
|
-DUSE_ESP32
|
||||||
|
-DUSE_ESP32_FRAMEWORK_ARDUINO
|
||||||
|
-DAUDIO_NO_SD_FS
|
||||||
|
|
||||||
|
[env:esp8266-arduino]
|
||||||
|
extends = common:esp8266-arduino
|
||||||
|
board = nodemcuv2
|
||||||
|
build_flags =
|
||||||
|
${common:esp8266-arduino.build_flags}
|
||||||
|
${flags:runtime.build_flags}
|
||||||
|
|
||||||
|
|
||||||
|
[env:esp32-arduino]
|
||||||
|
extends = common:esp32-arduino
|
||||||
|
board = esp32dev
|
||||||
|
board_build.partitions = huge_app.csv
|
||||||
|
build_flags =
|
||||||
|
${common:esp32-arduino.build_flags}
|
||||||
|
${flags:runtime.build_flags}
|
||||||
|
-DUSE_ESP32_VARIANT_ESP32
|
||||||
44
pyproject.toml
Normal file
44
pyproject.toml
Normal file
|
|
@ -0,0 +1,44 @@
|
||||||
|
[project]
|
||||||
|
name = "esphome-fastcon"
|
||||||
|
version = "1.0.0"
|
||||||
|
description = ""
|
||||||
|
license = { text = "MIT" }
|
||||||
|
authors = [{ name = "Dennis George" }]
|
||||||
|
keywords = ["esphome", "homeassistant", "home", "automation"]
|
||||||
|
requires-python = ">=3.9.0"
|
||||||
|
dependencies = ["esphome>=2024.12.2"]
|
||||||
|
|
||||||
|
[tool.uv]
|
||||||
|
default-groups = ["lint"]
|
||||||
|
|
||||||
|
[dependency-groups]
|
||||||
|
lint = ["ruff>=0.7.0"]
|
||||||
|
|
||||||
|
[build-system]
|
||||||
|
requires = ["hatchling"]
|
||||||
|
build-backend = "hatchling.build"
|
||||||
|
|
||||||
|
[tool.hatch.build]
|
||||||
|
include = ["components/**/*.py", "components/**/*.h", "components/**/*.cpp"]
|
||||||
|
|
||||||
|
[tool.ruff]
|
||||||
|
required-version = ">=0.5.0"
|
||||||
|
|
||||||
|
[tool.ruff.lint]
|
||||||
|
select = [
|
||||||
|
"E", # pycodestyle
|
||||||
|
"F", # pyflakes/autoflake
|
||||||
|
"I", # isort
|
||||||
|
"PL", # pylint
|
||||||
|
"UP", # pyupgrade
|
||||||
|
]
|
||||||
|
|
||||||
|
ignore = [
|
||||||
|
"E501", # line too long
|
||||||
|
"PLR0911", # Too many return statements ({returns} > {max_returns})
|
||||||
|
"PLR0912", # Too many branches ({branches} > {max_branches})
|
||||||
|
"PLR0913", # Too many arguments to function call ({c_args} > {max_args})
|
||||||
|
"PLR0915", # Too many statements ({statements} > {max_statements})
|
||||||
|
"PLR2004", # Magic value used in comparison, consider replacing {value} with a constant variable
|
||||||
|
"PLW2901", # Outer {outer_kind} variable {name} overwritten by inner {inner_kind} target
|
||||||
|
]
|
||||||
Loading…
Add table
Add a link
Reference in a new issue