RaspberryPi
IoT
Ambient

室内タンクの灯油残量をIoTで監視する

超音波距離センサを用いて,灯油の残量を確認でき,少なくなったらアラートするシステムを作ります.

用いるもの

  • Raspberry Pi 3B: 始めた時は Raspberry Pi zero WH がまだ発売されていなかったため,3Bです.
  • 超音波距離センサ HC-SR04: 今回のメインのセンサ.超音波の送信から受信までの時間差から距離を求めるもの.
  • 温度センサ ADT7410: 音速は温度によって変わるため,距離センサの計測精度向上のために用いる.
  • LED, タクトスイッチ, 自励式ブザー: 完成度を上げるため.
  • ユニバーサル基板,ジャンパー線,抵抗,トランジスタなど: 必要に応じて.

金額は税込,購入当時のもの.

Raspberry Pi 3B

適当にセットアップします.
もしよろしければ参考にしてください: Qiita: y_k: 【ヘッドレス】Raspbery Pi 3 セットアップ for macOS

追加のセットアップ項目としては,python3のインストールです.aptでインストールしてください.

超音波距離センサ (HC-SR04)

仕様

  • 電源: DC 5.0V, 15mA
  • 測距範囲: 2cm〜400cm (分解能: 0.3cm), センサー基板正面を中心とした15度の範囲
  • 動作周波数: 40kHz

入出力

  • 入力: トリガー
    • High 10μsで計測を開始
  • 出力: エコー
    • 反射(往復)時間

仕組み

  1. トリガーに10μs以上信号入力
  2. センサーからパルス波を8回送信 -> センサー受信
  3. パルス波の往復時間をエコーから出力
  4. $\frac{\text{往復時間}}{2}$を音速で割ることで距離を求めることができる

音速

  • 海面: 340.29 m/s (Google)
  • 一次のテイラー展開近似式: $v = 331.5 + 0.6 t$ ($t$: 摂氏温度)
    • 気温 0℃: 331.5 m/s
    • 気温 30℃: 349.5 m/s

回路

hc-sr04_ブレッドボード.png

実際に距離を測る

Pythonでpigpioを用いてプログラムを作成します.

$ sudo apt install pigpio python3-pigpio
$ sudo pigpiod
meas_dist.py
#!/usr/bin/env python3

import pigpio
import time

HC_SR04_trig = 17
HC_SR04_echo = 27

pi = pigpio.pi()
pi.set_mode(HC_SR04_trig, pigpio.OUTPUT)
pi.set_mode(HC_SR04_echo, pigpio.INPUT)

try:
  while True:
    # Trig
    # 10μs信号出力
    pi.gpio_trigger(HC_SR04_trig, 10, 1)

    # Echo
    while pi.read(HC_SR04_echo) == 0:
      signaloff = time.time()
    while pi.read(HC_SR04_echo) == 1:
      signalon = time.time()

    # 往復にかかった時間
    timepassed = signalon - signaloff

    # 距離を計算 (cm)
    # 音速は 340m/s とする
    distance = timepassed * 340 * 100 / 2

    print(distance)

    time.sleep(1)
except KeyboardInterrupt:
  pass

pi.stop()

$ python3 meas_dist.py
12.68625259399414
10.793447494506836
10.761022567749023
13.444185256958008
13.51308822631836
13.59415054321289
13.48876953125
13.50092887878418
^C%

ctrl+cで止めれます.

Ambientを用いて可視化

Ambientに測定値をアップロードして,可視化を行います.
無料で利用できますので,右上の「ユーザー登録」から,ユーザ登録してください.

ログイン後は「Myチャネル」から,チャネルを作成します.
「チャネルを作る」をクリックするとチャネルが作成されます.

次に,Ambient: Pythonライブラリーを参考にライブラリをインストールし,スクリプトを作成します.

ライブラリのインストール
$ pip install git+https://github.com/AmbientDataInc/ambient-python-lib.git

実際に,ライブラリを使い,データを送信するときは以下のようにします.

import ambient

am = ambient.Ambient(チャネルId, ライトキー[, リードキー[, ユーザーキー]])
r = am.send({'d1': 数値, 'd2': 数値})

