概要
コロナ禍において三密回避のためCo2センサを使って混雑を検出する事が増えているようです。また在宅勤務の増加に伴い換気を忘れCo2濃度が上がり効率が低下するという事もあるようです。各国でもカーボンゼロ施策が検討されCo2への興味が上がってきていることから身近なところで部屋のCo2濃度を検出して換気のお知らせ及び植物でCo2を減らす無駄な努力をする機器を作成しようと思います。
動作画像
SCD30というCo2センサーで二酸化炭素濃度を検出して、手作りメーターで赤/黄/青でのガイドを表示すると共に、1500ppmを超えたら換気を促すアラートをアヒル隊長が鳴いてお知らせします。その後、植物に光を当ててCo2の吸収を促します。
ハードウェア
使用機器
大物以外は出来るだけ安いものを探しましたが、Co2センサーが意外と値段が高いです。Co2だけ見る目的ならばDIYせずに完成品を買ったほうが安いと思います。
使用機器 | 説明 | 価格 |
---|---|---|
Obniz Board Y1 | コンテストで提供いただきましたObniz | 6,930円 |
Co2センサー | 二酸化炭素濃度測定 SCD30 (Groveコネクタ版) | 7,491円 |
Switch Bot | 電源入り切り Switch Bot Smart Plub版 | 1,980円 |
植物ライト | 光合成促進 5W LED植物ライト | 1,980円 |
DCモーター | 普通のモーター タイヤ付きモーター | 740円 |
サーボモーター | Tower Pro SG92R | 500円 |
アクリル台 | コの字のアクリル台を横で使用(ダイソー製) | 100円 |
ダンボール | マス目付きのダンボール板(ダイソー製) | 100円 |
両面テープ | ダンボール板接合の強力版(ダイソー製) | 100円 |
後付キャスター | 滑車の代わりに使用(ダイソー製) | 100円 |
布ゴム | 滑車の線に使用(ダイソー製) | 100円 |
ビー玉 | 25mmのビー玉(ダイソー製) | 100円 |
アヒル隊長 | お風呂で浮かべるアヒル(ダイソー製) | 100円 |
機器の接続
Co2センサー
Co2センサーの種類
Co2センサーはいくつか種類がありますが、大きく分けると以下の2つです。
1.NDIR方式 (Non Dispersive Infrared)
2.MOX方式 (Metal Oxide)
NDIR方式は閉空間に取り込んだ気体に赤外線を照射し、ガス分子が特定の波長の光を吸収することを利用して二酸化炭素を検出するもの。
MOX方式は温めた金属酸化物が揮発性有機化合物(TVOC)との反応により抵抗値が変わることを利用して検出するもの。後者は二酸化炭素相当を推定しているものとなるようです。
価格的にはMOX方式の方が割安なのですが、今回は正確な数値が測れて応答性が良いNDIR方式のものを選択しました。
Seeed SCD30
Seeedで出しているSCD30の中身はSensirionが出しているCo2センサーで、そこにカバーとGroveコネクタをつけたものになります。
SeeedのページからSCD30のスペックシートがダウンロード出来るので必要な内容を読み解いていくと、
1.電源 3.3V〜5.0V
2.I/F I2C 3.0V - SCD30内部 Pull-up
となります。ObnizはIOから5Vの出力が出来てObniz Y1は外部で3.3Vの出力があるのでどちらでも出来そうですが、今回は無印Obnizでも使えるようにI/Oの5Vで進めようと思います。I/FのI2Cは内部で3.0Vの電源を作ってPull-upをしてくれているようで、Obniz側でのPull-upは必要なさそうです。注意点はSCD30のI/Fの定格が3.0Vなので間違えてObniz側で5V出力したりすると破壊の可能性があります。(電源とI/Fの電圧が違うことに注意)
Obnizとの接続
ピン配は以下のようになります。
Obniz側 | SCD30側 Grove線色 | 配線名 |
---|---|---|
0 pin | 黒 | GND |
1 pin | 赤 | VCC (5V) |
2 pin | 白 | SDA (3V) |
3 pin | 黄 | SCL (3V) |
Groveは片側がバラ線になるものを利用すると便利です。
電源の発振
Obnizから電源として5Vの出力をしますが、SCD30側の突入電流が大きいらしく発振気味でしたので、暫定としてGNDラインに数Ω入れると安定します(手元にある10Ωを入れました)。無くても動きますがSCD30が立ち上がらない可能性もあるので今回は入れています。起動してしまえば数十mAなのでその後は安定します。
サーボモーター (Co2の濃度表示)
Co2濃度はObnizの画面に表示しますが、小さいのと書き換えでチカチカするので、遠くからでも確認出来るようにアナログのゲージを作ろうと思います。サーボモーターは使ったことは無かったですが、角度指定でその角度にキープしてくれるので便利そうです。0-180度までの回転キープが出来るので、それをCo2の400-3000ppmくらいに割り当てて表現する形にします。
回転方向を縦方向の動きに変えるので、100円ショップで滑車と布ゴムを買ってきてとてもアナログな二酸化炭素濃度表示を作成しました。
DCモーター (換気タイミングのお知らせ)
Co2濃度が1000-1500ppmを超えたら換気することがガイドラインのようですので、1500ppmを超えたらお知らせをする仕組みを作成します。LINEを送ったりブザーを鳴らしても良かったのですが、100円ショップでアヒル隊長と目が合ってしまったのでアヒル隊長に鳴いてもらおうと思います。アヒル隊長の隣に吊るしでビー玉があったのでビー玉を落下させてアヒル隊長を鳴かせることにします。高い位置からビー玉を落とす為にダンボールでピタゴラスイッチのビー玉の階段を作成してそれをDCモーターで駆動します。クランク機構がえらく面倒でしたが、最近はYou Tubeで優しく丁寧に解説してくれているところも多く参考にさせていただきました。
SwitchBot Plug と 植物ライト
室内のCo2濃度を減らすには換気するのが一番手っ取り早いですが、内側からも軽減努力をすべく植物に頑張ってもらおうと、植物ライトを使って光を当てて光合成によるCo2吸収を試みます(実際はほとんど吸収されないので無駄な抵抗ですが)。これを実現するために、AmazonでSwitchBot Plugと植物ライトを購入して、Co2濃度が上がってきたら植物にライトをあて仕組みを作ります。今回、前記の工作が大変だったのでここは手軽にスマートにIFTTTで解決したいと思います。ObnizからIFTTT経由の設定は行っている方もたくさん居ますのでここではあまり語りません。ON/OFFを頻繁にならないように1500ppmを超えたらON、800ppmを下回ったらOFF というヒステリシスをもたせます。
ソフトウェア
SCD30のパーツライブラリ
今回作成したSCD30を制御するJavaScriptは使いやすいようにパーツライブラリ化をしています。ただ、SCD30の全部の機能をアクセス出来るようなAPIは実装しておらず、測定するのに必要な最低限の内容を実装したものとなります。JavaScriptが初めてなので書き方の間違えがあるかもしれませんが、出来るだけ見やすくを心がけた実装にしています。CRC計算とByte配列のFloat変換が少し複雑かもしれません。ご自分のリポジトリにJsファイルを作ってコピペすればお使いいただけると思います。詳しくはObnizのドキュメントが参考になります。
GROVE_SCD30パーツライブラリ
class GROVE_SCD30 {
constructor() {
this.requiredKeys = [];
this.keys = ["vcc", "sda", "scl", "gnd"];
this.commands = {};
this.commands.continuousMeasurement = [0x00, 0x10];
this.commands.setMeasurementInterval = [0x46, 0x00];
this.commands.getDataReady = [0x02, 0x02];
this.commands.readMeasurement = [0x03, 0x00];
this.commands.automaticSelfCalibration = [0x53, 0x06];
this.commands.setForcedRecalibrationFactor = [0x52, 0x04];
this.commands.setTemperatureOffcet = [0x54, 0x03];
this.commands.setAltitudeCompensation = [0x51, 0x02];
this.commands.softReset = [0xD3, 0x04];
this.commands.stopMeasurement = [0x01, 0x04];
this.commands.readFwVersion = [0xD1, 0x00];
this.waitTime = {};
this.waitTime.wakeup = 500;
this.waitTime.softReset = 500;
this.waitTime.writeCmd = 10;
this.address = 0x61;
}
static info() {
return {
name: "GROVE_SCD30",
};
}
async wired(obniz) {
this.obniz = obniz;
this.obniz.setVccGnd(this.params.vcc, this.params.gnd, "5v");
this.params.clock = this.params.clock || 100 * 1000; // for i2c
this.params.mode = this.params.mode || "master"; // for i2c
// this.params.pull = this.params.pull || "3v"; // for i2c
this.i2c = obniz.getI2CWithConfig(this.params);
await this.obniz.wait(this.waitTime.wakeup);
// this.i2c.write(this.address, this.commands.softReset);
// await this.obniz.wait(this.waitTime.softReset);
}
async setMeasureInterval(interval) {
var crc = this.calculateCRC8(this.beUint16(interval));
var writeArray = this.commands.setMeasurementInterval;
writeArray = writeArray.concat(this.beUint16(interval));
writeArray = writeArray.concat(crc);
this.i2c.write(this.address, writeArray);
await this.obniz.wait(this.waitTime.writeCmd);
}
async startContinuousMeasurement(pressure) {
var crc = this.calculateCRC8(this.beUint16(pressure));
var writeArray = this.commands.continuousMeasurement;
writeArray = writeArray.concat(this.beUint16(pressure));
writeArray = writeArray.concat(crc);
this.i2c.write(this.address, writeArray);
var rtn = await this.obniz.wait(this.waitTime.writeCmd);
await this.obniz.wait(this.waitTime.writeCmd);
return rtn;
}
async getDataReadyStatus() {
this.i2c.write(this.address, this.commands.getDataReady);
var rtn = await this.i2c.readWait(this.address, 3);
await this.obniz.wait(this.waitTime.writeCmd);
return rtn;
}
async readMeasurement() {
this.i2c.write(this.address, this.commands.readMeasurement);
let data = await this.i2c.readWait(this.address, 18);
let rtn = new Float32Array(3); //array [co2,tmp,hum]
let buf = new ArrayBuffer(4);
let view = new DataView(buf);
view.setUint8(0,data[0]); //form co2
view.setUint8(1,data[1]);
view.setUint8(2,data[3]);
view.setUint8(3,data[4]);
rtn[0] = view.getFloat32(0,false);
view.setUint8(0,data[6]); //form temperature
view.setUint8(1,data[7]);
view.setUint8(2,data[9]);
view.setUint8(3,data[10]);
rtn[1] = view.getFloat32(0,false);
view.setUint8(0,data[12]); //form humidity
view.setUint8(1,data[13]);
view.setUint8(2,data[15]);
view.setUint8(3,data[16]);
rtn[2] = view.getFloat32(0,false);
return rtn;
}
calculateCRC8(byteArray) {
var len = byteArray.length;
var crc = 0xff;
for (var i = 0; i < len; i ++) {
crc ^= (byteArray[i] % 256);
for (var bit = 0; bit < 8; bit++) {
if (crc & 0x80) {
crc = (((crc << 1) ^ 0x31) %256);
} else {
crc = ((crc << 1) % 256);
}
}
}
return crc;
}
beUint16(val) {
if (val>0xFF) return ([(val>>8),(val&0xFF)]);
else return ([(0x00),(val&0xFF)]);
}
}
if (typeof module === 'object') {
module.exports = GROVE_SCD30;
}
パーツライブラリを使用する場合は以下のコードで読み込むことが出来ます。下記コードはSCD30からセンサー値を読み込んで画面に表示するところまで入っています。要点としては以下2点となります。
<script src="https://obniz.com/users/xxxx/repo/GROVE_SCD30.js"></script>
自分のライブラリの指定をする。(xxxxはご自分の公開パーツのものと読み替えてください。)
Obniz.PartsRegistrate(GROVE_SCD30);
パーツを登録する。
パーツライブラリの使い方
<!-- Obniz SCD30 Co2 Measurement -->
<html>
<head>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<script src="https://obniz.com/js/jquery-3.2.1.min.js"></script>
<script src="https://unpkg.com/obniz@3.14.0/obniz.js"></script>
</head>
<body>
<div id="obniz-debug"></div>
<h1>GROVE SCD30</h1>
<script src="https://obniz.com/users/xxxx/repo/GROVE_SCD30.js"></script>
<script>
var interval = 2;
var pressure = 0;
var obniz = new Obniz("OBNIZ_ID_HERE");
obniz.onconnect = async function () {
Obniz.PartsRegistrate(GROVE_SCD30);
var sensor = obniz.wired("GROVE_SCD30", {gnd:0, vcc:1, sda:2, scl:3});
sensor.setMeasureInterval(interval);
sensor.startContinuousMeasurement(pressure);
while(1) {
await obniz.wait(2100);
var dat = await sensor.getDataReadyStatus();
if (dat[1]==0x01) {
var val = await sensor.readMeasurement();
obniz.display.clear();
obniz.display.print("CO2 = " + val[0].toFixed() + " ppm");
obniz.display.print("TMP = " + val[1].toFixed() + " deg");
obniz.display.print("HUM = " + val[2].toFixed() + " %");
console.log("Co2=" + val[0]);
console.log("tmp=" + val[1]);
console.log("hum=" + val[2]);
console.log(val);
}
}
};
</script>
</body>
</html>
パーツライブラリを作る上では最初から別なライブラリファイルとして行うと、不具合位置が特定出来ないので非常にデバッグしづらいです。なので最初は通常クラスとしての作り込みをして、その後切り出してパーツライブラリ化することをおすすめします。
ソースコード
SCD30はパーツライブラリとして作っているので、こちらのメインの制御コードは表面的な制御のみに特化出来たので、コード全体は簡単になりました。Co2の検出、画面の表示、サーボモータ制御、DCモータ制御、IFTTTへの通知を行っていますが、これだけ短いソースで実装出来ているのは、Obnizならではと思います。
基本的な動きは、
1.SCD30の値(Co2,温度,湿度)を読み画面に表示
2.値によってサーボモータの角度を決める
3.しきい値以上になったらピタゴラ階段スイッチ発動
4.更に超えたらIFTTTで植物ライト発動
となります。
メインの制御コード
<!-- Obniz SCD30 Co2 Measurement -->
<html>
<head>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<script src="https://obniz.com/js/jquery-3.2.1.min.js"></script>
<script src="https://unpkg.com/obniz@3.14.0/obniz.js"></script>
</head>
<body>
<div id="obniz-debug"></div>
<h1>GROVE SCD30</h1>
<script src="https://obniz.com/users/xxxx/repo/GROVE-SCD30.js"></script>
<script>
var interval = 2;
var pressure = 0;
var obniz = new Obniz("OBNIZ_ID_HERE");
const fa = -0.06153;
const fb = 194.615;
const Co2Max = 3000;
const Co2Min = 400;
const Co2Alert = 1500;
const Co2AlertLow = 800;
var Co2AlertFlg = false;
var url_on = "https://maker.ifttt.com/trigger/light_on/with/key/xxxxxxxxxxxxxxxxx";
var url_off = "https://maker.ifttt.com/trigger/light_off/with/key/xxxxxxxxxxxxxxxxx";
obniz.onconnect = async function () {
Obniz.PartsRegistrate(GROVE_SCD30);
var sensor = obniz.wired("GROVE_SCD30", {gnd:0, vcc:1, sda:2, scl:3});
var servo = obniz.wired("ServoMotor", {gnd:5, vcc:6, signal:7});
var motor = obniz.wired("DCMotor", {forward:9, back:10});
sensor.setMeasureInterval(interval);
sensor.startContinuousMeasurement(pressure);
while(1) {
await obniz.wait(2100);
var dat = await sensor.getDataReadyStatus();
if (dat[1]==0x01) {
// SCD30
var val = await sensor.readMeasurement();
obniz.display.clear();
obniz.display.print("CO2 = " + val[0].toFixed() + " ppm");
obniz.display.print("TMP = " + val[1].toFixed() + " deg");
obniz.display.print("HUM = " + val[2].toFixed() + " %");
//ServoMotor
let inCo2 = (val[0] > Co2Max) ? Co2Max : val[0];
inCo2 = (inCo2 < Co2Min) ? Co2Min : inCo2;
let angle = inCo2 * fa + fb;
servo.angle(angle);
//DC motor
if ((val[0] > Co2Alert) && (Co2AlertFlg == false)) {
motor.power(50);
motor.move(true);
await obniz.wait(10000);
motor.stop();
Co2AlertFlg = true;
$.get(url_on, {value1: val[0].toFixed(),});
}
if ((val[0] < Co2AlertLow) && (Co2AlertFlg == true)) {
Co2AlertFlg = false;
$.get(url_off, {value1: val[0].toFixed(),});
}
}
}
};
</script>
</body>
</html>
グローバル変数で状態を持って場合分けしているのであまりきれいなコードではないです。もし、コードを参考にして頂く場合は、パーツライブラリを読み込むところのユーザーIDの「xxxx」と、IFTTTのkeyの「xxxxxxxxxxxxxxxx」はご自分の環境に合わせていただければと思います。
おわりに
Obnizは以前から知っていたのですが、今回の機会であらためて理解をすると、今更ですがWebの技術とデバイス制御技術がうまく融合したとてもおもしろいシステムで、いろんな難しい技術が簡単でスマートに実現出来てしまう仕組みは素晴らしいを思います。子供と一緒に手作りをしながら動くものが作れたので、とても楽しく実施させていただきました。ありがとうございます。いろんなアイデアを組み合わせられる環境であると思いますので、引き続き新しい使い方を考えてみようと思います。
-
kero
さんが
2021/05/16
に
編集
をしました。
(メッセージ: 初版)
ログインしてコメントを投稿する