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

自作テレビドアホン親機操作パネル

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

自作テレビドアホン親機操作パネル

自作テレビドアホン親機操作パネル

2025/02/11

 自作テレビドアホンの内、親機の操作パネルを作ってみた話。

パネル画面実装/機能概要

 実装内容の詳細は、各種ドアホン用スクリプトに譲るとして概要は次の通り。

自作ドアホン親機パネル待機時映像オフ

 通常、親機パネル上の映像は、オフ。

自作ドアホン親機パネル
*映像はダミー

 ドアホン子機の呼び出しボタンが押下された際に子機のマイクをON、自動で子機カメラ映像を親機操作パネル上のスクリーンに表示しつつ、同時に自動録画開始。

 この時、自動録画されるので同じ処理がなされる映像ボタンと録画ボタンを無効化、これらボタンの押下は不可(終了ボタン押下かタイマーで有効化)。

 ドアホン子機においては、曇天、夜間など周囲が暗い場合などには、併せて「白色」LEDライト(SMD LED48灯)自動点灯。

自作ドアホン親機パネルの通話ボタン/モーメンタリプッシュボタン
通話ボタン

 子機の呼び出しがあった際、応答する場合、通話ボタンを押し続けている間、子機マイクOFF/スピーカーON、放すと子機マイクON/スピーカーOFFでPTT/Push To Talkによる交互通話が可能(終了ボタンで子機マイクOFF/スピーカーOFF)。

 不在、もしくは在宅時でも応答の必要を感じない等で通話ボタンを押下しない場合、タイマーでパネル上の映像を非表示、子機マイクOFF、録画停止、映像・録画ボタンを有効化。

自作ドアホン親機パネルの映像ボタン/オルタネートプッシュボタン
映像ボタン

 また、子機から呼び出しがない(コールボタンが押下されてない)任意の時点で映像ボタンが押下された時、子機のマイクをONにしつつ、映像がオン、自動録画開始、もう一度、映像ボタンを押すと子機のマイクをOFFにしつつ、映像オフ、自動録画停止。

 映像ボタンが押されている時は、録画ボタンの無効化により押下は不可、逆も同様。

 ドアホン子機においては、曇天、夜間など周囲が暗い場合などには、併せて「赤外線」LEDライト(IR砲弾型LED48灯)自動点灯・消灯。

自作ドアホン親機パネルの録画ボタン/オルタネートプッシュボタン
録画ボタン

 子機からの呼び出しがない状態でかつ、録画ボタンを押下で子機のマイクをONにしつつ、録画開始、もう1度押すと子機のマイクをOFFにしつつ、録画終了。

 録画ボタンが押されている時は、映像ボタンの無効化により押下は不可、逆も同様。

 映像ボタンとの違いは、録画ボタンの場合、操作パネル上に映像が表示されないこと。

 ドアホン子機においては、曇天、夜間など周囲が暗い場合などには、併せて「赤外線」LEDライト(IR砲弾型LED48灯)自動点灯・消灯。

自作ドアホン親機パネルから自作スマートロックを操作
ドアロックボタン

 ドアホン操作パネル上の[ドアロック]ボタン押下で自作・運用中のスマートドアロック機能の1つであるブラウザ上でのWebロック・アンロック(施錠・解錠)操作も可能。

 その際は、スマートロック自体では既存のブラウザFirefoxを起動させている一方、当該パネル上では、Python/PyQt6製ブラウザが起動。

 これにより、来訪者との関係性次第では、映像を見ながら、音声応答で「(鍵を開けたから)入って」などと促すことも可能。

終了ボタン

 終了ボタンは、主に子機呼び出しボタンが押され、応答を終えるにあたる後始末で子機側マイクOFF、スピーカーOFF、Mumble終了等。

 親機パネルは、PyQt5/Qt Designerで自作スマートスピーカー用操作パネルを作成の経験が活き、PyQt5からのPyQt6含め、Pythonで実装。

