Private/Ftdi.Backend.ps1

# Ftdi.Backend.ps1
# Core FTDI backend functionality - platform agnostic

function Get-FtdiChipCapabilities {
    # Returns a capability descriptor hashtable for a given FTDI chip type name.
    # This is the single source of truth for GPIO method, pin availability, and
    # any EEPROM or setup requirements - used by enumeration, connect, and GPIO code.
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [string]$TypeName
    )

    switch -Regex ($TypeName) {
        '^FT232H$|^FT232HP$' {
            return @{
                GpioMethod     = 'MPSSE'
                GpioPins       = 'ACBUS0-7, ADBUS0-7'
                HasMpsse       = $true
                CapabilityNote = ''
            }
        }
        '^FT2232H$|^FT2232C$|^FT2232D$' {
            return @{
                GpioMethod     = 'MPSSE'
                GpioPins       = 'ACBUS0-7, ADBUS0-7 (dual channel)'
                HasMpsse       = $true
                CapabilityNote = 'MPSSE available on both channels A and B'
            }
        }
        '^FT4232H$' {
            return @{
                GpioMethod     = 'MPSSE'
                GpioPins       = 'ADBUS0-7 (channels A/B only)'
                HasMpsse       = $true
                CapabilityNote = 'MPSSE on channels A and B only; C and D are UART/GPIO'
            }
        }
        '^FT232R(L|NL)?$' {
            return @{
                GpioMethod     = 'CBUS'
                GpioPins       = 'CBUS0-4 (5 pins; CBUS0-3 runtime bit-bang, CBUS4 EEPROM-config only), ADBUS0-7 (async bit-bang)'
                HasMpsse       = $false
                CapabilityNote = 'No MPSSE. Device has CBUS0-4 (5 pins). CBUS bit-bang (mode 0x20) runtime: CBUS0-3 only (D2XX mask is 8 bits, 4 direction + 4 value). CBUS4 is EEPROM-configurable (Set-PsGadgetFt232rCbusMode -Pins @(4)) but cannot be driven at runtime via SetBitMode. Async bit-bang (mode 0x01): uses ADBUS0-7 (UART lines), no EEPROM change needed.'
            }
        }
        '^FT231X$|^FT230X$|^FT-X' {
            return @{
                GpioMethod     = 'CBUS'
                GpioPins       = 'CBUS0-3'
                HasMpsse       = $false
                CapabilityNote = 'CBUS bit-bang (mode 0x20): requires FT_PROG EEPROM config'
            }
        }
        '^FT232BM$|^FT232AM$|^FT100AX$' {
            return @{
                GpioMethod     = 'AsyncBitBang'
                GpioPins       = 'ADBUS0-7'
                HasMpsse       = $false
                CapabilityNote = 'Legacy chip. Async bit-bang (mode 0x01) on ADBUS0-7 only'
            }
        }
        default {
            return @{
                GpioMethod     = 'Unknown'
                GpioPins       = 'Unknown'
                HasMpsse       = $false
                CapabilityNote = "Unrecognised chip type: $TypeName"
            }
        }
    }
}

