ラズパイZEROとモーションセンサーで見守りシステムをつくる(その7 ノイズの除去・他) | そう備忘録

ラズパイZEROとモーションセンサーで見守りシステムをつくる(その7 ノイズの除去・他)

見守りシステム

ラズパイゼロとモーションセンサーで実家の両親宅の見守りシステムを作った時の7回目。

今回は外箱を作り直したらモーション検知(HC-SR501)の信号にノイズが乗るようようになったしまったので対応した時の備忘録。

何故か正確に2分毎にモーション検知の信号が検知されて実際の検知回数より1時間辺り30回多くカウントされるようになってしまったので対応した。

過去記事

念の為、過去の記事の一覧を載せておく。

  • 初回の「全体構成とハードウェア編」はこちら
  • 2回目の「事前設定編」はこちら
  • 3回目の「AWS IoT CoreとDynamoDBの設定」はこちら
  • 4回目の「プログラムの説明」はこちら
  • 5回目の「systmdや遠隔操作編」についてはこちら
  • 6回目の「外箱の作成編」についてはこちら

全体構成図

尚、このシステムの全体構成図は以下の通り。

見守りシステム全体構成図

ノイズ対策について

掲示板でのやりとり

モーションセンサー(HC-SR501)の誤検知について調べていたら海外の掲示板でのやり取りを見つけた。

その掲示板でやり取りされていた内容は概ね以下の通り(意訳している)

  • Wi-Fiモジュールの2分毎の信号を拾ってしまっているのではないか(Wi-Fiをオフにすると誤検知が止まる)
  • 対応案その1、ラズパイとセンサーとの距離を離す
  • 対応案その2、抵抗を使ってノイズ除去する
  • 対応案その3、金属ボックスにいれるかアルミホイルでシールドする
  • 対応案その4、5V供給を3.3V供給(ジャンパー側に接続)に変更する
  • 対応案その5、フェライトコアを使ってノイズを除去する

複数の対応策が掲示板でやりとりされていたがケース内でラズパイとモーションセンサーの距離を離すことが困難だったのでアルミホイルでラズパイゼロとモーションセンサーをシールドしたのだがうまく行かなかった。

フェライトコア

結局、今回はフェライトコアを使ってノイズを除去した。

フェライトコアはフェライトという、酸化鉄を主成分とするセラミックスで磁力を帯びている。

リング状になっておりリード線の周りに装着する事により磁力の力で主に高周波ノイズを除去するらしい。

正直、なぜ磁力でノイズが除去できるのか具体的な仕組みは理解していないが掲示板に載っていた事もあったのでφ3mm×10個入りフェライトコアを購入してみた。

装着してみた

人に聞いた所、ノイズは主に電源に乗るのでまずはそこに装着して駄目ならGPIOに接続しているリード線に接続してみたらとの事だった。

接続図

装着の仕方は簡単で爪を外すと2つに分かれるので中央のくぼみにリード線を入れてパチンとはめるだけ。

結論から言うと5Vの電源供給に接続しても効果が無く、ノイズを拾ってしまった。

しかしGPIO側に接続するとノイズは無くなった(素晴らしい!)

電源供給側のフェライトコアは今回は不要とは思ったが念の為、そのままにして残しておくことにした。

フェライトコア装着図

外箱の変更

以前の記事では見守りシステムの外箱はMDF材で製作していた。

しかし今回ダイソーで購入した緊急小物ケースとリメイクシートで作り直している。

素材の違いかラズパイとモーションセンサー配置の違いかは良く分からないがこの構成にした途端に信号にノイズがのるようになってしまった。

ダイソーの化粧箱とリメイクシート

プログラムの変更

またプログラムも以前の記事と比較して若干修正を加えている。

以前のプログラムでは0.5秒間隔でGPIOの値をチェックしてLOWからHIGHに変わった瞬間を検知していたが、add_event_detectでGPIO.RISINGを指定すれば同様の事を検知できると知ったのでロジックを若干変更している。

