温度・湿度などの計測 anchor.png

部屋の温度・湿度等をRaspberry Pi/RPZ-IR-Sensorで計測して,データをDBに保存して可視化するシステムを作成する。

使用するのはRaspberry Pi Zero Wで,RPZ-IR-Sensorをスタックしてもピッタリ。

Page Top

RPZ-IR-Sensor anchor.png

RPZ-IR-Sensor.png

RPZ-IR-SensorIndoor Corgi社が販売しているRaspberry Pi/Zero用の拡張ボードで,温度・湿度・気圧・照度の計測機能と赤外線リモコン機能が搭載されている。以前,Amazonから5000円ぐらいで購入した。
Raspberry Piの40pin拡張コネクタにスタック装着して使用する。

  • BME280(温度・湿度・気圧センサー)搭載
    温度・湿度・気圧を測定するセンサーでA/Dコンバーター内蔵している。
    スタックして使用するのでRaspberry Piの発熱の影響を受けてしまう。
    インターフェースはI2C。I2C拡張コネクターがあるので付属しているBME280搭載の拡張ボードを接続できる。これを使うとRaspberry Piの発熱の影響をあまり受けずに計測できる。
  • TSL2561 光(照度)センサー搭載
    周囲の明るさを計測するための光(照度)センサー。
    センサーはA/Dコンバータを内蔵している。インターフェースはI2C。
  • 赤外線送受信機能
    赤外線送信および赤外線受信機能を搭載。
    赤外線送信用のLEDは,チップLEDを3個搭載して広範囲に送信できるようになっている。
    この機能を使って学習リモコンを作っている。
  • LED,スイッチ搭載
    LEDは4色(緑,黄,青,白)の4個のLED,スイッチは小型タクトスイッチを2個搭載している。
Page Top

データ保存用のDBを用意する anchor.png

自前のサーバーでMariaDBを使用しているので,そこにRaspberry Piで計測データを保存するDBを作成することにした。

DB:      envdb
TABLE:   iot
COLUM:   id             int auto inc
         rooms          varchar(50)
         time           datetime
         temperrature   float
         humidity       float
         pressure       float
         illuminance    float

カラムは,id,rooms(部屋の名前), time(日付時間),temperrature(温度),humidity(湿度),pressure(気圧),illuminance(照度)で作成した。

Page Top

DBの作成 anchor.png

以下のコマンドでDB envdbを作成する。

$ mysql -u root -p
MariaDB[(none)]> create database envdb;
MariaDB[(none)]> grant all privileges on envdb.* to  yuji@"%" identified by 'zzzzzzzz' with grant option;
MariaDB[(none)]> flush privileges;

テーブルiotを作成する。

MariaDB[(none)]> use envdb;
MariaDB[(envdb)]> create table iot (id int not null auto_increment,rooms varchar(50), time datetime, temperature float, humidity float, pressure float, illuminance float,primary key (id)) auto_increment=1;

テーブルの確認をする。

MariaDB[(envdb)]> show tables; 
+-----------------+
| Tables_in_envdb |
+-----------------+
| iot             |
+-----------------+

MariaDB[(envdb)]> show columns from iot;
+-------------+-------------+------+-----+---------+----------------+
| Field       | Type        | Null | Key | Default | Extra          |
+-------------+-------------+------+-----+---------+----------------+
| id          | int(11)     | NO   | PRI | NULL    | auto_increment |
| rooms       | varchar(50) | YES  |     | NULL    |                |
| time        | datetime    | YES  |     | NULL    |                |
| temperature | float       | YES  |     | NULL    |                |
| humidity    | float       | YES  |     | NULL    |                |
| pressure    | float       | YES  |     | NULL    |                |
| illuminance | float       | YES  |     | NULL    |                |
+-------------+-------------+------+-----+---------+----------------+

これで計測データを保存するDBが作成出来た。

Page Top

Raspberry Piのソフトウェア anchor.png

Raspberry Pi Zero WのプログラムはPythonで作成した。

Page Top

Pi Zero Wのデバイスドライバー anchor.png

Raspberry Pi Zero W(Raspbian buster)でI2Cインターフェースを使用できるように,デバイスドライバーをDevice Treeを使ってロード出来るようにする。
/boot/config.txtに以下を追加する。

dtparam=i2c_arm=on

/etc/modulesに以下を追加する。

i2c-dev

