Skip to content

Commit fecf966

Browse files
committed
Merge dev-v1.5.6 into main for v1.5.6 release
2 parents 57b970c + 99bdc40 commit fecf966

File tree

5 files changed

+48
-9
lines changed

5 files changed

+48
-9
lines changed

ESPHome Devices/irk-capture-base.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ esphome:
3232
friendly_name: ${friendly_name}
3333
project:
3434
name: "derekseaman.irk-capture"
35-
version: "1.5.4"
35+
version: "1.5.6"
3636

3737
# IMPORTANT: Do NOT include esp32_ble:, esp32_ble_tracker:, or bluetooth_proxy: here.
3838
# The irk_capture component fully initializes and manages NimBLE itself.

ESPHome Devices/irk-capture-full.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ esphome:
5757
friendly_name: ${friendly_name}
5858
project:
5959
name: "derekseaman.irk-capture"
60-
version: "1.5.4"
60+
version: "1.5.6"
6161

6262
# IMPORTANT: Do NOT include esp32_ble:, esp32_ble_tracker:, or bluetooth_proxy: here.
6363
# The irk_capture component fully initializes and manages NimBLE itself.

RELEASE_NOTES_v1.5.6.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
# Release Notes v1.5.6
2+
3+
## Reliability Fixes
4+
5+
- **Fixed MAC rotation race condition**: `refresh_mac()` now sets the `suppress_next_adv_` flag before triggering disconnect, preventing `handle_gap_disconnect` from restarting advertising while the MAC rotation is still in progress.
6+
- **Component fails cleanly on critical init errors**: `setup()` now calls `mark_failed()` and returns early if the FreeRTOS mutex cannot be created, and `register_gatt_services()` marks the component as failed if GATT registration fails.
7+
- **NVS profile range validation**: Persisted BLE profile values are now range-checked before `static_cast`, preventing undefined behavior from corrupted NVS data.
8+
- **BLE name whitespace trimming**: `sanitize_ble_name()` now trims leading and trailing spaces after character filtering, preventing names like `" IRK "` from wasting bytes in the advertising packet.
9+
- **Fixed `host_synced_` cross-core visibility**: Changed from plain `bool` to `std::atomic<bool>` to ensure the NimBLE task's write is visible to the ESPHome main loop on dual-core ESP32 variants.
10+
11+
## Code Quality
12+
13+
- **Added explicit standard library includes**: Added `<string>`, `<vector>` to header and `<cstring>` to implementation, removing reliance on transitive includes from ESPHome/NimBLE headers.

components/irk_capture/irk_capture.cpp

Lines changed: 26 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
// ESP32-only implementation - requires Bluetooth hardware
44
#ifdef USE_ESP32
55

