Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 6 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,7 @@ Dolphin itself is [cross-platform](https://dolphin-emu.org/docs/faq/). Switch2Ki

Use the [C ABI and CMake integration](docs/switch2kit/cpp.md) to embed the same controller engine in a native host. The host does not need to be written in Swift and does not require the dashboard.

The [Dolphin and Cemu integrations](Integrations/Emulators/README.md) connect Switch2Kit to each emulator's existing SDL3 input backend, in process. The emulator owns discovery, Bluetooth permissions, and controller lifecycle; no network bridge, second SDL instance, or system virtual controller is required. The guide covers pinned source patches, builds, controller bindings, and motion-profile configuration.

These optional source integrations are maintained in this repository; do not assume an unmodified upstream emulator includes them. Enabling the backend requires macOS 15+ or the [Linux runtime dependencies](docs/switch2kit/linux.md). Builds with it disabled retain the emulator's other platforms and deployment targets.
The [Dolphin and Cemu integrations](Integrations/Emulators/README.md) connect Switch2Kit to each emulator's existing SDL3 input backend, in process. The emulator owns discovery, Bluetooth permissions, and controller lifecycle; no network bridge, second SDL instance, or system virtual controller is required. The guide covers pinned source patches, builds, controller bindings, and motion-profile configuration. These optional patches are maintained here, not supplied by unmodified upstream emulators; disabled builds retain upstream platforms and deployment targets.

## Library

Expand Down Expand Up @@ -62,9 +60,11 @@ On macOS, the host provides `NSBluetoothAlwaysUsageDescription` and, when sandbo

| Controller | Input | Rumble |
| --- | --- | --- |
| Switch 2 Pro Controller | Buttons, two sticks, motion, battery | Independent HD motors |
| Joy-Con 2, left or right | Buttons, stick, motion, optical sensor, battery | Single HD motor per unit |
| NSO GameCube | Buttons, two sticks, analog trigger travel and digital clicks, motion, battery | On/off motor and soft/strong firmware clips |
| Switch 2 Pro Controller | Buttons, two sticks, raw motion, battery | Independent HD motors |
| Joy-Con 2, left or right | Buttons, stick, raw motion, optical counters, battery | Single HD motor per unit |
| NSO GameCube | Buttons, two sticks, analog trigger travel and digital clicks, raw motion, battery | On/off motor and soft/strong firmware clips |

Calibrated SDL motion requires an explicitly selected [physical motion profile](docs/switch2kit/motion-profiles.md); no measured built-in profiles are supplied. See [controller and feature coverage](docs/switch2kit/coverage.md) for transport, output and physical-qualification boundaries.

[Library guide](docs/switch2kit/README.md) · [API](docs/switch2kit/api.md) · [SwiftUI](docs/switch2kit/swiftui.md) · [AppKit](docs/switch2kit/appkit.md) · [Bluetooth lifecycle](docs/switch2kit/bluetooth-lifecycle.md) · [Application actions](docs/switch2kit/actions.md)

Expand Down
30 changes: 16 additions & 14 deletions docs/protocol.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,17 @@ Controller 2, Joy-Con 2 L/R, and the NSO GameCube pad) over Bluetooth Low
Energy from a host that is **not** a Switch console.

This document covers connection, the command protocol, the input report
layout, motion/environmental sensors, rumble, LEDs, and pairing. It focuses on
what has been verified in practice, and calls out the platform quirks that
matter on macOS in particular.

> **Scope note.** NFC/amiibo is intentionally omitted here — that work is
> still in progress and will be published separately once it is fully worked
> out. Controller audio is covered in §10 to the extent it is currently
> understood (the wire framing is verified on real hardware; the codec is
> not yet identified). Everything else below is implemented and observed on
> real hardware.
layout, motion/environmental sensors, rumble, LEDs, and pairing. Retained
protocol observations are not a qualification report for the current library,
application or emulator integrations. See [controller and feature coverage](switch2kit/coverage.md)
for implemented behavior and the remaining physical acceptance.

> **Scope note.** Switch2Kit does not implement NFC/amiibo or headset audio.
> Section 10 records protocol observations, not an audio transport supplied by
> this repository. Raw motion counts are not calibrated SDL measurements;
> [explicit device profiles](switch2kit/motion-profiles.md) are required.
> Current automated tests do not establish physical gameplay, measured motion
> coefficients, or support for every firmware or controller edition.

Byte offsets are into the decrypted input report / command payloads.

Expand Down Expand Up @@ -185,8 +186,8 @@ calibrate with a figure-8 min/max per device).

