Remote play build guide · authoritative continuation

Finish the controller bridge

Resume from a Windows reboot, lock in the corrected Xbox mappings, automate the host, and prove the entire Animal Crossing remote-play path.

Windows 11 Pro Parsec Xbox 360 HORI/Pokkén Pico firmware 1080p60
The mapping fix Use the pinned SDL2 bridge. Do not use --swap-abxy.

Xbox A/B/X/Y now match Switch A/B/X/Y by label. Menu is Plus, View is Minus, and right-stick click is Home.

Final mapping

The four controls that matter most

Xbox A/B/X/Y
Switch A/B/X/Y
Menu · three lines
Plus
View · rectangles
Minus
Right stick click · R3
Home

12. Resume the controller setup after the PC reboot

12.1 Know what the reboot did—and did not—reset

A Windows restart stopped OBS, the temporary PowerShell bridge, and the active Parsec session. It did not erase:

  • The HORI/Pokkén firmware on the Pico.
  • The Python environment under C:\RemoteSwitch\switch-pico-sdl2.
  • The Parsec Virtual USB driver.
  • The OBS profile and scene.
  • The UART wiring.

For normal operation, leave the hardware connected like this:

Windows host USB
    -> Raspberry Pi Debug Probe
    -> UART U port
    -> GPIO4 / GPIO5 / GND on Pico

Switch dock USB
    -> Pico Micro-USB data port

The Pico belongs on the Switch dock. The Debug Probe belongs on the Windows PC.

12.2 Confirm the working bridge folder survived

Sign in to the dedicated remote-play Windows account. Open a normal, non-Administrator PowerShell window and run:

whoami
Test-Path C:\RemoteSwitch\switch-pico-sdl2\.venv\Scripts\python.exe
C:\RemoteSwitch\switch-pico-sdl2\.venv\Scripts\python.exe -c "import sys; print(sys._base_executable)"
git -C C:\RemoteSwitch\switch-pico-sdl2 rev-parse --short HEAD

Expected results:

...\RemoteSwitch
True
C:\Users\RemoteSwitch\AppData\Local\Python\pythoncore-3.13-64\python.exe
e13ba50

The exact computer name and Python patch release may differ. The important requirement is that the account and base-Python path belong to the same RemoteSwitch account that will run Parsec and the scheduled bridge. If the command reports Access is denied or points into another profile such as C:\Users\admin, the virtual environment is account-bound incorrectly. Do not run the bridge elevated; use the repair procedure in Step 14.2.

If Test-Path says False, do not continue to the bridge command. While still in the normal RemoteSwitch PowerShell session, recreate the pinned SDL2 environment:

Set-Location C:\RemoteSwitch
git clone https://github.com/jyapayne/switch-pico.git switch-pico-sdl2
Set-Location C:\RemoteSwitch\switch-pico-sdl2
git checkout e13ba506cfc31fa8f02f50a9ac2a445d1ecf246b
pymanager install 3.13
pymanager exec -V:3.13 -m venv .venv
.\.venv\Scripts\python.exe -m pip install --upgrade pip
.\.venv\Scripts\python.exe -m pip install -e .
.\.venv\Scripts\python.exe -m pip install "PySDL2-dll==2.32.10"

If pymanager is unavailable, install Python Install Manager for the current RemoteSwitch user using the command in Step 14.2, reopen a normal PowerShell window, and repeat. If the folder already exists, do not clone over it.

12.3 Reconfirm the Debug Probe COM port

Run:

C:\RemoteSwitch\switch-pico-sdl2\.venv\Scripts\python.exe -m serial.tools.list_ports -v

Find the USB Serial Device associated with the Raspberry Pi Debug Probe. The tested machine used COM3.

You can verify it visually:

  1. Open Device Manager.
  2. Expand Ports (COM & LPT).
  3. Unplug only the Debug Probe's USB cable.
  4. Note which COM entry disappears.
  5. Reconnect the probe and confirm the same entry returns.

Keep the Debug Probe in the same physical PC USB port. Windows normally preserves its COM number there.

Every command below uses COM3. If Windows now assigns another number, substitute that number everywhere, including the startup script.

12.4 Confirm the Switch-side prerequisites

Using a local Joy-Con if necessary:

  1. Confirm the Switch is awake and docked.
  2. Open System Settings > Controllers and Sensors.
  3. Confirm Pro Controller Wired Communication is On.
  4. Open System Settings > Sleep Mode.
  5. Confirm Auto-Sleep (Connected to TV) is Never.

The HORI compatibility firmware does not implement controller wake-from-sleep. Keeping the docked Switch awake is part of this design.

12.5 Confirm Parsec's host controller mode after reboot

On the Windows host:

  1. Open Parsec > Settings > Host.
  2. Confirm Parsec Virtual USB Gamepads is Enabled.
  3. Confirm the latest offered Virtual USB driver is installed.
  4. Set Virtual Gamepad Type to Xbox 360.
  5. Click Restart at the top of Parsec if you changed either setting.

Do not use DualShock 4 or DualSense emulation for this build. The SDL2 bridge and the tested remote path expect Parsec's standardized Xbox 360 device.

12.6 Prepare the sister's Xbox controller on her Mac

The recommended controller is the current official Xbox Wireless Controller—the Series X|S model with USB-C and a Share button.

On the Mac:

  1. Update macOS before first setup.
  2. Turn on the Xbox controller.
  3. Hold its pairing button until the Xbox light flashes.
  4. Open System Settings > Bluetooth and connect Xbox Wireless Controller.
  5. Open System Settings > Game Controllers.
  6. Select the Xbox controller and return its controls to their defaults if they were customized previously.
  7. Close Steam while commissioning this path so Steam Input cannot consume or duplicate controller events.
  8. Open Parsec > Settings > Gamepad.
  9. Select the Xbox controller.
  10. Press every control and watch Parsec's State display.

