如何使用PowerShell读写USB串行COM端口?

huangapple go评论73阅读模式
英文:

How to read/write to a USB Serial COM port using powershell?

问题

我现在已经在互联网上搜索过了,也尝试了无数的脚本和解决方案。也就是说,大多数解决方案都是关于单向读取,很少涉及写入和读取结合。

我已经编写了一个基于所有这些小部分的PowerShell脚本。它通过运行以下的pwsh.exe命令来运行,该命令启动另一个pwsh shell(最新的7.3.4),然后在那里运行脚本。

已连接设备是基于USB的3G/4G LTE数据调制解调器。
当连接到PS时,它报告Ports PNPClass中的以下3个COM端口。

这3个对应于modem+diag+at_mbim USB模式,可以通过AT+USBMODE?找到。因此,我可以使用COM 9和11。

问题陈述:

  1. 脚本成功将数据发送到串行COM端口。
  2. 但我无法读取响应,即使我知道它在那里。
  3. 我知道它在那里,因为如果在它发送AT命令(ATI)后杀死脚本,或者让它超时。如果我使用任何常见的终端模拟器程序连接到相同的端口,如putty, screen, picocom等,我可以看到发送的数据和结果(见屏幕截图)。
  4. 非常偶尔会显示一些OK,但目前似乎是随机的。

屏幕截图是从脚本发送的命令的结果中缓冲的数据,但脚本没有接收到这些数据。

脚本的问题是,它无法确保从发送的命令中获取所有结果。

英文:

I have now looked all over the internet and now tried countless scripts and solutions. That said, it seem most solutions is about one-way reading, and rarely writing and reading combined.

I have put together a powershell script, based on all these bits & pieces. It works by running the following pwsh.exe command that fires up another pwsh shell (latest 7.3.4) and running the script from there.

Start-Process pwsh.exe -ArgumentList '-noprofile -noexit -c "./mecom.ps1"' -PassThru  | Out-Null

Connected Device

The connected device in question is a USB based 3G/4G LTE data modem.
When connected to the PS it reports the following 3 COM ports in the Ports PNPClass.

Alcatel 3G modem debug (COM11) - modem
Application1 Interface (COM10) - diag
Application2 Interface (COM9)  - AT_MBIM  

These 3 corresponds to the modem+diag+at_mbim USB mode, found with AT+USBMODE?.
So here I can use both COM 9 & 11.

Problem Statement:

  1. The script successfully sends the data to the serial COM port.
  2. But I am not able to read the response, even if I know it's there.
  3. I know it's there, because if I kill the script after it has sent the at command (ATI), or just let it time out. I can see the data and results sent, if I connect to the same port using any common terminal emulator program, such as putty, screen, picocom, etc. (See screenshot.)
  4. Very occasionally I get a few OKs shown, but seem random at the moment.

The Screenshot

Buffer data with results from commands sent from script, but not received by script.

如何使用PowerShell读写USB串行COM端口?


The Script

#!/usr/bin/env pwsh
# 
#------------------------------------------------------------------------------
# Run with
#	Start-Process pwsh.exe -ArgumentList '-noprofile -noexit -c "./mecom.ps1"' -PassThru  | Out-Null
#------------------------------------------------------------------------------
$HL = '-'*50

# Register Exit event & Hit [CTRL+C] to exit shell
Register-EngineEvent PowerShell.Exiting -SupportEvent Action { 
    [console]::Beep(500,500); Write-Host "`nExiting...`n" -fore Magenta; sleep(1); Read-Host -Prompt "Hit Return 2 Exit";
}
# Hit [CTRL+D] to exit shell, after script is done.
try { Set-PSReadlineKeyHandler -Key ctrl+d -Function ViExit } catch {}

#------------------------------------------------------------------------------
# Helper Functions
#------------------------------------------------------------------------------
function setTerminalUI {
	# Setting the PowerShell UI Terminal Size
	# NOTE:  Must have: BufferWidth = WindowWidth

	[console]::Title = "MeCom Terminal (${PID})"
	[console]::BufferHeight=9001
	[console]::BufferWidth=140
	[console]::WindowHeight=50
	[console]::WindowWidth=140
}

function startUp {
	Write-Host "`n"
	Write-Host -Fo DarkGray "Starting " -NoN
	Write-Host -Fo Magenta "MeCom Terminal"
	Write-Host -Fo DarkGray "PID: $PID"
	Write-Host -Fo DarkGray "Use [CTRL+C] to quit."
}


function showPort($port){
	Write-Host -Fo DarkGray $HL
	$port | Out-Host
	Write-Host -Fo DarkGray $HL
}