組み合わせると以下のようになります.

meas_dist_amb.py
#!/usr/bin/env python3

import pigpio
import time
import ambient

HC_SR04_trig = 17
HC_SR04_echo = 27

ambient_ch_id = チャネルId
ambient_write_key = "ライトキー"

pi = pigpio.pi()
pi.set_mode(HC_SR04_trig, pigpio.OUTPUT)
pi.set_mode(HC_SR04_echo, pigpio.INPUT)

am = ambient.Ambient(ambient_ch_id, )

try:
  while True:
    # Trig
    # 10μs信号出力
    pi.gpio_trigger(HC_SR04_trig, 10, 1)

    # Echo
    while pi.read(HC_SR04_echo) == 0:
      signaloff = time.time()
    while pi.read(HC_SR04_echo) == 1:
      signalon = time.time()

    # 往復にかかった時間
    timepassed = signalon - signaloff

    # 距離を計算 (cm)
    # 音速は 340m/s とする
    distance = timepassed * 340 * 100 / 2

    r = am.send({'d1': distance})
    print(r.status_code, distance)

    time.sleep(10)
except KeyboardInterrupt:
    pass

pi.stop()

温度センサ (ADT7410)

せっかくなら,温度も取得してより正確な音速を用いて,距離を求めることで精度を高めたいと思います.

仕様

  • 電圧: 2.7V〜5.5V
  • 電源消費電流:
    • 3.3V時: 210μA
      • パワーセービングモード時: 46μA (1サンプル/s)
      • シャットダウンモード時: 2μA
  • 動作・測定範囲: -55℃〜+150℃ (分解能: 0.0078℃)
  • 精度:
    • ±0.5℃ (-40℃〜+105℃; 2.7V〜3.6V)
    • ±0.4℃ (-40℃〜+105℃; 3.0V)
  • 入出力: VDD, GND, SCL, SDA
    • I2C 互換インターフェース

回路

ADT4710_ブレッドボード.png

実際に温度を測る

I2Cの設定

Raspberry PiのI2Cを有効にします.

# 設定画面を起動
$ sudo raspi-config

設定画面が出てきたら以下の通り操作します:

  1. 「5 Interfacing Options」を選択
  2. 「P5 I2C」を選択
  3. 「Would you like the ARM I2C interface to be enabled?」と出てくるので,「はい」を選択
  4. 「The ARM I2C interface is enabled」と出てくるので,「了解」を選択
  5. メニューに戻るので,「Finish」を選択

そうすると,I2Cが有効になるので,必要なライブラリをインストールします:

$ sudo apt install i2c-tools

スクリプトの作成

meas_temp.py
#!/usr/bin/env python3

import pigpio
import time

address_adt7410 = 0x48
register_adt7410 = 0x00

pi = pigpio.pi()
sensor = pi.i2c_open(1, address_adt7410)

try:
  while True:
    b, d = pi.i2c_read_i2c_block_data(sensor, register_adt7410, 2)
    d = ((d[0]<<8) + d[1])
    if d & 0x8000:
      print((d-65536)/128) #negative
    else:
      print(d/128) #positive
    time.sleep(1)
except KeyboardInterrupt:
    pass

pi.i2c_close(temp_sensor)
pi.stop()

温度で補正して距離を算出

実際に超音波距離センサ (HC-SR04)と組み合わせ,測った温度で音速を求め,距離を算出してみます.

meas_dist_temp.py
#!/usr/bin/env python3

import pigpio
import time

HC_SR04_trig = 17
HC_SR04_echo = 27

address_adt7410 = 0x48
register_adt7410 = 0x00

pi = pigpio.pi()
pi.set_mode(HC_SR04_trig, pigpio.OUTPUT)
pi.set_mode(HC_SR04_echo, pigpio.INPUT)
temp_sensor = pi.i2c_open(1, address_adt7410)

def temp():
  b, d = pi.i2c_read_i2c_block_data(temp_sensor, register_adt7410, 2)
  d = ((d[0]<<8) + d[1])
  if d & 0x8000:
    d = d - 65536 #negative
  return d/128

def sonic_speed(t=temp()):
    return 331.5 + 0.6 * t

