Viewed   121 times

I am looking for a way to communicate with RS232 serial COM port on windows. I have found 2 solutions on the net, one which is not totally free (introduces deliberate delays on the function) and another with limited capability on Windows. The latter can only write to a COM port on Windows, not read.

I can't look at the code of the first solution since it is compiled into a .dll (makes sense, otherwise people can just edit the delay and not purchase it...) and the second one seems only to use fopen() to open the port and later fwrite() to it for writing, just like one would do to a stream. But apparently freading it returns nothing.

I know it's possible as the first solution did it, although it does require Apache to use php-cgi module instead of php5module.

Any ideas?

 Answers

5

Every solution above is either inefficient or too much work.

You can just use the PHP-DIO library (dio_fcntl, dio_open, dio_read, dio_write, dio_seek, ...). It's also in the PHP manual's entry for DIO:

This PECL package isn't available by default. To get it for Windows if you have PHP 5.2.x greater than 5.2.6, you can download it as part of a ZIP:

  • Thread-safe (for Apache)

  • Non-thread-safe (for IIS)

Both of these links were found in http://www.deveblog.com/index.php/download-pecl-extensions-for-windows/

Here is the build from Linux, just get it and do the phpize/configure/make/make install thing.

I don't know whether it should be used in an Apache session, but go for it.

Tuesday, December 6, 2022
3

You can of course use

shell_exec('wmic DISKDRIVE GET SerialNumber 2>&1')

or

shell_exec('wmic bios get serialnumber 2>&1') 

if wmic is installed on server and your PHP has permission to run it.

The '2>&1' part is used from the suggestion here.

Wednesday, August 3, 2022
 
2

Read data in a loop after write operation until get a full response. But you need to use synchronous API and Task.Run() as current version of the asynchronous API ignores SerialPort timeout properties completely and CancellationToken in Task based API almost completely.

Excerpt from the SerialPort.ReadTimeout Microsoft Docs that is relevant to SerialPort.BaseStream.ReadAsync() because it uses default implementation Stream.ReadAsync():

This property does not affect the BeginRead method of the stream returned by the BaseStream property.

Example implementation using synchronous API and dynamic timeout properties update:

static byte[] SendMessage(byte[] message, TimeSpan timeout)
{
    // Use stopwatch to update SerialPort.ReadTimeout and SerialPort.WriteTimeout 
    // as we go.
    var stopwatch = Stopwatch.StartNew();

    // Organize critical section for logical operations using some standard .NET tool.
    lock (_syncRoot)
    {
        var originalWriteTimeout = _serialPort.WriteTimeout;
        var originalReadTimeout = _serialPort.ReadTimeout;
        try
        {
            // Start logical request.
            _serialPort.WriteTimeout = (int)Math.Max((timeout - stopwatch.Elapsed).TotalMilliseconds, 0);
            _serialPort.Write(message, 0, message.Length);

            // Expected response length. Look for the constant value from 
            // the device communication protocol specification or extract 
            // from the response header (first response bytes) if there is 
            // any specified in the protocol.
            int count = ...;
            byte[] buffer = new byte[count];
            int offset = 0;
            // Loop until we recieve a full response.
            while (count > 0)
            {
                _serialPort.ReadTimeout = (int)Math.Max((timeout - stopwatch.Elapsed).TotalMilliseconds, 0);
                var readCount = _serialPort.Read(buffer, offset, count);
                offset += readCount;
                count -= readCount;
            }
            return buffer;
        }
        finally
        {
            // Restore SerialPort state.
            _serialPort.ReadTimeout = originalReadTimeout;
            _serialPort.WriteTimeout = originalWriteTimeout;
        }
    }
}

And example usage:

byte[] request = ...;
TimeSpan timeout = ...;

var sendTask = Task.Run(() => SendMessage(request, timeout));
try
{
    await await Task.WhenAny(sendTask, Task.Delay(timeout));
}
catch (TaskCanceledException)
{
    throw new TimeoutException();
}
byte[] response = await sendTask;

You can do similar thing with CancellationToken instance and use CancellationToken.ThrowIfCancellationRequested() between read and write operations but you have to make sure that proper timeouts are set on SerialPort or otherwise Thread pool thread will hang forever possible holding a lock. As far as I know you can't utilize CancellationToken.Register() because there is no SerialPort method to call to cancel an operation.

For more information check:

  • Top 5 SerialPort Tips article by Kim Hamilton
  • Recommended asynchronous usage pattern of SerialPort, Document that CancellationToken in Stream.ReadAsync() is advisory and NetworkStream.ReadAsync/WriteAsync ignores CancellationToken related issues on .NET GitHub
  • Should I expose asynchronous wrappers for synchronous methods? article by Stephen Toub
Thursday, August 18, 2022
 
xikuuky
 
2

You have to think of Serial Port communications as streaming data. Any time you receive data, you have to expect that it may be a complete message, only a partial message, or multiple messages. It all depends how fast the data is coming in and how fast you application is able to read from the queue. Therefore, you are right in thinking you need a buffer. However, what you may not be realizing yet, is that there is no way to know, strictly via, the Serial Port, where each message begins and ends. That has to be handled via some agreed upon protocol between the sender and the receiver. For instance, many people use the standard start-of-text (STX) and end-of-text (ETX) characters to indicate the beginning and ending of each message send. That way, when you receive the data, you can tell when you have received a complete message.

