この記事は、Zennにも投稿しています。
はじめに
温度センサーの値をRaspberry Piで受け取り、データベースに保存して、ブラウザに表示する。IoTシステムを作ると、画面に値が出た時点で「動いた!」と安心してしまいがちです。
でも、画面に値が表示されることと、その値が正しいことは別の話です。センサーの値は、読み取り・通信・保存・計算・表示と何段階も経由して画面に届くので、どこかで値がずれていても、画面だけ見ていては気づけません。
この記事では、IoTシステムで「センサーの値が本当に正しいか」を確かめるための考え方と、具体的な方法をまとめます。
「表示される」と「正しい」は別のテスト
テストで確かめることは、大きく2つに分けられます。
| 種類 | 確かめること | 例:温度の平均値を表示する機能 |
|---|---|---|
| 文法的(構文的)なテスト | 処理がエラーなく動き、決まった形式で結果が出るか | 平均値が画面に表示されるか |
| 意味的なテスト | 出てきた結果が、正しい値か | 表示された平均値が、本当に正しい平均か |
「画面に平均値が出た」ことを目で見て確認するのは、文法的なテストです。平均値の計算式が間違っていても、画面には何かしらの数字が出るので、このテストは通ってしまいます。
意味的なテストをするには、「正解の値」を用意して、それと比べる必要があります。ところがIoTでは、この正解が用意しにくいのが難しいところです。
- 正解は現実の世界(部屋の本当の温度)にあり、プログラムの中にはない
- センサー自体に誤差がある
- 値が届くまでに、通信・保存・計算など多くの段階を経由する
そこで、段階ごとに正解を用意して確かめていきます。
センサー値の正しさを確かめる5つの方法
1. 決まった値を流して、出口の値と比べる
まずは、センサーを使わずに決まった値(定数)を流して、データベースや画面に同じ値が届くかを確かめます。センサーの値は毎回変わるので比べにくいですが、自分で決めた値なら「正解」がわかっています。
import sqlite3
def save_reading(conn, temp, humid):
conn.execute("INSERT INTO readings (temp, humid) VALUES (?, ?)", (temp, humid))
conn.commit()
def test_保存した値がそのまま取り出せる():
conn = sqlite3.connect(":memory:")
conn.execute("CREATE TABLE readings (temp REAL, humid REAL)")
# センサーの代わりに、決まった値を流す
save_reading(conn, 25.5, 60.0)
temp, humid = conn.execute("SELECT temp, humid FROM readings").fetchone()
assert (temp, humid) == (25.5, 60.0)
この方法で、通信・保存・表示の経路で値が変わっていないことを確かめられます。例えば、「温度と湿度が入れ替わって保存されていた」「文字列を数値に変換するときに小数点以下が切り捨てられていた」といったバグは、ここで見つかります。
2. 計算した値は、手計算の結果と比べる
平均値や最大値など、プログラムで計算した値は、手で計算した答えと比べます。
import pytest
def average(values):
if not values:
return None # データがないときの扱いを決めておく
return sum(values) / len(values)
def test_平均値が正しい():
# (20.0 + 22.0 + 24.0) / 3 = 22.0 を手で計算しておく
assert average([20.0, 22.0, 24.0]) == pytest.approx(22.0)
def test_データがないとき():
assert average([]) is None
ポイントは、データが0件のときのような境界のケースも一緒に確かめることです。0件で平均を計算すると、ゼロ除算でエラーになります。
小数の計算には誤差が出ることがあるので、== で比べるのではなく、pytest.approx のように誤差を許して比べます。
3. 基準になる計器と並べて比べる
センサーの値そのものが正しいかは、市販の温度計・湿度計をセンサーの横に置いて比べます。
このとき、「ぴったり同じ値になるか」ではなく、センサーの仕様にある精度の範囲に収まっているかで判断します。例えば、よく使われる温湿度センサーのDHT11は、精度が温度±2℃、湿度±5%RH程度とされています。温度計が25℃のとき、センサーが26℃を示していても、仕様の範囲内なので異常ではありません。
逆に、精度の範囲を大きく外れている場合は、センサーの故障や配線の問題、センサーの近くに熱を出す部品がある、などの原因が考えられます。
4. 環境をわざと変えて、値が反応するかを見る
値が「それらしく見える」だけで、実は固定値や古い値を表示し続けている、ということもあります。そこで、環境をわざと変えて、値がきちんと反応するかを確かめます。
| 確かめたいこと | やり方 |
|---|---|
| 温度 | センサーを手で包んで温める |
| 湿度 | センサーに息を吹きかける、濡れた布を近くに置く |
見るポイントは、値が正しい向きに変わるか(温めたら上がるか)と、どれくらいの時間で画面に反映されるかです。反映までの時間がわかれば、「危険な温度になってから、何分後に通知が届くか」も説明できるようになります。
5. センサーの仕様と照らし合わせる
センサーのデータシート(仕様書)には、分解能(どれくらい細かい単位で値を出せるか)と測定範囲が書かれています。表示されている値が、これと矛盾していないかを確かめます。
例えば、DHT11の分解能は1℃・1%RHです。それなのに画面に「25.37℃」と表示されていたら、その細かい桁はセンサーが出したものではありません。平均を計算したときや、型を変換したときに生まれた桁です。
この場合は、表示する桁数をセンサーの分解能に合わせるほうが、利用者に誤解を与えません。
round(average_temp) # 分解能が1℃なら、整数で表示する
また、測定範囲(DHT11なら温度0〜50℃)を外れた値が出たら、それは「とても暑い」のではなく、読み取りの失敗や故障かもしれません。範囲外の値は、通常のデータとは分けて扱うようにしておくと安全です。
値以外にも見落としがちなポイント
時刻は正しいか
センサーの値には、たいてい「いつ測ったか」の時刻を一緒に保存します。ところが、Raspberry Piの多くのモデルには、電源が切れている間も時刻を保つ時計(RTC)が付いていません。ネットワークにつながらない状態で起動すると、時刻がずれたまま記録されることがあります。
時刻がインターネット経由で正しく合わせられているかは、次のコマンドで確認できます。
timedatectl
# System clock synchronized: yes なら同期できている
ネットワークがない場所で使う場合は、RTCモジュールを追加することも検討しましょう。
同時に書き込んでも壊れないか
複数の処理(センサーからの受信と、ブラウザからの入力など)が同じファイルに書き込む場合、タイミングによってはデータが混ざったり、壊れたりすることがあります。書き込む間は**ロック(排他制御)**をかけて、同時に書き込まないようにします。
import csv
import fcntl
def append_row(path, row):
with open(path, "a", newline="") as f:
fcntl.flock(f, fcntl.LOCK_EX) # 書き込みが終わるまで、他の処理を待たせる
csv.writer(f).writerow(row)
f.flush()
fcntl.flock(f, fcntl.LOCK_UN)
fcntl はLinuxやmacOSで使えるモジュールです(Windowsでは使えません)。
入力がない・おかしいときにどうなるか
ブラウザのフォームから値を登録できる場合は、何も入力せずに送信したときや、数値の欄に文字を入れたときの動きも確かめます。正常な値だけで確認していると、こうしたケースで処理が止まってしまうことに気づけません。
ログをファイルに残す
print でターミナルに出力するだけだと、ターミナルを閉じたら記録が消えてしまいます。長い時間動かし続けるIoTシステムでは、ログをファイルに残し、通常のログとエラーのログを分けておくと、あとから「いつ、何が起きたか」を追いやすくなります。
import logging
logger = logging.getLogger("sensor")
logger.setLevel(logging.INFO)
info_handler = logging.FileHandler("sensor.log") # すべてのログ
error_handler = logging.FileHandler("sensor_error.log") # エラーだけ
error_handler.setLevel(logging.ERROR)
formatter = logging.Formatter("%(asctime)s %(levelname)s %(message)s")
for handler in (info_handler, error_handler):
handler.setFormatter(formatter)
logger.addHandler(handler)
logger.info("温度: 25.0℃ 湿度: 60%")
logger.error("センサーの読み取りに失敗しました")
まとめ
IoTシステムのテストでは、「値が表示されるか」だけでなく、「その値が正しいか」まで確かめることが大切です。確かめる内容をチェックリストにまとめます。
| 確かめること | 方法 |
|---|---|
| 経路で値が変わっていないか | 決まった値を流して、保存・表示された値と比べる |
| 計算が正しいか | 手計算の答えと比べる。データが0件のときも確かめる |
| センサーの値が正しいか | 市販の計器と並べて、精度の範囲に収まるかを見る |
| 値がきちんと反応するか | 温める・湿らせるなど環境を変え、変化の向きと反映までの時間を見る |
| 仕様と矛盾していないか | 分解能より細かい桁が出ていないか、測定範囲を外れていないか |
| 時刻・同時書き込み・異常な入力 | 時刻の同期、排他制御、空の入力のときの動きを確かめる |
「画面に出たからOK」から一歩進んで、「この値は正しいと説明できる」状態を目指してみてください。