West Gate Laboratory

人生を少しでも,面白く便利にするモノづくり

ラズベリーパイ使って宅配便の再配達を撲滅した話(総集編)

概要

外出時でも荷物を受け取れるように、ラズベリーパイを使って受取までのプロセスをほぼ自動化した話。

我が家がオートロックマンションのため、共同玄関をどうセキュアに開けるかがキモ。

背景

私は宅配便の受取が苦手である。

時間指定できるならまだマシだが、指定したその2~3時間どのタイミングで来るかもわからず、待っていなければならないのは苦痛である。

以前はコンビニ受け取りもしていたが、やはり段ボールを抱えて家まで帰るのは面倒である。(我が家は3件隣がコンビニだがそれでも面倒は面倒)

時間指定ができない宅配便などは、もう諦めて一度不在票入れてもらってその上で時間指定で受け取っていた。配達員の方々には申し訳ないがそうせざるを得ない。

日本の宅配便の約6件に1件が再配達

一人暮らし、共働きが多い現在、再配達率は相当多いのでは?と思い調べてみたところ、国土交通省の発表では平成31年度4月の段階で16%、都市部に限れば18%に上るそうである。

www.mlit.go.jp

5~6回に1回が再配達ということになる。正直もっと多いと思っていた。

配達業界もこれは問題と思っているのだろう、最近はAmazonなどで置き配も始まっている。

我が家はオートロックマンション

置き配はセキュリティ上不安な面も残るが、再配達問題の解決の一歩にはなるだろう。私も是非利用してみたい。だが、我が家はオートロックマンションのため、インターホンで「解錠」ボタンを押さないと共同玄関が開かない。共同玄関が開かないことには配達員は玄関にたどり着くことができない。これでは置き配してもらうこともできない。

f:id:kaname_m:20191222141738j:plain
我が家のインターホン。解錠ボタンを押さないと共同玄関は開かない。

そこで、ラズベリーパイを使ってインターホンを監視、共同玄関を解錠したりユーザに配達を通知するシステムを作ってみた。

インターホン監視システム概要

今回構築したシステムの概要は以下の通りである。あらかじめ配達がわかっている場合と、予定外の配達の場合で動き方が分かれる。

まずは予定された配達の場合。

時間指定便など、あらかじめ配達時間がわかっている場合は、事前にGoogle Calendarに配達予定を登録しておく。ラズベリーパイは配達予定を取得し、その時間帯にインターホンが鳴ったら共同玄関を開け、配達員を中に入れる。ラズベリーパイは同時にユーザへ配達の旨をプッシュ通知する。

f:id:kaname_m:20200126140827p:plain
予定された配達の場合
(2020年1月27日 19時50分追記)

「指定時間帯は誰でも入れてしまうセキュリティホールになる」とのコメントが多くありましたので,このモードは廃止しました.

次が予定外の配達の場合。

時間指定ができなかったものなど、事前にいつ届くかわからなかいものの場合、ラズベリーパイはインターホンを検知するとその通知とインターホンのモニタ画面をスマホへプッシュする。ユーザはモニタ画面を見て配達員と判断したら、ラズベリーパイに解錠コマンドを送る。今回、Flaskでラズパイ上にサーバを立て、そこにコマンドを送ることにした。

f:id:kaname_m:20200126140822p:plain
予定外の配達の場合

オートロックマンションの共同玄関はセキュリティのためにあるため、来る者拒まずすべて通していたらオートロックの意味はなくなってしまう。そのため、「事前にわかっている配達」か、「ユーザが判断して許可した配達」のみ、通すようにした。

なお、部屋の玄関までたどり着いたら、簡易宅配ボックスであるOKIPPAを使って受け取る。

www.okippa.life

ラズパイでインターホン音検知

このシステムのトリガは全てインターホン音である。なにはともあれ、ラズパイでインターホン音を検知する。

家に転がっていたウェブカメラのLogicool C270をラズパイに接続し、インターホンが押されたときの「ピロピロピロ・・・」という音を検知している。具体的には、音声データを常に取得、FFTで最大音量の周波数を取得し、インターホン音と一致するかを判定する、というもの。詳細は以下の記事を参照。なお、プログラミング言語にはPythonを使っている。

westgate-lab.hatenablog.com

インターホンの音を検知したら、同時にユーザのスマホでプッシュ通知を送っている。プッシュ通知にはPushbulletを使った。PushbulletはPython APIも用意されており、アカウントを作れば簡単にラズパイ→スマホへのプッシュ通知を実装できる。

westgate-lab.hatenablog.com

ラズパイで解錠ボタンを押す

インターホン音が検知できたら、次にサーボモータで解錠ボタンを押せるようにしてみる。サーボモータにはマイクロサーボSG92Rを使った。秋月電子にも売っている。pigpioを使い、pythonで動かしている。サーボモータはインターホンに強力両面テープで貼り付けた。解錠ボタンをちょうどよく押せる角度を調整するのがキモである。

ここの詳細は以下の記事を参照。

westgate-lab.hatenablog.com

ここまでできると、こういうことができるようになる。

ラズパイでGoogle Calendarから配達予定を取得する

次に、Google Calendarから配達予定を取得してみる。なお、予定自体はあらかじめユーザがスマホで設定する。 PythonからGoogle Calendar APIを使う手順はQuickstartが整備されているので、それに従って設定していけば良い。

developers.google.com

