Why might the Serial Monitor show batches of messages?

Hello all.

I am working on a piece of code which compiles data from many sensors (for now represented as random data for testing purposes) into a JSON-formatted string to send over serial to an RPI.

I noticed while watching the serial monitor that the timestamp readout for the messages seem to come in groups of 2-3 messages all in the same millisecond, despite my code having a delay(). This might not be a problem but it makes me curious as to whether something is going on with my buffer filling up, or something else I do not know about.

The code:


#include <Wire.h>
#include <SensorMessage.h>
SensorMessage sensorMessage(0);

bool toggle = true;

void setup() {
  // put your setup code here, to run once:
  Serial.begin(115200);
}

void loop() {
  // put your main code here, to run repeatedly:
  if(toggle == true){
    sensorMessage.PT_HE = 1;                   ///< 16 bit unsigned integer representing the Helium presurrant supply
    sensorMessage.PT_Purge = random(0,500);                ///< 16 bit unsigned integer representing the Nitrogen purge supply
    sensorMessage.PT_Pneu = random(0,500);                 ///< 16 bit unsigned integer representing the Nitrogen pneumatic supply
    sensorMessage.PT_FUEL_PV = random(0,500);              ///< 16 bit unsigned integer representing the Gas above fuel in pressure vessel
    sensorMessage.PT_LOX_PV = random(0,500);               ///< 16 bit unsigned integer representing the Gas above LOX in pressure vessel
    sensorMessage.PT_FUEL_INJ = random(0,500);             ///< 16 bit unsigned integer representing the Fuel pressure before entering chamber
    sensorMessage.PT_CHAM = random(0,500);                 ///< 16 bit unsigned integer representing the Chamber pressure
    sensorMessage.TC_FUEL_PV = random(0,500);              ///< 16 bit unsigned integer representing the Gas above fuel in pressure vessel (possible surface mount)
    sensorMessage.TC_LOX_PV = random(0,500);               ///< 16 bit unsigned integer representing the Gas above LOX in pressure vessel (possible surface mount)
    sensorMessage.TC_LOX_Valve_Main = random(0,500);       ///< 16 bit unsigned integer representing the External sensor for valve chill in and force
    sensorMessage.RC_LOX_Level = random(0,500);            ///< 16 bit unsigned integer representing the Capacitive level sensor of LOX in pressure vessel
    sensorMessage.FT_Thrust = random(0,500);               ///< 16 bit unsigned integer representing the Load cell that measures thrust of engine
    sensorMessage.Send();
    delay(10);
    //toggle = false;
  }
}

It relies on a custom library called "SensorMessage" which has this in its header file:

#include "Arduino.h"
#include "SensorMessage.h"

/**
 * @brief Construct a new Sensor Message:: Sensor Message object
 * 
 * @param startCount 
 */
SensorMessage::SensorMessage(int startCount)
{
    count = startCount;
}

/**
 * @brief Builds and sends a serial information.
 * 
 */
void SensorMessage::Send()
{
    this->SetBuffer();
    Serial.println(buff);
}

/**
 * @brief Set the Buffer object
 * 
 */
void SensorMessage::SetBuffer()
{
    sprintf(buff, "{\"PT_HE\":%hu,\"PT_Purge\":%hu,\"PT_Pneu\":%hu,\"PT_FUEL_PV\":%hu,\"PT_LOX_PV\":%hu,\"PT_FUEL_INJ\":%hu,\"PT_CHAM\":%hu,\"TC_FUEL_PV\":%hu,\"TC_LOX_PV\":%hu,\"TC_LOX_Valve_Main\":%hu,\"RC_LOX_Level\":%hu,\"FT_Thrust\":%hu}",
            this->PT_HE, this->PT_Purge, this->PT_Pneu, this->PT_FUEL_PV, this->PT_LOX_PV, this->PT_FUEL_INJ, this->PT_CHAM, this->TC_FUEL_PV, this->TC_LOX_PV, this->TC_LOX_Valve_Main, this->RC_LOX_Level, this->FT_Thrust);
            this->count++;
}

I am trying to extract data as often as possible and continuously, but I do notice that with higher delay() values this effect disappears. What is it about low-delay code that can make this happen?

I would suspect the USB connection and your PC operating system/drivers. Your serial data is collected and send as packets. This is done ether by a separate USB chip in older Arduinos or the USB stack on Arduinos with native USB. Your PCs operating system is not real-time ether, so everything can be delayed by all kinds of things e.g., emails arriving, hard drives spinning up, running out of free RAM, virus scanners ...

