はじめに
Raspberry Pi picoW(以下、ラズパイpicoW)は、小型で高性能なマイクロコントローラーです。
Wi-Fi機能を備えているため、ルーター経由で簡単にサーバー等と通信を行うことができます。
ラズパイpicoWと各種センサーを組み合わせることでホームオートメーションに活用することもできます。
今回はラズパイpicoWとGroveのセンサーを使って部屋の環境を計測してみたいと思います。
計測する内容は、ホームオートメーションに活かせそうな以下の項目にします。
準備するもの
この記事では、以下の物を使います。
●Raspberry Pi Pico WH
通常のOSが載っているようなラズパイとは違い、より電子工作向けにスペックが落とされたマイコン。
Wi-Fi無線通信モジュールが搭載されたもので、簡単にWiFi通信を実現。
ピンヘッダが実装済みのため、はんだ付け不要ですぐに使い始めることが可能。
とても小さいので、今回のようなセンサー値をとりただけの用途には最適です。
Wifiに接続できるので、いろいろなホームオートメーションに利用できます。
●USBケーブル
ラズパイpicoWはMicro USB Type-B(2.0)端子のUSBケーブルが必要になります。
●Pi Pico v1.0用Grove シールド
ラズパイpicoと接続することで、はんだ付けなしでgroveのモジュールを使うことができます。
この商品はpico用のようですがpicoWでも問題なく使うことができます。
Groveポート | ラズパイpico GPIO |
アナログA0 | 26 |
アナログA1 | 27 |
アナログA2 | 28 |
デジタルD16 | 16 |
デジタルD18 | 18 |
デジタルD20 | 20 |
UART0 TX | 0 |
UART0 RX | 1 |
UART1 TX | 4 |
UART1 RX | 5 |
I2C0 SDA | 8 |
I2C0 SCL | 9 |
I2C1 SDA | 6 |
I2C1 SCL | 7 |
●GROVE – LCD RGB バックライト
I2C接続で16文字2行を表示できるLCDです。
バックライトはRGBをそれぞれ0~255まで調整することができるので、エラーは赤、成功は緑など直感的な表示が可能です。
無くても問題ないですが、ラズパイpicoWの処理状況などを表示するのに便利です。
なによりかっこいいです。
電子工作をしている感が跳ね上がります。
●GROVE – デジタル温度・湿度センサ(DHT20)
I2C接続で温湿度を測ることができるセンサです。
1日の温湿度を計測してみたところ納得のいく遷移をしており、とても満足なセンサでした。
●GROVE – VOC and eCO2 Gas Sensor(SGP30)
I2C接続でシックハウス等の原因となるVOC(総揮発性有機化合物)や、CO2濃度を計測することができます。
ただしCO2は実際に計測している訳ではなく、VOCから算出しているとのことです。
全く使っていない部屋のVOCが急に上がったり夜中は高かったりと若干使いづらいところはありますが、息を吹きかけると数値が上がる等、使える部分もあります。
安価なセンサにしては十分に楽しめるかと思います。
●GROVE – Light Sensor
アナログ入力で光の強度を測ることができるセンサです。
感度がよく、十分な商品だと思います。
●GROVE – mini pir motion sensor
デジタル入力で人等の動きを検知することができるセンサです。
動きを検知した後しばらくHighのままになったりする等、正確な検知には向いていませんが遊びで使う分には十分です。
使用するソースコード
以下のGitにこの記事で使用するソースコードを公開しています。
タグ2.0.0を使用してください。
ラズパイpicoWの環境構築や、ソースコードの書き込み方は以下の記事で詳しく解説しているので、参考にしてください。
Groveのセンサはseeed studioの公式ホームページで詳細な仕様や、サンプルのソースコードが紹介されています。
使用される場合は一読することをオススメします。
ソースコード解説
LCD RGB バックライト
「myLcd.py」に関する解説です。
公式サイトのライブラリはおそらくv4.0?の部品構成に合わせた実装となっているため、適宜v5.0用に書き換えていきます。
arduino用であれば、v4.0とv5.0の両方で動作するソースコードが公開されているので、こちらを参考に実装していきます。
ディスプレイの色を制御する(RGB)部分と、文字を表示させる部分(LCD)でI2Cのアドレスが違うようです。
RGBを制御するIC(SGM31323)のデータシート
https://www.sg-micro.com/rect/assets/9764dbc1-5c80-4210-ab71-ccb414e855b0/SGM31323.pdf
LCDを制御するIC(AIP310681)のデータシート
https://www.orientdisplay.com/wp-content/uploads/2022/08/AIP31068L.pdf
完成品(JHD1313)?のデータシート
https://files.seeedstudio.com/wiki/Grove_LCD_RGB_Backlight/res/JHD1313%20FP-RGB-1%201.4.pdf
I2Cアドレスは以下の通りです。
対象 | I2Cアドレス |
RGB(SGM31323) | 0x30 |
LCD(AIP310681) | 0x3e |
import utime
from initI2C import i2c0
# Grove-LCD RGB Backlight v5のI2Cアドレスを定義
DISPLAY_RGB_ADDR = 0x30
DISPLAY_TEXT_ADDR = 0x3e
RGB(SGM31323)にI2Cでコマンドを送信し、色を設定できる関数を作成します。
REG0に0x07(00000111)を書き込み、Reset Complete Chipし、
REG4に0x15(00010101)を書き込み、LED1~LED3をONにしています。
RGBそれぞれの設定値はREG6~REG8に0~255の値を書き込みます。
ついでに、RGBの色を連続で変えていく
デモコードも載せます。
# set backlight to (R,G,B) (values from 0..255 for each)
# LCDがv5であることに注意
def setRGB(r,g,b):
i2c0.writeto_mem(DISPLAY_RGB_ADDR,0x00,b'\x07')
i2c0.writeto_mem(DISPLAY_RGB_ADDR,0x04,b'\x15')
i2c0.writeto_mem(DISPLAY_RGB_ADDR,0x06,r.to_bytes(1,'little'))
i2c0.writeto_mem(DISPLAY_RGB_ADDR,0x07,g.to_bytes(1,'little'))
i2c0.writeto_mem(DISPLAY_RGB_ADDR,0x08,b.to_bytes(1,'little'))
# LCD RGB Demo
def breath(color):
for i in range(255):
i2c0.writeto_mem(DISPLAY_RGB_ADDR,0x06,i.to_bytes(1,'little'))
utime.sleep_ms(5)
for i in range(255):
i2c0.writeto_mem(DISPLAY_RGB_ADDR,0x07,i.to_bytes(1,'little'))
utime.sleep_ms(5)
for i in range(255):
i2c0.writeto_mem(DISPLAY_RGB_ADDR,0x08,i.to_bytes(1,'little'))
utime.sleep_ms(5)
utime.sleep_ms(500)
for i in range(254,-1, -1):
i2c0.writeto_mem(DISPLAY_RGB_ADDR,0x06,i.to_bytes(1,'little'))
utime.sleep_ms(5)
for i in range(254,-1, -1):
i2c0.writeto_mem(DISPLAY_RGB_ADDR,0x07,i.to_bytes(1,'little'))
utime.sleep_ms(5)
for i in range(254,-1, -1):
i2c0.writeto_mem(DISPLAY_RGB_ADDR,0x08,i.to_bytes(1,'little'))
utime.sleep_ms(5)
utime.sleep_ms(500)
LCD(AIP310681)にI2Cでコマンドを送信し、文字を表示できる関数を作成します。
(と言いつつ、ここは公式サイトのコピーです。)
「textCommand」はコマンドを送るための関数です。
コマンドを送る場合はコントロールバイトに「0x80」を指定する必要があるようです。
表示したい文字を送る場合はコントロールバイトは「0x40」になりそうです。
# send command to display (no need for external use)
def textCommand(cmd):
i2c0.writeto_mem(DISPLAY_TEXT_ADDR,0x80,cmd.to_bytes(1,'little'))
まずは、ディスプレイの動作モードなどを決定する初期化処理です。
コマンド「0x01」(00000001)はディスプレイをリセットするコマンド、
コマンド「0x08 | 0x04」(00001100)はカーソルなしでディスプレイをONにするコマンド、
コマンド「0x28」(00101000)は2Lineモードで動作するコマンドです。
コマンドを送信した後は、コマンドが実行される時間を稼ぐためにsleepしています。
なんとなくデータシートの初期化処理のシーケンスを満たしていないような気もしますが、動いているので良しとします。
次に表示したい文字をDDRAMへ1文字ずつ送信していきます。
送りたい文字列を1文字ずつ上述のコントロールバイト「0x40」を指定して送っていきます。
現状のアドレスはAC(アドレスカウンター)が自動でインクリメントしていくものと思われます。
ディスプレイをクリアした直後は1行目のDDRAMアドレス0x00から書き込みを開始し、改行が必要になれば、コマンド「0xC0」(11000000)でACに2行目のDDRAMアドレス0x40をセットしています。
# set display text \n for second line(or auto wrap)
def setText(text):
textCommand(0x01) # clear display
utime.sleep_ms(50)
textCommand(0x08 | 0x04) # display on, no cursor
textCommand(0x28) # 2 lines
utime.sleep_ms(50)
count = 0
row = 0
for c in text:
if c == '\n' or count == 16:
count = 0
row += 1
if row == 2:
break
textCommand(0xc0)
if c == '\n':
continue
count += 1
i2c0.writeto_mem(DISPLAY_TEXT_ADDR,0x40,c.encode("utf-8"))
以下の関数は、最初にディスプレイをクリアしていないので、前に表示した文字列を残しつつ1行目の最初から再度表示したいときに使います。
#Update the display without erasing the display
def setText_norefresh(text):
textCommand(0x02) # return home
utime.sleep_ms(50)
textCommand(0x08 | 0x04) # display on, no cursor
textCommand(0x28) # 2 lines
utime.sleep_ms(50)
count = 0
row = 0
while len(text) < 32: #clears the rest of the screen
text += ' '
for c in text:
if c == '\n' or count == 16:
count = 0
row += 1
if row == 2:
break
textCommand(0xc0)
if c == '\n':
continue
count += 1
i2c0.writeto_mem(DISPLAY_TEXT_ADDR,0x40,c.encode("utf-8"))
デジタル温度・湿度センサ(DHT20)
「myDht20.py」と「dht20.py」で構成されています。
「dht20.py」は公式サイトからダウンロードしたライブラリなので割愛し、「myDht20.py」について解説していきます。
「dht20.py」をインクルードして利用しつつ、必要な機能のみ実装していきます。
まずはクラスDHT20のインスタンスを生成します。
from initI2C import i2c0
from dht20 import DHT20
#温湿度計の設定
DHT20_ADDR = 0x38
dht20 = DHT20(DHT20_ADDR, i2c0)
コンソールに表示するだけの関数「showDht20Data」と、取得したデータを配列で返す関数「getDht20Data」を定義します。
このセンサはライブラリがあるので、とても簡単に実装できますね。
クラスDHT20の「measurements」を呼ぶことで、温度(t)と湿度(rh)の値が入った配列を返してもらえます。
後は適当に小数点を丸めてコンソールに表示、もしくは配列のまま返すだけです。
def showDht20Data():
measurements = dht20.measurements
temp_rounded = round(measurements['t'], 1)
humidity_rounded = round(measurements['rh'], 1)
print('temp = ' + str(temp_rounded) + ' ℃ \t humidity = ' + str(humidity_rounded) + ' ppb')
def getDht20Data():
measurements = dht20.measurements
temp_rounded = round(measurements['t'], 1)
humidity_rounded = round(measurements['rh'], 1)
return {
't': temp_rounded,
'rh': humidity_rounded,
}
VOC and eCO2 Gas Sensor(SGP30)
「mySgp30.py」と「adafruit_sgp30.py」で構成されています。
adafruitのライブラリがありましたので、活用させて頂きました。
https://github.com/alexmrqt/micropython-sgp30/tree/master
「sgp30_simpletest.py」を編集して「mySgp30.py」にしていますので、ここのみ解説します。
sgp30のデータシート
https://sensirion.com/file/datasheet_sgp30
まず、i2c0は複数のセンサで使用するので、専用のファイル「initI2C」で定義を行い、各センサはこれをインクルードして使うようにしています。
i2c0を引数にして、adafruit_sgp30クラスのインスタンスを定義します。
ついでに、sgp30のシリアルナンバーをコンソールに表示しています。
import time
from initI2C import i2c0
import adafruit_sgp30
# Create library object on our I2C port
sgp30 = adafruit_sgp30.Adafruit_SGP30(i2c0)
print("SGP30 serial #", [hex(i) for i in sgp30.serial])
次にsgp30の初期化を行います。
まずは「sgp30_iaq_init」コマンドでセンサを初期化してから15秒間待っています。
コマンド送信後15秒間、センサは初期化フェーズにあり、その間、「sgp_measure_iaq」コマンドを送信しても、400 ppm CO2eq および 0 ppb TVOC の固定値を返すようです。
# Initialize SGP-30 internal drift compensation algorithm.
sgp30.iaq_init()
# Wait 15 seconds for the SGP30 to properly initialize
print("Waiting 15 seconds for SGP30 initialization.")
time.sleep(15)
sgp30は起動後すぐに安定して動作しているときのTVOCとCO2のベースラインを読み込ませる機能があります。
ラズパイpico Wにベースラインが保存されていれば、sgp30に読み込ませます。
# Retrieve previously stored baselines, if any (helps the compensation algorithm).
has_baseline = False
try:
f_co2 = open('co2eq_baseline.txt', 'r')
f_tvoc = open('tvoc_baseline.txt', 'r')
co2_baseline = int(f_co2.read())
tvoc_baseline = int(f_tvoc.read())
#Use them to calibrate the sensor
sgp30.set_iaq_baseline(co2_baseline, tvoc_baseline)
f_co2.close()
f_tvoc.close()
has_baseline = True
except:
print('Impossible to read SGP30 baselines!')
#Store the time at which last baseline has been saved
baseline_time = time.time()
sgp30のデータを表示、もしくは配列で取得する関数です。
sgp30はベースラインを適切に更新するために、iaq_measure関数を1秒に1回呼び出す必要があります。
最後にベースラインを更新してから1時間後、もしくは、初めてベースラインをするときは起動してから12時間後にiaq_measureで読み取った値でベースラインを更新します。
#適切なベースライン更新のため1秒毎に呼ぶこと
def showSgp30Data():
global has_baseline
global baseline_time
co2eq, tvoc = sgp30.iaq_measure()
print('co2eq = ' + str(co2eq) + ' ppm \t tvoc = ' + str(tvoc) + ' ppb')
# Baselines should be saved after 12 hour the first timen then every hour,
# according to the doc.
if (has_baseline and (time.time() - baseline_time >= 3600)) \
or ((not has_baseline) and (time.time() - baseline_time >= 43200)):
print('Saving baseline!')
baseline_time = time.time()
try:
f_co2 = open('co2eq_baseline.txt', 'w')
f_tvoc = open('tvoc_baseline.txt', 'w')
bl_co2, bl_tvoc = sgp30.get_iaq_baseline()
f_co2.write(str(bl_co2))
f_tvoc.write(str(bl_tvoc))
f_co2.close()
f_tvoc.close()
has_baseline = True
except:
print('Impossible to write SGP30 baselines!')
def getSgp30Data():
global has_baseline
global baseline_time
co2eq, tvoc = sgp30.iaq_measure()
print('co2eq = ' + str(co2eq) + ' ppm \t tvoc = ' + str(tvoc) + ' ppb')
# Baselines should be saved after 12 hour the first timen then every hour,
# according to the doc.
if (has_baseline and (time.time() - baseline_time >= 3600)) \
or ((not has_baseline) and (time.time() - baseline_time >= 43200)):
print('Saving baseline!')
baseline_time = time.time()
try:
f_co2 = open('co2eq_baseline.txt', 'w')
f_tvoc = open('tvoc_baseline.txt', 'w')
bl_co2, bl_tvoc = sgp30.get_iaq_baseline()
f_co2.write(str(bl_co2))
f_tvoc.write(str(bl_tvoc))
f_co2.close()
f_tvoc.close()
has_baseline = True
except:
print('Impossible to write SGP30 baselines!')
return {
'co2eq': co2eq,
'tvoc': tvoc,
}
Light Sensor
light Sensorはラズパイpico Wのアナログピンで、アナログ値を読みだすだけです。
import machine
import utime
# Raspberry Pi Picoの26ピン=ADC0
pin26 = machine.ADC(0)
def showLightData():
value = pin26.read_u16()
print("lightness:{}".format(value))
def getLightData():
value = pin26.read_u16()
return value
mini pir motion sensor
mini pir motion sensorはラズパイpico Wのデジタルピンで、デジタル値を読みだすだけです。
from machine import Pin
import utime
pir_sensor = Pin(16, Pin.IN)
def showPirData():
try:
# Sense motion, usually human, within the target range
if pir_sensor.value():
print('Motion Detected')
else:
print('no Motion')
# if your hold time is less than this, you might not see as many detections
#utime.sleep(.2)
except Exception as e:
print('Pir Error')
def getPirData():
return pir_sensor.value()
計測結果
外観
ラズパイpico Wと上記のセンサを組み合わせて、出来上がったホームオートメーションスマートセンシングシステムがこちらです。
100均で買ったカードケースにカッターで穴をあけて、センサ類をはめ込んでいます。
温湿度グラフ
すっかり冬なので、気温は14℃~15℃の間を遷移しています。
湿度は朝になぜか一度高くなっていますね。
精度は十分で、気温や湿度を条件にしてエアコンや加湿器をONにする用途などに使えそうです。
空気質グラフ
計測した部屋はこの日使っていなかったのですが、8時頃にかなり空気質が悪くなっています。
湿度と謎の相関関係があり、信頼に足るかは分かりません
数値が高くなった時にサーキュレータ等を回してもいいですが、湿度との兼ね合いが難しそうです。
光強度グラフ
冬の光の入りずらい部屋に置いています。
(さらに半透明のカードケースに入れて使っているので、光強度の数値は低くなってしまっています)
日中12時過ぎからなぜか少し暗くなっていますが、これは毎日このようになっています。
太陽が真上にあるとき、部屋の中は少し暗くなるのでしょうか…。
精度は十分なので、暗くなってきたらシーリングライトをつける用途に使えそうです。
人感グラフ
人感センサは前にいる時だけ入力がhighになりますが、このグラフは10分間の内に一度でもhighになれば、その10分間はhighとしてプロットしています。
誰も部屋には入っていませんが、たまに信号がHighになっています。
玄関で人を検知したら電気をつける等の用途に使いたいと思いましたが、これでは、付いたり消えたりしそうです。
今のところ、眺めて喜ぶくらいしか思いつきません。
応用
switchbotやnature remoなどのスマートリモコンはAPIを公開しており、それを叩くことで登録した家電を操作することができます。
このAPIを使えば、今回ご紹介したセンサで取得した値が一定の値を超えたときに家電を操作するなどの自動化を実現可能です。
最高に快適なホーム作りに活かしていきたいと思います。
コメント