PythonでLINEBotはじめました(冷やし中華的な)【前編】

PythonでLINEBotはじめました(冷やし中華的な)【前編】

ずっと前からハッカソンでLINEbotとか使いたかったけど、手をだせなかったので今更ながらはじめましたあくまでも簡単な練習としてEcho的なBotを作成しています。今回はHerokuへデプロイをするつもりでしたが、ボリュームが多くなってしまったので前後編としました。 今回のエントリーは前編として、ローカル環境でのbot作成までを行ってみています。

今回はpythonとFlaskを使ってbotを作成しています。

今回のエントリーでは以下の2つを参考させていただいています。本当にありがとうございます。

【参考1】

qiita.com

【参考2】

qiita.com

Botを作成する。

まずはLINEのデベロッパーコンソールにログインしてBotの基本部分を作成していきます。

最初にLINE developersにアクセスするところからスタートです。アクセスすると以下の様な画面になります。

f:id:ueponx:20180614063810p:plain

画面中の【MessagingAPI(ボット)をはじめる】ボタンをクリックします。

f:id:ueponx:20180614065108p:plain

ログインを促されます。(すでにログインしている場合には出ないかもしれません)開発しようと考えている人がユーザー登録してないとは思いにくいのでこの部分の手順に関しては省略しますがこのあたりを参考してもらえればいいのかなと思います。

f:id:ueponx:20180614064929p:plain

ユーザ登録をしている人はそのまま【ログイン】ボタンをクリックしてください。

続いては画面左の【新規プロバイダー】を作成します。すでに実験などをしている場合にはプロバイダーやチャンネルが作成されていると思いますが、今回はそれとは別でそれらを作成していきます。(全くの新規の場合は少し画面が違うかもしれません)

f:id:ueponx:20180614070600p:plain

ログイン後このような画面に遷移すると思いますので画面の左側の【新規プロバイダー作成】のボタンをクリックします。すると以下のような画面になります。

f:id:ueponx:20180614071123p:plain

新規プロバイダーの名前の登録になります。わかりやすいものがいいとは思いますので、今回は【Mashup名古屋】というプロバイダーを作成してみることにしました。入力が終わったら確認ボタンをクリックします。

f:id:ueponx:20180614071336p:plain

クリックすると確認画面が表示されますので、間違いなければ再度【作成ボタン】をクリックします。 f:id:ueponx:20180614071557p:plain

クリックするとプロバイダーが作成されプロバイダー一覧にも表示されるようになります。

f:id:ueponx:20180614072025p:plain

プロバイダー名の隣にある歯車のアイコンをクリックすればプロバイダーの管理画面に入れます。名前の変更や削除も可能です。

f:id:ueponx:20180614072402p:plain

では、続いて【Messaging API】ボタンをクリックします。

f:id:ueponx:20180614072510p:plain

次の処理は【新規channel作成】となります。

f:id:ueponx:20180614072611p:plain

画面全体は以下のようになっています。

f:id:ueponx:20180614073032p:plain

Messaging APIの情報を入力する項目は以下の情報となります。

  • アイコン
  • アプリ名(※アプリ名を設定した後、7日間は変更できません。
  • アプリ説明
  • プラン(【Developer Trial】と【フリー】からの選択)
  • 大業種(選択式)
  • 小業種(選択式)
  • メールアドレス

アプリ名の変更が7日間できないのでここは厳しいかも?(ぬるいブログのサンプルだったら全く問題ありませんけど) あと、プランに2つから選択になりますが、画面内の説明にある通り

Developer Trial
MessagingAPIを利用したBotを試すプランです。友だちとメッセージの送受信を行うことができます。 ※追加可能友だち数は50人に制限されています。また、Developer Trialからプランの切り替えやプレミアムIDの購入はできません。


フリー
MessagingAPIを利用したBotを開発するプランです。友だちの人数に制限はありませんが、Push messagesを利用してBotから友だちにメッセージを送信することはできません。 ※サービス拡張に向けプラン変更が可能です。

このように説明されていました。開発のテストであれば間違いなく【Developer Trial】を選択すればいいかなと思います。まあ、LINEの友達も50人もいませんけどw。

今回はこんな感じで設定してみました。入力が終わったら画面下にある【確認】ボタンをクリックします。

f:id:ueponx:20180614074940p:plain

【確認】ボタンをクリックすると情報利用に関する同意が求められるのでよく読んで問題なければ【同意】ボタンをクリックしてください。(よっぽどおかしいことは書いていないと思いますが…)

f:id:ueponx:20180614075110p:plain

【同意】ボタンのクリックで確認のダイアログが閉じて規約同意の確認画面に遷移します。

f:id:ueponx:20180614075419p:plain

あとは利用規約部分にチェックを行い【作成】ボタンをクリックします。

f:id:ueponx:20180614075603p:plain

無事にチャンネルが作成されました。

f:id:ueponx:20180614075936p:plain

ですが新しいチャンネルはまだ設定が完了していないようです。ただ、まだ入力できない情報もあるので今はまだ設定が完了していいないことだけ覚えておくことにします

f:id:ueponx:20180614080241p:plain

botソースコードの準備

今回のbot用のサンプルコードの準備をします。サンプル用のコードは公式側からGithubで公開されていますのでこれを使用します。このサンプルコードは文字列を受信して同じものを返す機能になっています。(Echo的なものです)

github.com

今回はpythonを出しましたが他にも以下のような言語があるようです。

注意点はEchoのサンプルコードのファイルはREADME.rst(ドキュメント)に入っているので単純にGit Cloneしても見当たらないので覚えておいてください。

ドキュメントにあるソースコードを貼ってろうと思ったのですがいろいろどうだろうかというところもあるので少しコードを変更しています。

【main.py】

from flask import Flask, request, abort
#環境変数取得用
import os

from linebot import (
    LineBotApi, WebhookHandler
)
from linebot.exceptions import (
    InvalidSignatureError
)
from linebot.models import (
    MessageEvent, TextMessage, TextSendMessage,
)

app = Flask(__name__)

# オリジナルの処理
# line_bot_api = LineBotApi('YOUR_CHANNEL_ACCESS_TOKEN')
# handler = WebhookHandler('YOUR_CHANNEL_SECRET')

#環境変数取得
YOUR_CHANNEL_ACCESS_TOKEN = os.environ["YOUR_CHANNEL_ACCESS_TOKEN"]
YOUR_CHANNEL_SECRET = os.environ["YOUR_CHANNEL_SECRET"]

line_bot_api = LineBotApi(YOUR_CHANNEL_ACCESS_TOKEN)
handler = WebhookHandler(YOUR_CHANNEL_SECRET)

@app.route("/")
def hello_world():
    return "hello world!"

@app.route("/callback", methods=['POST'])
def callback():
    # get X-Line-Signature header value
    signature = request.headers['X-Line-Signature']

    # get request body as text
    body = request.get_data(as_text=True)
    app.logger.info("Request body: " + body)

    # handle webhook body
    try:
        handler.handle(body, signature)
    except InvalidSignatureError:
        abort(400)

    return 'OK'


@handler.add(MessageEvent, message=TextMessage)
def handle_message(event):
    line_bot_api.reply_message(
        event.reply_token,
        TextSendMessage(text=event.message.text))


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

サンプルではFlaskrequestを使用する必要あるのがわかります。requestはHTTP関連でお馴染みのモジュールですが、Flaskウェブアプリケーション用のマイクロフレームワークになります。

Welcome | Flask (A Python Microframework)

github.com

Djangoに似たような機能を持つもののようです。PCにAnacondaをインストールしているのですがpip freezeコマンドでもインストールされていました。RaspberryPi側でも確認しましたがインストールされていました(pip,pip3ともに)。

とはいっても今回は最終的にはHerokuホスティングしてもらうのでインストールに関してはHeroku側で行う必要があります

また、コード内を眺めてみるとYOUR_CHANNEL_ACCESS_TOKENYOUR_CHANNEL_SECRETが必要になりますが、Messaging APIの設定から取得できるので後から取得します。その際もHeroku側の環境変数として設定するのが一般的のようですがさんぷるのコードでは直接代入しているではないですか! その部分をあわせるためにYOUR_CHANNEL_ACCESS_TOKENYOUR_CHANNEL_SECRET環境変数から読み込むようにコードを変更してあります。

【オリジナル】

line_bot_api = LineBotApi('YOUR_CHANNEL_ACCESS_TOKEN')
handler = WebhookHandler('YOUR_CHANNEL_SECRET')

↓ 【変更後】

#環境変数取得用
import os

…(中略)…

#環境変数取得
YOUR_CHANNEL_ACCESS_TOKEN = os.environ["YOUR_CHANNEL_ACCESS_TOKEN"]
YOUR_CHANNEL_SECRET = os.environ["YOUR_CHANNEL_SECRET"]

line_bot_api = LineBotApi(YOUR_CHANNEL_ACCESS_TOKEN)
handler = WebhookHandler(YOUR_CHANNEL_SECRET)

あと、テスト用にルートアクセスをしたときにメッセージを表示するhello_worldも追加しました。

【追加部分】

@app.route("/")
def hello_world():
    return "hello world!"

続いてはHeroku側の設定としたいところですが、とりあえずはローカル環境で実行できるかを試してみます。

アクセスに必要なCHANNEL_ACCESS_TOKENCHANNEL_SECRETの取得

デベロッパーコンソールに戻って実行に必要な``CHANNEL_ACCESS_TOKENCHANNEL_SECRET```の取得します。

f:id:ueponx:20180616061151p:plain

作成したBotの【設定が完了していません】をクリックするとChannel基本設定の画面に遷移します。

f:id:ueponx:20180616061830p:plain

画面をスクロールさせると基本情報のカテゴリの中にCHANNEL_SECRETがあるのでメモしておきます。

f:id:ueponx:20180616062418p:plain

さらに画面をスクロールさせると【メッセージ送受信設定】のカテゴリに``CHANNEL_ACCESS_TOKEN```があるので右にある【再発行】のボタンをクリックします。

f:id:ueponx:20180616062135p:plain

クリックすると以下のようなダイアログが表示されますが、初回の場合は0時間で問題ないでしょう。再度【再発行】のボタンをクリックします。

f:id:ueponx:20180616062903p:plain

以下のように表示が行われればOKです。これもメモしておきます。

f:id:ueponx:20180616063154p:plain

ローカルでテストを行う

先程の手順で実行に必要な``CHANNEL_ACCESS_TOKENCHANNEL_SECRET```の取得できたので、環境変数にセットしてローカルで実行してみます(ここではアクセスできるかになります)。

まずは、``CHANNEL_ACCESS_TOKENCHANNEL_SECRETを環境変数にセットします。以下はAnacondaPrompt(Windows10のcmd環境)でセットしています。文字は伏せています。環境変数をsetコマンド(Linuxならばexport```)でセットしています。(Macはわかりませんw)

WindowsなAnaconda環境】

(base) >set YOUR_CHANNEL_ACCESS_TOKEN=***************************************************************************************************************************************************************************************************************

