<#
Disable-FTDI-SelectiveSuspend-ApplyIfMissing-WithPortName.ps1
- Only writes properties if missing OR if current value differs from desired.
- Writes PortName (from FriendlyName or WMI) only when missing or different.
- Creates selective-suspend DWords only if missing or different from 0.
- Does NOT change actual COM assignment.
- Logs to Desktop; launches pnputil non-blocking; schedules a 15s reboot if real changes occurred.
Run elevated. Use -WhatIf to dry-run.
#> param( [switch]$WhatIf, [switch]$NoReboot) # require elevation
if (-not ([Security.Principal.WindowsPrincipal] [Security.Principal.WindowsIdentity]::GetCurrent.IsInRole([Security.Principal.WindowsBuiltinRole]::Administrator) { Write-Error "Run this script elevated (Run as Administrator). Exiting." exit 1
} $timestamp = (Get-Date).ToString('yyyyMMdd_HHmmss')
$logFile = Join-Path $env:USERPROFILE ("Desktop\FTDI_ApplyIfMissing_WithPortName_$timestamp.log")
"Started: $(Get-Date -Format u)" | Out-File -FilePath $logFile -Encoding UTF8 function Get-ComForInstance { param([string]$instanceId) try { $sp = Get-CimInstance -ClassName Win32_SerialPort -ErrorAction SilentlyContinue | Where-Object { $_.PNPDeviceID -eq $instanceId } if ($sp) { return $sp.DeviceID } $tail = ($instanceId -split '\\')[-1] if ($tail) { $sp = Get-CimInstance -ClassName Win32_SerialPort -ErrorAction SilentlyContinue | Where-Object { $_.PNPDeviceID -like "*$tail*" } if ($sp) { return $sp.DeviceID } } } catch { } return $null
} # Helper: set property only if missing or value differs. Returns $true if written (or would be written under -WhatIf)
function Ensure-Property { param( [string]$Path, [string]$Name, [ValidateSet('String','DWord')] [string]$Type, $DesiredValue) # read existing $existing = $null try { $prop = Get-ItemProperty -Path $Path -Name $Name -ErrorAction SilentlyContinue if ($prop -ne $null) { $existing = $prop.$Name } } catch { $existing = $null } # Normalize for comparison: DWord -> int, String -> string if ($Type -eq 'DWord') { if ($null -ne $existing) { try { $existingInt = [int]$existing } catch { $existingInt = $null } } else { $existingInt = $null } $desiredInt = [int]$DesiredValue if ($existingInt -ne $null -and $existingInt -eq $desiredInt) { # already same return $false } else { if ($WhatIf) { "$Name would be set to $desiredInt at $Path (existing: $existingInt)" | Tee-Object -FilePath $logFile -Append return $true } else { try { New-Item -Path $Path -Force | Out-Null New-ItemProperty -Path $Path -Name $Name -PropertyType DWord -Value $desiredInt -Force -ErrorAction Stop | Out-Null "$Name set to $desiredInt at $Path" | Tee-Object -FilePath $logFile -Append return $true } catch { "ERROR writing $Name at $Path : $($_.Exception.Message)" | Tee-Object -FilePath $logFile -Append return $false } } } } else { # string type compare (case-insensitive) $existingStr = $null if ($null -ne $existing) { $existingStr = [string]$existing } $desiredStr = [string]$DesiredValue if ($existingStr -ne $null -and $existingStr.Equals($desiredStr, [System.StringComparison]::OrdinalIgnoreCase) { return $false } else { if ($WhatIf) { "$Name would be set to '$desiredStr' at $Path (existing: '$existingStr')" | Tee-Object -FilePath $logFile -Append return $true } else { try { New-Item -Path $Path -Force | Out-Null New-ItemProperty -Path $Path -Name $Name -PropertyType String -Value $desiredStr -Force -ErrorAction Stop | Out-Null "$Name set to '$desiredStr' at $Path" | Tee-Object -FilePath $logFile -Append return $true } catch { "ERROR writing $Name at $Path : $($_.Exception.Message)" | Tee-Object -FilePath $logFile -Append return $false } } } }
} # find FTDI ports
$ports = Get-PnpDevice -Class Ports -PresentOnly -ErrorAction SilentlyContinue | Where-Object { ($_.Manufacturer -and $_.Manufacturer -like '*FTDI*') -or ($_.InstanceId -and $_.InstanceId -match 'VID_0403') } if (-not $ports -or $ports.Count -eq 0) { "No FTDI COM ports found." | Tee-Object -FilePath $logFile -Append Write-Output "No FTDI COM ports found. See log: $logFile" exit 0
} "Found $($ports.Count) FTDI COM port(s)." | Tee-Object -FilePath $logFile -Append $createdCount = 0
$errors = @
$enumBase = 'HKLM:\SYSTEM\CurrentControlSet\Enum' foreach ($p in $ports) { "----" | Tee-Object -FilePath $logFile -Append "Device FriendlyName: $($p.FriendlyName)" | Tee-Object -FilePath $logFile -Append "InstanceId: $($p.InstanceId)" | Tee-Object -FilePath $logFile -Append $inst = $p.InstanceId if ([string]::IsNullOrWhiteSpace($inst) { "InstanceId empty; skipping" | Tee-Object -FilePath $logFile -Append continue } # build candidate Device Parameters paths (search Enum first) $dpCandidates = @ try { $tail = ($inst -split '\\')[-1] $matches = Get-ChildItem -Path $enumBase -Recurse -ErrorAction SilentlyContinue | Where-Object { $_.PSPath -match [regex]::Escape($inst) -or ($tail -and ($_.PSPath -match [regex]::Escape($tail) } foreach ($m in $matches) { $node = [string]$m.PSPath $dpCandidates += Join-Path $node 'Device Parameters' $dpCandidates += Join-Path (Join-Path $node '0000') 'Device Parameters' } } catch { } if ($dpCandidates.Count -eq 0) { $parts = $inst -split '\\' $subpath = [string]($parts -join '\') $regPathBase = Join-Path $enumBase $subpath $regPathBase = [string]$regPathBase $dpCandidates += Join-Path $regPathBase 'Device Parameters' $dpCandidates += Join-Path (Join-Path $regPathBase '0000') 'Device Parameters' } $dpFound = $false foreach ($dp in $dpCandidates) { "Checking: $($dp)" | Tee-Object -FilePath $logFile -Append if (Test-Path $dp) { $dpFound = $true "Using Device Parameters: $($dp)" | Tee-Object -FilePath $logFile -Append # detect COM (FriendlyName first, then WMI) $friendly = $p.FriendlyName $detectedCom = $null if ($friendly) { $m = [regex]::Match($friendly,'\(COM(\d+)\)') if ($m.Success) { $detectedCom = "COM$($m.Groups[1].Value)" ; "Detected COM from FriendlyName: $($detectedCom)" | Tee-Object -FilePath $logFile -Append } } if (-not $detectedCom) { $detectedCom = Get-ComForInstance -instanceId $inst if ($detectedCom) { "Detected COM via WMI: $($detectedCom)" | Tee-Object -FilePath $logFile -Append } } # PortName: only change if missing or different if ($detectedCom) { $changed = Ensure-Property -Path $dp -Name 'PortName' -Type 'String' -DesiredValue $detectedCom if ($changed) { $createdCount++ } } else { "Could not detect COM for $inst; PortName left alone." | Tee-Object -FilePath $logFile -Append } # selective-suspend DWords: ensure exist and equal 0 foreach ($kv in @{ DeviceIdleEnabled=0; DefaultIdleState=0; UserSetDeviceIdleEnabled=0; SSIdleTimeout=0 }.GetEnumerator { $name = $kv.Key; $val = $kv.Value $changed = Ensure-Property -Path $dp -Name $name -Type 'DWord' -DesiredValue $val if ($changed) { $createdCount++ } } break } else { "Not found: $($dp)" | Tee-Object -FilePath $logFile -Append } } if (-not $dpFound) { # create primary Device Parameters key and retry $parts = $inst -split '\\' $subpath = [string]($parts -join '\') $regPathBase = Join-Path $enumBase $subpath $dp = Join-Path $regPathBase 'Device Parameters' "Device Parameters not found - creating $($dp)" | Tee-Object -FilePath $logFile -Append if ($WhatIf) { "WhatIf: would create key $($dp) and attempt writes" | Tee-Object -FilePath $logFile -Append } else { try { New-Item -Path $dp -Force | Out-Null "Created key $($dp)" | Tee-Object -FilePath $logFile -Append } catch { "ERROR creating key $($dp) : $($_.Exception.Message)" | Tee-Object -FilePath $logFile -Append $errors += "Create key error for $inst : $($_.Exception.Message)" continue } } # same writes after creating key $friendly = $p.FriendlyName $detectedCom = $null if ($friendly) { $m = [regex]::Match($friendly,'\(COM(\d+)\)') if ($m.Success) { $detectedCom = "COM$($m.Groups[1].Value)" ; "Detected COM from FriendlyName: $($detectedCom)" | Tee-Object -FilePath $logFile -Append } } if (-not $detectedCom) { $detectedCom = Get-ComForInstance -instanceId $inst if ($detectedCom) { "Detected COM via WMI: $($detectedCom)" | Tee-Object -FilePath $logFile -Append } } if ($detectedCom) { $changed = Ensure-Property -Path $dp -Name 'PortName' -Type 'String' -DesiredValue $detectedCom if ($changed) { $createdCount++ } } else { "Could not detect COM for $inst after creating key." | Tee-Object -FilePath $logFile -Append } foreach ($kv in @{ DeviceIdleEnabled=0; DefaultIdleState=0; UserSetDeviceIdleEnabled=0; SSIdleTimeout=0 }.GetEnumerator { $changed = Ensure-Property -Path $dp -Name $kv.Key -Type 'DWord' -DesiredValue $kv.Value if ($changed) { $createdCount++ } } } } # end foreach ports "Created/changed count: $createdCount" | Tee-Object -FilePath $logFile -Append
if ($errors.Count -gt 0) { "Errors: $($errors.Count)" | Tee-Object -FilePath $logFile -Append; $errors | Tee-Object -FilePath $logFile -Append } # Rescan devices (launch pnputil asynchronously so script finishes)
try { "Launching device rescan (pnputil /scan-devices) in background" | Tee-Object -FilePath $logFile -Append $proc = Start-Process -FilePath 'pnputil.exe' -ArgumentList '/scan-devices' -NoNewWindow -PassThru "pnputil started (PID $($proc.Id)" | Tee-Object -FilePath $logFile -Append
} catch { "pnputil failed to start: $($_.Exception.Message)" | Tee-Object -FilePath $logFile -Append
} if (($createdCount -gt 0) -and (-not $WhatIf) -and (-not $NoReboot) { "Scheduling reboot in 15 seconds so changes take effect (abort with 'shutdown /a')." | Tee-Object -FilePath $logFile -Append Start-Process -FilePath 'shutdown.exe' -ArgumentList @('/r','/t','15','/c','Applying FTDI Device Parameter changes') -NoNewWindow
} else { "No reboot scheduled. If you created properties and want immediate recognition, run: pnputil /scan-devices or reboot manually." | Tee-Object -FilePath $logFile -Append
} "Completed: $(Get-Date -Format u)" | Tee-Object -FilePath $logFile -Append
Write-Output "Done. Log saved to: $logFile"