ftakeのアイコン画像
ftake 2021年02月27日作成 (2021年04月14日更新)
製作品 製作品 閲覧数 2411
ftake 2021年02月27日作成 (2021年04月14日更新) 製作品 製作品 閲覧数 2411

郵便物チェッカーの制作

郵便物チェッカーの制作

はじめに

リモートワークで外出しない日々が続くと、郵便受けに郵便物があるかどうかをチェックするのが遅れがちな今日この頃です。そこで、郵便受けに郵便物があることを通知する装置を作ってみました。

郵便物の確認というと、例えば次のような方法がよく出てきます。

  • 郵便受けの扉部分が動いたことを検知する
  • カメラで内部を撮影して、画像認識する
  • 重さを測ることで郵便物があることを検知する

今回はシンプルに郵便受けの底面に設置したフォトリフレクター(赤外線を照射して、反射して返って来るとトランジスタに電流が流れるセンサー)を使って、フォトリフレクターの上に郵便物があることを検知することで実現することにしました。この方法では、どのような郵便物が入っているかは分かりませんが十分でしょう。高性能なマイコンボードや、高価なセンサー、大容量の電池が要らないメリットもあります。

郵便物を検知する仕組み

また、郵便受けは集合型で自室の外にあり、検知結果を無線で通知する必要があります。そこで、Bluetooth Low Energy (BLE) で飛ばして、室内に置いた Raspberry Pi でキャッチすることにしました。金属の箱の中からでも意外と届くものですね。(ただし、Raspberry Pi を郵便受けから近く電源も取れるトイレの窓際に設置)

今回は、基本的な仕組みの動作検証ということで、通知はターミナルに出すだけです。Raspberry Pi まで来てしまえば、IFTTT 経由で LINE にメッセージを送ったりとか、なんとでもできますね。連続稼働時間についても実用面では今後検討ですが、M5 Stack でどこまでいけるかを試します。

材料

  • M5 Stack Gray: 手元にあったので…。連続稼働時間を考えると変えた方がよさそう。
  • フォトリフレクタ コーデンシ SG-105: 検知可能距離が短いのでこちらを選びました。
  • 抵抗: 750, 470 k(調整必要)
  • 電源が自動で切れないモバイルバッテリ(ノベルティでもらったもの)
  • 適当なユニバーサル基板、銅線
  • 自己融着テープ
  • 段ボール

作り方

回路定数決め

ブレッドボード上で調整

まずは、ブレッドボードで実装して回路定数を決めます。電源は 3.3 V を使います。Vcc からではなく、GPIO 5 から電源を供給しているのは、測定時以外は OFF できるようにするためです。LED の抵抗はスマホのカメラで点灯しているのを確認しながら大きな抵抗にしていきます(数 mA で点灯するので、だいたいこのくらいの抵抗ですね)。抵抗を小さくし過ぎると LED が焼き切れたり、消費電力が増えるのはもちろんですが、天井から反射して返って来る量が増えるのも注意が必要です。

次に、重要なセンサー値の読み取りです。A/D 変換を使ったり、可変抵抗をつければ後から調整することもできるのですが、シンプルに GPIO で取り込むことにしました。そのためには郵便物があるときに、GPIO 2 の電圧がプログラムから読み取ったときに1になる電圧になるように R2 の抵抗値を調整します。反射しにくい濃紺の(某宅配レンタルの)封筒をセンサーの上にかざした状態で、R2 を増やしていった結果、750 kΩ になりました。

回路図

実装

ユニバーサル基板でこのような感じに実装しました。この配置で大丈夫?と思う方もいるかと思いますが、久しぶりの電子工作で、半田付けしている最中に配線間違いに気づいて裏側は残念な感じになっています(一応ノージャンパー)。

ユニバーサル基板での実装

郵便受けは金属なので自己融着テープで簡単に絶縁しました。

絶縁した基板

郵便受けの床の部分は段ボールで作りました。真ん中に穴を開けて取り付けます。フォトインタラプタは対象物から多少離れていた方が感度が良いため、床面から少し離したほうが良いです。段ボールの穴のサイズも余裕を持っておかないと、赤外線がフチで反射する可能性があります。