The intended Parsec meanings are:

Physical Xbox control Parsec control
A A
B B
X X
Y Y
Menu, three horizontal lines Start
View, overlapping rectangles Back
Xbox logo Unused; macOS does not need to expose it for this build
Share Misc/Share, when exposed
LB / RB Left / Right Shoulder
LT / RT Left / Right Trigger
Left stick click Left Stick
Right stick click (R3) Right Stick; the Windows bridge changes it to Switch HOME
D-pad D-pad

If a control is wrong, use Map in Parsec's Gamepad tab to assign it. Use Show Raw Data when you need to determine whether macOS and Parsec can see a button at all. Keep R3 assigned to Right Stick, not Guide, and keep every D-pad direction assigned normally.

12.7 Create the Parsec virtual controller on the host

The virtual controller does not exist merely because the host PC booted. Parsec creates it after the remote player connects and presses a face button.

  1. On the Mac, connect to the Windows host in Parsec.
  2. Press Xbox A once.
  3. The host should play the Windows USB-connected sound.
  4. On the host, press Windows+R, enter joy.cpl, and press Enter.
  5. Confirm an Xbox 360 Controller appears.
  6. Open its Properties > Test page.
  7. Confirm A/B/X/Y, both stick axes, both stick clicks, D-pad, bumpers, triggers, Menu, and View respond correctly.

If joy.cpl is empty, do not start debugging the Pico. Fix Parsec first:

  • Confirm the remote user has gamepad permission.
  • Confirm the client pressed a face button after connecting.
  • Restart Parsec on the host, reconnect, and press A again.
  • Reinstall the Parsec Virtual USB driver if the host still creates no device.

12.8 Confirm the SDL2 bridge sees only the intended controller

Disconnect local controllers from the Windows host. Then run:

Set-Location C:\RemoteSwitch\switch-pico-sdl2
.\.venv\Scripts\python.exe -m switch_pico_bridge.controller_uart_bridge --list-controllers

Expected output includes:

Detected controller 0: Xbox 360 Controller

No controllers detected is normal if the sister is not currently connected or has not pressed a face button. It is not evidence that the Python installation broke.

Avoid using the older physical Xbox One controller directly on the Windows host. Its SDL GUID reproduced a known SDL input-read problem during testing. Parsec's virtual Xbox 360 controller is the validated path.

12.9 Apply the R3-to-HOME and COM3 hot-plug fixes

Stop any old bridge console with Ctrl+C. If the scheduled task exists, stop it too:

Stop-ScheduledTask -TaskName "Remote Switch - Controller Bridge" -ErrorAction SilentlyContinue

Now apply both idempotent source changes. This block keeps one backup, maps R3 to HOME, and changes controller-removal cleanup so Windows releases COM3 before Parsec's replacement controller tries to claim it:

Set-Location C:\RemoteSwitch\switch-pico-sdl2
$bridge = "C:\RemoteSwitch\switch-pico-sdl2\src\switch_pico_bridge\controller_uart_bridge.py"
$backup = "$bridge.pre-remote-switch-fixes"

if (-not (Test-Path $backup)) {
    Copy-Item $bridge $backup
}

@'
from pathlib import Path

path = Path(r"C:\RemoteSwitch\switch-pico-sdl2\src\switch_pico_bridge\controller_uart_bridge.py")
text = path.read_text(encoding="utf-8-sig")

text = text.replace(
    "sdl2.SDL_CONTROLLER_BUTTON_RIGHTSTICK: SwitchButton.RCLICK,",
    "sdl2.SDL_CONTROLLER_BUTTON_RIGHTSTICK: SwitchButton.HOME,",
)

start = text.find("def handle_device_removed(")
end = text.find("\n\ndef service_contexts(", start)
if start < 0 or end < 0:
    raise SystemExit("ERROR: Could not locate handle_device_removed; the pinned source does not match this guide.")

patched_handler = '''def handle_device_removed(
    event: sdl2.SDL_Event,
    pairing: PairingState,
    contexts: Dict[int, ControllerContext],
    uarts: List[PicoUART],
    console: Console,
) -> None:
    """Handle controller removal, close its UART, and release auto-pairing."""
    instance_id = event.cdevice.which
    ctx = contexts.pop(instance_id, None)
    if not ctx:
        return
    console.print(
        f"[yellow]Controller {instance_id} (id {ctx.stable_id}) removed[/yellow]"
    )
    # Windows opens COM ports exclusively. Close the old handle before
    # returning the port to the pool for Parsec's replacement controller.
    if ctx.uart:
        try:
            ctx.uart.close()
        except Exception:
            pass
        if ctx.uart in uarts:
            uarts.remove(ctx.uart)
        ctx.uart = None
    if ctx.controller_index in pairing.auto_assigned_indices:
        freed = pairing.mapping_by_index.pop(ctx.controller_index, None)
        pairing.auto_assigned_indices.discard(ctx.controller_index)
        if freed and freed not in pairing.available_ports:
            pairing.available_ports.append(freed)
            console.print(f"[cyan]Released UART {freed} back to pool[/cyan]")
    sdl2.SDL_GameControllerRumble(ctx.controller, 0, 0, 0)
    sdl2.SDL_GameControllerClose(ctx.controller)'''

text = text[:start] + patched_handler + text[end:]

old_call = "handle_device_removed(event, pairing, contexts, console)"
new_call = "handle_device_removed(event, pairing, contexts, uarts, console)"
if old_call in text:
    text = text.replace(old_call, new_call, 1)
