public inbox for linux-bluetooth@vger.kernel.org
 help / color / mirror / Atom feed
From: Martin BTS <martinbts@gmx.net>
To: linux-bluetooth@vger.kernel.org
Cc: hadess@hadess.net, luiz.dentz@gmail.com, vi@endrift.com,
	Martin BTS <martinbts@gmx.net>
Subject: [PATCH BlueZ v3 6/6] plugins/switch2: Add Nintendo Switch 2 Controller plugin
Date: Fri,  3 Apr 2026 10:55:53 +0200	[thread overview]
Message-ID: <20260403085555.23871-7-martinbts@gmx.net> (raw)
In-Reply-To: <20260403085555.23871-1-martinbts@gmx.net>

Thin device-specific wrapper around the generic GATT-UHID bridge for
Nintendo Switch 2 controllers (Pro Controller 2, Joy-Con 2 L/R,
GameCube Controller).

The plugin handles:
- Profile registration with the Switch 2 GATT service UUID
- Dynamic GATT characteristic discovery (all notify handles)
- Controller type detection from product ID
- Vendor/product IDs and payload sizes for uhid device creation
- Low-latency connection parameters and BT_SECURITY_LOW
- Skip secondary service discovery (controller rejects it)

All protocol knowledge (init handshake, calibration, stick normalization,
button mapping, rumble) is delegated to a matched kernel HID driver.
The plugin itself contains no Nintendo protocol logic.
---
 Makefile.plugins  |   5 +
 plugins/switch2.c | 255 ++++++++++++++++++++++++++++++++++++++++++++++
 2 files changed, 260 insertions(+)
 create mode 100644 plugins/switch2.c

diff --git a/Makefile.plugins b/Makefile.plugins
index c9efadb45..4c5d10a90 100644
--- a/Makefile.plugins
+++ b/Makefile.plugins
@@ -1,4 +1,9 @@
 # SPDX-License-Identifier: GPL-2.0