今回のシステムでは、CRONで1時間に1回Google Calendarを読みに行って新しい配達予定がないかを確認している。

読み取った予定をもとに「予定された配達」か「予定外の配達」かを識別している。

ここの詳細は以下の記事を参照。

westgate-lab.hatenablog.com

westgate-lab.hatenablog.com

ここまでくれば、一応「予定された配達」は自動で受け取れることができるようになる。

ラズパイ+ウェブカメラでインターホンのモニタを抽出、通知する

もし、予定外の配達だった場合、ユーザへインターホンのモニタ画像を通知する必要がある。今回は、OpenCVを使った。ただモニタを撮影してプッシュするとモニタ以外の無駄な領域が多いため、輝度値からモニタ部分のみを検出し、射影変換して向きを補正した上でPushbulletを使ってユーザへ通知している。

f:id:kaname_m:20200202221525p:plain
撮影した画像をそのままPushbulletで通知した場合

f:id:kaname_m:20200202221528p:plain
モニタ部分だけを抽出、向きを補正してPushbulletで通知した場合。こっちのほうが見やすい

ここの詳細は以下の記事を参照。

westgate-lab.hatenablog.com

westgate-lab.hatenablog.com

外出先からラズパイに接続されたサーボモータを操作する

さて、ここまで来たら、ユーザは通知されたモニタ画像を見て配達か否かを判断できる。もしこれが予定外の配達であると判断した場合は、ラズパイに解錠コマンドを送って共同玄関を開ける必要がある。しかし、ただコマンドを送るだけではない、「即座に判断し、即座に解錠コマンドを送る」必要がある。なぜならば、配達員はインターホンを鳴らして十数秒もすれば不在票を入れて帰ってしまうからである。

リモートでラズパイを操作するので有名なのはWebIOPiだが、これは基本GUIによる操作を前提としており、操作に時間がかかってしまう。今回のような用途には不向きである。

そこで、今回のシステムではFlaskを使ってラズパイにサーバを立ち上げ、そこにコマンドを送ることにした。

また、家のルータにポートマッピング設定を行い、Flaskで使うポートへのコマンドをラズパイに転送することで、外出先からのラズパイ操作を実現した。

ここまで来ると、スマホへの通知画面は以下のようになる。モニタ画像とともにプッシュされたURLを1タップすれば、サーボモータが動作、解錠ボタンが押され共同玄関が開く。

(2020年2月2日 追記) まとめのところにも同じ内容を追記しているが、解錠コマンドはインターホンが鳴った時のみ反応すれば良いため、インターホンを検知してから30秒間のみ、応答するようにしている。四六時中コマンドで開けられるわけではない。そもそもインターホンが鳴ってないときに解錠ボタンを押しても共同玄関は開かない。

f:id:kaname_m:20200202221521p:plain
配達時のスマホ通知画面。URLを1タップすれば共同玄関が開く

ここの詳細は以下の記事を参照。

westgate-lab.hatenablog.com

まとめ

ようやく、ここまででラズパイを使った再配達撲滅システムが完成した。

完成以来、再配達を依頼することはなくなった。ハッピーハッピー。

f:id:kaname_m:20200108231839p:plain
外出中でも荷物が受け取れた

(2020年1月27日 19時50分追記)

普段2,3人/日しか来ないブログのPV数がえげつないことになっていてビックリ・・・

上にも追記しましたが,「指定した時間帯は誰でも入れるセキュリティホールとなる」といったコメントが多くありましたので,このモードは廃止しました.

こうするとインターホンが鳴るとスマホに通知が飛び,その場でインターホンモニタを見てスマホから中央玄関を開けることもできるシステムになります.

なお,「解錠コマンド」はインターホンが鳴ってから30秒程度しかコマンドに応答しないようになっており,普段はコマンドを送ったところで何もしません.念の為.

(2020年2月9日 14時05分追記)

このシステムに対してアクセス制御を実装し、セキュリティ対策とした。 以下の記事参照。

westgate-lab.hatenablog.com

ESP32でBluetooth通信してるとASSERT_WARNが出る件

最近ESP-WROOM-32を買って弄っている。

ESP-WROOM-32にはBluetoothが搭載されているので、スマホなどBluetooth搭載機器と簡単に通信を行うことができる。

単純にBluetoothを試してみるだけなら、Arduino IDEファイル→スケッチ例→BluetoothSerial→SerialToSerialBTで手軽に通信することができる。

このスケッチ例を使って、ESP32とスマホとつないでBluetooth通信を試していたのだが、シリアルモニタにASSERT_WARN(1 9), in lc_task.c at line 5054というメッセージが定期的に生じる(ひどいと数秒に1回)。

別にメッセージが出るだけで、Bluetooth通信自体はできるのでいいっちゃいいのだが、かなり目障り。

f:id:kaname_m:20200120231420p:plain
Bluetooth通信時のシリアルモニタ。 一番下の行にASSERTメッセージが大量に。

対処方法

どうやら原因は使っていたノートPCのBluetoothドライバだったようだ。

自分のPCのデフォルトのドライバはRealtekのものだった。

f:id:kaname_m:20200120231423p:plain
デフォルトのBluetoothドライバ(赤枠)

こいつをWindows標準のドライバに変更すると症状が収まった。

ドライバを変更するには、デバイスマネージャーから件のドライバを右クリック→ドライバの更新→コンピューターを参照してドライバーソフトウェアを検索→コンピューター上の利用可能なドライバーの一覧から選択します をクリックする。

