mirror of
https://github.com/hakumaguro/vrcft-steam-frame.git
synced 2026-10-06 07:00:22 +02:00
- tune.py: reject reversed/flat per-eye ranges and keep the previous config; recommend wink assist; readiness check before recording; skip malformed trace lines - module: invalid steamframe-config.json is not applied (last good config kept, configError reported); neutral eyes when no source is fresh; trace written with invariant culture - setup: sandbox mode leaves real SteamVR settings and firewall alone; block rules handled separately; headset target only counts connected adapters - headset-setup.sh: build a pinned, reviewed frameeyeosc revision and move existing checkouts to it Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
303 lines
17 KiB
PowerShell
303 lines
17 KiB
PowerShell
<#
|
|
One-step setup for Steam Frame eye tracking on the PC (and optionally the headset).
|
|
Safe to run again at any time: every step checks first and only changes what is not already right.
|
|
|
|
usage:
|
|
powershell -ExecutionPolicy Bypass -File scripts\setup.ps1 # PC setup / update
|
|
powershell -ExecutionPolicy Bypass -File scripts\setup.ps1 -DryRun # show what would change, change nothing
|
|
powershell -ExecutionPolicy Bypass -File scripts\setup.ps1 -Headset steamos@192.168.1.50 # also set up the headset over SSH
|
|
powershell -ExecutionPolicy Bypass -File scripts\setup.ps1 -Uninstall # remove the module, restore the stock SteamLink module
|
|
|
|
What it does (PC):
|
|
1. finds VRCFaceTracking (any Steam library)
|
|
2. gets the module: prebuilt DLL next to this script's folder, or builds it (installs a per-user .NET 10 SDK if needed, asks first)
|
|
3. moves the stock SteamLink VRCFT module aside (it blocks UDP 9015; -Uninstall puts it back)
|
|
4. makes VRCFaceTracking send to VRChat on port 9000
|
|
5. Steam Link driver: OSC output port 9015 + share eye tracking (only while SteamVR is closed; backup kept)
|
|
6. installs/updates the module DLL (your steamframe-config.json calibration is never overwritten)
|
|
7. checks the Windows Firewall rule for the module (offers an admin prompt to add one)
|
|
8. restarts VRCFaceTracking through Steam, then runs the status check
|
|
#>
|
|
param(
|
|
[switch]$DryRun,
|
|
[switch]$Yes, # answer yes to every question
|
|
[switch]$Uninstall,
|
|
[string]$Headset, # user@host of the Steam Frame (optional)
|
|
[string]$HeadsetTarget, # PC address the headset should send to (default: auto-detect / keep existing)
|
|
[string]$KeyFile = "$env:USERPROFILE\.ssh\id_ed25519_frame",
|
|
[switch]$NoSpeech,
|
|
[switch]$NoRestart, # do not stop/start VRCFaceTracking (for testing against -VrcftData)
|
|
[string]$VrcftData, # VRCFT data folder override (testing). Sandbox mode: SteamVR settings and firewall are not touched
|
|
[string]$SteamVrSettings # steamvr.vrsettings to use instead of Steam's (testing; with -VrcftData only this file is edited)
|
|
)
|
|
$ErrorActionPreference = "Continue" # native tools (ssh, dotnet) write to stderr; each change below stops on its own errors
|
|
. "$PSScriptRoot\common.ps1"
|
|
$script:NoSpeech = $NoSpeech
|
|
$root = Split-Path -Parent $PSScriptRoot
|
|
$data = Get-VrcftDataDir $VrcftData
|
|
$sandbox = [bool]$VrcftData
|
|
if ($sandbox) { Write-Host "SANDBOX: VRCFT data in $VrcftData; SteamVR settings only if -SteamVrSettings is given; firewall skipped.`n" -ForegroundColor Cyan }
|
|
$parked = Join-Path $data "disabled-modules"
|
|
$changed = @() # human-readable list of changes made
|
|
$needVrcftRestart = $false
|
|
$script:pendingStock = @()
|
|
|
|
function Ask([string]$q) {
|
|
if ($Yes) { return $true }
|
|
if ($DryRun) { return $false }
|
|
$a = Read-Host "$q [Y/n]"
|
|
return ($a -eq "" -or $a -match '^[yY]')
|
|
}
|
|
function Act([string]$what, [scriptblock]$do) {
|
|
if ($DryRun) { Write-Step would $what; return }
|
|
try { $ErrorActionPreference = "Stop"; & $do }
|
|
catch { Write-Step fail "$what : $($_.Exception.Message)"; Say "Setup failed. See the screen."; exit 1 }
|
|
Write-Step done $what
|
|
$script:changed += $what
|
|
}
|
|
|
|
if ($DryRun) { Write-Host "DRY RUN: nothing will be changed.`n" -ForegroundColor Cyan }
|
|
|
|
# ---- 1. VRCFaceTracking -----------------------------------------------------------------------------
|
|
$vrcft = Find-VrcftDir
|
|
if (-not $vrcft) { Write-Step fail "VRCFaceTracking not found. Install it from Steam first (app $script:SteamAppId)."; Say "VRCFaceTracking is not installed."; exit 1 }
|
|
Write-Step ok "VRCFaceTracking: $vrcft"
|
|
|
|
# ---- uninstall ----------------------------------------------------------------------------------------
|
|
if ($Uninstall) {
|
|
$ours = Get-OurModule $data
|
|
$stockParked = @(Get-ChildItem $parked -Directory -ErrorAction SilentlyContinue | Where-Object { Test-Path (Join-Path $_.FullName $script:StockDll) })
|
|
if (-not $ours -and -not $stockParked) { Write-Step ok "nothing to uninstall"; exit 0 }
|
|
if (-not $NoRestart -and -not $DryRun) { [void](Stop-Vrcft) }
|
|
if ($ours) {
|
|
$cfg = Join-Path $ours.Dir "steamframe-config.json"
|
|
Act "remove the Steam Frame module (calibration kept in $parked\steamframe-config.backup.json)" {
|
|
New-Item -ItemType Directory -Force $parked | Out-Null
|
|
if (Test-Path $cfg) { Copy-Item $cfg (Join-Path $parked "steamframe-config.backup.json") -Force }
|
|
Remove-Item $ours.Dir -Recurse -Force
|
|
}
|
|
}
|
|
foreach ($p in $stockParked) {
|
|
$name = $p.Name -replace '^SteamLink-', ''
|
|
Act "restore the stock SteamLink module ($($p.Name))" { Move-Item $p.FullName (Join-Path (Join-Path $data "CustomLibs") $name) }
|
|
}
|
|
Write-Step info "SteamVR's Steam Link OSC settings were left as they are (port 9015). Set 'OSC Output Port' back to 9000 in SteamVR if you want."
|
|
if (-not $NoRestart -and -not $DryRun) { Start-VrcftViaSteam }
|
|
Say "Uninstalled."
|
|
exit 0
|
|
}
|
|
|
|
# ---- 2. module DLL: prebuilt or build ---------------------------------------------------------------
|
|
$prebuilt = Join-Path $root $script:ModuleDll
|
|
$src = Join-Path $root "module"
|
|
$dll = $null
|
|
if (Test-Path $prebuilt) {
|
|
$dll = $prebuilt
|
|
Write-Step ok "using the prebuilt module: $prebuilt"
|
|
} elseif (Test-Path $src) {
|
|
$dotnet = Find-DotnetSdk10
|
|
if (-not $dotnet) {
|
|
Write-Step warn ".NET 10 SDK not found (needed to build the module from source)"
|
|
if ((-not $DryRun) -and (Ask "Download and install the .NET 10 SDK for your user only (about 300 MB, no admin)?")) {
|
|
$inst = Join-Path $env:TEMP "dotnet-install.ps1"
|
|
Invoke-WebRequest "https://dot.net/v1/dotnet-install.ps1" -OutFile $inst -UseBasicParsing
|
|
& $inst -Channel 10.0 -InstallDir "$env:USERPROFILE\.dotnet" -NoPath
|
|
$dotnet = Find-DotnetSdk10
|
|
}
|
|
if (-not $dotnet) {
|
|
if ($DryRun) { Write-Step would "install a per-user .NET 10 SDK and build the module" }
|
|
else { Write-Step fail "cannot build without the .NET 10 SDK (or download a release zip that contains $script:ModuleDll)"; exit 1 }
|
|
}
|
|
}
|
|
if ($dotnet) {
|
|
if ($DryRun) { Write-Step would "build the module" }
|
|
else {
|
|
& $dotnet build $src -c Release --nologo -v q "-p:VrcftDir=$vrcft" | Out-Host
|
|
if ($LASTEXITCODE -ne 0) { Write-Step fail "build failed"; Say "Setup failed. The build did not work."; exit 1 }
|
|
Write-Step ok "module built"
|
|
}
|
|
$dll = Join-Path $src "bin\Release\net10.0\$script:ModuleDll"
|
|
}
|
|
} else { Write-Step fail "no module found (neither $script:ModuleDll nor module\ next to scripts\)"; exit 1 }
|
|
|
|
# ---- 3. stock SteamLink module ------------------------------------------------------------------------
|
|
$stock = @(Get-StockSteamLinkModules $data)
|
|
if ($stock.Count -eq 0) { Write-Step ok "stock SteamLink VRCFT module is not installed" }
|
|
else {
|
|
foreach ($m in $stock) {
|
|
Write-Step warn "stock SteamLink module installed ($($m.Name)); it blocks UDP 9015"
|
|
if ($DryRun -or (Ask "Move it aside? (setup.ps1 -Uninstall puts it back)")) {
|
|
$needVrcftRestart = $true
|
|
$script:pendingStock += ,$m
|
|
}
|
|
}
|
|
}
|
|
|
|
# ---- 4. VRCFT output port -----------------------------------------------------------------------------
|
|
$outPort = Get-VrcftOscOutPort $data
|
|
$fixOutPort = ($outPort -and $outPort -ne "9000")
|
|
if ($fixOutPort) { Write-Step warn "VRCFaceTracking sends to port $outPort (VRChat listens on 9000)"; $needVrcftRestart = $true }
|
|
elseif ($outPort) { Write-Step ok "VRCFaceTracking sends to VRChat on port 9000" }
|
|
|
|
# ---- 5. Steam Link driver settings ---------------------------------------------------------------------
|
|
$vrPath = $SteamVrSettings
|
|
if (-not $vrPath -and -not $sandbox) { $vrPath = Get-SteamVrSettingsPath }
|
|
$vr = Get-VrlinkSettings $vrPath
|
|
if ($sandbox -and -not $vrPath) { Write-Step skip "sandbox: the real SteamVR settings are not touched" }
|
|
elseif ($vr.Ok) { Write-Step ok "Steam Link shares eye data on port 9015" }
|
|
elseif (-not $vrPath -or -not (Test-Path $vrPath)) { Write-Step warn "steamvr.vrsettings not found (start SteamVR once, then run setup again)" }
|
|
elseif ((-not $SteamVrSettings) -and (Test-SteamVrRunning)) {
|
|
Write-Step warn "Steam Link OSC settings need changing, but SteamVR is running (it would overwrite the change)."
|
|
Write-Host " Close SteamVR and run setup again, or in SteamVR: Settings > Advanced > Video > Steam Link: OSC Output Port 9015, share eye tracking on." -ForegroundColor DarkYellow
|
|
} else {
|
|
Act "Steam Link driver: OSC output port 9015 and eye data sharing (backup: steamvr.vrsettings.bak-steamframe)" {
|
|
$bak = "$vrPath.bak-steamframe"
|
|
if (-not (Test-Path $bak)) { Copy-Item $vrPath $bak }
|
|
$j = Get-Content $vrPath -Raw | ConvertFrom-Json
|
|
if (-not $j.driver_vrlink) { $j | Add-Member -NotePropertyName driver_vrlink -NotePropertyValue ([pscustomobject]@{}) }
|
|
$j.driver_vrlink | Add-Member -NotePropertyName OSCOutPort -NotePropertyValue 9015 -Force
|
|
$j.driver_vrlink | Add-Member -NotePropertyName shareEyeTrackingData -NotePropertyValue $true -Force
|
|
[IO.File]::WriteAllText($vrPath, ($j | ConvertTo-Json -Depth 32), (New-Object Text.UTF8Encoding($false)))
|
|
}
|
|
}
|
|
|
|
# ---- 6. module install --------------------------------------------------------------------------------
|
|
$ours = Get-OurModule $data
|
|
$installDir = $null; $dllChanged = $true
|
|
if ($ours) {
|
|
$installDir = $ours.Dir
|
|
$cur = Join-Path $installDir $script:ModuleDll
|
|
if ($dll -and (Test-Path $dll) -and (Test-Path $cur) -and ((Get-FileHash $dll).Hash -eq (Get-FileHash $cur).Hash)) { $dllChanged = $false }
|
|
}
|
|
if (-not $dllChanged) { Write-Step ok "module is up to date" }
|
|
else { $needVrcftRestart = $true }
|
|
|
|
$mjFix = $false
|
|
if ($ours) {
|
|
try { $mj = Get-Content (Join-Path $ours.Dir "module.json") -Raw | ConvertFrom-Json } catch { $mj = $null }
|
|
if (-not $mj -or -not $mj.ModulePageUrl -or -not $mj.DownloadUrl -or $mj.Version -ne $script:ModuleVersion) {
|
|
Write-Step warn "module.json is incomplete or outdated (missing links make VRCFaceTracking's Module Registry page crash)"
|
|
$mjFix = $true; $needVrcftRestart = $true
|
|
} else { Write-Step ok "module.json is complete" }
|
|
}
|
|
|
|
# apply the VRCFT-side changes together, with VRCFT closed (it holds the DLL and rewrites its settings on exit)
|
|
if ($needVrcftRestart) {
|
|
$wasRunning = [bool](Get-Process VRCFaceTracking -ErrorAction SilentlyContinue)
|
|
if ($wasRunning -and -not $NoRestart) { Act "close VRCFaceTracking to apply changes" { [void](Stop-Vrcft) } }
|
|
foreach ($m in @($script:pendingStock)) {
|
|
if (-not $m) { continue }
|
|
Act "move the stock SteamLink module to $parked" {
|
|
New-Item -ItemType Directory -Force $parked | Out-Null
|
|
Move-Item $m.Dir (Join-Path $parked ("SteamLink-" + $m.Id)) -Force
|
|
}
|
|
}
|
|
if ($fixOutPort) {
|
|
Act "VRCFaceTracking output port $outPort -> 9000" {
|
|
$p = Get-VrcftLocalSettingsPath $data
|
|
$raw = [IO.File]::ReadAllText($p)
|
|
$raw = [regex]::Replace($raw, '("OSCOutPort"\s*:\s*"?)\d+', '${1}9000')
|
|
[IO.File]::WriteAllText($p, $raw, (New-Object Text.UTF8Encoding($true)))
|
|
}
|
|
}
|
|
if ($mjFix) { Act "rewrite module.json" { Write-ModuleJson $installDir $ours.Id } }
|
|
if ($dllChanged) {
|
|
if (-not $installDir) {
|
|
$id = [guid]::NewGuid().ToString()
|
|
$installDir = Join-Path (Join-Path $data "CustomLibs") $id
|
|
Act "install the module into $installDir" {
|
|
New-Item -ItemType Directory -Force $installDir | Out-Null
|
|
Write-ModuleJson $installDir $id
|
|
Copy-Item $dll $installDir -Force
|
|
$saved = Join-Path $parked "steamframe-config.backup.json"
|
|
if (Test-Path $saved) { Copy-Item $saved (Join-Path $installDir "steamframe-config.json"); Write-Step info "restored your earlier calibration" }
|
|
}
|
|
} else {
|
|
Act "update the module DLL (calibration kept)" { Copy-Item $dll $installDir -Force }
|
|
}
|
|
}
|
|
}
|
|
|
|
# ---- 7. firewall --------------------------------------------------------------------------------------
|
|
if ($sandbox) { Write-Step skip "sandbox: firewall not checked" }
|
|
else {
|
|
$fw = Test-ModuleFirewall $vrcft
|
|
$allowCmd = "New-NetFirewallRule -DisplayName 'Steam Frame eye tracking (VRCFT module)' -Direction Inbound -Action Allow -Protocol UDP -LocalPort 9020 -Program '$($fw.Exe)' -Profile Any"
|
|
$fwCmd = $null; $fwQuestion = $null
|
|
if ($fw.State -eq "ok") { Write-Step ok "firewall allows the module process" }
|
|
elseif ($fw.State -eq "blocked") {
|
|
# A block rule wins over any allow rule, so adding an allow rule alone would not help.
|
|
Write-Step warn "firewall: a rule BLOCKS the module process (Windows adds one when its network prompt is answered with Cancel)"
|
|
$names = ($fw.BlockRules | ForEach-Object { "'" + $_ + "'" }) -join ","
|
|
$fwCmd = "Get-NetFirewallRule -Name $names | Disable-NetFirewallRule; $allowCmd"
|
|
$fwQuestion = "Disable the blocking rule(s) for the module and add an allow rule for UDP 9020? (Windows will ask for admin)"
|
|
} else {
|
|
Write-Step warn "firewall: no allow rule on: $($fw.Missing) network (headset data on UDP 9020 may not arrive)"
|
|
$fwCmd = $allowCmd
|
|
$fwQuestion = "Add a firewall rule allowing UDP 9020 to the module? (Windows will ask for admin)"
|
|
}
|
|
if ($fwCmd) {
|
|
if ($DryRun) { Write-Step would ($fwQuestion -replace '\?.*$', '') }
|
|
elseif (Ask $fwQuestion) {
|
|
# -EncodedCommand: Start-Process joins arguments without quoting, which breaks paths with spaces
|
|
$enc = [Convert]::ToBase64String([Text.Encoding]::Unicode.GetBytes($fwCmd))
|
|
Start-Process powershell -Verb RunAs -Wait -ArgumentList "-NoProfile", "-EncodedCommand", $enc
|
|
$after = (Test-ModuleFirewall $vrcft).State
|
|
if ($after -eq "ok") { Write-Step done "firewall now allows the module"; $changed += "firewall" }
|
|
elseif ($after -eq "blocked") { Write-Step warn "a blocking rule is still active (admin prompt declined, or the rule is set by policy); check Windows Defender Firewall > Inbound Rules" }
|
|
else { Write-Step warn "no allow rule yet (admin prompt declined?)" }
|
|
}
|
|
}
|
|
}
|
|
|
|
# ---- 8. headset (optional) ----------------------------------------------------------------------------
|
|
if ($Headset) {
|
|
$sshOpts = @("-i", $KeyFile, "-o", "IdentitiesOnly=yes", "-o", "ConnectTimeout=8", "-o", "StrictHostKeyChecking=accept-new")
|
|
Write-Host ""
|
|
Write-Step info "headset: $Headset"
|
|
if (-not (Test-Path $KeyFile)) {
|
|
if ($DryRun) { Write-Step would "create SSH key $KeyFile" }
|
|
else { ssh-keygen -q -t ed25519 -f $KeyFile -N '""' -C "vrcft-steam-frame"; Write-Step done "SSH key created: $KeyFile" }
|
|
}
|
|
$keyOk = $false
|
|
if (Test-Path $KeyFile) { & ssh @sshOpts -o BatchMode=yes $Headset "true" 2>$null; $keyOk = ($LASTEXITCODE -eq 0) }
|
|
if (-not $keyOk -and -not $DryRun) {
|
|
Write-Step info "authorising the key on the headset: enter the headset password when asked (one time only)"
|
|
Get-Content "$KeyFile.pub" | & ssh -o ConnectTimeout=8 -o StrictHostKeyChecking=accept-new $Headset "mkdir -p ~/.ssh && chmod 700 ~/.ssh && cat >> ~/.ssh/authorized_keys && chmod 600 ~/.ssh/authorized_keys"
|
|
& ssh @sshOpts -o BatchMode=yes $Headset "true" 2>$null; $keyOk = ($LASTEXITCODE -eq 0)
|
|
}
|
|
if ($DryRun) { Write-Step would "copy the headset scripts, build frameeyeosc, install the auto-start service" }
|
|
elseif (-not $keyOk) { Write-Step fail "cannot log in to $Headset with the key; is SSH enabled on the headset and the address right?" }
|
|
else {
|
|
Write-Step ok "SSH login works"
|
|
# copy the scripts first; remote commands stay free of quotes (Windows PowerShell 5.1 mangles them)
|
|
& ssh @sshOpts -o BatchMode=yes $Headset "mkdir -p ~/steamframe"
|
|
& scp @sshOpts -q "$root\scripts\headset-setup.sh" "$root\scripts\headset-install.sh" "$root\scripts\frameeyeosc.service" "${Headset}:steamframe/"
|
|
& ssh @sshOpts -o BatchMode=yes $Headset 'sed -i s/\r$// ~/steamframe/*'
|
|
$info = & ssh @sshOpts -o BatchMode=yes $Headset "bash ~/steamframe/headset-install.sh --info"
|
|
$client = (($info | Where-Object { $_ -like "CLIENT=*" }) -replace '^CLIENT=', '')
|
|
$unitTarget = (($info | Where-Object { $_ -like "UNIT=*" }) -replace '^UNIT=', '')
|
|
$pick = Select-HeadsetTarget -Override $HeadsetTarget -Existing $unitTarget -Client $client
|
|
$target = $pick.Target
|
|
Write-Step info $pick.Reason
|
|
if (-not $target) { Write-Step fail "could not work out this PC's address as seen from the headset; pass -HeadsetTarget <PC-IP>"; exit 1 }
|
|
& ssh @sshOpts -o BatchMode=yes $Headset "bash ~/steamframe/headset-install.sh $target 9020"
|
|
if ($LASTEXITCODE -eq 0) { Write-Step done "headset: frameeyeosc installed and sending to ${target}:9020"; $changed += "headset service" }
|
|
else { Write-Step fail "headset setup failed (see the output above)" }
|
|
}
|
|
}
|
|
|
|
# ---- 9. restart + status ------------------------------------------------------------------------------
|
|
Write-Host ""
|
|
if ($DryRun) { Write-Host "Dry run finished." -ForegroundColor Cyan; exit 0 }
|
|
if (-not $NoRestart -and -not (Get-Process VRCFaceTracking -ErrorAction SilentlyContinue)) {
|
|
Write-Step info "starting VRCFaceTracking through Steam, then checking (about 20 s)"
|
|
Start-VrcftViaSteam
|
|
for ($i = 0; $i -lt 30; $i++) { Start-Sleep 1; $s = Read-ModuleStatus; if ($s -and $s.AgeMs -lt 2000 -and $s.source -ne "starting") { break } }
|
|
}
|
|
if ($changed.Count -eq 0) { Write-Host "Nothing needed changing." -ForegroundColor Green } else { Write-Host ("Changed: " + ($changed -join "; ")) -ForegroundColor Green }
|
|
if ($NoRestart) { exit 0 }
|
|
$docArgs = @("-ExecutionPolicy", "Bypass", "-File", "$PSScriptRoot\doctor.ps1")
|
|
if ($NoSpeech) { $docArgs += "-NoSpeech" }
|
|
& powershell @docArgs
|