From 39bad55f7346372cfd6a2fa7c1270014e2dd15e9 Mon Sep 17 00:00:00 2001 From: Dan Halbert Date: Sun, 11 Oct 2026 18:51:26 -0400 Subject: [PATCH 1/2] espressif: fix connect() timeout and Ctrl-C handling When connect() timed out, it usually returned a useless Connection, with connected False, instead of raising an exception. The timeout was used both by NimBLE and by connect()'s own wait loop. NimBLE reports its timeout only after the controller cancels the connection attempt, so the wait loop usually stopped first. The connection status was then still invalid, which matched an unused connection slot, so connect() returned that. NimBLE also kept trying to connect, so the next connect() raised "Already in progress". When NimBLE did report first, its timeout came out as "Unknown BLE error: 13". Wait for NimBLE's BLE_GAP_EVENT_CONNECT instead, which it always sends, and raise "Failed to connect: timeout" on BLE_HS_ETIMEOUT. On Ctrl-C, cancel the attempt and wait for the cancellation. Reset the status before ble_gap_connect(), and end the wait if the MTU exchange can't be started. Co-Authored-By: Claude Opus 5.5 --- ports/espressif/common-hal/_bleio/Adapter.c | 31 +++++++++++++++------ 1 file changed, 23 insertions(+), 8 deletions(-) diff --git a/ports/espressif/common-hal/_bleio/Adapter.c b/ports/espressif/common-hal/_bleio/Adapter.c index 34122a1048c..9bd532e1a7a 100644 --- a/ports/espressif/common-hal/_bleio/Adapter.c +++ b/ports/espressif/common-hal/_bleio/Adapter.c @@ -407,7 +407,10 @@ static void _new_connection(uint16_t conn_handle, bool user_owned) { connection->mtu = 0; connection->user_owned = user_owned; - ble_gattc_exchange_mtu(conn_handle, _mtu_reply, connection); + if (ble_gattc_exchange_mtu(conn_handle, _mtu_reply, connection) != 0) { + // _mtu_reply() won't be called, so report that the connection is established here. + _connection_status = conn_handle; + } // Change the callback for the connection. ble_gap_set_event_cb(conn_handle, bleio_connection_event_cb, connection); @@ -468,6 +471,10 @@ mp_obj_t common_hal_bleio_adapter_connect(bleio_adapter_obj_t *self, bleio_addre ble_addr_t addr; _convert_address(address, &addr); + // _connection_status gets set to either a positive connection handle or a negative error code. + // Reset it before connecting, so an early event isn't lost. + _connection_status = BLEIO_HANDLE_INVALID; + const int timeout_ms = SEC_TO_UNITS(timeout, UNIT_1_MS) + 0.5f; CHECK_NIMBLE_ERROR( ble_gap_connect(own_addr_type, &addr, @@ -475,20 +482,28 @@ mp_obj_t common_hal_bleio_adapter_connect(bleio_adapter_obj_t *self, bleio_addre &conn_params, _connect_event, self)); - // Wait an extra 50 ms to give the connect method the opportunity to time out. - - const uint64_t timeout_time_ms = common_hal_time_monotonic_ms() + timeout_ms; - // _connection_status gets set to either a positive connection handle or a negative error code. - _connection_status = BLEIO_HANDLE_INVALID; - while (_connection_status == BLEIO_HANDLE_INVALID && (common_hal_time_monotonic_ms() < timeout_time_ms)) { + // NimBLE always ends a connection attempt with a BLE_GAP_EVENT_CONNECT, including when + // timeout_ms expires, so wait for that instead of keeping a separate deadline here. + // If we stopped waiting first, NimBLE would still be connecting, and the next connect() + // would fail. + while (_connection_status == BLEIO_HANDLE_INVALID) { RUN_BACKGROUND_TASKS; if (mp_hal_is_interrupted()) { - // Return prematurely. Then the interrupt exception will be raised. + // Stop connecting, and wait for the cancellation event, so NimBLE is ready + // for the next connect(). The interrupt exception is raised after we return. + if (ble_gap_conn_cancel() == 0) { + while (_connection_status == BLEIO_HANDLE_INVALID) { + RUN_BACKGROUND_TASKS; + } + } return mp_const_none; } } // Negative values are error codes, connection handle otherwise. + if (_connection_status == -BLE_HS_ETIMEOUT) { + mp_raise_bleio_BluetoothError(MP_ERROR_TEXT("Failed to connect: timeout")); + } if (_connection_status < 0) { CHECK_BLE_ERROR(-_connection_status); } From 9c1ce536e4689527ec31947d8afdd7f13feb978b Mon Sep 17 00:00:00 2001 From: Dan Halbert Date: Sun, 11 Oct 2026 19:18:39 -0400 Subject: [PATCH 2/2] espressif: get MTU when the peer already exchanged it A peer such as Zephyr starts the MTU exchange itself right after connecting. NimBLE answers it before _new_connection() runs, and then ble_gattc_exchange_mtu() returns BLE_HS_EALREADY, so _mtu_reply() is never called. The BLE_GAP_EVENT_MTU went to _connect_event(), which ignores it, so connection->mtu stayed 0 and max_packet_length reported 20 instead of the negotiated MTU minus 3. Get the MTU with ble_att_mtu() in that case. Co-Authored-By: Claude Opus 5.5 --- ports/espressif/common-hal/_bleio/Adapter.c | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/ports/espressif/common-hal/_bleio/Adapter.c b/ports/espressif/common-hal/_bleio/Adapter.c index 9bd532e1a7a..63549fc2ee6 100644 --- a/ports/espressif/common-hal/_bleio/Adapter.c +++ b/ports/espressif/common-hal/_bleio/Adapter.c @@ -408,7 +408,12 @@ static void _new_connection(uint16_t conn_handle, bool user_owned) { connection->user_owned = user_owned; if (ble_gattc_exchange_mtu(conn_handle, _mtu_reply, connection) != 0) { - // _mtu_reply() won't be called, so report that the connection is established here. + // _mtu_reply() won't be called, so do its work here. + // This is the usual case with a peer that starts the MTU exchange itself, + // such as Zephyr: NimBLE answers that before this runs, and then refuses + // to start a second exchange. The BLE_GAP_EVENT_MTU it sent went to + // _connect_event(), not to the connection's event callback, so get the MTU now. + connection->mtu = ble_att_mtu(conn_handle); _connection_status = conn_handle; }