Public/Set-PsGadgetGpio.ps1

# Set-PsGadgetGpio.ps1
# Public GPIO control function for FTDI devices

function Set-PsGadgetGpio {
    <#
    .SYNOPSIS
    Controls GPIO pins on connected FTDI devices.
     
    .DESCRIPTION
    Sets GPIO pins on FTDI devices to HIGH or LOW states. Supports both MPSSE-
    capable devices (FT232H, FT2232H, FT4232H) using ACBUS0-7, and CBUS bit-bang
    devices (FT232R / FT232RL / FT232RNL) using CBUS0-3.
 
    For FT232R CBUS GPIO, the CBUS pins must first be programmed in the device
    EEPROM as FT_CBUS_IOMODE. Run Set-PsGadgetFt232rCbusMode once per device,
    replug the USB device, then use this function normally.
 
    Supports timing control and multiple pin operations.
     
    .PARAMETER Index
    Index of the FTDI device to control (from Get-PsGadgetFtdi)
     
    .PARAMETER Pins
    For MPSSE devices (FT232H, FT2232H, FT4232H): ACBUS pin numbers 0-7
      ACBUS0=pin21, ACBUS1=pin25, ACBUS2=pin26, ACBUS3=pin27
      ACBUS4=pin28, ACBUS5=pin29, ACBUS6=pin30, ACBUS7=pin31 (FT232H)
    For CBUS devices (FT232R): CBUS pin numbers 0-3 only
      (Pins outside 0-3 are rejected for CBUS devices)
     
    .PARAMETER State
    Pin state: HIGH/H/1 or LOW/L/0
     
    .PARAMETER DurationMs
    Optional duration to hold the pin state in milliseconds
     
    .PARAMETER SerialNumber
    Alternative to Index - specify device by serial number
 
    .PARAMETER Connection
    An already-open connection object returned by Connect-PsGadgetFtdi.
    When using this parameter set the caller is responsible for closing the connection.
     
    .EXAMPLE
    # FT232H / MPSSE device - control ACBUS pins
    Set-PsGadgetGpio -Index 0 -Pins @(2, 4) -State HIGH
     
    .EXAMPLE
    # FT232R CBUS GPIO (after running Set-PsGadgetFt232rCbusMode -Index 1 once)
    Set-PsGadgetGpio -Index 1 -Pins @(0, 1) -State HIGH
     
    .EXAMPLE
    # Pulse ACBUS0 LOW for 500ms
    Set-PsGadgetGpio -SerialNumber "ABC123" -Pins @(0) -State LOW -DurationMs 500
 
    .EXAMPLE
    # Connect once, call GPIO multiple times, close when done
    $conn = Connect-PsGadgetFtdi -SerialNumber "BG01X3GX"
    Set-PsGadgetGpio -Connection $conn -Pins @(0) -State HIGH # LED on
    Set-PsGadgetGpio -Connection $conn -Pins @(1) -State HIGH
    Set-PsGadgetGpio -Connection $conn -Pins @(0, 1) -State LOW # Both off
    $conn.Close()
 
    .EXAMPLE
    # OOP style via New-PsGadgetFtdi
    $dev = New-PsGadgetFtdi -SerialNumber "BG01X3GX" # connected immediately
    $dev.SetPin(0, "HIGH")
    $dev.SetPin(0, "LOW")
    $dev.Close()
     
    .EXAMPLE
    # LED Control Example (FT232H)
    Set-PsGadgetGpio -Index 0 -Pins @(2) -State HIGH # Red LED on
    Set-PsGadgetGpio -Index 0 -Pins @(4) -State HIGH # Green LED on
    Set-PsGadgetGpio -Index 0 -Pins @(2, 4) -State LOW # Both off
     
    .NOTES
    Requires FTDI D2XX drivers and FTD2XX_NET.dll assembly.
    FT232H MPSSE: ACBUS0-7 = physical pins 21,25-31.
    FT232R CBUS: CBUS0-3 require prior EEPROM configuration via Set-PsGadgetFt232rCbusMode.
    Use Get-PsGadgetFtdi to see available devices.
    #>

    
    [CmdletBinding(DefaultParameterSetName = 'ByIndex')]
    param(
        [Parameter(Mandatory = $true, ParameterSetName = 'ByIndex', Position = 0)]
        [int]$Index,
        
        [Parameter(Mandatory = $true, ParameterSetName = 'BySerial')]
        [string]$SerialNumber,

        [Parameter(Mandatory = $true, ParameterSetName = 'ByConnection', Position = 0)]
        [ValidateNotNull()]
        [object]$Connection,

        [Parameter(Mandatory = $true, ParameterSetName = 'PsGadget', Position = 0)]
        [ValidateNotNull()]
        [PsGadgetFtdi]$PsGadget,
        
        [Parameter(Mandatory = $true, Position = 1)]
        [ValidateRange(0, 7)]
        [int[]]$Pins,
        
        [Parameter(Mandatory = $true, Position = 2)]
        [ValidateSet('HIGH', 'LOW', 'H', 'L', '1', '0')]
        [string]$State,
        
        [Parameter(Mandatory = $false)]
        [ValidateRange(1, 60000)]
        [int]$DurationMs
    )
    
    try {
        # Track whether this function opened the connection (must close it in finally)
        $ownsConnection = $true

        if ($PSCmdlet.ParameterSetName -eq 'ByConnection') {
            # Caller provides an already-open connection - use it directly, do not close on exit
            $ownsConnection = $false
            if (-not $Connection.IsOpen) {
                throw "The supplied connection is not open. Call Connect-PsGadgetFtdi or New-PsGadgetFtdi first."
            }
            Write-Verbose "Using caller-supplied connection: $($Connection.Description) ($($Connection.SerialNumber))"
        } elseif ($PSCmdlet.ParameterSetName -eq 'PsGadget') {
            # Caller provides a PsGadgetFtdi class instance - unwrap its internal connection
            $ownsConnection = $false
            if (-not $PsGadget.IsOpen -or -not $PsGadget._connection) {
                throw "PsGadgetFtdi is not open. Use New-PsGadgetFtdi, which connects automatically."
            }
            $Connection = $PsGadget._connection
            Write-Verbose "Using PsGadgetFtdi connection: $($Connection.Description) ($($Connection.SerialNumber))"
        } else {
            # Get available devices
            $devices = Get-FtdiDeviceList
            if (-not $devices -or $devices.Count -eq 0) {
                throw "No FTDI devices found. Run Get-PsGadgetFtdi to check available devices."
            }
            
            # Find target device
            $targetDevice = $null
            if ($PSCmdlet.ParameterSetName -eq 'ByIndex') {
                if ($Index -lt 0 -or $Index -ge $devices.Count) {
                    throw "Device index $Index is out of range. Available devices: 0-$($devices.Count - 1)"
                }
                $targetDevice = $devices[$Index]
            } else {
                $targetDevice = $devices | Where-Object { $_.SerialNumber -eq $SerialNumber }
                if (-not $targetDevice) {
                    throw "No device found with serial number '$SerialNumber'"
                }
            }
            
            Write-Verbose "Targeting device: $($targetDevice.Description) ($($targetDevice.SerialNumber))"
            
            # Check if device is available
            if ($targetDevice.IsOpen) {
                Write-Warning "Device $($targetDevice.SerialNumber) appears to be in use by another application"
            }
            
            # Open device connection
            $Connection = Connect-PsGadgetFtdi -Index $targetDevice.Index
            if (-not $Connection) {
                throw "Failed to connect to FTDI device"
            }
        }
        
        try {
            # Determine GPIO method - dispatch to the appropriate backend
            $gpioMethod = if ($Connection.PSObject.Properties['GpioMethod']) {
                $Connection.GpioMethod
            } else {
                'Unknown'
            }

            Write-Verbose "Device $($Connection.Type): GpioMethod=$gpioMethod, pins=[$($Pins -join ',')], state=$State"

            $success = $false
            $pinLabel = ''

            switch ($gpioMethod) {
                'MPSSE' {
                    # FT232H / FT2232H / FT4232H - ACBUS control via MPSSE command 0x82
                    $params = @{
                        DeviceHandle = $Connection
                        Pins         = $Pins
                        Direction    = $State
                    }
                    if ($DurationMs) { $params.DurationMs = $DurationMs }

                    $success  = Set-FtdiGpioPins @params
                    $pinLabel = "ACBUS pins [$($Pins -join ', ')]"
                }

                'IoT' {
                    # FT232H via .NET IoT GpioController (PS 7.4+ / .NET 8+ backend)
                    # PsGadget ACBUS pin 0-7 -> IoT GpioController pin 8-15 (C0-C7)
                    if (-not $Connection.GpioController) {
                        throw "IoT connection is missing GpioController. Re-open the device with Connect-PsGadgetFtdi."
                    }
                    $iotParams = @{
                        GpioController = $Connection.GpioController
                        Pins           = $Pins
                        State          = $State
                    }
                    if ($DurationMs) { $iotParams.DurationMs = $DurationMs }

                    $success  = Set-FtdiIotGpioPins @iotParams
                    $pinLabel = "ACBUS pins [$($Pins -join ', ')] (IoT)"
                }

                'CBUS' {
                    # FT232R / FT231X / FT230X - CBUS bit-bang via SetBitMode 0x20
                    # Validate pin range - CBUS bit-bang only supports CBUS0-3
                    $badPins = $Pins | Where-Object { $_ -gt 3 }
                    if ($badPins) {
                        throw (
                            "Pin(s) [$($badPins -join ', ')] are out of range for CBUS bit-bang. " +
                            "$($Connection.Type) CBUS GPIO supports CBUS0-3 only (pins 0-3)."
                        )
                    }

                    $cbusParams = @{
                        Connection = $Connection
                        Pins       = $Pins
                        State      = $State
                    }
                    if ($DurationMs) { $cbusParams.DurationMs = $DurationMs }

                    $success  = Set-FtdiCbusBits @cbusParams
                    $pinLabel = "CBUS pins [$($Pins -join ', ')]"
                }

                'AsyncBitBang' {
                    # Async bit-bang on ADBUS0-7 (UART data pins). This mode must be
                    # enabled beforehand via Set-PsGadgetFtdiMode -Mode AsyncBitBang
                    # with an appropriate direction mask (default 0xFF).
                    # We simply write a byte with the requested pin states to the
                    # device; higher-level sequence streaming is handled by user
                    # scripts (see example StepperMotor.md).
                    $badPins = $Pins | Where-Object { $_ -lt 0 -or $_ -gt 7 }
                    if ($badPins) {
                        throw "Pin(s) [$($badPins -join ', ')] are out of range for async bit-bang. ADBUS GPIO supports pins 0-7."
                    }
                    # build output byte
                    [int]$outByte = 0
                    foreach ($p in $Pins) {
                        if ($State -in @('HIGH','H','1')) {
                            $outByte = $outByte -bor (1 -shl $p)
                        }
                    }
                    $written = 0
                    $Connection.Write([byte[]]@($outByte), 1, [ref]$written) | Out-Null
                    if ($DurationMs) {
                        Start-Sleep -Milliseconds $DurationMs
                        # clear outputs after duration
                        $Connection.Write([byte[]]@(0), 1, [ref]$written) | Out-Null
                    }
                    $pinLabel = "ADBUS pins [$($Pins -join ', ')]"
                    $success = $true
                }

                default {
                    # Unknown or unsupported type - attempt MPSSE as last resort
                    Write-Warning "Unknown GpioMethod '$gpioMethod' for device '$($Connection.Type)'. Attempting MPSSE fallback."
                    $params = @{
                        DeviceHandle = $Connection
                        Pins         = $Pins
                        Direction    = $State
                    }
                    if ($DurationMs) { $params.DurationMs = $DurationMs }
                    $success  = Set-FtdiGpioPins @params
                    $pinLabel = "pins [$($Pins -join ', ')]"
                }
            }

            if (-not $success) {
                throw "GPIO operation failed"
            }
            
        } finally {
            # Only close the connection if this function opened it
            if ($ownsConnection -and $Connection -and $Connection.Close) {
                try {
                    $Connection.Close()
                } catch {
                    Write-Warning "Failed to close device connection: $_"
                }
            }
        }
        
    } catch {
        Write-Error "GPIO control failed: $_"
        throw
    }
}