Microcontroller projects

SA1200p CO2 hacks

last updated: 2021-02-21

Introduction

sa1200p

It is important to hack devices to learn things and understand how things work. All schools in Luxembourg got due to the COVID situation CO2 monitors called SA1200P. They can be purchased in different countries; here some links:

Unfortunately this devices are not produced in Europe, they are not very smart nor IoT, nor are they well suited to use in a classroom. So let's change this and get them at least smarter :).

The hacks

First we look at the hardware to understand with what we are dealing here:

Hardware hack

A first inspection shows that the PCB is not fully populated and only very few components are used:

envisense circuit
click for a better view

BOM

ESP8266 hack

WiFi with ESP8266

So clearly we need something to get the data out of the device. I choose the beloved LOLIN/WEMOS D1 mini pro (ESP8266) with WiFi. As we get idle pads for the USB data lines (D+, D-) we can connect the board directly to the Mini USB connector. So it is powered and we are able to reprogram the ESP even with a closed housing.

CO2

To get the data from the CO2 sensor is the easy part. I use softserial with pin D5. Hardware RX is no good choice because it is used when uploading a sketch. Softserial gives here no problem because the bit-rate is only at 9600 bit/s (8N1).

Temperature and humidity

To sniff on an I2C or TWI bus is more challenging. It can be done using an interrupt on the clock line and a little bit bit fiddeling :). Fortunately the master uses a software interface with only 15 kHz. In the net we read that the ESP8266 is, because of the RTOS quite slow on the interrupts. 100 kHz, the normal clock speed of I2C, could be too fast.

The master asks, after sending the address (0x40) + write bit (0, 0x80) by sending the command 0xE5 for the humidity. The third byte is the address + read byte (1, 0x81). The slaves ACK bits are only 1.5 V.
Left screen: 0x80 + ACK + 0xE5 + ACK + 0x81 + ACK (2 ms).

The sensor uses a halt mechanism (hold master) when converting the data, as we can see in the right screen (other time scale!). The clock line (blue) is pulled to GND after the first 3 byte (only visible as falling edge) for 150 ms to get the humidity. In the middle we have 3 byte answer and 3 byte where the master asks for the temperature and then again 150 ms before the 3 byte answer:

i2c<em>sht21  i2c</em>sht21

The next two screens show the humidity answer (MSB 0x58 + ACK + LSB 0x82 + ACK + CRC 0xC8 + ACK) and the master asking for the temperature (0x80 + ACK + 0xE3 + ACK + 0x81 + ACK (2 ms).)

i2c<em>sht21  i2c</em>sht21

Finally we get the answer for the temperature (MSB 0x66 + ACK + LSB 0xA4 + ACK + CRC 0x37 + ACK)

i2c_sht21

Soldering

We connect SDA with D2, SCL with D1 and serial TX with D5. These pins can be changed if needed in software.

envisense circuit
click for a better view

Software

Our software can send CO2, temperature and humidity values over WiFi to an MQTT server.

envisense circuit      envisense circuit  

For more infos about MQTT look here: http://weigu.lu/tutorials/sensors2bus/06_mqtt/index.html

