Raspberry Pi ZERO WH + ACM1602NI LCD液晶 + BME280温湿度気圧センサーで色々表示してみる

ラズパイに液晶をくっつけて色々表示したら面白いんじゃないかと唐突に思いついたので作ってみました。
※最終的には魔改造の結果、ラズパイ起動後に自動実行され**日付、時刻、温度、湿度、CPU使用率、メモリー使用率、IPアドレスが表示されるようになりました。**
※pythonプログラミングは超初心者です。美しいコードとか一切意識してません。また作りたいから作っただけで実用性は無視です。基本的には自分用の覚書です。

#用意したもの
Raspberry Pi ZERO WH
http://akizukidenshi.com/catalog/g/gM-12958/
LCD液晶　ACM1602NI
http://akizukidenshi.com/catalog/g/gP-05693/
１００ｋΩ半固定抵抗（↑のLCDを使うならこれが必要）
http://akizukidenshi.com/catalog/g/gP-08014/
温湿度気圧センサー　BME280　
http://akizukidenshi.com/catalog/g/gK-09421/
その他、ハンダゴテ、ブレッドボード、ラズパイ接続環境などの電子工作及びRaspberry Pi関連一般機器

#①I2C接続を有効にしてLCDに文字を出してみる
ラズパイではデフォルトではI2C接続が無効になっているはずです。
設定を行いI2Cを有効化、サンプルプログラムを使いなにか文字を投影しLCDの動作確認まで行います。
ポイントは、**dtparam=i2c_baudrate=50000を設定する**こと、**i2cdetectをしない**ことです。
私の場合はボーレートを設定する*config.txt*内でi2cに関する設定文のコメントアウトをはずしてやらないと動きませんでした。
その後*https://github.com/yuma-m/raspi_lcd_acm1602ni.git*のサンプルプログラムで文字が表示されればOK
詳細な説明は参考ページをがわかりやすいです。↓
https://www.denshi.club/pc/raspi/i2caqmlcdarduinode1-aqm0802-2.html
http://yura2.hateblo.jp/entry/2016/02/13/Raspberry_Pi%E3%81%AB%E6%8E%A5%E7%B6%9A%E3%81%97%E3%81%9FLCD%28ACM1602NI%29%E3%82%92Python%E3%81%A7%E5%8B%95%E3%81%8B%E3%81%99
https://ameblo.jp/raspberrypi-iot/entry-12345308004.html

#②BME280を接続して温度、湿度、気圧を取得してみる
次にBME280センサーを接続してデータの取得をしてみます。
先にLCD液晶の回路があるはずなのでそこに追加する形でいきました。
I2C接続は線を共用できるので配線が少なく楽ですね。
配線後に*https://github.com/SWITCHSCIENCE/BME280/blob/master/Python27/bme280_sample.py*のサンプルプログラムを使って各種データが取得できればOK
ポイントは、今回のようにACM1602NIをすでに接続した状態でテストをするならやっぱり**i2cdetectはやってはいけません。**
これをやると液晶が止まります・・・
詳細な説明は参考ページをがわかりやすいです。↓
https://deviceplus.jp/hobby/raspberrypi_entry_039/

ここまでの配線状況
![DSC_0631.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/493615/0765eeef-ab8b-cd66-d052-90651e51cf92.jpeg)


#③BME280のデータをLCDに投影する
LCD表示のサンプルプログラムを見るとmain関数の中は次のようになっていました。

```raspi_lcd.py
（前略）
def main():
    if not 2 <= len(sys.argv) <= 3:
        print('Usage: python raspi_lcd.py "message for line 1" ["message for line 2"]')
        return
    else:
        lcd_controller = LCDController()
        lcd_controller.display_messages(sys.argv[1:3])
（後略）
```

