Private/Ftdi.Windows.ps1

# Ftdi.Windows.ps1
# Windows-specific FTDI D2XX implementation

function Invoke-FtdiWindowsEnumerate {
    [CmdletBinding()]
    [OutputType([System.Object[]])]
    param()
    
    try {
        # Check if FTDI assembly is available, but try to use it anyway if types exist
        $ftdiAssemblyAvailable = $script:FtdiInitialized -or ($null -ne [System.Type]::GetType("FTD2XX_NET.FTDI"))
        
        if (-not $ftdiAssemblyAvailable) {
            throw [System.NotImplementedException]::new("FTDI assembly not loaded - Windows FTDI enumeration not available")
        }
        
        # Create FTDI instance for enumeration
        $ftdi = [FTD2XX_NET.FTDI]::new()
        
        # Get device count (use int like the working old function)
        [int]$deviceCount = 0
        $status = $ftdi.GetNumberOfDevices([ref]$deviceCount)
        
        # Use enum directly like the working old function
        if ($status -ne [FTD2XX_NET.FTDI+FT_STATUS]::FT_OK) {
            $ftdi.Close() | Out-Null
            throw "FTDI GetNumberOfDevices failed: $status"
        }
        
        # Build D2XX device list
        $enrichedDevices = @()

        if ($deviceCount -eq 0) {
            Write-Verbose "No FTDI devices found via D2XX on Windows"
            $ftdi.Close() | Out-Null
        } else {
        Write-Verbose "Found $deviceCount FTDI device(s) via D2XX on Windows"
        
        # Get device info list (use New-Object like the working old function)
        $deviceList = New-Object 'FTD2XX_NET.FTDI+FT_DEVICE_INFO_NODE[]' $deviceCount
        $status = $ftdi.GetDeviceList($deviceList)
        
        if ($status -ne [FTD2XX_NET.FTDI+FT_STATUS]::FT_OK) {
            $ftdi.Close() | Out-Null  
            throw "FTDI GetDeviceList failed: $status"
        }
        
        # Build enriched device objects with friendly type names
        for ($i = 0; $i -lt $deviceList.Count; $i++) {
            $device = $deviceList[$i]
            
            # Map device type to friendly name
            $typeName = switch ($device.Type) {
                ([FTD2XX_NET.FTDI+FT_DEVICE]::FT_DEVICE_BM)       { "FT232BM" }
                ([FTD2XX_NET.FTDI+FT_DEVICE]::FT_DEVICE_AM)       { "FT232AM" }
                ([FTD2XX_NET.FTDI+FT_DEVICE]::FT_DEVICE_100AX)    { "FT100AX" }
                ([FTD2XX_NET.FTDI+FT_DEVICE]::FT_DEVICE_2232C)    { "FT2232C" }
                ([FTD2XX_NET.FTDI+FT_DEVICE]::FT_DEVICE_232R)     { "FT232R" }
                ([FTD2XX_NET.FTDI+FT_DEVICE]::FT_DEVICE_2232H)    { "FT2232H" }
                ([FTD2XX_NET.FTDI+FT_DEVICE]::FT_DEVICE_4232H)    { "FT4232H" }
                ([FTD2XX_NET.FTDI+FT_DEVICE]::FT_DEVICE_232H)     { "FT232H" }
                ([FTD2XX_NET.FTDI+FT_DEVICE]::FT_DEVICE_X_SERIES) { "FT-X Series" }
                default { $device.Type.ToString() }
            }
            
            # Check if device is in use (bit 0 of flags indicates if device is open)
            $isOpen = ($device.Flags -band 0x00000001) -ne 0
            
            # Create enriched device object
            $caps = Get-FtdiChipCapabilities -TypeName $typeName
            $enrichedDevice = [PSCustomObject]@{
                Index          = $i
                Type           = $typeName
                Description    = $device.Description
                SerialNumber   = $device.SerialNumber
                LocationId     = $device.LocId
                IsOpen         = $isOpen
                Flags          = "0x{0:X8}" -f $device.Flags
                DeviceId       = "0x{0:X8}" -f $device.ID
                Handle         = $device.ftHandle
                Driver         = "ftd2xx.dll"
                Platform       = "Windows"
                GpioMethod     = $caps.GpioMethod
                GpioPins       = $caps.GpioPins
                HasMpsse       = $caps.HasMpsse
                CapabilityNote = $caps.CapabilityNote
            }
            
            $enrichedDevices += $enrichedDevice
        }

        # Close D2XX instance after enumeration
        $ftdi.Close() | Out-Null
        } # end D2XX block

        # Supplement: find VCP-mode FTDI devices not visible to D2XX
        Write-Verbose "Scanning registry for VCP-mode FTDI devices..."
        $vcpDevices = Invoke-FtdiWindowsEnumerateVcp
        foreach ($vcpDev in $vcpDevices) {
            $alreadyFound = $false
            foreach ($d in $enrichedDevices) {
                if ($d.SerialNumber -eq $vcpDev.SerialNumber) {
                    $alreadyFound = $true
                    # Enrich D2XX entry with COM port info if available
                    if ($vcpDev.ComPort -and -not $d.PSObject.Properties['ComPort']) {
                        $d | Add-Member -MemberType NoteProperty -Name ComPort -Value $vcpDev.ComPort -Force
                    }
                    break
                }
            }
            if (-not $alreadyFound) {
                $vcpDev.Index = $enrichedDevices.Count
                $enrichedDevices += $vcpDev
            }
        }

        if ($enrichedDevices.Count -eq 0) {
            Write-Verbose "No FTDI devices found on Windows"
            return @()
        }

        return $enrichedDevices
        
    } catch [System.NotImplementedException] {
        # Return enhanced stub data for Windows development
        return @(
            [PSCustomObject]@{
                Index          = 0
                Type           = "FT232H"
                Description    = "FT232H USB-Serial (Windows STUB)"
                SerialNumber   = "WINSTUB001"
                LocationId     = 0x1001
                IsOpen         = $false
                Flags          = "0x00000000"
                DeviceId       = "0x04036014"
                Handle         = $null
                Driver         = "ftd2xx.dll (STUB)"
                Platform       = "Windows"
                IsVcp          = $false
                GpioMethod     = "MPSSE"
                GpioPins       = "ACBUS0-7, ADBUS0-7"
                HasMpsse       = $true
                CapabilityNote = ""
            },
            [PSCustomObject]@{
                Index          = 1
                Type           = "FT232R"
                Description    = "FT232R USB-Serial (Windows STUB)"
                SerialNumber   = "WINSTUB002"
                LocationId     = 0x1002
                IsOpen         = $false
                Flags          = "0x00000000"
                DeviceId       = "0x04036001"
                Handle         = $null
                Driver         = "ftdibus.sys (VCP) (STUB)"
                Platform       = "Windows"
                IsVcp          = $true
                GpioMethod     = "CBUS"
                GpioPins       = "CBUS0-3 (CBUS bit-bang), ADBUS0-7 (async bit-bang)"
                HasMpsse       = $false
                CapabilityNote = "No MPSSE. CBUS bit-bang (mode 0x20): requires FT_PROG EEPROM config to set CBUS0-3 as 'CBUS I/O'. Async bit-bang (mode 0x01): uses ADBUS0-7 (UART lines), no EEPROM change needed."
            }
        )
    } catch {
        Write-Warning "Windows FTDI enumeration failed: $($_.Exception.Message)"
        return @()
    }
}

