Private/Ftdi.Cbus.ps1

# Ftdi.Cbus.ps1
# CBUS bit-bang GPIO and EEPROM configuration helpers for FT232R / FT231X / FT230X devices.
#
# How FT232R CBUS GPIO works (two-step process):
# Step 1 (one-time): Program EEPROM so the desired CBUS pins are set to FT_CBUS_IOMODE.
# Use Set-FtdiFt232rCbusPinMode (or the public Set-PsGadgetFt232rCbusMode).
# Reconnect USB after writing EEPROM for the change to take effect.
# Step 2 (runtime): Call SetBitMode(mask, 0x20) to drive those pins HIGH/LOW.
# Use Set-FtdiCbusBits (called by Set-PsGadgetGpio for CBUS devices).
#
# CBUS bit-bang mask byte layout (FTDI D2XX Programmer's Guide, section 5.3):
# Bit 7 = CBUS3 direction (1=output, 0=input)
# Bit 6 = CBUS2 direction
# Bit 5 = CBUS1 direction
# Bit 4 = CBUS0 direction
# Bit 3 = CBUS3 output value
# Bit 2 = CBUS2 output value
# Bit 1 = CBUS1 output value
# Bit 0 = CBUS0 output value
# mask = (direction_nibble << 4) | value_nibble
#
# Notes:
# - Only CBUS0-CBUS3 are available for bit-bang (not CBUS4).
# - Only pins programmed as FT_CBUS_IOMODE in EEPROM can be driven; others are ignored.
# - SetBitMode for CBUS (0x20) sets direction AND value in a single call.

#Requires -Version 5.1

# FT_CBUS_OPTIONS integer-to-name and name-to-integer lookup tables.
# Values match the FTD2XX_NET FT_CBUS_OPTIONS enum (net48 and netstandard20).
# Reference: FTD2XX_NET source + confirmed by ReadFT232REEPROM on FT232R with
# CBUS0/1 programmed to I/O MODE via FT_Prog (returns byte 10 = FT_CBUS_IOMODE).
$script:FT_CBUS_NAMES = @{
    0  = 'FT_CBUS_TXDEN'
    1  = 'FT_CBUS_PWREN'
    2  = 'FT_CBUS_RXLED'
    3  = 'FT_CBUS_TXLED'
    4  = 'FT_CBUS_TXRXLED'
    5  = 'FT_CBUS_SLEEP'
    6  = 'FT_CBUS_CLK48'
    7  = 'FT_CBUS_CLK24'
    8  = 'FT_CBUS_CLK12'
    9  = 'FT_CBUS_CLK6'
    10 = 'FT_CBUS_IOMODE'
    11 = 'FT_CBUS_BITBANG_WR'
    12 = 'FT_CBUS_BITBANG_RD'
}
$script:FT_CBUS_VALUES = @{}
foreach ($k in $script:FT_CBUS_NAMES.Keys) {
    $script:FT_CBUS_VALUES[$script:FT_CBUS_NAMES[$k]] = [byte]$k
}

# FT_232H_CBUS_OPTIONS integer-to-name lookup.
# Reference: FTD2XX_NET FT_232H_CBUS_OPTIONS enum + AN_146 FT232H datasheet.
$script:FT_232H_CBUS_NAMES = @{
    0  = 'FT_CBUS_TRISTATE'
    1  = 'FT_CBUS_TXLED'
    2  = 'FT_CBUS_RXLED'
    3  = 'FT_CBUS_TXRXLED'
    4  = 'FT_CBUS_PWREN'
    5  = 'FT_CBUS_SLEEP'
    6  = 'FT_CBUS_DRIVE_0'
    7  = 'FT_CBUS_DRIVE_1'
    8  = 'FT_CBUS_IOMODE'
    9  = 'FT_CBUS_TXDEN'
    10 = 'FT_CBUS_CLK30'
    11 = 'FT_CBUS_CLK15'
    12 = 'FT_CBUS_CLK7_5'
}