組み立て後

M5 Stack のプログラミング

M5 Stack で BLE は色々な人が実践していますが、今回はこちらを参考にしました。
https://pages.switch-science.com/letsiot/bleperiph/index.html

今回は、BLE でもブロードキャストモードを使用しています。ブロードキャストモードは自身をアドバタイズする(デーバイス名などを通知する)ときに、データを送る方法で、この場合は郵便物の状態を配ります。受信するデバイスとはペアリングしたり、コネクションを確立する必要はありません。ただし、ポストの状態は周りに丸見えになってしまうので注意が必要です。

このプログラムでは 30分に1回起きて、フォトリフレクタの赤外線を点灯して郵便物の有無をチェックします。その後、10秒間の間 Bluetooth で郵便物の状態を送ります。普通の Bluetooth デバイスはスマートフォンなどのデバイスからスキャンすると、これに応答する形でアドバタイズします。このプログラムでは M5 Stack 側から勝手に送るので、このスキャンに応答する必要がありません(setScanResponse(false))。

#include <Arduino.h>
#include <M5Stack.h>
#include <BLEDevice.h>
#include <BLEServer.h>


static BLEServer *pBLEServer;


static void setup_ble() {
  BLEDevice::init("Post Notifier");
  pBLEServer = BLEDevice::createServer(); 
}


void setup() {
  M5.begin();
  Serial.begin(115200);
  pinMode(GPIO_NUM_2, INPUT);
  pinMode(GPIO_NUM_5, OUTPUT);

  // スピーカーとディスプレイを止めておく
  pinMode(GPIO_NUM_25, OUTPUT);
  M5.Lcd.writecommand(ILI9341_DISPOFF);
  M5.Lcd.setBrightness(0);

  setup_ble();
}


static void advertise_data(char val) {
  BLEAdvertisementData advertisementData {};
  advertisementData.setFlags(0x06);
  std::string data;
  data += (char)0x04;   // Length
  data += (char)0xff;   // AD Type: Manufacture Specific
  data += (char)0xff;   // Company Identifier Code
  data += (char)0xff;   // Company Identifier Code
  data += val;          // Specific data
  advertisementData.addData(data);

  BLEAdvertising *pAdvertising = pBLEServer->getAdvertising();
  pAdvertising->setAdvertisementData(advertisementData);
  // Passive スキャンを使うので、スキャンに応答不要
  pAdvertising->setScanResponse(false);
  pAdvertising->start();

  delay(10 * 1000);

  pAdvertising->stop();
}


void loop() {
  digitalWrite(GPIO_NUM_5, HIGH);
  delay(50);
  auto input1 = digitalRead(GPIO_NUM_2);
  digitalWrite(GPIO_NUM_5, LOW);
  Serial.printf("%5d\n", input1);
  advertise_data(input1);

  M5.Power.lightSleep(SLEEP_MIN(30));
}

Raspberry Pi 側のプログラミング

Python で 10 秒タイムアウトで繰り返しスキャンを実行し、M5 Stack からのデータを拾います。BLE へのアクセスは bluepy というライブラリで簡単にできます。bluepy の説明は世の中にたくさんありますので今回は詳細を省きます。

スキャンは scan() 関数を呼び出すだけです。前述の通り、スキャンでアドバタイズを要求せずに受信するパッシブモードを使っています。これにより、M5 Stack 側で Scan Response に応答する必要がなくなります。

read_received_data() はスキャンして受信したデータから目的のデータを取り出します。スキャンすると周辺の BLE デバイスから色々と受信してしまいますので、設置した M5 Stack からのデータを見つけ、さらにポストの状態を取り出します。Manufacture Specific の3バイト目にデータが入っていますが、16進数の文字列として返って来るため4文字目と5文字目が目的のデータです。

import os
import urllib.parse
import urllib.request
from datetime import datetime
from time import sleep

from bluepy.btle import BTLEException, ScanEntry, Scanner


