Search code examples
clinuxserial-porttermios

Trying to test C code by creating a simulated serial device


I hope this question hasn't been asked before, I have had a check and stumbled across this but it doesn't really help for the automated testing that I am trying to do.

I want to create a simple testing script that will be able to create a serial device, allow me to "echo 'w' > device" and get a value X back from it.

Manually using socat to create multiple points doesn't really help as my code works with viewing just one port, not a slave/master style one.

I've included my function which interacts with a serial port, and works with actual physical hardware!

int interactWithPort(char* portID, char BW, char* offOn){

  speed_t baud = B9600; // baud rate
  int fd = open(portID, (BW == 'B') ? O_RDWR : O_WRONLY); //Open the port with the correct RW settings

  struct termios settings; // structure for the settings that will be used for the port 
  tcgetattr(fd, &settings);

  cfsetospeed(&settings, baud); // baud rate 
  settings.c_cflag &= ~PARENB; // no parity 
  settings.c_cflag &= ~CSTOPB; // 1 stop bit 
  settings.c_cflag &= ~CSIZE;
  settings.c_cflag |= CS8 | CLOCAL; // 8 bits 
  settings.c_lflag = ICANON; // canonical mode 
  settings.c_oflag &= ~OPOST; // raw output 
  tcsetattr(fd, TCSANOW, &settings); // apply the settings 
  tcflush(fd, TCOFLUSH);
  fcntl(fd, F_SETFL, 0); // apply file control operations 

  // Initialize file descriptor sets
  fd_set read_fds, write_fds, except_fds;
  FD_ZERO(&read_fds);
  FD_ZERO(&write_fds);
  FD_ZERO(&except_fds);
  FD_SET(fd, &read_fds);

  // Set timeout to 1.0 seconds
  struct timeval timeout;
  timeout.tv_sec = 3; // Read timeout of around 3 seconds, any more than this and something has went wrong
  timeout.tv_usec = 1;

  int w = 0;
  if(BW == 'W'){
    w = (int) write(fd, offOn, 1); 
  }else{
    w = (int) write(fd, "w", 1); // writes to the port a w or a 1/0 depending on function 
  }

  //If theres an error in writing to the scales then tell us!
  if(w < 0)
    fprintf(stderr, "Error writting to device: %s\n", portID);

  //If we flip switch to water then return as it's worked
  if(BW == 'W') return w;


  // Wait for input to become ready or until the time out; the first parameter is
  // 1 more than the largest file descriptor in any of the sets
  if (select(fd + 1, &read_fds, &write_fds, &except_fds, &timeout) == 1) {

    //This buffer holds the data from the serial port
    char buffer[32]; //Could reduce this to around 18 in length but better to have more buffering room

    // fd is ready for reading
    u_short n = (u_short) read(fd, buffer, sizeof(buffer));  //Reads the length of the serial data

    buffer[n] = 0;

    char * result = deblank(buffer);

    close(fd); // close the connection

    return atoi(result); // convert the result to a number and cast to be short
  }
  else
    fprintf(stderr, "Timeout error\n");
  return 0; // timeout or error
}

I have played around with some python scripting but it still has the same problem of slave/master and can't read from the desired address, but can write to it no problem.

I have played around with something like this python script but it still has the slave/master setup that doesn't work for me:

import os
import pty
import serial
import time

master, slave = pty.openpty()
s_name = os.ttyname(slave)
ser = serial.Serial(s_name, baudrate=9600)

print(os.ttyname(slave))
print(os.ttyname(master))


# Create a symbolic link for the C program to use!
src = s_name
dst = "/home/nathan/test"
# This creates a symbolic link on python in tmp directory
os.symlink(src, dst)


# To Write to the device to get a reading
# ser.write('w'.encode('ascii'))

# To read from the device
while(True):
    if "w" in str(os.read(master, 1000)):
        ser.write("100".encode('ascii'))

        print("worked")
        break
    time.sleep(1)


print(os.read(slave, 1000))
# Clean up by deleting the temp link that we used
os.unlink(dst)

Solution

  • ... it still has the same problem of slave/master and can't read from the desired address, but can write to it no problem.

    Your code has missing salient initialization.

    The input baudrate should be specified:

    cfsetispeed(&settings, baud);
    

    The CREAD flag is required to enable the receiver:

    settings.c_cflag |= CREAD;
    

    It's also common to specify the serial port as a non-controlling terminal:

    fd = open(portID, O_RDWR | O_NOCTTY);
    

    For consistent logic (i.e. match your other tests), the conditional should be

    fd = open(portID, (BW == 'W' ? : O_WRONLY : O_RDWR) | O_NOCTTY);
    

    Between the write() and select(), a tcdrain operation (to wait until all output written has been transmitted) would be appropriate:

    tcdrain(fd);
    

    The big picture isn't obvious, but appears that you're opening, initializing, and closing the serial port just to write and read once; i.e. all that is in just one procedure.
    This should be typically be separated into at least three sections of a program for modularity.

    You seem to use blocking canonical mode, but output only one byte (i.e. where's the line terminator?).

    Overall your code fails to check the return codes from syscalls for error conditions. Since your program does not behave as expected, you could be ignoring salient information.