Raspberry Pi Zero Wを再起動した後,デバイスドライバーがロードされているのとデバイスファイルが作成されているのを確認しておく。

# lsmod | grep i2c
i2c_bcm2835             7818  0
i2c_dev                 7171  0
# ls -la /dev/i2c*
crw-rw---- 1 root i2c 89, 1  4月 13 04:05 /dev/i2c-1

I2Cインターフェースに接続されているデバイスを確認してみる。

$ i2cdetect -l
i2c-1   i2c             bcm2835 I2C adapter                     I2C adapter
$ i2cdetect -y 1
     0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f
00:          -- -- -- -- -- -- -- -- -- -- -- -- --
10: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
20: -- -- -- -- -- -- -- -- -- 29 -- -- -- -- -- --
30: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
40: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
50: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
60: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
70: -- -- -- -- -- -- 76 77

RPZ-IR-SensorのBME280(0x76,0x77)とTSL2561(0x29)が接続されているのが確認できる。

i2cdetectコマンドが無い場合は,

# apt install i2c-tools

でインストールする。

Page Top

BME280とTSL2561用のPythonパッケージ anchor.png

BME280とTSL2561用のPythonパッケージは,RPZ-IR-Sensorのサンプルプログラムに入っていたものを少し修正して使用している。

BME280で温度・湿度・気圧を計測する時に使用するパッケージはbme280i2c.py。温度が高く検出されるため修正した。

filebme280i2c.py
Everything is expanded.Everything is shortened.
-
!
-
|
|
|
!
 
 
 
 
 
-
!
 
 
 
 
 
-
!
 
 
 
 
 
-
!
 
 
 
 
 
 
 
 
 
 
 
-
!
 
 
 
 
 
 
 
 
 
-
!
 
 
-
!
 
 
 
 
 
 
 
 
 
 
 
-
!
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
-
!
-
!
-
!
-
!
 
 
 
 
 
 
 
 
 
 
 
-
!
 
 
 
 
 
 
 
 
-
!
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
-
!
 
 
 
 
 
 
 
 
 
 
 
 
 
 
-
!
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
#!/usr/bin/env python3
 
"""
BME280 Control Module via I2C
 2017/10/22
 2018/07/10 modified by Yuji Ueno
"""
 
import smbus
import time
 
