|
| 1 | +# Lab Mode |
| 2 | + |
| 3 | +Lab mode is an opt-in remote-flash session. The host starts a single |
| 4 | +subcommand, gets back a short URL, and shares it with the operator |
| 5 | +who is iterating on firmware from a different machine. The operator |
| 6 | +uploads UF2 builds, forces the Pico into BOOTSEL drive mode, flashes, |
| 7 | +runs the bridge's diagnostic ladder, and pulls the firmware diag ring |
| 8 | +-- all over a tunnel, with no shell or arbitrary file-read surface. |
| 9 | + |
| 10 | +This page is for hosts (people who have the Pico physically plugged |
| 11 | +in) and operators (developers who want remote access for testing). |
| 12 | + |
| 13 | +--- |
| 14 | + |
| 15 | +## Trust model up front |
| 16 | + |
| 17 | +* The host runs `couchlink lab-mode` intentionally. The bridge is a |
| 18 | + normal foreground process; Ctrl+C ends the session and rotates the |
| 19 | + tokens so any URL that was shared stops working. |
| 20 | +* The accepted command surface is enumerated in code on both ends. |
| 21 | + The tunnel relay validates `kind` against an allowlist before |
| 22 | + queuing; the bridge validates again before dispatching. No |
| 23 | + arbitrary shell, no arbitrary file read, no process spawn. |
| 24 | +* Every command lands in |
| 25 | + `%LOCALAPPDATA%\ParsecCouchLink\data\logs\lab-mode.log` on the host |
| 26 | + as an audit trail. |
| 27 | +* Cleartext Wi-Fi credentials never traverse the tunnel. They live in |
| 28 | + a DPAPI-encrypted vault on the host, decrypted only when the |
| 29 | + operator triggers `wifi_apply_saved` and zeroized immediately after |
| 30 | + the CDC `SET_WIFI` call. |
| 31 | + |
| 32 | +--- |
| 33 | + |
| 34 | +## Host: starting a session |
| 35 | + |
| 36 | +```powershell |
| 37 | +# One-time: stash your 2.4 GHz Wi-Fi credentials locally so you don't |
| 38 | +# have to type them every iteration. DPAPI keeps the blob bound to |
| 39 | +# your Windows login. |
| 40 | +couchlink save-wifi --ssid 'home2g' # prompts for the password |
| 41 | +# (or pass --password if you really want to) |
| 42 | +# (--clear deletes the saved vault) |
| 43 | +
|
| 44 | +# Per session: |
| 45 | +couchlink lab-mode |
| 46 | +``` |
| 47 | + |
| 48 | +Lab mode prints two lines and goes silent: |
| 49 | + |
| 50 | +``` |
| 51 | +Lab session: https://couchlink.whyknot.dev/v/<token> |
| 52 | +Ctrl+C to end. Activity log: %LOCALAPPDATA%\ParsecCouchLink\data\logs\ |
| 53 | +``` |
| 54 | + |
| 55 | +Send the URL to the operator out-of-band (DM, email, signal -- it |
| 56 | +doubles as the auth, so treat it like a password). Press Ctrl+C any |
| 57 | +time to end. The tokens rotate whenever the bridge restarts. |
| 58 | + |
| 59 | +--- |
| 60 | + |
| 61 | +## Operator: driving a session |
| 62 | + |
| 63 | +From a separate machine, in PowerShell: |
| 64 | + |
| 65 | +```powershell |
| 66 | +iwr https://couchlink.whyknot.dev/client.ps1 -UseBasicParsing | iex |
| 67 | +Set-CouchlinkSession 'https://couchlink.whyknot.dev/v/<token>' |
| 68 | +Get-CouchlinkInfo |
| 69 | +``` |
| 70 | + |
| 71 | +`Get-CouchlinkInfo` lists `allowed_kinds` so you can see the live |
| 72 | +command set at any time. |
| 73 | + |
| 74 | +### Iteration loop |
| 75 | + |
| 76 | +```powershell |
| 77 | +Send-CouchlinkUf2 -Path .\couchlink-pico2w.uf2 # chunked + sha256 |
| 78 | +Invoke-CouchlinkBootsel # CDC -> UDP -> picotool |
| 79 | +Wait-Couchlink -Ms 1500 # BOOTSEL drive mounts |
| 80 | +Invoke-CouchlinkFlash # flash the upload |
| 81 | +Wait-Couchlink -Ms 4000 # Pico reboots |
| 82 | +Get-CouchlinkPicoIdentity # CDC HELLO |
| 83 | +Invoke-CouchlinkWifiApplySaved # vault on the host |
| 84 | +Find-CouchlinkPico # UDP discovery |
| 85 | +Read-CouchlinkPicoLog | Out-Host # firmware diag ring |
| 86 | +``` |
| 87 | + |
| 88 | +For a full forensic snapshot: |
| 89 | + |
| 90 | +```powershell |
| 91 | +Invoke-CouchlinkBundle -OutPath .\bundle.zip |
| 92 | +``` |
| 93 | + |
| 94 | +For situational awareness: |
| 95 | + |
| 96 | +```powershell |
| 97 | +Watch-CouchlinkStream # live events |
| 98 | +Get-CouchlinkLabState # bridge state snapshot |
| 99 | +Get-CouchlinkStateJournal -Tail 50 # host's state-journal.log |
| 100 | +Get-CouchlinkBridgeLog -Tail 200 # today's bridge log |
| 101 | +Test-CouchlinkPing # relay + bridge RTT |
| 102 | +``` |
| 103 | + |
| 104 | +--- |
| 105 | + |
| 106 | +## Accepted commands |
| 107 | + |
| 108 | +The relay rejects any other `kind` with a 400 before queueing. The |
| 109 | +bridge enforces the same list as a second gate. |
| 110 | + |
| 111 | +| `kind` | Purpose | |
| 112 | +|-----------------------|--------------------------------------------------------------------| |
| 113 | +| `upload_uf2` | Upload a UF2 to the host's lab-mode slot, chunked. | |
| 114 | +| `flash` | Flash the most recently uploaded UF2 onto a mounted BOOTSEL drive. | |
| 115 | +| `force_bootsel` | Try CDC, then UDP, then `picotool reboot -u -f` to enter BOOTSEL. | |
| 116 | +| `doctor` | Run the bridge's 7-check diagnostic ladder. | |
| 117 | +| `bundle` | Build a support zip and stream it back via file_chunk / file_eof. | |
| 118 | +| `identify` | CDC HELLO. Returns fw triple + board + creds_present + wifi_joined.| |
| 119 | +| `discover` | UDP broadcast on 4242. Returns the first ACK. | |
| 120 | +| `ping` | Round-trip nonce echo for relay+bridge liveness. | |
| 121 | +| `state` | Bridge state snapshot (version, last Pico, vault presence). | |
| 122 | +| `pull_log` | UDP TYPE_GET_LOG against the latched Pico's diag ring. | |
| 123 | +| `read_state_journal` | Tail of `state-journal.log` (1..2000 lines). | |
| 124 | +| `read_bridge_log` | Tail of today's tracing log (1..2000 lines). | |
| 125 | +| `wifi_apply_saved` | Decrypt the host's DPAPI vault and CDC SET_WIFI in one shot. | |
| 126 | +| `wifi_clear` | CDC CLEAR_WIFI -- wipe Pico flash creds, re-enter setup mode. | |
| 127 | +| `sleep` | Bridge-side bounded sleep so scripts can sequence without timers. | |
| 128 | + |
| 129 | +--- |
| 130 | + |
| 131 | +## Limits |
| 132 | + |
| 133 | +* 240 commands per 60 s per session at the relay (sliding window). |
| 134 | +* 2 MiB hard cap per command body at the relay. |
| 135 | +* 10 session mints per 60 s per source IP at the relay. |
| 136 | +* 24 h idle TTL on a session whose bridge has disconnected. |
| 137 | +* Wi-Fi vault: SSID up to 32 bytes, password up to 63 bytes (matches |
| 138 | + the firmware's flash_creds_t). |
| 139 | +* `read_state_journal` / `read_bridge_log`: 2000-line hard cap. |
| 140 | +* `sleep`: 60 000 ms hard cap. |
| 141 | + |
| 142 | +--- |
| 143 | + |
| 144 | +## Troubleshooting |
| 145 | + |
| 146 | +| Symptom | Likely cause | Action | |
| 147 | +|-----------------------------------------------|-------------------------------------------------------------|------------------------------------------------------------------------------| |
| 148 | +| 404 on `Get-CouchlinkInfo` | View token expired (host restarted the bridge). | Ask the host for a fresh URL. | |
| 149 | +| `bridge_connected: False` persists | Bridge not running, or host network can't reach the relay. | Have the host restart `couchlink lab-mode`. Check their firewall / DNS. | |
| 150 | +| 400 with `kind not in lab-mode allowlist` | Unknown `kind` in the POST body. | Check the live list at `Get-CouchlinkInfo.allowed_kinds`. | |
| 151 | +| 413 on `Send-CouchlinkUf2` | Single chunk over 2 MiB. | Re-run with `-ChunkBytes 131072` (or smaller). | |
| 152 | +| 429 from the relay | More than 240 commands in 60 s on this session. | Back off; sequence with `Wait-Couchlink -Ms`. | |
| 153 | +| `bootsel_result.ok == false` for all methods | Pico USB is fully wedged. | Ask the host to physically power-cycle the Pico. | |
| 154 | +| `identify` reports `no setup-mode Pico found` | Pico is in run mode (XInput, not CDC). | Run `force_bootsel` then `flash`, or `wifi_clear` then power-cycle. | |
| 155 | +| `wifi_apply_saved` says `no saved Wi-Fi` | Host hasn't populated the vault yet. | Ask the host to run `couchlink save-wifi` on their own terminal. | |
0 commit comments