(base) >set YOUR_CHANNEL_SECRET=***********************

Linuxなど】

$ export YOUR_CHANNEL_ACCESS_TOKEN=***************************************************************************************************************************************************************************************************************

$ export YOUR_CHANNEL_SECRET=***********************

あとは先程のpythonのコードを実行します。(この実行仕方では失敗してます。正解は後述しますが、それ以外の工程に関してはそのままで大丈夫です。)

【間違えた実行方法】

(base) > python main.py

【(念の為)後述する正解の実行方法-WindowsなAnaconda環境】

> set FLASK_APP=main.py
> set FLASK_DEBUG=1
> flask run --host=0.0.0.0

f:id:ueponx:20180616064724p:plain

デフォルトではポート5000番でサービスが開始されます。

実行したら、Webブラウザからアクセスしてみます。

f:id:ueponx:20180616065143p:plain

HelloWorld!が表示されました。サンプルコードに追加したHelloWorldの処理が動作したことがわかります。

続いてはこれをインターネット経由でアクセスします。ルータの設定をするのは危険なので、ここでもおなじみのngrokを使用します。

【ngrokに関しては以下を参照】

ngrok.com

使い方に関しては過去のエントリーを参照してください。エントリーではRaspberryPiで使用していますが、WindowsMacでも変わらないと思います。

uepon.hatenadiary.com

ngrokをダウンロードして任意の場所(パスの通ったところなど)にいれて以下の様に実行します。実行は別のcmdやコンソールからの実行となります。pythonプログラムとは異なるコンソールで実行してください。

> ngrok.exe http 5000

【2つのコンソールを開いての実行の様子】

f:id:ueponx:20180616070224p:plain

実行すると以下のような画面になります。その中のForwardingのところにあるアドレスがサービスが公開されたURLとなっています。

f:id:ueponx:20180616070606p:plain

それでは、このURLへブラウザからアクセスします。httpでも、httpsでもどちらでもアクセス出来ますが、今回はhttpsを使用しています。(botではhttpは使えませんのでご注意。)

f:id:ueponx:20180616070913p:plain

ではこのURLをメモっておきます。これがBotのWebhook用のURLとなります。

では、デベロッパーコンソールに移動して、今回ngrokで取得したURLを設定します。 デベロッパーコンソールの画面に戻って、作成したBotの【設定が完了していません】のあたりをクリックします。

f:id:ueponx:20180618071924p:plain

クリックすると、【Channel基本設定】画面に遷移します。

f:id:ueponx:20180618072111p:plain

画面をスクロールさせて【メッセージ送受信設定】の項目の中から

f:id:ueponx:20180618072434p:plain

【Webhook送信】の右側にある【ペン】のアイコンをクリックします。

f:id:ueponx:20180619045345p:plain

表示されたラジオボタンで【利用する】に設定をして、【更新】ボタンをクリックします。

f:id:ueponx:20180619045622p:plain

次に【Webhook URL】の右側にある【ペン】のアイコンをクリックします。

f:id:ueponx:20180618072646p:plain

すると、入力ボックスが表示されるので、ここに先程ngrokで得られたURL(httpsのもの)を入力します。画面内の注意書きにもありますが、今回はhttpは使えませんのでご注意。 また、URLの末尾に/callbackをつけるのも忘れずに!

入力値が[任意の文字列].ngrok.com/callbackとなっていれば問題ありません。

f:id:ueponx:20180618072834p:plain

入力が終わったら、更新ボタンを押して設定を完了させてください。 デベロッパーコンソールの画面に戻ると【設定が完了していません】の文字が消えています。

f:id:ueponx:20180618073616p:plain

実行してみる

実行は一度このボットと友達になる必要があります。デベロッパーコンソールから今回作成したbotのアイコンをクリックすると、【Channel基本設定】画面に遷移します。 その画面の一番下の部分に友達設定用のQRコードが表示されているので、それを使用してLINEアプリから登録を行います。

f:id:ueponx:20180618073822p:plain

QRコードで友だち追加の画面から、QRコードを読み込ませると以下のような画面になるので【追加】ボタンをタップします。

f:id:ueponx:20180619220438p:plain

無事、友だち登録が完了しました。

f:id:ueponx:20180619221110p:plain

トーク】を開くと以下のようなウエルカムメッセージが届きます。

f:id:ueponx:20180619221258p:plain

そして、いよいよ文字列を送信してみると…

f:id:ueponx:20180619221442p:plain

あれ?Echoが帰ってきていない…アクセスログをみると200(正常)が帰ってきているので問題はなさそう…コードが違う?

f:id:ueponx:20180619051428p:plain

いろいろとググったところFlaskでの実行では通常のpythonとは少し異なる実行方法が推奨されているようです。

【参照】

qiita.com

WindowsなAnaconda環境】

> set FLASK_APP=main.py
> set FLASK_DEBUG=1
> flask run --host=0.0.0.0

Linuxなど】

$ export FLASK_APP=main.py
$ export FLASK_DEBUG=1
$ flask run --host=0.0.0.0

ちなみにpythonのコードの最後の部分でapp.runの引数を以下の様に変更すると

【変更前】

if __name__ == '__main__':
    app.run()

【変更後】

if __name__ == '__main__':
    app.run(debug=False, host='0.0.0.0', port=5000)

環境変数を使用せず、そのままflask runとさせることが出来るようです。

f:id:ueponx:20180619052340p:plain

f:id:ueponx:20180619053429p:plain

あえて実験しましたが、環境変数をセットしておけば以前のようにpythonからアプリを実行しても問題はなさそうです。

(base)  >python main.py

スマートフォン側実行画面】 f:id:ueponx:20180619221835p:plain

【コンソール側実行画面】 f:id:ueponx:20180619061041p:plain

スマートフォン側の実行では、途中なんか変なメッセージがでているのですが…

ぐぐってみるとこのメッセージは自動応答系のメッセージのようです。LineデベロッパーコンソールでこれをOFFすると表示されないようですので設定をしてみます。 以下の画面から作成したアプリをクリックして

f:id:ueponx:20180619053909p:plain

【Channel基本設定】の画面に入ります。

f:id:ueponx:20180619054013p:plain

画面をスクロールさせていくと【LINE@機能の利用】の項目があると思います。 現在の設定は以下のようになっていると思います。

f:id:ueponx:20180619054222p:plain

このなかで【自動応答メッセージ】がONになっているので、毎回メッセージが表示されています。これをOFFすることにします。項目の右側にある【ペン】アイコンをクリックして

f:id:ueponx:20180619054853p:plain

ラジオボタンの「利用しない」を選択して【更新】ボタンをクリックします。

以下のようになっていればOKです。

f:id:ueponx:20180619055033p:plain

では、改めてLINEでテストで実行してみます。

f:id:ueponx:20180619222002p:plain

実行に成功しました。

おわりに

今回も長くなってしまったのでローカルでのテストができたので前半戦は終了です。今回はpythonがどうとかいうよりはFlaskの関連で躓いた感じは否めません。ただ無事に動かせてよかったかなと思います。ngrokを使用すればRaspberryPiでもBotをローカルで動かせるかなと思います。

続いてはHelokuへのデプロイをしていきたいと思います。(つづく)

RaspberryPiでNFCタグを使ってみる

RaspberryPiでNFCタグを使ってみる

久しぶりにいろいろと思うところがあって、RaspberryPiでNFCのタグを使ってみることにしました。

基本的には過去エントリー見てもらえればいいのですが、その頃からRaspbianのバージョンもベースが変わっているのでちょっと変わったようです。 というか楽になっただけなので大したことはないです。

【参考1】 uepon.hatenadiary.com 【参考2】 uepon.hatenadiary.com

使用したタグはサンワサプライさんのシールタイプになります。

一枚あたり100円を切っているので、前よりも安くなってきていますね。

今回のOS情報も念の為。今回はRaspberry Pi3ではなく、使っていなかったRaspberry Pi2を使用してます。

$ lsb_release -a
No LSB modules are available.
Distributor ID: Raspbian
Description:    Raspbian GNU/Linux 9.4 (stretch)
Release:        9.4
Codename:       stretch

最新のイメージを使用していますのでRaspbian GNU/Linux 9.4 (stretch)となっています。(イメージ名は2018-04-18-raspbian-stretch.imgでした)

RC-S380の認識

基本的には以前のエントリーと同様にUSBを挿すだけで認識は行われます。不確定な情報ではありますが、過去のバージョンのリーダーではどうも認識はされますが、` ``nfcpy```からは認識出来ないようです。自分も何回か試してみましたがだめでした。nfcpy側では対応デバイスになっていますが、情報から察するにその他ライブラリなどの依存関係も疑われるのではないかと思います。ネットの情報を調べるとベースがDebian 6.0 (squeeze)からDebian 7 (wheezy)に変わったあたりか使えなくなっているのではないかと推測しています。

とりあえずUSBケーブルで接続すると以下のようになります。

【リーダーを1つ接続した場合】

$ lsusb
Bus 001 Device 006: ID 056e:4008 Elecom Co., Ltd
Bus 001 Device 007: ID 054c:06c3 Sony Corp.
Bus 001 Device 003: ID 0424:ec00 Standard Microsystems Corp. SMSC9512/9514 Fast Ethernet Adapter
Bus 001 Device 002: ID 0424:9514 Standard Microsystems Corp. SMC9514 Hub
Bus 001 Device 001: ID 1d6b:0002 Linux Foundation 2.0 root hub

特に問題ありません。2つ接続しても問題なく認識されています。

【リーダーを2つ接続した場合】

$ lsusb
Bus 001 Device 006: ID 056e:4008 Elecom Co., Ltd
Bus 001 Device 007: ID 054c:06c3 Sony Corp.
Bus 001 Device 008: ID 054c:06c3 Sony Corp.
Bus 001 Device 003: ID 0424:ec00 Standard Microsystems Corp. SMSC9512/9514 Fast Ethernet Adapter
Bus 001 Device 002: ID 0424:9514 Standard Microsystems Corp. SMC9514 Hub
Bus 001 Device 001: ID 1d6b:0002 Linux Foundation 2.0 root hub

nfcpyモジュールのインストール

前回とは異なり、pipに登録されていました。気になる点としてはpython2系にしかまだ対応していない点でしょうか。ドキュメント通りインストールすれば問題はありません。 -Uスイッチをつけて置かないとエラーがでるようです。

$ sudo pip install -U nfcpy
Collecting nfcpy
  Downloading https://files.pythonhosted.org/packages/89/2c/3d7378d65c6f21312fae4cc44849606eefa08f5980e06c5bc220c2086808/nfcpy-0.13.5-py2-none-any.whl (214kB)
    100% |????????????????????????????????| 215kB 627kB/s
Collecting libusb1 (from nfcpy)
  Downloading https://files.pythonhosted.org/packages/ec/5d/4fdac6c53525786fe35cff035c3345452e24e2bee5627893be65d12555cb/libusb1-1.6.4.tar.gz (55kB)
    100% |????????????????????????????????| 61kB 1.0MB/s