function Invoke-FtdiWindowsEnumerateVcp {
    # Scan the FTDIBUS registry hive for VCP-mode FTDI devices (FT232R, etc.)
    # Returns device objects for any FTDI device not visible via D2XX.
    [CmdletBinding()]
    [OutputType([System.Object[]])]
    param()

    $results = @()
    $ftdibusPath = 'HKLM:\SYSTEM\CurrentControlSet\Enum\FTDIBUS'

    if (-not (Test-Path $ftdibusPath)) {
        Write-Verbose "FTDIBUS registry key not found - no VCP FTDI devices installed"
        return $results
    }

    # Map FTDI PID (hex string, upper) to friendly device type name
    $pidTypeMap = @{
        '6001' = 'FT232R'    # FT232R / FT232RL / FT232RNL (same PID, all VCP-only)
        '6010' = 'FT2232D'   # FT2232D / FT2232C
        '6011' = 'FT4232H'
        '6014' = 'FT232H'
        '6015' = 'FT231X'    # FT-X Series (FT230X/FT231X)
        '6040' = 'FT232HP'
    }

    try {
        $comboKeys = Get-ChildItem $ftdibusPath -ErrorAction SilentlyContinue
        foreach ($comboKey in $comboKeys) {
            # Key name pattern: VID_0403+PID_6001+{SERIAL}
            if ($comboKey.PSChildName -match 'VID_([0-9A-Fa-f]{4})\+PID_([0-9A-Fa-f]{4})\+(.+)$') {
                $vid      = $Matches[1].ToUpper()
                $pidHex   = $Matches[2].ToUpper()
                $serial   = $Matches[3]

                $typeName = if ($pidTypeMap.ContainsKey($pidHex)) { $pidTypeMap[$pidHex] } else { "FT-Unknown (PID $pidHex)" }

                # Each combo key has one or more instance sub-keys (typically '0000')
                $instanceKeys = Get-ChildItem $comboKey.PSPath -ErrorAction SilentlyContinue
                foreach ($inst in $instanceKeys) {
                    # Friendly name stored on the instance key
                    $friendlyName = $null
                    try {
                        $friendlyName = (Get-ItemProperty -Path $inst.PSPath -Name FriendlyName -ErrorAction SilentlyContinue).FriendlyName
                    } catch {}

                    # COM port stored under Device Parameters sub-key
                    $comPort = $null
                    $devParams = Join-Path $inst.PSPath 'Device Parameters'
                    try {
                        $comPort = (Get-ItemProperty -Path $devParams -Name PortName -ErrorAction SilentlyContinue).PortName
                    } catch {}

                    if (-not $friendlyName) { $friendlyName = "$typeName USB Serial" }

                    $caps = Get-FtdiChipCapabilities -TypeName $typeName
                    $results += [PSCustomObject]@{
                        Index          = -1   # assigned by caller
                        Type           = $typeName
                        Description    = $friendlyName
                        SerialNumber   = $serial
                        LocationId     = 0
                        IsOpen         = $false
                        Flags          = '0x00000000'
                        DeviceId       = "0x0403$pidHex"
                        Handle         = $null
                        Driver         = 'ftdibus.sys (VCP)'
                        Platform       = 'Windows'
                        ComPort        = $comPort
                        VID            = $vid
                        PID            = $pidHex
                        IsVcp          = $true
                        GpioMethod     = $caps.GpioMethod
                        GpioPins       = $caps.GpioPins
                        HasMpsse       = $caps.HasMpsse
                        CapabilityNote = $caps.CapabilityNote
                    }
                }
            }
        }
    } catch {
        Write-Verbose "VCP registry scan error: $($_.Exception.Message)"
    }

    return $results
}

