0

I am writing an executable that uses a 3rd party C library (libmodbus if it matters) to communicate via serial device (in my case, /dev/ttyUSB0 or similar to talk RS-485 via an FTDI chipset based USB-to-RS485 adapter). This executable, based on CLI args, can initiate commands (in my case, act like a modbus client) then await a response (in my case, from an external modbus server), or listen for incoming commands (in my case, act like a modbus server) then generate a response.

I would like to automate the testing of my executable, without a need for some external device. In other words, I'd like to launch two instances of my executable, where:

  • The first instance is put into modbus client mode and uses /dev/xxxx for comms
  • The second instance is put into modbus server mode and uses /dev/yyyy for comms
  • Set up /dev/xxxx and /dev/yyyy to both act as serial devices that are are essentially the two ends of the same wire.

From what I read on pty manpage, I believe /dev/xxxx and /dev/yyyy are the two ends of a pseudo-terminal. Which brings me to my questions:

  1. The man page refers to BSD-style pseudoterminals which seem more appropriate to what I am trying to do.
    • Is my understanding of BSD-style pseudoterminals correct?
    • If so, is it possible to create BSD-style pseudoterminals on non-BSD linux distributions? In pacticular, I am using debian 10, 11, 12 (and debian based, like Ubuntu 20.04, 22.04)
  2. The man page also refers to UNIX 98 pseudoterminals, which are implemented using posix_openpt(). However, even after the subsequent grantpt() and unlockpt(), I can only one one /dev/pts device for the client side of the pty, with the master side being only a file descriptor inside the executable.
    • Is my understanding (which is loosely based on code like this) correct?
    • If so, what tricks may I use to convert the master side file descriptor to an proper /dev/xxxx which is the only API available to get a modbus context
  3. Are there other "standard" linux tools for doing what I am attempting to do? It seems like tools like the ones mentioned here are expecting to connect an executable's STDIO to the pty.

1 Answers1

3

You can set up PTY "virtual serial ports" using socat.

socat \
  pty,rawer,echo=0,link=/tmp/portA \
  pty,rawer,echo=0,link=/tmp/portB

This will create two PTY devices and two symlinks to those devices. On my system, the above command created:

$ ls -l /tmp/port*
lrwxrwxrwx 1 lars lars 11 Jul 24 11:49 /tmp/portA -> /dev/pts/20
lrwxrwxrwx 1 lars lars 11 Jul 24 11:49 /tmp/portB -> /dev/pts/21

You can treat these pty devices as serial ports. For example, I can attach slcand to these devices to create CANbus interfaces:

slcand -o -c -f -s6 $(readlink /tmp/portA)
slcand -o -c -f -s6 $(readlink /tmp/portB)

Or I can attach picocom to each port and chat across the virtual link. In one window:

picocom $(readlink /tmp/portA)

And in another window:

picocom $(readlink /tmp/portB)

Etc.


Both UNIX-98 ptys and BSD-style ptys behave identically; the difference is how they are allocated (UNIX-98 ptys are allocated dynamically while BSD-style ptys are pre-allocated devices).

However, even after the subsequent grantpt() and unlockpt(), I can only one one /dev/pts device for the client side of the pty, with the master side being only a file descriptor inside the executable.

That's correct; to link two ptys to create a virtual serial line, your code needs to open two pty devices and then handle moving data between the two (that's what socat is doing in the above example).

larsks
  • 34,737
  • 1
    I needed to modify the socat command slightly (partly for debugging purposes, and partly to make it work for my needs). The final command was

    socat -x -lu -d -d -d pty,rawer,wait-slave,echo=0,link=/tmp/portA pty,rawer,echo=0,link=/tmp/portB

    – Paul Grinberg Jul 24 '23 at 19:08