Collecting pydes (from nfcpy)
  Downloading https://www.piwheels.org/simple/pydes/pyDes-2.0.1-py2.py3-none-any.whl
Collecting pyserial (from nfcpy)
  Downloading https://files.pythonhosted.org/packages/0d/e4/2a744dd9e3be04a0c0907414e2a01a7c88bb3915cbe3c8cc06e209f59c30/pyserial-3.4-py2.py3-none-any.whl (193kB)
    100% |????????????????????????????????| 194kB 680kB/s
Collecting ndeflib (from nfcpy)
  Downloading https://files.pythonhosted.org/packages/01/76/39eb236dc5566618abdb169cb88ba4eabd22245b29cc9e5f8d91d5fcf261/ndeflib-0.3.2-py2.py3-none-any.whl (57kB)
    100% |????????????????????????????????| 61kB 1.0MB/s
Building wheels for collected packages: libusb1
  Running setup.py bdist_wheel for libusb1 ... done
  Stored in directory: /root/.cache/pip/wheels/98/8d/8b/bac0a20eb9757e7dbf46e8ab1f1695c78ad919f53080a58bc1
Successfully built libusb1
Installing collected packages: libusb1, pydes, pyserial, ndeflib, nfcpy
  Found existing installation: pyserial 3.2.1
    Not uninstalling pyserial at /usr/lib/python2.7/dist-packages, outside environment /usr
Successfully installed libusb1-1.6.4 ndeflib-0.3.2 nfcpy-0.13.5 pydes-2.0.1 pyserial-3.4

インストールは終わりましたが、テスト用のコマンドなどはインストールされていませんので、Githubから以下のサイトからcloneします。

github.com

$ git clone  https://github.com/nfcpy/nfcpy.git
$ cd nfcpy/
$ ls
HISTORY.rst  README.rst  requirements-dev.txt   setup.py  tox.ini
LICENSE      docs        requirements-pypi.txt  src
MANIFEST.in  examples    setup.cfg              tests

nfcpyの実行を行う

インストール準備がおわったので続いてはnfcpyモジュールのチェックを行います。 以下のコマンドで行います。ですが、インストール直後はsudoをつけないと以下のようなエラーメッセージが表示されます。(sudoをつければ実行は問題はありません。)

今回はRaspberryPiに2つのリーダーを接続しているのでエラーも2つ表示されています。

$ python -m nfc
No handlers could be found for logger "nfc.llcp.sec"
This is the 0.13.5 version of nfcpy run in Python 2.7.13
on Linux-4.14.34-v7+-armv7l-with-debian-9.4
I'm now searching your system for contactless devices
** found usb:054c:06c3 at usb:001:007 but access is denied
-- the device is owned by 'root' but you are 'pi'
-- also members of the 'root' group would be permitted
-- you could use 'sudo' but this is not recommended
-- better assign the device to the 'plugdev' group
   sudo sh -c 'echo SUBSYSTEM==\"usb\", ACTION==\"add\", ATTRS{idVendor}==\"054c\", ATTRS{idProduct}==\"06c3\", GROUP=\"plugdev\" >> /etc/udev/rules.d/nfcdev.rules'
   sudo udevadm control -R # then re-attach device
** found usb:054c:06c3 at usb:001:008 but access is denied
-- the device is owned by 'root' but you are 'pi'
-- also members of the 'root' group would be permitted
-- you could use 'sudo' but this is not recommended
-- better assign the device to the 'plugdev' group
   sudo sh -c 'echo SUBSYSTEM==\"usb\", ACTION==\"add\", ATTRS{idVendor}==\"054c\", ATTRS{idProduct}==\"06c3\", GROUP=\"plugdev\" >> /etc/udev/rules.d/nfcdev.rules'
   sudo udevadm control -R # then re-attach device
I'm not trying serial devices because you haven't told me
-- add the option '--search-tty' to have me looking
-- but beware that this may break other serial devs
Sorry, but I couldn't find any contactless device

このメッセージをよくみると

-- better assign the device to the 'plugdev' group
   sudo sh -c 'echo SUBSYSTEM==\"usb\", ACTION==\"add\", ATTRS{idVendor}==\"054c\", ATTRS{idProduct}==\"06c3\", GROUP=\"plugdev\" >> /etc/udev/rules.d/nfcdev.rules'
   sudo udevadm control -R # then re-attach device

このメッセージの内容は、「使用するデバイスplugdevグループに入れて続くコマンドをいれるとsudoなしで実行できるようになります。」とのことでした、ベンダIDなどもセットされた形でメッセージが表示されるので、コピペして以下のように実行します。

$ sudo sh -c 'echo SUBSYSTEM==\"usb\", ACTION==\"add\", ATTRS{idVendor}==\"054c\", ATTRS{idProduct}==\"06c3\", GROUP=\"plugdev\" >> /etc/udev/rules.d/nfcdev.rules'
$ sudo udevadm control -R

リーダーが複数ある場合はエラーメッセージにある分だけ実行します。 実行が終わったら、念の為RaspberryPiを再起動し、改めてチェック行ってみます。

$ python -m nfc
No handlers could be found for logger "nfc.llcp.sec"
This is the 0.13.5 version of nfcpy run in Python 2.7.13
on Linux-4.14.34-v7+-armv7l-with-debian-9.4
I'm now searching your system for contactless devices
** found SONY RC-S380/P NFC Port-100 v1.11 at usb:001:005
** found SONY RC-S380/P NFC Port-100 v1.11 at usb:001:004
I'm not trying serial devices because you haven't told me
-- add the option '--search-tty' to have me looking
-- but beware that this may break other serial devs

これでsudoをつけなくてもエラーメッセージは表示されないようになります。デバイスも正しく認識されているようです。

続いてはGithubで落としてきたテスト用のコマンドを使用して交通系ICカードを読み込ませてみます。exampleディレクトリ内のtagtool.pyがテスト用のコマンドです。実行すると読み込み待ちになります。

$ git clone  https://github.com/nfcpy/nfcpy.git
$ cd ~/nfcpy/examples/
$ python tagtool.py
No handlers could be found for logger "nfc.llcp.sec"
[nfc.clf] searching for reader on path usb
[nfc.clf] using SONY RC-S380/P NFC Port-100 v1.11 at usb:001:005
** waiting for a tag **

ここで交通系ICカードをタッチすると読み込んだデータの一部が表示されます。 (IDの部分は一部伏せています)

$ python tagtool.py
No handlers could be found for logger "nfc.llcp.sec"
[nfc.clf] searching for reader on path usb
[nfc.clf] using SONY RC-S380/P NFC Port-100 v1.11 at usb:001:005
** waiting for a tag **
Type3Tag 'FeliCa Standard (RC-S915)' ID=**************** PMM=**************** SYS=0003

このコマンドでは使用するリーダーを指定して起動することになります。(指定しない場合には最初に見つかったリーダーが使用される)

では、改めて今回準備したNFCタグを読み込ませてみます。(IDの部分は一部伏せています) うまく読み込めたようです。購入時は何も書き込まれていないので以下のような表示になります。(この商品は144Byteのデータ書き込みができるようですが、実際には137byteの書き込みになってしまうようです。)

$ python tagtool.py
No handlers could be found for logger "nfc.llcp.sec"
[nfc.clf] searching for reader on path usb
[nfc.clf] using SONY RC-S380/P NFC Port-100 v1.11 at usb:001:005
** waiting for a tag **
Type2Tag 'NXP NTAG213' ID=04D803********
NDEF Capabilities:
  readable  = yes
  writeable = yes
  capacity  = 137 byte
  message   = 0 byte

pythonからタグを操作する

タグ情報をreadする

on_ほげほげみたいな感じのイベントハンドラーのメソッドを作成して、usb接続のデバイスに登録することで読み込むことができます。今回の例ではon_connectが読み込みに当たりますが、今後の例もこの部分を編集して実装しています。

【readNFC.py】

import nfc

def on_startup(targets):
        print("on_startup()")
        return targets

def on_connect(tag):
        print("Tag: {}".format(tag))
        print("Tag type: {}".format(tag.type))
        #print '\n'.join(tag.dump())
        if tag.ndef:
                print tag.ndef.message.pretty()
        #return True

def on_release(tag):
        print("on_release()")
        if tag.ndef:
                print(tag.ndef.message.pretty())

clf = nfc.ContactlessFrontend('usb')
if clf:
        print("Clf: {}".format(clf))
        clf.connect(rdwr={
                'on-startup': on_startup,
                'on-connect': on_connect,
                'on-release': on_release
        })

clf.close()

このプログラム例ではタグ情報をダンプしていますが、初期状態ではタグの内容は空になってます。

【実行】

$ python readNFC.py
No handlers could be found for logger "nfc.llcp.sec"
Clf: SONY RC-S380/P on usb:001:005
on_startup()
Tag: Type2Tag 'NXP NTAG213' ID=04A903********
Tag type: Type2Tag
record 1
  type   = ''
  name   = ''
  data   = ''

タグをフォーマットする

【formatNFC.py】

import nfc

def on_connect(tag):
        print("format:", tag.format())

clf = nfc.ContactlessFrontend('usb')
if clf:
        print("Clf: {}".format(clf))
        clf.connect(rdwr={
                'on-connect': on_connect
        })

clf.close()

【実行】

$ python formatNFC.py
No handlers could be found for logger "nfc.llcp.sec"
Clf: SONY RC-S380/P on usb:001:005
('format:', True)

情報がフォーマットされているかを先程のreadNFC.pyで もう一度読み込ませてみると

【実行】

$ python readNFC.py
No handlers could be found for logger "nfc.llcp.sec"
Clf: SONY RC-S380/P on usb:001:005
on_startup()
Tag: Type2Tag 'NXP NTAG213' ID=04A903********
Tag type: Type2Tag
record 1
  type   = ''
  name   = ''
  data   = ''

無事にフォーマットされていることがわかります。

タグに情報を書き込む

"Hello World!"という文字列を書き込んでみます。

【writeNFC.py】

import nfc

def on_startup(targets):
        print("on_startup()")
        return targets

def on_connect(tag):
        print("Tag: {}".format(tag))
        print("Tag type: {}".format(tag.type))
        #print '\n'.join(tag.dump())
        if tag.ndef:
                record = nfc.ndef.TextRecord("Hello World!")
                tag.ndef.message = nfc.ndef.Message(record)
                print tag.ndef.message.pretty()
        #return True

def on_release(tag):
        print("on_release()")
        if tag.ndef:
                print(tag.ndef.message.pretty())

clf = nfc.ContactlessFrontend('usb')
if clf:
        print("Clf: {}".format(clf))
        clf.connect(rdwr={
                'on-startup': on_startup,
                'on-connect': on_connect,
                'on-release': on_release
        })

clf.close()

【実行】

$ python writeNFC.py
No handlers could be found for logger "nfc.llcp.sec"
Clf: SONY RC-S380/P on usb:001:005
on_startup()
Tag: Type2Tag 'NXP NTAG213' ID=04A903********
Tag type: Type2Tag
record 1
  type   = 'urn:nfc:wkt:T'
  name   = ''
  data   = '\x02enHello World!'

