本記事は、Raspberry Pi Advent Calendar 2019
の22日目です。
はじめに
ここではRasberry PiにCO2センサーモジュールを搭載して部屋のCO2を計ろうとした男の記録です。
もう最初から謝っておくのですが、+アルファでやりたい事はできていなくて、先人のナレッジをやったらできました、というなんとも恥ずかしい記録でもあります...。
※正直、同じ日に二つもアドベントカレンダー登録するんじゃなかったなと深く反省しております。
とはいえ、参加した以上何かの役にも立つかもしれないので気を取り直して書いていきます。
調達資材
今回準備したのはRabbery Pi 2と、SparkfunのCCS811です。
SparkFun CCS811
SparkFun Raspberry Pi用Qwiic拡張基板
Qwiicケーブル(50mm
今回資材を集めるに当たって、一番の目玉はこのQwiicという接続システムがでした。
私のようなRasbbery Pi初心者にはとても助かりました、ブレッドボード、配線いらずで本運用しようとした際にもそこまで不安がなく使えそうでした。
Qwiic connect system
構築
何も考えずOSはRaspbian Busterを選択しました。
Rasbian Download site
インストール方法は公式のドキュメントに従いました、特にこの子を使ってハマるような事はなかったです。
センサーモジュールの取り付けもQwiicを使っていたのでとても簡単でここもハマりどころはなかったです。
次に実際に動かすソースコードを探しました。
参考にしたURLは下記です。
python-qwiic-CCS811-BME280
このCCS811.pyはI2Cプロトコルを使って、センサーモジュールからデータを取ってくるモジュールとなっています。
後述しますが、このI2Cをよく調べる時間がなかったのが敗因です。
ここのドキュメントを見るとわかるのですが、このモジュールは気温も取得可能です。
そこで、参考にしたソースコードではeCO2とTVOCしか取得しないようになっていた(0x02しか叩いていない)ので、ソースコードの理解がてら0x05の気温のデータも取れるようにしようとしたのですが、時間が足りず断念しました。
CCS811 ドキュメント
CCS811.py
#!/usr/bin python3
import os
import smbus2 as smbus
from collections import OrderedDict
from logging import basicConfig, getLogger, DEBUG, FileHandler, Formatter
from time import sleep
CCS811_ADDRESS = 0x5B
CCS811_STATUS = 0x00
CCS811_MEAS_MODE = 0x01
CCS811_ALG_RESULT_DATA = 0x02
CCS811_HW_ID = 0x20
CCS811_DRIVE_MODE_IDLE = 0x00
CCS811_DRIVE_MODE_1SEC = 0x01
CCS811_DRIVE_MODE_10SEC = 0x02
CCS811_DRIVE_MODE_60SEC = 0x03
CCS811_DRIVE_MODE_250MS = 0x04
CCS811_BOOTLOADER_APP_START = 0xF4
CCS811_HW_ID_CODE = 0x81
class CCS811:
LOG_FILE = '{script_dir}/logs/ccs811.log'.format(
script_dir = os.path.dirname(os.path.abspath(__file__))
)
def __init__(self, mode = CCS811_DRIVE_MODE_1SEC, address = CCS811_ADDRESS):
self.init_logger()
if mode not in [CCS811_DRIVE_MODE_IDLE, CCS811_DRIVE_MODE_1SEC, CCS811_DRIVE_MODE_10SEC, CCS811_DRIVE_MODE_60SEC, CCS811_DRIVE_MODE_250MS]:
raise ValueError('Unexpected mode value {0}. Set mode to one of CCS811_DRIVE_MODE_IDLE, CCS811_DRIVE_MODE_1SEC, CCS811_DRIVE_MODE_10SEC, CCS811_DRIVE_MODE_60SEC or CCS811_DRIVE_MODE_250MS'.format(mode))
self._address = address
self._bus = smbus.SMBus(1)
self._status = Bitfield([('ERROR' , 1), ('unused', 2), ('DATA_READY' , 1), ('APP_VALID', 1), ('unused2' , 2), ('FW_MODE' , 1)])
self._meas_mode = Bitfield([('unused', 2), ('INT_THRESH', 1), ('INT_DATARDY', 1), ('DRIVE_MODE', 3)])
self._error_id = Bitfield([('WRITE_REG_INVALID', 1), ('READ_REG_INVALID', 1), ('MEASMODE_INVALID', 1), ('MAX_RESISTANCE', 1), ('HEATER_FAULT', 1), ('HEATER_SUPPLY', 1)])
self._TVOC = 0
self._eCO2 = 0
if self.readU8(CCS811_HW_ID) != CCS811_HW_ID_CODE:
raise Exception("Device ID returned is not correct! Please check your wiring.")
self.writeList(CCS811_BOOTLOADER_APP_START, [])
sleep(0.1)
if self.checkError():
raise Exception("Device returned an Error! Try removing and reapplying power to the device and running the code again.")
if not self._status.FW_MODE:
raise Exception("Device did not enter application mode! If you got here, there may be a problem with the firmware on your sensor.")
self.disableInterrupt()
self.setDriveMode(mode)
def init_logger(self):
self._logger = getLogger(__class__.__name__)
file_handler = FileHandler(self.LOG_FILE)
formatter = Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
file_handler.setFormatter(formatter)
self._logger.addHandler(file_handler)
self._logger.setLevel(DEBUG)
def disableInterrupt(self):
self._meas_mode.INT_DATARDY = 1
self.write8(CCS811_MEAS_MODE, self._meas_mode.get())
def setDriveMode(self, mode):
self._meas_mode.DRIVE_MODE = mode
self.write8(CCS811_MEAS_MODE, self._meas_mode.get())
def available(self):
self._status.set(self.readU8(CCS811_STATUS))
if not self._status.DATA_READY:
return False
else:
return True
def readData(self):
if not self.available():
return False
else:
buf = self.readList(CCS811_ALG_RESULT_DATA, 8)
self._eCO2 = (buf[0] << 8) | (buf[1])
self._TVOC = (buf[2] << 8) | (buf[3])
if self._status.ERROR:
return buf[5]
else:
return 0
def getTVOC(self):
return self._TVOC
def geteCO2(self):
return self._eCO2
def checkError(self):
self._status.set(self.readU8(CCS811_STATUS))
return self._status.ERROR
def readU8(self, register):
result = self._bus.read_byte_data(self._address, register) & 0xFF
self._logger.debug("Read 0x%02X from register 0x%02X", result, register)
return result
def write8(self, register, value):
value = value & 0xFF
self._bus.write_byte_data(self._address, register, value)
self._logger.debug("Wrote 0x%02X to register 0x%02X", value, register)
def readList(self, register, length):
results = self._bus.read_i2c_block_data(self._address, register, length)
self._logger.debug("Read the following from register 0x%02X: %s", register, results)
return results
def writeList(self, register, data):
self._bus.write_i2c_block_data(self._address, register, data)
self._logger.debug("Wrote to register 0x%02X: %s", register, data)
class Bitfield:
def __init__(self, _structure):
self._structure = OrderedDict(_structure)
for key, value in self._structure.items():
setattr(self, key, 0)
def get(self):
fullreg = 0
pos = 0
for key, value in self._structure.items():
fullreg = fullreg | ( (getattr(self, key) & (2**value - 1)) << pos )
pos = pos + value
return fullreg
def set(self, data):
pos = 0
for key, value in self._structure.items():
setattr(self, key, (data >> pos) & (2**value - 1))
pos = pos + value
ここのmain.pyでは実際に取得した値をlogの中に吐き出すといった処理をしています。
main.py
#!/usr/bin python3
import os
import sys
from logging import basicConfig, getLogger, DEBUG, FileHandler, Formatter
from time import sleep
from CCS811_1 import CCS811
class AirConditionMonitor:
CO2_PPM_THRESHOLD_1 = 1000
CO2_PPM_THRESHOLD_2 = 2000
CO2_LOWER_LIMIT = 400
CO2_HIGHER_LIMIT = 8192
CO2_STATUS_CONDITIONING = 'CONDITIONING'
CO2_STATUS_LOW = 'LOW'
CO2_STATUS_HIGH = 'HIGH'
CO2_STATUS_TOO_HIGH = 'TOO HIGH'
CO2_STATUS_ERROR = 'ERROR'
LOG_FILE = '{script_dir}/logs/air_condition_monitor.log'.format(
script_dir = os.path.dirname(os.path.abspath(__file__))
)
def __init__(self):
self._ccs811 = CCS811()
self.co2_status = self.CO2_STATUS_LOW
self.init_logger()
def init_logger(self):
self._logger = getLogger(__class__.__name__)
file_handler = FileHandler(self.LOG_FILE)
formatter = Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
file_handler.setFormatter(formatter)
self._logger.addHandler(file_handler)
self._logger.setLevel(DEBUG)
def status(self, co2):
if co2 < self.CO2_LOWER_LIMIT or co2 > self.CO2_HIGHER_LIMIT:
return self.CO2_STATUS_CONDITIONING
elif co2 < self.CO2_PPM_THRESHOLD_1:
return self.CO2_STATUS_LOW
elif co2 < self.CO2_PPM_THRESHOLD_2:
return self.CO2_STATUS_HIGH
else:
return self.CO2_STATUS_TOO_HIGH
def execute(self):
while not self._ccs811.available():
pass
while True:
if not self._ccs811.available():
sleep(1)
continue
try:
if not self._ccs811.readData():
co2 = self._ccs811.geteCO2()
co2_status = self.status(co2)
if co2_status == self.CO2_STATUS_CONDITIONING:
self._logger.debug("Under Conditioning...")
sleep(2)
continue
if co2_status != self.co2_status:
self.co2_status = co2_status
self._logger.info("CO2: {0}ppm, TVOC: {1}".format(co2, self._ccs811.getTVOC()))
else:
self._logger.error('ERROR!')
while True:
pass
except:
self._logger.error(sys.exc_info())
sleep(2)
if __name__ == '__main__':
air_condition_monitor = AirConditionMonitor()
air_condition_monitor.execute()
実行結果は以下のようになっています。
2019-12-22 12:22:51,789 - AirConditionMonitor - INFO - CO2: 418ppm, TVOC: 2
2019-12-22 12:22:53,814 - AirConditionMonitor - INFO - CO2: 423ppm, TVOC: 3
2019-12-22 12:22:55,840 - AirConditionMonitor - INFO - CO2: 423ppm, TVOC: 3
2019-12-22 12:22:57,865 - AirConditionMonitor - INFO - CO2: 416ppm, TVOC: 2
2019-12-22 12:22:59,891 - AirConditionMonitor - INFO - CO2: 413ppm, TVOC: 1
2019-12-22 12:23:01,916 - AirConditionMonitor - INFO - CO2: 413ppm, TVOC: 1
2019-12-22 12:23:03,941 - AirConditionMonitor - INFO - CO2: 413ppm, TVOC: 1
2019-12-22 12:23:05,966 - AirConditionMonitor - INFO - CO2: 413ppm, TVOC: 1
2019-12-22 12:23:07,991 - AirConditionMonitor - INFO - CO2: 408ppm, TVOC: 1
2019-12-22 12:23:10,016 - AirConditionMonitor - INFO - CO2: 408ppm, TVOC: 1
2019-12-22 12:23:12,041 - AirConditionMonitor - INFO - CO2: 410ppm, TVOC: 1
2019-12-22 12:23:14,066 - AirConditionMonitor - INFO - CO2: 405ppm, TVOC: 0
2019-12-22 12:23:16,091 - AirConditionMonitor - INFO - CO2: 408ppm, TVOC: 1
2019-12-22 12:23:18,116 - AirConditionMonitor - INFO - CO2: 410ppm, TVOC: 1
2019-12-22 12:23:20,142 - AirConditionMonitor - INFO - CO2: 405ppm, TVOC: 0
2019-12-22 12:23:22,167 - AirConditionMonitor - INFO - CO2: 408ppm, TVOC: 1
2019-12-22 12:23:24,192 - AirConditionMonitor - INFO - CO2: 408ppm, TVOC: 1
2019-12-22 12:23:26,217 - AirConditionMonitor - INFO - CO2: 405ppm, TVOC: 0
2019-12-22 12:23:28,242 - AirConditionMonitor - INFO - CO2: 405ppm, TVOC: 0
まとめ
先人のナレッジがありとても簡単にやりたいことが実現出来たことは素直に楽しかったです。
ただ、まだ課題が残っているので以下を片付けたいと思います。
- 気温の取得が出来ていないので実施する
- 現在の実行間隔がmodeに0x01を投げて1secになっていたので、0x03に変更して60秒間隔の取得にしたい(これは準備されているので簡単そうです)
- I2Cを理解する
- 取得したデータをELKに投げる
わぁ、課題がいっぱいでまだまだ勉強できそうですね、やったー!
ただのインスト記事ですみません、次回は新規性じゃないですけど、自分の+アルファを加えた有益な記事を書きます。