All of lore.kernel.org
 help / color / mirror / Atom feed
* [RFC PATCH 1/2] shared/asha: Cancel callbacks before resetting the device
@ 2026-10-15  4:55 Kyle Pepper
  2026-10-15  4:56 ` [RFC PATCH 2/2] asha: Gate streaming on the observed connection interval Kyle Pepper
  0 siblings, 1 reply; 2+ messages in thread
From: Kyle Pepper @ 2026-10-15  4:55 UTC (permalink / raw)
  To: linux-bluetooth; +Cc: Marcel Holtmann, Luiz Augusto von Dentz

Track and cancel the pending ACP request before releasing stream state.
Detach state callbacks before invoking an owner that may remove the device.
Send STOP when canceling a START that is already queued.

Detach set membership before clearing its identity, retain replacement
members and clear notification registration on reset.

Fixes: 8e4bece63ce9 ("src/shared: Add initial implementation for an ASHA profile")
Fixes: f44d3e08e9a1 ("shared/asha: Add support for other side update")
Assisted-by: OpenCode:gpt-6.1-sol
---
First of two new patches. Apply on BlueZ dd9d5ba1fa4f with the already-sent
STATUS v2 and STOP changes, in that order. These prerequisites are not resent:
https://lore.kernel.org/linux-bluetooth/20261013222556.513513-1-cochcoder@proton.me/
https://lore.kernel.org/linux-bluetooth/20261013034345.513789-1-cochcoder@proton.me/

This patch handles pending ACP request/callback ownership, cancellation and
set/notification reset. Patch 2 uses these lifecycle operations to gate ASHA
startup and stop acquired streams after incompatible completed intervals.

The prerequisite alone passes its ASHA sanitizer fixtures. The combined
standalone source passes 38/38 strict-maintainer ASan/UBSan and 38/38 ordinary
LTO unit binaries. The full strict-maintainer plus LTO combination retains
documented baseline GATT/monitor errors; it is not claimed passing.

The Arch integration was built in a clean chroot and is reported working on
hardware. That trial also includes other existing runtime fixes; it is not
isolated attribution or coverage of every cancellation/teardown interleaving.
No remote CI, severity assessment or proof of all surrounding lifetimes is claimed.

 src/shared/asha.c         |  63 +++++++++++-------
 src/shared/asha.h         |   1 +
 unit/test-asha-interval.c | 137 ++++++++++++++++++++++++++++++++++++++
 3 files changed, 177 insertions(+), 24 deletions(-)

diff --git a/src/shared/asha.c b/src/shared/asha.c
index f73bbce1d5..e6862e8423 100644
--- a/src/shared/asha.c
+++ b/src/shared/asha.c
@@ -106,10 +106,10 @@ static void update_asha_set(struct bt_asha *asha, bool connected)
 			return;
 		}
 
-		if (asha->right_side && set->right) {
+		if (asha->right_side && set->right == asha) {
 			set->right = NULL;
 			DBG("Right side unregistered for ASHA set");
-		} else if (!asha->right_side && set->left) {
+		} else if (!asha->right_side && set->left == asha) {
 			set->left = NULL;
 			DBG("Left side unregistered for ASHA set");
 		}
@@ -163,9 +163,13 @@ struct bt_asha *bt_asha_new(void)
 
 void bt_asha_reset(struct bt_asha *asha)
 {
+	bt_asha_state_reset(asha);
+	update_asha_set(asha, false);
+
 	if (asha->status_notify_id) {
 		bt_gatt_client_unregister_notify(asha->client,
 						asha->status_notify_id);
+		asha->status_notify_id = 0;
 	}
 
 	gatt_db_unref(asha->db);
@@ -174,19 +178,20 @@ void bt_asha_reset(struct bt_asha *asha)
 	bt_gatt_client_unref(asha->client);
 	asha->client = NULL;
 
-	bt_asha_state_reset(asha);
-
 	asha->psm = 0;
 	memset(asha->hisyncid, 0, sizeof(asha->hisyncid));
 
 	asha->attach_cb = NULL;
 	asha->attach_cb_data = NULL;
-
-	update_asha_set(asha, false);
 }
 
 void bt_asha_state_reset(struct bt_asha *asha)
 {
+	if (asha->acp_id) {
+		bt_gatt_client_cancel(asha->client, asha->acp_id);
+		asha->acp_id = 0;
+	}
+
 	asha->state = ASHA_STOPPED;
 
 	asha->state_cb = NULL;
@@ -195,33 +200,40 @@ void bt_asha_state_reset(struct bt_asha *asha)
 
 void bt_asha_free(struct bt_asha *asha)
 {
-	update_asha_set(asha, false);
-	gatt_db_unref(asha->db);
-	bt_gatt_client_unref(asha->client);
+	bt_asha_reset(asha);
 	free(asha);
 }
 
 static void asha_acp_sent(bool success, uint8_t err, void *user_data)
 {
 	struct bt_asha *asha = user_data;
+	bt_asha_cb_t callback = asha->state_cb;
+	void *data = asha->state_cb_data;
+
+	asha->acp_id = 0;
 
 	if (success) {
 		DBG("AudioControlPoint command successfully sent");
 	} else {
 		error("Failed to send AudioControlPoint command: %d", err);
 
-		if (asha->state_cb)
-			asha->state_cb(-1, asha->state_cb_data);
-
 		bt_asha_state_reset(asha);
+
+		if (callback)
+			callback(-1, data);
 	}
 }
 
 static int asha_send_acp(struct bt_asha *asha, uint8_t *cmd,
 		unsigned int len, bt_asha_cb_t cb, void *user_data)
 {
-	if (!bt_gatt_client_write_value(asha->client, asha->acp_handle, cmd,
-				len, asha_acp_sent, asha, NULL)) {
+	if (asha->acp_id)
+		bt_gatt_client_cancel(asha->client, asha->acp_id);
+
+	asha->acp_id = bt_gatt_client_write_value(asha->client,
+				asha->acp_handle, cmd, len, asha_acp_sent,
+				asha, NULL);
+	if (!asha->acp_id) {
 		error("Error writing ACP command");
 		return -1;
 	}
@@ -307,18 +319,17 @@ unsigned int bt_asha_stop(struct bt_asha *asha)
 	};
 	int ret;
 
-	if (asha->state != ASHA_STARTED)
+	if (asha->state == ASHA_STOPPED)
 		return 0;
 
-	asha->state = ASHA_STOPPED;
+	/* Cancel a pending START before queuing STOP on the same ATT link. */
+	bt_asha_state_reset(asha);
 
 	/* State is reset below without waiting for an ATT response. */
 	ret = asha_send_acp_without_response(asha, acp_stop_cmd,
 			sizeof(acp_stop_cmd));
 	asha_set_send_status(asha, false);
 
-	/* We reset our state without waiting for a response */
-	bt_asha_state_reset(asha);
 	DBG("ASHA stop done");
 
 	return ret;
@@ -468,10 +479,17 @@ static void audio_status_notify(uint16_t value_handle, const uint8_t *value,
 					uint16_t length, void *user_data)
 {
 	struct bt_asha *asha = user_data;
-	uint8_t status = *value;
+	uint8_t status;
 	/* Back these up to survive the reset paths */
 	bt_asha_cb_t state_cb = asha->state_cb;
-	bt_asha_cb_t state_cb_data = asha->state_cb_data;
+	void *state_cb_data = asha->state_cb_data;
+
+	if (length != 1)
+		return;
+
+	status = value[0];
+	asha->state_cb = NULL;
+	asha->state_cb_data = NULL;
 
 	DBG("ASHA status %u", status);
 
@@ -487,11 +505,8 @@ static void audio_status_notify(uint16_t value_handle, const uint8_t *value,
 		}
 	}
 
-	if (state_cb) {
+	if (state_cb)
 		state_cb(status, state_cb_data);
-		asha->state_cb = NULL;
-		asha->state_cb_data = NULL;
-	}
 }
 
 static void handle_characteristic(struct gatt_db_attribute *attr,
diff --git a/src/shared/asha.h b/src/shared/asha.h
index e6b2e7a0d2..cbacb5ddff 100644
--- a/src/shared/asha.h
+++ b/src/shared/asha.h
@@ -31,6 +31,7 @@ struct bt_asha {
 	uint16_t acp_handle;
 	uint16_t volume_handle;
 	unsigned int status_notify_id;
+	unsigned int acp_id;
 
 	uint16_t psm;
 	bool right_side;
diff --git a/unit/test-asha-interval.c b/unit/test-asha-interval.c
index 8cb77c07d3..5432cfb041 100644
--- a/unit/test-asha-interval.c
+++ b/unit/test-asha-interval.c
@@ -3,13 +3,136 @@
 
 /* Redirect source-level calls so LTO cannot bypass the GATT mock. */
 #define bt_gatt_client_write_without_response test_gatt_write
+#define bt_gatt_client_write_value test_gatt_request
+#define bt_gatt_client_cancel test_gatt_cancel
+#define bt_gatt_client_unref test_client_unref
+#define bt_gatt_client_unregister_notify test_unregister_notify
 #include "src/shared/asha.c"
 #undef bt_gatt_client_write_without_response
+#undef bt_gatt_client_write_value
+#undef bt_gatt_client_cancel
+#undef bt_gatt_client_unref
+#undef bt_gatt_client_unregister_notify
 #include "src/shared/tester.h"
 
 static unsigned int writes;
 static struct bt_gatt_client *failed_client;
 static uint8_t expected_interval;
+static unsigned int cancels;
+static unsigned int callbacks;
+static unsigned int unregisters;
+static unsigned int stops;
+
+void test_client_unref(struct bt_gatt_client *client)
+{
+}
+
+bool test_unregister_notify(struct bt_gatt_client *client, unsigned int id)
+{
+	assert(client && id == 7);
+	unregisters++;
+	return true;
+}
+
+unsigned int test_gatt_request(struct bt_gatt_client *client, uint16_t handle,
+			const uint8_t *value, uint16_t length,
+			bt_gatt_client_callback_t callback, void *user_data,
+			bt_gatt_client_destroy_func_t destroy)
+{
+	assert(client && handle == 0x79);
+	assert(length == 5 && value[0] == 1);
+	return 42;
+}
+
+bool test_gatt_cancel(struct bt_gatt_client *client, unsigned int id)
+{
+	assert(client && id == 42);
+	cancels++;
+	return true;
+}
+
+static void state_callback(int status, void *user_data)
+{
+	struct bt_asha *asha = user_data;
+
+	assert(asha->state == ASHA_STOPPED);
+	assert(!asha->state_cb && !asha->state_cb_data);
+	callbacks++;
+}
+
+static void test_start_cancel(const void *user_data)
+{
+	struct bt_asha asha = { .client = (void *) 1, .acp_handle = 0x79 };
+
+	cancels = callbacks = 0;
+	assert(!bt_asha_start(&asha, state_callback, &asha));
+	bt_asha_state_reset(&asha);
+	assert(cancels == 1 && !callbacks);
+	bt_asha_state_reset(&asha);
+	assert(cancels == 1);
+	assert(!bt_asha_start(&asha, state_callback, &asha));
+	asha_acp_sent(false, 1, &asha);
+	assert(callbacks == 1 && cancels == 1);
+	tester_test_passed();
+}
+
+static void test_start_stop(const void *user_data)
+{
+	struct bt_asha asha = { .client = (void *) 1, .acp_handle = 0x79 };
+	uint8_t status = 0;
+
+	cancels = callbacks = stops = 0;
+	assert(!bt_asha_start(&asha, state_callback, &asha));
+	assert(!bt_asha_stop(&asha));
+	assert(cancels == 1 && stops == 1 && !asha.state_cb);
+	audio_status_notify(1, &status, 1, &asha);
+	assert(asha.state == ASHA_STOPPED && !callbacks);
+	assert(!bt_asha_stop(&asha) && stops == 1);
+	tester_test_passed();
+}
+
+static void test_status_callback(const void *user_data)
+{
+	struct bt_asha asha = { .client = (void *) 1, .acp_handle = 0x79 };
+	uint8_t status = 1;
+
+	callbacks = 0;
+	assert(!bt_asha_start(&asha, state_callback, &asha));
+	asha_acp_sent(true, 0, &asha);
+	audio_status_notify(1, NULL, 0, &asha);
+	assert(!callbacks && asha.state_cb == state_callback);
+	audio_status_notify(1, &status, 1, &asha);
+	assert(callbacks == 1);
+	tester_test_passed();
+}
+
+static void test_pair_reset(const void *user_data)
+{
+	struct bt_asha left = { .hisyncid = { 1 } };
+	struct bt_asha right = { .hisyncid = { 1 }, .right_side = true };
+	struct bt_asha replacement = { .hisyncid = { 1 } };
+	struct bt_asha_set *set;
+
+	asha_devices = queue_new();
+	update_asha_set(&left, true);
+	update_asha_set(&right, true);
+	update_asha_set(&replacement, true);
+	bt_asha_reset(&left);
+	set = find_asha_set(&right);
+	assert(set && set->left == &replacement && set->right == &right);
+	bt_asha_reset(&replacement);
+	assert(!set->left && set->right == &right);
+	bt_asha_reset(&right);
+	assert(!asha_devices);
+
+	right.client = (void *) 1;
+	right.status_notify_id = 7;
+	unregisters = 0;
+	bt_asha_reset(&right);
+	bt_asha_reset(&right);
+	assert(unregisters == 1 && !right.status_notify_id);
+	tester_test_passed();
+}
 
 void btd_error(uint16_t index, const char *format, ...)
 {
@@ -26,6 +149,13 @@ unsigned int test_gatt_write(
 	assert(client);
 	assert(handle == 0x79);
 	assert(!signed_write);
+
+	if (length == 1) {
+		assert(value[0] == 0x02);
+		stops++;
+		return true;
+	}
+
 	assert(length == 3);
 	assert(value[0] == 0x03 && value[1] == 0x02);
 	assert(value[2] == expected_interval);
@@ -100,5 +230,12 @@ int main(int argc, char *argv[])
 {
 	tester_init(&argc, &argv);
 	tester_add("ASHA/interval/pair", NULL, NULL, test_interval, NULL);
+	tester_add("ASHA/start/cancel", NULL, NULL, test_start_cancel, NULL);
+	tester_add("ASHA/start/stop-pending", NULL, NULL,
+							test_start_stop, NULL);
+	tester_add("ASHA/start/status-callback", NULL, NULL,
+						test_status_callback, NULL);
+	tester_add("ASHA/interval/pair-reset", NULL, NULL,
+							test_pair_reset, NULL);
 	return tester_run();
 }

base-commit: dd9d5ba1fa4f682b082b1275a8c38523d95a58fd
prerequisite-patch-id: ddf57fe789414af62220e755ca04c36ec8d46260
prerequisite-patch-id: 612e58f161bb31025d30aabe6a68809255e314ba


^ permalink raw reply related	[flat|nested] 2+ messages in thread

* [RFC PATCH 2/2] asha: Gate streaming on the observed connection interval
  2026-10-15  4:55 [RFC PATCH 1/2] shared/asha: Cancel callbacks before resetting the device Kyle Pepper
@ 2026-10-15  4:56 ` Kyle Pepper
  0 siblings, 0 replies; 2+ messages in thread
