前回

前回はプログラムの自動起動について書きました

今回

GPIOを使って、スイッチの状態を読み取りテスト撮影などの機能を盛り込みます

また、これまでの内容を総括した全体プログラムを紹介します

現在、猫カメラとして実働しているものになります

お出かけには必須になりました

作ってよかった😁

間が結構飛んでいるかもしれませんが、大きな心でご容赦ください笑

スイッチの機能

カメラの設置場所を決めるために撮影用のスイッチをつけます

以下のような機能をつけます

  • 短押し(5秒以下)でテスト撮影
  • 長押し(5秒以上)でラズパイシャットダウン

撮ったらLINEします

スイッチの状態を読み取ってみる

スイッチは秋月電子でこれを買いました↓

プッシュスイッチは電源(3.3V)ースイッチーラズパイ のように接続します

GPIO26に接続し、プルダウンに設定しています

ですので何もしなければ0Vで、スイッチを押せばhigh(3.3V)となります

whileループ内でスイッチの状態を読み取ります

プルダウンとして設定しているので、何もしなければGPIO26の電圧はGNDレベル=0Vでありaの値をprintすると0となります

スイッチを押すとGPIO26の電位がHigh=3.3Vとなるのでa=1となります

ctrl + Cでwhileループから抜けて終了です

import RPi.GPIO as GPIO

button = 26
GPIO.setmode(GPIO.BCM)
GPIO.setup(button,GPIO.IN,pull_up_down = GPIO.PUD_DOWN) # プルダウン

try:
    while True:
        a = GPIO.input(button)
        print(a)
except KeyboardInterrupt: # ctrl + C
    print('\nbreak')
    GPIO.cleanup(button)

短押し、長押し判定

割り込みは使っていません

whileループ中に50msec毎にスイッチを見に行っています

50msecはチャタリングを考慮しての時間です

ボタンを最初に押してから離すまでの時間を測り、5秒以下なら短押しと判定、5秒以上なら長押しと判定しています

長押しでシャットダウン

import osをして sudo shutdown -h now を実行するだけです

長押し時の処理にこれを実行します

実働プログラム

レビジョン7まで上げてやっと安定しました

今のところ問題なく動いているプログラムです

繰り返しの記述があるので、簡素化したいところです

が、まあ動いているのでやる気が出たら手を入れます笑

"""
にゃんCAM

更新履歴
2021/09/26 : rev0 new
2021/09/29 : rev1 撮影時のみカメラ起動、日本語化
2021/10/08 : rev2 リトライ処理実装(おもにwifiが無くなったことを想定)
2021/10/09 : rev3 カメラ2台対応(1台用とは別にする 1台用はrev2),カメラ自動認識(1台でも認識はするがエラーになる。あくまで2台用)
2021/10/11 : rev4 カメラ2台用 リトライ処理削除(リトライはsystemdのRestart=alwaysが行うため)
2021/10/13 : rev6 カメラ2台用(ベース:rev4) 起動時にまず撮影
2021/10/13 : rev7 いじり respをそとに resp間にsleep(resp_time) 送る間隔が早すぎてエラーになってたかも(ネットが低速で画像が送れない)


自動起動
sudo nano /lib/systemd/system/nyancam.service
sudo systemctl start nyancam.service
sudo systemctl stop nyancam.service
sudo systemctl enable nyancam.service
sudo systemctl disable nyancam.service

/etc/systemd/system.conf
#DefaultStartLimitInterval=10s
#DefaultStartLimitBurst=5
systemdは10秒の間に5回まで再起動が行われ、それを超えると再起動をやめてしまう
ので、そうならないように起動時にtime.sleep(initwait)する
initwait = 3
10秒では3秒*3回=9秒より、3回しか再起動できないため、再起動しつづける動きとなる

再起動 1日4回 0,6,12,18時
systemdで実装
saikidou.service
saikidou.timer ← enable


機能
・ラズパイ起動時に自動起動、撮影開始
 ・プログラムが失敗(wifi消失などによる)したときはsystemdにより自動再起動する 再試行中はカメラの青LEDが点灯(接続カメラチェックによる)するためわかる 再試行周期はinitwait+5秒くらい
・短押し : テスト撮影(カメラ設置場所決定のため)
・長押し(5秒以上) : shutdown

エラー時(wifi消失)
wifi消失によりエラーとなるのはAPIで通信する
resp = requests.post(api,headers = headers,data = data)
がある以下3箇所
1.### にゃんCAM 起動 ###
2.ボタン短押し(テスト撮影)
3.定期撮影(30分ごととか)

エラーパターン1 : 最初からwifiがない
1.でエラーになる

エラーパターン2 : 途中でwifiがなくなる
1.はきっと通過している
2.や3.でエラーとなる
wifiが復帰したら、最初からやり直しなのでLINEで### にゃんCAM 起動 ###がまた来る

"""
import cv2,time,requests,os
import RPi.GPIO as GPIO
import datetime
import sys