For instance, if you used STX and ETX characters, you could make a class like this:

Public Class DataBuffer
    Private ReadOnly _startOfText As String = ASCII.GetChars(New Byte() {2})
    Private ReadOnly _endOfText As String = ASCII.GetChars(New Byte() {4})

    Public Event MessageReceived(ByVal message As String)
    Public Event DataIgnored(ByVal text As String)

    Private _buffer As StringBuilder = New StringBuilder

    Public Sub AppendText(ByVal text As String)
        _buffer.Append(text)
        While processBuffer(_buffer)
        End While
    End Sub

    Private Function processBuffer(ByVal buffer As StringBuilder) As Boolean
        Dim foundSomethingToProcess As Boolean = False
        Dim current As String = buffer.ToString()
        Dim stxPosition As Integer = current.IndexOf(_startOfText)
        Dim etxPosition As Integer = current.IndexOf(_endOfText)
        If (stxPosition >= 0) And (etxPosition >= 0) And (etxPosition > stxPosition) Then
            Dim messageText As String = current.Substring(0, etxPosition + 1)
            buffer.Remove(0, messageText.Length)
            If stxPosition > 0 Then
                RaiseEvent DataIgnored(messageText.Substring(0, stxPosition))
                messageText = messageText.Substring(stxPosition)
            End If
            RaiseEvent MessageReceived(messageText)
            foundSomethingToProcess = True
        ElseIf (stxPosition = -1) And (current.Length <> 0) Then
            buffer.Remove(0, current.Length)
            RaiseEvent DataIgnored(current)
            foundSomethingToProcess = True
        End If
        Return foundSomethingToProcess
    End Function


    Public Sub Flush()
        If _buffer.Length <> 0 Then
            RaiseEvent DataIgnored(_buffer.ToString())
        End If
    End Sub
End Class

I should also mention that, in communication protocols, it is typical to have a checksum byte by which you can determine if the message got corrupted during its transmission between the sender and the receiver.

Monday, August 8, 2022
 
gilloux
 
2

The first workable solution I have is to use a powershell script. It takes in parameters from Lua including the COM port, Baud Rate, and a string to write.

First off, here the the Lua Script.

writeThenReadCOMinLua.lua

local comPort = "COM2"
local baud = "9600"
local dataToWrite = "Hello. Is Anyone There?"
--run the powershell script with supplied params. Spaces are important.
local file = io.popen("powershell.exe -file ./liblocal/psLibs/writeAndReadCOM.ps1 
"..comPort.. " " .. baud .. " " .. dataToWrite)

--Wait for a reply (indefinitely)
local rslt = file:read("*a")
print("Response: " .. rslt)

And, the powershell script to write then wait for reply.

writeAndReadCOM.ps1

$nargs = $args.Count #args is the list of input arguments
$comPortName=$args[0] #This is the com port. It has zero spaces
$baud = $args[1] #this is the numberical baud rate
#the remainder of the arguments are processed below


#Combine argument 2,3,...,n with a space because of command prompt shortfalls to pass arguments with spaces
$dataToWrite = ""
For ($i=2; $i -le $nargs ; $i++) {
    $dataToWrite = "$($dataToWrite) $($args[$i])"
}

#$port= new-Object System.IO.Ports.SerialPort COM2,9600,None,8,one
$port= new-Object System.IO.Ports.SerialPort $comPortName,$baud,None,8,one

#open port if it's not yet open
IF ($port.IsOpen) {
    #already open
} ELSE {
    #open port
    $port.Open()
}

#write the data
$port.WriteLine($dataToWrite)

#wait for a response (must end in newline). This removes the need to have a hard coded delay
$line = $port.ReadLine()
Write-Host $line #send read data out

#if the response was multiple lines, then read the rest of the buffer. If the port was just opened.
while ($port.BytesToRead -ne 0) {
    $dataReturned = 1
    $line = $port.ReadLine()
    Write-Host $line #send read data out for the remainder of the buffer
}

$port.Close()

#IF ($dataReturned -eq 0) {'PS_NO_BYTES_TO_READ'}

There are a few things going on here. First, the serial data sent by lua may have spaces. Unfortunately, these all get separated into multiple args by the terminal, so powershell then goes and regroups them into a single string. Second, the port must be opened to read or write data. If any serial data is sent before it is opened, that data is lost.

Issue Remaining: I can't leave the port open then go do stuff in Lua and periodically check the port for new data. This is unfortunate because the hardware I am using sometimes sends data without a request, or takes a long time to reply with all the data (in the case of an iterative level calibration). Additionally, each time I open the port, the hardware restarts. At this point, I have no good solution.

Here was my attempt at a fix: Create three separate powershell scripts #1) open the port #2) read the port and return nil within 500mS if no data exists otherwise reply with all the data and #3)close it. Unfortunately, #2 throws an error about the port being closed even after #1 is run. I'd love to hear some thoughts, and will gladly update this answer with any solutions.

Many thanks to Egor for all the help so far.

Monday, December 26, 2022
Only authorized users can answer the search term. Please sign in first, or register a free account.
Not the answer you're looking for? Browse other questions tagged :