class BME280I2C:
    # Return signed int from 16bit uint
    @staticmethod
    def get_signed8(uint):
        if uint>127:
            return uint-256
        return uint
 
    # Return signed int from 16bit uint
    @staticmethod
    def get_signed16(uint):
        if uint>32767:
            return uint-65536
        return uint
 
    # i2c_addr 0x76 or 0x77
    def __init__(self, i2c_addr):
        self.i2c_addr = i2c_addr
        self.i2c = smbus.SMBus(1)
        self.cal = {}               # Calibration data
        self.adc_T = 0
        self.adc_P = 0
        self.adc_H = 0
        self.T = 0
        self.P = 0
        self.H = 0
        self.t_fine = 0
 
    # I2C read length byte from addr
    def read_address(self, addr, length):
        try:
            return self.i2c.read_i2c_block_data(self.i2c_addr, addr, length)
        except IOError:
            return [0 for i in range(length)]
 
    def read_address_twobyte(self, addr):
        data = self.i2c.read_i2c_block_data(self.i2c_addr, addr, 2)
        return data[0] + (data[1]<<8)
 
    # I2C write data to addr
    def write_address(self, addr, data):
        self.i2c.write_i2c_block_data(self.i2c_addr, addr, data)
 
    # Read BME280 ID and return True if success
    def id_read(self):
        data = self.read_address(0xD0, 1)
        if data[0] == 0x60:
            return True
        return False
 
    def status_read(self):
        data = self.read_address(0xF3, 1)
        measuring = (data[0]&0x8)>>3
        im_update = data[0]&0x1
        return measuring, im_update
 
    # Read calibration registers and store in cal dict
    def read_cal(self):
        self.cal['dig_T1'] = self.read_address_twobyte(0x88)
        self.cal['dig_T2'] = self.get_signed16(self.read_address_twobyte(0x8A))
        self.cal['dig_T3'] = self.get_signed16(self.read_address_twobyte(0x8C))
        self.cal['dig_P1'] = self.read_address_twobyte(0x8E)
        self.cal['dig_P2'] = self.get_signed16(self.read_address_twobyte(0x90))
        self.cal['dig_P3'] = self.get_signed16(self.read_address_twobyte(0x92))
        self.cal['dig_P4'] = self.get_signed16(self.read_address_twobyte(0x94))
        self.cal['dig_P5'] = self.get_signed16(self.read_address_twobyte(0x96))
        self.cal['dig_P6'] = self.get_signed16(self.read_address_twobyte(0x98))
        self.cal['dig_P7'] = self.get_signed16(self.read_address_twobyte(0x9A))
        self.cal['dig_P8'] = self.get_signed16(self.read_address_twobyte(0x9C))
        self.cal['dig_P9'] = self.get_signed16(self.read_address_twobyte(0x9E))
        self.cal['dig_H1'] = self.read_address(0xA1,1)[0]
        self.cal['dig_H2'] = self.get_signed16(self.read_address_twobyte(0xE1))
        self.cal['dig_H3'] = self.read_address(0xE3,1)[0]
        self.cal['dig_H4'] = self.get_signed16(
            (self.read_address(0xE4,1)[0]<<4) + 
            (self.read_address(0xE5,1)[0]&0xF))
        self.cal['dig_H5'] = self.get_signed16(self.read_address_twobyte(0xE5)>>4)
        self.cal['dig_H6'] = self.get_signed8(self.read_address(0xE7,1)[0])
 
    def print_cal(self):
        for k, v in sorted(self.cal.items(), key=lambda x: x[0]):
            print(' {} : {}'.format(k, v))
 
    # Measure sensor data and store in adc_T, adc_P and adc_H
    def forced(self):
        #self.write_address(0xF5, [0x0])  # config
        self.write_address(0xF5, [0x10])  # config
        #self.write_address(0xF2, [0x5])  # ctrl_hum, oversampling x16
        self.write_address(0xF2, [0x01])  # ctrl_hum, oversampling x1
        #self.write_address(0xF4, [0xB5]) # ctrl_meas, oversampling x16 x16, forced mode
        self.write_address(0xF4, [0x55])  # ctrl_meas, oversampling x2 x16, forced mode
        time.sleep(0.01)
        measuring, im_update = self.status_read()
        while 1==measuring:
            time.sleep(0.001)
            measuring, im_update = self.status_read()
 
        data = self.read_address(0xF7, 8)
        self.adc_P = (data[0]<<12) + (data[1]<<4) + (data[2]>>4)
        self.adc_T = (data[3]<<12) + (data[4]<<4) + (data[5]>>4)
        self.adc_H = (data[6]<<8) + data[7]
 
    # Calculate temp from adc_T and calibration data
    def comp_T(self):
        var1 = ((((self.adc_T>>3) - (self.cal['dig_T1']<<1))) * (self.cal['dig_T2'])) >> 11
        var2  = (((((self.adc_T>>4) - (self.cal['dig_T1'])) * 
            ((self.adc_T>>4) - (self.cal['dig_T1']))) >> 12) *
            (self.cal['dig_T3'])) >> 14
        self.t_fine = var1 + var2
        self.T  = ((self.t_fine * 5 + 128) >> 8)/100
        self.T  = self.T - 2.2                                         # adj value by Yuji Ueno
 
    # Calculate pressure from adc_P and calibration data
    def comp_P(self):
        var1 = self.t_fine - 128000
        var2 = var1 * var1 * self.cal['dig_P6']
        var2 = var2 + ((var1*self.cal['dig_P5'])<<17)
        var2 = var2 + (self.cal['dig_P4']<<35)
        var1 = ((var1 * var1 * self.cal['dig_P3'])>>8) + ((var1 * self.cal['dig_P2'])<<12)
        var1 = (((1<<47)+var1))*(self.cal['dig_P1'])>>33
        if var1 == 0:
            return
 
        p = 1048576 - self.adc_P
        p = (((p<<31)-var2)*3125)//var1
        var1 = (self.cal['dig_P9'] * (p>>13) * (p>>13)) >> 25
        var2 = (self.cal['dig_P8'] * p) >> 19
        p = ((p + var1 + var2) >> 8) + ((self.cal['dig_P7'])<<4)
        self.P = p/25600
 
    # Calculate humidity from adc_H and calibration data
    def comp_H(self):
        v_x1_u32r = (self.t_fine - 76800)
        v_x1_u32r = (((((self.adc_H << 14) - ((self.cal['dig_H4']) << 20) - 
            ((self.cal['dig_H5']) * v_x1_u32r)) + 16384) >> 15) * 
            (((((((v_x1_u32r * self.cal['dig_H6']) >> 10) * (((v_x1_u32r *
            self.cal['dig_H3']) >> 11) + 32768)) >> 10) + 2097152) *
            self.cal['dig_H2'] + 8192) >> 14))
        v_x1_u32r = (v_x1_u32r - (((((v_x1_u32r >> 15) * (v_x1_u32r >> 15)) >> 7) * 
            self.cal['dig_H1']) >> 4))
        if v_x1_u32r < 0:
            v_x1_u32r = 0
        if v_x1_u32r > 419430400:
            v_x1_u32r = 419430400
        self.H = (v_x1_u32r>>12)/1024
 
    # Measure T/P/H
    def meas(self):
        if not self.id_read():
            return False
        self.read_cal()
        self.forced()
        self.comp_T()
        self.comp_P()
        self.comp_H()
        return True
        
    def print_reg(self):
        print( ' t_fine : {}'.format(self.t_fine))
        print( ' adc_T  : {}'.format(self.adc_T))
        print( ' adc_P  : {}'.format(self.adc_P))
        print( ' adc_H  : {}'.format(self.adc_H))
    
    def print_meas(self):
        print( ' Temp     : {:.1f}C'.format(self.T))
        print( ' Pressure : {:.1f}hPa'.format(self.P))
        print( ' Humidity : {:.1f}%'.format(self.H))
 