"""
Created on Sun Dec 22 10:13:04 2019

@author: Souichirou Kikuchi

2020-10-20 add_event_detectでGPIO.RISINGを検知するように変更

"""

import paho.mqtt.client as mqtt # MQTT
import ssl
import json
import datetime as dt
from time import sleep
import sys # ErrorLog出力用
sys.path.append('/home/login名/.local/lib/python3.7/site-packages') # Pathを明示的に指定
import RPi.GPIO as GPIO
import gspread # Google SpreadSheet
from oauth2client.service_account import ServiceAccountCredentials # GoogleSheet認証用
import os # シャットダウン等
import pandas as pd # 検知件数保存CSV作成に使用する
import requests # LINEメッセージ

INITIAL_FILE= './initial.json' # 初期設定ファイル
TOPIC1 = 'dt' # AWS Publish時のTOPICS 1~5で構成される。データを表す
TOPIC2 = '/WatchOver' # アプリケーション名
TOPIC3 = '/own' # context
TOPIC5 = '/count' # カウント TOPIC4はself.client_idがセットされる
MOTION_PIN = 23 # モーションセンサーのGPIOピン番号
PUBLISH_INTERVAL = 10 # この分数毎にpublishを繰り返す
CHECK_INTERVAL = 0.5 # この秒数毎にモーションセンサーをチェックする
COUNT_CSV  = './csv/count_data.csv' # 検知件数はCSVで30日分保存する
ERROR_LOG  = './log/error.log' # エラーログ
LINE_URL = 'https://notify-api.line.me/api/notify' # LINEメッセージ用URL