その他機能

 その他の機能として動体検知による自動録画機能を搭載。

 例えば、ドアホン子機呼び出しボタンを押さずにドアを叩いたり、ドア前に人がいることを検知した場合、子機マイクON、自動録画、ドア付近(カメラアングル)から見えなくなるなど一定時間動きがなければ、マイクOFF、録画停止。

 ドアホン子機においては、曇天、夜間など周囲が暗い場合などには、併せて赤外線LEDライト(IR砲弾型LED48灯)自動点灯・消灯。

 これは、同コールボタンを押す習慣がないとか、これを押すのは大げさだと感じてドアをノックする方、声で呼びかける方、また、これから少なくなっていくことが予想される電気・ガス・水道メーターなどのチェックに訪れた方、不審者の他、通路に面している場合は通行人なども該当する可能性もあるかもしれません。

 親しい方なら、後で声掛けすることもできますし、万一不審者なら通報もできる一方、電気・ガス・水道メーターなどのチェック、通行人なら、同機能目的外。

 営業・勧誘等、見知らぬ人物は無視、行動によっては不審者と認識・対応を検討。

 保存データは、経過時間による自動削除としているのですが、一定期間運用したあと、必要ならサイズによる判定ほか、何らかの方法による自動削除を追加検討するかも。

録画機能について

 録画については、子機のコール(呼び出し)ボタン押下、親機操作パネル上の映像ボタンか、録画ボタンの押下、ドア前の動体検知などにより作動します。

 この中で、例えば、コールボタン押下よりも人物検知の方が早く、後者により録画開始されることになるかと思います。

 何れにせよ、全て同じ録画と録画停止メソッドを使っており、ここで排他にしてあるので多重録画されることも、その他の影響もありません(状況想定における思い違い含め、バグがなければ)。

 親機システム・ストレージ用SSDやラズパイサーバ(NAS)に自動保存するデータは、cronで自動削除の要領で定期的に削除。

主なソフトウェア

 映像は、ドアホン子機側の技適ありなfreenove ESP32 S3カメラボードからのRTSPストリームをcvlcで受けてユニキャストアドレスとSAP/Session Announcement Protocolを関連付けた上、マルチキャストアドレスに転送、PyQt6/Python+OpenCVで再生・表示。

 ドアホン子機との音声通話・終了は、ドアホン親機のマイク・スピーカー、親機からssh経由での子機マイク・スピーカーのON/OFFと同じく子機側Mumbleの起動・終了の組み合わせ。

 映像録画・静止画撮影は、PyQt6/Python+OpenCVで映像フレームから取得。

 映像・画像の保存は、適宜、自作ドアホン親機のシステム兼一時ストレージであるSSD及びラズパイサーバ(NAS)に。

 ドアホン子機コールボタン押下時における通話応答は、今回、遅延も許容範囲かなとユニキャストを採用、同一LAN上にいるドアホン親機専用機の他、パソコンやスマホ・タブレット何れであっても映像付きで応答可、また、音声のみながら既設のインターホン親機でも応答可。

 マルチキャストによる遅延は、配信サーバか否か、時間の経過などにより増幅することもありそう、長くなればなるほどドアホン応答には不向きとなるので、そうなった場合、円滑に運用するなら、ユニキャスト配信でドアホン専用親機などデバイスは1台に限定するのが賢明かと。

 スマホ・タブレットに関しては、現在、React Native+Expoで自身初となるスマホアプリ製作中(ハイブリッド対応ながらiOS用はともかく少なくともAndroid用に、リリースはせずローカル専用としapk生成・インストールで運用予定)。

 ちなみに昔、Android Studioで作ろうとしたからか別件で検討した際、Google Play登録、リリース必須と思われる記述に遭遇し断念したのですが、React Nativeなら、その必要もなく、いけそう、という感触が間違いでなければ(とは言え、外出時のVPNアプリは別起動になるかも?)。

 アプリを作らなくても応答はできなくもないでしょうが、おそらく次のような煩雑な手順が必要になるかと。

 バックグラウンド起動もできるMumble(F-Droid版でMumble非公式なMumla)で声で応答にすれば、RTSPにせよ、RTPにせよ、映像は、F-droidのCams等で表示できるものの、常時起動させておくにしても少なくとも最初の一度はCamsを手動起動、必要なときは、Cams画面を選択する必要が、屋内専用とすればMumbleも常時起動でも良いとしても、スマホを外で持ち歩く場合、チャンネル接続失敗時のベル音があったりで、そうはいかないとなれば、AndroidならMacroDroidなど自動化ソフトを使う必要があったり。

 VPN経由でLANに入って外から応答する場合、VPNアプリ、外では終了させてあるであろうMumbleの起動もさせる必要あったりもすると思われるので。

 尚、今回は、極力映像出力遅延を回避するため、カメラ自体は常時機能しているライブ状態とし、パネル上の映像表示をON/OFFで対応。

 遅延が気になって直前までユニキャストでいくつもりだったのですが、それもあってマルチキャストでも、かろうじて実用に足ると判断するに至りました。

 もちろん、当初からマルチキャストにする気、満々、これならマルチクライアント・マルチデバイス対応可能で、ドアホン親機専用機の他、スマホ、タブレット、パソコンでも映像付きで柔軟に応答できると思っていたので願ったり、叶ったり。