def main():
    bme280ch1 = BME280I2C(0x76)
    bme280ch2 = BME280I2C(0x77)
 
    if bme280ch1.meas():
        print('BME280 0x76')
        bme280ch1.print_cal()
        bme280ch1.print_reg()
        bme280ch1.print_meas()
 
    if bme280ch2.meas():
        print('BME280 0x77')
        bme280ch2.print_cal()
        bme280ch2.print_reg()
        bme280ch2.print_meas()
 
if __name__ == '__main__':
    main()

TSL2561で照度を計測するパッケージはtsl2561.py。照度が0luxの時にdevide by zeroエラーが出るbugを修正した。

filetsl2561.py
Everything is expanded.Everything is shortened.
-
!
-
|
|
|
!
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
-
!
 
 
 
 
 
 
-
!
 
 
 
-
!
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
-
!
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
#!/usr/bin/env python3
 
"""
TSL2561 Control Module via I2C
 2018/3/16
 2018/07/10 modified by Yuji Ueno
"""
 
import smbus
import time
 
class TSL2561:
    AGAIN_LOW = 0
    AGAIN_HIGH = 1
 
    ATIME_13MS = 0
    ATIME_101MS = 1
    ATIME_402MS = 2
 
    def __init__(self, i2c_addr):
        self.i2c_addr = i2c_addr
        self.i2c = smbus.SMBus(1)
        self.ch0 = 0
        self.ch1 = 0
        self.lux = 0
        self.again = TSL2561.AGAIN_HIGH
        self.atime = TSL2561.ATIME_402MS
 
    # I2C read length byte from addr
    def read_address(self, addr, length):
        addr = addr | 0x80
        try:
            return self.i2c.read_i2c_block_data(self.i2c_addr, addr, length)
        except IOError:
            return [0 for i in range(length)]
 
    # I2C write data to addr
    def write_address(self, addr, data):
        addr = addr | 0x80
        self.i2c.write_i2c_block_data(self.i2c_addr, addr, data)
 
    # Read ID and return True if success
    def id_read(self):
        data = self.read_address(0xA, 1)
        data[0] = data[0]>>4
        if data[0] == 0x1 or data[0] == 0x5 or data[0] == 0x7:
            return True
        return False
 
    def set_timing(self, again, atime):
        self.write_address(0x1, [(again<<4) | atime])
 
    def als_integration(self):
        self.write_address(0x0, [0x0])     # Power OFF
        self.set_timing(self.again, self.atime)
        self.write_address(0x0, [0x3])     # Power ON
 
        if TSL2561.ATIME_13MS == self.atime:
            time.sleep(0.020)
        elif TSL2561.ATIME_101MS == self.atime:
            time.sleep(0.120)
        elif TSL2561.ATIME_402MS == self.atime:
            time.sleep(0.430)
 
        data = self.read_address(0xC, 4)
        self.write_address(0x0, [0x0])     # Power OFF
        self.ch0 = (data[1] << 8) | data[0]
        self.ch1 = (data[3] << 8) | data[2]
 
    def meas_single(self):
        if not self.id_read():
            return False
 
        self.again = TSL2561.AGAIN_HIGH
        self.atime = TSL2561.ATIME_101MS
 
        while True:
            self.als_integration()
            if self.ch0 > 37000 or self.ch1 > 37000:
                if self.again == TSL2561.AGAIN_HIGH and self.atime == TSL2561.ATIME_101MS:
                    self.again = TSL2561.AGAIN_LOW
                else:
                    break
            elif self.ch0 < 300 or self.ch1 < 300:
                if self.again == TSL2561.AGAIN_HIGH and self.atime == TSL2561.ATIME_101MS:
                    self.atime = TSL2561.ATIME_402MS
                else:
                    break
            else:
                break
        self.calc_lux()
        return True
 
    def calc_lux(self):
        if TSL2561.ATIME_13MS == self.atime:
            t = 13.7
        elif TSL2561.ATIME_101MS == self.atime:
            t = 101
        elif TSL2561.ATIME_402MS == self.atime:
            t = 402
 
        if TSL2561.AGAIN_LOW == self.again:
            g = 1
        elif TSL2561.AGAIN_HIGH == self.again:
            g = 16
 
        if 0==self.ch0:                                     # modify by Yuji Ueno
            lux = 0                                         # against divide by zero error
        else:
            ratio = self.ch1 / self.ch0
 
            # T, FN, CL package
            if ratio <= 0.5:
                lux = 0.0304*self.ch0 - (0.062*self.ch0 * pow(ratio,1.4))
            elif ratio <= 0.61:
                lux = 0.0224*self.ch0 - 0.031*self.ch1
            elif ratio <= 0.8:
                lux = 0.0128*self.ch0 - 0.0153*self.ch1
            elif ratio <=1.3:
                lux = 0.00146*self.ch0 - 0.00112*self.ch1
            else:
                lux = 0
 
        self.lux = lux * 16/g * 402/t
        
    def print_reg(self):
        if TSL2561.ATIME_13MS == self.atime:
            print(' ADC Time : 13ms')
        elif TSL2561.ATIME_101MS == self.atime:
            print(' ADC Time : 101ms')
        elif TSL2561.ATIME_402MS == self.atime:
            print(' ADC Time : 402ms')
 
        if TSL2561.AGAIN_LOW == self.again:
            print(' ADC Gain : Low')
        elif TSL2561.AGAIN_HIGH == self.again:
            print(' ADC Gain : High')
 
        print(' ch0 : 0x{:X}'.format(self.ch0))
        print(' ch1 : 0x{:X}'.format(self.ch1))
        
    
    def print_meas(self):
        print( ' Lux : {:.1f}lux'.format(self.lux))
 
