Public/Invoke-PsGadgetUart.ps1

#Requires -Version 5.1
# Invoke-PsGadgetUart.ps1
# High-level UART dispatch cmdlet for FTDI FT232R, FT232H, and compatible devices.

function Invoke-PsGadgetUart {
    <#
    .SYNOPSIS
    Performs a UART write, read, or readline on an FTDI device.
 
    .DESCRIPTION
    Opens an FTDI device (or reuses an existing one), configures it for D2XX UART,
    then performs the requested operation:
 
        -Data only Write-only: sends bytes or a string to the device.
        -ReadCount only Read-only: waits for N bytes (up to ReadTimeout).
        -ReadLine Read-only: waits for a newline-terminated line.
        -Data + -ReadCount Write then read: sends Data, then reads ReadCount bytes.
        -Data + -ReadLine Write then readline: sends Data, then waits for a line.
 
    The FTDI device is opened and closed automatically unless -PsGadget is supplied,
    in which case the caller retains ownership and the device stays open after the call.
 
    .PARAMETER PsGadget
    An already-open PsGadgetFtdi object. Device will NOT be closed after the call.
 
    .PARAMETER Index
    FTDI device index (0-based). Default 0.
 
    .PARAMETER SerialNumber
    FTDI device serial number (preferred over Index for stable identification).
 
    .PARAMETER Data
    Bytes or a string to write. Strings are UTF-8 encoded. No line ending is added
    automatically -- include "`r`n" or "`n" in the string as needed.
 
    .PARAMETER ReadCount
    Number of raw bytes to read after the optional write.
 
    .PARAMETER ReadLine
    After the optional write, read bytes until a newline (\n) is received or
    LineTimeout elapses. Returns $null on timeout, "" if device sent a bare \n,
    or the received line (with \r stripped) otherwise.
 
    .PARAMETER BaudRate
    Baud rate in bits per second. Default 9600.
 
    .PARAMETER DataBits
    Word length: 7 or 8. Default 8.
 
    .PARAMETER StopBits
    Stop bits: 1 or 2. Default 1.
 
    .PARAMETER Parity
    Parity: 'None', 'Odd', 'Even', 'Mark', 'Space'. Default 'None'.
 
    .PARAMETER FlowControl
    Flow control: 'None', 'RtsCts', 'DtrDsr', 'XonXoff'. Default 'None'.
 
    .PARAMETER ReadTimeout
    Milliseconds to wait for read data. Default 500.
 
    .PARAMETER WriteTimeout
    Milliseconds allowed for write completion. Default 500.
 
    .PARAMETER LineTimeout
    Milliseconds to wait for a newline when -ReadLine is used. Default 2000.
 
    .PARAMETER MaxLineLength
    Maximum bytes to buffer per ReadLine call before giving up. Default 1024.
 
    .EXAMPLE
    # Send "AT\r\n" and read the response line
    Invoke-PsGadgetUart -Index 0 -Data "AT`r`n" -ReadLine -BaudRate 9600
 
    .EXAMPLE
    # Raw read of 16 bytes at 115200 baud
    $bytes = Invoke-PsGadgetUart -Index 0 -ReadCount 16 -BaudRate 115200
 
    .EXAMPLE
    # Write binary bytes (no read)
    Invoke-PsGadgetUart -Index 0 -Data ([byte[]](0x01, 0x02, 0x03)) -BaudRate 57600
 
    .EXAMPLE
    # Reuse an open device (device stays open after the call)
    $dev = New-PsGadgetFtdi -SerialNumber 'FTAXBFCQ'
    $line = Invoke-PsGadgetUart -PsGadget $dev -Data "STATUS`r`n" -ReadLine
 
    .EXAMPLE
    # Polling loop -- keep device open to avoid per-call open/close overhead
    $dev = New-PsGadgetFtdi -SerialNumber 'FTAXBFCQ'
    try {
        while ($true) {
            $resp = Invoke-PsGadgetUart -PsGadget $dev -Data "READ`r`n" -ReadLine
            # $null = timeout (no \n received); "" = device sent bare \n; else = response line
            if ($null -ne $resp) {
                Write-Host "$(Get-Date -Format 'HH:mm:ss') $resp"
            }
            Start-Sleep -Seconds 30
        }
    } finally {
        $dev.Close()
    }
 
    .OUTPUTS
    Write-only: [bool] $true on success, $false on failure.
    Read-only: [byte[]] for -ReadCount.
                  [string] for -ReadLine when a newline was received (may be "" for bare \n).
                  $null for -ReadLine when LineTimeout elapsed without receiving a newline.
    Write+Read: [byte[]] or [string]/$null depending on read mode.
 
    .NOTES
    Supported on FT232R, FT232H, FT2232H, FT4232H in UART mode (SetBitMode 0x00).
    UART is the factory-default mode; no mode-switch is required.
    Wire guide (FT232R): TX(D0)->RX of target RX(D1)<-TX of target GND->GND
    #>


    [CmdletBinding(DefaultParameterSetName = 'ByIndex')]
    param(
        [Parameter(Mandatory = $true,  ParameterSetName = 'ByDevice', Position = 0)]
        [ValidateNotNull()]
        [object]$PsGadget,

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

        [Parameter(Mandatory = $true,  ParameterSetName = 'BySerial')]
        [ValidateNotNullOrEmpty()]
        [string]$SerialNumber,

        # Accept byte[] or string
        [Parameter(Mandatory = $false)]
        [object]$Data,

        [Parameter(Mandatory = $false)]
        [ValidateRange(1, 65536)]
        [int]$ReadCount,

        [Parameter(Mandatory = $false)]
        [switch]$ReadLine,

        [Parameter(Mandatory = $false)]
        [ValidateRange(300, 12000000)]
        [int]$BaudRate = 9600,

        [Parameter(Mandatory = $false)]
        [ValidateSet(7, 8)]
        [int]$DataBits = 8,

        [Parameter(Mandatory = $false)]
        [ValidateSet(1, 2)]
        [int]$StopBits = 1,

        [Parameter(Mandatory = $false)]
        [ValidateSet('None', 'Odd', 'Even', 'Mark', 'Space')]
        [string]$Parity = 'None',

        [Parameter(Mandatory = $false)]
        [ValidateSet('None', 'RtsCts', 'DtrDsr', 'XonXoff')]
        [string]$FlowControl = 'None',

        [Parameter(Mandatory = $false)]
        [ValidateRange(0, 60000)]
        [int]$ReadTimeout = 500,

        [Parameter(Mandatory = $false)]
        [ValidateRange(0, 60000)]
        [int]$WriteTimeout = 500,

        [Parameter(Mandatory = $false)]
        [ValidateRange(0, 60000)]
        [int]$LineTimeout = 2000,

        [Parameter(Mandatory = $false)]
        [ValidateRange(1, 65536)]
        [int]$MaxLineLength = 1024
    )

    if (-not $PSBoundParameters.ContainsKey('Data') -and
        -not $PSBoundParameters.ContainsKey('ReadCount') -and
        -not $ReadLine) {
        throw "At least one of -Data, -ReadCount, or -ReadLine must be specified."
    }

    $ownsDevice = $PSCmdlet.ParameterSetName -ne 'ByDevice'
    $ftdi       = $null

    try {
        # --- open device ---
        if ($PSCmdlet.ParameterSetName -eq 'ByDevice') {
            $ftdi = $PsGadget
            if (-not $ftdi -or -not $ftdi.IsOpen) {
                throw "PsGadgetFtdi object is not open."
            }
        } elseif ($PSCmdlet.ParameterSetName -eq 'BySerial') {
            $ftdi = New-PsGadgetFtdi -SerialNumber $SerialNumber
        } else {
            $ftdi = New-PsGadgetFtdi -Index $Index
        }

        if (-not $ftdi -or -not $ftdi.IsOpen) {
            throw "Failed to open FTDI device"
        }

        # --- get or create UART instance ---
        $uart = $ftdi.GetUart($BaudRate, $DataBits, $StopBits, $Parity, $FlowControl,
                              [uint32]$ReadTimeout, [uint32]$WriteTimeout)
        if (-not $uart.IsInitialized) {
            throw "UART initialization failed"
        }

        # --- write phase ---
        $hasData = $PSBoundParameters.ContainsKey('Data')
        if ($hasData) {
            if ($Data -is [string]) {
                if (-not $uart.Write([string]$Data)) {
                    throw "UART write failed"
                }
            } elseif ($Data -is [byte[]]) {
                if (-not $uart.Write([byte[]]$Data)) {
                    throw "UART write failed"
                }
            } else {
                # Try converting to byte array
                [byte[]]$byteData = $Data
                if (-not $uart.Write($byteData)) {
                    throw "UART write failed"
                }
            }
        }

        # --- read phase ---
        $hasReadCount = $PSBoundParameters.ContainsKey('ReadCount')

        if ($hasReadCount) {
            return $uart.Read($ReadCount)
        } elseif ($ReadLine) {
            return $uart.ReadLine($MaxLineLength, $LineTimeout)
        } elseif ($hasData) {
            # write-only: return success bool already confirmed above
            return $true
        }

    } finally {
        if ($ownsDevice -and $ftdi -and $ftdi.IsOpen) {
            $ftdi.Close()
        }
    }
}