「えだまめ」しているラズパイ

30年ぶりに半田ごて握ってラズパイ勉強中。

IoT学習HATをラズパイTrixieで使う

◯やりたいこと

その昔色々とお世話になった秋月電子のIoT学習HATキット。今回BME280とモーションセンサーをラズパイ上で同時に使う機会が生じたため久し振りに引っ張り出して来て最新版OSのTorixieで動作させてみる事にしました。


◯やったこと

秋月電子で販売されている学習キット、特徴は
・8文字x2行のLCD(バックライト付き:AQM0802A-FLW-GBWを実装
・3色の抵抗内蔵LED(赤・黄・緑)、Φ9mmのブザーを実装
・焦電型赤外線(人感)センサー搭載用3Pソケットを実装
・BME280(温度・湿度・気圧センサー)搭載用6Pソケット実装
・シャットダウン用スイッチ実装(黒色:別途スクリプト使用)
・自由に使えるトリガ用スイッチ実装(白色)
(HPから)

と比較的お安いのに多機能。このうち今回はモーションセンサーとBME280の機能を主に使っていきます。

・問題点&解決方法

実を言うとこのキットにはちょっとした問題点があります。LCDのドライブ用にI2C-SDAへ専用の回路が組み込まれているためBME280を基板上にあるi2cソケットに接続するとi2c通信が全滅してしまいます。最初は目が点になりました。
ただこのエラーへの対策は比較的簡単。i2cクロックをマニュアルに書いてある50KHzに下げ、BME280にはソケットに来ているSDAでなくラズパイのSDAを直接つなげば普通に通信が復活します。

・改造

ソケットに来ているSDAを使わないとはいえi2c端子を外部に引き出すには6pinモジュール用の基板ランドを利用するのがやはり1番簡単。そこでソケット部分を改修しgroveソケットをムリクリ取り付けてそこにBME280を接続する事にします。
6pin端子には上手い具合にN.CがありますのでそこへハジにあるSCLをジャンパし、SDAに来ているパターンはカットしてトランジスタ(2SA1015)のエミッタにジャンパ、あとはgroveソケットを足を広げて取り付ければ改造完了です。


・チェック

ラズパイにターミナルで接続し i2cdetect コマンドを実行して

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

のようにLCDとBME280のアドレスが表示されれば成功です。

・gpioをTrixieで使う

OSがbookwormになってからgipoを直接制御するpigpioが非推奨になりました。推奨のlgpio,gpiozeroを使った場合gpioを複数のプロセス・スレッドから呼び出すとbusyエラーが出てえらく苦労したのですが、最終的にはgpioの資源を取得するプロセスを常駐させLEDやSWなどのpin制御をモジュール化しそれをインポートして使っています。
確保したgpioのデバイス番号、インポートしたモジュールのインスタンスなどはglobal()を使ってデータを共有、スタートすると常駐プロセスがデバイス資源を握って離さずそれを子のモジュールが必要部分を借りて使い、親が資源を離す(close)するのはシステム停止時のみという形に落ち着きました。これはi2cやSPI、赤外線制御なども同様に行なっています。

では具体的にどうやっているんだ?とここでコードを示せればいいのですが、「えだまめ」はここ半年でver2にバージョンアップしMQTTに完全移行したためコードがあまり一般的ではありません。asyncioなどの非同期関数も多用しているのでどうも公開には向かない感じです。とはいえ何も示さないのも何ですので使用中のBMP280とAHT20用のモジュールをあげておきます。AIに作ってもらったものをBugFixして使ってます。

・BMP280

class IoFncBMP280:
	def __init__(self, bus_number, i2c_addr=0x76):
		self.bus_num = int(bus_number)
		self.addr = i2c_addr
		self.bus = None
		self.temperature = 0.0
		self.pressure = 0.0
		
		# 補正係数保持用
		self.calib_T = []
		self.calib_P = []

	def __enter__(self):
		self.bus = smbus2.SMBus(self.bus_num)
		self._load_calibration()	# 起動時に補正係数を読み込む
		return self

	def __exit__(self, exc_type, exc_val, exc_tb):
		if self.bus is not None:
			self.bus.close()

	#------------------------------------------------------
	# センサー動作状況 チェック
	#------------------------------------------------------
	def check_id(self):
		try:
			# BMP280のチップIDレジスタは 0xD0 (値は 0x58)
			chip_id = self.bus.read_byte_data(self.addr, 0xD0)
			return chip_id == 0x58
		except Exception:
			return False

	# BMP280固有の補正係数をレジスタから読み出す
	def _load_calibration(self):
		try:
			# 温度用 (dig_T1 ~ dig_T3) -> 6バイト
			b1 = self.bus.read_i2c_block_data(self.addr, 0x88, 6)
			self.calib_T.append((b1[1] << 8) | b1[0]) # dig_T1 (unsigned)
			
			for i in range(1, 3): # dig_T2, dig_T3 (signed)
				val = (b1[i*2+1] << 8) | b1[i*2]
				if val & 0x8000: val -= 65536
				self.calib_T.append(val)

			# 気圧用 (dig_P1 ~ dig_P9) -> 18バイト
			b2 = self.bus.read_i2c_block_data(self.addr, 0x8E, 18)
			self.calib_P.append((b2[1] << 8) | b2[0]) # dig_P1 (unsigned)
			
			for i in range(1, 9): # dig_P2 ~ dig_P9 (signed)
				val = (b2[i*2+1] << 8) | b2[i*2]
				if val & 0x8000: val -= 65536
				self.calib_P.append(val)
		except Exception as e:
			raise Exception(f"補正係数の読み込みに失敗: {e}")

	#------------------------------------------------------
	# 測定値 取得
	#------------------------------------------------------
	def forced(self):
		# ctrl_meas (0xF4): 湿度なし、温度x1, 気圧x1, Forcedモード(0x01) -> 0x29
		# config (0xF5): フィルターなし -> 0x00
		self.bus.write_byte_data(self.addr, 0xF5, 0x00)
		self.bus.write_byte_data(self.addr, 0xF4, 0x29)

		# 測定完了待ち (Forcedモードは測定後自動でスリープに戻る)
		for _ in range(10):
			sleep(0.01)
			status = self.bus.read_byte_data(self.addr, 0xF3)
			if (status & 0x08) == 0:	# measuring bit が 0 で完了
				break
		else:
			raise Exception("BMP280 測定タイムアウト")

		# データ読み出し (0xF7 から 6バイト [Pressure x3, Temperature x3])
		data = self.bus.read_i2c_block_data(self.addr, 0xF7, 6)
		
		raw_pres = (data[0] << 12) | (data[1] << 4) | (data[2] >> 4)
		raw_temp = (data[3] << 12) | (data[4] << 4) | (data[5] >> 4)

		# --- 温度計算 (データシート準拠) ---
		v1 = (raw_temp / 16384.0 - self.calib_T[0] / 1024.0) * self.calib_T[1]
		v2 = ((raw_temp / 131072.0 - self.calib_T[0] / 8192.0) ** 2) * self.calib_T[2]
		t_fine = v1 + v2
		self.temperature = t_fine / 5120.0

		# --- 気圧計算 (データシート準拠) ---
		v1 = (t_fine / 2.0) - 64000.0
		v2 = v1 * v1 * self.calib_P[5] / 32768.0
		v2 = v2 + v1 * self.calib_P[4] * 2.0
		v2 = (v2 / 4.0) + (self.calib_P[3] * 65536.0)
		v1 = (self.calib_P[2] * v1 * v1 / 51200.0 + self.calib_P[1] * v1) / 524288.0
		v1 = (1.0 + v1 / 32768.0) * self.calib_P[0]

		if v1 == 0.0:
			self.pressure = 0.0
		else:
			p = 1048576.0 - raw_pres
			p = (p - (v2 / 4096.0)) * 6250.0 / v1
			v1 = self.calib_P[8] * p * p / 2147483648.0
			v2 = p * self.calib_P[7] / 32768.0
			self.pressure = (p + (v1 + v2 + self.calib_P[6]) / 16.0) / 100.0	# hPa変換

・AHT20

class IoFncAHT20:
	def __init__(self, bus_number, i2c_addr=0x38):
		self.bus_num = int(bus_number)
		self.addr = i2c_addr
		self.bus = None
		self.temperature = 0.0
		self.humidity = 0.0

	def __enter__(self):
		self.bus = smbus2.SMBus(self.bus_num)
		return self

	def __exit__(self, exc_type, exc_val, exc_tb):
		if self.bus is not None:
			self.bus.close()

	#------------------------------------------------------
	# センサー動作状況 チェック
	#------------------------------------------------------
	def check_id(self):
		# AHT20 初期化状態チェック (0x71コマンド)
		try:
			self.bus.write_byte(self.addr, 0x71)
			sleep(0.01)
			status = self.bus.read_byte(self.addr)
			# 状態ビット確認
			if (status & 0x18) == 0x18:
				return True
			return False
		except Exception:
			return False

	#------------------------------------------------------
	# 測定値 取得
	#------------------------------------------------------
	def forced(self):
		# 初期化状態チェック。未初期化なら 0xBE コマンドを送信
		if not self.check_id():
			self.bus.write_i2c_block_data(self.addr, 0xBE, [0x08, 0x00])
			sleep(0.01)

		# 1. 測定開始コマンド (0xAC, 0x33, 0x00) を送信
		self.bus.write_i2c_block_data(self.addr, 0xAC, [0x33, 0x00])
		
		# 2. 安全のために 100ms (0.1秒) しっかり待つ (ビジーチェック用のreadは挟まない)
		sleep(0.1)

		# 3. アドレス指定なしで 6バイト を一気に読み出す
		msg = smbus2.i2c_msg.read(self.addr, 6)
		self.bus.i2c_rdwr(msg)
		data = list(msg)

		# 4. ステータスバイト(先頭の1バイト目)のビジーフラグを念のため確認
		# ※もしそれでもビジーなら例外を投げる
		if (data[0] & 0x80) != 0:
			raise Exception("AHT20 が測定中(ビジー)のままです")

		# 5. 20bitの raw データから物理値へ変換
		raw_hum = ((data[1] << 12) | (data[2] << 4) | (data[3] >> 4))
		raw_tmp = (((data[3] & 0x0F) << 16) | (data[4] << 8) | data[5])

		self.humidity = (raw_hum / 1048576.0) * 100.0
		self.temperature = ((raw_tmp / 1048576.0) * 200.0) - 50.0

◯やってみて

今回はある部屋の室温を計りながら部屋の使用状況を測定する目的で使用しました。ESP8266でやってもよかったのですが電池交換が難しかったためラズパイで常時通電運転でデータを収集しました。LCDにはとりあえず日時を表示しておきLEDは点滅と人感検知表示をさせています。

我が家のえだまめネットワークもここ半年かけてやっとMQTT(Mosqutto)ベースのVer2に移行しました。この半年間はすっかりソフトざんまい。ボチボチハードの方も手をつけていこうかなと考えてます。

◯「学習用」別の意味での...

このキットは名前の通りあくまでも学習用キットなので本格的な連続運用に向いてはいません。現に現状のままでは数日程度でハングアップする事が多く無対策では少々信用のないデバイスです。
モーションセンサーなども自己ノイズで感知してしまう事が多くありパスコンは必須です。

長時間運転をさせるべく色々と対策を取る訓練ができるという意味でもある意味「学習用」なのかもしれません。笑