Can you show the whole "SensorMessage" ? Perhaps the 'buff' is not large enough.

The numbers don't add up.
Suppose you are sending 100 characters. At 115200 baud, that will take 8.6 ms.
That will be transmitted in the background while the sketch continues.
However, the buffer size is only 64 (on some boards). That means the first 64 bytes fit in the buffer and the Serial.println() pauses the sketch and puts the next character into the buffer as soon as there is a free space.

So Serial.println() puts the first 64 bytes in the buffer, then getting the resulting 36 bytes into the buffer takes 3.1 ms. After that is the delay of 10 ms, while the 64 are still transmitted during the first 5.6 ms.

That would result in a delay of 13 ms instead of 10 ms and a gap between the data of 4.4 ms instead of 10 ms.
If you have a SAMD board or a Leonardo or a board with a other buffer size, then it is different. It is also different for a basic 16MHz board, because of the overhead at 115200 baud.

Save for some descriptive (and personally identifying) comments that I omitted from the top of the file, that is the entirety of the SensorMessage.cpp file.

This is the header file, which I should have shown in my first post:

#ifndef SensorMessage_h
#define SensorMessage_h

#include "Arduino.h"

/**
 * @brief 
 * 
 */
class SensorMessage
{
  public:
    /**
     * @brief Construct a new Sensor Message object
     * 
     * @param startCount 
     */
    SensorMessage(int startCount);

    /**
     * @brief Builds and sends a serial information.
     * 
     */
    void Send();

    uint16_t PT_HE;                   ///< 16 bit unsigned integer representing the Helium presurrant supply
    uint16_t PT_Purge;                ///< 16 bit unsigned integer representing the Nitrogen purge supply
    uint16_t PT_Pneu;                 ///< 16 bit unsigned integer representing the Nitrogen pneumatic supply
    uint16_t PT_FUEL_PV;              ///< 16 bit unsigned integer representing the Gas above fuel in pressure vessel
    uint16_t PT_LOX_PV;               ///< 16 bit unsigned integer representing the Gas above LOX in pressure vessel
    uint16_t PT_FUEL_INJ;             ///< 16 bit unsigned integer representing the Fuel pressure before entering chamber
    uint16_t PT_CHAM;                 ///< 16 bit unsigned integer representing the Chamber pressure
    uint16_t TC_FUEL_PV;              ///< 16 bit unsigned integer representing the Gas above fuel in pressure vessel (possible surface mount)
    uint16_t TC_LOX_PV;               ///< 16 bit unsigned integer representing the Gas above LOX in pressure vessel (possible surface mount)
    uint16_t TC_LOX_Valve_Main;       ///< 16 bit unsigned integer representing the External sensor for valve chill in and force
    uint16_t RC_LOX_Level;            ///< 16 bit unsigned integer representing the Capacitive level sensor of LOX in pressure vessel
    uint16_t FT_Thrust;               ///< 16 bit unsigned integer representing the Load cell that measures thrust of engine
    
  private:
    /**
     * @brief Set the Buffer object
     * 
     */
    void SetBuffer();

    uint8_t count;                    ///< Counts the number of messages being sent
    char buff[225];                   ///< Maximum char array of a sensor message
};

#endif

I do see that the definition of 'buff' is a particular number. I am not sure why the original author selected this.

Can I arbitrarily increase it?

You can increase it as long as your Arduino board has enough memory.

So a %hu is a unsigned short integer.
The string could be:

"{"PT_HE":12345,"PT_Purge":12345,"PT_Pneu":12345,"PT_FUEL_PV":12345,"PT_LOX_PV":12345,"PT_FUEL_INJ":12345,"PT_CHAM":12345,"TC_FUEL_PV":12345,"TC_LOX_PV":12345,"TC_LOX_Valve_Main":12345,"RC_LOX_Level":12345,"FT_Thrust":12345}"

That is 224 characters, plus a zero terminator makes 225. The buffer is exactly the maximum what is could be. If you add a comma, then things go wrong.

If your Arduino has enough memory, then you could make the buffer 256 bytes. But also the behavior of the buffer and speed and delay depends on which Arduino board you have. Some slow Arduino boards have overhead. Some Arduino boards ignore the baudrate and transfer data with maximum speed over the USB.

As it happens, I need to increase the length of the SensorMessage output to accommodate new sensors.

Based on what you are saying, it sounds like the absolute maximum is 256 bytes. Do I understand correctly?