Here the code of a version without WiFi and MQTT, which is not so overloaded with information. We get the data over USB, and can view the data in the Serial Monitor of the Arduino IDE. The other version is in "Downloads" (end of the page). With Arduino we can

    // sa1200p_with_esp_no_wifi.ino
    // weigu.lu

    #include <SoftwareSerial.h>

    //#define DEBUG

    const byte CO2_RX = 14; // D5
    const byte SDA_PIN = 4; // D2
    const byte SCL_PIN = 5; // D1
    byte event_counter = 0;
    int co2;
    float sht21_value;
    float hum, temp;
    const byte buffer_bytes = 8;
    volatile byte bit_buffer[buffer_bytes];
    volatile byte bit_counter = 0;  

    SoftwareSerial SerialCO2(CO2_RX); // RX

    void setup() {
      pinMode(SDA_PIN,INPUT);
      pinMode(SCL_PIN,INPUT);
      Serial.begin(115200);
      SerialCO2.begin(9600);
      delay(500);    
      Serial.println("Hi");
      attachInterrupt(digitalPinToInterrupt(SCL_PIN), isr_scl, RISING);
      noInterrupts();  
      clear_buffer(buffer_bytes);
      bit_counter = 0;  
      interrupts(); // allow interrupts
    }

    void loop() {    
      if (SerialCO2.available()) {    
        co2 = get_CO2();
        if (co2 == -1) {
          Serial.println("Serial data (co2) corrupt");
        }
        else {
          Serial.println("CO2 = " + String(co2) + " ppm");
        }    
      }  
      sht21_value = get_sht21_data();
      if ((event_counter == 2) && (sht21_value > 0.0)) {
        hum = sht21_value;
        Serial.println("Humidity is: " + String(hum) + "%");
      }
      else if ((event_counter == 3) && (sht21_value > 0.0)) {
        temp = sht21_value;
        Serial.println("Temperature is: " + String(temp) + "°C");
        event_counter = 0;
      }    
      else if ((sht21_value == -1)) {
        Serial.println("SHT21 CRC error"); 
      }    

      delay(100);
      yield();
    }

    ICACHE_RAM_ATTR void isr_scl() {  
      //delayMicroseconds(15);  
      if (digitalRead(SDA_PIN)) {
        bitSet(bit_buffer[bit_counter/8], 7-bit_counter%8);    
      }
      else {
        bitClear(bit_buffer[bit_counter/8], 7-bit_counter%8);    
      }
      bit_counter++;
    }

    //clear buffer
    void clear_buffer(byte lbuffer_bytes) {
      for (byte i=0; i<lbuffer_bytes; i++) {
        bit_buffer[i]=0;    
      }
    }  

    // get the CO2 data
    int get_CO2() {
      int co2_lvalue;
      byte co2_byte_counter = 0, co2_data[10], co2_checksum;
      while (SerialCO2.available()) {    
        co2_data[co2_byte_counter] = SerialCO2.read();
        co2_byte_counter++;
      }
      // are the three first byte ok?
      if ((co2_data[0] != 0x16) || (co2_data[1] != 0x05) || (co2_data[2] != 0x01)) {
        #ifdef DEBUG
          Serial.println("data corrupt");  
        #endif    
        return -1;
      }
      // calculate the checksum and test if ok 
      co2_checksum = 0;
      for (byte i=0; i<co2_byte_counter-1; i++) {
        co2_checksum += co2_data[i];
      }
      co2_checksum = 256-co2_checksum;
      if (co2_data[co2_byte_counter-1]!=co2_checksum) {
        #ifdef DEBUG
          Serial.println("Checksum not ok");  
        #endif    
        return -1;
      }  
      co2_lvalue = co2_data[3]*256+co2_data[4];
      #ifdef DEBUG
        for (byte i=0; i<co2_byte_counter; i++) {    
          Serial.print(co2_data[i],HEX);  
          Serial.print(' ');  
        }  
        Serial.print(" checksum: " + String(co2_checksum,HEX));  
      #endif  
      for (byte i=0; i<sizeof(co2_data); i++) { //clear buffer
        co2_data[i]=0;    
      }  
      co2_byte_counter = 0;
      return co2_lvalue;
    }

    // we need global vars bit_counter, bitbuffer, envent_counter
    float get_sht21_data() {
      byte msb, lsb, crc;
      unsigned int data_word = 0;
      float lhum, ltemp;
      if (bit_counter >= 28) {
        delay(2); //wait 2ms for the other bits
        noInterrupts();   
        if (bit_buffer[0]==0x80) { // master
          event_counter = 1;
        }
        else if (event_counter == 1) { // hum data from slave
          msb = bit_buffer[0];
          lsb = bit_buffer[1] << 1;    // eliminate ACK bits
          crc = bit_buffer[2] << 2;      
          lsb = lsb + (bit_buffer[2] >> 7);       
          crc = crc + (bit_buffer[3] >> 6);      
          if (check_crc(msb, lsb, crc) == false) {
            event_counter = 0;
            return -1;      
          }
          lsb &= 0xFC;                 // clear last 2 bits (data sheet)
          data_word = msb*256+lsb;
          lhum = -6.0+(125.0*data_word/65536.0);      
          check_crc(0x66, 0xA4, 0x37);      
          #ifdef DEBUG        
            Serial.print("msb: " + String(msb,HEX) + " lsb: " + String(lsb,HEX));
            Serial.println(" crc: " + String(crc,HEX) + " data word: " + String(data_word));
          #endif  
          event_counter = 2;      
        }
        else if (event_counter == 2) { // temp data from slave
                msb = bit_buffer[0];
          lsb = bit_buffer[1] << 1;    // eliminate ACK bits
          crc = bit_buffer[2] << 2;      
          lsb = lsb + (bit_buffer[2] >> 7);       
          crc = crc + (bit_buffer[3] >> 6);      
          if (check_crc(msb, lsb, crc) == false) {
            event_counter = 0;
            return -1;      
          }
          lsb &= 0xFC;                 // clear last 2 bits (data sheet)
          data_word = msb*256+lsb;
          ltemp = -46.85+(175.72*data_word/65536.0); 
          ltemp = ltemp -1.65;          // correction for housing error?
          #ifdef DEBUG
            Serial.print("msb: " + String(msb,HEX) + " lsb: " + String(lsb,HEX));
            Serial.println(" crc: " + String(crc,HEX) + " data word: " + String(data_word));
          #endif  
          event_counter = 3;
        }
        #ifdef DEBUG
          Serial.print(String(event_counter) + ":  " + String(bit_counter) + " bit:  ");
          for (byte i=0; i<buffer_bytes; i++) {
            Serial.print(bit_buffer[i],HEX);
            Serial.print('\t');    
          }
          delay(10);
          Serial.println();
        #endif  
        clear_buffer(buffer_bytes);
        bit_counter = 0;    
        interrupts();  
        if (event_counter == 2) {
          return lhum;
        }
        else if (event_counter == 3) {
          return ltemp;
        }        
      }
    }

    // from Sensiron application note and code
    bool check_crc(byte lmsb, byte llsb, byte lcrc) {  
      byte bitmask;  
      byte calc_crc = 0x00; // initial value as per Table 17  
      calc_crc ^= (lmsb); // do msb
      for (bitmask = 8; bitmask > 0; --bitmask)  {
        if (calc_crc & 0x80) {
          calc_crc = (calc_crc << 1) ^ 0x131;
        }
        else {
          calc_crc = (calc_crc << 1);  
        }  
      }  
      calc_crc ^= (llsb); // do lsb
      for (bitmask = 8; bitmask > 0; --bitmask)  {
        if (calc_crc & 0x80) {
          calc_crc = (calc_crc << 1) ^ 0x131; // polynomial from Table 17
        }
        else {
          calc_crc = (calc_crc << 1);
        }  
      }  
      #ifdef DEBUG
        Serial.println("------------------");
        Serial.print("msb: " + String(lmsb,HEX) + " lsb: " + String(llsb,HEX));
        Serial.println(" crc: " + String(lcrc,HEX) + ""  calculated checksum = " + String(calc_crc,HEX));
        Serial.println("------------------");
      #endif
      if (calc_crc != lcrc) {
        return false;     // checksum error
      }
      else {
        return true;      // no error
      }
    }

Getting the firmware

Now let's see if the firmware is locked.

To get the original firmware we use the STM32CubeProgrammer software from st.com that is free and runs also under Linux. We need an ST-LINK/V2 programmer. Fortunately they are cheap and the best way is to buy a Nucleo board where the programmers are integrated. We connect the ST-Link header with the programming header SWD1 on the mainboard (GND (G), SWDIO, SWCK, 3V(V)).

stlink
click for a better view

As we can see in the picture we need to remove the two jumpers (ST-Link) as we want to program an external board. The data (SWDIO) is on pin 2 of the ST-Link header, clock (SWCK) on pin 4 and GND in the middle (pin3)) We find the 3 V to power the board near the Arduino header.

stlink
click for a better view

Now we open the software and click on Connect (top right)) in the software and see the content of the Flash in our window. The firmware is not locked as we can see in the !

stlink
click for a better view

Downloads

Links