ここでコマンドラインから引数としてうけとった文字列の数を判定し、受け取った引数が１または２なら*lcd_controller.display_messages(sys.argv[1:3])*に渡してLCDに投影されるようです。
※ちなみに引数はsys.argvに格納されますがsys.argv[0]はプログラム名が入るらしいです。
・・・適当にリストを作ってそこに文字列を格納し*lcd_controller.display_messages*に渡せば投影できるのでは？（単純
というわけでBME280のサンプルプログラムをLCD表示サンプルプログラムに無理やり追記しLCD表示に関するmain関数部分のみ次のように書き換えました
var_h,temperature,pressurear_h,temperature,pressureの各変数をグローバル変数化し参照できるように変えているので注意
また、このLCDは仕様上「℃」の文字が使えません。**BME280のサンプルプログラムをそのまま使うと「℃」が表示できずエラーを起こします**のでなにかしら対策が必要です。私は代わりに「’C」を表示するようにしました。

```lcd.py
（前略）
def main():
    while 1:
        lcddisp = []　#LCD表示文字列格納リスト
        readData()　#BME280のデータ取得関数
        lcdhum = ("HUM:%6.2f ％" % (var_h)) #取得した湿度に表示用の文字列を追加
        lcdtemp = ("TEMP: %-6.2f'C" % (temperature))　#取得した気温に表示用の文字列を追加 
        lcdpressure = ("pressure:%7.2fhPa" % (pressure/100))  #取得した気圧に表示用の文字列を追加      
        lcddisp.append(lcdtemp)　#LCD表示文字列格納リストに気温を追加
        lcddisp.append(lcdhum)　#LCD表示文字列格納リストに湿度を追加
        lcd_controller = LCDController()
        lcd_controller.display_messages(lcddisp[0:2])　#LCD表示文字列格納リストを表示関数に渡す
        time.sleep(2)　#２秒待機後にLCD表示アップデート
（後略）
```

#④日付と時刻を表示する
せっかくなので日付と時刻ぐらい表示したくね？（いきあたりばったりの思いつき）
main関数が以下のようになりました。（*import datetime*忘れないでね）


```lcd.py
（前略）
import datetime
（中略）
def main():
    while 1:
        lcddisp = [] #LCD表示文字列格納リスト
        readData() #BME280のデータ取得関数
        lcdhum = ("HUM:%6.2f ％" % (var_h)) #取得した湿度に表示用の文字列を追加
        lcdtemp = ("TEMP: %-6.2f'C" % (temperature)) #取得した気温に表示用の文字列を追加 
        lcdpressure = ("pressure:%7.2fhPa" % (pressure/100))  #取得した気圧に表示用の文字列を追加      
        lcddisp.append(lcdtemp) #LCD表示文字列格納リストに気温を追加
        lcddisp.append(lcdhum) #LCD表示文字列格納リストに湿度を追加
        lcd_controller = LCDController()
        lcd_controller.display_messages(lcddisp[0:2]) #LCD表示文字列格納リストを表示関数に渡す
        time.sleep(2) #２秒待機後にLCD表示アップデート

        lcddisp = [] #LCD表示文字列格納リストリセット
        dt_now = datetime.datetime.now() #現在時刻の取得
        lcddisp.append(dt_now.strftime('DATE: %Y/%m/%d')) #年月日を格納
        lcddisp.append(dt_now.strftime('TIME: %H:%M:%S')) #時分秒を格納
        lcd_controller = LCDController()
        lcd_controller.display_messages(lcddisp[0:2]) #LCD表示文字列格納リストを表示関数に渡す
        time.sleep(2) #２秒待機後にLCD表示アップデート
（後略）
```

#⑤CPU使用率とメモリー使用率を表示する
将来のヘッドレス運用に備えてCPU使用率とメモリー使用率も監視したいよね？
どうやらpsutilモジュールなるものを使うとできるらしい
参考↓
https://algorithm.joho.info/programming/python/psutil-cpu-memory-usage/
そして度重なるmain関数の魔改造

