Search code examples
clinuxserial-portttytermios

Why is it that when this ttyUSB is addressed via C, sometimes something is written to stdout and sometimes not?


Hi I have the following code written in c for a ttyUSB device. Some parts of the code are from another source:

// C library headers
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
// Linux headers
#include <fcntl.h> // Contains file controls like O_RDWR
#include <errno.h> // Error integer and strerror() function
#include <termios.h> // Contains POSIX terminal control definitions
#include <unistd.h> // write(), read(), close()
// Custom headers
//// klammern
////#include "some_functions.h"

////int write_log(char*);

int main(int argc, char* argv[]) {
  if (argc < 2) {
    printf("Usage: %s <at-command>\n", argv[0]);
    exit(-1);
  }
  int serial_port = open("/dev/ttyUSB4", O_RDWR);
  // Check for errors
  if (serial_port < 0) {
      printf("Error %i from open: %s\n", errno, strerror(errno));
      exit(1);
  }
  struct termios tty;

  // Read in existing settings, and handle any error
  if(tcgetattr(serial_port, &tty) != 0) {
      printf("Error %i from tcgetattr: %s\n", errno, strerror(errno));
      return 1;
  }

  tty.c_cflag &= ~PARENB; // Clear parity bit, disabling parity (most common)
  tty.c_cflag &= ~CSTOPB; // Clear stop field, only one stop bit used in communication (most common)
  tty.c_cflag &= ~CSIZE; // Clear all bits that set the data size
  tty.c_cflag |= CS8; // 8 bits per byte (most common)
  tty.c_cflag &= ~CRTSCTS; // Disable RTS/CTS hardware flow control (most common)
  tty.c_cflag |= CREAD | CLOCAL; // Turn on READ & ignore ctrl lines (CLOCAL = 1)

  tty.c_lflag &= ~ICANON;
  tty.c_lflag &= ~ECHO; // Disable echo
  tty.c_lflag &= ~ECHOE; // Disable erasure
  tty.c_lflag &= ~ECHONL; // Disable new-line echo
  tty.c_lflag &= ~ISIG; // Disable interpretation of INTR, QUIT and SUSP
  tty.c_iflag &= ~(IXON | IXOFF | IXANY); // Turn off s/w flow ctrl
  tty.c_iflag &= ~(IGNBRK|BRKINT|PARMRK|ISTRIP|INLCR|IGNCR|ICRNL); // Disable any special handling of received bytes

  tty.c_oflag &= ~OPOST; // Prevent special interpretation of output bytes (e.g. newline chars)
  tty.c_oflag &= ~ONLCR; // Prevent conversion of newline to carriage return/line feed
  // tty.c_oflag &= ~OXTABS; // Prevent conversion of tabs to spaces (NOT PRESENT ON LINUX)
  // tty.c_oflag &= ~ONOEOT; // Prevent removal of C-d chars (0x004) in output (NOT PRESENT ON LINUX)

  tty.c_cc[VTIME] = 50;    // Wait for up to 1s (10 deciseconds), returning as soon as any data is received.; 5 sek ^= 50
  tty.c_cc[VMIN] = 0;

  // Set in/out baud rate to be 9600
  cfsetispeed(&tty, B9600);
  cfsetospeed(&tty, B9600);

  // Save tty settings, also checking for error
  if (tcsetattr(serial_port, TCSANOW, &tty) != 0) {
      printf("Error %i from tcsetattr: %s\n", errno, strerror(errno));
      return 1;
  }

  // Write to serial port
  //unsigned char msg[] = { 'a', 't', '+', 'c', 'g', 'p', 'a', 'd', 'd', 'r', '=', '1', '\r' };
  unsigned int argv1_len = strlen(argv[1]);
  char msg[argv1_len+2]; // unsigned char
  strcpy(msg, argv[1]); 
  ////write_log(msg);
  strcat(msg, "\r");
  
  write(serial_port, msg, sizeof(msg));
  
  // Allocate memory for read buffer, set size according to your needs
  char read_buf [256];

  // Normally you wouldn't do this memset() call, but since we will just receive
  // ASCII data for this example, we'll set everything to 0 so we can
  // call printf() easily.
  memset(&read_buf, '\0', sizeof(read_buf));

  // Read bytes. The behaviour of read() (e.g. does it block?,
  // how long does it block for?) depends on the configuration
  // settings above, specifically VMIN and VTIME
  int num_bytes = read(serial_port, &read_buf, sizeof(read_buf));

  // n is the number of bytes read. n may be 0 if no bytes were received, and can also be -1 to signal an error.
  if (num_bytes < 0) {
      printf("Error reading: %s", strerror(errno));
      close(serial_port);
      return 1;
  }else if (num_bytes == 0) {
      printf("0");
      close(serial_port);
      return 1;
  }

  // Here we assume we received ASCII data, but you might be sending raw bytes (in that case, don't try and
  // print it to the screen like this!)
  //printf("Read %i bytes. Received message: %s", num_bytes, read_buf);
  printf("%s", read_buf);
  fflush(stdout); 
 
  ////removeLeadingNewlines(read_buf);
  
  ////write_log(read_buf);

  close(serial_port);
  return 0; // success
}