```
0x00000001 Y 0x00000800 L-stick 0x00080000 D-Left
0x00000002 X 0x00001000 Home 0x00100000 SL (L)
0x00000004 B 0x00002000 Capture 0x00200000 SR (L)
0x00000002 X 0x00001000 Home 0x00100000 SR (L)
0x00000004 B 0x00002000 Capture 0x00200000 SL (L)
0x00000008 A 0x00004000 C 0x00400000 L
0x00000010 SR (R) 0x00010000 D-Down 0x00800000 ZL
0x00000020 SL (R) 0x00020000 D-Up 0x01000000 GR
Expand All @@ -195,8 +196,9 @@ calibrate with a figure-8 min/max per device).
0x00000400 R-stick
```

ZL/ZR are digital bits on every model except the NSO GameCube pad, which
reports true analog triggers at `0x3C`/`0x3D`.
ZL/ZR are digital bits, including the independent L/R clicks on the NSO
GameCube pad. GameCube additionally reports analog trigger travel at
`0x3C`/`0x3D`; travel does not imply a click.

---

Expand Down
8 changes: 4 additions & 4 deletions docs/switch2kit/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ The source's `///` comments document public declarations, including field units
| Ownership and observation | `Switch2ControllerManager`, `Switch2ControllerObservation`, `Switch2ControllerConfiguration`, `Switch2ManagerSnapshot` |
| Physical controllers | `Switch2ControllerID`, `Switch2Controller`, `Switch2ControllerModel`, `Switch2ControllerCapabilities` |
| Input | `Switch2ControllerState`, `Switch2Buttons`, `Switch2Stick`, `Switch2Trigger`, `Switch2Battery`, `Switch2Motion`, `Switch2RawVector3`, `Switch2OpticalState`, `Switch2Color` |
| Explicit motion conversion | `Switch2Vector3`, `Switch2MotionAxis`, `Switch2SensorCalibration`, `Switch2MotionCalibration`, `Switch2CalibratedMotion` |
| Explicit motion conversion and profiles | `Switch2Vector3`, `Switch2MotionAxis`, `Switch2SensorCalibration`, `Switch2MotionCalibration`, `Switch2CalibratedMotion`, `Switch2MotionProfile` |
| Lifecycle | `Switch2BluetoothState`, `Switch2DiscoveryState`, `Switch2DiscoveryMode`, `Switch2ConnectionState`, `Switch2DisconnectionReason`, `Switch2ControllerEvent`, `Switch2KitError` |
| Application actions | `Switch2ActionRouter`, `Switch2ActionBinding`, `Switch2ActionControl`, `Switch2ActionAxis`, `Switch2ActionSource`, `Switch2ActionEvent`, `Switch2NavigationAction` |
| Diagnostics | `Switch2LogLevel`, `Switch2LogCategory`, `Switch2LogRecord`, `Switch2LogHandler` |
Expand All @@ -24,23 +24,23 @@ Application actions run in-process in the library. System keyboard/mouse injecti

`buttons` is a 32-bit option set containing A/B/X/Y, D-pad, L/R/ZL/ZR, stick clicks, Plus/Minus/Home/Capture/C, GL/GR and handed Joy-Con SL/SR controls. Unknown bits survive value construction. Opposing D-pad bits may coexist; cancellation/remapping is host policy.

`leftStick` and `rightStick` are optional named two-dimensional vectors. Absence means no such physical stick, not `(0,0)`. Values are calibrated and normalized/clamped to `-1...1`, with positive x right and positive y up; no application dead zone or Joy-Con grouping rotation is applied. Calibration chooses the retained validated user/factory data and preserves the protocol's handed stick placement. Public constructors clamp finite coordinates and replace non-finite coordinates with zero.
`leftStick` and `rightStick` are optional named two-dimensional vectors. Absence means no such physical stick, not `(0,0)`. Values are calibrated and normalized/clamped to `-1...1`, with positive x right and positive y up; no application dead zone or Joy-Con grouping rotation is applied. Calibration chooses the retained validated user/factory data and preserves the protocol's handed stick placement. Public constructors clamp finite coordinates and replace non-finite ones with zero.

Each `Switch2Trigger` separates `isPressed` (ZL/ZR report bit) from optional analog `travel`. Only GameCube supplies travel; it is the retained raw byte divided by 255, in `0...1`. Travel and click are independent. Do not infer a click from a nonzero travel value, and do not silently turn nil travel into a claim of an analog sensor.

`Switch2Battery.millivolts` is voltage, not percentage; zero/unavailable becomes nil. `estimatedCharge` is a rough clamped `0...1` voltage estimate using 3.30–4.15 V, not calibrated fuel state or battery health. Charge-state bits remain a raw UInt8. Current remains signed Int16 counts; positive indicates charging, but conversion to amperes is not qualified.

