Sony Spresense LTE拡張ボードで温湿度、eCO2、TVOCのデータをCloud(Thingspeak)にアップロード

Sony Spresenseで室内環境データを取得してLTE拡張ボードを使ってデータを[Thingspeak](https://thingspeak.com/)にアップロードしました。
なお取得したデータは**メディアンフィルター処理**をしています。  

機器構成  

| 名称 | 型名 | メーカー |
|:----|:----|:----|
| メインボード | CXD5602PWBMAIN1 | Sony |
| LTE拡張ボード | CXD5602PWBLM1J | Sony |
| センサーボード | NS-SPR-0308 | Next Step |
| SIMカード |  IoT SIM(Plan-D) | SORACOM |

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/99249/dbbdb49e-c057-2794-a79b-b7404ef4c89a.png)

使用したセンサー  

| センサー | データ | データシート |
|:----|:----|:----|
| SHTC1 | 温度・湿度  | https://www.mouser.jp/datasheet/2/682/Sensirion_Humidity_Sensors_SHTC1_Datasheet-1511754.pdf |
| SGP30-2.5k | eCO2・TVOC | https://www.mouser.jp/datasheet/2/682/Sensirion_Gas_Sensors_SGP30_Datasheet-1843629.pdf |

センサーボード回路図
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/99249/1b6bea7f-4a83-6666-a9bb-d5a269e37443.png)

Thingspeakの設定は以下のように
Field 1 : eCO2
Field 2 : TVOC
Field 3 : TEMP
Field 4 : HUM
にしています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/99249/a103e925-918e-21da-8873-bb12cb44450c.png)

以下が今回のスケッチになります。  
すみません、コメント少ないです

```LteWebClient_thingspeak.ino
/*
  Sample program for the Sensirion SGP30&SHTC1 Indoor Air Quality Sensor
  By: Kazuaki Ueno
  Next Step LLC
  Date: Dec. 30th, 2020
  License: This code is public domain.

  SGP30 Datasheet: https://cdn.sparkfun.com/assets/c/0/a/2/e/Sensirion_Gas_Sensors_SGP30_Datasheet.pdf

  Feel like supporting our work? Buy a board from Next Step!

  This example reads the sensors calculated CO2 and TVOC values
*/

// libraries
#include <LTE.h>
#include <Wire.h>
#include <stdlib.h>
#include "SparkFun_SGP30_Arduino_Library.h"                 // Click here to get the library: http://librarymanager/All#SparkFun_SGP30
#include <shtc1.h>

SGP30 mySensor;                                             //create an object of the SGP30 class
SHTC1 shtc1;

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

// アップロード間隔
#define SAMPLE_NUMS (12)                                    // 2分間隔

// initialize the library instance
LTE lteAccess;
LTEClient client;

// URL, path & port (for example: arduino.cc)
char server[] = "api.thingspeak.com";
char path[100];
int  port = 80;                                             // port 80 is the default for HTTP


int compareFloat(const void*, const void*);
float get_median(const float*, uint8_t);

int 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 setup()
{
  Wire.begin();
  //Initialize sensor for air CO2&TVOC readings
  if (mySensor.begin() == false) {
    Serial.println("No SGP30 Detected. Check connections.");
    while (1);
  }
  //Initializes sensor for air quality readings
  //measureAirQuality should be called in one second increments after a call to initAirQuality
  mySensor.initAirQuality();


  // 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("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.");
        break;
      }
      Serial.println("An error occurred, shutdown and try again.");
      lteAccess.shutdown();
      sleep(1);
    }
  }
}

void loop()
{
  static float co2_arry[5]  = {0.0};
  static float tvoc_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 tvoc_sum = 0.0;
  static float temp_sum = 0.0;
  static float hum_sum  = 0.0;

  static uint8_t counter = 0;

  float co2,tvoc,temp,hum;
  uint8_t i;

  //First fifteen readings will be
  //CO2: 400 ppm  TVOC: 0 ppb
  //measure CO2 and TVOC levels
  mySensor.measureAirQuality();
  //measure TEMP and HUM levels
  shtc1.readSample();

  // Obtain data from the sensors.
  co2  = float(mySensor.CO2);
  tvoc = float(mySensor.TVOC);
  temp = shtc1.getTemperature();
  hum  = shtc1.getHumidity();

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

  co2_sum  += co2;
  tvoc_sum += tvoc;
  temp_sum += temp;
  hum_sum  += hum;
  
  Serial.print("CO2: ");
  Serial.print(co2,0);
  Serial.print("ppm  tTVOC: ");
  Serial.print(tvoc,0);
  Serial.print("ppb");

  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){
    // Set data for Thingspeak.
    sprintf(path,"/update?api_key=xxxxxxxxxxxxxxxxxxxxxxx&field1=%4.0f&field2=%4.0f&field3=%5.2f&field4=%5.2f",co2_sum/SAMPLE_NUMS,tvoc_sum/SAMPLE_NUMS,temp_sum/SAMPLE_NUMS,hum_sum/SAMPLE_NUMS);
    // (xxxxxxxxxxxxxxxxxxxxxxxはThingspeakで発行されたAPIキーに変更して下さい)
  
    // if you get a connection, report back via serial:
    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();
    } else {
      // if you didn't get a connection to the server:
      Serial.println("connection failed");
    }
    delay(100);
    // if there are incoming bytes available
    // from the server, read them and print them:
    if (int len = client.available()) {
      char buff[len + 1];
      buff[len] = '\0';
      client.read((uint8_t*)buff, len);
      Serial.print(buff);
    }

    // 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;
    tvoc_sum = 0.0;
    temp_sum = 0.0;
    hum_sum  = 0.0;
    counter = 0;
  }
  delay(10000);
}
```

[Thingspeakのデータ表示ページ](https://thingspeak.com/channels/1266876)

上記ページの画面キャプチャー
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/99249/79c85e10-fd2b-b96e-8e78-23b97c141d4e.png)

---------
センサーボードのEagleデータを公開中
https://github.com/kaz19610303/SPRESENSE_CO2/