63
47

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

「Amazon Dash Button」が日本上陸したけどあえて自作してみた

Last updated at Posted at 2016-12-11

DashButtonCover.jpg

概要

先日「Amazon Dash Button」がついに日本でも発売になりましたね。
これ、要はボタンをポチッと押すと対象の商品がAmazonで自動注文されて自宅に届くというものです。
今回はこれを自作しちゃおう!という旨の記事です。

わかる人はわかると思いますが、正確には「Amazon Dash Button」のプログラム可能版となる「AWS IoT Button」という方が近いと思います。
※話題にのってタイトルは「Amazon Dash Button」としてみました。

全体構成

全体のアーキテクチャは次の通りです。

全体構成.png

  1. Raspberry Pi Zeroに取り付けたボタンを押すとMQTT経由でAWS IoTに接続します。
  2. AWS IoTのルールエンジンによりAWS Lambdaへメッセージをルーティングします。
  3. Lambda関数ではSelenium + PhantomJSよりAmazonのWebサイトをクローリングし、指定の商品を購入します。

処理の後ろから(3 -> 2 -> 1の順に)詳細に説明していきます。

1. Amazonで自動購入を行うLambda関数の作成

この章ではSeleniumとPhantomJSを利用してAmazonのWebサイトをクローリングし、指定の商品を自動購入するLambda関数を作成します。

AWS Lambdaとは?

http://docs.aws.amazon.com/ja_jp/lambda/latest/dg/welcome.html
AWS LambdaはコードをAWS Lambdaにアップロードすると、サービスがAWSインフラストラクチャを使用してコードの実行を代行するコンピューティングサービスとなります。

ランタイムにはJava, Node.js, Python, C#が選べますが、今回はPythonを利用します。

パッケージ構成

プロジェクトディレクトリ直下にLambda関数本体及び必要なライブラリを配置していきます。

$ tree -L 1
.
├── amzorderer.py				# Lambda関数本体
├── phantomjs					# PhantomJSバイナリ
├── selenium					# Python用Seleniumライブラリ
└── selenium-3.0.2.dist-info

参考: https://docs.aws.amazon.com/ja_jp/lambda/latest/dg/lambda-python-how-to-create-deployment-package.html

Selenium インストール

pipを利用してプロジェクトディレクトリ直下にSeleniumをインストールします。

pip install selenium -t /path/to/project-dir

PhantomJS インストール

PhantomJSの公式よりLinux 64bit版のtarをダウンロードし、binディレクトリ配下のphantomjsをプロジェクトディレクトリ直下に配置します。

Lambda関数の作成

ソースコードは以下でも公開しています。(追って公開予定)

amzorderer.py
# -*- coding:utf-8 -*-

__author__ = 'H.Takeda'
__version__ = '1.0.0'

import os
import boto3

from argparse import ArgumentParser
from base64 import b64decode
from selenium import webdriver
from selenium.webdriver.common.desired_capabilities import DesiredCapabilities

# Amazon top page url.
AMAZON_URL = "https://www.amazon.co.jp"
# Amazon user id (email).
AMAZON_USER = boto3.client('kms').decrypt(
    CiphertextBlob=b64decode(os.environ['user']))['Plaintext']
# Amazon user password.
AMAZON_PASS = boto3.client('kms').decrypt(
    CiphertextBlob=b64decode(os.environ['password']))['Plaintext']
# User agent.
USER_AGENT = "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/53 (KHTML, like Gecko) Chrome/15.0.87"
# Item dictionary.
ITEMS = {
    "01": "1fLhF7q",    # Toilet Paper
    "02": "fhYcbp7"     # Saran Wrap
}


def lambda_handler(event, context):
    # Create web driver for PhantomJS.
    dcap = dict(DesiredCapabilities.PHANTOMJS)
    dcap["phantomjs.page.settings.userAgent"] = USER_AGENT
    driver = webdriver.PhantomJS(desired_capabilities=dcap,
                                 service_log_path=os.path.devnull,
                                 executable_path="/var/task/phantomjs")

    # Get amazon top page.
    driver.get(AMAZON_URL)
    # Transition to sign in page.
    driver.find_element_by_id("nav-link-yourAccount").click()
    # Input user id.
    driver.find_element_by_id("ap_email").send_keys(AMAZON_USER)
    # Input user password.
    driver.find_element_by_id("ap_password").send_keys(AMAZON_PASS)
    # Sign in.
    driver.find_element_by_id("signInSubmit").click()
    # Select item.
    driver.get("http://amzn.asia/" + ITEMS[event["item"]])
    # Add to cart.
    driver.find_element_by_id("add-to-cart-button").click()
    # Proceed to checkout.
    driver.find_element_by_id("hlb-ptc-btn-native").click()
    # Order.
    # driver.find_element_by_name("placeYourOrder1")[0].click()
    driver.save_screenshot("hoge.png")
    driver.quit()

基本的にシンプルなSeleniumの操作を行なっているだけですが、ポイントをピックアップします。

  • 関数のインプット(event)として購入する商品の区分値を受け取ります。
  • ログインID(Email)、ログインパスワードはLambdaの環境変数から複合化して取得します。
  • ユーザエージェントを設定しないとCookieを有効にしてくださいという旨のエラーが出てログインできません。
  • PhantomJSのログ( ghostdriver.log )がデフォルトだとカレントディレクトリに作成されるので出力しない(/dev/null)ようにします。
    ※Lambda上で動かすと権限エラーでファイル作成できないため。

Lambda関数のデプロイ

アーカイブ

プロジェクトディレクトリをzip化します。
zipファイルの名称は適当で構いません。

$ zip -r upload.zip /path/to/project-dir/*

Lambda関数設定

AWSの管理コンソールからポチポチLambda関数の設定を行います。
作成したzipファイルのアップロードもここで行います。

Lambda Management Console.png

  • 環境変数に「user」と「password」を定義し、IAMにてあらかじめ作成しておいた暗号鍵(lambda/amzorderer)で暗号化します。
  • IAMにてLambda関数に付与するロールはあらかじめ作成しておきます。
  • メモリは128MBだと苦しかったので256MBにしています。

Lambda関数テスト

Actions > Configure test eventよりテストイベント(Lambda関数に引き渡すパラメータ)を設定し、
Testボタンを押下してLambda関数を実行します。

Lambda Management Console2.png

正常に処理が完了すればOKです。
ここまででAmazonで自動購入を行うLambda関数の作成は完了です。

2. AWS IoTの設定

この章ではAWS IoTを利用して、MQTTのリクエストを受付け、上で作成したLambda関数を呼出す設定を行います。

AWS IoTとは?

https://aws.amazon.com/jp/iot/how-it-works/
AWS IoTを利用することでさまざまなデバイスをAWSの各種サービスに接続し、データと通信を保護し、デバイスデータに対する処理やアクションを実行することが可能になります。

デバイスの登録

AWS IoTのコンソール画面よりAWS IoTに接続するデバイスの登録を行います。

  1. 「Get started」より登録画面へ進みます。

AWS IoT0.png
 
2. クライアントデバイスの環境情報を選択します。今回はRaspberry Pi Zero(OS:Raspbian)からPython SDKを利用してAWS IoTへ接続します。

AWS IoT4.png
 
3. 任意のデバイス名(ここではraspi0)を入力し「Next step」を押下します。

AWS IoT5.png
 
4. 「Liux/OSX」を押下して、公開鍵、秘密鍵、クライアント証明書をダウンロードしておきます(後で使います)。「Next step」を押下して次画面へ進みます。

AWS IoT7.png
 
5. デバイスの設定手順が出てくるので、「Done」を押下して完了となります。

ルールの登録

特定のトピックにメッセージがPublishされたら、Lambda関数を呼出すよう設定します。

  1. 「Create a rule」より登録画面へ進みます。

AWS IoT R1.png
 
2. 任意のルール名(ここではamzorderer)を入力し、ルールクエリを設定します。
「amzordere」トピックにメッセージがPublishされた場合、item属性の値を抽出し、Actionを実行します。

AWS IoT R2.png
 
3. ActionとしてLambda関数の呼出しを選択します。

AWS IoT3.png
 
4. 呼び出す関数は先ほど作成した「amzorderer」となります。

AWS IoT R4.png
 
5. 「Create rule」を押下して作成完了となります。

AWS IoT R.png

3. クライアントデバイスの作成

この章ではRaspberry Piを利用してボタンを持つクライアントデバイスの作成していきます。

Raspberry Pi Zeroとは

言わずと知れたラズパイの小型モデルになります。
CPUクロック1GHz、メモリ512MBで約5ドルと破格の価格設定になってます。
今回はこのRaspberry Pi Zeroを利用して、
ボタンを押したらAWS IoTにリクエストを投げるデバイスを作っていきたいと思います。

必要なもの

どのご家庭にもある基本的な電子工作部品をつかって作っていきます。

IMG_0307.JPG

  • Raspberry Pi Zero
  • GPIOピン
  • ブレットボード
  • 抵抗(10KΩ)
  • タクトスイッチ
  • ジャンパワイヤ(オス-メス)× 3
  • 無線LAN子機
  • はんだ(0.6mm推奨)
  • miniUSB変換ケーブル

miniUSB の端子が一つしかないのでUSBハブがあると初期設定時は便利です。

Raspberry Pi の初期設定(OSインストール)

ここら辺を参考にRaspbian(Jessie)をインストールします。

無線LAN接続

今回はBuffaloの無線子機を利用してRaspberry Pi Zeroを無線LANに接続します。

USBに無線LAN子機を接続します。
lsusb コマンドを打つと子機を認識しているのがわかりますね。

$ lsusb
Bus 001 Device 002: ID 0411:01a2 BUFFALO INC. (formerly MelCo., Inc.) WLI-UC-GNM Wireless LAN Adapter [Ralink RT8070]
Bus 001 Device 001: ID 1d6b:0002 Linux Foundation 2.0 root hub

wpa_passphrase コマンドを使い、無線LAN接続に必要なSSID、パスワードを生成します。

$ wpa_passphrase [SSID] [パスフレーズ]
network={
        ssid=[SSID]
        #psk=[パスフレーズ] <- この行は削除して構いません
        psk=[暗号化されたパスフレーズ]
}

上記のテキストをコピーし、/etc/wpa_supplicant/wpa_supplicant.conf に追記します。

/etc/wpa_supplicant/wpa_supplicant.conf
country=GB
ctrl_interface=DIR=/var/run/wpa_supplicant GROUP=netdev
update_config=1
network={
        ssid=[SSID]
        psk=[暗号化されたパスフレーズ]
}

/etc/dhcpcd.conf を編集し、固定IP化も行なっておきます。
IPアドレス、ルーター、DNSは適宜自分の環境に合わせて設定してください。

interface wlan0
static ip_address=192.168.11.30/24
static routers=192.168.11.1
static domain_name_servers=192.168.11.1

再起動して、母艦からSSH接続できれば設定完了です。

$ sudo shutdown -r now

GPIOのハンダ付け

他のモデルと違いRaspberry Pi ZeroでGPIOを使う場合はハンダ付けが必要となります。
思ったより細かい作業になるのでハンダの線形はφ0.6mmのものを使うと良いでしょう。

IMG_0309.JPG

回路を組む

正直電子工作はど素人ですが、ネットで調べた情報を参考に組んでいきます。
Raspberry Pi ZeroのGPIOの配置は次のようになっています。

Raspberry-Pi-Model-Zero-Mini-PC.jpg

1番から3.3Vの電源をとり、GPIO25タクトスイッチのON/OFFはGPIO9番で受け取ります。
GPIO25とGNDの間に10kオームの抵抗を入れます。これはプルダウン抵抗と呼ばれHIGH(3.3V)かLOW(0V)信号を確実に伝える役割を担います。
画像が分かりづらくてすみません。

S__2564101.jpg

プログラムを作成する

回路が組めたのでタクトスイッチの入力を受取り、AWS IoTにメッセージをPublishするプロフラムを作成します。

ランタイム

Raspbian(Jessie)にはPython2.7がインスインストールされているので、Pythonでプログラムを書いていきます。

$ python -V
Python 2.7.9

必要なライブラリのインストール

AWS IoTに接続するためのPythonSDKが公開されているので利用します。

$ sudo pip install AWSIoTPythonSDK

実装

publisher.py
# -*- coding:utf-8 -*-

__author__ = 'H.Takeda'
__version__ = '1.0.0'


from AWSIoTPythonSDK.MQTTLib import AWSIoTMQTTClient
from argparse import ArgumentParser
import json
import logging
import RPi.GPIO as GPIO
import signal
import sys
import time


def configure_logging():
    # Configure logging
    logger = logging.getLogger("AWSIoTPythonSDK.core")
    logger.setLevel(logging.DEBUG)
    streamHandler = logging.StreamHandler()
    formatter = logging.Formatter(
        '%(asctime)s - %(name)s - %(levelname)s - %(message)s')
    streamHandler.setFormatter(formatter)
    logger.addHandler(streamHandler)


def parse():
    argparser = ArgumentParser()
    argparser.add_argument("-e", "--endpoint", type=str, required=True)
    argparser.add_argument("-r", "--rootCA", type=str, required=True)
    argparser.add_argument("-c", "--cert", type=str, required=True)
    argparser.add_argument("-k", "--key", type=str, required=True)
    args = argparser.parse_args()
    return vars(args)


def careate_client(endpoint, root_ca, cert, private_pey):
    # For certificate based connection.
    client = AWSIoTMQTTClient("raspi0")
    # Configurations
    client.configureEndpoint(endpoint, 8883)
    client.configureCredentials(root_ca, private_pey, cert)
    client.configureOfflinePublishQueueing(1)
    client.configureConnectDisconnectTimeout(10)    # 10 sec
    client.configureMQTTOperationTimeout(5)         # 5 sec
    return client


def handler(signum, frame):
    print "Signal handler called with signal", signum
    client.disconnect()
    GPIO.cleanup()
    sys.exit(0)


if __name__ == '__main__':
    # Parse command-line arguments.
    args = parse()
    # Configure logging
    configure_logging()
    # Create mqtt client.
    client = careate_client(
        args["endpoint"], args["rootCA"], args["cert"], args["key"])
    # Connect.
    client.connect()

    signal.signal(signal.SIGINT, handler)
    GPIO.setmode(GPIO.BCM)
    GPIO.setup(9, GPIO.IN)
    before = 0
    while True:
        now = GPIO.input(9)
        if before == 0 and now == 1:
            # Create message.
            message = {"item": "01"}
            # Publish.
            client.publish("amzorderer", json.dumps(message), 0)
            print "message published."
        time.sleep(0.1)
        before = now

RPi.GPIO を利用してGPIO9番に入力があった場合にAWS IoTにメッセージ(トイレットペーパーを購入するためのアイテム区分値「01」)をPublishしています。

動作確認

Rapsberry Pi Zero上で上記のスクリプトを実行し、動作確認してみます。
スクリプトの引数にAWS IoTのエンドポイント、ルートCA、クライアント証明書、秘密鍵のパスを指定します。
AWS IoT SDKのREADMEに記載されていますがルートCAはこちらから取得します。
クライアント証明書とは秘密鍵はAWS IoTの設定時にダウンロードしたzipに含まれています。

$ python test.py -e <endpoint> -r <rootCA path> -c <certificate path> -k <private key path>
2016-12-11 08:15:31,661 - AWSIoTPythonSDK.core.protocol.mqttCore - DEBUG - Paho MQTT Client init.
2016-12-11 08:15:31,664 - AWSIoTPythonSDK.core.protocol.mqttCore - INFO - ClientID: raspi0
2016-12-11 08:15:31,667 - AWSIoTPythonSDK.core.protocol.mqttCore - INFO - Protocol: MQTTv3.1.1
2016-12-11 08:15:31,672 - AWSIoTPythonSDK.core.protocol.mqttCore - DEBUG - Register Paho MQTT Client callbacks.
2016-12-11 08:15:31,675 - AWSIoTPythonSDK.core.protocol.mqttCore - DEBUG - mqttCore init.
2016-12-11 08:15:31,680 - AWSIoTPythonSDK.core.protocol.mqttCore - DEBUG - Load CAFile from: root-CA.crt
2016-12-11 08:15:31,683 - AWSIoTPythonSDK.core.protocol.mqttCore - DEBUG - Load Key from: raspi0.private.key
2016-12-11 08:15:31,687 - AWSIoTPythonSDK.core.protocol.mqttCore - DEBUG - Load Cert from: raspi0.cert.pem
2016-12-11 08:15:31,691 - AWSIoTPythonSDK.core.protocol.mqttCore - DEBUG - Custom setting for publish queueing: queueSize = 1
2016-12-11 08:15:31,696 - AWSIoTPythonSDK.core.protocol.mqttCore - DEBUG - Custom setting for publish queueing: dropBehavior = Drop Newest
2016-12-11 08:15:31,699 - AWSIoTPythonSDK.core.protocol.mqttCore - DEBUG - Set maximum connect/disconnect timeout to be 10 second.
2016-12-11 08:15:31,704 - AWSIoTPythonSDK.core.protocol.mqttCore - DEBUG - Set maximum MQTT operation timeout to be 5 second
2016-12-11 08:15:31,710 - AWSIoTPythonSDK.core.protocol.mqttCore - INFO - Connection type: TLSv1.2 Mutual Authentication
2016-12-11 08:15:32,384 - AWSIoTPythonSDK.core.protocol.mqttCore - INFO - Connected to AWS IoT.
2016-12-11 08:15:32,386 - AWSIoTPythonSDK.core.protocol.mqttCore - DEBUG - Connect time consumption: 70.0ms.

タクトスイッチを押すと、AWS IoTにメッセージがPublishされ無事にAmazonで「トイレットペーパー」の購入が完了していました。

やりたかったこと

  • クライアントデバイスの小型化
    今回電源はモバイルバッテリーを利用していますが、リチウムポリマーを利用して小型化したい。ブレットボードも小さいものを利用したい。

  • 複数ボタン対応
    ボタン①を押したときはトイレットペーパー、ボタン②を押したときはサランラップを購入というように、複数ボタン設けたい。

  • ボタンのケース作成

63
47
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
63
47

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?