public inbox for linux-bluetooth@vger.kernel.org
 help / color / mirror / Atom feed
* [PATCH BlueZ v2 0/6] Nintendo Switch 2 support
@ 2026-03-08 12:47 Martin BTS
  2026-03-08 12:47 ` [PATCH BlueZ v2 1/6] shared/gatt: make secondary discovery optional Martin BTS
                   ` (6 more replies)
  0 siblings, 7 replies; 11+ messages in thread
From: Martin BTS @ 2026-03-08 12:47 UTC (permalink / raw)
  To: linux-bluetooth; +Cc: hadess, luiz.dentz, vi, Martin BTS

Changes v2:
* Drop original patches 1 and 2 (tollerate ATT timeout)
* New patches 1, 2, 3 introduce a device property skip_secondary,
  that makes secondary device discovery optional
* set_alias() is now public btd_device_set_alias() as suggested
* Fix assigned numbers reference comment
* Link to v1: https://lore.kernel.org/all/20260301152930.221472-1-martinbts@gmx.net/

v2 blurb:

The problem is the secondary service discovery. It will time out on the
Procon2 which is essentially an unrecoverable error and we cannot
connect the controller as a result.

This patchset proposes making the secondary service discovery optional,
so that we can prevent dealing with the Procon2's behaviour.
It now is a property of the device, if it wants/needs a secondary
discovery, or not. This allows device specific plugins to make the
correct configuration in time, bevor a gatt-client is created. The
default is the original behaviour: do a secondary services discovery.

I marked patch 1 as a breaking change, because of how it changes the
gatt-client interface. It appears this gatt-client is only used
internally and never exposed so it technically isn't a breaking change
for BlueZ, but I cannot be sure.

For the record: The Procon2 reports appearance 0x0A82 Portable handheld console

Martin BTS (6):
  shared/gatt: make secondary discovery optional
  device: allow skip secondary discovery
  fixup: propagate new gatt interface through codebase
  device: Rename set_alias to  btd_device_set_alias()
  dbus-common: Add Gaming appearance class (0x2a)
  plugins/switch2: Add Nintendo Switch 2 Controller plugin

 Makefile.plugins         |    3 +
 peripheral/gatt.c        |    5 +-
 plugins/switch2.c        | 1070 ++++++++++++++++++++++++++++++++++++++
 src/dbus-common.c        |    2 +
 src/device.c             |   33 +-
 src/device.h             |    2 +
 src/shared/gatt-client.c |   22 +-
 src/shared/gatt-client.h |    4 +-
 tools/btgatt-client.c    |    5 +-
 unit/test-bap.c          |    3 +-
 unit/test-gatt.c         |    3 +-
 unit/test-gmap.c         |    3 +-
 unit/test-mcp.c          |    3 +-
 unit/test-micp.c         |    3 +-
 unit/test-tmap.c         |    3 +-
 15 files changed, 1133 insertions(+), 31 deletions(-)
 create mode 100644 plugins/switch2.c

-- 
2.47.3


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

* [PATCH BlueZ v2 1/6] shared/gatt: make secondary discovery optional
  2026-03-08 12:47 [PATCH BlueZ v2 0/6] Nintendo Switch 2 support Martin BTS
@ 2026-03-08 12:47 ` Martin BTS
  2026-03-08 14:18   ` Nintendo Switch 2 support bluez.test.bot
  2026-03-09 13:58   ` [PATCH BlueZ v2 1/6] shared/gatt: make secondary discovery optional Luiz Augusto von Dentz
  2026-03-08 12:47 ` [PATCH BlueZ v2 2/6] device: allow skip secondary discovery Martin BTS
                   ` (5 subsequent siblings)
  6 siblings, 2 replies; 11+ messages in thread
From: Martin BTS @ 2026-03-08 12:47 UTC (permalink / raw)
  To: linux-bluetooth; +Cc: hadess, luiz.dentz, vi, Martin BTS

BREAKING CHANGE!

* Remove gatt_client_init from bt_gatt_client_new. Consumers must now
  call gatt_client_init themselves!
* Remove mtu paramter from bt_gatt_client_new
* Rename gatt_client_init to bt_gatt_client_init and make it public
* Introduce a new bt_gatt_client field "skip_secondary", default false
* Introduce public skip_secondary setter
* If true, skip_secondary makes discover_primary_cb goto done
  (instead of discoverying secondary services)
---
 src/shared/gatt-client.c | 22 ++++++++++++++--------
 src/shared/gatt-client.h |  4 +++-
 2 files changed, 17 insertions(+), 9 deletions(-)

diff --git a/src/shared/gatt-client.c b/src/shared/gatt-client.c
index df1541b88..7896ed329 100644
--- a/src/shared/gatt-client.c
+++ b/src/shared/gatt-client.c
@@ -93,6 +93,7 @@ struct bt_gatt_client {
 	struct queue *notify_chrcs;
 	int next_reg_id;
 	unsigned int disc_id, nfy_id, nfy_mult_id, ind_id;
+	bool skip_secondary;
 
 	/*
 	 * Handles of the GATT Service and the Service Changed characteristic
@@ -1344,7 +1345,7 @@ secondary:
 	 * functionality of a device and is referenced from at least one
 	 * primary service on the device.
 	 */
-	if (queue_isempty(op->pending_svcs))
+	if (queue_isempty(op->pending_svcs) || client->skip_secondary)
 		goto done;
 
 	/* Discover secondary services */
@@ -2106,7 +2107,7 @@ done:
 	notify_client_ready(client, success, att_ecode);
 }
 
-static bool gatt_client_init(struct bt_gatt_client *client, uint16_t mtu)
+bool bt_gatt_client_init(struct bt_gatt_client *client, uint16_t mtu)
 {
 	struct discovery_op *op;
 
@@ -2549,7 +2550,6 @@ fail:
 
 struct bt_gatt_client *bt_gatt_client_new(struct gatt_db *db,
 							struct bt_att *att,
-							uint16_t mtu,
 							uint8_t features)
 {
 	struct bt_gatt_client *client;
@@ -2561,11 +2561,6 @@ struct bt_gatt_client *bt_gatt_client_new(struct gatt_db *db,
 	if (!client)
 		return NULL;
 
-	if (!gatt_client_init(client, mtu)) {
-		bt_gatt_client_free(client);
-		return NULL;
-	}
-
 	return bt_gatt_client_ref(client);
 }
 
@@ -2592,6 +2587,17 @@ struct bt_gatt_client *bt_gatt_client_clone(struct bt_gatt_client *client)
 	return bt_gatt_client_ref(clone);
 }
 
+bool bt_gatt_client_set_skip_secondary(struct bt_gatt_client *client,
+								bool skip)
+{
+	if (!client)
+		return false;
+
+	client->skip_secondary = skip;
+
+	return true;
+}
+
 struct bt_gatt_client *bt_gatt_client_ref(struct bt_gatt_client *client)
 {
 	if (!client)
diff --git a/src/shared/gatt-client.h b/src/shared/gatt-client.h
index 63cf99500..e510ad455 100644
--- a/src/shared/gatt-client.h
+++ b/src/shared/gatt-client.h
@@ -18,9 +18,11 @@ struct bt_gatt_client;
 
 struct bt_gatt_client *bt_gatt_client_new(struct gatt_db *db,
 							struct bt_att *att,
-							uint16_t mtu,
 							uint8_t features);
 struct bt_gatt_client *bt_gatt_client_clone(struct bt_gatt_client *client);
+bool bt_gatt_client_init(struct bt_gatt_client *client, uint16_t mtu);
+bool bt_gatt_client_set_skip_secondary(struct bt_gatt_client *client,
+								bool skip);
 
 struct bt_gatt_client *bt_gatt_client_ref(struct bt_gatt_client *client);
 void bt_gatt_client_unref(struct bt_gatt_client *client);
-- 
2.47.3


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

* [PATCH BlueZ v2 2/6] device: allow skip secondary discovery
  2026-03-08 12:47 [PATCH BlueZ v2 0/6] Nintendo Switch 2 support Martin BTS
  2026-03-08 12:47 ` [PATCH BlueZ v2 1/6] shared/gatt: make secondary discovery optional Martin BTS
@ 2026-03-08 12:47 ` Martin BTS
  2026-03-08 12:47 ` [PATCH BlueZ v2 3/6] fixup: propagate new gatt interface through codebase Martin BTS
                   ` (4 subsequent siblings)
  6 siblings, 0 replies; 11+ messages in thread
From: Martin BTS @ 2026-03-08 12:47 UTC (permalink / raw)
  To: linux-bluetooth; +Cc: hadess, luiz.dentz, vi, Martin BTS

* Introduce new btd_device field skip_secondary
* Introduce public skip_secondary setter
* Use new gatt client interface to push device->skip_secondary into
  device->client->skip_secondary after new() but before init()
* Doing secondary service discovery is now a device property that can be
  set by device specific plugins.
---
 src/device.c | 18 ++++++++++++++++--
 src/device.h |  1 +
 2 files changed, 17 insertions(+), 2 deletions(-)

diff --git a/src/device.c b/src/device.c
index 3ea683667..4c2c0b635 100644
--- a/src/device.c
+++ b/src/device.c
@@ -275,6 +275,7 @@ struct btd_device {
 	struct gatt_db *db;			/* GATT db cache */
 	unsigned int db_id;
 	struct bt_gatt_client *client;		/* GATT client instance */
+	bool		skip_secondary;
 	struct bt_gatt_server *server;		/* GATT server instance */
 	unsigned int gatt_ready_id;
 
@@ -5113,6 +5114,11 @@ void device_get_name(struct btd_device *device, char *name, size_t len)
 	}
 }
 
+void btd_device_set_skip_secondary(struct btd_device *device, bool skip)
+{
+	device->skip_secondary = skip;
+}
+
 bool device_name_known(struct btd_device *device)
 {
 	return device->name[0] != '\0';
@@ -6301,10 +6307,18 @@ static void gatt_client_init(struct btd_device *device)
 		bt_att_set_security(device->att, BT_ATT_SECURITY_MEDIUM);
 	}
 
-	device->client = bt_gatt_client_new(device->db, device->att,
-						device->att_mtu, features);
+	device->client = bt_gatt_client_new(device->db, device->att, features);
 	if (!device->client) {
+		DBG("Failed to create gatt client");
+		return;
+	}
+
+	bt_gatt_client_set_skip_secondary(device->client,
+						device->skip_secondary);
+
+	if (!bt_gatt_client_init(device->client, device->att_mtu)) {
 		DBG("Failed to initialize");
+		gatt_client_cleanup(device);
 		return;
 	}
 
diff --git a/src/device.h b/src/device.h
index c7b8b2a16..19f270388 100644
--- a/src/device.h
+++ b/src/device.h
@@ -22,6 +22,7 @@ char *btd_device_get_storage_path(struct btd_device *device,
 
 
 void btd_device_device_set_name(struct btd_device *device, const char *name);
+void btd_device_set_skip_secondary(struct btd_device *device, bool skip);
 void device_store_cached_name(struct btd_device *dev, const char *name);
 void device_get_name(struct btd_device *device, char *name, size_t len);
 bool device_name_known(struct btd_device *device);
-- 
2.47.3


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

* [PATCH BlueZ v2 3/6] fixup: propagate new gatt interface through codebase
  2026-03-08 12:47 [PATCH BlueZ v2 0/6] Nintendo Switch 2 support Martin BTS
  2026-03-08 12:47 ` [PATCH BlueZ v2 1/6] shared/gatt: make secondary discovery optional Martin BTS
  2026-03-08 12:47 ` [PATCH BlueZ v2 2/6] device: allow skip secondary discovery Martin BTS
@ 2026-03-08 12:47 ` Martin BTS
  2026-03-08 12:47 ` [PATCH BlueZ v2 4/6] device: Rename set_alias to btd_device_set_alias() Martin BTS
                   ` (3 subsequent siblings)
  6 siblings, 0 replies; 11+ messages in thread