If so, I will need to shorten the names of the keys in my JSON-like string. It sounds like shortening them should also improve performance in that I am using up the serial buffer a little bit less?

I'm also not sure I've fully understood what you were saying about the numbers not adding up. I can follow what you are saying, but I'm not making the connection between the delay of 13ms and what causes the messages to arrive at my serial monitor with identical timestamps.

The maximum is not 256 bytes, but perhaps you want to add a comma, so perhaps you can add a little extra. However that depends on which Arduino board you have.

Shorten the names might improve performance, but that depends on which Arduino board you have.

When you call Wire.println(), then the text is copied into a buffer and the sketch continues. The text will be send from an interrupt (while the sketch is running).
If you call, for example, a delay(10), then the Arduino is sending text during that delay.
But again, that depends on which Arduino board you have.

I am using an Elegoo UNO R3 which uses an MEGA16U2 chip and an ATMEGA328P chip. The Serial converter is, I believe, a 74HC595.

The source of my concern is that I will be inserting each of these messages into a database which accepts many entries in the same millisecond (redis). I do want to keep a chronological history which can be plotted but I worry that these kinds of issues can produce data which cannot be plotted simply. Certainly there will be other delays in execution that cause them to become staggered, so this may not manifest. Nonetheless I am curious.

Forgive me for belaboring the point but something is not clicking for me. It seems you are suggesting that for a string of 100 characters, at 115200baud, the math shakes out as follows:

115200 baud = 115200 bits/s = 11520 (byte + start + stop)/s.
100 bytes would then take 100/11520 = 8.7e-4 seconds = 8.7ms.
We can instantly load 64 bytes due to the buffer size. To load the remaining 36 bytes, we need to wait for the serial buffer to clear enough space: 36/11520 = 3.1ms.
When all of this is finished, the blocking Serial.println() unblocks and allows the program to continue to the delay(10).
The result of this is that the runtime of the sketch is 3.1ms + 10ms = 13.1ms.

On the receiving side, the program reading the serial transmission will have to clear all 100 bytes, which should take 8.7ms, as before.

This makes sense to me. So how then might 2 or 3 different messages arrive at the same millisecond time?

If the explanation is as simple as Klaus_K suggests then I will accept it and assume there will be no problems. If I am really concerned with this I can always add a timestamp to the string, I suppose.

Then you need to have the Arduino send timestamps that are collected along with each data point. The value of millis() or micros() could be used.

The PC timestamps are not very useful.

You probably don't need to buffer the data, either, just use Serial.print().

In outline, something like:

Serial.print(timestamp);
Serial.print(",");
Serial.print(variable_name);
Serial.print(",");
Serial.println(variable_value);

The 115200 baud is fast for the 16MHz Uno. The transmitted data will still be 115200 baud, but there will be gaps between the bytes, because the code for the interrupt has to run.
The full message might take longer than 10ms, so the next message starts when the buffer is not empty yet.

If you can shorten the message to be less than 64 bytes, then it will fit in the buffer. Then you can start with a delay of 100ms, and shorten it until you notice that things start to congest.

The Uno has only little SRAM memory, so the suggestion by @anon94801460 is very good.
Everything will work in the same way, so the total message should be better below 64 and a delay of 10ms might be too short.

The Raspberry Pi is a linux operating system. I suppose it uses a interrupt and uses a buffer as well. When you read the data, that is at a higher level which is influenced by the operating system. I suppose that can cause these problems as well. However, a Raspberry Pi is very fast compared to the Uno.

I see. I was under the mistaken impression that Serial.print and Serial.write were blocking. Now it is more clear. For the theoretical example of 100 characters being sent at 115200 baud, I would need to use a minimum theoretical delay of 3.1ms, plus the time for the interrupts to execute which is more difficult to quantify and likely exceeds the 10ms I initially used. Is this correct?

I think I will scrap this library and move the JSON creation onto the Pi, as suggested. I am gathering data from 13 sensors currently, so I do not think it is possible to get below 64 bytes (13 * (5 + 1) = 78, plus timestamp data and terminator) without splitting the load to another Arduino MCU, at which point I start to have to worry about synchronizing messages so that data arrives appropriately timestamped for databasing.

So I will do as you suggest, and manually step down the delay to approach the theoretical minimum until problems start to arise.

You have 13 sensors. Most sensors create 10 or 12-bit data. Lets say 16-bit (2 bytes). When you send the data raw you can reduce the data to 26 bytes plus a little bit of overhead for a little protocol or CRC.

