Tenderlove Making

Using Serial Ports with Ruby

Lets 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!

« go back