Private/Ftdi.Uart.ps1

#Requires -Version 5.1
# Ftdi.Uart.ps1
# D2XX UART backend for FTDI FT232R, FT232H, and compatible devices.
#
# All FTDI chips support UART (serial) as their default mode (SetBitMode 0x00).
# This backend configures baud rate, data format, and flow control via the D2XX
# API, then provides Write/Read/ReadLine helpers with [PROTO] tracing.
#
# Parity map: None=0 Odd=1 Even=2 Mark=3 Space=4
# Stop bits map: 1 stop bit=0 2 stop bits=2 (FTD2XX_NET byte values)
# Flow control: None=0x0000 RtsCts=0x0100 DtrDsr=0x0200 XonXoff=0x0400
#
# Wire guide (FT232R as USB-UART adapter):
# TX (pin 1 / ADBUS0) -> RX of target device
# RX (pin 5 / ADBUS1) <- TX of target device
# GND -> common ground

function Initialize-FtdiUart {
    <#
    .SYNOPSIS
    Configures an FTDI device for UART (serial) communication via D2XX.
 
    .PARAMETER DeviceHandle
    Open connection object from Connect-PsGadgetFtdi / PsGadgetFtdi._connection.
 
    .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
    Read timeout in milliseconds. 0 = non-blocking, 4294967295 = infinite. Default 500.
 
    .PARAMETER WriteTimeout
    Write timeout in milliseconds. Default 500.
    #>

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

        [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, 4294967295)]
        [uint32]$ReadTimeout = 500,

        [Parameter(Mandatory = $false)]
        [ValidateRange(0, 4294967295)]
        [uint32]$WriteTimeout = 500
    )

    try {
        if (-not $DeviceHandle -or -not $DeviceHandle.IsOpen) {
            throw "Device handle is invalid or not open"
        }

        $rawDevice = Get-FtdiD2xxHandle -DeviceHandle $DeviceHandle
        $isReal    = $script:FtdiInitialized -and ($null -ne $rawDevice)

        # D2XX numeric values
        $parityMap = @{ None=0; Odd=1; Even=2; Mark=3; Space=4 }
        $stopMap   = @{ 1=0; 2=2 }   # FT_STOP_BITS_1=0, FT_STOP_BITS_2=2
        $flowMap   = @{ None=0x0000; RtsCts=0x0100; DtrDsr=0x0200; XonXoff=0x0400 }

        $parityByte   = [byte]$parityMap[$Parity]
        $stopByte     = [byte]$stopMap[$StopBits]
        $flowWord     = [uint16]$flowMap[$FlowControl]

        # Human-readable format string e.g. "8N1", "7E2"
        $parityChar = @{ None='N'; Odd='O'; Even='E'; Mark='M'; Space='S' }[$Parity]
        $formatStr  = "${DataBits}${parityChar}${StopBits}"

        if ($isReal) {
            $ftdi_ok = [FTD2XX_NET.FTDI+FT_STATUS]::FT_OK

            $st = $rawDevice.SetBaudRate([uint32]$BaudRate)
            if ($st -ne $ftdi_ok) { throw "SetBaudRate failed: status=$st" }

            $st = $rawDevice.SetDataCharacteristics([byte]$DataBits, $stopByte, $parityByte)
            if ($st -ne $ftdi_ok) { throw "SetDataCharacteristics failed: status=$st" }

            $st = $rawDevice.SetFlowControl($flowWord, [byte]0x11, [byte]0x13)
            if ($st -ne $ftdi_ok) { throw "SetFlowControl failed: status=$st" }

            $st = $rawDevice.SetTimeouts($ReadTimeout, $WriteTimeout)
            if ($st -ne $ftdi_ok) { throw "SetTimeouts failed: status=$st" }

            $rawDevice.Purge(3) | Out-Null   # purge RX+TX

            $script:PsGadgetLogger.WriteProto('UART.INIT',
                "baud=${BaudRate} ${formatStr} flow=${FlowControl} rto=${ReadTimeout}ms wto=${WriteTimeout}ms")
            Write-Verbose "UART initialized: ${BaudRate} ${formatStr} flow=${FlowControl}"
        } else {
            $script:PsGadgetLogger.WriteProto('UART.INIT',
                "baud=${BaudRate} ${formatStr} flow=${FlowControl} (STUB)")
            Write-Verbose "UART initialized (STUB MODE)"
        }

        # Cache UART config on connection object
        $DeviceHandle | Add-Member -MemberType NoteProperty -Name 'UartBaudRate'    -Value $BaudRate     -Force
        $DeviceHandle | Add-Member -MemberType NoteProperty -Name 'UartDataBits'    -Value $DataBits     -Force
        $DeviceHandle | Add-Member -MemberType NoteProperty -Name 'UartStopBits'    -Value $StopBits     -Force
        $DeviceHandle | Add-Member -MemberType NoteProperty -Name 'UartParity'      -Value $Parity       -Force
        $DeviceHandle | Add-Member -MemberType NoteProperty -Name 'UartFlowControl' -Value $FlowControl  -Force
        $DeviceHandle | Add-Member -MemberType NoteProperty -Name 'UartReadTimeout' -Value $ReadTimeout  -Force
        $DeviceHandle | Add-Member -MemberType NoteProperty -Name 'UartWriteTimeout'-Value $WriteTimeout -Force
        return $true

    } catch {
        Write-Error "Failed to initialize UART: $_"
        return $false
    }
}

