Files
esphome-devices/components/ibeacon_rotator/ibeacon_rotator.cpp

231 lines
7.1 KiB
C++

#include "ibeacon_rotator.h"
#ifdef USE_ESP32
#include <algorithm>
#include <cstring>
#include <esp_bt_main.h>
#include <esp_gap_ble_api.h>
#include "esphome/core/hal.h"
#include "esphome/core/log.h"
namespace esphome::ibeacon_rotator {
static const char *const TAG = "ibeacon_rotator";
// Apple iBeacon advertising prefix: flags + Apple manufacturer-specific header.
static const uint8_t IBEACON_HEAD[9] = {
0x02, 0x01, 0x06, // flags AD
0x1A, // length of mfg AD payload
0xFF, // type: manufacturer specific
0x4C, 0x00, // Apple company id
0x02, 0x15, // iBeacon sub-type
};
void IBeaconRotator::setup() {
this->ble_adv_params_ = {
.adv_int_min = static_cast<uint16_t>(this->min_interval_ / 0.625f),
.adv_int_max = static_cast<uint16_t>(this->max_interval_ / 0.625f),
.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,
};
}
void IBeaconRotator::dump_config() {
char prefix[21];
for (int i = 0; i < 10; i++) {
snprintf(prefix + 2 * i, 3, "%02X", this->prefix_[i]);
}
ESP_LOGCONFIG(TAG, "iBeacon Rotator:");
ESP_LOGCONFIG(TAG, " UUID Prefix: %s (last 6 bytes filled from MAC)", prefix);
ESP_LOGCONFIG(TAG, " Broadcast Length: %ums", this->broadcast_length_ms_);
ESP_LOGCONFIG(TAG, " Min Interval: %ums, Max Interval: %ums", this->min_interval_, this->max_interval_);
ESP_LOGCONFIG(TAG, " Major: %u, Minor: %u, Measured Power: %ddBm", this->major_, this->minor_,
this->measured_power_);
}
void IBeaconRotator::loop() {
this->evict_expired_();
if (this->entries_.empty()) {
if (this->advertising_) {
this->stop_advertising_();
}
return;
}
uint32_t now = millis();
if (!this->advertising_) {
this->advertising_ = true;
this->cursor_ = 0;
this->last_rotate_ms_ = now;
this->apply_current_payload_();
} else if (this->entries_.size() > 1 && (now - this->last_rotate_ms_) >= this->max_interval_) {
this->cursor_ = (this->cursor_ + 1) % this->entries_.size();
this->last_rotate_ms_ = now;
this->apply_current_payload_();
}
}
void IBeaconRotator::evict_expired_() {
uint32_t now = millis();
// Capture the MAC at cursor before erase so we can re-locate it after.
uint8_t cursor_mac[6];
bool had_cursor = !this->entries_.empty() && this->cursor_ < this->entries_.size();
if (had_cursor) {
std::memcpy(cursor_mac, this->entries_[this->cursor_].mac, 6);
}
auto before = this->entries_.size();
this->entries_.erase(std::remove_if(this->entries_.begin(), this->entries_.end(),
[now](const Entry &e) {
// Signed compare handles millis() wraparound.
return static_cast<int32_t>(now - e.expires_at_ms) >= 0;
}),
this->entries_.end());
if (this->entries_.size() == before) {
return;
}
if (this->entries_.empty()) {
this->cursor_ = 0;
return;
}
// Restore cursor to the same MAC if it survived; otherwise wrap.
if (had_cursor) {
for (size_t i = 0; i < this->entries_.size(); i++) {
if (std::memcmp(this->entries_[i].mac, cursor_mac, 6) == 0) {
this->cursor_ = i;
return;
}
}
}
if (this->cursor_ >= this->entries_.size()) {
this->cursor_ = 0;
}
}
void IBeaconRotator::add_mac(const uint8_t mac[6]) {
uint32_t now = millis();
for (auto &e : this->entries_) {
if (std::memcmp(e.mac, mac, 6) == 0) {
e.expires_at_ms = now + this->broadcast_length_ms_;
ESP_LOGD(TAG, "Refreshed %02X:%02X:%02X:%02X:%02X:%02X (+%ums)", mac[0], mac[1], mac[2], mac[3], mac[4],
mac[5], this->broadcast_length_ms_);
return;
}
}
Entry e;
std::memcpy(e.mac, mac, 6);
e.expires_at_ms = now + this->broadcast_length_ms_;
this->entries_.push_back(e);
ESP_LOGD(TAG, "Added %02X:%02X:%02X:%02X:%02X:%02X (now %u active)", mac[0], mac[1], mac[2], mac[3], mac[4],
mac[5], static_cast<unsigned>(this->entries_.size()));
}
bool IBeaconRotator::add_mac_from_string(const std::string &mac_str) {
uint8_t mac[6];
int idx = 0;
uint8_t cur = 0;
int nibbles = 0;
for (char c : mac_str) {
if (c == ':' || c == '-' || c == ' ') {
continue;
}
uint8_t v;
if (c >= '0' && c <= '9') {
v = c - '0';
} else if (c >= 'a' && c <= 'f') {
v = c - 'a' + 10;
} else if (c >= 'A' && c <= 'F') {
v = c - 'A' + 10;
} else {
ESP_LOGW(TAG, "Invalid MAC string: '%s'", mac_str.c_str());
return false;
}
cur = (cur << 4) | v;
nibbles++;
if (nibbles == 2) {
if (idx >= 6) {
ESP_LOGW(TAG, "MAC string too long: '%s'", mac_str.c_str());
return false;
}
mac[idx++] = cur;
cur = 0;
nibbles = 0;
}
}
if (idx != 6 || nibbles != 0) {
ESP_LOGW(TAG, "MAC string wrong length: '%s'", mac_str.c_str());
return false;
}
this->add_mac(mac);
return true;
}
void IBeaconRotator::apply_current_payload_() {
uint8_t payload[30];
std::memcpy(payload, IBEACON_HEAD, 9);
std::memcpy(payload + 9, this->prefix_.data(), 10);
std::memcpy(payload + 19, this->entries_[this->cursor_].mac, 6);
payload[25] = static_cast<uint8_t>(this->major_ >> 8);
payload[26] = static_cast<uint8_t>(this->major_ & 0xFF);
payload[27] = static_cast<uint8_t>(this->minor_ >> 8);
payload[28] = static_cast<uint8_t>(this->minor_ & 0xFF);
payload[29] = static_cast<uint8_t>(this->measured_power_);
esp_err_t err = esp_ble_gap_config_adv_data_raw(payload, sizeof(payload));
if (err != ESP_OK) {
ESP_LOGE(TAG, "config_adv_data_raw failed: %s", esp_err_to_name(err));
}
}
void IBeaconRotator::stop_advertising_() {
this->advertising_ = false;
esp_err_t err = esp_ble_gap_stop_advertising();
if (err != ESP_OK) {
ESP_LOGW(TAG, "stop_advertising failed: %s", esp_err_to_name(err));
}
ESP_LOGD(TAG, "All MACs expired; stopped advertising");
}
void IBeaconRotator::gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) {
if (!this->advertising_) {
return;
}
switch (event) {
case ESP_GAP_BLE_ADV_DATA_RAW_SET_COMPLETE_EVT: {
esp_err_t err = esp_ble_gap_start_advertising(&this->ble_adv_params_);
// start_advertising is benign-redundant when already advertising; log only hard errors.
if (err != ESP_OK) {
ESP_LOGD(TAG, "start_advertising returned: %s", esp_err_to_name(err));
}
break;
}
case ESP_GAP_BLE_ADV_START_COMPLETE_EVT:
if (param->adv_start_cmpl.status != ESP_BT_STATUS_SUCCESS) {
ESP_LOGD(TAG, "adv_start status %d", param->adv_start_cmpl.status);
}
break;
case ESP_GAP_BLE_ADV_STOP_COMPLETE_EVT:
if (param->adv_stop_cmpl.status != ESP_BT_STATUS_SUCCESS) {
ESP_LOGW(TAG, "adv_stop status %d", param->adv_stop_cmpl.status);
}
break;
default:
break;
}
}
} // namespace esphome::ibeacon_rotator
#endif // USE_ESP32