elif new_call not in text:
    raise SystemExit("ERROR: Could not update the controller-removal call site.")

if "sdl2.SDL_CONTROLLER_BUTTON_RIGHTSTICK: SwitchButton.HOME," not in text:
    raise SystemExit("ERROR: R3-to-HOME mapping was not applied.")

path.write_text(text, encoding="utf-8")
print("Applied R3-to-HOME and Windows COM-port release fixes.")
'@ | .\.venv\Scripts\python.exe -

.\.venv\Scripts\python.exe -m py_compile $bridge
Select-String -Path $bridge -Pattern "RIGHTSTICK","ctx.uart.close","handle_device_removed\(event, pairing, contexts, uarts, console\)"

Expected output contains all three of these:

sdl2.SDL_CONTROLLER_BUTTON_RIGHTSTICK: SwitchButton.HOME,
ctx.uart.close()
handle_device_removed(event, pairing, contexts, uarts, console)

If Python reports a syntax error, or any expected line is absent, do not start the bridge. Restore $backup and repeat this step against the pinned e13ba50 source.

This deliberately bypasses Guide. R3 reaches the Parsec Xbox 360 virtual controller as an ordinary, dependable button, and the Windows bridge changes that button to the HOME bit before sending it over UART. Right-stick movement is unaffected; only right-stick click is reassigned.

The COM3 change is equally important for unattended use. Parsec may remove and recreate the host's virtual Xbox 360 controller during connection or reconnection. Closing the old UART object before releasing its mapping prevents the replacement controller from becoming stuck in an Access is denied retry loop.

12.10 Run the corrected bridge manually

Run:

Set-Location C:\RemoteSwitch\switch-pico-sdl2
.\.venv\Scripts\python.exe -u -m switch_pico_bridge.controller_uart_bridge --ports COM3 --no-imu --zero-sticks --deadzone 0.12 --zero-hotkey= --swap-hotkey=

Expected startup messages include:

Prepared 1 specified UART port(s) for auto-pairing.
Detected controller 0: Xbox 360 Controller
Auto-paired controller 0 to COM3
Controller 0 (...) paired to COM3

The terminal does not print a line for every button. Silence while playing is normal.

Why these options are final:

Option Reason
--ports COM3 Auto-pairs the first arriving Parsec controller with the known Debug Probe port and supports hot-plugging.
--no-imu The HORI firmware intentionally has no gyro path, and Animal Crossing does not require it.
--zero-sticks Captures the initial neutral positions when the virtual controller arrives. Keep both sticks released during connection.
--deadzone 0.12 Suppresses the approximately 8% return-to-center drift observed during testing without harming Animal Crossing.
--zero-hotkey= Disables the terminal re-zero hotkey for unattended operation.
--swap-hotkey= Prevents an accidental terminal keypress from changing ABXY later.

There is deliberately no --swap-abxy flag. In this pinned SDL2 revision, no flag means Xbox labels map directly to the same Switch labels.

12.11 Final controller mapping

The resulting mapping is:

Xbox controller Nintendo Switch
A A
B B
X X
Y Y
LB L
RB R
LT ZL
RT ZR
Menu, three lines Plus +
View, overlapping rectangles Minus
Xbox logo Unused
Share Capture, when Parsec exposes the Share/Misc button
Left stick click L-stick click
Right stick click (R3) Home
D-pad D-pad
Left and right sticks Left and right sticks

This is label-for-label ABXY, not physical-position mapping. When Animal Crossing says “Press A,” press the Xbox button labeled A. There is no separate Switch R-stick-click input in this build because R3 is reserved for HOME.

12.12 Test every control and reconnect before automating it

On the Switch, open Controllers > Change Grip/Order, then test:

  1. Release the controller and wait five seconds. Nothing should move.
  2. Press A, B, X, and Y individually.
  3. Test all four D-pad directions.
  4. Move each stick in a full circle and release it.
  5. Press LB, RB, LT, and RT.
  6. Press Menu and verify Switch Plus.
  7. Press View and verify Switch Minus.
  8. Press R3 and confirm it opens the Switch HOME Menu.
  9. Press Share and look for the Switch capture confirmation.
  10. Click the left stick and confirm the Switch L-stick click.

Then launch Animal Crossing and confirm:

  • Xbox A selects.
  • Xbox B cancels.
  • Xbox X and Y perform their expected in-game functions.
  • Menu opens the NookPhone or the appropriate Plus action.
  • View performs the Minus action.
  • R3 returns to HOME.

Finally test the COM3 fix:

  1. Leave the bridge running.
  2. Disconnect the Parsec client.
  3. Wait five seconds.
  4. Reconnect through Parsec and press A once.
  5. Confirm the same bridge process automatically pairs the recreated Xbox 360 controller to COM3.
  6. Confirm the controller works again without restarting the scheduled task.

The bridge should report controller removal, Released UART COM3 back to pool, and then a new paired to COM3 line. It must not repeat PermissionError(13): Access is denied.

Do not automate the bridge until the controls pass this manual test.

On the tested Mac, macOS did not expose the Xbox-logo button to Parsec as a usable controller input. Assigning ordinary controls such as R3 or D-pad Up to Parsec Guide also failed to produce Switch HOME, even though those physical controls were detected.

Do not spend more time trying to make Guide work:

  • Leave the Xbox logo unused. If macOS opens Music when it is pressed, that does not affect remote play.
  • Restore R3 to ordinary Right Stick in Parsec.
  • Restore every D-pad direction to its matching D-pad direction.
  • Apply the Windows R3-to-HOME change in Step 12.9.
  • If Share appears in Parsec, leave it mapped to Misc/Share for optional Switch Capture.

