3

Summary

I have two programs; one written in Python3, another in C++. Both perform the same task; read from a serial port, filter out the two header floats, and print the remaining message. The Python script works properly (see the output below for the proper numbers); the C++ one, using a serial library, does not and I can't figure out why. (Running on Raspberry Pi 4, Raspbian Buster).

In order to save anyone from reading this whole thing; if my decision to use this library for serial reading is bad, how can I properly go about reading from a serial port in C++?

What I've tried

I'm very new to C++, so perhaps I'm looking in all the wrong places, but I couldn't find a universally accepted library for reading from a serial port, so I picked the one with the most stars on github (serial). This answer gives a sample for windows and links to a couple libraries, however they either are intended to work with windows, or are in C, not C++. This uses another library. This is in C (my code will be compiled alongside a Simulink-based C++ class, so I think I need to stick with C++ (?))

My Code

Python

Here is the fully functional Python code:

import serial
import struct
import time

PORT_NUMBER = '/dev/ttyACM0'
BAUDRATE = 115200
MESSAGE_LENGTH = 8 
HEADER_NUMBER = float(112)

header_1_received = False       # Has the first header byte been received
header_2_received = False       # Has the second header byte been received
dat = []                        # The actual message

# Open serial port
ser = serial.Serial(
        port=PORT_NUMBER,
        baudrate=BAUDRATE,
        parity=serial.PARITY_NONE,
        stopbits=serial.STOPBITS_ONE,
        timeout=0.001)

ser.isOpen()
readings = 0

print('Receive data from Pixhawk using 2 header floats and {} message floats (32-bit) \nIf you wish to close the program, hit \"Ctrl+C\" on your keyboard and it (should) shut down gracefully.'.format(MESSAGE_LENGTH))
start_time = time.time()        # Start time for the program

try:
    # Main loop
    while (True):
        # Read 4 bytes (32-bits) to get a full float number
        buffer = ser.read(4)
        # Only proceed if the buffer is not empty (an empty buffer is b'')
        if buffer != b'':
            # Since struct.unpack() returns a tuple, we only grab the first element
            try:
                new_dat = struct.unpack("f",buffer)[0]
                if header_1_received==True and header_2_received==True:
                    dat.append(new_dat)
                elif new_dat == HEADER_NUMBER:
                    # We found a header single; treat it
                    if header_1_received == False:
                        header_1_received = True
                    elif header_2_received == False:
                        header_2_received = True
                    else:
                        # Since below, we reset headers once the full message is received, kind of pointless else
                        pass
                else:
                    # If a non-header character is received, but we haven't identified the headers yet, then we're starting in the middle of a message or have lost the rest of our previous message
                    dat = []
                    header_1_received = False
                    header_2_received = False
            except:
                # struct.unpack likely failed; throw away the message and start again
                header_1_received = False
                header_2_received = False
                dat = []
            if(len(dat) == MESSAGE_LENGTH):
                # Reset flags
                #print(dat)
                header_1_received = False
                header_2_received = False
                dat = []
                readings += 1
except KeyboardInterrupt:
    ser.close()
    elapsed_time = time.time() - start_time
    if readings > 0:
        print("Number of readings: {}\nRun time: {}s\nAverage time per reading: {}s ({}ms)".format(readings,elapsed_time,elapsed_time/readings,(elapsed_time/readings)*1000))

C++

Here is the dysfunctional C++ code:

#include <string>
#include <iostream>
#include <cstdio>
#include <unistd.h>
#include "serial/serial.h"

using std::string;
using std::exception;
using std::cout;
using std::cerr;
using std::endl;
using std::vector;

