eucaly が 2021年02月28日14時40分00秒 に編集
初版
タイトルの変更
Raspberry Pi PicoとmicroPythonで環境センサーの作成
タグの変更
RaspberryPi
秋葉原2021
メイン画像の変更
記事種類の変更
製作品
本文の変更
# やりたいことは簡単なのです - センサーの値を読みたい - それを、USB経由で出力したい - センサーは、UARTとI2C ・・・まあ、今時どんなマイコンでもできそうなのですが。 敢えてのRaspberry Pi picoで!。 「純正でUSB積んでコンパイル不要で550円」てのは、魅力的だなと。 というわけで、習作がてら、がんばってみましたよと。 # Co2センサーの選定 以下センサーを準備。 ![キャプションを入力できます](https://camo.elchika.com/7a11967aa7c4319951ab2828c6901cabf3d5e504/687474703a2f2f73746f726167652e676f6f676c65617069732e636f6d2f656c6368696b612f76312f757365722f38666637633237372d646465642d343333632d393736342d6162303436346433326631302f39353136633762622d363835622d346433342d383934312d383163656237313735343432/) - 赤外線式 MH-Z19B - マイクロホットプレート式 CCS811 - マイクロヒーター式 VME680 事前にRaspberry Pi (picoじゃない)に接続し、相関を取ったりして。 「赤外線式」なCo2センサーが良さげなので、コイツに決めました。 他のはみょーに、リニアじゃないような・・・。 ![キャプションを入力できます](https://camo.elchika.com/349a6b4357928cc6fd35af45434a7414204e6105/687474703a2f2f73746f726167652e676f6f676c65617069732e636f6d2f656c6368696b612f76312f757365722f38666637633237372d646465642d343333632d393736342d6162303436346433326631302f39653736346237302d616631372d343436312d383461642d363035396566343164656364/) まあこのへんの経緯は、以下記事そのまんまだったので。 以下記事を参照のこと。 https://pc.watch.impress.co.jp/docs/column/ali/1298772.html ついでに、温度湿度気圧センサも、BME280を選定しました、とさ。 ![キャプションを入力できます](https://camo.elchika.com/d0957c7c3bb2dbbaf90c69fca0fced23a29312b0/687474703a2f2f73746f726167652e676f6f676c65617069732e636f6d2f656c6368696b612f76312f757365722f38666637633237372d646465642d343333632d393736342d6162303436346433326631302f39666337393362352d363836662d346566362d383633622d633463643632313735396438/) # Raspberry Pi picoの準備 まあ普通に入手、開発環境は「Tera Term」のみで!。 ![キャプションを入力できます](https://camo.elchika.com/491e56404eb4c4f42afec88165371193ea99c894/687474703a2f2f73746f726167652e676f6f676c65617069732e636f6d2f656c6368696b612f76312f757365722f38666637633237372d646465642d343333632d393736342d6162303436346433326631302f64306161393537322d636164372d346536382d616633392d336464303863316163333566/) このへんの経緯は、別記事にまとめてありますよ、と。 https://elchika.com/article/d5c0dd9a-8b8a-4bf5-bca0-2181b0158a6d/ # I2CとUART接続 このへんほとんどナレッジ無くて、手探り手探り。 ![キャプションを入力できます](https://camo.elchika.com/10e18fa03f2104fa95f47b71479fd2ad5427715f/687474703a2f2f73746f726167652e676f6f676c65617069732e636f6d2f656c6368696b612f76312f757365722f38666637633237372d646465642d343333632d393736342d6162303436346433326631302f65653732376564332d333731662d343766352d383264362d363064313364626239623037/) まあ、プログラムと波形をいったりきたりして、なんとか折り合いをつけました。 ![キャプションを入力できます](https://camo.elchika.com/4f2c2aecbcef004e580f500f3a3f329eb80cabf2/687474703a2f2f73746f726167652e676f6f676c65617069732e636f6d2f656c6368696b612f76312f757365722f38666637633237372d646465642d343333632d393736342d6162303436346433326631302f66323162366336352d383538362d346165372d616632612d323165623139363464336630/) 最終的に使ったネットは、以下の通りでした。 ![キャプションを入力できます](https://camo.elchika.com/3e3780e4abaef5446230aaac39c37439c6256096/687474703a2f2f73746f726167652e676f6f676c65617069732e636f6d2f656c6368696b612f76312f757365722f38666637633237372d646465642d343333632d393736342d6162303436346433326631302f39383461343239332d636264642d346138332d393361312d646230356164326132633931/) いろんなピンに同じ名称がついてたり(多分「使える」って意味だけど)、USBなシリアル入出力とUARTがバッティングしているかどうかいまいちだったりで、まあ左上の0、1ピンをI2Cに、6、7ピンをUARTに使う感じで納まりました。 I2Cに使う場合、インターナルプルアップが効いているような動作しておりました(が、不安なのでプルアップ外付けでつけましたとさ)。 # Raspberry Pi pico側ソフト まあ、microPythonで!。 ```python:main.py import utime import machine led = machine.Pin(25, machine.Pin.OUT) conversion_factor = 3.3 / (65535) i2c_bus_number = 0 i2c_address = 0x76 i2c_sda=machine.Pin(0) i2c_scl=machine.Pin(1) i2c=machine.I2C(i2c_bus_number,sda=i2c_sda, scl=i2c_scl, freq=100000) digT = [] digP = [] digH = [] t_fine = 0.0 def writeReg(reg,data): i2c.writeto_mem(i2c_address, reg, data) def get_calib_param(): calib = [] for i in range (0x88,0x88+24): readdata = i2c.readfrom_mem(i2c_address,i,1) calib.append(int.from_bytes(readdata,'big')) readdata = i2c.readfrom_mem(i2c_address,0xA1,1) calib.append(int.from_bytes(readdata,'big')) for i in range (0xE1,0xE1+7): readdata = i2c.readfrom_mem(i2c_address,i,1) calib.append(int.from_bytes(readdata,'big')) teststr = calib[1] << 8 teststr = calib[1] | calib[0] digT.append((calib[1] << 8) | calib[0]) digT.append((calib[3] << 8) | calib[2]) digT.append((calib[5] << 8) | calib[4]) digP.append((calib[7] << 8) | calib[6]) digP.append((calib[9] << 8) | calib[8]) digP.append((calib[11]<< 8) | calib[10]) digP.append((calib[13]<< 8) | calib[12]) digP.append((calib[15]<< 8) | calib[14]) digP.append((calib[17]<< 8) | calib[16]) digP.append((calib[19]<< 8) | calib[18]) digP.append((calib[21]<< 8) | calib[20]) digP.append((calib[23]<< 8) | calib[22]) digH.append( calib[24] ) digH.append((calib[26]<< 8) | calib[25]) digH.append( calib[27] ) digH.append((calib[28]<< 4) | (0x0F & calib[29])) digH.append((calib[30]<< 4) | ((calib[29] >> 4) & 0x0F)) digH.append( calib[31] ) 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 def readData(): data = [] for i in range (0xF7, 0xF7+8): readdata = i2c.readfrom_mem(i2c_address,i,1) data.append(int.from_bytes(readdata,'big')) 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] compensate_T(temp_raw) compensate_P(pres_raw) compensate_H(hum_raw) def compensate_P(adc_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 if v1 == 0: return 0 pressure = ((1048576 - adc_P) - (v2 / 4096)) * 3125 if pressure < 0x80000000: pressure = (pressure * 2.0) / v1 else: pressure = (pressure / v1) * 2 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)) / 100 pressStr = "pressure: {:7.2f} hPa".format(pressure) print (pressStr) def compensate_T(adc_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 tempStr = "temperature: {:5.2f} c".format(temperature) print (tempStr) def compensate_H(adc_H): global t_fine var_h = t_fine - 76800.0 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 humStr = "hum: {:6.2f} %".format(var_h) print (humStr) def readCo2(): uart=machine.UART(1,baudrate=9600) b=bytearray([0xff, 0x01, 0x86, 0x00, 0x00, 0x00, 0x00, 0x00, 0x79]) uart.write(b) utime.sleep(0.1) n=uart.read(8) f=n[2]*256+n[3] co2Str = "co2: {} ppm".format(str(f)) print (co2Str) utime.sleep(0.5) uart = None 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 ctrl_meas_reg = bytearray(1) config_reg = bytearray(1) ctrl_hum_reg = bytearray(1) ctrl_meas_reg[0] = (osrs_t << 5) | (osrs_p << 2) | mode config_reg[0] = (t_sb << 5) | (filter << 2) | spi3w_en ctrl_hum_reg[0] = osrs_h writeReg(0xF2,ctrl_hum_reg) writeReg(0xF4,ctrl_meas_reg) writeReg(0xF5,config_reg) setup() get_calib_param() if __name__ == '__main__': for i in range(40): led.toggle() utime.sleep(0.15) while True: led.toggle() readCo2() readData() utime.sleep(10) ``` BME280周りは、以下ソースを改造。 https://github.com/SWITCHSCIENCE/BME280/blob/master/Python27/bme280_sample.py ハマりポイントとしては、smbusの「read_byte_data」は、戻り値が「int型」なんだけど、machine.i2cの「from_bytes」は、戻り値が「bytes型」なこと。 pythonのbytes型は、読み込み専用で演算に使えないので、int変換が要りますよ、と。 動かすと、USBシリアルから、10秒ごとにデータがつらつらと出てきます。 ```txt co2: 512 ppm temperature: 18.23 c hum: 42.15 % pressure: 1023.74 hPa ``` # 受け側 まあオマケ要素ですが。 どうせならサイネージに組み込もうかと。 というわけで、受け側。 会社で使ってるサイネージシステムは、raspbianで。 かつAPIサーバにデータ送る形なので。 受け渡し用のスクリプトを仕込みました。 ```python:send.py #!/usr/bin/env python # -*- coding: utf-8 -*- import urllib import urllib2 import serial import time import sys import os import re url = 'https://okuru.url' def main(): if not (os.path.exists('/dev/ttyACM0')): sys.exit() dataco2 = 0 datatemp = 0 datahum = 0 datapres = 0 con=serial.Serial('/dev/ttyACM0', 115200) while True: measdata=con.readline() if 'co2:' in measdata: result = re.match('co2: (\d+) ppm', measdata) if result != None: dataco2 = result.group(1) if 'temperature:' in measdata: result = re.match('temperature: (.+) c', measdata) if result != None: datatemp = result.group(1) if 'hum:' in measdata: result = re.match('hum: (.+) %', measdata) if result != None: datahum = result.group(1) if 'pressure:' in measdata: result = re.match('pressure: (.+) hPa', measdata) if result != None: datapres = result.group(1) hostname = os.uname()[1] if len(str(dataco2)) > 2 and len(str(datatemp)) > 2 and len(str(datahum)) > 2 and len(str(datapres)) > 2: params = { 'hostname' : hostname, 'dataco2' : dataco2, 'datatemp' : datatemp, 'datahum' : datahum, 'datapres' : datapres } params = urllib.urlencode(params) req = urllib2.Request(url) req.add_data(params) res = urllib2.urlopen(req) dataco2 = 0 datatemp = 0 datahum = 0 datapres = 0 sys.exit() if __name__ == '__main__': main() ``` ま、パラメータにデータくっつけて送ってるだけの単発ものです。 これをsystemdに設定して、タイマー動作、と。 ```sh:/etc/systemd/system/send.service [Unit] Description=send [Service] Type=simple ExecStart=/opt/send.py [Install] WantedBy=multi-user.target ``` ```sh:/etc/systemd/system/send.timer [Unit] Description=Runs send [Timer] OnBootSec=2min OnUnitActiveSec=1min Unit=send.service [Install] WantedBy=multi-user.target ``` あとはAPIサーバ側で適当に画面作って・・・。 ![キャプションを入力できます](https://camo.elchika.com/ae3541835bb857926237614b545a35f42345445b/687474703a2f2f73746f726167652e676f6f676c65617069732e636f6d2f656c6368696b612f76312f757365722f38666637633237372d646465642d343333632d393736342d6162303436346433326631302f66663566353839612d356538642d346635642d386436612d343834336337303335393832/) ま、こんな感じに!。 ![キャプションを入力できます](https://camo.elchika.com/8995e5424dc4c5c1677e94f8d9ee678e91a9a948/687474703a2f2f73746f726167652e676f6f676c65617069732e636f6d2f656c6368696b612f76312f757365722f38666637633237372d646465642d343333632d393736342d6162303436346433326631302f33333730316465382d356664372d343561632d613038372d626136653363626130393035/) # 基板起こし とはいってもユニバーサルでね。 秋月C基板を使いましたとさ。 さっくり部品仮置きして。 ![キャプションを入力できます](https://camo.elchika.com/cb9402dec6d6016bda4231e565812d70faf27056/687474703a2f2f73746f726167652e676f6f676c65617069732e636f6d2f656c6368696b612f76312f757365722f38666637633237372d646465642d343333632d393736342d6162303436346433326631302f62666133346361342d343231302d343061652d396166342d323635653037396661636139/) 配線!。 ![キャプションを入力できます](https://camo.elchika.com/8a6ac525982b6c891d664b89772843751d2cad3e/687474703a2f2f73746f726167652e676f6f676c65617069732e636f6d2f656c6368696b612f76312f757365722f38666637633237372d646465642d343333632d393736342d6162303436346433326631302f36653838373834642d623137302d343866382d393434332d663737636165393766643963/) ジュンフロンなETFEより線大好きです。 で、動作確認、納まりいいかんじ!。 ![キャプションを入力できます](https://camo.elchika.com/bdaa9472b0d2d27b8d1b4e2d5e979428d415c6ea/687474703a2f2f73746f726167652e676f6f676c65617069732e636f6d2f656c6368696b612f76312f757365722f38666637633237372d646465642d343333632d393736342d6162303436346433326631302f39353432643137662d373337632d343435652d386165632d653430643632646137303332/) # マウンタ作成 適当に作画。 ![キャプションを入力できます](https://camo.elchika.com/8b38d74a98707e6457bc09bf6a3713f0abcd5ac2/687474703a2f2f73746f726167652e676f6f676c65617069732e636f6d2f656c6368696b612f76312f757365722f38666637633237372d646465642d343333632d393736342d6162303436346433326631302f62636532383931392d646433662d343663632d383539362d663534303332336133376233/) いまいち寸法が合わないので、調整可能な穴具合にしてあります。 んで、レーザーカッターで、ガーっと。 ![キャプションを入力できます](https://camo.elchika.com/b14e036f7d894497d29e7c0e496c06ae37045849/687474703a2f2f73746f726167652e676f6f676c65617069732e636f6d2f656c6368696b612f76312f757365722f38666637633237372d646465642d343333632d393736342d6162303436346433326631302f32333965313039642d313834382d343231622d386232362d316632303961333562343365/) 切り抜き切り抜き。 ![キャプションを入力できます](https://camo.elchika.com/79f059288c3cf43b07079498fbb0fc65ac8da6e2/687474703a2f2f73746f726167652e676f6f676c65617069732e636f6d2f656c6368696b612f76312f757365722f38666637633237372d646465642d343333632d393736342d6162303436346433326631302f62383761303332382d363635382d343365632d396234372d353739623766633563613238/) できました!。 ![キャプションを入力できます](https://camo.elchika.com/b5f51e10f3071bcd942420553dc6ff8488890fa6/687474703a2f2f73746f726167652e676f6f676c65617069732e636f6d2f656c6368696b612f76312f757365722f38666637633237372d646465642d343333632d393736342d6162303436346433326631302f65323864343862392d376235392d346638372d396537322d666165333763623935316231/) # 接続と設置 まあ、無事完成!。 相手がラズパイなら、まあラズパイ側のUART I2Cを使えばいいのですが、別のサイネージ用STBにも使いたいなってね。 ![キャプションを入力できます](https://camo.elchika.com/afc5e3c6c6bf45be055993a57a6a655889d71d59/687474703a2f2f73746f726167652e676f6f676c65617069732e636f6d2f656c6368696b612f76312f757365722f38666637633237372d646465642d343333632d393736342d6162303436346433326631302f34393138653962372d376265622d343738612d393465382d333234313561383533306330/) 無事会社に置いて、ソーシャルディスタンスだのを意識させる系のサイネージとして、活躍しております。 ![キャプションを入力できます](https://camo.elchika.com/da59db92134dd4ea538921fd06c3146a986c8a78/687474703a2f2f73746f726167652e676f6f676c65617069732e636f6d2f656c6368696b612f76312f757365722f38666637633237372d646465642d343333632d393736342d6162303436346433326631302f61343736613638612d626232632d346561632d613934612d353762336563373964643764/) すてきすてきー!。