initwait = 3
time.sleep(initwait)

######## Variables ########
rev = 7 # プログラムレビジョン
token = 'あなたのトークン' # LINE Notifyトークン

init = True # 初回のみフラグ
Snapshot_period = 1800 # 撮影周期 sec 30分=60秒*30=1800秒
Longpush_period = 5 # 長押し時間 sec 5秒
shutter_time = 5 # 露光?時間 sec elecomピンクが性能悪いので5秒
resp_time = 1 # カメラ2台 resp間ウエイト(画像が重い)

# リトライ関連
# デバッグ時は短く設定 エラー行数が出ないのが惜しい
MAXTRY = 0 # トライ回数 20回
Try_period = 1 # トライ周期 sec 1分
Trycnt = 0 # トライカウント

######## 接続カメラチェック ########
true_cam = [] # 配列用意
for camera_num in range(10): # カメラ番号を0~9まで変えて、COM_PORTに認識されているカメラを探す
    cap = cv2.VideoCapture(camera_num)
    ret, frame = cap.read()
    if ret == True: # capture.read()に画像が格納されていたら=画像が取得できたら=カメラが接続されていたら
        true_cam.append(camera_num)
        #print('camera number {} Find!'.format(camera_num))
    else: # ここでワーニング
        #print('camera number {} None'.format(camera_num))
        pass
print('接続されているカメラは {} 台です'.format(len(true_cam)))

for i in range(len(true_cam)): # カメラ番号調べ

    print('カメラ{} : {} 番'.format(i,true_cam[i]))

CamNum0 = true_cam[0] # 接続されたカメラの番号
CamNum1 = true_cam[1]
print('CamNum0 : {}'.format(CamNum0))
print('CamNum1 : {}'.format(CamNum1))
#################################

# Setup working directory
os.chdir('/home/pi/Desktop')

# Setup GPIO
button = 26 # VDD(3.3V) - button - GPIO26
GPIO.setmode(GPIO.BCM)
GPIO.setup(button,GPIO.IN,pull_up_down = GPIO.PUD_DOWN) # pull down
button_old = time.time()
trig = False
val = False
long = False
flag = False

# Setup LINE Notify
api = 'https://notify-api.line.me/api/notify'
headers = {'Authorization': 'Bearer' + ' ' + token}
filename0 = 'cat0.jpeg'
images0 = '/home/pi/Desktop/' + filename0
filename1 = 'cat1.jpeg'
images1 = '/home/pi/Desktop/' + filename1

# Setup OpenCV
windowsize = (1024,768) # 4:3 LINE Notifyの送信可能な最大解像度
ImageNum = 0
old = time.time() # 初回時間


