iOS
RaspberryPi
casperJs
BLE

Raspberry Pi Zeroを使ってAmazonの商品を即購入するデバイスを作る

More than 1 year has passed since last update.

v1.0: 新規作成(やったことだけ記載)

v1.1: 12/24時点の変更 → フロント部分の実装までを追記


はじめに

ついこの前, Amazonから日常生活に利便性をもたらすボタンが発売されましたね!

仕事柄そういった話題がすぐ入ってきて, 周りでも賑やかに話をしていて技術の良さをまた実感したところです(´∀`)

そんな中, どうやら既にハックしている人たちもいるようで, ボタンからBLE経由で送信されたMACアドレスを拾うことで単純なイベントトリガーとして扱い, 処理だけ実装すれば自分がやりたいことをボタンポチーだけで実現できちゃうとか💡 (Slackにメッセージ送るとか, ピザ買う人とかいましたね笑)

でもこの技術の良さをもっと味わうには, ハードウェアの部分も作ったらいいんじゃないかなぁと思い, せっかく Raspberry Pi Zero を持っているのでボタンから作ることにしました!

前置きはこのくらいにしておいて記事の本題に移ります!


作るもの


  • ボタンデバイス

  • Amazonの1-Click実行スクリプト(即購入のやつです)

  • 購入完了通知用iOSアプリ


利用するハードウェア・ソフトウェア


Raspberry Pi Zero

Pi Zero ではGPIOでボタン制御&BLEモジュール制御をおこないます


CasperJS

本来はUIテストやスクレイピングなどにも使えるライブラリですが, ベースとなるのはブラウザの自動操作なのでAmazonの1-Clickボタンを押すこともできます

もちろん PhantomJS でもできますが, そちらは触ったことがないので採用していないです


使った道具

なかなかに多い笑


  • Raspberry Pi Zero

  • テザリング用 iPhone6

  • Bluetooth Low Energyモジュール IMBLE

  • IMBLE専用アダプタ

  • LED付き押しボタンスイッチ

  • コネクタなど


    • スマホ充電用ケーブル

    • USBメス-miniUSBオス変換アダプタ

    • USBハブ(4穴)

    • HDMIメス-MiniHDMIオス変換アダプタ

    • キーボード

    • 無線LAN子機



  • 電子部品


    • ブレッドボード

    • 1kΩ抵抗(1/2W炭素皮膜)

    • ジャンパーワイヤー(オス・メス, オス・オス, メス・メス)

    • 2.54mm10ピンヘッダ

    • 半田

    • 半田ごて

    • 半田ごて立て




システム構成

本来ならばサーバを立てて, どこからでも商品購入できるようにしようとしたのですが, 時間が足らず断念しました...

簡易的な構成図を以下に示します!


本来の構成

PiZeroDashButton.001.jpeg


今回の構成

PiZeroDashButton.002.jpeg

ということで, 商品を即購入&BLE経由で購入完了通知できるような仕組みで説明します!


実装


ボタンの作成


半田付け

まず Pi Zero にはピンヘッダがついてません😱

ということで最初は半田付けからはじまるんですね〜〜〜〜(これが一番時間かかった)

普通の Raspberry Pi とは違って40ピン構成ですが, 言うて40ピンあるし久しぶりにやったし失敗したから2周したしで散々でした...

IMG_0983.JPG

うーん, 汚い!www

あと一応共有しておくと, 基盤を溶かしてしまったり半田がピンヘッダ外の基盤上にくっついてしまってもちゃんと動くので Pi Zero は結構強いやつです\(^o^)/

とはいえ正常に動作しないピンがあるはずです笑

現状問題ないです!

こちらはLED付き押しボタンスイッチなので, LEDの点灯とボタンを兼ねています

制御は BLEデバイスの作成 の方で説明します!


1-Clickボタン実行スクリプトの作成

Amazonでは1-Clickで商品を即購入する機能があるので, こいつを利用しようと思います

※ あらかじめAmazonのページにいって設定しておくことが前提条件となります!

casperjs は, Webページの要素を解析して要素に対するテキスト入力やボタンのクリックなど, 我々が普段ブラウザ操作しているときと同じような処理が可能です💡

ただ, casperjs の設定としては cookie を保存することができないので(ローカルに保存しておけばいいだけの話ですが), 毎回サインインをおこなわなければなりません

それ以外は単純に要素を探索してクリックするだけなので, 何も難しくないです😄

( casperjs のスクリプト書いたの2回目なのでクソコードですがw)

ただ, casperjs をインストールするまでに一悶着あったので共有しておきます!


casperjsとphantomjsのインストール

casperjs のインストールには phantomjsslimerjs が必要になります💡

MacOSではnpm入れて npm install casperjs とか npm install phantomjs とかやっておけばよかったのですが, RaspbianDebian GNU/Linux 系ということでnpmではインストール&ビルドはできなかったようです...

ということで Raspbian を使用している方は以下を実行してインストールしてください!

その前に, 念のためメモリのスワップ領域を広げてメモリアロケーションを防ぎましょう

# /etc/dphys-swapfile - user settings for dphys-swapfile package

# author Neil Franklin, last modification 2010.05.05
# copyright ETH Zuerich Physics Departement
# use under either modified/non-advertising BSD or GPL license

# this file is sourced with . so full normal sh syntax applies

# the default settings are added as commented out CONF_*=* lines

# where we want the swapfile to be, this is the default
#CONF_SWAPFILE=/var/swap

# set size to absolute value, leaving empty (default) then uses computed value
# you most likely don't want this, unless you have an special disk situation
CONF_SWAPSIZE=4096 # ← 多分コメント外すだけ

# set size to computed value, this times RAM size, dynamically adapts,
# guarantees that there is enough swap without wasting disk space on excess
#CONF_SWAPFACTOR=2

# restrict size (computed and absolute!) to maximally this limit
# can be set to empty for no limit, but beware of filled partitions!
# this is/was a (outdated?) 32bit kernel limit (in MBytes), do not overrun it
# but is also sensible on 64bit to prevent filling /var or even / partition
CONF_MAXSWAP=4096 # ← 100だったのを4096に変更

再読込します(若干時間かかる)

sudo /etc/init.d/dphys-swapfile restart

以下ビルド手順です

ビルドするまえに1点注意!!!!!

今回 編集中 の時間が長かった原因が...

一番最後の行 python build.py はめちゃくちゃ時間かかりますw(環境によるかと思いますが)

外出する時とか寝る時にスクリプト走らせたほうがいいですホントに

30時間くらいかかった...

とりあえず放置しても大丈夫な状態にしておいてくださいw

もし casperjs のバージョンが低くてもいいなら(1.1.0とか?), phantomjs を1.9.1にすることができるので, こちらのビルド済みphantomjsをとってくると良いと思います!

# casperjs

$ git clone git://github.com/casperjs/casperjs.git
$ cd casperjs
$ sudo ln -sf `pwd`/bin/casperjs /usr/local/bin/casperjs

# phantomjs
$ sudo apt-get install build-essential g++ flex bison gperf ruby perl \
libsqlite3-dev libfontconfig1-dev libicu-dev libfreetype6 libssl-dev \
libpng-dev libjpeg-dev python libx11-dev libxext-dev gperf bison
$ git clone git://github.com/ariya/phantomjs.git
$ cd phantomjs
$ git checkout 2.1.1 # お任せしますが, 現時点での最新版です
$ git submodule init
$ git submodule update
$ python build.py

最後に phantomjs -vcasperjs --version とかやってインストールされたことを確認しておいてください!

ということで本題のコード解説にまいります


コードの解説

var casper = require('casper').create();

var utils = require('utils');

casper.userAgent('Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.95 Safari/537.36');

casper.options.viewportSize = {width: 1280, height: 768};
casper.options.waitTimeout = 30000;

casper.on('remote.message', function(msg) {
this.echo("remote.msg: " + msg);
});

// サインイン画面に遷移
casper.start('https://www.amazon.co.jp/ap/signin?_encoding=UTF8&openid.assoc_handle=jpflex&openid.claimed_id=http%3A%2F%2Fspecs.openid.net%2Fauth%2F2.0%2Fidentifier_select&openid.identity=http%3A%2F%2Fspecs.openid.net%2Fauth%2F2.0%2Fidentifier_select&openid.mode=checkid_setup&openid.ns=http%3A%2F%2Fspecs.openid.net%2Fauth%2F2.0&openid.ns.pape=http%3A%2F%2Fspecs.openid.net%2Fextensions%2Fpape%2F1.0&openid.pape.max_auth_age=0&openid.return_to=https%3A%2F%2Fwww.amazon.co.jp%2F%3Fref_%3Dnav_ya_signin', function(response) {
if (response.status < 200 && response.status >= 400) {
this.echo('Error response');
process.exit(1);
}
});

// サインイン処理
casper.then(function() {
this.echo('Login start');
var signinFormSelector = 'form[name="signIn"]';
this.waitForSelector(signinFormSelector, function then() {
this.evaluate(function(username, password) {
document.querySelector('#ap_email').value = username;
document.querySelector('#ap_password').value = password;
document.querySelector('#signInSubmit').click();
}, 'E-mail', 'password'); // メールアドレスとパスワードを設定
});
});

// トップページのロゴが見えたら商品ページに遷移
casper.then(function() {
var titleSelector = '#nav-logo';
this.waitForSelector(titleSelector, function then() {
this.echo('You are in TOP now.');
this.thenOpen('URL', function() { // 購入したい商品のURLを設定
this.echo('Open item page.');
});
});
});

// タイトル取得
casper.then(function() {
var titleSelector = '#productTitle';
this.waitForSelector(titleSelector, function then() {
this.echo('title = ' + this.getElementInfo(titleSelector).text);
});
});

/*
// カートに入れるボタンの押下
casper.then(function() {
var addCartSelector = '#add-to-cart-button';
this.waitForSelector(addCartSelector, function then() {
this.click(addCartSelector);
});
});
*/

// 1-Clickの有効設定リンク押下
casper.then(function() {
var oneClickSigninSelector = 'a.oneClickSignInLink';
this.waitForSelector(oneClickSigninSelector, function then() {
this.click(oneClickSigninSelector);
});
});

// 1-Clickボタンの押下
casper.then(function() {
var oneClickBuyButtonSelector = '#oneClickBuyButton';
this.waitForSelector(oneClickBuyButtonSelector, function then() {
this.click(oneClickBuyButtonSelector);
});
});

// 購入完了ページが表示されるのを待機
casper.then(function() {
var titleSelector = '#nav-logo';
this.waitForSelector(titleSelector, function then() {
this.echo('You are in result page.');
});
});

casper.run();


ちょっとした設定

まだ学習していない部分ではありますが, 最初に以下の設定をおこなっています

casper.userAgent('Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.95 Safari/537.36');


そのままの通りユーザエージェントを設定しています💡

自分が普段使っているブラウザのリクエストを Developer Tool 等で見るのが手っ取り早いです!

casper.options.viewportSize = {width: 1280, height: 768};

画面サイズ指定です

casperjs ではブラウザを表示するわけではないですが, 例えばキャプチャを撮るときに見たい範囲を指定することもあるかと思います!

今回は特に意味はありませんが, 念のため普段見ている画面サイズを指定しておくのがいいかと思います

casper.options.waitTimeout = 30000;

こちらタイムアウト設定ですが, 非同期処理はもちろん画面の描画などで要素が見つからない場合があるので, 長めにとっています(30000ミリ秒 → 30秒)

casper.on('remote.message', function(msg) {

this.echo("remote.msg: " + msg);
});

こちらはデバッグ用のコードです

ところどころに this.echo が使われているのが見てとれますが, 普通に呼び出しても動作しません

this.echo が呼ばれたことを上記コードが監視している, という感覚でしょうか


ブラウザ操作実行

サインイン画面遷移 〜 即購入までの道のりを解説します


サインイン画面遷移

Amazonのページを開いてサインインのボタン要素をChromeのElementsで確認したところ, 非常に長ったらしい URL になりましたがこれは間違いないです

casper.start('URL', function(response) {}) を呼び出すことで, 開始ページの指定とレスポンスに応じた処理を定義します

かなり適当ですが, HTTPステータスコードが200 ~ 300以外はスクリプトを終了させています

とはいえ, よっぽどのことがない限り無難にサインイン画面から開始することができるはずです


サインイン処理

ここは是非マスターしておきたいところです👀

サインイン画面に遷移できたら次はフォームの入力になります

① 「name 属性が signInform 要素」を一旦文字列として保持します

this.waitForSelector('selector' function then() {}) によって上記の要素が表示されるのを待ちます

this.evaluate(function(arg1, arg2,...) {}, arg1, arg2,...) の部分では, 表示されている要素に対してテキスト入力やフォーム送信などをおこないます

実行結果を受け取ることができますが, 今回はスルーしています

document.querySelector('selector') は指定した要素をページから検索し, ヒットしたものを全て取得します

今回はそのページに1つしかない要素をそれぞれ取っているので, 対象の要素を更に深掘る必要はありません

そして取得した要素に対し value への値入力, click() でフォームデータ送信などをおこなっているわけです

中身のない説明になってしまいましたが, もっと深く知りたい方は casperjsのドキュメントを見てもらえればと思います!


商品ページ遷移

続いて, 商品ページへ遷移します

idnav-logo (左上のロゴマーク)要素を待っていますが, こちらはサインイン成功したことを確認するためです(サインインに失敗すると画面にとどまるので, nav-logo は見えないはずです)

② ロゴマークを見つけたら, this.thenOpen('URL', function() {}) で商品ページを開きます(完了後処理は特にせずデバッグのみ)


タイトル取得

特に意味は無いです笑

まぁデバッグコードとして置いておきましょう


1-Clickボタンの有効化

※ 前提条件として, Amazonで1-Clickボタンの設定はしておいてください!

ここは躓いたところです😫

普段使っているブラウザだと, サインイン後に商品ページ遷移すれば1-Clickボタンは表示されてますが, 恐らく cookie が毎回消されるような環境(今回のスクリプトや chrome の新規シークレットウィンドウなど)だと 「1-Click注文を有効にしてください」 のリンクがカートに入れるボタンの下に表示されています

こいつを押さないと1-Clickボタンが出てこないので押しましょう!

特筆すべき事項はないので説明は省きます


1-Clickボタンの押下

さぁ, いよいよ手に汗握る処理ですがここも特に難しいことはしてないので説明を省きます笑

もし即購入したい商品がなければ, ちょっと上にコメントした部分で「カートに入れるボタンの押下」ってところがあるので, そちらで試してみるといいと思います💡


完了ページを待機

ここはおまじないみたいなもんです

なぜか1-Clickボタンの押下が成功しているのに商品が購入されていない現象が続いていました😣

あくまでも推測ですが, 1-Clickボタンが押下された後すぐにスクリプトが終了して, リクエスト送信が途中で終わってる可能性があります

なので完了ページにもあるロゴマーク(またしても)を待機してリクエストが送信されるようにします

はい, というわけでAmazonの商品購入自動化はこんなに簡単にできちゃいます!というのを示せたかなぁと思います

注意だけしておきますが, あまりこのスクリプトを走らせていると(連続で10回弱程度?), 一定時間Amazonへのリクエストができなくなります

(普通のブラウザからは大丈夫です)


BLEデバイスの作成

実際ここが本質のところかもしれませんが, 役割的にはそんなに大きくないという |ω・)

Amazonの例のボタンは500円(実質無料)ですが, 我々が手に入る範囲内で一から作ろうとすると結構高いです笑(ボタンやらワイヤー・抵抗などは安いですが, IMBLEとアダプタで4500円くらい?笑)

回路は複雑なわけではないですが, 初心者が手を出すのはちょっと無謀でした...

でもちゃんと Pi Zero で制御処理できたので, 書いておきます💡


設定

前提として, GPIO と シリアル通信をおこなうので以下をやっておいてください

$ sudo raspi-config

# CLI上で画面が切り替わるので, 「5 Interfacing Options」を選択
# 「P6 Serial」と「8 Remote GPIO」をenableにする
# 「Finish」すれば再起動がかかると思います

$ sudo vim /boot/cmdline.txt

# 多分, 「console=ttyAMA0,115200 kgdboc=ttyAMA0,115200」と書かれてあるところがあると思うので消す
# dwc_otg.lpm_enable=0 console=tty1 root=/dev/mmcblk0p2 rootfstype=ext4 elevator=deadline fsck.repair=yes rootwait
# のようになっていればいいかと思います> <

$ sudo usermod -a -G dialout,gpio USERNAME # ← パーミッションで怒られるかもしれないので念のため...(やらなくていいかも)

$ sudo reboot


回路

さて, 素人には超難敵の回路です

今回はまだ良心的でしたが, 今でもこの回路が正しいかは謎です(ただ動くだけであって)

とりあえず実際の写真と回路図を載せておきます


実際の写真

BLEモジュール & Pi Zero との未接続状態
完成状態

IMG_0985.JPG
IMG_0986.JPG

なんじゃこりゃって感じですねw

ブレッドボードはまだキレイにできたと思うですが, オス-メスのジャンパーワイヤーが長くてみにくくなってしまった...


回路図

BLEモジュールと未接続状態
完成状態

IMBLE&RPiZero.png
IMBLE&RPiZero2.png

これもなかなかわかりにくいですね笑

BLEに関してはこちらを参考にしていますが, とてもわかりやすいので IMBLE 使ってみたい人は見ておくと良いと思います💡

IMBLE はブレッドボードギリギリに差し込まないといけないので注意してください!

↑ の図ではモジュールの両隣が空いてるように見えますが, 左と右のどちらかが完全に埋まってしまうので, 下からジャンパーワイヤーを伸ばして接続しないといけないです😫

IMBLE のハード・ソフト仕様はこちらからどうぞ

LED付き押しボタンスイッチに関してはそんなに難しくないので, 「raspberry pi タクトスイッチ」とかで検索してみてください!

LED部分に関してはLチカと同じですし, 電池の容量で「+(3.3V or 5V) から −(GND)に流れる」ことを意識すれば, なんとなく分かってきます


GPIO制御コード

ボタンとBLEモジュールの制御を GPIO 経由でおこないます

今回は Python で書いてみました

まずはシリアル通信用のライブラリ pyserialGPIO 制御用ライブラリの RPi.GPIO を入れましょう

$ sudo apt-get install python-rpi.gpio pyserial

コードは以下のとおりです!

# -*- coding: utf-8 -*-

import os
import RPi.GPIO as GPIO
import serial
import signal
import sys
import time

BUTTON_PIN = 23
LED_PIN = 24

# GPIO.BOARD: ピン番号, GPIO.BCM: GPIO番号
GPIO.setmode(GPIO.BOARD)

# ボタンの入力と, LEDの出力設定
GPIO.setup(BUTTON_PIN, GPIO.IN)
GPIO.setup(LED_PIN, GPIO.OUT)

# シリアル接続(BLEモジュール接続)
connection = serial.Serial('/dev/ttyAMA0', baudrate=19200, timeout=5.0)

isPushed = False

# ctrl + c が実行された時に呼ぶhandler(お掃除)
def cancelHandler(signum, frame):
print 'End: ', signum
GPIO.cleanup()
connection.close()
sys.exit(0)

signal.signal(signal.SIGINT, cancelHandler)

while True:
# ボタンの入力を受け取る
buttonInput = GPIO.input(BUTTON_PIN)
# 1ループ前でボタンが押されていなかった場合かつ, 今回ボタンが押されたら発火
if isPushed == False and buttonInput == 1:
print 'Push!!!'
# ボタンが押されたらLEDを消しておく
GPIO.output(LED_PIN, False)

# casperjsで即購入スクリプト実行し, resultに結果を格納する(success: 0, failure: 0以外)
result = os.system('casperjs amazon_dash.js')

if result == 0:
# 'success' の文字列を16進数に変換したデータを送信
connection.write('TXDT 73756363657373\r\n')
else:
# こちらは 'failure'
connection.write('TXDT 6661696c757265\r\n')
else:
# LED_PINがONじゃなければONにして点灯(入力可能状態)
if GPIO.input(LED_PIN) == 0:
print 'LED ON'
GPIO.output(LED_PIN, True)

# 0.1秒間隔で入力を受け付ける
time.sleep(0.1)
# 前回の入力状態を更新
isPushed = buttonInput

説明はコードの中に全て埋め込んじゃいました😅

処理の流れとしては,

① GPIO設定

② BLEモジュールと接続

③ ボタン入力待ち(LED点灯)

④ ボタンが押されたら商品購入スクリプト実行

⑤ BLEモジュールに結果を出力命令

⑥ ③ ~ ⑤を繰り返す

という感じです!

LEDが光っている間だけ入力が可能で, 多重処理は受け付けないようになっているので何回も商品購入スクリプトが実行されることはありません


iOSアプリの作成

iOSアプリでは, 即購入したい商品のURL登録とか購入完了通知とかやっておきたかったのですが, サーバ側を実装, もしくはAWS利用してサーバレスな環境を整備する必要があったので断念しました...

なので上の方でも述べましたが, Pi Zero につけたBLEモジュールから信号を受け取って, 購入通知を実現します!

説明が長いのでコードだけお見せします...笑😅

gates1de/BLEConnectionExample

こっちは必要最低限の機能だけ実装していて,

アプリ起動時にBLEモジュールと接続 → それ以降は商品購入結果待ち

という感じです💡

一応バックグラウンド状態でも動作するように以下を設定しています

BLE_background.png


デモ

一通り動いた結果を載せておきます!

(gifのサイズの関係で github に上げました😅)

https://github.com/gates1de/BLEConnectionExample/blob/master/PiZeroDashButton_Demo.gif

即購入だけでなく, 通知とか入力可能状態をLEDで表現するところとか, やりたいことは実現できました😃


おわりに

大体丸2日くらい(作業時間)で実装できるものにしては, 割りとハードルが低いかつ役に立つものが作れたので個人的には満足しています(・∀・)

とはいえ半田付けさえなければサーバ実装まで着手できたのにアレほんとにふざk(ry

自分は普段アプリケーションしか書かないですし, この記事を見てくださっている方もそういう方が多いかと思いますが, ハードウェアに触れるとアプリケーションのアイディアにエッセンスが加わるので, ここまではやらないにしてもIoTのキャッチアップなどはやっておくと良いことがあったりするのかなぁと思います!

また, 既存のものをハックするのは楽しいですが, 自分でデバイス作るのも同じくらい楽しいので手を出して見てはいかがでしょうか😝

今後も時間見つけてPi Zeroを使ったデバイスの話をしていければと思います〜

それではこのへんで失礼します!