int run(int argc, char **argv)
{
  // Argument 1 is the serial port or enumerate flag
  string port(argv[1]);

  // Argument 2 is the baudrate
  unsigned long baud = 0;
  sscanf(argv[2], "%lu", &baud);

  // port, baudrate, timeout in milliseconds
  serial::Serial my_serial(port, baud, serial::Timeout::simpleTimeout(0.001));

  cout << "Is the serial port open?";
  if(my_serial.isOpen())
    cout << " Yes." << endl;
  else
    cout << " No." << endl;

  /* MY CUSTOM VARIABLES */
  const float header = 112;
  const int msg_size = 8;
  int msg_index = 0;                
  float f;                  // the read float
  float msg [msg_size] = { };           // the collected floats will be placed here, auto-filled with 0s
  bool header_1_received = false;
  bool header_2_received = false;
  uint8_t *buffer = new uint8_t[sizeof(f)]; // buffer that will be converted to 32-bit float
  int count = 0;

  while (count < 1000) {

    size_t number_of_bytes_read = my_serial.read(buffer, sizeof(f));
    memcpy(&f, buffer, sizeof(f));

    // Logic for adding new element to array
    if (header_1_received and header_2_received){
      msg[msg_index] = f;
      msg_index += 1;
    } else if (f == header) {
      if (header_1_received == false){
        header_1_received = true;
      } else if (header_2_received == false){
        header_2_received = true;
      } else {
        // Do nothing
      }
    } else {
      // A non-header character was received, but headers are also not identified;
      // Throw partial message away and restart
      std::fill_n(msg, msg_size, 0);       // Fill with zeroes
      msg_index = 0;
      header_1_received = false;
      header_2_received = false;
    }

    // Check to see if message is full
    if(msg_index == msg_size){
      cout << "Msg: [";
      for (int i = 0; i < msg_size; i += 1){
        cout << msg[i] << ",";
      }
      cout << "]" << endl;
      // Reset flags
      header_1_received = false;
      header_2_received = false;
      std::fill_n(msg, msg_size, 0);
      msg_index = 0;
    }
    count += 1;
  }
  return 0;
}

int main(int argc, char **argv) {
  try {
    return run(argc, argv);
  } catch (exception &e) {
    cerr << "Unhandled Exception: " << e.what() << endl;
  }
}

The C++ library can be found here, and the documentation on the read method here. As far as I understand it, his read method writes the requested number of bytes (if available) to the buffer; there is a constant stream of incoming bytes from the other device, so I don't see this being the issue.

Expected Result

The Python3 script functions properly and outputs the following:

[0.08539174497127533, 0.17273111641407013, -9.816835403442383, 0.0, 0.0, 0.0, 0.0, 0.0]
[0.08539174497127533, 0.17273111641407013, -9.816835403442383, 0.0, 0.0, 0.0, 0.0, 0.0]
[0.08539174497127533, 0.17273111641407013, -9.816835403442383, 0.0, 0.0, 0.0, 0.0, 0.0]
[0.08539174497127533, 0.17273111641407013, -9.816835403442383, 0.0, 0.0, 0.0, 0.0, 0.0]

(it should be two very smaller numbers, followed by approximately -9.81, then 5 zeroes).

Actual Result

The C++ program can be built and executed by running the following command:

g++ serial_example.cc -lserial -L ../build/devel/lib -I../include -o test_serial
LD_LIBRARY_PATH=`pwd`/../build/devel/lib ./test_serial

and outputs the following:

[112,112,112,112,112,112,112,112,]
[112,-9.82691,-9.82691,-9.82691,-9.82691,-9.82691,-9.82691,0,]
[112,112,112,112,-9.82691,-9.82691,-9.82691,-9.82691,]
[112,112,112,112,112,112,112,112,]

If I add the following line

cout << "Float extracted from buffer: " << f << endl;

then it outputs every float it reconstructs from the read operation, resulting in a mish-mash of 9.81, 112 and 0s.

Question

What has gone wrong in my C++ program that causes it to read bytes/float differently than the Python program, and if the library is at fault, what is an alternative method or library for reading serial messages in C++?

Edit

After some troubleshooting with @Barmar and @Gaspa79, it seems that the number of bytes read by the library's read() method is inconsistent. I'll try re-writing my program and leave the new version as an answer.