try:
    while(True): # にゃんCAM main loop

        if init == True: # 初回のみ
            message = '\n\n### にゃんCAM 起動 ###\nrev:{}\n\n{}分ごとに撮影しまーす!\n接続カメラ : {} 台'.format(rev,Snapshot_period/60,len(true_cam))
            data = {'message': message}
            resp = requests.post(api,headers = headers,data = data)

            print(message)
            print(datetime.datetime.now())

            # 初回にまず撮影する
            shutter_t0 = time.time()
            cap0 = cv2.VideoCapture(CamNum0)
            cap1 = cv2.VideoCapture(CamNum1)

            while True:
                ret, frame0 = cap0.read() # capture(too slow!!)
                ret, frame1 = cap1.read() # capture(too slow!!)
                shutter_t1 = time.time()

                if(shutter_t1 - shutter_t0) > shutter_time: # shutter time
            
                    frame0 = cv2.resize(frame0, windowsize) # resize the window
                    frame1 = cv2.resize(frame1, windowsize) # resize the window

                    cv2.imwrite(filename0,frame0) # save
                    cv2.imwrite(filename1,frame1) # save

                    cap0.release()
                    cap1.release()
                    break # break while loop

            ratelimit_image = resp.headers.get("X-RateLimit-ImageLimit") # max image upload at 1hour
            ratelimit_image_remaining = resp.headers.get("X-RateLimit-ImageRemaining") # image upload remaining

            # for LINE Notify
            # カメラ0
            message0 = '\nカメラ0\nうp残 : {} / {}'.format(ratelimit_image_remaining,ratelimit_image)
            data0 = {'message': message0}
            files0 = {'imageFile': open(images0,'rb')}
            requests.post(api,headers = headers,data = data0,files = files0)

            time.sleep(resp_time)

            # カメラ1
            message1 = '\nカメラ1'
            data1 = {'message': message1}
            files1 = {'imageFile': open(images1,'rb')}
            requests.post(api,headers = headers,data = data1,files = files1)

            # for debug
            print(message0)
            print(message1)

            init = False # 初回のみより、initのTrueへの復帰は不要

        debug0 = time.time() # for measure time of main loop
        
        # button detect
        button_t0 = time.time() # now

        # snapshot
        t0 = time.time() # now
        
        debug1 = time.time() # for debug

        # button detect
        if button_t0 - button_old > 50/1000: # 50msecごと(以上)にボタンの状態を監視
            
            button_state = GPIO.input(button) # check button state
            
            if button_state == 1: # button pushed
                if trig == False: # first pushed
                    firstpush = time.time() # for debug
                    trig = True
                val = True

            else: # button released
                if val == True and long == False: # short pushed( < 5sec ) : Test shot
                    shortpush = time.time() # for debug
                    shutter_t0 = time.time()
                    cap0 = cv2.VideoCapture(CamNum0)
                    cap1 = cv2.VideoCapture(CamNum1)

                    while True:
                        ret, frame0 = cap0.read() # capture(too slow!!)
                        ret, frame1 = cap1.read() # capture(too slow!!)
                        shutter_t1 = time.time()

                        if(shutter_t1 - shutter_t0) > shutter_time: # shutter time
                    
                            frame0 = cv2.resize(frame0, windowsize) # resize the window
                            frame1 = cv2.resize(frame1, windowsize) # resize the window

                            cv2.imwrite(filename0,frame0) # save
                            cv2.imwrite(filename1,frame1) # save

                            cap0.release()
                            cap1.release()
                            break # break while loop

                    ratelimit_image = resp.headers.get("X-RateLimit-ImageLimit") # max image upload at 1hour
                    ratelimit_image_remaining = resp.headers.get("X-RateLimit-ImageRemaining") # image upload remaining

                    # for LINE Notify
                    # カメラ0
                    # message0 = '\nカメラ0\nテスト撮影\n短押し : {} sec\nうp残 : {} / {}\nエラー時リトライ回数 : {} / {}\nリトライ周期 : {} sec'.format(round((shortpush - firstpush),2),ratelimit_image_remaining,ratelimit_image,Trycnt,MAXTRY,Try_period)
                    message0 = '\nカメラ0\nテスト撮影\n短押し : {} sec\nうp残 : {} / {}'.format(round((shortpush - firstpush),2),ratelimit_image_remaining,ratelimit_image)
                    data0 = {'message': message0}
                    files0 = {'imageFile': open(images0,'rb')}
                    requests.post(api,headers = headers,data = data0,files = files0)

                    time.sleep(resp_time)

                    # カメラ1
                    message1 = '\nカメラ1\nテスト撮影'
                    data1 = {'message': message1}
                    files1 = {'imageFile': open(images1,'rb')}
                    requests.post(api,headers = headers,data = data1,files = files1)

                    # for debug
                    print(message0)
                    print(message1)
                    print('main loop : {} sec'.format(debug1 - debug0))
                    
                val = False
                trig = False
                flag = False
                long = False
            
            if val == True: # button pushed
                longpush = time.time() # for debug
                if (longpush - firstpush) > Longpush_period: # long pushed ( > 5sec ) : shutdown
                    if flag == False: # once count
                        print('\n長押し : {} sec'.format(round((longpush - firstpush),2))) # for debug
                        message0 = '\n\n### にゃんCAM シャットダウン ###\n\nばーい!'
                        data = {'message': message0}
                        requests.post(api,headers = headers,data = data)
                        print(message0) # for debug
                        time.sleep(3) # シャットダウンするまで少し待ち
                        # os.system('sudo reboot')
                        os.system('sudo shutdown -h now')
                        flag = True
                        long = True
                        trig = False

            button_old = button_t0

        # 定期撮影
        if t0 - old > Snapshot_period:

            shutter_t0 = time.time() # initial time
            cap0 = cv2.VideoCapture(CamNum0) # VideoCapture
            cap1 = cv2.VideoCapture(CamNum1) # VideoCapture

            while True:
                ret, frame0 = cap0.read() # capture(too slow!!)
                ret, frame1 = cap1.read() # capture(too slow!!)
                shutter_t1 = time.time()

                if(shutter_t1 - shutter_t0) > shutter_time: # shutter time
    
                    frame0 = cv2.resize(frame0, windowsize) # resize the window
                    frame1 = cv2.resize(frame1, windowsize) # resize the window

                    cv2.imwrite(filename0,frame0) # save
                    cv2.imwrite(filename1,frame1) # save

                    cap0.release()
                    cap1.release()
                    break # break while loop
            ratelimit_image = resp.headers.get("X-RateLimit-ImageLimit") # max image upload at 1hour
            ratelimit_image_remaining = resp.headers.get("X-RateLimit-ImageRemaining") # image upload remaining

            #カメラ0
            files0 = {'imageFile': open(images0,'rb')}
            # message0 = '\nカメラ0\nImage_{}\nうp残 : {} / {}\n撮影周期 : {} 分\nエラー時リトライ回数 : {} / {}\nリトライ周期 : {} sec'.format(ImageNum,ratelimit_image_remaining,ratelimit_image,round((t0-old)/60,2),Trycnt,MAXTRY,Try_period)
            message0 = '\nカメラ0\nImage_{}\nうp残 : {} / {}\n撮影周期 : {} 分'.format(ImageNum,ratelimit_image_remaining,ratelimit_image,round((t0-old)/60,2))
            data0 = {'message': message0}
            requests.post(api,headers = headers,data = data0,files = files0)
            
            time.sleep(resp_time)

            #カメラ1
            files1 = {'imageFile': open(images1,'rb')}
            message1 = '\nカメラ1\nImage_{}'.format(ImageNum)
            data1 = {'message': message1}
            requests.post(api,headers = headers,data = data1,files = files1)
            
            # for debug
            print(message0)
            print(message1)
            print(datetime.datetime.now())
            print('main loop : {} sec'.format(debug1 - debug0)) # メインループ実行時間

            old = t0
            ImageNum = ImageNum + 1
        Trycnt = 0 # プログラムに復帰したらトライカウントをリセット



