Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,14 @@ All notable changes to this project are documented here.

The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and the project follows [Semantic Versioning](https://semver.org/).

## [1.4.0] - 2026-08-13

### Added
- **SteelSeries Arctis Nova 5/5X provider** (`SteelSeriesNova5` detection mode): reads the headset physical state directly over HID (P/Invoke to `hid.dll`/`setupapi.dll`, no SteelSeries GG or third-party software). Adds `lib/SteelSeriesNova5.psm1`, Pester tests and a diagnostic tool (`tools/Test-SteelSeriesNova5Hid.ps1`) to watch the receiver state live.
- Installer offers a "SteelSeries Arctis Nova 5/5X" option when picking the headset type; it verifies the HID receiver is present and writes `DetectionMode = SteelSeriesNova5`.
- Runtime worker supports the `SteelSeriesNova5` mode (state read every poll; `Unknown` never switches). Verifier shows the receiver presence for this mode.
- The release package now bundles the SteelSeries module and the diagnostic tool.

## [1.3.0] - 2026-08-13

### Changed
Expand Down
58 changes: 43 additions & 15 deletions Instalar-PROX2-AutoSwitch.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ $RuntimeSrc = Join-Path $PackageDir "Runtime-PROX2-AutoSwitch.ps1"
$UninstallSrc = Join-Path $PackageDir "Desinstalar-PROX2-AutoSwitch.ps1"
$VerifySrc = Join-Path $PackageDir "Verificar-PROX2-AutoSwitch.ps1"
$ModuleSrc = Join-Path $PackageDir "lib\AutoSwitchCore.psm1"
$SteelModuleSrc = Join-Path $PackageDir "lib\SteelSeriesNova5.psm1"
$HelperSrc = Join-Path $PackageDir "Toggle-AudioEnhancements.ps1"
$IconSrc = Join-Path $PackageDir "assets\icon.ico"

Expand All @@ -20,7 +21,7 @@ $StartupDir = [Environment]::GetFolderPath("Startup")
$ShortcutPath = Join-Path $StartupDir "PRO X 2 AutoSwitch.lnk"


foreach ($required in @($RuntimeSrc, $UninstallSrc, $VerifySrc, $ModuleSrc, $HelperSrc, $IconSrc)) {
foreach ($required in @($RuntimeSrc, $UninstallSrc, $VerifySrc, $ModuleSrc, $SteelModuleSrc, $HelperSrc, $IconSrc)) {
if (-not (Test-Path $required)) {
throw "A package file is missing: $required. Extract the full ZIP before installing."
}
Expand Down Expand Up @@ -84,6 +85,7 @@ Copy-Item $HelperSrc (Join-Path $InstallDir "Toggle-AudioEnhancements.ps1") -For
Copy-Item $IconSrc (Join-Path $InstallDir "icon.ico") -Force
New-Item -ItemType Directory -Path (Join-Path $InstallDir "lib") -Force | Out-Null
Copy-Item $ModuleSrc (Join-Path $InstallDir "lib\AutoSwitchCore.psm1") -Force
Copy-Item $SteelModuleSrc (Join-Path $InstallDir "lib\SteelSeriesNova5.psm1") -Force

# --- G HUB functions for the installer ---
$script:Ws = $null
Expand Down Expand Up @@ -378,18 +380,22 @@ try {
Write-Host " -> Use WindowsEndpoint: Windows detects the endpoint as" -ForegroundColor DarkGray
Write-Host " Active when ON and Unplugged/absent when OFF." -ForegroundColor DarkGray
Write-Host ""
Write-Host " [2] Logitech PRO X 2 (LIGHTSPEED dongle stays plugged in)" -ForegroundColor White
Write-Host " [2] Logitech PRO X (PRO X 2 / PRO X Wireless)" -ForegroundColor White
Write-Host " -> Use LogitechGHub: Windows keeps the endpoint Active even" -ForegroundColor DarkGray
Write-Host " when OFF, so detection uses the G HUB battery signal." -ForegroundColor DarkGray
Write-Host ""
Write-Host " [3] Not sure - validate automatically" -ForegroundColor White
Write-Host " -> Run the ON->OFF->ON cycle and auto-detect the mode" -ForegroundColor DarkGray
Write-Host " (recommended if this is a new headset)." -ForegroundColor DarkGray
Write-Host ""
Write-Host " [4] SteelSeries Arctis Nova 5/5X" -ForegroundColor White
Write-Host " -> Use SteelSeriesNova5: reads the headset state over HID" -ForegroundColor DarkGray
Write-Host " (no SteelSeries GG or third-party software needed)." -ForegroundColor DarkGray
$cycleChoice = 0
do {
$cc = Read-Host "Choose 1, 2 or 3"
$cc = Read-Host "Choose 1, 2, 3 or 4"
[int]::TryParse($cc, [ref]$cycleChoice) | Out-Null
} until ($cycleChoice -ge 1 -and $cycleChoice -le 3)
} until ($cycleChoice -ge 1 -and $cycleChoice -le 4)

if ($cycleChoice -eq 1) {
# Standard wireless headset: assume WindowsEndpoint. The selected endpoints
Expand All @@ -400,18 +406,18 @@ try {
}
elseif ($cycleChoice -eq 2) {
# Logitech PRO X 2: needs G HUB. The endpoint alone cannot tell ON from OFF.
Write-Host " Assuming a Logitech PRO X 2. Looking up the headset in G HUB..." -ForegroundColor Yellow
Write-Host " Assuming a Logitech PRO X. Looking up the headset in G HUB..." -ForegroundColor Yellow
try {
Connect-GHub
$devices = Invoke-GHubGet -Path "/devices/list"
$deviceInfos = @($devices.payload.deviceInfos)

$ghubCandidates = @($deviceInfos | Where-Object {
$_.extendedDisplayName -match 'PRO\s*X\s*2'
Test-LogitechProXDeviceName -Name $_.extendedDisplayName
})

if ($ghubCandidates.Count -eq 0) {
Write-Host " G HUB reports no PRO X 2. Try option 3 (auto-detect)." -ForegroundColor Red
Write-Host " G HUB reports no Logitech PRO X headset. Try option 3 (auto-detect)." -ForegroundColor Red
}
else {
$ghubHeadset = $ghubCandidates[0]
Expand All @@ -422,7 +428,7 @@ try {
}
$ghubChoice = 0
do {
$gc = Read-Host "Enter the number of the PRO X 2 that matches '$headsetName'"
$gc = Read-Host "Enter the number of the PRO X headset that matches '$headsetName'"
$ghubValid = [int]::TryParse($gc, [ref]$ghubChoice) -and
$ghubChoice -ge 1 -and
$ghubChoice -le $ghubCandidates.Count
Expand All @@ -441,6 +447,28 @@ try {
Write-Host " No G HUB association was set; the config will not be written." -ForegroundColor DarkGray
}
}
elseif ($cycleChoice -eq 4) {
# SteelSeries Arctis Nova 5/5X: HID receiver detection via the module.
$steelModule = Join-Path $InstallDir "lib\SteelSeriesNova5.psm1"
if (-not (Test-Path $steelModule)) {
Write-Host " SteelSeries module not present in the package; cannot use this mode." -ForegroundColor Red
}
else {
try {
Import-Module $steelModule -ErrorAction Stop
if (Test-SteelSeriesNova5Receiver) {
$DetectionMode = "SteelSeriesNova5"
Write-Host " SteelSeries Nova 5/5X receiver detected over HID." -ForegroundColor Green
}
else {
Write-Host " No compatible SteelSeries Nova 5/5X receiver found." -ForegroundColor Red
}
}
catch {
Write-Host " SteelSeries check failed: $($_.Exception.Message)" -ForegroundColor Red
}
}
}
else {
# --- Validate the headset ON -> OFF -> ON cycle ---
# The headset must be ON now. We ask for OFF and then ON, and check that
Expand Down Expand Up @@ -563,32 +591,32 @@ try {
# G HUB fallback: ONLY if the user confirms that the chosen headset is
# a Logitech PRO X 2 listed by G HUB. Never associate $logi[0].
Write-Host ""
$conf = Read-Host "Is this headset a Logitech PRO X 2 detected by G HUB? (y/N)"
$conf = Read-Host "Is this headset a Logitech PRO X (PRO X 2 / PRO X Wireless) detected by G HUB? (y/N)"
if ($conf -match '^(s|si|sí|y|yes)$') {
try {
Connect-GHub
$devices = Invoke-GHubGet -Path "/devices/list"
$deviceInfos = @($devices.payload.deviceInfos)

Write-Host ""
# Filter to ONLY PRO X 2 candidates: prevents accidentally
# Filter to ONLY Logitech PRO X candidates: prevents accidentally
# picking a Logitech mouse/keyboard and watching its battery.
$ghubCandidates = @($deviceInfos | Where-Object {
$_.extendedDisplayName -match 'PRO\s*X\s*2'
Test-LogitechProXDeviceName -Name $_.extendedDisplayName
})

if ($ghubCandidates.Count -eq 0) {
Write-Host " G HUB reports no PRO X 2." -ForegroundColor Red
Write-Host " G HUB reports no Logitech PRO X headset." -ForegroundColor Red
}
else {
Write-Host "PRO X 2 headsets detected by G HUB:" -ForegroundColor Yellow
Write-Host "Logitech PRO X headsets detected by G HUB:" -ForegroundColor Yellow
for ($i = 0; $i -lt $ghubCandidates.Count; $i++) {
Write-Host (" [{0}] {1} ({2})" -f ($i + 1), $ghubCandidates[$i].extendedDisplayName, $ghubCandidates[$i].id)
}

$ghubChoice = 0
do {
$gc = Read-Host "Enter the number of the PRO X 2 that matches '$headsetName'"
$gc = Read-Host "Enter the number of the PRO X headset that matches '$headsetName'"
$ghubValid = [int]::TryParse($gc, [ref]$ghubChoice) -and
$ghubChoice -ge 1 -and
$ghubChoice -le $ghubCandidates.Count
Expand Down Expand Up @@ -640,7 +668,7 @@ try {
}

$config = [ordered]@{
Version = "1.3.0"
Version = "1.4.0"
DetectionMode = $DetectionMode
HeadsetName = [string]$headsetOutput.Name
HeadsetId = [string]$headsetOutput.ItemId
Expand Down
43 changes: 40 additions & 3 deletions Runtime-PROX2-AutoSwitch.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,20 @@ $ModulePath = Join-Path $InstallDir "lib\AutoSwitchCore.psm1"
if (-not (Test-Path $ModulePath)) { exit 12 }
Import-Module $ModulePath -ErrorAction Stop

# SteelSeries Nova 5/5X HID detection (optional provider module).
$script:SteelSeriesModulePath = Join-Path $InstallDir "lib\SteelSeriesNova5.psm1"
$script:SteelSeriesAvailable = $false
if (Test-Path $script:SteelSeriesModulePath) {
try {
Import-Module $script:SteelSeriesModulePath -ErrorAction Stop
$script:SteelSeriesAvailable = $true
}
catch {
Write-AutoSwitchLog ("SteelSeries module present but failed to import: {0}" -f $_.Exception.Message)
$script:SteelSeriesAvailable = $false
}
}

# Timeouts de G HUB (ms). Overridables desde config.json.
$script:ConnectTimeoutMs = 5000
$script:ReceiveTimeoutMs = 5000
Expand Down Expand Up @@ -271,15 +285,15 @@ function Get-ProX2BatteryPath {

if (-not $headset) {
$headset = $all |
Where-Object { $_.extendedDisplayName -match "PRO\s*X\s*2" } |
Where-Object { Test-LogitechProXDeviceName -Name $_.extendedDisplayName } |
Select-Object -First 1
}

if (-not $headset) {
throw "G HUB did not return the PRO X 2 in /devices/list."
throw "G HUB did not return a Logitech PRO X headset in /devices/list."
}

Write-AutoSwitchLog ("PRO X 2 detected by G HUB: {0} ({1})" -f $headset.extendedDisplayName, $headset.id)
Write-AutoSwitchLog ("Logitech PRO X detected by G HUB: {0} ({1})" -f $headset.extendedDisplayName, $headset.id)
return "/battery/$($headset.id)/state"
}

Expand Down Expand Up @@ -1095,6 +1109,29 @@ function Start-WorkerLoop {
}
}
}
elseif ($script:DetectionMode -eq 'SteelSeriesNova5') {
# SteelSeries Arctis Nova 5/5X HID state via the optional module.
if (-not $script:SteelSeriesAvailable) {
if (-not $availabilityLogged) {
Write-AutoSwitchLog "SteelSeries module not available; not switching."
$availabilityLogged = $true
}
}
else {
try {
$steelState = Get-SteelSeriesNova5State -TimeoutMilliseconds 800
if ($steelState -eq 'Connected') { $isOn = $true; $known = $true }
elseif ($steelState -eq 'Disconnected') { $isOn = $false; $known = $true }
# Unknown -> no tocar nada.
}
catch {
if (-not $availabilityLogged) {
Write-AutoSwitchLog ("SteelSeries unavailable: {0}. It will retry." -f $_.Exception.Message)
$availabilityLogged = $true
}
}
}
}
else {
# LogitechGHub: PERSISTENT connection. Connect once and resolve
# batteryPath once; reconnect only on failure (re-resolving the
Expand Down
22 changes: 22 additions & 0 deletions Verificar-PROX2-AutoSwitch.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,28 @@ if ($mode -eq 'LogitechGHub') {
elseif ($mode -eq 'WindowsEndpoint') {
Write-Host "[SKIP] G HUB (not used in WindowsEndpoint mode)" -ForegroundColor DarkGray
}
elseif ($mode -eq 'SteelSeriesNova5') {
Write-Host "[SKIP] G HUB (not used in SteelSeriesNova5 mode)" -ForegroundColor DarkGray
# SteelSeries receiver presence over HID.
$steelModule = Join-Path $InstallDir "lib\SteelSeriesNova5.psm1"
if (Test-Path $steelModule) {
try {
Import-Module $steelModule -ErrorAction Stop
if (Test-SteelSeriesNova5Receiver) {
Show-Test "SteelSeries Nova 5/5X receiver" $true "HID receiver found"
}
else {
Show-Test "SteelSeries Nova 5/5X receiver" $false "No compatible HID receiver found"
}
}
catch {
Show-Test "SteelSeries Nova 5/5X receiver" $false $_.Exception.Message
}
}
else {
Show-Test "SteelSeries Nova 5/5X receiver" $false "Module not installed"
}
}
else {
Show-Test "Detection mode" $false "Could not read a valid DetectionMode from config.json"
}
Expand Down
28 changes: 25 additions & 3 deletions lib/AutoSwitchCore.psm1
Original file line number Diff line number Diff line change
Expand Up @@ -977,10 +977,32 @@ function Get-ConfigDetectionMode {
return 'LogitechGHub'
}

if ($mode -ieq 'WindowsEndpoint') { return 'WindowsEndpoint' }
if ($mode -ieq 'LogitechGHub') { return 'LogitechGHub' }
if ($mode -ieq 'WindowsEndpoint') { return 'WindowsEndpoint' }
if ($mode -ieq 'LogitechGHub') { return 'LogitechGHub' }
if ($mode -ieq 'SteelSeriesNova5') { return 'SteelSeriesNova5' }

return $null
}

Export-ModuleMember -Function Get-RenderItemIdFromText, Resolve-HeadsetState, Test-ValidAudioConfig, New-GHubTimeoutToken, ConvertFrom-SvclCsv, ConvertFrom-CsvLine, Get-CsvColumn, Resolve-EndpointState, Resolve-DetectedState, Test-SvclExportValid, Get-SvclRenderDevice, Get-SvclDeviceLabel, Find-SvclRenderDeviceByIdentity, Get-EndpointFxState, Get-ConfigDetectionMode, Initialize-CoreAudioBackend, Get-CoreAudioRenderDevices, Get-CoreAudioDefaultRenderDeviceId, Get-CoreAudioDefaultRenderDeviceIds, Test-CoreAudioDefaultRenderDevice, Set-CoreAudioDefaultRenderDevice
function Test-LogitechProXDeviceName {
<#
.SYNOPSIS
True if a G HUB extendedDisplayName matches a Logitech PRO X headset
(PRO X 2, PRO X Wireless, PRO X, etc.). PRO X 2 and PRO X Wireless
both expose a LIGHTSPEED endpoint that stays Active when off, so the
G HUB battery signal is the ON/OFF source for all of them.
.DESCRIPTION
Logitech mice are named "G PRO X Superlight" etc.; "PRO X" alone is a
headset family, so require the "PRO X" token and exclude mouse names.
#>
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)][string]$Name
)

if ($Name -match '(?i)superlight|mouse') { return $false }

return ($Name -match '\bPRO\s*X(?:\s*2|\s*Wireless)?\b')
}

Export-ModuleMember -Function Get-RenderItemIdFromText, Resolve-HeadsetState, Test-ValidAudioConfig, New-GHubTimeoutToken, ConvertFrom-SvclCsv, ConvertFrom-CsvLine, Get-CsvColumn, Resolve-EndpointState, Resolve-DetectedState, Test-SvclExportValid, Get-SvclRenderDevice, Get-SvclDeviceLabel, Find-SvclRenderDeviceByIdentity, Get-EndpointFxState, Get-ConfigDetectionMode, Test-LogitechProXDeviceName, Initialize-CoreAudioBackend, Get-CoreAudioRenderDevices, Get-CoreAudioDefaultRenderDeviceId, Get-CoreAudioDefaultRenderDeviceIds, Test-CoreAudioDefaultRenderDevice, Set-CoreAudioDefaultRenderDevice
Loading