PyQtのインストール

$ sudo apt update && sudo apt upgrade -y
$ sudo apt install qttools5-dev-tools qt5-default pyqt5-dev-tools

 Orange Pi OSにDebian系を使う前提だと、こんな感じ。

 PyQtの最新バージョンは5でPyQt5、自作スマートスピーカー用操作パネルに倣って当時うまくいかなかったpipではなく、aptでインストールします。

 ただ、後にqt5-defaultパッケージは、他に取り込まれたようで独立したパッケージとしてはなくなったようです。

 と思いきや、pyQt6にアップグレードする(入れ替える)ことに...。

 と言うのも、実は、この記事、1年半ほど前にアップする気で、まだラズパイを使うつもりの頃に下書きしてあったものの、ドアホン子機自体の造りなどを決め兼ねたりで記事も放置、その間に一通りできていたarm64(Raspberry Pi 400/Raspberry Pi OS)、amd64 dynabook/Debianでも素直に動かなくなっていました。

 ただ、後で気づけば、何れも、venv環境のpythonではなく、システムの/usr/bin/pythonを使うことでできましたが、以下のように寄り道散歩...。

 が、気になるのは、venv/bin/pythonと/usr/bin/pythonのバージョンが同一だったのでvenv以下を確認してみるとvenv/bin/には、python、python3、python3.11とあってpythonは、/usr/bin/pythonを、後2つは、pythonを指しているので何が違うのか...権限?依存関係?のバージョン?

 先に着手したRaspberry Pi OSについては、python -m venvしてたことを失念、またやってvenv先を変えて前のところからbin以下をコピーしてみたら失敗、当初からうまくいかなかったpip pyqt5してもmetaファイル云々でエラーとなり行き詰まりました。

 Debianについては、後から試したので当時からpython -m venvしていた環境で実行するもインデントエラーに見舞われ、そんなはずはと端末からpythonインタプリタを起動しても端末上のviから、また、GUIテキストエディタからコピー・ペーストしても同様...という状況に。

 試行錯誤の結果、Raspberry Pi OSは、最新となっていたPyQt6への移行で、Debianの方は、そうせずとも後述のようにすることでPyQt5のまま、それぞれ解決しました。

 まず、なぜか、utf-8じゃなくなっていたのか、エンコードがおかしなことになっていたようなので以下の作業...。

 GUIテキストエディタで改行部分をコピーしつつ、\nに置換して保存...

 だけではダメだったので、端末からviで開いて余計なところにできたスペースを除去、置換によって一部、改行に相当する部分に機能文字?[^@]が出現したので1つずつ[s]、[改行]して整形、pythonインタプリタにかけると...まだ、ダメ、

 結果、Classごとにブロックを、改行してから次のClassを、そしてまた改行を入れ、最後に[if __name__=="__main__":]をコピー&ペーストしてプログラムの実行ができる状態にしておきました。

 その後、エンコードの件は、いつの間にか解消したものの、原因不明。

 さておき、前者は、apt install python3-qt6としてaptでPyQt6をインストールしつつ、スクリプトも