The physical Xbox-logo button is not part of the validated path. R3 is the reliable HOME control.

13. Finish the Parsec video, audio, and access settings

13.1 Host settings

On the Windows host, open Parsec > Settings > Host and use:

  • Hosting Enabled: On
  • Resolution: 1920×1080
  • FPS: 60
  • Bandwidth Limit: 30 Mbps
  • Display: the 1080p60 display carrying the OBS projector
  • Audio: CABLE Input (VB-Audio Virtual Cable)
  • Echo Cancelling: Off
  • Parsec Virtual USB Gamepads: Enabled
  • Virtual Gamepad Type: Xbox 360
  • Quality: Lowest Latency

Restart Parsec after changing the gamepad type or Virtual USB driver.

13.2 Sister's client settings

On the Mac:

  • Window Mode: Fullscreen
  • VSync: Off initially
  • Decoder: Hardware
  • H.265/HEVC: Off for the baseline test
  • 10-bit color: Off
  • Controller: verified in Settings > Gamepad
  • R3 mapping: ordinary Right Stick, not Guide

If she sees tearing, enable VSync and accept its small latency cost. If the network cannot sustain 30 Mbps, lower the host limit to 25 Mbps, then 20 Mbps.

13.3 Give access only to her account

Use separate Parsec accounts.

  1. Add the sister as a friend or invited guest.
  2. Enable Gamepad permission.
  3. Enable keyboard and mouse only if you want her to perform recovery actions.
  4. Enable unattended connection only for her trusted account.
  5. Do not share the host account password.
  6. Do not create a permanent public Parsec share link.

14. Make the controller bridge start automatically

14.1 Create the permanent bridge launcher

Open Notepad and save the following as:

C:\RemoteSwitch\start-switch-bridge.cmd

Use this exact content, replacing COM3 only if the Debug Probe uses another port:

@echo off
setlocal
set "ROOT=C:\RemoteSwitch\switch-pico-sdl2"
set "PYTHON=%ROOT%\.venv\Scripts\python.exe"
set "LOGDIR=C:\RemoteSwitch\logs"

if not exist "%LOGDIR%" mkdir "%LOGDIR%"
cd /d "%ROOT%"

echo [%date% %time%] Starting Remote Switch controller bridge>>"%LOGDIR%\switch-pico-bridge.log"
echo Account: %USERDOMAIN%\%USERNAME%>>"%LOGDIR%\switch-pico-bridge.log"

if not exist "%PYTHON%" (
  echo ERROR: Missing virtual-environment Python at "%PYTHON%">>"%LOGDIR%\switch-pico-bridge.log"
  exit /b 2
)

"%PYTHON%" -c "import sys; print('Virtual environment:', sys.executable); print('Base Python:', sys._base_executable)" >>"%LOGDIR%\switch-pico-bridge.log" 2>&1
if errorlevel 1 (
  echo ERROR: The virtual environment cannot reach its base Python. Rebuild it under the same Windows account that runs this launcher.>>"%LOGDIR%\switch-pico-bridge.log"
  exit /b 10
)

"%PYTHON%" -u -m switch_pico_bridge.controller_uart_bridge --ports COM3 --no-imu --zero-sticks --deadzone 0.12 --zero-hotkey= --swap-hotkey= >>"%LOGDIR%\switch-pico-bridge.log" 2>&1
exit /b %errorlevel%

Important:

  • The path is switch-pico-sdl2, not switch-pico.
  • There is no --swap-abxy.
  • The source line for SDL_CONTROLLER_BUTTON_RIGHTSTICK must still end in SwitchButton.HOME.
  • The controller-removal handler must close ctx.uart and receive the uarts list as described in Step 12.9.
  • The launcher must run as the same RemoteSwitch Windows account that owns the base Python runtime and .venv.
  • Do not launch debug-bridge.py for daily use.

Both source edits survive a Windows reboot. Reapply Step 12.9 if you later replace controller_uart_bridge.py, forcibly restore the Git checkout, or delete and recreate switch-pico-sdl2.

14.2 Verify the Python account and test the launcher

A virtual environment is not a self-contained copy of Python. Its pyvenv.cfg records the base interpreter that created it. If .venv was created in an Administrator PowerShell belonging to admin, a launcher running as RemoteSwitch can find .venv\Scripts\python.exe but still fail with:

did not find executable at 'C:\Users\admin\AppData\Local\Python\pythoncore-3.13-64\python.exe': Access is denied.

That error is unrelated to the controller, UART, COM3, or Pico.

First close every Administrator: PowerShell window. From the RemoteSwitch desktop, open ordinary PowerShell—do not choose Run as administrator—and run:

whoami
Get-Content C:\RemoteSwitch\switch-pico-sdl2\.venv\pyvenv.cfg
C:\RemoteSwitch\switch-pico-sdl2\.venv\Scripts\python.exe -c "import sys; print(sys._base_executable)"

Pass conditions:

  • whoami ends in \RemoteSwitch, not \admin.
  • The base Python prints without an error.
  • Neither output points into another user's profile.

If all three pass, skip to Retest the launcher below.

Repair an admin-bound .venv

Stop every running bridge before changing .venv. Stay in the normal, non-Administrator RemoteSwitch PowerShell window. If pymanager is not available for this account, install the current Python Install Manager:

winget install 9NQ7512CXL7T -e --accept-package-agreements --accept-source-agreements --disable-interactivity

Close and reopen ordinary PowerShell after installation. Confirm the account again, then install a Python 3.13 runtime owned by this user:

whoami
pymanager install 3.13
pymanager exec -V:3.13 -c "import sys; print(sys.executable)"

