raspberry pi picoWは何ができる?picoWでホームオートメーション

ホームオートメーションスマートセンシングシステム ホームオートメーション
スポンサーリンク

はじめに

Raspberry Pi picoW(以下、ラズパイpicoW)は、小型で高性能なマイクロコントローラーです。
Wi-Fi機能を備えているため、ルーター経由で簡単にサーバー等と通信を行うことができます。
ラズパイpicoWと各種センサーを組み合わせることでホームオートメーションに活用することもできます。

今回はラズパイpicoWとGroveのセンサーを使って部屋の環境を計測してみたいと思います。
計測する内容は、ホームオートメーションに活かせそうな以下の項目にします。

チェック
  • 温湿度
  • TVOC(総揮発性有機化合物)、CO2
  • 照度
  • 人感
リビングの画像

準備するもの

この記事では、以下の物を使います。

●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
アナログA026
アナログA127
アナログA228
デジタルD1616
デジタルD1818
デジタルD2020
UART0 TX0
UART0 RX1
UART1 TX4
UART1 RX5
I2C0 SDA8
I2C0 SCL9
I2C1 SDA6
I2C1 SCL7

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を使用してください。

Tsubablog / SmartHome · GitLab
GitLab.com

ラズパイpicoWの環境構築や、ソースコードの書き込み方は以下の記事で詳しく解説しているので、参考にしてください。

ラズパイpicoWで家の温度と湿度を管理してスマートホーム化
はじめに 家の中の温湿度管理は、快適さだけでなく、エネルギー効率の向上にも重要です。 今回は、Raspberry Pi Pico WとGrove温湿度センサー、家にある古いノートパソコンを使って温湿度監視システムを作りました。このシステムは...

Groveのセンサはseeed studioの公式ホームページで詳細な仕様や、サンプルのソースコードが紹介されています。
使用される場合は一読することをオススメします。

Grove Ecosystem Introduction | Seeed Studio Wiki
Grove Ecosystem Introduction

ソースコード解説

LCD RGB バックライト

「myLcd.py」に関する解説です。

公式サイトのライブラリはおそらくv4.0?の部品構成に合わせた実装となっているため、適宜v5.0用に書き換えていきます。
arduino用であれば、v4.0とv5.0の両方で動作するソースコードが公開されているので、こちらを参考に実装していきます。

GitHub - Seeed-Studio/Grove_LCD_RGB_Backlight: Seeedstudio, Grove - LCD RGB Backlight
Seeedstudio, Grove - LCD RGB Backlight. Contribute to Seeed-Studio/Grove_LCD_RGB_Backlight development by creating an account on GitHub.

ディスプレイの色を制御する(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を使えば、今回ご紹介したセンサで取得した値が一定の値を超えたときに家電を操作するなどの自動化を実現可能です。

最高に快適なホーム作りに活かしていきたいと思います。

コメント

タイトルとURLをコピーしました