function Invoke-FtdiWindowsOpen {
    [CmdletBinding()]
    [OutputType([System.Object])]
    param(
        # Resolved device info from Invoke-FtdiWindowsEnumerate / Connect-PsGadgetFtdi.
        # We open by SerialNumber so no second enumeration is needed.
        [Parameter(Mandatory = $true)]
        [PSCustomObject]$DeviceInfo
    )
    
    try {
        # Check if FTDI assembly is available
        if (-not $script:FtdiInitialized) {
            throw [System.NotImplementedException]::new("FTDI assembly not loaded - cannot open device")
        }

        Write-Verbose "Opening FTDI device: $($DeviceInfo.Description) ($($DeviceInfo.SerialNumber))"
        
        # Create new FTDI instance for this connection
        $ftdi = [FTD2XX_NET.FTDI]::new()

        # Open by serial number - avoids a second GetDeviceList call.
        # D2XX returns 0 devices on rapid back-to-back enumerations.
        $status = $ftdi.OpenBySerialNumber($DeviceInfo.SerialNumber)

        if ($status -ne $script:FTDI_OK) {
            # Fall back to index if serial number open failed and index is valid
            Write-Verbose "OpenBySerialNumber failed ($status), trying OpenByIndex $($DeviceInfo.Index)..."
            $ftdi.Close() | Out-Null
            $ftdi = [FTD2XX_NET.FTDI]::new()
            $status = $ftdi.OpenByIndex([uint32]$DeviceInfo.Index)
            if ($status -ne $script:FTDI_OK) {
                $ftdi.Close() | Out-Null
                throw "Failed to open FTDI device: $status"
            }
        }

        # Configure device for MPSSE mode if supported
        if ($DeviceInfo.HasMpsse) {
            Write-Verbose "Configuring device for MPSSE mode..."
            
            # Reset the device
            $status = $ftdi.ResetDevice()
            if ($status -ne $script:FTDI_OK) {
                Write-Warning "Device reset failed: $status"
            }
            
            # Set bit mode for MPSSE (mode 0x02)
            $status = $ftdi.SetBitMode(0x00, 0x02)  # 0x02 = MPSSE mode
            if ($status -ne $script:FTDI_OK) {
                Write-Warning "Failed to set MPSSE mode: $status"
                # Continue anyway - some operations might still work
            } else {
                Write-Verbose "MPSSE mode enabled successfully"
            }

            # Set timeouts
            $ftdi.SetTimeouts(5000, 5000) | Out-Null  # 5 second read/write timeouts

            # Flush buffers then send mandatory MPSSE initialisation sequence:
            # 0x8A - Disable divide-by-5 clock (60 MHz base clock)
            # 0x97 - Disable adaptive clocking
            # 0x8D - Disable 3-phase data clocking
            # Without these the MPSSE engine can misinterpret the first 0x82 command.
            $ftdi.Purge(3) | Out-Null
            Start-Sleep -Milliseconds 5
            [uint32]$initBytesWritten = 0
            $ftdi.Write([byte[]](0x8A, 0x97, 0x8D), 3, [ref]$initBytesWritten) | Out-Null
            Start-Sleep -Milliseconds 5
            Write-Verbose "MPSSE init sequence sent ($initBytesWritten bytes)"
        } else {
            Write-Verbose "Device uses $($DeviceInfo.GpioMethod) GPIO (no MPSSE setup needed on open)"
            $ftdi.SetTimeouts(5000, 5000) | Out-Null
        }
        
        # Create connection object with device info
        $connection = [PSCustomObject]@{
            Device       = $ftdi
            Index        = $DeviceInfo.Index
            SerialNumber = $DeviceInfo.SerialNumber
            Description  = $DeviceInfo.Description
            Type         = $DeviceInfo.Type
            LocationId   = $DeviceInfo.LocationId
            IsOpen       = $true
            GpioMethod   = $DeviceInfo.GpioMethod
            GpioPins     = $DeviceInfo.GpioPins
            HasMpsse     = $DeviceInfo.HasMpsse
            MpsseEnabled = $DeviceInfo.HasMpsse
            Platform     = "Windows"
        }
        
        # Add methods to the connection object
        $connection | Add-Member -MemberType ScriptMethod -Name 'Close' -Value {
            if ($this.Device) {
                $this.Device.Close()
                $this.IsOpen = $false
            }
        }
        
        $connection | Add-Member -MemberType ScriptMethod -Name 'Write' -Value {
            param([byte[]]$data, [int]$length, [ref]$bytesWritten)
            return $this.Device.Write($data, $length, $bytesWritten)
        }
        
        $connection | Add-Member -MemberType ScriptMethod -Name 'Read' -Value {
            param([byte[]]$buffer, [int]$length, [ref]$bytesRead)
            return $this.Device.Read($buffer, $length, $bytesRead)
        }

        # Soft reset - clears internal buffers and restores chip state; handle stays open
        $connection | Add-Member -MemberType ScriptMethod -Name 'Reset' -Value {
            if (-not $this.IsOpen -or -not $this.Device) {
                throw "Device is not open. Call Connect-PsGadgetFtdi first."
            }
            $status = $this.Device.ResetDevice()
            if ([int]$status -ne 0) { throw "ResetDevice failed: $status" }
        }

        # USB port cycle - equivalent to a physical replug; triggers re-enumeration so
        # EEPROM changes (e.g. CBUS mode) take effect without manually unplugging the cable.
        # Automatically reconnects after re-enumeration so the connection object stays usable.
        $connection | Add-Member -MemberType ScriptMethod -Name 'CyclePort' -Value {
            if (-not $this.IsOpen -or -not $this.Device) {
                throw "Device is not open. Call Connect-PsGadgetFtdi first."
            }
            $serial = $this.SerialNumber
            $status = $this.Device.CyclePort()
            $this.IsOpen = $false
            $this.Device = $null
            if ([int]$status -ne 0) { throw "CyclePort failed: $status" }

            # Poll until device reappears by serial number (max 5 seconds, 250ms intervals)
            Write-Verbose "USB port cycled for $serial - polling for re-enumeration..."
            $newConn    = $null
            $deadline   = (Get-Date).AddSeconds(5)
            while ((Get-Date) -lt $deadline) {
                Start-Sleep -Milliseconds 250
                try {
                    $newConn = Connect-PsGadgetFtdi -SerialNumber $serial -ErrorAction SilentlyContinue
                    if ($newConn -and $newConn.IsOpen) { break }
                } catch {
                    # device not yet visible - keep polling
                }
            }

            # Reconnect by serial number so the object stays usable
            if (-not $newConn) {
                throw "CyclePort succeeded but reconnect to '$serial' failed. Try Connect-PsGadgetFtdi manually."
            }
            $this.Device       = $newConn.Device
            $this.IsOpen       = $newConn.IsOpen
            $this.MpsseEnabled = $newConn.MpsseEnabled
            Write-Verbose "Reconnected to $serial after port cycle."
        }
        
        Write-Verbose "Successfully opened FTDI device $($DeviceInfo.SerialNumber)"
        return $connection
        
    } catch [System.NotImplementedException] {
        # Return stub connection for development
        $stubSerial = if ($DeviceInfo) { $DeviceInfo.SerialNumber } else { 'WINSTUB' }
        Write-Verbose "Creating stub connection for device $stubSerial (Windows)"
        
        return [PSCustomObject]@{
            Device = $null
            Index = if ($DeviceInfo) { $DeviceInfo.Index } else { -1 }
            SerialNumber = $stubSerial
            Description = "Windows STUB Connection"
            Type = "FT232H"
            IsOpen = $true
            MpsseEnabled = $true
            Platform = "Windows (STUB)"
            Close = { $this.IsOpen = $false }
            Write = { param($data, $length, $bytesWritten); $bytesWritten.Value = $length; return $script:FTDI_OK }
            Read = { param($buffer, $length, $bytesRead); $bytesRead.Value = 1; return $script:FTDI_OK }
        }
    } catch {
        Write-Error "Failed to open Windows FTDI device: $_"
        return $null
    }
}

function Invoke-FtdiWindowsClose {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [int]$Handle
    )
    
    try {
        # TODO: Implement Windows FTDI device close via D2XX
        throw [System.NotImplementedException]::new("Windows FTDI device close not yet implemented")
        
    } catch [System.NotImplementedException] {
        Write-Verbose "Closed FTDI device handle $Handle on Windows (STUB MODE)"
        return [PSCustomObject]@{
            Success = $true
            Message = "Device closed successfully (Windows STUB)"
        }
    } catch {
        Write-Warning "Failed to close Windows FTDI device: $($_.Exception.Message)"
        throw
    }
}