#include "ibeacon_rotator.h" #ifdef USE_ESP32 #include #include #include #include #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(this->min_interval_ / 0.625f), .adv_int_max = static_cast(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(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(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(this->major_ >> 8); payload[26] = static_cast(this->major_ & 0xFF); payload[27] = static_cast(this->minor_ >> 8); payload[28] = static_cast(this->minor_ & 0xFF); payload[29] = static_cast(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