The printed path should be under C:\Users\RemoteSwitch, never C:\Users\admin.

Now preserve the broken environment as a backup and recreate it. This does not touch the repository, the R3 source change, the Pico firmware, or any Switch data:

Set-Location C:\RemoteSwitch\switch-pico-sdl2

$backup = ".venv-admin-bound-" + (Get-Date -Format "yyyyMMdd-HHmmss")
if (Test-Path .venv) {
    Rename-Item .venv $backup
}

pymanager exec -V:3.13 -m venv .venv
.\.venv\Scripts\python.exe -m pip install --upgrade pip
.\.venv\Scripts\python.exe -m pip install -e .
.\.venv\Scripts\python.exe -m pip install "PySDL2-dll==2.32.10"
.\.venv\Scripts\python.exe -c "import sys; print('Virtual environment:', sys.executable); print('Base Python:', sys._base_executable)"
Select-String -Path .\src\switch_pico_bridge\controller_uart_bridge.py -Pattern "RIGHTSTICK","ctx.uart.close","handle_device_removed\(event, pairing, contexts, uarts, console\)"

The final two commands must show a base Python belonging to RemoteSwitch and all three patched lines:

sdl2.SDL_CONTROLLER_BUTTON_RIGHTSTICK: SwitchButton.HOME,
ctx.uart.close()
handle_device_removed(event, pairing, contexts, uarts, console)

Retest the launcher

  1. Stop every manual or scheduled bridge.

  2. Archive the mixed old log so the new test is unambiguous:

    $log = "C:\RemoteSwitch\logs\switch-pico-bridge.log"
    if (Test-Path $log) {
        $archive = "C:\RemoteSwitch\logs\switch-pico-bridge-before-venv-fix-{0}.log" -f (Get-Date -Format "yyyyMMdd-HHmmss")
        Move-Item $log $archive
    }
    
  3. Double-click C:\RemoteSwitch\start-switch-bridge.cmd from the RemoteSwitch desktop.

  4. Have the sister connect through Parsec and press A.

  5. Test the Switch, including R3 opening HOME.

  6. Close the launcher with Ctrl+C.

  7. Open the new log:

    Get-Content C:\RemoteSwitch\logs\switch-pico-bridge.log -Tail 40
    

The new log should identify the RemoteSwitch account and its base Python, then show the Xbox 360 controller paired to COM3. Do not schedule the launcher until this exact test works without elevation.

14.3 Create the Task Scheduler entry

  1. Open Task Scheduler.
  2. Select Create Task, not Create Basic Task.
  3. On General:
    • Name: Remote Switch - Controller Bridge
    • User: the same RemoteSwitch account shown by whoami in Step 14.2
    • Select Run only when user is logged on
    • Leave Run with highest privileges cleared
  4. On Triggers:
    • New
    • Begin the task: At log on
    • Specific user: the remote-play Windows account
    • Delay task for: 15 seconds
  5. On Actions:
    • Program: C:\Windows\System32\cmd.exe
    • Arguments: /c "C:\RemoteSwitch\start-switch-bridge.cmd"
    • Start in: C:\RemoteSwitch
  6. On Conditions:
    • Clear Start the task only if the computer is on AC power if shown.
  7. On Settings:
    • Enable Allow task to be run on demand.
    • Enable If the task fails, restart every 1 minute.
    • Set restart attempts to 999.
    • Clear Stop the task if it runs longer than.
    • If already running: Do not start a new instance.
  8. Save.

The task may start before a Parsec controller exists. That is fine after the Step 12.9 COM-release fix: the bridge waits for hot-plug events and safely releases COM3 when Parsec replaces a controller instance.

14.4 Test the scheduled task without rebooting

Run:

Start-ScheduledTask -TaskName "Remote Switch - Controller Bridge"
Start-Sleep -Seconds 3
Get-ScheduledTaskInfo -TaskName "Remote Switch - Controller Bridge"
Get-Content C:\RemoteSwitch\logs\switch-pico-bridge.log -Tail 40

Then connect through Parsec, press A, and confirm the Switch responds. Press R3 and confirm it opens HOME.

Disconnect Parsec, reconnect, press A, and test again without restarting the task. The log should show the old controller removed, Released UART COM3 back to pool, and the new controller paired to COM3 without PermissionError(13).

Only one process may own COM3. Do not leave a manual bridge running when starting the scheduled task.

15. Make OBS return to the clean Switch view

15.1 Install the automatic full-screen projector script

OBS's Save projectors on exit setting works after a clean OBS exit, but Windows sign-out or reboot can close OBS before it persists or restores the projector. OBS also has no projector launch parameter. Use its built-in scripting API to open the Switch scene projector five seconds after every OBS launch.

Create the script from normal PowerShell:

@'
obs = obslua
local timer_armed = false

local function open_remote_switch_projector()
    obs.timer_remove(open_remote_switch_projector)
    timer_armed = false
    obs.obs_frontend_open_projector("Scene", 0, "", "Switch")
end

function script_description()
    return "Opens the Remote Switch scene as a fullscreen projector after OBS starts."
end

function script_load(settings)
    timer_armed = true
    obs.timer_add(open_remote_switch_projector, 5000)
end

function script_unload()
    if timer_armed then
        obs.timer_remove(open_remote_switch_projector)
        timer_armed = false
    end
end
'@ | Set-Content C:\RemoteSwitch\obs-auto-projector.lua -Encoding utf8

The same file is available as obs-auto-projector.lua.

