Search code examples
c++linuxterminalserial-portmodem

Why does this serial/modem code mess up my terminal display?


I have written some code to find any modems on a unix system using the regex /dev/tty* basically and then for any matches see if can open the port and if so send an AT command and check if the response message contains the characters 'OK'.

The code does find a modem but unfortunately it messes up the terminal display. See below. I notice that it also prints the AT command - see output below. Why is my terminal display altered and how can I fix that?

After running the program, if you enter a command and enter, eg ls, the command is not shown but when you press enter you do see the output.

Here is the code:

#include <iostream>
#include <string>
#include <unordered_map>
#include <iomanip>

#include <memory>

#include <sstream>
#include <thread>

#include <iostream>
#include <filesystem>
#include <regex>

#include <unistd.h>  // close
#include <fcntl.h>   // open, O_RDWR, etc
#include <termios.h>

#include <string.h>

#include <sys/select.h>  // timeouts for read

#include <sys/timeb.h>   // measure time taken

int set_interface_attribs(int fd, int speed)
{
    struct termios tty;

    if (tcgetattr(fd, &tty) < 0) {
        // Error from tcgetattr - can use strerror(errno)
        return -1;
    }

    cfsetospeed(&tty, (speed_t)speed);
    cfsetispeed(&tty, (speed_t)speed);

    tty.c_cflag |= (CLOCAL | CREAD);    /* ignore modem controls */
    tty.c_cflag &= ~CSIZE;
    tty.c_cflag |= CS8;         /* 8-bit characters */
    tty.c_cflag &= ~PARENB;     /* no parity bit */
    tty.c_cflag &= ~CSTOPB;     /* only need 1 stop bit */
    tty.c_cflag &= ~CRTSCTS;    /* no hardware flowcontrol */

    /* setup for non-canonical mode */
    tty.c_iflag &= ~(IGNBRK | BRKINT | PARMRK | ISTRIP | INLCR | IGNCR | ICRNL | IXON);
    tty.c_lflag &= ~(ECHO | ECHONL | ICANON | ISIG | IEXTEN);
    tty.c_oflag &= ~OPOST;

    /* fetch bytes as they become available */
    tty.c_cc[VMIN] = 1;
    tty.c_cc[VTIME] = 1;

    if (tcsetattr(fd, TCSANOW, &tty) != 0) {
      // Error from tcsetattr- use strerror(errno)
        return -1;
    }
    return 0;
}

long enumerate_ports(std::unordered_map <std::string, std::string>& ports) {

    // ls /dev | grep ^tty.*
    const std::regex my_filter( "^tty.*" );
    std::string path = "/dev/";
    for (const auto & entry : std::filesystem::directory_iterator(path)) {
     std::smatch sm;

     std::string tmp = entry.path().filename().string();
     // if we have a regex match attempt to open port and send AT command
     if (std::regex_match(tmp, sm, my_filter)) {
     std::string portname = entry.path().string();
         int fd = ::open(portname.c_str(), O_RDWR | O_NOCTTY);
         if (fd < 0) {
       // Error opening port
             continue;
         } else {
       // port was opened successfully
           // try to write AT command and do we get an OK response
           // baudrate 9600, 8 bits, no parity, 1 stop bit
           if(set_interface_attribs(fd, B9600) != 0) {
             ::close(fd);
         continue;
           }

           int wlen = ::write(fd, "AT\r\n", 4);
           if (wlen != 4) {
         // Error from write
               ::close(fd);
               continue;
           }

          // tcdrain() waits until all output written to the object referred 
          // to by fd has been transmitted.
           tcdrain(fd);

           fd_set set;
           struct timeval timeout;

           FD_ZERO(&set); /* clear the set */
           FD_SET(fd, &set); /* add our file descriptor to the set */

           timeout.tv_sec = 0;
           timeout.tv_usec = 100000; // 100 milliseconds

           // wait for data to be read or timeout
           int rv = select(fd + 1, &set, NULL, NULL, &timeout);
           if(rv > 0) {  // no timeout or error
               unsigned char buf[80];
               const int bytes_read = ::read(fd, buf, sizeof(buf) - 1);
               if (bytes_read > 0) {
                   buf[bytes_read] = 0;
                   unsigned char* p = buf;
                   // scan for "OK"
                   for (int i = 0; i < bytes_read; ++i) {
             if (*p == 'O' && i < bytes_read - 1 && *(p+1) == 'K') {
                       // we have a positive response from device so add to ports
                       ports[portname] = "";
                       break;
                     }
                     p++;
                   }
               }
        }
       ::close(fd);
         }
     }
   }

   return ports.size();
}


int main() {

    struct timeb start, end;
    int diff;
    ftime(&start);

    // get list of ports available on system
    std::unordered_map <std::string, std::string> ports;
    long result = enumerate_ports(ports);
    std::cout << "No. found modems: " << result << std::endl;
    for (const auto& item : ports) {
        std::cout << item.first << "->" << item.second << std::endl;
    }

    ftime(&end);
    diff = (int) (1000.0 * (end.time - start.time)
        + (end.millitm - start.millitm));

    printf("Operation took %u milliseconds\n", diff);
}

And the output:

acomber@mail:~/Documents/projects/modem/serial/gdbplay$ ls
main.cpp  main.o  Makefile  serial
acomber@mail:~/Documents/projects/modem/serial/gdbplay$ make serial
g++ -Wall -Werror -ggdb3 -std=c++17 -pedantic  -c main.cpp
g++ -o serial -Wall -Werror -ggdb3 -std=c++17 -pedantic  main.o -L/usr/lib -lstdc++fs
acomber@mail:~/Documents/projects/modem/serial/gdbplay$ sudo ./serial
[sudo] password for acomber: 
AT
No. found modems: 1
                   /dev/ttyACM0->
                                 Operation took 8643 milliseconds
                                                                 acomber@mail:~/Documents/projects/modem/serial/gdbplay$

Solution

  • Why does this serial/modem code mess up my terminal display?

    A precise answer requires you to post the terminal's settings prior to executing your code, i.e. stty -a.

    The code does find a modem but unfortunately it messes up the terminal display.

    The simplest (i.e. straightforward) workaround/solution is to adhere to the old (but rarely followed) advice of saving and then restoring the terminal's termios settings, as in this example.

    The simple changes needed in your code would be something like (please overlook mixing of C and C++; I only know C) the following patch.

     struct termios savetty;
    
     int set_interface_attribs(int fd, int speed)
     {
    +    struct termios tty;
    
         if (tcgetattr(fd, &tty) < 0) {
             // Error from tcgetattr - can use strerror(errno)
             return -1;
         }
    +    savetty = tty;    /* preserve original settings for restoration */
    
         cfsetospeed(&tty, (speed_t)speed);
         cfsetispeed(&tty, (speed_t)speed);
    

    Then in enumerate_ports(), the last two instances of ::close(fd); need to be replaced with the sequence that will perform the restoration:

    +    if (tcsetattr(fd, &savetty) < 0) {
    +        // report cannot restore attributes
    +    }
         ::close(fd);
    

    After running the program, if you enter a command and enter, eg ls, the command is not shown ...

    That is obviously the result of leaving the ECHO attribute cleared.
    The "missing" carriage returns are probably due to a cleared OPOST.
    Other salient attributes that were cleared by your program but probably are expected to be set by the shell are ICANON, ICRNL, and IEXTEN.
    But rather than try to determine what exactly needs to be undone, the proper and guaranteed fix is to simply restore the termios settings back to its original state.

    An alternative (lazy) approach would to use the stty sane command after you execute your program.