とすることでプログラムの実行が可能になりました。

 後者は、それでもあらゆる場所でインデントエラーとなるので 、よくよく見てみると以下のエラーが。

 DeprecationWarning: sipPyTypeDict() is deprecated, the extension module should use sipPyTypeDictRef() instead super().__init__() # Call __init__ of parent class.

 検索するとsipパッケージやpythonなどのバージョンをダウングレードする話が多い中、Chuck R氏やAntoine Weisrock氏の警告を消すフラグ[-Wi::DeprecationWarning]をつけるというアイデアを発見、python -Wi::DeprecationWarning myscript.pyしてみると無事実行できました。

 警告が出ていたことでpythonインタプリタにペーストした際、警告部分から以降がインデントエラーとなっていたようです。

 尚、amd64 Debianでは、このフラグ付きでvenvのpythonでいけたわけですが、そもそも、/usr/bin/pythonなど実環境上のpythonならフラグなしでもいけました。

 え?venv使わないでいけた?もしや...とやってみると、前述のように何れもそもそも/usr/bin/pythonで実行すればいけた、しかし、シムリンクになっており、何が違うのかわからない...という...。

 まぁ、PyQt6に移行する方法もわかりましたし、移行に当たって一部ながらPyQt5からの変更部分がわかったことも収穫なので有益でしたけどね。

 尚、PyQt6は、aptパッケージとしては、[python3-pyqt6]、pipパッケージとしては、[PyQt6]。

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

 ドアホン親機用スクリプトとしては、次の11個。

 ボタンについては、通話、終了、ドアロックボタンは、モーメンタリ、映像、録画ボタンは、オルタネートなプッシュボタン。

 映像ボタン、録画ボタン、ドアロックボタンの他、スクリプトにしていな機能については、1のドアホン親機操作パネル用スクリプト内メソッドやクラスで対応。

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

$ cat /path/to/boot_doorphone.sh
#!/bin/bash
 
/usr/bin/flatpak run --command=cvlc org.videolan.VLC \
rtsp://192.168.1.252:8554/mjpeg/1 \
--sout '#transcode{vcodec=mp4v,acodec=mpga,vb=800,ab=128,deinterlace}\
:duplicate{dst=display,dst=rtp{mux=ts,dst=239.255.1.1\
,sdp=sap,name="Koki Camera Stream"}\
,dst=rtp{mux=ts,dst=192.168.1.253}}' &
 
export DISPLAY=:0
/usr/bin/python /path/to/ctrlpanel.py &
sleep 10
/usr/bin/ssh koki.local /path/to/call_bell.sh &
/usr/bin/ssh koki.local vncserver :2
/usr/bin/ssh koki.local export DISPLAY=:2
/usr/bin/ssh koki.local /path/to/detect_person.py &
$

 ドアホンシステム起動スクリプトは、1のドアホン親機用操作パネル表示等とドアホン子機コールボタン押下をチェック、玄関前にいる人を検知するスクリプトを起動(そのための準備含む)。

 ssh koki.localとして親機から子機側の制御もしています。

 ただ、後者2つのドアホン子機用スクリプト用のvncserver起動、DISPLAY設定は、特に開発過程では、環境変数を設定しないまま、スクリプトを起動するとエラーになり、都度設定することになったりするので、ここではなく、子機側の設定ファイル(~/.profileやbashなら~./bashrc等)で行っておく方が良いかもしれません。

 なんならスクリプトも子機側でsystemdや~/.config/autostart/などで自動起動設定しておく方が良いかもしれません。

 また、カメラ自体は子機にあり、今回は、子機に内蔵のカメラ付きESP32 S3ボードのESP32-CAM_RTSP等に倣ったスケッチ|プログラムでRTSP配信を実装済みでユニキャスト(1対1)配信できる状態になっています。

 親機が1台ならそれでも良いのですが、今回は、自作したタッチパネルの親機専用機の他、同様のプログラムで台数関わらず、パソコンも親機にでき、アプリを作ればスマホやタブレットも親機となり、外から応答もでき、マルチキャストゆえの遅延をカバーするため、配信・停止ではなく、映像の「表示・非表示」とすることでマルチキャストでも許容範囲と判断。

 カメラ映像のRTSP/RTPマルチキャスト配信については、親機でも子機でも他でも良いのですが、今回は、親機専用機で。

 マルチキャスト配信に際しては、cvlcを使うことにしたため、Streaming HowTo/Command Line Examplesを参考にrtspでのLAN内ストリームを1度、末尾にあたるLAN内URLで受けてから中程のマルチキャストアドレスに配信。

 RTSP含め、LAN内URLは、固定IP(が賢明)、マルチキャストアドレスは、224.0.0.0〜239.255.255.255(IPv4の旧クラスD)の内、任意のアドレス。

 マルチキャストアドレスについては、LAN内で使う分には、どれでも使えそう...

 ですが、放送局による番組配信などインターネット上でも使われることもあり、IP マルチキャスト アドレスのスコーピング by cisco等々によれば、マルチキャストアドレスにも登録済みアドレスや予約済みアドレスがある模様でLAN内では239.0.0.0〜239.255.255.255範囲かつ、登録済みアドレスや予約済みアドレスを除いたアドレス使うのが無難そうです。

