Asynchronous Event Driven PowerShell Serial Communication

Asynchronous Event Driven PowerShell Serial Communication

Just in the past few months, I’ve learned that, like other scripting languages, PowerShell supports using Classes, which opened up a new world for me on what is possible.

Did you know that PowerShell can also communicate via the Serial (COM) port? Of course, you knew that, because you’re here trying to understand why asynchronous event-driven, serial communication is not working for you, and you may also be using Classes so you’re trying to understand how you can do something with the received data if you have been able to get event-driven communication working. The problem is that most code examples you’re finding regarding using PowerShell with serial communication are time-based, and so you’re missing data. I’m about to tell you exactly how to do properly capture every bit of serial communication.

What I’ve done is create a Serial class. This class handles everything needed to setup a serial port, and it’s reusable so I can include it in a different script that needs to access a serial port. Here’s my class:

class PortProperties{

    [string] $PortName;

    [int] $BaudRate;

    [System.IO.Ports.Parity] $Parity;

    [int] $DataBits;

    [System.IO.Ports.StopBits] $StopBits;

    [System.IO.Ports.Handshake] $Handshake;

    [string] $NewLine;
}

class SerialPort{

    [System.IO.Ports.SerialPort] $port = [System.IO.Ports.SerialPort]::new();

    [byte[]]$readBuffer = [byte[]]::new(21);

    $eventJob;

    SerialPort([PortProperties] $props){

        try{

            $this.port.PortName = [string] $props.PortName;

            $this.port.BaudRate = [int] $props.BaudRate;

            $this.port.Parity = [System.IO.Ports.Parity] $props.Parity;

            $this.port.DataBits = [int] $props.DataBits;

            $this.port.StopBits = [System.IO.Ports.StopBits] $props.StopBits;

            $this.port.Handshake = [System.IO.Ports.Handshake] $props.Handshake;

            $this.port.NewLine = [string] $props.NewLine;

            $this.port.DTREnable = $true;

        }catch{  }
    }

    start(){

        $this.openPort();
    }

    stop(){

        $this.closePort();

        Get-EventSubscriber | % {

            Unregister-Event -SubscriptionId $_.SubscriptionId;
        }
    }

    openPort(){

        try{

            Write-Debug "Opening port";

            $this.port.Open();

            Start-Sleep -Milliseconds 1000;

            $this.port.DiscardInBuffer();

            $this.port.DiscardOutBuffer();

        }catch{
            
            Write-Debug "Count not open port SerialPort:openPort()";

            Write-Debug $_.ScriptStackTrace
        }
    }

    closePort(){

        Write-Debug "Closing port";

        $this.port.Close();

        Start-Sleep -Milliseconds 1000;

        $this.port.Dispose();
    }

    [bool] checkOpen(){
        
        if($this.port.IsOpen){

            return $true;

        }else{

            return $false;
        }
    }
    
    sendData([byte[]] $data){

 
        if($this.checkOpen()){

            if($data -eq $null){

                return;
            }

            $this.port.Write(([byte[]] $data), 0, $data.Count);

            Start-Sleep -Milliseconds 20;

        }else{

            Write-Debug "Port is not open";
        }
    }

    initEventSub(){
        
        $act = {

            if($serial){
     
                if($Sender.BytesToRead){

                    $readBuffer = New-Object byte[] ($Sender.BytesToRead);

                    [int] $count = $Sender.Read($readBuffer, 0, $Sender.BytesToRead);
                
                    $serial.processData($readBuffer);
                }  

            }else{
            
                Write-Debug "No object exists to handle the data. You must create an object in global scope so the method can be accessed";
            }        
        }

        $this.eventJob = Register-ObjectEvent -InputObject $this.port.port -EventName "DataReceived" -Action $act -MessageData {$serial = $this};
    }

    processData([byte[]] $bytedata){

        [string] $data = [System.Text.Encoding]::ASCII.GetString($bytedata, 0, $bytedata.Count);

        Write-Host "Received Data [String]: $($data)";
    }

    Dispose(){

        $this.port.Close();
    }
}

First, PowerShell is not as advanced as some other scripting languages, like PHP, so there’s no such thing as a Struct or an Interface; in PowerShell, a class can be used as a Struct, which I’ve done for the PortProperties. PortProperties are the essential needs to be able to communicate with a serial port–may be except for the NewLine. Note: It is best to type the PortProperties properly, or you’ll get errors from the driver when, for example, you put “9600” for the baud, when it needs to be 9600. Pay attention!

I’m not going to go over everything the script does because that’s not the focus of this post.

If PowerShell supported Interfaces, we’d make initEventSub and processData required overwriteable methods that are implemented from the importing script. In other words, the script that is importing the serial class needs to have a method called initEventSub, and another method called processData. You should copy the content within these two methods into that parent script.

What initEventSub does is it sets up the action script to perform when then “data received” event is triggered. Because the event watcher is running on a different thread, the data received by this action script are not accessible to the class directly, unless the class is defined in the global scope. For example, here is the template I start with when I want to create a class that uses a serial port:

Using module ".\serial.class.psm1";

class SerialTest{
    
    [PortProperties] $comprops;

    start(){

        $this.openPort();

        $this.initEventSub();
    }

    stop(){

        $this.closePort();

        Get-EventSubscriber | % {

            Unregister-Event -SubscriptionId $_.SubscriptionId;
        }
    }

    openPort(){

        $this.comprops = [PortProperties]::new();

        $this.comprops.BaudRate = 115200;

        $this.comprops.DataBits = 8;

        $this.comprops.Handshake = [System.IO.Ports.Handshake]::None;

        $this.comprops.NewLine = "\n";

        $this.comprops.Parity = [System.IO.Ports.Parity]::None;

        $this.comprops.PortName = "COM6";

        $this.comprops.StopBits = [System.IO.Ports.StopBits]::One;

        #Write-Debug ($this.comprops | ConvertTo-Json);

        $this.port = [SerialPort]::new($this.comprops);

        $this.port.start();

    }

    closePort(){
        
        if(($this.port -ne $null) -and ($this.port.checkOpen())){

            $this.port.stop();
        }
    }

    sendData([byte[]] $data){

        Start-Sleep -Milliseconds 100;

        if($this.port -and ($this.port.checkOpen())){

            $this.port.sendData($data);

        }
    }

    initEventSub(){
        
        $act = {

            if($serialtest){
     
                if($Sender.BytesToRead){

                    $readBuffer = New-Object byte[] ($Sender.BytesToRead);

                    #Write-Debug "BytesToRead: $($Sender.BytesToRead)";
                
                    [int] $count = $Sender.Read($readBuffer, 0, $Sender.BytesToRead);
                
                    $serialtest.processData($readBuffer);
                }  
            }else{
            
                Write-Debug "No object exists to handle the data. You must create an object in global scope so the method can be accessed";
            }        
        }

        $this.eventJob = Register-ObjectEvent -InputObject $this.port.port -EventName "DataReceived" -Action $act -MessageData {$serialtest = $this};

    }

    processData([byte[]] $bytedata){

        [string] $data = [System.Text.Encoding]::ASCII.GetString($bytedata, 0, $bytedata.Count);

        Write-Host "Received Data [String]: $($data)";
    }
}

$serialtest = [SerialTest]::new();

Once I’ve filled out the other methods that will generate and parse the serial data, I instantiate a new instance of the class at the bottom of the class. So, in our example, the very last line of the code is where I instantiate the class. Since this instantiation is in the global scope, I can feed it to the event handler as a variable so that the received data is now accessible within the class.

To summarize, the magic is both in instantiating in the global scope and then this line which sets up the event handler:

$this.eventJob = Register-ObjectEvent -InputObject $this.port.port -EventName "DataReceived" -Action $act -MessageData {$serialtest = $this};

Why did I set -MessageData like that? Because nothing else worked. If I used {$serialtest}, it doesn’t work. If I used just $serialtest, it doesn’t work. The only thing that works is when I use what I’ve used above. Don’t ask why, because I don’t know. I can only chalk it up to another PowerShell nuance.

Do you see that within the $act script, we can call the processData method on the $serialtest object? It works! And when in the processData method, $this also works.

Do be careful though; if you’re using other events in your script then calling $serialtest.stop() will remove them. For myself, I keep things simple and only use event-driven functionality in PowerShell when it is necessary, so for me, removing all events when closing the port is fine.

Leave a Reply

Your email address will not be published. Required fields are marked *