Using Serial Ports with Ruby
Feb 16, 2024 @ 11:56 amLets mess around with serial ports today! I love doing hardware hacking, and dealing with serial ports is a common thing you have to do when working with embedded systems. Of course I want to do everything with Ruby, and I had found Ruby serial port libraries to be either lacking, or too complex, so I decided to write my own. I feel like I’ve not done a good enough job promoting the library, so today we’re going to mess with serial ports using the UART gem. Don’t let the last commit date on the repo fool you, despite being over 6 years ago, this library is actively maintained (and I use it every day!).
I’ve got a GMC-320 Geiger counter. Not only is the price pretty reasonable, but it also has a serial port interface! You can log data, then download the logged data via serial port. Today we’re just going to write a very simple program that gets the firmware version via serial port, and then gets live updates from the device. This will allow us to start with UART basics in Ruby, and then work with streaming data and timeouts.
The company that makes the Geiger counter published a spec for the UART commands that the device supports, so all we need to do is send the commands and read the results.
According to the spec, the default UART config for my Geiger counter is 115200 BPS, Data bit: 8, no parity, Stop bit: 1, and no control. This is pretty easy to configure with the UART gem, all of these values are default except for the baud rate. The UART gem defaults to 9600 for the baud rate, so that’s the only thing we’ll have to configure.
Getting the hardware version
To get the hardware model and version, we just have to send <GETVER>>
over the serial port and then read the response.
Let’s write a small program that will fetch the hardware model and version and print them out.
require "uart"
UART.open ARGV[0], 115200 do |serial|
# Send the "get version" command
serial.write "<GETVER>>"
# read and print the result
puts serial.read
end
The first thing we do is require the uart
library (make sure to gem install uart
if you haven’t done so yet).
Then we open the serial interface.
We’ll pass the tty file in via the command line, so ARGV[0]
will have the path to the tty.
When I plug my Geiger counter in, it shows up as /dev/tty.usbserial-111240
.
We also configure the baud rate to 115200.
Once the serial port is open, we are free to read and write to it as if it were a Ruby file object. In fact, this is because it really is just a regular file object.
First we’ll send the command <GETVER>>
, then we’ll read the response from the serial port.
Here’s what it looks like when I run it on my machine:
$ ruby rad.rb /dev/tty.usbserial-111240
GMC-320Re 4.09
Live updates
According to the documentation, we can get live updates from the hardware.
To do that, we just need to send the <HEARTBEAT1>>
command.
Once we send that command, the hardware will write a value to the serial port every second, and it’s our job to read the data when it becomes available.
We can use IO#wait_readable
to wait until there is data to be read from the serial port.
According to the specification, there are two bytes (a 16 bit integer), and we need to ignore the top 2 bits. We’ll create a mask to ignore the top two bits, and combine that with the two bytes we read to get our value:
require "uart"
MASK = (~(3 << 14)) & 0xFFFF
UART.open ARGV[0], 115200 do |serial|
# turn on heartbeat
serial.write "<HEARTBEAT1>>"
loop do
if serial.wait_readable
count = ((serial.readbyte << 8) | serial.readbyte) & MASK
p count
end
end
ensure
# make sure to turn off heartbeat
serial.write "<HEARTBEAT0>>"
end
After we’ve sent the “start heartbeat” command, we enter a loop.
Inside the loop, we block until there is data available to read by calling serial.wait_readable
.
Once there is data to read, we’ll read two bytes and combine them to a 16 bit integer.
Then we mask off the integer using the MASK
constant so that the two top bits are ignored.
Finally we just print out the count.
The ensure
section ensures that when the program exits, we’ll tell the hardware “hey, we don’t want to stream data anymore!”
When I run this on my machine, the output is like this (I hit Ctrl-C to stop the program):
$ ruby rad.rb /dev/tty.usbserial-111240
0
0
0
0
0
0
0
1
0
1
0
0
0
1
^Crad.rb:10:in 'IO#wait_readable': Interrupt
from rad.rb:10:in 'block (2 levels) in <main>'
from <internal:kernel>:191:in 'Kernel#loop'
from rad.rb:9:in 'block in <main>'
from /Users/aaron/.rubies/arm64/ruby-trunk/lib/ruby/gems/3.4.0+0/gems/uart-1.0.0/lib/uart.rb:57:in 'UART.open'
from rad.rb:5:in '<main>'
Lets do two improvements, and then call it a day. First, lets specify a timeout, then lets calculate the CPM.
Specifying a timeout
Currently, serial.wait_readable
will block forever, but we expect an update from the hardware about every second.
If it takes longer than say 2 seconds for data to be available, then something must be wrong and we should print a message or exit the program.
Specifying a timeout is quite easy, we just pass the timeout (in seconds) to the wait_readable
method like below:
require "uart"
MASK = (~(3 << 14)) & 0xFFFF
UART.open ARGV[0], 115200 do |serial|
# turn on heartbeat
serial.write "<HEARTBEAT1>>"
loop do
if serial.wait_readable(2)
count = ((serial.readbyte << 8) | serial.readbyte) & MASK
p count
else
$stderr.puts "oh no, something went wrong!"
exit(1)
end
end
ensure
# make sure to turn off heartbeat
serial.write "<HEARTBEAT0>>"
end
When data becomes available, wait_readable
will return a truthy value, and if the timeout was reached, then it will return a falsy value.
So, if it takes more than 2 seconds for data to become available wait_readable
will return nil
, and we print an error message and exit the program.
Calculating CPM
CPM stands for “counts per minute”, meaning the number of ionization events the hardware has detected within one minute. However, the value that we’re reading from the serial port is actually the “counts per second” (or ionization events the hardware detected in the last second). Most of the time that value is 0 so it’s not super fun to read. Lets calculate the CPM and print that instead.
We know the samples are arriving about every second, so I’m just going to modify this code to keep a list of the last 60 samples and just sum those:
require "uart"
MASK = (~(3 << 14)) & 0xFFFF
UART.open ARGV[0], 115200 do |serial|
# turn on heartbeat
serial.write "<HEARTBEAT1>>"
samples = []
loop do
if serial.wait_readable(2)
# Push the sample on the list
samples.push(((serial.readbyte << 8) | serial.readbyte) & MASK)
# Make sure we only have 60 samples in the list
while samples.length > 60; samples.shift; end
# Print a sum of the samples (if we have 60)
p CPM: samples.sum if samples.length == 60
else
$stderr.puts "oh no, something went wrong!"
exit(1)
end
end
ensure
# make sure to turn off heartbeat
serial.write "<HEARTBEAT0>>"
end
Here is the output on my machine:
$ ruby rad.rb /dev/tty.usbserial-111240
{:CPM=>9}
{:CPM=>8}
{:CPM=>8}
{:CPM=>8}
{:CPM=>8}
{:CPM=>9}
{:CPM=>9}
{:CPM=>9}
After about a minute or so, it starts printing the CPM.
Conclusion
I love playing with embedded systems as well as dealing with UART. Next time you need to do any serial port communications in Ruby, UART to consider using my gem. Thanks for your time, and I hope you have a great weekend!