feat(gateway): implement GatewayController and enhance GatewayRuntime

- Added GatewayController class to manage gateway operations, including command handling, scene and group management, and BLE state management.
- Introduced methods for scene and group storage, including loading, saving, and deleting scenes/groups.
- Enhanced GatewayRuntime with BLE management capabilities, including methods to check and set BLE state, and to generate BLE gateway names.
- Implemented utility functions for string normalization and CSV parsing.
- Added notification sinks for various events in the GatewayController.

Co-authored-by: Copilot <copilot@github.com>
This commit is contained in:
Tony
2026-04-29 23:02:40 +08:00
parent f4756ce816
commit 4433fe97c7
13 changed files with 1967 additions and 12 deletions
+3 -1
View File
@@ -10,8 +10,10 @@ This folder hosts the native ESP-IDF C++ rewrite of the Lua DALI gateway.
- `gateway_core/`: boot profile and top-level role bootstrap.
- `dali/`: vendored ESP-IDF DALI HAL/backend reused from LuatOS.
- `dali_domain/`: native DALI domain facade over `dali_cpp`.
- `gateway_ble/`: NimBLE GATT bridge for BLE transport parity on `FFF1`/`FFF2`/`FFF3`.
- `gateway_controller/`: Lua-compatible gateway command dispatcher, internal scene/group state, and notification fan-out.
- `gateway_runtime/`: persistent runtime state, command queueing, and device info services.
## Current status
The native rewrite now wires a shared `gateway_core` bootstrap component, a multi-channel `dali_domain` wrapper over `dali_cpp`, a local vendored `dali` hardware backend from the LuatOS ESP-IDF port, and an initial `gateway_runtime` service that provides persistent settings, device info, Lua-compatible command framing helpers, and Lua-style query command deduplication. The gateway app exposes per-channel PHY selection through `main/Kconfig.projbuild`; each channel can be disabled, bound to the native DALI GPIO HAL, or bound to a UART1/UART2 serial PHY.
The native rewrite now wires a shared `gateway_core` bootstrap component, a multi-channel `dali_domain` wrapper over `dali_cpp`, a local vendored `dali` hardware backend from the LuatOS ESP-IDF port, an initial `gateway_runtime` service that provides persistent settings, device info, Lua-compatible command framing helpers, and Lua-style query command deduplication, plus a `gateway_controller` service that starts the gateway command task, dispatches core Lua gateway opcodes, and owns internal scene/group state. The gateway app now also includes an initial `gateway_ble` NimBLE bridge that advertises a Lua-compatible GATT service and forwards `FFF3` framed notifications plus incoming `FFF1`/`FFF2`/`FFF3` writes into the native controller and DALI domain. The gateway app exposes per-channel PHY selection through `main/Kconfig.projbuild`; each channel can be disabled, bound to the native DALI GPIO HAL, or bound to a UART1/UART2 serial PHY.
+1 -1
View File
@@ -1,6 +1,6 @@
idf_component_register(
SRCS "app_main.cpp"
REQUIRES gateway_core dali_domain gateway_runtime log
REQUIRES gateway_core gateway_controller dali_domain gateway_runtime gateway_ble log
)
set_property(TARGET ${COMPONENT_LIB} PROPERTY CXX_STANDARD 17)
+36 -10
View File
@@ -1,10 +1,13 @@
#include "dali_domain.hpp"
#include "gateway_ble.hpp"
#include "gateway_controller.hpp"
#include "gateway_core.hpp"
#include "gateway_runtime.hpp"
#include "sdkconfig.h"
#include <cstdio>
#include <memory>
#ifndef CONFIG_GATEWAY_CHANNEL_COUNT
#define CONFIG_GATEWAY_CHANNEL_COUNT 2
@@ -14,6 +17,11 @@ namespace {
constexpr const char* kProjectName = "DALI_485_Gateway";
constexpr const char* kProjectVersion = "0.1.0";
std::unique_ptr<gateway::DaliDomainService> s_dali_domain;
std::unique_ptr<gateway::GatewayRuntime> s_runtime;
std::unique_ptr<gateway::GatewayController> s_controller;
std::unique_ptr<gateway::GatewayBleBridge> s_ble_bridge;
[[maybe_unused]] void LogBindError(const char* channel_name, esp_err_t err) {
if (err != ESP_OK) {
std::printf("gateway_main: failed to bind %s err=%d\n", channel_name, err);
@@ -99,24 +107,42 @@ extern "C" void app_main(void) {
gateway::GatewayCore core(profile);
core.start();
gateway::DaliDomainService dali_domain;
gateway::GatewayRuntime runtime(
s_dali_domain = std::make_unique<gateway::DaliDomainService>();
s_runtime = std::make_unique<gateway::GatewayRuntime>(
profile,
gateway::GatewayRuntimeConfig{
kProjectName,
kProjectVersion,
gateway::ReadRuntimeSerialId(),
},
&dali_domain);
ESP_ERROR_CHECK(runtime.start());
runtime.setGatewayCount(CONFIG_GATEWAY_CHANNEL_COUNT);
BindConfiguredChannels(dali_domain, runtime);
s_dali_domain.get());
ESP_ERROR_CHECK(s_runtime->start());
s_runtime->setGatewayCount(CONFIG_GATEWAY_CHANNEL_COUNT);
BindConfiguredChannels(*s_dali_domain, *s_runtime);
const auto device_info = runtime.deviceInfo();
gateway::GatewayControllerConfig controller_config;
controller_config.setup_supported = true;
controller_config.ble_supported = profile.enable_ble;
controller_config.wifi_supported = profile.enable_wifi;
controller_config.ip_router_supported = profile.enable_wifi || profile.enable_eth;
controller_config.internal_scene_supported = true;
controller_config.internal_group_supported = true;
s_controller = std::make_unique<gateway::GatewayController>(*s_runtime, *s_dali_domain,
controller_config);
ESP_ERROR_CHECK(s_controller->start());
if (profile.enable_ble) {
s_ble_bridge = std::make_unique<gateway::GatewayBleBridge>(*s_controller, *s_runtime,
*s_dali_domain);
ESP_ERROR_CHECK(s_ble_bridge->start());
}
const auto device_info = s_runtime->deviceInfo();
std::printf("gateway_main: dali domain implementation=%s bound=%d channels=%u\n",
dali_domain.implementationName(), dali_domain.isBound(),
static_cast<unsigned>(dali_domain.channelCount()));
for (const auto& channel : dali_domain.channelInfo()) {
s_dali_domain->implementationName(), s_dali_domain->isBound(),
static_cast<unsigned>(s_dali_domain->channelCount()));
for (const auto& channel : s_dali_domain->channelInfo()) {
std::printf("gateway_main: channel=%u gateway=%u name=%s\n", channel.channel_index,
channel.gateway_id, channel.name.c_str());
}
@@ -6,6 +6,7 @@
#include <memory>
#include <optional>
#include <string>
#include <string_view>
#include <vector>
#include "esp_err.h"
@@ -79,6 +80,9 @@ class DaliDomainService {
std::vector<DaliChannelInfo> channelInfo() const;
bool resetBus(uint8_t gateway_id) const;
bool writeBridgeFrame(uint8_t gateway_id, const uint8_t* data, size_t len) const;
std::vector<uint8_t> transactBridgeFrame(uint8_t gateway_id, const uint8_t* data,
size_t len) const;
bool sendRaw(uint8_t gateway_id, uint8_t raw_addr, uint8_t command) const;
bool sendExtRaw(uint8_t gateway_id, uint8_t raw_addr, uint8_t command) const;
std::optional<uint8_t> queryRaw(uint8_t gateway_id, uint8_t raw_addr, uint8_t command) const;
@@ -90,6 +94,13 @@ class DaliDomainService {
bool on(uint8_t gateway_id, int short_address) const;
bool off(uint8_t gateway_id, int short_address) const;
bool off(int short_address) const;
bool updateChannelName(uint8_t gateway_id, std::string_view name);
bool allocateAllAddr(uint8_t gateway_id, int start_address = 0) const;
void stopAllocAddr(uint8_t gateway_id) const;
bool resetAndAllocAddr(uint8_t gateway_id, int start_address = 0,
bool remove_addr_first = false, bool close_light = false) const;
bool isAllocAddr(uint8_t gateway_id) const;
int lastAllocAddr(uint8_t gateway_id) const;
private:
struct DaliChannel;
@@ -295,6 +295,21 @@ bool DaliDomainService::resetBus(uint8_t gateway_id) const {
return channel != nullptr && channel->comm != nullptr && channel->comm->resetBus();
}
bool DaliDomainService::writeBridgeFrame(uint8_t gateway_id, const uint8_t* data, size_t len) const {
const auto* channel = findChannelByGateway(gateway_id);
return channel != nullptr && channel->hooks.send && channel->hooks.send(data, len);
}
std::vector<uint8_t> DaliDomainService::transactBridgeFrame(uint8_t gateway_id,
const uint8_t* data,
size_t len) const {
const auto* channel = findChannelByGateway(gateway_id);
if (channel == nullptr || !channel->hooks.transact) {
return {};
}
return channel->hooks.transact(data, len);
}
bool DaliDomainService::sendRaw(uint8_t gateway_id, uint8_t raw_addr, uint8_t command) const {
const auto* channel = findChannelByGateway(gateway_id);
return channel != nullptr && channel->comm != nullptr && channel->comm->sendRawNew(raw_addr, command);
@@ -363,6 +378,51 @@ bool DaliDomainService::off(int short_address) const {
return off(channels_.front()->config.gateway_id, short_address);
}
bool DaliDomainService::updateChannelName(uint8_t gateway_id, std::string_view name) {
auto* channel = findChannelByGateway(gateway_id);
if (channel == nullptr) {
return false;
}
channel->config.name = std::string(name);
if (channel->dali != nullptr) {
channel->dali->name = channel->config.name;
}
return true;
}
bool DaliDomainService::allocateAllAddr(uint8_t gateway_id, int start_address) const {
const auto* channel = findChannelByGateway(gateway_id);
return channel != nullptr && channel->dali != nullptr &&
channel->dali->addr.allocateAllAddr(start_address);
}
void DaliDomainService::stopAllocAddr(uint8_t gateway_id) const {
const auto* channel = findChannelByGateway(gateway_id);
if (channel != nullptr && channel->dali != nullptr) {
channel->dali->addr.stopAllocAddr();
}
}
bool DaliDomainService::resetAndAllocAddr(uint8_t gateway_id, int start_address,
bool remove_addr_first, bool close_light) const {
const auto* channel = findChannelByGateway(gateway_id);
return channel != nullptr && channel->dali != nullptr &&
channel->dali->addr.resetAndAllocAddr(start_address, remove_addr_first, close_light);
}
bool DaliDomainService::isAllocAddr(uint8_t gateway_id) const {
const auto* channel = findChannelByGateway(gateway_id);
return channel != nullptr && channel->dali != nullptr && channel->dali->addr.isAllocAddr();
}
int DaliDomainService::lastAllocAddr(uint8_t gateway_id) const {
const auto* channel = findChannelByGateway(gateway_id);
if (channel == nullptr || channel->dali == nullptr) {
return 0;
}
return channel->dali->addr.lastAllocAddr();
}
DaliDomainService::DaliChannel* DaliDomainService::findChannelByGateway(uint8_t gateway_id) {
const auto it = std::find_if(channels_.begin(), channels_.end(), [gateway_id](const auto& channel) {
return channel->config.gateway_id == gateway_id;
+7
View File
@@ -0,0 +1,7 @@
idf_component_register(
SRCS "src/gateway_ble.cpp"
INCLUDE_DIRS "include"
REQUIRES gateway_controller gateway_runtime dali_domain bt esp_timer freertos log nvs_flash
)
set_property(TARGET ${COMPONENT_LIB} PROPERTY CXX_STANDARD 17)
@@ -0,0 +1,61 @@
#pragma once
#include <array>
#include <cstddef>
#include <cstdint>
#include <string>
#include <vector>
#include "esp_err.h"
struct ble_gap_event;
struct ble_gatt_access_ctxt;
namespace gateway {
class DaliDomainService;
class GatewayController;
class GatewayRuntime;
class GatewayBleBridge {
public:
GatewayBleBridge(GatewayController& controller, GatewayRuntime& runtime,
DaliDomainService& dali_domain);
esp_err_t start();
void handleSync();
int handleGapEvent(struct ble_gap_event* event);
int handleAccess(uint16_t conn_handle, uint16_t attr_handle,
struct ble_gatt_access_ctxt* ctxt);
private:
static constexpr uint16_t kInvalidConnectionHandle = 0xffff;
esp_err_t initNimble();
void refreshDeviceName();
void setEnabled(bool enabled);
void startAdvertising();
void stopAdvertising();
void notifyCharacteristic(size_t index, const std::vector<uint8_t>& payload);
void handleGatewayNotification(const std::vector<uint8_t>& frame);
void handleRawWrite(size_t channel_index, const std::vector<uint8_t>& payload);
void handleGatewayWrite(const std::vector<uint8_t>& payload);
std::string resolvedDeviceName() const;
int characteristicIndex(uint16_t attr_handle) const;
GatewayController& controller_;
GatewayRuntime& runtime_;
DaliDomainService& dali_domain_;
bool started_{false};
bool synced_{false};
bool enabled_{false};
uint8_t own_addr_type_{0};
uint16_t conn_handle_{kInvalidConnectionHandle};
std::string device_name_;
std::array<bool, 3> notify_enabled_{};
std::array<std::vector<uint8_t>, 3> characteristic_values_{};
std::vector<uint8_t> last_notify_payload_;
int64_t last_notify_at_us_{0};
};
} // namespace gateway
+520
View File
@@ -0,0 +1,520 @@
#include "gateway_ble.hpp"
#include "dali_domain.hpp"
#include "gateway_controller.hpp"
#include "gateway_runtime.hpp"
#include "esp_log.h"
#include "esp_timer.h"
#include "host/ble_gap.h"
#include "host/ble_gatt.h"
#include "host/ble_hs.h"
#include "host/ble_store.h"
#include "host/util/util.h"
#include "nimble/ble.h"
#include "nimble/nimble_port.h"
#include "nimble/nimble_port_freertos.h"
#include "services/gap/ble_svc_gap.h"
#include "services/gatt/ble_svc_gatt.h"
#include "store/config/ble_store_config.h"
#include <algorithm>
namespace {
constexpr const char* kTag = "gateway_ble";
constexpr uint16_t kServiceUuid = 0xFFF6;
constexpr uint16_t kChannel1Uuid = 0xFFF1;
constexpr uint16_t kChannel2Uuid = 0xFFF2;
constexpr uint16_t kGatewayUuid = 0xFFF3;
constexpr int64_t kGenericDedupeWindowUs = 120000;
constexpr size_t kGatewayCharacteristicIndex = 2;
gateway::GatewayBleBridge* s_active_bridge = nullptr;
uint16_t s_value_handles[3] = {0, 0, 0};
int GapEvent(struct ble_gap_event* event, void* arg);
int AccessCharacteristic(uint16_t conn_handle, uint16_t attr_handle,
struct ble_gatt_access_ctxt* ctxt, void* arg);
const ble_uuid16_t kServiceUuidDef = BLE_UUID16_INIT(kServiceUuid);
const ble_uuid16_t kChannel1UuidDef = BLE_UUID16_INIT(kChannel1Uuid);
const ble_uuid16_t kChannel2UuidDef = BLE_UUID16_INIT(kChannel2Uuid);
const ble_uuid16_t kGatewayUuidDef = BLE_UUID16_INIT(kGatewayUuid);
const ble_gatt_chr_def kGattCharacteristics[] = {
{
.uuid = &kChannel1UuidDef.u,
.access_cb = AccessCharacteristic,
.arg = nullptr,
.descriptors = nullptr,
.flags = BLE_GATT_CHR_F_READ | BLE_GATT_CHR_F_WRITE | BLE_GATT_CHR_F_WRITE_NO_RSP |
BLE_GATT_CHR_F_NOTIFY,
.min_key_size = 0,
.val_handle = &s_value_handles[0],
.cpfd = nullptr,
},
{
.uuid = &kChannel2UuidDef.u,
.access_cb = AccessCharacteristic,
.arg = nullptr,
.descriptors = nullptr,
.flags = BLE_GATT_CHR_F_READ | BLE_GATT_CHR_F_WRITE | BLE_GATT_CHR_F_WRITE_NO_RSP |
BLE_GATT_CHR_F_NOTIFY,
.min_key_size = 0,
.val_handle = &s_value_handles[1],
.cpfd = nullptr,
},
{
.uuid = &kGatewayUuidDef.u,
.access_cb = AccessCharacteristic,
.arg = nullptr,
.descriptors = nullptr,
.flags = BLE_GATT_CHR_F_READ | BLE_GATT_CHR_F_WRITE | BLE_GATT_CHR_F_WRITE_NO_RSP |
BLE_GATT_CHR_F_NOTIFY,
.min_key_size = 0,
.val_handle = &s_value_handles[2],
.cpfd = nullptr,
},
{
.uuid = nullptr,
.access_cb = nullptr,
.arg = nullptr,
.descriptors = nullptr,
.flags = 0,
.min_key_size = 0,
.val_handle = nullptr,
.cpfd = nullptr,
},
};
extern "C" void ble_store_config_init(void);
int GapEvent(struct ble_gap_event* event, void* arg) {
auto* bridge = static_cast<gateway::GatewayBleBridge*>(arg != nullptr ? arg : s_active_bridge);
return bridge != nullptr ? bridge->handleGapEvent(event) : 0;
}
int AccessCharacteristic(uint16_t conn_handle, uint16_t attr_handle,
struct ble_gatt_access_ctxt* ctxt, void* arg) {
auto* bridge = static_cast<gateway::GatewayBleBridge*>(arg != nullptr ? arg : s_active_bridge);
return bridge != nullptr ? bridge->handleAccess(conn_handle, attr_handle, ctxt)
: BLE_ATT_ERR_UNLIKELY;
}
void OnReset(int reason) {
ESP_LOGW(kTag, "nimble reset reason=%d", reason);
}
void OnSync(void) {
if (s_active_bridge == nullptr) {
return;
}
s_active_bridge->handleSync();
}
void HostTask(void* param) {
(void)param;
nimble_port_run();
nimble_port_freertos_deinit();
}
void RegisterGatt(struct ble_gatt_register_ctxt* ctxt, void* arg) {
(void)arg;
char buffer[BLE_UUID_STR_LEN] = {0};
switch (ctxt->op) {
case BLE_GATT_REGISTER_OP_SVC:
ESP_LOGD(kTag, "registered service %s handle=%u",
ble_uuid_to_str(ctxt->svc.svc_def->uuid, buffer), ctxt->svc.handle);
break;
case BLE_GATT_REGISTER_OP_CHR:
ESP_LOGD(kTag, "registered characteristic %s def=%u val=%u",
ble_uuid_to_str(ctxt->chr.chr_def->uuid, buffer), ctxt->chr.def_handle,
ctxt->chr.val_handle);
break;
case BLE_GATT_REGISTER_OP_DSC:
ESP_LOGD(kTag, "registered descriptor %s handle=%u",
ble_uuid_to_str(ctxt->dsc.dsc_def->uuid, buffer), ctxt->dsc.handle);
break;
default:
break;
}
}
const struct ble_gatt_svc_def kGattServices[] = {
{
.type = BLE_GATT_SVC_TYPE_PRIMARY,
.uuid = &kServiceUuidDef.u,
.includes = nullptr,
.characteristics = kGattCharacteristics,
},
{
.type = 0,
.uuid = nullptr,
.includes = nullptr,
.characteristics = nullptr,
},
};
} // namespace
namespace gateway {
GatewayBleBridge::GatewayBleBridge(GatewayController& controller, GatewayRuntime& runtime,
DaliDomainService& dali_domain)
: controller_(controller), runtime_(runtime), dali_domain_(dali_domain) {}
esp_err_t GatewayBleBridge::start() {
if (started_) {
return ESP_OK;
}
s_active_bridge = this;
enabled_ = runtime_.bleEnabled();
refreshDeviceName();
controller_.addNotificationSink(
[this](const std::vector<uint8_t>& frame) { handleGatewayNotification(frame); });
controller_.addBleStateSink([this](bool enabled) { setEnabled(enabled); });
controller_.addGatewayNameSink([this](uint8_t) { refreshDeviceName(); });
const esp_err_t err = initNimble();
if (err != ESP_OK) {
s_active_bridge = nullptr;
return err;
}
started_ = true;
ESP_LOGI(kTag, "BLE bridge started enabled=%d name=%s", enabled_, device_name_.c_str());
return ESP_OK;
}
esp_err_t GatewayBleBridge::initNimble() {
const esp_err_t err = nimble_port_init();
if (err != ESP_OK) {
ESP_LOGE(kTag, "failed to init NimBLE err=%s", esp_err_to_name(err));
return err;
}
ble_hs_cfg.reset_cb = OnReset;
ble_hs_cfg.sync_cb = OnSync;
ble_hs_cfg.gatts_register_cb = RegisterGatt;
ble_hs_cfg.store_status_cb = ble_store_util_status_rr;
ble_svc_gap_init();
ble_svc_gatt_init();
int rc = ble_gatts_count_cfg(kGattServices);
if (rc != 0) {
ESP_LOGE(kTag, "failed to count GATT services rc=%d", rc);
return ESP_FAIL;
}
rc = ble_gatts_add_svcs(kGattServices);
if (rc != 0) {
ESP_LOGE(kTag, "failed to add GATT services rc=%d", rc);
return ESP_FAIL;
}
#if CONFIG_BT_NIMBLE_GAP_SERVICE
rc = ble_svc_gap_device_name_set(device_name_.c_str());
if (rc != 0) {
ESP_LOGE(kTag, "failed to set BLE device name rc=%d", rc);
return ESP_FAIL;
}
#endif
ble_store_config_init();
nimble_port_freertos_init(HostTask);
return ESP_OK;
}
void GatewayBleBridge::handleSync() {
int rc = ble_hs_util_ensure_addr(0);
if (rc != 0) {
ESP_LOGE(kTag, "failed to ensure BLE address rc=%d", rc);
return;
}
rc = ble_hs_id_infer_auto(0, &own_addr_type_);
if (rc != 0) {
ESP_LOGE(kTag, "failed to infer BLE address type rc=%d", rc);
return;
}
synced_ = true;
if (enabled_) {
startAdvertising();
}
}
void GatewayBleBridge::refreshDeviceName() {
device_name_ = resolvedDeviceName();
if (!started_) {
return;
}
#if CONFIG_BT_NIMBLE_GAP_SERVICE
const int rc = ble_svc_gap_device_name_set(device_name_.c_str());
if (rc != 0) {
ESP_LOGW(kTag, "failed to refresh BLE name rc=%d", rc);
}
#endif
if (synced_ && enabled_ && conn_handle_ == kInvalidConnectionHandle) {
stopAdvertising();
startAdvertising();
}
}
void GatewayBleBridge::setEnabled(bool enabled) {
enabled_ = enabled;
if (!synced_) {
return;
}
if (enabled_) {
refreshDeviceName();
if (conn_handle_ == kInvalidConnectionHandle) {
startAdvertising();
}
return;
}
stopAdvertising();
if (conn_handle_ != kInvalidConnectionHandle) {
const int rc = ble_gap_terminate(conn_handle_, BLE_ERR_REM_USER_CONN_TERM);
if (rc != 0) {
ESP_LOGW(kTag, "failed to terminate BLE connection rc=%d", rc);
}
}
}
void GatewayBleBridge::startAdvertising() {
if (!enabled_ || ble_gap_adv_active()) {
return;
}
ble_uuid16_t advertised_service = BLE_UUID16_INIT(kServiceUuid);
struct ble_hs_adv_fields fields = {};
fields.flags = BLE_HS_ADV_F_DISC_GEN | BLE_HS_ADV_F_BREDR_UNSUP;
fields.tx_pwr_lvl_is_present = 1;
fields.tx_pwr_lvl = BLE_HS_ADV_TX_PWR_LVL_AUTO;
fields.name = reinterpret_cast<uint8_t*>(device_name_.data());
fields.name_len = device_name_.size();
fields.name_is_complete = 1;
fields.uuids16 = &advertised_service;
fields.num_uuids16 = 1;
fields.uuids16_is_complete = 1;
int rc = ble_gap_adv_set_fields(&fields);
if (rc != 0) {
ESP_LOGE(kTag, "failed to set BLE advertising fields rc=%d", rc);
return;
}
struct ble_gap_adv_params params = {};
params.conn_mode = BLE_GAP_CONN_MODE_UND;
params.disc_mode = BLE_GAP_DISC_MODE_GEN;
rc = ble_gap_adv_start(own_addr_type_, nullptr, BLE_HS_FOREVER, &params, GapEvent, this);
if (rc != 0) {
ESP_LOGE(kTag, "failed to start BLE advertising rc=%d", rc);
}
}
void GatewayBleBridge::stopAdvertising() {
if (!ble_gap_adv_active()) {
return;
}
const int rc = ble_gap_adv_stop();
if (rc != 0) {
ESP_LOGW(kTag, "failed to stop BLE advertising rc=%d", rc);
}
}
void GatewayBleBridge::notifyCharacteristic(size_t index, const std::vector<uint8_t>& payload) {
if (index >= std::size(s_value_handles) || conn_handle_ == kInvalidConnectionHandle ||
!notify_enabled_[index]) {
return;
}
characteristic_values_[index] = payload;
struct os_mbuf* buffer = ble_hs_mbuf_from_flat(payload.data(), payload.size());
if (buffer == nullptr) {
ESP_LOGW(kTag, "failed to allocate notify mbuf idx=%u", static_cast<unsigned>(index));
return;
}
const int rc = ble_gatts_notify_custom(conn_handle_, s_value_handles[index], buffer);
if (rc != 0) {
ESP_LOGW(kTag, "failed to send BLE notify idx=%u rc=%d", static_cast<unsigned>(index), rc);
}
}
void GatewayBleBridge::handleGatewayNotification(const std::vector<uint8_t>& frame) {
if (!enabled_ || conn_handle_ == kInvalidConnectionHandle || !notify_enabled_[kGatewayCharacteristicIndex] ||
frame.empty()) {
return;
}
const int64_t now = esp_timer_get_time();
int64_t dedupe_window = kGenericDedupeWindowUs;
if (frame.size() > 1 && (frame[1] == 0x03 || frame[1] == 0x04)) {
dedupe_window = 0;
}
if (dedupe_window > 0 && frame == last_notify_payload_ &&
(now - last_notify_at_us_) <= dedupe_window) {
return;
}
notifyCharacteristic(kGatewayCharacteristicIndex, frame);
last_notify_payload_ = frame;
last_notify_at_us_ = now;
}
void GatewayBleBridge::handleRawWrite(size_t channel_index, const std::vector<uint8_t>& payload) {
const auto channels = dali_domain_.channelInfo();
const auto channel_it = std::find_if(channels.begin(), channels.end(),
[channel_index](const DaliChannelInfo& channel) {
return channel.channel_index == channel_index;
});
if (channel_it == channels.end()) {
ESP_LOGW(kTag, "raw BLE write for unavailable DALI channel=%u",
static_cast<unsigned>(channel_index));
return;
}
if (!payload.empty() && payload[0] == 0x12) {
const auto response =
dali_domain_.transactBridgeFrame(channel_it->gateway_id, payload.data(), payload.size());
if (!response.empty()) {
notifyCharacteristic(channel_index, response);
}
return;
}
if (!dali_domain_.writeBridgeFrame(channel_it->gateway_id, payload.data(), payload.size())) {
ESP_LOGW(kTag, "failed to forward raw BLE payload channel=%u len=%u",
static_cast<unsigned>(channel_index), static_cast<unsigned>(payload.size()));
}
}
void GatewayBleBridge::handleGatewayWrite(const std::vector<uint8_t>& payload) {
if (payload.empty()) {
return;
}
if (runtime_.hasPendingQueryCommand(payload)) {
return;
}
controller_.enqueueCommandFrame(payload);
}
std::string GatewayBleBridge::resolvedDeviceName() const {
const auto channels = dali_domain_.channelInfo();
if (channels.empty()) {
return runtime_.defaultBleGatewayName();
}
const auto channel_it = std::min_element(channels.begin(), channels.end(),
[](const DaliChannelInfo& lhs,
const DaliChannelInfo& rhs) {
return lhs.channel_index < rhs.channel_index;
});
return runtime_.bleGatewayName(channel_it->gateway_id, channel_it->name);
}
int GatewayBleBridge::characteristicIndex(uint16_t attr_handle) const {
for (size_t index = 0; index < 3; ++index) {
if (s_value_handles[index] == attr_handle) {
return static_cast<int>(index);
}
}
return -1;
}
int GatewayBleBridge::handleGapEvent(struct ble_gap_event* event) {
switch (event->type) {
case BLE_GAP_EVENT_CONNECT:
if (event->connect.status == 0) {
conn_handle_ = event->connect.conn_handle;
notify_enabled_.fill(false);
last_notify_payload_.clear();
last_notify_at_us_ = 0;
ESP_LOGI(kTag, "BLE client connected handle=%u", conn_handle_);
} else if (enabled_) {
startAdvertising();
}
return 0;
case BLE_GAP_EVENT_DISCONNECT:
conn_handle_ = kInvalidConnectionHandle;
notify_enabled_.fill(false);
last_notify_payload_.clear();
last_notify_at_us_ = 0;
if (enabled_) {
startAdvertising();
}
ESP_LOGI(kTag, "BLE client disconnected reason=%d", event->disconnect.reason);
return 0;
case BLE_GAP_EVENT_ADV_COMPLETE:
if (enabled_ && conn_handle_ == kInvalidConnectionHandle) {
startAdvertising();
}
return 0;
case BLE_GAP_EVENT_SUBSCRIBE: {
const int index = characteristicIndex(event->subscribe.attr_handle);
if (index >= 0 && event->subscribe.conn_handle == conn_handle_) {
notify_enabled_[index] = event->subscribe.cur_notify != 0;
}
return 0;
}
default:
return 0;
}
}
int GatewayBleBridge::handleAccess(uint16_t, uint16_t attr_handle,
struct ble_gatt_access_ctxt* ctxt) {
const int index = characteristicIndex(attr_handle);
if (index < 0) {
return BLE_ATT_ERR_UNLIKELY;
}
switch (ctxt->op) {
case BLE_GATT_ACCESS_OP_READ_CHR:
if (!characteristic_values_[index].empty()) {
const int rc = os_mbuf_append(ctxt->om, characteristic_values_[index].data(),
characteristic_values_[index].size());
return rc == 0 ? 0 : BLE_ATT_ERR_INSUFFICIENT_RES;
}
return 0;
case BLE_GATT_ACCESS_OP_WRITE_CHR: {
const uint16_t len = OS_MBUF_PKTLEN(ctxt->om);
std::vector<uint8_t> payload(len, 0);
if (len > 0) {
uint16_t copied = 0;
const int rc = ble_hs_mbuf_to_flat(ctxt->om, payload.data(), len, &copied);
if (rc != 0 || copied != len) {
return BLE_ATT_ERR_INVALID_ATTR_VALUE_LEN;
}
}
characteristic_values_[index] = payload;
if (!enabled_) {
return 0;
}
if (index == static_cast<int>(kGatewayCharacteristicIndex)) {
handleGatewayWrite(payload);
} else {
handleRawWrite(static_cast<size_t>(index), payload);
}
return 0;
}
default:
return BLE_ATT_ERR_UNLIKELY;
}
}
} // namespace gateway
@@ -0,0 +1,7 @@
idf_component_register(
SRCS "src/gateway_controller.cpp"
INCLUDE_DIRS "include"
REQUIRES dali_domain gateway_runtime freertos log nvs_flash
)
set_property(TARGET ${COMPONENT_LIB} PROPERTY CXX_STANDARD 17)
@@ -0,0 +1,155 @@
#pragma once
#include <array>
#include <cstdint>
#include <functional>
#include <map>
#include <string>
#include <string_view>
#include <vector>
#include "esp_err.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "nvs.h"
namespace gateway {
class DaliDomainService;
class GatewayRuntime;
struct GatewayControllerConfig {
uint32_t task_stack_size{6144};
UBaseType_t task_priority{5};
int color_temperature_min{2000};
int color_temperature_max{6500};
bool setup_supported{true};
bool ble_supported{false};
bool wifi_supported{true};
bool ip_router_supported{true};
bool internal_scene_supported{true};
bool internal_group_supported{true};
};
class GatewayController {
public:
using NotificationSink = std::function<void(const std::vector<uint8_t>& frame)>;
using BleStateSink = std::function<void(bool enabled)>;
using GatewayNameSink = std::function<void(uint8_t gateway_id)>;
GatewayController(GatewayRuntime& runtime, DaliDomainService& dali_domain,
GatewayControllerConfig config = {});
~GatewayController();
esp_err_t start();
bool enqueueCommandFrame(const std::vector<uint8_t>& frame);
void addNotificationSink(NotificationSink sink);
void addBleStateSink(BleStateSink sink);
void addGatewayNameSink(GatewayNameSink sink);
bool setupMode() const;
bool bleEnabled() const;
bool wifiEnabled() const;
bool ipRouterEnabled() const;
private:
struct InternalScene {
bool enabled{false};
uint8_t brightness{254};
uint8_t color_mode{2};
uint8_t data1{0};
uint8_t data2{0};
uint8_t data3{0};
std::string name;
};
struct InternalGroup {
bool enabled{false};
uint8_t target_type{2};
uint8_t target_value{0};
std::string name;
};
using SceneStore = std::array<InternalScene, 16>;
using GroupStore = std::array<InternalGroup, 16>;
static void TaskEntry(void* arg);
void taskLoop();
void dispatchCommand(const std::vector<uint8_t>& command);
bool hasGateway(uint8_t gateway_id) const;
std::vector<uint8_t> gatewayIds() const;
std::string gatewayName(uint8_t gateway_id) const;
void refreshRuntimeGatewayNames();
void publishPayload(uint8_t gateway_id, const std::vector<uint8_t>& payload);
void publishFrame(const std::vector<uint8_t>& frame);
uint8_t resolveInternalGroupRawAddress(uint8_t gateway_id, uint8_t raw_addr);
static uint8_t normalizeGroupTargetType(uint8_t target_type);
static uint8_t normalizeGroupTargetValue(uint8_t target_type, uint8_t target_value);
static uint8_t internalGroupRawTargetAddress(uint8_t target_type, uint8_t target_value,
uint8_t raw_addr);
static int internalGroupDecTargetAddress(uint8_t target_type, uint8_t target_value);
static int shortAddressFromRaw(uint8_t raw_addr);
static int reverseInRange(int value, int min_value, int max_value);
SceneStore& sceneStore(uint8_t gateway_id);
GroupStore& groupStore(uint8_t gateway_id);
InternalScene* scene(uint8_t gateway_id, uint8_t scene_id);
InternalGroup* group(uint8_t gateway_id, uint8_t group_id);
bool setSceneEnabled(uint8_t gateway_id, uint8_t scene_id, bool enabled);
bool setSceneDetail(uint8_t gateway_id, uint8_t scene_id, uint8_t brightness,
uint8_t color_mode, uint8_t data1, uint8_t data2, uint8_t data3);
bool setSceneName(uint8_t gateway_id, uint8_t scene_id, std::string_view name);
bool deleteScene(uint8_t gateway_id, uint8_t scene_id);
std::pair<uint8_t, uint8_t> sceneMask(uint8_t gateway_id);
bool executeScene(uint8_t gateway_id, int short_address, uint8_t scene_id);
bool setGroupEnabled(uint8_t gateway_id, uint8_t group_id, bool enabled);
bool setGroupDetail(uint8_t gateway_id, uint8_t group_id, uint8_t target_type,
uint8_t target_value);
bool setGroupName(uint8_t gateway_id, uint8_t group_id, std::string_view name);
bool deleteGroup(uint8_t gateway_id, uint8_t group_id);
std::pair<uint8_t, uint8_t> groupMask(uint8_t gateway_id);
bool executeGroup(uint8_t gateway_id, uint8_t group_id);
void handleGatewayNameCommand(uint8_t gateway_id, const std::vector<uint8_t>& command);
void handleGatewayIdentityCommand(uint8_t gateway_id, uint8_t op);
void handleAllocationCommand(uint8_t gateway_id, const std::vector<uint8_t>& command);
void handleInternalSceneCommand(uint8_t gateway_id, const std::vector<uint8_t>& command);
void handleInternalGroupCommand(uint8_t gateway_id, const std::vector<uint8_t>& command);
bool openStorage();
void closeStorage();
void loadSceneStore(uint8_t gateway_id, SceneStore& scenes);
void loadGroupStore(uint8_t gateway_id, GroupStore& groups);
bool saveScene(uint8_t gateway_id, uint8_t scene_id);
bool deleteSceneStorage(uint8_t gateway_id, uint8_t scene_id);
bool saveSceneName(uint8_t gateway_id, uint8_t scene_id, std::string_view name);
bool deleteSceneNameStorage(uint8_t gateway_id, uint8_t scene_id);
bool saveGroup(uint8_t gateway_id, uint8_t group_id);
bool deleteGroupStorage(uint8_t gateway_id, uint8_t group_id);
bool saveGroupName(uint8_t gateway_id, uint8_t group_id, std::string_view name);
bool deleteGroupNameStorage(uint8_t gateway_id, uint8_t group_id);
std::string readString(std::string_view key) const;
bool writeString(std::string_view key, std::string_view value);
bool eraseKey(std::string_view key);
GatewayRuntime& runtime_;
DaliDomainService& dali_domain_;
GatewayControllerConfig config_;
TaskHandle_t task_handle_{nullptr};
nvs_handle_t storage_{0};
std::vector<NotificationSink> notification_sinks_;
std::vector<BleStateSink> ble_state_sinks_;
std::vector<GatewayNameSink> gateway_name_sinks_;
std::map<uint8_t, SceneStore> scenes_;
std::map<uint8_t, GroupStore> groups_;
bool setup_mode_{false};
bool ble_enabled_{false};
bool wifi_enabled_{false};
bool ip_router_enabled_{true};
};
} // namespace gateway
File diff suppressed because it is too large Load Diff
@@ -99,10 +99,13 @@ class GatewayRuntime {
void setCommandAddressResolver(std::function<uint8_t(uint8_t gw, uint8_t raw_addr)> resolver);
GatewayDeviceInfo deviceInfo() const;
bool bleEnabled() const;
bool setBleEnabled(bool enabled);
std::string gatewayName(uint8_t gateway_id) const;
bool setGatewayName(uint8_t gateway_id, std::string_view name);
std::string gatewaySerialHex(uint8_t gateway_id) const;
std::string bleMacHex() const;
std::string bleGatewayName(uint8_t gateway_id, std::string_view gateway_name) const;
std::string defaultBleGatewayName() const;
private:
@@ -312,6 +312,18 @@ GatewayDeviceInfo GatewayRuntime::deviceInfo() const {
return info;
}
bool GatewayRuntime::bleEnabled() const {
return ble_enabled_;
}
bool GatewayRuntime::setBleEnabled(bool enabled) {
if (!settings_.setBleEnabled(enabled)) {
return false;
}
ble_enabled_ = enabled;
return true;
}
std::string GatewayRuntime::gatewayName(uint8_t gateway_id) const {
return settings_.getGatewayName(gateway_id, defaultGatewayName(gateway_id));
}
@@ -342,6 +354,17 @@ std::string GatewayRuntime::bleMacHex() const {
return toHex(serialBytes());
}
std::string GatewayRuntime::bleGatewayName(uint8_t gateway_id, std::string_view gateway_name) const {
std::string normalized(gateway_name);
if (normalized.size() > kMaxGatewayNameBytes) {
normalized.resize(kMaxGatewayNameBytes);
}
if (!normalized.empty() && normalized != defaultGatewayName(gateway_id)) {
return normalized;
}
return defaultBleGatewayName();
}
std::string GatewayRuntime::defaultBleGatewayName() const {
return "DALIGW_" + bleMacHex();
}