Commit Graph
16532 Commits
Author SHA1 Message Date
J. Nick Koston 9a5aab63d5 Address review: retry a dropped freed-slot notification too
The connections-free retry left its sibling asymmetric: on the same
full buffer the slot could report free while the device still read as
connected, with nothing resending the connected=false. A one-deep latch
(address + error) drains at the same 100 ms cadence and re-latches on a
repeat failure; a re-reservation of the address clears it, since the
client re-requesting the connect has already acted on the disconnect
and a late resend would shadow the new connection. A lost
connected=true stays client-timeout territory.
2026-08-09 16:24:03 -05:00
J. Nick Koston 735f810f08 Cover the unknown-platform raise in frameworks_for_platforms 2026-08-09 16:07:35 -05:00
J. Nick Koston 9330204908 Address review: unknown platforms raise, the filter pin asserts equality
An unknown or misspelled platform name yielded an empty set - the file
would be filtered out of every build (link failure at the end of a long
compile) and the subset guard passed vacuously.
2026-08-09 16:03:25 -05:00
J. Nick Koston 2e6ad94bd1 Tighten the listener doc to repo style 2026-08-09 15:58:56 -05:00
J. Nick Koston 0ac9a0f8bd Check the cache-clean status in release_services()
The one IDF call in the backend that skipped check_and_log_error_. A
failed clean leaves a stale database the next connection could serve as
authoritative, with nothing in the log to point at it. (The call fails
only at dispatch - stack down - not for an empty cache, so this cannot
warn-spam routine teardowns.)
2026-08-09 15:57:20 -05:00
J. Nick Koston 3728669fa6 Restore the reconnect-during-teardown cancel fast path
The collapse of the CONNECTING branch dropped a real race fix: HA
re-requesting a connect while a scheduled teardown was still pending
used to cancel the teardown and let the in-flight open complete;
without it the request was ignored and HA paid a teardown plus a fresh
connect during exactly the reconnect churn this path sees most.

cancel_gatt_disconnect() joins the contract: true only for a scheduled
teardown that has not started closing (Bluedroid clears the latched
want_disconnect_); rp2 and the stubs return false since their teardowns
start inside gatt_disconnect(). The wrapper's cancel_teardown() returns
its state to CONNECTING and the proxy handler carries the dev branch
verbatim.
2026-08-09 15:54:56 -05:00
J. Nick Koston 8696255ccc Give connection_index_ an in-class initializer like every sibling 2026-08-09 15:43:57 -05:00
J. Nick Koston b0f753a7a2 Address review: check prefer-conn-params, pace the retry drain
- Both esp_ble_gap_set_prefer_conn_params calls route through
  check_and_log_error_ like the class this PR replaces did; a rejected
  preference leaves the link on the controller default interval, which
  is exactly the WiFi-coex failure the shared constants exist to avoid,
  so it must be visible in logs
- The connections-free retry moves below the 100 ms gate so a full TCP
  buffer is retried at the loop cadence instead of every iteration
2026-08-09 15:15:33 -05:00
J. Nick Koston d586a6c8d4 Address review: check the reclaim close, log the mid-stream park
- The late-OPEN_EVT reclaim was the one unchecked IDF call in the
  backend; a failed close there leaks a live link nothing tracks
- The mid-stream services_released_ park now leaves a trace like its
  API-lost sibling, so a stuck GetServices is explicable from logs
2026-08-09 14:54:01 -05:00
J. Nick Koston 1b801427c4 Dry-pass round 2: comment accuracy polish
Round 2 found no code issues - the round-1 mechanisms verify clean on
disk (every IDLE transition through the one door, the teardown-guard
trio minimal-complete per event, the error latch airtight, the reset
lists disjoint by construction). What remained was comment drift:
disconnect_pending() spelling, the OPEN_EVT and cached-MTU comments,
the contract header re-wrap, the two claim-a-slot docstrings
disambiguated, and ragged wraps from earlier text excisions.
2026-08-09 14:46:30 -05:00
J. Nick Koston 7e369f332c Dry-pass round 1: one door to IDLE, complete teardown guards, residue
- set_idle_() is the single door back to IDLE (bootstrap, open-fail and
  the DISCOVERED park went through bare set_state), so the per-attempt
  reset list holds only per-attempt latches
- CFG_MTU joins OPEN_EVT and SEARCH_CMPL in suppressing reports while a
  teardown owns the link - one spelling of the guard across all three
  events, and the wrapper's race arm becomes defense instead of the only
  cover
- The connections-free retry drain compiles on every proxy build (the
  advertisement-only arm sends the message too; the latch already did)
- latch_pending_error_ makes first-cause-wins the mechanism at all three
  latch sites; the dead freed-slot refused branch and its retired
  vocabulary go; the initiate_connection/start_connect_ pair collapses