function Invoke-FtdiUartWrite {
    <#
    .SYNOPSIS
    Writes bytes to the UART TX line via D2XX.
    #>

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

    try {
        if (-not $DeviceHandle -or -not $DeviceHandle.IsOpen) {
            throw "Device handle is invalid or not open"
        }
        if ($Data.Length -eq 0) { return $true }

        $rawDevice = Get-FtdiD2xxHandle -DeviceHandle $DeviceHandle
        $isReal    = $script:FtdiInitialized -and ($null -ne $rawDevice)

        $hexStr = $script:PsGadgetLogger.FormatHex($Data)

        # Attempt ASCII decode for summary (printable chars only)
        $summary = "$($Data.Length)B"
        $ascii = [System.Text.Encoding]::ASCII.GetString($Data)
        $isPrintable = ($ascii -cmatch '^[\x20-\x7E\r\n\t]+$')
        if ($isPrintable) {
            $escaped = $ascii -replace '\r','\\r' -replace '\n','\\n' -replace '\t','\\t'
            $summary += " `"$escaped`""
        }

        if ($isReal) {
            [uint32]$bw = 0
            $st = $rawDevice.Write($Data, [uint32]$Data.Length, [ref]$bw)
            $ftdi_ok = [FTD2XX_NET.FTDI+FT_STATUS]::FT_OK
            if ($st -ne $ftdi_ok) { throw "UART write failed: D2XX status=$st" }
            $script:PsGadgetLogger.WriteProto('UART.TX', $summary, $hexStr)
        } else {
            $script:PsGadgetLogger.WriteProto('UART.TX', "$summary (STUB)", $hexStr)
        }
        return $true

    } catch {
        Write-Error "UART write failed: $_"
        return $false
    }
}

function Invoke-FtdiUartRead {
    <#
    .SYNOPSIS
    Reads up to Count bytes from the UART RX line via D2XX.
    Waits up to the configured read timeout for data to arrive.
    #>

    [CmdletBinding()]
    [OutputType([byte[]])]
    param(
        [Parameter(Mandatory = $true)]  [System.Object]$DeviceHandle,
        [Parameter(Mandatory = $true)]  [ValidateRange(1, 65536)] [int]$Count
    )

    try {
        if (-not $DeviceHandle -or -not $DeviceHandle.IsOpen) {
            throw "Device handle is invalid or not open"
        }

        $rawDevice = Get-FtdiD2xxHandle -DeviceHandle $DeviceHandle
        $isReal    = $script:FtdiInitialized -and ($null -ne $rawDevice)

        if ($isReal) {
            [byte[]]$rxBuf = [byte[]]::new($Count)
            [uint32]$br    = 0
            $st = $rawDevice.Read($rxBuf, [uint32]$Count, [ref]$br)
            $ftdi_ok = [FTD2XX_NET.FTDI+FT_STATUS]::FT_OK
            if ($st -ne $ftdi_ok) { throw "UART read failed: D2XX status=$st" }

            if ($br -lt [uint32]$Count) {
                $trimmed = [byte[]]::new($br)
                if ($br -gt 0) { [Array]::Copy($rxBuf, $trimmed, [int]$br) }
                $rxBuf = $trimmed
            }

            $summary = "$br/${Count}B"
            if ($br -gt 0) {
                $hexStr = $script:PsGadgetLogger.FormatHex($rxBuf)
                $ascii = [System.Text.Encoding]::ASCII.GetString($rxBuf)
                $isPrintable = ($ascii -cmatch '^[\x20-\x7E\r\n\t]+$')
                if ($isPrintable) {
                    $escaped = $ascii -replace '\r','\\r' -replace '\n','\\n' -replace '\t','\\t'
                    $summary += " `"$escaped`""
                }
                $script:PsGadgetLogger.WriteProto('UART.RX', $summary, $hexStr)
            } else {
                $script:PsGadgetLogger.WriteProto('UART.RX', "$summary (timeout/empty)")
            }
            return $rxBuf
        } else {
            $script:PsGadgetLogger.WriteProto('UART.RX', "${Count}B (STUB)")
            return [byte[]]::new($Count)
        }

    } catch {
        Write-Error "UART read failed: $_"
        return [byte[]]@()
    }
}