$ cat /path/to/ctrlpanel.py
from PyQt6 import QtCore
from PyQt6.QtCore import pyqtSignal, pyqtSlot, Qt, QThread, QUrl, QObject
from PyQt6 import QtGui
from PyQt6.QtGui import QPixmap
from PyQt6.QtWidgets import QWidget, QApplication, QPushButton, QLabel, QVBoxLayout, QGridLayout, QMainWindow
from PyQt6.QtWebEngineWidgets import QWebEngineView
import sys
import cv2
import time
import datetime
import subprocess
import numpy as np
 
import socket
import pickle
from subprocess import PIPE
 
class SOCKWorker(QThread):
 
  sock_signal_int = pyqtSignal(int)
 
  def run(self):
    chk_exist_one = "ssh doorphone-handset.local ps aux | grep mumble | grep -v grep | wc -l"
    run_mumble_cmd = "ssh doorphone-handset.local xvfb-run mumble mumble://handset@192.168.0.206/doorphone"
 
    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    s.bind(("0.0.0.0", 1234))
    s.listen(5)
 
    cnt = 0
    while True:
      clientsocket, address = s.accept()
 
      ret_byte=clientsocket.recv(1024)
      ret = int(pickle.loads(ret_byte))
 
      if ret == 0:
        if cnt == 0:
          chk_exist_cnt = subprocess.Popen(chk_exist_one, shell=True, stdout=PIPE, stderr=subprocess.PIPE)
          stdout, stderr = chk_exist_cnt.communicate(timeout=None)
          if int(stdout) == 0:
            run_mumble = subprocess.Popen(run_mumble_cmd, shell=True, stdout=PIPE, stderr=subprocess.PIPE)
            try:
              stdout, stderr = run_mumble.communicate(timeout=1)
            except subprocess.TimeoutExpired:
              cnt += 1
              self.sock_signal_int.emit(1)
      else:
        if cnt == 1:
          cnt = 0
 
      clientsocket.close()
  
class VideoThread(QThread):
 
  change_pixmap_signal = pyqtSignal(np.ndarray)
 
  def __init__(self):
    super().__init__()
    self._run_flag = True
 
  def run(self):
    url = "rtsp://IPADDRESS:8554/mjpeg/1"
    cap = cv2.VideoCapture(url)
    while self._run_flag:
      ret, cv_img = cap.read()
      if ret:
        self.change_pixmap_signal.emit(cv_img)
    cap.release()
 
  def stop(self):
    self._run_flag = False
    self.wait()
 