+builtin_sources += plugins/gatt-uhid.h plugins/gatt-uhid.c
+
+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..0e389cab1
--- /dev/null
+++ b/plugins/switch2.c
@@ -0,0 +1,255 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ *  BlueZ - Bluetooth protocol stack for Linux
+ *
+ *  Nintendo Switch 2 controller BLE plugin
+ *
+ *  Thin device-specific wrapper around the generic GATT-UHID bridge.
+ *  Provides the GATT service UUID, characteristic discovery, and
+ *  vendor/product IDs for uhid device matching.
+ *
+ *  This wires the BLE device(s) so that HID drivers can take over.
+ */
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include <stdint.h>
+#include <stdbool.h>
+#include <stdlib.h>
+#include <string.h>
+#include <errno.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 "plugins/gatt-uhid.h"
+
+/*
+ * Hard facts about Nintendo and the Switch 2 controllers
+ */
+
+#define SWITCH2_SERVICE_UUID "ab7de9be-89fe-49ad-828f-118f09df7fd0"
+
+#define NS2_VID           0x057e /* Nintendo Vendor ID */
+#define NS2_PID_JOYCON_R  0x2066
+#define NS2_PID_JOYCON_L  0x2067
+#define NS2_PID_PROCON    0x2069
+#define NS2_PID_GCCON        0x2073
+
+#define NS2_INPUT_SIZE    63 /* Max observed on Procon2 in bytes */
+#define NS2_OUTPUT_SIZE   64 /* Max observed on Procon2; no off by one */
+
+#define NS2_MAX_NOTIFY    8 /* Max |notify characteristics in the service| */
+
+struct switch2_ctlr_info {
+	uint16_t    pid;
+	const char *alias;
+};
+
+static const struct switch2_ctlr_info ctlr_table[] = {
+	{ NS2_PID_PROCON,   "Nintendo Switch 2 Pro Controller" },
+	{ NS2_PID_JOYCON_L, "Nintendo Switch 2 Joy-Con (L)" },
+	{ NS2_PID_JOYCON_R, "Nintendo Switch 2 Joy-Con (R)" },
+	{ NS2_PID_GCCON,    "Nintendo Switch 2 GameCube Controller" },
+};
+
+/* Struct representing a controller */
+struct switch2_device {
+	struct btd_device    *device;
+	struct gatt_uhid     *bridge;
+	const struct switch2_ctlr_info *info;
+};
+
+/*
+ * GATT characteristic discovery
+ */
+
+/* We iterate gatt_db_foreach_service->gatt_db_service_foreach_char->inspect.
+ * Collect progress in char_walk_state */
+struct char_walk_state {
+	uint16_t   notify_handles[NS2_MAX_NOTIFY];
+	unsigned int notify_count;
+};
+
+static void inspect_char(struct gatt_db_attribute *attr, void *user_data)
+{
+	struct char_walk_state *state = user_data;
+	uint16_t handle, value_handle;
+	uint8_t properties;
+
+	if (!gatt_db_attribute_get_char_data(attr, &handle, &value_handle,
+						&properties, NULL, NULL))
+		return;
+
+	/* Collect every characteristic that supports notification */
+	if ((properties & 0x10) &&
+			state->notify_count < NS2_MAX_NOTIFY) {
+		state->notify_handles[state->notify_count++] = value_handle;
+	}
+}
+
+static void find_chars_in_service(struct gatt_db_attribute *service,
+							void *user_data)
+{
+	gatt_db_service_foreach_char(service, inspect_char, user_data);
+}
+
+/*
+ * Plugin functions
+ */
+
+static int switch2_probe(struct btd_service *service)
+{
+	struct btd_device *device = btd_service_get_device(service);
+	uint16_t pid = btd_device_get_product(device);
+	struct switch2_device *dev;
+	unsigned int c;
+
+	DBG("switch2: probe %s", device_get_path(device));
+
+	dev = g_new0(struct switch2_device, 1);
+	dev->device = btd_device_ref(device);
+	dev->info = &ctlr_table[0]; /* default to Procon 2 */
+
+	for (c = 0; c < G_N_ELEMENTS(ctlr_table); c++) {
+		if (ctlr_table[c].pid == pid) {
+			dev->info = &ctlr_table[c];
+			break;
+		}
+	}
+
+	DBG("switch2: detected %s (pid=0x%04x)", dev->info->alias, pid);
+
+	btd_device_set_alias(device, dev->info->alias);
+	btd_device_set_skip_secondary(device, true);
+
+	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);
+
+	DBG("switch2: remove %s", device_get_path(dev->device));
+
+	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 bt_gatt_client *client;
+	struct gatt_db *db;
+	struct char_walk_state state;
+	bt_uuid_t service_uuid;
+	struct gatt_uhid_params params;
+
+	DBG("switch2: accept %s", device_get_path(device));
+
+	client = btd_device_get_gatt_client(device);
+	if (!client) {
+		error("switch2: no GATT client");
+		return -EINVAL;
+	}
+
+	/* NS2 controllers reject pairing; avoid pairing */
+	bt_gatt_client_set_security(client, BT_SECURITY_LOW);
+
+	/* Low-latency connection, otherwise unplayable */
+	btd_device_set_conn_param(device, 6, 6, 0, 200);
+
+	/* Discover GATT characteristics */
+	memset(&state, 0, sizeof(state));
+
+	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);
+
+	if (!state.notify_count) {
+		error("switch2: no notify characteristics found");
+		return -ENOENT;
+	}
+
+	/* Set up the GATT-UHID bridge */
+	memset(&params, 0, sizeof(params));
+	/* Static info */
+	params.version = 0x0001;
+	params.vendor = NS2_VID;
+	params.input_size = NS2_INPUT_SIZE;
+	params.output_size = NS2_OUTPUT_SIZE;
+	/* Our dev->info override in _probe() */
+	params.name = dev->info->alias;
+	params.product = dev->info->pid;
+	/* Discovered handles at runtime */
+	params.notify_handles = state.notify_handles;
+	params.notify_count = state.notify_count;
+
+	dev->bridge = gatt_uhid_new(client, &params);
+	if (!dev->bridge) {
+		error("switch2: failed to create GATT-UHID bridge");
+		return -EIO;
+	}
+
+	btd_service_connecting_complete(service, 0);
+	return 0;
+}
+
+static int switch2_disconnect(struct btd_service *service)
+{
+	struct switch2_device *dev = btd_service_get_user_data(service);
+
+	DBG("switch2: disconnect %s", device_get_path(dev->device));
+
+	gatt_uhid_free(dev->bridge);
+	dev->bridge = NULL;
+
+	btd_service_disconnecting_complete(service, 0);
+	return 0;
+}
+
+/*
+ * Plug in the plugin
+ */
+
+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)
+{
+	return btd_profile_register(&switch2_profile);
+}
+
+static void switch2_exit(void)
+{
+	btd_profile_unregister(&switch2_profile);
+}
+
+BLUETOOTH_PLUGIN_DEFINE(switch2, VERSION, BLUETOOTH_PLUGIN_PRIORITY_DEFAULT,
+						switch2_init, switch2_exit)
-- 
2.47.3


  parent reply	other threads:[~2026-04-03  8:56 UTC|newest]

Thread overview: 9+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-04-03  8:55 [PATCH BlueZ v3 0/6] BLE-HID/Nintendo Switch 2 support Martin BTS
2026-04-03  8:55 ` [PATCH BlueZ v3 1/6] shared/gatt: Add skip_secondary option for GATT client Martin BTS
2026-04-03 10:18   ` BLE-HID/Nintendo Switch 2 support bluez.test.bot
2026-04-03  8:55 ` [PATCH BlueZ v3 2/6] shared/gatt: Add timeout for secondary service discovery Martin BTS
2026-04-03  8:55 ` [PATCH BlueZ v3 3/6] device: Rename set_alias to btd_device_set_alias() Martin BTS
2026-04-03  8:55 ` [PATCH BlueZ v3 4/6] dbus-common: Add Gaming appearance class (0x2a) Martin BTS
2026-04-03  8:55 ` [PATCH BlueZ v3 5/6] plugins/gatt-uhid: Add generic GATT-to-UHID bridge Martin BTS
2026-04-03  8:55 ` Martin BTS [this message]
  -- strict thread matches above, loose matches on Subject: below --
2026-03-17 20:26 [PATCH BlueZ v3 0/6] BLE-HID/Nintendo Switch 2 support Martin BTS
2026-03-17 20:26 ` [PATCH BlueZ v3 6/6] plugins/switch2: Add Nintendo Switch 2 Controller plugin Martin BTS

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=20260403085555.23871-7-martinbts@gmx.net \
    --to=martinbts@gmx.net \
    --cc=hadess@hadess.net \
    --cc=linux-bluetooth@vger.kernel.org \
    --cc=luiz.dentz@gmail.com \
    --cc=vi@endrift.com \
    /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 a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox