箱罠の見回りがだるい!DIYで作る、箱罠が閉まったらメール通知して、ついでにXで狩猟体験を呼びかけツイートシステム

こんにちは、みなさん!今回は、私が最近取り組んだDIYプロジェクト、「箱罠センサーシステム」についてご紹介します。このシステムは、箱罠が閉まったときに自動的にメールとTwitterで通知してくれる優れものです。
ゆくゆくは箱罠が閉じた瞬間に狩猟体験を呼びかけて、明日急にイノシシを捌きたいって思った人にピンポイントでお知らせする、超ニッチシステムにしたいです。
SORACOMとLaravelを使用した実装方法をご紹介します!

目次

プロジェクトの概要

このプロジェクトの目的は、以下の機能を持つセンサーシステムを作ることでした:

  1. 箱罠が閉まったことを検知する
  2. 検知時にメールで通知を送信する
  3. 同時にXでツイートする

使用した材料と道具

今回のプロジェクトでは、SORACOMのドアの開閉キットを使用しました。具体的には以下のリンクにあるキットです:
SORACOM ドアの開閉キット

このキットには以下のものが含まれています:

  • SORACOM LTE-M Buttonプラス
  • マグネットセンサー
  • ジャンパーワイヤー
  • その他の必要な電子部品

このキットが大体12,000円ぐらいで、注文から1週間ぐらいしたら届きました。

システムの仕組み

  1. センサーの設置: 箱罠のドアにマグネットセンサーを取り付けます。
  2. SORACOM LTE-M Buttonプラスとの連携: センサーからの信号をSORACOM LTE-M Buttonプラスで受け取ります。
  3. SORACOM Beamの利用: UDP → HTTP/HTTPS エントリポイントを使用して、センサーデータをLaravelで構築したAPIに転送します。
  4. Laravel APIの処理: 受信したデータを処理し、メール送信とTwitter投稿を行います。

1に関してはまだ実施はしていなくて、今後行います。

プログラミングのポイント

SORACOM Beamの設定

SORACOM BeamのUDP → HTTP/HTTPS エントリポイントを使用して、センサーデータをLaravel APIに転送するのがポイントです。これにより、セキュアかつ効率的にデータを処理することができます。

SORACOM BeamはUDPを選択しましょう。SORACOM LTE-M ButtonはUDPでデータ送信するらしく、それを知らずにHTTPを設定して、動かないという迷宮にハマりました。
サポートにメールしたらその辺丁寧に教えてくれました。

こんな感じで設定して下さい。

Laravel APIの実装

Laravel側はAPIの受け口である
・TrapApiController

X側への認証とツイートする
・TwitterController
から成ります。

TrapApiController.php

<?php

namespace App\Http\Controllers\Api;

use App\Http\Controllers\Controller;
use App\Http\Controllers\TwitterController;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Mail;
use App\Mail\TrapNotification;
use App\Services\TwitterService;

class TrapApiController extends Controller
{
    //
    /**
     * @OA\Get(
     *     path="/api/traps/noti",
     *     @OA\Response(response="201", description="Create a new trap")
     * )
     */
    public function noti(Request $request, TwitterService $twitterService)
    {
        // ここに処理を追加します(例:新しいトラップを作成)
        Log::debug("noti_test");
        // .envからカンマ区切りのメールアドレスを取得
        $emails = explode(',', env('NOTIFICATION_EMAILS'));

        // Gmailを使ってメールを送信
        try {
            foreach ($emails as $email) {
                Mail::to(trim($email))->send(new TrapNotification());
            }
            Log::debug('Emails sent successfully');
        } catch (\Exception $e) {
            Log::error('Caught exception: ' . $e->getMessage());
        }

        // Twitterにツイート
        $tweetMessage = "箱罠にケモノが入ったぞ!狩猟体験したい人は千葉県富津市に集合だ!\n\n箱罠センサーのテストです。正常に動くようになったら、センサーが起動したら即狩猟体験会を開けるようにしたい。\n\n#狩猟体験 #ジビエ #狩猟初心者 #富津";

        // 現在のタイムスタンプを取得し、指定された形式にフォーマット
        $timestamp = date('Ymd H:i');

        // タイムスタンプをメッセージの最後に追加
        $tweetMessage .= "\n\n" . $timestamp;
        try {
            $twitterController = new TwitterController();
            $twitterController->tweet($tweetMessage);
            Log::debug('Tweet sent successfully: ' . $tweetMessage);
        } catch (\Exception $e) {
            Log::error('Caught exception when tweeting: ' . $e->getMessage());
        }
        return response()->json(['message' => 'Trap created successfully'], 201);
    }
}

TwitterController.php

<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use GuzzleHttp\Client;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Session;

class TwitterController extends Controller
{
    public function redirectToProvider()
    {
        $url = 'https://twitter.com/i/oauth2/authorize?response_type=code';
        $url .= '&client_id=' . env('TWITTER_CLIENT_ID');
        $url .= '&redirect_uri=' . urlencode(env('TWITTER_REDIRECT_URI'));
        $url .= '&scope=tweet.read tweet.write users.read offline.access';
        $url .= '&state=state&code_challenge=challenge&code_challenge_method=plain';

        return redirect($url);
    }

