Private/Send-BMSMessage.ps1
Function Send-BMSMessage { [cmdletbinding()] Param($iO) begin { #trap errors, but try and close port always trap { Write-Error $error[0] $port.BaseStream.Dispose() break } #REGION private function definitions function Close-SerialPort { [cmdletbinding()] param() #Write-Progress -id 30 -Activity '[Serial]:[Close]' -Status "Closing Port" Write-Verbose ("[Serial]: Closing Port " + $port.PortName) #dispose of port properly (I imagine dropping a piece of soggy cardboard into a wastebasket...plop) $port.BaseStream.Dispose() #dispose of port Remove-Variable port -Scope Global } function Open-SerialPort { [cmdletbinding()] param() #Write-Progress -id 30 -Activity '[Serial]:[Init]' -Status "Initialising Port" #using System.IO.Ports.SerialPort.BaseStream method #https://www.sparxeng.com/blog/software/must-use-net-system-io-ports-serialport if (!$global:Port) { #create a new serial port object. Write-Verbose ("[Serial]: Created serial object") $global:Port = new-Object System.IO.Ports.SerialPort } else { Write-Verbose ("[Serial]: Existing serial port instance found. Resetting") $port.BaseStream.Dispose() Write-Verbose ("[Serial]: Waiting " + $BMSInstructionSet.Config.Session.SessionTimeout + " milliseconds for port restart") Start-Sleep -Milliseconds $BMSInstructionSet.Config.Session.SessionTimeout Remove-Variable port -Scope Global $global:Port = new-Object System.IO.Ports.SerialPort } #REGION Serial Setup #Set up serial port parameters. #the items in the metadata exactly match properties for a System.IO.Ports.SerialPort object $SerialConfigurables = $BMSInstructionSet.Config.Client.PSObject.Properties.Name #REGION serial setup try { ForEach ($item in $SerialConfigurables) { $port.$item = $BMSInstructionSet.Config.Client.$item Write-Verbose ("[Serial]: " + $item + " : " + $port.$item) } } catch { Throw "Couldn't set a System.IO.Ports.SerialPort configurable from configuration metadata" } #ENDREGION serial setup #Write-Progress -id 30 -Activity '[Serial]:[Open]' -Status "Opening Port" $r = 1 do { #open the port this session try { if (!$port.IsOpen) { #Write-Progress -id 30 -Activity '[Serial]:[Open]' -Status "Retry Port Open" -PercentComplete (($r / $BMSInstructionSet.Config.Session.Retries) * 100) #Lazyish open of port Write-Verbose ("[Serial]: Serial IsClosed(). Attempting to open.") Start-Sleep -Milliseconds ($BMSInstructionSet.Config.Session.SessionTimeout * $r) $port.Open() } if ($port.IsOpen) { Write-Verbose ("[Serial]: Waited [$r] tries to open serial port") #remove existind data on port if any $port.BaseStream.Flush() | Out-Null break } } catch { Write-Error $Error[0] $port.Close() write-warning "[Serial]: Couldn't open port, Retrying: $r" write-Verbose ($port | Format-Table | Out-String) $r++ } } until ($r -gt $BMSInstructionSet.Config.Session.Retries) if (!$port.IsOpen) { $ErrorString = ("Couldn't open serial port. Retried " + $r + " times.") Throw $ErrorString } } Function Send-SerialBytes { [cmdletbinding()] param($iOInstance) #internalize sendbytes $SendBytes = $iOInstance.ByteStreamSend.RawStream #Write the message on the line. Bon Voyage! Write-Verbose ("[SendByte]: [" + $SendBytes.count + "] bytes on [" + $port.PortName + "]") try { #using System.IO.Ports.SerialPort.BaseStream method #https://www.sparxeng.com/blog/software/must-use-net-system-io-ports-serialport $port.BaseStream.Write([byte[]] $SendBytes, 0, ($SendBytes.count)) Write-Verbose "[SendByte]: Sucessful TX of instruction" } catch { #catch the rest of the errors related to opening serial ports. Throw "Couldn't send bytes on serial port" } } Function Read-SerialBytes { [cmdletbinding()] # $baz = Send-BMSMessage (Build-BMSMessage (Assert-BMSMessage -Command rint -verbose) -Verbose) -verbose #initalize message part index $WatchDog = New-Object -TypeName System.Diagnostics.Stopwatch $IndexMessagePart = 1 #message part ordered keypair array container #receieved bms messages have up to two parts returned per instruction sent #this is a hard typed handler that only collects up to two parts #there's probably some trickery to make this continuously window byte arrays into collections #but I just don't really need it #initalize byte index $BufferSize = 512 $Stream = [System.Byte[]]::new($BufferSize) $Indexes = [ordered]@{} $StreamComplete = $false $i = 0 $byteSTX = [system.convert]::ToByte($BMSInstructionSet.Config.Message.Components.STX,16) $byteETX = [system.convert]::ToByte($BMSInstructionSet.Config.Message.Components.ETX,16) Write-Verbose ("--------------------------------------------------------------") Write-Verbose ("|Mesg type|Pointer ID| Descriptor| (Conditionally) Data |") Write-Verbose ("--------------------------------------------------------------") $WatchDog.Start() do { #clear last data value before attempting another read $Byte = $null if ($StreamComplete -eq $false) { #cast these bytes to hex, which makes it easy to test for difference between #null byte and a zero byte #I had significant issues with this until I started using BaseStream method #using System.IO.Ports.SerialPort.BaseStream method #https://www.sparxeng.com/blog/software/must-use-net-system-io-ports-serialport $Byte = $port.BaseStream.ReadByte() $Stream[$i] = $Byte Write-Verbose ("[ReadByte]: Index: [" + $i + "]: Data:[" + ("{0:x2}" -f $Byte) + "]") #Write-Verbose ("[" + $i + "]: Null header byte found during port readbyte") } else { Write-Verbose "StreamComplete indicates true. Exiting serial read loop." #return out of do loop because stream is complete break } # behold my IF-THEN state machine. :) #REGION: First message start (STX) pointer logic if (($Stream[$i] -eq $byteSTX) -and ($i -le 1)) { Write-Verbose ("[CtrlByte]: Index: [" + $i + "]: <STX> Start Message Received") #first byte of the stream. only occurs once. #crossing this event with index 0 ensures we don't get a false start positive in the stream later. $firstIndexSTX = $i $firstIndexLEN = $i + 3 } #REGION: first message length byte read logic if ($i -eq $firstIndexLEN) { #time to calculate total bytes of this message #7 bytes added to message length: # 4 <STX><DST><SND><LEN> for message header # 3 <CRC><CRC><ETX> for footer $firstIndexMessageLength = (([int][byte]$Stream[$firstIndexLEN]) + 7) #message length, added to firststx (0) minus 1 to index $i from zero #the point of this exercise is to maybe get to a point of understanding how this works #so I can make an unlimited parser instead of a two message parser $firstIndexETX = (($firstIndexMessageLength + $firstIndexSTX) -1) Write-Verbose ("[CtrlByte]: Index: [" + $i + "]: <LEN> (" + $firstIndexMessageLength + ") Received") } #REGION: end of first transmission logic if ($i -eq $firstIndexETX) { Write-Verbose ("[MesgFlow]: Index: [" + $i + "]: <ETX> End Message part 1") #this should be the end of the first message part, since stream index equals byte count of (message + padding) if ($Stream[$i] -ne $byteETX) { #expected end of stream, got something else #something has gone wrong, break out Write-Warning "Expected end of message byte 0xaa. Malformed message." break } if ($IndexMessagePart -eq ($iOInstance.HandlerCount)) { #at end of message stream, if no more messages are expected, #bail from loop Write-Verbose ("[EXIT] No more message parts expected: MessageIndex: " + $IndexMessagePart) Write-Verbose "[EXIT] Returning serial port collection loop" $Indexes.Add($IndexMessagePart,@{"STX"=$firstIndexSTX;"ETX"=$firstIndexETX}) $StreamComplete = $true break } if (($iOInstance.HandlerCount) -gt $IndexMessagePart) { #if we haven't reached the count of handles (messages) for this instruction reception #increment message part so we can process the next time we fall into a sensible <ETX> condition #store indexes for first message part $Indexes.Add($IndexMessagePart,@{"STX"=$firstIndexSTX;"ETX"=$firstIndexETX}) $IndexMessagePart++ } } #REGION End of first message, but now there's a second if (($Stream[$i] -eq $byteSTX) -and ($Stream[$i -1] -eq $byteETX)) { Write-Verbose ("[MesgFlow]: Index: [" + $i + "]: Next message continues") Write-Verbose ("[CtrlByte]: Index: [" + $i + "]: <STX> received: Data: [" + ("{0:x2}" -f $Stream[$i]) + "]") # clearly, this is the start of the second message # index of (first <ETX> byte + 1) is the <STX> of second message. # save index of of second <STX> $secondIndexSTX = $i # predict index of second message length in stream $secondIndexLEN = $i + 3 } #REGION second message length read logic if ($i -eq $secondIndexLEN) { #time to calculate total bytes of this message #7 bytes added to message length: # 4 <STX><DST><SND><LEN> for message header # 3 <CRC><CRC><ETX> for footer $secondIndexMessageLength = ([int]$Stream[$secondIndexLEN] + 7) #message length, added to firststx (0) minus 1 to index $i from zero #the point of this exercise is to maybe get to a point of understanding how this works #so I can make an unlimited parser instead of an only two message parser $secondIndexETX = (($secondIndexMessageLength + $secondIndexSTX) -1) Write-Verbose ("[MesgFlow]: Index: [" + $i + "]: Next message continues") Write-Verbose ("[CtrlByte]: Index: [" + $i + "]: <LEN> (" + $secondIndexMessageLength + ") Received") } #REGION end of second message part and related logic if (($i -eq $secondIndexETX) -and ($Stream[$i] -eq $byteETX)) { $secondIndexETX = $i Write-Verbose ("[MesgFlow]: Index: [" + $i + "] <ETX> [Message part 2]") #this should be the end of the second message part, since stream index equals byte count of (message + padding) if ($Stream[$i] -ne $byteETX) { #expected end of stream, got something else #something has gone wrong, break out Write-Warning "[ERROR] Expected end of message byte 0xaa. Malformed message." break } #at end of message stream, if no more messages are expected, #bail from loop Write-Verbose "[EXIT] No more message parts expected." Write-Verbose "[EXIT] Returning serial port collection loop" $Indexes.Add($IndexMessagePart,@{"STX"=$secondIndexSTX;"ETX"=$secondIndexETX}) $StreamComplete = $true break if (($iOInstance.HandlerCount) -gt $IndexMessagePart) { #if we haven't reached the count of handles (messages) for this instruction reception #throw a fit because something has gone wrong - only two messages in a stream are allowed Throw "REC BMS messages only contain up to two messages in return stream." } } $i++ } until ($WatchDog.ElapsedMilliseconds -ge $BMSInstructionSet.Config.Session.SessionTimeout) #REGION close up shop and emit data $WatchDog.Stop() $ParsedStream = [ordered]@{} $ParsedStream.Add("0",$Stream[$firstIndexSTX..$firstIndexETX]) #add next part if there's more than one if ($IndexMessagePart -gt 1) { $ParsedStream.Add("1",$Stream[$secondIndexSTX..$secondIndexETX]) } return @{"RawStream"=$Stream[$firstIndexSTX..$secondIndexETX];"ParsedStream"=$ParsedStream} } #ENDREGION private function definitions #REGION timer setup #Define a timer to see how long things take $Timer = New-Object -TypeName System.Diagnostics.Stopwatch #ENDREGION timer setup } process { #REGION Main loop #Open the serial port #openserialport has it's own progress bars on id 30 Open-SerialPort #zzz TODO add a loop to process multiple message send events $ProgressStepNumber = 1 foreach ($iOInstance in $iO) { #Write-Progress -id 20 -Activity '[Serial]' -Status "Processing Instructions" -PercentComplete (($ProgressStepNumber / $iO.Count) * 100) #start the timer for transmit event. $Timer.Start() #port should stay open for immediate receieve #Write-Progress -id 40 -Activity '[Serial]:[Send]' -Status ("Sending Instruction: [" + $iOInstance.Command + "]") Send-SerialBytes $iOInstance #stop the timer for transmit event. $Timer.Stop() Write-Verbose ("[Serial]: TX milliseconds: " + $WatchDog.ElapsedMilliseconds) #reset the timer for the next event. $Timer.Reset() #Wait a specified number of milliseconds. Write-Verbose ("[Serial]: Sleeping " + $BMSInstructionSet.Config.Session.SessionThrottle + " milliseconds") Start-Sleep -m $BMSInstructionSet.Config.Session.SessionThrottle #start the timer for the receieve event. $Timer.Start() #Call read serial bytes #Write-Progress -id 40 -Activity '[Serial]:[Receive]' -Status ("Getting Response: [" + $iOInstance.Command + "]") $StreamObject = Read-SerialBytes #return the message as a hash array #next better version of this should be to define a custom class for this. $iOInstance | Add-Member -Name "ByteStreamReceive" -Type NoteProperty -Value (@{ "RawStream" = $StreamObject.RawStream; "InspectedStream" = ($StreamObject.RawStream | Format-Hex -Encoding ascii); "ParsedStream" = $StreamObject.ParsedStream}) $Timer.Stop() Write-Verbose ("[Serial]: RX milliseconds: " + $WatchDog.ElapsedMilliseconds) Write-Verbose ("[Serial]: Received [" + $StreamObject.RawStream.count + "] bytes on port [" + $port.PortName +"]") $Timer.Reset() Write-Verbose "[Serial]: Returning stream" #this error is a failure and can cause dependent calls to fall on their face #if (unlikely) any good data comes out, crc check will provide some validation Test-MessageCRC $iOInstance | Out-Null #increment step number for progressbar $ProgressStepNumber++ } } end { Close-SerialPort return $iO } } |