class Browse(QWebEngineView):
  def __init__(self):
    super().__init__()
 
  def load(self, url):
    self.browser = QWebEngineView()
    self.browser.setUrl(QUrl(url))
    self.browser.resize(320, 240)
    self.browser.move(800, 100)
    self.browser.show()
 
class CtrlPanel(QWidget):
 
  def __init__(self):
    super().__init__()
    self.setWindowTitle("ドアホン親機パネル")
    self.initUI()
    self.counter = 0
 
  def initUI(self):
    #self.setWindowFlags(QtCore.Qt.Window | QtCore.Qt.CustomizeWindowHint | Qt.WindowStaysOnTopHint)
    #self.setWindowFlags(QtCore.Qt.WindowType.Window | QtCore.Qt.WindowType.CustomizeWindowHint | Qt.WindowType.WindowStaysOnTopHint)
 
    self.disply_width = 600
    self.display_height = 280
    self.resize(1024, 260)
    self.move(10, -100)
 
 
    self.setWindowTitle('ドアホンパネル')
 
    self.image_label = QLabel(self)
    self.image_label.resize(self.disply_width, self.display_height)
    self.image_label.setScaledContents(True)
    self.image_label.hide()
 
    self.textLabel = QLabel('ドアホンカメラ')
 
    self.ptt_btn = QPushButton('通話')
    self.ptt_btn.setStyleSheet(
        "QPushButton {width:100px ;height:60px ;color:black ;background-color:lightgrey ;font-size:30px ;font-weight:bold ;}"
        "QPushButton:pressed{color:yellow ;background-color:#87cefa ;font-weight:bold ;}"
        );
    self.ptt_btn.pressed.connect(self.startConversation)
    self.ptt_btn.released.connect(self.endConversation)
 
    self.end_btn = QPushButton('終了')
    self.end_btn.setStyleSheet("width:100px ;height:60px ;background-color:lightgrey ;font-size:30px ;font-weight:bold ;")
    self.end_btn.clicked.connect(self.complete_handset)
 
    self.live_btn = QPushButton('映像')
    self.live_btn.setCheckable(True)
    self.live_btn.setStyleSheet("width:100px ;height:60px ;background-color:lightgrey ;font-size:30px ;font-weight:bold ;")
    self.live_btn.clicked.connect(self.live_toggle)
 
    self.rec_btn = QPushButton('録画')
    self.rec_btn.setCheckable(True)
    self.rec_btn.setStyleSheet("width:100px ;height:60px ;background-color:lightgrey ;font-size:30px ;font-weight:bold ;")
    self.rec_btn.clicked.connect(self.rec_toggle)
 
    self.keylock_btn = QPushButton('ドアロック')
    self.keylock_btn.setStyleSheet("width:100px ;height:60px ;background-color:lightgrey ;font-size:30px ;font-weight:bold ;")
    self.keylock_btn.clicked.connect(self.door_key_lock)
 
    self.grid = QGridLayout()
    self.grid.addWidget(self.image_label, 0, 0, 10, 2)
    #self.grid.addWidget(self.pic, 0, 0, 10, 2)
    self.grid.addWidget(self.textLabel, 11, 0)
    self.grid.addWidget(self.ptt_btn, 13, 0, 1, 1)
    self.grid.addWidget(self.end_btn, 13, 1, 1, 1)
    self.grid.addWidget(self.live_btn, 15, 0, 1, 1)
    self.grid.addWidget(self.rec_btn, 15, 1, 1, 1)
    self.grid.addWidget(self.keylock_btn, 16, 0, 1, 1)
 
    self.setLayout(self.grid)
    self.thread = VideoThread()
    self.thread.change_pixmap_signal.connect(self.update_image)
    self.thread.start()
 
    self.sock = SOCKWorker()
    self.sock.sock_signal_int.connect(self.show_imagelabel)
    self.sock.start()
 
  def startConversation(self):
    subprocess.call("/path/to/doorphone/script/start_reply_to_visitor.sh > /dev/null 2>&1 &", shell=True)
 
  def endConversation(self):
    subprocess.call("/path/to/doorphone/script/end_reply_to_visitor.sh > /dev/null 2>&1 &", shell=True)
 
  def complete_handset(self):
      self.end_btn.setStyleSheet("width:100px ;height:60px ;background-color:lightgrey ;font-size:30px ;font-weight:bold ;")
      subprocess.call("/path/to/doorphone/script/finish_btn.sh > /dev/null 2>&1 &", shell=True)
      self.image_label.hide()
 
  def live_toggle(self):
    if self.live_btn.isChecked():
      self.live_btn.setStyleSheet("width:100px ;height:60px ;background-color:darkgrey ;font-size:30px ;font-weight:bold ;")
      self.show_imagelabel(1)
    else:
      self.live_btn.setStyleSheet("width:100px ;height:60px ;background-color:lightgrey ;font-size:30px ;font-weight:bold ;")
      self.show_imagelabel(0)
 
  def rec_toggle(self):
    if self.rec_btn.isChecked():
      self.rec_btn.setStyleSheet("width:100px ;height:60px ;background-color:darkgrey ;font-size:30px ;font-weight:bold ;")
    else:
      self.rec_btn.setStyleSheet("width:100px ;height:60px ;background-color:lightgrey ;font-size:30px ;font-weight:bold ;")
 
  def door_key_lock(self):
    url = "http://IPADDRESS"
    self.browser = Browse()
    self.browser.load(url)
 
  @pyqtSlot(int)
  def show_imagelabel(self, n):
    #print("show image_label")
    if n == 1:
      self.image_label.show()
      self.textLabel.setText("set 1")
    elif n == 0:
      self.image_label.hide()
      self.textLabel.setText("set 0")
 
  def countdown(self, h, m, s):
    total_seconds = h * 3600 + m * 60 + s
    while total_seconds > 0:
      timer = datetime.timedelta(seconds = total_seconds)
      #print(timer, end="\r")
      time.sleep(1)
      total_seconds -= 1
    return 1
 
  def closeEvent(self, event):
    self.thread.stop()
    event.accept()
 
  @pyqtSlot(np.ndarray)
  def update_image(self, cv_img):
    qt_img = self.convert_cv_qt(cv_img)
    self.image_label.setPixmap(qt_img)
 
  def convert_cv_qt(self, cv_img):
    rgb_image = cv2.cvtColor(cv_img, cv2.COLOR_BGR2RGB)
    h, w, ch = rgb_image.shape
    bytes_per_line = ch * w
    convert_to_Qt_format = QtGui.QImage(rgb_image.data, w, h, bytes_per_line, QtGui.QImage.Format.Format_RGB888)
    p = convert_to_Qt_format.scaled(self.disply_width, self.display_height, Qt.AspectRatioMode.KeepAspectRatio)
    return QPixmap.fromImage(p)
 