書き込んだタグを先程のreadNFC.pyで読み込ませると…

$ python readNFC.py
No handlers could be found for logger "nfc.llcp.sec"
Clf: SONY RC-S380/P on usb:001:005
on_startup()
Tag: Type2Tag 'NXP NTAG213' ID=04A903********
Tag type: Type2Tag
record 1
  type   = 'urn:nfc:wkt:T'
  name   = ''
  data   = '\x02enHello World!'

無事に書き込まれているようです。また先程のフォーマットをすれば初期化も問題なくできます。

$ python formatNFC.py
No handlers could be found for logger "nfc.llcp.sec"
Clf: SONY RC-S380/P on usb:001:005
('format:', True)

$ python readNFC.py
No handlers could be found for logger "nfc.llcp.sec"
Clf: SONY RC-S380/P on usb:001:005
on_startup()
Tag: Type2Tag 'NXP NTAG213' ID=04A903********
Tag type: Type2Tag
record 1
  type   = ''
  name   = ''
  data   = ''

複数のリーダーを使用した読み込み(スレッド+interruptシグナル対応)

今回は複数のリーダーを接続してそれぞれでタグを読み込ませたいと思っていたのでそれを実装してみます。各デバイスごとにスレッドを生成し、且つデーモン化してCtrl+Cでの終了にも対応させています。

【multiPollingNFC.py】

#!/usr/bin/env python
# -*- coding: utf-8 -*-
import sys
import signal
import nfc
import time
import threading

def on_startup(targets):
    # print("on_startup()")
    return targets

def on_connect(tag):
    print("Tag: {}".format(tag))
    print("Tag type: {}".format(tag.type))
    if tag.ndef:
        print(tag.ndef.message.pretty())
    print("-----------------")

def on_release(tag):
    # print("on_release()")
    if tag.ndef:
        print(tag.ndef.message.pretty())

def readNFCThread(device):
    clf = nfc.ContactlessFrontend(device) # バス番号とデバイス番号を指定
    while True:
        if clf:
                # print("Clf: {}".format(clf0))
                clf.connect(rdwr={
                        'on-startup': on_startup,
                        'on-connect': on_connect,
                        'on-release': on_release
                })
    clf.close()

def main(argv):
    t1 = threading.Thread(target=readNFCThread,name="readNFC1",args=('usb:001:004',))
    t2 = threading.Thread(target=readNFCThread,name="readNFC2",args=('usb:001:005',))
    t1.setDaemon(True)
    t2.setDaemon(True)
    t1.start()
    t2.start()
    while True:
        pass

def handler(signal, frame):
    print("Process Interrupt!")
    sys.exit(0)

if __name__ == '__main__':
    signal.signal(signal.SIGINT, handler)
    main(sys.argv)

ちょっと解説

各デバイスの読み込みをスレッド化しているので以下のようにデバイスの設定をできるようにしています。

clf = nfc.ContactlessFrontend(device) # バス番号とデバイス番号を指定

ここでいうデバイスが接続したリーダーの設定になります。引数として与える情報としてはlsusbコマンドで認識されたBusDeviceIDを与えることになります。

$ lsusb
【略】
Bus 001 Device 005: ID 054c:06c3 Sony Corp.
Bus 001 Device 004: ID 054c:06c3 Sony Corp.
【略】

上記のような表示が行われた場合には以下のように引数で与えれることになります。 USBの接続状態によって毎回変わる可能性のある情報なので注意が必要となります。

t1 = threading.Thread(target=readNFCThread,name="readNFC1",args=('usb:001:004',))
t2 = threading.Thread(target=readNFCThread,name="readNFC2",args=('usb:001:005',))

このプログラムではThread化しているのでCtrl+Cを押してinterruptを発生させても、Threadが終了しない状態になります。 そのため、以下のようにしてDeamonの設定をしています。

    t1.setDaemon(True)
    t2.setDaemon(True)

実行してみる

フォーマットされたタグとHello World!の書き込まれたタグを別のリーダーに読み込ませると以下のような表示になります。

【実行】

$ python multiPollingNFC.py
No handlers could be found for logger "nfc.llcp.sec"
Tag: Type2Tag 'NXP NTAG213' ID=04D803********
Tag type: Type2Tag
record 1
  type   = ''
  name   = ''
  data   = ''
-----------------
Tag: Type2Tag 'NXP NTAG213' ID=04A903********
Tag type: Type2Tag
record 1
  type   = 'urn:nfc:wkt:T'
  name   = ''
  data   = '\x02enHello World!'
-----------------

一応はできていますが、なんとなく処理が遅いです。それぞれのリーダーでポーリングをしているということもあるので、少し待ち時間があるようです。(別のターミナルで別々のreadNFC.pyを動作させたほうが早いかもw)このままの仕組みで高速化するのであれば、もっと別の方法での工夫がいるかなと思います。

終わりに

raspbianなどのバージョンも変わると設定方法もいい感じで簡単になってきているのでいいですね。

Raspberry Pi Desktopをインストール(2018.06.03版)

Raspberry Pi Desktopをインストール

以前からあったx86版のRaspberryPixel(Raspbian JESSIE With Pixelが正式名称だった?)がバージョンアップしていました。ちょっと、PC版のLinuxも少し触る機会があったのですが、aptコマンドではディストリビューションのアップグレードが出来ないようだった(バージョンがダウンしたような表記になってしまったw)ので、インストールし直すことにしました。

使用したPCは【Acer Aspire One 753】になります。 www.notebookcheck.net

結論からいうと以前やったものとはかなり印象的に変わっていたのでメモを取ることにしました。

【参照】 uepon.hatenadiary.com

OSの正式名称としてはRaspberry Pi Desktopという名称でいいのかなと思います。

www.raspberrypi.org

f:id:ueponx:20180602223417p:plain

USBメモリへイメージの転送

この部分に関しては以前と同様にEtcherを使用します。

etcher.io

f:id:ueponx:20180602225304p:plain

rufusでもいけるかなと思ったのですが、最新のrufusの書き込みモードにdd形式の選択がUIから消えていたので、Etcherで行ったほうがいいかもしれません。(古いバージョンだと問題はないのかなと勝手に予想)

f:id:ueponx:20180602223653j:plain

↑ dd形式の書き込みができない

書き込み操作は簡単で、インストールしたらEtcherを起動 【Select Image】ボタンを押してダウンロードしたOSイメージを選択します。

f:id:ueponx:20180602223711j:plain

イメージを選択したら次は【Select drive】で書き込み先のUSBメモリを選択します。

f:id:ueponx:20180602223724j:plain

ドライブ選択のダイアログが表示されます。

f:id:ueponx:20180602223736j:plain

あとは【Flash!】ボタンをクリックして書き込みを行います。

f:id:ueponx:20180602223743j:plain

下記の画面が出れば処理終了になります。

f:id:ueponx:20180602225348p:plain

ここまでは特に大きく差はありません。

USBをPCに挿してを起動する

起動すると以下のような画面が出てきます。前はこのような画面はなかったのですが…。このバージョンからはこのイメージはLiveイメージのようなものになり、Persistence領域(Liveイメージとは別の領域に変更した情報を入れる)ようになったようです。基本的にはメニューの一番上にある【Run with persistence】選択すれば良いようです。

f:id:ueponx:20180602230845p:plain

あとは起動すれば普通のRasbianと大きくは変わりません。ただ、以前のバージョンにはなかったraspi-configがある点が大きく変わった気がします。起動時にSSHの起動はありますが、VNCなどの起動がない(その他はRaspberryPi固有なのでなくても仕方ないですが)のが少し困った感じです。ただ、dpkg-reconfigureを使うよりはずいぶん進歩したなって感じです。

GUI起動もできますが、設定中の作業でCUI起動しかできなくなってる…(日本語フォントっぽい感じがしなくもない)

f:id:ueponx:20180603092306p:plain

CUIでも問題はないのでいいのですが…。

f:id:ueponx:20180603084458p:plain

f:id:ueponx:20180603084411p:plain

また、SSHに関しては有効化を行ってもそのままusernameとpasswordでログインできるモードではありませんでした。鍵推奨ということでその設定なのかもしれませんが、最初にしらないとなぜ?になってしまうかも。

SSHの設定変更

usernameとpasswordでログインできるようにするには /etc/ssh/sshd_configを編集することになります。

$ sudo vim /etc/ssh/sshd_config

ファイルの中ほどにある PasswordAuthentication noPasswordAuthentication yes に変更します。

変更後はサービスの再起動を行えばOKです。

$ sudo systemctl restart ssh
$ sudo systemctl enable ssh

(注)2行目は念の為ですが多分いらないような気がします。

これで無事にSSHができるようになりました。

f:id:ueponx:20180603083621p:plain

VNCの設定

次はVNCになります。Raspbianにデフォルトで入っているのはRealVNCなのですがなんとRaspberry Pi DesktopにはRealVNCのパッケージが存在しません。仕方ないので、昔使っていたTightVNCで代用します。

RealVNCwww.realvnc.com

【TightVNC】 www.tightvnc.com

以下2つの情報を参考に設定を行っていきます。

VNC server in Jessie (using a systemd service)

[メモ] Raspberry Pi : VNCサーバ設定(自動起動)

インストールに関しては以下でOKです。

$ sudo apt-get update
$ sudo apt-get install -y tightvncserver

インストール後、一度vncserverを起動します。起動しなくてもいいのですが一回目の起動時にパスワードを登録することになりますのでここでやっておいたほうが無難です。登録しなかった場合にはパスワードなしでの設定になります。(設定する場合にはvncpasswdコマンドを実行すれば再設定出来ます。)

Serverを手動にて起動し、パスワード設定を行う。

$ vncserver

つづいて設定ファイルの編集を行います。

$ sudo vim /etc/systemd/system/vncserver@.service

【/etc/systemd/system/vncserver@.service】

[Unit]
Description=Remote desktop service (VNC)
After=syslog.target network.target
[Service]
Type=forking
User=pi
PAMName=login
PIDFile=/home/pi/.vnc/%H:%i.pid
ExecStartPre=-/usr/bin/vncserver -kill :%i > /dev/null 2>&1
ExecStart=/usr/bin/vncserver -depth 24 -geometry 1280x800 :%i
ExecStop=/usr/bin/vncserver -kill :%i
[Install]
WantedBy=multi-user.target

編集後に設定を有効化して

$ sudo systemctl daemon-reload && sudo systemctl enable vncserver@1.service

再起動を行います。

$ sudo reboot

あと、Windowsからはリモートデスクトップ接続を使用してVNC接続を行いたいので xrdpをインストールしておきます。

$ sudo apt-get install -y xrdp

設定が完了するとWindowsからリモートデスクトップ接続でVNCへ接続できるようになります。

f:id:ueponx:20180603083757p:plain

f:id:ueponx:20180603084240p:plain

日本語化

日本語化に関しては以下を参照して行いました。ありがとうございます。

【参考】 PIXEL for x86 (RaspberryPiのデスクトップ環境)

日本語入力(IM)