6+
#include <cstring>
67
#include <esp_random.h>
78
#include <esp_timer.h>
89
#include <freertos/FreeRTOS.h>
@@ -23,7 +24,7 @@ namespace esphome {
2324
namespace irk_capture {
2425

2526
static const char* const TAG = "irk_capture";
26-
static constexpr char VERSION[] = "1.5.5";
27+
static constexpr char VERSION[] = "1.5.6";
2728
static constexpr char HEX[] = "0123456789abcdef";
2829

2930
// Global instance pointer for NimBLE callbacks that don't accept user args
@@ -70,6 +71,7 @@ THREAD SAFETY RULES:
7071
- RAII MutexGuard class ensures exception-safe lock/unlock
7172
- Mutex MUST be released before calling BLE stack APIs (prevents deadlock)
7273
- UI updates (publish_state) are safe outside mutex (internally thread-safe)
74+
- host_synced_ uses std::atomic<bool> (one-shot write from NimBLE, reads from main loop)
7375
7476
IMPORTANT: Do NOT rely on "aligned writes are atomic" - always use mutex for
7577
shared state to ensure:
@@ -821,6 +823,15 @@ std::string IRKCaptureComponent::sanitize_ble_name(const std::string& name) {
821823
}
822824
}
823825

826+
// Trim leading and trailing whitespace
827+
size_t start = sanitized.find_first_not_of(' ');
828+
size_t end = sanitized.find_last_not_of(' ');
829+
if (start != std::string::npos) {
830+
sanitized = sanitized.substr(start, end - start + 1);
831+
} else {
832+
sanitized.clear();
833+
}
834+
824835
// Final validation: ensure we have at least one character
825836
if (sanitized.empty()) {
826837
ESP_LOGW(TAG, "BLE name contained only invalid characters, using default 'IRK Capture'");
@@ -1293,17 +1304,22 @@ void IRKCaptureComponent::setup() {
12931304
state_mutex_ = xSemaphoreCreateMutex();
12941305
if (!state_mutex_) {
12951306
ESP_LOGE(TAG, "CRITICAL: Failed to create state mutex - thread safety compromised!");
1296-
// Continue anyway - component will work but may have race conditions
1307+
this->mark_failed();
1308+
return;
12971309
}
12981310

12991311
// Load persisted BLE profile from NVS (before BLE stack init so GATT is correct)
13001312
nvs_handle_t nvs_handle;
13011313
if (nvs_open("irk_capture", NVS_READONLY, &nvs_handle) == ESP_OK) {
13021314
uint8_t profile_val = 0;
13031315
if (nvs_get_u8(nvs_handle, "ble_profile", &profile_val) == ESP_OK) {
1304-
ble_profile_ = static_cast<BLEProfile>(profile_val);
1305-
ESP_LOGI(TAG, "Loaded persisted BLE profile: %s",
1306-
ble_profile_ == BLEProfile::KEYBOARD ? "Keyboard" : "Heart Sensor");
1316+
if (profile_val <= static_cast<uint8_t>(BLEProfile::KEYBOARD)) {
1317+
ble_profile_ = static_cast<BLEProfile>(profile_val);
1318+
ESP_LOGI(TAG, "Loaded persisted BLE profile: %s",
1319+
ble_profile_ == BLEProfile::KEYBOARD ? "Keyboard" : "Heart Sensor");
1320+
} else {
1321+
ESP_LOGW(TAG, "Invalid persisted profile value %u, using default", profile_val);
1322+
}
13071323
}
13081324
nvs_close(nvs_handle);
13091325
}
@@ -1717,6 +1733,7 @@ void IRKCaptureComponent::register_gatt_services() {
17171733
if (rc == 0) rc = ble_gatts_add_svcs(gatt_svcs);
17181734
if (rc != 0) {
17191735
ESP_LOGE(TAG, "GATT registration failed rc=%d", rc);
1736+
this->mark_failed();
17201737
return;
17211738
}
17221739
hr_char_handle_ = g_hr_handle;
@@ -1890,6 +1907,10 @@ void IRKCaptureComponent::refresh_mac() {
18901907
mac_rotation_retries_ = 0; // Reset retry counter for new rotation attempt
18911908
std::memcpy(pending_mac_, temp_mac, sizeof(pending_mac_));
18921909

1910+
// Prevent handle_gap_disconnect from restarting advertising immediately,
1911+
// which would race with ble_hs_id_set_rnd() in loop() (BLE_HS_EBUSY)
1912+
suppress_next_adv_ = true;
1913+
18931914
// Snapshot connection state for disconnect logic
18941915
should_stop_adv = advertising_;
18951916
conn_handle_copy = conn_handle_;

components/irk_capture/irk_capture.h

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
#pragma once
22

3+
#include <atomic>
4+
#include <string>
5+
#include <vector>
6+
37
#include "esphome/components/button/button.h"
48
#include "esphome/components/select/select.h"
59
#include "esphome/components/switch/switch.h"
@@ -264,8 +268,9 @@ class IRKCaptureComponent : public Component {
264268
uint32_t last_publish_time_ { 0 }; // Last IRK publish timestamp
265269
uint32_t pairing_start_time_ { 0 }; // Global pairing timeout
266270

267-
// Host state (ESPHome-managed host; this is a soft flag)
268-
bool host_synced_ { false };
271+
// Host state — written once by NimBLE task (sync_cb), read by ESPHome main task
272+
// Uses std::atomic for cross-core visibility without requiring state_mutex_
273+
std::atomic<bool> host_synced_ { false };
269274

270275
// Timer targets and cached peer ids
271276
struct Timers {

0 commit comments

Comments
 (0)