気の向くままに辿るIT/ICT/IoT
IoT・電子工作

自作テレビドアホン子機

ホーム前へ次へ
Orange Piって?

自作テレビドアホン子機

自作テレビドアホン子機

2025/02/11

 自作テレビドアホンの内、子機を作ってみた話。

 万一にもどっからともなくな身バレはこっ恥ずかしいのでドアホン子機の外観の掲載は控えてみようかと。

テレビドアホン子機概要

 数ヶ月前まで壁のスイッチボックスに自作子機を格納する予定が、入りさえすれば収まるものの、まさかのスイッチボックス内に入らない自体が発生、前後して縁あったインターホンを、流用すれば良いかと今更ながらWiFiドアベルと交換する恰好で設置。

 というわけでスイッチボックスにはインターホン用子機が設置済みながら、このインターホン用コールボタンにTTP223という小さなタッチセンサーを被せる恰好でドアホンでも流用できるようハックしつつ、自作テレビドアホン子機の構成にあるようにSBC・カメラ付きマイコン・照明・マイク・スピーカー等を仕込んだ筐体を別途設置することに。

 これにより来訪者は、タッチセンサーTTP223を意識することなく、これに触れつつ、インターホン子機コールボタンを押すことでインターホンのベルが鳴り、インターホン親機でも応答できつつ、タッチセンサーにより自作ドアホンのトリガーの1つにもなり、自作したテレビドアホン親機でも応答できるという一石二鳥な仕組み。

 この方法をとったのは、インターホン親機パネルには、無電圧の移報|メーク|A接点の出力がなく、入力のみだったこと、補助音響器用出力を利用する方法をとも思ったものの、無線にするにも余計にマイコンが1ついり、これ用の電源を電池とするには電池交換が、パネル以外からとるとなると配線が面倒なので。

 もう1つのトリガーとしてドアホン子機側での動体検知機能があります。

 尚、タッチセンサーに触れるか、または、動体検知における感知時、自動録画、タイマーや親機操作パネルの終了ボタン押下時に録画が自動停止するようしてあります。

 ESP32 S3ボード上のカメラOV2640は、広角160度でナイトビジョン対応タイプ、曇天・夜間など暗い時は、操作・機能に応じてSMD白色LED48灯ライトか、砲弾型IR|赤外線LED48灯が点灯。

ドアホン子機用スクリプト

 タッチセンサーがタッチ(≒インターホンのコールボタンが押)されたこと、動体検知が感知したことをドアホン子機からドアホン親機にソケット通信で通知するので通知後の動作については、自作テレビドアホン親機操作パネルを参照。

 子機側では、タッチセンサーがタッチされたことを感知するにあたっては、SBCであるOrange Pi Zero RAM1GB/Orange Pi OS BookwormにおいてBashによるシェルスクリプトとGPIO操作可能なWiringOP(Orange Piシリーズ版WiringPi)で、動体検知については、PythonスクリプトとOpenCV、クライアント用ソケット通信についてはPythonスクリプトで実装。

 前者の方法に至ったのは、SBCの?Orange Piの?GPIO界隈が微妙な状況にある模様なため。

 尚、近年、Python界隈では、ユーザー環境用としてvirtualenv|venvを使うことが強く推奨されており、自身も他ではそうしていますが、なぜか、自作ドアホン環境においては、それだとうまくいかず、一方、/bin/pythonや/usr/bin/pythonを使うと期待通りになるので全て実環境のpythonを使用しています。

 というわけでドアホン子機用スクリプトは、次の3つ。

  1. 子機コールボタン押下時判定スクリプト/Bash+WiringOP
  2. ドア付近にいる人の自動検知用スクリプト/Python
  3. 1,2共通のソケット通信スクリプト/Python
$ cat /path/to/calling_bell.sh
#!/bin/bash
 
# ドアホン子機コールボタン押下検知
/usr/bin/gpio mode 6 in
/usr/bin/gpio mode 6 up
calling=0
cnt=0
 
while :
do
  calling=`/usr/bin/gpio read 6`
  #echo $calling
  #echo $cnt
 
  if [ $calling -eq 1 ] || [ $calling -eq 0 ]; then
    if [ $calling -eq 1 ] || [ $cnt == 0 ]; then
      cnt=1
    if [ $calling -eq 0 ] || [ $cnt == 1 ]; then
      cnt=0
    fi
    /usr/bin/python /path/to/doorphone/script/sock_client.py $calling
   fi