すると、以下の画面になるので、Generic Bluetooth Adapterを選択し、次へ、からインストールする。

f:id:kaname_m:20200120231427p:plain

バイスマネージャー上でドライバが切り替わっていればOKだ。これでASSERT_WARNメッセージは出なくなった。

f:id:kaname_m:20200120232421p:plain

ESP-WROOM-32(DevKitC)で0からLチカまで

先日タイトルのESP-WROOM-32を購入した。

akizukidenshi.com

私は今までずっとPICかRaspberryPiを使っていた。これまではその2つのどちらかで十分だった。 ただ、今回「PICよりも高速」かつ「RaspberryPiより低消費電力」なマイコンが欲しくなりESP-WROOM-32を使うことにした。

f:id:kaname_m:20200119181708p:plain
ESP-WROOM-32(DevKitC)

ESP-WROOM-32の主なスペックを抜粋すると、

  • WiFi:802.11 b/g/n
  • Bluetooth v4.2
  • デュアルコア Tensilica LX6
  • クロック:240MHz
  • SRAM:520KB
  • 電源電圧:2.2V~3.6V
  • 消費電流:平均80mA
  • 使用温度範囲:-40~85度

他にも色々あるが、とにかくWifiもBTも載っててクロックが240MHzも出せるすごいマイコン、という感じ。 さらに、こいつはArduinoの開発環境が使える。

今回初めてArduinoの開発環境に触ったのだが、あまりにあっという間に何でもできてしまうので拍子抜けしてしまった。

ESP32でLチカなんてこれまで数多の人間がネットに方法を載せているが、ボードのリビジョンや開発環境は日々更新されており、それによって手順も多少異なる(実際に微妙に人によって書いてある手順が異なる)ので、今日段階での手順を載せておく。

というわけで今回、今まで一度もArduinoを触ったことがなかった私がほんの数瞬でLチカできるようになるまでの備忘録。

環境は以下の通り。

ボード:ESP-WROOM-32(DevKitC)

PC:Windows10 64bit

ESP-WROOM-32で0からLチカ

まずは、Arduinoの開発環境(Arduino IDE)をインストールする。 以下の公式サイトから「Windows Installer, for Windows XP and up」をダウンロードしてインストールする。

www.arduino.cc

次にESP32のサポートパッケージをダウンロードする。ESP-WROOM-32はArduinoの公式ボードではないため、追加でボードの情報をIDEに入れる必要があるためだ。

espressifのGithubのStable release linkのURLをコピーする。

f:id:kaname_m:20200119182424p:plain
アンダーライン部分をコピーする

Arduino IDEを立ち上げ、ファイル→環境設定→追加のボードマネージャ のURLに貼り付け、OK

f:id:kaname_m:20200119182847p:plain
ArduinoIDEの環境設定画面

次にIDEのツール→ボード:hogehoge→ボードマネージャ を開いて、esp32で検索する。すると、esp32のパッケージが出てくるので最新バージョンを選んでインストールする。(2020年1月19日時点で最新バージョンは1.0.4)

f:id:kaname_m:20200119182426p:plain
ESP32のボードパッケージ

次に、ツール→ボードからESP32 dev moduleを選択。

ここまで来たらブレッドボードでLチカ用回路を用意する。実はこのESP-WROOM-32のDev-kitC、ブレッドボードに絶妙にうまくはまらない。普通に挿すと片側しかピンが取り出せないし、電源ラインをまたごうとすると0.5ピン分間隔がずれ、これも上手く挿せない。

そこで、ブレッドボードの一部を引っ剥がし、0.5ピン分ずらしてもう一度はっつけた。(以下の画像の赤矢印部分、少し隙間があるのがわかるだろうか) こうすることで両側のピンを使うことができる。

(なお、このE-CALLのブレッドボードはユニットごとにテープではっつけてあるだけなので力をかけて剥がせば外れる。ただし導通用金属片が抜ける場合があるので、慎重に)

f:id:kaname_m:20200119184433p:plain
少し改造したブレッドボードにESP-WROOM-32を挿す

あとはソースコードを書いてコンパイル、書き込みをする。 LチカをするならIDEのファイル→スケッチ例→01.Basics→Blink のコードを少し改造して、

// the setup function runs once when you press reset or power the board
void setup() {
  // initialize digital pin LED_BUILTIN as an output.
  pinMode(4, OUTPUT);
}

// the loop function runs over and over again forever
void loop() {
  digitalWrite(4, HIGH);   // turn the LED on (HIGH is the voltage level)
  delay(500);                       // wait for a second
  digitalWrite(4, LOW);    // turn the LED off by making the voltage LOW
  delay(500);                       // wait for a second
}

で良い。なお、PIN4とはDev-kitCに「4」と書いてあるピンのことだ。あとはマイコンボードに書き込めばOK。ボード上のスイッチ操作は不要である。

環境構築からLチカまで、本当に数瞬でできてしまう。PICでレジスタいじいじしていた身からすると、信じられない・・・。

本当に、やりたいことを1行書けばできてしまう世界が目の前にあるようだ。

Flaskを使って外出先からURL経由でRaspberryPiを操作する

はじめに

この記事はオートロックマンション用不在時荷物受け取りシステムの開発記事です。

westgate-lab.hatenablog.com

