Win32 Serial Port for Ruby
Introduction
The Win32SerialPort::SerialPort
is a simple class for Ruby which helps to access a serial port in Windows. This class uses the standard Win32API and does not require any external C/C++ libraries.
Using the code
Create and open 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 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:
# Open COM1 serial port in 115200,8,n,1 mode without flow control
return 0 if false == serial.open(
"COM1", # port name
115200, # baudrate
Win32SerialPort::FLOW_NONE, # no flow control
8, # 8 data bits
Win32SerialPort::NOPARITY, # no parity bits
Win32SerialPort::ONESTOPBIT) # one stop bit
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 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 flags NOPARITY
, ODDPARITY
, EVENPARITY
, MARKPARITY
, and SPACEPARITY
.
To change the number of stop bits, the choice is:
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.
Preparing data to send
Because of the special nature of the Ruby language, 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 it 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 items extracted from the binary string according to the formatter
parameter.
For example, let's see how to 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 one 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. ‘i’ is the signed integer number, and ‘a12’ is the string of 12 bytes. As a result, the binaryString
contains 20 bytes: 4 bytes for each integer (32 bit Windows) and 12 bytes of characters.
The binaryString
contains data in the format ready to send.
Sending data
The write
method takes only one parameter. The parameter is a string. Here are a few examples:
- Send a simple string:
# send simple string
written = serial.write(“Hello World!”)
# one character “7”
i = 7
written = serial.write(i.to_s)
# two characters: 7 and 6
i = 76
Written = serial.write(i.to_s)
# an array of items
toSend = [4,7,"Hello World!"]
# the array of items converted to a binary string
binaryString = toSend.pack("iia12")
# send the binary string
written = serial.write(binaryString)
#
# Test if data has been sent
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.
Receiving data
There are two methods in the class allowing 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 it was available in the input buffer of the serial port but not more than specified.
The second method readUntil
blocks execution of the program until it reads the specified number of bytes. It will return with less bytes than specified if the serial port is closed.
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 the received data. Here are some examples:
- Read all available (received) bytes:
# Reads as many bytes as it is stored in
# the input buffer of the serial port.
binString = serial.read
if binString.length > 0
print "Received data: " + binString + "\n"
else
print "Nothing received\n"
end
# Returns 10 or less bytes of data
binString = serial.read(10)
if binString.length > 0
print "Received " + binString.length.to_s + " bytes\n"
else
print "Nothing received\n"
end
# blocks until 10 bytes is available
binString = serial.readUntil(10)
# test the length in case if the serial port has been closed
if binString.length > 0
print "Received " + binString.length.to_s + " bytes\n"
else
print "Nothing received\n"
end
There is a 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)
Missing features
A quick look at the System.IO.Ports.SerialPort
class from the Microsoft .NET library, for example, is enough to see a lack of interesting features in the class described here. Probably the most important would be:
- Implementation of the IO interface,
- Comm timeouts configuration (see the
SetCommTimeouts
function of the MS Windows API), - And access to the modem pins (DCD, DTR, etc.).