From 881b633096eb1cb5980cb01c1cc4cbb239e63690 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 12 Nov 2026 06:00:41 +0000 Subject: [PATCH 1/6] Fix BLE reconnect: resume advertising after rejected peers - Clear stale advertising flags and restart advertising when an unauthorized peer connects and is rejected (previously advertising stayed off until reboot). - Advertise at 20 ms for 30 s after boot/disconnect/enroll, then 152.5 ms (Apple Accessory Design Guidelines) instead of fixed 160 ms. - Re-arm advertising every 60 s while disconnected to heal lost state. - Request 30-45 ms interval and 6 s supervision timeout after auth. Co-Authored-By: Claude Fable 5.2 Claude-Session: https://claude.ai/code/session_017YMfNVLFCmfr5zAFPD2Fqy --- components/ancs_client/ancs_client.c | 83 ++++++++++++++++++++++++++++ 1 file changed, 83 insertions(+) diff --git a/components/ancs_client/ancs_client.c b/components/ancs_client/ancs_client.c index a4af2d3..290c55b 100644 --- a/components/ancs_client/ancs_client.c +++ b/components/ancs_client/ancs_client.c @@ -40,6 +40,16 @@ #define SCAN_RESPONSE_PENDING (1U << 1) #define CONTROL_REQUEST_MAX (CONFIG_ANCS_APP_ID_MAX + 3U) #define ADVERTISING_BACKOFF_MAX_MS 4000U +/* Apple Accessory Design Guidelines: 20 ms for the first 30 s, then one of the + * recommended slow intervals (152.5 ms) so iOS background scanning finds us. */ +#define ADVERTISING_FAST_INTERVAL_UNITS 0x0020U /* 20 ms */ +#define ADVERTISING_SLOW_INTERVAL_UNITS 0x00F4U /* 152.5 ms */ +#define ADVERTISING_FAST_WINDOW_MS INT64_C(30000) +#define ADVERTISING_REFRESH_MS INT64_C(60000) +/* 30-45 ms interval, no latency, 6 s supervision timeout (Apple maximum). */ +#define CONNECTION_INTERVAL_MIN_UNITS 0x0018U +#define CONNECTION_INTERVAL_MAX_UNITS 0x0024U +#define CONNECTION_SUPERVISION_TIMEOUT_UNITS 600U #define DISCOVERY_BACKOFF_MAX_MS 5000U #define AUTH_FAILURE_REPAIR_THRESHOLD 3U #define AUTH_FAILURE_WINDOW_MS 120000LL @@ -155,6 +165,8 @@ typedef struct { uint8_t advertising_attempt; uint8_t discovery_attempt; uint8_t adv_config_pending; + int64_t advertising_fast_until_ms; + int64_t advertising_refresh_due_ms; QueueHandle_t work_queue; TaskHandle_t worker_task; @@ -227,6 +239,9 @@ static esp_ble_adv_params_t s_adv_params = { static void start_advertising(void); static void stop_advertising(void); +static void begin_fast_advertising_window(void); +static void note_advertising_stopped_by_controller(void); +static void check_advertising_refresh(void); static void start_discovery(void); static int sync_existing_ble_bond(void); static esp_err_t open_replacement_window(void); @@ -897,6 +912,7 @@ static void worker_task(void *argument) for (;;) { check_connection_deadline(); + check_advertising_refresh(); enroll_timeout_callback(NULL); /* Also covers lost/old timer wakeups. */ const uint32_t generation = current_generation(); if (s_worker.generation != generation) { @@ -1118,6 +1134,15 @@ static void start_advertising(void) return; } s_client.advertising_starting = true; + const int64_t now = now_ms(); + const bool fast = now < s_client.advertising_fast_until_ms; + const uint16_t interval = fast ? ADVERTISING_FAST_INTERVAL_UNITS + : ADVERTISING_SLOW_INTERVAL_UNITS; + s_adv_params.adv_int_min = interval; + s_adv_params.adv_int_max = interval; + /* Re-arm periodically: switches fast->slow and heals lost stack state. */ + s_client.advertising_refresh_due_ms = + fast ? s_client.advertising_fast_until_ms : now + ADVERTISING_REFRESH_MS; taskEXIT_CRITICAL(&s_shared_state_lock); const esp_err_t error = esp_ble_gap_start_advertising(&s_adv_params); if (error != ESP_OK) { @@ -1141,6 +1166,44 @@ static void stop_advertising(void) } } +static void begin_fast_advertising_window(void) +{ + taskENTER_CRITICAL(&s_shared_state_lock); + s_client.advertising_fast_until_ms = now_ms() + ADVERTISING_FAST_WINDOW_MS; + taskEXIT_CRITICAL(&s_shared_state_lock); +} + +/* The controller stops legacy advertising on every incoming connection, even + * one we reject. Forget the stale flags so advertising can be started again. */ +static void note_advertising_stopped_by_controller(void) +{ + taskENTER_CRITICAL(&s_shared_state_lock); + s_client.advertising = false; + s_client.advertising_starting = false; + taskEXIT_CRITICAL(&s_shared_state_lock); +} + +static void check_advertising_refresh(void) +{ + const int64_t now = now_ms(); + taskENTER_CRITICAL(&s_shared_state_lock); + const bool wanted = !s_client.connected && + s_client.adv_config_pending == 0U && + ble_enroll_should_advertise(&s_enroll, now); + const bool armed = s_client.advertising || s_client.advertising_starting; + const bool due = wanted && armed && + s_client.advertising_refresh_due_ms != 0 && + now >= s_client.advertising_refresh_due_ms; + if (due) { + s_client.advertising_refresh_due_ms = 0; + } + taskEXIT_CRITICAL(&s_shared_state_lock); + if (due) { + stop_advertising(); + start_advertising(); + } +} + static void maybe_start_discovery(void) { start_discovery(); @@ -1559,6 +1622,18 @@ static void gap_event_handler(esp_gap_ble_cb_event_t event, taskEXIT_CRITICAL(&s_shared_state_lock); (void)esp_timer_stop(s_client.enroll_timer); stop_advertising(); + esp_ble_conn_update_params_t connection_params = { + .min_int = CONNECTION_INTERVAL_MIN_UNITS, + .max_int = CONNECTION_INTERVAL_MAX_UNITS, + .latency = 0, + .timeout = CONNECTION_SUPERVISION_TIMEOUT_UNITS, + }; + memcpy(connection_params.bda, + parameter->ble_security.auth_cmpl.bd_addr, + sizeof(esp_bd_addr_t)); + if (esp_ble_gap_update_conn_params(&connection_params) != ESP_OK) { + ESP_LOGW(TAG, "connection parameter update request failed"); + } transition(ANCS_SIGNAL_BONDED); ESP_LOGI(TAG, "encryption and bonding complete auth_mode=0x%x addr_type=%u", @@ -1680,6 +1755,7 @@ static void handle_gattc_event(esp_gattc_cb_event_t event, s_client.gattc_if = gattc_if; taskEXIT_CRITICAL(&s_shared_state_lock); ESP_ERROR_CHECK(esp_ble_gap_set_device_name(s_client.device_name)); + begin_fast_advertising_window(); ESP_ERROR_CHECK(esp_ble_gap_config_local_privacy(true)); break; @@ -1687,6 +1763,8 @@ static void handle_gattc_event(esp_gattc_cb_event_t event, if (!connection_peer_allowed(parameter->connect.remote_bda)) { ESP_LOGW(TAG, "rejected connection from unauthorized peer"); (void)esp_ble_gap_disconnect(parameter->connect.remote_bda); + note_advertising_stopped_by_controller(); + schedule_advertising_retry(); break; } taskENTER_CRITICAL(&s_shared_state_lock); @@ -2062,6 +2140,8 @@ static void handle_gattc_event(esp_gattc_cb_event_t event, parameter->disconnect.conn_id, parameter->disconnect.remote_bda)) { ESP_LOGW(TAG, "ignored disconnect event from stale peer"); + /* A rejected peer never became the session; resume advertising. */ + schedule_advertising_retry(); break; } ESP_LOGW(TAG, @@ -2071,6 +2151,7 @@ static void handle_gattc_event(esp_gattc_cb_event_t event, s_client.last_disconnect_reason = parameter->disconnect.reason; taskEXIT_CRITICAL(&s_shared_state_lock); reset_connection_session(); + begin_fast_advertising_window(); transition(ANCS_SIGNAL_DISCONNECTED); taskENTER_CRITICAL(&s_shared_state_lock); const bool reopen = s_client.reopen_after_disconnect; @@ -2539,6 +2620,7 @@ esp_err_t ancs_client_request_enroll(void) taskENTER_CRITICAL(&s_shared_state_lock); ble_enroll_close_window(&s_enroll); taskEXIT_CRITICAL(&s_shared_state_lock); + begin_fast_advertising_window(); start_advertising(); return ESP_OK; } @@ -2566,6 +2648,7 @@ esp_err_t ancs_client_request_enroll(void) taskEXIT_CRITICAL(&s_shared_state_lock); return timer_error; } + begin_fast_advertising_window(); start_advertising(); return ESP_OK; } From 555db82071fc68b6cf739795cc6dff892e5e252b Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 12 Nov 2026 06:02:38 +0000 Subject: [PATCH 2/6] Bump firmware source version to 0.4.1 (BLE reconnect fix) Co-Authored-By: Claude Fable 5.2 Claude-Session: https://claude.ai/code/session_017YMfNVLFCmfr5zAFPD2Fqy --- CMakeLists.txt | 2 +- tools/build.ps1 | 2 +- tools/build.sh | 2 +- tools/build_matrix.ps1 | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 97f3a50..3b8531d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -3,5 +3,5 @@ cmake_minimum_required(VERSION 3.16) set(MINIMAL_BUILD ON) include($ENV{IDF_PATH}/tools/cmake/project.cmake) -set(PROJECT_VER "0.4.0") +set(PROJECT_VER "0.4.1") project(ios_ancs_capture_c6) diff --git a/tools/build.ps1 b/tools/build.ps1 index 0ba4836..920b35a 100644 --- a/tools/build.ps1 +++ b/tools/build.ps1 @@ -11,7 +11,7 @@ param( )] [string]$Target = 'esp32c6', [string]$IdfPath = $env:IDF_PATH, - [string]$Version = '0.4.0' + [string]$Version = '0.4.1' ) $ErrorActionPreference = 'Stop' diff --git a/tools/build.sh b/tools/build.sh index ac4ef26..1541df9 100644 --- a/tools/build.sh +++ b/tools/build.sh @@ -4,7 +4,7 @@ set -euo pipefail project_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" idf_path="${IDF_PATH:-}" target="${TARGET:-esp32c6}" -version="${VERSION:-0.4.0}" +version="${VERSION:-0.4.1}" if [[ -z "${idf_path}" ]]; then bundled_idf="$(cd "${project_root}/../.." && pwd)/work/sdk/esp-idf-6.0.2" diff --git a/tools/build_matrix.ps1 b/tools/build_matrix.ps1 index aaaa00e..8e3be5c 100644 --- a/tools/build_matrix.ps1 +++ b/tools/build_matrix.ps1 @@ -22,7 +22,7 @@ param( [string]$BuildRoot, [ValidateRange(0, 64)] [int]$Jobs = 0, - [string]$Version = '0.4.0', + [string]$Version = '0.4.1', [switch]$KeepGoing ) From 5366a90a75c3afa08d1761eb67627e91fa9b8f16 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 12 Nov 2026 06:03:31 +0000 Subject: [PATCH 3/6] Prepare release surfaces for 0.6.16 / firmware 0.4.1 Version anchors, manifests, tests and changelog. The verified 0.4.1 installer images and provenance follow once the firmware build of 555db82 completes. Co-Authored-By: Claude Fable 5.2 Claude-Session: https://claude.ai/code/session_017YMfNVLFCmfr5zAFPD2Fqy --- CHANGELOG.md | 9 +++++++ README.en.md | 8 +++---- README.md | 6 ++--- SECURITY.md | 2 +- custom_components/ha_ios_ancs/manifest.json | 2 +- docs/app.js | 26 ++++++++++----------- docs/index.html | 10 ++++---- docs/manifests/esp32-c6.json | 4 ++-- docs/manifests/ios-ancs.json | 16 ++++++------- tests/test_config_flow.py | 4 ++-- tools/tests/test_documentation_contract.py | 2 +- tools/tests/test_multi_target_contract.py | 10 ++++---- tools/tests/test_release_integrity.py | 20 ++++++++-------- 13 files changed, 64 insertions(+), 55 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3a1f1ae..2196e45 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,14 @@ # 변경 �력 +## Home Assistant 통합 0.6.16 / 펌웨어 0.4.1 — 2026-11-12 + +- 펌웨어: 등��지 않� 기기가 BLE로 접�했다가 거부� 뒤 광고가 다시 시작�지 않� 문제를 고쳤습니다. 0.4.0� � �태가 �면 재부팅 전까지 iPhone� 다시 연결할 수 없었습니다. +- 펌웨어: 광고 주기를 Apple 액세서리 권장값으로 바꿨습니다. 부팅, 연결 �김, 등� 요청 뒤 30초 �안� 20 ms, 그 뒤�는 152.5 ms로 광고합니다(0.4.0� 160 ms 고정). +- 펌웨어: 연결� 없는 �안 60초마다 광고를 다시 설정해, 광고 �태가 어긋나� 스스로 복구합니다. +- 펌웨어: ��� �나면 연결 간격 30~45 ms, �시 타임아웃 6초를 iPhone� 요청합니다. iPhone� 받아들�면 신호가 잠� 약해져� 연결� � ��니다. +- 설정 AP, Bluetooth PIN `123456`, 등� 창, MQTT 토픽과 Discovery, Home Assistant 엔티티, 저장� 설정 형�, 파티션 표는 바꾸지 않았습니다. 통합(HACS) 코드는 버전만 올렸습니다. +- 실제 보드�서는 시험하지 않았습니다. 문제가 있으면 v0.6.15 릴리스� 펌웨어 0.4.0 �미지로 ��릴 수 있습니다. + ## Home Assistant 통합 0.6.15 / 펌웨어 0.4.0 — 2026-10-30 - 펌웨어: MQTT 서버� 한 번 연결� 설정� 기억해 둡니다(설정 파티션� `mqtt_ok`). 그 뒤 브로커가 꺼지거나 재시작하거나 네트워�가 �기면 설정 AP를 열지 않고 5초, 15초, 30초, �후 60초 간격으로 계� 다시 연결합니다. 0.3.9는 �때 설정 AP를 바로 열어, 브로커가 재시작하는 �안 근처 누구나 접�해 설정� 바꿀 수 있었습니다. diff --git a/README.en.md b/README.en.md index cc79274..6f51e69 100644 --- a/README.en.md +++ b/README.en.md @@ -45,11 +45,11 @@ From firmware 0.4.0 the relay remembers MQTT settings once they have connected. - Changing the host or port of settings that connected before requires entering the MQTT password again. - Devices updated from 0.3.9 follow these rules after their first successful MQTT connection on 0.4.0. -## Supported Boards And v0.4.0 Build Facts +## Supported Boards And v0.4.1 Build Facts -The shared firmware uses a 4 MB minimum flash layout. All v0.4.0 images shown in the installer are compile, link, partition, and merged-image verified. ESP32-S2 is excluded because it has no BLE. ESP32-H2 is excluded because it has no Wi-Fi, and ESP32-P4 has no integrated Wi-Fi/BLE radio. +The shared firmware uses a 4 MB minimum flash layout. All v0.4.1 images shown in the installer are compile, link, partition, and merged-image verified. ESP32-S2 is excluded because it has no BLE. ESP32-H2 is excluded because it has no Wi-Fi, and ESP32-P4 has no integrated Wi-Fi/BLE radio. -| Target | Typical module/board | Factory image | v0.4.0 status | +| Target | Typical module/board | Factory image | v0.4.1 status | | --- | --- | ---: | --- | | `esp32` | ESP32-WROOM-32 / WROOM-D32 | 1,451,936 bytes | Build verified; this version not hardware validated | | `esp32c2` | ESP32-C2 | 1,474,224 bytes | Build verified; this version not hardware validated | @@ -59,7 +59,7 @@ The shared firmware uses a 4 MB minimum flash layout. All v0.4.0 images shown in | `esp32c61` | ESP32-C61 | 1,751,520 bytes | Build verified; this version not hardware validated | | `esp32s3` | ESP32-S3 | 1,434,416 bytes | Build verified; this version not hardware validated | -The table describes the published v0.4.0 images. Build verification, physical flashing, BLE enrollment, and live iPhone notification capture are separate validation scopes; boards without physical-device evidence are labeled as build verified only. +The table describes the published v0.4.1 images. Build verification, physical flashing, BLE enrollment, and live iPhone notification capture are separate validation scopes; boards without physical-device evidence are labeled as build verified only. ## Home Assistant And HACS diff --git a/README.md b/README.md index 826e4d3..ff5dc33 100644 --- a/README.md +++ b/README.md @@ -71,11 +71,11 @@ BOOT 버튼� 3초 눌러 설정 AP �는 iPhone 등� 창� 엽니다. 저 설정 �털� **iPhone 기기 등�** 버튼, Home Assistant� **iPhone 등� 시작** 버튼 �는 BOOT 버튼 3초 누르기로 120초 등� 창� 엽니다. 설정 �털� 표시�는 고정 PIN `123456`를 iOS Bluetooth 설정� 입력하고 알림 공유를 허용합니다. 저장� 기존 iPhone 페어� 정보가 있으면 같� �작� 기존 iPhone 재연결만 요청합니다. -## 지� 보드와 v0.4.0 빌드 사실 +## 지� 보드와 v0.4.1 빌드 사실 공통 펌웨어는 최소 4 MB 플래시 레�아웃� 사용합니다. ESP32-S2는 BLE가 없고, ESP32-H2는 Wi-Fi가 없으며, ESP32-P4는 내장 Wi-Fi/BLE �디오가 없어 제외�니다. -| Target | �반 모듈/보드 | Factory �미지 | v0.4.0 �태 | +| Target | �반 모듈/보드 | Factory �미지 | v0.4.1 �태 | | --- | --- | ---: | --- | | `esp32` | ESP32-WROOM-32 / WROOM-D32 | 1,451,936 bytes | 빌드 검� · � 버전 실기기 미검� | | `esp32c2` | ESP32-C2 | 1,474,224 bytes | 빌드 검� · � 버전 실기기 미검� | @@ -85,7 +85,7 @@ BOOT 버튼� 3초 눌러 설정 AP �는 iPhone 등� 창� 엽니다. 저 | `esp32c61` | ESP32-C61 | 1,751,520 bytes | 빌드 검� · � 버전 실기기 미검� | | `esp32s3` | ESP32-S3 | 1,434,416 bytes | 빌드 검� · � 버전 실기기 미검� | -표� �태는 공개� v0.4.0 �미지 기준입니다. 빌드 검�, 실제 보드 플래시, BLE 등�, iPhone 알림 수신� 서로 다른 검� 범위�며 보드별 실기기 결과가 없는 항목� 빌드 검�으로만 표시합니다. +표� �태는 공개� v0.4.1 �미지 기준입니다. 빌드 검�, 실제 보드 플래시, BLE 등�, iPhone 알림 수신� 서로 다른 검� 범위�며 보드별 실기기 결과가 없는 항목� 빌드 검�으로만 표시합니다. ## Home Assistant와 HACS diff --git a/SECURITY.md b/SECURITY.md index 9252e08..c211bec 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -2,7 +2,7 @@ ## 지� 버전 -보안 수정� 최신 GitHub 릴리스�만 제공합니다. 릴리스마다 Home Assistant 통합 버전과, 함께 배�하는 ESP32 펌웨어 버전� 제목� 있습니다(예: `iOS ANCS 0.6.15 / Firmware 0.4.0`). +보안 수정� 최신 GitHub 릴리스�만 제공합니다. 릴리스마다 Home Assistant 통합 버전과, 함께 배�하는 ESP32 펌웨어 버전� 제목� 있습니다(예: `iOS ANCS 0.6.16 / Firmware 0.4.1`). | 구성 요소 | 지� | | --- | --- | diff --git a/custom_components/ha_ios_ancs/manifest.json b/custom_components/ha_ios_ancs/manifest.json index 211da59..b618dd4 100644 --- a/custom_components/ha_ios_ancs/manifest.json +++ b/custom_components/ha_ios_ancs/manifest.json @@ -13,5 +13,5 @@ "iot_class": "local_push", "issue_tracker": "https://github.com/1bobby-git/HA-iOS-ANCS/issues", "requirements": [], - "version": "0.6.15" + "version": "0.6.16" } diff --git a/docs/app.js b/docs/app.js index 6e3ec99..356b74e 100644 --- a/docs/app.js +++ b/docs/app.js @@ -3,52 +3,52 @@ const boards = { chip: "32", name: "ESP32 / WROOM-32", boot: "GPIO 0", - validation: "v0.4.0 build verified", + validation: "v0.4.1 build verified", status: "BUILD VERIFIED", - description: "ESP32/WROOM-32 계열용 v0.4.0 ANCS-to-MQTT factory build입니다.", + description: "ESP32/WROOM-32 계열용 v0.4.1 ANCS-to-MQTT factory build입니다.", hash: "SHA256 A2F1AA9088ED", }, esp32c2: { chip: "C2", name: "ESP32-C2", boot: "GPIO 9", - validation: "v0.4.0 build verified", + validation: "v0.4.1 build verified", status: "BUILD VERIFIED", - description: "ESP32-C2 계열용 v0.4.0 ANCS-to-MQTT factory build입니다.", + description: "ESP32-C2 계열용 v0.4.1 ANCS-to-MQTT factory build입니다.", hash: "SHA256 D2899E6C9E4D", }, esp32c3: { chip: "C3", name: "ESP32-C3", boot: "GPIO 9", - validation: "v0.4.0 build verified", + validation: "v0.4.1 build verified", status: "BUILD VERIFIED", - description: "ESP32-C3 계열용 v0.4.0 ANCS-to-MQTT factory build입니다.", + description: "ESP32-C3 계열용 v0.4.1 ANCS-to-MQTT factory build입니다.", hash: "SHA256 D768DDE94A94", }, esp32c5: { chip: "C5", name: "ESP32-C5", boot: "GPIO 28", - validation: "v0.4.0 build verified", + validation: "v0.4.1 build verified", status: "BUILD VERIFIED", - description: "ESP32-C5 계열용 v0.4.0 ANCS-to-MQTT factory build입니다.", + description: "ESP32-C5 계열용 v0.4.1 ANCS-to-MQTT factory build입니다.", hash: "SHA256 0C481C3C416C", }, esp32c6: { chip: "C6", name: "ESP32-C6", boot: "GPIO 9", - validation: "v0.4.0 build verified", + validation: "v0.4.1 build verified", status: "BUILD VERIFIED", - description: "ESP32-C6 계열용 v0.4.0 ANCS-to-MQTT factory build입니다.", + description: "ESP32-C6 계열용 v0.4.1 ANCS-to-MQTT factory build입니다.", hash: "SHA256 334472B2C151", }, esp32c61: { chip: "C61", name: "ESP32-C61", boot: "GPIO 9", - validation: "v0.4.0 build verified", + validation: "v0.4.1 build verified", status: "BUILD VERIFIED", description: "ESP32-C61� ESP32-C6� 리비전� 아니� 별� 최신 칩입니다. 실제 보드 ��� C61� 경우� 선�하세요.", @@ -58,9 +58,9 @@ const boards = { chip: "S3", name: "ESP32-S3", boot: "GPIO 0", - validation: "v0.4.0 build verified", + validation: "v0.4.1 build verified", status: "BUILD VERIFIED", - description: "ESP32-S3 계열용 v0.4.0 ANCS-to-MQTT factory build입니다.", + description: "ESP32-S3 계열용 v0.4.1 ANCS-to-MQTT factory build입니다.", hash: "SHA256 6D165A01729B", }, }; diff --git a/docs/index.html b/docs/index.html index 12f3e9a..ceb89bf 100644 --- a/docs/index.html +++ b/docs/index.html @@ -30,7 +30,7 @@ Home Assistant iOS ANCS Installer