done
$

 ドアホン子機呼び出しボタン押下時は、今回の場合、既存インターホンボタン上に小型タッチモジュールを設置する恰好でハック、同時に押されることで既存インターホン親機でも応答できると共に自作ドアホンにも連動。

 当該スクリプトは、ドアホン子機のコールボタン押下を検知すべく、デーモンとして常時起動。

 SBCであるOrange Pi Zero 3のGPIO経由で、無限ループ、プルアップすることにしたので0の時、コールボタン(上の小型タッチモジュール)が押されたことを検知(1の時は押されていない状態)。

 ここで指定しているGPIOの番号は、Orange Pi Zero 3において端末から[gpio readall]した際、表示される一覧の[wPi]列の値。

$ cat /path/to/detect_person.py
import concurrent.futures
import time
 
import beepy
import cv2
import numpy as np
 
import socket
import subprocess
from subprocess import PIPE
 
def send_by_sock(arg):
  narg = str(arg)
  print("send_by_sock narg type(narg) == ", narg, type(narg))
  swrap = subprocess.Popen(["/usr/bin/python ./sock_client.py $0", narg], shell=True, stdout=PIPE, stderr=subprocess.PIPE)
  try:
    trun = swrap.communicate()
  finally:
    print("finally trun == ",type(trun), trun)
 