try:
  while True:
    # Trig
    # 10μs信号出力
    pi.gpio_trigger(HC_SR04_trig, 10, 1)

    # Echo
    while pi.read(HC_SR04_echo) == 0:
      signaloff = time.time()
    while pi.read(HC_SR04_echo) == 1:
      signalon = time.time()

    # 往復にかかった時間
    timepassed = signalon - signaloff

    # 距離を計算 (cm)
    distance = timepassed * sonic_speed() * 100 / 2

    print(distance)

    time.sleep(1)
except KeyboardInterrupt:
    pass

pi.i2c_close(temp_sensor)
pi.stop()

タクトスイッチ,LED,ブザー

灯油給油中は,測定を中止できるように,タクトスイッチを取り付けます.
また,残量測定プログラムが動作中はLEDが点灯,休止中は点滅するように,休止が一定時間続けば注意を促すためのブザーを取り付けます.

回路

etc_ブレッドボード.png

タクトスイッチとLED

led.py
#!/usr/bin/env python3

import pigpio
import time

flag = False
LED_PIN = 10
SWI_PIN = 22

pi = pigpio.pi()
pi.set_mode(SWI_PIN, pigpio.INPUT)
pi.set_pull_up_down(SWI_PIN, pigpio.PUD_DOWN)

pi = pigpio.pi()
pi.set_mode(LED_PIN, pigpio.OUTPUT)

def cb_interrupt(gpio, level, tick):
    global flag
    print (gpio, level, tick)
    flag = not flag

cb = pi.callback(SWI_PIN, pigpio.FALLING_EDGE, cb_interrupt)

try:
    while True:
        if flag:
          pi.write(LED_PIN, 1)
        else:
          pi.write(LED_PIN, 0)
        time.sleep(0.3)
except KeyboardInterrupt:
    pass

pi.set_mode(LED_PIN, pigpio.INPUT)
pi.stop()

折角なので,もう一種類:

#!/usr/bin/env python3

import pigpio
import time

LED_PIN = 10
SWI_PIN = 22

pi = pigpio.pi()
pi.set_mode(SWI_PIN, pigpio.INPUT)
pi.set_pull_up_down(SWI_PIN, pigpio.PUD_DOWN)

pi.set_mode(LED_PIN, pigpio.OUTPUT)

def cb_interrupt(gpio, level, tick):
    global flag
    global pi
    print (gpio, level, tick)
    flag = (pi.read(LED_PIN) == 1)
    pi.write(LED_PIN, 0 if flag else 1)

cb = pi.callback(SWI_PIN, pigpio.FALLING_EDGE, cb_interrupt)

try:
    while True:
        time.sleep(0.3)
except KeyboardInterrupt:
    pass

pi.set_mode(LED_PIN, pigpio.INPUT)
pi.stop()

ブザーの動作確認

buzzer.py
#!/usr/bin/env python3

import pigpio
import time

PIN = 26

pi = pigpio.pi()
pi.set_mode(PIN, pigpio.OUTPUT)

for _ in range(3):
  pi.write(PIN, 1)
  time.sleep(0.3)
  pi.write(PIN, 0)
  time.sleep(0.3)

pi.set_mode(PIN, pigpio.INPUT)
pi.stop()

メイン

今までは,各パーツの使い方など,要素ごとの話でした.
ここからは,要素を組み合わせて実際にメインのスクリプトを書きます.

その前に,まずはタンクの中が空のときの値を取得します:

# 測定値を empty.dat に書き込む
# ある程度したら,`ctrl-c`でストップ
$ python3 meas_dist_temp.py > empty.dat
^C%
# データ数を確かめる
$ wc -l empty.dat
264 empty.dat
# 中央値を求める
$ sort -n empty.dat | awk ' { a[i++]=$1; } END { print a[int(i/2)]; }'
70.43320387601852

次に18L灯油を入れてから,値を取得します:

$ python3 meas_dist_temp.py > 18l.dat
^C%
$ wc -l 18l.dat
462 18l.dat
$ sort -n 18l.dat | awk ' { a[i++]=$1; } END { print a[int(i/2)]; }'
55.835982263088226