def main():
    start_time = datetime.now()
    print(f"{start_time} Post Notifier GW")

    last_updated = start_time
    prev_state = 0
    no_signal_notified = False

    scanner = Scanner()

    # 環境変数から設定の読み込み
    # デバイスのHWアドレス
    device_addr = os.environ["DEVICE_ADDR"]
    # LINE Notify の開発者用トークン
    line_notify_token = os.environ["LINE_NOTIFY_TOKEN"]

    while True:
        try:
            entries = scanner.scan(10.0, passive=True)
            now = datetime.now()

            received_data = read_received_data(entries, device_addr)
            if received_data is not None:
                # データ受信
                print(f"{now} status: {received_data}")
                if prev_state != received_data and received_data:
                    notify("郵便物が届きました", line_notify_token)
                prev_state = received_data
                last_updated = now
                no_signal_notified = False

            # 電池切れ通知
            if not no_signal_notified and (now - last_updated).seconds > 60 * 60 * 2:
                notify("2時間以上受信していません", line_notify_token)
                no_signal_notified = True

        except BTLEException as e:
            print(f"{datetime.now()} Error {e}")
            sleep(60)


def read_received_data(entries, device_addr):
    for entry in entries:
        entry: ScanEntry
        if entry.addr == device_addr:
            data = entry.getValueText(ScanEntry.MANUFACTURER)
            return int(str(data[4:6]), 16)
    return None


def notify(msg: str, token: str) -> None:
    api_url = "https://notify-api.line.me/api/notify"
    headers = {"Authorization": f"Bearer {token}"}
    data = {"message": msg}
    request = urllib.request.Request(
        api_url,
        headers=headers,
        data=urllib.parse.urlencode(data).encode())
    try:
        with urllib.request.urlopen(request) as response:
            print(f"{datetime.now()} [LINE Notify] msg: '{msg}'; response: {response.read()}")
    except Exception as e:
        print(f"{datetime.now()} [LINE Notify] Error {e}")


if __name__ == "__main__":
    main()

郵便物を見つけたら LINE Notify を使って LINE に通知します。自分の LINE アカウントに送るだけであれば、開発者サイト で開発者用のトークンを取得すれば簡単にできます。notify() 関数が LINE に送るコードです。API を1回叩くだけです。

取得した開発者用トークンは環境変数の LINE_NOTIFY_TOKEN にセットしておいて下さい。

出力はこのような感じです。03:59〜04:28 の間に投函されたことが分かります。

2021-04-10 03:29:17.942892 status: 0
2021-04-10 03:29:27.965471 status: 0
2021-04-10 03:58:51.928888 status: 0
2021-04-10 03:59:01.953031 status: 0
2021-04-10 04:28:35.967972 status: 1
2021-04-10 04:28:36.165504 [LINE Notify] msg: '郵便物が届きました'; response: b'{"status":200,"message":"ok"}'
2021-04-10 04:28:46.197977 status: 1
2021-04-10 04:58:20.200485 status: 1

おわりに

フォトリフレクタを使った郵便物チェッカーを作ってみました。

現状と今後の改造についても書いておきたいと思います。

  • バッテリーの持ち: 9.25 Wh のモバイルバッテリーで4日ほど持ちます。残念ながら M5 Stack は待機時の消費電力が大きいようで、スリープを Deep sleep にしても、検知間隔を増やしてもバッテリーの持ちは変わりません。マイコンを変える必要があるようです。
  • LPWA を使ってみる: 今回は BLE で届いてしまったのですが、当初の計画では Sigfox で飛ばしてみたいなと思っていました
  • ftake さんが 2021/02/27 に 編集 をしました。 (メッセージ: 初版)
  • ftake さんが 2021/03/14 に 編集 をしました。
  • ftake さんが 2021/04/05 に 編集 をしました。 (メッセージ: 仕組みについて、図と説明を差し替え)
  • ftake さんが 2021/04/12 に 編集 をしました。 (メッセージ: 説明を追加)
  • ftake さんが 2021/04/14 に 編集 をしました。 (メッセージ: LINE Notify 版)
ログインしてコメントを投稿する