LoginSignup
16
11

More than 1 year has passed since last update.

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

Last updated at Posted at 2018-06-12

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

用いるもの

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

まとめ

完成したプログラム

/home/yk/kerosene_tank_oil_gauge/start.sh
#!/bin/bash
sleep 30
screen -UmdS kerosene_tank_oil_gauge python3 /home/yk/kerosene_tank_oil_gauge/main.py
/home/yk/kerosene_tank_oil_gauge/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():
  signalon = time.time()
  signaloff = time.time()

  time.sleep(1)

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

if __name__ == '__main__':
  print("起動")

  cb = pi.callback(SWI_PIN, pigpio.FALLING_EDGE, cb_interrupt)
  print("setup finished callbacks")

  try:
    print("実行開始")
    while True:
      if working:
        distances = [d for d in [meas() for _ in range(1)] if 2 <= d <= 400]
        if len(distances) == 0:
          print("計測エラー")
        else:
          distance = mean(distances)
          temperature = temp()
          print(calc_cm2l(distance), 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': calc_cm2l(distance),
              'd2': distance,
              'd3': temperature
            })
      else:
        if time.time() - switch_stop_datetime >= 300:
          to_buzz()

      time.sleep(30)
  except KeyboardInterrupt:
      pass

  pi.i2c_close(temp_sensor)
  pi.set_mode(LED_PIN, pigpio.INPUT)
  pi.set_mode(BUZZER_PIN, pigpio.INPUT)
  pi.stop()
$ crontab -e
crontab
@reboot /home/pi/kerosene_tank_oil_gauge/start.sh

値段

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. いまいち,この距離センサの分解能の考え方がわからない… 

16
11
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
16
11