    public function handleProviderCallback(Request $request)
    {
        $code = $request->query('code');

        if (!$code) {
            return redirect('/')->with('error', 'Authorization code not found');
        }

        $client = new Client();
        $clientID = env('TWITTER_CLIENT_ID');
        $clientSecret = env('TWITTER_CLIENT_SECRET');
        $basicAuth = base64_encode("$clientID:$clientSecret");

        try {
            $response = $client->post('https://api.twitter.com/2/oauth2/token', [
                'headers' => [
                    'Authorization' => 'Basic ' . $basicAuth,
                    'Content-Type' => 'application/x-www-form-urlencoded',
                ],
                'form_params' => [
                    'code' => $code,
                    'grant_type' => 'authorization_code',
                    'redirect_uri' => env('TWITTER_REDIRECT_URI'),
                    'code_verifier' => 'challenge'
                ],
            ]);

            $data = json_decode($response->getBody(), true);
            // 既存のトークンを削除する
            DB::table('twitter_tokens')->truncate();

            // 新しいトークンを保存する
            DB::table('twitter_tokens')->insert([
                'access_token' => $data['access_token'],
                'refresh_token' => $data['refresh_token'],
                'created_at' => now(),
                'updated_at' => now(),
            ]);

            return redirect('/')->with('success', 'Logged in with Twitter');
        } catch (\Exception $e) {
            Log::error('Error obtaining access token: ' . $e->getMessage());
            return redirect('/')->with('error', 'Error obtaining access token');
        }
    }

    public function refreshAccessToken($refreshToken)
    {
        $client = new Client();
        $clientID = env('TWITTER_CLIENT_ID');
        $clientSecret = env('TWITTER_CLIENT_SECRET');
        $basicAuth = base64_encode("$clientID:$clientSecret");

        try {
            $response = $client->post('https://api.twitter.com/2/oauth2/token', [
                'headers' => [
                    'Authorization' => 'Basic ' . $basicAuth,
                    'Content-Type' => 'application/x-www-form-urlencoded',
                ],
                'form_params' => [
                    'refresh_token' => $refreshToken,
                    'grant_type' => 'refresh_token',
                    'redirect_uri' => env('TWITTER_REDIRECT_URI'),
                ],
            ]);

            $data = json_decode($response->getBody(), true);
            // 既存のトークンを削除する
            DB::table('twitter_tokens')->truncate();

            // 新しいトークンを保存する
            DB::table('twitter_tokens')->insert([
                'access_token' => $data['access_token'],
                'refresh_token' => $data['refresh_token'],
                'created_at' => now(),
                'updated_at' => now(),
            ]);

            return $data['access_token'];
        } catch (\Exception $e) {
            Log::error('Error refreshing access token: ' . $e->getMessage());
            return null;
        }
    }



    public function tweet($message)
    {
        $tokenData = DB::table('twitter_tokens')->first();
        if (!$tokenData) {
            return redirect('/login/twitter')->with('error', 'Please log in with Twitter');
        }

        $accessToken = $tokenData->access_token;

        if (!$accessToken) {
            return redirect('/login/twitter')->with('error', 'Please log in with Twitter');
        }

        $client = new Client();

        try {
            $response = $client->post('https://api.twitter.com/2/tweets', [
                'headers' => [
                    'Authorization' => 'Bearer ' . $accessToken,
                    'Content-Type' => 'application/json',
                ],
                'json' => [
                    'text' => $message,
                ],
            ]);

            $data = json_decode($response->getBody(), true);
            Log::debug('Tweet sent successfully', ['response' => $data]);
        } catch (\Exception $e) {
            if ($e->getCode() == 401) {
                // トークンが期限切れの場合、リフレッシュトークンを使用して再取得
                $accessToken = $this->refreshAccessToken($tokenData->refresh_token);
                if ($accessToken) {
                    // 新しいアクセストークンを使用して再試行
                    try {
                        $response = $client->post('https://api.twitter.com/2/tweets', [
                            'headers' => [
                                'Authorization' => 'Bearer ' . $accessToken,
                                'Content-Type' => 'application/json',
                            ],
                            'json' => [
                                'text' => $message,
                            ],
                        ]);

                        $data = json_decode($response->getBody(), true);
                        Log::debug('Tweet sent successfully', ['response' => $data]);
                    } catch (\Exception $e) {
                        Log::error('Caught exception when tweeting with new token: ' . $e->getMessage());
                    }
                } else {
                    Log::error('Failed to refresh access token.');
                }
            } else {
                Log::error('Caught exception when tweeting: ' . $e->getMessage());
            }
        }
    }
}

ざっくりコードを表示するとこんな感じです。

Larvel側のポイントはredirectToProviderメソッドを実行してXの画面から投稿を許可します。
それによりhandleProviderCallbackメソッドが呼ばれ、access_tokenとrefresh_tokenをdbに保存しておきます。
access_tokenが切れて401を出したら、refresh_tokenを使ってaccess_tokenを再取得するところですかね。

    実際の運用と結果

    動画ではドアの閉開センサーで、センサーが離れたらXにツイートをしています。

    注目すべき点は以下の通りです:

    1. SORACOM Beamを使用することで、セキュアなデータ転送が可能になりました。
    2. Laravelで構築したAPIにより、柔軟なデータ処理と通知システムを実現しました。
    3. Twitter APIの認証にOAuthを使用し、access_tokenとrefresh_tokenをDBに保存することで、長期的な運用を可能にしました。
    4. ツイート内容にタイムスタンプを追加することで、同一内容による投稿制限を回避しています。

    まとめと今後の展望

    まずは実際の実地前のシステムとしは開発が完了しました。

    今後の展望としては以下を考えています:

    1. この後は千葉県の富津の山にこもって実地テストを行います。
    2. 正しく撮れたかを判定するためにカメラも入れたい
    3. 1台12,000円は高いのでもっと安くできないか検討

    みなさんも、身の回りの課題をテクノロジーで解決する方法を考えてみてはいかがでしょうか?IoTとクラウドサービスを組み合わせることで、驚くほど強力なシステムが作れるかもしれません。

    それでは、また次回のDIYプロジェクトでお会いしましょう!

    よかったらシェアしてね!
    • URLをコピーしました!
    • URLをコピーしました!

    コメント

    コメントする

    目次