I am using Adafruit's ADS1115 16-bit ADC to gather the information, so that is indeed possible. From the Serial.print()

To do so I would use something such as

#include <Adafruit_ADS1X15.h>

void setup() {
  Serial.begin(115200);
}

void loop(){
  int16_t adc0;
  adc0 = ads1115.readADC_SingleEnded(0);
  Serial.println(adc0);
}

directly, then? I was under the impression that this would render as ASCII characters in the buffer and thus take up more space. Perhaps I should extract the individual bytes and use them to compose the Serial.print() sequence?

You cannot use Serial.print but should use Serial.write instead. Print formats your data so you can read the values in ASCII code in the Serial Monitor. Write allows you to send a buffer of bytes. Then the data will no longer be human readable. But your PC/Raspberry Pi are not human. :slight_smile:

https://www.arduino.cc/reference/en/language/functions/communication/serial/write/

Thank goodness for that!

I see. In this case it seems simple enough to treat my variable adc0 as an array and use Serial.write(adc0). Or, use a buffer array and send that, in which case I need the bytes to identify a comma and a terminator character as well. Is that correct?

True, but why is that a problem? Transmission of human readable data is MUCH easier to understand and to debug if there is a problem.

True, but per @Klaus_K 's and @Koepel 's suggestions it seems that I can reduce my serial message size to be less than the buffer to increase the rate at which I can sample all of my sensors and feed this information upstream to the RPi.

However I am having trouble understanding how these conversions work. Currently, if I use this:

  uint16_t adc0 = 100;
  int bytesSent = Serial.write(adc0);
  //Serial.write( '\r' );
  Serial.write( '\n' );
  delay(1000);

The Serial monitor as well as my Python script running on the RPi yield d. I don't know how this value is obtained, nor do I know how to decode it.

I will continue to search.

That scheme won't work. There is nothing to distinguish the start and end of binary data, as the value of the character '\n' or '\r' can appear at any place in a binary message.

If you want to send binary data, you have to define a packet structure that both ends can agree on, and make sure that both ends of the connection agree on byte order of variables (look up "big and little endian").

There is a variant of Serial.write that takes a buffer and length (see link in reply #14). Create an array of bytes and copy all your data into the array (what jremington called packet structure). Then send the array. In your python script you will need to separate the array back into its components.

Because raw serial data can have any value as jremington said, you need to find a way to synchronize the transmission or in other words make sure the receiver knows when the next packet starts. This could be a hardware signal, or you format your data in a way that either 0x0000 or 0xFFFF are not valid. Then you send this value in between packets.

Hi guys. I got a bit busy but am back on the problem.

I've started to use unions to do as you suggested, and it is working pretty well. The code now looks like

#include <Wire.h>
#include <SensorMessage.h>
#include <stdint.h>
SensorMessage sensorMessage(0);

typedef union FourBytes{
    uint32_t timestamp;
    unsigned char bytes[4];
  };

typedef union TwoBytes {
    uint16_t data;
    unsigned char bytes[2];
  };

bool toggle = true;

void setup() {
  // put your setup code here, to run once:
  Serial.begin(115200);

  
}

void loop() {
FourBytes TimeStamp;
  
  TimeStamp.timestamp = millis()+16777217;
  //Serial.println(timestamp);
  Serial.write(TimeStamp.bytes, 4);
  Serial.write('\n');
  //Serial.println(TimeStamp.timestamp);
  delay(100);
}

And the messages come through on my Python side nicely. For that I am using

while ser.is_open == True:
      serial_buffer = ser.read_until()      // Defaults to '\n'
      print("================")
      print("Bytes:")
      print(serial_buffer.rstrip())
      serial_int = int.from_bytes(serial_buffer.rstrip(),"little",signed=False)
      print("Int:")
      print(serial_int)

However there are occasionally some instabilities on what gets sent through despite a delay of 100ms, which I believe to be what @anon94801460 and @Klaus_K were telling me about with specific "invalid" separators that I should use instead. This happens when \x0A (part of the bytes/hex representation of 16779817) is sent because it is, itself, a newline character, which gets stripped from my input currently. Obviously this isn't desired behavior, so...

I think I want to try and use the invalid packets. To do this I need to find a way to send 0xFFFFFFFF (because a timestamp using millis() would surpass 65535 rather quickly) right? I can do this using the same union method I employed above, and have ser.read_until() look for a byte string such as \xFF\xFF\xFF\xFF or \x00\x00\x00\x00. Then remove that from the input before conversion back to int. Does that sound like a proper implementation?