def main():
    tsl2561 = TSL2561(0x29)
    tsl2561.meas_single()
    tsl2561.print_reg()
    tsl2561.print_meas()
 
if __name__ == '__main__':
    main()
Page Top

温度・湿度・気圧・照度の計測とDB保存プログラム anchor.png

Raspberry Piで計測して計測したデータを,Ambientに送信するのと,ローカルのDBに保存するプログラム。

Ambientにデータを送信するためPythonのambientパッケージをインストールした。

$ pip3 install git+https://github.com/AmbientDataInc/ambient-python-lib.git

ローカルのDB(MariaDB)と接続するために,PythonのPyMySQLパッケージをインストールした。

$ pip3 install PyMySQL

部屋の温度・湿度・気圧・照度を計測してDBへの記録するプログラムsensor.pyを作成した。
センサーは拡張ボードのBME280を使っている。

filesensor.py
Everything is expanded.Everything is shortened.
-
!
-
|
|
|
|
|
|
|
|
|
|
!
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
#!/usr/bin/env python3
 
"""
Read temperature,humidity,pressure,illuminance Data (BME280/TSL2561)
Usage:
    sensor.py [-l <log_file>] [-v] [-a]
    sensor.py -h --help
 
Options:
    -l <log_file>  Output csv log file name
    -v             Show verbose message
    -a             Send data to Ambient and local DB(envdb)
    -h --help      Show this screen and exit
"""
 
import os
import csv
import pymysql.cursors
import ambient
from bme280i2c import BME280I2C
from tsl2561 import TSL2561
from datetime import datetime
from docopt import docopt
 