function Get-FtdiFt232hEeprom {
    <#
    .SYNOPSIS
    Reads the FT232H EEPROM and returns a rich object with all fields.
 
    .DESCRIPTION
    Opens the FT232H device by index via D2XX, reads the FT232H_EEPROM_STRUCTURE,
    and returns a PSCustomObject with common USB descriptor fields plus all FT232H-
    specific fields (ACBUS/ADBUS drive settings, CBUS0-9 pin modes, interface
    configuration flags, etc.).
 
    The device must not already be opened by another handle.
 
    .PARAMETER Index
    Zero-based device index (from Get-FTDevice).
 
    .PARAMETER SerialNumber
    Optional fallback when OpenByIndex fails.
 
    .EXAMPLE
    Get-FtdiFt232hEeprom -Index 0
 
    .OUTPUTS
    PSCustomObject with EEPROM fields.
    #>

    [CmdletBinding()]
    [OutputType([System.Object])]
    param(
        [Parameter(Mandatory = $true)]
        [int]$Index,

        [Parameter(Mandatory = $false)]
        [string]$SerialNumber = ''
    )

    try {
        if (-not $script:FtdiInitialized) {
            throw [System.NotImplementedException]::new("FTDI assembly not loaded")
        }

        $ftdi   = [FTD2XX_NET.FTDI]::new()
        $status = $ftdi.OpenByIndex([uint32]$Index)

        if ($status -ne [FTD2XX_NET.FTDI+FT_STATUS]::FT_OK -and $SerialNumber -ne '') {
            Write-Verbose "OpenByIndex($Index) -> $status; retrying via OpenBySerialNumber('$SerialNumber')"
            $ftdi.Close() | Out-Null
            $ftdi   = [FTD2XX_NET.FTDI]::new()
            $status = $ftdi.OpenBySerialNumber($SerialNumber)
        }

        if ($status -ne [FTD2XX_NET.FTDI+FT_STATUS]::FT_OK) {
            $ftdi.Close() | Out-Null
            if ($status -eq [FTD2XX_NET.FTDI+FT_STATUS]::FT_DEVICE_NOT_OPENED) {
                throw ("Device is already open - close the existing connection first. " +
                       "Call .Close() on any open `$dev variable or restart the PowerShell session.")
            }
            throw "Failed to open FT232H device: $status"
        }

        $eeprom = [FTD2XX_NET.FTDI+FT232H_EEPROM_STRUCTURE]::new()
        $status = $ftdi.ReadFT232HEEPROM($eeprom)
        $ftdi.Close() | Out-Null

        if ($status -ne [FTD2XX_NET.FTDI+FT_STATUS]::FT_OK) {
            throw "ReadFT232HEEPROM failed: $status"
        }

        # Helper: resolve FT_232H_CBUS_OPTIONS byte -> friendly name
        $resolveCbus = {
            param([object]$val)
            $intVal = [int]$val
            if ($script:FT_232H_CBUS_NAMES.ContainsKey($intVal)) {
                return $script:FT_232H_CBUS_NAMES[$intVal]
            }
            return "UNKNOWN($intVal)"
        }

        return [PSCustomObject]@{
            # USB descriptor fields
            VendorID            = '0x{0:X4}' -f $eeprom.VendorID
            ProductID           = '0x{0:X4}' -f $eeprom.ProductID
            Manufacturer        = $eeprom.Manufacturer
            ManufacturerID      = $eeprom.ManufacturerID
            Description         = $eeprom.Description
            SerialNumber        = $eeprom.SerialNumber
            MaxPower            = $eeprom.MaxPower
            SelfPowered         = $eeprom.SelfPowered
            RemoteWakeup        = $eeprom.RemoteWakeup
            PullDownEnable      = $eeprom.PullDownEnable
            SerNumEnable        = $eeprom.SerNumEnable
            # ACBUS (C-bus, MPSSE high-byte) drive settings
            ACSlowSlew          = $eeprom.ACSlowSlew
            ACSchmittInput      = $eeprom.ACSchmittInput
            ACDriveCurrent      = $eeprom.ACDriveCurrent
            # ADBUS (D-bus, MPSSE low-byte) drive settings
            ADSlowSlew          = $eeprom.ADSlowSlew
            ADSchmittInput      = $eeprom.ADSchmittInput
            ADDriveCurrent      = $eeprom.ADDriveCurrent
            # CBUS pin function assignments (ACBUS0-9)
            Cbus0               = (& $resolveCbus $eeprom.Cbus0)
            Cbus1               = (& $resolveCbus $eeprom.Cbus1)
            Cbus2               = (& $resolveCbus $eeprom.Cbus2)
            Cbus3               = (& $resolveCbus $eeprom.Cbus3)
            Cbus4               = (& $resolveCbus $eeprom.Cbus4)
            Cbus5               = (& $resolveCbus $eeprom.Cbus5)
            Cbus6               = (& $resolveCbus $eeprom.Cbus6)
            Cbus7               = (& $resolveCbus $eeprom.Cbus7)
            Cbus8               = (& $resolveCbus $eeprom.Cbus8)
            Cbus9               = (& $resolveCbus $eeprom.Cbus9)
            # Interface configuration flags
            IsFifo              = $eeprom.IsFifo
            IsFifoTar           = $eeprom.IsFifoTar
            IsFastSer           = $eeprom.IsFastSer
            IsFT1248            = $eeprom.IsFT1248
            FT1248Cpol          = $eeprom.FT1248Cpol
            FT1248Lsb           = $eeprom.FT1248Lsb
            FT1248FlowControl   = $eeprom.FT1248FlowControl
            IsVCP               = $eeprom.IsVCP
            PowerSaveEnable     = $eeprom.PowerSaveEnable
        }

    } catch [System.NotImplementedException] {
        Write-Warning "Get-FtdiFt232hEeprom: FTDI assembly not loaded."
        return $null
    } catch {
        Write-Error "Get-FtdiFt232hEeprom failed: $_"
        return $null
    }
}

