Search code examples
pythonserial-portpyserialcrystal-lang

Crystal-lang Accessing Serial port


I want to access the serial port using Crystal lang.

I have following code in python. I want to write the equivalent Crystal-lang code for a pet project.

import serial

def readSerData():

    s = ser.readline()
    if s:
        print(s)
        result = something(s) #do other stuff
        return result

if __name__ == '__main__':

    ser = serial.Serial("/dev/ttyUSB0", 9600)
    while True:
        data = readSerData()
        #do something with data

I couldn't find any library for accessing the serial port.

What is the proper way for accessing serial port in crystal-lang?

Thanks in advance.


Solution

  • It is easier to answer this question in multiple parts to really cover it all:

    Q: How do I access a serial port on linux/bsd?

    A: Open it as a file. On linux/bsd a serial connection is established the moment a device is plugged in, and is then listed somewhere under /dev/ (these days, usually as /dev/ttyUSB0). In order to access this connection you simply open it like you would a regular file. Sometimes this is actually good enough to start communicating with the device as modern hardware typically works with all baud rates and default flags.

    Q: How do I configure a serial/tty device on linux/bsd?

    A: Set termios flags on the file. If you do need to configure your connection to set things like baud rate, IXON/IXOFF etc, you can do it before even running your program using stty if it is available. Eg. to set the baud rate you could run: stty -F /dev/ttyUSB0 9600. And after this is set up you can just open it as a file and start using it.

    You can spawn stty from crystal using Process.run if you wanted an easy way to configure the device from your app. I would probably recommend this approach over the next solution..

    Q: How do I set termios flags from crystal, without using stty?

    A: Use the termios posix functions directly. Crystal actually provides FileDescriptor handles with a few common termios settings such as cooked, which means it has minimal termios bindings already. We can start by using the existing code for our inspiration:

    require "termios" # See above link for contents
    
    #Open the file
    serial_file = File.open("/dev/ttyACM0")
    raise "Oh no, not a TTY" unless serial_file.tty?
    
    # Fetch the unix FD. It's just a number.
    fd = serial_file.fd
    
    # Fetch the file's existing TTY flags
    raise "Can't access TTY?" unless LibC.tcgetattr(fd, out mode) == 0
    
    # `mode` now contains a termios struct. Let's enable, umm.. ISTRIP and IXON
    mode.c_iflag |= (Termios::InputMode::ISTRIP | Termios::InputMode::IXON).value
    # Let's turn off IXOFF too.
    mode.c_iflag &= ~Termios::InputMode::IXOFF.value
    
    # Unfun discovery: Termios doesn't have cfset[io]speed available
    # Let's add them so changing baud isn't so difficult.
    lib LibC
      fun cfsetispeed(termios_p : Termios*, speed : SpeedT) : Int
      fun cfsetospeed(termios_p : Termios*, speed : SpeedT) : Int
    end
    
    # Use the above funcs to set the ispeed and ospeed to your nominated baud rate.
    LibC.cfsetispeed(pointerof(mode), Termios::BaudRate::B9600)
    LibC.cfsetospeed(pointerof(mode), Termios::BaudRate::B9600)
    # Write your changes to the FD.
    LibC.tcsetattr(fd, Termios::LineControl::TCSANOW, pointerof(mode))
    
    # Done! Your serial_file handle is ready to use.
    

    To set any other flags, refer to the termios manual, or this nice serial guide I just found.

    Q: Is there a library to do all this for me?

    A: No :( . Not that I can see, but it would be great if someone made it. It's probably not much work for someone to make one if they had a vested interest :)