function Get-FtdiDeviceList {
    [CmdletBinding()]
    [OutputType([System.Object[]])]
    param()
    
    try {
        Write-Verbose "Enumerating FTDI devices via platform-specific backend..."
        
        # Determine platform and call appropriate implementation.
        # IoT backend is tried first on PS7.4+/.NET8+. If it throws (e.g. libftd2xx.so
        # absent on a Linux dev machine with no physical device) fall through to the
        # platform-specific backend so that Unix stubs remain active.
        $devices = $null
        if ($script:IotBackendAvailable) {
            Write-Verbose "Using IoT .NET backend for enumeration"
            try {
                $devices = Invoke-FtdiIotEnumerate
            } catch {
                Write-Verbose "IoT backend unavailable ($($_.Exception.GetType().Name)); falling back to platform-specific backend"
                $devices = $null
            }
            if (-not $devices -or @($devices).Count -eq 0) {
                Write-Verbose "IoT enumeration returned no devices; falling back to platform-specific backend"
                $devices = $null
            }

            # Detect ftdi_sio conflict: D2XX sees the device but can't query its type
            # because the kernel VCP driver has it claimed. All devices come back as
            # 'UnknownDevice' with DeviceId=0x00000000 and Flags bit 0 set (PortOpened).
            # Fall back to sysfs for accurate chip identification; warn about the conflict.
            # Only emit the ftdi_sio warning if the module is actually loaded - IoT can
            # return UnknownDevice for other reasons (e.g. incomplete descriptor read on
            # Linux), so we verify lsmod before attributing it to ftdi_sio.
            if ($devices -and @($devices).Count -gt 0) {
                $allUnknown = (@($devices) | Where-Object { $_.Type -ne 'UnknownDevice' } | Measure-Object).Count
                if ($allUnknown -eq 0) {
                    $isWindows = [System.Environment]::OSVersion.Platform -eq 'Win32NT'
                    if (-not $isWindows) {
                        $ftdiSioLoaded = $false
                        try {
                            $lsmodOut = & lsmod 2>/dev/null
                            $ftdiSioLoaded = $lsmodOut -match '\bftdi_sio\b'
                        } catch {}

                        if ($ftdiSioLoaded) {
                            Write-Warning "ftdi_sio kernel module is holding the device - D2XX cannot read chip type."
                            Write-Warning "Enumeration metadata falls back to sysfs (read-only; connect will fail until ftdi_sio is unloaded)."
                            Write-Warning "To enable D2XX/IoT hardware access: sudo rmmod ftdi_sio"
                            Write-Warning "To make permanent: echo 'blacklist ftdi_sio' | sudo tee /etc/modprobe.d/ftdi-d2xx.conf"
                        } else {
                            Write-Verbose "IoT returned UnknownDevice (ftdi_sio not loaded); falling back to sysfs for chip identification"
                        }
                        $devices = $null   # trigger sysfs fallback below
                    }
                }
            }
        }
        if ($null -eq $devices) {
            if ($PSVersionTable.PSVersion.Major -le 5 -or [System.Environment]::OSVersion.Platform -eq 'Win32NT') {
                Write-Verbose "Using Windows FTDI backend"
                $devices = Invoke-FtdiWindowsEnumerate
            } else {
                Write-Verbose "Using Unix FTDI backend"
                $devices = Invoke-FtdiUnixEnumerate
            }
        }
        
        # Validate and enrich device list
        if ($devices -and @($devices).Count -gt 0) {
            Write-Verbose "Successfully enumerated $(@($devices).Count) FTDI device(s)"
            
            # Ensure consistent Index values and backfill any missing capability properties.
            # Windows and future platform backends may already stamp these; this pass ensures
            # that any backend which omits Get-FtdiChipCapabilities still produces a complete object.
            $deviceArray = @($devices)
            for ($i = 0; $i -lt $deviceArray.Count; $i++) {
                $deviceArray[$i].Index = $i
                if (-not $deviceArray[$i].PSObject.Properties['GpioMethod']) {
                    $caps = Get-FtdiChipCapabilities -TypeName $deviceArray[$i].Type
                    $deviceArray[$i] | Add-Member -MemberType NoteProperty -Name GpioMethod     -Value $caps.GpioMethod     -Force
                    $deviceArray[$i] | Add-Member -MemberType NoteProperty -Name GpioPins       -Value $caps.GpioPins       -Force
                    $deviceArray[$i] | Add-Member -MemberType NoteProperty -Name HasMpsse       -Value $caps.HasMpsse       -Force
                    $deviceArray[$i] | Add-Member -MemberType NoteProperty -Name CapabilityNote -Value $caps.CapabilityNote -Force
                }
                # Stamp IsVcp based on Driver field (VCP devices use ftdibus.sys)
                if (-not $deviceArray[$i].PSObject.Properties['IsVcp']) {
                    $isVcp = $deviceArray[$i].Driver -like '*VCP*'
                    $deviceArray[$i] | Add-Member -MemberType NoteProperty -Name IsVcp -Value $isVcp -Force
                }
            }
            
            return $deviceArray
        } else {
            Write-Verbose "No FTDI devices found"
            return @()
        }
        
    } catch [System.NotImplementedException] {
        Write-Verbose "FTDI enumeration not implemented - returning unified stub devices"
        
        # Return platform-agnostic stub device list for development
        $isWindows = [System.Environment]::OSVersion.Platform -eq 'Win32NT'
        
        $stubDevices = @(
            [PSCustomObject]@{
                Index          = 0
                Type           = 'FT232H'
                Description    = 'FT232H USB-Serial (UNIFIED STUB)'
                SerialNumber   = 'STUB001'
                LocationId     = if ($isWindows) { 0x1234 } else { '/dev/ttyUSB0' }
                IsOpen         = $false
                Flags          = '0x00000000'
                DeviceId       = '0x04036014'
                Handle         = $null
                Driver         = if ($isWindows) { 'ftd2xx.dll (STUB)' } else { 'libftdi (STUB)' }
                Platform       = if ($isWindows) { 'Windows' } else { 'Unix' }
                IsVcp          = $false
                GpioMethod     = 'MPSSE'
                GpioPins       = 'ACBUS0-7, ADBUS0-7'
                HasMpsse       = $true
                CapabilityNote = ''
            },
            [PSCustomObject]@{
                Index          = 1
                Type           = 'FT232R'
                Description    = 'FT232R USB UART (UNIFIED STUB)'
                SerialNumber   = 'STUB002'
                LocationId     = if ($isWindows) { 0x5678 } else { '/dev/ttyUSB1' }
                IsOpen         = $false
                Flags          = '0x00000000'
                DeviceId       = '0x04036001'
                Handle         = $null
                Driver         = if ($isWindows) { 'ftdibus.sys (VCP) (STUB)' } else { 'libftdi (STUB)' }
                Platform       = if ($isWindows) { 'Windows' } else { 'Unix' }
                IsVcp          = if ($isWindows) { $true } else { $false }
                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."
            }
        )
        return $stubDevices
    } catch {
        Write-Warning "FTDI device enumeration failed: $($_.Exception.Message)"
        return @()
    }
}

function Test-FtdiDeviceAvailable {
    [CmdletBinding()]
    [OutputType([bool])]
    param(
        [Parameter(Mandatory = $true)]
        [int]$Index
    )
    
    try {
        $Devices = Get-FtdiDeviceList
        return ($null -ne ($Devices | Where-Object { $_.Index -eq $Index }))
    } catch {
        Write-Warning "Failed to check FTDI device availability: $($_.Exception.Message)"
        return $false
    }
}

function Get-FtdiDeviceInfo {
    [CmdletBinding()]
    [OutputType([System.Object])]
    param(
        [Parameter(Mandatory = $true)]
        [int]$Index
    )
    
    try {
        $Devices = Get-FtdiDeviceList
        $Device = $Devices | Where-Object { $_.Index -eq $Index }
        
        if ($null -eq $Device) {
            throw [System.ArgumentException]::new("FTDI device at index $Index not found")
        }
        
        return $Device
    } catch {
        Write-Warning "Failed to get FTDI device info: $($_.Exception.Message)"
        throw
    }
}