except KeyboardInterrupt:
    print('\nプログラム終了!!')
    sys.exit()

実働で調整したこと(関係ないことも含む。つまり何でもあり)

実際の現場で動かさないと見えてこないことが沢山ありました

アジャイル開発って大事

Wi-Fiのスリープ回避(一応)

10分おきに動かしていたらネットワークに繋がらない的なメッセージでエラー停止していました

上記に習い、/etc/dhcpcd.exit-hookは無かったのでsudoで新規作成したんですが、ラズパイ側ではなく、インターネット側(iPhone)が原因でした

開発中は、iPhoneのテザリングでインターネットに出ていたのですが、iPhoneは本体を触り続けていないとテザリングが切れるような挙動をします(触った感じがそうでした)

試験的に、寝てから朝起きるまで動かしましたが、Androidスマホ(V20Pro)でテザリングした時は切れませんでした

とは言いつつ、ラズパイもスリープしては困るので設定は残してあります

シャッター速度を長めに

こちらの記事でも書きましたが、安い古いWEBカメラはシャッター速度を長くして明るさを確保しないと取得画像が真っ暗になります

ですので、撮影前の数秒だけ起動して明るくなったら撮影しています

「明るくなったら」は取れた画像を見て後から判断しているので、シャッター速度の設定値はトライ・アンド・エラーです