From: Kyle Pepper @ 2026-10-15  4:56 UTC (permalink / raw)
  To: linux-bluetooth; +Cc: Marcel Holtmann, Luiz Augusto von Dentz

Wait for a completed 20 ms interval and queued STATUS before sending
G.722 START. Bound acquisition through AudioStatusPoint and cancel
pending CoC, timer and ACP work before releasing its owner.

Report later completed intervals and close an acquired stream if its
interval becomes incompatible. Defer transport teardown outside HCI
iteration and unregister its callback when the transport is destroyed.

Fixes: c26389c466a0 ("profiles/audio: Add an ASHA plugin")
Assisted-by: OpenCode:gpt-6.1-sol
---
Second of two new patches; requires patch 1 and the already-sent STATUS v2
and STOP prerequisites. It does not replace or resend either prerequisite.

Observe a completed 20 ms interval and queue STATUS before START. Bound setup
through AudioStatusPoint and cancel pending acquisition on stop/reset/removal.
On an incompatible completed interval after acquisition, report STATUS and defer
STOP, CoC closure and transport-owner removal outside the HCI stream iteration.
A failed update proposal does not invalidate a last observed working interval.

Coverage includes 21 startup, seven pending-CoC and 12 acquired-stream scenarios.
Combined standalone source: 38/38 strict ASan/UBSan and 38/38 ordinary LTO unit
binaries. Actual BtIO watch fixture: 500 cycles; extracted transport closure
functions: 1,000 mocked-owner cycles. No Valgrind or remote-CI pass is claimed.