前回の記事で、WebIOPiを使って外出先からリモートでRaspberryPiにつないだサーボモータを操作した。

westgate-lab.hatenablog.com

WebIOPiではURLによるGPIOの操作をサポートしているが、HTTP POSTでアクセスする必要があった。 通常のURLによるアクセスはHTTP GETのため、前回はRaspberryPiに接続したサーボモータを操作するために一旦WebIOPiのGPIO操作画面を開き、そこからサーボモータを操作していた。

今開発しているオートロックマンション用不在時荷物受け取りシステムでは、配達員がインターホンを鳴らしてから不在を判断して持ち帰ってしまうまでの間に通知を受けたユーザが反応してサーボモータをリモート操作、共同玄関を解錠する必要がある。 WebIOPiではURLを表示してからどうしても2タップ必要になるため、そこの時間がもったいない。 また、WebIOPiを使ってGPIOを操作する場合はGUI画面でピン番号をタップすることになるが、短時間で焦って押すと間違って別のピンをタップしてしまう恐れもある。

f:id:kaname_m:20200109215354p:plain
WebIOPiのGPIO操作画面。ピンの番号をタップしてOn/Offを操作する

そこで、より短時間・確実にユーザがインターホン通知に反応するため、今回Flaskを導入してURL1タップで共同玄関をサーボモータで解錠するところまでやってみた。

なお、RaspberryPi 3B+、Python3.7.3を使っている。

Flaskとは

Flask(フラスク)は、Python用の、軽量なウェブアプリケーションフレームワークである。小規模で簡単なウェブアプリケーションを作るのに適しているらしい。

今回の場合は「URLタップ」→「サーボモータを操作」だけなので、このような用途に向いているようだ。

Flaskのインストール

python3を使っているので、

sudo pip3 install flask

でFlaskをインストール。

FlaskでHello World

ウィキペディアに載っているFlaskのサンプルコードをもとにHello Worldしてみる。

from flask import Flask
app = Flask(__name__)

@app.route("/")
def hello():
    return "Hello World!"

if __name__ == "__main__":
    app.run("0.0.0.0")

内容としては、app = Flask(__name__)でFlaskのインスタンスを作成し、app.run("0.0.0.0")でローカルサーバでアプリケーションを実行する。run()に何もIPを指定しないとローカルホストで動作するため外部からアクセスすることはできないが、"0.0.0.0"を指定すると自分以外の外部からアクセスすることが可能だ。(この段階ではポートマッピングしていないのでアクセスできるのは同じLAN内の端末のみ)

Flaskはポート番号5000を使うため、例えばルート/にアクセスしたい場合は、簡易的にはブラウザでhttp://[ローカルIP]:5000にアクセスすれば良い。例えば、以下はFlaskを動かしているRaspberryPiのローカルIPが192.168.0.6の場合。もちろん、これは同じLANに接続されている端末からだけアクセス可能だ。

f:id:kaname_m:20200112224247p:plain
FlaskのHello World実行結果

こんな感じに”Hello World!”できる。簡単。

加えて、例えばサーボモータを操作するURLを実装するには以下のようなコードを追加実装し、http://[RaspberryPiのIP]:5000/push_servo/にアクセスすれば良い。

@app.route("/push_servo/")
def push_servo():
    pushServo()     # サーボを動かす関数に飛ばす
    return "Push Servo!"

アクセスしたユーザには"Push Servo!"が返る。

こんな感じにして、アクセスするURLを返ることで自由な操作を実装、実行することが可能である。

Flaskをスレッド化

Flaskは大変便利なのだが、app.run()するとそこでブロックされてしまうため、その他の処理を行うことができない。 今回の場合は常にウェブカメラから音声データを取得してインターホン検知を行う必要があるが、それがブロックされてしまう。 そこで、Flaskの処理をスレッド化し、メインルーチンの処理をブロックしないようにする。スレッド化にはthreadingを使う。

from flask import Flask
import time
import threading

app = Flask(__name__)

@app.route("/push_servo/")
def push_servo():
    return "Push Servo!"

@app.route("/")
def hello():
    return "Hello World!"

if __name__=="__main__":
    rest_service_thread = threading.Thread(name='rest_service', target=app.run, args=('0.0.0.0',), kwargs=dict(debug=False))
    rest_service_thread.start()
        
    while True:
        print("Main routine!")
        time.sleep(1)

    rest_service_thread.join()

これを実行した上で例えばLANの別のPCからhttp://[RaspberryPiのローカルIP]:5000/http://[RaspberryPiのローカルIP]:5000/push_servo/にアクセスすると、

$ python3 flasktest.py
Main routine!
 * Serving Flask app "flasktest" (lazy loading)
 * Environment: production
   WARNING: This is a development server. Do not use it in a production deployment.
   Use a production WSGI server instead.
 * Debug mode: off
 * Running on http://0.0.0.0:5000/ (Press CTRL+C to quit)
Main routine!
Main routine!
Main routine!
Main routine!
Main routine!
Main routine!
192.168.0.4 - - [12/Jan/2020 22:54:19] "GET / HTTP/1.1" 200 -
Main routine!
Main routine!
Main routine!
Main routine!
192.168.0.4 - - [12/Jan/2020 22:54:23] "GET /push_servo/ HTTP/1.1" 200 -
Main routine!
Main routine!
Main routine!
Main routine!