Now configure OBS:

  1. Open File > Settings > General.
  2. In Projectors:
    • Clear Save projectors on exit so the script cannot create a duplicate.
    • Enable Hide cursor over projectors.
    • Enable Always on top if available.
  3. Open Tools > Scripts.
  4. Select the + button.
  5. Choose C:\RemoteSwitch\obs-auto-projector.lua.
  6. Close the Scripts window.
  7. If the projector opened while you were adding the script, press Esc once.
  8. Exit OBS with File > Exit and reopen it.
  9. Wait five seconds and confirm the Switch scene automatically fills the display.

The script uses monitor index 0, which is correct for the tested single-monitor host. If it opens on the wrong PC display, edit the 0 in obs_frontend_open_projector("Scene", 0, "", "Switch") to 1, reload the script in Tools > Scripts, and retest.

Press Esc on the host to leave projector mode temporarily. That does not disable the script; it opens a fresh projector the next time OBS starts.

15.2 Create the OBS scheduled task

Create another Task Scheduler task:

  • Name: Remote Switch - OBS
  • Run only when the remote-play Windows user is logged on
  • Trigger: At log on, delayed 25 seconds
  • Program:
C:\Program Files\obs-studio\bin\64bit\obs64.exe
  • Arguments:
--profile "Remote Switch" --collection "Remote Switch" --scene "Switch" --disable-missing-files-check
  • Start in:
C:\Program Files\obs-studio\bin\64bit

Use the same retry and no-time-limit settings as the bridge task.

The scheduled task starts OBS. The loaded Lua script—not the command line—opens the full-screen scene projector five seconds later.

15.3 Reconfirm audio after the reboot

In OBS:

  1. Confirm the Game Capture Neo audio meter moves.
  2. Open Advanced Audio Properties.
  3. Set the Neo to Monitor Only (mute output).
  4. Open Settings > Audio > Advanced.
  5. Set Monitoring Device to CABLE Input (VB-Audio Virtual Cable).

In Parsec Host settings, Audio must also be CABLE Input.

The TV still receives audio independently through the Neo's HDMI OUT.

16. Finish unattended Windows recovery

16.1 Confirm Parsec is installed per-computer

The per-computer installation lets the Parsec service start before a user signs in.

In PowerShell:

Get-CimInstance Win32_Service |
    Where-Object {
        $_.Name -match "parsec" -or
        $_.DisplayName -match "parsec"
    } |
    Format-Table Name, DisplayName, State, StartMode

At least one Parsec service should be Running with an automatic start mode.

If no service appears, reinstall Parsec using its Per Computer option, restart Windows, and recheck.

16.2 Automatic sign-in decision

OBS and the controller bridge need an interactive Windows session.

For a dedicated, physically secure host:

  1. Download Microsoft Sysinternals Autologon.
  2. Run Autologon64.exe as administrator.
  3. Enter the dedicated remote-play Windows account and password.
  4. Select Enable.
  5. Restart and verify the account signs in.

Use BitLocker and keep the machine physically secure. A local administrator can recover the stored autologon credential.

If you do not enable autologon, Parsec can reach the Windows sign-in screen, but someone must sign in before OBS and the user-level scheduled bridge become available.

16.3 Power-loss settings

  • In BIOS/UEFI, set Restore on AC Power Loss to Power On.
  • Keep Windows sleep disabled.
  • Disable USB selective suspend for this host.
  • Keep Windows Update active, but configure Active Hours around likely play times.
  • Prefer a small UPS for the PC, network equipment, dock, and capture card.
  • A smart plug is useful for the dock or capture hardware; it is not a safe routine way to restart Windows.

17. Staged acceptance test after the reboot

Pass each stage before moving to the next.

Stage A — Local HDMI and OBS

  • TV shows Switch video and audio through Neo passthrough.
  • OBS shows 1080p60 Switch video.
  • OBS audio meter moves.
  • Fullscreen Scene Projector fills the host display.

Stage B — Parsec video and audio

  • Sister connects.
  • She sees only the clean Switch scene.
  • She hears one clean stereo feed.
  • Parsec reports hardware decode and stable network timing.

Stage C — Parsec virtual controller

  • Client Gamepad State responds correctly.
  • After connecting and pressing A, host joy.cpl shows Xbox 360 Controller.
  • Its Test page shows correct controls, including R3 as an ordinary right-stick click.

Stage D — Bridge

  • Scheduled task is running.
  • Log shows controller 0 paired to COM3.
  • Idle input is neutral.
  • Every control follows the mapping table in Step 12.11.
  • R3 opens HOME; the Xbox logo is not required.
  • Disconnecting and reconnecting Parsec releases and re-pairs COM3 without an access-denied loop.

Stage E — Animal Crossing

  1. Press R3 to return to the Switch HOME Menu.
  2. Select the sister's existing Switch user.
  3. Launch Animal Crossing.
  4. Confirm her existing resident and house load on the family island.
  5. Walk, open inventory, use the NookPhone, enter and leave a building, and save normally.

Stage F — True unattended restart

  1. Disconnect Parsec.
  2. Restart Windows normally.
  3. Wait three to five minutes.
  4. Confirm the host comes back online.
  5. Reconnect.
  6. Press A once to create the virtual controller.
  7. Verify video, audio, Plus, Minus, R3-to-HOME, ABXY, and both stick axes.
  8. Disconnect and reconnect Parsec once without restarting Windows or the bridge, then verify the controller again.
  9. Repeat the full restart test a second time.

Do not call the build unattended until it passes Stage F twice.

18. Normal daily use

