All of lore.kernel.org
 help / color / mirror / Atom feed
From: Kyle Pepper <cochcoder@proton.me>
To: linux-bluetooth@vger.kernel.org
Cc: Marcel Holtmann <marcel@holtmann.org>,
	Luiz Augusto von Dentz <luiz.dentz@gmail.com>
Subject: [RFC PATCH 2/2] asha: Gate streaming on the observed connection interval
Date: Thu, 15 Oct 2026 04:56:14 +0000	[thread overview]
Message-ID: <20261015045525.66971-2-cochcoder@proton.me> (raw)
In-Reply-To: <20261015045525.66971-1-cochcoder@proton.me>

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();
 }


      reply	other threads:[~2026-10-15  4:56 UTC|newest]

Thread overview: 2+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
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 [this message]

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=20261015045525.66971-2-cochcoder@proton.me \
    --to=cochcoder@proton.me \
    --cc=linux-bluetooth@vger.kernel.org \
    --cc=luiz.dentz@gmail.com \
    --cc=marcel@holtmann.org \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
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.