Joshua O'Reilly
  • 109
  • 1
  • 9
  • 1
    You don't need the `buffer` variable, you can read directly into `f`: `my_serial.read(static_cast(&f), sizeof(f))` – Barmar Aug 29 '19 at 19:15
  • 1
    You don't really need to use a library to read from the serial port on Unix. See https://blog.mbedded.ninja/programming/operating-systems/linux/linux-serial-ports-using-c-cpp/ for how to do it using plain C or C++. – Barmar Aug 29 '19 at 19:23
  • @Jarod42, duly noted and changed! @Barmar I'll give reading directly into `f` a shot, then try from scratch without the library. Thanks! – Joshua O'Reilly Aug 29 '19 at 19:27
  • @JoshuaO'Reilly Maybe you're sometimes reading the same value more than once in C++ code, and sometimes you're missing one? Try moving the "else if (f == header)" to the first part of the if chain, and do it in a way that if both headers are received and you receive 112 you just do nothing and wait for the next value. That way you could see if the first part of my assumption is true and it would take like 30 seconds – Gaspa79 Aug 29 '19 at 19:30
  • @Barmar I get this error when replacing my `read` line with the direct one: `error: invalid static_cast from type ‘float*’ to type ‘uint8_t*’ {aka ‘unsigned char*’}` – Joshua O'Reilly Aug 29 '19 at 19:30
  • That's because f not an unsigned char (uint8_t typedef) – Gaspa79 Aug 29 '19 at 19:31
  • Try using `reinterpret_cast` instead. – Barmar Aug 29 '19 at 19:34
  • @Gaspa79 I re-arranged the if-else logic and no message printed; I then added `cout << f << endl;` to just see everything and get this group of numbers repeating: `0 0 0 0 0 6.1415e-39 3.93547e-26 -9.1325e-33 -8.98278e+23 1.77345e-38`. It seems that the floats being extracted from the buffer are not what they should be, so I'm thinking the if-else logic doesn't matter much until the actual float values are what they should be – Joshua O'Reilly Aug 29 '19 at 19:37
  • @Barmar Read directly using `my_serial.read(reinterpret_cast(&f), sizeof(f));` and get the same numbers as the comment I put above in response to @Gaspa79 instead of the correct ones – Joshua O'Reilly Aug 29 '19 at 19:40
  • I wasn't suggesting it as a solution to the problem, just simpler code. – Barmar Aug 29 '19 at 19:41
  • I have a suspicion that the problem is with byte ordering. – Barmar Aug 29 '19 at 19:43
  • @JoshuaO'Reilly I think you're right. Btw dude, you sure you're not flipping the byte order? I'm not 100% sure but when you do a memcopy you are putting the first received byte at the "end" – Gaspa79 Aug 29 '19 at 19:44
  • @JoshuaO'Reilly also be absolutely sure that your platform is using IEEE754 32 bit single precision floats. Just create a debug statement that always concatenates the binary representation of the uint8s and prints them to see if MSB or float standard has something to do with it. Sorry if this may end up being useless, it's just that it's where I would start if I had this problem – Gaspa79 Aug 29 '19 at 19:47
  • @Barmar Oops, sorry! – Joshua O'Reilly Aug 29 '19 at 19:47
  • @Gaspa79 The order of the bytes being inverted didn't even occur to me. I created a second buffer, inverted the order (sloppy, I know), and then ran memcpy on that one. Different numbers, still way off. I'll give the concatenation a try (might take a smidge, figuring this out as I go) – Joshua O'Reilly Aug 29 '19 at 19:49
  • @Gaspa79 I copied the quick test a guy wrote [here](https://stackoverflow.com/questions/507819/pi-and-accuracy-of-a-floating-point-number) in the top answer. The float varies at the 16th decimal placed; any idea what that's indicative of? – Joshua O'Reilly Aug 29 '19 at 19:54
  • @JoshuaO'Reilly yeah do the binary values so we can know if you're getting crap data or if you're just converting it wrong. Also maybe my_serial is doing some conversions, and if that happens the uint8s you are getting are already screwed. – Gaspa79 Aug 29 '19 at 19:54
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/198682/discussion-between-joshua-oreilly-and-gaspa79). – Joshua O'Reilly Aug 29 '19 at 19:57
  • @JoshuaO'Reilly that's normal, doubles have 16 decimals of precision. The rest is just conversion errors between binary and decimal and just exponent inaccuracies. – Gaspa79 Aug 29 '19 at 19:57
  • 1
    For anyone reading this in the future: the issue was that the underlying library was reading more than 4 bytes at a time =) – Gaspa79 Aug 29 '19 at 20:40
  • @Gaspa79 please add it as an answer and accept it for future readers. – B.Letz Aug 30 '19 at 08:48
  • @B.Letz I'm not the one who asked the question, but sure – Gaspa79 Aug 30 '19 at 13:22

1 Answers1

1

After verifying that the conversions were indeed correct, we realized that OP never actually checked the number_of_bytes_read variable, and the underlying library was reading different numbers of bytes for some reason.

Gaspa79
  • 5,488
  • 4
  • 40
  • 63