SONY Spresense用の co2センサー SCD41 Addonボードを作ったので評価してみました。
SCD4xのページ
##構成
名称 | 型名 | メーカー |
---|---|---|
CPU | SPRESENSE | SONY |
LTE-M ボード | CXD5602PWBLM1J | SONY |
CO2センサー | SCD41 | Sensirion |
LTE SIM | 定額10MBプラン | MEEQ |
##回路図
SpresensesとSCD41はI2Cで接続しますが、現状レベルシフターICがことごとく入手不可なので N-Ch MOSFET で構成しました。
##基板
3D View
##グラフ
まだサンプル数は少ないですがこんな感じです。
(Google chart)
##SPRESENSE プログラム
テストに使ったプログラムです、整理していませんが参考にしてください。
SCD41(40)用のライブラリが Sensirion から提供されているのでそれを使いました。
SCD40 arduinoライブラリ
データは 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 を使った装置でも計測していますのでもう少しデータがたまったら比較をしてみます。
2021年9月7日 追記
SCD30とSCD41の比較