function Get-FtdiFt232rEeprom {
    <#
    .SYNOPSIS
    Reads the FT232R EEPROM and returns a rich object with all fields.
 
    .DESCRIPTION
    Opens the FT232R device by index via D2XX, reads the FT232R_EEPROM_STRUCTURE,
    and returns a PSCustomObject with common USB descriptor fields plus all FT232R-
    specific fields (CBUS pin modes, signal inversion, driver load mode, etc.).
 
    The device must not already be opened by another handle.
 
    .PARAMETER Index
    Zero-based device index (from Get-FTDevice).
 
    .EXAMPLE
    Get-FtdiFt232rEeprom -Index 0
 
    .OUTPUTS
    PSCustomObject with EEPROM fields. CbusN properties hold the FT_CBUS_OPTIONS
    enum value name (e.g. 'FT_CBUS_IOMODE', 'FT_CBUS_TXLED', etc.).
    #>

    [CmdletBinding()]
    [OutputType([System.Object])]
    param(
        [Parameter(Mandatory = $true)]
        [int]$Index,

        # Optional serial number used as fallback when OpenByIndex fails (e.g. device in VCP mode)
        [Parameter(Mandatory = $false)]
        [string]$SerialNumber = ''
    )

    try {
        if (-not $script:FtdiInitialized) {
            $isWindows = [System.Environment]::OSVersion.Platform -eq 'Win32NT'
            if (-not $isWindows) {
                Write-Warning (
                    "Get-PsGadgetFtdiEeprom: FT232R EEPROM read is not supported on Linux.`n" +
                    "Use an FT232H device instead -- it has MPSSE and full Linux support via the IoT backend."
                )
                return $null
            }
            throw [System.NotImplementedException]::new("FTDI assembly not loaded")
        }

        $ftdi   = [FTD2XX_NET.FTDI]::new()
        $status = $ftdi.OpenByIndex([uint32]$Index)

        # VCP-mode devices (shown as COM ports) cause OpenByIndex to return FT_DEVICE_NOT_FOUND.
        # Fall back to OpenBySerialNumber which works regardless of driver mode.
        if ($status -ne [FTD2XX_NET.FTDI+FT_STATUS]::FT_OK -and $SerialNumber -ne '') {
            Write-Verbose "OpenByIndex($Index) -> $status; retrying via OpenBySerialNumber('$SerialNumber')"
            $ftdi.Close() | Out-Null
            $ftdi   = [FTD2XX_NET.FTDI]::new()
            $status = $ftdi.OpenBySerialNumber($SerialNumber)
        }

        if ($status -ne [FTD2XX_NET.FTDI+FT_STATUS]::FT_OK) {
            $ftdi.Close() | Out-Null
            $openMethod = if ($SerialNumber -ne '') { "OpenByIndex($Index) and OpenBySerialNumber('$SerialNumber')" } else { "OpenByIndex($Index)" }
            # FT_DEVICE_NOT_OPENED (3) when trying to open usually means another handle is already open.
            # The D2XX library will not allow a second handle on the same device.
            if ($status -eq [FTD2XX_NET.FTDI+FT_STATUS]::FT_DEVICE_NOT_OPENED) {
                throw ("Device is already open - close the existing connection first. " +
                       "If you have a `$dev or `$conn variable, call .Close() on it. " +
                       "Otherwise restart the PowerShell session to release all handles.")
            }
            throw "Failed to open device via $openMethod : $status"
        }

        $eeprom = [FTD2XX_NET.FTDI+FT232R_EEPROM_STRUCTURE]::new()
        $status = $ftdi.ReadFT232REEPROM($eeprom)
        $ftdi.Close() | Out-Null

        if ($status -ne [FTD2XX_NET.FTDI+FT_STATUS]::FT_OK) {
            throw "ReadFT232REEPROM failed: $status"
        }

        return [PSCustomObject]@{
            # USB descriptor fields (FT_EEPROM_DATA base)
            VendorID        = '0x{0:X4}' -f $eeprom.VendorID
            ProductID       = '0x{0:X4}' -f $eeprom.ProductID
            Manufacturer    = $eeprom.Manufacturer
            ManufacturerID  = $eeprom.ManufacturerID
            Description     = $eeprom.Description
            SerialNumber    = $eeprom.SerialNumber
            MaxPower        = $eeprom.MaxPower
            SelfPowered     = $eeprom.SelfPowered
            RemoteWakeup    = $eeprom.RemoteWakeup
            # FT232R-specific fields
            UseExtOsc       = $eeprom.UseExtOsc
            HighDriveIOs    = $eeprom.HighDriveIOs
            EndpointSize    = $eeprom.EndpointSize
            PullDownEnable  = $eeprom.PullDownEnable
            SerNumEnable    = $eeprom.SerNumEnable
            InvertTXD       = $eeprom.InvertTXD
            InvertRXD       = $eeprom.InvertRXD
            InvertRTS       = $eeprom.InvertRTS
            InvertCTS       = $eeprom.InvertCTS
            InvertDTR       = $eeprom.InvertDTR
            InvertDSR       = $eeprom.InvertDSR
            InvertDCD       = $eeprom.InvertDCD
            InvertRI        = $eeprom.InvertRI
            # CBUS pin mode assignments - use lookup table; Cbus0-4 may be plain bytes
            # in some FTD2XX_NET builds, making enum reflection unreliable.
            Cbus0           = if ($script:FT_CBUS_NAMES.ContainsKey([int]$eeprom.Cbus0)) { $script:FT_CBUS_NAMES[[int]$eeprom.Cbus0] } else { "UNKNOWN($($eeprom.Cbus0))" }
            Cbus1           = if ($script:FT_CBUS_NAMES.ContainsKey([int]$eeprom.Cbus1)) { $script:FT_CBUS_NAMES[[int]$eeprom.Cbus1] } else { "UNKNOWN($($eeprom.Cbus1))" }
            Cbus2           = if ($script:FT_CBUS_NAMES.ContainsKey([int]$eeprom.Cbus2)) { $script:FT_CBUS_NAMES[[int]$eeprom.Cbus2] } else { "UNKNOWN($($eeprom.Cbus2))" }
            Cbus3           = if ($script:FT_CBUS_NAMES.ContainsKey([int]$eeprom.Cbus3)) { $script:FT_CBUS_NAMES[[int]$eeprom.Cbus3] } else { "UNKNOWN($($eeprom.Cbus3))" }
            Cbus4           = if ($script:FT_CBUS_NAMES.ContainsKey([int]$eeprom.Cbus4)) { $script:FT_CBUS_NAMES[[int]$eeprom.Cbus4] } else { "UNKNOWN($($eeprom.Cbus4))" }
            # Flag: driver mode (true = D2XX, false = VCP)
            RIsD2XX         = $eeprom.RIsD2XX
        }

    } catch [System.NotImplementedException] {
        # Only reached on Windows when FTD2XX_NET assembly failed to load.
        Write-Verbose "EEPROM read: FTD2XX_NET assembly not loaded - returning stub EEPROM data"
        return [PSCustomObject]@{
            VendorID       = '0x0403'
            ProductID      = '0x6001'
            Manufacturer   = 'FTDI'
            ManufacturerID = 'FT'
            Description    = 'FT232R USB UART (STUB)'
            SerialNumber   = "STUB$Index"
            MaxPower       = 90
            SelfPowered    = $false
            RemoteWakeup   = $false
            UseExtOsc      = $false
            HighDriveIOs   = $false
            EndpointSize   = 64
            PullDownEnable = $false
            SerNumEnable   = $true
            InvertTXD      = $false
            InvertRXD      = $false
            InvertRTS      = $false
            InvertCTS      = $false
            InvertDTR      = $false
            InvertDSR      = $false
            InvertDCD      = $false
            InvertRI       = $false
            Cbus0          = 'FT_CBUS_TXLED'
            Cbus1          = 'FT_CBUS_RXLED'
            Cbus2          = 'FT_CBUS_TXDEN'
            Cbus3          = 'FT_CBUS_PWREN'
            Cbus4          = 'FT_CBUS_SLEEP'
            RIsD2XX        = $false
        }
    } catch {
        Write-Error "Get-FtdiFt232rEeprom failed: $_"
        return $null
    }
}

