0

I am using Termios in an (ressource constrained) embedded Linux platform (in C) to send commands and receive data from various tty peripherals (CP2102 USB-UART). Apparently there are various ways to do that and many are improper. I tried few with mitigated success, I came across something that works but i am not sure it is optimal or even correct :

#define BAUDRATE    115200
#define DEVICE      "/dev/ttyUSB0"
#define DATA        "BLK\r"

int handler(void){

    char buf[255];
    struct pollfd fds[1];
    int fd, ret, res, retry = 0;

connect:
    fd = open(DEVICE, O_RDWR | O_NOCTTY | O_NONBLOCK);
    if (fd == 0){
        perror(DEVICE);
        printf("Failed to open %s\n",DEVICE);
        sleepms(2000);
        if(retry++<5) goto connect;
        //exit(-1);
    }
    set_interface_attribs (fd, BAUDRATE, 0); // 8n1 no parity
    set_blocking (fd, 0);                    // not blocking
    fds[0].fd = fd;              // streams
    fds[0].events = POLLRDNORM;
    for (;;)
    {
        int count = write(fd, DATA, strlen(DATA));
        ret = poll(fds, 1, 1000);
        if (ret > 0){
            if (fds[0].revents & POLLHUP){ printf("Hangup\n"); close(fd); goto connect;}
            if (fds[0].revents & POLLRDNORM){
                res = read(fd,buf,255);
                if(!res){close(fd); goto connect;}
                buf[res-2]=0;
                printf("Received %d bytes : %s",res,buf);
            }
        }

    }
} 

Basically the command is sent, then its polling until some data come or timeout occurs. This worked for over 24h and shown no issue communication wise however there is one problem : if the peripheral is disconnected then i get the "hangout" notification but it never reconnects, i was expecting it would since i close the file and retry connection to interface.

Also, is poll is the best approach here? i dont want to spend CPU time in pure lost by polling (unless polling has some mecanism to release the CPU time to other threads) but i still want the call to be blocking until the timeout occurs or response arrive (i.e. i dont want to send command and get the response later with a callback). The response come from peripheral under few millisecond.

