Problem MH-Z16 and Particle Software Serial

Hello everyone, I hope you are very well. I am trying to read an MH-Z16 sensor, on a Photon using the Particle Software Serial. It is assumed that if I send the reading frame, the sensor answers me with 9 bytes, however, the Photon returns 18 bytes, at the end part of the reading, the final byte of chk does not arrive. My reading code is:

while((len!=9 && ntry<nmtry) || len==0)
      {
      MHZ_16.flush();
      Log.info("Available--: %d", len);
      MHZ_16.write(cmd_get_sensor,9);
      delay(200);
      len = MHZ_16.available();
      ntry++;
      Log.info("NumMHZ try-: %d", ntry);
      }
  Log.info("LenMHZ-----: %d", len);

The response example is: FFFFFFFFFFFFFFFFFFFF8619443000A2.

Do you have any idea what might be happening?, thanks.

The code snippet does not tell us everything we’d need to properly assess the situation.

For one, when you say this is your reading code, how come that there are not MHZ_16.read() statements?

BTW, do you already use the HW UART Serial1 for something else, or why are you using a software implementation?

2 Likes

Hi, I attach the complete code:

#include <ParticleSoftSerial.h>
//------------------------------------------------------------------------------
ParticleSoftSerial MHZ_16(D5, D6);
//------------------------------------------------------------------------------
const unsigned char cmd_get_sensor[] = {0xff,0x01,0x86,0x00,0x00,0x00,0x00,0x00,0x79};
const unsigned char cmd_calibratea[] = {0xff,0x87,0x87,0x00,0x00,0x00,0x00,0x00,0xf2};
//------------------------------------------------------------------------------
int temMH;
int CO2MH;
//------------------------------------------------------------------------------
SerialLogHandler logHandler;
//------------------------------------------------------------------------------
void setup()
  {
  MHZ_16.begin(9600,SERIAL_8N1);
  Serial.begin(115200);
  Log.info("Main start");
  Log.info("System version: %s", (const char*)System.version());
  //MHZ_16.write(cmd_calibratea,9);
  }
//------------------------------------------------------------------------------
void loop()
  {
  if(dataRecieve())
    {
    Log.info("Temperature: %d", temMH);
    Log.info("CO2--------: %d", CO2MH);
    }
  delay(3000);
  }
//------------------------------------------------------------------------------
bool dataRecieve(void)
  {
  byte data[9], chksum=0x00;
  int len = -1, ntry=0, nmtry=10, i =0;
//------------------------------------------------------------------------------
  MHZ_16.flush();
//------------------------------------------------------------------------------
  //(unsigned char)MHZ_16.peek() != 0xFF
  //while((len%9!=0 && (unsigned char)MHZ_16.peek()==0xFF) && ntry<nmtry)
  //while((len%9!=0 && ntry<nmtry) || len==0)
  while((len!=9 && ntry<nmtry) || len==0)
      {
      MHZ_16.flush();
      Log.info("Available--: %d", len);
      MHZ_16.write(cmd_get_sensor,9);
      delay(200);
      len = MHZ_16.available();
      ntry++;
      Log.info("NumMHZ try-: %d", ntry);
      }
  Log.info("LenMHZ-----: %d", len);
//------------------------------------------------------------------------------
  while(MHZ_16.available()) for(int i=0;i<len; i++) data[i] = MHZ_16.read();
//------------------------------------------------------------------------------
  for(i=0; i<len; i++) Serial.print(data[i],HEX);
//------------------------------------------------------------------------------
  Serial.println();
//------------------------------------------------------------------------------
  for(i=1; i<9; i++) chksum=chksum+data[i];
  Serial.println(chksum,HEX);
  if(len==9) CO2MH = (int)data[2] * 256 + (int)data[3];
  if(len==18) CO2MH = (int)data[2+8] * 256 + (int)data[3+8];
  temMH = (int)data[4] - 40;
  return true;
  }

Yes, I use the uart1 with another sensor that is working well and the reading is more complex, an SPS30. Thank you.

What does the Log.info("LenMHZ-----: %d", len) line yield?
Can you also print out MHT_16.available() in your reading for loop?
My suspicion is that you stay in the while(MHZ_16.available()) loop although you have less than len bytes left in the RX buffer but the for loop will unconditionally try to read len bytes and hence MHZ_16.read() will give you -1 when you try to read from an empty RX buffer.

Since you have this test code anyway you can try the same code with Serial1 and I’d expect it will behave pretty much the same way :wink:

BTW, there is a Stream::readBytes() function that would probably work better than your implementation with some delays and loops and whatnot.

BTW², MHZ_16.flush() does not flush the RX buffer, so when you go through the first while more than once, you will end up with multiple sets of responses in the RX buffer, some of them may even only be fragments.