And this is my Makefile, it builds with no errors or warnings:

WARNFLAGS = -W -Wall -Werror
OPTFLAGS = -O3
DEBUGFLAGS = -ggdb3 -DDEBUG
CFLAGS += $(WARNFLAGS)
binaries = at_commander

ifdef DEBUG
  CFLAGS += $(DEBUGFLAGS)
else
  CFLAGS += $(OPTFLAGS)
endif

all: $(binaries)

at_commander: some_functions.c

clean:
    $(RM) *~ $(binaries) *.o

The question is why what is now being output to the console irregularly:

root@OpenMPTCProuter:~/autostart# ./at_commander at
root@OpenMPTCProuter:~/autostart# ./at_commander at
root@OpenMPTCProuter:~/autostart# ./at_commander at
root@OpenMPTCProuter:~/autostart# ./at_commander at

OK
root@OpenMPTCProuter:~/autostart# ./at_commander at
root@OpenMPTCProuter:~/autostart# ./at_commander at
root@OpenMPTCProuter:~/autostart# ./at_commander at

OK
root@OpenMPTCProuter:~/autostart# ./at_commander at

OK
root@OpenMPTCProuter:~/autostart# ./at_commander at
root@OpenMPTCProuter:~/autostart# ./at_commander at
root@OpenMPTCProuter:~/autostart# ./at_commander at

OK
root@OpenMPTCProuter:~/autostart#

First I thought it has something to do with flushing, but maybe it has something to do with wrong timing in my code.

Thank you in advance!


Solution

  • Why is it that when this ttyUSB is addressed via C, ...

    "A ttyUSB device" is not an end-point device (such as a flash drive), but rather a communications interface. Presumably you have something connected to this "ttyUSB device" using a cable, and your intent is to communicate with that connected device. However you completely neglect to mention anything about any other device.

    ... sometimes something is written to stdout and sometimes not?

    Your use of the noun "something" is ambiguous and completely disregards the fact that this involves (a) a command message is transmitted by your program, (b) that message has to be received and processed by a remote unit, (c) a response message has to be transmitted by that remote unit, and (d) that response message has to be received/read by your program.
    In other words that "something" is a response from a remote unit/device, and the failure (of your program) to "sometimes not" indicates that all of the required steps with that remote unit/device in that sequence were not successful .

    There are only a few clues in your post to identify the connected device that your program is trying to communicate with. One is a commented-out initialization. There are references to "at-command". So apparently the connected device is a modem that uses the AT (aka Hayes) command set.


    Your program as posted has a variety of minor issues, most of which have been mentioned in comments. IMO the salient bug in your program is expecting to receive the entire response when using a noncanonical read() with a timeout.

    Your program configures the serial terminal to noncanonical mode using the assignment

      tty.c_lflag &= ~ICANON;
    

    The two termios attributes that control when and what a read() will return are assigned the following (note the discrepancy between the actual value and the comment):

      tty.c_cc[VTIME] = 50;    // Wait for up to 1s (10 deciseconds), returning as soon as any data is received.; 5 sek ^= 50
      tty.c_cc[VMIN] = 0;
    

    Per the termios man page, you have setup a read with timeout:

       MIN == 0, TIME > 0 (read with timeout)
             TIME specifies the limit for a timer in tenths of a second.  The timer is started when read(2) is called.  read(2) returns  either  when  at
             least  one byte of data is available, or when the timer expires.  If the timer expires without any input becoming available, read(2) returns
             0.  If data is already available at the time of the call to read(2), the call behaves as though the data was received immediately after  the
             call.
    

    The salient point is that the timeout will occur only when no data is available. This configuration does not mean that read() will wait for the VTIME duration, and then return with all available data. Rather, as soon as at least one byte is available, the read() could return (with just that single byte).

    Your program's results indicates that the full response from the modem is the string \r\nOK\r\n. That is six character consisting of CR, LF,'O', 'K', CR, and LF. This is indicated on the "good" output of a blank line after the command, the OK text, and your shell prompt showing up on a new line. Omit any of the mentioned control characters, and you would then see a different "good" output.

    When your program "sometimes not" display a response, the read() probably returned with only the first character of the six-byte response. When your program prints that first character of the response, you do not notice it because (1) it's a control character (i.e. carriage return) rather than a printable character, and (2) the cursor is already at the first column, so another CR does nothing.

    To verify this hypothesis, revise the print statement to be more informative and distinctive (as @Gerhardh suggested in his comment).

    -   printf("%s", read_buf);
    +   printf("received %d: \'%s\'\n", num_bytes, read_buf);
    


    To read the entire response try

      tty.c_cc[VTIME] = 2;    // rtn after 0.2 sec of idle after first char
      tty.c_cc[VMIN] = 80;