function showPortInfo {
	# Extract the COM port number into a list

	$cports = [System.IO.Ports.SerialPort]::GetPortNames()	# is a string array .GetType()
	$comList = @() 
	foreach ($i in $cports) {
		$i -Match "COM(\d{1,2})" | Out-Null
		$comList += [int]$Matches[1]
	}

	Write-Host "`nAvailable Ports:"
	Write-Host $HL
	Write-Host -Fo DarkYellow "$cports"
	Write-Host $HL

	$mydevs = (Get-PnPDevice | Where-Object{$_.PNPClass -in  "WPD","AndroidUsbDeviceClass","Modem","Ports" } | 
		Where-Object{$_.Present -in "True"} | 
		Select-Object Name,Description,Manufacturer,PNPClass,Service,Present,Status,DeviceID | 
		Sort-Object Name)

	#$allDevs = (
	$mydevs | Format-Table Description, Manufacturer, PNPClass, Service,
		@{Label="COM port"; Expression={ ($_.Name -Match "\((COM\d{1,2})\)" | Out-Null && $Matches[1]) }},
		@{Label="VID:PID"; Expression={ ($_.DeviceID -Match "USB\\VID_([0-9a-fA-F]{4})\&PID_([0-9a-fA-F]{4})" | Out-Null && ('{0}{1}{2}' -f ${Matches}[1], ":", ${Matches}[2]).ToLower() ) }},
		Present, Status
	#	)

	#Write-Host $allDevs 

	return $comList #| Out-Null -PassThru
}

function getComPort ($cList) {
	# Select a COM port
	
	Write-Host $HL; Write-Host -Fo DarkGray "comList:`n ${cList}" ;	Write-Host $HL

	do {
		Write-Host -Fo DarkGreen 'Select a serial COM port number [default is 9]' -NoN
		$pNum = Read-Host -Prompt ' '
		if ( !($pNum -in $cList)) { Write-Host -Fo Red "ERROR:  No COM port with that number!" }
	} while ( !($pNum -match '^\d+$') -or ($pNum -notin $cList))
	$cNum = "COM${pNum}" #| Out-Null

	Write-Host -Fo DarkGray "`nReading from port  :  " -NoN
	Write-Host -Fo White "${cNum}"

	return $cNum
}


function ReadCom ($cNum) {
	#------------------------------------------------------------
	# Configuring the Serial Ports Connection
	#------------------------------------------------------------
	if (!$cNum) {Write-Host -Fo Red "[WARNING]  No cNum received, setting to default COM9."; $cNum ='COM9'}

	$port = New-Object System.IO.Ports.SerialPort "${cNum}",115200,None,8,one

	#------------------------------------------------------------
	$port.ReadTimeout  				= 10000		# 20 sec 	- 
	$port.WriteTimeout 				= 2000		#  5 sec 	- 
	$port.NewLine 					= "`r"		# \r 		-  
	$port.ReceivedBytesThreshold 	= 1			# 256
	#------------------------------------------------------------

	Start-Sleep -M 500
	Write-Host -Fo DarkGray "Opening connection..."
	try {
		$port.Open()
	} catch {
		Write-Host -Fo Red "`nERROR:  Failed to Open serial port!"
		Write-Error "ERROR:  ${Error}"
		Write-Host -Fo Yellow "`nQuitting!`n"
		Read-Host -Prompt "Hit any key to Exit"
		Break
	}
	
	Start-Sleep -m 1000
	showPort($port)
	#------------------------------------------------------------
    # Housekeeping (removing garbage from previous sessions...
	#------------------------------------------------------------
	#$port.DiscardInBuffer()
	#$port.DiscardOutBuffer()
	
	#------------------------------------------------------------
	# AT Command(s) to send
	#------------------------------------------------------------
	# NOTE:  
	#	Sending AT commands require them to use an "\r" as EOL. 
	# 	This can be done automatically if using $port.NewLine="`n"
	# 	You probably must use double quotes, otherwise you get the wrong character.
	#------------------------------------------------------------
	$ATC = 'ATI'

	Write-Host -Fo DarkGray "Sending AT Command :  " -NoN
	Write-Host -Fo White "$ATC"
	$port.WriteLine($ATC)

	Start-Sleep -m 1000
	showPort($port)
	Write-Host -Fo Gray "Attempting to use ReadLine..."

	do {
        $key = if ($host.UI.RawUI.KeyAvailable) { $host.UI.RawUI.ReadKey('NoEcho, IncludeKeyDown') }
		
		if ($port.IsOpen) {
			try {
				$data = $port.ReadLine()
				#$data = $port.ReadExisting()
			}
			catch [TimeoutException] {
				Write-Host -Fo Red "`nERROR:  TimeoutException in ReadLine"
				Write-Error "ERROR:  ${Error}"
				break
			}
			if (($data).Lengths -gt 0) {
				Write-Host -Fo DarkYellow "${data}`n" # -NoN | Out-Host
			}
			Start-Sleep -m 1000
		} else {
			Write-Host -Fo Yellow "[INFO] Port was Closed!"
			break
		}
	} until ($key.VirtualKeyCode -eq 81) # Repeat until a 'q' is pressed

	Write-Host -Fo DarkGray "`nClosing connection..." -NoN
	$port.Close()
	Write-Host -Fo Green "OK`n"
}

