f005d2bc09
Signed-off-by: Tony <tonylu@tony-cloud.com>
547 lines
16 KiB
C++
547 lines
16 KiB
C++
#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;
|
|
}
|
|
}
|
|
|
|
std::vector<uint8_t> LegacyRawPayload(const std::vector<uint8_t>& data) {
|
|
if (data.size() == 1) {
|
|
return {0xBE, data[0]};
|
|
}
|
|
return data;
|
|
}
|
|
|
|
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(); });
|
|
dali_domain_.addRawFrameSink([this](const DaliRawFrame& frame) { handleDaliRawFrame(frame); });
|
|
|
|
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, ¶ms, 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::handleDaliRawFrame(const DaliRawFrame& frame) {
|
|
if (!enabled_ || conn_handle_ == kInvalidConnectionHandle || frame.data.empty()) {
|
|
return;
|
|
}
|
|
notifyCharacteristic(frame.channel_index, LegacyRawPayload(frame.data));
|
|
}
|
|
|
|
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;
|
|
struct ble_gap_upd_params params = {
|
|
.itvl_min = 15,
|
|
.itvl_max = 15,
|
|
.latency = 3,
|
|
.supervision_timeout = 1000,
|
|
.min_ce_len = 0,
|
|
.max_ce_len = 0,
|
|
};
|
|
int rc = ble_gap_update_params(event->connect.conn_handle, ¶ms);
|
|
if (rc != 0) {
|
|
ESP_LOGW(kTag, "ble_gap_update_params rc=%d", rc);
|
|
}
|
|
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
|