function Set-FtdiFt232rCbusPinMode {
    <#
    .SYNOPSIS
    Programs FT232R EEPROM to set specified CBUS pins to a given functional mode.
 
    .DESCRIPTION
    Reads the current FT232R EEPROM, sets the Cbus0-Cbus3 entries for the specified
    pin numbers to the chosen FT_CBUS_OPTIONS mode (default: FT_CBUS_IOMODE which
    enables bit-bang GPIO control), then writes the modified EEPROM back.
 
    IMPORTANT: The EEPROM change takes effect only after the USB device is
    disconnected and reconnected (power cycle or USB replug).
 
    Available mode names (FT_CBUS_OPTIONS enum):
        FT_CBUS_IOMODE - GPIO bit-bang (needed for Set-PsGadgetGpio / CBUS control)
        FT_CBUS_TXLED - Tx LED (pulses on transmit)
        FT_CBUS_RXLED - Rx LED (pulses on receive)
        FT_CBUS_TXRXLED - Tx/Rx LED
        FT_CBUS_PWREN - Power-on signal (PWREN#, active low)
        FT_CBUS_SLEEP - Sleep indicator
        FT_CBUS_CLK48 - 48 MHz clock output
        FT_CBUS_CLK24 - 24 MHz clock output
        FT_CBUS_CLK12 - 12 MHz clock output
        FT_CBUS_CLK6 - 6 MHz clock output
        FT_CBUS_TXDEN - Tx Data Enable
        FT_CBUS_BITBANG_WR - Bit-bang write strobe
        FT_CBUS_BITBANG_RD - Bit-bang read strobe
 
    .PARAMETER Index
    Zero-based device index (from Get-FTDevice).
 
    .PARAMETER Pins
    One or more CBUS pin numbers to reconfigure (0-3). CBUS4 is not available for
    bit-bang and is not accepted.
 
    .PARAMETER Mode
    Name of the FT_CBUS_OPTIONS enum value to assign. Defaults to FT_CBUS_IOMODE.
 
    .EXAMPLE
    # Configure CBUS0-3 as GPIO on device 0 (one-time setup):
    Set-FtdiFt232rCbusPinMode -Index 0 -Pins @(0,1,2,3)
 
    .EXAMPLE
    # Set CBUS0 to Rx LED, keep others unchanged:
    Set-FtdiFt232rCbusPinMode -Index 0 -Pins @(0) -Mode FT_CBUS_RXLED
    #>

    [CmdletBinding(SupportsShouldProcess)]
    param(
        [Parameter(Mandatory = $true)]
        [int]$Index,

        [Parameter(Mandatory = $true)]
        [ValidateRange(0, 4)]
        [int[]]$Pins,

        [Parameter(Mandatory = $false)]
        [ValidateSet(
            'FT_CBUS_TXDEN','FT_CBUS_PWREN','FT_CBUS_RXLED','FT_CBUS_TXLED',
            'FT_CBUS_TXRXLED','FT_CBUS_SLEEP','FT_CBUS_CLK48','FT_CBUS_CLK24',
            'FT_CBUS_CLK12','FT_CBUS_CLK6','FT_CBUS_IOMODE',
            'FT_CBUS_BITBANG_WR','FT_CBUS_BITBANG_RD'
        )]
        [string]$Mode = 'FT_CBUS_IOMODE',

        # Optional serial number used as fallback when OpenByIndex fails (e.g. device in VCP mode)
        [Parameter(Mandatory = $false)]
        [string]$SerialNumber = '',

        # Additional EEPROM fields to write alongside the CBUS pin modes.
        # Pass $null (default) to leave the existing EEPROM value unchanged.
        [Parameter(Mandatory = $false)]
        [System.Nullable[bool]]$HighDriveIOs = $null,

        [Parameter(Mandatory = $false)]
        [System.Nullable[bool]]$PullDownEnable = $null,

        [Parameter(Mandatory = $false)]
        [System.Nullable[bool]]$RIsD2XX = $null
    )

    try {
        if (-not $script:FtdiInitialized) {
            $isWindows = [System.Environment]::OSVersion.Platform -eq 'Win32NT'
            if (-not $isWindows) {
                # Linux/macOS: use native P/Invoke EEPROM path when available
                if ($script:FtdiNativeAvailable) {
                    Write-Verbose "Set-FtdiFt232rCbusPinMode: using native P/Invoke EEPROM path on Linux"
                    $ok = Set-FtdiNativeCbusEeprom -Index $Index -Pins $Pins -Mode $Mode
                    if ($ok) {
                        return [PSCustomObject]@{
                            Success        = $true
                            DeviceIndex    = $Index
                            PinsChanged    = $Pins
                            NewMode        = $Mode
                            HighDriveIOs   = $null
                            PullDownEnable = $null
                            RIsD2XX        = $null
                            Message        = "EEPROM written via native D2XX. Replug device to activate."
                        }
                    }
                    return [PSCustomObject]@{ Success = $false; Error = 'Set-FtdiNativeCbusEeprom returned false' }
                }
                Write-Warning (
                    "Set-PsGadgetFt232rCbusMode: FT232R EEPROM programming is not supported on Linux.`n" +
                    "Install libftd2xx.so and reload the module to enable native EEPROM access."
                )
                return [PSCustomObject]@{ Success = $false; Error = 'libftd2xx.so not loaded. Install from ftdichip.com.' }
            }
            throw [System.NotImplementedException]::new("FTDI assembly not loaded")
        }

        $ftdi   = [FTD2XX_NET.FTDI]::new()
        $status = $ftdi.OpenByIndex([uint32]$Index)

        # VCP-mode devices (shown as COM ports) cause OpenByIndex to return FT_DEVICE_NOT_FOUND.
        # Fall back to OpenBySerialNumber which works regardless of driver mode.
        if ($status -ne [FTD2XX_NET.FTDI+FT_STATUS]::FT_OK -and $SerialNumber -ne '') {
            Write-Verbose "OpenByIndex($Index) -> $status; retrying via OpenBySerialNumber('$SerialNumber')"
            $ftdi.Close() | Out-Null
            $ftdi   = [FTD2XX_NET.FTDI]::new()
            $status = $ftdi.OpenBySerialNumber($SerialNumber)
        }

        if ($status -ne [FTD2XX_NET.FTDI+FT_STATUS]::FT_OK) {
            $ftdi.Close() | Out-Null
            $openMethod = if ($SerialNumber -ne '') { "OpenByIndex($Index) and OpenBySerialNumber('$SerialNumber')" } else { "OpenByIndex($Index)" }
            # FT_DEVICE_NOT_OPENED (3) when trying to open usually means another handle is already open.
            if ($status -eq [FTD2XX_NET.FTDI+FT_STATUS]::FT_DEVICE_NOT_OPENED) {
                throw ("Device is already open - close the existing connection first. " +
                       "If you have a `$dev or `$conn variable, call .Close() on it. " +
                       "Otherwise restart the PowerShell session to release all handles.")
            }
            throw "Failed to open device via $openMethod : $status"
        }

        # Read current EEPROM - preserve all fields, only modify requested CBUS pins
        $eeprom = [FTD2XX_NET.FTDI+FT232R_EEPROM_STRUCTURE]::new()
        $status = $ftdi.ReadFT232REEPROM($eeprom)
        if ($status -ne [FTD2XX_NET.FTDI+FT_STATUS]::FT_OK) {
            $ftdi.Close() | Out-Null
            throw "ReadFT232REEPROM failed: $status"
        }

        # Resolve FT_CBUS_OPTIONS value by name using lookup table.
        # Cbus0-4 may be plain bytes in some FTD2XX_NET builds; avoid enum reflection.
        if (-not $script:FT_CBUS_VALUES.ContainsKey($Mode)) {
            $ftdi.Close() | Out-Null
            throw "Unknown CBUS mode '$Mode'. Valid values: $($script:FT_CBUS_VALUES.Keys -join ', ')"
        }
        $targetMode = $script:FT_CBUS_VALUES[$Mode]

        $pinNames = $Pins | ForEach-Object { "CBUS$_" }
        $action   = "Set $($pinNames -join ', ') to $Mode on device index $Index"

        if (-not $PSCmdlet.ShouldProcess("FT232R EEPROM (device $Index)", $action)) {
            $ftdi.Close() | Out-Null
            return
        }

        foreach ($pin in $Pins) {
            switch ($pin) {
                0 { $eeprom.Cbus0 = $targetMode }
                1 { $eeprom.Cbus1 = $targetMode }
                2 { $eeprom.Cbus2 = $targetMode }
                3 { $eeprom.Cbus3 = $targetMode }
                4 { $eeprom.Cbus4 = $targetMode }  # EEPROM-configurable; not runtime bit-bangable
            }
        }

        # Apply additional EEPROM fields sourced from config (or explicit param overrides)
        if ($null -ne $HighDriveIOs)   { $eeprom.HighDriveIOs   = [bool]$HighDriveIOs }
        if ($null -ne $PullDownEnable)  { $eeprom.PullDownEnable  = [bool]$PullDownEnable }
        if ($null -ne $RIsD2XX)         { $eeprom.RIsD2XX         = [bool]$RIsD2XX }

        $status = $ftdi.WriteFT232REEPROM($eeprom)
        $ftdi.Close() | Out-Null

        if ($status -ne [FTD2XX_NET.FTDI+FT_STATUS]::FT_OK) {
            throw "WriteFT232REEPROM failed: $status"
        }

        Write-Verbose "EEPROM updated: $action"
        Write-Warning "EEPROM written. Disconnect and reconnect the USB device for the changes to take effect."

        return [PSCustomObject]@{
            Success        = $true
            DeviceIndex    = $Index
            PinsChanged    = $Pins
            NewMode        = $Mode
            HighDriveIOs   = $eeprom.HighDriveIOs
            PullDownEnable = $eeprom.PullDownEnable
            RIsD2XX        = $eeprom.RIsD2XX
            Message        = "EEPROM written. Replug device to activate."
        }

    } catch [System.NotImplementedException] {
        # Only reached on Windows when FTD2XX_NET assembly failed to load.
        Write-Verbose "Set-FtdiFt232rCbusPinMode: FTDI assembly not loaded - stub mode (no EEPROM written)"
        return [PSCustomObject]@{
            Success        = $true
            DeviceIndex    = $Index
            PinsChanged    = $Pins
            NewMode        = $Mode
            HighDriveIOs   = if ($null -ne $HighDriveIOs)   { [bool]$HighDriveIOs }   else { $false }
            PullDownEnable = if ($null -ne $PullDownEnable)  { [bool]$PullDownEnable }  else { $false }
            RIsD2XX        = if ($null -ne $RIsD2XX)         { [bool]$RIsD2XX }         else { $false }
            Message        = "STUB: EEPROM write simulated (assembly not loaded)"
        }
    } catch {
        Write-Error "Set-FtdiFt232rCbusPinMode failed: $_"
        return [PSCustomObject]@{
            Success  = $false
            Error    = $_.Exception.Message
        }
    }
}

function Set-FtdiCbusBits {
    <#
    .SYNOPSIS
    Drives FT232R CBUS pins via CBUS bit-bang mode (SetBitMode 0x20).
 
    .DESCRIPTION
    Calls SetBitMode on an already-open D2XX connection with mode 0x20 (CBUS bit-bang).
    The mask byte encodes both direction and value:
        Bits 7-4: direction for CBUS3-CBUS0 (1=output)
        Bits 3-0: output value for CBUS3-CBUS0
 
    Prerequisites:
      - The target CBUS pins must be programmed as FT_CBUS_IOMODE in the device EEPROM.
        If they are not, run Set-FtdiFt232rCbusPinMode (public: Set-PsGadgetFt232rCbusMode)
        once, replug the device, then retry.
      - The connection must be open (from Connect-PsGadgetFtdi or Invoke-FtdiWindowsOpen).
 
    .PARAMETER Connection
    Open FTDI connection object (returned by Connect-PsGadgetFtdi / Invoke-FtdiWindowsOpen).
 
    .PARAMETER Pins
    One or more CBUS pin numbers to control (0-3).
 
    .PARAMETER State
    Target state for the specified pins: HIGH/H/1 or LOW/L/0.
 
    .PARAMETER OutputPins
    Which CBUS pins (0-3) to configure as outputs in this call.
    Defaults to the same set as Pins. Pins not listed are configured as inputs.
 
    .PARAMETER DurationMs
    If specified, hold the state for this many milliseconds then invert the pins.
 
    .EXAMPLE
    Set-FtdiCbusBits -Connection $conn -Pins @(0,1) -State HIGH
    #>

    [CmdletBinding()]
    [OutputType([bool])]
    param(
        [Parameter(Mandatory = $true)]
        [System.Object]$Connection,

        [Parameter(Mandatory = $true)]
        [ValidateRange(0, 3)]
        [int[]]$Pins,

        [Parameter(Mandatory = $true)]
        [ValidateSet('HIGH', 'LOW', 'H', 'L', '1', '0')]
        [string]$State,

        [Parameter(Mandatory = $false)]
        [ValidateRange(0, 3)]
        [int[]]$OutputPins,

        [Parameter(Mandatory = $false)]
        [ValidateRange(1, 60000)]
        [int]$DurationMs
    )

    try {
        if (-not $Connection -or -not $Connection.IsOpen) {
            throw "Connection is not open"
        }

        $isHigh = $State -in @('HIGH', 'H', '1')

        # Build output pin set - default to the same pins being driven
        $outputSet = if ($OutputPins) { $OutputPins } else { $Pins }

        # Build direction nibble (bits 3-0 of the upper nibble in the mask)
        $dirNibble = 0
        foreach ($p in $outputSet) {
            $dirNibble = $dirNibble -bor (1 -shl $p)
        }

        # Build value nibble
        $valNibble = 0
        if ($isHigh) {
            foreach ($p in $Pins) {
                $valNibble = $valNibble -bor (1 -shl $p)
            }
        }

        # Combined mask: upper nibble = direction, lower nibble = value
        [byte]$mask = (($dirNibble -band 0x0F) -shl 4) -bor ($valNibble -band 0x0F)

        Write-Verbose ("CBUS bit-bang: pins=[{0}] state={1} dir=0x{2:X1} val=0x{3:X1} mask=0x{4:X2}" -f
            ($Pins -join ','), $State, $dirNibble, $valNibble, $mask)

        if ($script:FtdiInitialized -and $null -ne $Connection.Device) {
            # Windows path: FTD2XX_NET managed object
            $status = $Connection.Device.SetBitMode($mask, 0x20)

            if ($status -ne [FTD2XX_NET.FTDI+FT_STATUS]::FT_OK) {
                # Provide targeted help if CBUS pins are not configured
                if ($status -eq [FTD2XX_NET.FTDI+FT_STATUS]::FT_OTHER_ERROR) {
                    Write-Warning (
                        "CBUS bit-bang failed. Ensure CBUS pins are programmed as FT_CBUS_IOMODE " +
                        "in the device EEPROM. Run: Set-PsGadgetFt232rCbusMode -Index <n> " +
                        "-Pins @($($Pins -join ',')) then replug the device."
                    )
                }
                throw "SetBitMode(CBUS bit-bang) failed: $status"
            }

            if ($DurationMs) {
                Start-Sleep -Milliseconds $DurationMs

                # Invert value nibble to pulse
                [byte]$invertMask = (($dirNibble -band 0x0F) -shl 4) -bor ((-bnot $valNibble) -band $dirNibble -band 0x0F)
                $Connection.Device.SetBitMode($invertMask, 0x20) | Out-Null
            }

        } elseif ($script:FtdiNativeAvailable -and
                  $Connection.PSObject.Properties['NativeHandle'] -and
                  $Connection.NativeHandle -ne [IntPtr]::Zero) {
            # Linux/Unix path: native P/Invoke via libftd2xx.so
            Invoke-FtdiNativeSetBitMode -Handle $Connection.NativeHandle -Mask $mask -Mode 0x20

            if ($DurationMs) {
                Start-Sleep -Milliseconds $DurationMs

                [byte]$invertMask = (($dirNibble -band 0x0F) -shl 4) -bor ((-bnot $valNibble) -band $dirNibble -band 0x0F)
                Invoke-FtdiNativeSetBitMode -Handle $Connection.NativeHandle -Mask $invertMask -Mode 0x20
            }
        } else {
            Write-Verbose ("CBUS bit-bang (STUB): mask=0x{0:X2}" -f $mask)

            if ($DurationMs) {
                Start-Sleep -Milliseconds $DurationMs
            }
        }

        return $true

    } catch {
        Write-Error "Set-FtdiCbusBits failed: $_"
        return $false
    }
}

function Set-FtdiFt232hEepromMode {
    <#
    .SYNOPSIS
    Writes FT232H EEPROM fields: VCP mode flag and optional ACBUS pin functions.
 
    .DESCRIPTION
    Reads the current FT232H EEPROM, applies the requested changes (IsVCP,
    ACBUS/ADBUS drive settings, Cbus0-9 pin modes), and writes the result back.
    Change takes effect after USB replug or CyclePort.
 
    .PARAMETER Index
    Zero-based device index (from Get-FTDevice).
 
    .PARAMETER SerialNumber
    Optional fallback when OpenByIndex fails.
 
    .PARAMETER IsVCP
    When $false (default intent), sets the EEPROM IsVCP flag to $false so the
    device enumerates as D2XX-only (no COM port). When $true, re-enables VCP mode.
 
    .PARAMETER CbusPins
    Optional hashtable of ACBUS pin numbers -> FT_232H_CBUS_OPTIONS name to write.
    E.g. @{ 5 = 'FT_CBUS_IOMODE' }
 
    .PARAMETER ACDriveCurrent
    Optional override for ACBUS drive current (4, 8, 12, or 16 mA).
 
    .PARAMETER ADDriveCurrent
    Optional override for ADBUS drive current (4, 8, 12, or 16 mA).
    #>

    [CmdletBinding(SupportsShouldProcess)]
    param(
        [Parameter(Mandatory = $true)]
        [int]$Index,

        [Parameter(Mandatory = $false)]
        [string]$SerialNumber = '',

        [Parameter(Mandatory = $false)]
        [System.Nullable[bool]]$IsVCP = $null,

        [Parameter(Mandatory = $false)]
        [hashtable]$CbusPins = $null,

        [Parameter(Mandatory = $false)]
        [System.Nullable[int]]$ACDriveCurrent = $null,

        [Parameter(Mandatory = $false)]
        [System.Nullable[int]]$ADDriveCurrent = $null
    )

    try {
        if (-not $script:FtdiInitialized) {
            throw [System.NotImplementedException]::new("FTDI assembly not loaded")
        }

        $ftdi   = [FTD2XX_NET.FTDI]::new()
        $status = $ftdi.OpenByIndex([uint32]$Index)

        if ($status -ne [FTD2XX_NET.FTDI+FT_STATUS]::FT_OK -and $SerialNumber -ne '') {
            Write-Verbose "OpenByIndex($Index) -> $status; retrying via OpenBySerialNumber('$SerialNumber')"
            $ftdi.Close() | Out-Null
            $ftdi   = [FTD2XX_NET.FTDI]::new()
            $status = $ftdi.OpenBySerialNumber($SerialNumber)
        }

        if ($status -ne [FTD2XX_NET.FTDI+FT_STATUS]::FT_OK) {
            $ftdi.Close() | Out-Null
            if ($status -eq [FTD2XX_NET.FTDI+FT_STATUS]::FT_DEVICE_NOT_OPENED) {
                throw ("Device is already open - close the existing connection first. " +
                       "Call .Close() on any open `$dev variable or restart the PowerShell session.")
            }
            throw "Failed to open FT232H device: $status"
        }

        # Read current EEPROM - preserve all fields, only modify what was requested
        $eeprom = [FTD2XX_NET.FTDI+FT232H_EEPROM_STRUCTURE]::new()
        $status = $ftdi.ReadFT232HEEPROM($eeprom)
        if ($status -ne [FTD2XX_NET.FTDI+FT_STATUS]::FT_OK) {
            $ftdi.Close() | Out-Null
            throw "ReadFT232HEEPROM failed: $status"
        }

        # Build description of changes for ShouldProcess
        $changes = [System.Collections.Generic.List[string]]::new()
        if ($null -ne $IsVCP)         { $changes.Add("IsVCP=$IsVCP") }
        if ($null -ne $ACDriveCurrent) { $changes.Add("ACDriveCurrent=$ACDriveCurrent") }
        if ($null -ne $ADDriveCurrent) { $changes.Add("ADDriveCurrent=$ADDriveCurrent") }
        if ($CbusPins) {
            foreach ($pin in $CbusPins.Keys) {
                $changes.Add("Cbus$pin=$($CbusPins[$pin])")
            }
        }

        $action = "Write FT232H EEPROM: $($changes -join ', ')"
        if (-not $PSCmdlet.ShouldProcess("FT232H device index $Index", $action)) {
            $ftdi.Close() | Out-Null
            return $null
        }

        # Apply changes
        if ($null -ne $IsVCP)          { $eeprom.IsVCP = [bool]$IsVCP }
        if ($null -ne $ACDriveCurrent)  { $eeprom.ACDriveCurrent = [byte]$ACDriveCurrent }
        if ($null -ne $ADDriveCurrent)  { $eeprom.ADDriveCurrent = [byte]$ADDriveCurrent }

        if ($CbusPins) {
            foreach ($pin in $CbusPins.Keys) {
                $modeName = $CbusPins[$pin]
                # Resolve name to integer using reverse lookup
                $modeVal = $null
                foreach ($k in $script:FT_232H_CBUS_NAMES.Keys) {
                    if ($script:FT_232H_CBUS_NAMES[$k] -eq $modeName) {
                        $modeVal = [byte]$k
                        break
                    }
                }
                if ($null -eq $modeVal) {
                    $ftdi.Close() | Out-Null
                    throw "Unknown FT232H CBUS mode '$modeName'. Valid: $($script:FT_232H_CBUS_NAMES.Values -join ', ')"
                }
                switch ([int]$pin) {
                    0 { $eeprom.Cbus0 = $modeVal }
                    1 { $eeprom.Cbus1 = $modeVal }
                    2 { $eeprom.Cbus2 = $modeVal }
                    3 { $eeprom.Cbus3 = $modeVal }
                    4 { $eeprom.Cbus4 = $modeVal }
                    5 { $eeprom.Cbus5 = $modeVal }
                    6 { $eeprom.Cbus6 = $modeVal }
                    7 { $eeprom.Cbus7 = $modeVal }
                    8 { $eeprom.Cbus8 = $modeVal }
                    9 { $eeprom.Cbus9 = $modeVal }
                    default {
                        $ftdi.Close() | Out-Null
                        throw "FT232H Cbus pin must be 0-9, got: $pin"
                    }
                }
            }
        }

        $status = $ftdi.WriteFT232HEEPROM($eeprom)
        $ftdi.Close() | Out-Null

        if ($status -ne [FTD2XX_NET.FTDI+FT_STATUS]::FT_OK) {
            throw "WriteFT232HEEPROM failed: $status"
        }

        Write-Verbose "FT232H EEPROM updated: $action"

        return [PSCustomObject]@{
            Success        = $true
            DeviceIndex    = $Index
            ChangesApplied = $changes
            IsVCP          = $eeprom.IsVCP
            ACDriveCurrent = $eeprom.ACDriveCurrent
            ADDriveCurrent = $eeprom.ADDriveCurrent
            Message        = "EEPROM written. Replug device (or call .CyclePort()) to activate."
        }

    } catch [System.NotImplementedException] {
        Write-Warning "Set-FtdiFt232hEepromMode: FTDI assembly not loaded."
        return [PSCustomObject]@{ Success = $false; Error = 'FTDI assembly not loaded' }
    } catch {
        Write-Error "Set-FtdiFt232hEepromMode failed: $_"
        return [PSCustomObject]@{ Success = $false; Error = $_.Exception.Message }
    }
}