Introduction
Win32SerialPort::SerialPort
is a simple class for Ruby which helps to access a serial port in Windows. This class uses the standard Win32 API and does not require any external C/C++ libraries.
Using the code
This code was tested on Windows XP, Ruby 1.8.6, and Win32API gem 1.4.8.
1. Create and open a serial port
The serial port class is stored in the win32serial
module and is encapsulated in the Win32SerialPort
namespace. Use require
to attach the module to your script.
require "win32serial"
Create an instance of the serial port object using the new
function as follows:
serial = Win32SerialPort::SerialPort.new
The next step is to open the serial port and make it ready to use. For example, you might want to open COM1 in the 115200,8,n,1 mode without a flow control mode (baudrate: 115200, 8 data bits, no parity, and 1 stop bit). The following example does the job:
return 0 if false == serial.open(
"COM1",
115200,
Win32SerialPort::FLOW_NONE,
8,
Win32SerialPort::NOPARITY,
Win32SerialPort::ONESTOPBIT)
The parameters passed to the function are forwarded as they are to the Windows library, and if the open fails, it is because of Windows limitations. The open
function returns false
if it fails to open the serial port and true
when the serial port is ready to use.
To turn on the hardware flow control, use the FLOW_HARDWARE
switch instead of FLOW_NONE
.
To switch the parity, use the flags NOPARITY
, ODDPARITY
, EVENPARITY
, MARKPARITY
, and SPACEPARITY
.
To change the number of stop bits, the choices are:
ONESTOPBIT
– one stop bitONE5STOPBITS
- 1.5 stop bitsTWOSTOPBITS
– two stop bits
The number of data bits must be 5 to 8 bits.
The use of 5 data bits with 2 stop bits is an invalid combination, as is 6, 7, or 8 data bits with 1.5 stop bits.
Use the close
function to close the serial port.
There is an option to configure serial port timeouts. There are five timeouts defined in the COMMTIMEOUTS
structure in the Windows API: ReadIntervalTimeout
, ReadTotalTimeoutMultiplier
, ReadTotalTimeoutConstant
, WriteTotalTimeoutMultiplier
, and WriteTotalTimeoutConstant
.
The following example sets the first three read timeouts to 0 and the last two write timeouts to 100ms and 1000ms. These timeouts are passed to the setCommTimeouts
function as a table of 5 numbers.
timeouts = [0,0,0,100,1000]
result = serial.setCommTimeouts(timeouts)
print "\nSetCommTimeouts result: " + result.to_s + "\n\n"
2. Preparing data to send
The special nature of the Ruby language causes that the data to be sent has to be specially prepared. This paragraph is more about how to prepare data than about using the serial port class itself. If you are familiar with the Array::pack
and String::unpack
methods, you can skip this paragraph.
The Array
in Ruby is an object and cannot be interpreted as a stream of bytes as it is required when parameters are passed to the Windows kernel. It means that data to be sent to a serial port must be prepared before.
There are two functions in the Ruby library which helps to convert data from an array to a stream of bytes and the stream of bytes back to the array format. The first one is useful when transmitting and the second when receiving bytes.
Each item of an Array
class instance may have a different type and that means that a different size too. The Array
class has the pack
method which returns the items of the array as a string of bytes. Each item of the array takes the required number of bytes in a string and is placed one after another every item. The pack
method does not know how to interpret its items. So the method takes a parameter called formatter
which describes how to format the items. For example, ‘i’ means a signed integer number, ‘f’ means a floating point number, and ‘a’ is a string. All formatters are described in the pack
method documentation.
When data is received from the serial port, it is represented as a string of bytes. Each application expecting to receive data from the serial port also knows how to parse the received bytes and knows how to extract information from that string. The String
class has the unpack
method which takes the formatter parameter (with the same switches as the pack
method mentioned earlier) describing how to interpret the received bytes. The method returns an instance of the Array
class containing the items extracted from the binary string according to the formatter parameter.
For example, prepare a binary string of two integers and a characters string. First of all, create an array:
toSend = [4,7,"Hello World!"]
The array toSend
has three items. Two integers (4, 7) and a string (Hello World!).
The array must be converted to a binary string as follows:
binaryString = toSend.pack("iia12")
“iia12” is the formatter parameter which tells the pack
method how to interpret the items stored in the toSend
array. The ‘i’ is the signed integer number and ‘a12’ is the string of 12 bytes. As a result, binaryString
contains 20 bytes: 4 bytes for each integer (32 bit Windows) and 12 bytes of characters.
binaryString
contains data in the format ready to send.
3. Sending data
The write
method takes only one parameter. The parameter is a string. Here are a few examples:
written = serial.write("Hello World!")
Send a number as a string:
i = 7
written = serial.write(i.to_s)
i = 76
Written = serial.write(i.to_s)
Send an array of bytes (see the ‘Preparing data to send’ paragraph for an explanation):
toSend = [4,7,"Hello World!"]
binaryString = toSend.pack("iia12")
written = serial.write(binaryString)
if 0 < written
print "Data has been successfully sent\n"
else
print "Could not send data\n"
end
The write
method returns the number of bytes that have been sent.
4. Receiving data
There are two methods in the class allowing the reading of received bytes. The read
method tries to read the number of bytes specified in the input parameter. It returns immediately with as many bytes as was available in the input buffer of the serial port but not more than specified.
The second method readUntil
blocks execution of a program until it reads the specified number of bytes. It will return with less bytes than specified if the serial port is closed. If serial port timeouts are set not to wait for data, readUntil
will return immediately with the bytes available to read.
Both functions return a binary string containing the received bytes. See the ‘Preparing data to send’ paragraph for an explanation of how to parse/interpret received data. Examples:
- Read all available (received) bytes:
binString = serial.read
if binString.length > 0
print "Received data: " + binString + "\n"
else
print "Nothing received\n"
end
Read no more than 10 bytes:
binString = serial.read(10)
if binString.length > 0
print "Received " + binString.length.to_s + " bytes\n"
else
print "Nothing received\n"
end
Read no less than 10 bytes:
binString = serial.readUntil(10)
if binString.length > 0
print "Received " + binString.length.to_s + " bytes\n"
else
print "Nothing received\n"
end
There is the bytesToRead
attribute in the class which returns the number of bytes available to read from the receive buffer of the serial port.
bytesAvailable = serial.bytesToRead
print "\nBytes available to read: " + bytesAvailable.to_s + "\n"
binString = serial.readUntil(bytesAvailable)
5. Missing features
A quick look at the System.IO.Ports.SerialPort
class from the Microsoft .NET library, for example, is enough to see the lack of a few interesting features of the class described here. Probably the most important would be:
- implementation of the IO interface,
- and access to the modem pins (DCD, DTR, etc.)