プログラム中では、以下が該当します

if(shutter_t1 - shutter_t0) > shutter_time: # shutter time

カメラと解像度

プログラムでは2台のカメラを認識していますが、それぞれ違う製品です

アスペクト比が違ったりします(今回はたまたま一緒で4:3です)

きれいに写したいので、最大の解像度としたいのですが、LINE Notifyの最大解像度は1024です(こちらに書いてあり↓)

このピンクelecomカメラが4:3なので 1024:768 が最大となります

なので、OpenCVでこのサイズにリサイズしてから送信しています

もう一台買った広角バッファローも 1920 * 1080 なので 4:3 になります

今回は未使用ですが、暗視もできれば面白いと思って買ったIRカメラは 1280 * 720 なので 16:9 になります

これは調整が必要になります

systemd の restart

エラー処理はsystemdのrestart機能を使っていることはこちらで書きました↓

systemdのrestartはプログラムを自動で再実行してくれるので助かります

systemd の Unit ファイルの [Service] のセクションでは Restart という設定項目があり1、これを使うことでプロセスが勝手に終了しても自動でプロセスを再起動させることができます。 なお、systemctl stop のように systemd のコマンドを使って終了させた場合は再起動しません。

家のWi-Fiを切り、プログラムエラーを起こさせたのち、Wi-Fiを復帰させたらプログラムが再起動して普通の動くことが確認できました

プログラムにはリトライ処理を実装してみましたが、動かしてみてsystemdのrestartを使うことにしました。

プログラム中にはリトライ処理の記述は残してあります。

このあたり↓

# リトライ関連
# デバッグ時は短く設定 エラー行数が出ないのが惜しい
MAXTRY = 0 # トライ回数 20回
Try_period = 1 # トライ周期 sec 1分
Trycnt = 0 # トライカウント

コメントアウトしても良いですし、回数を少なく周期を短くしておけば無いも同然なのでそのままにしています(名残)

systemd ログ

ちゃんと再起動したか(機能しているか)が確認できます

そしたら systemd のログは /var/log/messages に書かれているので、tail -f で開いて中身を確認します。

tailf /var/log/messages

一番下に、プロセスが終了したこととプロセス再起動したことがログにあれば、自動的にプロセスが再起動したことが確認できます。

systemd の restart の回数