ということで,0Lは70.433cm,1Lで0.811cm変わると設定します1

main.py
#!/usr/bin/env python3

import pigpio
import time
import ambient
from statistics import mean

tank_empty = 70.433
tank_1l = 0.811

HC_SR04_trig = 17
HC_SR04_echo = 27

LED_PIN = 10
SWI_PIN = 22

BUZZER_PIN = 26

I2C_BUS = 1

address_adt7410 = 0x48
register_adt7410 = 0x00

ambient_ch_id = チャネルID
ambient_write_key = "ライトキー"

working = True
switch_stop_datetime = time.time()

am = ambient.Ambient(ambient_ch_id, ambient_write_key)

pi = pigpio.pi()
pi.set_mode(HC_SR04_trig, pigpio.OUTPUT)
pi.set_mode(HC_SR04_echo, pigpio.INPUT)
temp_sensor = pi.i2c_open(I2C_BUS, address_adt7410)

pi.set_mode(SWI_PIN, pigpio.INPUT)
pi.set_pull_up_down(SWI_PIN, pigpio.PUD_DOWN)

pi.set_mode(LED_PIN, pigpio.OUTPUT)
pi.set_mode(BUZZER_PIN, pigpio.OUTPUT)

def temp():
  b, d = pi.i2c_read_i2c_block_data(temp_sensor, register_adt7410, 2)
  d = ((d[0]<<8) + d[1])
  if d & 0x8000:
    d = d - 65536 #negative
  return d/128

def sonic_speed(t=temp()):
  return 331.5 + 0.6 * t

def to_buzz():
  for _ in range(3):
    pi.write(BUZZER_PIN, 1)
    time.sleep(0.3)
    pi.write(BUZZER_PIN, 0)
    time.sleep(0.3)

def meas():
  # Trig
  # 10μs信号出力
  pi.gpio_trigger(HC_SR04_trig, 10, 1)

  # Echo
  while pi.read(HC_SR04_echo) == 0:
    signaloff = time.time()
  while pi.read(HC_SR04_echo) == 1:
    signalon = time.time()

  # 往復にかかった時間
  timepassed = signalon - signaloff

  # 距離を計算 (cm)
  return timepassed * sonic_speed() * 100 / 2

def calc_cm2l(cm):
  return (tank_empty - cm)/tank_1l

def cb_interrupt(gpio, level, tick):
  global working
  global pi
  print (gpio, level, tick)
  working = not working
  pi.write(LED_PIN, 1 if working else 0)
  if not working:
    switch_stop_datetime = time.time()

cb = pi.callback(SWI_PIN, pigpio.FALLING_EDGE, cb_interrupt)

try:
  while True:
    if working:
      distances = [d for d in [meas() for _ in range(10)] if 2 <= d <= 400]
      if len(distances) == 0:
        print("計測エラー")
      else:
        distance = mean(distances)
        temperature = temp()
        print(distance, temperature)

        now = time.localtime()
        r = None
        while not r or not r.status_code == 200:
          r = am.send({
            'created': time.strftime('%Y-%m-%d %H:%M:%S', now),
            'd1': distance,
            'd2': temperature
          })
    else:
      if time.time() - switch_stop_datetime >= 300:
        to_buzz()

    time.sleep(10)
except KeyboardInterrupt:
    pass

pi.i2c_close(temp_sensor)
pi.set_mode(LED_PIN, pigpio.INPUT)
pi.set_mode(BUZZER_PIN, pigpio.INPUT)
pi.stop()

まとめ

完成したプログラム

Coming soon...

値段

Raspberry Pi 3B + ACアダプタ ¥5,292 1
Raspberry Pi 3B ケース ¥1069 1
超音波距離センサ HC-SR04 ¥400 1
温度センサ ADT7410 ¥500 1
自励式ブザー BUZ-1.5-9.5 ¥108 1
タクトスイッチ TVDP01-6.5 ¥39 1
LED L3G2530-12 ¥54 1
LED キャップ ¥54 1
トランジスタ 2SC1815 ¥16.2/個 1

参考


  1. いまいち,この距離センサの分解能の考え方がわからない…