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(¶ms, 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, ¶ms);
+ 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
next prev 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