<# .SYNOPSIS 实时显示并保存设备串口数据。 .DESCRIPTION 使用 .NET SerialPort 直接独占打开串口。收到的数据会原样写入二进制文件, 同时解码后实时显示到当前 PowerShell,并按行写入带时间戳的文本日志。 默认只读取数据,不向设备发送指令。DtrEnable 和 RtsEnable 会改变硬件控制线, 只有用户明确要求时才能启用。 .EXAMPLE .\serial_monitor.ps1 -Port COMx -LogPath F:\tmp\embedded-app-pc-debug\task\logs\serial.log .EXAMPLE .\serial_monitor.ps1 -Port COMx -LogPath .\serial.log -DurationSeconds 60 .PARAMETER DtrEnable 改变 DTR 硬件控制线;仅在用户明确要求时使用。 .PARAMETER RtsEnable 改变 RTS 硬件控制线;仅在用户明确要求时使用。 #> [CmdletBinding()] param( [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [string]$Port, [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [string]$LogPath, [ValidateRange(1, 4000000)] [int]$BaudRate = 115200, [ValidateRange(5, 8)] [int]$DataBits = 8, [ValidateSet('None', 'Odd', 'Even', 'Mark', 'Space')] [string]$Parity = 'None', [ValidateSet('One', 'OnePointFive', 'Two')] [string]$StopBits = 'One', [ValidateSet('None', 'XOnXOff', 'RequestToSend', 'RequestToSendXOnXOff')] [string]$Handshake = 'None', [string]$EncodingName = 'utf-8', [ValidateRange(0, 2147483647)] [int]$DurationSeconds = 0, [ValidateRange(1, 5000)] [int]$PollMilliseconds = 100, [ValidateRange(64, 1048576)] [int]$BufferSize = 4096, [string]$RawPath, [switch]$DtrEnable, [switch]$RtsEnable ) $ErrorActionPreference = 'Stop' $serialPort = $null $textWriter = $null $rawStream = $null $pendingLine = '' function Write-TimestampedLines { param( [Parameter(Mandatory = $true)] [string]$Text, [Parameter(Mandatory = $true)] [System.IO.StreamWriter]$Writer ) $script:pendingLine += $Text while ($true) { $newlineIndex = $script:pendingLine.IndexOf("`n", [System.StringComparison]::Ordinal) if ($newlineIndex -lt 0) { break } $line = $script:pendingLine.Substring(0, $newlineIndex).TrimEnd("`r") $script:pendingLine = $script:pendingLine.Substring($newlineIndex + 1) $Writer.WriteLine(('[{0}] {1}' -f (Get-Date -Format 'yyyy-MM-dd HH:mm:ss.fff'), $line)) } } try { $resolvedLogPath = [System.IO.Path]::GetFullPath($LogPath) if ([string]::IsNullOrWhiteSpace($RawPath)) { $resolvedRawPath = $resolvedLogPath + '.raw.bin' } else { $resolvedRawPath = [System.IO.Path]::GetFullPath($RawPath) } $logDirectory = [System.IO.Path]::GetDirectoryName($resolvedLogPath) $rawDirectory = [System.IO.Path]::GetDirectoryName($resolvedRawPath) if (-not [string]::IsNullOrWhiteSpace($logDirectory)) { [System.IO.Directory]::CreateDirectory($logDirectory) | Out-Null } if (-not [string]::IsNullOrWhiteSpace($rawDirectory)) { [System.IO.Directory]::CreateDirectory($rawDirectory) | Out-Null } $utf8NoBom = New-Object System.Text.UTF8Encoding($false) $textFileStream = New-Object System.IO.FileStream( $resolvedLogPath, [System.IO.FileMode]::Append, [System.IO.FileAccess]::Write, [System.IO.FileShare]::Read ) $textWriter = New-Object System.IO.StreamWriter($textFileStream, $utf8NoBom) $textWriter.AutoFlush = $true $rawStream = New-Object System.IO.FileStream( $resolvedRawPath, [System.IO.FileMode]::Append, [System.IO.FileAccess]::Write, [System.IO.FileShare]::Read ) try { $encoding = [System.Text.Encoding]::GetEncoding($EncodingName) } catch { throw "无法识别编码 '$EncodingName':$($_.Exception.Message)" } $decoder = $encoding.GetDecoder() $serialPort = New-Object System.IO.Ports.SerialPort $serialPort.PortName = $Port $serialPort.BaudRate = $BaudRate $serialPort.DataBits = $DataBits $serialPort.Parity = [System.Enum]::Parse([System.IO.Ports.Parity], $Parity) $serialPort.StopBits = [System.Enum]::Parse([System.IO.Ports.StopBits], $StopBits) $serialPort.Handshake = [System.Enum]::Parse([System.IO.Ports.Handshake], $Handshake) $serialPort.DtrEnable = $DtrEnable.IsPresent $serialPort.RtsEnable = $RtsEnable.IsPresent $serialPort.ReadTimeout = $PollMilliseconds $serialPort.ReadBufferSize = [Math]::Max(4096, $BufferSize) try { $serialPort.Open() } catch { throw "无法打开串口 $Port。串口可能不存在或正被其他程序占用:$($_.Exception.Message)" } $startTime = Get-Date $startMessage = '[{0}] 已打开 {1},{2} {3}{4}{5},流控 {6};原始数据:{7}' -f ` (Get-Date -Format 'yyyy-MM-dd HH:mm:ss.fff'), $Port, $BaudRate, $DataBits, $Parity.Substring(0, 1), $(if ($StopBits -eq 'One') { '1' } elseif ($StopBits -eq 'Two') { '2' } else { '1.5' }), $Handshake, $resolvedRawPath [Console]::WriteLine($startMessage) $textWriter.WriteLine($startMessage) $byteBuffer = New-Object byte[] $BufferSize $charBuffer = New-Object char[] ($encoding.GetMaxCharCount($BufferSize)) while ($true) { if ($DurationSeconds -gt 0 -and ((Get-Date) - $startTime).TotalSeconds -ge $DurationSeconds) { break } try { $count = $serialPort.Read($byteBuffer, 0, $byteBuffer.Length) } catch [System.TimeoutException] { continue } if ($count -le 0) { continue } $rawStream.Write($byteBuffer, 0, $count) $rawStream.Flush() $charCount = $decoder.GetChars($byteBuffer, 0, $count, $charBuffer, 0, $false) if ($charCount -gt 0) { $text = New-Object string($charBuffer, 0, $charCount) [Console]::Write($text) Write-TimestampedLines -Text $text -Writer $textWriter } } } catch { # 使用字符串连接保留底层异常文本,避免异常内容再次参与格式化。 $errorMessage = '[' + (Get-Date -Format 'yyyy-MM-dd HH:mm:ss.fff') + '] 串口监测失败:' + [string]$_.Exception.Message [Console]::Error.WriteLine($errorMessage) if ($null -ne $textWriter) { $textWriter.WriteLine($errorMessage) } exit 1 } finally { if ($null -ne $textWriter -and -not [string]::IsNullOrEmpty($script:pendingLine)) { $textWriter.WriteLine(('[{0}] {1}' -f (Get-Date -Format 'yyyy-MM-dd HH:mm:ss.fff'), $script:pendingLine.TrimEnd("`r"))) } if ($null -ne $serialPort) { if ($serialPort.IsOpen) { $serialPort.Close() } $serialPort.Dispose() } if ($null -ne $rawStream) { $rawStream.Dispose() } if ($null -ne $textWriter) { $textWriter.WriteLine('[{0}] 串口监测结束' -f (Get-Date -Format 'yyyy-MM-dd HH:mm:ss.fff')) $textWriter.Dispose() } }