The combined Arch port passes 44/44 package tests and is reported working on
hardware; specific N7, paired-device and incompatible-update hardware coverage
was not supplied. Standalone source is submitted here, not package integration.

Unknown/missed initial intervals are not reconstructed from requested values:
setup waits for an observation and may time out, including after daemon restart.
Incompatible acquired timing closes the audio stream; seamless renegotiation
or uninterrupted playback is not guaranteed. No forced ACL disconnect or new
kernel ABI is added. A valid disjoint peer request can remain legal in Linux.

Companion Linux soft-preference RFC v2 and independent BlueZ interval tests:
https://lore.kernel.org/linux-bluetooth/20261015043336.47734-1-cochcoder@proton.me/
https://lore.kernel.org/linux-bluetooth/20261015044221.55631-1-cochcoder@proton.me/
Archive rendering of these links has not been independently verified.

 profiles/audio/asha.c      | 333 +++++++++++++++++---
 profiles/audio/asha.h      |   2 +
 profiles/audio/transport.c |  20 +-
 unit/test-asha-conn.c      | 603 +++++++++++++++++++++++++++++++++++--
 4 files changed, 904 insertions(+), 54 deletions(-)

diff --git a/profiles/audio/asha.c b/profiles/audio/asha.c
index 6596ff3633..310022ff4c 100644
--- a/profiles/audio/asha.c
+++ b/profiles/audio/asha.c
@@ -55,6 +55,8 @@
 #define ASHA_MIN_MTU 163
 /* The default of 672 does not work */
 #define ASHA_CONNECTION_MTU 512
+#define ASHA_STREAM_INTERVAL 0x0010
+#define ASHA_START_TIMEOUT 10
 
 /* HCI connection handles occupy 0x0000 through 0x0eff. Zero is unknown. */
 #define ASHA_NUM_HANDLES 0x0f00
@@ -68,15 +70,34 @@ struct asha_adapter {
 static struct queue *asha_adapters;
 static struct queue *asha_streams;
 
+struct asha_start_wait {
+	bt_asha_cb_t callback;
+	void *user_data;
+	unsigned int source_id;
+	int error;
+	bool connected;
+	bool sent;
+};
+
+struct connect_data {
+	struct bt_asha_device *asha_dev;
+};
+
 struct bt_asha_device {
 	struct bt_asha *asha;
 	struct btd_device *device;
 	struct media_transport *transport;
 
 	GIOChannel *io;
+	struct connect_data *connect_data;
 	uint16_t imtu, omtu;
 
 	unsigned int resume_id;
+	struct asha_start_wait *start_wait;
+	unsigned int close_id;
+	int close_error;
+	bt_asha_cb_t closed_cb;
+	void *closed_data;
 };
 
 static bool match_adapter(const void *data, const void *user_data)
@@ -113,20 +134,25 @@ static bool asha_get_handle(struct bt_asha_device *asha_dev, uint16_t *handle)
 	return true;
 }
 
