テレワークで部屋に籠ることが多い場合は換気が必要です。実際に2酸化炭素が1000PPMを超えると眠くなったり飽きたり・・・。前々回作った測定装置でそうなるのを体感しました。
梅雨明けした暑い夏は、部屋にエアコンかけて閉じっぱなしにすると益々危険そう。今回は赤外線吸収型(NDIR=nondispersive infrared )のCO2センサーを使って室内の炭酸ガス濃度ロガーを作ってみました。
2酸化炭素が4260nmの赤外線を吸収するという原理を使う古典的な方法のセンサーですが、長年にわたって改良されてきたもので小型化や高性能化が色々行われています。上の写真は今回使うMH-Z19Bという安価なセンサーモジュールです。
前々回の記事に金属酸化物を使ったCCS811半導体センサーで作ったロガーを紹介しました。なぜ、新たにNDIRセンサーでロガーを作ってみるかと言えば、半導体センサー(動作の殆どは内蔵ソフトウェアの機能)は比較的新しいセンサーで鋭敏なのですが、長時間記録するロガーとするには結構手間のかかるキャリブレーション(Baseline registerの設定)が度々必要です。
ArduinoのCCS811標準ライブラリーでは、なんと2019年1月にその設定機能が追加されたばかりです。現在それを別プログラムで行っていますが、校正がしばしば必要なのは、扱いが結構手間です。2台作った測定値の間にはこれまで大きな狂いはないですが、正しい数値かどうかを別原理のロガーと比較してみたいという興味もあります。
NDIRセンサーは多くのメーカーから工夫された安定製品がでていますが、どれも比較的高価です。調べた限りは、長期間の使用では当然キャリブレーションを必要としますが、方法も色々あります。MH-Z19Bではシンプルな方法3つが提供されていますし、なによりも安価(海外ネットでは15ドル程ですから、CCS811の倍ぐらい)なので、これを2個入手して挑戦しました。
測定値のとりかたには次の3通りあります。
①シリアル(UART)通信でコマンドを送り、値をデジタルで直接得る
②PWMのDuty幅(アナログ値)を介して得る
③アナログ出力値を読み取る
まず一番簡単な③をやってみたら旨く測れるのですが、なんと!周期的に特定ノイズが載ってきます。2個とも同じ現象があり、配線の問題ではなくこのセンサーモジュール自体の性格のようです。これを無視するようにプログラムすればできそうですが、この際は③を使わないことにしました。ついでに②もけっこう面倒そうなので、一番確実な①でやってみることにしました。
試しに値をとりプロットすると、おお!長時間忠実にアウトプットされるではないですか!
上は測定開始1010秒(約17分)後から2610秒(約44分)後の間のグラフです。エアコンの風があたっている間は測定値がギザギザになることがわかりました。空気取り入れ口にフィルターがあるからそういう現象になるかも。風のあたらない場所で測るとスムーズです。しかし、風が当たる場所でもプログラムでスムージングすれば問題ない感じです。
このセンサーは約1秒強の間隔でデータが得られますが、ロガーとしては安全のために2秒間隔で測定し毎分1度記録するとします。たとえば測定値を指数平滑法を使って、測定値x0.25+前回保存値x0.75でスムージングするとした場合、仮に500ppmの急激な変化があっても、記録する1分(つまり測定29回)以内で追従できます(計算してみました)ので問題ないわけです。
さて、UART接続したArduinoスケッチは次のように短い超簡単なものです。
/* CO2 plotter example with NDIR MH-Z19b via serial interface */
#include "MHZ19.h"
#include "SoftwareSerial.h"
#define Rx 8 // Master Rx (MISO) = MHZ19 Tx
#define Tx 9 // Master Tx (MOSI) = MHZ19 Rx
#define swSerB 9600 // MH-Z19 spec. serial baud rate
MHZ19 NDIR; // MH-Z19 symbol to be NDIR
SoftwareSerial swSer(Rx, Tx); // (Uno example) create device to MH-Z19 serial
void setup(){ // ***** Arduino setup *****
Serial.begin(9600); // start hardware serial to PC
swSer.begin(swSerB); // start software serial to MH-Z19
delay(100);
NDIR.begin(swSer); // begin NDIR CO2 sensor MHZ19
NDIR.autoCalibration(); // set auto caliration TRUE
}
void loop(){ // ***** Arduino loop *****
int CO2 = NDIR.getCO2(); // get CO2 PPM
int lL = 400; int hL = 1000; // set scale lines
Serial.print(lL); Serial.print(","); Serial.print(CO2);
Serial.print(","); Serial.println(hL);
delay(2000); // measure every 2 seconds
}
// *** end of sketch ***
このスケッチをためして気づいた重要なことは、次の2つです。
①これにArduino標準ライブラリーを使うとマイコン容量をかなり消耗する。ロガーには他のデバイスも複数使用するため、そのやりかたではUNOではとても困難になる。
②MH-Z19というセンサーモジュールは4ピンと5ピンの列があるが、列間距離の仕様が異常。このままブレッドボードなどに刺すとモジュールを壊す恐れがある。
①についてはコンパイル結果が次のとおりで、グローバル変数で実に34%消耗!
②については、サイズの仕様が次のとおりです。
29.54mmという間隔には、ホントにまいってしまいます!端数にわざわざ0.54mmをつけたりして。30.48mmとか、27.94mmとかなら適切で納得できますが。・・・きっと過去に仕様をきめたときの誰かの間違いが長年ずっとひきずっているのでしょうかね。
とにかくこのままブレッドボードなどに刺してはだめ(データシートにはPCBに無理な力をかけないようにと注意書きがあります、ということはそういうことで壊す例が多い?)。とにかく気を付けましょう。
さて、標準ライブラリーが大きすぎるなら、UNOをあきらめて別のモデルを使う方法もありますが、UNOにこだわるなら手で書けばよいわけです。このセンサーとのやりとりも、ソフトウェアSerialも、複雑なものではありませんから。次のようにRYO(Roll-Your-Own)プログラムに書き換えれば問題なく動作をしますし、容量もあまり食いません。
/* CO2 (PPM) testing plotter with MH-Z19b NDIR sensor,
* w/RYO serial-com. & RYO handling V00 Aug.2000 (c) Akira Tominaga */
#define sRx 8 // Master Rx (MISO) = MHZ19 Tx
#define sTx 9 // Master Tx (MOSI) = MHZ19 Rx
// for serial sending
#define sTuS 104-3 // time unit (9600bps) for sending
#define sTuhS 51 // time unit half for sending
char sByte; // character work area to be sent
// for serial receiving
#define tmChk 16 // timing checker for test (D16=A2)
#define sTuR 104-10 // time unit (9600bps) for receive
#define sTuhR 51-21 // time unit / 2 for receive
byte rByte; // character receiving byte
// for MH-Z19b, preset & auto-calib since July 2015, hence no setup required doday.
// Ref:Datasheet v1.0 (by Zhengzhou Winsen Electronics Technology Co.,Ltd)
byte rCO2cmd[] = {0xFF, 0x01, 0x86, 0x00, 0x00, 0x00, 0x00, 0x00, 0x79};
// flags, req, rCO2, 0, 0, 0, 0, 0, chksum
byte hV; // response byte2 high value
byte lV; // response byte3 low value
uint16_t CO2; // CO2=hV*256+lV
void setup(){ // ***** Arduino setup *****
Serial.begin(9600); // start hardware serial to PC
pinMode(sRx, INPUT_PULLUP); // soft Rx to be pulled-up
pinMode(sTx, OUTPUT); // soft Tx as output
digitalWrite(sTx, HIGH); // set it high
pinMode(tmChk, OUTPUT); // timing checker
digitalWrite(tmChk, LOW); // set it LOW
}
void loop(){ // ***** Arduino loop *****
// send "Read CO2" command
for (uint8_t j=0; j<9; j++){
sByte=rCO2cmd[j];
sSend();
}
// receive CO2 value
sRcv(); // byte0 is ignored (0x86 response)
sRcv(); // byte1 is ignored (sensor number)
sRcv(); // byte2 is CO2 value high
hV=rByte; // set it to highValue
sRcv(); // byte3 is CO2 value low
lV=rByte; // set it to lowValue
for (uint8_t j=0; j<5; j++){ // read and ignore residual 5 bytes
sRcv();
}
CO2=hV*256+lV; // calculate CO2 PPM value
// plot scales and CO2 via hardware serial to PC
int lowL = 400; int highL = 1000; // set scales
Serial.print(lowL); Serial.print(","); Serial.print(CO2);
Serial.print(","); Serial.println(highL);
delay(2000); // measure every 2 seconds
}
/**************************************
User defined functions
* *********************************** */
// *** sSend *** send a charater in sByte
void sSend(void) {
#define LSb B00000001 // Least Significant bit
digitalWrite(sTx, LOW);
delayMicroseconds(sTuS);
for (int i = 0; i < 8; i++) { // do the following for 8bits
if ((sByte & LSb) == LSb) { // if one then
digitalWrite(sTx, HIGH); // set sTx high
} else { // else
digitalWrite(sTx, LOW); // set sTx low
}
delayMicroseconds(sTuS); // take required time
sByte = sByte >> 1; // shift a bit to right
}
digitalWrite(sTx, HIGH); // set high as a stoP bit
delayMicroseconds(sTuS + sTuhS); // consume time-unit x 1.5
}
// *** sRcv *** receive a charater in rByte
void sRcv(void) {
#define MSb B10000000 // Most-Significant-bit
#define nMSb B01111111 // not Most-Significant-bit
while(digitalRead(sRx)==HIGH){} // wait for start bit
delayMicroseconds(sTuR + sTuhR); // skip start-bit + sampling-timing
rByte = 0x00; // ** for debug
for (uint8_t i = 0; i < 8; i++) { // do the following for 8bits
digitalWrite(tmChk, HIGH); // sampling-time-checker on
digitalWrite(tmChk, LOW); // sampling-time-checker off
if (digitalRead(sRx) == HIGH) { // if sRx high, then
rByte = rByte | MSb; // set MSb on
} else { // else
rByte = rByte & nMSb; // set MSb zero
}
if (i < 7) rByte = rByte >> 1; // shift rByte excluding last i
delayMicroseconds(sTuR);
}
while (digitalRead(sRx) == LOW) {}; // wait until HIGH (stop bit)
}
/* End of program */
標準ライブラリーではSetupでABC設定(Automatic Baseline calibration)コマンドを送りますが、こちらでは出しません。このデバイスの最近の仕様は出荷時にABC設定済で設定不要となっており、設定の必要性がありません。このスケッチで標準ライブラリーと同じく長時間安定した値が測定されます。
これならArduino Mega2560とかESPなどを持ち出さなくとも、Arduino UNO(ATmega328P)単体で余裕をもってできそうです。なお、タイミングが重要なシリアル通信などをどうやって簡単にRYOで作ったり観察したりするかの方法は、少し長くなりそうなので回を改めてご紹介したいと思います。
ロガーの回路は次のようにすれば、5.0V供給だけですむので前回の半導体センサーよりだいぶ単純です。毎度汚い手書きですみませんが。じつはこれを考えてからプロッターでのテストをしてみて、ソフトのRYO化が必要なことに気づいたのですが。
次はブレッドボードで試作したロガーです。
今回のセンサーはまだキャリブレーションをしていませんが、数値は前回作ったロガー(下の写真の下側)とあまり大きくは違いません。キャリブレーションはこのセンサーに接続したスライドスイッチをオンに(接地)するだけでできるようにしました。CO2が400ppm以下の環境を必要とするので、緑の多いところで光合成の盛んな午後に行うのが妥当かと思います。(あるいはCO2ゼロで赤外線吸収のない窒素ガスかアルゴンガスの中でやればよいのですが、そういうのはなかなかね)。
今回のロガーのプログラムでは、比較的複雑なSD入出力、SPI通信、I2C通信に標準ライブラリーを使い、単純なRTC(リアルタイムクロック)、TM1637表示、このMH-Z19センサー処理、およびソフトウェアシリアル通信をRYOにすることで、十分小さく収まりました。そういうわけでプログラムの記述は少し長いですがArduinoスケッチは次のとおりです。
(2020.8.11 下のスケッチで2行を直接直しました。①回路図に合わせてピン8と9を逆にしました。②Setupの最後の待ち時間を1000ミリ秒に直しました。前の100ミリ秒の場合にはMH-Z19センサーがReadyになってないことがまれにありますので。最近作った基板とともに使い心地の比較等を含めて、今後別記事で最新プログラムとともに改めて紹介するつもりです。)
/*
CO2 Logger with MH-Z19b NDIR sensor,
with RYO coding for RTC, TM1637, MHZ19, and Soft-Serial
Initial version V.00 Aug.1, 2020 (c) Akira Tominaga
*/
#include "Wire.h" // I2C for RTC-DS3231
#include "SPI.h" // SPI for SD drive interface
#include "SD.h" // Micro SD drive
// *** for micro SD writer ***
File aqLog; // aqLog as SD file symbol
#define Cs 10 // SD Chip-select pin is D10
// MOSI=11, MISO=12, CLK=13
#define SDsw 4 // button to close SD file
// *** for TM1637 display ***
#define lDIO 6 // DIO for TM1637 LED
#define lCLK 7 // CLK for TM1637 LED
#define lBrt 0x02 // duty-r 1/16x 02:4,03:10,05:12,07:14
#define lTu 50 // time unit in micro-second
byte lChr; // single byte sent to LED
byte Data[] = { 0x00, 0x00, 0x00, 0x00 }; // LED initial value 0000
// *** UART for CO2 sensor ***
#define sRx 9 // Master Rx (MISO) = MHZ19 Tx
#define sTx 8 // Master Tx (MOSI) = MHZ19 Rx
// (for sending)
#define sTuS 104-3 // time unit (9600bps) for sending
#define sTuhS 51 // time unit half for sending
char sByte; // character work area to be sent
// (for receiving)
#define tmChk 16 // timing checker for test (D16=A2)
#define sTuR 104-10 // time unit (9600bps) for receive
#define sTuhR 51-21 // time unit / 2 for receive
byte rByte; // character receiving byte
// *** for NDIR CO2 sensor MH-Z19b
byte rCO2cmd[] = {0xFF, 0x01, 0x86, 0x00, 0x00, 0x00, 0x00, 0x00, 0x79};
// flags, req, rCO2, 0, 0, 0, 0, 0, chksum
// Auto-calib & preset since Jul.2015, hence no setup required as of doday.
// Ref:datasheet v1.0 (by Zhengzhou Winsen Electronics Technology Co.,Ltd)
byte hV; // response byte2 high value
byte lV; // response byte3 low value
uint16_t CO2=600; // CO2 ppm smoothed value, init. 600
char strCO2[5]; // edit. area for SD recds and display
// *** for Real Time Clock DS3231
byte vR[8]; // values in RTC registers
int vI[8] = { 0, 0, 0, 0, 0, 0, 0, 0}; // RTC data, init 0000
#define RTC_addr 0x68
#define mdI 0 // Initial mode
#define mds 1 // ss
#define mdm 2 // mm
#define mdh 3 // hh
#define mdW 4 // WW=Day of Week
#define mdD 5 // DD
#define mdM 6 // MM
#define mdY 7 // YY
#define sS 0
#define mM 1
#define hH 2
char strMDhm[11]; // edit area for calendar and clock
uint8_t mmSv; // minute-llvalue save area
void setup() // ***** Arduino setup *****
{
Serial.begin(9600); // start hardware serial to PC
// *** for RYO-SW-UART
pinMode(sRx, INPUT_PULLUP); // soft Rx to be pulled-up
pinMode(sTx, OUTPUT); // soft Tx as output
digitalWrite(sTx, HIGH); // set it high
pinMode(tmChk, OUTPUT); // timing checker
digitalWrite(tmChk, LOW); // set it LOW
// *** for RYO-TM1637 interface
pinMode(lCLK, OUTPUT); // lCLK as output
pinMode(lDIO, OUTPUT); // lDIO as output
digitalWrite(lCLK, HIGH); // lCLK high
digitalWrite(lDIO, HIGH); // then lDIO high to avoid Start sig
lDispData(); // display 0000
// *** for SD
pinMode(SDsw, INPUT_PULLUP); // SD file closing button
Wire.begin();
if (!SD.begin(Cs)) {
Data[0]="E"; // show error
lDispData; // display E000
while (1) {}
}
// set SD file name MMDDhhmm
getTime();
String s = strMDhm;
s.concat(".txt"); // complete file name
aqLog = SD.open(s, FILE_WRITE);
aqLog.println("MMDDhhmm,CO2"); // write column hdr
mmSv = vI[mdm]; // save current minute value
delay(1000); // enough time for stability
}
void loop() // ***** Arduino loop *****
{
getTime();
// *** send "Read CO2" command ***
for (uint8_t j = 0; j < 9; j++) {
sByte = rCO2cmd[j];
sSend();
}
// *** get CO2 value ***
sRcv(); // byte0 is 0x86
sRcv(); // byte1 is Sensor number
sRcv(); // byte2 is CO2 value high
hV = rByte; // set it to hV
sRcv(); // byte3 is CO2 value low
lV = rByte; // set it to lV
// for (uint8_t j = 0; j < 5; j++) { // read residual 5 bytes
// sRcv(); // *** can be ignored
// } // hence, test use only
uint16_t mCO2 = hV * 256 + lV; // get CO2 PPM value
CO2=(float)(0.250*mCO2+0.750*CO2); // smoothed with exp.factor 0.25
// plot bottom, ceiling and CO2 to PC via HW-serial
int lowL = 400;
int highL = 1000;
Serial.print(lowL);
Serial.print(",");
Serial.print(CO2);
Serial.print(",");
Serial.println(highL);
edDisp(); // edit CO2 Data and display
// *** Write SD data every minute ***
if (vI[mdm] != mmSv) { // if minute changed
mmSv = vI[mdm]; // save new minute
String s = strMDhm; // set date-time
s.concat(",") ; // set comma
s.concat(strCO2); // set data
aqLog.println(s); // write a record
//Serial.println(s); // this is for debug only
}
// *** 2s delay, w/ SD-close-button checks every 100mS ***
for (int i = 0; i < 20; i++) {
if (digitalRead(SDsw) == LOW) { // if SD sw to close
aqLog.close();
// clear display and stop
for (int j=0; j<4; j++){
Data[j]=0x10;
}
lDispData();
while (1) {}
}
delay(100);
}
}
/**************************************
User defined functions
* *********************************** */
// *** edDisp() *** Edit and display values
void edDisp(void) {
sprintf(strCO2, "%04d", CO2);
// display CO2 value via Data
for (int i = 0; i < 4; i++) {
Data[i] = strCO2[i] & 0x0F;
}
lDispData();
}
// *** lDispData() *** display data to TM1637 4digits
void lDispData(void) {
#define showDat 0x40 // show this is data
#define showAd0 0xC0 // LED register addr is zero
#define showDcB 0x88+lBrt // show dCtl + brightness
lStart(); // start signal
lChr = showDat; // identifier for data
lSend(); // send it
lStop(); // stop signal
lStart(); // and restart
lChr = showAd0; // identifier for address
lSend(); // send it
for (int j = 0; j < 4; j++) { // for Data[0] to Data[3]
byte edChr = Data[j]; // set a byte to edChr for edit
switch (edChr) {
case 0x00: lChr = 0x3F; break; // 0
case 0x01: lChr = 0x06; break; // 1
case 0x02: lChr = 0x5B; break; // 2
case 0x03: lChr = 0x4F; break; // 3
case 0x04: lChr = 0x66; break; // 4
case 0x05: lChr = 0x6D; break; // 5
case 0x06: lChr = 0x7D; break; // 6
case 0x07: lChr = 0x07; break; // 7
case 0x08: lChr = 0x7F; break; // 8
case 0x09: lChr = 0x6F; break; // 9
case 0x10: lChr = 0x00; break; // blank
default: lChr = 0x79; // E for error
}
lSend(); // send each byte continuously
} // end of for bytes
lStop(); // stop signal
lStart(); // restart
lChr = showDcB; // identifier for display brightness
lSend(); // send it
lStop(); // stop signal
}
// *** lSend() *** send a charater in lChr to TM1637
void lSend(void) {
#define LSb B00000001 // Least Significant bit
for (int i = 0; i < 8; i++) { // do the following for 8bits
if ((lChr & LSb) == LSb) { // if one then
digitalWrite(lDIO, HIGH); // set lDIO high
} else { // else
digitalWrite(lDIO, LOW); // set lDIO low
}
lCLKon(); // clock on to show lDIO
lChr = lChr >> 1; // shift bits to right
}
digitalWrite(lDIO, LOW); // pull down during Ack
lCLKon(); // clock on to simulate reading
}
// *** lStart() *** send Start signal to TM1637
void lStart(void) {
digitalWrite(lDIO, LOW);
delayMicroseconds(lTu);
digitalWrite(lCLK, LOW);
delayMicroseconds(lTu);
}
// *** lStop() *** send stoP signal to TM1637
void lStop(void) {
digitalWrite(lCLK, HIGH);
delayMicroseconds(lTu);
digitalWrite(lDIO, HIGH);
delayMicroseconds(lTu);
}
// *** lCLKon() ** TM1637 clock on to show data to TM1637
void lCLKon(void) {
digitalWrite(lCLK, HIGH);
delayMicroseconds(lTu);
digitalWrite(lCLK, LOW);
delayMicroseconds(lTu);
}
// *** getTime() *** get RTC MMDD & hhmm into strMDhm
void getTime(void) {
rRTC(); // read RTC device
cR2I(); // convert RTC-format to Integers
sprintf(strMDhm, "%02d%02d%02d%02d", vI[mdM], vI[mdD], vI[mdh], vI[mdm]);
}
// *** rRTC() *** read Real-Time-Clock
void rRTC(void) {
Wire.beginTransmission(RTC_addr);
Wire.write(0x00);
Wire.endTransmission();
Wire.requestFrom(RTC_addr, 7);
while (Wire.available() < 7) {} // wait for data ready
for (int i = 1; i < 8; i++) {
vR[i] = Wire.read();
}
Wire.endTransmission();
}
// *** cR2I() *** convert RTC-format to Integers
void cR2I(void) {
vI[mds] = ((vR[mds] & B01110000) / 16) * 10 + (vR[mds] & B00001111);
vI[mdm] = ((vR[mdm] & B01110000) / 16) * 10 + (vR[mdm] & B00001111);
vI[mdh] = ((vR[mdh] & B00100000) / 32) * 20 + ((vR[mdh] & B00010000) / 16) * 10 + (vR[mdh] & B00001111);
vI[mdW] = vR[mdW];
vI[mdD] = ((vR[mdD] & B01110000) / 16) * 10 + (vR[mdD] & B00001111);
vI[mdM] = ((vR[mdM] & B00010000) / 16) * 10 + (vR[mdM] & B00001111);
vI[mdY] = ((vR[mdY] & B11110000) / 16) * 10 + (vR[mdY] & B00001111);
}
// *** sSend() *** send a charater in sByte via SW UART
void sSend(void) {
#define LSb B00000001 // Least Significant bit
digitalWrite(sTx, LOW);
delayMicroseconds(sTuS);
for (int i = 0; i < 8; i++) { // do the following for 8bits
if ((sByte & LSb) == LSb) { // if one then
digitalWrite(sTx, HIGH); // set sTx high
} else { // else
digitalWrite(sTx, LOW); // set sTx low
}
delayMicroseconds(sTuS); // take required time
sByte = sByte >> 1; // shift a bit to right
}
digitalWrite(sTx, HIGH); // set high as a stoP bit
delayMicroseconds(sTuS + sTuhS); // consume time-unit x 1.5
}
// *** sRcv() *** receive a charater in rByte, via SW UART
void sRcv(void) {
#define MSb B10000000 // Most-Significant-bit
#define nMSb B01111111 // not Most-Significant-bit
while (digitalRead(sRx) == HIGH) {} // wait for start bit
delayMicroseconds(sTuR + sTuhR); // skip start-bit + sampling-timing
rByte = 0x00; // ** for debug
for (uint8_t i = 0; i < 8; i++) { // do the following for 8bits
digitalWrite(tmChk, HIGH); // sampling-time-checker on
digitalWrite(tmChk, LOW); // sampling-time-checker off
if (digitalRead(sRx) == HIGH) { // if sRx high, then
rByte = rByte | MSb; // set MSb on
} else { // else
rByte = rByte & nMSb; // set MSb zero
}
if (i < 7) rByte = rByte >> 1; // shift rByte excluding last i
delayMicroseconds(sTuR);
}
while (digitalRead(sRx) == LOW) {}; // wait until HIGH (stop bit)
}
/* End of program */
このスケッチのコンパイルサイズは次のとおり、UNOの容量の50%台にしっかり収まっています。
測定値はいままでのところ長時間使っても安定しており、数値には換気がしっかり反映されています。とりあえず、めでたしめでたし^^
では今回はこの辺で。
追加記事はこちらにあります:
©2010 Akira Tominaga, All rights reserved.