Linux bluetooth development
 help / color / mirror / Atom feed
* [RFC PATCH] asha: Notify connection interval changes
@ 2026-10-13  3:56 Kyle Pepper
  2026-10-13  4:24 ` [RFC] " bluez.test.bot
  0 siblings, 1 reply; 2+ messages in thread
From: Kyle Pepper @ 2026-10-13  3:56 UTC (permalink / raw)
  To: linux-bluetooth; +Cc: Marcel Holtmann, Luiz Augusto von Dentz

Omitting the connection-update STATUS notification causes repeated N8
supervision timeouts even with a negotiated 20 ms interval. Restoring
the notification alone prevents the reported link failures.

Observe successful LE connection and update events per adapter, associate
them with the ATT socket's handle, and announce the observed interval
before START and on subsequent successful updates. Clear the observation
on disconnect so a reused handle cannot inherit another link's interval.
Notify the attached HiSyncId peer as well, matching Android's
connection-update STATUS handling, without truncating the interval byte.

Reuse the verified socket handle when delivering a live update.

Add tests for event delivery, controller isolation, disconnect/reuse,
pre-START ordering, failed updates and peer notification.
Use source-level mocks so the tests remain valid with link-time
optimization enabled.

Fixes: c26389c466a0 ("profiles/audio: Add an ASHA plugin")
Assisted-by: OpenCode:gpt-6.1-sol
---
Submission review notes (not part of the commit message):
Local validation is for base 7428ca2df9356363079db0f48412c20cbdb9471f.
Compatibility with newer upstream tips has not been verified locally.
Removing interval STATUS previously caused N8 timeouts; the tested
combined package restores it. The observer sends completed values, not
requested constants. Raw-HCI access, missed events, startup coordination
and observer/connection lifecycle need review. A link predating observer
creation remains unknown until a later successful update.

 .gitignore                |   4 +
 Makefile.am               |  17 ++
 profiles/audio/asha.c     | 237 +++++++++++++++++++++++++
 src/shared/asha.c         |  28 +++
 src/shared/asha.h         |   1 +
 unit/test-asha-conn.c     | 352 ++++++++++++++++++++++++++++++++++++++
 unit/test-asha-interval.c | 104 +++++++++++
 7 files changed, 743 insertions(+)
 create mode 100644 unit/test-asha-conn.c
 create mode 100644 unit/test-asha-interval.c

diff --git a/.gitignore b/.gitignore
index 06b24735df..3543bd815b 100644
--- a/.gitignore
+++ b/.gitignore
@@ -99,6 +99,8 @@ unit/test-tester
 unit/test-mainloop
 unit/test-ringbuf
 unit/test-queue
+unit/test-asha-conn
+unit/test-asha-interval
 unit/test-util
 unit/test-eir
 unit/test-uuid
@@ -112,6 +114,8 @@ unit/test-lib
 unit/test-mgmt
 unit/test-uhid
 unit/test-hfp
+unit/test-asha-interval
+unit/test-asha-conn
 unit/test-crypto
 unit/test-ecc
 unit/test-hog
diff --git a/Makefile.am b/Makefile.am
index f742458d11..e9fc3727c2 100644
--- a/Makefile.am
+++ b/Makefile.am
@@ -706,6 +706,23 @@ unit_test_avrcp_LDADD = lib/libbluetooth-internal.la \
 
 unit_tests += unit/test-hfp
 
+unit_tests += unit/test-asha-interval
+
+unit_test_asha_interval_SOURCES = unit/test-asha-interval.c
+unit_test_asha_interval_LDADD = src/libshared-glib.la \
+	lib/libbluetooth-internal.la $(GLIB_LIBS)
+
+unit_tests += unit/test-asha-conn
+
+unit_test_asha_conn_SOURCES = unit/test-asha-conn.c
+unit_test_asha_conn_CFLAGS = $(AM_CFLAGS) -DBLUETOOTH_PLUGIN_BUILTIN \
+	-ffunction-sections -fdata-sections
+unit_test_asha_conn_LDADD = src/libshared-glib.la \
+	lib/libbluetooth-internal.la $(GLIB_LIBS) $(DBUS_LIBS)
+unit_test_asha_conn_LDFLAGS = -Wl,--gc-sections \
+	-Wl,--wrap=socket -Wl,--wrap=bind -Wl,--wrap=setsockopt \
+	-Wl,--wrap=getsockopt
+
 unit_test_hfp_SOURCES = unit/test-hfp.c
 unit_test_hfp_LDADD = src/libshared-glib.la $(GLIB_LIBS)
 
diff --git a/profiles/audio/asha.c b/profiles/audio/asha.c
index 8e625fdca3..6596ff3633 100644
--- a/profiles/audio/asha.c
+++ b/profiles/audio/asha.c
@@ -39,6 +39,11 @@
 #include "src/profile.h"
 #include "src/service.h"
 #include "src/shared/util.h"
+#include "src/shared/queue.h"
+#include "src/shared/hci.h"
+#include "src/shared/att.h"
+#include "src/shared/gatt-client.h"
+#include "monitor/bt.h"
 
 #include "profiles/audio/asha.h"
 #include "profiles/audio/media.h"
@@ -51,6 +56,18 @@
 /* The default of 672 does not work */
 #define ASHA_CONNECTION_MTU 512
 
+/* HCI connection handles occupy 0x0000 through 0x0eff. Zero is unknown. */
+#define ASHA_NUM_HANDLES 0x0f00
+
+struct asha_adapter {
+	struct btd_adapter *adapter;
+	struct bt_hci *hci;
+	uint16_t intervals[ASHA_NUM_HANDLES];
+};
+
+static struct queue *asha_adapters;
+static struct queue *asha_streams;
+
 struct bt_asha_device {
 	struct bt_asha *asha;
 	struct btd_device *device;
@@ -62,6 +79,209 @@ struct bt_asha_device {
 	unsigned int resume_id;
 };
 
+static bool match_adapter(const void *data, const void *user_data)
+{
+	const struct asha_adapter *data_adapter = data;
+
+	return data_adapter->adapter == user_data;
+}
+
+static bool asha_get_handle(struct bt_asha_device *asha_dev, uint16_t *handle)
+{
+	struct bt_att *att;
+	struct l2cap_conninfo info;
+	socklen_t len = sizeof(info);
+	int fd;
+
+	if (!asha_dev->asha->client)
+		return false;
+
+	att = bt_gatt_client_get_att(asha_dev->asha->client);
+	fd = bt_att_get_fd(att);
+
+	/* The ATT socket identifies this link even when CoC is not open. */
+	if (fd < 0 || getsockopt(fd, SOL_L2CAP, L2CAP_CONNINFO,
+							&info, &len) < 0) {
+		DBG("Could not read ASHA connection handle");
+		return false;
+	}
+
+	if (len != sizeof(info) || info.hci_handle >= ASHA_NUM_HANDLES)
+		return false;
+
+	*handle = info.hci_handle;
+	return true;
+}
+
+static void 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;
+
+	DBG("ASHA connection h%u interval %u", handle, interval);
+
+	if (!bt_asha_set_connection_interval(asha_dev->asha, interval))
+		DBG("Could not send ASHA connection interval notification");
+}
+
+static void asha_send_interval(struct bt_asha_device *asha_dev)
+{
+	struct asha_adapter *data;
+	uint16_t handle;
+
+	data = queue_find(asha_adapters, match_adapter,
+					device_get_adapter(asha_dev->device));
+	if (!data || !asha_get_handle(asha_dev, &handle))
+		return;
+
+	asha_notify_interval(asha_dev, handle, data->intervals[handle]);
+}
+
+struct interval_update {
+	struct asha_adapter *adapter;
+	uint16_t handle;
+};
+
+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;
+
+	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]);
+}
+
+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;
+	uint16_t handle;
+
+	if (size != sizeof(*ev) || ev->status)
+		return;
+
+	handle = get_le16(&ev->handle);
+
+	if (handle >= ASHA_NUM_HANDLES)
+		return;
+
+	data->intervals[handle] = get_le16(&ev->interval);
+	update.adapter = data;
+	update.handle = handle;
+	queue_foreach(asha_streams, asha_update_stream, &update);
+}
+
+static void asha_conn_complete(const void *param, uint8_t size, void *user_data)
+{
+	const struct bt_hci_evt_le_conn_complete *ev = param;
+	struct asha_adapter *data = user_data;
+	uint16_t handle;
+
+	if (size != sizeof(*ev) || ev->status)
+		return;
+
+	handle = get_le16(&ev->handle);
+
+	if (handle < ASHA_NUM_HANDLES)
+		data->intervals[handle] = get_le16(&ev->interval);
+}
+
+static void asha_enhanced_conn_complete(const void *param, uint8_t size,
+							void *user_data)
+{
+	const struct bt_hci_evt_le_enhanced_conn_complete *ev = param;
+	struct asha_adapter *data = user_data;
+	uint16_t handle;
+
+	if (size != sizeof(*ev) || ev->status)
+		return;
+
+	handle = get_le16(&ev->handle);
+
+	if (handle < ASHA_NUM_HANDLES)
+		data->intervals[handle] = get_le16(&ev->interval);
+}
+
+static void asha_disconn_complete(const void *param, uint8_t size,
+							void *user_data)
+{
+	const struct bt_hci_evt_disconnect_complete *ev = param;
+	struct asha_adapter *data = user_data;
+	uint16_t handle;
+
+	if (size != sizeof(*ev) || ev->status)
+		return;
+
+	handle = get_le16(&ev->handle);
+
+	if (handle < ASHA_NUM_HANDLES)
+		data->intervals[handle] = 0;
+}
+
+static int asha_adapter_probe(struct btd_profile *profile,
+						struct btd_adapter *adapter)
+{
+	struct asha_adapter *data;
+
+	data = new0(struct asha_adapter, 1);
+	data->adapter = adapter;
+	data->hci = bt_hci_new_raw_device(btd_adapter_get_index(adapter));
+
+	if (!data->hci)
+		goto fail;
+
+	if (!bt_hci_register_subevent(data->hci, BT_HCI_EVT_LE_CONN_COMPLETE,
+					asha_conn_complete, data, NULL) ||
+		!bt_hci_register_subevent(data->hci,
+					BT_HCI_EVT_LE_ENHANCED_CONN_COMPLETE,
+				asha_enhanced_conn_complete, data, NULL) ||
+		!bt_hci_register_subevent(data->hci,
+					BT_HCI_EVT_LE_CONN_UPDATE_COMPLETE,
+					asha_conn_update, data, NULL) ||
+		!bt_hci_register(data->hci, BT_HCI_EVT_DISCONNECT_COMPLETE,
+					asha_disconn_complete, data, NULL))
+		goto fail;
+
+	if (!asha_adapters)
+		asha_adapters = queue_new();
+
+	queue_push_tail(asha_adapters, data);
+	return 0;
+
+fail:
+	bt_hci_unref(data->hci);
+	free(data);
+	return -EIO;
+}
+
+static void asha_adapter_remove(struct btd_profile *profile,
+						struct btd_adapter *adapter)
+{
+	struct asha_adapter *data;
+
+	data = queue_remove_if(asha_adapters, match_adapter, adapter);
+
+	if (!data)
+		return;
+
+	bt_hci_unref(data->hci);
+	free(data);
+
+	if (queue_isempty(asha_adapters)) {
+		queue_destroy(asha_adapters, NULL);
+		asha_adapters = NULL;
+	}
+}
+
 static char *make_endpoint_path(struct bt_asha_device *asha_dev)
 {
 	char *path;
@@ -107,6 +327,8 @@ static void connect_cb(GIOChannel *io, GError *err, gpointer user_data)
 
 	asha_dev->io = io;
 
+	asha_send_interval(asha_dev);
+
 	bt_asha_start(asha_dev->asha, conn_data->cb,
 					conn_data->cb_user_data);
 }
@@ -350,6 +572,11 @@ static int asha_source_device_probe(struct btd_service *service)
 	asha_dev->asha = bt_asha_new();
 	asha_dev->io = NULL;
 
+	if (!asha_streams)
+		asha_streams = queue_new();
+
+	queue_push_tail(asha_streams, asha_dev);
+
 	btd_service_set_user_data(service, asha_dev);
 
 	return 0;
@@ -371,6 +598,13 @@ static void asha_source_device_remove(struct btd_service *service)
 		return;
 	}
 
+	queue_remove(asha_streams, asha_dev);
+
+	if (queue_isempty(asha_streams)) {
+		queue_destroy(asha_streams, NULL);
+		asha_streams = NULL;
+	}
+
 	bt_asha_free(asha_dev->asha);
 	g_free(asha_dev);
 }
@@ -503,6 +737,9 @@ static struct btd_profile asha_source_profile = {
 	.remote_uuid	= ASHA_PROFILE_UUID,
 	.experimental	= true,
 
+	.adapter_probe	= asha_adapter_probe,
+	.adapter_remove	= asha_adapter_remove,
+
 	.device_probe	= asha_source_device_probe,
 	.device_remove	= asha_source_device_remove,
 
diff --git a/src/shared/asha.c b/src/shared/asha.c
index 79bb29bf9a..6d0323fb24 100644
--- a/src/shared/asha.c
+++ b/src/shared/asha.c
@@ -244,6 +244,34 @@ static int asha_send_acp_without_response(struct bt_asha *asha,
 	return 0;
 }
 
+bool bt_asha_set_connection_interval(struct bt_asha *asha, uint16_t interval)
+{
+	struct bt_asha_set *set;
+	struct bt_asha *other = NULL;
+	uint8_t cmd[] = { 0x03, 0x02, 0x00 };
+	bool success;
+
+	/* The connection-update STATUS encodes 1.25 ms units in one byte. */
+	if (!asha || !asha->client || !asha->acp_handle ||
+				interval < 0x0006 || interval > UINT8_MAX)
+		return false;
+
+	cmd[2] = interval;
+	success = asha_send_acp_without_response(asha, cmd, sizeof(cmd)) == 0;
+	set = find_asha_set(asha);
+
+	if (set)
+		other = asha->right_side ? set->left : set->right;
+
+	/* Notify an attached peer even if it has not started streaming yet. */
+	if (other && other != asha && other->client && other->acp_handle) {
+		if (asha_send_acp_without_response(other, cmd, sizeof(cmd)) < 0)
+			success = false;
+	}
+
+	return success;
+}
+
 unsigned int bt_asha_start(struct bt_asha *asha, bt_asha_cb_t cb,
 								void *user_data)
 {
diff --git a/src/shared/asha.h b/src/shared/asha.h
index 9e62fcde89..e6b2e7a0d2 100644
--- a/src/shared/asha.h
+++ b/src/shared/asha.h
@@ -66,6 +66,7 @@ unsigned int bt_asha_start(struct bt_asha *asha, bt_asha_cb_t cb,
 unsigned int bt_asha_stop(struct bt_asha *asha);
 
 bool bt_asha_set_volume(struct bt_asha *asha, int8_t volume);
+bool bt_asha_set_connection_interval(struct bt_asha *asha, uint16_t interval);
 
 bool bt_asha_attach(struct bt_asha *asha, struct gatt_db *db,
 		struct bt_gatt_client *client, bt_asha_attach_cb_t probe_cb,
diff --git a/unit/test-asha-conn.c b/unit/test-asha-conn.c
new file mode 100644
index 0000000000..dbf142cc0a
--- /dev/null
+++ b/unit/test-asha-conn.c
@@ -0,0 +1,352 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+#include <assert.h>
+#include <fcntl.h>
+#include <stdarg.h>
+
+/* Internal calls can be resolved before GNU ld --wrap when LTO is enabled. */
+#define bt_gatt_client_get_att test_get_att
+#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_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_io_get
+#include "bluetooth/hci.h"
+#include "src/shared/tester.h"
+
+static int peer_fd[2];
+static int observer_fd[2];
+static int pending_peer;
+static unsigned int writes;
+static uint16_t last_interval;
+static struct bt_asha *last_asha;
+static bool socket_ready = true;
+static bool fail_socket;
+static int att_fd = 100;
+static unsigned int handle_reads;
+static socklen_t conninfo_length = sizeof(struct l2cap_conninfo);
+
+int __wrap_socket(int domain, int type, int protocol)
+{
+	int fds[2];
+
+	assert(domain == PF_BLUETOOTH);
+	assert(protocol == BTPROTO_HCI);
+
+	if (fail_socket)
+		return -1;
+
+	assert(!socketpair(AF_UNIX, SOCK_SEQPACKET | SOCK_NONBLOCK, 0, fds));
+	pending_peer = fds[1];
+	return fds[0];
+}
+
+int __wrap_bind(int fd, const struct sockaddr *addr, socklen_t length)
+{
+	const struct sockaddr_hci *hci = (void *) addr;
+
+	/* Ignore tester's optional logging channel. */
+	if (hci->hci_dev == HCI_DEV_NONE) {
+		close(pending_peer);
+		return -1;
+	}
+
+	assert(hci->hci_dev < 2);
+	peer_fd[hci->hci_dev] = pending_peer;
+	observer_fd[hci->hci_dev] = fd;
+	return 0;
+}
+
+int __real_setsockopt(int fd, int level, int opt, const void *value,
+							socklen_t length);
+
+int __wrap_setsockopt(int fd, int level, int opt, const void *value,
+							socklen_t length)
+{
+	if (level == SOL_HCI)
+		return 0;
+
+	return __real_setsockopt(fd, level, opt, value, length);
+}
+
+int __wrap_getsockopt(int fd, int level, int opt, void *value,
+							socklen_t *length)
+{
+	struct l2cap_conninfo *info = value;
+
+	assert(level == SOL_L2CAP && opt == L2CAP_CONNINFO);
+	handle_reads++;
+
+	if (!socket_ready)
+		return -1;
+
+	assert(*length == sizeof(*info));
+	info->hci_handle = 16;
+	*length = conninfo_length;
+	return 0;
+}
+
+struct bt_att *test_get_att(struct bt_gatt_client *client)
+{
+	return (void *) client;
+}
+
+int test_att_fd(struct bt_att *att)
+{
+	return att_fd;
+}
+
+struct btd_adapter *device_get_adapter(struct btd_device *device)
+{
+	return (void *) device;
+}
+
+uint16_t btd_adapter_get_index(struct btd_adapter *adapter)
+{
+	return PTR_TO_UINT(adapter) - 1;
+}
+
+void btd_debug(uint16_t index, const char *format, ...)
+{
+}
+
+void btd_error(uint16_t index, const char *format, ...)
+{
+}
+
+bool test_set_interval(struct bt_asha *asha,
+							uint16_t interval)
+{
+	writes++;
+	last_asha = asha;
+	last_interval = interval;
+	return true;
+}
+
+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);
+	asha->state = ASHA_STARTING;
+	return 0;
+}
+
+gboolean test_io_get(GIOChannel *io, GError **err, BtIOOption opt1, ...)
+{
+	return TRUE;
+}
+
+static void event(unsigned int adapter, uint8_t subevent, const void *value,
+							uint8_t length)
+{
+	uint8_t packet[260] = { BT_H4_EVT_PKT, BT_HCI_EVT_LE_META_EVENT,
+						length + 1, subevent };
+
+	memcpy(packet + 4, value, length);
+	assert(write(peer_fd[adapter], packet, length + 4) == length + 4);
+
+	while (g_main_context_iteration(NULL, FALSE))
+		;
+}
+
+static void test_connection(const void *user_data)
+{
+	struct bt_asha asha = { .client = (void *) 1, .acp_handle = 0x79 };
+	struct bt_asha other = { .client = (void *) 2, .acp_handle = 0x79 };
+	struct bt_asha_device dev = { .asha = &asha, .device = (void *) 1 };
+	struct bt_asha_device dev2 = { .asha = &other, .device = (void *) 2 };
+	struct connect_data conn = { .asha_dev = &dev };
+	GIOChannel *io;
+	int io_fds[2];
+	struct asha_adapter *adapter;
+	unsigned int before;
+	struct bt_hci_evt_le_conn_complete complete = {
+		.handle = cpu_to_le16(16), .interval = cpu_to_le16(24),
+	};
+	struct bt_hci_evt_le_enhanced_conn_complete enhanced = {
+		.handle = cpu_to_le16(16), .interval = cpu_to_le16(32),
+	};
+	struct bt_hci_evt_le_conn_update_complete update = {
+		.handle = cpu_to_le16(16), .interval = cpu_to_le16(40),
+	};
+	struct bt_hci_evt_disconnect_complete disconnect = {
+		.handle = cpu_to_le16(16),
+	};
+	uint8_t packet[7] = { BT_H4_EVT_PKT, BT_HCI_EVT_DISCONNECT_COMPLETE,
+							sizeof(disconnect) };
+
+	assert(!pipe(io_fds));
+	io = g_io_channel_unix_new(io_fds[0]);
+	assert(!asha_adapter_probe(NULL, (void *) 1));
+	assert(!asha_adapter_probe(NULL, (void *) 2));
+	asha_streams = queue_new();
+	queue_push_tail(asha_streams, &dev);
+	queue_push_tail(asha_streams, &dev2);
+	writes = 0;
+	asha_send_interval(&dev);
+	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);
+	connect_cb(io, NULL, &conn);
+	assert(writes == 1 && last_interval == 24);
+
+	/* Identical handles on different controllers are separate links. */
+	event(1, BT_HCI_EVT_LE_ENHANCED_CONN_COMPLETE, &enhanced,
+							sizeof(enhanced));
+	before = handle_reads;
+	event(0, BT_HCI_EVT_LE_CONN_UPDATE_COMPLETE, &update, sizeof(update));
+	assert(handle_reads == before + 1);
+	assert(writes == 2 && last_asha == &asha && last_interval == 40);
+	other.state = ASHA_STOPPED;
+	event(1, BT_HCI_EVT_LE_CONN_UPDATE_COMPLETE, &update, sizeof(update));
+	assert(writes == 3 && last_asha == &other);
+
+	/* Failed/malformed updates leave the last successful value intact. */
+	update.status = 1;
+	event(0, BT_HCI_EVT_LE_CONN_UPDATE_COMPLETE, &update, sizeof(update));
+	update.status = 0;
+	event(0, BT_HCI_EVT_LE_CONN_UPDATE_COMPLETE, &update,
+							sizeof(update) - 1);
+	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);
+	assert(writes == 4 && last_interval == 40);
+
+	/* Closed ATT sockets cannot be associated with a cached handle. */
+	socket_ready = false;
+	asha_send_interval(&dev);
+	assert(writes == 4);
+	socket_ready = true;
+
+	memcpy(packet + 3, &disconnect, sizeof(disconnect));
+	assert(write(peer_fd[0], packet, sizeof(packet)) == sizeof(packet));
+
+	while (g_main_context_iteration(NULL, FALSE))
+		;
+
+	asha_send_interval(&dev);
+	assert(writes == 4);
+	asha_send_interval(&dev2);
+	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);
+	assert(writes == 6 && last_interval == 16);
+
+	adapter = queue_find(asha_adapters, match_adapter, (void *) 1);
+	assert(adapter->intervals[16] == 16);
+	queue_remove(asha_streams, &dev);
+	update.handle = cpu_to_le16(16);
+	event(0, BT_HCI_EVT_LE_CONN_UPDATE_COMPLETE, &update, sizeof(update));
+	assert(writes == 6);
+	asha_adapter_remove(NULL, (void *) 1);
+	asha_adapter_remove(NULL, (void *) 2);
+	assert(!asha_adapters);
+	queue_destroy(asha_streams, NULL);
+	asha_streams = NULL;
+	close(peer_fd[0]);
+	close(peer_fd[1]);
+	g_io_channel_unref(io);
+	close(io_fds[0]);
+	close(io_fds[1]);
+	dev.io = NULL;
+
+	fail_socket = true;
+	assert(asha_adapter_probe(NULL, (void *) 1) == -EIO);
+	assert(!asha_adapters);
+	fail_socket = false;
+	tester_test_passed();
+}
+
+static void test_observer_lifecycle(const void *user_data)
+{
+	struct bt_asha asha = { .client = (void *) 1, .acp_handle = 0x79 };
+	struct bt_asha_device dev = { .asha = &asha, .device = (void *) 1 };
+	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(32),
+	};
+	uint8_t pending[] = { BT_H4_EVT_PKT, BT_HCI_EVT_LE_META_EVENT,
+				sizeof(update) + 1,
+				BT_HCI_EVT_LE_CONN_UPDATE_COMPLETE };
+	uint8_t packet[sizeof(pending) + sizeof(update)];
+	uint8_t byte;
+
+	writes = 0;
+	assert(!asha_adapter_probe(NULL, (void *) 1));
+	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);
+	assert(writes == 1 && last_interval == 24);
+
+	/* Failed/truncated events cannot replace a successful interval. */
+	complete.status = 1;
+	complete.interval = cpu_to_le16(40);
+	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);
+	assert(writes == 2 && last_interval == 24);
+
+	conninfo_length--;
+	asha_send_interval(&dev);
+	conninfo_length += 2;
+	asha_send_interval(&dev);
+	conninfo_length--;
+	att_fd = -1;
+	asha_send_interval(&dev);
+	att_fd = 100;
+	assert(writes == 2);
+
+	/* The passive observer must never transmit controller commands. */
+	assert(read(peer_fd[0], &byte, 1) == -1);
+	assert(errno == EAGAIN);
+	asha_adapter_remove(NULL, (void *) 1);
+	assert(fcntl(observer_fd[0], F_GETFD) == -1 && errno == EBADF);
+	close(peer_fd[0]);
+	assert(!asha_adapter_probe(NULL, (void *) 1));
+
+	/* After restart, existing links stay unknown until a new update. */
+	asha_send_interval(&dev);
+	assert(writes == 2);
+	event(0, BT_HCI_EVT_LE_CONN_UPDATE_COMPLETE, &update, sizeof(update));
+	assert(writes == 3 && last_interval == 32);
+
+	/* Drop queued callbacks when their adapter and device are removed. */
+	memcpy(packet, pending, sizeof(pending));
+	memcpy(packet + sizeof(pending), &update, sizeof(update));
+	assert(write(peer_fd[0], packet, sizeof(packet)) == sizeof(packet));
+	queue_remove(asha_streams, &dev);
+	asha_adapter_remove(NULL, (void *) 1);
+
+	while (g_main_context_iteration(NULL, FALSE))
+		;
+
+	assert(writes == 3 && !asha_adapters);
+	assert(fcntl(observer_fd[0], F_GETFD) == -1 && errno == EBADF);
+	close(peer_fd[0]);
+	queue_destroy(asha_streams, NULL);
+	asha_streams = NULL;
+	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);
+	return tester_run();
+}
diff --git a/unit/test-asha-interval.c b/unit/test-asha-interval.c
new file mode 100644
index 0000000000..8cb77c07d3
--- /dev/null
+++ b/unit/test-asha-interval.c
@@ -0,0 +1,104 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+#include <assert.h>
+
+/* Redirect source-level calls so LTO cannot bypass the GATT mock. */
+#define bt_gatt_client_write_without_response test_gatt_write
+#include "src/shared/asha.c"
+#undef bt_gatt_client_write_without_response
+#include "src/shared/tester.h"
+
+static unsigned int writes;
+static struct bt_gatt_client *failed_client;
+static uint8_t expected_interval;
+
+void btd_error(uint16_t index, const char *format, ...)
+{
+}
+
+void btd_debug(uint16_t index, const char *format, ...)
+{
+}
+
+unsigned int test_gatt_write(
+	struct bt_gatt_client *client, uint16_t handle, bool signed_write,
+				const uint8_t *value, uint16_t length)
+{
+	assert(client);
+	assert(handle == 0x79);
+	assert(!signed_write);
+	assert(length == 3);
+	assert(value[0] == 0x03 && value[1] == 0x02);
+	assert(value[2] == expected_interval);
+	writes++;
+	return client != failed_client;
+}
+
+static void test_interval(const void *user_data)
+{
+	struct bt_asha left = {
+		.client = (void *) 1, .acp_handle = 0x79, .hisyncid = { 1 },
+	};
+	struct bt_asha right = {
+		.client = (void *) 2, .acp_handle = 0x79, .hisyncid = { 1 },
+		.right_side = true,
+	};
+	struct bt_asha unrelated = {
+		.client = (void *) 3, .acp_handle = 0x79, .hisyncid = { 2 },
+	};
+
+	asha_devices = queue_new();
+	update_asha_set(&left, true);
+	update_asha_set(&right, true);
+	update_asha_set(&unrelated, true);
+	writes = 0;
+	expected_interval = 16;
+
+	/* Both attached sides, including a stopped peer, receive the update. */
+	assert(bt_asha_set_connection_interval(&left, expected_interval));
+	assert(writes == 2);
+	assert(bt_asha_set_connection_interval(&right, expected_interval));
+	assert(writes == 4);
+	assert(bt_asha_set_connection_interval(&unrelated, expected_interval));
+	assert(writes == 5);
+
+	assert(!bt_asha_set_connection_interval(&left, 5));
+	assert(!bt_asha_set_connection_interval(&left, 256));
+	assert(!bt_asha_set_connection_interval(NULL, 16));
+	assert(writes == 5);
+
+	expected_interval = 6;
+	assert(bt_asha_set_connection_interval(&left, expected_interval));
+	assert(writes == 7);
+	expected_interval = 255;
+	assert(bt_asha_set_connection_interval(&right, expected_interval));
+	assert(writes == 9);
+
+	/* Failure on either side does not suppress the other side's attempt. */
+	failed_client = left.client;
+	assert(!bt_asha_set_connection_interval(&left, expected_interval));
+	assert(writes == 11);
+	assert(!bt_asha_set_connection_interval(&right, expected_interval));
+	assert(writes == 13);
+	failed_client = NULL;
+
+	right.client = NULL;
+	assert(bt_asha_set_connection_interval(&left, expected_interval));
+	assert(writes == 14);
+	assert(!bt_asha_set_connection_interval(&right, expected_interval));
+	assert(writes == 14);
+
+	update_asha_set(&right, false);
+	assert(bt_asha_set_connection_interval(&left, expected_interval));
+	assert(writes == 15);
+	update_asha_set(&left, false);
+	update_asha_set(&unrelated, false);
+	assert(!asha_devices);
+	tester_test_passed();
+}
+
+int main(int argc, char *argv[])
+{
+	tester_init(&argc, &argv);
+	tester_add("ASHA/interval/pair", NULL, NULL, test_interval, NULL);
+	return tester_run();
+}

base-commit: 7428ca2df9356363079db0f48412c20cbdb9471f


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

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

Thread overview: 2+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-10-13  3:56 [RFC PATCH] asha: Notify connection interval changes Kyle Pepper
2026-10-13  4:24 ` [RFC] " bluez.test.bot

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox