Skip to content

Commit 738f367

Browse files
committed
fix(setup): remove controller prompt from first-run setup
1 parent 7b6b416 commit 738f367

7 files changed

Lines changed: 60 additions & 85 deletions

File tree

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,9 +28,9 @@ The Windows host reads the Parsec virtual Xbox controller, sends the button stat
2828
powershell -ExecutionPolicy Bypass -File .\setup.ps1
2929
```
3030

31-
5. Follow the prompts. The script flashes the Pico, provisions Wi-Fi, checks that the PC can find it, and can add `couchlink.exe` to Windows startup.
31+
5. Follow the prompts. The script flashes the Pico, provisions Wi-Fi, checks that the PC can find it, and can add `couchlink.exe` to Windows startup. No controller is needed for setup.
3232

33-
After setup, have the remote player join through Parsec. The bridge sends their gamepad to the Pico, and the console sees the Pico as a wired controller.
33+
After setup, have the remote player join through Parsec, run `couchlink.exe`, and choose controller routing. The bridge sends their gamepad to the Pico, and the console sees the Pico as a wired controller.
3434

3535
## Release Contents
3636

bridge/src/cmd_home.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -211,6 +211,7 @@ async fn stream(routes: Vec<cmd_run::StreamRoute>, save: bool) -> Result<()> {
211211
async fn flash_menu() -> Result<()> {
212212
println!();
213213
println!("Firmware update");
214+
println!("Flashing does not require a controller. Controller routing is tested when streaming starts.");
214215
println!(
215216
"Use setup-mode USB for no-button reflashing when the Pico firmware is already running setup mode."
216217
);

bridge/src/cmd_setup.rs

Lines changed: 48 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
//! `couchlink setup` -- end-to-end first-run wizard.
22
//!
33
//! Walks the operator through: hardware confirmation, BOOTSEL flash,
4-
//! Wi-Fi provisioning over CDC, run-mode reboot, LAN discovery, XInput
5-
//! smoke test, and Startup-folder shortcut install. Each stage is
6-
//! re-entrant: re-running setup is safe.
4+
//! Wi-Fi provisioning over CDC, run-mode reboot, LAN discovery, and
5+
//! Startup-folder shortcut install. Each stage is re-entrant:
6+
//! re-running setup is safe.
77
//!
88
//! All `dialoguer` calls live on a blocking task so they don't stall the
99
//! Tokio runtime.
@@ -18,6 +18,15 @@ use zeroize::Zeroize;
1818

1919
use crate::{cdc, config, diag_usb, journal, protocol};
2020

21+
const SETUP_STAGE_NAMES: [&str; 6] = [
22+
"Pre-flight",
23+
"Pico firmware (.uf2)",
24+
"Flash the Pico",
25+
"Wi-Fi provisioning over USB-CDC",
26+
"LAN discovery",
27+
"Autostart on logon",
28+
];
29+
2130
pub async fn run(uf2_override: Option<PathBuf>) -> Result<()> {
2231
journal!("setup", "wizard started");
2332
println!("couchlink setup");
@@ -35,7 +44,6 @@ pub async fn run(uf2_override: Option<PathBuf>) -> Result<()> {
3544
stage_flash(uf2).await?;
3645
stage_wifi_provisioning().await?;
3746
let (peer_ip, identity) = stage_lan_discovery().await?;
38-
stage_smoke_test().await?;
3947
stage_install_autostart().await?;
4048

4149
let mut cfg = config::load().unwrap_or_default();
@@ -53,14 +61,14 @@ pub async fn run(uf2_override: Option<PathBuf>) -> Result<()> {
5361

5462
println!();
5563
println!("Setup is complete. From now on, couchlink runs at logon.");
56-
println!("Have the remote player connect via Parsec to start using the bridge.");
64+
println!("Have the remote player connect via Parsec, then run `couchlink` and choose controller routing.");
5765
Ok(())
5866
}
5967

6068
async fn stage_preflight() -> Result<()> {
61-
println!("[1/7] Pre-flight");
62-
tracing::info!("setup: stage 1/7 -- pre-flight");
63-
journal!("setup", "stage 1/7 pre-flight");
69+
print_stage(0);
70+
tracing::info!("setup: stage 1/6 -- pre-flight");
71+
journal!("setup", "stage 1/6 pre-flight");
6472
config::ensure_dirs().context("creating config/log dirs")?;
6573
println!(" config dir: {}", config::config_dir()?.display());
6674
println!(" log dir: {}", config::log_dir()?.display());
@@ -83,8 +91,8 @@ async fn stage_preflight() -> Result<()> {
8391

8492
async fn stage_pick_uf2(uf2_override: Option<PathBuf>) -> Result<Option<PathBuf>> {
8593
println!();
86-
println!("[2/7] Pico firmware (.uf2)");
87-
tracing::info!("setup: stage 2/7 -- pick UF2");
94+
print_stage(1);
95+
tracing::info!("setup: stage 2/6 -- pick UF2");
8896

8997
if let Some(p) = uf2_override {
9098
if !p.exists() {
@@ -114,8 +122,8 @@ async fn stage_pick_uf2(uf2_override: Option<PathBuf>) -> Result<Option<PathBuf>
114122

115123
async fn stage_flash(uf2: Option<PathBuf>) -> Result<()> {
116124
println!();
117-
println!("[3/7] Flash the Pico");
118-
tracing::info!("setup: stage 3/7 -- flash");
125+
print_stage(2);
126+
tracing::info!("setup: stage 3/6 -- flash");
119127
println!(" How to put the Pico into BOOTSEL (flashing) mode:");
120128
println!();
121129
println!(" 1. Unplug the Pico if it is currently connected.");
@@ -140,15 +148,15 @@ async fn stage_flash(uf2: Option<PathBuf>) -> Result<()> {
140148
crate::cmd_flash::run(uf2, false, false).await?;
141149
println!(" Flash complete. The Pico is rebooting into setup mode --");
142150
println!(" leave the BOOTSEL button alone during this reboot.");
143-
tracing::info!("setup: stage 3/7 complete -- Pico rebooting into setup mode");
151+
tracing::info!("setup: stage 3/6 complete -- Pico rebooting into setup mode");
144152
Ok(())
145153
}
146154

147155
async fn stage_wifi_provisioning() -> Result<()> {
148156
println!();
149-
println!("[4/7] Wi-Fi provisioning over USB-CDC");
150-
tracing::info!("setup: stage 4/7 -- Wi-Fi provisioning");
151-
journal!("setup", "stage 4/7 Wi-Fi provisioning over USB-CDC");
157+
print_stage(3);
158+
tracing::info!("setup: stage 4/6 -- Wi-Fi provisioning");
159+
journal!("setup", "stage 4/6 Wi-Fi provisioning over USB-CDC");
152160
println!(
153161
" Wait a few seconds for the Pico to come back as a USB serial device, \
154162
then continue."
@@ -222,8 +230,8 @@ async fn stage_wifi_provisioning() -> Result<()> {
222230

223231
async fn stage_lan_discovery() -> Result<(String, protocol::AckInfo)> {
224232
println!();
225-
println!("[5/7] LAN discovery");
226-
tracing::info!("setup: stage 5/7 -- LAN discovery");
233+
print_stage(4);
234+
tracing::info!("setup: stage 5/6 -- LAN discovery");
227235
println!(" Waiting for the Pico to join your Wi-Fi and answer a discover broadcast...");
228236
let socket = UdpSocket::bind("0.0.0.0:0").await?;
229237
socket.set_broadcast(true)?;
@@ -428,36 +436,10 @@ fn print_lan_timeout_walkthrough() {
428436
println!(" 5. Run `couchlink bundle` and attach the resulting zip to a bug report.");
429437
}
430438

431-
async fn stage_smoke_test() -> Result<()> {
432-
println!();
433-
println!("[6/7] Smoke test");
434-
tracing::info!("setup: stage 6/7 -- XInput smoke test");
435-
println!(
436-
" Plug a controller into your PC (or have someone join your Parsec with \
437-
their gamepad). Then press a button."
438-
);
439-
println!(
440-
" This step doesn't actually move bytes through the Pico; it just \
441-
verifies XInput sees a controller. End-to-end input/output is exercised \
442-
the first time you press a button after run mode starts."
443-
);
444-
445-
// XInput one-shot: wait up to 30 s for any state change.
446-
let ok = wait_for_xinput_input(Duration::from_secs(30)).await;
447-
if ok {
448-
println!(" XInput input observed. Looks good.");
449-
} else {
450-
println!(
451-
" No XInput change in 30 s -- skipping. You can re-run `couchlink test xinput` later."
452-
);
453-
}
454-
Ok(())
455-
}
456-
457439
async fn stage_install_autostart() -> Result<()> {
458440
println!();
459-
println!("[7/7] Autostart on logon");
460-
tracing::info!("setup: stage 7/7 -- autostart shortcut");
441+
print_stage(5);
442+
tracing::info!("setup: stage 6/6 -- autostart shortcut");
461443

462444
let install = ask_yes_no(
463445
"Install a Startup-folder shortcut so couchlink runs at every logon?",
@@ -490,6 +472,15 @@ async fn stage_install_autostart() -> Result<()> {
490472
Ok(())
491473
}
492474

475+
fn print_stage(index: usize) {
476+
println!(
477+
"[{}/{}] {}",
478+
index + 1,
479+
SETUP_STAGE_NAMES.len(),
480+
SETUP_STAGE_NAMES[index]
481+
);
482+
}
483+
493484
async fn ask_yes_no(prompt: &str, default: bool) -> Result<bool> {
494485
let prompt = prompt.to_string();
495486
let r = tokio::task::spawn_blocking(move || {
@@ -588,36 +579,16 @@ async fn wait_for_setup_cdc(timeout: Duration) -> Result<String> {
588579
}
589580
}
590581

591-
async fn wait_for_xinput_input(timeout: Duration) -> bool {
592-
#[cfg(windows)]
593-
{
594-
use windows::Win32::UI::Input::XboxController::{
595-
XInputGetState, XINPUT_STATE, XUSER_MAX_COUNT,
596-
};
597-
let deadline = std::time::Instant::now() + timeout;
598-
let mut baseline: [u32; 4] = [u32::MAX; 4];
599-
for s in 0..XUSER_MAX_COUNT {
600-
let mut state = XINPUT_STATE::default();
601-
if unsafe { XInputGetState(s, &mut state) } == 0 {
602-
baseline[s as usize] = state.dwPacketNumber;
603-
}
604-
}
605-
while std::time::Instant::now() < deadline {
606-
for s in 0..XUSER_MAX_COUNT {
607-
let mut state = XINPUT_STATE::default();
608-
if unsafe { XInputGetState(s, &mut state) } == 0
609-
&& state.dwPacketNumber != baseline[s as usize]
610-
{
611-
return true;
612-
}
613-
}
614-
tokio::time::sleep(Duration::from_millis(20)).await;
615-
}
616-
false
617-
}
618-
#[cfg(not(windows))]
619-
{
620-
let _ = timeout;
621-
false
582+
#[cfg(test)]
583+
mod tests {
584+
use super::SETUP_STAGE_NAMES;
585+
586+
#[test]
587+
fn first_run_setup_does_not_require_controller_input() {
588+
let joined = SETUP_STAGE_NAMES.join(" ").to_ascii_lowercase();
589+
assert_eq!(SETUP_STAGE_NAMES.len(), 6);
590+
assert!(!joined.contains("xinput"));
591+
assert!(!joined.contains("controller"));
592+
assert!(!joined.contains("smoke"));
622593
}
623594
}

bridge/src/main.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ enum Command {
7676
#[arg(long)]
7777
quiet: bool,
7878
},
79-
/// First-time setup wizard: flash the Pico, provision Wi-Fi, smoke test.
79+
/// First-time setup wizard: flash the Pico, provision Wi-Fi, and check LAN discovery.
8080
Setup {
8181
/// Path to the .uf2 firmware to flash.
8282
#[arg(long)]

setup.ps1

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ if (-not $SkipIntro) {
5959
Write-Host "Parsec CouchLink setup" -ForegroundColor Green
6060
Write-Host ""
6161
Write-Host "This will flash your Pico, put your Wi-Fi on it, test that the PC can find it, and offer to add the bridge to Windows startup."
62+
Write-Host "You do not need a controller for setup; controller routing is tested when you start streaming."
6263
Write-Host ""
6364
Write-Host "Have these ready:"
6465
Write-Host " - Windows 10/11 PC running Parsec"

wiki/Quick-Start.md

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,9 @@ The script explains each step before it starts. In short:
3636
6. The Pico comes back as a USB serial setup device.
3737
7. Enter the 2.4 GHz Wi-Fi credentials when prompted.
3838
8. Setup waits for the Pico to join Wi-Fi and answer discovery.
39-
9. Press a button on the Parsec gamepad or a local Xbox controller for the smoke test.
40-
10. Accept the Startup shortcut if you want the bridge to run at every logon.
39+
9. Accept the Startup shortcut if you want the bridge to run at every logon.
40+
41+
No controller is needed during setup. Controller detection and routing happen later, when you choose **Start or route controllers** from the guided menu or run `couchlink.exe run`.
4142

4243
## What Success Looks Like
4344

@@ -47,7 +48,7 @@ Setup ends with:
4748
Setup is complete. From now on, couchlink runs at logon.
4849
```
4950

50-
Then plug the Pico into the USB-to-console adapter. Start the console, have the remote player join through Parsec, and leave `couchlink.exe` running on the Windows host.
51+
Then plug the Pico into the USB-to-console adapter. Start the console, have the remote player join through Parsec, run `couchlink.exe`, and choose **Start or route controllers**.
5152

5253
## Daily Use
5354

wiki/Setup-and-Flashing.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,12 @@
1010
4. Detects the Pico in BOOTSEL mode and copies the matching UF2 onto it.
1111
5. Sends Wi-Fi credentials over USB setup mode.
1212
6. Waits for the Pico on the LAN.
13-
7. Runs a simple XInput smoke test.
14-
8. Offers to create a Windows Startup shortcut.
13+
7. Offers to create a Windows Startup shortcut.
1514

1615
The Wi-Fi password is held only long enough to send it to the Pico. It is not written to disk on the PC.
1716

17+
Setup and firmware flashing do not require a controller. Controller checks belong to the routing/streaming path after the Pico is online.
18+
1819
## Supported Boards
1920

2021
Either of these works -- the wizard auto-picks the right firmware:

0 commit comments

Comments
 (0)