Hi @ScruffR, Thank you very much for all your help and attention. I tried Serial1 (as I started with the other sensor I had not tested, my apologies), and in this the MH-Z16 works perfectly with what I posted. The output is:

0000510012 [app] INFO: Temperature: 29
0000510012 [app] INFO: CO2--------: 324
0000513014 [app] INFO: Available--: -1
0000513214 [app] INFO: NumMHZ try-: 1
0000513214 [app] INFO: LenMHZ-----: 9
FF8614345000F1
0

This loop:

while((len!=9 && ntry<nmtry) || len==0)

I was adjusting it since it detected that when writing the sensor buffer the response packet was of variable size, its task is that if the buffer is of the response size, if it has any response and makes 10 attempts to avoid getting stuck there. For example the response using software serial is:

FFFFFFFFFFFFFFFFFFFF8619443000A2

But it should be:

FF8619443000A2

The bad thing is that the package received with PSS is irregular and sometimes it truncates the information. With the Serial1 test it is clear that the problem is in PSS, I understand that it is built with timers, do you think the use of delay could affect it ?. If I don’t use it the buffer doesn’t fill, but I make the flush fill indefinitely until the Photon crashes. Any ideas?.

Although I’d go a completely different route if this was my project, I have tried to tweak your code a bit to address some of the issues I see.
Give this a try and feel free to report back

#include <ParticleSoftSerial.h>

ParticleSoftSerial MHZ_16(D5, D6);

const unsigned char cmd_get_sensor[] = {0xff,0x01,0x86,0x00,0x00,0x00,0x00,0x00,0x79};
const unsigned char cmd_calibratea[] = {0xff,0x87,0x87,0x00,0x00,0x00,0x00,0x00,0xf2};

int temMH;
int CO2MH;

SerialLogHandler logHandler;

void setup() {
  MHZ_16.begin(9600,SERIAL_8N1);
  Serial.begin(115200);
  Log.info("Main start");
  Log.info("System version: %s", (const char*)System.version());
  //MHZ_16.write(cmd_calibratea,9);
}

void loop() {
  static uint32_t msDelay = 0;
  if (millis() - msDelay < 3000) return;
  msDelay = millis();
  
  if(dataRecieve()) {
    Log.info("Temperature: %d", temMH);
    Log.info("CO2--------: %d", CO2MH);
  }
}

bool dataRecieve(void) {
  byte data[9], chksum=0x00;
  int len = -1, ntry=0, nmtry=10, i =0;

  while((len < 9 && ntry < nmtry)) {
    MHZ_16.flush();             // flush TX buffer
    while(MHZ_16.read() >= 0);  // flush RX buffer
    
    MHZ_16.write(cmd_get_sensor, 9);
    // delay until 9 bytes are present in RX buffer, timeout after 500ms
    for(uint32_t msTimeout = millis(); (len = MHZ_16.available()) < 9 && (millis() - msTimeout < 500); Particle.process());
    ntry++;
    Log.info("Available--: %d", len); 
    Log.info("NumMHZ try-: %d", ntry);
  }
  
  if (len < 9) {
    Log.warn("Not enough data received - bailing out!");
    return false; // no valid data found
  }
  // only read the first set of returned data
  for(i = 0; i < len; i++) { 
    data[i] = MHZ_16.read();
    Serial.print(data[i], HEX);
  }
  Serial.println();

  for(i = 1; i < 9; i++) 
    chksum += data[i];
  Serial.println(chksum, HEX);
  if(len == 9) 
    CO2MH = (int)data[2] * 256 + (int)data[3];
  // this is invalid as it would access array indexes beyond the array length!!!
  //if(len == 18) 
   // CO2MH = (int)data[2+8] * 256 + (int)data[3+8];
  temMH = (int)data[4] - 40;

  return true;
}

One thing puzzled me completely, you have your dataReceived() return a bool you even check in your loop() but that function always returns true, even if it would fail - that’s rather confusing IMO.
The other thing was the “illegal” access to out of bound indexes for your ˋdataˋ array.

This would be my (untested) take on the matter

#include <ParticleSoftSerial.h>

const uint8_t       cmd_get_sensor[] = { 0xff, 0x01, 0x86, 0x00, 0x00, 0x00, 0x00, 0x00, 0x79 };
const uint8_t       cmd_calibratea[] = { 0xff, 0x87, 0x87, 0x00, 0x00, 0x00, 0x00, 0x00, 0xf2 };
const uint8_t       READ_DELAY       = 3000;

SerialLogHandler    logHandler(LOG_LEVEL_ALL);
ParticleSoftSerial  MHZ_16(D5, D6);

void setup() {
  Serial.begin(115200);
  MHZ_16.begin(9600, SERIAL_8N1);
  MHZ_16.setTimeout(500);                                       // wait up to 500ms for the data to return in full
  
  Log.info("Main start");
  Log.info("System version: %s", (const char*)System.version());
  //MHZ_16.write(cmd_calibratea, sizeof(cmd_calibratea));
}