Sister

  1. Connect or wake the Xbox controller on the Mac.
  2. Open Parsec.
  3. Verify the controller under Settings > Gamepad if anything changed.
  4. Connect to the home PC.
  5. Press A once to create the host virtual controller.
  6. Wait a few seconds for the bridge to pair it to COM3.
  7. Play normally.
  8. Choose her existing Switch user when launching Animal Crossing.
  9. Save and press R3 to return to the HOME Menu when finished.
  10. Disconnect from Parsec.
  11. Do not put the Switch to sleep.

Family at home

  • Use the TV normally through the Neo's HDMI passthrough.
  • Avoid using another controller during a remote session.
  • If controller order becomes confused, open Controllers > Change Grip/Order and reconnect only the intended controller.
  • Leave the Pico on the dock and Debug Probe on the PC.

19. Troubleshooting

The bridge says No controllers detected after boot

This is normal before a remote player connects.

  1. Connect the sister through Parsec.
  2. Press a face button.
  3. Confirm the host USB-connected sound.
  4. Check joy.cpl.
  5. The already-running bridge should hot-detect and pair it.

joy.cpl sees no controller

The problem is before switch-pico:

  1. Verify the controller in the Mac's Parsec Gamepad tab.
  2. Confirm the guest has gamepad permission.
  3. Press A after connecting.
  4. Restart host Parsec and reconnect.
  5. Reinstall the Parsec Virtual USB driver if needed.

The log says pythoncore...python.exe: Access is denied

This is not the COM3 access error. It means .venv was created from a base Python installed inside another Windows user's profile—typically an elevated admin account—but the launcher is now running as RemoteSwitch.

  1. Do not run the daily bridge as Administrator.
  2. Open normal PowerShell as RemoteSwitch.
  3. Run whoami and inspect .venv\pyvenv.cfg.
  4. Follow Repair an admin-bound .venv in Step 14.2.
  5. Keep the Task Scheduler user set to that same RemoteSwitch account.

The presence of .venv\Scripts\python.exe does not prove the environment is usable; it still depends on the base interpreter recorded when the virtual environment was created.

The bridge reports Access is denied for COM3

Windows allows only one process or stale serial object to own COM3. In the pinned SDL2 revision, this can also happen when Parsec removes a virtual controller and the unpatched removal handler fails to close that controller's UART object.

First verify the Step 12.9 hot-plug patch:

$bridge = "C:\RemoteSwitch\switch-pico-sdl2\src\switch_pico_bridge\controller_uart_bridge.py"
Select-String -Path $bridge -Pattern "ctx.uart.close","handle_device_removed\(event, pairing, contexts, uarts, console\)"

Both lines must appear. If either is absent, repeat Step 12.9.

For immediate recovery after applying or verifying the patch:

Stop-ScheduledTask -TaskName "Remote Switch - Controller Bridge" -ErrorAction SilentlyContinue
Start-Sleep -Seconds 2
Start-ScheduledTask -TaskName "Remote Switch - Controller Bridge"
Start-Sleep -Seconds 5
Get-Content C:\RemoteSwitch\logs\switch-pico-bridge.log -Tail 50

If access-denied messages continue, stop or close every other possible owner:

  • Any manual bridge window.
  • debug-bridge.py.
  • test-pico.py.
  • A serial terminal.
  • Another scheduled or Startup-folder copy of the launcher.

List likely duplicate processes with:

Get-CimInstance Win32_Process |
    Where-Object {
        $_.CommandLine -match "controller_uart_bridge|start-switch-bridge|debug-bridge|test-pico"
    } |
    Format-List ProcessId, ParentProcessId, Name, CommandLine

Then start exactly one scheduled bridge. A healthy Parsec disconnect/reconnect logs Released UART COM3 back to pool followed by a new paired to COM3 line.

The bridge uses the wrong controller

Disconnect all local host controllers. In Parsec, reset virtual gamepads, reconnect only the sister, and press her A button first.

ABXY letters are reversed

The final SDL2 command must not contain:

--swap-abxy

Confirm the scheduled script points to switch-pico-sdl2, not the newer switch-pico folder. Stop and restart the task after correcting it.

On the client:

  1. Open Parsec Settings > Gamepad.
  2. Confirm Menu reports Start.
  3. Confirm View reports Back.
  4. Remap them if necessary.
  5. Reconnect so Parsec recreates the host controller.

The pinned bridge already maps Start to Plus and Back to Minus.

Right-stick click does not produce Home

  1. On the Mac, open Parsec > Settings > Gamepad.
  2. Confirm physical R3 reports ordinary Right Stick, not Guide.
  3. On the Windows host, open joy.cpl and confirm R3 registers on the virtual Xbox 360 controller.
  4. Verify the host source mapping:
Select-String -Path C:\RemoteSwitch\switch-pico-sdl2\src\switch_pico_bridge\controller_uart_bridge.py -Pattern "RIGHTSTICK"

It must show:

sdl2.SDL_CONTROLLER_BUTTON_RIGHTSTICK: SwitchButton.HOME,

If it says SwitchButton.RCLICK, repeat Step 12.9. Then stop every bridge process and restart exactly one corrected bridge.

If the line is correct and R3 registers in joy.cpl, stop every bridge process and send HOME directly over UART:

Set-Location C:\RemoteSwitch\switch-pico-sdl2
@'
import time
from switch_pico_bridge import SwitchUARTClient, SwitchButton

with SwitchUARTClient("COM3", send_interval=1/500, auto_send=True) as controller:
    controller.neutral()
    time.sleep(0.5)
    controller.press_for(0.5, SwitchButton.HOME)
    time.sleep(0.5)
'@ | .\.venv\Scripts\python.exe -

No terminal output is expected. If the direct test opens HOME, the firmware and UART path are healthy; restart the corrected bridge and check that no stale manual or scheduled bridge is still running.

Share does not produce Capture

