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 v2] asha: Notify connection interval changes
Date: Tue, 13 Oct 2026 22:26:19 +0000	[thread overview]
Message-ID: <20261013222556.513513-1-cochcoder@proton.me> (raw)
In-Reply-To: <20261013035559.527818-1-cochcoder@proton.me>

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
---
Changes since v1: fix sanitizer-retained daemon references with
fail-fast test stubs. Declare socket wrappers in a test header.
Refreshed to BlueZ 9dc7e40962f11d1d9a5568f8a7633a695288329c.
Strict non-LTO ASan/UBSan build and make check pass; ordinary LTO
build and make check pass. Strict LTO has an unchanged upstream
peripheral/btsensor warning failure, reproduced without this topic.
No new hardware trial or remote functional-lab result is claimed.

Submission review notes (not part of the commit message):
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     | 469 ++++++++++++++++++++++++++++++++++++++
 unit/test-asha-conn.h     |  11 +
 unit/test-asha-interval.c | 104 +++++++++
 8 files changed, 871 insertions(+)
 create mode 100644 unit/test-asha-conn.c
 create mode 100644 unit/test-asha-conn.h
 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..9ba8b93625 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.h
+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..4b0354c67e
--- /dev/null
+++ b/unit/test-asha-conn.c
@@ -0,0 +1,469 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+#include <assert.h>
+#include <fcntl.h>
+#include <stdarg.h>
+#include "test-asha-conn.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"
+
+/* Sanitizers retain the plugin's globals, including unused daemon callbacks. */
+int btd_profile_register(struct btd_profile *profile)
+{
+	assert(false);
+}
+
+void btd_profile_unregister(struct btd_profile *profile)
+{
+	assert(false);
+}
+
+const char *device_get_path(const struct btd_device *device)
+{
+	assert(false);
+}
+
+const bdaddr_t *device_get_address(struct btd_device *device)
+{
+	assert(false);
+}
+
+const bdaddr_t *btd_adapter_get_address(struct btd_adapter *adapter)
+{
+	assert(false);
+}
+
+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);
+}
+
+struct gatt_db *btd_device_get_gatt_db(struct btd_device *device)
+{
+	assert(false);
+}
+
+struct bt_gatt_client *btd_device_get_gatt_client(struct btd_device *device)
+{
+	assert(false);
+}
+
+struct btd_device *btd_service_get_device(const struct btd_service *service)
+{
+	assert(false);
+}
+
+void *btd_service_get_user_data(const struct btd_service *service)
+{
+	assert(false);
+}
+
+void btd_service_set_user_data(struct btd_service *service, void *user_data)
+{
+	assert(false);
+}
+
+void btd_service_connecting_complete(struct btd_service *service, int err)
+{
+	assert(false);
+}
+
+void btd_service_disconnecting_complete(struct btd_service *service, int err)
+{
+	assert(false);
+}
+
+DBusConnection *btd_get_dbus_connection(void)
+{
+	assert(false);
+}
+
+gboolean g_dbus_register_interface(DBusConnection *connection,
+				const char *path, const char *name,
+				const GDBusMethodTable *methods,
+				const GDBusSignalTable *signals,
+				const GDBusPropertyTable *properties,
+				void *user_data, GDBusDestroyFunction destroy)
+{
+	assert(false);
+}
+
+gboolean g_dbus_unregister_interface(DBusConnection *connection,
+					const char *path, const char *name)
+{
+	assert(false);
+}
+
+const struct media_endpoint *media_endpoint_get_asha(void)
+{
+	assert(false);
+}
+
+struct media_transport *media_transport_create(struct btd_device *device,
+				const char *remote_endpoint,
+				uint8_t *configuration, size_t size,
+				void *data, void *stream)
+{
+	assert(false);
+}
+
+void media_transport_destroy(struct media_transport *transport)
+{
+	assert(false);
+}
+
+const char *media_transport_get_path(struct media_transport *transport)
+{
+	assert(false);
+}
+
+GIOChannel *bt_io_connect(BtIOConnect connect, gpointer user_data,
+				GDestroyNotify destroy, GError **gerr,
+							BtIOOption opt1, ...)
+{
+	assert(false);
+}
+
+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 __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-conn.h b/unit/test-asha-conn.h
new file mode 100644
index 0000000000..5e88aed490
--- /dev/null
+++ b/unit/test-asha-conn.h
@@ -0,0 +1,11 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+#include <sys/socket.h>
+
+int __wrap_socket(int domain, int type, int protocol);
+int __wrap_bind(int fd, const struct sockaddr *addr, socklen_t length);
+int __wrap_setsockopt(int fd, int level, int opt, const void *value,
+							socklen_t length);
+int __wrap_getsockopt(int fd, int level, int opt, void *value,
+							socklen_t *length);
+int __real_setsockopt(int fd, int level, int opt, const void *value,
+							socklen_t length);
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: 9dc7e40962f11d1d9a5568f8a7633a695288329c
-- 
2.56.0



  parent reply	other threads:[~2026-10-13 22:26 UTC|newest]

Thread overview: 4+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-10-13  3:56 [RFC PATCH] asha: Notify connection interval changes Kyle Pepper
2026-10-13  4:24 ` [RFC] " bluez.test.bot
2026-10-13 22:26 ` Kyle Pepper [this message]
2026-10-13 23:53   ` [RFC,v2] " bluez.test.bot

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=20261013222556.513513-1-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.