Raspberry Pi3 でのlirc信号生成スクリプトの作成
目的
前回信号解析を行ったリモコンの信号を解析結果に基づいて生成することを目指します。
方針
システムは以下の方針で設計します。
- 今後、他のリモコンも登録して使う可能性があるので汎用的なシステムとする
- 設定入力は人間が理解できるフォーマットにする
- 将来的にはGUIによって設定可能なものとする
- 出力はlircのフォーマット
システム構成
上記の方針と自分の勉強もかねて以下のシステム構成にしました。
設定はjson形式で入力し、内部には機械の設定情報と使用する信号フォーマットのIDを格納します。
コンバートスクリプトはこのフォーマットIDから信号フォーマットライブラリを参照して、標準出力にlirc用の信号を出力します。
信号フォーマットライブラリを拡張することでコンバートスクリプトを大幅に変更することなく信号が出力できるようになる仕組みです。
json形式にしたのはwebアプリケーションとの親和性が高そうなことと、自分の勉強のためです。
詳細
次にそれぞれの詳細を説明していきます。
設定(json)
{ "format_id":"a01", "data":{ "mode":"d", "status":"on", "temperature":28, "strength":"auto", "direction":"auto", "dry":"off", "power":"off", "silent":"off", "smell":"on" } }
大きく分けて二つのオブジェクトからなります。
使用するフォーマットIDと設定値格納オブジェクトです。
フォーマットIDはこの後に'.json'をつけると信号フォーマットライブラリのフォーマットファイルになります。
設定値格納オブジェクトは運転モードや温度などの設定情報をさらにネストして格納しています。
信号フォーマットファイル(json)
今回解析したエアコンの信号をこのようなjson形式として書きました。
{ "id":1, "type":"AEHA", "target":"aircon", "signal":{ "T":436, "trailer_t":10000, "frame":[ { "bitmap":[ [0], [0], [0,4], [0], [0], [4,0], [1], [0], [4,0], [0], [0], [0], [0], [6,5,0], [0], [0], [0], [0], [0] ], "value":[ "0x2", "0x20", "0x0+0xe", "0x4", "0x0", "mode+status", "temperature", "0x80", "strength+direction", "0x0", "0x0", "0x6", "0x60", "dry+silent+power", "0x0", "0x80", "0x0", "smell", "sum" ] }, { "bitmap":[ [0], [0], [0,4], [0], [0], [0], [0], [0] ], "value":[ "0x2", "0x20", "0x0+0xe", "0x4", "0x0", "0x0", "0x0", "0x6" ] } ], "rule":{ "mode":{ "a":"0x0", "h":"0x4", "c":"0x3", "d":"0x2" }, "status":{ "off":"0x0", "on":"0x1", "toff":"0x5", "ton":"0x8" }, "strength":{ "auto":"0xa", "1":"0x3", "2":"0x4", "3":"0x5", "4":"0x7" }, "direction":{ "auto":"0xf", "1":"0x1", "2":"0x2", "3":"0x3", "4":"0x4", "5":"0x5" }, "smell":{ "on":"0x16", "off":"0x6" }, "dry":{ "on":"0x1", "off":"0x0" }, "silent":{ "on":"0x1", "off":"0x0" }, "power":{ "on":"0x1", "off":"0x0" } } } }
大雑把には以下のような構成です。
header部ではこのファイル自体の情報を定義しています。
次に信号設定部で信号における必要情報を定義しておきます(単位時間Tなど)
次に出力されるフレームを1フレーム毎にビットシフト量の定義である"bitmap"と格納する値の定義である"value"をセットにして配列として格納します。
基本的な考え方として8bit(1バイト)を一単位として考えその中でどのようなデータがどのようなビットシフトがなされて格納されているのかといった情報をフレーム毎に持っているというものです。
各バイトが固定値であればその値を、そうでなければ内容について"value"に定義します。上位4bitと下位4bitで意味が異なる場合は"+"で上位ビットから順番に連結して表現します。
また、設定ファイルから設定値を反映するためのルールを"rule"オブジェクトとして格納します。人間が理解できる値から信号化するための16進数変換するルールをここに定義します。
コンバートスクリプト
次にコンバートスクリプトになります。
機能としては
- 設定jsonの読み込み
- 信号フォーマットjsonの読み込み
- 信号を各バイトごとに10進数化して生成
- 信号を2進数化およびLSB,MSB対応
- 2進数化された信号をpulse,space記法に変換
- 結果を標準出力
の6個の機能からなります。
# coding:utf-8 #import mod import json import sys import rev_bit
依存関係は以上のようになります。rev_bitは前回に作成したMSB/LSBの反転スクリプトです。
#check argvs argvs = sys.argv argc = len(argvs) if(argc !=2): print 'Usage: #python %s filename' % argvs[0] quit() #set local value SIGNAL_LIB_DIR = "/home/pi/system/settings/format/"
初期化処理です。引数チェックと信号ライブラリのディレクトリを設定しています。
ディレクトリ指定は絶対パスかスクリプトを置いたところからの相対ディレクトリです。
################################## #load setting ################################## try: s = open(argvs[1],'r') set_data = json.load(s) data = set_data["data"] except ValueError: print 'setting json format error' s.close() quit() except IOError: print 'No Such File or Directory %s' %(argvs[1]) quit() except NameError: print 'Unknown format of setting %s' %(argvs[1]) s.close() quit() except : print 'Unexpected error:',sys.exc_info()[0] quit()
引数で指定された設定ファイルを読み込み処理です。例外処理を入れていますが基本的にいれなくてもよいと思います。今回少し私が勉強したことを試す意味で入れています。
################################## #load format ################################## format_file = set_data["format_id"] + ".json" f = open(SIGNAL_LIB_DIR + format_file,'r') format = json.load(f)
信号フォーマットファイルの読み込みです。設定ファイルに格納された["format_id"]+'.json'のファイルを初期化時に指定した"SIGNAL_LIB_DIR"内からロードするといった処理になります。
################################## #convert setting to binary signal ################################## #initialize signal = format["signal"] rule = signal["rule"] format_type = format["type"] bin_signal = '' #convert setting to binary signal for frame in signal["frame"]: bin_signal = bin_signal+'L' bitmap = frame["bitmap"] raw_signal = [] for i in xrange(len(bitmap)): val = frame["value"][i].split("+") sig = 0 for j in xrange(len(val)): #append fixed_bit if '0x' in val[j]: sig = sig + int(val[j],16) << bitmap[i][j] #append setting_bit elif val[j] in rule.keys(): sig = sig + int(rule[val[j]][data[val[j]]],16)<<bitmap[i][j] #append error_check_bit elif val[j] == 'sum': sig = reduce(lambda x,y:x+y,raw_signal) & 0x0ff else: sig = sig + data[val[j]] << bitmap[i][j] #append new signal raw_signal.append(sig) bin_signal = bin_signal + bin(rev_bit.reverse_bit8(sig)).replace('0b','').zfill(8) if format_type == 'AEHA': #append tracer tag bin_signal = bin_signal + 'T'
'bin_signal'にLeaderは'L',信号は'0'or'1',Trailerは'T'として格納していきます。
各バイトごとのデータをsigに格納し、これを変換していくことで実現しています。
sigの計算は大きく3つあります.
- 固定値をビットシフトして追加
- 設定値を読みこんでビットシフトして追加
- エラー処理用の信号を処理して追加
1つ目は固定値の追加です。1バイトすべてが固定値の場合はvalue='0x16',bitmap=[0]という組み合わせになっていて0x16を左側0bitシフトして追加という処理になります。
Data0とParityコードは4bitずつの固定値となっているので以下のような組み合わせで記述されていますvalue='0x0+0xe',bitmap=[0,4]。これは固定値をそれぞれ順番に"+"で表現し、それらをいくつ左側にbitシフトするかといったようにbitmapを記述しています。
これは設定値を反映する場合にも使用されています。たとえば"mode+status"のように1バイトに運転モードと電源ステータスが共存する場合にも同じように"+"で結合して記述します。その後,ruleオブジェクト内に記載された設定値変換ルールによって置換してbitmap=[4,0]に従ってそれぞれビットシフトして加算されます。
最後にエラー処理用のバイトは今回のエアコンのリモコン信号がすべてのバイト信号の総和で定義されていたのでその処理を実装しています。今後別の信号を扱う際に拡張する予定です。
最後にこのsigをMSB/LSBのため並び順を逆順に変換して、8ケタの2進数文字列化を行って結合していきます。
リモコン信号は下の桁から順番に1桁目,2桁目,3桁目…,8桁目と流れていますが、プログラム上では下の桁が最後にくるように8桁目,7桁目,...2桁目,1桁目とならんでいるからです。
################################### #convert binary signal to ir_signal ################################### #initialize T = signal["T"] ir_signal = '' #AEHA format setting if format_type == 'AEHA': trailer_t = signal["trailer_t"] for i in xrange(len(bin_signal)): #append 0 if bin_signal[i] == '0': ir_signal = ir_signal + 'pulse ' + str(T) + '\nspace ' + str(T) + '\n' #append 1 elif bin_signal[i] == '1': ir_signal = ir_signal + 'pulse ' + str(T) + '\nspace ' + str(3*T)+ '\n' #append Leader elif bin_signal[i] == 'L': ir_signal = ir_signal + 'pulse ' + str(8*T) + '\nspace ' + str(4*T)+ '\n' #append Trailer elif bin_signal[i] == 'T': ir_signal = ir_signal + 'pulse ' + str(T) + '\nspace ' + str(trailer_t) + '\n' print ir_signal
最後はAEHA,NEC,sonyフォーマット毎にそれらに従ったpulse,space信号に変換していきます。
置換で生成せずわざわざfor文で順番に処理するのは変換した文字列に0,1が生成された場合に誤って変換されるのを防ぐためです。
最後にprint文を用いて生成した文字列を標準出力に出力しています。
ソースコード
以下にjson2python.pyとテスト用のjsonをlibに格納していますのでどうぞご自由にご利用ください。
github.com
Rasberry Pi 3でリモコン信号を解析する (10/1リモコン信号解析結果更新)
目的
多くのサイトで送られた信号を記録してそのまま出力するといったものが見られますがこれだと多くのパターンを記録させなければならないので非効率です。
今回、エアコンに設定したい状態があった場合にその状態に変更するための信号を生成できるようにリモコンの信号生成ルールを明らかにします。
リモコンについて
今回使用したリモコンについて紹介します
ボタン
ボタン名 | 動作 |
運転ON/OFF | 電源ON/OFF状態のトグル |
温度UP | 設定温度を1度あげる |
温度DOWN | 設定温度を1度下げる |
運転切替 | 自動/暖房/冷房/除湿の順に切り替え |
風量 | 自動/静か/弱~強4段階で順番に切り替え |
風向 | 自動/下~上を4段階で順番に切り替え |
におい除去 | 長押しでにおい除去モードに切り替え |
切タイマ | 電源ON時に0.5,1,2,3,4,5,6,7,8,9,10,11,12hの順でタイマの時間を変更 |
入タイマ | 電源OFF時に0.5,1,2,3,4,5,6,7,8,9,10,11,12hの順でタイマの時間を変更 |
予約 | 切タイマ,入タイマで設定した時間を送信する |
取消 | 予約の取消 |
パワフル | パワフルモードに切り替え |
メニュー | エアコンへの信号は特になし |
信号解析
次にこのリモコンの信号を解析します
信号フォーマット
リモコンの信号は以下のサイトで詳しく解説されています。
赤外線リモコンの通信フォーマット
大きく分けて以下の3つのフォーマットに分類されるそうです。
今回使用するNational(Panasonic)は2番目の家製協フォーマットを使用しています。
家製協フォーマットについて詳しく説明していきます。
赤外線の信号は一定時間T(数100μsec)を1単位としてON/OFFが何単位時間の長さかで情報を表現しています。
前回紹介したリモコンの信号を取得する"mode2"の出力におけるpulseがON spaceがOFFに対応しています。
家製協フォーマットではON→OFFの長さの組み合わせで以下のような意味を持ちます。
ON | OFF | 意味 | 詳細 |
8T | 4T | Leader | Frameの始めを意味する |
1T | 1T | 0(bit) | bitにおける0を意味する(DataBit) |
1T | 3T | 1(bit) | bitにおける1を意味する(DataBit) |
1T | >=800ms | Trailer | Frameの終了を意味する |
つまり1Tの長さのOnの後にOFFの信号が3T続くと"1"を意味するということです。
さらに、信号の構成について見ていきます。以下は信号を含んだFrameと呼ばれるブロックをしめしています。
Leader | Customer Code1 | Customer Code2 | Parity Code | Data0 | Data1 | ・・・ | DataN | Trailer |
8bit | 8bit | 4bit | 4bit | 8bit | ・・・ | 8bit |
さきほど出てきたLeaderというのはFrameの開始を示す信号です。これによって受信側はこれからデータが来ることがわかります。
この後にDataBitフォーマットで4bitもしくは8bitのデータが順番に二進数で表現されて続きます。
一般的に最初の二つの8bit信号はカスタマーコードと呼ばれ固有のコードが格納されています。その後4bitがparityコードと呼ばれるカスタマーコードのエラーチェック部になるようです。
次に4bitのデータ0のデータコードが格納された後、8bit単位でデータコードが格納されます。
そしてFrameの最後にはTrailerと呼ばれる信号が格納されて終了となります。
リモコンによってキーを押している間、ずっとframeが繰り返されたりリピート信号が送信されたりするそうなのでこのあたりは実際に信号を確認してみないとわかりません。
以上が信号フォーマットについてになります。
信号データの保存
次に受信した信号を4bitもしくは8bitのコードに変換して人間が読めるような形式に変換します。
まず信号をファイルに保存します。
$mode2 -d /dev/lirc0 | tee ファイル名
このコマンドを実行した後に、センサにリモコンを向けて信号を送信します。
いくつかの信号を送ったらctri+cで処理から抜けます。
すると指定したファイルに以下のように信号が記録されます。
space 4121433 pulse 142 space 2075 pulse 145 space 111986 pulse 136 space 12832 pulse 142 space 190883 pulse 139 space 1998 pulse 287 space 272314 pulse 142 space 2031 pulse 235
mode2コマンドの出力がそのままファイルに保存されています。
これで信号出力を保存できました。
単位時間Tの取得
しかしこのままでは人間が理解できる形式ではないので分析できません。
分析するためにはまず単位時間Tを調べる必要があります。
家製協フォーマットはPuiseの長さが1Tと8Tしかありません。1TがFrameに大量に現れますが8TはFrameの最初1回しか現れません。
なので頻度分布を調べて最も頻度が高い値が単位時間Tになるはずです。
なのでまず保存したファイルの中のpulseで始まる行に書かれた値のヒストグラムをとってその最大となる範囲の値を平均化してTを求めます。
今回ヒストグラムは100μsec単位で0~10msecのレンジまでを解析するようにします。
hist = [0] * 100 sum = [0] * 100 for row in reader: #make histgram length = int(row[1]) if row[0] == 'pulse' and length <= 10000: hist[length/100] = hist[length/100] + 1 sum[length/100] = sum[length/100] + length
hist配列に頻度、sum配列に平均値演算用の和を格納していきます。indexは2列目/100とすることで200から299まではindex=2というように簡単に振り分けができます。
index = hist.index(max(hist)) T = sum[index]/hist[index]
配列作成が終わったら最大頻度のindexを探してそのindexでの平均値を出力します。
この時得られたTは今回476となりました。
tmp_hist=[] for x in range((index-1)*8,(index+1)*8+1): tmp_hist.append(hist[x]) tmp_index = tmp_hist.index(max(tmp_hist)) index = hist.index(tmp_hist[tmp_index]) T8 = sum[index]/hist[index]
次に8Tを探します。
8Tがあるであろうindex周辺でもっとも頻度が高いindexを探してそのindexの平均値を出力します。
この時得られた8Tは3531になりました。
Tをそのまま8倍にしても8Tと同じ時間になりません(476*8 = 3808 > 3531)
これは信号の立上がりと立下りに検出の遅れが発生しているためと推定されます。
なので今回検出されたTと8Tは真の単位時間と検出遅れを用いて表すと
よってはこの二つの差から求められます。
true_T = (T8-T)/7
これで単位時間Tが取得できます。
このコードでリモコンの出力する単位時間Tは436となりました。
以下に検出プログラムを公開しています。家製協フォーマットであれば以上のロジックで単位時間Tを検出できると思います。
https://github.com/kagemomiji/raspi3/blob/master/ir_detect_T.py
使用方法は以下になります。
$python ir_detect_T.py ファイル名
信号のデコード
単位時間Tがわかりましたので次はpulseとspaceの信号を人間が読めるようにデコードします。
処理の流れとしては以下のようになります。
- mode2コマンドで取得したファイルを一行ずつ読み込む
- 各信号の長さを単位長さの何倍か計算する
- Leaderを検出する
- 1TのPulseを検出してその後のSpaceの長さで信号を判定する
- データ毎にビットを格納してビット反転を行う
- Frame配列にデータを格納する
- Frame配列に格納されたデータを16進数に変換して出力する。
順番に見ていきます
#detect pulse length length = int(round(int(row[1])/T))
まず単位時間に対して何倍の長さかを検出します。
row[1]はmode2の出力の2列目(数値の部分)です。
最後のintが冗長な気もしますがこれでlengthに何倍という情報が格納されます。
if row[0] == 'pulse' and length == 8: leader_flag = 1 #8length pulse detect if row[0] == 'space' and length == 4 and leader_flag == 1: leader_flag = 2 #leader_detect data_counter = 0 frame = [] datum = 0 pulse_flag = 0
次にleaderを検出します.pulseの長さが8Tの後にspaceの長さが4Tであればleaderと検出して以降の信号をFrameの信号という判定leader_flag = 2としています。
また、leaderの時に各変数を初期化しています。
if row[0] == 'pulse' and length == 1: data_counter = data_counter + 1 pulse_flag = 1 if row[0] == 'space' and pulse_flag: #print "%d,%d" % (data_counter, length) #pulse_flag off pulse_flag = 0 #detect bit on/off/trailer if length == 1: bit = 0 elif length == 3: bit = 1 datum = datum << 1 | bit
1Tのpulseの後にspaceがあればそれを信号と判定してspaceの長さによってbitの値を判定します。
1Tなら0、3Tなら1です。
最後にdatumにbitシフトさせながら格納していきます。
#detect 4bit datum(parity, data0) if data_counter == 20 or data_counter == 24: frame.append(rev_bit.reverse_bit4(datum)) datum = 0 #detect 8bit datum(customer, data1~) elif data_counter % 8 == 0: frame.append(rev_bit.reverse_bit8(datum)) datum=0
datumに格納されたデータをparityコードやdata0コードでは4bit毎にその他コードでは8bit毎にLSB/MSB反転を行います。
なぜならリモコン信号のデータ並び順はD0,D1,D2,D3....D7と下の桁から順番となっています。しかし数値処理する場合それは逆でD7,D6,...D1,D0というように並んでいなければなりません。
そのためLSB/MSBを反転する処理が必要です。
各コードごとに反転したコードをframe配列に順番に格納し行きます。
data_str = [hex(x) for x in frame] writer.writerow(data_str)
最後にcsvに対してframeに格納されたデータを16進数表記に変換して信号を出力します。
以下に私の書いたサンプルコードがありますのでよければそちらをご確認ください。
https://github.com/kagemomiji/raspi3/blob/master/decode_ir.py
使い方は
$ python decode_ir.py 入力ファイル名
とすると "ファイル名_format.csv"として出力されます
このコードによってデコードされた我が家のエアコンのリモコン信号が以下になります。
0x2,0x20,0x0,0xe,0x4,0x0,0x31,0x3a,0x80,0xaf,0x0,0x0,0x6,0x60,0x0,0x0,0x80,0x0,0x16,0x9c trailer 0x2,0x20,0x0,0xe,0x4,0x0,0x0,0x0,0x6 0x2,0x20,0x0,0xe,0x4,0x0,0x31,0x3c,0x80,0xaf,0x0,0x0,0x6,0x60,0x0,0x0,0x80,0x0,0x16,0x9e trailer 0x2,0x20,0x0,0xe,0x4,0x0,0x0,0x0,0x6 0x2,0x20,0x0,0xe,0x4,0x0,0x31,0x3c,0x80,0xaf,0x0,0x0,0x6,0x60,0x0,0x0,0x80,0x0,0x16,0x9e trailer 0x2,0x20,0x0,0xe,0x4,0x0,0x0,0x0,0x6
一つのボタンを押すと3行ずつがセットで出力されているようです。
状態信号+trailer+識別信号のような順番で出力されているようです。
信号対応付け
信号を分析できる形式で見ることができるようになりました。
ここですべてのボタンを押していきそれぞれの状態がどこにどのように割り当てられているのか分析していきました。
短い式別信号のような信号は基本的に固定されていたので長いほうの状態信号を調べました。
Dataコードの結果は以下のようになりました。
0 | 固定(4bit) |
1 | 固定 |
2 | 固定 |
3 | モード(上位4bit) 電源ステータス(下位4bit) |
4 | 温度の2倍(8bit) |
5 | 固定 |
6 | 風量(上位4bit) 風向(下位4bit) |
7 | 固定 |
8 | ONタイマの時間上位8bit |
9 | ONタイマの時間下位8bit/OFFタイマの時間上位8bit |
10 | OFFタイマの時間下位8bit |
11 | 乾燥機能ON/OFF(上位7bit目)/静モード(上位6bit目)/パワフルモード/通常(下位4bit) |
12 | 固定 |
13 | 固定 |
14 | 固定 |
15 | におい防止 ON/OFF |
16 | カスタマーコードからデータコードを8bit単位にしたものの総和のうち下位8bit |
最後のデータビットの解析に手間取りました。CRCチェックやXORなどを考えたのですが一番単純な総和でした。
データコードの12~14はもしかするとためしていないボタンがいくつかあるのでそれに割り振られているかもしれませんが
今回は解析をここで終了したいと思います。
10/1追記分
11バイト目と15バイト目について修正加筆しました。
メニューから設定される以下の設定信号について追加しました。
11バイト目は冷房、除湿機能時に有効になる乾燥機能
15バイト目はON/OFF機能だと考えていましたが、実際は冷房・除湿機能時に有効になるにおい防止
になります。
なかなかリバースエンジニアリングは奥が深いです。
まとめ
今回は家にあるエアコンのリモコン信号を取得して、家製協フォーマットの信号を解析しました。
そして信号の単位時間Tを求めて、人間が読める形に信号を変換して分析を行いました。
次はこの解析した信号をもとに信号を生成して出力することを目指します。
Rasberry Pi 3でリモコン信号(赤外線)を受信する
目的
今回はRasberry Pi3を用いてリモコン信号(赤外線)を取得することを目指します。
回路
まずリモコンの受信モジュールですが秋月電子さんで売られているこちらを使います。
消費電流が小さく3.3Vの電源に対応しています。
こちらを接続する回路は以下のようになります。
GPIOとセンサの間にはGPIOの設定を誤っれ出力0Vとした時にショートしないように保護抵抗をいれています。
また、このセンサにはプルアップ抵抗が内蔵されているようなのでRaspberry PiのGPIOのプルアップを解除します。
今回私は詰めて実装したい人間なのでGPIO4を用いて実装します。
通常の設定だとGPIO16になります。適宜読み替えていただければと思います。
$raspi-gpio set 4 ip pn
その後このように配線します。
これで回路の準備ができました。
Rasberry Pi設定
Rasberry Piで信号を取得するためにlircを使用します.
$sudo apt-get install lirc
まずlircをインストールします。
次にlircのドライバを有効にしなければならないのですがRasberry Pi 3なのかRasbian jessieの設定のせいなのか最初は有効にすることができません。
Raspberry Pi • View topic - I2C, SPI, I2S, LIRC, PPS, stopped working? Read this.
このフォーラムに書いてある通り/boot/config.txtを編集しなければなりません。
#dtoverlay=lirc-rpi dtoverlay=lirc-rpi,gpio_in_pin=4
51行目のこの記述がコメントアウトされているのでこのコメントアウトを解除して入力するgpioの番号をセットします。これはピンの番号ではなくGPIO4といったほうの番号なので注意してください。何も設定しない(コメントアウトしただけ)であればGPIO16がInputとなっています。
Raspberry Pi 3 model B でエアコンをコントロールした話 - s4kr4.blog
次にこちらで紹介されているように/etc/lirc/hardware.confを編集します。
ここで一度Raspberry Piを再起動します。
これで受信の準備は完了しました。
mode2 -d /dev/lirc
このコマンドで受信する準備ができました。
センサに向けてエアコンのリモコンの信号を送って見ましょう。
space 4121433 pulse 142 space 2075 pulse 145 space 111986 pulse 136 space 12832 pulse 142 space 190883 pulse 139 space 1998 pulse 287 space 272314 pulse 142 space 2031 pulse 235
このような表示が出ていれば受信ができています。
最後にCtrl+cで処理から抜けます。
まとめ
今回は受信回路を設計してRaspberry pi3で受信するところまでできました。
次はエアコンの信号を解析して温度を設定できるように信号を生成するところまでしたいと思います。
Raspberry Pi 3 でK09421(BME280)のセンサ出力を定期的にcsvへ出力する
目的
K09421からセンサ出力を取得できるようになりましたので今度は定期的にデータを取得してログファイルに残すことを目指します。
方針
方針としてはcronでBME280から出力を読みとるプログラムを定期的に呼び出して"yyyyMMdd_bme280.csv"という形式で一日ごとにファイルを分割してcsvファイルに書き込むという流れです。
センサの値を読みとってcsvに出力するプログラムは前回に作ったものを改造して実装します。
hh:mm:ss,温度(℃),圧力(hPa),湿度(%)
保存するcsvのフォーマットはこのようにします。時間(hh):分(mm):秒(dd)としています。これは今後処理するときに容易に他のプログラムでも取り込めるようにiso拡張フォーマットとしています。
この方針に従って設定やコーディングを進めていきます。
コーディング
まず前回作成したセンサ値取得プログラムを先ほど決めた方針に従って書き換えていきます。
追加する機能は以下の2つです。
- 現在の時刻と日付を取得する
- 取得した時刻と日付からファイル名を生成し、時刻とセンサ値をcsvに書き込む
モジュールのインポート
ここで新しい機能を実現するために datetimeモジュールとcsvモジュールを追加します。
import smbus #import time #不使用 import datetime #追加 import csv #追加
変数設定部
変数を設定する部分にデータを保存するディレクトリを設定します。
i2c_address = 0x76 DATA_DIR = '/home/pi/data/' #追加 bus = smbus.SMBus(bus_number)
関数
次に変更を加える関数の一覧です。
- readData
- compensate_T
- compensate_P
- compensate_H
readDataではcsvへの書き出しを行うように変更します。
compensate_*ではreadDataにデータを受け渡すように変更を行います。
compensate_T
まず、compensate_Tでは以下のようにプログラムの最後に一文を追加します。
print "temp : %-6.2f ℃ %" %(temperature) return temperature #追加
compensate_P
print "pressure : %7.2 f hPa" %(pressure/100) return pressure/100.0 #追加
compensate_H
print "hum : %6.2f %" %(var h) return var_h #追加
readData
#get record time and date record_datetime = datetime.datetime.now() #現在の日時と時刻を取得 record_file_name = record_datetime.strftime('%Y%m%d')+'_bme280.csv' #ファイル名を生成 record_time = record_datetime.strftime('%X') #時刻文字列を生成 data=[]
このように現在時刻を取得してファイル名と時刻の文字列を生成します。
T=compensate_T(temp_raw) P=compensate_P(pres_raw) H=compensate_H(hum_raw)
先ほど返り値が出力されるように変更したのでその出力を変数に格納します。
writer = csv.writer(open(DATA_DIR+record_file_name, 'ab')) writer.writerow([record_time,T,P,H])
プログラム配布
以下に作成したプログラム bme280_csv.pyを公開しています。使用される方はご自由にご利用ください。
GitHub - kagemomiji/raspi3
cronの設定
次に作成したプログラムを定期的に実行するcronの設定をします。
crontab -e
crontabを用いて設定します。
*/5 * * * * python {ファイルへの絶対パス}
上記のように5分おきに実行されるように設定しました。
10分おきでは */10 * * * * コマンド
1時間おきでは (実行したい分) * * * * コマンド
というように設定します。
設定を終わり編集を完了したらcronに設定が反映されその後実行されるようになります。
出力例
22:30:01,29.46672882,1001.354688,56.09067817 22:35:01,29.39716974,1001.405821,54.73285085 22:40:01,29.43100929,1001.44832,54.44254727 22:45:01,29.4075096,1001.442746,54.00647973 22:50:02,29.36019691,1001.457592,53.52315578 22:55:01,29.33419061,1001.406453,52.97012485 23:00:01,29.30661768,1001.435586,52.58357062 23:05:01,29.25241182,1001.510298,52.34396932 23:10:01,29.23110548,1001.519919,52.43595777 23:15:01,29.58548072,1001.406111,53.4440672 23:20:02,29.4075096,1001.373139,53.34538086 23:25:01,29.31006429,1001.384926,53.00620219 23:30:01,29.56448763,1001.39142,54.19737383 23:35:01,29.5441212,1001.356211,53.91712263 23:40:01,29.38996317,1001.359144,53.30443224 23:45:01,29.59174732,1001.302045,53.63377335 23:50:02,29.55947436,1001.348349,54.10231546 23:55:01,29.40030303,1001.362922,53.60009775
1時間のデータを確認してみましたがエアコンを付けているためほとんど変化していないことがわかります。
おわりに
これで定期的にログを取得するシステムが完成しました。
次はエアコンの制御をするためのリモコン部分の設計をしたいと思います。
K09421(BME280)のSwitch Sensorサンプルプログラムの詳細分析
目的
前回は定期的にログを取得するプログラムを作ると書いたのですが取得プログラムの仕様がわからないままでは勉強になりませんので
サンプルプログラムを詳細に見ていくことにしました。
サンプルプログラム
GitHub - SWITCHSCIENCE/BME280github.com
rev a43306eのものを参考とします。
以下に示した表はサンプルプログラムの関数一覧になります。
関数 | 機能 |
writeReg | registryへ書き込む |
get_calib_param | registryから校正パラメータ読み込み |
readData | センサ出力の生値を取得する |
compensate_P | 圧力補正値を出力する |
compensate_T | 温度補正値を出力する |
cmopensate_H | 湿度補正値を出力する |
set_up | センサの設定を書き込む |
これからそれぞれの関数について詳しく見ていきます。
writeReg
def writeReg(reg_address, data): bus.write_byte_data(i2c_address,reg_address,data)
i2c_addressは7行目にグローバルで定義されていて、通常であれば0x76となっています。
なのでreg_addressに変更するレジストリのアドレス、dataにそのアドレスに書き込む値(8bit)をいれることでレジストリの値が変更できるようになっています。
呼び出されているのはsetup()です。
get_calib_param
calib = [] for i in range (0x88,0x88+24): calib.append(bus.read_byte_data(i2c_address,i)) calib.append(bus.read_byte_data(i2c_address,0xA1)) for i in range (0xE1,0xE1+7): calib.append(bus.read_byte_data(i2c_address,i))
校正値が格納されているレジストリから値を読みだしてcalib配列に格納しています。
しかし格納されているのは0x88~0x9F,0xA1,0xE1~0xE7となっています。
取り扱い説明書に記載されているMemory Mapには0x88~0xA1,0xE1~0xF0にあると書かれています。
BME280 – Attachments – スイッチサイエンス
これではなぜこのアドレスが読まれているのかわかりません。そこでBME280のデータシートを確認してみます。
https://ae-bst.resource.bosch.com/media/_tech/media/datasheets/BST-BME280_DS001-11.pdf
すると22~23ページに校正値の保存されているアドレスが記述されています。
この表を簡易的にまとめると以下の表のようになります。
補正値種類 | アドレス | calib番号 |
温度 | 0x88~0x8D | 0~5 |
圧力 | 0x8E~0x9F | 6~23 |
湿度 | 0xA1,0xE1~0xE7 | 25~32 |
これでどうしてあのようなアドレス指定で校正値を取得しているのかがわかりました。
digT.append((calib[1] << 8) | calib[0]) digT.append((calib[3] << 8) | calib[2]) digT.append((calib[5] << 8) | calib[4])
digTという温度校正値を取得する配列にそれぞれ校正値を代入しています。
calib[1] << 8 | calib[0]
この処理でmsbとlsbを結合する処理をしています。
'<<8'で8bit左にシフト、'|'で論理和という処理になっています。
圧力補正値digPについても特に特殊な処理はなくdigP1~9までdigTと同様の処理で取得しています。
湿度については若干特殊なアドレス構成になっているのでシフトが4ビットとなっているものがありますがほぼ同様の処理をが行われています。
for i in range(1,2): if digT[i] & 0x8000: digT[i] = (-digT[i] ^ 0xFFFF) + 1 for i in range(1,8): if digP[i] & 0x8000: digP[i] = (-digP[i] ^ 0xFFFF) + 1 for i in range(0,6): if digH[i] & 0x8000: digH[i] = (-digH[i] ^ 0xFFFF) + 1
次の処理はunsigned→signedの変換処理ですがこの処理は誤りがあります。
データシートP22からp23に書かれた表から型と校正変数についてのみ抜粋すると以下のようになります。
校正値 | 型 |
T1 | unsigned |
T2,3 | signed |
P1 | unsigned |
P2-9 | signed |
H1,H3 | unsigned |
H2,H4,H5 | signed |
これに従うと上記コードだけでは変換が不十分であることがわかります。以下のように書きなおす必要があります。
for i in range(1,3): #T2,T3 if digT[i] & 0x8000: digT[i] = (-digT[i] ^ 0xFFFF) + 1 for i in range(1,9): #P2~P9 if digP[i] & 0x8000: digP[i] = (-digP[i] ^ 0xFFFF) + 1 for i in [1,3,4]: #H2,H4,H5 if digH[i] & 0x8000: digH[i] = (-digH[i] ^ 0xFFFF) + 1
readData
data = [] for i in range (0xF7, 0xF7+8): data.append(bus.read_byte_data(i2c_address,i))
data配列に測定値が格納されている0xF7~0xFEのメモリの値を格納します。
pres_raw = (data[0] << 12) | (data[1] << 4) | (data[2] >> 4) temp_raw = (data[3] << 12) | (data[4] << 4) | (data[5] >> 4) hum_raw = (data[6] << 8) | data[7]
それぞれをMemoryMapに従ってビットシフトと論理和で結合してそれぞれの補正前の値を生成します。
compensate_T(temp_raw) compensate_P(pres_raw) compensate_H(hum_raw)
最後に補正値を出力するプログラムを呼び出しています。
compensate_T
global t_fine
圧力や湿度の補正用の温度をグローバルで定義しています。
v1 = (adc_T / 16384.0 - digT[0] / 1024.0) * digT[1] v2 = (adc_T / 131072.0 - digT[0] / 8192.0) * (adc_T / 131072.0 - digT[0] / 8192.0) * digT[2] t_fine = v1 + v2 temperature = t_fine / 5120.0
ここが補正になっています。しかしこれではどういった補正なのかわからないので実際にこの式を何かしらの意味がある形に変形して数式に起こしていきます。
温度は32bitのsigned型に対して20bitのフォーマットで出力されているのでdig_T1(16bit)を4bit左にビットシフトして20bitフォーマットにして減算し、20bit右にbitシフトさせます。
このTに対して2次近似式で補正値を出力しています。
digT3の16bitを同様に4bit左にビットシフトして20bitにし2次の係数に、同様にdigT2の4bitシフトして20bitにして1次の係数として演算し、最後に8bit右にビットシフトしたものを5で除算して最終出力値としています。
最後に圧力や湿度の補正用のt_fineはTcompを10bit左へビットシフト×5倍となっています。
とても難解でこの解釈は私の推測でしかないので気になる方は自分でもう少し掘り下げていただければと思います。
最後に以下のprint出力で温度を出力しています。
print "temp : %-6.2f ℃" % (temperature)
compensate_P
global t_fine pressure = 0.0 v1 = (t_fine / 2.0) - 64000.0 v2 = (((v1 / 4.0) * (v1 / 4.0)) / 2048) * digP[5] v2 = v2 + ((v1 * digP[4]) * 2.0) v2 = (v2 / 4.0) + (digP[3] * 65536.0) v1 = (((digP[2] * (((v1 / 4.0) * (v1 / 4.0)) / 8192)) / 8) + ((digP[1] * v1) / 2.0)) / 262144 v1 = ((32768 + v1) * digP[0]) / 32768
こちらも同様に数式に起こしていきたいと思います。
先ほどの温度でのとの関係から
最初に気温から25減算して 10を乗算し8bit左シフト下値がv1となります。ここから25℃を基準に補正をしていることがわかります。
ここでとすると
と表わされます。2次近似式が生成されます。若干意味が通らない式なので何かしらの意図があってこのような値になっているのでしょう。
次にとすると
となります。こちらも温度の2次近似式ではありますが非常に小さい値になっていると考えられます。
if v1 == 0: return 0
v1で除算するため0の場合returnで抜けるようになっています。
pressure = ((1048576 - adc_P) - (v2 / 4096)) * 3125
これを数式に起こすと
となります。adc_Pも20bit仕様ですので2^20と減算をしてv2のオフセット項でオフセットさせた後、10万倍して5bit右シフトしています。v2に対して12bitも右シフトをしています。
if pressure < 0x80000000: pressure = (pressure * 2.0) / v1 else: pressure = (pressure / v1) * 2
この部分はオーバーフロー対策と精度を両立させるために計算順序を入れ替えているようです。
pythonでこの処理をする必要性がないので以下のように書き換えます。
pressure = pressure * 2.0 /v1
最後に以下の処理を数式に起こします。
v1 = (digP[8] * (((pressure / 8.0) * (pressure / 8.0)) / 8192.0)) / 4096 v2 = ((pressure / 4.0) * digP[7]) / 8192.0 pressure = pressure + ((v1 + v2 + digP[6]) / 16.0)
ここでとすると
ここでは演算されたpressureに2次の近似補正式で補正を行っています。
分解すればするほど非常に難解な補正が入っていることがわかります。正直補正値を書き変えるのは難しいと感じます。
print "pressure : %7.2f hPa" % (pressure/100)
最後もprintで表示しています。 pressureはPa単位ですのでhPaにするために100で割っています。
compensate_H
global t_fine
補正用温度 t_fineを定義しています。
var_h = t_fine - 76800.0
t_fineとT_compの関係から数式に起こすと
となります。ここから温度30度を基準にして補正していることが読み取れます。
if var_h != 0: var_h = (adc_H - (digH[3] * 64.0 + digH[4]/16384.0 * var_h)) * (digH[1] / 65536.0 * (1.0 + digH[5] / 67108864.0 * var_h * (1.0 + digH[2] / 67108864.0 * var_h))) else: return 0 var_h = var_h * (1.0 - digH[0] * var_h / 524288.0)
この処理を数式に起こします。
ここまで起こしてみましたが流石にこの補正のかけ方は何をしているのかさっぱりです。センサの設計思想がわからないとわかりません。
if var_h > 100.0: var_h = 100.0 elif var_h < 0.0: var_h = 0.0 print "hum : %6.2f %" % (var_h)
最後に%表示のために0~100までの値に制限して表示を行っています。
setup
def setup(): osrs_t = 1 #Temperature oversampling x 1 osrs_p = 1 #Pressure oversampling x 1 osrs_h = 1 #Humidity oversampling x 1 mode = 3 #Normal mode t_sb = 5 #Tstandby 1000ms filter = 0 #Filter off spi3w_en = 0 #3-wire SPI Disable
変数名 | 設定項目 | 値 | 意味 |
osrs_t | 気温取得サンプリング数 | 1 | x1 |
osrs_p | 圧力取得サンプリング数 | 1 | x1 |
osrs_h | 湿度取得サンプリング数 | 1 | x1 |
mode | センサ動作モード | 3 | normal |
t_sb | スタンバイ時間 | 5 | 1000ms |
filter | IIRfilter | 0 | off |
spi3w_en | 3-sire SPI on/off | 0 | off |
この設定ですが,今回私の用途は室内の温度を取得することが前提です。データシートにそのための設定例があるのでそちらに変更したいと思います。
def setup(): osrs_t = 1 #Temperature oversampling x 1 osrs_p = 5 #Pressure oversampling x 16 osrs_h = 2 #Humidity oversampling x 2 mode = 3 #Normal mode t_sb = 0 #Tstandby 0.5ms filter = 4 #Filter off spi3w_en = 0 #3-wire SPI Disable
変数名 | 設定項目 | 値 | 意味 |
osrs_t | 気温取得サンプリング数 | 2 | x2 |
osrs_p | 圧力取得サンプリング数 | 5 | x16 |
osrs_h | 湿度取得サンプリング数 | 1 | x1 |
mode | センサ動作モード | 3 | normal |
t_sb | スタンバイ時間 | 0 | 0.5ms |
filter | IIRfilter | 4 | 16 |
spi3w_en | 3-sire SPI on/off | 0 | off |
ctrl_meas_reg = (osrs_t << 5) | (osrs_p << 2) | mode config_reg = (t_sb << 5) | (filter << 2) | spi3w_en ctrl_hum_reg = osrs_h writeReg(0xF2,ctrl_hum_reg) writeReg(0xF4,ctrl_meas_reg) writeReg(0xF5,config_reg)
次にこれらの設定値を以下のメモリーマップに従って格納しています。
それぞれアドレスに格納する値を設定し、writeReg関数でレジスタに書き込みをを行っています。
修正プログラム
以下に私の修正したbme280読み込み修正プログラムを公開しています。
利用される方はご自由にお使いください。
github.com
Raspberry Pi3 と K09421(BME280使用)のI2C接続
Raspberry Pi3 のI2Cを有効化
Raspberry Pi 3は設定を何もしないとI2C通信ができるようになっていません。
なので設定からI2C通信を有効化します。
$ sudo raspi-config
とコンソールに入力します。
するとこのような画面が表示されます。
この中から 9 Advanced Optionsを選択してEnterを押してください。
次にこのような画面が表示されます。その中のA6 I2Cを選択してEnterを押してください。
最後にこの画面が表示され<はい>を選択するとI2Cが有効化されます。
その後最初の画面に戻るのでFinishを選択して終了してください。
以上がI2Cの有効化手順となります。
K09421(BME280使用)の準備
接続するセンサを準備します。
このように袋のなかにセンサ本体とピンが別々にはいっています。これらは半田づけをしてつけなければなりません。
まずこのようにピン6本セットをニッパーなどをつかって切り出します。
できたらこのように組み合わせて半田づけをしていきます。少し弱く細い半田ごてをお進めします。30Wは若干強すぎる気がしました。
つけ終わるとこのような感じになります。
次にセンサのI2C通信を有効化します。
図のように基盤右上のJ3に半田を乗せて導通させます。
これでセンサの準備が完了です。
Raspberry pi3とセンサの配線
K0942は下表のようなピン構成となっています。
ピン番号 | 信号 |
1 | Vdd |
2 | GND |
3 | DNC |
4 | SDA |
5 | VccかGNDに接続するアドレス設定用ピン(通常はGND) |
6 | SCL |
そこで配線を図に起こすと以下のようになります。
これをブレッドボードとジャンパ線を使って実装すると以下のようになります。
Raspberry Pi3からセンサの認識
次に接続したセンサが認識されているかを確認します。
$sudo apt-get install i2ctools
上記コマンドでi2cを扱うためのツールをインストールします。
完了したら以下のコマンドを入力します。
$ i2cdetect -y 1
- y は対話式を無効にする。1はi2cのbus番号です。
正常であれば以下の出力が得られます。
0 1 2 3 4 5 6 7 8 9 a b c d e f 00: -- -- -- -- -- -- -- -- -- -- -- -- -- 10: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 20: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 30: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 40: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 50: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 60: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 70: -- -- -- -- -- -- 76 --
これはI2C通信を行っているアドレスを検出して出力するものです。
K0942の取り扱い説明書にはデフォルトアドレスは0x76とのことですから認識されていることが確認できました。
次にこのセンサの値を確認します。
この部分のやり方はよくわかっていないので何かしら簡単にできるツールを探してくることにしました。
www.switch-science.com
こちらのSWITCH SCIENCEでサンプルプログラムのpythonがgithubで公開されています。
紹介したページの下のほうにgithubへのリンクが張ってあります。
$sudo apt-get install python-smbus
サンプルプログラムはsmbusを使用するためライブラリをインストールしておきます。
$git clone https://github.com/SWITCHSCIENCE/BME280.git $cd BME280/Python27 $python BME280_sample.py
サンプルプログラムをダウンロードして起動します。
すると以下のような出力が得られます。
temp:29.10 ℃ pressure:1006.03 hPa hum: 50.57 %
これでセンサが正常に動作していることが確認できました。
若干値に誤差があるように感じるので校正が必要なようですが保留としたいと思います。
次はセンサ値を定期的に取得してログとして残すことに挑戦します。
Raspberry Pi3 センサの購入とプルアップ/プルダウン
センサ購入
前回Raspberry Pi3とセンサをつなげる際に使用する通信規格を何にするかを考えてI2Cを採用することにしました。
そこで今回、秋月電子さんで以下のSPIとI2C対応の気温・湿度・気圧を取得できるセンサキットK-09421を購入しました。
BME280使用 温湿度・気圧センサモジュールキット: センサ一般 秋月電子通商 電子部品 ネット通販akizukidenshi.com
今回作成するシステムは湿度と温度を用いてエアコンを制御するシステム(詳しくは以下のリンク)でしたので気圧が取得できるのは若干オーバースペックですが、仕事ではないので扱えるデータが増えると割り切って使ってみたいと思います。
Raspberry pi 3でエアコン制御システム構築の構想とGPIO仕様について - Raspberry Pi3 で家庭用IoTkagemomiji.hateblo.jp
GPIOのプルアップ/ダウンについて
今回購入したセンサキットK-09421は基盤自体に通信線のプルアップ抵抗が乗っていて、はんだでジャンパさせることで有効化できます。
GPIOでI2Cに使用するのはGPIO2,GPIO3です。この端子におけるPullUp、もしくはPullDownがどうなっているのか調べてみました。
pinout.xyz
上記ページからGPIO2,GPIO3は1.8kΩのプルアップ抵抗が入っているようです。なのでセンサ側でPullUpする必要はありません。
ちなみにその他のGPIOはプログラム上どちらかに設定できるということでしたが実際に現状どうなっているのかわからりません。
なので確認するすべがないか探してみました。
結論から言うと以下のコマンドをraspbianのターミナル上で使用することで確認できます。
$sudo raspi-gpio funcs
詳細を以下のコマンドで確認します。
$ raspi-gpio help
raspi-gpio funcs [GPIO] Note that omitting [GPIO] from raspi-gpio get prints all GPIOs. raspi-gpio funcs will dump all the possible GPIO alt funcions in CSV format or if [GPIO] is specified the alternate funcs just for that specific GPIO.
とあり raspi-gpio funcでGPIOの選択可能な機能がCSV形式で出力するコマンドのようです。
また、さらに下の記述でGPIOのpull up/downの抵抗は50kΩということも読み取れます。
$sudo raspi-gpio funcs | less
何も設定していない時の出力は以下のようになります。(GPIO1~10まで)
GPIO | DEFAULT PULL | ALT0 | ALT1 | ALT2 | ALT3 | ALT4 | ALT5 |
0 | UP | SDA0 | SA5 | PCLK | AVEOUT_VCLK | AVEIN_VCLK | - |
1 | UP | SCL0 | SA4 | DE | AVEOUT_DSYNC | AVEIN_DSYNC | - |
2 | UP | SDA1 | SA3 | LCD_VSYNC | AVEOUT_VSYNC | AVEIN_VSYNC | - |
3 | UP | SCL1 | SA2 | LCD_HSYNC | AVEOUT_HSYNC | AVEIN_HSYNC | - |
4 | UP | GPCLK0 | SA1 | DPI_D0 | AVEOUT_VID0 | AVEIN_VID0 | ARM_TDI |
5 | UP | GPCLK1 | SA0 | DPI_D1 | AVEOUT_VID1 | AVEIN_VID1 | ARM_TDO |
6 | UP | GPCLK2 | SOE_N_SE | DPI_D2 | AVEOUT_VID2 | AVEIN_VID2 | ARM_RTCK |
7 | UP | SPI0_CE1_N | SWE_N_SRW_N | DPI_D3 | AVEOUT_VID3 | AVEIN_VID3 | - |
8 | UP | SPI0_CE0_N | SD0 | DPI_D4 | AVEOUT_VID4 | AVEIN_VID4 | - |
9 | DOWN | SPI0_MISO | SD1 | DPI_D5 | AVEOUT_VID5 | AVEIN_VID5 | - |
10 | DOWN | SPI0_MOSI | SD2 | DPI_D6 | AVEOUT_VID6 | AVEIN_VID6 | - |
GPIO0~8はデフフォルトPullUp 9,10はPullDownのようです。ちなみに44,45はNoneという記述があるようですのでPullUp,PullDown,Noneの3状態が存在するようです。Noneの時は気を付けなければショートさせてしまいますので接続するまえにこのPullの欄は確認しましょう。
今回はここまでです。
次はセンサを接続して値を読み込たいと思います。