Public/New-PsGadgetFtdi.ps1

#Requires -Version 5.1
# New-PsGadgetFtdi.ps1
# Factory function to create and connect a PsGadgetFtdi instance.
# Because PowerShell module classes are not exported to the caller's type scope,
# consumers cannot write [PsGadgetFtdi]::new() directly. Use this function
# instead. The returned object is already connected - no .Connect() call needed.
# Mirrors MicroPython convention: construction = connection.

function New-PsGadgetFtdi {
    <#
    .SYNOPSIS
    Creates and connects a PsGadgetFtdi device object, ready to use immediately.
 
    .DESCRIPTION
    Instantiates a PsGadgetFtdi object and opens the hardware connection in one
    step. The returned object is already open -- call .ScanI2CBus(), .Display(),
    .SetPin() etc. directly. Call .Close() when done, or wrap in try/finally
    for deterministic cleanup.
 
    Mirrors MicroPython convention where construction implies connection:
        i2c = I2C(0, scl=Pin(1), sda=Pin(0)) # MicroPython - connected immediately
        $dev = New-PsGadgetFtdi -Index 0 # PSGadget - connected immediately
 
    .PARAMETER SerialNumber
    FTDI device serial number (e.g. "FT9ZLJ51").
    Preferred: stable across USB re-plugs regardless of port order.
    Use Get-FtdiDevice to find the serial number.
 
    .PARAMETER Index
    FTDI device index (0-based) from Get-FtdiDevice.
    May change if devices are plugged in different order.
 
    .PARAMETER LocationId
    FTDI USB LocationId (hub+port address) from Get-FtdiDevice.
    Stable for a fixed physical USB port - useful for demo rigs.
 
    .PARAMETER DisplayHeight
    SSD1306 OLED display height in pixels. Use 32 for 128x32 displays, 64 (default) for 128x64.
    Sets $dev.DisplayHeight before the first GetDisplay() call. Can also be changed afterwards:
        $dev.DisplayHeight = 32
 
    .EXAMPLE
    # 128x32 OLED
    $dev = New-PsGadgetFtdi -Index 0 -DisplayHeight 32
    $dev.Display('hello')
 
    .EXAMPLE
    # Minimal - index workflow
    $dev = New-PsGadgetFtdi -Index 0
    $dev.ScanI2CBus() | Format-Table
    $dev.Display("Hello", 0)
    $dev.Close()
 
    .EXAMPLE
    # Preferred - serial number (stable across replug / hub reorder)
    $dev = New-PsGadgetFtdi -SerialNumber "FT9ZLJ51"
    $dev.SetPin(0, "HIGH")
    $dev.Close()
 
    .EXAMPLE
    # LocationId - best for fixed-port demo rigs
    $dev = New-PsGadgetFtdi -LocationId 197634
    $dev.Display("Ready", 0)
    $dev.Close()
 
    .EXAMPLE
    # try/finally for deterministic cleanup in scripts
    $dev = New-PsGadgetFtdi -Index 0
    try {
        $dev.Display("Running", 0)
        Start-Sleep -Seconds 2
        $dev.ClearDisplay()
    } finally {
        $dev.Close()
    }
 
    .OUTPUTS
    PsGadgetFtdi (already connected, IsOpen = $true)
    #>


    [CmdletBinding(DefaultParameterSetName = 'ByIndex', SupportsShouldProcess = $true)]
    [OutputType('PsGadgetFtdi')]
    param(
        [Parameter(Mandatory = $true, ParameterSetName = 'BySerial', Position = 0)]
        [ValidateNotNullOrEmpty()]
        [string]$SerialNumber,

        [Parameter(Mandatory = $true, ParameterSetName = 'ByIndex', Position = 0)]
        [ValidateRange(0, 127)]
        [int]$Index,

        [Parameter(Mandatory = $true, ParameterSetName = 'ByLocation', Position = 0)]
        [ValidateNotNullOrEmpty()]
        [string]$LocationId,

        [Parameter(Mandatory = $false)]
        [ValidateSet(32, 64)]
        [int]$DisplayHeight = 64
    )

    if ($PSCmdlet.ParameterSetName -eq 'BySerial') {
        $dev = [PsGadgetFtdi]::new($SerialNumber)
    } elseif ($PSCmdlet.ParameterSetName -eq 'ByIndex') {
        $dev = [PsGadgetFtdi]::new($Index)
    } else {
        # ByLocation: create via index constructor with -1, then set LocationId property
        $dev = [PsGadgetFtdi]::new(-1)
        $dev.LocationId  = $LocationId
        $dev.Description = "FTDI @ Location $LocationId"
    }

    # Connect immediately - mirrors MicroPython construction-implies-connection convention.
    # .Connect() is idempotent: if already open it returns immediately; if closed it reconnects.
    $dev.DisplayHeight = $DisplayHeight
    if ($PSCmdlet.ShouldProcess($dev.Description, 'Connect')) {
        # FT232R CBUS pins have internal 200k pull-ups to VCCIO (datasheet section 6, Note 1).
        # They float HIGH after power-up/reset regardless of software state.
        # EEPROM read opens its own D2XX handle -- must happen BEFORE Connect() to avoid conflict.
        # Suppress verbose on internal calls; only our summary message is relevant to the user.
        $cbusIoPins = [int[]]@()
        $savedVerbose = $VerbosePreference
        $VerbosePreference = 'SilentlyContinue'
        try {
            $devices = @(Get-FtdiDeviceList)
            $preConnectDev = switch ($PSCmdlet.ParameterSetName) {
                'ByIndex'    { $devices | Where-Object { $_.Index -eq $Index } | Select-Object -First 1 }
                'BySerial'   { $devices | Where-Object { $_.SerialNumber -eq $SerialNumber } | Select-Object -First 1 }
                'ByLocation' { $devices | Where-Object { "$($_.LocationId)" -eq $LocationId } | Select-Object -First 1 }
            }
            if ($preConnectDev -and $preConnectDev.Type -match '^FT232R' -and -not $preConnectDev.IsOpen) {
                $eeprom = Get-FtdiEeprom -Index $preConnectDev.Index
                if ($eeprom) {
                    $cbusIoPins = [int[]](0..3 | Where-Object { $eeprom."Cbus$_" -eq 'FT_CBUS_IOMODE' })
                }
            }
        } finally {
            $VerbosePreference = $savedVerbose
        }

        $dev.Connect()

        if ($cbusIoPins.Count -gt 0) {
            $pinList = ($cbusIoPins | ForEach-Object { "CBUS$_" }) -join ', '
            Write-Verbose "FT232R: CBUS pins float HIGH on power-up due to internal 200k ohm pull-ups (datasheet section 6, Note 1). This is safe for signal/LED use but could trigger relays or actuators. Auto-driving $pinList LOW."
            $dev.SetPins($cbusIoPins, $false)
        }
    }
    return $dev
}