Switch 2 Phase 1 codec and replay boundary¶
The subsequent exact USB/BLE Pro Common05 profile projection is documented in
switch2-pro-profile-input.md. It preserves the
12-bit source observation across the existing mapping pipeline and keeps the C
button distinct from DualSense Mute.
Status: read-only protocol foundation plus a pure physical USB admission/input lifetime boundary. Nothing in this module performs OS discovery, connects or registers a runtime controller, pairs, reads from, writes to, or publishes runtime controller state. The command codec can construct a closed set of volatile startup requests, but it owns no transport and cannot send them.
That status describes the original Phase 1 tranche. Later documents own the production USB/BLE composition. In particular, the production BLE adapter now uses the calibration parser and precedence rules described below.
Scope¶
DS4Windows/DS4Control/Switch2 contains:
- strict USB framing: one report-ID byte plus a 63-byte body;
- strict BLE framing: the exact primary-service UUID, characteristic UUID,
required
Read | Notifyproperties, and one 63-byte body; - raw decoders for common report
0x05and the evidenced basic fields of model-specific reports0x07,0x08, and0x09; - lossless common-report retention and re-encoding, including every opaque byte range, for deterministic replay/property tests;
- an exact-identity, transport-neutral input session and versioned canonical frame retaining the device/transport generations, completion QPC timestamp, 12-bit sticks, raw counters, unknown fields, and an owned copy of every body byte; the bounded Pro USB replay projection validates through this same core;
- a body-relative descriptor and both zero-copy replay slice and owned-frame copy helpers for model-specific motion bytes, whose format remains opaque;
- privacy-safe advertisement classification as no remembered host, this host, or a foreign host, without returning the advertised address;
- a bounded, generation-fenced discovery candidate registry using per-scan
keyed peer tokens, documented in
switch2-bluetooth-discovery-boundary.md; - nine-byte factory and marked eleven-byte user stick-calibration parsers with model-aware primary/secondary storage metadata plus a separate fail-closed adoption gate for sentinel and out-of-domain calibration records;
- immutable, versioned, input-only fixture envelopes and deterministic replay;
- raw modular counter deltas plus first/forward/duplicate/backward-or-out-of- order classification; and
- a pure, value-owned Joy-Con pair reducer with explicit pair epoch, one clock domain, bounded skew, stale-half reporting, generation-fenced loss/replacement, and terminal split behavior; and
- an exact, side-effect-free Pro USB composite admission and canonical input
lifetime adapter, documented in
switch2-physical-input-boundary.md, that has noHidDevice, path, handle, discovery, or live-I/O dependency.
The decoder returns raw values. It does not apply calibration, dead zones, axis transforms, sensor scaling, fusion, mappings, or virtual-pad publication. The dictionary-based replay engine is test/offline infrastructure and is not a live hot-path design.
USB ingress deliberately accepts only Pro Controller 2 report 0x05. Reports
0x07, 0x08, and 0x09 remain available only through their exact BLE
characteristic identities. Public sources describe 0x09 as the Pro
model-specific report, but the project-owned Pro USB bcdDevice 0x0201
evidence contains only 0x05, and the licensed SDL USB initialization
explicitly selects 0x05. Wired/Charging-Grip Joy-Con 2 is not promoted from
the BLE layout without its own evidence. Treating a model-specific BLE body as
a USB report merely because it also has 63 bytes would be a length-based
protocol guess.
Byte-level input specification¶
All offsets below are relative to the 63-byte body. For USB, add one to obtain the offset in the complete 64-byte packet. BLE notifications omit the report ID and already consist of the body.
Common report 0x05¶
| Body offset | Size | Phase 1 interpretation | Applicability/status |
|---|---|---|---|
0x00 |
4 | little-endian raw counter | all supported models |
0x04 |
4 | little-endian button bits | all supported models |
0x08 |
2 | uninterpreted | unknown |
0x0A |
3 | packed 12-bit left-stick slot | left Joy-Con 2 and Pro; absent-side Joy-Con bytes may be garbage |
0x0D |
3 | packed 12-bit right-stick slot | right Joy-Con 2 and Pro; absent-side Joy-Con bytes may be garbage |
0x10 |
8 | four raw little-endian mouse words | Joy-Con 2 only; not exposed as a Pro control |
0x18 |
1 | uninterpreted | observed zero in public research |
0x19 |
6 | three raw signed magnetometer words | feature-dependent |
0x1F |
2 | little-endian battery millivolts | raw |
0x21 |
2 | little-endian battery-current field | raw; scale/direction not promoted |
0x23 |
1 | uninterpreted | no charging-state meaning assigned |
0x24 |
6 | uninterpreted | includes the observed 0x29 == 0x01 byte |
0x2A |
4 | little-endian motion timestamp | units unknown |
0x2E |
2 | temperature raw bits (ushort) |
signedness and scale unknown |
0x30 |
6 | three raw signed accelerometer words | scale/orientation unknown |
0x36 |
6 | three raw signed gyroscope words | scale/orientation unknown |
0x3C |
2 | uninterpreted for supported models | analog triggers only on unsupported NSO GameCube controller |
0x3E |
1 | uninterpreted/reserved | unknown |
Dynamic battery status¶
The common-report power layout follows the corroborated Switch2Connect
implementation: voltage is the little-endian word at 0x1F..0x20, current is
the little-endian word at 0x21..0x22, and 0x23 remains opaque. Earlier
worktree code incorrectly split byte 0x21 into a purported charging flag and
decoded current one byte late; strict round-trip and boundary tests now pin the
correct layout. Current direction and charging semantics are not inferred.
Switch2BatteryStatus adapts Switch2Connect's validated 2.5..5.0 V input
range and its three visible bands: greater than 3.25 V is high, greater than
3.125 V is medium, and the remaining valid range is low. DS4Windows' legacy
controller API accepts only a percentage, so the bands are exposed there as
stable 90, 50, and 10 compatibility markers. Those values are UI
categories, not a lithium state-of-charge estimate; in particular, high is not
reported as fully charged. Invalid readings preserve the last valid status.
Pro and standalone Joy-Con runtimes publish their own status. Joined Joy-Cons
retain both physical statuses and present the lowest currently valid half to
the legacy combined-controller UI. BatteryChanged is emitted only after the
corresponding input report has been published and only when the visible band
changes, so same-band voltage noise cannot add work to the input hot path.
Charging remains false until captures establish a direction/flag contract.
The four opaque regions (0x08..09, 0x18, 0x24..29, and 0x3C..3E)
are retained as raw little-endian values. Together with the named raw fields,
they cover all 63 body bytes. TryEncodeCommon05 reconstructs the exact body
and exists solely as a losslessness/replay invariant; it is not a controller
output command. Deterministic property tests round-trip 20,000 arbitrary exact
bodies byte for byte.
The HasLeftStick, HasRightStick, and HasMouseData properties are the
model-applicability boundary. Raw absent/inapplicable slots remain available
for forensic comparison but must not be published as controls.
Model-specific reports¶
| Report/model | Basic fields | Opaque motion descriptor |
|---|---|---|
0x07, left Joy-Con 2 |
counter 0x00 (8-bit), power 0x01, buttons 0x02..03, packed stick 0x05..07 |
length byte at body offset 0x0F, payload region 0x10..37 (40-byte capacity) |
0x08, right Joy-Con 2 |
counter 0x00 (8-bit), power 0x01, buttons 0x02..03, packed stick 0x05..07 |
length byte at body offset 0x0F, payload region 0x10..37 (40-byte capacity) |
0x09, Pro Controller 2 |
counter 0x00 (8-bit), power 0x01, buttons 0x02..04, sticks 0x05..0A |
length byte at body offset 0x0E, payload region 0x0F..36 (40-byte capacity) |
Declared motion length above 40 is rejected. Lengths 0, 30, and 40 are
marked publicly observed; other bounded lengths stay opaque. The decoded
struct owns only the body-relative descriptor. Motion bytes remain owned by
the caller or immutable fixture, and TrySliceOpaqueMotionBody returns the
declared span after validating the exact body length.
Pro Controller 2 USB canonical projection¶
The offline projector accepts only the exact identity
VID 057E / PID 2069 / bcdDevice 0201, model ProController2, USB transport,
report 0x05, and fixture firmware evidence unknown. This is intentionally
narrow. bcdDevice is a USB device-descriptor revision, not a firmware version;
the passive audit did not query firmware. A fixture claiming a numeric firmware
revision is rejected until that revision has its own source/capture review.
The legacy replay projection uses the replay envelope's monotonic host timestamp for ordering and validates its packet through the same canonical builder as a future live transport. A live input session instead requires its caller to provide one nonzero device generation, transport generation, QPC frequency, and read-completion QPC timestamp. The session rejects descriptor drift and timestamp regression. Reset accepts only an advanced lifetime fence, then clears only the controller-counter baseline so a reconnect cannot inherit device sequence state. It preserves the accepted completion-time baseline because QPC remains one absolute monotonic host clock across reconnects.
The frame owns all 63 body bytes in bounded value storage, so reuse of a USB or
BLE read buffer cannot mutate an already-published frame. It carries the 32-bit
controller counter and modular delta only as raw movement; +4 does not mean
four lost reports and does not establish a report rate. A backward/out-of-order
observation is retained and classified but does not replace the last accepted
counter baseline. The body 0x2A motion timestamp is retained raw because its
clock units and firmware behavior are not established.
Pro button facts use the common report's original bit positions. Physical face labels are included only to make the positional names unambiguous:
| Body byte/bit | Canonical control | Physical label |
|---|---|---|
0x04.0 / .1 / .2 / .3 |
face west / north / south / east | Y / X / B / A |
0x04.6 / .7 |
right shoulder / right digital trigger | R / ZR |
0x05.0 / .1 |
back / start | Minus / Plus |
0x05.2 / .3 |
right-stick / left-stick click | R-stick / L-stick |
0x05.4 / .5 / .6 |
guide / capture / C | Home / Capture / C |
0x06.0 / .1 / .2 / .3 |
D-pad down / up / right / left | same |
0x06.6 / .7 |
left shoulder / left digital trigger | L / ZL |
0x07.0 / .1 |
right / left paddle | GR / GL |
The proven-bit mask is 0x03CF7FCF. Every other set bit remains available as
UnknownButtonBits; it is never silently cleared or promoted to a control.
Sticks remain packed-source 12-bit X/Y values. The canonical session retains
the raw values alongside signed center offsets and effective positive/negative
ranges. A structurally valid factory record is adopted only when every value is
interior and both endpoints remain inside the 12-bit domain; missing, malformed,
sentinel, and out-of-domain records receive explicit statuses and a symmetric
neutral fallback. Y-axis orientation and final normalization remain mapping
concerns, so this tranche does not reduce input through 8-bit DS4State or
invent an axis convention. Each calibration snapshot is bound to its physical
device generation; advancing that generation requires a newly bound snapshot,
while a transport-only reset must retain the structurally identical snapshot.
Pure Joy-Con pair reducer¶
Pairing is deliberately separate from discovery and transport I/O. The reducer accepts only exact BLE left/right canonical frames carrying the caller's coordinated nonzero pair epoch. It joins halves only when their QPC frequencies match and completion-time skew is within the configured microsecond budget. Otherwise it names the stale half without discarding either value.
A newer generation may replace one half. An older generation or timestamp is rejected without changing state, and a loss event removes a half only when both device and transport generations exactly match the stored frame. Loss retains a per-side lifetime tombstone, so buffered input from the retired or any older lifetime cannot resurrect a missing half; rejoining that side must advance its lifetime and preserve absolute QPC chronology. Split clears both halves and is terminal for that pair epoch. Rejoining after a split requires a fresh state with a new epoch. This is a deterministic merge primitive, not a claim that Windows discovery, ownership, reconnect, or profile integration is implemented.
Factory and user stick calibration¶
An exact nine-byte record contains three packed X/Y pairs. This implementation
uses the licensed SDL ordering: center at bytes 0..2, positive/maximum range
at 3..5, and negative/minimum range at 6..8. Switch2Connect v2.7 reverses
the latter two semantic labels; no asymmetric project-owned hardware golden
supports that reversal, so it is not treated as authority.
Addresses name physical storage slots, not logical axes:
| Model and logical side | Storage slot | Address |
|---|---|---|
| Joy-Con 2 left / left | primary | 0x130A8 |
| Joy-Con 2 right / right | primary | 0x130A8 |
| Pro Controller 2 / left | primary | 0x130A8 |
| Pro Controller 2 / right | secondary | 0x130E8 |
Read-only user calibration is now admitted only when the eleven-byte record
starts with little-endian magic 0xA1B2 and its nine-byte payload passes the
same bounded-range adoption gate as factory data. A valid user record overrides
factory for that side. An absent marker, malformed payload, out-of-domain span,
or optional-read failure preserves the validated factory record.
| Model and logical side | User slot | Address |
|---|---|---|
| Joy-Con 2 left / left | primary | 0x1FC040 |
| Joy-Con 2 right / right | primary | 0x1FC040 |
| Pro Controller 2 / left | primary | 0x1FC040 |
| Pro Controller 2 / right | secondary | 0x1FC080 |
Current upstream SDL (c71abd08605b8bb7078372307a93274725c99fe0) and the
independent GPL-2.0-or-later hid-nintendo2 implementation
(32a981ea7f916f1792a7e35aa0ecf79063ec4001) agree on 0x1FC080 and the
0xA1B2 marker. ndeadly/switch2_controller_research@d1c5a7f7 confirms the
primary slot and eleven-byte record but lists the secondary slot at
0x1FC060. Switch2Connect v2.8 also uses 0x1FC060. That conflicting address
is not queried; tests pin 0x1FC080 so it cannot be reintroduced silently.
The production Pro USB owner performs the same factory/user precedence after the five volatile startup commands and before publishing input. All four reads run synchronously through the already-owned MI_01 bulk OUT/IN command lane; there is no second handle, competing reader, polling worker, or runtime hot-path work. A codec-valid but absent/invalid user marker preserves factory data. A missing or invalid factory record retains the established centered wired fallback. An uncertain native command outcome retires/quarantines the shared command lifetime instead of continuing with a potentially misassociated response.
Byte-level HD-rumble codec¶
Status: implemented and offline-test verified. The codec remains a pure byte boundary, but production runtime owners now compose it with generation-scoped USB and Bluetooth physical writers. The VIIPER ns2pro virtual-output boundary also uses it for strict validation and lossless semantic retention.
One five-byte subframe is a little-endian 40-bit value with four contiguous 10-bit raw fields:
bits 0.. 9 oscillator 0 control
bits 10..19 oscillator 0 amplitude
bits 20..29 oscillator 1 control
bits 30..39 oscillator 1 amplitude
The control name is deliberate. Licensed upstream SDL interprets all ten
bits as a frequency code and calls oscillator 0 high/oscillator 1 low.
Switch2Connect and the provenance-tainted PadForge BLE experiment instead split
the top bit into a tone flag and reverse the band labels. The same bytes do not
resolve those semantics. Runtime adapters must wait for project-owned
single-field spectral basis tests; the pure codec preserves all bits without
choosing a theory.
One actuator group is exactly 16 bytes: 0x50 | counter followed by three
five-byte subframes. The strict USB codec uses one modulo-16 counter and:
- Joy-Con report
0x01: one group at bytes1..16, bytes17..63zero; - Pro report
0x02: independent left/right groups at1..16and17..32, bytes33..63zero.
Strict Pro decode rejects a non-0x5? group header, differing left/right
counter nibbles, wrong report ID/length, or nonzero reserved tail. It does not
require successive reports to advance by one; cadence/duplicate/skip telemetry
belongs to the eventual transport. The low/high compatibility values retain
SDL's licensed amplitude and control-code basis. For a source that provides one
sustained motor state rather than three temporal samples, DS4Windows repeats
that state in all three subframes. This follows the GPL-3.0 Switch2Connect
fill-forward behavior audited at commit
61ac6642ce12fe7217e38a860b14863b18ca7e28 and avoids turning a continuous
source into a one-subframe pulse. All destinations are completely overwritten.
The permissively licensed source is upstream SDL at
c71abd08605b8bb7078372307a93274725c99fe0,
whose audited release-3.4.x file is byte-identical. Capture-backed facts
corroborate the independent groups and matching counters, but that repository
has no license and supplied no code or prose.
Four-actuator synthesis policy¶
Switch2HdRumbleFeedbackTranslator provides an allocation-free, pure
translation from the versioned canonical four-actuator frame into independent
left and right HD-rumble groups. It does not perform I/O, choose a transport
envelope, own a sequence counter, schedule a cadence, or authorize physical
output.
The body-motor portion follows the pinned zlib-licensed SDL compatibility
basis exactly: control codes 0x187 and 0x112, a 16-bit amplitude ceiling of
29000, and the same two integer truncation points before packing the 10-bit
amplitude (scaled16 >> 6, maximum packed value 453). The resulting sustained
state is repeated in all three subframes. Body-low and body-high are mirrored
to both physical sides as SDL does.
The impulse-trigger portion is explicitly a project conversion, not a claim of
a physical trigger actuator. A left trigger controls only the high-frequency
field in the left encoded group; a right trigger does the same only in the
right encoded group. The low-frequency field remains the body lane. Dynamic
mode maps the nonzero Xbox trigger intensity monotonically from 300 through
481 Hz; fixed mode maps profile levels 1 through 10 across the same range, and
the separate 1-through-10 strength setting remains bounded by the physical
10-bit field. The profile setting is enabled by default so all four decoded
Xbox feedback lanes are represented, and can be disabled for SDL body-only
compatibility. Hardware sidedness remains a physical basis-test gate. High-band
overlap uses bounded saturating addition: body-high and side-local impulse
contributions are both retained, their 10-bit sum is capped at 1023, and
wraparound is impossible. Total physical energy still requires measurement.
Results identify either SDL body
compatibility or side-local impulse approximation and retain the source
sequence, device generation, transport generation, ownership epoch, and
Neutral/Stop distinction. Source identity, timestamp, and TTL are also retained
so the eventual sole writer must re-check freshness immediately before physical
delivery rather than treating translation-time freshness as a durable grant.
Expired or malformed source frames fail closed;
Neutral and Stop synthesize the licensed fixed-control compatibility basis with
zero amplitudes. This is exact logical amplitude neutral, not yet a claim about
physical stop/watchdog behavior.
DualSense PCM does not use the sustained fallback. Its 32-sample, approximately
10.7 ms stereo window is split chronologically into three slices. Each side and
slice independently contributes RMS low-band energy and first-difference
transient high-band energy to the matching Switch 2 subframe. Compatibility
motors are composed with PCM rather than being discarded by a silent valid
carrier. Supported DualSense trigger modes (0x01, 0x02, 0x21, 0x22,
0x23, 0x25, 0x26, and 0x27) are decoded into a bounded side-local
three-region envelope while the matching digital Switch 2 trigger is held.
The source frequency byte is retained as a bounded carrier hint for vibrating
modes; position regions select bounded carriers for resistance/break modes.
This preserves available tactile detail but cannot reproduce adaptive
resistance. Unknown, off, malformed, or empty programs fail closed. Native
Switch 2 groups bypass synthesis and retain all three subframes and oscillator
fields exactly.
The production composition now routes canonical Xbox feedback through a generation-scoped state-lane pump to exactly one physical writer per transport (or one coordinated writer per Joy-Con half). Bluetooth writes use the source-corroborated 17-byte Joy-Con or 33-byte Pro envelope and destination- owned modulo-16 counters. Successful delivery commits the counter; an uncertain write retains the identical payload for bounded retry. Runtime retirement seals publication and requires terminal neutral before it releases the transport. DS4Windows profile/macro rumble and test-preview rumble also use fixed origins in this same owner; withdrawing preview produces its exact Stop before a lower-priority source can resume.
This proves deterministic mapping, ownership, and lifecycle behavior in offline tests. Switch 2 has no physical trigger actuator, and no claim is made that the side-local conversion is perceptually equivalent to Xbox impulse triggers. Spectral sidedness, safe amplitude, onset/stop timing, accepted sustained cadence, controller watchdog behavior, thermal limits, and preference tuning remain hardware gates. The code therefore makes no 500 Hz, energy-equivalence, or hardware-delivery claim until the authorized controller/radio matrix runs.
VIIPER's 34-byte virtual output envelope is independently decoded as left
group, right group, flags, and player-LED mask. Rumble requires two valid group
headers with matching counters. When the rumble flag is clear, all 32 rumble
bytes must be zero; when the LED flag is clear, the LED mask must be zero.
Unknown flags fail closed. DS4Windows deliberately does not max-fold these
bytes into legacy motor magnitudes: doing so would mix the 0x5? header and
frequency/control fields with amplitude. A physical Switch 2 target now
preserves the exact decoded oscillator groups through its authenticated output
lifetime. Other physical families still receive no guessed reduction from
these fields. LED-only traffic never changes rumble, and rejection does not
synthesize a stop. For a physical Switch 2 target, player-indicator state uses
the same virtual-output lifetime: BLE carries any exact four-bit mask while
USB remains restricted to its six capture-backed commands.
Bounded USB command replay codec¶
Status: source/replay and offline-test verified; the exact volatile startup,
battery, and player-LED paths are also verified on the project-owned
057E:2069 / bcdDevice 0201 controller. Switch2UsbCommandCodec contains no
transport, device opening, pairing, firmware mutation, runtime wiring, or
arbitrary-command forwarding. Its memory surface is restricted to four
read-only calibration records. It admits only these pinned USB forms:
| Operation | Exact request | Exact response shape |
|---|---|---|
Enable USB HID reports (03:03) |
03 91 00 03 00 04 00 00 01 00 00 00 |
03 01 00 03 00 f8 00 00 01 00 00 00 |
Select common input report (03:0A) |
03 91 00 0a 00 04 00 00 05 00 00 00 |
03 01 00 0a 00 f8 00 00 |
Set or enable the closed 0x27 feature mask (0C:02/04) |
0c 91 00 SS 00 04 00 00 27 00 00 00 |
0c 01 00 SS 00 f8 00 00 00 00 00 00 for admitted bcdDevice 0x0201 hardware |
Get raw battery voltage (0B:03) |
0b 91 00 03 00 00 00 00 |
0b 01 00 03 HH AA 00 00 VV VV 00 00 |
Player LED 1 through 4, all on, or all off (09:01..06) |
09 91 00 SS 00 00 00 00 |
09 01 00 SS HH AA 00 00 |
Factory primary/secondary calibration (02:04) |
02 91 00 04 00 08 00 00 09 7e 00 00 AA AA AA AA |
02 01 00 04 10 78 00 00 09 00 00 00 AA AA AA AA plus exactly 9 bytes |
User primary/secondary calibration (02:04) |
02 91 00 04 00 08 00 00 0b 7e 00 00 AA AA AA AA |
02 01 00 04 10 78 00 00 0b 00 00 00 AA AA AA AA plus exactly 11 bytes |
For battery and LED, (HH, AA) is accepted only as the indivisible observed
pair (0x10, 0x78) or (0x00, 0xF8); mixed pairs fail. The former comes from
the pinned public capture, while the latter was observed repeatedly after the
two exact volatile startup transactions on project-owned hardware. Their
unknown semantics are not generalized. Successful battery and LED validation
also returns the closed Switch2UsbCommandResponseStyle value naming the exact
pair, so callers do not have to infer it from a generic success boolean.
VV VV is returned as an uninterpreted
little-endian ushort; the trailing payload bytes must be zero. SS is closed
per operation and invalid CLR enum casts are rejected at every entry point.
Responses are checked against the specific subcommand sent. Length, command,
direction, USB transport, header style, reserved bytes, and battery payload
reserved bytes are all exact.
The 0x27 feature request encoder admits only set and enable. The response
validator admits only the exact 12-byte tuples captured in the authorized
2026-08-31 USB run against 057E:2069, bcdDevice 0x0201; every byte mutation,
cross-step substitution, alternate tuple, and nonzero payload byte is rejected.
This does not generalize the tuple to another firmware, transport, mask, or
feature command. The codec does not expose 03:0D host-address USB initialisation or
any pairing, association, arbitrary-address memory access, flash block read,
memory write/erase, firmware, clear/disable/configure, or raw command form.
AA AA AA AA is little-endian and is accepted only as 0x130A8, 0x130E8,
0x1FC040, or 0x1FC080 with the corresponding exact length. The response
must echo both address and length before its payload can escape the native
buffer.
The six player-LED operations are treated only as volatile controller state; this codec contains no persistent-state command.
The authority is the command header and captured examples in
ndeadly/switch2_controller_research@d1c5a7f7,
used as independently expressible wire facts only. SDL at the pinned licensed
revision corroborates use of a claimed USB bulk OUT/IN command path, but it is
not authority for broadening this allowlist.
Player-LED AllOff is the logical neutral operation available in the pinned
forms. It cannot restore a prior LED pattern that was never observed. The
bounded hardware verifier therefore calls the exactly acknowledged operation
explicit neutralization, never restoration.
Evidence and license ledger¶
Licensed implementation reference¶
SDL was reviewed at main commit
c71abd08605b8bb7078372307a93274725c99fe0
and the audited release-3.4.x tip
58ff755ea58cd4c64e72678f4b3e0d720aa79206.
SDL uses the zlib license. The commits contain the same
SDL_hidapi_switch2.c. Facts corroborated there include 64-byte USB input,
common button/stick offsets, the six-value packed-12 calibration layout, and
factory primary/secondary stick addresses 0x130A8/0x130E8. Pro maps its
logical left/right sticks to primary/secondary respectively. Either single
Joy-Con stores its only stick in primary; the right Joy-Con maps that primary
record to the logical right stick. No SDL source text was copied; the C#
implementation is independent.
The USB calibration request/response tuple was cross-checked against current
SDL's 16-byte ReadFlashBlock request and the independently implemented
GPL-2.0 hid-nintendo2 cmd=0x02/sub=0x04 path. Only the protocol facts and
closed addresses/lengths are reproduced; no reference implementation body is
copied.
For the Pro USB input tranche, the exact licensed source trace is
src/joystick/hidapi/SDL_hidapi_switch2.c:944-1028 (button positions, digital
triggers, and packed 12-bit sticks), :1113-1148 (64-byte input dispatch and
USB product-model gate), and :420-423 (selection of report 0x05). SDL's
size < 64 check is not copied as policy: this codec requires exactly 64 bytes
so an oversized or concatenated record cannot be accepted accidentally.
Public research used as facts only¶
The following pinned revisions contain no LICENSE, COPYING, or NOTICE.
Their source, prose, private-key material, and encrypted/decrypted captures
were not copied or redistributed:
Only independently expressible facts were used: UUIDs/properties, exact lengths and offsets, advertisement layout and reversed remembered-host field, known product IDs, observed motion lengths, and calibration addresses.
Additional implementations were inspected but not sourced:
TommyWabg/Switch2Connect@4487322, GPLv3-or-later in authored source;TheFrano/joycon2py@9d80b76, MIT;TheFrano/joycon2cpp@1db6999, MIT; andhifihedgehog/SDL@d98c580, not treated as protocol authority.
The later read-only user-calibration tranche additionally compared current
upstream SDL c71abd08605b8bb7078372307a93274725c99fe0,
XenuIsWatching/hid-nintendo2-dkms@32a981ea7f916f1792a7e35aa0ecf79063ec4001,
ndeadly/switch2_controller_research@d1c5a7f7ba298f83017fae84952a4e6d2ef8fc92,
and Switch2Connect v2.8 61ac6642ce12fe7217e38a860b14863b18ca7e28.
Switch2Connect@4487322 independently agrees on the common body fields in
src/controller.py:1422-1434 and records the complete raw button table in
src/usb_hid_controller.py:1125-1133. Its authored code is GPL-3.0-or-later,
so it was used only to corroborate independently expressible wire facts. Its
README's “up to 500Hz” statement is not evidence for a different decoder or
transport: the audited implementation and captures do not establish 500Hz.
PadForge stable 0794fd01bd19f4c096b982ffc824b88bce5ed743 consumes its
bundled SDL state rather than implementing a second Switch 2 report parser.
PadForge is CC BY-NC-SA 4.0 and remains behavioral evidence only. BlueRetro
e1a9831a875f5313a923160a1379a7ebbfaa2b11 (Apache-2.0) corroborates the
Nintendo product IDs and 63-byte BLE report containers, but does not promote a
common 0x05 Pro button parser; it therefore supplied no canonical field code.
Project-owned hardware evidence¶
On 2026-08-29, a physical Switch 2 Pro Controller enumerated read-only as USB
057E:2069, bcdDevice 0x0201: HID MI_00 (HidUsb, usage page 1/usage 5,
64-byte IN/OUT and no feature report), WinUSB MI_01, and USB audio MI_02. The
initial topology/input audit performed no write, serial query, association, or
memory command.
Two passive USB HID captures were kept outside Git and decoded with the exact compiled strict codec. Their digests remain in the out-of-tree lab manifest so a committed test vector cannot be used as a stable link to raw hardware data:
- all 2,048 reports in the first run were exact 64-byte report
0x05and decoded. Raw counter deltas were+42,043 times,+3twice,+5once, and+556once; the+556followed a 0.683-second host-capture gap; and - all 4,096 reports in the second run were exact 64-byte report
0x05, all decoded, and every raw counter delta was+4. Host completion deltas in that passive run had mean 3.990 ms, p50 4.001 ms, and p99 4.112 ms, observing approximately 250 reports/second. Those managed-read timestamps are cadence evidence only, not transport or end-to-end latency proof.
No wrap was observed in hardware; 8-bit and 32-bit wrap behavior is verified
with synthetic vectors. A raw +4 is therefore classified as forward movement,
not four packets lost. Duplicate/backward classification uses a documented
half-range modular ordering policy, not a firmware guarantee.
The committed golden fixture is now fully synthetic, not a redaction or
derivation of either hardware capture. Its counters, button bit, packed stick
values, clock identifier, timestamps, and opaque identifiers were deliberately
constructed from the public byte layout. Every mouse, magnetometer,
battery/current, timestamp, temperature, accelerometer, gyroscope, reserved,
environmental, and uninterpreted packet byte is zero. It contains no raw source
digest, device path/path hash, serial, MAC, key, host, console, account value,
or hardware stick fingerprint. Tests verify the zeroed ranges and replay the
synthetic +4 counter movement.
A later separately authorized, bounded verifier run used only the exact
volatile startup, battery, Player-1 LED, and AllOff forms above. Before the
startup pair, the battery request completed its OUT write but received no IN
bytes and timed out; restarting only the exact composite device did not change
that result. Both startup operations then returned their exact USB ACKs, after
which battery and LED responses used the exact (0x00, 0xF8) header pair.
That schema-2 run observed 3533 mV, exactly acknowledged Player-1, and exactly
acknowledged AllOff. It passively captured 256/256 exact common-05 reports
at 249.999730392448 host completions/second with all 255 counter deltas +4.
No haptic report was emitted: the live nonzero gate failed closed before the
first write because physical neutralization after a noncooperative HID transfer
is not yet provable. That build allowed HID write sharing, so the artifact is
historical command/cadence mechanism evidence, not sole-writer or current-build
evidence.
The prior schema-3 procedure instead opened MI_00 read-only with only read sharing, recorded writer admission explicitly, and bound its result to the executing verifier assembly SHA-256. Its authorized run failed that admission with Win32 error 32 before any hardware I/O. This establishes the fail-closed gate only: the error neither identifies a handle owner nor proves whether the incompatible handle was a writer or a restrictively shared reader.
The schema-4 procedure additionally gives every WinUSB transaction an atomic
hard ownership handoff, serializes the exact admitted response-header style,
bounds input disposal, and validates canonical result shape/types/ranges. Its
authorized Release run also failed the MI_00 sole-writer admission with Win32
error 32 before opening the command interface or attempting input, battery,
LED, haptic, or cleanup mutation. The result file is
_results/switch2-usb-hardware-2026-08-29-schema4-sole-writer-v2.json, SHA-256
0DFC1F5240FF3BBBFD9DD8CC0F8689039981EEEA3EF9856E9E626A099E7F8F44.
The verifier DLL was
87D2A053DD6FF7A447D71E27B13D2715B188547E8A33E3816EF95164184F4A4E
and the EXE was
B68998F98B5FA3A95E9050A0F055456169E16E6B0D678F29C1D4A2044786AA2C.
No process was attributed as the handle owner. The out-of-tree artifacts and
limitations are documented in utils/Switch2UsbHardwareVerify/README.md; no
successful isolated current-source hardware run is claimed.
Coordinate note: an observed change at complete USB packet offset 0x3C is
body offset 0x3B after removing report ID 0x05, i.e. the high byte of gyro
Z. The supported-model body bytes 0x3C/0x3D are not exposed as triggers.
Defensive policies versus unknowns¶
Defensive policies, not undocumented firmware claims:
- exact lengths only; never zero-pad a truncated report;
- USB accepts only Pro model report
0x05; model-specific0x07..0x09bodies require their exact BLE characteristic identities, and wired/Charging-Grip Joy-Con 2 remains unsupported; - exact service and characteristic identity plus required GATT properties;
- privacy-safe host classification compares the six advertised reversed bytes transiently with the selected radio address and returns only an enum;
- calibration decodes every exact nine-byte record, then a separate adoption validator rejects zero/saturated components and endpoints outside the 12-bit domain; factory address selection is by controller model plus primary/secondary physical storage slot, never logical side alone;
- stream, clock, synthetic-fact, derived-golden, and capture IDs require
type-specific prefixes plus caller-generated 128-bit lowercase nonces;
formatted MAC addresses and arbitrary provenance prose are rejected;
firmware is
unknownor a bounded numericfw-version; - derived golden sources carry a nonzero derivation-manifest revision but no raw-capture digest or redaction revision, so they cannot masquerade as either synthetic protocol facts or minimally redacted hardware captures;
- project-owned hardware sources additionally require a source SHA-256 and nonzero redaction-manifest revision;
- timestamps are monotonic per declared host-clock domain, device generation is per stream, frequency is stable per clock domain, and pair epoch is independently caller-coordinated;
- model, transport, firmware, source, clock identity, and pair epoch cannot
drift within one
(StreamId, Generation); and - the transport-neutral session fences exact protocol identity, device and transport generations, QPC frequency, and nondecreasing completion time; and
- the pair reducer cannot combine different frequencies or pair epochs, and loss/replacement events are generation-fenced.
The exact Pro USB canonical identity gate currently recognizes only
057E:2069:bcdDevice-0201; unsupported revisions fail closed and are not
reinterpreted by packet length. The gate records firmware as not queried and
rejects a fixture that claims firmware-specific evidence.
Original phase-1 exclusions (historical)¶
The following list records the scope boundary at the original codec-only cut; it is not current product status. The production transport, runtime, mapping, feedback, and association documents supersede the items completed after that cut:
- OS USB/HID, WinUSB, libusb, BLE scanning, GATT, and connection parameters;
- registration, runtime selection, mapping, and virtual-pad publication;
- Nintendo application-layer association, SMP interaction, LTK derivation, Windows key provisioning, wake, and automatic reconnect;
- controller-memory access and every initialization command beyond the closed, volatile USB-HID enable/common-report forms above;
- persistent controller-memory writes and user-calibration authoring (the runtime only performs bounded reads and never mutates controller memory);
- packed motion decoding, timestamp units, scale, orientation, and fusion;
- HD-rumble semantic adaptation, BLE envelope, scheduling, pacing, and stop behavior (raw 40-bit/group/USB codecs are offline-only);
- firmware-dependent GATT changes and achievable Windows BLE interval; and
- Joy-Con discovery, runtime ownership/association, reconnect scheduling, and profile integration beyond the pure pair reducer.
Offline verification (2026-08-29)¶
No controller was enumerated or contacted by these tests.
dotnet test DS4WindowsTests\DS4WindowsTests.csproj -c Release -p:Platform=x64 --no-restore --filter "FullyQualifiedName~Switch2"
Passed: 89, Failed: 0, Skipped: 0
dotnet test DS4WindowsTests\DS4WindowsTests.csproj -c Release -p:Platform=x64 --no-restore
Passed: 1141, Failed: 2, Skipped: 3
The current full-suite failures are
ControllerFeedbackRuntimeTests.PublishSelectClaimAdmitCompleteSteadyStateAllocatesNothing
and DualSenseInputReadPipelineTests.WarmSynchronousReadAndRearmCycleAllocatesZero.
An isolated rerun of those two tests reproduced the feedback-runtime failure and
passed the DualSense allocation test. The three skips are the opt-in live
application-loopback audio tests. The focused set includes 20,000 randomized
exact-body lossless round trips, all 256 USB report IDs, malformed destination
lengths, model /
transport / firmware-evidence gates, counter wrap and host-clock separation,
calibration adoption/fallback, lifecycle reset fences, Joy-Con pair
skew/loss/split behavior, exact physical-interface admission, and zero managed
allocations after warmup for decode, raw re-encode, canonical
processing/projection, physical input adaptation, and the pure pair reducer.
Before a live adapter, obtain sanitized USB and BLE captures for every model, common/model-specific modes, reconnect/wake states, more than one Windows Bluetooth chipset, and both single and concurrent Joy-Con operation. Any association, key, memory, or write experiment requires explicit user consent, a console re-pair recovery procedure, and a separate secret-redaction review.