- UNSET_CONN_ID hoisted next to the field it initializes; count-status
  shadow renamed; stale busy-error rationale replaced with the real one
  (a repeat call would re-arm the teardown timer)
- Fixture states the batch-grouping caveat like its rp2 sibling; the
  get_service_table stub carries a greppable direct-consumer warning
2026-08-09 14:37:57 -05:00
J. Nick Koston 9a6765eb8f Log the deferred connections-free resend at verbose
Log lines ride the same API connection; a debug line at the exact
moment the TCP buffer is full adds traffic when it can least afford it
(the api layer's own buffer-full log is verbose for the same reason).
2026-08-09 14:22:37 -05:00
J. Nick Koston aceff27948 Retry a dropped connections-free update
Field bug on a Pico W: the slot freed device-side but the API client
kept free=0 with the address still allocated. send_message drops the
response when the TCP buffer is full (a boot storm makes that likely
right when HA reconnects), and nothing ever resent it - the client's
slot state is verbatim the last response received, so it stayed stale
until reboot. The cached response is current by construction, so a
pending bit retried from loop() is an idempotent resync; the
subscribe-time send heals through the same bit.
2026-08-09 14:13:50 -05:00
J. Nick Koston 4276815734 Address review: scope the rp2 defines pin, contract-back the teardown semantics
- The esp32 arm's ESPHOME_BLE_GATT_CLIENT_COUNT shadowed the unanchored
  regex; the pin now searches the USE_RP2 platform block
- The contract states what the wrapper relies on: nonzero from
  gatt_disconnect means nothing to tear down, an accepted teardown
  always reaches a terminal report
2026-08-09 14:03:04 -05:00
J. Nick Koston 484674b85d Derive the bluedroid source-filter entry from the shared helper
frameworks_for_platforms() gains its production caller in the same file
as the platform registry; the derived set is identical to the hand list.
2026-08-09 13:29:00 -05:00
J. Nick Koston 356c1d9134 Guard every GATT op on an unset conn id
discover_services() already returned the contract's not-connected value;
the read/write/notify/pair ops fell through to an arbitrary stack error
instead. Dead for the proxy (the wrapper gates on connected()), live for
a direct consumer racing an op against a teardown.
2026-08-09 13:22:28 -05:00
J. Nick Koston 9534a1df55 Move the dark service-table materializer and lookup helpers to #18205
Nothing in this PR emits USE_BLE_GATT_SERVICE_TABLE, so the Bluedroid
materializer, the neutral find_service/find_characteristic/find_cccd
helpers, their host tests, and the defines.h entry were scaffolding with
no caller here. They move to the PR that introduces their first
consumer. The contract op stays: get_service_table() keeps its {} stub
(the proxy streams in place).
2026-08-09 13:21:36 -05:00
J. Nick Koston d8ec76e02b Fail on premature enumeration terminators; explicit terminal states
- An INVALID_OFFSET/NOT_FOUND before the count from the same cache is a
  contradiction, not an end-of-range: the walker fails the build and the
  streamer aborts with the real cause instead of silently truncating
  (the walker's descriptor loop keeps its terminator - the 64 cap is its
  only bound)
- unconditional_disconnect_'s unset-conn-id path drives the slot to a
  terminal reported state instead of leaning on the scheduled-teardown
  timer
- find_characteristic/find_cccd log a corrupt range before returning the
  not-found sentinel (moved out of line - no ESP_LOG in headers)
2026-08-09 12:57:36 -05:00
J. Nick Koston d86bdb36cf Address review: honor SEARCH_CMPL status, close a late OPEN, backstops
- handle_search_cmpl_ takes the event status: a failed discovery no
  longer reads as a clean zero-service success (the empty cached
  database satisfies the count calls), and the table invalidation plus
  param step-down still run on the failure path
- A late successful OPEN_EVT on a slot the teardown net already gave up
  is closed instead of leaking a live controller link with no conn id
- The counting pass logs its failure like the two paths below it
- static_assert pins the wrapper's compile-time streamer detection; a
  signature drift would silently fall back to a table proxy builds
  compile without
- find_characteristic/find_cccd range math widened to 32-bit (correct by
  type; the wrapped-end case was already loop-safe)
2026-08-09 12:42:17 -05:00
J. Nick Koston c64684dd00 Tighten review-round comments to repo style 2026-08-09 12:36:55 -05:00
J. Nick Koston d04ef2378e Contract doc: writes without response also report on_write_result
Both backends report the no-response completion (Bluedroid at
WRITE_CHAR_EVT, rp2 synthesized or via the can-send path); the neutral
write action already relies on it.
2026-08-09 12:18:35 -05:00
J. Nick Koston 6c78b480c4 Return the wrapper to 48 bytes - exact per-slot RAM parity with dev
The measured layout showed 56+48 = 104 B/slot vs dev's 96. Two changes
that only pay together (the wrapper is 8-aligned, so nothing under a
full 8 helps):

- The tail packs into 2 bytes of bitfields; remote_addr_type_ becomes a
  start_connect_ parameter (written and read on adjacent lines only)
- The wrapper's duplicate 10 s teardown timer is gone: the backend owns
  the whole safety window. Its timer now also arms for a teardown
  scheduled during CONNECTING (the one case the wrapper covered alone),
  a late OPEN_EVT on a given-up slot closes the link instead of
  resurrecting it, and the wrapper's transient-refusal branch collapses
  because both backends return nonzero only when already idle

48 + 48 = 96 B/slot, the split at dev parity.
2026-08-09 12:07:10 -05:00
J. Nick Koston be5e00aefd Address review: discovery status, loop lifetime, event conn id
- SEARCH_CMPL honors the event's own status: a failed discovery leaves
  an empty cached database that the count calls read as a clean zero,
  which would have become an authoritative empty service list
- The loop stays enabled while a link exists and settles only at IDLE -
  the stack-down recovery needs a tick to run in ESTABLISHED, which the
  settle-on-established optimization was silently blocking
- The pre-started search uses the OPEN_EVT conn id (replaced-class
  parity)
2026-08-09 11:50:22 -05:00
J. Nick Koston 28491f83b4 Close the silent-failure paths from review
- A refused MTU request no longer wedges the connection: OPEN_EVT
  reports with the default MTU so the consumer proceeds (previously no
  CFG_MTU_EVT meant no connected report and a slot that never freed)
- Disabling the BLE stack settles a live link with a connected=false
  report before forgetting it, so the consumer frees its slot instead of
  holding a phantom connection
- A refused security response answers the pairing request with the
  failure instead of hanging it
- The service-table walk mismatch logs its reason before discarding
- The shared streamer's two bounds-check aborts use abort_service_stream
  with an ATT Unlikely Error cause (new shared GATT_ERR_UNLIKELY)
2026-08-09 11:19:55 -05:00
J. Nick Koston a513cef4e2 Encode the pre-started search lifecycle as a state machine
The three coordinated flags become one SearchState (NONE / PRESTARTED /
PRESTART_DONE / CLAIMED / REPORT_PENDING): illegal combinations are
unrepresentable, delivery and the two reset sites collapse to single
assignments, and the requested-meets-done subtlety becomes a named
state. A second discover_services() while one is in flight now returns 0
instead of issuing a duplicate search (one completion is already owed).
Same RAM: the enum sits in a 4-bit bitfield in the same flags byte.
2026-08-09 10:56:27 -05:00
J. Nick Koston 3a24d0b03b Reset the search latches per connection attempt
The latches were cleared only in set_idle_(), but the stack-down branch
in loop() reaches IDLE through set_state() without it, so after a
ble.disable/enable cycle the next connection's discover_services() would
complete immediately from the previous connection's latched result and
HA would cache an authoritative empty service list. tracker_connect_()
now resets the latch alongside the two per-attempt flags it already
clears, and delivery consumes the whole latch so a re-discovery on a
live link issues a real search instead of re-reporting the first
result.
2026-08-09 10:22:35 -05:00
J. Nick Koston 0512339c58 Deduplicate the stream-abort sequence and small cleanups
- abort_service_stream() on the wrapper (which owns the latch discipline)
  replaces five identical field-poke sequences in the streamer
- The pre-start reuses check_and_log_error_ like the serialized path
- discover_services() sets the requested flag where it sticks instead of
  set-then-rollback; deliver_pending_search_() returns void (no caller
  reads it); loop()'s IDLE arm folds into the settle arm
- SEARCH_CMPL during a teardown skips the param/count work whose result
  is never delivered
- frameworks_for_platforms() in config_helpers derives framework sets
  from platform lists; the hub.cpp filter entry is now a named map pinned
  by a test against the proxy's platform list
2026-08-09 10:05:42 -05:00
J. Nick Koston 084ff9d6e3 Restore the discovery/MTU overlap and latch streamer abort causes
The split serialized service discovery behind the MTU exchange: the
backend reported connected only at CFG_MTU_EVT and the wrapper started
discovery on that report, costing one ATT round trip per uncached
connection. The backend now pre-starts the search at OPEN_EVT (it knows
the connection type) and completes the consumer's discover_services()
from it: SEARCH_CMPL latches silently until requested, the flush after
the connected report delivers in the same event drain, and a refused
pre-start falls back to the serialized path. Net RAM cost is zero (the
flags pack into one bitfield byte plus a status byte, replacing the two
existing bools).

Also from review: SEARCH_CMPL no longer clobbers a teardown in progress
with ESTABLISHED, the five streamer abort sites latch pending_error_ so
HA sees the real Bluedroid status instead of a generic HCI reason, and a
completed re-discovery frees a materialized service table before the
counts its offsets derive from move.
2026-08-09 09:52:58 -05:00
J. Nick Koston 4a9e5f5da9 Require the proxy in the wrapper's compile gate
The hub wrapper serves the proxy's API surface, but its gate keyed on the
backend define alone while the backend guarded its own proxy pieces on
USE_BLUETOOTH_PROXY. Narrow BLUETOOTH_CONNECTION_HAS_GATT to require both
so a future backend-only consumer build agrees with the backend's guards.
No change to any build this PR can produce (only the proxy emits
USE_BLE_GATT_CLIENT here).
2026-08-09 09:44:23 -05:00
J. Nick Koston 26c000c417 Collapse shim-era indirection left by the merge of the two halves
- Fold the one-line event-handler forwarders, the state accessor renames,
  and the report helper into direct calls; drop the dead forward
  declaration and the tracker_client() accessor (the backend is the
  tracker client)
- Hoist DEFAULT_ATT_MTU next to the other shared GATT constants and use
  it in all three engines
- Emit BLUETOOTH_PROXY_MAX_CONNECTIONS once, in _connections_to_code
- Drop the unused esp_gatt_common_api.h include and DOMAIN constant
- Comment accuracy: stale shim/esp32-parity wording, registry roles,
  request_gatt_client() caller
2026-08-09 09:36:17 -05:00
J. Nick Koston 22af86a65b Tighten new comments to repo style 2026-08-09 09:19:32 -05:00
J. Nick Koston 6957e2ef15 Type gattc_if_ at its real width instead of shrinking the timer 2026-08-09 09:17:38 -05:00
J. Nick Koston f7b1e9ae65 Shrink the disconnect timestamp to a tick so the backend packs to 48 bytes 2026-08-09 09:04:31 -05:00
J. Nick Koston 54c50c07c5 Merge remote-tracking branch 'origin/dev' into esp32-gatt-backend
# Conflicts:
#	esphome/components/bluetooth_proxy/bluetooth_proxy.cpp
#	esphome/components/bluetooth_proxy/bluetooth_proxy.h
#	esphome/core/defines.h
2026-08-09 08:59:12 -05:00
J. Nick KostonandGitHub e9f428983e [ble_device_base] Bind BLEHub to the build's tracker at compile time (#18181) 2026-08-09 08:57:31 -05:00
J. Nick Koston 93124f1f1f Align two comments with the code they describe 2026-08-09 08:55:49 -05:00
J. Nick Koston 12fb1b0697 Name the MTU default and bound the descriptor walk sensibly 2026-08-09 04:20:39 -05:00
J. Nick Koston be818a5bf2 Apply review polish: bounded walk, real statuses, guarded settles 2026-08-09 03:44:10 -05:00
J. Nick Koston 612033062f Apply combined review: wire MTU parity, abort teardown parity, closure ownership 2026-08-09 03:08:47 -05:00
J. Nick Koston 833a835e0f Combine the flip into the backend PR and put the table define in the esp32 arm 2026-08-09 02:27:53 -05:00
J. Nick Koston f2261d6632 Merge branch 'esp32-gatt-backend' into esp32-proxy-flip 2026-08-09 02:16:17 -05:00
J. Nick Koston d224a82e38 Fold the tracker shim into the backend and pass the MTU through 2026-08-09 02:16:14 -05:00
J. Nick Koston ceabfeb6f4 Include the header that declares the bond removal 2026-08-09 01:52:39 -05:00
J. Nick Koston 5e11a886f1 Merge branch 'esp32-gatt-backend' into esp32-proxy-flip 2026-08-09 01:51:59 -05:00
J. Nick Koston e771b2522a Terminate a released stream without services-done and keep the net armed 2026-08-09 01:51:42 -05:00
J. Nick Koston 06a52f682f Name the platform explicitly in schema builders for the dumper 2026-08-09 01:01:21 -05:00
J. Nick Koston 409ff027cc Collapse the identical ignore branches 2026-08-09 00:59:16 -05:00
J. Nick Koston 3c7e5000a5 Merge branch 'esp32-gatt-backend' into esp32-proxy-flip 2026-08-09 00:44:41 -05:00
J. Nick Koston 7af211ab54 Deliver completions through a GattClientListener interface 2026-08-09 00:44:35 -05:00
J. Nick Koston cc81069d02 Keep the esp32 maintenance functions outside the gated surface 2026-08-09 00:28:54 -05:00