From: Martin BTS @ 2026-03-08 12:47 UTC (permalink / raw)
  To: linux-bluetooth; +Cc: hadess, luiz.dentz, vi, Martin BTS

In theory we are supposed to create one patch for each affected directory,
but this patch is the immediate result of splitting gatt_client_init
from bt_gatt_client_new.
---
 peripheral/gatt.c     | 5 +++--
 tools/btgatt-client.c | 5 +++--
 unit/test-bap.c       | 3 ++-
 unit/test-gatt.c      | 3 ++-
 unit/test-gmap.c      | 3 ++-
 unit/test-mcp.c       | 3 ++-
 unit/test-micp.c      | 3 ++-
 unit/test-tmap.c      | 3 ++-
 8 files changed, 18 insertions(+), 10 deletions(-)

diff --git a/peripheral/gatt.c b/peripheral/gatt.c
index d1ddf0c97..da32b1d82 100644
--- a/peripheral/gatt.c
+++ b/peripheral/gatt.c
@@ -121,9 +121,10 @@ static struct gatt_conn *gatt_conn_new(int fd)
 		return NULL;
 	}
 
-	conn->client = bt_gatt_client_new(gatt_cache, conn->att, mtu, 0);
-	if (!conn->gatt) {
+	conn->client = bt_gatt_client_new(gatt_cache, conn->att, 0);
+	if (!conn->client || !bt_gatt_client_init(conn->client, mtu)) {
 		fprintf(stderr, "Failed to create GATT client\n");
+		bt_gatt_client_unref(conn->client);
 		bt_gatt_server_unref(conn->gatt);
 		bt_att_unref(conn->att);
 		free(conn);
diff --git a/tools/btgatt-client.c b/tools/btgatt-client.c
index 667b3d651..9bdfc3023 100644
--- a/tools/btgatt-client.c
+++ b/tools/btgatt-client.c
@@ -206,9 +206,10 @@ static struct client *client_create(int fd, uint16_t mtu)
 		return NULL;
 	}
 
-	cli->gatt = bt_gatt_client_new(cli->db, cli->att, mtu, 0);
-	if (!cli->gatt) {
+	cli->gatt = bt_gatt_client_new(cli->db, cli->att, 0);
+	if (!cli->gatt || !bt_gatt_client_init(cli->gatt, mtu)) {
 		fprintf(stderr, "Failed to create GATT client\n");
+		bt_gatt_client_unref(cli->gatt);
 		gatt_db_unref(cli->db);
 		bt_att_unref(cli->att);
 		free(cli);
diff --git a/unit/test-bap.c b/unit/test-bap.c
index 3a67e7016..da60a4a83 100644
--- a/unit/test-bap.c
+++ b/unit/test-bap.c
@@ -582,8 +582,9 @@ static void test_setup(const void *user_data)
 	db = gatt_db_new();
 	g_assert(db);
 
-	data->client = bt_gatt_client_new(db, att, 64, 0);
+	data->client = bt_gatt_client_new(db, att, 0);
 	g_assert(data->client);
+	g_assert(bt_gatt_client_init(data->client, 64));
 
 	bt_gatt_client_set_debug(data->client, print_debug, "bt_gatt_client:",
 						NULL);
diff --git a/unit/test-gatt.c b/unit/test-gatt.c
index 535baafc6..05830e9bb 100644
--- a/unit/test-gatt.c
+++ b/unit/test-gatt.c
@@ -684,8 +684,9 @@ static struct context *create_context(uint16_t mtu, gconstpointer data)
 		g_assert(context->client_db);
 
 		context->client = bt_gatt_client_new(context->client_db,
-							context->att, mtu, 0);
+							context->att, 0);
 		g_assert(context->client);
+		g_assert(bt_gatt_client_init(context->client, mtu));
 
 		bt_gatt_client_set_debug(context->client, print_debug,
 						"bt_gatt_client:", NULL);
diff --git a/unit/test-gmap.c b/unit/test-gmap.c
index 8b37efd18..a02df1c9e 100644
--- a/unit/test-gmap.c
+++ b/unit/test-gmap.c
@@ -323,8 +323,9 @@ static void test_setup(const void *user_data)
 	db = gatt_db_new();
 	g_assert(db);
 
-	data->client = bt_gatt_client_new(db, att, 64, 0);
+	data->client = bt_gatt_client_new(db, att, 0);
 	g_assert(data->client);
+	g_assert(bt_gatt_client_init(data->client, 64));
 
 	bt_gatt_client_set_debug(data->client, print_debug, "bt_gatt_client:",
 						NULL);
diff --git a/unit/test-mcp.c b/unit/test-mcp.c
index 7d922bb83..2187ee8f2 100644
--- a/unit/test-mcp.c
+++ b/unit/test-mcp.c
@@ -509,8 +509,9 @@ static void test_setup(const void *user_data)
 	db = gatt_db_new();
 	g_assert(db);
 
-	data->client = bt_gatt_client_new(db, att, 64, 0);
+	data->client = bt_gatt_client_new(db, att, 0);
 	g_assert(data->client);
+	g_assert(bt_gatt_client_init(data->client, 64));
 
 	bt_gatt_client_set_debug(data->client, print_debug, "bt_gatt_client:",
 						NULL);
diff --git a/unit/test-micp.c b/unit/test-micp.c
index ff17300d5..7ea88d8fb 100644
--- a/unit/test-micp.c
+++ b/unit/test-micp.c
@@ -500,8 +500,9 @@ static void test_setup(const void *user_data)
 	db = gatt_db_new();
 	g_assert(db);
 
-	data->client = bt_gatt_client_new(db, att, MICP_GATT_CLIENT_MTU, 0);
+	data->client = bt_gatt_client_new(db, att, 0);
 	g_assert(data->client);
+	g_assert(bt_gatt_client_init(data->client, MICP_GATT_CLIENT_MTU));
 
 	bt_gatt_client_set_debug(data->client, print_debug, "bt_gatt_client:",
 						NULL);
diff --git a/unit/test-tmap.c b/unit/test-tmap.c
index e75d62119..f89ea7ac1 100644
--- a/unit/test-tmap.c
+++ b/unit/test-tmap.c
@@ -288,8 +288,9 @@ static void test_setup(const void *user_data)
 	db = gatt_db_new();
 	g_assert(db);
 
-	data->client = bt_gatt_client_new(db, att, 64, 0);
+	data->client = bt_gatt_client_new(db, att, 0);
 	g_assert(data->client);
+	g_assert(bt_gatt_client_init(data->client, 64));
 
 	bt_gatt_client_set_debug(data->client, print_debug, "bt_gatt_client:",
 						NULL);
-- 
2.47.3


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

* [PATCH BlueZ v2 4/6] device: Rename set_alias to  btd_device_set_alias()
  2026-03-08 12:47 [PATCH BlueZ v2 0/6] Nintendo Switch 2 support Martin BTS
                   ` (2 preceding siblings ...)
  2026-03-08 12:47 ` [PATCH BlueZ v2 3/6] fixup: propagate new gatt interface through codebase Martin BTS
@ 2026-03-08 12:47 ` Martin BTS
  2026-03-08 12:47 ` [PATCH BlueZ v2 5/6] dbus-common: Add Gaming appearance class (0x2a) Martin BTS
                   ` (2 subsequent siblings)
  6 siblings, 0 replies; 11+ messages in thread
From: Martin BTS @ 2026-03-08 12:47 UTC (permalink / raw)
  To: linux-bluetooth; +Cc: hadess, luiz.dentz, vi, Martin BTS

Renamed set_alias is exposed, so that plugins and others may set
the device alias progammatically. This is usefule for devices whose
Bluetooth name is generic (e.g. a bare BD addess, or literally
"DeviceName") but whose identity is known to the plugin after
protocol-level interrogation.

The signature was changed. The first parameter,
GDBusPendingPropertySet id was dropped and
g_dbus_pending_property_success moved to dev_property_set_alias().
---
 src/device.c | 15 +++++----------
 src/device.h |  1 +
 2 files changed, 6 insertions(+), 10 deletions(-)

diff --git a/src/device.c b/src/device.c
index 4c2c0b635..ac1b7e88e 100644
--- a/src/device.c
+++ b/src/device.c
@@ -1063,17 +1063,12 @@ static gboolean dev_property_get_alias(const GDBusPropertyTable *property,
 	return TRUE;
 }
 
-static void set_alias(GDBusPendingPropertySet id, const char *alias,
-								void *data)
+void btd_device_set_alias(struct btd_device *device, const char *alias)
 {
-	struct btd_device *device = data;
-
 	/* No change */
 	if ((device->alias == NULL && g_str_equal(alias, "")) ||
-					g_strcmp0(device->alias, alias) == 0) {
-		g_dbus_pending_property_success(id);
+					g_strcmp0(device->alias, alias) == 0)
 		return;
-	}
 
 	g_free(device->alias);
 	device->alias = g_str_equal(alias, "") ? NULL : g_strdup(alias);
@@ -1082,8 +1077,6 @@ static void set_alias(GDBusPendingPropertySet id, const char *alias,
 
 	g_dbus_emit_property_changed(dbus_conn, device->path,
 						DEVICE_INTERFACE, "Alias");
-
-	g_dbus_pending_property_success(id);
 }
 
 static void dev_property_set_alias(const GDBusPropertyTable *property,
@@ -1101,7 +1094,9 @@ static void dev_property_set_alias(const GDBusPropertyTable *property,
 
 	dbus_message_iter_get_basic(value, &alias);
 
-	set_alias(id, alias, data);
+	btd_device_set_alias(data, alias);
+
+	g_dbus_pending_property_success(id);
 }
 
 static gboolean dev_property_exists_class(const GDBusPropertyTable *property,
diff --git a/src/device.h b/src/device.h
index 19f270388..ab9654a64 100644
--- a/src/device.h
+++ b/src/device.h
@@ -22,6 +22,7 @@ char *btd_device_get_storage_path(struct btd_device *device,
 
 
 void btd_device_device_set_name(struct btd_device *device, const char *name);
+void btd_device_set_alias(struct btd_device *device, const char *alias);
 void btd_device_set_skip_secondary(struct btd_device *device, bool skip);
 void device_store_cached_name(struct btd_device *dev, const char *name);
 void device_get_name(struct btd_device *device, char *name, size_t len);
-- 
2.47.3


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

* [PATCH BlueZ v2 5/6] dbus-common: Add Gaming appearance class (0x2a)
  2026-03-08 12:47 [PATCH BlueZ v2 0/6] Nintendo Switch 2 support Martin BTS
                   ` (3 preceding siblings ...)
  2026-03-08 12:47 ` [PATCH BlueZ v2 4/6] device: Rename set_alias to btd_device_set_alias() Martin BTS
@ 2026-03-08 12:47 ` Martin BTS
  2026-03-08 12:47 ` [PATCH BlueZ v2 6/6] plugins/switch2: Add Nintendo Switch 2 Controller plugin Martin BTS
  2026-03-10  3:30 ` [PATCH BlueZ v2 0/6] Nintendo Switch 2 support Vicki Pfau
  6 siblings, 0 replies; 11+ messages in thread
From: Martin BTS @ 2026-03-08 12:47 UTC (permalink / raw)
  To: linux-bluetooth; +Cc: hadess, luiz.dentz, vi, Martin BTS

Bluetooth 5.0+ defines appearance category 0x2a for gaming devices
(generic gaming, handheld game console, game controller, etc.).
Map it to "input-gaming" so the correct icon is exposed over D-Bus.
---
 src/dbus-common.c | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/src/dbus-common.c b/src/dbus-common.c
index 5e2c83d52..42539116a 100644
--- a/src/dbus-common.c
+++ b/src/dbus-common.c
@@ -144,6 +144,8 @@ const char *gap_appearance_to_icon(uint16_t appearance)
 			return "scanner";
 		}
 		break;
+	case 0x2a: /* Gaming — Assigned Numbers, section 2.6.2 */
+		return "input-gaming";
 	}
 
 	return NULL;