class WOClass: # Watch Over(見守り) Class
    def __init__(self): # コンストラクタ
        with open(INITIAL_FILE) as f: # 初期設定ファイルの読み込み
            jsn = json.load(f)
            self.client_id = jsn['client_id'] # 端末ID
            __awsport = jsn['awsport'] # AWS接続用ポート番号
            __awscert = jsn['awscert'] # AWS認証用
            __awskey = jsn['awskey'] # AWSプライベートキー
            __awsroot_ca = jsn['awsroot_ca'] # AmazonRootCA
            __awsend_point = jsn['awsend_point'] # AWSエンドポイント
            self.gskey_name = jsn['gskey_name'] # 設定用のGoogleSheet
            self.gssheet_name = jsn['gssheet_name'] # GoogleSheetのName
            self.token = jsn['line_token'] # LINE用tokenの読み込み
        # 初期値の設定
        self.client = mqtt.Client(self.client_id, protocol=mqtt.MQTTv311) #MQTT初期化
        self.client.tls_set(ca_certs=__awsroot_ca, # TLS通信のセット
                            certfile=__awscert,
                            keyfile=__awskey,
                            cert_reqs=ssl.CERT_REQUIRED,
                            tls_version=ssl.PROTOCOL_TLSv1_2,
                            ciphers=None)
        self.client.connect(__awsend_point, port=__awsport, keepalive=60) #AWS IoTに接続
        self.client.loop_start() # ループスタート

    def __del__(self):
        self.client.disconnect() # 停止時にdisconnect

    def count_publish(self, from_now, now, find_count): # AWS IoT CoreにPublishすると当時にCSVに保存する
        # メッセージを組み立ててAWS IoT CoreにPublish
        try:
            __message = {'ClientID':self.client_id, # Client ID
                         'CountTimeFrom':'{0:%Y-%m-%d %H:%M:%S}'.format(from_now), # カウント開始日付時刻
                         'CountTimeTo':'{0:%Y-%m-%d %H:%M:%S}'.format(now), # カウント終了日付時刻
                         'Count':find_count} # 検知件数
            self.client.publish(TOPIC1+TOPIC2+TOPIC3+'/'+self.client_id+TOPIC5, json.dumps(__message)) # AWS IoTに送信(Publish)
            # Publishした検知件数をCSVに書き込み
            if (os.path.isfile(COUNT_CSV)): # ファイルが存在しているとき
                __df = pd.read_csv(COUNT_CSV, header=0, encoding='UTF-8') # 読み込み 0行目がヘッダー(有り)
            else: # ファイルが無ければヘッダーを作成
                __df = pd.DataFrame(columns=['CountTimeFrom', 'CountTimeTo', 'Count'])
            __count = pd.Series( ['{0:%Y-%m-%d %H:%M:%S}'.format(from_now),
                                  '{0:%Y-%m-%d %H:%M:%S}'.format(now),
                                  find_count], index=__df.columns)
            __df = __df.append(__count, ignore_index=True ) # dataframeを作成して行追加、ignore_indexで新たな行番号を振っている
            __df.to_csv(COUNT_CSV, index=False) # CSV書き込み indexは書き込まない
        except: # 例外時
            ex, ms, tb = sys.exc_info()
            self.put_error_log(type(ms)) # エラーログ処理

    def get_rule(self): # GoogleSheetよりルール(各種条件)を取得する
        results = []
        try:
            __scope = ['https://spreadsheets.google.com/feeds','https://www.googleapis.com/auth/drive']
            __credentials = ServiceAccountCredentials.from_json_keyfile_name(self.gskey_name, __scope)
            __gc = gspread.authorize(__credentials)
            __wks = __gc.open(self.gssheet_name).sheet1 # 該当シートをオープン
            __records = __wks.get_all_values() # __recordsに全ての値を保存する
            __row_count = len(__records) # 行数を取得
            self.exp_flg = 0
            for i in range(1, __row_count): # 最下行まで繰り返す
                if __records[i][0]  == self.client_id: # ClientIDが一致する行の値を取り出す
                    result = {
                            'Action': __records[i][1], # アクション
                            'Parameter': __records[i][2] # パラメータ
                            }
                    results.append(result)
                    if (__records[i][1] == 'EXP' and # EXPで当日が指定されていたらアラートフラグをオン
                        __records[i][2] == '{0:%Y-%m-%d}'.format(dt.datetime.now())):
                        self.exp_flg = 1
        except: # 例外時
            ex, ms, tb = sys.exc_info()
            self.put_error_log(type(ms)) # エラーログ処理
        finally:
            return results

    def put_report(self): # 直近24時間のレポートを1時間毎に集計してLINEに送付する
        try:
            if (os.path.isfile(COUNT_CSV)): # ファイルが存在しているとき
                __df = pd.read_csv(COUNT_CSV, header=0, encoding='UTF-8') # 読み込み 0行目がヘッダー(有り)
                __df['Hour'] = __df['CountTimeFrom'].apply(lambda x: x[0:13]) # YYYY-MM-DD HHまでを抽出 
                __df_grouped = __df.groupby(['Hour'], as_index=False)['Count'].sum() # Hourをindexにしない
                yesterday = (dt.datetime.now() + dt.timedelta(hours=-24)).strftime('%Y-%m-%d %H') # 24時間前
                __df_grouped = __df_grouped[__df_grouped['Hour'] >= yesterday] # 24時間以内のみを抽出
                # LINE用のメッセージを組み立てる
                __headers = {'Authorization' : 'Bearer ' + self.token}
                __message = '\n24時間以内の検知件数一覧\n'
                for i in range(__df_grouped.shape[0]):
                    __message = __message + __df_grouped.iat[i, 0] + '時台は ' + str(__df_grouped.iat[i, 1]) + '件\n'
                    # __message = __message + __df_grouped.iat[i, 0] + '時台は ' + str(int(__df_grouped.iat[i, 1])-30) + '件\n' # Wi-Fiが反応してしまう対応(暫定)
                __payload = {'message' : __message}
                requests.post(LINE_URL, headers=__headers, params=__payload) # LINEメッセージを送信
                # CSVから30日以内のデータのみを残す
                delete_day = (dt.datetime.now() + dt.timedelta(days=-30)).strftime('%Y-%m-%d %H') # 30日前
                __df = __df[__df['Hour'] >= delete_day] # 30日以内を抽出
                __df = __df.drop(columns='Hour') # Hour列を削除
                __df.to_csv(COUNT_CSV, index=False) # 保存する
        except: # 例外時
            ex, ms, tb = sys.exc_info()
            self.put_error_log(type(ms)) # エラーログ処理

    def put_alert(self, now, from_time, to_time): # From Toの範囲内の時刻で検知件数がゼロの時アラートを送信する
        try:
            if self.exp_flg == 0: # EXPで当日が指定されていたらアラートは出力しない
                if (os.path.isfile(COUNT_CSV)): # ファイルが存在しているとき
                    __df = pd.read_csv(COUNT_CSV, header=0, encoding='UTF-8') # 読み込み 0行目がヘッダー(有り)
                    __df['Day'] = __df['CountTimeFrom'].apply(lambda x: x[0:10]) # YYYY-MM-DDまでを抽出 
                    __df['FromHour'] = __df['CountTimeFrom'].apply(lambda x: x[11:16]) # FromのHH:MMを抽出 
                    __df['ToHour'] = __df['CountTimeTo'].apply(lambda x: x[11:16]) # ToのHH:MMを抽出 
                    __df = __df[__df['Day'] >= '{0:%Y-%m-%d}'.format(now)] # 今日のデータのみ
                    __df = __df[__df['FromHour'] >= from_time] # FromとToが範囲内
                    __df = __df[__df['ToHour'] <= to_time]
                    __df_grouped = __df.groupby(['Day'], as_index=False)['Count'].sum() # 日付でグルーピング(基本的に1件のみになる)
                    __total = 0
                    for i in range(__df_grouped.shape[0]):
                        __total = __total + __df_grouped.iat[i, 1]
                    if __total == 0: # 検知件数がゼロ
                        # LINE用のメッセージを組み立てる
                        __headers = {'Authorization' : 'Bearer ' + self.token}
                        __message = '\n***注意!!検知件数がゼロ***\n' + from_time + ' ~ ' + to_time + ' の間で検知件数がゼロでした'
                        __payload = {'message' : __message}
                        requests.post(LINE_URL, headers=__headers, params=__payload) # LINEメッセージを送信
        except: # 例外時
            ex, ms, tb = sys.exc_info()
            self.put_error_log(type(ms)) # エラーログ処理

    def put_error_log(self, message): # エラーログファイルを出力する
        if (os.path.isfile(ERROR_LOG)): # ファイルが存在しているとき
            __df = pd.read_csv(ERROR_LOG, header=0, encoding='UTF-8') # 読み込み 0行目がヘッダー(有り)
        else: # ファイルが無ければヘッダーを作成
            __df = pd.DataFrame(columns=['ErrorData', 'ErrorMessage'])
        __errorlog = pd.Series( ['{0:%Y-%m-%d %H:%M:%S.%f}'.format(dt.datetime.now()), message], index=__df.columns)
        __df = __df.append(__errorlog, ignore_index=True ) # dataframeを作成して行追加、ignore_indexで新たな行番号を振っている
        __df.to_csv(ERROR_LOG, index=False) # Log書き込み indexは書き込まない
        