function Invoke-FtdiUartReadLine {
    <#
    .SYNOPSIS
    Reads bytes from UART until a newline (\n) is received or timeout elapses.
    Returns the line as a string (decoded UTF-8, newline stripped).
 
    .PARAMETER DeviceHandle
    Open connection object.
 
    .PARAMETER MaxLength
    Maximum line length in bytes before aborting. Default 1024.
 
    .PARAMETER TimeoutMs
    Maximum milliseconds to wait for a complete line. Default 2000.
    #>

    [CmdletBinding()]
    [OutputType([object])]
    param(
        [Parameter(Mandatory = $true)]  [System.Object]$DeviceHandle,
        [Parameter(Mandatory = $false)] [ValidateRange(1, 65536)] [int]$MaxLength  = 1024,
        [Parameter(Mandatory = $false)] [ValidateRange(0, 60000)] [int]$TimeoutMs  = 2000
    )

    try {
        if (-not $DeviceHandle -or -not $DeviceHandle.IsOpen) {
            throw "Device handle is invalid or not open"
        }

        $rawDevice = Get-FtdiD2xxHandle -DeviceHandle $DeviceHandle
        $isReal    = $script:FtdiInitialized -and ($null -ne $rawDevice)

        if ($isReal) {
            $ftdi_ok  = [FTD2XX_NET.FTDI+FT_STATUS]::FT_OK
            $deadline = [System.Diagnostics.Stopwatch]::StartNew()
            $lineBytes = [System.Collections.Generic.List[byte]]::new()
            [byte[]]$oneByte = [byte[]]::new(1)
            [bool]$gotNewline = $false

            while ($deadline.ElapsedMilliseconds -lt $TimeoutMs -and $lineBytes.Count -lt $MaxLength) {
                [uint32]$available = 0
                $rawDevice.GetRxBytesAvailable([ref]$available) | Out-Null
                if ($available -eq 0) {
                    Start-Sleep -Milliseconds 5
                    continue
                }
                [uint32]$br = 0
                $st = $rawDevice.Read($oneByte, 1, [ref]$br)
                if ($st -ne $ftdi_ok -or $br -eq 0) { break }
                $b = $oneByte[0]
                if ($b -eq 0x0A) { $gotNewline = $true; break }   # newline -- end of line
                if ($b -ne 0x0D) {            # strip \r, keep everything else
                    $lineBytes.Add($b)
                }
            }

            $deadline.Stop()
            [byte[]]$lineArr = $lineBytes.ToArray()
            $elapsed = $deadline.ElapsedMilliseconds

            if (-not $gotNewline) {
                # Timed out without receiving a newline. Return $null so callers can
                # distinguish timeout from a device that sent an empty line (bare \n).
                $script:PsGadgetLogger.WriteProto('UART.RX', "ReadLine timeout=${elapsed}ms (no newline received)")
                return $null
            }

            $line = [System.Text.Encoding]::UTF8.GetString($lineArr)
            $hexStr = $script:PsGadgetLogger.FormatHex($lineArr)
            $script:PsGadgetLogger.WriteProto('UART.RX',
                "$($lineArr.Length)B line=${elapsed}ms `"$($line -replace '"','\"')`"",
                $hexStr)
            return $line
        } else {
            $script:PsGadgetLogger.WriteProto('UART.RX', "ReadLine (STUB)")
            return $null
        }

    } catch {
        Write-Error "UART ReadLine failed: $_"
        return $null
    }
}

function Get-FtdiUartBytesAvailable {
    <#
    .SYNOPSIS
    Returns the number of bytes waiting in the UART RX buffer.
    #>

    [CmdletBinding()]
    [OutputType([uint32])]
    param(
        [Parameter(Mandatory = $true)] [System.Object]$DeviceHandle
    )

    try {
        $rawDevice = Get-FtdiD2xxHandle -DeviceHandle $DeviceHandle
        if (-not $rawDevice) { return [uint32]0 }

        [uint32]$available = 0
        $rawDevice.GetRxBytesAvailable([ref]$available) | Out-Null
        return $available
    } catch {
        return [uint32]0
    }
}