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$
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.