$ sudo apt-get install fcitx-mozc  -y

インストール後に優先順位などの設定が必要かと思ったのですが大丈夫そうでした。

日本語フォント

$ sudo apt-get install fonts-ipafont fonts-ipaexfont fonts-takao

Chromiumの日本語化

$ sudo apt-get install chromium-l10n

f:id:ueponx:20180603084240p:plain

libreofficeの日本語化

$ sudo apt-get install libreoffice-help-ja

f:id:ueponx:20180603084724p:plain

Node.jsのインストール

これで大丈夫かなーと思ったのですが、開発環境も一応確認。 pythonは2系も3系もそれなりに使えそうなバージョンだったのですが、Node.jsはインストールされていませんでした。aptでインストールをするのですが、インストールされるのはVersion4系。さすがにletなどが使えないということでもあったので、少なくとも現在のLTSのバージョンにはしておこうと思います。(ちなみにNode 4.x is End Of Lifeとのことです。)

ただ、Version4系の頃にはそもそもnpmのパッケージがないので、nを使おうと思ってもnpmを経由してインストールは出来ない状況でした。

公式情報でインストール方法があったので、それを使用してインストールを行います。

$ curl -sL https://deb.nodesource.com/setup_8.x | sudo -E bash -
$ sudo apt-get install -y nodejs
$ sudo apt-get install -y build-essential

これでバージョンが8.11.2のLTSまで来ました。npmも同時にインストールされるのでバージョンが5.6になっています。

$ node -v && npm -v
v8.11.2
5.6.0

おわりに

一応これで、Windows7時代のCeleronなPCもそれなりに作業用に使用できるようになりました。実の所はUbuntuにする予定だったのですがUSBにうまく入れられなかったので苦し紛れには近いです。インストール時の苦労を考えるとUSBメモリを2つ使ってUbuntuを入れるのがいいと思います。

…どんなだよ。

ちなみにScratch2も日本語設定できています!

f:id:ueponx:20180603093629p:plain

f:id:ueponx:20180603093725p:plain

Microsoft Flow経由でWioNodeに接続したリレーを制御してLED表示灯を点灯してみる【後編】

Microsoft Flow経由でWioNodeに接続したリレーを制御してLED表示灯を点灯してみる【後編】

前回のエントリーでMicrosoft Flowの設定が終わりました。これでメールが送信されてくることでリレー回路を制御して電源などのON/OFFといったようなことを行うことができます。

uepon.hatenadiary.com

あとはLED表示灯と接続するだけです。 回路といってもリレー側にLEDをつける、ただそれだけって感じなんですが。

今回使用するLED表示灯は以下のものになります。

LED小型表示灯NE-24型(DC24V仕様) https://www.patlitestore.jp/images/large/NE_LRG.jpg

昔はLED表示灯も結構いい値段していましたが、最近はやすくなってきてますよね。

こちらの表示灯は電源電圧が24VなのでWioNodeでUSBの電源とは別に電源が必要になります。5Vでうまくいく表示灯があればもう少しシンプルになったかなと思いますがありものを使っているのでしかたないかなと思います。ACアダプタに関しても家に転がっていたACアダプタを使用しています。

akizukidenshi.com

あと表示灯の配線図にはフューズが必要ってことだったんですが、手持ちがないのでここはバッサリカットしましたw。ちゃんと使うときにはいれてくださいね。 配線図の外部接点の部分にWioNodeに接続したリレーを挟み込めば基本的には完了です。

https://www.patlitestore.jp/images/setsumeiform/ne_haisen.png

以下のような配線になっています。

f:id:ueponx:20180513002651p:plain

実行してみる

前回のエントリーで使用した設定した、メールのパターンで送信をしてみます。

f:id:ueponx:20180502161605p:plain

メールを受信するとLEDの表示灯がうまく点滅してくれました。

www.youtube.com

終わりに

あとはケースって感じなんですが、ここが一番めんどくさいんですよね…。 ケースにいれて設置ができたら、このエントリーに写真を追加しておこうと思います。

WioNodeの個人的になんとかならないかなと思っているのは本体に固定穴がないというところです。 ケースに固定する方法があんまり想像できないのでこういうときってどうするんでしょうか。

そのあたりの知識がほしいなあ。あと、Fritzingで回路図がかけるようにもなりたいですね。(独自パーツの作り方のほうが近いかもしれませんが)

【参考】

uepon.hatenadiary.com

Microsoft Flow経由でWioNodeに接続したリレーを制御してLED表示灯を点灯してみる【前編】

Microsoft Flow経由でWioNodeに接続したリレーを制御してLED表示灯を点灯してみる

年度が替わって、ほげほげと仕事をしていたところ

「某社、FAXの運用やめるってよ」

という話がきました。自分の担当は24h/365dな環境で連絡があってもメールなどに目を向けてほしくない(監視作業)ので、可能であれば そういう確認の作業には時間を割きたくないのでFAXのほうが作業が楽です。(どうせ内職することがわかっていて且つ質が落ちる)でも、個人の意見としては伝送手段にFAX使わなくてもよいので撤廃も時代なんだろうなという点では納得はしています。

業務としては、連絡があったときにブザーが鳴って紙面に印刷してくれればこだわりはありませんし、最悪、情報の確認の忘れさえ防げれば問題はありません。 つまりブザーや音声、パトランプだけでもいい割り切りができます。(音声も監視しているので、できればブザーや音声は鳴らしたくないけど電話はあるのでそこは目を瞑ります)

んじゃ作ってみるか

要素としては単純なので、まあできるかなーとは思っていたのですが仕事なので以下の点が引っかかります。

  • 手持ちの機材でなんとかできるといいなあ
  • コードは書きたくないなあ(嘘くさいけど)
  • メンテナンス(ハード・ソフトともに)はあんまりしたくないなあ
  • 少しはセキュリティ関連にも配慮がしたいなあ

うちの会社にはなんちゃって技術者っぽい人しかいない(弊社には一般にいう開発者はいません、キリッ!)ので 引き継ぎのことも考えると、コードを書くなんていう愚行行為は薦めることができません。 ちなみに、自社でプログラミングするという雰囲気をみせると絶命する拒絶反応を見せる人が多いです。

こういう前提がある中で採用するものの候補を考えてみました。

要素としては

  1. 定期的なメールの受信確認
  2. 受信時にリレーの制御

これだけが抑えられればいいので、ハードとしてはWioNodeGroveでリレーを付ければよさそうです。

WioNodeはWebへのPOSTメソッドでのアクセスができればいいので

  1. 定期的なメールの受信確認
  2. 受信時にWebアクセス(→リレー制御)

こんな感じになります。

さて、次にメールの受信検知に関してなのですが、Office365なメールを使用しているので、そちらを使用し、IFTTTMicrosoft Flowを選択肢として考えました。 (実質にたようなサービスはありますので類似したサービスでもいい感じがします。)Microsoft Flowでは認証がOffice365上でできることもあるので、今回はこちらを採用することにしました。

  1. 定期的なメールの受信確認(Microsoft FlowのOffice365メールの受信をトリガとする)
  2. メール受信時にWebアクセス(→リレー制御)(Microsoft FlowのWebアクセスをアクションとする)

ほぼこれで処理が出来上がったような気がします。

WioNodeの設定

ハードウエアはこちらを準備しました。(家に転がっていたので)

【WioNode】

【GROVE - リレー】

まずは使用するWioNodeの設定を行います。

設定に関しては基本的に過去のエントリーを参照してもらえればいいかなと思います。

uepon.hatenadiary.com

今回は、以前設定を行ったWioNodeWiFiのアクセスポイントの変更処理を行う必要があったので、その部分を記載しておきます。 (デバイスを削除して新規に行っても問題はありません)

ネットワークの再設定

スマートフォンのアプリを使用してデバイスの設定のハンバーガーボタンを押し、その中から【Change WiFi Network】を選びます。

f:id:ueponx:20180501172635p:plain

ここで確認のダイアログが表示されるので、変更を行う場合には【Confirm】を選択します。

f:id:ueponx:20180501172645p:plain

【Confirm】を押すとネットワークの変更手順を最初から行うことになります。ただし、この画面からでも設定中断するとネットワークの設定は変更されないようです。

f:id:ueponx:20180501172654p:plain

リレーの設定

WiFiの設定が変更できたら、今度は接続デバイスの設定を変更します。メニューのOutputから【Relay】を探し出し、

f:id:ueponx:20180501172729p:plain

Groveの端子にドラッグ&ドロップします。

f:id:ueponx:20180501172743p:plain

設定の変更が行われると画面の下に【Update Firmware】というボタンが表示されるのでクリックすると、

f:id:ueponx:20180501172751p:plain

接続されたセンサーなどのファームウェアWioNodeにダウンロードされて

f:id:ueponx:20180501172759p:plain

センサーが使用可能な状態になります。 続いては設定後のテストを行ってみます。

APIの確認

設定と同様に右上のハンバーガーボタンをクリックし、その中から【View API】を選びます。

f:id:ueponx:20180501172635p:plain

するとAPIの説明とテストページが表示されるのでこれを使ってテストを行います。

f:id:ueponx:20180501172806p:plain

リレーのモジュールには状態の確認用のAPIと状態(ON/OFF)の変更のAPIの2つがあります。

f:id:ueponx:20180501172814p:plain

ページ内からもAPIへのアクセスができるのでこれで動作を確認することができます。

Microsoft Flowの設定

以下のURLにアクセスをします。認証が必要であることもあるのでその際はログインをしてください。

japan.flow.microsoft.com

画面上部に【マイフロー】をクリックします。

f:id:ueponx:20180501133512p:plain

すると自分の作成したフロー処理の一覧が表示されます。

f:id:ueponx:20180501133734p:plain

この画面上でウインドウの上の方にある【一から作成】をクリックして新しいフロー処理を作成します。

f:id:ueponx:20180501134409p:plain

すると画面が以下のように変わるので更に【一から作成】ボタンをクリックします。

f:id:ueponx:20180501134644p:plain

これでフロー処理を作成する準備ができました。ウインドウ内にコネクタとトリガーの候補が並んでいます。

f:id:ueponx:20180501134754p:plain

表示されていない場合には検索バーにサービス名を入力することになりますが、今回はOffice365 Outlookがトリガーになりますのでこれを選択します。

f:id:ueponx:20180501135218p:plain

ユーザによっては候補一覧の並びが異なっていると思いますので、その場合には検索バーにOutlookと入力することで選択肢を絞ることができます。今回はOffice365 Outlookを選択します。(以下のアイコンのものになります。)

f:id:ueponx:20180501135343p:plain

アイコンをクリックすると、サービスに含まれるトリガーが一覧として表示されます。 今回は【Office365 Outlook メールが届いたとき】のトリガーをこの中から選択します。

f:id:ueponx:20180501135709p:plain

すると以下のような表示にかわります。デフォルトではInboxフォルダにメールが受信メールが保存されたことがトリガーになっています。

f:id:ueponx:20180501135843p:plain

【新しいメールが届いたとき】ボックスの中の【詳細オプションを表示する】をクリックすると設定を詳細にすることができます。