```lcd.py
（前略）
import psutil
（中略）
def main():
    while 1:
        lcddisp = [] #LCD表示文字列格納リスト
        readData() #BME280のデータ取得関数
        lcdhum = ("HUM:%6.2f ％" % (var_h)) #取得した湿度に表示用の文字列を追加
        lcdtemp = ("TEMP: %-6.2f'C" % (temperature))　#取得した気温に表示用の文字列を追加 
        lcdpressure = ("pressure:%7.2fhPa" % (pressure/100))  #取得した気圧に表示用の文字列を追加      
        lcddisp.append(lcdtemp) #LCD表示文字列格納リストに気温を追加
        lcddisp.append(lcdhum) #LCD表示文字列格納リストに湿度を追加
        lcd_controller = LCDController()
        lcd_controller.display_messages(lcddisp[0:2])　#LCD表示文字列格納リストを表示関数に渡す
        time.sleep(2) #２秒待機後にLCD表示アップデート

        lcddisp = [] #LCD表示文字列格納リストリセット
        dt_now = datetime.datetime.now() #現在時刻の取得
        lcddisp.append(dt_now.strftime('DATE: %Y/%m/%d')) #年月日を格納
        lcddisp.append(dt_now.strftime('TIME: %H:%M:%S')) #時分秒を格納
        lcd_controller = LCDController()
        lcd_controller.display_messages(lcddisp[0:2]) #LCD表示文字列格納リストを表示関数に渡す
        time.sleep(2) #２秒待機後にLCD表示アップデート

        lcddisp = [] #LCD表示文字列格納リストリセット
        memory = psutil.virtual_memory() #メモリー使用率取得
        cpu_percent = psutil.cpu_percent(interval=1) #CPU使用率取得
        lcdmem = ('MEM: ' + str(memory.percent) + " %") #メモリ使用率に表示用の文字列を追加
        lcdcpu = ('CPU: ' + str(cpu_percent) + " %") #CPU使用率に表示用の文字列を追加
        lcddisp.append(lcdcpu) #CPU使用率を格納
        lcddisp.append(lcdmem) #メモリー使用率を格納
        lcd_controller = LCDController()
        lcd_controller.display_messages(lcddisp[0:2]) #LCD表示文字列格納リストを表示関数に渡す
        time.sleep(2)　#２秒待機後にLCD表示アップデート
（後略）
```

#⑥IPアドレスを表示する
ラズパイ起動後にIPアドレス表示してくれたらSSHとかRDPとかするとき便利だよね、と思いついたので・・・
どうやら*gethostname*を使うとできるとあるが127.0.0.1しか取得できない・・・
調べると以下のページに答えがあったので参考に↓
https://qiita.com/kjunichi/items/8e4967d04c3a1f6af35e
そしてmain関数を含めた最終的なコードの全容は次の通り

