AWS Lambda の Ruby ランタイムを試す

 AWS re:Invent 2018 の Keynote の中で AWS Lambda や Serverless 関連のアップデートが色々と発表されましたが、その中に AWS Lambda で Ruby がサポートされたという発表がありました。

aws.amazon.com

 下記のように AWS のブログでチュートリアルも公開されているので、今回はとりあえず Ruby で Lambda を動かしてみます。

aws.amazon.com

 現在サポートされている Ruby のバージョンは 2.5 で、 AWS SDK for Ruby はデフォルトで使えるようになっているようです。

まずは Hello World

 とりあえずは Lambda Function を作成して実行してみます。関数の作成画面では下記画像のようにランタイムとして Ruby2.5 が選択できるようになっています。

f:id:akanuma-hiroaki:20181201133930p:plain

 関数名やロール名は任意に決めて下記のような内容で作成します。

f:id:akanuma-hiroaki:20181201134519p:plain

 デフォルトで生成されるコードは下記のような内容です。

require 'json'

def lambda_handler(event:, context:)
    # TODO implement
    { statusCode: 200, body: JSON.generate('Hello from Lambda!') }
end

 テストイベントを下記のように空のリクエストで設定します。

f:id:akanuma-hiroaki:20181201135128p:plain

 正しく実行できていれば下記のように成功レスポンスが表示されます。 

f:id:akanuma-hiroaki:20181201135426p:plain

 ログは下記のような出力になります。

Response:
{
  "statusCode": 200,
  "body": "\"Hello from Lambda!\""
}

Request ID:
"04397307-f507-11e8-abba-97121f20de35"

Function Logs:
START RequestId: 04397307-f507-11e8-abba-97121f20de35 Version: $LATEST
END RequestId: 04397307-f507-11e8-abba-97121f20de35
REPORT RequestId: 04397307-f507-11e8-abba-97121f20de35  Duration: 10.04 ms  Billed Duration: 100 ms     Memory Size: 128 MB Max Memory Used: 31 MB  

RubyGems を使う(Zipアップロード)

 Ruby を使うからにはやっぱり gem が使いたくなります。AWS ブログのチュートリアルでは SAM でアップロードする方法が紹介されていますが、先にシンプルに zip に固めたものをアップロードする方法を試してみます。

 まずは作業用ディレクトリを作ります。

$ mkdir lambda_gem_sample
$ cd lambda_gem_sample/

 gem は bundler を使って管理するので、下記の内容で Gemfile を作成します。今回はとりあえず gem が使えることが確認できれば良いので、 uuid という gem を使ってみます。

source 'https://rubygems.org'
gem 'uuid'

 bundler のインストール等については説明は割愛しますが、 インストールされているものとして下記コマンドを実行します。ローカルで使うだけであれば bundle install だけで使えますが、 zip に固めてアップロードするには gem のファイルもローカルにダウンロードしておく必要があるので、 bundle install --deployment も実行します。

$ bundle install
$ bundle install --deployment

 実際に実行する Ruby スクリプトは下記のような内容で lambda_function.rb というファイル名で作成します。生成した UUID をレスポンスの文字列に含めているだけのものになります。

require 'json'
require 'uuid'

def lambda_handler(event:, context:)
    uuid = UUID.new
    { statusCode: 200, body: JSON.generate("Generated UUID: #{uuid.generate}") }
end

 ここまでで必要なものは用意できたので、作業ディレクトリのファイルを zip に固めます。

