1

I have a smart energy meter which sends energy consumption data every second. The daemon program I've written (C++/C Arch Linux) to read the data doesn't exit when the USB cable is disconnected and stalls indefinitely in the blocking read() call.

How to interrupt a blocking read() call (i.e. fail with EINTR instead of waiting for the next char)?

I extensively searched Google and looked here in SO and could not find an answer to this problem.

Details:

  • Smartmeter project source on Github
  • IR dongle with FT232RL USB to UART bridge
  • Datagrams have fixed length of 328 bytes sent every second
  • Read method detects beginning \ and end ! markers of a datagram
  • sigaction to catch CTRL+C SIGINT and SIGTERM signals
  • termios is setup to do blocking read() with VMIN = 1 and VTIME = 0.

Tried:

  • Playing with VMIN and VTIME
  • Removed SA_RESTART

Possible solution:

  • Use a non-blocking read method, maybe with select() and poll()
  • Or VMIN > 0 (datagram is longer than 255 characters and I would need to read the datagram in smaller chunks)
  • Not sure how to handle datagram begin/end detection and the one second interval between datagrams for a non-blocking read method

EDIT: The code below now buffers the read() call into an intermediate buffer of 255 bytes (VMIN = 255 and VTIME = 5) adapted from here. This avoids the small overhead of calling read() for every char. In practise this doesn't make a difference compared to reading one char at a time though. Read() still doesn't exit gracefully on cable disconnect. The daemon needs to be killed with kill -s SIGQUIT $PID. SIGKILL has no effect.

main.cpp:

volatile sig_atomic_t shutdown = false;

void sig_handler(int)
{
  shutdown = true;
}