```lcd.py
#!/usr/bin/env python
# -*- coding: utf-8 -*-

from __future__ import print_function
from smbus2 import SMBus
import sys
import time
import smbus
import unicodedata
import datetime
import psutil
import socket

from config import BUS_NUMBER, LCD_ADDR, SLEEP_TIME, DELAY_TIME
from character_table import INITIALIZE_CODES, LINEBREAK_CODE, CHAR_TABLE

COMMAND_ADDR = 0x00
DATA_ADDR = 0x80
bus_number  = 1
i2c_address = 0x76

bus = SMBus(bus_number)

digT = []
digP = []
digH = []

t_fine = 0.0

pressure = 0.0
temperature = 0.0
var_h = 0.0
lcddisp = ["0","0"]

def writeReg(reg_address, data):
	bus.write_byte_data(i2c_address,reg_address,data)

def 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))

	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):
		data.append(bus.read_byte_data(i2c_address,i))
	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
	global  pressure
	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)  

def compensate_T(adc_T):
	global t_fine
        global temperature
	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
        
def compensate_H(adc_H):
	global t_fine
	global var_h
        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

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 = (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)


setup()
get_calib_param()

class LCDController:
    def __init__(self):
        self.bus = smbus.SMBus(BUS_NUMBER)
        pass

    def send_command(self, command, is_data=True):
        if is_data:
            self.bus.write_i2c_block_data(LCD_ADDR, DATA_ADDR, [command])
        else:
            self.bus.write_i2c_block_data(LCD_ADDR, COMMAND_ADDR, [command])
        time.sleep(DELAY_TIME)

    def initialize_display(self):
        for code in INITIALIZE_CODES:
            self.send_command(code, is_data=False)

    def send_linebreak(self):
        for code in LINEBREAK_CODE:
            self.send_command(code, is_data=False)

    def normalize_message(self, message):
        if isinstance(message, str):
            message = message.decode('utf-8')
        return unicodedata.normalize('NFKC', message)

    def convert_message(self, message):
        char_code_list = []
        for char in message:
            if char not in CHAR_TABLE:
                error_message = 'undefined character: %s' % (char.encode('utf-8'))
                raise ValueError(error_message)
            char_code_list += CHAR_TABLE[char]
        if len(char_code_list) > 16:
            raise ValueError('Exceeds maximum length of characters for each line: 16')
        return char_code_list

    def display_one_line(self, line_no, message):
        message = self.normalize_message(message)
        char_code_list = self.convert_message(message)
        for code in char_code_list:
            self.send_command(code)

    def display_messages(self, message_list):
        self.initialize_display()
        time.sleep(SLEEP_TIME)
        for line_no, message in enumerate(message_list):
            if line_no == 1:
                self.send_linebreak()
            self.display_one_line(line_no, message)


def main():
    while 1:
        lcddisp = [] 
        s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
        s.connect(("8.8.8.8",80))
        ip = s.getsockname()[0]
        lcddisp.append("IP address:")
        lcddisp.append(ip)
        lcd_controller = LCDController()
        lcd_controller.display_messages(lcddisp[0:2])
        time.sleep(2)

        lcddisp = []
        dt_now = datetime.datetime.now()
        lcddisp.append(dt_now.strftime('DATE: %Y/%m/%d'))
        lcddisp.append(dt_now.strftime('TIME: %H:%M:%S'))
        lcd_controller = LCDController()
        lcd_controller.display_messages(lcddisp[0:2])
        time.sleep(2)

        lcddisp = []
        readData()
        lcdhum = ("HUM:%6.2f ％" % (var_h)) 
        lcdtemp = ("TEMP: %-6.2f'C" % (temperature)) 
        lcdpressure = ("pressure:%7.2fhPa" % (pressure/100))        
        lcddisp.append(lcdtemp)
        lcddisp.append(lcdhum)
        lcd_controller = LCDController()
        lcd_controller.display_messages(lcddisp[0:2])
        time.sleep(2)

        lcddisp = []
        memory = psutil.virtual_memory()
        cpu_percent = psutil.cpu_percent(interval=1)
        lcdmem = ('MEM: ' + str(memory.percent) + " %")
        lcdcpu = ('CPU: ' + str(cpu_percent) + " %")
        lcddisp.append(lcdcpu)
        lcddisp.append(lcdmem)
        lcd_controller = LCDController()
        lcd_controller.display_messages(lcddisp[0:2])
        time.sleep(2)

if __name__ == '__main__':
    main()
```

クソコード臭が拭えませんが普通に動くので気にしません。

#⑦ラズパイ起動後に自動起動させる
ヘッドレス運用時にラズパイ起動後に自動で起動してくれればIPアドレスとかとれるし正常起動したかわかるし便利そうなので設定してみました。
どうやらsystemdを設定すればいけるそうなので参考ページのとおりに設定したらあっさりいけました。
参考↓
https://qiita.com/molchiro/items/ee32a11b81fa1dc2fd8d
https://qiita.com/ikemura23/items/6f9adce99a3db555a0e4

#⑧動作の様子
こんな感じで動いてます
<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">最終的にラズパイ起動時に自動実行されて日付、時刻、温度、湿度、cpu使用率、メモリー使用率、ipアドレスが自動表示される謎表示機になりました。<br>ipアドレスやcpu使用率はヘッドレス運用時に便利かな <a href="https://t.co/qAPwQ4F8pQ">pic.twitter.com/qAPwQ4F8pQ</a></p>&mdash; しょうとくた (@syotokuta) <a href="https://twitter.com/syotokuta/status/1171065401616166912?ref_src=twsrc%5Etfw">September 9, 2019</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>