-static void asha_notify_interval(struct bt_asha_device *asha_dev,
+static bool asha_notify_interval(struct bt_asha_device *asha_dev,
 					uint16_t handle, uint16_t interval)
 {
 	/* Only announce an interval observed on this connection. */
 	if (!interval)
-		return;
+		return false;
 
 	DBG("ASHA connection h%u interval %u", handle, interval);
 
-	if (!bt_asha_set_connection_interval(asha_dev->asha, interval))
+	if (!bt_asha_set_connection_interval(asha_dev->asha, interval)) {
 		DBG("Could not send ASHA connection interval notification");
+		return false;
+	}
+
+	return true;
 }
 
-static void asha_send_interval(struct bt_asha_device *asha_dev)
+static bool asha_send_interval(struct bt_asha_device *asha_dev,
+						uint16_t *interval)
 {
 	struct asha_adapter *data;
 	uint16_t handle;
@@ -134,39 +160,234 @@ static void asha_send_interval(struct bt_asha_device *asha_dev)
 	data = queue_find(asha_adapters, match_adapter,
 					device_get_adapter(asha_dev->device));
 	if (!data || !asha_get_handle(asha_dev, &handle))
+		return false;
+
+	if (interval)
+		*interval = data->intervals[handle];
+
+	return asha_notify_interval(asha_dev, handle, data->intervals[handle]);
+}
+
+static void asha_start_wait_cancel(struct bt_asha_device *asha_dev)
+{
+	struct asha_start_wait *wait = asha_dev->start_wait;
+
+	if (!wait)
+		return;
+
+	asha_dev->start_wait = NULL;
+
+	if (wait->source_id)
+		g_source_remove(wait->source_id);
+
+	g_free(wait);
+}
+
+static void asha_cancel_connect(struct bt_asha_device *asha_dev)
+{
+	if (!asha_dev->connect_data)
+		return;
+
+	asha_dev->connect_data->asha_dev = NULL;
+	asha_dev->connect_data = NULL;
+}
+
+static gboolean asha_start_wait_error(gpointer user_data)
+{
+	struct bt_asha_device *asha_dev = user_data;
+	struct asha_start_wait *wait = asha_dev->start_wait;
+	bt_asha_cb_t callback = wait->callback;
+	void *data = wait->user_data;
+	int err = wait->error;
+
+	wait->source_id = 0;
+	bt_asha_device_stop(asha_dev);
+
+	/* The owner callback may remove the device. */
+	if (callback)
+		callback(err, data);
+
+	return G_SOURCE_REMOVE;
+}
+
+static gboolean asha_start_wait_timeout(gpointer user_data)
+{
+	struct bt_asha_device *asha_dev = user_data;
+
+	asha_dev->start_wait->error = -ETIMEDOUT;
+	return asha_start_wait_error(asha_dev);
+}
+
+static void asha_start_wait_fail(struct bt_asha_device *asha_dev, int err)
+{
+	struct asha_start_wait *wait = asha_dev->start_wait;
+
+	if (!wait || wait->error)
+		return;
+
+	if (wait->source_id)
+		g_source_remove(wait->source_id);
+
+	wait->error = err;
+	/* Do not call the owner from inside the HCI stream iteration. */
+	wait->source_id = g_idle_add(asha_start_wait_error, asha_dev);
+}
+
+static void asha_start_complete(int status, void *user_data)
+{
+	struct bt_asha_device *asha_dev = user_data;
+	struct asha_start_wait *wait = asha_dev->start_wait;
+	bt_asha_cb_t callback;
+	void *data;
+
+	if (!wait || wait->error)
 		return;
 
-	asha_notify_interval(asha_dev, handle, data->intervals[handle]);
+	if (status) {
+		asha_start_wait_fail(asha_dev, -EIO);
+		return;
+	}
+
+	callback = wait->callback;
+	data = wait->user_data;
+	asha_start_wait_cancel(asha_dev);
+
+	if (callback)
+		callback(0, data);
+}
+
+static void asha_start_wait_try(struct bt_asha_device *asha_dev)
+{
+	struct asha_start_wait *wait = asha_dev->start_wait;
+	uint16_t interval = 0;
+	bool notified;
+
+	if (!wait || wait->error || !wait->connected || wait->sent)
+		return;
+
+	notified = asha_send_interval(asha_dev, &interval);
+	if (!interval)
+		return;
+
+	if (!notified) {
+		asha_start_wait_fail(asha_dev, -EIO);
+		return;
+	}
+
+	if (interval != ASHA_STREAM_INTERVAL)
+		return;
+
+	wait->sent = true;
+
+	if (bt_asha_start(asha_dev->asha, asha_start_complete, asha_dev))
+		asha_start_wait_fail(asha_dev, -EIO);
+}
+
+static void asha_start_wait_begin(struct bt_asha_device *asha_dev,
+					bt_asha_cb_t callback, void *user_data)
+{
+	struct asha_start_wait *wait;
+
+	wait = g_new0(struct asha_start_wait, 1);
+	wait->callback = callback;
+	wait->user_data = user_data;
+	asha_dev->start_wait = wait;
+	wait->source_id = g_timeout_add_seconds(ASHA_START_TIMEOUT,
+					asha_start_wait_timeout, asha_dev);
+}
+
+static void asha_stream_close_cancel(struct bt_asha_device *asha_dev)
+{
+	if (!asha_dev->close_id)
+		return;
+
+	g_source_remove(asha_dev->close_id);
+	asha_dev->close_id = 0;
+}
+
+static gboolean asha_stream_closed(gpointer user_data)
+{
+	struct bt_asha_device *asha_dev = user_data;
+	bt_asha_cb_t callback = asha_dev->closed_cb;
+	void *data = asha_dev->closed_data;
+	int err = asha_dev->close_error;
+
+	asha_dev->close_id = 0;
+	bt_asha_device_stop(asha_dev);
+
+	if (callback)
+		callback(err, data);
+
+	return G_SOURCE_REMOVE;
+}
+
+static void asha_stream_fail(struct bt_asha_device *asha_dev, int err)
+{
+	if (asha_dev->close_id || asha_dev->asha->state != ASHA_STARTED)
+		return;
+
+	asha_dev->close_error = err;
+	/* Transport teardown must not run inside the HCI stream iteration. */
+	asha_dev->close_id = g_idle_add(asha_stream_closed, asha_dev);
 }
 
 struct interval_update {
 	struct asha_adapter *adapter;
 	uint16_t handle;
+	int error;
 };
 
 static void asha_update_stream(void *data, void *user_data)
 {
 	struct bt_asha_device *asha_dev = data;
 	struct interval_update *update = user_data;
-	uint16_t handle;
+	uint16_t handle, interval;
+	bool notified;
 
 	if (device_get_adapter(asha_dev->device) != update->adapter->adapter ||
 			!asha_get_handle(asha_dev, &handle) ||
 			handle != update->handle)
 		return;
 
-	asha_notify_interval(asha_dev, handle,
-					update->adapter->intervals[handle]);
+	if (update->error) {
+		asha_start_wait_fail(asha_dev, update->error);
+
+		if (update->error == -ENOTCONN)
+			asha_stream_fail(asha_dev, update->error);
+
+		return;
+	}
+
+	interval = update->adapter->intervals[handle];
+
+	/* An update can race the START response or AudioStatus notification. */
+	if (asha_dev->start_wait && asha_dev->start_wait->sent &&
+					interval != ASHA_STREAM_INTERVAL) {
+		asha_start_wait_fail(asha_dev, -ERANGE);
+		return;
+	}
+
+	if (asha_dev->start_wait) {
+		asha_start_wait_try(asha_dev);
+		return;
+	}
+
+	notified = asha_notify_interval(asha_dev, handle, interval);
+
+	if (interval != ASHA_STREAM_INTERVAL)
+		asha_stream_fail(asha_dev, -ERANGE);
+	else if (!notified)
+		asha_stream_fail(asha_dev, -EIO);
 }
 
 static void asha_conn_update(const void *param, uint8_t size, void *user_data)
 {
 	const struct bt_hci_evt_le_conn_update_complete *ev = param;
 	struct asha_adapter *data = user_data;
-	struct interval_update update;
+	struct interval_update update = { 0 };
 	uint16_t handle;
 
-	if (size != sizeof(*ev) || ev->status)
+	if (size != sizeof(*ev))
 		return;
 
 	handle = get_le16(&ev->handle);
@@ -174,7 +395,10 @@ static void asha_conn_update(const void *param, uint8_t size, void *user_data)
 	if (handle >= ASHA_NUM_HANDLES)
 		return;
 
-	data->intervals[handle] = get_le16(&ev->interval);
+	if (!ev->status)
+		data->intervals[handle] = get_le16(&ev->interval);
+	else
+		update.error = -EIO;
 	update.adapter = data;
 	update.handle = handle;
 	queue_foreach(asha_streams, asha_update_stream, &update);
@@ -216,6 +440,7 @@ static void asha_disconn_complete(const void *param, uint8_t size,
 {
 	const struct bt_hci_evt_disconnect_complete *ev = param;
 	struct asha_adapter *data = user_data;
+	struct interval_update update = { .adapter = data, .error = -ENOTCONN };
 	uint16_t handle;
 
 	if (size != sizeof(*ev) || ev->status)
@@ -223,8 +448,12 @@ static void asha_disconn_complete(const void *param, uint8_t size,
 
 	handle = get_le16(&ev->handle);
 
-	if (handle < ASHA_NUM_HANDLES)
-		data->intervals[handle] = 0;
+	if (handle >= ASHA_NUM_HANDLES)
+		return;
+
+	data->intervals[handle] = 0;
+	update.handle = handle;
+	queue_foreach(asha_streams, asha_update_stream, &update);
 }
 
 static int asha_adapter_probe(struct btd_profile *profile,
@@ -263,6 +492,17 @@ fail:
 	return -EIO;
 }
 
+static void asha_adapter_cancel_stream(void *data, void *user_data)
+{
+	struct bt_asha_device *asha_dev = data;
+
+	if (device_get_adapter(asha_dev->device) != user_data)
+		return;
+
+	asha_start_wait_fail(asha_dev, -ENODEV);
+	asha_stream_fail(asha_dev, -ENODEV);
+}
+
 static void asha_adapter_remove(struct btd_profile *profile,
 						struct btd_adapter *adapter)
 {
@@ -273,6 +513,7 @@ static void asha_adapter_remove(struct btd_profile *profile,
 	if (!data)
 		return;
 
+	queue_foreach(asha_streams, asha_adapter_cancel_stream, adapter);
 	bt_hci_unref(data->hci);
 	free(data);
 
@@ -297,20 +538,20 @@ static char *make_endpoint_path(struct bt_asha_device *asha_dev)
 	return path;
 }
 
-struct connect_data {
-	struct bt_asha_device *asha_dev;
-	bt_asha_cb_t cb;
-	void *cb_user_data;
-};
-
 static void connect_cb(GIOChannel *io, GError *err, gpointer user_data)
 {
 	struct connect_data *conn_data = user_data;
 	struct bt_asha_device *asha_dev = conn_data->asha_dev;
 	GError *gerr = NULL;
 
+	if (!asha_dev)
+		return;
+
+	asha_dev->connect_data = NULL;
+
 	if (err) {
 		error("%s", err->message);
+		asha_start_wait_fail(asha_dev, -EIO);
 		return;
 	}
 
@@ -325,12 +566,8 @@ static void connect_cb(GIOChannel *io, GError *err, gpointer user_data)
 		g_error_free(gerr);
 	}
 
-	asha_dev->io = io;
-
-	asha_send_interval(asha_dev);
-
-	bt_asha_start(asha_dev->asha, conn_data->cb,
-					conn_data->cb_user_data);
+	asha_dev->start_wait->connected = true;
+	asha_start_wait_try(asha_dev);
 }
 
 static int asha_connect_socket(struct bt_asha_device *asha_dev,
@@ -340,21 +577,22 @@ static int asha_connect_socket(struct bt_asha_device *asha_dev,
 	const bdaddr_t *src_addr;
 	struct connect_data *conn_data;
 
-	if (asha_dev->asha->state != ASHA_STOPPED) {
+	if (asha_dev->io || asha_dev->start_wait ||
+					asha_dev->asha->state != ASHA_STOPPED) {
 		error("ASHA device connect failed. Bad state %d",
 							asha_dev->asha->state);
-		return 0;
+		return -EBUSY;
 	}
 
 	conn_data = g_new0(struct connect_data, 1);
 	conn_data->asha_dev = asha_dev;
-	conn_data->cb = cb;
-	conn_data->cb_user_data = user_data;
+	asha_dev->connect_data = conn_data;
+	asha_start_wait_begin(asha_dev, cb, user_data);
 
 	src_addr = btd_adapter_get_address(
 			device_get_adapter(asha_dev->device));
 
-	if (!bt_io_connect(connect_cb, conn_data,
+	asha_dev->io = bt_io_connect(connect_cb, conn_data,
 				g_free, &gerr,
 				BT_IO_OPT_MODE, BT_IO_MODE_LE_FLOWCTL,
 				BT_IO_OPT_SOURCE_TYPE, BDADDR_LE_PUBLIC,
@@ -365,9 +603,13 @@ static int asha_connect_socket(struct bt_asha_device *asha_dev,
 				BT_IO_OPT_PSM, asha_dev->asha->psm,
 				BT_IO_OPT_OMTU, ASHA_MIN_MTU,
 				BT_IO_OPT_IMTU, ASHA_CONNECTION_MTU,
-				BT_IO_OPT_INVALID)) {
+				BT_IO_OPT_INVALID);
+	if (!asha_dev->io) {
 		error("Could not open L2CAP CoC socket: %s", gerr->message);
 		g_error_free(gerr);
+		asha_dev->connect_data = NULL;
+		g_free(conn_data);
+		asha_start_wait_cancel(asha_dev);
 		goto error;
 	}
 
@@ -393,13 +635,20 @@ unsigned int bt_asha_device_start(struct bt_asha_device *asha_dev,
 
 	if (ret < 0)
 		return 0;
-	else
-		return (++asha_dev->resume_id);
+
+	if (!++asha_dev->resume_id)
+		++asha_dev->resume_id;
+
+	return asha_dev->resume_id;
 }
 
 unsigned int bt_asha_device_stop(struct bt_asha_device *asha_dev)
 {
+	asha_stream_close_cancel(asha_dev);
+	asha_start_wait_cancel(asha_dev);
+	asha_cancel_connect(asha_dev);
 	bt_asha_stop(asha_dev->asha);
+	bt_asha_state_reset(asha_dev->asha);
 
 	if (asha_dev->io) {
 		g_io_channel_shutdown(asha_dev->io, TRUE, NULL);
@@ -412,6 +661,10 @@ unsigned int bt_asha_device_stop(struct bt_asha_device *asha_dev)
 
 void bt_asha_device_state_reset(struct bt_asha_device *asha_dev)
 {
+	asha_stream_close_cancel(asha_dev);
+	asha_start_wait_cancel(asha_dev);
+	asha_cancel_connect(asha_dev);
+
 	if (asha_dev->io) {
 		g_io_channel_shutdown(asha_dev->io, TRUE, NULL);
 		g_io_channel_unref(asha_dev->io);
@@ -419,8 +672,6 @@ void bt_asha_device_state_reset(struct bt_asha_device *asha_dev)
 	};
 
 	bt_asha_state_reset(asha_dev->asha);
-
-	asha_dev->resume_id = 0;
 }
 
 unsigned int bt_asha_device_device_get_resume_id(
@@ -432,9 +683,19 @@ unsigned int bt_asha_device_device_get_resume_id(
 enum bt_asha_state_t bt_asha_device_get_state(
 					struct bt_asha_device *asha_dev)
 {
+	if (asha_dev->start_wait)
+		return ASHA_STARTING;
+
 	return asha_dev->asha->state;
 }
 
+void bt_asha_device_set_stream_closed_cb(struct bt_asha_device *asha_dev,
+					bt_asha_cb_t callback, void *user_data)
+{
+	asha_dev->closed_cb = callback;
+	asha_dev->closed_data = user_data;
+}
+
 uint16_t bt_asha_device_get_render_delay(struct bt_asha_device *asha_dev)
 {
 	return asha_dev->asha->render_delay;
@@ -598,6 +859,7 @@ static void asha_source_device_remove(struct btd_service *service)
 		return;
 	}
 
+	bt_asha_device_state_reset(asha_dev);
 	queue_remove(asha_streams, asha_dev);
 
 	if (queue_isempty(asha_streams)) {
@@ -723,6 +985,7 @@ static int asha_source_disconnect(struct btd_service *service)
 	}
 
 	asha_source_endpoint_unregister(asha_dev);
+	bt_asha_device_state_reset(asha_dev);
 	bt_asha_reset(asha_dev->asha);
 
 	btd_service_disconnecting_complete(service, 0);
diff --git a/profiles/audio/asha.h b/profiles/audio/asha.h
index 9ffd9d307d..6c3550dc81 100644
--- a/profiles/audio/asha.h
+++ b/profiles/audio/asha.h
@@ -20,6 +20,8 @@ struct bt_asha_device;
 unsigned int bt_asha_device_start(struct bt_asha_device *asha_dev,
 					bt_asha_cb_t cb, void *user_data);
 unsigned int bt_asha_device_stop(struct bt_asha_device *asha_dev);
+void bt_asha_device_set_stream_closed_cb(struct bt_asha_device *asha_dev,
+					bt_asha_cb_t callback, void *user_data);
 
 void bt_asha_device_state_reset(struct bt_asha_device *asha_dev);
 unsigned int bt_asha_device_device_get_resume_id(
diff --git a/profiles/audio/transport.c b/profiles/audio/transport.c
index e001f73151..794303464a 100644
--- a/profiles/audio/transport.c
+++ b/profiles/audio/transport.c
@@ -2658,11 +2658,29 @@ static int transport_asha_set_volume(struct media_transport *transport,
 	return bt_asha_device_set_volume(asha_dev, scaled_volume) ? 0 : -EIO;
 }
 
+static void asha_transport_closed(int status, void *user_data)
+{
+	struct media_transport *transport = user_data;
+
+	if (transport->owner)
+		media_transport_remove_owner(transport);
+	else
+		transport_set_state(transport, TRANSPORT_STATE_IDLE);
+}
+
 static void *transport_asha_init(struct media_transport *transport, void *data)
 {
+	bt_asha_device_set_stream_closed_cb(data, asha_transport_closed,
+								transport);
+
 	/* We just store the struct asha_device on the transport */
 	return data;
 }
+
+static void transport_asha_destroy(void *data)
+{
+	bt_asha_device_set_stream_closed_cb(data, NULL, NULL);
+}
 #endif /* HAVE_ASHA */
 
 #ifdef HAVE_HFP
@@ -2857,7 +2875,7 @@ static void transport_hfp_cancel(struct media_transport *transport, guint id)
 			transport_asha_resume, transport_asha_suspend, \
 			transport_asha_cancel, NULL, NULL, \
 			transport_asha_get_volume, transport_asha_set_volume, \
-			NULL, NULL, NULL, NULL)
+			NULL, NULL, transport_asha_destroy, NULL)
 
 #define HFP_OPS(_uuid) \
 	TRANSPORT_OPS(_uuid, transport_hfp_properties, NULL, NULL, \
diff --git a/unit/test-asha-conn.c b/unit/test-asha-conn.c
index 4b0354c67e..e7e669867d 100644
--- a/unit/test-asha-conn.c
+++ b/unit/test-asha-conn.c
@@ -9,12 +9,14 @@
 #define bt_att_get_fd test_att_fd
 #define bt_asha_set_connection_interval test_set_interval
 #define bt_asha_start test_start
+#define bt_asha_stop test_stop
 #define bt_io_get test_io_get
 #include "profiles/audio/asha.c"
 #undef bt_gatt_client_get_att
 #undef bt_att_get_fd
 #undef bt_asha_set_connection_interval
 #undef bt_asha_start
+#undef bt_asha_stop
 #undef bt_io_get
 #include "bluetooth/hci.h"
 #include "src/shared/tester.h"
@@ -37,19 +39,23 @@ const char *device_get_path(const struct btd_device *device)
 
 const bdaddr_t *device_get_address(struct btd_device *device)
 {
-	assert(false);
+	static const bdaddr_t address;
+
+	return &address;
 }
 
 const bdaddr_t *btd_adapter_get_address(struct btd_adapter *adapter)
 {
-	assert(false);
+	static const bdaddr_t address;
+
+	return &address;
 }
 
 void btd_device_set_conn_param(struct btd_device *device,
 				uint16_t min_interval, uint16_t max_interval,
 					uint16_t latency, uint16_t timeout)
 {
-	assert(false);
+	assert(min_interval == 16 && max_interval == 16);
 }
 
 struct gatt_db *btd_device_get_gatt_db(struct btd_device *device)
@@ -131,17 +137,45 @@ const char *media_transport_get_path(struct media_transport *transport)
 	assert(false);
 }
 
+static BtIOConnect pending_connect;
+static GDestroyNotify pending_destroy;
+static void *pending_data;
+static GIOChannel *pending_io;
+static int connect_peer;
+static bool connect_error;
+
 GIOChannel *bt_io_connect(BtIOConnect connect, gpointer user_data,
 				GDestroyNotify destroy, GError **gerr,
 							BtIOOption opt1, ...)
 {
-	assert(false);
+	int fds[2];
+
+	if (connect_error) {
+		g_set_error_literal(gerr, g_quark_from_static_string("test"),
+							EIO, "connect failed");
+		return NULL;
+	}
+
+	assert(!pending_io);
+	assert(!socketpair(AF_UNIX, SOCK_STREAM, 0, fds));
+	pending_io = g_io_channel_unix_new(fds[0]);
+	g_io_channel_set_close_on_unref(pending_io, TRUE);
+	pending_connect = connect;
+	pending_data = user_data;
+	pending_destroy = destroy;
+	connect_peer = fds[1];
+	return g_io_channel_ref(pending_io);
 }
 
 static int peer_fd[2];
 static int observer_fd[2];
 static int pending_peer;
 static unsigned int writes;
+static unsigned int starts;
+static bool start_error;
+static bool interval_error;
+static bt_asha_cb_t start_callback;
+static void *start_data;
 static uint16_t last_interval;
 static struct bt_asha *last_asha;
 static bool socket_ready = true;
@@ -241,14 +275,27 @@ bool test_set_interval(struct bt_asha *asha,
 	writes++;
 	last_asha = asha;
 	last_interval = interval;
-	return true;
+	return !interval_error;
 }
 
 unsigned int test_start(struct bt_asha *asha, bt_asha_cb_t cb,
 							void *user_data)
 {
-	assert(writes == 1 && last_asha == asha && last_interval == 24);
+	assert(writes && last_asha == asha && last_interval == 16);
+	starts++;
+
+	if (start_error)
+		return -1;
+
 	asha->state = ASHA_STARTING;
+	start_callback = cb;
+	start_data = user_data;
+	return 0;
+}
+
+unsigned int test_stop(struct bt_asha *asha)
+{
+	bt_asha_state_reset(asha);
 	return 0;
 }
 
@@ -304,14 +351,18 @@ static void test_connection(const void *user_data)
 	queue_push_tail(asha_streams, &dev);
 	queue_push_tail(asha_streams, &dev2);
 	writes = 0;
-	asha_send_interval(&dev);
+	asha_send_interval(&dev, NULL);
 	assert(writes == 0);
 
 	/* Cache the initial event before GATT discovery/CoC. */
 	event(0, BT_HCI_EVT_LE_CONN_COMPLETE, &complete, sizeof(complete));
 	assert(writes == 0);
+	asha_start_wait_begin(&dev, NULL, NULL);
+	dev.io = io;
 	connect_cb(io, NULL, &conn);
 	assert(writes == 1 && last_interval == 24);
+	assert(dev.start_wait && starts == 0);
+	asha_start_wait_cancel(&dev);
 
 	/* Identical handles on different controllers are separate links. */
 	event(1, BT_HCI_EVT_LE_ENHANCED_CONN_COMPLETE, &enhanced,
@@ -333,12 +384,12 @@ static void test_connection(const void *user_data)
 	update.handle = cpu_to_le16(ASHA_NUM_HANDLES);
 	event(0, BT_HCI_EVT_LE_CONN_UPDATE_COMPLETE, &update, sizeof(update));
 	assert(writes == 3);
-	asha_send_interval(&dev);
+	asha_send_interval(&dev, NULL);
 	assert(writes == 4 && last_interval == 40);
 
 	/* Closed ATT sockets cannot be associated with a cached handle. */
 	socket_ready = false;
-	asha_send_interval(&dev);
+	asha_send_interval(&dev, NULL);
 	assert(writes == 4);
 	socket_ready = true;
 
@@ -348,15 +399,15 @@ static void test_connection(const void *user_data)
 	while (g_main_context_iteration(NULL, FALSE))
 		;
 
-	asha_send_interval(&dev);
+	asha_send_interval(&dev, NULL);
 	assert(writes == 4);
-	asha_send_interval(&dev2);
+	asha_send_interval(&dev2, NULL);
 	assert(writes == 5 && last_interval == 40);
 
 	/* Reused handle gets the new connection's value, never the old one. */
 	complete.interval = cpu_to_le16(16);
 	event(0, BT_HCI_EVT_LE_CONN_COMPLETE, &complete, sizeof(complete));
-	asha_send_interval(&dev);
+	asha_send_interval(&dev, NULL);
 	assert(writes == 6 && last_interval == 16);
 
 	adapter = queue_find(asha_adapters, match_adapter, (void *) 1);
@@ -405,7 +456,7 @@ static void test_observer_lifecycle(const void *user_data)
 	asha_streams = queue_new();
 	queue_push_tail(asha_streams, &dev);
 	event(0, BT_HCI_EVT_LE_CONN_COMPLETE, &complete, sizeof(complete));
-	asha_send_interval(&dev);
+	asha_send_interval(&dev, NULL);
 	assert(writes == 1 && last_interval == 24);
 
 	/* Failed/truncated events cannot replace a successful interval. */
@@ -414,16 +465,16 @@ static void test_observer_lifecycle(const void *user_data)
 	event(0, BT_HCI_EVT_LE_CONN_COMPLETE, &complete, sizeof(complete));
 	complete.status = 0;
 	event(0, BT_HCI_EVT_LE_CONN_COMPLETE, &complete, sizeof(complete) - 1);
-	asha_send_interval(&dev);
+	asha_send_interval(&dev, NULL);
 	assert(writes == 2 && last_interval == 24);
 
 	conninfo_length--;
-	asha_send_interval(&dev);
+	asha_send_interval(&dev, NULL);
 	conninfo_length += 2;
-	asha_send_interval(&dev);
+	asha_send_interval(&dev, NULL);
 	conninfo_length--;
 	att_fd = -1;
-	asha_send_interval(&dev);
+	asha_send_interval(&dev, NULL);
 	att_fd = 100;
 	assert(writes == 2);
 
@@ -436,7 +487,7 @@ static void test_observer_lifecycle(const void *user_data)
 	assert(!asha_adapter_probe(NULL, (void *) 1));
 
 	/* After restart, existing links stay unknown until a new update. */
-	asha_send_interval(&dev);
+	asha_send_interval(&dev, NULL);
 	assert(writes == 2);
 	event(0, BT_HCI_EVT_LE_CONN_UPDATE_COMPLETE, &update, sizeof(update));
 	assert(writes == 3 && last_interval == 32);
@@ -459,11 +510,527 @@ static void test_observer_lifecycle(const void *user_data)
 	tester_test_passed();
 }
 
+struct gate_result {
+	struct bt_asha_device *device;
+	unsigned int calls;
+	int error;
+	bool remove;
+};
+
+static void gate_callback(int status, void *user_data)
+{
+	struct gate_result *result = user_data;
+
+	assert(!result->device->start_wait);
+
+	if (status) {
+		assert(!result->device->io);
+		assert(bt_asha_device_get_state(result->device) ==
+							ASHA_STOPPED);
+	} else {
+		assert(result->device->io);
+		assert(bt_asha_device_get_state(result->device) ==
+							ASHA_STARTED);
+	}
+	result->calls++;
+	result->error = status;
+
+	if (result->remove) {
+		queue_remove(asha_streams, result->device);
+		bt_asha_device_state_reset(result->device);
+		g_free(result->device);
+		result->device = NULL;
+	}
+}
+
+static void finish_connect(bool failed)
+{
+	GError *err = NULL;
+
+	if (failed)
+		g_set_error_literal(&err, g_quark_from_static_string("test"),
+							EIO, "connect failed");
+
+	pending_connect(pending_io, err, pending_data);
+	pending_destroy(pending_data);
+	g_io_channel_unref(pending_io);
+	pending_io = NULL;
+	g_clear_error(&err);
+	close(connect_peer);
+}
+
+static void test_pending_connect(const void *user_data)
+{
+	unsigned int scenario;
+
+	for (scenario = 0; scenario < 7; scenario++) {
+		struct bt_asha asha = { .client = (void *) 1 };
+		struct bt_asha_device *dev = g_new0(struct bt_asha_device, 1);
+		struct gate_result result = { .device = dev };
+		GSource *timer;
+		unsigned int id;
+		int fds[2];
+
+		dev->asha = &asha;
+		dev->device = (void *) 1;
+		starts = writes = 0;
+		connect_error = scenario == 2;
+		id = bt_asha_device_start(dev, gate_callback, &result);
+
+		if (connect_error) {
+			assert(!id && !dev->io && !dev->start_wait);
+			connect_error = false;
+			g_free(dev);
+			continue;
+		}
+
+		assert(id && bt_asha_device_get_state(dev) == ASHA_STARTING);
+		assert(!bt_asha_device_start(dev, gate_callback, &result));
+
+		switch (scenario) {
+		case 0:
+			bt_asha_device_stop(dev);
+			assert(bt_asha_device_get_state(dev) == ASHA_STOPPED);
+			g_free(dev);
+			result.device = NULL;
+			/* Dispatch the old watch after fd reuse. */
+			assert(!socketpair(AF_UNIX, SOCK_STREAM, 0, fds));
+			finish_connect(false);
+			close(fds[0]);
+			close(fds[1]);
+			break;
+		case 1:
+			finish_connect(true);
+			while (g_main_context_iteration(NULL, FALSE))
+				;
+			assert(result.calls == 1 && result.error == -EIO);
+			break;
+		case 2:
+			assert(false);
+			break;
+		case 3:
+			timer = g_main_context_find_source_by_id(NULL,
+						dev->start_wait->source_id);
+			g_source_set_ready_time(timer, 0);
+			while (g_main_context_iteration(NULL, FALSE))
+				;
+			finish_connect(false);
+			assert(result.calls == 1 && result.error == -ETIMEDOUT);
+			break;
+		case 4:
+			bt_asha_device_state_reset(dev);
+			finish_connect(false);
+			assert(bt_asha_device_start(dev, gate_callback, &result)
+									!= id);
+			bt_asha_device_stop(dev);
+			finish_connect(false);
+			break;
+		case 5:
+			assert(!asha_adapter_probe(NULL, (void *) 1));
+			asha_streams = queue_new();
+			queue_push_tail(asha_streams, dev);
+			asha_adapter_remove(NULL, (void *) 1);
+			while (g_main_context_iteration(NULL, FALSE))
+				;
+			finish_connect(false);
+			assert(result.calls == 1 && result.error == -ENODEV);
+			queue_destroy(asha_streams, NULL);
+			asha_streams = NULL;
+			close(peer_fd[0]);
+			break;
+		case 6:
+			bt_asha_device_state_reset(dev);
+			finish_connect(false);
+			break;
+		}
+
+		assert(!starts);
+
+		if (result.device) {
+			bt_asha_device_state_reset(dev);
+			g_free(dev);
+		}
+	}
+
+	tester_test_passed();
+}
+
+static void test_start_gate(const void *user_data)
+{
+	unsigned int scenario;
+
+	for (scenario = 0; scenario < 21; scenario++) {
+		struct bt_asha asha = { .client = (void *) 1 };
+		struct bt_asha_device *dev = g_new0(struct bt_asha_device, 1);
+		struct gate_result result = { .device = dev };
+		struct connect_data conn = { .asha_dev = dev };
+		struct bt_hci_evt_le_conn_complete complete = {
+			.handle = cpu_to_le16(16),
+			.interval = cpu_to_le16(24),
+		};
+		struct bt_hci_evt_le_conn_update_complete update = {
+			.handle = cpu_to_le16(16), .interval = cpu_to_le16(16),
+		};
+		struct bt_hci_evt_disconnect_complete disconnect = {
+			.handle = cpu_to_le16(16),
+		};
+		struct asha_adapter *adapter;
+		int fds[2];
+		unsigned int source;
+		GSource *timer;
+
+		dev->asha = &asha;
+		dev->device = (void *) 1;
+		starts = writes = 0;
+		start_error = false;
+		interval_error = scenario == 14;
+		assert(!pipe(fds));
+		assert(!asha_adapter_probe(NULL, (void *) 1));
+		adapter = queue_find(asha_adapters, match_adapter, (void *) 1);
+		asha_streams = queue_new();
+		queue_push_tail(asha_streams, dev);
+
+		if (scenario == 0 || scenario == 14 || scenario >= 15)
+			complete.interval = cpu_to_le16(16);
+
+		/* Scenario 2 starts with no observed connection interval. */
+		if (scenario != 2)
+			event(0, BT_HCI_EVT_LE_CONN_COMPLETE, &complete,
+							sizeof(complete));
+
+		asha_start_wait_begin(dev, gate_callback, &result);
+		dev->io = g_io_channel_unix_new(fds[0]);
+		g_io_channel_set_close_on_unref(dev->io, TRUE);
+		connect_cb(dev->io, NULL, &conn);
+
+		if (scenario == 0 || scenario >= 15) {
+			assert(starts == 1 && dev->start_wait->sent);
+		} else {
+			assert(starts == 0 && dev->start_wait);
+			assert(bt_asha_device_get_state(dev) == ASHA_STARTING);
+		}
+
+		switch (scenario) {
+		case 0:
+			asha.state = ASHA_STARTED;
+			start_callback(0, start_data);
+			assert(!dev->start_wait && result.calls == 1);
+			break;
+		case 1:
+		case 2:
+			event(0, BT_HCI_EVT_LE_CONN_UPDATE_COMPLETE, &update,
+								sizeof(update));
+			assert(starts == 1 && dev->start_wait->sent);
+			break;
+		case 3:
+			update.status = 1;
+			event(0, BT_HCI_EVT_LE_CONN_UPDATE_COMPLETE, &update,
+								sizeof(update));
+			assert(result.calls == 1 && result.error == -EIO);
+			break;
+		case 4:
+			source = dev->start_wait->source_id;
+			timer = g_main_context_find_source_by_id(NULL, source);
+			assert(timer);
+			g_source_set_ready_time(timer, 0);
+			while (g_main_context_iteration(NULL, FALSE))
+				;
+			assert(!g_main_context_find_source_by_id(NULL, source));
+			assert(result.calls == 1 && result.error == -ETIMEDOUT);
+			break;
+		case 5:
+			source = dev->start_wait->source_id;
+			bt_asha_device_stop(dev);
+			assert(!g_main_context_find_source_by_id(NULL, source));
+			event(0, BT_HCI_EVT_LE_CONN_UPDATE_COMPLETE, &update,
+								sizeof(update));
+			assert(starts == 0 && result.calls == 0);
+			break;
+		case 6:
+			bt_asha_device_state_reset(dev);
+			break;
+		case 7:
+			asha_adapter_remove(NULL, (void *) 1);
+			while (g_main_context_iteration(NULL, FALSE))
+				;
+			assert(result.calls == 1 && result.error == -ENODEV);
+			break;
+		case 8:
+			asha_disconn_complete(&disconnect, sizeof(disconnect),
+								adapter);
+			while (g_main_context_iteration(NULL, FALSE))
+				;
+			assert(result.calls == 1 && result.error == -ENOTCONN);
+			break;
+		case 9:
+			start_error = true;
+			event(0, BT_HCI_EVT_LE_CONN_UPDATE_COMPLETE, &update,
+								sizeof(update));
+			assert(starts == 1 && result.calls == 1);
+			assert(result.error == -EIO);
+			break;
+		case 10:
+			result.remove = true;
+			update.status = 1;
+			event(0, BT_HCI_EVT_LE_CONN_UPDATE_COMPLETE, &update,
+								sizeof(update));
+			assert(!result.device && result.calls == 1);
+			break;
+		case 11:
+			update.status = 1;
+			asha_conn_update(&update, sizeof(update), adapter);
+			assert(dev->start_wait->error == -EIO);
+			bt_asha_device_stop(dev);
+			while (g_main_context_iteration(NULL, FALSE))
+				;
+			assert(!result.calls && !starts);
+			break;
+		case 12:
+			update.handle = cpu_to_le16(17);
+			event(0, BT_HCI_EVT_LE_CONN_UPDATE_COMPLETE, &update,
+								sizeof(update));
+			update.handle = cpu_to_le16(16);
+			event(0, BT_HCI_EVT_LE_CONN_UPDATE_COMPLETE, &update,
+							sizeof(update) - 1);
+			assert(starts == 0 && dev->start_wait);
+			event(0, BT_HCI_EVT_LE_CONN_UPDATE_COMPLETE, &update,
+								sizeof(update));
+			assert(starts == 1 && dev->start_wait->sent);
+			break;
+		case 13:
+			interval_error = true;
+			event(0, BT_HCI_EVT_LE_CONN_UPDATE_COMPLETE, &update,
+								sizeof(update));
+			assert(starts == 0 && result.calls == 1);
+			assert(result.error == -EIO && !dev->start_wait);
+			interval_error = false;
+			break;
+		case 14:
+			while (g_main_context_iteration(NULL, FALSE))
+				;
+			assert(!starts && result.calls == 1);
+			assert(result.error == -EIO && !dev->start_wait);
+			interval_error = false;
+			break;
+		case 15:
+			/* AudioStatusPoint never arrives after START. */
+			source = dev->start_wait->source_id;
+			timer = g_main_context_find_source_by_id(NULL, source);
+			g_source_set_ready_time(timer, 0);
+			while (g_main_context_iteration(NULL, FALSE))
+				;
+			assert(result.calls == 1 && result.error == -ETIMEDOUT);
+			break;
+		case 16:
+			/* An update races the START acknowledgment. */
+			update.interval = cpu_to_le16(24);
+			event(0, BT_HCI_EVT_LE_CONN_UPDATE_COMPLETE, &update,
+								sizeof(update));
+			assert(result.calls == 1 && result.error == -ERANGE);
+			break;
+		case 17:
+			start_callback(-1, start_data);
+			while (g_main_context_iteration(NULL, FALSE))
+				;
+			assert(result.calls == 1 && result.error == -EIO);
+			break;
+		case 18:
+			bt_asha_device_stop(dev);
+			assert(asha.state == ASHA_STOPPED && !dev->start_wait);
+			break;
+		case 19:
+			result.remove = true;
+			asha.state = ASHA_STARTED;
+			start_callback(0, start_data);
+			assert(!result.device && result.calls == 1);
+			break;
+		case 20:
+			/* Duplicate completion must not issue another START. */
+			event(0, BT_HCI_EVT_LE_CONN_UPDATE_COMPLETE, &update,
+								sizeof(update));
+			assert(starts == 1 && !result.calls);
+			break;
+		}
+
+		if (result.device) {
+			bt_asha_device_state_reset(dev);
+			queue_remove(asha_streams, dev);
+			g_free(dev);
+		}
+
+		if (scenario != 7)
+			asha_adapter_remove(NULL, (void *) 1);
+
+		queue_destroy(asha_streams, NULL);
+		asha_streams = NULL;
+		close(peer_fd[0]);
+		close(fds[1]);
+	}
+
+	tester_test_passed();
+}
+
+static void test_acquired_interval(const void *user_data)
+{
+	unsigned int scenario;
+
+	for (scenario = 0; scenario < 12; scenario++) {
+		struct bt_asha asha = {
+			.client = (void *) 1, .state = ASHA_STARTED,
+		};
+		struct bt_asha_device *dev = g_new0(struct bt_asha_device, 1);
+		struct gate_result result = { .device = dev };
+		struct bt_hci_evt_le_conn_complete complete = {
+			.handle = cpu_to_le16(16), .interval = cpu_to_le16(16),
+		};
+		struct bt_hci_evt_le_conn_update_complete update = {
+			.handle = cpu_to_le16(16), .interval = cpu_to_le16(24),
+		};
+		struct bt_hci_evt_disconnect_complete disconnect = {
+			.handle = cpu_to_le16(16),
+		};
+		struct asha_adapter *adapter;
+		unsigned int source;
+		int fds[2];
+
+		dev->asha = &asha;
+		dev->device = (void *) 1;
+		assert(!pipe(fds));
+		dev->io = g_io_channel_unix_new(fds[0]);
+		g_io_channel_set_close_on_unref(dev->io, TRUE);
+		bt_asha_device_set_stream_closed_cb(dev, gate_callback,
+								&result);
+		assert(!asha_adapter_probe(NULL, (void *) 1));
+		adapter = queue_find(asha_adapters, match_adapter, (void *) 1);
+		asha_streams = queue_new();
+		queue_push_tail(asha_streams, dev);
+		writes = starts = 0;
+		event(0, BT_HCI_EVT_LE_CONN_COMPLETE, &complete,
+							 sizeof(complete));
+
+		switch (scenario) {
+		case 0:
+			event(0, BT_HCI_EVT_LE_CONN_UPDATE_COMPLETE, &update,
+								sizeof(update));
+			assert(result.calls == 1 && result.error == -ERANGE);
+			break;
+		case 1:
+			/* A rejected update leaves the 20 ms link intact. */
+			update.status = 1;
+			event(0, BT_HCI_EVT_LE_CONN_UPDATE_COMPLETE, &update,
+								sizeof(update));
+			assert(!result.calls && dev->io && !writes);
+			assert(asha.state == ASHA_STARTED);
+			break;
+		case 2:
+			update.interval = cpu_to_le16(16);
+			event(0, BT_HCI_EVT_LE_CONN_UPDATE_COMPLETE, &update,
+								sizeof(update));
+			assert(!result.calls && dev->io && writes == 1);
+			break;
+		case 3:
+			asha_conn_update(&update, sizeof(update), adapter);
+			assert(dev->close_id && !result.calls);
+			bt_asha_device_stop(dev);
+			while (g_main_context_iteration(NULL, FALSE))
+				;
+			assert(!result.calls && !dev->close_id && !dev->io);
+			break;
+		case 4:
+			asha_conn_update(&update, sizeof(update), adapter);
+			bt_asha_device_set_stream_closed_cb(dev, NULL, NULL);
+			while (g_main_context_iteration(NULL, FALSE))
+				;
+			assert(!result.calls && !dev->io);
+			break;
+		case 5:
+			result.remove = true;
+			event(0, BT_HCI_EVT_LE_CONN_UPDATE_COMPLETE, &update,
+								sizeof(update));
+			assert(result.calls == 1 && !result.device);
+			break;
+		case 6:
+			asha_adapter_remove(NULL, (void *) 1);
+			while (g_main_context_iteration(NULL, FALSE))
+				;
+			assert(result.calls == 1 && result.error == -ENODEV);
+			break;
+		case 7:
+			asha_disconn_complete(&disconnect, sizeof(disconnect),
+								adapter);
+			while (g_main_context_iteration(NULL, FALSE))
+				;
+			assert(result.calls == 1 && result.error == -ENOTCONN);
+			break;
+		case 8:
+			interval_error = true;
+			update.interval = cpu_to_le16(16);
+			event(0, BT_HCI_EVT_LE_CONN_UPDATE_COMPLETE, &update,
+								sizeof(update));
+			assert(result.calls == 1 && result.error == -EIO);
+			interval_error = false;
+			break;
+		case 9:
+			asha_conn_update(&update, sizeof(update), adapter);
+			source = dev->close_id;
+			asha_conn_update(&update, sizeof(update), adapter);
+			assert(source && dev->close_id == source);
+			while (g_main_context_iteration(NULL, FALSE))
+				;
+			assert(result.calls == 1 && !dev->close_id);
+			break;
+		case 10:
+			update.handle = cpu_to_le16(17);
+			event(0, BT_HCI_EVT_LE_CONN_UPDATE_COMPLETE, &update,
+								sizeof(update));
+			assert(!result.calls && dev->io && !writes);
+			break;
+		case 11:
+			asha_conn_update(&update, sizeof(update), adapter);
+			source = dev->close_id;
+			bt_asha_device_state_reset(dev);
+			assert(!g_main_context_find_source_by_id(NULL, source));
+			queue_remove(asha_streams, dev);
+			g_free(dev);
+			result.device = NULL;
+			while (g_main_context_iteration(NULL, FALSE))
+				;
+			assert(!result.calls);
+			break;
+		}
+
+		assert(!starts);
+
+		if (result.device) {
+			bt_asha_device_state_reset(dev);
+			queue_remove(asha_streams, dev);
+			g_free(dev);
+		}
+
+		queue_destroy(asha_streams, NULL);
+		asha_streams = NULL;
+
+		if (scenario != 6)
+			asha_adapter_remove(NULL, (void *) 1);
+
+		close(peer_fd[0]);
+		close(fds[1]);
+	}
+
+	tester_test_passed();
+}
+
 int main(int argc, char *argv[])
 {
 	tester_init(&argc, &argv);
 	tester_add("ASHA/interval/HCI", NULL, NULL, test_connection, NULL);
 	tester_add("ASHA/interval/observer-lifecycle", NULL, NULL,
 						test_observer_lifecycle, NULL);
+	tester_add("ASHA/interval/start-gate", NULL, NULL,
+							test_start_gate, NULL);
+	tester_add("ASHA/interval/pending-connect", NULL, NULL,
+						test_pending_connect, NULL);
+	tester_add("ASHA/interval/acquired-interval", NULL, NULL,
+						test_acquired_interval, NULL);
 	return tester_run();
 }


^ permalink raw reply related	[flat|nested] 2+ messages in thread

end of thread, other threads:[~2026-10-15  4:56 UTC | newest]

Thread overview: 2+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-10-15  4:55 [RFC PATCH 1/2] shared/asha: Cancel callbacks before resetting the device Kyle Pepper
2026-10-15  4:56 ` [RFC PATCH 2/2] asha: Gate streaming on the observed connection interval Kyle Pepper

This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.