$ zip -r lambda_gem_sample.zip ./*

 作成した zip ファイルを Lambda のコンソールからアップロードします。正しくアップロードされていれば下記のようにアップロードしたファイルがコンソールに表示されます。

f:id:akanuma-hiroaki:20181201141646p:plain

 空のテストイベントを作成してテストを実行すると、下記のように UUID の gem を使用したコードが実行され、生成された UUID が含まれた文字列がレスポンスとして返ってきます。

f:id:akanuma-hiroaki:20181201141943p:plain

RubyGems を使う(SAM)

 チュートリアルで紹介されている SAM を使う方法も試してみます。まずは作業ディレクトリを作成します。

$ mkdir hello_lambda_ruby
$ cd hello_lambda_ruby/

 Gemfile は下記のような内容で作成します。 aws-record は DynamoDB を操作するための gem です。

source 'https://rubygems.org'
gem 'aws-record', '~> 2'

 Gemfile を作成したら bundle install を実行します。

$ /Users/akanuma/.rbenv/shims/bundle install
$ /Users/akanuma/.rbenv/shims/bundle install --deployment

 Ruby のスクリプトは下記のような内容で hello_lambda_ruby_record.rb というファイル名で作成します。ちなみに AWS ブログに掲載されているコードをそのままコピペすると、 ENV[‘DDB_TABLE’] のシングルクォートが正しくない(アポストロフィーになってる?)ので実行時にエラーになります。

require 'aws-record'

class DemoTable
  include Aws::Record
  set_table_name ENV['DDB_TABLE']
  string_attr :id, hash_key: true
  string_attr :body
end

def put_item(event:,context:)
  body = event["body"]
  item = DemoTable.new(id: SecureRandom.uuid, body: body)
  item.save! # raise an exception if save fails
  item.to_h
end 

 AWS SAM はサーバレスアプリケーションの構成を管理するためのツールで、 Lambda アプリケーションの構造やセキュリティーポリシーの定義、AWSリソースの作成や管理を行うことができます。今回は DynamoDB を使用するのでそのための設定も含んでいます。設定ファイルは YAML で下記のような内容を template.yaml として作成します。

AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: 'sample ruby application'

Resources:
  HelloLambdaRubyRecordFunction:
    Type: AWS::Serverless::Function
    Properties:
      Handler: hello_lambda_ruby_record.put_item
      Runtime: ruby2.5
      Policies:
      - DynamoDBCrudPolicy:
          TableName: !Ref RubyExampleDDBTable 
      Environment:
        Variables:
          DDB_TABLE: !Ref RubyExampleDDBTable

  RubyExampleDDBTable:
    Type: AWS::Serverless::SimpleTable
    Properties:
      PrimaryKey:
        Name: id
        Type: String

Outputs:
  HelloLambdaRubyRecordFunction:
    Description: Hello Lambda Ruby Record Lambda Function ARN
    Value:
      Fn::GetAtt:
      - HelloLambdaRubyRecordFunction
      - Arn

 AWS SAM のテンプレートは CloudFormation のコンソールか、 AWS CLI、 AWS SAM CLI のいずれかを使ってデプロイすることができます。このチュートリアルでは AWS SAM CLI でデプロイしていますので、まだインストールしていない場合はインストールしておきます。また、 s3 のバケット作成に AWS CLI も使用していますので、こちらもまだであればインストールしておきます。

 CLI の準備ができたら、アプリケーションのコードをホストするための s3 のバケットを作成します。

$ aws s3 mb s3://hello-lambda-ruby
make_bucket: hello-lambda-ruby

 AWS SAM CLI を使用して、アプリケーションをパッケージングします。これによって packaged-template.yaml というファイル名のテンプレートファイルが作成されます。

$ sam package --template-file template.yaml \
> --output-template-file packaged-template.yaml \
> --s3-bucket hello-lambda-ruby                                                                                                                                                                                                               
Uploading to 02055cc5fec807d137f97cd532f60cd5  993290 / 993290.0  (100.00%)
Successfully packaged artifacts and wrote output template to file packaged-template.yaml.
Execute the following command to deploy the packaged template
aws cloudformation deploy --template-file /Users/akanuma/workspace/hello_lambda_ruby/packaged-template.yaml --stack-name <YOUR STACK NAME>

 続いて AWS SAM CLI を使ってアプリケーションをデプロイします。この時にAWSユーザに CloudFormation の権限が足りていないと下記のようにエラーになります。

$ sam deploy --template-file packaged-template.yaml \
> --stack-name helloLambdaRubyRecord \
> --capabilities CAPABILITY_IAM

An error occurred (AccessDenied) when calling the CreateChangeSet operation: User: arn:aws:iam::365361468908:user/hiroaki.akanuma is not authorized to perform: cloudformation:CreateChangeSet on resource: arn:aws:cloudformation:ap-northeast-1:365361468908:stack/helloLambdaRubyRecord/*

 とりあえず CloudFormation のフルアクセス権限を追加したかったのですが、ポリシーを検索してもそれに当たるものが見つからなかったので、インラインポリシーで直接フルアクセス権限を追加してみました。

f:id:akanuma-hiroaki:20181201151456p:plain

 追加後に実行すると下記のようにデプロイが成功しました。

$ sam deploy --template-file packaged-template.yaml --stack-name HelloLambdaRubyRecord --capabilities CAPABILITY_IAM

Waiting for changeset to be created..
Waiting for stack create/update to complete
Successfully created/updated stack - HelloLambdaRubyRecord

 デプロイが終わると Lambda のアプリケーションコンソールに下記のようにアプリケーションが表示されます。

f:id:akanuma-hiroaki:20181201151636p:plain

 アプリケーションのリソースには Lambda Function と DynamoDB のテーブルが含まれています。

f:id:akanuma-hiroaki:20181201151828p:plain

 Lambda Function の方を選択して Lambda のコンソールを開き、下記のような内容のテストイベントでテストを実行します。

f:id:akanuma-hiroaki:20181201152026p:plain

 正しくデプロイされていてコードの内容に間違いがなければ、下記のように成功レスポンスが返ってきます。

f:id:akanuma-hiroaki:20181201152126p:plain

 DynamoDB のコンソールからテーブルの中身を見てみると、テストイベントで送信したリクエストの内容が保存されています。

f:id:akanuma-hiroaki:20181201152216p:plain

まとめ

 Lambda で Ruby が使えるようになったことで、 Ruby エンジニアにとってはかなりハードルが下がりましたね。bundler 管理で gem を使うこともできますので、色々と便利に使えそうです。ローカルでの開発と比べると、開発途中での gem の追加の容易さや、修正から実行確認の手返しの良さは劣るところになりそうですが、サーバレス環境での開発方法も最適な方法を探していきたいと思います。

MicroPython版の M5Stack Avatar を作ってみた

 以前の記事で MicroPython のスレッドを使ってみたときに、簡単にアバター表示をしてみました。

blog.akanumahiroaki.com

Arduino 版のアバターは @meganetaaan さんが公開されていて、こちらを参考にさせていただいています。

github.com

 前回はとりあえず自分のところで表示させるために実装してましたが、その時のアバターをもう少し使いやすくして、とは言えだいぶ雑ではありますが GitHub に載せてみました。

github.com

機能

 今のところはまだ大した機能はなく、下記のみとなっています。

  • アバターの顔表示&瞬き

  • アバターが話している風にテキストをスクロール表示する

  • 何か気づいた風にエクスクラメーションマークを表示する

  • 青ざめた感じの表情にする

 アバター表示と瞬きはそれぞれスレッドを使用して実行しています。それ以外は単純にLCDに描画しているだけです。

使い方

 使い方は単純で、 m5stack_avatar.py をダウンロードし、 main.py 等から import します。今のところは特に依存している外部ライブラリはないので、 M5Stack での開発ができる環境ができていればその他には必要ありません。

 初期化とアバターの表示までは下記のようにします。

from m5stack_avatar import M5StackAvatar

avatar = M5StackAvatar()
avatar.start()

 喋っている風にテキストをスクロール表示するには speak() メソッドを使用します。

avatar.speak('Hello from M5StackAvatarPython!!')

f:id:akanuma-hiroaki:20181124215207j:plain:w500

 エクスクラメーションを ON/OFF するには exclamation_on()/exclamation_off() メソッドを使用します。

avatar.exclamation_on()
avatar.exclamation_off()

f:id:akanuma-hiroaki:20181124215233j:plain:w500

 青ざめた感じで顔に縦線表示するのは pale_on()/pale_off() メソッドを使います。

avatar.pale_on()
avatar.pale_off()

f:id:akanuma-hiroaki:20181124215303j:plain:w500

使用例

簡単な使用例として、上記機能をループで順番に繰り返す処理は下記のようになります。

from m5stack_avatar import M5StackAvatar

import time

avatar = M5StackAvatar()
avatar.start()

while True:
    avatar.speak('Hello from M5StackAvatarPython!!')
    time.sleep(10)
    avatar.exclamation_on()
    time.sleep(5)
    avatar.exclamation_off()
    time.sleep(5)
    avatar.pale_on()
    time.sleep(5)
    avatar.pale_off()
    time.sleep(5)

 これを実行した様子は下記のような感じになります。

まとめ

ある程度汎用的に使えるようにしようと思うとなかなか難しいところも多いですね。あと太めの斜線を描画しようと思うと簡単にはいかなかったりと色々とありますが、まずは不十分でも公開してみようということで、今後機能追加や改善していけると良いなと思っています。改善点のご指摘等ありましたらぜひいただければと思います。

M5Stack で Google Calendar のスケジュールを表示する(MicroPython)

 M5Stack でスケジュール管理に役立つ機能が実装できないかなと思い、 Google Calendar に登録しているスケジュールを表示させてみました。

Google Calendar API の利用設定

 まずは Google Calendar API を利用できるように設定する必要があります。GCP のコンソールから API とサービスを追加 をクリックします。

f:id:akanuma-hiroaki:20181117143616p:plain

 API のリストの中から Google Calendar API をクリックします。

f:id:akanuma-hiroaki:20181117143733p:plain

 API の詳細ページで 有効にする をクリックして API を使える状態にします。

f:id:akanuma-hiroaki:20181117143844p:plain

 次に API の認証情報を作成します。左メニューから 認証情報 をクリックします。

f:id:akanuma-hiroaki:20181117143949p:plain

 認証情報の種別を選択するプルダウンで サービスアカウントキー を選択します。

f:id:akanuma-hiroaki:20181117144053p:plain

 サービスアカウント名には任意の名前を設定します。役割については今回は参照だけできれば良いので、 閲覧者 を設定しました。サービスアカウント ID はサービスアカウント名から自動的に設定されます。キーのタイプはデフォルトが JSON になっているのでそのままにしておきます。最後に 作成 をすると認証情報が作成され、ダウンロードできるようになりますので、ローカルに取得しておきます。

f:id:akanuma-hiroaki:20181117161048p:plain

Google Calendar の共有設定

 次に先ほど作成したサービスアカウントからカレンダーを参照できるように、共有設定を行います。共有するカレンダーの共有設定画面から ユーザーの追加 をクリックします。

f:id:akanuma-hiroaki:20181117144504p:plain

 作成したサービスアカウントのサービスアカウント ID を設定します。権限は閲覧権限のみにしておきます。設定したら 送信 をクリックします。

f:id:akanuma-hiroaki:20181117144710p:plain

AWS Lambda の関数作成

 Google Calendar 側の設定はここまでで完了なので、次に AWS Lambda の関数を作成します。関数の作成画面で任意の関数名を指定します。ランタイムは今回は Python 3.6 を使用しています。

f:id:akanuma-hiroaki:20181117144835p:plain

 認証情報はコードの中に極力書きたくないので、 Lambda の実装画面で環境変数にサービスアカウントのキー ID を設定しておきます。

f:id:akanuma-hiroaki:20181117144930p:plain

 Google Calendar API を使うために Google が提供しているクライアントモジュールを使用します。 Lambda で外部モジュールを使用するにはローカルで zip に固めたものをアップロードする形になります。まずはローカルのプロジェクト用ディレクトリで pip を使用して google-api-python-client と oauth2client をインストールします。

$ pip3 install --upgrade google-api-python-client oauth2client -t ./

 下記のようなファイルがインストールされます。 Google Calendar API の認証情報作成時にダウンロードした認証情報ファイルも google_key.json として同じディレクトリに置いておきます。

$ ls -l
total 112
drwxr-xr-x   4 akanuma  staff    128 Nov 17 12:39 __pycache__
drwxr-xr-x   4 akanuma  staff    128 Nov 17 12:39 apiclient
drwxr-xr-x  12 akanuma  staff    384 Nov 17 12:39 cachetools
drwxr-xr-x   9 akanuma  staff    288 Nov 17 12:39 cachetools-3.0.0.dist-info
drwxr-xr-x   4 akanuma  staff    128 Nov 17 12:39 google
drwxr-xr-x   7 akanuma  staff    224 Nov 17 12:39 google_api_python_client-1.7.4.dist-info
-rw-r--r--   1 akanuma  staff    539 Nov 17 12:39 google_auth-1.6.1-py3.7-nspkg.pth
drwxr-xr-x   8 akanuma  staff    256 Nov 17 12:39 google_auth-1.6.1.dist-info
drwxr-xr-x   9 akanuma  staff    288 Nov 17 12:39 google_auth_httplib2-0.0.3.dist-info
-rw-r--r--   1 akanuma  staff   8434 Nov 17 12:39 google_auth_httplib2.py
-rw-r--r--@  1 akanuma  staff   2345 Nov 17 11:24 google_key.json
drwxr-xr-x  15 akanuma  staff    480 Nov 17 12:39 googleapiclient
drwxr-xr-x   8 akanuma  staff    256 Nov 17 12:39 httplib2
drwxr-xr-x   7 akanuma  staff    224 Nov 17 12:39 httplib2-0.12.0-py3.6.egg-info
-rw-r--r--   1 akanuma  staff    625 Nov 17 12:37 lambda_function.py
drwxr-xr-x  17 akanuma  staff    544 Nov 17 12:39 oauth2client
drwxr-xr-x   7 akanuma  staff    224 Nov 17 12:39 oauth2client-4.1.3.dist-info
drwxr-xr-x   9 akanuma  staff    288 Nov 17 12:39 pyasn1
drwxr-xr-x  11 akanuma  staff    352 Nov 17 12:39 pyasn1-0.4.4.dist-info
drwxr-xr-x  31 akanuma  staff    992 Nov 17 12:39 pyasn1_modules
drwxr-xr-x  11 akanuma  staff    352 Nov 17 12:39 pyasn1_modules-0.2.2.dist-info
drwxr-xr-x  19 akanuma  staff    608 Nov 17 12:39 rsa
drwxr-xr-x  11 akanuma  staff    352 Nov 17 12:39 rsa-4.0.dist-info
drwxr-xr-x   9 akanuma  staff    288 Nov 17 12:39 six-1.11.0.dist-info
-rw-r--r--   1 akanuma  staff  30888 Nov 17 12:39 six.py
drwxr-xr-x   7 akanuma  staff    224 Nov 17 12:39 uritemplate
drwxr-xr-x   9 akanuma  staff    288 Nov 17 12:39 uritemplate-3.0.0.dist-info

 この中で *.dist-info は不要なので削除しておきます。

$ rm -rf *.dist-info 

 削除後のリストは下記のようになります。

$ ls -l
total 112
drwxr-xr-x   4 akanuma  staff    128 Nov 17 12:39 __pycache__
drwxr-xr-x   4 akanuma  staff    128 Nov 17 12:39 apiclient
drwxr-xr-x  12 akanuma  staff    384 Nov 17 12:39 cachetools
drwxr-xr-x   4 akanuma  staff    128 Nov 17 12:39 google
-rw-r--r--   1 akanuma  staff    539 Nov 17 12:39 google_auth-1.6.1-py3.7-nspkg.pth
-rw-r--r--   1 akanuma  staff   8434 Nov 17 12:39 google_auth_httplib2.py
-rw-r--r--@  1 akanuma  staff   2345 Nov 17 11:24 google_key.json
drwxr-xr-x  15 akanuma  staff    480 Nov 17 12:39 googleapiclient
drwxr-xr-x   8 akanuma  staff    256 Nov 17 12:39 httplib2
drwxr-xr-x   7 akanuma  staff    224 Nov 17 12:39 httplib2-0.12.0-py3.6.egg-info
-rw-r--r--   1 akanuma  staff    625 Nov 17 12:37 lambda_function.py
drwxr-xr-x  17 akanuma  staff    544 Nov 17 12:39 oauth2client
drwxr-xr-x   9 akanuma  staff    288 Nov 17 12:39 pyasn1
drwxr-xr-x  31 akanuma  staff    992 Nov 17 12:39 pyasn1_modules
drwxr-xr-x  19 akanuma  staff    608 Nov 17 12:39 rsa
-rw-r--r--   1 akanuma  staff  30888 Nov 17 12:39 six.py
drwxr-xr-x   7 akanuma  staff    224 Nov 17 12:39 uritemplate

 これを zip に圧縮しておきます。

$ zip -r google_calendar_m5stack.zip ./*

 圧縮した zip ファイルを Lambda のコンソールからアップロードします。

 メインの関数(lambda_function.py)の内容は下記のようにしました。クラスの初期化時に認証情報を取得し、 get_schedules() メソッドで Google Calendar API をコールしてスケジュールの情報を取得しています。今回はとりあえず直近5件のスケジュールの開始日時、終了日時とサマリだけ使用しています。

from dateutil.parser import parse
from apiclient import discovery
from oauth2client.service_account import ServiceAccountCredentials
import datetime
import httplib2
import json
import logging
import os

logger = logging.getLogger()
logger.setLevel(logging.INFO)

class GoogleCalendar:
    def __init__(self):
        self.service_account_id = os.environ['GOOGLE_SERVICE_ACCOUNT_ID']
        scopes = 'https://www.googleapis.com/auth/calendar.readonly'
        
        self.credentials = ServiceAccountCredentials.from_json_keyfile_name(
            'google_key.json',
            scopes = scopes
        )
        
        self.calendar_id = 'XXXXXXXXXXXXXXX@gmail.com'
        self.max_results = 5

    def get_schedules(self):
        http = self.credentials.authorize(httplib2.Http())
        service = discovery.build('calendar', 'v3', http = http)
        
        now = datetime.datetime.utcnow().isoformat() + 'Z'
        
        events_result = service.events().list(
            calendarId   = self.calendar_id,
            timeMin      = now,
            maxResults   = self.max_results,
            singleEvents = True,
            orderBy      = 'startTime'
        ).execute()
        
        events = events_result.get('items', [])
        
        if not events:
            logger.info('No upcoming events found.')
        
        schedules = []
        for event in events:
            start   = event['start'].get('dateTime', event['start'].get('date'))
            end     = event['end'].get('dateTime', event['end'].get('date'))
            summary = event['summary']
            schedules.append({
                'start':   parse(start).strftime('%Y/%m/%d %H:%M:%S'),
                'end':     parse(end).strftime('%Y/%m/%d %H:%M:%S'),
                'summary': summary
            })

        return schedules

def lambda_handler(event, context):
    calendar = GoogleCalendar()
    schedules = calendar.get_schedules()
    
    return {
        'statusCode': 200,
        'body': json.dumps({'schedules': schedules})
    }

API Gateway の設定

 Lambda の関数が作成できたので、次に関数を API として実行できるように、 API Gateway の設定を行います。 AWS Lambda の実装画面でトリガーの追加メニューから API Gateway を選択します。

f:id:akanuma-hiroaki:20181117145413p:plain

 トリガーの設定フォームで API Gateway の設定が行えます。新規の API を作成するか既存の API から選択するかを選べますので、今回は 新規 API の作成 を選択します。セキュリティでは API キー使用でのオープン を選択して、 API キーで認証するようにしておきます。

f:id:akanuma-hiroaki:20181117145549p:plain

 下記のように未保存の状態で API が作成されますので、 Lambda コンソールの 保存 ボタンをクリックして設定を保存します。

f:id:akanuma-hiroaki:20181117145636p:plain

 すると実際に API が作成され、下記のように API の情報が表示されます。下記画像は色々とマスクしてあります。

f:id:akanuma-hiroaki:20181117145914p:plain

 試しに API キーなしで API にアクセスしてみると、 Forbidden となりアクセスが拒否されます。

$ curl https://XXXXXXXXXX.XXXXXXXXXXX.ap-northeast-1.amazonaws.com/default/googleCalendarM5Stack
{"message":"Forbidden"}

 API キーを Header に設定してリクエストを投げると下記のように情報を取得することができます。

$ curl https://XXXXXXXXXX.XXXXXXXXXXX.ap-northeast-1.amazonaws.com/default/googleCalendarM5Stack --header 'x-api-key:EnXb6oRB957iVzKXXXXXXXXXXXXXXXXXXXXXXXXX'
{"schedules": [{"start": "2018-11-18", "end": "2018-11-19", "summary": "\u6771\u4eac\u30aa\u30d5\u30a3\u30b9\u79fb\u8ee2"}, {"start": "2018-11-18", "end": "2018-11-19", "summary": "\u30bf\u30c3\u30d7\u516c\u6f14\u30ea\u30cf"}, {"start": "2018-11-22", "end": "2018-11-23", "summary": "SORACOM Technology Camp 2018"}, {"start": "2018-11-22T13:30:00+09:00", "end": "2018-11-22T19:30:00+09:00", "summary": "SORACOM Technology Camp 2018"}, {"start": "2018-11-23T12:00:00+09:00", "end": "2018-11-23T22:00:00+09:00", "summary": "TAP\u516c\u6f14\u30ea\u30cf"}]}

M5Stack のファームウェア実装(MicroPython)

 では最後に M5Stack 側の実装です。コードの全体は下記の通りです。主な処理は Lambda 側でやっているので、 M5Stack 側では単純に API を呼んで結果をループで表示しているだけのものになります。

from m5stack import lcd

import time
import ujson
import urequests

class GoogleCalendar:
    def __init__(self):
        self.base_url = 'https://XXXXXXXXXX.XXXXXXXXXXX.ap-northeast-1.amazonaws.com/default/googleCalendarM5Stack'
        self.api_key = 'EnXb6oRB957iVzKXXXXXXXXXXXXXXXXXXXXXXXXX'

        lcd.setCursor(0, 0)
        lcd.setColor(lcd.WHITE)
        lcd.font(lcd.FONT_DejaVu18)
        self.fw, self.fh = lcd.fontSize()

    def get_schedules(self):
        headers = {'x-api-key': self.api_key}
        response = urequests.get(self.base_url, headers = headers)
        json = response.json()
        return json['schedules']

    def display(self, schedules):
        lcd.clear()
        lcd.setCursor(0, 0)
        for schedule in schedules:
            print(schedule)
            lcd.println("{}".format(schedule['start']))
            lcd.println(" {}".format(schedule['summary']))

calendar = GoogleCalendar()
while True:
    schedules = calendar.get_schedules()
    calendar.display(schedules)
    time.sleep(60)

動作確認

 実行した結果は下記のようになります。表示は適当ですが、とりあえず Google Calendar の情報を M5Stack に表示することができました。ただ、日本語はそのままでは表示できませんので、テスト用のスケジュールを英語で登録して表示してみました。

f:id:akanuma-hiroaki:20181117155257j:plain

まとめ

 今回ひとまず Google Calendar から情報が取得できるようになりました。実際に使用するには表示を見やすく工夫したり、エラーハンドリングなどももっとちゃんと作りこむ必要がありますが、スケジュールの情報が使えると色々やれそうな気がします。また、今回 Lambda を経由して Google Calendar にアクセスすることで、 Google Calendar の認証情報は Lambda 側に保持し、デバイス上には持たせない構成になっているのはセキュリティ面では良い点かと思います。

 今回 Google Calendar API の使い方については下記チュートリアルを参考にしました。

Python Quickstart  |  Calendar API  |  Google Developers

 また、 Lambda で外部モジュールを使う方法については下記サイトを参考にさせていただきました。

qiita.com

 Lambda から Google Calendar API を利用する方法については下記サイトを参考にさせていただいています。

www.yamamanx.com

SORACOM LTE-M Button で SMS 送信

 2018年10月下旬に販売開始された SORACOM LTE-M Button が 11月に入って出荷開始されました。

blog.soracom.jp

 ボタンの機能等についてはオフィシャルサイト等参照いただくとして割愛しますが、私も購入して出荷開始後にすぐ届いたので、チュートリアル的にSMS送信までをとりあえずやってみたので、PCでの設定手順を書いてみます。

f:id:akanuma-hiroaki:20181105075002j:plain:w400

デバイス登録

 まずは AWS IoT 1-Click に購入したボタンを登録します。 AWS IoT 1-Click のコンソールから デバイスの登録 をクリックします。

f:id:akanuma-hiroaki:20181105075232p:plain

 登録コードもしくはデバイスIDの登録フォームが表示されますので、 SORACOM LTE-M Button の場合はデバイスIDを入力します。デバイスIDはボタンの裏蓋を外した右下に QR コードと一緒に記載されています。入力したらフォーム右下の 登録 ボタンをクリックします。

f:id:akanuma-hiroaki:20181105075415p:plain

 この時コンソールにログインしているAWSアカウントに IoT 1-Click に関する権限が不足していると、フォーム右上に下記のように「Errors.General.UnknownWithDSN」というエラーが表示されます。

f:id:akanuma-hiroaki:20181105080023p:plain

 ちなみにスマートフォンアプリでの登録時にも権限が不足していればやはりエラーになります。こちらの方がエラーメッセージの内容はわかりやすく、「iot1click:InitiateDeviceClaim」という権限がないというエラーが表示されます。

f:id:akanuma-hiroaki:20181105080312p:plain:w400

 本当は各操作の権限を必要最低限で付与するべきですが、今回はとりあえずお試しということで、 「AWSIoT1ClickFullAccess」というポリシーを追加して、 IoT 1-Click に関する全ての権限を付与します。

f:id:akanuma-hiroaki:20181105080442p:plain

 権限付与後に再度デバイスIDを入力して 登録 ボタンをクリックすると、下記のようにボタンのクリックを待機する画面になります。

f:id:akanuma-hiroaki:20181105080637p:plain

 ここで実際に LTE-M Button をクリックすると、下記画面のように表示が変わり、デバイスが登録されます。

f:id:akanuma-hiroaki:20181105080659p:plain

 完了 ボタンをクリックすると、下記のようにデバイス一覧画面で登録したデバイスが確認できます。

f:id:akanuma-hiroaki:20181105080815p:plain

 デバイスID部分をクリックすると、下記のようにデバイスについての詳細が表示されます。デバイス登録時はデフォルトでは「無効」状態になっているので、 アクション から デバイスの有効化 をクリックして有効にします。

f:id:akanuma-hiroaki:20181106082605p:plain

プロジェクトの作成

 デバイスが登録できたので次はプロジェクトを作成します。プロジェクトとは自分の理解では、ボタンクリック時の動作を定義して、同様の処理を行いたいデバイスをグルーピングするためのもので、今回であれば、「ボタンをクリックしたら SMS を送信するという処理を定義して、その動作をさせたいデバイスを紐づけるためのもの」という事かと思います。

 プロジェクトを作成するにはプロジェクト画面で プロジェクトの作成 ボタンをクリックします。

f:id:akanuma-hiroaki:20181105081019p:plain

 プロジェクト名を入力して 次へ ボタンをクリックします。

f:id:akanuma-hiroaki:20181105081142p:plain

 ボタンクリック時の動作を定義するために、 デバイステンプレートの定義 をクリックします。

f:id:akanuma-hiroaki:20181105081248p:plain

 テンプレートのデバイスタイプとしては現状では すべてのボタンタイプ だけが表示されているのでこれをクリックします。

f:id:akanuma-hiroaki:20181105081337p:plain

 テンプレートが表示され、アクションはデフォルトでは「SMS を送信する」が選択されていますので、デバイステンプレート名を考えて入力します。

 アクションには「SMS を送信する」以外には「E メールを送信する」、「Lambda 関数の選択」という選択肢があります。 SMS ではなく E メールを送信するというだけであれば「E メールを送信する」を選択し、独自の処理を行わせたい時には「Lambda 関数の選択」を選択して、独自に実装した Lambda 関数を呼び出すように設定します。今回は「SMS を送信する」ケースを試します。

 テンプレートを設定したらプレイスメントの属性を設定します。プレイスメントとは、各デバイスが個別にもつ属性値になります。ここではこのテンプレートでのプレイスメントの属性のデフォルト値を設定します。各デバイスでプレイスメントが設定されなかった場合はこのデフォルト値が使われることになります。 SMS 送信のケースでは、電話番号とメッセージのデフォルト属性を設定します。

 最後に プロジェクトの作成 ボタンをクリックするとプロジェクトが作成されます。

f:id:akanuma-hiroaki:20181105081452p:plain

プレイスメントの作成

 デバイスの登録、プロジェクトの作成まで終わったので、最後に登録済みのデバイスをプロジェクトに紐付けます。 プレイスメントの作成 ボタンをクリックします。

f:id:akanuma-hiroaki:20181105081547p:plain

 プレイスメントの作成フォームが表示されますので、まず個別のデバイスを表すプレイスメント名を考えて入力します。

 次に デバイスの選択 をクリックして、登録済みのデバイスの中から今回紐づけるデバイスを選択します。下記画像はデバイス選択済みの状態です。

 最後にこのデバイス固有のプレイスメントとして SMS のメッセージの内容と電話番号を入力したら プレイスメントの作成 をクリックして完了です。

f:id:akanuma-hiroaki:20181105081846p:plain

SMS 送信実行

 ここまでで一通りの設定は完了なので、 LTE-M Button のボタンをクリックして動作を確認します。シングルクリック、ダブルクリック、長押しのそれぞれで下記のような SMS が送信されました。メインのメッセージはプレイスメントで設定したものですが、クリックタイプを示す内容も含まれています。

f:id:akanuma-hiroaki:20181105082508p:plain

f:id:akanuma-hiroaki:20181105082522p:plain

f:id:akanuma-hiroaki:20181105082535p:plain

まとめ

 権限が不足していた部分だけはちょっと手間取りましたが、それ以外は特にプログラミングすることもなく、コンソールから設定するだけで SMS 送信が実行できるようになってしまいました。 IoT 1-Click ではスマートフォンアプリも提供されていて、そちらでも一通りの設定が行えるので、 SMS 送信だけであれば PC すらなくてもボタンを利用することができてしまいます。 LTE-M なので Wi-Fi 設定も必要なく、ここまで手軽に使い始められてしまうのはすごいですね。機能としても3種類のクリックタイプのみというのがシンプルで良いです。今回はテンプレートの SMS 送信だけでしたが、 Lambda を実装すればアイディア次第で色んなことができるので、面白いことができないか考えてみたいと思います。

M5Stack をシリアル接続してデバッグする(MicroPython)

 最近は M5Stack を主に m5cloud を使って MicroPython で色々と試しています。 m5cloud での開発は、 M5Stack が Wi-Fi に繋がっていればPCと直接接続しなくても良いというのもメリットの一つだと思うのですが、その分デバッグはしづらいところがあります。ちょっとした確認であれば lcd.print() 等で画面に出力して確認することもできるのですが、 SyntaxError などでそもそもちゃんと動かない時には起動時に下記のような画面で止まってしまい、原因が何なのかがわかりません。

f:id:akanuma-hiroaki:20181103120541j:plain

 なのでやはりデバッグするにはシリアルケーブルで接続して、出力を確認しながらするのが効率的です。

シリアル接続

 私の Mac 環境では M5Stack を USB ケーブルで接続すると、下記のようなデバイスとして認識されます。

$ ls -l /dev/tty.SLAB_USBtoUART 
crw-rw-rw-  1 root  wheel   21,  24 Nov  2 08:27 /dev/tty.SLAB_USBtoUART

 screen コマンドでデバイスに接続します。ボーレートは 115,200 を指定します。

$ screen /dev/tty.SLAB_USBtoUART 115200

シリアル出力確認

 シリアル接続した状態で M5Stack を再起動すると、下記のようにシリアルコンソールに情報が出力されます。

[M5Cloud] Downloading:/flash/main.py  ......
[M5Cloud] Downloading:/flash/README.md  .
ets Jun  8 2016 00:22:57

rst:0xc (SW_CPU_RESET),boot:0x17 (SPI_FAST_FLASH_BOOT)
configsip: 0, SPIWP:0xee
clk_drv:0x00,q_drv:0x00,d_drv:0x00,cs0_drv:0x00,hd_drv:0x00,wp_drv:0x00
mode:DIO, clock div:2
load:0x3fff0018,len:4
load:0x3fff001c,len:4636
load:0x40078000,len:0
load:0x40078000,len:12948
entry 0x4007852c

Internal FS (SPIFFS): Mounted on partition 'internalfs' [size: 2424832; Flash address: 0x1B0000]
----------------
Filesystem size: 2221568 B
           Used: 56320 B
           Free: 2165248 B
----------------

Device ID:807d3ac471bc
Connect WiFi: SSID:XXXXXXXXXXXXXXXX PASSWD:XXXXXXXX network...
....
Connected. Network config: ('172.20.10.12', '255.255.255.240', '172.20.10.1', '172.20.10.1')
[M5-807d3ac471bc] M5Cloud connected.

FreeRTOS running on BOTH CORES, MicroPython task started on App Core (1).

 Reset reason: Soft CPU reset
    uPY stack: 19456 bytes
     uPY heap: 80000/19280/60720 bytes

MicroPython ESP32_LoBo_v3.2.16 - 2018-05-15 on M5Stack with ESP32
Type "help()" for more information.
>>>

 上記の例はエラーなく正常に起動できたケースですが、例えば SyntaxError があると上記の出力の途中に下記のようにエラーメッセージが出力されます。これでエラーの原因や場所が特定できます。

Traceback (most recent call last):
  File "main.py", line 11
SyntaxError: invalid syntax

 また、print デバッグを行いたいときは、 print() を使用すればその引数がシリアルコンソールに出力されます。例えば下記のように API のレスポンスの内容を print() で出力するようにしてみます。

response      = urequests.get(self.base_url.format(prefecture, self.api_key))
json          = response.json()
main          = json['main']
print(main)

 これを実行すると下記のように API のレスポンスがシリアルコンソールに出力されて行きます。

{'pressure': 1025, 'humidity': 54, 'temp_min': 283.15, 'temp_max': 287.15, 'temp': 284.75}
{'pressure': 1027, 'humidity': 74, 'temp_min': 283.15, 'temp_max': 287.15, 'temp': 284.81}
{'pressure': 1025, 'humidity': 54, 'temp_min': 288.15, 'temp_max': 288.15, 'temp': 288.15}
{'pressure': 1025, 'humidity': 54, 'temp_min': 283.15, 'temp_max': 287.15, 'temp': 284.75}
{'pressure': 1027, 'humidity': 74, 'temp_min': 283.15, 'temp_max': 287.15, 'temp': 284.81}

MicroPython のインタラクティブ実行

 正しく起動していれば、起動時の出力が終わると下記のように MicroPython のインタラクティブシェルが起動しますので、ここでインタラクティブの MicroPython を実行して動作を確認することができます。

MicroPython ESP32_LoBo_v3.2.16 - 2018-05-15 on M5Stack with ESP32
Type "help()" for more information.
>>> import time
>>> time.gmtime()
(1970, 1, 1, 0, 22, 50, 5, 1)
>>> 

 Python にはあっても MicroPython にはないクラスやメソッドもありますので、インタラクティブシェルで実際のボード上での挙動が確認できるのは便利ですね。

まとめ

 直接ケーブルで Mac と接続する必要はあるものの、上記のような情報なしで開発していくのは非効率なので、開発中はシリアルコンソールの利用が必須かと思います。今のところは m5cloud で開発をしていますが、バージョン管理等ができない不便さはあるので、開発もローカルで行ってシリアル接続で転送するやり方も検討してみようかと思います。

M5Stack Gray の MPU9250 の値を MicroPython で読み取る

 M5Stack Gray には MPU9250 が搭載されていて、加速度、ジャイロ、磁気を計測することができますが、買ってすぐに Arduino のサンプルスケッチを動かしてみただけだったので、 MicroPython で値を読み出してみました。

www.switch-science.com

MPU9250 モジュール

 M5Stack の MicroPython では MPU9250 モジュールが組み込まれているので、 import するだけで使えるようになっています。モジュールの内容は下記で参照することができます。

github.com

 MPU9250 は MPU6500(加速度、ジャイロ)+ AK8963(磁気) の組み合わせになっているようで、それぞれを明示的に初期化して MPU9250 の初期化時に指定することもできるようになっています。今回は磁気センサーの値から方位を計算することをしてみようとしたのですが、 AK8963 のキャリブレーションについてはメソッドが用意されているわけでもなさそうで、方法が調べ切れなかったので、ひとまず値をそのまま読み出すところまでにしています。

サンプルコード

 今回実装したサンプルは下記のような内容になります。 MPU9250 は I2C 接続になっていて、 SDA が 21番ピン、 SCL が 22 番ピンになっているようなので、 I2C のインスタンス作成時にそのピンを指定し、 MPU9250 のインスタンス作成時に I2C のインスタンスを渡しています。あとはそのインスタンスから acceleration gyro magnetic でそれぞれの値がタプルで返ってくるので、それをバラして画面表示させています。

from m5stack import lcd
from machine import I2C, Pin
from mpu9250 import MPU9250
import time

i2c = I2C(sda = 21, scl = 22)
sensor = MPU9250(i2c)

lcd.clear()
lcd.setCursor(0, 0)
lcd.setColor(lcd.WHITE)

fw, fh = lcd.fontSize()

lcd.print('Acceleration', 0, 0)
lcd.print('Gyro', 0, fh * 4)
lcd.print('Magnetic', 0, fh * 8)

while True:
    ax, ay, az = sensor.acceleration
    gx, gy, gz = sensor.gyro
    mx, my, mz = sensor.magnetic
    
    lcd.print(' ax: {:+.5f}'.format(ax), 0, fh * 1)
    lcd.print(' ay: {:+.5f}'.format(ay), 0, fh * 2)
    lcd.print(' az: {:+.5f}'.format(az), 0, fh * 3)
    
    lcd.print(' gx: {:+.5f}'.format(gx), 0, fh * 5)
    lcd.print(' gy: {:+.5f}'.format(gy), 0, fh * 6)
    lcd.print(' gz: {:+.5f}'.format(gz), 0, fh * 7)
    
    lcd.print(' mx: {:+.5f}'.format(mx), 0, fh * 9)
    lcd.print(' my: {:+.5f}'.format(my), 0, fh * 10)
    lcd.print(' mz: {:+.5f}'.format(mz), 0, fh * 11)
    
    time.sleep_ms(20)

 これを実行すると下記のような表示になります。

f:id:akanuma-hiroaki:20181027233113j:plain

 公式のサンプルも下記に公開されています。

https://github.com/m5stack/M5Cloud/blob/master/examples/mpu9250/basic/main.py

まとめ

 今回はとりあえずセンサーの値をそのまま読み出すだけでしたが、これらのセンサーの値はこのままでは意味がなく、計算して方位などがわかるようにしたかったのですが、そのためにはキャリブレーションが必要になってくるので、今後その方法も調べて、アバター等と組み合わせて有効に使えるようにしていきたいと思います。

M5Stack UI Flow で画像表示(v0.8.0)

 M5Stack UI Flow の v0.8.0 がリリースされて、簡単に画像が表示できるようになったようなので、試してみました。 公式のツイートはこちら。

 UI Flow の基本的な環境設定についてはこちらもどうぞ。

blog.akanumahiroaki.com

画像のアップロード

 まずは表示したい画像ファイルをアップロードします。 v0.8.0 では画面右上にファイルをアップロードするためのメニューが追加されていますので、これをクリックします。

f:id:akanuma-hiroaki:20181020201534p:plain

 すると下記のダイアログが表示されますので、 Images を選択します。 Blocklys の方は特にファイルのアップロードができるようにはなっていないようなので、今後機能が追加されていくのかもしれません。 Images の方ではすでにアップロード済みのファイルがあればリストが表示されます。アップロードしたファイルは画面をリロードしたり、ブラウザを閉じて再度アクセスした時にも保存されているようです。新しい画像ファイルをアップロードするには、 Add Image ボタンをクリックして、ローカルのファイルを選択してアップロードします。

f:id:akanuma-hiroaki:20181020202043p:plain

 ちなみにアップロードできる画像ファイルは JPEG のみで、25KB以下のものに制限されています。また、ファイル名も10文字以下という制約があります。

f:id:akanuma-hiroaki:20181020202626p:plain

f:id:akanuma-hiroaki:20181020202639p:plain

画像の配置

 画像がアップロードできたら次は画面に画像を配置します。 v0.8.0 では画面のコンポーネントに画像ファイルが追加されていますので、これをドラッグ&ドロップで配置します。

f:id:akanuma-hiroaki:20181020203354p:plain

 配置したモジュールを選択するとプロパティが表示されますので、 imgName のプロパティでアップロード済みの画像ファイルの中から表示したいものを選択します。

f:id:akanuma-hiroaki:20181020203736p:plain

 この状態で実行すると、下記画像のように画像ファイルが表示されます。 UI Flow の画面上で配置した画像コンポーネントには実際の画像サイズは反映されないので、実際の表示は実行して確認する必要があります。

f:id:akanuma-hiroaki:20181020204801j:plain

 他のコンポーネントと組み合わせて表示させれば、簡単に画面を構成することができます。ちなみに v0.8.0 ではラベルのフォントが選択できるようになっています。ラベルのプロパティにフォントのプロパティが追加されていますので、使用したいフォントを選択します。

f:id:akanuma-hiroaki:20181020204518p:plain

 上記の内容で実行した様子は下記画像のようになります。

f:id:akanuma-hiroaki:20181020204824j:plain

 とりあえず画像を表示することはできましたが、確認した限りではロジックの中で扱うことはまだできないようなので、静的な表示に限定されそうです。

m5cloud の場合

 ちなみに m5cloud で同様に画像を表示するとしたら、画像ファイルを m5cloud のメニューからアップロードした上で、下記のようなコードを実行すると大体同じような表示をすることができます。タイトルやラベルの表示はちょっと面倒ですが、画像の配置については lcd.image() で lcd.CENTER や lcd.BOTTOM などの指定をすることができるので、 UI Flow よりも位置が調整しやすいですし、ロジックの中に組み込んで使うことができますので、まだまだこちらの方が実用的ですね。

from m5stack import lcd

lcd.clear()
lcd.setCursor(0, 0)
lcd.setColor(lcd.WHITE, lcd.BLUE)

fw, fh = lcd.fontSize()
ww, wh = lcd.winsize()

lcd.rect(0, 0, ww, fh + 1, lcd.BLUE, lcd.BLUE)
lcd.println("Photo Album")

lcd.font(lcd.FONT_DejaVu24)
lcd.setColor(lcd.WHITE, lcd.BLACK)
lcd.print('My Cat', 10, 30)

lcd.image(lcd.CENTER, lcd.BOTTOM, 'IMG_s.JPG')

まとめ

 UI Flow v0.8.0 で画像を扱うことができるようになり、画像を表示するだけであればコードを書かずに実現できるようになりましたが、まだ静的な表示に限られるので使用用途は限られますね。ただ UI Flow は短いスパンでどんどんアップデートされてきているので、画像についても今後様々な使い方ができるようになってくるかと思います。Remote Config 等と組み合わせられると面白い気もするので、今後の機能追加に期待したいですね。