`Switch2Motion` exposes named accelerometer/gyroscope/magnetometer `Switch2RawVector3` values in signed 16-bit sensor-native counts. These are **not** calibrated acceleration, angular velocity, gravity-removed motion, world axes or orientation quaternions. Model/physical orientation determines axes. Retained research associates magnetometer counts with 0.15 µT/count, but the API deliberately exposes raw counts. IMU die temperature is the existing `25 + raw/127` Celsius estimate, not ambient temperature. A present all-zero sample does not prove that hardware sensing is active. Motion is nil when the selected configuration does not request it.

[Explicit motion calibration](motion.md) converts raw counts using host-supplied measured bias, gain and signed axis order. Acceleration output is in m/s² and retains gravity; angular velocity is in rad/s. No model-specific coefficients, sample timing, orientation fusion or SDL sensor registration are inferred. Raw state remains unchanged.
[Explicit motion calibration](motion.md) converts raw counts using host-supplied measured bias, gain and signed axis order. Acceleration output is in m/s² and retains gravity; angular velocity is in rad/s. No model-specific coefficients, sample timing, orientation fusion or SDL sensor registration are inferred. Raw state remains unchanged. The separate [physical profile and SDL adapter path](motion-profiles.md) performs explicit selection and sensor registration; a converter alone does not establish physical calibration.

Joy-Con optical telemetry exposes UInt16 absolute x/y counters wrapping modulo 65536 and raw surface-quality/lift counts. These are not cursor pixels or millimeters. Compute wrap-aware deltas, interpret orientation in the host, and treat gaps/reconnects as a reset rather than a large pointer jump.

`receivedAt` is host monotonic seconds since boot, not controller time or a wall clock. `sequence` starts at one per physical connection; synthetic host values may use zero. `connectedAt` is host wall-clock readiness time. Do not compare monotonic times between machines or boots.

## Identity, metadata and capabilities

`Switch2ControllerID` wraps CoreBluetooth's locally scoped UUID, supports Codable restoration, and is potentially identifying data. It is not a serial, player slot or authentication proof. `connectionID` is a new transient token for each connection; do not persist it. Names are safe verified model labels; host custom names remain host state. Optional body/button colors contain eight-bit sRGB components, without alpha.
`Switch2ControllerID` is a locally scoped, Codable identifier and potentially identifying data. macOS uses CoreBluetooth's UUID; Linux derives a stable UUID from the adapter address, remote address type and remote address. Changing adapters or rotating a remote address changes Linux identity. See [Linux identity and ownership](linux.md). It is not a serial, player slot or authentication proof. `connectionID` is a new transient token for each connection; do not persist it. Names are safe verified model labels; host custom names remain host state. Optional body/button colors contain eight-bit sRGB components, without alpha.

Serial numbers are nil by default. A legacy host can explicitly opt in through configuration to preserve existing serial-keyed mappings; the library still does not log them. `capabilities` describes understood physical-controller functions, not whether a game, browser or output adapter supports them. All models provide `.rumble` through `playRumble`. All models provide `.continuousRumble` (on/off for GameCube); GameCube also provides `.rumblePresets`. A paired Joy-Con grip is a host abstraction over two capability sets.

Expand Down
99 changes: 99 additions & 0 deletions docs/switch2kit/coverage.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
# Controller and feature coverage

This is the implemented software contract, not a physical-controller acceptance
report. Recognition uses Nintendo manufacturer data and the four product IDs in
[`Switch2ControllerModel`](../../Sources/Switch2Kit/Public/ControllerTypes.swift).
A supported controller can have usable buttons while its motion is unavailable.

| Physical model | Product ID | Sticks and triggers | Raw optical input | Rumble contract |
| --- | --- | --- | --- | --- |
| Pro Controller 2 | `0x2069` | Two sticks; digital ZL/ZR | None exposed | Independent HD motors; sustained and duration-controlled effects |
| Joy-Con 2 (L) | `0x2067` | Left stick; digital trigger | Wrapping x/y counters, lift and surface quality | One HD motor |
| Joy-Con 2 (R) | `0x2066` | Right stick; digital trigger | Wrapping x/y counters, lift and surface quality | One HD motor |
| NSO GameCube | `0x2073` | Two sticks; analog travel separate from digital L/R clicks | None exposed | On/off motor plus separate device-timed soft/strong clips |

The [Swift and C interfaces](cpp.md) expose the understood buttons, battery
telemetry, player LEDs, and raw acceleration/gyro/magnetometer telemetry for these
models. Capability bits describe understood functions, not a sensor enablement
readback or proof of a working physical unit. Battery charge is a rough voltage
estimate; magnetic counts are not a calibrated compass or heading.