#------------------------------------------------------------------------------
#  MAIN
#------------------------------------------------------------------------------
setTerminalUI
startUp
$comList = showPortInfo
$comList

$cNum = getComPort($comList) 

ReadCom($cNum)
Read-Host -Prompt "Hit any key to Exit"

#------------------------------------------------------------------------------
#  END
#------------------------------------------------------------------------------


References:

1 system.io.ports.serialport
2 Writing and Reading info from Serial Ports
[3] Serial Port communication using PowerShell
[4] Reading Serial port data continuously in powershell script
[5] How to continuously read Serial COM port in
[6] SerialPort.DataReceived Event
[7] Problems writing AT command to internal modem with System.IO.Ports.SerialPort
[8] System.IO.Ports Namespace
[9] unlock-a-pinlocked-broadband-device-using-powershell-and-atcommands


Given the lack of working public solutions for this, I am starting to think it may be an issue with powershell itself!? Some people are loosely talking about making an Event Listener, but they always fail to provide any working powershell examples, and always refer back to the same crummy Microsoft C#/.NET web pages on the same topic. [1,2]

How can I fix the script to ensure to get all the results from the commands I send?

答案1

得分: 2

I discovered to my horror that:

  • 默认设置的 $port.Handshake 被设置为 None (0),而它应该被设置为 RequestToSend (2)。
  • 在我的 shell 中不断玩耍串口和变量后,某些东西被损坏,使得使用 $port.ReadLine()$port.ReadExisting() 用于接收数据在命令行中默默失败。
  • 此外,我在使用建议的改进时犯了个错,使用了 ($data).Length,使得 if() 语句始终默默忽略任何数据!

现在它运行得很好,还添加了一个 EventHandler

事件处理程序

在使用 [System.IO.Ports.SerialPort] 时,使用事件处理程序的最基本用法是这样的:

# 从命令行

# 确保在注册之前串口已打开!
$p.Open()

# 注册一个新的处理程序
Register-ObjectEvent -InputObject $p -EventName "DataReceived" -SourceIdentifier COM_EVENT_HAND -Action { $Sender.ReadExisting() | Out-Host }

# 获取有关您的事件处理程序的一些信息
Get-EventSubscriber -SourceIdentifier COM_EVENT_HAND

# 发送一些命令以进行查看
$p.Write("ATI`r")

# 注销(在更改之前)
Unregister-Event -SourceIdentifier "COM_EVENT_HAND"

最后注意一点,人们正在使用各种延迟(如 Start-Sleep -m 2000)。除非您使用非常古老的糟糕的调制解调器并且速度远低于 115200 bps,否则不需要这些延迟,甚至可能破坏正常行为。

祝您愉快! 💖

英文:

I discovered to my horror that:

  • That the default setting for $port.Handshake was set to None (0),
    when it have to be set to RequestToSend (2).
  • After continuously playing with ports and variables in my shell, something got corrupted, making the most basic usage of $port.ReadLine() and $port.ReadExisting() for receiving, silently fail from CLI.
  • In addition I made a typo from the suggested improvement by using ($data).Length, making the if() statement always silent any data!

Now it works beautifully, including an added EventHandler.

Event Handlers

The most basic usage of an event handler when using [System.IO.Ports.SerialPort], you need to use it like this:

# From CLI

# Make sure the port is open before registration!
$p.Open()

# Register the a new handler
Register-ObjectEvent -InputObject $p -EventName "DataReceived" -SourceIdentifier COM_EVENT_HAND -Action { $Sender.ReadExisting() | Out-Host }

# Get some info about your event handler
Get-EventSubscriber -SourceIdentifier COM_EVENT_HAND

# Send some command to be seen
$p.Write("ATI`r")

# Unregister (before changing it)
Unregister-Event -SourceIdentifier "COM_EVENT_HAND"

One last note. People are using all sorts of delays (like Start-Sleep -m 2000). Those are not needed, and may even corrupt normal behavior, unless you have some really old crummy modem and running on speeds much less than 115200 bps.

Enjoy! 💖

huangapple
  • 本文由 发表于 2023年5月15日 07:20:27
  • 转载请务必保留本文链接:https://go.coder-hub.com/76250070.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定