connection  = pymysql.connect(
    user    = "yuji",
    passwd  = "zzzzzzzz",
    host    = "192.168.xxx.yyy",
    db      = "envdb",
    charset = "utf8mb4",
    )
 
def main():
    args = docopt(__doc__)
    loglist = ['']*6
 
    bme280ch = BME280I2C(0x76)
    tsl2561 = TSL2561(0x29)
    r1 = bme280ch.meas()
    r2 = tsl2561.meas_single()
 
    if not (r1 or r2 ):
        print('No Sensor Available')
 
    if r1:
        if args['-v']:
            bme280ch.print_cal()
            bme280ch.print_reg()
        bme280ch.print_meas()
        loglist[2] = '{:.1f}'.format((bme280ch.T))
        loglist[3] = '{:.1f}'.format(bme280ch.P)
        loglist[4] = '{:.1f}'.format(bme280ch.H)
 
    if r2:
        if args['-v']:
            tsl2561.print_reg()
        tsl2561.print_meas()
        loglist[5] = '{:.1f}'.format(tsl2561.lux)
    
    loglist[0] = 'リビングルーム'
    loglist[1] = datetime.now()
    
    if args['-a']:
        try:
            cur = connection.cursor()
            cur.execute("INSERT INTO iot (rooms,time,temperature,humidity,pressure,illuminance) VALUES(%s,%s,%s,%s,%s,%s)",(loglist[0],loglist[1],loglist[2],loglist[4],loglist[3],loglist[5]))
            connection.commit()
            cur.close()
            
        except:
            print("DB write failure.")
        
        am = ambient.Ambient(1234, "BBBBBBBBBBBBBBB")
        try:
            r = am.send({'d1': loglist[2], 'd2': loglist[3], 'd3': loglist[4], 'd4': loglist[5]})
            print(r.status_code)
            r.close()
        
        except:
            print("Ambient send failure.")
        
    
    if args['-l'] != None:
        exist = os.path.isfile(args['-l'])
        
        with open(args['-l'], 'a') as f:
            writer = csv.writer(f, lineterminator='\n')
            if not exist:
                writer.writerow(['rooms', 'Time', 'Temp', 'Pressure', 'Humidity', 'Lux'])
            
            writer.writerow(loglist)
            
if __name__ == '__main__':
    main()

cronで10分ごとにsensor.pyを起動するようにしている。

0-59/10 * * * *      /home/yuji/work/homesensor/sensor.py -a 2>&1

これで10分毎に計測して,計測したデータをAmbientに送信し,同時に上記のローカルDBに保存している。

Page Top

計測データの可視化 anchor.png

上記のように計測したデータはAmbientとローカルサーバーのDBに保存している。

これでAmbientにアクセスして,データをグラフで表示できるようになった。

Page Top

Ambientで計測データを見る anchor.png

AmbientはIoTデータを可視化するCloud WEBサービスで,無料で使用することが出来る :D

  • Ambientからユーザー登録でアカウントを作成する。
  • ログインする。
  • チャンネルを作成。
    「リビングルーム」というチャンネル名で作成した。
    チャンネルを作成すると,データ送受信のためのキーが発行されるのでメモする。
  • データをAmbientに送信する。
    上記のプログラム(発行されたキーを埋め込む)で,Raspberry Pi Zero Wからデータを定期的にAmbientに送信する。
  • データをグラフで確認。
    データを送信した後は,WEBブラウザでチャンネルを開くとすでにグラフ表示されている。
    表示はいろいろカスタマイズ出来るので,好みに設定する。
    • チャンネルの設定
      チャンネル情報や場所,色をカスタマイズできる。
      場所を表示するようにすると,Google Mapで場所を表示できるようになる。
    • チャートの設定
      表示されているグラフの設定アイコンをクリックすると,グラフ表示のカスタマイズが出来る。
      サイズを変更したり,一つのグラフにいくつかのデータを同時に表示するようにも出来る。

このようにして,WEBブラウザでチャンネルを表示するとグラフでデータを確認できるようになった。

ambient.png

いつでもいろいろな端末から,WEBアクセスすることで計測データを見ることが出来る。

無料でこれだけの機能を提供してもらえるので,1年間のデータ保存期間(2023/05/01から4ヶ月)はしょうがないと思う。

  • 1チャンネルで8種類のデータに対応。
  • 最大8チャンネル作成できる。
  • 1チャンネルあたり最大3000データ/1日(約30sに1回)まで使用できる。
  • 1チャンネル8個までのチャート表示が可能。
  • データーの保存期間は1年間(2023/05/01から4ヶ月)。