f:id:ueponx:20180501140148p:plain

詳細項目は以下のようになっています。

  • フォルダー
  • 宛先
  • 開始
  • 重要度
  • 添付ファイルあり
  • 添付ファイルを含める
  • 名フィルター

今回はメーリングリストから送信されたメールのfromアドレスと件名を確認することでトリガーとして設定を行いますので以下のオレンジ色の部分に関係する情報を入力します。(宛先は不要かも)

f:id:ueponx:20180501140711p:plain

テストだけをおこなうのであれば、【宛先】【開始】は自分のメールアドレスでもいいかなと思います。今回はテストなので、グレーアウトしている部分は自分のアドレスにしてみました。件名はML_Titleが含まれるものとしています。

f:id:ueponx:20180501141054p:plain

これでメールの受信した際のトリガーが作成できました。そのトリガーに対応するアクションを記述していきます。トリガー設定の下にある【新しいステップ】ボタンをクリックします。

f:id:ueponx:20180501141300p:plain

すると、【アクションの追加】、【条件の追加】、【さらに追加】と選択肢が出るので今回はこの中から【アクションの追加】をクリックします。

f:id:ueponx:20180501141448p:plain

クリックすると今度はアクションの選択画面になります。メール受信後にWebアクセスを行うことになるので検索BOXにHTTPと入力し、絞り込みを行います。

f:id:ueponx:20180501141638p:plain

絞り込みを行うと次のような画面になります。コネクタのHTTPをクリックし

f:id:ueponx:20180501141841p:plain

f:id:ueponx:20180501141928p:plain

さらにアクション一覧の中からHTTPをクリックします。

f:id:ueponx:20180501141950p:plain

一覧から下のものを選びます。

f:id:ueponx:20180501142117p:plain

するとWebアクセスの設定画面になります。

f:id:ueponx:20180501142142p:plain

設定項目は

  • 方法
  • URI
  • ヘッダー(KeyとValueのペア)
  • 本文

となります。WioNodeのリレーのアクセス設定は以下のようになっているので

f:id:ueponx:20180501172814p:plain

  • 方法:POST
  • URIhttps://us.wio.seeed.io/v1/node/GroveRelayD1/onoff/1?access_token=XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX(XXXXXXXXXXX…の部分は固有のキーになりますので各自のものに修正をお願いします。)

f:id:ueponx:20180501142618p:plain

こんな感じでOKです。

あとはこのフロー処理に名前を付けて保存をします。画面上部の無題のところを今回はMail_WioNode_Relayとしてみました。日本語名でも問題はありません。名前を変更したら上部にある【保存】ボタンをクリックします。

f:id:ueponx:20180501143653p:plain

保存したら名前の部分の左側にある【←】をクリックします。こんな風に表示されていればOKです。Office365との接続ができていないと画面の右側が少し違うかもしれません。事前に接続用の認証を行えば問題ないかと思います。また、画面右上の【このフローはオン】になっていればテストもできるようになっています。

作成中に認証ができていない場合はこんな風に表示されます。

f:id:ueponx:20180502002841p:plain

フロー作成後に認証が完了していない場合はこんな風に表示されます。

f:id:ueponx:20180502002918p:plain

f:id:ueponx:20180501144156p:plain

では、テストを行ってみます。

テストしてみる!

あとはメールを送信して、RelayがONになるかをテストしてみます。今回はfromもtoも自分でsubjectにML_Titleが含まれるようなメールを送信しています。

f:id:ueponx:20180501145331p:plain

送信し、受信が行われると

f:id:ueponx:20180501145720j:plain

リレー基板にあるLEDが点灯します。(結構時間がかかるので2-3分ぐらいかかっても目をつぶってください)

f:id:ueponx:20180501145833j:plain

一応、動作してくれました。ログ上にも成功(Succeeded)として表示されています。

f:id:ueponx:20180501150123p:plain

制御は成功したんのですが、これ点灯したLEDどうやって消そうw。

どうやって消灯させよう

基本的には、備忘的な表示灯なので一定時間ついていたら消灯するという感じでもいいのかなとは思いますが、Groveにはボタンもあるのでそれを使おうかと思っていました。ですが、ボタンの押下処理はWebSocketを使用するため、常時アクセスのようなものが必要になってしまいます。(Arduino化すればいいのかもしれないけど…後任が引き継げなくなるのでそれはやめたい。)

一定時間光ったり、点滅させることで今回は妥協しようかと思います。

IFTTTではループ処理などができなかったような気がしましたが、Microsoft Flowではループ処理も問題なく作成することができます。

こんな感じで処理を考えました。

  • メールの受信トリガー
  • カウンタ変数初期化
  • ループ処理(カウンタ値が5以上になったら抜ける)
    • リレーON
    • 5秒ウエイト
    • リレーOFF
    • 5秒ウエイト
    • カウンタ変数をインクリメント

上記にするためのポイントだけを抜粋します。

変数を追加する

フロー処理に変数を追加する場合にはアクションの追加で検索Boxに変数と入力します。

f:id:ueponx:20180501153354p:plain

変数のアクションの一覧から【変数の初期化】をクリックします。

f:id:ueponx:20180501153432p:plain

すると以下のように表示されるので、【名前】(変数名)【種類】(型)【値】(初期値)を設定していきます。

f:id:ueponx:20180501153504p:plain

今回は変数名をcount、型を整数、値を0として初期化を行いました。

f:id:ueponx:20180501153631p:plain

変数をインクリメントする

同様に定義した変数を更新するには フロー処理に変数を追加する場合にはアクションの追加で検索Boxに変数と入力します。

f:id:ueponx:20180501153354p:plain

変数アクションの中から【変数の値を増やす】などのアクションを選択します。

f:id:ueponx:20180501163940p:plain

すると以下のように表示が変更されるので

f:id:ueponx:20180501164100p:plain

既に定義済みの変数の値を変更する形になるので、【名前】には定義済みの変数名、【値】はインクリメントをする増分値を設定します。

先ほど作成した変数値を+1する場合には以下のような設定をすることになります。

f:id:ueponx:20180501164429p:plain

ループを生成する

ループを生成するには【新しいステップ】>【さらに追加】>【Do Untilの追加】を選択します。

f:id:ueponx:20180501154023p:plain

すると設定画面が表示されます。

f:id:ueponx:20180501154231p:plain

【値の選択】を行おうとするとこれまでに使用したトリガやアクションで使用されている値が候補として表示されます。今回はループ制御に先ほど初期化したcount変数を用いますので【以前の手順からパラメータを挿入】>【変数count】をクリックして選択を行います。

f:id:ueponx:20180501154510p:plain

クリックすると以下のようになります。

f:id:ueponx:20180501154646p:plain

今回は5回の処理を行うことを考えていたので以下のように設定を完了させます。

f:id:ueponx:20180501154818p:plain

ループの中の処理に関してはループ内の【アクションの追加】などを追加していくことになります。ちなみにBox内はループの処理、外部はループを抜けてからの処理になります。ループの外にある処理をドラッグ&ドロップして移動することも可能です。

f:id:ueponx:20180501155313p:plain

HTTPの処理をループ内にドラッグ&ドロップさせると

f:id:ueponx:20180501155144p:plain

上記のようになります。このようにして処理をネストさせていくことになります。

タイマー処理を行う(Delay)

タイマー処理のアクション名がスケジュールなのに気が付くまでに2時間ぐらいかかりました。タイマーとかの方が探しやすい…アクションの検索Boxにスケジュールと入力して候補を絞ります。

f:id:ueponx:20180501155439p:plain

その中から【スケジュール - 待ち時間】を選択すると

f:id:ueponx:20180501155500p:plain

以下のような設定画面になります。

f:id:ueponx:20180501155529p:plain

5秒間の待ち時間(wait)を今回は使用するため、以下のように設定しました。

f:id:ueponx:20180501155553p:plain

最終形

最終的な処理のフローはこんな感じになります。

f:id:ueponx:20180501164741p:plain

おわりに

一応、これでやりたいことはできた感じです。点灯と消灯の間隔を5秒にしていますが、処理時間にかなり誤差があります。それより短い値にしても動作に大きく変化がないので5秒としています。(この部分に関して言えば、正確さを求めてはいけないかなと思います)

長くなったので、回路部分に関しては次回のエントリーにしようと思います。

GoogleColaboratoryから希望するディレクトリにGoogleスプレッドシートを作成する

希望するディレクトリにGoogleスプレッドシートを作成する

前回のエントリーのなんとなく続きのエントリーになります。

uepon.hatenadiary.com

前回は意図したGoogleDriveのディレクトリにスプレッドシートを作成することができませんでした。作成できるのはルート(マイドライブ)の直下のみの制限付き。(gspreadの制限という感じですが…)

今回は、直接GoogleDriveの指定したパスに対してファイル(スプレッドシート)を作成し、以後そのスプレッドシートのKey(id)またはURLを別途取得してファイルを更新をするにはということを考えたいと思います。

使用するもの

今回はgspread以外にPyDriveというパッケージを使用することにしました。

pypi.python.org

github.com

PyDriveは以下のような機能を持っています。

  • Simplifies OAuth2.0 into just few lines with flexible settings.
  • Wraps Google Drive API into classes of each resource to make your program more object-oriented.
  • Helps common operations else than API calls, such as content fetching and pagination control.

簡単にいうとGoogleDriveの操作用のラッパーパッケージになります。こちらもわざわざGoogleDriveをマウントしなくてもファイル群を使えるので便利です。マウント作業とはいったい何だったのか!

PyDriveモジュールのインストール

PyDriveのインストールは以下のコマンドを実行すればOKです。

!pip install PyDrive

f:id:ueponx:20180406161707p:plain

無事にインストールできました。

基本的にはこれまでと同様に認証してからファイルへのアクセスを行うことになります。 ドキュメントをみると、以下のようなコードを実行すれば認証ができるということだったのですが、

from pydrive.auth import GoogleAuth
from pydrive.drive import GoogleDrive

gauth = GoogleAuth()
gauth.LocalWebserverAuth()

drive = GoogleDrive(gauth)

ちょっと書き換えをして以下の様にしてみました。

from pydrive.auth import GoogleAuth
from pydrive.drive import GoogleDrive
from google.colab import auth
from oauth2client.client import GoogleCredentials

# auth.authenticate_user()
gauth = GoogleAuth()
gauth.credentials = GoogleCredentials.get_application_default()
drive = GoogleDrive(gauth)

print('--- PyDrive Auth OK ---')

コードを実行してみるとこんな感じになります。

f:id:ueponx:20180408004235p:plain

恒例の認証処理を行っていきます。基本はクリック程度の作業で、一度行ってしまえばインスタンスが有効である間は認証の2回目以降省略されます。

まずは使用するアカウントを選択します。

f:id:ueponx:20180406183813j:plain

アカウントを選択したらGoogleDriveのアクセスへの許可を行います。【許可】ボタンをクリックします。すると以下の様な認証コードが表示されるので

f:id:ueponx:20180406183918j:plain