n.b. i know some eyes are bleeding because of the goto statement, no worries, goto has been used here to test quickly the "reconnect" approach (which didn't worked) but in the end if the connection has to be restarted it will be in a separate function, goto will never be used in the final implementation.

EDIT:

After rewrite. It works but there are still problems, mainly, when i disconnect the peripheral, Linux will randomly keep the port name or change it. Sometimes it will keep the port name for multiple consecutive disconnect, sometime it will rename it and stick with the new name for a while. So i have to find a way to identify the peripheral from USB and get current port name whatever it is.

On disconnect it throws error 9 from tcgetattr (coming from set_interface_attribs or set_blocking i think) but once it is reconnected and if Linux does not change the port name then it reconnects right away and restart to send and receive as it should, however, when Linux rename the port then it fail.

int fd=-1,retry=0;
struct pollfd fds[1];

int connect(void){

    if(fd==-1) fd = open(MODEMDEVICE, O_RDWR | O_NOCTTY | O_NONBLOCK);
    else return 0; // already opened
    printf("Connecting to fd #%d\n",fd);
    if (fd==0){
        perror(MODEMDEVICE);
        printf("Failed to open %s\n",MODEMDEVICE);
        retry++;
        sleepms(2000);
        return -1;
    }
    set_interface_attribs (fd, BAUDRATE, 0);    // 8n1 no parity
    set_blocking (fd, 0);                       // not blocking
    fds[0].fd = fd;                             // streams
    fds[0].events = POLLERR|POLLHUP|POLLRDNORM;
    return 0;
}
int handlePoll(){

    int ret=0,res=0;
    char buf[255];

    ret = poll(fds, 1, 1000); // 1000ms
    if (ret > 0){
        if (fds[0].revents & POLLERR){ close(fd); fd=-1; return -1; } // IO error
        if (fds[0].revents & POLLHUP){ close(fd); fd=-1; return -2; } // interface closed
        if (fds[0].revents & POLLRDNORM){
            res = read(fd,buf,255);
            if(!res){ close(fd); return -3; } // data receive error
            buf[res-2]=0;
            printf("Received %d bytes : %s\n",res,buf);
            return 0;
        }
        return -4; // unknown error
    }
    return -5; // timeout
}
int sendCMD(char* cmd){

    int res=1;
    retry=0;

    while(res && retry<5){ res=connect(); }
    if(res) return -7;
    if(retry>5) return -8;
    int len = strlen(cmd);
    int count = write(fd, cmd, len);
    if(count<len){ return -6;}
    return handlePoll();
}
int main(void){

    while(1){
        switch(sendCMD(DATA)){
        case -1: printf("IO error\n"); break;
        case -2: printf("Interface closed error\n"); break;
        case -3: printf("data receive error\n"); break;
        case -4: printf("Unknown error\n"); break;
        case -5: printf("Timeout\n"); break;
        case -6: printf("Command send error\n"); sleepms(200); break;
        case -7: printf("Interface open error\n"); break;
        case -8: printf("Cannot open interface after 5 try\n"); break;
        default: break;
        }
    }
}

I think there should be a better way to deal with disconnect (Detecting if a character device has disconnected in Linux in with termios api (c++))

marc_s
  • 732,580
  • 175
  • 1,330
  • 1,459
Owayl
  • 43
  • 5
  • unless polling has some mecanism to release the CPU tiem to other threads - thet's exactly what `poll()` call is for, to release the cpu for other processes (and threads). There are mostly two schools - `poll()` or `select()`. Except the horrible goto that jumps fron inside `for{if{if{` outside and some indentation subjective marks, this is good code. Would be good to set `fds[0].events = POLLERR|POLLHUP|POLLRDNORM` and also handle POLLERR. If you use `poll()`, you can just `set_blocking(, 1)`. – KamilCuk May 22 '19 at 20:41
  • Oh nice, thank you, i didnt realized it, comming from bare metal embedded systems where polling is very cycle expensive (unless there is an RTOS). OK i will check on fds[0].events = POLLERR|POLLHUP|POLLRDNORM, handle POLLERR, and then properly deal with disconnect / reconnect. – Owayl May 22 '19 at 20:45
  • 1
    You tagged `linux`, so `poll()` is probably implemented with GNU libc `glibc` and is forwarded to kenrel via `poll()` syscall. Then kernel set's your process to blocked state, so it is not executing, it's waiting for I/O. This code works, there is really no problem here, I'm voting as off-topic for stackoverflow. It could be better suited for https://codereview.stackexchange.com/ – KamilCuk May 22 '19 at 20:47
  • Yes indeed, it is implemented in GNU libc, thank you for the review ! – Owayl May 22 '19 at 20:50
  • Sorry, but your 2nd version still has the same problems as the 1st (and maybe more!) and could only work by fluke. –  May 22 '19 at 23:56

1 Answers1

0
connect:
    fd = open(DEVICE, O_RDWR | O_NOCTTY | O_NONBLOCK);
    if (fd == 0){
        perror(DEVICE);

The open(2) doesn't return 0, but -1 in case of error. 0 is a valid file descriptor -- by convention the standard input.

        int count = write(fd, DATA, strlen(DATA));
        ret = poll(fds, 1, 1000);
        if (ret > 0){

Why aren't you checking the return value of write()? If the open() above actually failed and returned -1, this write() will fail too, return -1 and set errno to EBADF.

                res = read(fd,buf,255);
                if(!res){close(fd); goto connect;}
                buf[res-2]=0;

Ditto, you don't care if read() fails, which it will certainly do if the open() above failed to open the file. Just like open(), write() and all system calls, read() will return -1 in case of error.

Since your file descriptor is non-blocking, be prepared to handle transient errors like EAGAIN separately [2].

In the case where res == -1 the buf[res-2]=0 will corrupt memory by writing before the start of buf.

Notice that poll() will not set POLLERR in revents if passed an invalid fd -- if will either ignore it if it's negative (like the fd returned by your failing open()), or set POLLNVAL if it's positive.

            if (fds[0].revents & POLLHUP){ printf("Hangup\n"); close(fd); goto connect;}
            if (fds[0].revents & POLLRDNORM){

POLLHUP and POLLRDNORM[1] can be returned together in revents, signaling the last possible read, which your code will miss.

I strongly suggest you strace(1) your program, which you could do while it's running, with strace -p PID.

[1] btw, POLLRDNORM is equivalent to POLLIN in Linux.

[2] and EINTR, if your program or the libraries it's using are setting any signal handler.