def countup_find(channel): # 検出件数をカウントアップする
    global find_gl_count # Global変数 検出件数
    if channel == MOTION_PIN:
        find_gl_count += 1 # 検知回数のカウントアップ

GPIO.setmode(GPIO.BCM) # ピンをGPIOの番号で指定
GPIO.setup(MOTION_PIN, GPIO.IN, pull_up_down=GPIO.PUD_DOWN) # GPIOセットアップ
GPIO.add_event_detect(MOTION_PIN, GPIO.RISING, callback=countup_find, bouncetime=200) # GPIOがRIGINB(LOW→HIGH)の時にcountup_findを呼び出して200ミリ秒は間隔をあける


#main
try:
    if __name__ == '__main__':
        os.chdir(os.path.dirname(os.path.abspath(__file__))) # カレントディレクトリをプログラムのあるディレクトリに移動する
        find_gl_count = 0 # 検知フラグ、回数の初期化
        from_now = dt.datetime.now() # 現在時刻を取得(カウント開始時刻)
        last_Minute = Rep_last_Minute = '{0:%Y-%m-%d %H:%M}'.format(dt.datetime.now()) # 前回の分をセット(INTERVAL毎の処理の為)
        Alrt_last_Minute = '{0:%H:%M}'.format(dt.datetime.now()) # アラート時間(前回分)
        watch_over = WOClass() # WatchOverインスタンスの作成
        rule = watch_over.get_rule() # GoogleSheetよりルールを取得する
        while True:
            now = dt.datetime.now() # 現在時刻を取得
            mod_minute = now.minute % PUBLISH_INTERVAL # 分がPUBLISH_INTERVALの倍数かをチェック
            if (mod_minute == 0 and # 分が倍数で
                last_Minute != '{0:%Y-%m-%d %H:%M}'.format(now)): # 同一分では1回のみAWS IoT CoreにPublish
                watch_over.count_publish(from_now, now, find_gl_count) # publish
                from_now = now # Publishした時刻を前回の時刻として保存
                last_Minute = '{0:%Y-%m-%d %H:%M}'.format(now) # 時刻をセット
                find_gl_count = 0 # カウンターをリセット
                rule = watch_over.get_rule() # Publish毎にGoogleSheetよりルールを取得する
            for obj in rule: # ルールを検索する
                if obj['Action'] == 'RPT': # 直近24時間レポート
                    if (obj['Parameter'] == '{0:%H:%M}'.format(now) and # 時刻が一致
                        Rep_last_Minute != '{0:%Y-%m-%d %H:%M}'.format(now)): # 同一分では1回のみレポート
                        watch_over.put_report() # レポートを出力
                        Rep_last_Minute = '{0:%Y-%m-%d %H:%M}'.format(now) # 時刻をセット
                elif obj['Action'] == 'ALT': # この時間帯にゼロ件だったらアラート報告
                    __from_time = obj['Parameter'][0:5] # From HH:MMを抽出
                    __to_time = obj['Parameter'][6:11] # To HH:MMを抽出
                    if (__to_time == '{0:%H:%M}'.format(now) and # アラートのTo時刻で
                        Alrt_last_Minute != '{0:%H:%M}'.format(now)): # 一度も送信していなければ
                        watch_over.put_alert(now, __from_time, __to_time) # アラート
                        Alrt_last_Minute = '{0:%H:%M}'.format(now)
            sleep(CHECK_INTERVAL) # 指定秒数の間隔で繰り返す
