1

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.

enter image description here


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?

not2qubit
  • 14,531
  • 8
  • 95
  • 135
  • The variable $data is a string (not a boolean). So change From: if ($data) To : if ($data.Length -gt 0) – jdweng May 15 '23 at 08:53
  • @jdweng I don't think that's important, as `if ()` is always true, unless it's Null. – not2qubit May 15 '23 at 15:28
  • A string is not a Boolean. 'IF' requires a Boolean results. A Boolean uses an integer and is False if zero and True if one. Your values in most cases are not zero or one. – jdweng May 15 '23 at 15:47
  • Did you do any debugging and set breakpoints at the part where you expect to recieve a result? Did you try to run the problemetic code one statement at a time? I've done a lot of stuff with serial ports in Powershell (Arduino stuff) and I can both send and recieve data. – bluuf May 15 '23 at 16:40
  • Also: did you test the ReadByte() method on the serialport class to see if you get a single byte (or ReadExisting to read all)? According to the documentation of the dotnet serialport class ReadLine() reads until the Newline character (which you set to `r, but possibly isn't in the response) – bluuf May 15 '23 at 17:15
  • @bluuf Regarding using debugger on PowerShell, that is something I never tried. Either way, I don't think the debugger is necessary, as the key ingredients are just a few lines. I.e. I tried this at ad infinitum on the CLI. Like I said, **there is a correct response** from the modem, but I am not able to have it shown in powershell. Can this be a permission issue? It is weird that `$port` **occasionally** does reports a non-zero `BytesToRead` count. Yet neither `ReadLine`, not `ReadExisting` returns anything! Then if I open *screen, picocom* or Putty, on the same COM port, it just appears. – not2qubit May 15 '23 at 17:41
  • Testing something from another CLI application is not the same as running the lines of code in Powershell using the dotnet serialport class. I suggest to at least run the lines of code to create a serial port object, send a command and then inspect the serialport object (and try the read command). – bluuf May 15 '23 at 19:50
  • @bluuf I did that already 300 times, and keep on doing that as well. Believe me when I say I cannot receive any data. – not2qubit May 15 '23 at 20:44
  • [This](https://www.laserlance.com/asynchronous-event-driven-powershell-serial-communication/) is also a good reference for handling serial Events. – not2qubit May 16 '23 at 03:22

1 Answers1

2

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!

not2qubit
  • 14,531
  • 8
  • 95
  • 135