上のように、メインルーチンをブロックせずにHTTPのリクエストを受けることができる。これで、ウェブカメラでインターホンを監視しながら同時にURLでユーザのリモート操作を受け付けられるようになった。

外出先からアクセスする

これまではLAN内での操作だったが、実際には外出先からRaspberryPiに接続されたサーボモータを操作したい。ここでは前回と同じく、ルータのポートマッピングの設定を行い外部からアクセス可能にする。

ポートマッピングの方法は前回の記事を参照。前回はWebIOPiのポート8000を設定していたが、今回のFlaskはポート5000なので、5000を設定すること。

westgate-lab.hatenablog.com

これで、例えば/unlock/というサーボモータを動かす機能を持ったURLを用意しておけば、http://[グローバルIP]:5000/unlock/というURLにユーザがアクセスすることで外出先からサーボモータを操作し、共同玄関を解錠する事が可能である。

さらに、このURLとインターホンの画面(以前の記事参照)をPushBulletで通知することで、ユーザはインターホン画面を確認したら1タップで共同玄関を解錠することが可能である。

PushBulletでの通知については、過去記事を参照。

westgate-lab.hatenablog.com

WebIOPiを使ってRaspberryPiのGPIOを外出先から操作する

はじめに

RaspberryPiに外出先から(LANの外から)スマホなどでアクセスできると工作の幅がグッと広がる。 例えば、GPIOにアクセスできるだけでも外からLEDを操作したり、リレーを操作したり、サーボモータだって操作できるだろう。

RaspberryPiに外からアクセスする方法にもいろいろあるが、今回は手軽な方法として、WebIOPiを使った方法を試してみる。

WebIOPiとは

webiopi.trouch.com

RaspberryPiに外部からアクセスしてGPIOなどを操作できるライブラリらしい。Python+RaspberryPi 2or3で動作し、REST APIもついてくる。