-- 
2.47.3


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

* [PATCH BlueZ v2 6/6] plugins/switch2: Add Nintendo Switch 2 Controller plugin
  2026-03-08 12:47 [PATCH BlueZ v2 0/6] Nintendo Switch 2 support Martin BTS
                   ` (4 preceding siblings ...)
  2026-03-08 12:47 ` [PATCH BlueZ v2 5/6] dbus-common: Add Gaming appearance class (0x2a) Martin BTS
@ 2026-03-08 12:47 ` Martin BTS
  2026-03-10  3:30 ` [PATCH BlueZ v2 0/6] Nintendo Switch 2 support Vicki Pfau
  6 siblings, 0 replies; 11+ messages in thread
From: Martin BTS @ 2026-03-08 12:47 UTC (permalink / raw)
  To: linux-bluetooth; +Cc: hadess, luiz.dentz, vi, Martin BTS

Add a BLE plugin for the Nintendo Switch 2 controllers.

Currently this is only developed, tried and tested with the Nintendo
Switch 2 Pro Controller (ProCon2).

The controller uses a vendor-specific GATT service for all HID
communication.  The plugin discovers the service, binds its
characteristics, and runs an ACK-driven initialization state machine
that sends 13 configuration commands one at a time (each waiting for
the controller's ACK notification before proceeding).

The controller requires BT_SECURITY_LOW — any SMP pairing attempt
causes it to respond with "Pairing Not Supported" and drop the link.

After initialization completes, input notifications are forwarded to
userspace via a uhid device that presents a standard gamepad HID
report descriptor.  A back channel allows the host to send commands
back to the controller (e.g. rumble, LED control).
---
 Makefile.plugins  |    3 +
 plugins/switch2.c | 1070 +++++++++++++++++++++++++++++++++++++++++++++
 2 files changed, 1073 insertions(+)
 create mode 100644 plugins/switch2.c

diff --git a/Makefile.plugins b/Makefile.plugins
index c9efadb45..4882d6808 100644
--- a/Makefile.plugins
+++ b/Makefile.plugins
@@ -1,4 +1,7 @@
 # SPDX-License-Identifier: GPL-2.0
+builtin_modules += switch2
+builtin_sources += plugins/switch2.c
+
 builtin_modules += hostname
 builtin_sources += plugins/hostname.c
 
diff --git a/plugins/switch2.c b/plugins/switch2.c
new file mode 100644
index 000000000..8f577c5c9
--- /dev/null
+++ b/plugins/switch2.c
@@ -0,0 +1,1070 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ *  BlueZ - Bluetooth protocol stack for Linux
+ *
+ *  Nintendo Switch 2 controller BLE plugin
+ *
+ *  Handles the proprietary 0x91 GATT protocol used by Nintendo Switch 2
+ *  controllers (Pro Controller 2, Joy-Con 2 L/R) over BLE.  The GATT
+ *  service UUID is shared by all three variants; the init sequence, HID
+ *  descriptor, and input handling are currently ProCon2-only.  JoyCon
+ *  connections are accepted but not initialised (no uhid device is
+ *  created).
+ *
+ *  Protocol references:
+ *    - joycon2cpp (TheFrano) — minimal Windows BLE implementation
+ *    - hid-switch2-dkms (Senko-p / Valve / Vicki Pfau) — NS2_REPORT_PRO format
+ *    - ble_test.py — working Linux BLE proof of concept (this project)
+ *
+ *  GATT service: ab7de9be-89fe-49ad-828f-118f09df7fd0  (handles 0x0008–0x002a)
+ *
+ *  Key characteristic roles (confirmed by LED test and input capture):
+ *    Command  0x0014  649d4ac9-8eb7-4e6c-af44-1ea54fe5f005  write-no-resp
+ *    ACK      0x001a  c765a961-????-????-????-????????????  notify
+ *    Input    0x000e  7492866c-ec3e-4619-8258-32755ffcc0f8  notify  (63 bytes @ ~80Hz)
+ *
+ *  The ACK characteristic UUID (c765a961-...) was not fully captured; it is
+ *  identified at runtime as the first notify-only characteristic following the
+ *  command characteristic in the service attribute list.
+ *
+ *  Init sequence ordering (critical):
+ *    1. Subscribe ACK CCCD (0x001b)
+ *    2. Send all 13 init commands to 0x0014
+ *    3. Subscribe input CCCDs (0x000f, 0x000b, 0x001f, 0x0023, 0x0027)
+ *    If input CCCDs are enabled before init, the ~80 report/sec notification
+ *    flood drowns the ACK responses and init commands are silently dropped.
+ *
+ *  MTU: BlueZ's bt_gatt_client negotiates MTU during connection setup.
+ *  Input reports are 63 bytes; default ATT MTU (23) is insufficient.
+ *  Verified MTU 512 works (controller offers 512).
+ */
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include <stdint.h>
+#include <stdbool.h>
+#include <stdlib.h>
+#include <string.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <unistd.h>
+
+#include <linux/uhid.h>
+
+#include <glib.h>
+
+#include "bluetooth/bluetooth.h"
+#include "bluetooth/uuid.h"
+
+#include "src/adapter.h"
+#include "src/device.h"
+#include "src/profile.h"
+#include "src/service.h"
+#include "src/plugin.h"
+#include "src/log.h"
+#include "src/shared/att.h"
+#include "src/shared/gatt-client.h"
+#include "src/shared/gatt-db.h"
+#include "src/shared/queue.h"
+#include "src/shared/util.h"
+
+/* ------------------------------------------------------------------ */
+/* UUIDs                                                                */
+/* ------------------------------------------------------------------ */
+
+/* Proprietary service 2 — handles 0x0008–0x002a */
+#define SWITCH2_SERVICE_UUID \
+	"ab7de9be-89fe-49ad-828f-118f09df7fd0"
+
+/* Command channel — write-no-response, handle 0x0014 */
+#define SWITCH2_CMD_UUID \
+	"649d4ac9-8eb7-4e6c-af44-1ea54fe5f005"
+
+/* Primary input channel — notify, handle 0x000e, 63-byte reports @ ~80Hz
+ * (macOS project uses ...f9 — one-byte variant, likely a firmware difference) */
+#define SWITCH2_INPUT_UUID \
+	"7492866c-ec3e-4619-8258-32755ffcc0f8"
+
+/* ACK channel UUID (c765a961-...) is not fully documented; discovered at
+ * runtime — see find_chars_in_service(). */
+
+/* ------------------------------------------------------------------ */
+/* Product IDs (from hid-switch2's hid-ids.h)                          */
+/* ------------------------------------------------------------------ */
+
+#define NS2_VID            0x057e
+#define NS2_PID_JOYCON_R   0x2066
+#define NS2_PID_JOYCON_L   0x2067
+#define NS2_PID_PROCON     0x2069
+
+enum switch2_ctlr_type {
+	NS2_CTLR_TYPE_JOYCON_L,
+	NS2_CTLR_TYPE_JOYCON_R,
+	NS2_CTLR_TYPE_PROCON,
+};
+
+struct switch2_ctlr_info {
+	uint16_t                pid;
+	enum switch2_ctlr_type  type;
+	const char             *alias;
+};
+
+static const struct switch2_ctlr_info ctlr_table[] = {
+	{ NS2_PID_PROCON,   NS2_CTLR_TYPE_PROCON,   "Nintendo Pro Controller 2" },
+	{ NS2_PID_JOYCON_L, NS2_CTLR_TYPE_JOYCON_L, "Nintendo Joy-Con 2 (L)" },
+	{ NS2_PID_JOYCON_R, NS2_CTLR_TYPE_JOYCON_R, "Nintendo Joy-Con 2 (R)" },
+};
+
+/* ------------------------------------------------------------------ */
+/* HID report descriptor for the uhid device                           */
+/* ------------------------------------------------------------------ */
+/*
+ * Describes NS2_REPORT_PRO (ID=0x09).  hid-core.c calls raw_event() with
+ * the original buffer including the report-ID byte, so raw_data[0]=0x09.
+ * The 63-byte payload (report-ID excluded) maps to raw_data[1..] as
+ * expected by hid-switch2's switch2_event():
+ *
+ *   raw_data[0]     0x09 (report ID — present in raw_event buffer)
+ *   raw_data[1-2]   seq, status(0x20)  — vendor, not parsed
+ *   raw_data[3]     btnsR  B A Y X R ZR + RS       — 8 buttons
+ *   raw_data[4]     btnsL  Dn Rt Lt Up L ZL - LS   — 8 buttons
+ *   raw_data[5]     btns3  Home Cap GripR GripL Camera + 3-bit pad — 5 buttons
+ *   raw_data[6-8]   left stick  (2×12-bit, Switch packing)
+ *   raw_data[9-11]  right stick (2×12-bit, Switch packing)
+ *   raw_data[12-63] IMU + constants (52 bytes, not parsed by HID)
+ *
+ * Bit count: 2×8 + 8 + 8 + 5+3 + 2×12 + 2×12 + 52×8 = 504 bits = 63 bytes ✓
+ *
+ * The BLE report is 63 bytes with no report ID.  The plugin prepends report
+ * ID 0x09 → 64 bytes total for uhid.
+ */
+static const uint8_t switch2_hid_desc[] = {
+	0x05, 0x01,              /* Usage Page (Generic Desktop)        */
+	0x09, 0x05,              /* Usage (Gamepad)                     */
+	0xa1, 0x01,              /* Collection (Application)            */
+	0x85, 0x09,              /*   Report ID (9)  — NS2_REPORT_PRO  */
+
+	/* raw_data[1-2]: seq, status — 2 bytes vendor constant (no pad) */
+	0x06, 0x00, 0xff,        /*   Usage Page (Vendor Defined)       */
+	0x09, 0x20,              /*   Usage (0x20)                      */
+	0x15, 0x00,              /*   Logical Minimum (0)               */
+	0x26, 0xff, 0x00,        /*   Logical Maximum (255)             */
+	0x75, 0x08,              /*   Report Size (8)                   */
+	0x95, 0x02,              /*   Report Count (2)                  */
+	0x81, 0x03,              /*   Input (Const, Variable, Absolute) */
+
+	/* raw_data[3]: btnsR — B(0) A(1) Y(2) X(3) R(4) ZR(5) +(6) RS(7) */
+	0x05, 0x09,              /*   Usage Page (Button)               */
+	0x19, 0x01,              /*   Usage Minimum (1)                 */
+	0x29, 0x08,              /*   Usage Maximum (8)                 */
+	0x15, 0x00,              /*   Logical Minimum (0)               */
+	0x25, 0x01,              /*   Logical Maximum (1)               */
+	0x75, 0x01,              /*   Report Size (1)                   */
+	0x95, 0x08,              /*   Report Count (8)                  */
+	0x81, 0x02,              /*   Input (Data, Variable, Absolute)  */
+
+	/* raw_data[4]: btnsL — Dn(0) Rt(1) Lt(2) Up(3) L(4) ZL(5) -(6) LS(7) */
+	0x19, 0x09,              /*   Usage Minimum (9)                 */
+	0x29, 0x10,              /*   Usage Maximum (16)                */
+	0x75, 0x01,              /*   Report Size (1)                   */
+	0x95, 0x08,              /*   Report Count (8)                  */
+	0x81, 0x02,              /*   Input (Data, Variable, Absolute)  */
+
+	/* raw_data[5]: btns3 — Home(0) Cap(1) GripR(2) GripL(3) Camera(4) + 3-bit pad */
+	0x19, 0x11,              /*   Usage Minimum (17)                */
+	0x29, 0x15,              /*   Usage Maximum (21)                */
+	0x75, 0x01,              /*   Report Size (1)                   */
+	0x95, 0x05,              /*   Report Count (5)                  */
+	0x81, 0x02,              /*   Input (Data, Variable, Absolute)  */
+	0x95, 0x03,              /*   Report Count (3) — padding        */
+	0x81, 0x03,              /*   Input (Const, Variable, Absolute) */
+
+	/* raw_data[6-8]: left stick — X then Y, each 12-bit LE */
+	0x05, 0x01,              /*   Usage Page (Generic Desktop)      */
+	0x09, 0x30,              /*   Usage (X)                         */
+	0x09, 0x31,              /*   Usage (Y)                         */
+	0x15, 0x00,              /*   Logical Minimum (0)               */
+	0x26, 0xff, 0x0f,        /*   Logical Maximum (4095)            */
+	0x75, 0x0c,              /*   Report Size (12)                  */
+	0x95, 0x02,              /*   Report Count (2)                  */
+	0x81, 0x02,              /*   Input (Data, Variable, Absolute)  */
+
+	/* raw_data[9-11]: right stick — Rx then Ry, each 12-bit LE */
+	0x09, 0x33,              /*   Usage (Rx)                        */
+	0x09, 0x34,              /*   Usage (Ry)                        */
+	0x75, 0x0c,              /*   Report Size (12)                  */
+	0x95, 0x02,              /*   Report Count (2)                  */
+	0x81, 0x02,              /*   Input (Data, Variable, Absolute)  */
+
+	/* raw_data[12-63]: IMU + constants — 52 bytes, not parsed by HID */
+	0x06, 0x00, 0xff,        /*   Usage Page (Vendor Defined)       */
+	0x09, 0x21,              /*   Usage (0x21)                      */
+	0x15, 0x00,              /*   Logical Minimum (0)               */
+	0x26, 0xff, 0x00,        /*   Logical Maximum (255)             */
+	0x75, 0x08,              /*   Report Size (8)                   */
+	0x95, 0x34,              /*   Report Count (52)                 */
+	0x81, 0x03,              /*   Input (Const, Variable, Absolute) */
+
+	/*
+	 * Output report — used by hid-switch2's BLE transport path.
+	 * switch2-ble.c calls hid_hw_output_report() with a pre-formatted
+	 * 0x91 frame (command or haptic); the kernel uhid driver delivers
+	 * it here as a UHID_OUTPUT event, which uhid_output_cb() picks up
+	 * and forwards to GATT 0x0014.
+	 * hid-generic has no output handler and ignores this report.
+	 */
+	0x85, 0x01,              /*   Report ID (1)                     */
+	0x06, 0x00, 0xff,        /*   Usage Page (Vendor Defined)       */
+	0x09, 0x23,              /*   Usage (0x23)                      */
+	0x75, 0x08,              /*   Report Size (8)                   */
+	0x95, 0x40,              /*   Report Count (64)                 */
+	0x91, 0x02,              /*   Output (Data, Variable, Absolute) */
+
+	0xc0,                    /* End Collection                      */
+};
+
+/* ------------------------------------------------------------------ */
+/* Stick calibration types                                             */
+/* ------------------------------------------------------------------ */
+
+struct stick_axis_calib {
+	uint16_t neutral;
+	uint16_t positive;   /* excursion from neutral toward max */
+	uint16_t negative;   /* excursion from neutral toward min */
+};
+
+struct stick_calib {
+	struct stick_axis_calib x;
+	struct stick_axis_calib y;
+};
+
+/* ------------------------------------------------------------------ */
+/* Init command byte arrays                                             */
+/* Header format: [CMD][0x91][TRANSPORT=0x01 BT][SUBCMD][0x00]        */
+/*                [PAYLOAD_LEN][0x00][0x00][...PAYLOAD]                */
+/* ------------------------------------------------------------------ */
+
+/* 1. INIT — starts HID output (analogous to Switch1 SET_REPORT_MODE) */
+static const uint8_t CMD_INIT[] = {
+	0x03, 0x91, 0x01, 0x0d, 0x00, 0x08, 0x00, 0x00,
+	0x01, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff
+};
+/* 2. Unknown 0x07/0x01 */
+static const uint8_t CMD_07[] = {
+	0x07, 0x91, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00
+};
+/* 3. Unknown 0x16/0x01 */
+static const uint8_t CMD_16[] = {
+	0x16, 0x91, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00
+};
+/* 4. Unknown 0x15/0x03 */
+static const uint8_t CMD_15_03[] = {
+	0x15, 0x91, 0x01, 0x03, 0x00, 0x01, 0x00, 0x00, 0x00
+};
+/* 5. FEATSEL SET_MASK — 0x2F = buttons|analog|imu|bit3|rumble */
+static const uint8_t CMD_FEATSEL_SET_MASK[] = {
+	0x0c, 0x91, 0x01, 0x02, 0x00, 0x04, 0x00, 0x00,
+	0x2f, 0x00, 0x00, 0x00
+};
+/* 6. Device info request 0x11/0x03 */
+static const uint8_t CMD_11[] = {
+	0x11, 0x91, 0x01, 0x03, 0x00, 0x00, 0x00, 0x00
+};
+/* 7. VIBRATE config 0x0A/0x08 */
+static const uint8_t CMD_VIBRATE_CFG[] = {
+	0x0a, 0x91, 0x01, 0x08, 0x00, 0x14, 0x00, 0x00,
+	0x01,
+	0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+	0x35, 0x00, 0x46,
+	0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
+};
+/* 8. FEATSEL ENABLE — 0x2F */
+static const uint8_t CMD_FEATSEL_ENABLE[] = {
+	0x0c, 0x91, 0x01, 0x04, 0x00, 0x04, 0x00, 0x00,
+	0x2f, 0x00, 0x00, 0x00
+};
+/* 9. SELECT_REPORT — 0x09 = NS2_REPORT_PRO (full sticks + IMU + buttons) */
+static const uint8_t CMD_SELECT_REPORT[] = {
+	0x03, 0x91, 0x01, 0x0a, 0x00, 0x04, 0x00, 0x00,
+	0x09, 0x00, 0x00, 0x00
+};
+/* 10. FW_INFO_GET */
+static const uint8_t CMD_FW_INFO_GET[] = {
+	0x10, 0x91, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00
+};
+/* 11. Unknown 0x01/0x0C */
+static const uint8_t CMD_01_0C[] = {
+	0x01, 0x91, 0x01, 0x0c, 0x00, 0x00, 0x00, 0x00
+};
+/* 12. SET_PLAYER_LED — player 1 (LED value 0x01) */
+static const uint8_t CMD_SET_PLAYER_LED[] = {
+	0x09, 0x91, 0x01, 0x07, 0x00, 0x08, 0x00, 0x00,
+	0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
+};
+/*
+ * 14-15. Factory stick calibration SPI reads.
+ * Command: NS2_CMD_FLASH(0x02) / NS2_SUBCMD_FLASH_READ(0x04)
+ * Payload: [size=9][0x7e][0x00][0x00][addr_le32]
+ * ACK data at value[16..24]: 9 bytes of packed 12-bit calibration values.
+ * Addresses from hid-switch2's NS2_FLASH_ADDR_FACTORY_*_CALIB defines.
+ */
+/* Factory primary calibration — left stick @ 0x000130a8, 9 bytes */
+static const uint8_t CMD_CALIB_LEFT[] = {
+	0x02, 0x91, 0x01, 0x04, 0x00, 0x08, 0x00, 0x00,
+	0x09, 0x7e, 0x00, 0x00, 0xa8, 0x30, 0x01, 0x00
+};
+/* Factory secondary calibration — right stick @ 0x000130e8, 9 bytes */
+static const uint8_t CMD_CALIB_RIGHT[] = {
+	0x02, 0x91, 0x01, 0x04, 0x00, 0x08, 0x00, 0x00,
+	0x09, 0x7e, 0x00, 0x00, 0xe8, 0x30, 0x01, 0x00
+};
+
+struct init_cmd {
+	const char    *name;
+	const uint8_t *data;
+	uint16_t       len;
+};
+
+#define INIT_CMD(name, arr) { name, arr, sizeof(arr) }
+
+static const struct init_cmd init_sequence[] = {
+	INIT_CMD("INIT",             CMD_INIT),           /* [0]  */
+	INIT_CMD("CMD_07",           CMD_07),             /* [1]  */
+	INIT_CMD("CMD_16",           CMD_16),             /* [2]  */
+	INIT_CMD("CMD_15_03",        CMD_15_03),          /* [3]  */
+	INIT_CMD("FEATSEL_SET_MASK", CMD_FEATSEL_SET_MASK), /* [4] */
+	INIT_CMD("CMD_11",           CMD_11),             /* [5]  */
+	INIT_CMD("VIBRATE_CFG",      CMD_VIBRATE_CFG),   /* [6]  */
+	INIT_CMD("FEATSEL_ENABLE",   CMD_FEATSEL_ENABLE), /* [7]  */
+	INIT_CMD("SELECT_REPORT",    CMD_SELECT_REPORT), /* [8]  */
+	INIT_CMD("FW_INFO_GET",      CMD_FW_INFO_GET),   /* [9]  */
+	INIT_CMD("CMD_01_0C",        CMD_01_0C),          /* [10] */
+	INIT_CMD("SET_PLAYER_LED",   CMD_SET_PLAYER_LED), /* [11] */
+	INIT_CMD("CALIB_LEFT",       CMD_CALIB_LEFT),    /* [12] */
+	INIT_CMD("CALIB_RIGHT",      CMD_CALIB_RIGHT),   /* [13] */
+};
+
+/* Input CCCDs enabled AFTER init (value handles, not CCCD handles) */
+static const uint16_t post_init_notify_handles[] = {
+	0x000e,  /* primary input (NS2_REPORT_PRO, 63 bytes) */
+	0x000a,  /* secondary input (joycon2cpp-style) */
+	0x001e,  /* unknown notify */
+	0x0022,  /* unknown notify */
+	0x0026,  /* unknown notify */
+};
+
+/* ------------------------------------------------------------------ */
+/* Per-connection state                                                 */
+/* ------------------------------------------------------------------ */
+
+struct switch2_device {
+	struct btd_device    *device;
+	struct btd_service   *service;   /* stored for async connecting_complete */
+	struct bt_gatt_client *client;
+
+	uint16_t  cmd_handle;    /* write-no-resp target  */
+	uint16_t  ack_handle;    /* first notify-only char after cmd */
+	uint16_t  input_handle;  /* primary 63-byte input char */
+
+	unsigned int  ack_notify_id;
+	unsigned int  input_notify_ids[G_N_ELEMENTS(post_init_notify_handles)];
+
+	/* Init state machine: send one command at a time, wait for ACK between */
+	unsigned int  init_idx;   /* index of command most recently sent */
+	bool          init_done;  /* true after all init commands have been ACK'd */
+
+	/* uhid device — kernel HID subsystem sees the controller as a gamepad */
+	int           uhid_fd;
+	guint         uhid_watch_id;   /* GLib I/O watch for UHID_OUTPUT events */
+
+	/* Factory stick calibration, read from SPI flash during init */
+	struct stick_calib  stick_calib[2];  /* [0]=left, [1]=right */
+	bool                calib_valid;
+
+	/* Controller variant */
+	enum switch2_ctlr_type          ctlr_type;
+	const struct switch2_ctlr_info *info;
+};
+
+static struct queue *devices = NULL;
+
+/* ------------------------------------------------------------------ */
+/* GATT database walk — find our three characteristics                  */
+/* ------------------------------------------------------------------ */
+
+struct char_walk_state {
+	bt_uuid_t  cmd_uuid;
+	bt_uuid_t  input_uuid;
+
+	uint16_t   cmd_handle;
+	uint16_t   ack_handle;   /* first notify-only char seen after cmd */
+	uint16_t   input_handle;
+
+	bool       past_cmd;     /* have we passed the cmd characteristic? */
+};
+
+static void inspect_characteristic(struct gatt_db_attribute *attr,
+							void *user_data)
+{
+	struct char_walk_state *state = user_data;
+	uint16_t handle, value_handle;
+	uint8_t properties;
+	bt_uuid_t uuid;
+
+	if (!gatt_db_attribute_get_char_data(attr, &handle, &value_handle,
+						&properties, NULL, &uuid))
+		return;
+
+	/* Command channel: match by UUID */
+	if (bt_uuid_cmp(&uuid, &state->cmd_uuid) == 0) {
+		state->cmd_handle = value_handle;
+		state->past_cmd   = true;
+		return;
+	}
+
+	/* Input channel: match by UUID */
+	if (bt_uuid_cmp(&uuid, &state->input_uuid) == 0) {
+		state->input_handle = value_handle;
+		return;
+	}
+
+	/* ACK channel: first Notify-only char after the command channel.
+	 * The full UUID (c765a961-...) was not captured; this is the only
+	 * Notify-only characteristic in the service after handle 0x0014. */
+	if (state->past_cmd && !state->ack_handle) {
+		/* Notify bit set, Read bit clear — pure notify, not read+notify */
+		if ((properties & 0x10) && !(properties & 0x02))
+			state->ack_handle = value_handle;
+	}
+}
+
+static void find_chars_in_service(struct gatt_db_attribute *service,
+							void *user_data)
+{
+	gatt_db_service_foreach_char(service, inspect_characteristic, user_data);
+}
+
+/* ------------------------------------------------------------------ */
+/* uhid device                                                          */
+/* ------------------------------------------------------------------ */
+
+static int uhid_create(const struct switch2_ctlr_info *ctlr)
+{
+	struct uhid_event ev = {};
+	int fd;
+
+	fd = open("/dev/uhid", O_RDWR | O_CLOEXEC);
+	if (fd < 0) {
+		error("switch2: open /dev/uhid: %s", strerror(errno));
+		return -1;
+	}
+
+	ev.type = UHID_CREATE2;
+	strncpy((char *)ev.u.create2.name, ctlr->alias, 127);
+	ev.u.create2.bus     = BUS_BLUETOOTH;
+	ev.u.create2.vendor  = NS2_VID;
+	ev.u.create2.product = ctlr->pid;
+	ev.u.create2.version = 0x0001;
+	ev.u.create2.country = 0;
+	ev.u.create2.rd_size = sizeof(switch2_hid_desc);
+	memcpy(ev.u.create2.rd_data, switch2_hid_desc,
+					sizeof(switch2_hid_desc));
+
+	if (write(fd, &ev, sizeof(ev)) < 0) {
+		error("switch2: UHID_CREATE2: %s", strerror(errno));
+		close(fd);
+		return -1;
+	}
+
+	info("switch2: uhid device created");
+	return fd;
+}
+
+/* ------------------------------------------------------------------ */
+/* Stick calibration helpers                                            */
+/* ------------------------------------------------------------------ */
+
+/*
+ * Parse 9 bytes of factory stick calibration data into a stick_calib.
+ * Layout: 6 × 12-bit LE values packed into 9 bytes:
+ *   [0-2]  x.neutral (12-bit), y.neutral (12-bit)
+ *   [3-5]  x.positive (12-bit), y.positive (12-bit)
+ *   [6-8]  x.negative (12-bit), y.negative (12-bit)
+ * Matches hid-switch2's switch2_parse_stick_calibration().
+ * Returns false if the data is all-0xFF (uncalibrated flash).
+ */
+static bool parse_stick_calib(struct stick_calib *out, const uint8_t *data)
+{
+	static const uint8_t uncal[9] = {
+		0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff
+	};
+	if (memcmp(data, uncal, 9) == 0)
+		return false;
+
+	out->x.neutral  = data[0] | ((data[1] & 0x0f) << 8);
+	out->y.neutral  = (data[1] >> 4) | (data[2] << 4);
+	out->x.positive = data[3] | ((data[4] & 0x0f) << 8);
+	out->y.positive = (data[4] >> 4) | (data[5] << 4);
+	out->x.negative = data[6] | ((data[7] & 0x0f) << 8);
+	out->y.negative = (data[7] >> 4) | (data[8] << 4);
+
+	/* Guard against zero excursion (avoid divide-by-zero) */
+	if (!out->x.positive) out->x.positive = 1;
+	if (!out->x.negative) out->x.negative = 1;
+	if (!out->y.positive) out->y.positive = 1;
+	if (!out->y.negative) out->y.negative = 1;
+
+	return out->x.neutral != 0;
+}
+
+/*
+ * Apply axis calibration: map a raw 12-bit value to a normalized 12-bit
+ * value centred at 2048 with symmetric excursion ±2047.  When sent to
+ * hid-switch2's BLE path (zero calib → fallback), the formula
+ * (value − 2048) × 16 produces the correct ±32752 output range.
+ */
+static uint16_t apply_axis_calib(const struct stick_axis_calib *c,
+				  uint16_t raw)
+{
+	int delta     = (int)raw - (int)c->neutral;
+	int excursion = delta > 0 ? (int)c->positive : (int)c->negative;
+	int norm      = delta * 2047 / excursion;
+
+	if (norm < -2047) norm = -2047;
+	if (norm >  2047) norm =  2047;
+	return (uint16_t)(2048 + norm);
+}
+
+/*
+ * Pack two 12-bit stick axis values (x, y) back into the Switch 3-byte
+ * little-endian format used in HID reports.
+ */
+static void pack_stick(uint8_t *dst, uint16_t x, uint16_t y)
+{
+	dst[0] =  x & 0xff;
+	dst[1] = ((x >> 8) & 0x0f) | ((y & 0x0f) << 4);
+	dst[2] =  (y >> 4) & 0xff;
+}
+
+/* ------------------------------------------------------------------ */
+/* uhid output callback — forwards UHID_OUTPUT to GATT 0x0014          */
+/* ------------------------------------------------------------------ */
+
+/*
+ * Called by GLib whenever the uhid fd becomes readable.  hid-switch2's
+ * BLE transport (switch2-ble.c) calls hid_hw_output_report() to send
+ * pre-formatted 0x91 frames; the kernel uhid driver queues them as
+ * UHID_OUTPUT events that we read here and write to GATT 0x0014.
+ */
+static gboolean uhid_output_cb(GIOChannel *io, GIOCondition cond,
+							gpointer user_data)
+{
+	struct switch2_device *dev = user_data;
+	struct uhid_event ev;
+	ssize_t n;
+
+	if (cond & (G_IO_ERR | G_IO_HUP | G_IO_NVAL)) {
+		dev->uhid_watch_id = 0;
+		return FALSE;
+	}
+
+	n = read(dev->uhid_fd, &ev, sizeof(ev));
+	if (n < 0 || (size_t)n < sizeof(ev))
+		return TRUE;
+
+	if (ev.type != UHID_OUTPUT)
+		return TRUE;
+
+	if (!dev->client || !dev->cmd_handle || !dev->init_done)
+		return TRUE;
+
+	bt_gatt_client_write_without_response(dev->client,
+					dev->cmd_handle, false,
+					ev.u.output.data, ev.u.output.size);
+	return TRUE;
+}
+
+/* ------------------------------------------------------------------ */
+/* Notification callbacks                                               */
+/* ------------------------------------------------------------------ */
+
+/* Forward declarations needed by ack_notify_cb */
+static void input_registered_cb(uint16_t att_ecode, void *user_data);
+static void input_notify_cb(uint16_t value_handle, const uint8_t *value,
+			uint16_t length, void *user_data);
+
+static void ack_registered_cb(uint16_t att_ecode, void *user_data)
+{
+	struct switch2_device *dev = user_data;
+	const struct init_cmd *cmd;
+
+	if (att_ecode) {
+		error("switch2: ACK notify registration failed: 0x%04x",
+								att_ecode);
+		btd_service_connecting_complete(dev->service, -EIO);
+		return;
+	}
+
+	/* JoyCon variants share the GATT service but the init sequence and
+	 * HID descriptor are ProCon2-specific.  Accept the connection
+	 * without starting init or creating a uhid device. */
+	if (dev->ctlr_type != NS2_CTLR_TYPE_PROCON) {
+		info("switch2: %s connected (no init sequence for this type)",
+						dev->info->alias);
+		btd_service_connecting_complete(dev->service, 0);
+		return;
+	}
+
+	DBG("switch2: ACK notify registered, starting init sequence");
+
+	/* Send first init command; the rest are sent one-by-one from
+	 * ack_notify_cb as each ACK notification is received. */
+	cmd = &init_sequence[0];
+	if (!bt_gatt_client_write_without_response(dev->client,
+				dev->cmd_handle, false, cmd->data, cmd->len)) {
+		error("switch2: failed to send %s", cmd->name);
+		btd_service_connecting_complete(dev->service, -EIO);
+		return;
+	}
+	DBG("switch2: sent %s (1/%zu)", cmd->name,
+					G_N_ELEMENTS(init_sequence));
+}
+
+static void ack_notify_cb(uint16_t value_handle, const uint8_t *value,
+				uint16_t length, void *user_data)
+{
+	struct switch2_device *dev = user_data;
+	const struct init_cmd *cmd;
+	unsigned int i;
+
+	/* ACK format: [CMD][STATUS: 01=ok 00=fail][00][ARG][10][78][00][00][data...] */
+	if (length < 2)
+		return;
+
+	DBG("switch2: ACK cmd=0x%02x status=%s", value[0],
+					value[1] == 0x01 ? "ok" : "FAIL");
+
+	/* After init is done, ACK notifications are from normal operations. */
+	if (dev->init_done)
+		return;
+
+	/*
+	 * Parse calibration data from SPI flash read ACKs.
+	 * The calibration commands are the last two in init_sequence ([13] and
+	 * [14]).  dev->init_idx is the index of the command just ACK'd.
+	 * Flash read ACK payload layout (after the 8-byte ACK header):
+	 *   value[8]     read_size (9)
+	 *   value[9]     0x7e
+	 *   value[10-11] padding
+	 *   value[12-15] address LE32 (echoed)
+	 *   value[16-24] 9 bytes of calibration data
+	 */
+	if (value[0] == 0x02 && value[1] == 0x01 && length >= 25) {
+		unsigned int calib_left_idx  = G_N_ELEMENTS(init_sequence) - 2;
+		unsigned int calib_right_idx = G_N_ELEMENTS(init_sequence) - 1;
+		bool ok;
+
+		if (dev->init_idx == calib_left_idx) {
+			ok = parse_stick_calib(&dev->stick_calib[0], &value[16]);
+			if (ok)
+				DBG("switch2: left stick calibration parsed "
+				     "(x_n=%u x_p=%u x_neg=%u)",
+				     dev->stick_calib[0].x.neutral,
+				     dev->stick_calib[0].x.positive,
+				     dev->stick_calib[0].x.negative);
+			else
+				DBG("switch2: left stick calibration not present");
+		} else if (dev->init_idx == calib_right_idx) {
+			ok = parse_stick_calib(&dev->stick_calib[1], &value[16]);
+			if (ok) {
+				DBG("switch2: right stick calibration parsed "
+				     "(x_n=%u x_p=%u x_neg=%u)",
+				     dev->stick_calib[1].x.neutral,
+				     dev->stick_calib[1].x.positive,
+				     dev->stick_calib[1].x.negative);
+				/* Both reads attempted; mark valid if at least
+				 * left stick parsed (right may be uncalibrated
+				 * on some units — left is always present). */
+				dev->calib_valid =
+					dev->stick_calib[0].x.neutral != 0;
+			} else {
+				DBG("switch2: right stick calibration not present");
+				dev->calib_valid =
+					dev->stick_calib[0].x.neutral != 0;
+			}
+		}
+	}
+
+	/* Advance to the next init command. */
+	dev->init_idx++;
+
+	if (dev->init_idx < G_N_ELEMENTS(init_sequence)) {
+		cmd = &init_sequence[dev->init_idx];
+		if (!bt_gatt_client_write_without_response(dev->client,
+					dev->cmd_handle, false,
+					cmd->data, cmd->len)) {
+			error("switch2: failed to send %s", cmd->name);
+		} else {
+			DBG("switch2: sent %s (%u/%zu)", cmd->name,
+				dev->init_idx + 1,
+				G_N_ELEMENTS(init_sequence));
+		}
+		return;
+	}
+
+	/* All init commands have been ACK'd.  Create the uhid gamepad device,
+	 * register a watch for UHID_OUTPUT (rumble/LED from hid-switch2),
+	 * then subscribe to input CCCDs. */
+	dev->init_done = true;
+	dev->uhid_fd = uhid_create(dev->info);
+	if (dev->uhid_fd >= 0) {
+		GIOChannel *io = g_io_channel_unix_new(dev->uhid_fd);
+		dev->uhid_watch_id = g_io_add_watch(io,
+					G_IO_IN | G_IO_ERR | G_IO_HUP,
+					uhid_output_cb, dev);
+		g_io_channel_unref(io);
+	}
+	DBG("switch2: init complete, subscribing input CCCDs");
+
+	for (i = 0; i < G_N_ELEMENTS(post_init_notify_handles); i++) {
+		dev->input_notify_ids[i] = bt_gatt_client_register_notify(
+					dev->client,
+					post_init_notify_handles[i],
+					input_registered_cb,
+					post_init_notify_handles[i] == dev->input_handle
+						? input_notify_cb : NULL,
+					dev, NULL);
+	}
+
+	btd_service_connecting_complete(dev->service, 0);
+}
+
+static void input_registered_cb(uint16_t att_ecode, void *user_data)
+{
+	if (att_ecode)
+		error("switch2: input notify registration failed: 0x%04x",
+								att_ecode);
+	else
+		DBG("switch2: input notify registered");
+}
+
+static void input_notify_cb(uint16_t value_handle, const uint8_t *value,
+				uint16_t length, void *user_data)
+{
+	struct switch2_device *dev = user_data;
+	struct uhid_event ev = {};
+	uint8_t *d;
+
+	/* Defence-in-depth: input CCCDs are only subscribed for ProCon2,
+	 * but guard against unexpected notifications for other types. */
+	if (dev->ctlr_type != NS2_CTLR_TYPE_PROCON)
+		return;
+
+	/*
+	 * BLE report format (63 bytes, no HID report ID prefix):
+	 *   [0]    sequence counter
+	 *   [1]    status (always 0x20)
+	 *   [2]    btnsR: B(0) A(1) Y(2) X(3) R(4) ZR(5) +(6) RS(7)
+	 *   [3]    btnsL: Dn(0) Rt(1) Lt(2) Up(3) L(4) ZL(5) -(6) LS(7)
+	 *   [4]    btns3: Home(0) Cap(1) GripR(2) GripL(3) Camera(4) ...
+	 *   [5-7]  left stick  (12-bit packing)
+	 *   [8-10] right stick (12-bit packing)
+	 *   [11-62] IMU + constants
+	 *
+	 * uhid report (64 bytes = report_id + 63-byte payload):
+	 *   d[0]   = 0x09  (report ID = NS2_REPORT_PRO)
+	 *   d[1]   = value[0]  (seq)          -> raw_data[1]
+	 *   d[2]   = value[1]  (status 0x20)  -> raw_data[2]
+	 *   d[3..] = value[2..]               -> raw_data[3..]
+	 *
+	 * hid-core.c calls hdrv->raw_event(hid, report, data, size) with
+	 * the original buffer — the report-ID is at raw_data[0].  The
+	 * report-ID stripping (cdata++) happens afterwards inside
+	 * hid_report_raw_event() for the HID core's own field parser only.
+	 * hid-switch2 therefore sees raw_data[0]=0x09, raw_data[3]=btnsR,
+	 * raw_data[4]=btnsL, raw_data[6..8]=left stick, etc.  No pad byte.
+	 *
+	 * The HID descriptor exposes 5 bits of btns3 (Home Cap GripR GripL
+	 * Camera → buttons 17–21) rather than 2, so GripR/GripL/Camera are
+	 * not silently consumed as padding.  Y axes are inverted below.
+	 */
+	if (length < 11 || dev->uhid_fd < 0)
+		return;
+
+	ev.type = UHID_INPUT2;
+	ev.u.input2.size = 64;
+	d = ev.u.input2.data;
+
+	d[0] = 0x09;		/* report ID = NS2_REPORT_PRO   -> raw_data[0] */
+	d[1] = value[0];	/* seq                          -> raw_data[1] */
+	d[2] = value[1];	/* status 0x20                  -> raw_data[2] */
+	memcpy(&d[3], &value[2], MIN(length - 2, 61));
+
+	/*
+	 * Decode sticks, apply factory calibration if available, invert Y.
+	 * Sticks sit at d[6..8] (left) and d[9..11] (right) = raw_data[6..11].
+	 *
+	 * Nintendo's Y axis is high=up; HID convention is high=down.
+	 * Invert unconditionally so the axis direction is correct regardless
+	 * of whether factory calibration data was read from SPI flash.
+	 */
+	{
+		uint16_t lx = d[6] | ((d[7] & 0x0f) << 8);
+		uint16_t ly = (d[7] >> 4) | (d[8] << 4);
+		uint16_t rx = d[9] | ((d[10] & 0x0f) << 8);
+		uint16_t ry = (d[10] >> 4) | (d[11] << 4);
+
+		if (dev->calib_valid) {
+			lx = apply_axis_calib(&dev->stick_calib[0].x, lx);
+			ly = apply_axis_calib(&dev->stick_calib[0].y, ly);
+			rx = apply_axis_calib(&dev->stick_calib[1].x, rx);
+			ry = apply_axis_calib(&dev->stick_calib[1].y, ry);
+		}
+
+		/* Invert Y: Nintendo high=up, HID expects high=down */
+		ly = 4095 - ly;
+		ry = 4095 - ry;
+
+		pack_stick(&d[6], lx, ly);
+		pack_stick(&d[9], rx, ry);
+	}
+
+	if (write(dev->uhid_fd, &ev, sizeof(ev)) < 0)
+		error("switch2: uhid write: %s", strerror(errno));
+}
+
+/* ------------------------------------------------------------------ */
+/* btd_profile callbacks                                                */
+/* ------------------------------------------------------------------ */
+
+static int switch2_probe(struct btd_service *service)
+{
+	struct btd_device *device = btd_service_get_device(service);
+	struct switch2_device *dev;
+	char gap_name[248];
+	unsigned int i;
+
+	info("switch2: probe %s", device_get_path(device));
+
+	dev = g_new0(struct switch2_device, 1);
+	dev->device  = btd_device_ref(device);
+	dev->uhid_fd = -1;
+
+	/* Detect controller type from GAP Device Name.  The ProCon2
+	 * advertises "Pro Controller 2", JoyCon L "Joy-Con (L) 2",
+	 * JoyCon R "Joy-Con (R) 2".  Default to ProCon2 (the only
+	 * tested variant) if no match. */
+	device_get_name(device, gap_name, sizeof(gap_name));
+	dev->info = &ctlr_table[0]; /* default: ProCon2 */
+	dev->ctlr_type = NS2_CTLR_TYPE_PROCON;
+
+	for (i = 0; i < G_N_ELEMENTS(ctlr_table); i++) {
+		if (ctlr_table[i].type == NS2_CTLR_TYPE_JOYCON_L &&
+				strstr(gap_name, "Joy-Con") &&
+				strstr(gap_name, "(L)")) {
+			dev->info = &ctlr_table[i];
+			dev->ctlr_type = ctlr_table[i].type;
+			break;
+		}
+		if (ctlr_table[i].type == NS2_CTLR_TYPE_JOYCON_R &&
+				strstr(gap_name, "Joy-Con") &&
+				strstr(gap_name, "(R)")) {
+			dev->info = &ctlr_table[i];
+			dev->ctlr_type = ctlr_table[i].type;
+			break;
+		}
+	}
+
+	info("switch2: detected %s (GAP name \"%s\")", dev->info->alias,
+								gap_name);
+
+	/* Override the controller's GAP Device Name with a human-readable
+	 * alias.  The alias is stored in the device info file and takes
+	 * priority over the GAP name on every reconnect. */
+	btd_device_set_alias(device, dev->info->alias);
+	btd_device_set_skip_secondary(device, true);
+
+	if (!devices)
+		devices = queue_new();
+
+	queue_push_tail(devices, dev);
+	btd_service_set_user_data(service, dev);
+
+	return 0;
+}
+
+static void switch2_remove(struct btd_service *service)
+{
+	struct switch2_device *dev = btd_service_get_user_data(service);
+
+	info("switch2: remove %s", device_get_path(dev->device));
+
+	queue_remove(devices, dev);
+	if (queue_isempty(devices)) {
+		queue_destroy(devices, NULL);
+		devices = NULL;
+	}
+
+	btd_device_unref(dev->device);
+	g_free(dev);
+}
+
+static int switch2_accept(struct btd_service *service)
+{
+	struct switch2_device *dev = btd_service_get_user_data(service);
+	struct btd_device *device  = btd_service_get_device(service);
+	struct gatt_db *db;
+	struct char_walk_state state;
+	bt_uuid_t service_uuid;
+
+	info("switch2: accept %s", device_get_path(device));
+
+	dev->client   = btd_device_get_gatt_client(device);
+	dev->service  = service;
+	dev->init_idx = 0;
+	dev->init_done = false;
+	if (!dev->client) {
+		error("switch2: no GATT client");
+		return -EINVAL;
+	}
+
+	/* NS2 controllers only accept SMP AuthReq=0x00 (no bonding, no MITM,
+	 * no SC).  Any security elevation attempt causes them to reply with
+	 * SMP Pairing Not Supported (0x05) and drop the link.  Keep the
+	 * bearer at BT_SECURITY_LOW so bt_gatt_client never sends a Pairing
+	 * Request. */
+	bt_gatt_client_set_security(dev->client, BT_SECURITY_LOW);
+
+	/* Request minimum BLE connection interval for low-latency gaming input.
+	 * Intervals in 1.25ms units: 6 = 7.5ms (spec minimum).
+	 * Latency 0: controller must respond every interval (no skipping).
+	 * Timeout in 10ms units: 200 = 2s supervision timeout.
+	 * BlueZ forwards this via MGMT LOAD_CONN_PARAM → kernel sends
+	 * HCI_LE_CONNECTION_UPDATE on the active connection. */
+	btd_device_set_conn_param(device, 6, 6, 0, 200);
+
+	DBG("switch2: GATT client MTU = %u", bt_gatt_client_get_mtu(dev->client));
+
+	/* Walk the GATT database to locate our three characteristics */
+	memset(&state, 0, sizeof(state));
+	bt_string_to_uuid(&state.cmd_uuid,   SWITCH2_CMD_UUID);
+	bt_string_to_uuid(&state.input_uuid, SWITCH2_INPUT_UUID);
+
+	db = btd_device_get_gatt_db(device);
+	bt_string_to_uuid(&service_uuid, SWITCH2_SERVICE_UUID);
+	gatt_db_foreach_service(db, &service_uuid,
+					find_chars_in_service, &state);
+
+	dev->cmd_handle   = state.cmd_handle;
+	dev->ack_handle   = state.ack_handle;
+	dev->input_handle = state.input_handle;
+
+	if (!dev->cmd_handle || !dev->ack_handle || !dev->input_handle) {
+		error("switch2: characteristic discovery failed "
+			"(cmd=0x%04x ack=0x%04x input=0x%04x)",
+			dev->cmd_handle, dev->ack_handle, dev->input_handle);
+		return -ENOENT;
+	}
+
+	DBG("switch2: cmd=0x%04x ack=0x%04x input=0x%04x MTU=%u",
+		dev->cmd_handle, dev->ack_handle, dev->input_handle,
+		bt_gatt_client_get_mtu(dev->client));
+
+	/* Subscribe to ACK notifications.  The init sequence starts in
+	 * ack_registered_cb once the CCCD Write Request is acknowledged by the
+	 * controller — ensuring the ACK channel is live before any command
+	 * is sent.  btd_service_connecting_complete() is called from
+	 * ack_notify_cb after the last init command is ACK'd and the input
+	 * CCCDs have been registered. */
+	dev->ack_notify_id = bt_gatt_client_register_notify(dev->client,
+					dev->ack_handle,
+					ack_registered_cb,
+					ack_notify_cb,
+					dev, NULL);
+	if (!dev->ack_notify_id) {
+		error("switch2: failed to register ACK notify");
+		return -EIO;
+	}
+
+	return 0;
+}
+
+static int switch2_disconnect(struct btd_service *service)
+{
+	struct switch2_device *dev = btd_service_get_user_data(service);
+	unsigned int i;
+
+	info("switch2: disconnect %s", device_get_path(dev->device));
+
+	/* If connect is still in progress (init not done), fail it now. */
+	if (dev->service && !dev->init_done)
+		btd_service_connecting_complete(dev->service, -ECONNRESET);
+
+	for (i = 0; i < G_N_ELEMENTS(post_init_notify_handles); i++) {
+		if (dev->input_notify_ids[i]) {
+			bt_gatt_client_unregister_notify(dev->client,
+						dev->input_notify_ids[i]);
+			dev->input_notify_ids[i] = 0;
+		}
+	}
+
+	if (dev->ack_notify_id) {
+		bt_gatt_client_unregister_notify(dev->client,
+						dev->ack_notify_id);
+		dev->ack_notify_id = 0;
+	}
+
+	if (dev->uhid_watch_id) {
+		g_source_remove(dev->uhid_watch_id);
+		dev->uhid_watch_id = 0;
+	}
+
+	if (dev->uhid_fd >= 0) {
+		struct uhid_event ev = { .type = UHID_DESTROY };
+		if (write(dev->uhid_fd, &ev, sizeof(ev)) < 0)
+			error("switch2: UHID_DESTROY: %s", strerror(errno));
+		close(dev->uhid_fd);
+		dev->uhid_fd = -1;
+	}
+
+	dev->client       = NULL;
+	dev->cmd_handle   = 0;
+	dev->ack_handle   = 0;
+	dev->input_handle = 0;
+
+	btd_service_disconnecting_complete(service, 0);
+
+	return 0;
+}
+
+/* ------------------------------------------------------------------ */
+/* Profile and plugin registration                                      */
+/* ------------------------------------------------------------------ */
+
+static struct btd_profile switch2_profile = {
+	.name         = "switch2",
+	.bearer       = BTD_PROFILE_BEARER_LE,
+	.remote_uuid  = SWITCH2_SERVICE_UUID,
+	.device_probe  = switch2_probe,
+	.device_remove = switch2_remove,
+	.accept        = switch2_accept,
+	.disconnect    = switch2_disconnect,
+	.auto_connect  = true,
+};
+
+static int switch2_init(void)
+{
+	info("switch2: plugin init");
+	return btd_profile_register(&switch2_profile);
+}
+
+static void switch2_exit(void)
+{
+	info("switch2: plugin exit");
+	btd_profile_unregister(&switch2_profile);
+}
+
+BLUETOOTH_PLUGIN_DEFINE(switch2, VERSION, BLUETOOTH_PLUGIN_PRIORITY_DEFAULT,
+						switch2_init, switch2_exit)
-- 
2.47.3


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

* RE: Nintendo Switch 2 support
  2026-03-08 12:47 ` [PATCH BlueZ v2 1/6] shared/gatt: make secondary discovery optional Martin BTS
@ 2026-03-08 14:18   ` bluez.test.bot
  2026-03-09 13:58   ` [PATCH BlueZ v2 1/6] shared/gatt: make secondary discovery optional Luiz Augusto von Dentz
  1 sibling, 0 replies; 11+ messages in thread
From: bluez.test.bot @ 2026-03-08 14:18 UTC (permalink / raw)
  To: linux-bluetooth, martinbts

[-- Attachment #1: Type: text/plain, Size: 1262 bytes --]

This is automated email and please do not reply to this email!

Dear submitter,

Thank you for submitting the patches to the linux bluetooth mailing list.
This is a CI test results with your patch series:
PW Link:https://patchwork.kernel.org/project/bluetooth/list/?series=1063221

---Test result---

Test Summary:
CheckPatch                    PENDING   0.26 seconds
GitLint                       PENDING   0.28 seconds
BuildEll                      PASS      21.24 seconds
BluezMake                     PASS      642.66 seconds
MakeCheck                     PASS      18.67 seconds
MakeDistcheck                 PASS      246.70 seconds
CheckValgrind                 PASS      298.72 seconds
CheckSmatch                   PASS      364.69 seconds
bluezmakeextell               PASS      182.79 seconds
IncrementalBuild              PENDING   0.24 seconds
ScanBuild                     PASS      1039.90 seconds

Details
##############################
Test: CheckPatch - PENDING
Desc: Run checkpatch.pl script
Output:

##############################
Test: GitLint - PENDING
Desc: Run gitlint
Output:

##############################
Test: IncrementalBuild - PENDING
Desc: Incremental build with the patches in the series
Output:



---
Regards,
Linux Bluetooth


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

* Re: [PATCH BlueZ v2 1/6] shared/gatt: make secondary discovery optional
  2026-03-08 12:47 ` [PATCH BlueZ v2 1/6] shared/gatt: make secondary discovery optional Martin BTS
  2026-03-08 14:18   ` Nintendo Switch 2 support bluez.test.bot
@ 2026-03-09 13:58   ` Luiz Augusto von Dentz
  2026-03-09 15:40     ` Martin BTS
  1 sibling, 1 reply; 11+ messages in thread
From: Luiz Augusto von Dentz @ 2026-03-09 13:58 UTC (permalink / raw)
  To: Martin BTS; +Cc: linux-bluetooth, hadess, vi

Hi Martin,

On Sun, Mar 8, 2026 at 8:48 AM Martin BTS <martinbts@gmx.net> wrote:
>
> BREAKING CHANGE!
>
> * Remove gatt_client_init from bt_gatt_client_new. Consumers must now
>   call gatt_client_init themselves!
> * Remove mtu paramter from bt_gatt_client_new

Why?

> * Rename gatt_client_init to bt_gatt_client_init and make it public
> * Introduce a new bt_gatt_client field "skip_secondary", default false
> * Introduce public skip_secondary setter
> * If true, skip_secondary makes discover_primary_cb goto done
>   (instead of discoverying secondary services)

Overengineer, this could probably be handled gracefully internally.
Maybe have a short timeout, e.g. 2 seconds, and if the device doesn't
respond, continue as if it was ignored or something like that.

> ---
>  src/shared/gatt-client.c | 22 ++++++++++++++--------
>  src/shared/gatt-client.h |  4 +++-
>  2 files changed, 17 insertions(+), 9 deletions(-)
>
> diff --git a/src/shared/gatt-client.c b/src/shared/gatt-client.c
> index df1541b88..7896ed329 100644
> --- a/src/shared/gatt-client.c
> +++ b/src/shared/gatt-client.c
> @@ -93,6 +93,7 @@ struct bt_gatt_client {
>         struct queue *notify_chrcs;
>         int next_reg_id;
>         unsigned int disc_id, nfy_id, nfy_mult_id, ind_id;
> +       bool skip_secondary;
>
>         /*
>          * Handles of the GATT Service and the Service Changed characteristic
> @@ -1344,7 +1345,7 @@ secondary:
>          * functionality of a device and is referenced from at least one
>          * primary service on the device.
>          */
> -       if (queue_isempty(op->pending_svcs))
> +       if (queue_isempty(op->pending_svcs) || client->skip_secondary)
>                 goto done;
>
>         /* Discover secondary services */
> @@ -2106,7 +2107,7 @@ done:
>         notify_client_ready(client, success, att_ecode);
>  }
>
> -static bool gatt_client_init(struct bt_gatt_client *client, uint16_t mtu)
> +bool bt_gatt_client_init(struct bt_gatt_client *client, uint16_t mtu)
>  {
>         struct discovery_op *op;
>
> @@ -2549,7 +2550,6 @@ fail:
>
>  struct bt_gatt_client *bt_gatt_client_new(struct gatt_db *db,
>                                                         struct bt_att *att,
> -                                                       uint16_t mtu,
>                                                         uint8_t features)
>  {
>         struct bt_gatt_client *client;
> @@ -2561,11 +2561,6 @@ struct bt_gatt_client *bt_gatt_client_new(struct gatt_db *db,
>         if (!client)
>                 return NULL;
>
> -       if (!gatt_client_init(client, mtu)) {
> -               bt_gatt_client_free(client);
> -               return NULL;
> -       }
> -
>         return bt_gatt_client_ref(client);
>  }
>
> @@ -2592,6 +2587,17 @@ struct bt_gatt_client *bt_gatt_client_clone(struct bt_gatt_client *client)
>         return bt_gatt_client_ref(clone);
>  }
>
> +bool bt_gatt_client_set_skip_secondary(struct bt_gatt_client *client,
> +                                                               bool skip)
> +{
> +       if (!client)
> +               return false;
> +
> +       client->skip_secondary = skip;
> +
> +       return true;
> +}
> +
>  struct bt_gatt_client *bt_gatt_client_ref(struct bt_gatt_client *client)
>  {
>         if (!client)
> diff --git a/src/shared/gatt-client.h b/src/shared/gatt-client.h
> index 63cf99500..e510ad455 100644
> --- a/src/shared/gatt-client.h
> +++ b/src/shared/gatt-client.h
> @@ -18,9 +18,11 @@ struct bt_gatt_client;
>
>  struct bt_gatt_client *bt_gatt_client_new(struct gatt_db *db,
>                                                         struct bt_att *att,
> -                                                       uint16_t mtu,
>                                                         uint8_t features);
>  struct bt_gatt_client *bt_gatt_client_clone(struct bt_gatt_client *client);
> +bool bt_gatt_client_init(struct bt_gatt_client *client, uint16_t mtu);
> +bool bt_gatt_client_set_skip_secondary(struct bt_gatt_client *client,
> +                                                               bool skip);
>
>  struct bt_gatt_client *bt_gatt_client_ref(struct bt_gatt_client *client);
>  void bt_gatt_client_unref(struct bt_gatt_client *client);
> --
> 2.47.3
>


-- 
Luiz Augusto von Dentz

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

* Re: [PATCH BlueZ v2 1/6] shared/gatt: make secondary discovery optional
  2026-03-09 13:58   ` [PATCH BlueZ v2 1/6] shared/gatt: make secondary discovery optional Luiz Augusto von Dentz
@ 2026-03-09 15:40     ` Martin BTS
  0 siblings, 0 replies; 11+ messages in thread
From: Martin BTS @ 2026-03-09 15:40 UTC (permalink / raw)
  To: Luiz Augusto von Dentz; +Cc: linux-bluetooth, hadess, vi, Martin BTS

Hi Luiz!

On 09.03.26 14:58, Luiz Augusto von Dentz wrote:
> Hi Martin,
>
> On Sun, Mar 8, 2026 at 8:48 AM Martin BTS <martinbts@gmx.net> wrote:
>> BREAKING CHANGE!
>>
>> * Remove gatt_client_init from bt_gatt_client_new. Consumers must now
>>    call gatt_client_init themselves!
>> * Remove mtu paramter from bt_gatt_client_new
> Why?

The only thing bt_gatt_client_new did with this parameter was to pass it 
on to gatt_client_init. Since the latter is no longer part of 
bt_gatt_client_init, the parameter is never used.

When implementing that, my approach was to squeeze in between _new() 
populating the struct and the invocation of _init(). I did that by 
splitting _init() from _new() which allows consumers to configure the 
newly created client, before calling _init(), which implicitly does the 
primary and secondary discovery.

With a few hours between me and me writing that ... I guess since I am 
changing the interface anyways, I could just as well have added 
skip_secondary as an additional parameter to bt_gatt_client_new(), which 
would store it in its bt_gatt_client before calling _init(). Would still 
break the interface, but would have resulted in a fewer lines of code.

>> * Rename gatt_client_init to bt_gatt_client_init and make it public
>> * Introduce a new bt_gatt_client field "skip_secondary", default false
>> * Introduce public skip_secondary setter
>> * If true, skip_secondary makes discover_primary_cb goto done
>>    (instead of discoverying secondary services)
> Overengineer, this could probably be handled gracefully internally.
> Maybe have a short timeout, e.g. 2 seconds, and if the device doesn't
> respond, continue as if it was ignored or something like that.

I have virtually no experience with bluetooth and rely on your 
expertise. In 
https://lore.kernel.org/all/CABBYNZLNJnnO+WUQCgzZ2BvgCqftvsSbX3qKDh=fZcfm-KQf5Q@mail.gmail.com/ 
you point out the importance of the current spec compliance and said we 
"need a configuration setting, which should probably default to 
compliance". Do you want me to introduce a 2 second timeout instead of 
the configuration setting? Please advise.

>> ---
>>   src/shared/gatt-client.c | 22 ++++++++++++++--------
>>   src/shared/gatt-client.h |  4 +++-
>>   2 files changed, 17 insertions(+), 9 deletions(-)
>>
>> diff --git a/src/shared/gatt-client.c b/src/shared/gatt-client.c
>> index df1541b88..7896ed329 100644
>> --- a/src/shared/gatt-client.c
>> +++ b/src/shared/gatt-client.c
>> @@ -93,6 +93,7 @@ struct bt_gatt_client {
>>          struct queue *notify_chrcs;
>>          int next_reg_id;
>>          unsigned int disc_id, nfy_id, nfy_mult_id, ind_id;
>> +       bool skip_secondary;
>>
>>          /*
>>           * Handles of the GATT Service and the Service Changed characteristic
>> @@ -1344,7 +1345,7 @@ secondary:
>>           * functionality of a device and is referenced from at least one
>>           * primary service on the device.
>>           */
>> -       if (queue_isempty(op->pending_svcs))
>> +       if (queue_isempty(op->pending_svcs) || client->skip_secondary)
>>                  goto done;
>>
>>          /* Discover secondary services */
>> @@ -2106,7 +2107,7 @@ done:
>>          notify_client_ready(client, success, att_ecode);
>>   }
>>
>> -static bool gatt_client_init(struct bt_gatt_client *client, uint16_t mtu)
>> +bool bt_gatt_client_init(struct bt_gatt_client *client, uint16_t mtu)
>>   {
>>          struct discovery_op *op;
>>
>> @@ -2549,7 +2550,6 @@ fail:
>>
>>   struct bt_gatt_client *bt_gatt_client_new(struct gatt_db *db,
>>                                                          struct bt_att *att,
>> -                                                       uint16_t mtu,
>>                                                          uint8_t features)
>>   {
>>          struct bt_gatt_client *client;
>> @@ -2561,11 +2561,6 @@ struct bt_gatt_client *bt_gatt_client_new(struct gatt_db *db,
>>          if (!client)
>>                  return NULL;
>>
>> -       if (!gatt_client_init(client, mtu)) {
>> -               bt_gatt_client_free(client);
>> -               return NULL;
>> -       }
>> -
>>          return bt_gatt_client_ref(client);
>>   }
>>
>> @@ -2592,6 +2587,17 @@ struct bt_gatt_client *bt_gatt_client_clone(struct bt_gatt_client *client)
>>          return bt_gatt_client_ref(clone);
>>   }
>>
>> +bool bt_gatt_client_set_skip_secondary(struct bt_gatt_client *client,
>> +                                                               bool skip)
>> +{
>> +       if (!client)
>> +               return false;
>> +
>> +       client->skip_secondary = skip;
>> +
>> +       return true;
>> +}
>> +
>>   struct bt_gatt_client *bt_gatt_client_ref(struct bt_gatt_client *client)
>>   {
>>          if (!client)
>> diff --git a/src/shared/gatt-client.h b/src/shared/gatt-client.h
>> index 63cf99500..e510ad455 100644
>> --- a/src/shared/gatt-client.h
>> +++ b/src/shared/gatt-client.h
>> @@ -18,9 +18,11 @@ struct bt_gatt_client;
>>
>>   struct bt_gatt_client *bt_gatt_client_new(struct gatt_db *db,
>>                                                          struct bt_att *att,
>> -                                                       uint16_t mtu,
>>                                                          uint8_t features);
>>   struct bt_gatt_client *bt_gatt_client_clone(struct bt_gatt_client *client);
>> +bool bt_gatt_client_init(struct bt_gatt_client *client, uint16_t mtu);
>> +bool bt_gatt_client_set_skip_secondary(struct bt_gatt_client *client,
>> +                                                               bool skip);
>>
>>   struct bt_gatt_client *bt_gatt_client_ref(struct bt_gatt_client *client);
>>   void bt_gatt_client_unref(struct bt_gatt_client *client);
>> --
>> 2.47.3
>>
>

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

* Re: [PATCH BlueZ v2 0/6] Nintendo Switch 2 support
  2026-03-08 12:47 [PATCH BlueZ v2 0/6] Nintendo Switch 2 support Martin BTS
                   ` (5 preceding siblings ...)
  2026-03-08 12:47 ` [PATCH BlueZ v2 6/6] plugins/switch2: Add Nintendo Switch 2 Controller plugin Martin BTS
@ 2026-03-10  3:30 ` Vicki Pfau
  6 siblings, 0 replies; 11+ messages in thread
From: Vicki Pfau @ 2026-03-10  3:30 UTC (permalink / raw)
  To: Martin BTS, linux-bluetooth; +Cc: hadess, luiz.dentz

Hi Martin

On 3/8/26 5:47 AM, Martin BTS wrote:
> Changes v2:
> * Drop original patches 1 and 2 (tollerate ATT timeout)
> * New patches 1, 2, 3 introduce a device property skip_secondary,
>   that makes secondary device discovery optional
> * set_alias() is now public btd_device_set_alias() as suggested
> * Fix assigned numbers reference comment
> * Link to v1: https://lore.kernel.org/all/20260301152930.221472-1-martinbts@gmx.net/
> 
> v2 blurb:
> 
> The problem is the secondary service discovery. It will time out on the
> Procon2 which is essentially an unrecoverable error and we cannot
> connect the controller as a result.
> 
> This patchset proposes making the secondary service discovery optional,
> so that we can prevent dealing with the Procon2's behaviour.
> It now is a property of the device, if it wants/needs a secondary
> discovery, or not. This allows device specific plugins to make the
> correct configuration in time, bevor a gatt-client is created. The
> default is the original behaviour: do a secondary services discovery.
> 
> I marked patch 1 as a breaking change, because of how it changes the
> gatt-client interface. It appears this gatt-client is only used
> internally and never exposed so it technically isn't a breaking change
> for BlueZ, but I cannot be sure.
> 
> For the record: The Procon2 reports appearance 0x0A82 Portable handheld console
> 
> Martin BTS (6):
>   shared/gatt: make secondary discovery optional
>   device: allow skip secondary discovery
>   fixup: propagate new gatt interface through codebase
>   device: Rename set_alias to  btd_device_set_alias()
>   dbus-common: Add Gaming appearance class (0x2a)
>   plugins/switch2: Add Nintendo Switch 2 Controller plugin
> 
>  Makefile.plugins         |    3 +
>  peripheral/gatt.c        |    5 +-
>  plugins/switch2.c        | 1070 ++++++++++++++++++++++++++++++++++++++
>  src/dbus-common.c        |    2 +
>  src/device.c             |   33 +-
>  src/device.h             |    2 +
>  src/shared/gatt-client.c |   22 +-
>  src/shared/gatt-client.h |    4 +-
>  tools/btgatt-client.c    |    5 +-
>  unit/test-bap.c          |    3 +-
>  unit/test-gatt.c         |    3 +-
>  unit/test-gmap.c         |    3 +-
>  unit/test-mcp.c          |    3 +-
>  unit/test-micp.c         |    3 +-
>  unit/test-tmap.c         |    3 +-
>  15 files changed, 1133 insertions(+), 31 deletions(-)
>  create mode 100644 plugins/switch2.c
> 

I can't comment on the other patches, but I will say I think it's premature to be merging Switch 2 support, as I'm still working on the kernel side of this in ways that makes me want to not lay down any API that gets merged into userspace software yet. I left a comment to this end on the DKMS port of driver as well.

Vicki


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

end of thread, other threads:[~2026-03-10  3:38 UTC | newest]

Thread overview: 11+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-03-08 12:47 [PATCH BlueZ v2 0/6] Nintendo Switch 2 support Martin BTS
2026-03-08 12:47 ` [PATCH BlueZ v2 1/6] shared/gatt: make secondary discovery optional Martin BTS
2026-03-08 14:18   ` Nintendo Switch 2 support bluez.test.bot
2026-03-09 13:58   ` [PATCH BlueZ v2 1/6] shared/gatt: make secondary discovery optional Luiz Augusto von Dentz
2026-03-09 15:40     ` Martin BTS
2026-03-08 12:47 ` [PATCH BlueZ v2 2/6] device: allow skip secondary discovery Martin BTS
2026-03-08 12:47 ` [PATCH BlueZ v2 3/6] fixup: propagate new gatt interface through codebase Martin BTS
2026-03-08 12:47 ` [PATCH BlueZ v2 4/6] device: Rename set_alias to btd_device_set_alias() Martin BTS
2026-03-08 12:47 ` [PATCH BlueZ v2 5/6] dbus-common: Add Gaming appearance class (0x2a) Martin BTS
2026-03-08 12:47 ` [PATCH BlueZ v2 6/6] plugins/switch2: Add Nintendo Switch 2 Controller plugin Martin BTS
2026-03-10  3:30 ` [PATCH BlueZ v2 0/6] Nintendo Switch 2 support Vicki Pfau

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