int main(int argc, char* argv[])
{
  struct sigaction action;
  action.sa_handler = sig_handler;
  sigemptyset(&action.sa_mask);
  action.sa_flags = SA_RESTART;
  sigaction(SIGINT, &action, NULL);
  sigaction(SIGTERM, &action, NULL);

  while (shutdown == false)
  {
      if (!meter->Receive())
      {
        std::cout << meter->GetErrorMessage() << std::endl;
      return EXIT_FAILURE;
      }
  }

Smartmeter.cpp:

bool Smartmeter::Receive(void)
{
  memset(ReceiveBuffer, '\0', Smartmeter::ReceiveBufferSize);  
  if (!Serial->ReadBytes(ReceiveBuffer, Smartmeter::ReceiveBufferSize)) 
  {
    ErrorMessage = Serial->GetErrorMessage();
    return false;
  }
}

SmartMeterSerial.cpp:

#include <cstring>
#include <iostream>
#include <thread>
#include <unistd.h>
#include <termios.h>
#include <sys/file.h>
#include <sys/ioctl.h>
#include "SmartmeterSerial.h"

const unsigned char SmartmeterSerial::BufferSize = 255;

SmartmeterSerial::~SmartmeterSerial(void)
{
  if (SerialPort > 0) {
    close(SerialPort);
  }
}

bool SmartmeterSerial::Begin(const std::string &device)
{
  if (device.empty()) {
    ErrorMessage = "Serial device argument empty";
    return false;
  }
  if ((SerialPort = open(device.c_str(), (O_RDONLY | O_NOCTTY))) < 0)
  {
    ErrorMessage = std::string("Error opening serial device: ") 
      + strerror(errno) + " (" + std::to_string(errno) + ")";
    return false;
  }
  if(!isatty(SerialPort))
  {
    ErrorMessage = std::string("Error: Device ") + device + " is not a tty.";
    return false;
  }
  if (flock(SerialPort, LOCK_EX | LOCK_NB) < 0)
  {
    ErrorMessage = std::string("Error locking serial device: ")
      + strerror(errno) + " (" + std::to_string(errno) + ")";
    return false;
  }
  if (ioctl(SerialPort, TIOCEXCL) < 0)
  {
    ErrorMessage = std::string("Error setting exclusive access: ") 
      + strerror(errno) + " (" + std::to_string(errno) + ")";
    return false;
  }

  struct termios serial_port_settings;

  memset(&serial_port_settings, 0, sizeof(serial_port_settings));
  if (tcgetattr(SerialPort, &serial_port_settings))
  {
    ErrorMessage = std::string("Error getting serial port attributes: ")
      + strerror(errno) + " (" + std::to_string(errno) + ")";
    return false;
  }

  cfmakeraw(&serial_port_settings);

  // configure serial port
  // speed: 9600 baud, data bits: 7, stop bits: 1, parity: even
  cfsetispeed(&serial_port_settings, B9600);
  cfsetospeed(&serial_port_settings, B9600);
  serial_port_settings.c_cflag |= (CLOCAL | CREAD);
  serial_port_settings.c_cflag &= ~CSIZE;
  serial_port_settings.c_cflag |= (CS7 | PARENB);
  
  // vmin: read() returns when x byte(s) are available
  // vtime: wait for up to x * 0.1 second between characters
  serial_port_settings.c_cc[VMIN] = SmartmeterSerial::BufferSize;
  serial_port_settings.c_cc[VTIME] = 5;

  if (tcsetattr(SerialPort, TCSANOW, &serial_port_settings))
  {
    ErrorMessage = std::string("Error setting serial port attributes: ") 
      + strerror(errno) + " (" + std::to_string(errno) + ")";
    return false;
  }
  tcflush(SerialPort, TCIOFLUSH);

  return true;
}

char SmartmeterSerial::GetByte(void)
{
  static char buffer[SmartmeterSerial::BufferSize] = {0};
  static char *p = buffer;
  static int count = 0;   

  if ((p - buffer) >= count)
  {
    if ((count = read(SerialPort, buffer, SmartmeterSerial::BufferSize)) < 0)
    {
      // read() never fails with EINTR signal on cable disconnect   
      ErrorMessage = std::string("Read on serial device failed: ")
        + strerror(errno) + " (" + std::to_string(errno) + ")";
      return false;
    }
    p = buffer;
  }
  return *p++;
}

bool SmartmeterSerial::ReadBytes(char *buffer, const int &length)
{
  int bytes_received = 0;
  char *p = buffer;
  bool message_begin = false;
  
  tcflush(SerialPort, TCIOFLUSH);
  
  while (bytes_received < length)
  {
    if ((*p = GetByte()) == '/')
    {
      message_begin = true;
    }
    if (message_begin)
    {
      ++p;
      ++bytes_received;
    }
  }
  if (*(p-3) != '!')
  {
    ErrorMessage = "Serial datagram stream not in sync.";
    return false;
  }
  return true;
}

Many thanks for your help.

apohl
  • 11
  • 3
  • Is this a duplicate of this article? [Detecting if a character device has disconnected in Linux in with termios api (c++)](https://stackoverflow.com/q/34170350/9014308) – kunif May 19 '21 at 11:24
  • Does the revised code in this article actually work? What is the setting of VMIN and VTIME, i.e. blocking or non-blocking read? I've seen this, but it wasn't conclusive to me. – apohl May 19 '21 at 13:19
  • This also seems to be less directly related to your question, but there is another article. [Linux read() call not returning error when i Unplug serial cable \[duplicate\]](https://stackoverflow.com/q/40214863/9014308) – kunif May 19 '21 at 15:45
  • I've seen this one also. But where is the solution in this SO? How does a program, which uses termios and does a blocking read(), return in case there is an error? My program only terminates *after* read() has received another byte, not within the blocking read call. In case the cable is diconnected it cannot receive another byte and hence stalls forever. Is there any solution to this? – apohl May 19 '21 at 17:20
  • You may be asking a XY question. Why is *"USB cable is disconnected"* a problem for your daemon? What about a stable USB connection with the host, but the meter fails & stops sending data; how is that handled? Is that really an error condition? – sawdust May 19 '21 at 21:11
  • @sawdust If the meter fails it is currently not handled. My network hub, where the meter is connected to an Odroid, is not on an UPS so in that case the whole house will loose power and everything is powered off anyway. But you are right with the _XY question_, in case there isn't a simple way to detect if the dongle has been physically disconnected I'll leave it this way and live with it. However, currently when the cable has been removed I can only exit with `kill -s SIGQUIT $PID`, which is annoying. – apohl May 20 '21 at 19:23

1 Answers1

0

While the code below is not a solution to the original question on how to interrupt a blocking read() call, at least it would be a vialble workaround for me. With VMIN = 0 and VTIME = 0 this is now a non-blocking read():

bool SmartmeterSerial::ReadBytes(char *buffer, const int &length)
{
  int bytes_received = 0;
  char *p = buffer;
  tcflush(SerialPort, TCIOFLUSH);
  bool message_begin = false;
  const int timeout = 10000;
  int count = 0;
  char byte;

  while (bytes_received < length) 
  {
    if ((byte = read(SerialPort, p, 1)) < 0)
    {
      ErrorMessage = std::string("Read on serial device failed: ")
        + strerror(errno) + " (" + std::to_string(errno) + ")";
      return false;
    }
    if (*p == '/')
    {
      message_begin = true;
    }
    if (message_begin && byte)
    {
      ++p;
      bytes_received += byte;
    }
    if (count > timeout)
    {
      ErrorMessage = "Read on serial device failed: Timeout";
      return false;
    }
    ++count;
    std::this_thread::sleep_for(std::chrono::microseconds(100));
  }

  if (*(p-3) != '!')
  {
    ErrorMessage = "Serial datagram stream not in sync.";
    return false;
  }
  return true;
}

However, I am still curious to know if it is actually possible to interrupt a blocking `read()´ as this workaround is constantly polling the serial port.

I believe reading one char at a time is not a problem as the received bytes from the UART are buffered by the OS - but constantly polling the buffer with read() is! Maybe I'll try a ioctl(SerialPort, FIONREAD, &bytes_available) before read(), although I don't know if this would actually make a difference.

Any suggestions?

apohl
  • 11
  • 3
  • *"this workaround is constantly polling the serial port"* -- It polls a system buffer (not the hardware), as you later mention. *"I believe reading one char at a time is not a problem..."* -- It's a system call, so there is overhead, e.g. switching CPU modes. It is inefficient. Using a FIONREAD ioctl is just a different syscall with similar overhead. – sawdust May 19 '21 at 19:06
  • So reading the UART buffer in chunks would be better similar to [How to handle buffering serial data](https://stackoverflow.com/questions/54392470/how-to-handle-buffering-serial-data/54394770#54394770)? Thats why I prefer the blocking read() - it just waits until a character is received. Calling read() 368 times is not as bad as 10000 times. – apohl May 19 '21 at 19:29
  • @sawdust 6.7 % single thread CPU time for non-buffered and non-blocking _vs._ negligible CPU time for non-buffered and blocking. Blocking read() is the clear winner! – apohl May 19 '21 at 19:42
  • *"So reading the UART buffer ..."* -- No, **read()** is several layers removed from the UART hardware, and fetches bytes from the serial terminal buffer. See [Linux serial drivers](http://www.linux.it/~rubini/docs/serial/serial.html) Of course blocking reads are more efficient. Don't poll for events and leave the scheduling to the OS. – sawdust May 19 '21 at 20:55
  • @sawdust So what is your suggested approach to read a fixed length message of 328 bytes with a one second delay between messages? I think I'll stick with the blocking read() approach posted in the topic question, but maybe read the message in chunks of say 100 bytes instead of one char at a time. The only piece missing to me is to gracefully exit the daemon on USB cable disconnect, which I haven't found a solution yet. – apohl May 20 '21 at 13:16
  • Where is this *"read the message in chunks of say 100 bytes"* coming from? With noncanonical reads, you cannot reliably obtain one complete datagram per **read()**. You can try, but your program still has to validate the message and be capable of reassembling fragments. The buffering code example you linked to can do that. Try it with VMIN=255 and VTIME=5. Focusing on just *"USB cable disconnect"* is IMO shortsighted; there's a bigger issue. – sawdust May 20 '21 at 18:26
  • _"The buffering code example you linked to can do that. Try it with VMIN=255 and VTIME=5"_: its now implemented and it makes no difference on CPU usage. Both versions - blocking and buffered (255 char read) _vs._ blocking and non-buffered (1 char read) consume 0.0 % CPU cycles. I stop here and move on - unless someone can post a solution to the original question. – apohl May 20 '21 at 19:54