Classes/PsGadgetUart.ps1

#Requires -Version 5.1
# Classes/PsGadgetUart.ps1
# UART device handle returned by PsGadgetFtdi.GetUart().
# Wraps an FTDI connection for D2XX UART transactions and exposes
# Write(), Read(), ReadLine(), and Flush() methods backed by Ftdi.Uart.ps1.
#
# Obtain via:
# $uart = $dev.GetUart() # 9600 8N1, no flow
# $uart = $dev.GetUart(115200) # 115200 8N1, no flow
# $uart = $dev.GetUart(115200, 8, 1, 'None') # full control
#
# Typical usage:
# $uart = $dev.GetUart(9600)
# $uart.Write("AT`r`n") # string overload
# $line = $uart.ReadLine() # read until \n
# $bytes = $uart.Read(4) # read 4 raw bytes

class PsGadgetUart {
    [PsGadgetLogger]$Logger
    [System.Object]$FtdiDevice
    [int]$BaudRate
    [int]$DataBits
    [int]$StopBits
    [string]$Parity
    [string]$FlowControl
    [uint32]$ReadTimeout
    [uint32]$WriteTimeout
    [bool]$IsInitialized

    PsGadgetUart(
        [System.Object]$ftdiConnection,
        [int]$baudRate,
        [int]$dataBits,
        [int]$stopBits,
        [string]$parity,
        [string]$flowControl,
        [uint32]$readTimeout,
        [uint32]$writeTimeout
    ) {
        $this.Logger        = Get-PsGadgetModuleLogger
        $this.FtdiDevice    = $ftdiConnection
        $this.BaudRate      = $baudRate
        $this.DataBits      = $dataBits
        $this.StopBits      = $stopBits
        $this.Parity        = $parity
        $this.FlowControl   = $flowControl
        $this.ReadTimeout   = $readTimeout
        $this.WriteTimeout  = $writeTimeout
        $this.IsInitialized = $false

        $parityChar = @{ None='N'; Odd='O'; Even='E'; Mark='M'; Space='S' }[$parity]
        $this.Logger.WriteInfo(
            "PsGadgetUart created: baud=${baudRate} ${dataBits}${parityChar}${stopBits} flow=${flowControl}")
    }

    [bool] Initialize() {
        return $this.Initialize($false)
    }

    [bool] Initialize([bool]$force) {
        if ($this.IsInitialized -and -not $force) {
            $this.Logger.WriteInfo("UART already initialized")
            return $true
        }
        if (-not $this.FtdiDevice) {
            $this.Logger.WriteError("No FTDI device assigned to UART instance")
            return $false
        }
        $result = Initialize-FtdiUart `
            -DeviceHandle  $this.FtdiDevice   `
            -BaudRate      $this.BaudRate      `
            -DataBits      $this.DataBits      `
            -StopBits      $this.StopBits      `
            -Parity        $this.Parity        `
            -FlowControl   $this.FlowControl   `
            -ReadTimeout   $this.ReadTimeout   `
            -WriteTimeout  $this.WriteTimeout
        if ($result) {
            $this.IsInitialized = $true
            $this.Logger.WriteInfo("UART initialized: baud=$($this.BaudRate)")
        } else {
            $this.Logger.WriteError("UART initialization failed")
        }
        return $result
    }

    # Write raw bytes to UART TX.
    [bool] Write([byte[]]$data) {
        if (-not $this.IsInitialized) {
            $this.Logger.WriteError("UART not initialized. Call Initialize() first.")
            return $false
        }
        return (Invoke-FtdiUartWrite -DeviceHandle $this.FtdiDevice -Data $data)
    }

    # Write a string to UART TX (UTF-8 encoded). Line endings are the caller's responsibility.
    # Use "AT`r`n" to send CR+LF, or just "AT`n" for LF-only devices.
    [bool] Write([string]$text) {
        if (-not $this.IsInitialized) {
            $this.Logger.WriteError("UART not initialized. Call Initialize() first.")
            return $false
        }
        [byte[]]$encoded = [System.Text.Encoding]::UTF8.GetBytes($text)
        return (Invoke-FtdiUartWrite -DeviceHandle $this.FtdiDevice -Data $encoded)
    }

    # Read Count raw bytes from UART RX. Waits up to ReadTimeout milliseconds.
    [byte[]] Read([int]$count) {
        if (-not $this.IsInitialized) {
            $this.Logger.WriteError("UART not initialized. Call Initialize() first.")
            return [byte[]]@()
        }
        return (Invoke-FtdiUartRead -DeviceHandle $this.FtdiDevice -Count $count)
    }

    # Read bytes until a newline (\n) arrives or TimeoutMs elapses.
    # Returns the line as a UTF-8 string (\r stripped, \n not included).
    # Returns $null when no newline was received within the timeout (distinguishable
    # from a device that sent a bare \n, which returns "").
    [object] ReadLine() {
        return $this.ReadLine(1024, 2000)
    }

    [object] ReadLine([int]$maxLength) {
        return $this.ReadLine($maxLength, 2000)
    }

    [object] ReadLine([int]$maxLength, [int]$timeoutMs) {
        if (-not $this.IsInitialized) {
            $this.Logger.WriteError("UART not initialized. Call Initialize() first.")
            return $null
        }
        return (Invoke-FtdiUartReadLine `
            -DeviceHandle $this.FtdiDevice `
            -MaxLength    $maxLength       `
            -TimeoutMs    $timeoutMs)
    }

    # Return the number of bytes currently waiting in the RX buffer.
    [uint32] BytesAvailable() {
        if (-not $this.FtdiDevice) { return [uint32]0 }
        return (Get-FtdiUartBytesAvailable -DeviceHandle $this.FtdiDevice)
    }

    # Purge TX and RX buffers.
    [void] Flush() {
        if (-not $this.FtdiDevice) { return }
        $rawDevice = Get-FtdiD2xxHandle -DeviceHandle $this.FtdiDevice
        if ($rawDevice) {
            $rawDevice.Purge(3) | Out-Null
            $this.Logger.WriteProto('UART.FLUSH', 'TX+RX purged')
        }
    }
}