ObnizにCo2センサを接続 その5
単にセンサをつなげるだけなのに、長々とやってきましたが、今回はようやくObnizにSCD30を接続しての値の取得をします。
(しばらく更新をサボっていたので、どこまで書いたかわからなくなってきました・・何事も後回しにしてはいけないですね。)
|ObnizとSCD30の接続
SHT31の時と同じ様に、Groveからバラ線に変換するコネクタケーブルを用いて、SCD30とObnizの接続をします。以下のように接続しました。
0pin : GND (10Ω抵抗)
1pin : VCC
2pin : SDA
3pin : SCL
オシロで電源の突入を観測してみたところ、SCD30の突入電流が思いのほか大きいらしく、電源が少し発振してしまっていたため、GNDとの間に手元にあった10Ωを加えました。あれば2Ω程度で十分かと思います。
|SCD30の制御条件
この記事のその2から、
1. VCCは3.3V-5.5Vの範囲の投入が必要
2. SCD30の突入電流は大きそうなので引込電流に注意
3. SDA/SCLはSDC30側で3.0VでPull-upされている
4. なのでMCU側でのPull-upは必要ない
5. 電源投入から3秒くらい待ってからI2Cのコマンドを打つ
となるため、まず電源を決めます。Obnizは5V系と3V系があるので、上記条件から見ると3Vでは電圧が足りず、5Vを選択する必要があります。Obnizのドキュメントを見ると、
仮にSCD30の電源電圧が3Vまで駆動したとしても、1mAでは動かないので、どちらにしても5Vを選択する必要があります。また、SDA/SCLもSCD30側でPull-upがされているので、Obniz側ではPull-upをしない設定にする注意が必要です。これを踏まえてコードを作成していきます。
|SCD30のパーツライブラリ
前回のパーツとしてのテストは簡易なLEDを使用しましたが、今回はI2Cを使用するために、SHT31のパーツライブラリをベースに作成していきます。
前回のLEDとは違い、コンストラクタの中に端子の接続情報だけではなく、引数でのモード設定やレジスタアドレス、定数、変数など色々と定義をされていて、わかりやすく作られているので、これに習って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.i2c = obniz.getI2CWithConfig(this.params);
await this.obniz.wait(this.waitTime.wakeup);
}
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;
}
そして、使い方は以下の通り。
<script src="https://obniz.com/users/xxxx/repo/GROVE_SCD30.js"></script>
のxxxxはご自分の公開パーツのものと読み替えてください。
<!-- 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>
以下の様に表示されました。
|パーツライブラリ使い方の流れ
パーツライブラリ使い方の流れはシンプルで、
1. I2Cの端子構成 / 周波数設定 (SDA,SCL,100kHz)
2. 測定周期設定コマンド (Set Measurement Interval)
3. 測定開始コマンド (Start Continuous Measurement)
4. 測定データ読み取り (Read Measurement)
となります。パーツライブラリのコンストラクタの中には今回は使用していないいくつかのレジスタ(キャリブレーションなど)も定義していますので、今後機会があったら実装していこうと思います。
SCD30のマニュアルを読むと、設定値はSCD30内部のnon-volatile memoryに保存されて、再起動時にはその設定値で動き出すようで、IntervalやStart Continuousは1回コマンドを打てば、その後は電源を投入するだけで繰り返し測定を開始するようです。フェールセーフとしてよく考えられているモジュールだと思います。
|パーツライブラリの注意点
パーツライブラリの中身としては実際にSCD30のコマンド作法に則りデータのやり取りを行う必要があるため、少し複雑な計算が必要となります。今回Java Scriptの為、特に難しいのが、
1. CRC8の計算
2. Byte Streamからの32bit Float数値取り出し
の2点です。
1. CRC8
SCD30のコマンド体系は、コマンドのデータフィールドには必ずCRCが入ることになります。
CRCの対象はデータ部分であるため、上記の場合はPressureのMSB/LSBの2byteが対象となります。コマンドの数値が限定されていて2byte程度であれば、計算した値を定数として持っておいても良いのですが、今後の応用も考えてbyteArrayで何バイトでも計算出来るように関数化しています。
その部分だけ抜き出すと以下の通りです。
var interval = 2;
var crc = this.calculateCRC8(this.beUint16(interval));
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)]);
}
2. 32bit Float数値取り出し
Java Scriptでもう一つ難しいのが、メモリ内のbyte配列に対する型の当てはめです。C言語ではポインタとキャストで簡単に出来ますが、Java Scriptではbyte操作やbit操作には少し工夫が必要です。
まず、SCD30からRead出来るByte列は以下の通りで、
32bitのFloatデータとして4byteで構成されており、2byte毎にCRCが挟まるというちょっと嫌な順列となっています。Floatなので整数の様に256倍して足すなどの力技も厳しいため、DataViewを使用して数値型を変換します。
実際のコードを抜き出すと以下の様になります。
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);
dataにはI2Cから取り出したbyte stream
bufは4byteの配列空間
viewはその配列空間をDataViewとして定義
必要なbyteを配列空間に並べて、getFloat32()でFloatに変換します。
このような変換で困っている人、結構いるんじゃないかと思いますので、参考になれば幸いです。
|パーツライブラリのデバッグ
上記までは、出来上がったパーツライブラリとして書いていますが、パーツライブラリとして登録する前にライブラリ化する為のコードのデバッグが必要と思います。この時リポジトリの中に先にパーツライブラリファイルを作って、それをhtmlから参照してデバッグを行うと、パーツライブラリ内のエラーコードがきちんと吐き出せれずに、どこが悪いのだかわからない状況となります。このため、パーツライブラリ化する前に、html内にclassとして配置しておき、デバッグ後にパーツライブラリ化することをおすすめします。
classとして配置した場合のコードは以下となります。
<!-- 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>SCD30</h1>
<script>
var interval = 2;
var pressure = 0;
var obniz = new Obniz("OBNIZ_ID_HERE");
obniz.onconnect = async function () {
var sensor = new GROVE_SCD30;
sensor.params = {gnd:0, vcc:1, sda:2, scl:3};
sensor.wired(obniz);
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);
}
}
};
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.i2c = obniz.getI2CWithConfig(this.params);
await this.obniz.wait(this.waitTime.wakeup);
}
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)]);
}
}
</script>
</body>
</html>
パーツライブラリのコードを下にくっつけた様な形になるのですが、違いの要点は以下です。
・パーツライブラリとして使用する場合
GROVE_SCD30.jsという外部のライブラリを取り込み、PartsRegistrate()関数にてパーツの登録をします。その後obniz.wired()関数で呼び出し。
<script src="https://obniz.com/users/xxxx/repo/GROVE_SCD30.js"></script>
Obniz.PartsRegistrate(GROVE_SCD30);
var sensor = obniz.wired("GROVE_SCD30", {gnd:0, vcc:1, sda:2, scl:3});
・Classとして同じソース内で使用する場合
new()でGROVE_SCD30 classを作成し、パラメータを設定後、class内のwired関数を直接呼び出し。
var sensor = new GROVE_SCD30;
sensor.params = {gnd:0, vcc:1, sda:2, scl:3};
sensor.wired(obniz);
となります。
一部コードの書き換えが必要となりますが、デバッグ状況に応じて使い分けるとデバッグ効率が上がります。
|さいごに
ライブラリがあれば接続して実行すれば出来てしまう内容なのですが、ライブラリがない場合は、結構苦労するのが身を持って体験しました。Obnizだけでは無いですが、Arduinoなどでもライブラリを作って公開していただいている方にもっと感謝しなくてはならないと思います。
今回、部屋の中のCO2濃度を測定してみましたが、窓を閉めている状態では結構濃度が上がることが分かりました。最近ではお店の換気の指標として使用されている様ですが、測定してみると有効な指標であると思います。
今後はせっかく数値が取れる様になったので、他のセンサーと共にデータをロギングしてグラフにしてという事をやってみようかと思います。
この記事が気に入ったらサポートをしてみませんか?