Sensirionのco2センサーSCD41をSpresenseで試してみた

SONY Spresense用の **co2センサー SCD41 Addonボード**を作ったので評価してみました。
[SCD4xのページ](https://www.sensirion.com/jp/environmental-sensors/carbon-dioxide-sensors/carbon-dioxide-sensor-scd4x/)
##構成
| 名称 | 型名 | メーカー |
|:-----------|:------------|:------------:|
| CPU | SPRESENSE | SONY|
|LTE-M ボード|CXD5602PWBLM1J | SONY|
|CO2センサー| SCD41| Sensirion|
|LTE SIM|定額10MBプラン| MEEQ|

##写真
![IMG_0115.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/99249/a062e9c8-33f4-143d-3933-548fc09943b4.jpeg)

##回路図
SpresensesとSCD41は**I2C**で接続しますが、現状レベルシフターICがことごとく入手不可なので **N-Ch MOSFET** で構成しました。
![CO2_TEMP_SCD40.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/99249/d777ad64-dbd0-d86a-711c-c89d3ed95140.png)

##基板
![CO2_TEMP_SCD40_a.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/99249/94ccbac3-df23-94a0-a75c-497e245a8c96.png)
![CO2_TEMP_SCD40_b.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/99249/ddbe1133-c5dc-2068-1e22-a1f21c30083e.png)
![Spresense_SCD4X.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/99249/57dedf70-68d7-4dd5-8810-e7349ab19d98.png)
[3D View](https://campus1194.autodesk360.com/g/projects/20210627423369826/data/dXJuOmFkc2sud2lwcHJvZDpmcy5mb2xkZXI6Y28uMXdjd3FzaGNRVGlaYjg4alRSb3lXdw/dXJuOmFkc2sud2lwcHJvZDpkbS5saW5lYWdlOmdxOFZKYXhmUUxHNy1UNjBLV1RYZ1E/viewer
)


##グラフ
まだサンプル数は少ないですがこんな感じです。
(Google chart)
![SCD40_graph.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/99249/7f2c59d4-c057-a73f-7d68-f5804cc88bd0.jpeg)


##SPRESENSE プログラム
テストに使ったプログラムです、整理していませんが参考にしてください。
**SCD41(40)用のライブラリ**が Sensirion から提供されているのでそれを使いました。
[SCD40 arduinoライブラリ](https://github.com/Sensirion/arduino-i2c-scd4x)

データは **HTTP GET** で自社システムに5分毎に送信しています。

```SCD40_test.ino
/*
  Sample program for the Sensirion SCD30 Sensor
  By: Kazuaki Ueno
  Next Step LLC
  Date: Jun. 20th, 2021
  License: This code is public domain.

  SCD30 Datasheet: https://www.mouser.jp/datasheet/2/682/Sensirion_CO2_Sensors_SCD30_Datasheet-1901872.pdf

  This example reads the sensors calculated CO2 and TEMP and Humidity.
*/

// libraries
#include <stdlib.h>
#include <LTE.h>                                                                // LTEライブラリ
#include <Wire.h>                                                               // I2Cライブラリ
#include <Watchdog.h>                                                           // ウォッチドッグライブラリ https://github.com/janelia-arduino/Watchdog
#include <SensirionI2CScd4x.h>                                                  // SCD4Xライブラリ        https://github.com/Sensirion/arduino-i2c-scd4x
                                                                                // Sensirion Arduino Core Library  https://github.com/Sensirion/arduino-core/
                                                                                //                                 SCD4Xライブラリで include されている

SensirionI2CScd4x scd4x;

// APN data
// MEEQ
#define LTE_APN       "meeq.io"                                                 // replace your APN
#define LTE_USER_NAME "meeq"                                                    // replace with your username
#define LTE_PASSWORD  "meeq"                                                    // replace with your password
// SORACOM
// #define LTE_APN       "soracom"                                              // replace your APN
// #define LTE_USER_NAME "sora"                                                 // replace with your username
// #define LTE_PASSWORD  "sora"                                                 // replace with your password

// アップロード間隔
#define SAMPLE_NUMS (30)                                                        // 5分間隔 設定値×10秒

// Timer割り込み間隔
#define INTERVAL 1000                                                           // 1000uS = 1mS

// センシング間隔
#define SENSOR_INTERVAL (10000)                                                 // センサーの読み込み間隔(mS)

// Watchdog
#define WATCHDOG_TIME   (15000)

// initialize the library instance
// U8G2_SSD1306_128X64_NONAME_F_HW_I2C u8g2(U8G2_R0, /* reset=*/ U8X8_PIN_NONE);
// SCD30 airSensor;                                                             //create an object of the SCD30 class
LTEModem modem;
LTE lteAccess;
LTEClient client;
LTEScanner scannerNetworks;

// URL, path & port (for example: arduino.cc)
char server[] = "server_url";                                                   // 使用するザーバーの情報に書き換えて下さい。
char path[200];
int  port = 80;                                                                 // port 80 is the default for HTTP

// Grubal variable
bool lte_connect = false ;
uint16_t int_counter  = 0;
uint8_t  sensing_flag = 0;
String IMEI;
char imei_char[50];

// Function prototype
uint8_t callback_func(void);
uint8_t compareFloat(const void*, const void*);
float   get_median(const float*, uint8_t);                                        // メディアンフィルター
void    printUint16Hex(uint16_t);
void    printSerialNumber(uint16_t, uint16_t, uint16_t);
void    sensing_job();

uint8_t callback_func()
{
  int_counter++ ;
  if(int_counter >= SENSOR_INTERVAL){
    sensing_flag = 1;
    int_counter = 0;
  }
  return INTERVAL;
}

uint8_t compareFloat(const void* a, const void* b)
{
    int aNum = *(float*)a;
    int bNum = *(float*)b;

    if( aNum < bNum ){
        return -1;
    }
    else if( aNum > bNum ){
        return 1;
    }
    return 0;
}

/* メディアンフィルタ処理 */
float get_median(const float* array, uint8_t size)
{
    float median;

    float* array_copy = malloc(sizeof(float) * size);
    memcpy(array_copy, array, sizeof(float) * size);
    qsort(array_copy, size, sizeof(float), compareFloat);
    median = array_copy[size / 2];
    free(array_copy);
    return median;
}

void printUint16Hex(uint16_t value)
{
    Serial.print(value < 4096 ? "0" : "");
    Serial.print(value < 256 ? "0" : "");
    Serial.print(value < 16 ? "0" : "");
    Serial.print(value, HEX);
}

void printSerialNumber(uint16_t serial0, uint16_t serial1, uint16_t serial2)
{
    Serial.print("Serial: 0x");
    printUint16Hex(serial0);
    printUint16Hex(serial1);
    printUint16Hex(serial2);
    Serial.println();
}


void sensing_job()
{
  static uint8_t counter = 0 ;
  static float co2_arry[5]  = {0.0};
  static float temp_arry[5] = {0.0};
  static float hum_arry[5]  = {0.0};

  static float co2_sum  = 0.0;
  static float temp_sum = 0.0;
  static float hum_sum  = 0.0;

  static uint16_t disp_counter = 0;

  float co2,temp,hum;
  uint16_t co2_i;
  uint16_t temp_i;
  uint16_t hum_i;

  uint8_t i;
  
  String rssi_str;
  int16_t  rssi;

  uint16_t error;
  char errorMessage[256];

  disp_counter++;
  Watchdog.start(WATCHDOG_TIME);

  // Obtain data from the sensors.
  error = scd4x.readMeasurement(co2_i, temp_i, hum_i);
  if (error) {
      Serial.print("Error trying to execute readMeasurement(): ");
      errorToString(error, errorMessage, 256);
      Serial.println(errorMessage);
  } else if (co2_i == 0) {
      Serial.println("Invalid sample detected, skipping.");
  } else {
      co2 = co2_i * 1.0;
      temp = temp_i * 175.0 / 65536.0 - 45.0;
      hum  = hum_i * 100.0 / 65536.0;
  }

  // Median Filtering.
  for(i = 1 ; i < 5; i++){
    co2_arry[i-1]  = co2_arry[i];
    temp_arry[i-1] = temp_arry[i];
    hum_arry[i-1]  = hum_arry[i];
  }
  co2_arry[4]  = co2;
  temp_arry[4] = temp;
  hum_arry[4]  = hum;

  co2   = get_median(co2_arry,5);
  temp  = get_median(temp_arry,5);
  hum   = get_median(hum_arry,5);
  // End Median Filtering.
  co2_sum  += co2;
  temp_sum += temp;
  hum_sum  += hum;

  // Data sent to serial port
  Serial.print("Counter: ");
  Serial.print(disp_counter);
  Serial.print(" ");

  Serial.print("CO2: ");
  Serial.print(co2,0);
  Serial.print("ppm");

  Serial.print("  RH: ");
  Serial.print(hum, 2);
  Serial.print("%  TEMP: ");
  Serial.print(temp, 2);
  Serial.println("℃");
  
  counter++;

  // Data upload to Thingspeak.
  if(counter >= SAMPLE_NUMS){
    if(lte_connect == true){
      // read signal strength
      rssi_str = scannerNetworks.getSignalStrength();
      rssi = rssi_str.toInt(); 
      Serial.print("RSSI : ");
      Serial.println(rssi);

      // データ送信先と送信データのセット
      sprintf(path,"/uec/sensor.php?id=%s&co2=%3.0f&temp=%5.2f&hum=%5.2f&rssi=%2d",imei_char,co2_sum/SAMPLE_NUMS,temp_sum/SAMPLE_NUMS,hum_sum/SAMPLE_NUMS,rssi);
      Serial.print(server);
      Serial.println(path);

      if (client.connect(server, port)) {
        Serial.println("connected");
        // Make a HTTP request:
        client.print("GET ");
        client.print(path);
        client.println(" HTTP/1.1");
        client.print("Host: ");
        client.println(server);
        client.println("Connection: close");
        client.println();
        Serial.println("Data send.");
      } else {
        // if you didn't get a connection to the server:
        Serial.println("connection failed");
      }
      delay(100);

      // if the server's disconnected, stop the client:
      if (!client.available() && !client.connected()) {
        Serial.println();
        Serial.println("disconnecting.");
        client.stop();

      // do nothing forevermore:
        for (;;)
          sleep(1);
      }
    }
    co2_sum  = 0.0;
    temp_sum = 0.0;
    hum_sum  = 0.0;
    counter = 0;
  }
  
  // Watch dog clear
  Watchdog.kick();
}

void setup()
{
  uint16_t error;
  char errorMessage[256];
  Wire.begin();

  // initialize serial communications and wait for port to open:
  Serial.begin(115200);
  while (!Serial) {
      ; // wait for serial port to connect. Needed for native USB port only
  }
  Serial.println("SCD41 Example");

  if (LTE_IDLE == modem.begin()) {               
    Serial.println("modem.begin() succeeded.");
  } else {
    Serial.println("ERROR, no modem answer.");
  }
  scd4x.begin(Wire);

  // stop potentially previously started measurement
  error = scd4x.stopPeriodicMeasurement();
  if (error) {
    Serial.print("Error trying to execute stopPeriodicMeasurement(): ");
    errorToString(error, errorMessage, 256);
    Serial.println(errorMessage);
  }

  uint16_t serial0;
  uint16_t serial1;
  uint16_t serial2;
  
  error = scd4x.getSerialNumber(serial0, serial1, serial2);
  if (error) {
    Serial.print("Error trying to execute getSerialNumber(): ");
    errorToString(error, errorMessage, 256);
    Serial.println(errorMessage);
  } else {
    printSerialNumber(serial0, serial1, serial2);
  }

  // Start Measurement
  error = scd4x.startPeriodicMeasurement();
  if (error) {
    Serial.print("Error trying to execute startPeriodicMeasurement(): ");
    errorToString(error, errorMessage, 256);
    Serial.println(errorMessage);
  }

IMEI = modem.getIMEI();
  IMEI.toCharArray(imei_char, 50);
  
  Serial.print("Modem IMEi = ");
  Serial.println(IMEI);
  
  /* LTE 通信開始 */
  Serial.println("Starting web client.(LTE connection)");

  // If your SIM has PIN, pass it as a parameter of begin() in quotes
  while (true) {
    if (lteAccess.begin() == LTE_SEARCHING) {
      if (lteAccess.attach(LTE_APN, LTE_USER_NAME, LTE_PASSWORD) == LTE_READY) {
        Serial.println("attach succeeded.");
        lte_connect = true;
        break;
      }
      Serial.println("An error occurred, shutdown and try again.");
      lteAccess.shutdown();
//      sleep(1);
      break;
    }
  }  
  // Watch dog start
  attachTimerInterrupt(callback_func, INTERVAL);
  Watchdog.begin();
}

void loop()
{
  if(sensing_flag == 1){
    sensing_job();
    sensing_flag = 0;
  }
}
```

昨日データを取り始めたのでまだ評価は難しいですが、データ的には実用の範囲内だと思っています。
同じ場所で [Sensirion の SCD30](https://www.sensirion.com/jp/environmental-sensors/humidity-sensors/co2-sensor/) を使った装置でも計測していますのでもう少しデータがたまったら比較をしてみます。

2021年9月7日 追記
[SCD30とSCD41の比較](https://qiita.com/kaz19610303/items/4bddf6ed0f40efc03f3b)