## Motion and native emulator input

The existing shared adapter places controllers in the host's own SDL3 instance.
Buttons, sticks, hotplug, physical identity and game rumble do not need a motion
profile. Pro/Joy-Con support amplitude-controlled rumble; GameCube SDL rumble uses
its dedicated on/off motor channel, including stop and duration handling. Its
separate short-feedback API selects device-timed firmware clips; those clips are
not arbitrary-duration effects and are not repeated to emulate continuous rumble.
See the [rumble contract](../rumble.md) for the distinction and validation limits.

Calibrated accelerometer/gyro sensors require a compatible user-selected
[physical profile](motion-profiles.md), explicit sensor enablement and fresh,
continuous reports. No built-in measured coefficients are supplied. The
[calibration tool](../../tools/motion-calibration/README.md) acquires six stationary
acceleration poses and gyro zero-rate bias. Gyro scale and positive-rotation axes
still require known-rate measurements or controller-specific configuration
evidence; neither follows from holding a controller still. The profile is bound
to the physical ID, model, report configuration and holding orientation, not a
player slot or transient SDL instance.

Dolphin and Cemu provide profile selection, status, persistence and actual motion
processing in their pinned integrations. These are source patches maintained in
[this repository](../../Integrations/Emulators/README.md), not upstream emulator
PRs. Independent Joy-Con halves are separate native devices and sensor streams.
The dashboard's logical pairing feature does not silently fuse their native
motion clocks or turn them into one SDL device.

Optical counters remain available to Swift/C consumers. The macOS dashboard has
an opt-in system-pointer adapter; the native Dolphin/Cemu adapter does **not**
inject system mouse movement or map optical counters to game-specific aiming.
Applications can interpret raw optical input without confusing it with calibrated
accelerometer/gyro events. No native emulator input path requires the dashboard,
Accessibility, synthetic keyboard input or system virtual HID.

## Platforms and hardware features outside this implementation

The reusable engine has live macOS/CoreBluetooth and experimental Linux/BlueZ
backends. Linux supports native Swift/C/SDL hosts, not the macOS dashboard. See
[Linux requirements and qualification limits](linux.md). Windows and Android
have no supported Switch2Kit backend. USB-driver work under
[`sdl/`](../../sdl/README.md) is separate from these Bluetooth transports; it does
not make USB a transport of the reusable Swift library.

Nintendo's [accessory descriptions](https://www.nintendo.com/us/gaming-systems/switch-2/accessories/)
include Pro Controller 2 amiibo/headset features, C-button GameChat access, and
GL/GR controls on the Joy-Con 2 charging grip. Switch2Kit exposes C and GL/GR
report bits as bindable controls; it does not provide Nintendo GameChat,
NFC/amiibo operations, a headset audio transport, console-side remapping storage,
or controller firmware updates. Those are not hidden behind a motion setting.
Charging-grip operation needs accessory-specific physical acceptance; exposing
its report bits is not an end-to-end grip qualification.

Recognition is not based on a color or marketing edition. A variant reporting a
recognized model ID uses that model's code path; unmeasured firmware, sensor
configuration or holding changes still need requalification. An unfamiliar ID is
not accepted by guessing its protocol. First-generation Switch controllers and
third-party controllers are outside this four-ID Bluetooth backend, even when
[Nintendo lists console compatibility](https://en-americas-support.nintendo.com/app/answers/detail/a_id/68426/p/897/c/182).
Their host's ordinary input backends remain available.

## What verification establishes

Automated tests cover synthetic profiles and fake controller boundaries feeding
the real Swift hub, C ABI, pinned SDL and emulator motion components. Linux tests
also exercise the real BlueZ transport through an isolated synthetic D-Bus service.
Native CI builds and inspects full Dolphin/Cemu applications on Linux and on
macOS arm64/x86_64. The macOS [fresh-runner launch suite](../../tests/emulator-launch/README.md)
observes configured onscreen GUI startup and normal quit with developer dependency
paths inaccessible. It does not complete first-use dialogs or test a stock clean
Mac, Gatekeeper/notarization, Bluetooth, or gameplay. Linux build, installation and
loader checks do not establish interactive GUI or gameplay acceptance.

No measured per-controller profiles or new physical acceptance records are supplied.
Before claiming hardware qualification, execute the existing calibration and
[physical acceptance procedure](../../tools/motion-calibration/README.md#physical-acceptance):
validate gravity/bias/positive rotations, both emulator motion paths, rapid input
edges and trigger clicks/travel, reversed identical-controller reconnects,
Bluetooth and sleep/wake transitions, shutdown, rumble and sustained latency,
drops and memory. Record only observed outcomes and explicitly chosen captures.
Loading
Loading