Fake serial communication under Linux

Question:

I have an application where I want to simulate the connection between a device and a “modem”. The device will be connected to a serial port and will talk to the software modem through that.

For testing purposes I want to be able to use a mock software device to test send and receive data.

Example Python code

device = Device()
modem  = Modem()
device.connect(modem)

device.write("Hello")
modem_reply = device.read()

Now, in my final app I will just pass /dev/ttyS1 or COM1 or whatever for the application to use.
But how can I do this in software? I am running Linux and application is written in Python.

I have tried making a FIFO (mkfifo ~/my_fifo) and that does work, but then I’ll need one FIFO for writing and one for reading. What I want is to open ~/my_fake_serial_port and read and write to that.

I have also lpayed with the ptymodule, but can’t get that to work either. I can get a master and slave file descriptor from pty.openpty() but trying to read or write to them only causes IOError Bad File Descriptor error message.

Update

Comments pointed me to the SO question Are there some program like COM0COM in linux? which uses socat to setup a virtual serial connection.
I used it like this:

socat PTY,link=$HOME/COM1 PTY,link=$HOME/COM2

To the rest of you, thank you for giving me valuable information.
I chose to accept Vinay Sajips‘s answer since that is the solution which I went for before the socat suggestion showed up. It seems to work well enough.

Asked By: Hannes Ovrén

||

Answers:

It’s probably best to use pyserial to communicate with the serial port, and you can just create a mock version of the serial.Serial class which implements read, readline, write and any other methods you need.

Answered By: Vinay Sajip

You are on the right track with pseudo-terminals. To do this, your mock software device needs to first open a pseudo-terminal master – this is the file descriptor it will read from and write to, when it is talking to the serial software that you’re testing. It then needs to grant access to and unlock the pseudo-terminal slave, and obtain the name of the slave device. It should then print out the name of the slave device somewhere, so that you can tell the other software to open that as it’s serial port (ie. that software will be opening a name like /dev/pts/0 instead of /dev/ttyS1).

The simulator software then just reads and writes from the master side of the pseudoterminal. In C, it would look like this:

#define _XOPEN_SOURCE
#include <stdlib.h>
#include <stdio.h>
#include <fcntl.h>

int main(int argc, char *argv[])
{
    int pt;

    pt = open("/dev/ptmx", O_RDWR | O_NOCTTY);
    if (pt < 0)
    {
        perror("open /dev/ptmx");
        return 1;
    }

    grantpt(pt);
    unlockpt(pt);

    fprintf(stderr, "Slave device: %sn", ptsname(pt));

    /* Now start pretending to be a modem, reading and writing "pt" */
    /* ... */
    return 0;
}

Hopefully that is easy enough to convert to Python.

Answered By: caf

Here’s pythonic version of pts-emulated (caf’s) serial communication:

from serial import Serial

driver = MyDriver()  # what I want to test
peer = serial.Serial()
driver.port.fd, peer.fd = posix.openpty()
driver.port._reconfigurePort()
peer.setTimeout(timeout=0.1)
peer._reconfigurePort()
driver.start()

# peer.write("something")
# driver.get_data_from_serial()

It has some advantages over mocking Serial, namely that Serial code is used and some serial port artefacts are exercised.

If you want to test opening of serial ports, you could swap master and slave around and use os.ttyname(salve_fd) as serial port name. I can’t vouch for side-effects of swapping master and slave around though. Most notable is that you can close and reopen slave, but fi you close master slave dies too.

This works like a charm if your test code runs within same process. I didn’t iron out the kinks with multiple/separate processes yet.

Answered By: Dima Tisnek

Here’s some code that worked for me (Python 3.10.9 and pySerial 3.5), using posix.openpty(). It opens one end as a file and another as a pySerial port.

#!/usr/bin/env python3

import os, serial, posix

# Create a pySerial port and a binary file linked to a PTY (simulated
# serial cable).
def sim_serial():
    fd1, fd2 = posix.openpty()
    fd_file = os.fdopen(fd1, "wb")
    serial_port = serial.Serial(os.ttyname(fd2), 115200)
    os.close(fd2)

    return fd_file, serial_port

if __name__ == "__main__":
    fd_file, serial_port = sim_serial()
    fd_file.write(b'This is a test.n')
    fd_file.flush()
    print(serial_port.read_until(b'n'))

Opening both ends as a serial port doesn’t seem to work. I could write() data to one end, but read() would never return on the other end.

def sim_serial():               # Doesn't work.
    fd1, fd2 = posix.openpty()
    p1 = serial.Serial(os.ttyname(fd1), 115200)
    p2 = serial.Serial(os.ttyname(fd2), 115200)

    return p1, p2

I didn’t find a workaround until I saw this pySerial test which opens one end as a file:

https://github.com/pyserial/pyserial/blob/master/test/test_pty.py

Answered By: remcycles
Categories: questions Tags: , ,
Answers are sorted by their score. The answer accepted by the question owner as the best is marked with
at the top-right corner.