if __name__=="__main__":
  app = QApplication(sys.argv)
  main_panel = CtrlPanel()
  main_panel.show()
  sys.exit(app.exec())
$

 1のドアホン親機操作パネル用スクリプトは、2のドアホン子機コールボタン押下信号を受信すべく、ソケット通信、また、子機カメラ映像取得を含む。

 もちろん、IPADDRESSなど必要に応じて要変更。

$ cat /path/to/talk_from_oyaki_to_koki.sh
#!/bin/bash
pactl set-source-mute alsa_input.xxx 0
pactl set-sink-mute alsa_output.yyy 1
 
ssh doorphone-koki.local pactl set-source-mute alsa_input.zzz 1
ssh doorphone-koki.local pactl set-sink-mute alsa_output.xyz 0
ssh doorphone-koki.local pactl set-sink-volume 0 80%
$

 1-Aの通話ボタン押下時用スクリプトでは、親機から子機へ音声を届けるべく、ドアホン親機及び子機のマイクとスピーカーを設定。

 今回は、それぞれマイクとスピーカーは排他。

$ cat /path/to/talk_from_koki_to_oyaki.sh
#!/bin/bash
pactl set-source-mute alsa_input.xxx 1
pactl set-sink-mute alsa_output.yyy 0
 
ssh doorphone-koki.local pactl set-source-mute alsa_input.zzz 0
ssh doorphone-koki.local pactl set-sink-mute alsa_output.xyz 1
ssh doorphone-koki.local pactl set-sink-volume 0 80%
$

 1-Bの通話ボタンリリース時用スクリプトでは、通話ボタン押下時用と逆にドアホン子機からドアホン親機へ音声を届けるべく、親機及び子機のマイクとスピーカーを設定。