制限があり、10秒間に5回まで再起動するがそれ以降は諦めちゃうようです

なお、短い時間で何回も再起動が発生すると再起動を諦める機能があります2。 具体的には StartLimitInterval の間に StartLimitBurst の回数だけ再起動が起きると、systemd は自動的に再起動するのを止めます。デフォルトでは 10 秒の間に 5 回まで再起動が行われ、それを超えると再起動を諦めます。

デフォルト値は /etc/systemd/system.conf に書かれています。

/etc/systemd/system.confはデフォルトのままなので、10秒間、5回です

それは困るので、起動時にtime.sleep(initwait)を実装して諦めないようにしました

initwait = 3

10秒では3秒*3回=9秒より、3回しか再起動できないため、再起動しつづける動きとなる

該当箇所↓

initwait = 3
time.sleep(initwait)

Wi-Fi は 2.4GHz で統一すべき

3B+からは5GHzに対応していますが、3Bまでは2.4GHzのみです

3Bと3B+の両方で開発しており、3B+で動いていたのに3Bで動かなかった理由はこれでした

プログラム一緒なのになぜ!!でしたが、SSHで作業だったのでなかなか気づけませんでした

ラズパイにディスプレイを繋いでGUIで見ればWi-Fiを拾ってないのは一目瞭然なんですがね

有線ならこの問題は発生しませんが、設置場所の自由度をあげるために無線運用です

デスクトップは入れていませんので、wpa_supplicant.confを作ってWi-Fiに接続します(別のPCで作ってから、ラズパイのSDに移します)

country=JP
ctrl_interface=DIR=/var/run/wpa_supplicant GROUP=netdev
update_config=1
network={
    ssid="2.4HGzのWi-FiのSSID"
    psk="Wi-Fiのパスワード"
}

電源電圧は5.1V程度が好ましい

パワーランプがついたり消えたり、なんか良く分からないけどエラーになる、などが生じていました

電源エラー(低電圧エラー)でした

とりあえず起動はしますが、電圧が低いとエラーのもとで振り回される原因になります

なので一般的な5VのUSB-ACアダプタではなく、ラズパイ用の5.1V品をあてがいました

電源エラーは消えたので一つ不安が消えました

これを使っています↓

安いんじゃないでしょうか

Type-Cなので、C-MicroB変換アダプタをつけて3B+に接続しています

電圧が低いのが原因ですので、一般的なUSB-ACアダプタで1Aや2Aなど、電流容量を上げてもだめです

しょうがなく一般的なアダプタを使うのであれば、電流容量が大きい方が電圧降下がすくなるなるので良いです

アダプタは世の中にたくさんあり、物によって電圧が高いのもあるので、色々試してみるのもありです(使ってみないと何とも言えませんが)

買った5.1V品は通常使用時で5.1~5.2V程度でしたので、重負荷でも5Vを切ることはなさそうな印象で安心できます

画像が重くてエラー

画像が重くてエラーになりました

カメラを2台使う環境です(画像を2枚送る)

このようにカメラ0、カメラ1の送信処理が連続しているところにウエイトを入れて、送信完了を待ってあげることで解決しました

該当箇所↓

resp_time = 1 # カメラ2台 resp間ウエイト(画像が重い)

# カメラ0
requests.post(api,headers = headers,data = data0,files = files0) # LINE Notity送信処理

time.sleep(resp_time)

# カメラ1
requests.post(api,headers = headers,data = data1,files = files1) # LINE Notify送信処理

実験当時は、iPhoneテザリングを使用していて、かつキャリア回線が低速モード(200kbps程度だったかな)になっていました

このため、送信が終わらずエラーになったのだと思います

1Mbpsくらい出ていれば全く問題ないので、普通の環境(固定回線、キャリアの回線)では気にすることは無いと思います

まあ、極限環境でも動作することが確認できたということで😇

長くなりましたが以上😁