void loop() {
  // non-blocking delay to reduce cadence of loop() 
  static uint32_t msDelay = 0;
  if (millis() - msDelay < READ_DELAY) return;
  
  int temMH;
  int CO2MH;
  if(dataRecieve(temMH, CO2MH)) {
    Log.info("Temperature: %d", temMH);
    Log.info("CO2--------: %d", CO2MH);
    msDelay = millis();                                         // delay next read attempt
  }
}

bool dataRecieve(int& temp, int& co2) {
  char data[9];
  char chksum = 0x00;

  MHZ_16.flush();                                               // flush TX buffer
  while(MHZ_16.read() >= 0);                                    // flush RX buffer

  MHZ_16.write(cmd_get_sensor, sizeof(cmd_get_sensor));         // send read request
  if (MHZ_16.readBytes(data, sizeof(data)) < sizeof(data)) {    // attempt to read response frame
    Log.warn("Not enough data received - bailing out!");
    return false;                                               // bail out
  }
  
  for(int i = 0; i < sizeof(data); i++) 
    Serial.print(data[i], HEX);
  Serial.println();

  for(int i = 1; i < sizeof(data); i++) 
    chksum += data[i];

  if (chksum != data[0]) {
    Log.warn("wrong checksum (%02x vs. %02x)", chksum, data[0]);
    return false;                                               // bail out
  }

  co2  = (int)data[2] * 256 + (int)data[3];
  temp = (int)data[4] - 40;

  return true;
}

Hi,

I apologize for the delay, I tried many more things with the MH-Z16, particularly I was using the DFROBOT version and I couldn’t get stable code. I decided to buy another one from the same manufacturer with the same protocol (MH-Z19B). With this, there were fewer problems using the PSS. The DFROBOT version has a board with resistors in series, the problem may be the mixing of these with the pull ones of the Photon ?. Below the code that works with the MH-Z19B, I thank @ScruffR very much for their contributions, I integrated many ideas of your code, I hope that someone else will help.

#include <ParticleSoftSerial.h>
//------------------------------------------------------------------------------
ParticleSoftSerial MHZ_16(D2, D3);
//------------------------------------------------------------------------------
const unsigned char cmd_get_sensor[] = {0xff,0x01,0x86,0x00,0x00,
                                        0x00,0x00,0x00,0x79};
const unsigned char cmd_calibratea[] = {0xff,0x87,0x87,0x00,0x00,
                                        0x00,0x00,0x00,0xf2};
//------------------------------------------------------------------------------
const uint32_t  READ_DELAY  = 10000;
static uint32_t msDelay     = millis();
//------------------------------------------------------------------------------
SerialLogHandler logHandler(LOG_LEVEL_INFO);
//==============================================================================
//Setup
//==============================================================================
void setup()
  {
  MHZ_16.begin(9600,SERIAL_8N1);
  MHZ_16.setTimeout(500);                                                       // Wait up to 500ms for the data to return in full
  Serial.begin(115200);
  Log.info("Main start");
  Log.info("System version: %s", (const char*)System.version());
  Log.info("WiFi RSSI-----: %d", (int8_t) WiFi.RSSI());
  //MHZ_16.write(cmd_calibratea, sizeof(cmd_calibratea));
  }
//==============================================================================
//Loop
//==============================================================================
void loop()
  {
  int temMH;
  int CO2MH;
  if(millis() - msDelay<READ_DELAY) return;
  msDelay = millis();
  if(dataRecieve(temMH, CO2MH)) Log.info("TemMH: %d, CO2MH: %d", temMH, CO2MH);
  }
//==============================================================================
//Function read
//==============================================================================
bool dataRecieve(int& temMH, int& CO2MH)
  {
  byte data[9];
  char chksum = 0x00;
  int len = -1, ntry=0, nmtry=10, i =0;
  while((len < 9 && ntry < nmtry))
    {
    MHZ_16.flush();                                                             // Flush TX buffer
    while(MHZ_16.read() >= 0);                                                  // Flush RX buffer
    MHZ_16.write(cmd_get_sensor, 9);
    for(uint32_t msTimeout=millis(); (len=MHZ_16.available())<9 && (millis()-msTimeout<1000); Particle.process());
    Log.trace("[MHZ] Available: %d", len);
    Log.trace("[MHZ] Num. try-: %d", ntry);
    ntry++;
    }
  if (len != 9) return false;
  for(i = 0; i < len; i++) data[i] = MHZ_16.read();
  for(i = 1; i < 9; i++) chksum += data[i];
  if(chksum==0) CO2MH = (int)data[2] * 256 + (int)data[3];
  if(chksum==0) temMH = (int)data[4] - 40;
  return true;
  }