Page Top

Grafanaで計測データを見る anchor.png

Ambientは無料で便利に利用していたのだが,規約が変更になりデータ保存期間が2023/05/01から4ヶ月になってしまう。
そこでローカルDB保存してあるデータを直接グラフ表示することにした。

いろいろ探してみると,GrafanaというGrafana Labsがオープンソースで開発しているマルチプラットフォームで動作するログ・データ可視化ツールを見つけた。

GrafanaはDBにクエリ問い合わせをして取得したデータを,グラフ等に表示することも出来るようだ。
表示する期間や時間帯を変えたり,パーツ毎に特定のノードだけのデータを表示したり,インタラクティブに可視化を操作することが出来る。

対応しているデータソースやDBは数多くある。データソースプラグインを作成することも出来る。

これ以外にもいろいろ対応しているようだ。

Page Top

Grafanaのインストール anchor.png

インストールは公式サイトに説明がある。
自前のローカルサーバー(CentOS 7.9)に以下のようにしてインストールした。

Grafanaリポジトリを追加する。
/etc/yum.repos.d/grafana.repoを作成する。

[grafana]
name=grafana
baseurl=https://rpm.grafana.com
repo_gpgcheck=1
enabled=1
gpgcheck=1
gpgkey=https://rpm.grafana.com/gpg.key
sslverify=1
sslcacert=/etc/pki/tls/certs/ca-bundle.crt
exclude=*beta*

パッケージ管理ツールで,Grafanaをインストールする。

# yum check-update
# yum install grafana

インストール後にいくつか設定を行った。
Apache経由でアクセスできるようにするため,/etc/httpd/conf.d/grafana.confファイルを新規作成する。

<IfModule mod_proxy.c>
  ProxyPreserveHost On
  ProxyPass /iot http://xxxx.xxx.xxx.xxx:3000
  ProxyPassReverse  /iot http://xxx.xxx.xxx.xxx:3000
</IfModule>

Grafanaの設定ファイル/etc/grafana/grafana.iniを編集する。

 :
protocol = http
http_adr = 192.168.XXX.YYY
domain = yueno.net
root_url = https://xxxx.xxx/iot
 :

Apacheを再起動する。

# systemctl restart httpd

Grafanaの自動起動を有効にして,起動する。

# systemctl enable grafana-server
# systemctl start grafana-server
# systemctl status grafana-server
Page Top

Grafanaでデータを可視化出来るように設定 anchor.png

Grafanaで可視化する場合は,Dashboardを作成してDashboard毎に可視化パーツを設定していく。

  • Dashboardを作成
  • Panelを追加
  • データ取得のクエリをセット

全てGrafanaに管理者でログインしてWEBで設定できる。
作成・設定したDashboardは保存出来て,設定をJson形式でexportしてバックアップも出来る。

Page Top

Grafanaにログインする anchor.png

GrafanaへWEBブラウザでURLに https://xxxxxx.xxxx/iot としてアクセスする。
ログイン画面が表示されるので,最初は管理者ユーザーでログインする。

user: admin
password: admin

でログイン出来る。ログイン後パスワードを変更するように促されるので変更する。

Page Top

新規のユーザーを作成する anchor.png

configuration → users → inviteをクリックして,必要事項を入力する。
ユーザー追加は招待形式みたいだ。メールで招待を通知することも可能。

Page Top

チームを作成する anchor.png

Grafanaはユーザーをチームというユーザーグループで管理している。
チーム別にトップ画面を変更することが出来るみたい。

Teams → New teamをクリックして,チーム名を入力した後にCreateでチームを作成できる。

チームを作成したらユーザーを追加する。
Add member → Add to teamでユーザーを追加出来る。

Teamsで作成したチームをクリックするとチームの設定ができる。
Settings → Home dashboardでデフォルトのダッシュボードを設定できる。(前もって☆マークを付けておく必要がある。)

Page Top

GrafanaでDBにアクセスする anchor.png

GrafanaでサーバーのDBにアクセスするための設定を行う。

  • 画面左のメニューバーの歯車マークのConfigulation → Data sourcesを選択する。
  • Add data sourceをクリック
    下の方へスクロールしてMySQLを選択して,DBへの接続設定を行う。
    Host: DBのIP Address:3306
    Database: envdb
    User: yuji
    Password: ZZZZZZZZ