except KeyboardInterrupt:
    pass
finally:
    del watch_over # デストラクタ
    GPIO.cleanup()

補足説明

add_event_detect

177行目のGPIO.add_event_detectでGPIOがLOW→HIGHになるイベントを検知してcountup_find関数を呼び出している。

bouncetime=200はLOW→HIGHになる瞬間を検知したら200ミリ秒間隔を空けるという意味。

電圧は直線的に上昇するのではなく上昇中に一旦下がることもあるので一定の間隔を空けて複数回イベントが発生することを避けている。

countup_find

検知件数(グローバル変数)をカウントアップしている。

最後に

以上で今回の記事は終了とする。

最後に

この記事が何処かで誰かの役に立つことを願っている。

尚、当記事中の商品へのリンクはAmazonアソシエイトへのリンクが含まれています。Amazonのアソシエイトとして、当メディアは適格販売により収入を得ていますのでご了承ください。

souichirou

やった事を忘れない為の備忘録 同じような事をやりたい人の参考になればと思ってブログにしてます。 主にレゴ、AWS(Amazon Web Services)、WordPress、Deep Learning、RaspberryPiに関するブログを書いています。 仕事では工場に協働ロボットの導入や中小企業へのAI/IoT導入のアドバイザーをやっています。 2019年7月にJDLA(一般社団法人 日本デイープラーニング協会)Deep Learning for GENERALに合格しました。 質問は記事一番下にあるコメントかメニュー上部の問い合わせからお願いします。

おすすめ

質問やコメントや励ましの言葉などを残す

名前、メール、サイト欄は任意です。
またメールアドレスは公開されません。