コードをコピーして

f:id:ueponx:20180407183436p:plain

Notebookのタブに戻って入力ボックスにこのコードをペーストして

f:id:ueponx:20180408004323p:plain

Enterキーを押します。

f:id:ueponx:20180407182758p:plain

これで認証は完了です。自由にGoogleDriveへアクセスができるようになっています。 以下のようにすればファイルの作成ができるようになります。

from pydrive.auth import GoogleAuth
from pydrive.drive import GoogleDrive
from google.colab import auth
from oauth2client.client import GoogleCredentials

auth.authenticate_user()
gauth = GoogleAuth()
gauth.credentials = GoogleCredentials.get_application_default()
drive = GoogleDrive(gauth)

print('--- PyDrive Auth OK ---')

file1 = drive.CreateFile({'title': 'Hello.txt'})
file1.SetContentString('Hello')
file1.Upload()

print('--- PyDrive CreateFile OK ---')

実行すると以下のようになります。

f:id:ueponx:20180407190133p:plain

正常に実行できました。あとはGoogleDriveにファイルが生成されているかを確認します。

f:id:ueponx:20180407184423p:plain

Hello.txtのファイルが無事にできていると思います。内容にも'''Hello```と入っているかなと思います。内容を簡単に説明するとファイルオブジェクトを作成しているのがCreateFile()、ファイルの内容のセットがSetContentString()、具体的なGoogleDriveファイルの生成処理がUpload()になっています。

うまく行ったかなと思うのですが…ちょっと気になる点があります。この処理を2回実行すると同名のファイルが2つ作成されてしました。 画像としては以下の様になります。

f:id:ueponx:20180407190447p:plain

そのままCreateFile()を行うとファイル名は同じですがKeyは異なるファイルが生成されるようです。別途上書きをするメソッドもなさそうです。 つまり、2回目以降のアクセスに関しては名前からファイルを作成するのではなく一意に振られるKeyやURLを使用する必要があります。

重複したファイルが生成されないようにするには?

いろいろ考えてみました。

簡単に考えると以下のような処理になるかなと。

  1. ファイルの所在を確認
  2. ファイルが所在していなければファイルを作成
  3. ファイルのKey(id)を取得
  4. Key(id)を使用してgspreadなどでスプレッドシートのファイルを開く
  5. スプレッドシートの編集

ファイルの所在の確認処理は、PyDriveを使って各ディレクトリを走査し、 ファイル一覧を取得していくことになります。また、ファイルの作成は、GoogleDriveディレクトリ構成を書くファイルの属性値として持っている構成なので、その中のparent_id属性に親フォルダのkey(id)を与えることで指定したディレクトリの下にファイルを作成することになります。

例えば/aaa/bbb/c.sheetなるファイルを処理する場合にはこうなるのかなと思います。

  1. マイドライブのファイルリストからaaaというディレクトリ名のkeyを取り出す
  2. 前の手順で取得したkeyをもとにマイドライブ/aaaのファイルリストからbbbというディレクトリ名のkey(id)を取り出す
  3. 前の手順で取得したkeyをもとにマイドライブ/aaa/bbbのファイルリストの中にc.sheetというファイルがあるか調べる
  4. c.sheetというファイルがなければファイルを作成する。(作成時にKey(id)が取得できるようになる)
  5. c.sheetというファイルがあればKey(id)が取得する
  6. 取得したスプレッドシートファイルのkey(id)を使ってgspreadを使用して処理を行う

もう少しロジックを考えてみた

とはいっても、GoogleDriveはフラットな構成なのでリスト取得は一回やればいいのかなと思うので以下の様に修正してみました。

  1. マイドライブのファイルリストを取得する
  2. ファイルリスト中にparentがマイドライブ(root)でありaaaという名前のディレクトリがあるか調べる
  3. ない場合にはフォルダを作成する
  4. aaaディレクトリのkeyを取得する
  5. ファイルリスト中にparentがkeyでありbbbという名前のディレクトリがあるか調べる
  6. ない場合にはフォルダを作成する
  7. bbbディレクトリのkeyを取得する
  8. ファイルリスト中にparentがkeyでありc.sheetという名前のファイルがあるか調べる
  9. c.sheetというファイルがなければファイルを作成する
  10. c.sheetのkeyを取得する
  11. 取得したスプレッドシートファイルのkeyを使ってgspreadを使用して処理を行う

処理が増えたような気がしますが単純になったこととリストを何回も取得しなくて良くなった点はよいのかなと思います。

このロジックから以下のようなidの取得までのコードをかいてみました。(途中のパス(ディレクトリ)でエラーが発生した場合の対応できてません)

from pydrive.auth import GoogleAuth
from pydrive.drive import GoogleDrive
from google.colab import auth
from oauth2client.client import GoogleCredentials

auth.authenticate_user()
gauth = GoogleAuth()
gauth.credentials = GoogleCredentials.get_application_default()
drive = GoogleDrive(gauth)

print('--- PyDrive Auth OK ---')

def get_id(path_str):

  fileDict = {}

  file_list = drive.ListFile({'q': "trashed=false"}).GetList()
  for f in file_list:
    if(len(f['parents']) != 0): # 他のユーザからの共有ファイルはparents属性がない。
      # print('title: %s, id: %s, parent_id %s' % (f['title'], f['id'], f['parents'][0]['id']))
      tmp = {'title':f['title'], 'ID':f['id'], 'parentsID':f['parents'][0]['id']}
      fileDict[f['title']] = tmp

  #for key, value in fileDict.items():
  #  print(key, value)

  path_str_list = path_str.strip('/').split('/')

  print('-> pathList:')
  for i,p in enumerate(path_str_list):
    if(p in fileDict):
      print(fileDict[p])
    else:
      print('ファイル新規作成')
      nf = drive.CreateFile({'title': p, 'mimeType': 'application/vnd.google-apps.spreadsheet', 'parents': [{'kind': 'drive#parentReference', 'id': fileDict[path_str_list[i-1]]['ID']}]})
      nf.Upload()
      return nf['id']
  else:
    return fileDict[p]['ID']
  
path_string = "/Colab Notebooks/sample/SpreadSample"
d = get_id(path_string)
print('id = %s' % d)

注意点としてはファイルの作成時にMime-Typeを指定していないとスプレッドシートファイルを作成できない点でしょうか。具体的に抜き出すとこの処理になります。

nf = drive.CreateFile({'title': p, 'mimeType': 'application/vnd.google-apps.spreadsheet', 'parents': [{'kind': 'drive#parentReference', 'id': fileDict[path_str_list[i-1]]['ID']}]})

ここ実行するとこんな感じになります。

f:id:ueponx:20180421143800p:plain

スプレッドシートもできています。

f:id:ueponx:20180421144103p:plain

ちなみに2回目の実行をおこなってみたのですが、新規作成は行われませんでした。

f:id:ueponx:20180421145246p:plain

f:id:ueponx:20180421145428p:plain

一応、無事に動作できたようです。

終わりにというか、追記

今回はこんなロジックでつくってみたのですが、GoogleDriveのファイル数多くある場合にはかなり遅くなります。そのため、最初に作成したロジックのものも作ってみました。ファイル数が多い場合にはこちらのほうが有効かもしれません。

from pydrive.auth import GoogleAuth
from pydrive.drive import GoogleDrive
from google.colab import auth
from oauth2client.client import GoogleCredentials

auth.authenticate_user()
gauth = GoogleAuth()
gauth.credentials = GoogleCredentials.get_application_default()
drive = GoogleDrive(gauth)

print('--- PyDrive Auth OK ---')

# ファイルのPathからIDを検索する
def get_id_list(path_str):

  # parentsのidで検索してフォルダ、ファイル名が一致するオブジェクトのidを返す
  def find_path_id(parent_id, name):
    file_list = drive.ListFile({'q': "'%s' in parents and trashed=false" % parent_id}).GetList()
    for f in file_list:
      if (f['title']==name):
        return f['id']
  
  path_str_list = path_str.strip('/').split('/')
  parent_id_dict = {}
  filename = path_str_list[-1]
      
  print('---Process---')
  parent_id = 'root'
  for p in path_str_list:
    current = p
    # print('parent_id: %s current: %s' % (parent_id, current))
    parent_id_dict[current] = parent_id
    parent_id = find_path_id(parent_id, current)
  print('<-- parent_id -->: %s <-- current -->: %s' % (parent_id , current))
  if(parent_id == None):
    print('ファイル新規作成')
    print('<-- parent_id -->: %s <-- filename -->: %s' % ((parent_id_dict[filename]), filename))
    f = drive.CreateFile({'title': filename, 'mimeType': 'application/vnd.google-apps.spreadsheet', 'parents': [{'kind': 'drive#parentReference', 'id': parent_id_dict[filename]}]})
    f.Upload()
    # print(f['id'])
    return f['id']
  else:
    id = find_path_id(parent_id_dict[filename], filename)
    # print(id)
    return id
  
path_string = "/Colab Notebooks/sample/SpreadSample"
try:
  d = get_id_list(path_string)
  print('id = %s' % d)
except:
  print('raise exception')

1回目の実行 f:id:ueponx:20180421153831p:plain

2回目の実行 f:id:ueponx:20180421153910p:plain

あとはこのkey(id)を使用して、以下のようなコードを追記してあげると

import gspread

gc = gspread.authorize(GoogleCredentials.get_application_default())
sh = gc.open_by_key(key)
worksheet = sh.get_worksheet(0)

# set value
worksheet.update_acell('A1', 'foo')

# set values
cell_list = worksheet.range('A2:C3')
for cell in cell_list:
    cell.value = 'bar'

worksheet.update_cells(cell_list)

# get value
val = worksheet.acell('A1').value
print(val)

# get value
val = worksheet.cell(2, 2).value # B2 cell
print(val)

実行するとこんな感じでちゃんと動作していました。

f:id:ueponx:20180421161053p:plain

スプレッドシートにもちゃんと値も設定されていますし、値も取得できました!

f:id:ueponx:20180421161135p:plain

一応、やりたいことはできたのですがかなり辛かった…多分もっと簡単な方法があるに違いない。

【参考】

uepon.hatenadiary.com uepon.hatenadiary.com uepon.hatenadiary.com uepon.hatenadiary.com

Google ColaboratoryでGoogleスプレッドシートを読み書きしてみる

Google ColaboratoryでGoogleスプレッドシートを読み書きしてみる

前回のエントリでは、GoogleColabで画像表示ができるようになりました。これでOpenCVを使った画像処理も安心して勉強ができるようになりました。

uepon.hatenadiary.com

では、そのほかのファイル、特にWebスクレイピングをした結果をGoogleスプレッドシートに格納といった用途はあるかなと思います。 具体的にはセルから値を取り出したり、格納したりとなりますが、それができるかを確認してみようと思います。

f:id:ueponx:20180408110634p:plain

必要になるパッケージ

GoogleColabというかpythonGoogleスプレッドシートのファイルを扱うには‘‘‘gspread‘‘‘というモジュールを用いると便利です。

github.com

特徴としては…

  • Google Sheets API v4.
  • Open a spreadsheet by its title or url.
  • Extract range, entire row or column values.
  • Python 3 support.

python3に対応しているので助かります。また、スプレッドシートを開く際にはファイル名(タイトル)かURLが指定できるので、共有設定の入っているようなGoogle Drive上のスプレッドシートに関してもデータの操作が行えるのが便利です。

gspreadパッケージのインストール

以下のコマンドでインストールすることができます。

!pip install gspread

既にインストール済みであれば-Uつけてインストールしますが今のところ不要の様です。

f:id:ueponx:20180406161141p:plain

このように表示されればインストールは完了です。

gspreadパッケージを使用することで以前のエントリーのような特別なGoogle Driveディレクトをマウントする作業なしで(というかこのパッケージとgoogle.colabパッケージ、oauth2clientパッケージを使用することで、直接Google Driveへアクセスする事ができます)

python側の標準的なファイルアクセス処理を行う場合にはマウントすることにもメリットはありますが、Googleスプレッドシートを単に読み込んだりするだけであればgspreadパッケージを使用するほうが楽かもしれません。(認証作業の回数も減りますし)

基本的な操作

基本的な操作としてはGithubのパッケージのドキュメントで抑えられると思いますが、念のため。

github.com

以下のテストの実行にあたっては、事前にマイドライブ上SpreadsheetSampleというスプレッドシートファイルを作成しておきます。存在しないとエラーが出ます!

Google Drive上は以下のような感じになっていて

f:id:ueponx:20180406162730p:plain

ファイルは存在していればよいので、ワークシートは空で大丈夫です。

f:id:ueponx:20180406183020p:plain

ではNotebookに以下のコードを張り付けて実行してみます。 内容としてはファイルをオープンして指定したセルに値を格納し、格納後さらに値を取得するという単純なものです。

from google.colab import auth
from oauth2client.client import GoogleCredentials
import gspread

# 認証処理
auth.authenticate_user()
gc = gspread.authorize(GoogleCredentials.get_application_default())

# 'SpreadsheetSample'というスプレッドシートの先頭ワークシートをオープン
worksheet = gc.open('SpreadsheetSample').get_worksheet(0)

# A1セルに'foo'という値を上書き
worksheet.update_acell('A1', 'foo')

# A2からC3のセルエリアに'bar'を一括で上書き
cell_list = worksheet.range('A2:C3')
for cell in cell_list:
    cell.value = 'bar'
worksheet.update_cells(cell_list)

# A1セルの値を取得し、表示
val = worksheet.acell('A1').value
print(val)

# A1セルを0,0とするようなセル指定で
# 2,2(B2)の位置のセルを取得
val = worksheet.cell(2, 2).value
print(val)

このコードを【Shift】+【Enter】で実行します。

すると、認証処理が実行されます。(一度認証すればインスタンス実行中は認証処理は不要の様です)

f:id:ueponx:20180406183720j:plain

認証用のURLと認証キーのinputboxが表示されるので、URLのリンクをクリックします。 すると使用するDriveのアカウント選択に遷移します。

f:id:ueponx:20180406183813j:plain

使用するアカウントを選択すると、使用可能な機能の確認画面に遷移します。

f:id:ueponx:20180406183918j:plain

問題ないかを念のため確認して【許可】ボタンをクリックします。すると画面が遷移し、認証キーが発行されるのでこれを コピーして、Notebookのタブへ移動します。

f:id:ueponx:20180406184041j:plain

あとは認証キーをinputboxに張り付けて

f:id:ueponx:20180406184147j:plain

【Enter】キーを押せば認証が完了します。認証の完了後はPythonのコードが実行され結果表示に以下のような表示がされれば 正常に実行できました。

【実行結果】

foo
bar

f:id:ueponx:20180406163443p:plain

では、スプレッドシートの中身を確認してみます。

f:id:ueponx:20180406184524p:plain

A1のセルに'foo'が入っていて、A2-C2、A3-C3の領域に'bar'が格納されていれば正常に動作しています。 これでスプレッドシートの値の取得や格納もNotebookのコードから実行ができるようになりました。 意外と簡単にできました。

マイドライブ以外のディレクトリに存在する既存のスプレッドシートを開いてみる

先ほどはマイドライブ内にあるスプレッドシートのファイル名を指定して開きましたが、今度は任意のフォルダにあるスプレッドシートを開いてみます。 マイドライブの下にsheetディレクトリを作成し、その中にsampleというスプレッドシートファイルを作成します。

以下の画面のような状況になります。

f:id:ueponx:20180406193026p:plain

f:id:ueponx:20180406193114p:plain

ではこれを開いてみます。

エラーの例(読まなくても問題ないので飛ばしましょう)

単純に以下のようなコードでいいのかなと思うのですが…

from google.colab import auth
from oauth2client.client import GoogleCredentials
import gspread

# 認証処理
auth.authenticate_user()
gc = gspread.authorize(GoogleCredentials.get_application_default())

# 'マイドライブ/sheet/sample'というスプレッドシートの先頭ワークシートをオープン
worksheet = gc.open('./sheet/sample').get_worksheet(0)```

ダメです。エラーがでます。

f:id:ueponx:20180406193657p:plain

エラーメッセージとしてはSpreadsheetNotFound:となっているのでパス指定に問題がありそうです。 何回か実験してみたのですが、python上でカレントディレクトリの変更をしたりしてみたのですが、パス指定を変えることが出来なさそうです。 Google Drive上のファイルは名前の方やパスを属性として扱い、基本的にはツリー構造ではなくフラットな構造になっているため、 基本はファイルの所在としてはマイドライブの下にあるのも、ある特定のディレクトリにあることも大きな差がないということの様です。

うまくいった例

前のエラーを踏まえて、ではどうするか?

ファイルをオープンする方法としてgspreadモジュールではkeyをしてする方法とURLから指定する方法があるようです。 ドキュメントには以下のような記述があります。

# If you want to be specific, use a key (which can be extracted from
# the spreadsheet's url)
sht1 = gc.open_by_key('0BmgG6nO_6dprdS1MN3d3MkdPa142WFRrdnRRUWl1UFE')

# Or, if you feel really lazy to extract that key, paste the entire url
sht2 = gc.open_by_url('https://docs.google.com/spreadsheet/ccc?key=0Bm...FE&hl')

Google Driveに格納されるファイルには固有のキーがあるのでそれを使用するようです。URLも基本的には固有キーを使用したURLになっているので同じような感じです。 つまりキーがわかっていれば問題ありません。

ファイルの固有キーをつかって任意のフォルダのスプレッドシートへアクセス

スプレッドシートをブラウザで開いて

f:id:ueponx:20180406201013p:plain

ブラウザのURL表示の中の https://docs.google.com/spreadsheets/d/14TU4**********************************************Hxt1Y/edit#gid=0(自分のファイルのものを使用してください)

/d/のあとから次の/までの間が固有キーになります。それを使用して以下のようなスクリプトを実行します。

from google.colab import auth
from oauth2client.client import GoogleCredentials
import gspread

# 認証処理
auth.authenticate_user()
gc = gspread.authorize(GoogleCredentials.get_application_default())

# 'マイドライブ/sheet/sample'というスプレッドシートをオープン
sh = gc.open_by_key('14TU4**********************************************Hxt1Y')
worksheet = sh.get_worksheet(0)

# set value
worksheet.update_acell('A1', 'key')

f:id:ueponx:20180406201547p:plain

スプレッドシートにも反映されています。

f:id:ueponx:20180406201640p:plain

URLをつかって任意のフォルダのスプレッドシートへアクセス

スプレッドシートをブラウザで開いて

f:id:ueponx:20180406194727p:plain

ブラウザのURLボックスの中身がそのままURLになります。(URLの末尾に/edit#gid=0がついています。本来はないものが正しいURLですが、末尾に編集情報がついていても問題はないようです)

f:id:ueponx:20180406201055p:plain

このURLをつかってスプレッドシートへアクセスします。 以下のコードを実行します。

from google.colab import auth
from oauth2client.client import GoogleCredentials
import gspread

# 認証処理
auth.authenticate_user()
gc = gspread.authorize(GoogleCredentials.get_application_default())

# 'マイドライブ/sheet/sample'というスプレッドシートをオープン
sh = gc.open_by_url('https://docs.google.com/spreadsheets/d/14TU4**********************************************Hxt1Y/edit#gid=0')
worksheet = sh.get_worksheet(0)

# set value
worksheet.update_acell('A1', 'URL')

実行結果・編集したスプレッドシートがこのように変化すれば正常に実行されています。

f:id:ueponx:20180406201917p:plain

f:id:ueponx:20180406202020p:plain

オープン処理はできました

これで既存にあるスプレッドシートのファイルに関してはおおよそ処理できるようになりました。 これでもいいのですが…新規にファイルを作成することだってありますよね?

スプレッドシートの作成

先ほどの例ではあらかじめあるスプレッドシートを読み込んでいましたが、今度は新規に作成してみようと思います。

新規にスプレッドシートを作成してみる

マニュアルをみると以下のような記述で大丈夫のようです。

sh = gc.create('test_sheet')

では新規作成してみましょう。新規作成するスプレッドシートの名前は'test_sheet'にしてみます。

from google.colab import auth
from oauth2client.client import GoogleCredentials
import gspread

# 認証処理
auth.authenticate_user()
gc = gspread.authorize(GoogleCredentials.get_application_default())

# 'SpreadsheetSample'というスプレッドシートの先頭ワークシートをオープン
worksheet = gc.create('test_sheet').get_worksheet(0)

# A1セルに'foo'という値を上書き
worksheet.update_acell('A1', 'Create')

# A1セルの値を取得し、表示
val = worksheet.acell('A1').value
print(val)

このコードを実行すると以下のようになります。

f:id:ueponx:20180406203817p:plain

作成されたのはマイドライブの直下に作成されています。

f:id:ueponx:20180406204016p:plain

スプレッドシートのセルも期待通り編集されています。

f:id:ueponx:20180406204119p:plain

これでめでたしめでたし…となるはずなんですが、やっぱりなんとなくしっくりきません。 時系列のデータをファイルを作りつつ保存するような処理をするほうが比較的一般的ではないかと思います。

と入っても、gspreadモジュールでできるのは以下の2つになります。

  • マイドライブ直下へのファイル作成(open()、open_by_url()、open_by_key())
  • マイドライブ・任意のディレクトリのスプレッドシートへのアクセス処理(create())

どうやらgspreadモジュールだけでは難しいようです。

終わりに

長くなったのでこのあとは別のエントリーにしようと思います。 次回はディレクトリを指定してスプレッドシートの読み書きを作成するにはという感じになると思います。 (途中まで書いていてあまりにも長く感じたので分けました)

これまで、あんまりネットワーク上のストレージを使ったプログラムを使ったことがなかったので結構戸惑ったような感じでしたが、 こういう考え方や扱いがむしろ普通になってきているんしょうね。

【関連エントリ】

uepon.hatenadiary.com

uepon.hatenadiary.com

uepon.hatenadiary.com