$ cat /path/to/finish_btn.sh
#!/bin/bash
pactl set-source-mute alsa_input.xxx 0
pactl set-sink-mute alsa_output.yyy 0
%nbsp;
ssh doorphone-koki.local pactl set-source-mute alsa_input.zzz 1
ssh doorphone-koki.local pactl set-sink-mute alsa_output.xyz 1
#ssh doorphone-koki.local pactl set-sink-volume 0 100%
ssh doorphone-koki.local pkill -f mumble
ssh doorphone-koki.local pkill -f Xvfb
ssh doorphone-koki.local pkill -f call_bell.sh
ssh doorphone-handset.local /path/to/doorphone/script/call_bell.sh &
kill -9 `ps aux | grep -v grep | grep xvfb-run | awk '{print $2}' | tr -d "\n"`
$

 1-Cの終了ボタン用スクリプトは、ドアホン親機のマイク・スピーカーを共にON、ドアホン子機のマイク・スピーカーを共にOFF、子機のMumble、これをディスプレイなしで起動すべく使用したXvfb、呼び出しボタン押下時用のスクリプトを終了。

 改めて呼び出しベルが押されるのを待つべく、改めてスクリプトを起動。

 親機側スクリプトから実行したxvfb-runが残っていたときのために一応、終了。

$ cat /path/to/finish_btn.sh
#!/bin/bash
pactl set-source-mute alsa_input.xxx 1
pactl set-sink-mute alsa_output.yyy 0
%nbsp;
ssh doorphone-koki.local pactl set-source-mute alsa_input.zzz 0
ssh doorphone-koki.local pactl set-sink-mute alsa_output.xyz 1
#ssh doorphone-koki.local pactl set-sink-volume 0 100%
$

 親機・子機PTT通話における子機発信時設定スクリプトは、ドアホン親機のマイクをOFF・スピーカーをON、ドアホン子機のマイクをON・スピーカーをOFFにするだけのBashスクリプト。

 同機能は、親機操作パネルで押下した応答ボタンを放した時に適用。

$ cat /path/to/run_cvlc_for_rec.sh
#!/bin/bash
exec /usr/bin/flatpak run --command=cvlc org.videolan.VLC $1 --sout file/wmv:$2 &
$

 OpenCV動体検知時RTPストリーム録画用スクリプトは、その名の通り、動体検知で感知した際に自動録画するためのBashスクリプト。

 同機能は、動体検知スクリプトからRTPストリームURLと保存ファイル名を受け取り、自動録画するもの。

$ cat /path/to/finish_cvlc_for_rec.sh
#!/bin/bash
for i in `ps aux | grep -v grep | grep wmv | grep vlc.bin | awk '{print $2}'`;do kill -9 $i;done
$

 OpenCV動体検知時録画終了スクリプトは、タイマーにより一定時間以上検知されたなかった場合、録画を自動停止するためのBashスクリプト。

 cvlcで実行したプロセスは、複数できますが、vlc-binのプロセスを終了させれば、関連するプロセスも終了することを確認しました。

$ cat /path/to/finish_btn.sh
#!/bin/bash
ssh doorphone-koki.local pactl set-source-mute alsa_input.zzz 1
ssh doorphone-koki.local pactl set-sink-mute alsa_output.xyz 1
#ssh doorphone-koki.local pactl set-sink-volume 0 100%
$

 動体検知における自動録画終了時スクリプトは、ドアホン子機のマイク・スピーカーを共にOFFにするだけのBashスクリプト。

 同機能は、動体検知で実行される自動録画をタイマーで自動的に終了させる際に適用。

備考

 検討事項含む残作業としては、次の通り。

ホーム前へ次へ