Capture is optional. Some client/virtual-controller combinations do not expose a distinct Misc/Share button. Do not remap R3 away from ordinary Right Stick to recover Capture.

Buttons again become directions or Up is stuck

That was the old Switch-facing Pro Controller report failure.

  1. Stop the bridge.
  2. Confirm the Pico was not reflashed with the original rollback UF2.
  3. Reflash switch-pico-hori-compat-rp2040.uf2.
  4. Restart with the SDL2 command from Step 12.10.

Do not troubleshoot this symptom by swapping physical controllers.

Stick movement continues at idle

  1. Release both sticks before connecting.
  2. Keep --zero-sticks.
  3. Keep --deadzone 0.12.
  4. Disconnect and reconnect the Parsec controller so neutral is captured again.
  5. If needed, increase to --deadzone 0.15.

Scheduled task runs but the Switch does nothing

Check:

Get-ScheduledTaskInfo -TaskName "Remote Switch - Controller Bridge"
Get-Content C:\RemoteSwitch\logs\switch-pico-bridge.log -Tail 80

Then verify:

  • Task user is the same RemoteSwitch account that owns .venv.
  • The log's Account: and Base Python: lines both identify that account.
  • Correct COM port.
  • Pico USB is on the Switch dock.
  • Debug Probe USB is on the Windows host.
  • Probe UART U port is used.
  • GPIO4 → probe RX, GPIO5 → probe TX, and GND → GND.
  • Pro Controller Wired Communication is On.
  • Step 12.9's COM-release handler is present.
  • Exactly one bridge process is running.

Parsec shows the desktop or OBS mixer instead of the Switch

  1. In OBS, open Tools > Scripts and confirm obs-auto-projector.lua is loaded.
  2. Confirm its scene name is exactly Switch.
  3. Reload the script or restart OBS and wait five seconds.
  4. If the projector opens on the wrong PC display, change its monitor index from 0 to 1.
  5. For immediate recovery, right-click Switch and choose Fullscreen Projector (Scene) on the display Parsec captures.

Press Esc locally to exit projector mode temporarily.

Sister hears silence

  • OBS Neo meter must move.
  • OBS Monitoring Device must be CABLE Input.
  • Neo monitoring must be Monitor Only.
  • Parsec Host Audio must be CABLE Input.
  • Reconnect the Parsec session after changing audio devices.

Switch is asleep

The HORI compatibility controller cannot wake it. Use a local power-button press or a tested dock-power workaround, then reconfirm docked auto-sleep is Never.

OBS shows an unclean-shutdown prompt

Connect through Parsec with mouse permission, choose Run Normally, and select the Remote Switch profile and collection. Confirm obs-auto-projector.lua remains loaded; it should open the full-screen scene projector five seconds after the next normal OBS launch.

20. Recovery ladder

Use the least disruptive step that works:

  1. Disconnect and reconnect the Parsec session.
  2. Restart Parsec on the client.
  3. Reset Parsec gamepads on the host, reconnect, and press A.
  4. Stop and start Remote Switch - Controller Bridge in Task Scheduler.
  5. Inspect the bridge log.
  6. Deactivate and reactivate the Neo source in OBS.
  7. Restart OBS and reopen its projector.
  8. Restart Windows normally.
  9. Power-cycle the dock/Neo HDMI chain if HDMI negotiation is stuck.
  10. Cut PC power only as a last resort.

Do not expose RDP, OBS WebSocket, serial ports, or ad-hoc web panels directly to the public Internet.

21. Expected experience

Image and sound

  • Switch output: 1920×1080, up to 60 fps.
  • Neo capture: 1080p60 SDR.
  • OBS: 1080p60 NV12, Rec. 709, Limited/Partial.
  • Parsec: H.264 hardware-encoded 1080p60 at up to 30 Mbps.
  • Audio: HDMI stereo → Neo → OBS → VB-CABLE → Parsec.
  • Family TV: independent HDMI passthrough.

Animal Crossing should look very good. Heavy rain, foliage, or rapid full-screen motion can show some compression.

Latency

A realistic healthy interstate target is approximately 70–140 ms of added button-to-image latency:

  • Controller forwarding, bridge, UART: a few milliseconds.
  • USB capture and OBS preview: commonly two to four 60 Hz frames.
  • Parsec encode, network, and decode: commonly another 20–60 ms.

This is comfortable for Animal Crossing and Jackbox. It is not a competitive fighting-game or rhythm-game target.

22. Final acceptance checklist

  • Pico still runs the HORI/Pokkén compatibility firmware.
  • Host bridge path is C:\RemoteSwitch\switch-pico-sdl2.
  • Pinned SDL2 commit begins with e13ba50.
  • .venv base Python belongs to the same RemoteSwitch account used by Task Scheduler.
  • Debug Probe COM port is correct in the launcher.
  • Parsec host emulates Xbox 360.
  • Xbox A/B/X/Y map to Switch A/B/X/Y by label.
  • Menu maps to Plus.
  • View maps to Minus.
  • R3 maps to Home through the host source change.
  • The patched removal handler closes the old UART before returning COM3 to the pairing pool.
  • Xbox logo is not required.
  • Share maps to Capture when the client exposes it.
  • Sticks remain neutral when released.
  • Scheduled bridge starts at logon and survives one Parsec disconnect/reconnect.
  • obs-auto-projector.lua opens the clean fullscreen Switch scene after OBS starts.
  • Sister receives one clean stereo audio feed.
  • Family TV retains normal video and audio.
  • Animal Crossing loads the sister's original user and resident.
  • Two unattended Windows restart tests pass.

Once every required item passes, the remote-play system is ready for normal use.

23. Primary references