これでGrafanaからMariaDBに接続できるようになる。

Page Top

新しいダッシュボードの作成 anchor.png

Grafanaに新規に作成したユーザーでログインするとデフォルトのダッシュボードが表示されるので,データをグラフ表示するダッシュボードを作成する。

  • 画面の左にあるメニューバーのCreateからDashboardを選択して新しいダッシュボードを作成する画面に移る。
  • パネル(グラフのこと)を追加する。
    新しいダッシュボードにはまだ何も配置されていないため,パネルを作成してグラフを表示させる。
    Add panelボタンをクリックしてAdd a new panelを選択すると,パネル設定画面が開きグラフの種類やパラメータが設定できる。
    デフォルトパネルはTime Seriesで,横軸が時間でデータをグラフ化出来る。
  • 画面下部にDBからデータを読み込む設定をするところがある。この画面のToggle text edit modeをクリックする。
    DBからデータを読み込むためのSQLコマンドを編集する画面になるので,以下のように編集した。
    SELECT
      CONVERT_TZ(time, '+00:00', '-09:00') AS "time",
      temperature 温度,humidity 湿度
    FROM
      envdb.iot
    WHERE
      $__timeFilter(time)
    ORDER BY time
    ダッシュボード上で表示している時間のデータのみを表示させる場合,そのテーブルにdatetime型のカラムがあればクエリ入力項目のWHERE句に「$__timeFilter(<datetime 型のカラム>)」を設定することで,ダッシュボード上の表示時間と連動するようになる。
  • Title設定 右上のPanel optionsのTitleに「温度・湿度」と入力し,画面上中央部の時計印のセルをLast 24 hoursに変更して右上のApplyをクリックする。
  • 同様にして,気圧と照度も単独のグラフが表示されるようにパネルを作成する。
  • ダッシュボードに表示されているパネルは大きさや位置などをマウスで修正できる。

これでダッシュボード画面に戻ると温度・湿度,気圧,照度のデータを表示するグラフが作成できた。
Grafanaでグラフ化できるデータは,「時間の要素を持つ数値データ」のみのようだ。

画面右上の丸い矢印アイコンのRefresh dashboardをクリックすると,アイコンの右にドロップダウンメニューが表示されるため,5sを選択するとグラフが5秒おきに更新されるみたいだ。
最後に,新しく作成したダッシュボードをSave dashboardをクリックして保存する。

これで,計測したデータを可視化出来るように出来た。

grafana.png
Page Top

I2C通信ポートを別のGPIOを使って行う方法 anchor.png

Raspberry Pi Zero WとRPZ-IR-SensorのBME280とTSL2561との通信はI2Cバスを使用している。
このため,GPIOコネクタの3pinにスイッチ等は接続できない。

Raspbianでは,RPZ-IR-Sensorを改造するなどしてI2Cバスを別のGPIOを使ってソフトウェア的にI2C通信できるようにDevice Treeのoverlay機能にi2c-gpio(デバイスドライバー)が用意されている。

例えば,GPIO5(29pin)とGPIO6(31pin)をI2Cバスとして使いたい場合は,/boot/config.txtに以下のように追加する。

dtoverlay=i2c-gpio,bus=2,i2c_gpio_sda=5,i2c_gpio_scl=6,i2c_gpio_delay_us=2

i2c_gpio_delay_us=2にすることで,通信速度が通常のI2Cバスと同じ100kbpsになる。
この時,GPIO5とGPIO6の回路にプルアップ抵抗10kΩを実装する必要がある。

GPIO0(27pin)とGPIO1(28pin)は1.8kΩでプルアップされているため,ここを使う場合はハードウェア追加は必要ないと思われる。

 dtoverlay=i2c-gpio,bus=2,i2c_gpio_sda=0,i2c_gpio_scl=1,i2c_gpio_delay_us=2

新しくコメントをつける

題名
ゲスト名
投稿本文
より詳細なコメント入力フォームへ

トップ   凍結 差分 バックアップ 複製 名前変更 リロード   ページ新規作成 全ページ一覧 単語検索 最新ページの一覧   ヘルプ   最新ページのRSS 1.0 最新ページのRSS 2.0 最新ページのRSS Atom
Counter: 905, today: 2, yesterday: 2
最終更新: 2023-04-13 (木) 11:41:25 (JST) (373d) by yuji