#def detect_motion(video_source="rtp://239.255.1.1:5004", area_threshold_ratio=0.8, detection_region='right'):
def detect_motion():
 
  NOT_ARG = 0
  START_REC = 2
  FINISH_REC = 3
 
  detect_cnt = 0
  first_detect_t = 0
  during_detect_t = 0
  out_of_detect_t = 0
 
  video_source="rtp://239.255.1.1:5004"
  area_threshold_ratio=0.1
  detection_region='right'
 
  print("start detect_motion")
  cap = cv2.VideoCapture(video_source)
 
  if not cap.isOpened():
    print("Error reading video file")
 
  fgbg = cv2.createBackgroundSubtractorMOG2()
 
  with concurrent.futures.ThreadPoolExecutor(max_workers=2) as executor:
    print("start concurrent")
    out_of_detect_t = time.time()
    while True:
      ret, frame = cap.read()
      if not ret:
        #break
        continue
 
      height, width, _ = frame.shape
 
      if detection_region == 'right':
        roi = frame[:, width // 2:]
      elif detection_region == 'left':
        roi = frame[:, :width // 2]
      elif detection_region == 'top':
        roi = frame[:height // 2, :]
      elif detection_region == 'bottom':
        roi = frame[height // 2:, :]
      else:
        raise ValueError("Invalid detection_region. Use 'left', 'right', 'top', or 'bottom'.")
 
      fgmask = fgbg.apply(roi)
 
      kernel = np.ones((5, 5), np.uint8)
      fgmask = cv2.morphologyEx(fgmask, cv2.MORPH_OPEN, kernel)
 
      contours, _ = cv2.findContours(fgmask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
 
      frame_area = height * width
      for contour in contours:
        area = cv2.contourArea(contour)
        if area > (frame_area * area_threshold_ratio):
          x, y, w, h = cv2.boundingRect(contour)
          if detection_region == 'right':
            cv2.rectangle(frame, (x + width // 2, y), (x + w + width // 2, y + h), (0, 255, 0), 2)
          elif detection_region == 'left':
            cv2.rectangle(frame, (x, y), (x + w, y + h), (0, 255, 0), 2)
          elif detection_region == 'top':
            cv2.rectangle(frame, (x, y), (x + w, y + h), (0, 255, 0), 2)
          elif detection_region == 'bottom':
            cv2.rectangle(frame, (x, y + height // 2), (x + w, y + h + height // 2), (0, 255, 0), 2)
 
          if detect_cnt <= 0:
            first_detect_t = time.time()
            executor.submit(send_by_sock, START_REC)
          during_detect_t = time.time()
          detect_cnt += 1
 
      out_of_detect_t = time.time()
      sub = out_of_detect_t - first_detect_t
      #print("sub == ", sub)
      if 65.0 >= sub >= 60.0:
        detect_cnt = 0
        first_detect_t = 0
        executor.submit(send_by_sock, FINISH_REC)
 
      cv2.imshow('Motion Detection', frame)
      cv2.waitKey(10)
 
    cap.release()
    cv2.destroyAllWindows()
 
if __name__ == "__main__":
  detect_motion()
  '''
  detect_motion(
    video_source="rtp://239.255.1.1:5004",
    area_threshold_ratio=0.8,
    detection_region='right'
  )
  '''
$

 その他機能とした子機コールボタン押下や親機操作パネルでの操作もなく、玄関前に一定時間以上人がいる場合を想定してOpenCVによる動体検知による自動録画を実装したPythonスクリプト。

 このスクリプトもドア外にいる人を検知すべく、デーモンとして常時起動。

 目的としては、子機コールボタンを押さず、ドアをノックする人、声で呼びかける人、めちゃめちゃ恥ずかしがりやで固まっている人、はたまた、不審者。

 ノックや呼びかけ、極度の恥ずかしがりやさんなら、知人であれば、場合によっては、後で電話やSNS、声掛けができますし、不審者なら通報もでき、必要なら動画・画像も提供できるので。

 尚、通行人、井戸端会議、置き配、スマートメーター化が進めばなくなるだろう電気/ガス/水道メーター検針員、各種営業や宗教等の勧誘なども含まれる可能性はあるものの、何れも目的外。

 ここでは、動体検知・自動録画する際に[2]、動体検知外となりタイマーで一定時間経過後、自動録画を終了する際に[3]をソケット通信でドアホン親機(ソケットサーバ)側に通知。

 この時、OpenCVによる動体検知においては、ディスプレイ必須な模様、よって、まず、VNCで仮想デスクトップのサーバとしてvncserver :2のように起動、export DISPLAY=:2|export DISPLAY=:5902等として環境変数DISPLAYにこれを予め割り当てておきます。

 続いて実際にOpenCVで動体検知するにあたり、ストリーム上ではなく、ディスプレイ上をベースに検知するようで仮想といえどもディスプレイが起動している必要がある模様、かと言ってヘッドレス環境なドアホン子機側でvncviewerを起動すべく、別デバイスの物理ディスプレイを使うわけにはいかないのでVNCによる仮想デスクトップ用の仮想ディスプレイをXvfbで用意する必要があります。

 より具体的には、Python+OpenCVによるスクリプトを起動する際に[xvfb-run python detect_person.py]のように実行しています。

 検知した際、ソケット通信、録画は、親機操作パネル用スクリプト内で対応。

 途中、親機側に...とも思ったものの、相当数のマルチタスクで忙し過ぎるかなと子機で対応することに。

$ cat /path/to/sock_client.py
import sys
import socket
import pickle
import logging
 
logger = logging.getLogger(__name__)
 
class SOCK_CLIENT():
   def run():
    url = "192.168.5.6"
    port = 1234
    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    s.connect((url, port))
    msg = pickle.dumps(sys.argv[1])
    s.send(msg)
    print(type(msg),msg)
 
if __name__=="__main__":
   SOCK_CLIENT.run()
   sys.exit()
$

 これは、実際にサーバにソケット通信するスクリプトであり、前段の2つ、つまり、ドアホン子機呼び出しボタン押下をチェックするBashスクリプトと玄関前にいる人を検知するPythonスクリプトから呼ばれる恰好でソケット通信、親機操作パネル側に通知する共通のPythonスクリプト。

備考

 8年ほど前に電子工作、IoT/スマートガジェット製作を始めるにあたり、遠い目標としていたテレビドアホン、買ってしまうと作る気が失せる、買ったとしても自作品を作り、完成したとして交換するにあたり100V配線だと交換する際に電気工事士の資格がいり、電池式なら資格不要も電池交換を考えると面倒そうだなと。

 かと言って、そもそもインターホンなら要らない、100V配線なら尚、先の市販品ドアホンと同じようなことに、が、さすがにドアをノックしてもらうのもと思うに至った5年ほど前、Wi-Fiドアベルを設置した経緯があり、インターホンを設置してしまったのは自分でも意外でしたが。

 スイッチボックスに自作ドアホン子機が入らなかったショックで魔が差したのかも?

 その必要がなくなったことでサイズにこだわりがなくなった上、早期実現、実装の簡便性を最優先させた結果、チョイスする個々のパーツも小型とは言い難いものばかりでスイッチボックスに収まるどころの話ではなくなりました。

 気が向いたら、コンパクトにすることも検討してみるかも。

ホーム前へ次へ