f:id:kaname_m:20200109212946p:plain
WebIOPiの概要(http://webiopi.trouch.com/より)

やはりこれもすでに試した人が多くいる。WebIOPiを使ってブラウザからRaspberry PiのGPIOを操作してみる | Developers.IOなどを参考に実装した。ここでも備忘録を兼ねて動作確認までの手順を述べる。

WebOIPiのインストール、動作確認

まず、WebIOPiをダウンロードする。2020/01/09現在の最新バージョンは0.7.1である。

ダウンロード・インストール手順はWebIOPiを使ってブラウザからRaspberry PiのGPIOを操作してみる | Developers.IOをそのまま実行して問題なし。

sudo systemctl start webiopiでWebIOPiサーバを立てたらまずはLAN内のPCやスマホからアクセスしてみる。

例えば、ブラウザからアクセスする場合はhttp://"RaspberryPiのIPアドレス":8000でアクセスできる。今はLAN内なのでIPアドレスは192.168...のような感じだろう。例えば、http://192.168.0.6:8000のような感じ。アクセスするとIDとパスワードが聞かれる。デフォルトはID:webiopi, pass:raspberryである。

最初に表示されるのは以下の画面。

f:id:kaname_m:20200109215352p:plain
WebIOPiアクセス画面

今回使うのはGPIO Headerである。クリックすると以下のIO一覧が見える。

f:id:kaname_m:20200109215354p:plain
GPIO Header

それぞれのピンの横に書いてあるのが現在のそのピンの属性(INなら入力、OUTなら出力)、数字の色が出力状態を表す(黒:L、黄:H)。 出力がOUTになっているピンなら、数字をクリックするだけでON/OFFが切り替わる。少なくともLAN内からのアクセスなら瞬時に切り替わる。

WebIOPiをRaspberryPiブート時に自動起動

基本的にWebIOPiはRaspberryPiを再起動する度にstartさせる必要があるが、面倒なので以下のコマンドでブート時に自動起動できる。

sudo update-rc.d webiopi defaults

逆に自動起動をやめたい場合は以下のコマンド。

sudo update-rc.d webiopi remove

REST APIで操作する

ピンの状態取得などをGET,POSTで行うことも可能だ。(以下IPアドレス192.168.0.6、GPIO21の場合)

状態取得

GET http://192.168.0.6:8000/GPIO/21/value

属性の変更(OUT:出力に変更)

POST http://192.168.0.6:8000/GPIO/21/function/out

ピンをH

POST http://192.168.0.6:8000/GPIO/21/value/1

ピンをL

POST http://192.168.0.6:8000/GPIO/21/value/0

ブラウザのURLバーからアクセスする場合はGETなので、状態取得はブラウザでできる。以下はGPIO21の状態を取得した場合だが、ただただ数字で0or1が帰ってくる。

f:id:kaname_m:20200109223754p:plain
GPIO21の状態取得

URLバーからPOSTはできないので、ChromeならAdvanced REST client - Chrome Web Storeなどの拡張機能を入れてPOSTの試験ができる。

LANの外からアクセスする

家の中だけでやっていてもつまらないので、外出先からいじりたくなる。そうなると、ルータの設定をいじる必要がある。

基本的に一般的な家の中なら、ルータで各機器にプライベートIPを割り当てているはずである。我が家の場合ならRaspberryPiに192.168.0.6を当てている。 外からアクセスする場合は、グローバルIPでアクセスするわけだが、グローバルIPはルータに割り当てられているわけであって、RaspberryPiには割り当てられていない。なので、グローバルIPだけではプライベートIPしか割り当てられていないRaspberryPiにはアクセスができない。

そこで、ルータのポートマッピング機能を使う。ポートマッピングとは、ルータが持つ機能の一つで、グローバルIPアドレスの特定のポートを、特定のプライベートIPアドレスの特定のポートに転送するものである。

今回の事例では、WebIOPiはポート番号8000を使っているので、グローバルIP宛でポート8000宛のパケットをRaspberryPiに転送してやることで外部からRaspberryPiにアクセスする。

ポートマッピングの設定は一般的にルータの管理画面からできる。

我が家はAtermのルータだが、詳細設定→ポートマッピング設定から設定できる。

f:id:kaname_m:20200109221755p:plain
ポートマッピング設定

これが設定できたら、http://"グローバルIP":8000でWebIOPiにアクセスできる。

なお、グローバルIPはルータ設定画面やアクセス情報【使用中のIPアドレス確認】などでわかる。 CUIグローバルIPを知りたい場合は、What Is My IP Address? - ifconfig.meのサービスを利用できる。

curl ifconfig.me

グローバルIPを取得できる。

WebIOPiのパスワードを変更する

外部からアクセスする場合は、パスワードがデフォルトのままでは不安である。 パスワードは以下の通り変えられる。

$ sudo webiopi-passwd
WebIOPi passwd file generator
Enter Login: webiopi
Enter Password: 
Confirm password: 

Hash: e70c940a189251e9cd4515b3a1a6c6f02aa05c744a456ce360fe14bf2c5c0353
Saved to /etc/webiopi/passwd

パスワードを変更したら、stop,startすること。これで反映される。

参考:Change Password

WebIOPiからサーボモータを操作する

上記の使い方は、あくまでGPIOのIN/OUT, H/Lを設定するものだった。外出先からWebIOPiを使って簡易的にサーボモータを操作できないだろうか?

今回は以下の方法で実装した。

  • GPIO20の出力状態(On/Off)をポーリングで監視する

  • WebIOPiでGPIO20の出力状態を変更する(Off→On)

  • ポーリングでGPIO20がOnになったことを確認したら、サーボモータを操作する関数を呼び出す

少しまどろっこしいが、WebIOPiを使って気軽にサーボモータをいじるならこれが楽ではないだろうか。 例えば、以下は常にGPIO20を監視して、WebIOPiでGPIO20をOnしたらサーボモータを動かすスクリプトだ。

import pigpio
import time

def servoPush():
    moveServo(SERVO_PUSH)
    time.sleep(SERVO_TIME)
    moveServo(SERVO_RELEASE)

def calcServoDuty(ratio):
    duty = 25000 + (120000 - 25000) * ratio
    return duty

def moveServo(ratio):
    duty = calcServoDuty(ratio)
    pi.hardware_PWM(PORT_PWM, SERVO_PERIOD, int(duty))

# pigpioの設定
pi = pigpio.pi()
PORT_PWM = 18
PORT_WEBIOPI = 20
pi.set_mode(PORT_WEBIOPI, pigpio.OUTPUT)

# サーボモータ設定
SERVO_RELEASE = 0.5
SERVO_PUSH = 0.35
SERVO_TIME = 0.2
SERVO_PERIOD = 50
pi.set_mode(PORT_PWM, pigpio.OUTPUT)
moveServo(0.5)


pi.write(PORT_WEBIOPI, 0)

while True:
    try:
        #WebIOPi polling
        if pi.read(PORT_WEBIOPI):
            pi.write(PORT_WEBIOPI, 0)
            servoPush()
        
        time.sleep(1)
    
    except KeyboardInterrupt:
        break 

(2020年1月10日追記)

上のコードでサーボモータを操作してみた。 スマホWiFiを切って、モバイル回線につないでおり、グローバルIPでアクセスしている。

Python+opencvでウェブカメラの画像からインターホンモニタ部分を抽出・射影変換する

はじめに

この記事はオートロックマンション用不在時荷物受け取りシステムの開発記事です。

westgate-lab.hatenablog.com

前回までに、インターホン音の検知、共同玄関の解錠、インターホンモニタの撮影、スマホへのプッシュ通知、Google Calendarとの連携などができるようになった。これで不在中に荷物が届いても、あらかじめ配達時間帯を指定しておけば自動で共同玄関が開き、家の玄関前まで配達員がたどり着ける。そこまで来たらOKIPPAなどの簡易宅配ボックスに荷物を入れてもらうことができる。その意味で最低限の機能は実装できたが、もう少し工夫していこうと思う。

今回は、ウェブカメラで撮影してスマホにプッシュする画像を改良する。

具体的には、前回はウェブカメラで撮影した画像をそのままプッシュしていたが、今回は加えて実質的に重要なモニタ部分のみをPythonで抜き出し、射影変換して補正した上でプッシュする。

もしカメラの位置が固定されているのなら座標固定で抜き出せばいいのだが、今回の環境ではウェブカメラはきちんと固定されておらず、衝撃などで微妙に位置がずれる恐れがあったので、毎回自動的にモニタを抽出することとした。

なお、RaspberryPi 3B+、Python3.7.3を使っている。

撮影画像からモニタ部分の抽出する

Pythonで画像処理をするのに今回OpenCVを使ったわけだが、やはり似たことをした人はいるもので、今回は以下の記事を参考にしつつ実装した。(というかほとんどそのまま)

qiita.com

備忘録を兼ねてこの記事でも方法を述べていこうと思う。

今回相手にする画像は我が家のインターホンの画像だ。共同玄関で呼び出されるとモニタが表示される。スマホにプッシュするとしたら全体は必要なく、中央のモニタ部分だけユーザに送れば良い

f:id:kaname_m:20200108224150p:plain
我が家のインターホンの画像(呼び出し中)

前回の記事に、ウェブカメラを使って撮影する方法を書いたが、それを使ってframeという変数にBGRの画像データが入っているものとする。

まずはデータを二値化する。

import cv2
import matplotlib.pyplot as plt

gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
ret, th = cv2.threshold(gray, 100, 255, cv2.THRESH_BINARY)
plt.imshow(th, cmap="gray")
plt.show()

f:id:kaname_m:20200108224153p:plain
二値化画像

明るい環境ではモニタ以外も二値化で抽出されてしまうが、このあとのfindContourとapproxPolyDPが最強なので気にせず進む。

次に二値化画像から輪郭を抽出する。抽出した輪郭の中から面積でインターホンモニタを選別し、四角形に近似する。計測してみたところ、インターホンモニタは面積が25000前後だったので、それ前後の輪郭があえばそれをモニタと見なした。

#輪郭検出
contours, hierarchy = cv2.findContours(th, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)

# 面積がインターホンモニタに近いもののみ選別
areas = []
for cnt in contours:
    area = cv2.contourArea(cnt)
    if 22500 < area and area < 27500 :
        print(area)
        epsilon = 0.1*cv2.arcLength(cnt, True)
        approx = cv2.approxPolyDP(cnt, epsilon, True)
        areas.append(approx)

img_contour = cv2.drawContours(frame, areas, -1,(255,0,0),3)
plt.imshow(img_contour)
plt.show()

f:id:kaname_m:20200108224156p:plain
インターホンモニタを抽出した

無事にモニタ部分を四角形で抽出することができた。

ここでは何をやっているかというと、findContoursで二値化画像から輪郭を抽出し、approxPolyDPで検出した輪郭をより少ない点数で近似している。epsilonは近似のパラメータである。正直これらの関数が最強なので、周りの照明環境が変動してもほとんど問題ない。

最後に、抽出したモニタ領域を射影変換して補正する。つまり、斜めから撮影した画像をあたかも正面から見たかのように変換する。

import numpy as np

# 射影変換
dst_size = [640,450]   # 射影変換後の画像サイズ
dst = []
pts1 = np.float32(areas[0])   # 抽出したモニタ領域の四隅の座標
pts2 = np.float32([[0,0],[0,dst_size[1]],[dst_size[0],dst_size[1]],[dst_size[0],0]])   # 射影変換後の四隅の座標

# ホモグラフィ行列を求め、射影変換する
M = cv2.getPerspectiveTransform(pts1,pts2)
dst = cv2.warpPerspective(frame, M, (dst_size[0],dst_size[1]))

plt.imshow(dst)
plt.show()

f:id:kaname_m:20200108224200p:plain
射影変換後のモニタ画像

無事モニタ部分を射影変換することができた。

実際に配達員が来てインターホンを押した時にはこんな感じにスマホに通知される。このときは不在時だったが、無事に荷物を受け取ることができた。やったね。

f:id:kaname_m:20200108230925p:plain
実際の通知画像(スマホキャプチャ)

f:id:kaname_m:20200108231839p:plain
OKIPPAで荷物受け取り

RaspberryPiにつないだLogicool C270でインターホンのモニタを撮影し、呼び出し音に応じてスマホに写真をPUSHする

はじめに

この記事はオートロックマンション用不在時荷物受け取りシステムの開発記事です。

westgate-lab.hatenablog.com

前回までに、RaspberryPiにつないだウェブカメラ(Logicool C270、以下C270)でインターホン音を検知し、スマホに通知し、さらにGoogle Calendarと連携して配達指定時間帯の場合はサーボモータを操作して解錠するところまでいった。 不在時受取システムとしての最低限の機能はできたわけだが、今回はそれに加えて、インターホン音が鳴ったらメッセージだけでなく、C270で撮影したインターホンモニタの写真も一緒に送ってみる

この記事で述べることを具体的に列挙すると、

  • C270のカメラを使って撮影する

  • C270の露出を調整・固定してインターホンモニタに合わせる

  • C270のピントを調整する

となる。

なお、RaspberryPi 3B+、Python3.7.3を使っている。

何もせずC270でインターホンモニタを撮影する

Logicool C270は非常に安価なウェブカメラであることから、RaspberryPiと一緒に使っている人が多いようである。

今回は、昔買ったものが家に転がっていたのでそれを利用した。

デフォルト設定のC270はオート露出になっているため、画角内の輝度が大きく異なると思ったとおりに撮影できない場合がある。例えば、以下の画像は家のインターホンモニタをC270で撮影したものだが、モニタが周りに比べ輝度値が高いため、モニタが異常に明るく写ってしまっている

f:id:kaname_m:20200106222327j:plainf:id:kaname_m:20200106222330j:plain
我が家のインターホンをC270で撮影した画像。(画像1:部屋を暗くした場合、画像2:部屋を明るくした場合)

どのような照明環境でもモニタ部分をキレイに撮影するには、露出を固定する必要がある。

C270を固定露出にする

以下の記事を参考しつつ設定した。

qiita.com

まず、現状のC270の設定を確認する。コマンドはv4l2-ctl --list-ctrlsだ。デフォルトでは以下のような設定である。

                     brightness 0x00980900 (int)    : min=0 max=255 step=1 default=128 value=128
                       contrast 0x00980901 (int)    : min=0 max=255 step=1 default=32 value=32
                     saturation 0x00980902 (int)    : min=0 max=255 step=1 default=32 value=32
 white_balance_temperature_auto 0x0098090c (bool)   : default=1 value=1
                           gain 0x00980913 (int)    : min=0 max=255 step=1 default=0 value=32
           power_line_frequency 0x00980918 (menu)   : min=0 max=2 default=2 value=2
      white_balance_temperature 0x0098091a (int)    : min=0 max=10000 step=10 default=4000 value=3840 flags=inactive
                      sharpness 0x0098091b (int)    : min=0 max=255 step=1 default=24 value=24
         backlight_compensation 0x0098091c (int)    : min=0 max=1 step=1 default=1 value=1
                  exposure_auto 0x009a0901 (menu)   : min=0 max=3 default=3 value=3
              exposure_absolute 0x009a0902 (int)    : min=1 max=10000 step=1 default=166 value=671 flags=inactive
         exposure_auto_priority 0x009a0903 (bool)   : default=0 value=1

今回注目すべきはexposure_autoexposure_absoluteで、前者が露出のオート・固定を、後者が露出量を規定している。現状exposure_autoはvalue=3となっており、これはオート露出を表す。また、露出量はexposure_absoluteによるとvalue=671である。これらの値を固定露出(exposure_auto=1)、また露出量は任意の値に設定する。

pythonスクリプト中でv4l2-ctlのコマンドを打つ場合は以下のようにsubprocessを使う。以下は露出を200に固定する例。

import subprocess

cmd = 'v4l2-ctl -d /dev/video0 -c exposure_auto=1 -c exposure_absolute=200'
ret = subprocess.check_output(cmd, shell=True)
print(ret)

これで露出を固定できる。自分の環境でやると、なぜか設定後の露出量の値が設定値より5高い値になっていたが、特段問題ないので気にしない。

実際に撮影する際にはopencvを使うのが楽なのだが、設定後一度プログラムを再立ち上げしないと露光設定が反映されなかったり、不安定だった。結果として以下のようにVideoCapture(0)の前にコマンドを打てば、即座に反映されることがわかった。最後のDummyは、これがあったほうが設定の反映が安定していたので、とりあえず入れている。

import cv2
import subprocess

# opencv設定
cmd = 'v4l2-ctl -d /dev/video0 -c exposure_auto=1 -c exposure_absolute=200' # 露光時間をマニュアル設定
ret = subprocess.check_output(cmd, shell=True)
cam = cv2.VideoCapture(0)
if cam.isOpened() is False:
    print("Camera cannot open")
    sys.exit()
else:
    _,_ = cam.read()    # Dummy

# あとは好きに撮影する
ret, frame = cam.read()   # frameに画像データが入っている

上記のexposure_absoluteの値を任意に設定すれば、好きな露出設定で画像が取得できる。

例えば、下は部屋を真っ暗にした上でインターホンの通話ボタンを押すと出る青い画面を様々な露光設定で撮影した際の画像である。

f:id:kaname_m:20200106224635p:plain
exposure_absolute=150 やや暗すぎ

f:id:kaname_m:20200106224638p:plain
exposure_absolute=250 ちょうどいいくらい

f:id:kaname_m:20200106224643p:plain
exposure_absolute=500 明るすぎ

結局、exposure_absolute=200とした。

C270のピントを調整する

よくよく上の画像を見てみると、文字がぼやけておりピントが合っていない。C270はSkypeなどのWeb通話を想定して作られているため、デフォルトでは1m程度離れた位置でちょうどピントが合うようになっているためだ。今回はインターホンから30cm程度しか離れていない場所から撮影しているため、ピントがずれている。そこで、C270のピントを手動調整する。

C270のピント調整は簡単で、まずは正面のカバーをマイナスドライバー等でパカッと外し、内部のネジを外すと基板及びレンズが見える。 レンズについている歯車のようなものがピントの調整機構になっており、手で回すとピントが調整可能である。デフォルトでは一部に接着剤のようなものがついているため、ドライバー等ではがせばレンズを手で回せる。

f:id:kaname_m:20200106232313j:plain
C270正面カバーを外したところ。ネジが見える。

f:id:kaname_m:20200106232316j:plain
基板部分。赤矢印がピント調整ネジ。

C270のピント調整の更に詳細は以下の記事に詳しい。

tomokin-gadget.com

ピント調整をした結果が以下の通りである。わかりやすさのため、モニタ部分のみクロップしている。 文字部分が明らかに鮮明になった。

f:id:kaname_m:20200106232923p:plain
上:ピント調整前 下:ピント調整後

画像が取得できたら、jpgで保存する。画像ファイルをPushbulletでスマホにプッシュする方法は以下の記事で述べたとおりである。

westgate-lab.hatenablog.com

以上の設定をしてC270で撮影したインターホンモニタをPushbulletでスマホへ通知すると、以下のような感じになる。

f:id:kaname_m:20200106233555p:plain
Pushbulletで通知したインターホンモニタ

最後に

この記事では、Logicool C270を使って露出固定撮影をする方法、ピント調整をする方法と、それらを使ってインターホンモニタをキレイに撮影する方法を述べた。