From 2ddbb0bb4b5c88d59f33e9f9d8bf6c8aea992c50 Mon Sep 17 00:00:00 2001 From: Johnny D Date: Thu, 17 Sep 2026 12:16:03 -0400 Subject: [PATCH] Document controller coverage and correct rail-button reference bits Separate four-ID Bluetooth support, raw telemetry, explicit calibrated SDL motion and dashboard-only outputs. State the implemented GameCube clip limit, independent Joy-Con streams, charging-grip/edition qualification, and excluded audio/NFC/GameChat/firmware transports without inventing hardware validation. Correct swapped left Joy-Con SL/SR labels in the protocol reference to match the existing public API; retain GameCube digital trigger clicks alongside analog travel. Add a repository regression that fails against the previous documentation. Remove the blanket claim that all documented functions are implemented and physically tested, and the unsupported promise of future NFC publication. Index the existing motion-profile API. Documentation and repository tests only. Local source/link, research-boundary and application-identity checks pass. No production controller code, coefficients, acceptance records or packaging changes. --- README.md | 8 +-- docs/protocol.md | 30 ++++++----- docs/switch2kit/api.md | 6 +-- docs/switch2kit/coverage.md | 90 +++++++++++++++++++++++++++++++++ tests/repository/test_layout.py | 10 ++++ 5 files changed, 124 insertions(+), 20 deletions(-) create mode 100644 docs/switch2kit/coverage.md diff --git a/README.md b/README.md index 6115821..2761119 100644 --- a/README.md +++ b/README.md @@ -42,9 +42,11 @@ The host provides `NSBluetoothAlwaysUsageDescription` and, when sandboxed, `com. | 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 | 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 | 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) diff --git a/docs/protocol.md b/docs/protocol.md index 8534895..cc124c1 100644 --- a/docs/protocol.md +++ b/docs/protocol.md @@ -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. @@ -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 @@ -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. --- diff --git a/docs/switch2kit/api.md b/docs/switch2kit/api.md index 6951d4c..76ab0f9 100644 --- a/docs/switch2kit/api.md +++ b/docs/switch2kit/api.md @@ -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` | @@ -24,7 +24,7 @@ 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. @@ -32,7 +32,7 @@ Each `Switch2Trigger` separates `isPressed` (ZL/ZR report bit) from optional ana `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. diff --git a/docs/switch2kit/coverage.md b/docs/switch2kit/coverage.md new file mode 100644 index 0000000..c423960 --- /dev/null +++ b/docs/switch2kit/coverage.md @@ -0,0 +1,90 @@ +# 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 | Device-timed soft/strong clips only | + +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 Pro/Joy-Con game rumble do not need +a motion profile. GameCube is not advertised as an arbitrary-duration cancellable +SDL rumble device; the short-feedback API can select its firmware clips, but there +is no qualified stop command to invent or repeat into a continuous effect. + +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 dashboard has an +opt-in system-pointer adapter; the native Dolphin/Cemu adapter does **not** inject +system mouse movement or map optical counters to a game-specific aiming mode. +Applications can explicitly 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. + +## Hardware features outside this implementation + +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. USB-driver work under +[`sdl/`](../../sdl/README.md) is separate from the CoreBluetooth engine; it does not +make USB a transport of the reusable Swift library. + +## 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. Native CI +also builds both full applications for arm64 and x86_64 and inspects their +bundled dependencies. The [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. + +No measured per-controller profiles or 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. diff --git a/tests/repository/test_layout.py b/tests/repository/test_layout.py index e8af9ed..0d7d04b 100644 --- a/tests/repository/test_layout.py +++ b/tests/repository/test_layout.py @@ -39,6 +39,16 @@ def test_documentation_links(self): with self.subTest(file=str(file.relative_to(ROOT)), link=link): self.assertTrue((file.parent / unquote(link)).exists(), f"Missing link: {link}") + def test_documented_rail_bits_match_public_api(self): + source = (ROOT / "Sources/Switch2Kit/Public/ControllerTypes.swift").read_text() + bits = {name: int(value.replace("_", ""), 16) for name, value in re.findall( + r"public static let (s[rl][LR]) = Self\(rawValue: (0x[0-9a-fA-F_]+)\)", source)} + notes = (ROOT / "docs/protocol.md").read_text() + documented = {label: int(value, 16) for value, label in re.findall( + r"(0x[0-9a-fA-F]{8})\s+(S[LR] \([LR]\))", notes)} + self.assertEqual(documented, {"SL (L)": bits["slL"], "SR (L)": bits["srL"], + "SL (R)": bits["slR"], "SR (R)": bits["srR"]}) + def test_source_only_tree(self): tracked = subprocess.check_output(["git", "ls-files", "-z"], cwd=ROOT).decode().split("\0") for path in filter(None, tracked):