スティード400を復活 〜サスペンション&オイルフィルター交換編〜

昨年の夏に実家に眠っているスティード400 のキャブ洗浄を行いました。
kittagon.hateblo.jp

今年のGWにはオイルエレメントの交換と、サスペンションの交換をしたのでまとめます。
手探りでやったのであまり写真に残せませんでしたが。。

サスペンションの交換

僕のスティード400はリジットバーをつけているので、今回はそれを交換したいと思います。
リジットバーを付けていると車体が下がってカッコいいんですが、なにせショック吸収が皆無なのでお尻が結構辛いです。
高校生の頃はそれで良かったのですが、今の歳になると絶対にキツい、、、
なので純正とまでは行きませんが、最低限のショック吸収があるサスペンションに交換したいと思います。

今回はヤフオクで購入した、デイトナのサスペンションを装着します。
f:id:ti_taka:20190804005917j:plain

前回と同様に、シートとサイドカバーを外しておきます。
f:id:ti_taka:20190804010011j:plain
f:id:ti_taka:20190804010023j:plain
f:id:ti_taka:20190804010033j:plain

とりあえずリジットバーが固定されている、シート下側のボルトを緩めてしまいます。(↓の赤丸箇所) f:id:ti_taka:20190804010110j:plain

ここのボルトを引き抜くためには、後輪が動ける状態でないと外せないので、この辺りをジャッキアップします。
f:id:ti_taka:20190804010137j:plain

他のブログ等見てみると、ボルトにテンションを掛けておいて、スルッと外れる高さまでうまいこと丁度にジャッキアップすると書いてありますが、僕は微調整も面倒だったので、がっつりジャッキアップした後、後輪を手で持ち上げながらボルトを引き抜きました。
どうせ後から付けるサスペンションは高さが違うのでここで微調整してもつけるときはまた高さ調整しなくてはなりませんからね。

左右それぞれこれぐらいの高さまで上げてしまいました。 f:id:ti_taka:20190804010211j:plain

リジットバーが付いているので、リアのタイヤも浮いた状態になります。写真を撮るのを忘れましたが)

ボルトを外しておいて、スイングアームの↓の赤丸の辺りを持ち上げると、ボルトにかかるテンションが緩むので、持ち上げた状態でボルトを引き抜くと簡単に取れます。
f:id:ti_taka:20190804010226j:plain
f:id:ti_taka:20190804010238j:plain
f:id:ti_taka:20190804010637j:plain

こうしてシート側のボルトが無事外せたので、今度はスイングアーム側に固定されているボルトを緩めて外せばOKです。
f:id:ti_taka:20190804010821j:plain
f:id:ti_taka:20190804010832j:plain
f:id:ti_taka:20190804010844j:plain

若干ステーが邪魔ですが、シートの下からボルトを回して外せます。
f:id:ti_taka:20190804010948j:plain
f:id:ti_taka:20190804011002j:plain
f:id:ti_taka:20190804011016j:plain

次はいよいよサスペンションを取り付けていきます。
外すときはタイヤの横から手を突っ込んで外せたので意識する必要はなかったのですが、取り付けるときはシート下のステーを外す必要があります。
最初からステーを外してしまうほうが、リジットバーを外す際も楽です。僕は外すのが面倒臭かかったので横着してステーを外さないままリジットバーを外したのですが、結局交換のサスを取り付ける際に外す必要があったので、最初から外しておけばよかったと思いました。
↓これです。 f:id:ti_taka:20190804011111j:plain

これが少し面倒臭くて、シート下からは取れず、左右のサイドカバー下にあるボルトを外さないと取れません。
↓左側 f:id:ti_taka:20190804011154j:plain
f:id:ti_taka:20190804011203j:plain

↓右側。ステーの表にイグニッションコイルがついているので一旦外す必要があります。
f:id:ti_taka:20190804011230j:plain

イグニッションコイルを外した下に見えるボルトを外します。
f:id:ti_taka:20190804011302j:plain

こんな感じでシート下でサスペンションを固定しているところが見えるようになるので、あとは先程リジットバーを取り外したのと逆の手順でサスペンションを取り付ければOKです。
f:id:ti_taka:20190804011325j:plain

取り付けるときは、スイングアーム側を固定するのは簡単ですが、シート下を固定するためにはジャッキの高さを微調整してボルトの穴がちょうど良い位置にしないといけません。
これがかなり面倒臭いです。。

無事取り付けることができました。
リジットバーをつけていたときと比べるとだいぶ車高が高くなりました。
これなら乗りやすそうですね。
f:id:ti_taka:20190804011353j:plain

オイル、オイルフィルターの交換

去年せっかくキャブを洗浄したので、オイルも交換しようと思いますが、オイルフィルターも前回いつ交換したんだっけ?という感じだったので、今回の機に交換しようと思います。

オイルフィルターについてはあまり知識が無いのですが、とりあえず型をみてスティードに適合しそうなものの中から以下を選定しました。

また、オイルフィルターを外すためにはオイルフィルターレンチが必要になります。
バイク屋さん曰く、オイルフィルターレンチのサイズは、スズキ車とホンダ・カワサキヤマハで違うそうです。
ホンダ車用のこちらを購入し使用しました。

まずは、オイル交換です。こちらは簡単ですね。
エンジンの下にあるボルトを外してオイルを出します。
f:id:ti_taka:20190804011421j:plain

市販の廃油吸収剤を買ってきて下に敷いておきます。
f:id:ti_taka:20190804011452j:plain

敷いた状態で↑のボルトを外すとオイルが出てきます。
f:id:ti_taka:20190804011531j:plain

次にオイルフィルターを交換します。
ティードのオイルフィルターはここ↓にあります。

f:id:ti_taka:20190804011603j:plain

これを先程のオイルフィルターレンチで回して外します。
このオイルフィルターを外す際も、外したあとオイルが出てくるので、廃油吸収剤を下に置いておいたほうが良いです。

写真を取り忘れてしまいましたが、オイルフィルターを交換したら、オイルを適量入れて作業終了です。

終わりに

サスペンションの交換は、高校の時リジットバーに交換した際も、詳しい先輩にやっていただいたのですが、とても大変そうだった印象だったので、とりあえず自力でやりきれてよかったです。
ちゃんと安定させてジャッキアップできれば特に難しい部分はありませんでした。
オイルも交換したし改めてエンジンをかけてみようとしたらかからなくなってました。
色々調べた結果プラグが悪くなっている可能性があるので、次回時間があるときはプラグの交換をしたいと思います。

iOSのheic形式画像ファイルを一括でjpg形式に変換

単にsipsコマンドとxargsコマンドを実行すればOK。
xargsコマンドを使いこなせてなかったのでメモ。

$ ls | grep heic | xargs -t -n 1 -I{} sips --setProperty format jpeg {} --out {}.jpg

$ ls | grep heicで取得したheicファイル一覧をxargsコマンドに渡し、-Iオプションで引数の位置を明示的に指定してあげるだけ。

hogehoge.heic→hogehoge.heic.jpgとして変換される。

参考

qiita.com

orebibou.com

Vue.js + Bootstrap-vue でモーダルウィンドウとフラッシュメッセージの表示サンプル

忘れそうなのでメモしておきます。子が親のメソッドを呼び出したり親が子のメソッドを呼び出したりして分かりづらくなっているので後日もっと良い方法を検討したいと思います。

基本的にはBootstrap-vueの公式に書いてあった方法で実装していきます。
https://bootstrap-vue.js.org/docs

作成したファイルは以下の4つです。
簡単なメモアプリ上でメモを削除する動きを作ってみます。

↓メインのコンポーネント

// App.vue
<template>
  <div id="app">
    <Modal @delete="deleteMemo" ref="childModal"/>
    <Memos @confirm="deleteConfirm" ref="childMemos"/>
    <Message ref="childMessage"/>
  </div>
</template>

<script>
import Modal from './components/Modal.vue'
import Message from './components/Message.vue'
import Memos from './components/Memos.vue'

export default {
  name: 'app',
  components: {
    Modal,
    Memos,
    Message
  },
  methods: {
    deleteConfirm(i) {
      this.$refs.childModal.showModal(i)
    },
    deleteMemo(i) {
      this.$refs.childMemos.deleteMemo(i)
      this.$refs.childModal.hideModal(i)
      this.$refs.childMessage.showMessage(true)
    },
  }
}
</script>

<style>
#app {
  font-family: 'Avenir', Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
  margin-top: 60px;
}
</style>

↓メモリストのコンポーネント
これは適当に書きました。

// Memos.vue
<template>
  <div class="container">
    <b-list-group>
      <b-list-group-item v-for="memo,i in memos"><b>{{ memo.title }}</b>{{ memo.text }}<b-button variant="danger" @click="deleteComfirm(i)">削除</b-button></b-list-group-item>
    </b-list-group>
  </div>
</template>

<script>
export default {
  name: 'Memos',
  data() {
    return {
      memos: [
        {"title": "title1", "text": "memomemoemomeo"},
        {"title": "title2", "text": "memomemomemomemo"},
        {"title": "title3", "text": "memomemomemomemo"},
        {"title": "title4", "text": "memomemomemomemo"},
        {"title": "title5", "text": "memomemomemomemo"},
        {"title": "title6", "text": "memomemomemomemo"},
        {"title": "title7", "text": "memomemomemomemo"},
        {"title": "title8", "text": "memomemomemomemo"},
        {"title": "title9", "text": "memomemomemomemo"},
      ]
    }
  },
  methods: {
    deleteComfirm(i) {
      this.$emit('confirm', i);
    },
    deleteMemo(i) {
      // 実際にはここに削除処理を記述
      this.memos.splice(i, 1);
    }
  }
}
</script>

<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
</style>

↓モーダルのコンポーネント
https://bootstrap-vue.js.org/docs/components/modal/#using-show-hide-and-toggle-component-methods
こちらの
Using show(), hide(), and toggle() component methods
の方法を使って、モーダルの表示、非表示を操作しています。

// Modal.vue
<template>
  <b-modal ref="modal" size="xl" hide-footer hide-header>
    <div class="d-block text-center">
      <p>本当に削除しますか?</p>
    </div>
    <b-button class="delete-button" variant="success" block @click="hideModal()">いいえ</b-button>
    <b-button class="delete-button" variant="danger" block @click="deleteMemo()">はい</b-button>
  </b-modal>
</template>

<script>
export default {
  name: 'Modal',
  data() {
    return {
      deleteIndex: ""
    }
  },
  methods: {
    showModal(i){
      this.deleteIndex = i;
      this.$refs.modal.show()
    },
    hideModal(){
      this.deleteIndex = "";
      this.$refs.modal.hide()
    },
    deleteMemo(){
      this.$emit('delete', this.deleteIndex);
    }
  }
}
</script>

<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
</style>

↓フラッシュメッセージのコンポーネント
https://bootstrap-vue.js.org/docs/components/alert/#auto-dismissing-alerts
Auto dismissing alerts
の実装サンプルを使い表示後2秒経過すると消えるようにしています。
また、transitionでアニメーションを入れています。
https://jp.vuejs.org/v2/guide/transitions.html

// Message.vue
<template>
  <transition name="message">
    <div v-if="messageActive" id="message-parent">
      <div id="message">
        <b-alert :show="dismissCountDown" v-bind:variant="variant" @dismiss-count-down="countDownChanged">
          {{ message }}
        </b-alert>
      </div>
     </div>
  </transition>
</template>
<script>
export default {
  name: 'Message',
  data() {
    return {
      // message表示用データ
      dismissSecs: 2,
      dismissCountDown: 0,
      message: '',
      variant:'',
      messageActive:false,
    }
  },
  methods: {
    countDownChanged (dismissCountDown) {
      this.dismissCountDown = dismissCountDown
      if (dismissCountDown == 0) {
        this.messageActive = false;
      }
    },
    showMessage: function (success) {
      this.message = success ? "削除しました" : "エラーが発生しました";
      this.variant = success ? "success" : "danger";
      this.dismissCountDown = this.dismissSecs;
      this.messageActive = true;
    },
  }
}
</script>

<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
.message-enter-active, .message-leave-active {
  transition: opacity 0.5s;
}
.message-enter, .message-leave-to {
  opacity: 0;
}
#message-parent {
  width:100%;
}
#message {
  position: fixed;
  top: 5px;
  right: 0;
  left: 0;
  margin-left: auto;
  margin-right: auto;
  box-sizing:border-box;
  width:95%;
}
</style>

↓こんな表示になります。
f:id:ti_taka:20190409014407g:plain

処理の順番としては、

  1. Memos.vue中のdeleteComfirm(i)が発火
  2. App.vue中の@confirm="deleteConfirm" → deleteConfirm(i)が発火
  3. Modal.vue中のshowModal(i)が発火
  4. Modalの表示
  5. 「はい」を選択
  6. Modal.vue中のdeleteMemo()が発火
  7. App.vue中の@delete="deleteMemo" → deleteMemo(i)が発火
  8. Memos.vue中のdeleteMemo(i)が発火
  9. Modal.vue中のhideModal(i)が発火
  10. Message.vue中のshowMessage(true)が発火
  11. countDownChanged (dismissCountDown)にて1秒毎にカウントダウンし、設定秒経過したらメッセージを消す

という流れになっています。

とりあえず作りたいものは作れましたが、状態を管理するデータがいろんなコンポーネントに散在してしまっているので、今後はVuexなどを使って管理できるようにしたいと思います。

人間の時間の捉え方は結局今やりたい事、やらなくてはならない事がどれぐらいあるかに依存する

小さい頃から時間の感じ方の違いがなんで起きうるのか疑問に思っていました。
どうして楽しい時間は早く感じるのか、めんどくさい作業をしている時間は長く感じるのか。
小学校の時、国語の教科書に代謝時間という考え方が載っていたのを覚えています。
自分の体の持っている全エネルギーのうち単位時間でどの程度エネルギーを消費しているか、どの程度の心拍数があるか。
それが大きければ時間は早く感じ、小さければ時間は長く感じる
という理屈です。
(この辺りあまり覚えてませんがそんな定義だったと思います。)
なので、蟻にとっては一日もあっという間に過ぎるけれども、象にとっては長く感じると。
この考え方に、「確かにそうかもなぁ〜。」と思う反面、なんとなく納得しない部分もありました。
エネルギー消費が大きく、疲れる退屈な仕事でも時間は長く感じるし、消費が少なくても時間が短く感じることはある。
その違いはなんだろうかと思いました。

大人になってから、誰かから「1歳児にとっての一年間は一生分と同じ時間だが、50歳にとっての一年間はせいぜい人生の中の50分の1に過ぎない。」という話を聞きました。
x時間生きた人にとっての単位時間は人生の中での1/xでしか無い、それを積分していくと対数の関数になる、だから時間の感じ方は齢とともに短く感じるようになっていくんだなぁと思ってました。
と思ったら、既に「ジャネの法則」というものが、フランスの哲学者・ポール・ジャネにより発案されていたようです。

人間の五感は対数に変換されている

この記事を見て妙に納得したのを覚えています。

しかし、このジャネの法則は、あくまで過去を振り返っての感覚時間における法則であり、この先の1時間、1年間、更には自分の人生の長さをどう捉えるかの感覚とはまた違います。

仕事をし始めて、あらゆるプロジェクトに携わる中で、「これだけのプロジェクトをあと一年でやりきるのか、、あと一年しかないのか、、」などと思う事が増えたり、自身のキャリアを考えた時にも、「**年後には**が出来るようになっていたい!!その為には毎日勉強時間を確保したとしても***時間しかない。。全然時間ないなぁ〜。」と思う事が多くなりました。
また、子供ができた事で、「子供が**歳になるまでには家が欲しいなあ〜、その為には毎年**円貯金しなきゃー」と考えるようになりましたが、この先の数年間はとても短い時間のように感じられます。

こうしたことから、新たに以下の仮説を提唱したいと思います。
「人間の未来に向けた感覚時間は、この先の時間の中でやるべき、やりたいと考えている事柄の総量に反比例し、その内ある時間の中で実施し切れる(と想定される)量に比例する」

つまり、やりたいこと、やるべきことが多い人は、その中である時間の中で実施しきれる割合は相対的に少なくなります。
逆に、あまりタスクを抱えていない状態だと、時間は相対的に長く感じるようになります。
当たり前の事を言っているようですが、退屈な作業をしているときに時間が長く感じられるのは、作業のゴールイメージが無いか、やりたくもなく、やるべきことだとも認識出来ていないため、分母が極限まで0に近くなってしまうためかとも考えられ、なんとなく説明が付く気がします。

  1. 過去の時間を振り返る際の感覚時間は、現在自分が持っている記憶の量に反比例し、ある時間の中で増加した記憶の量に比例する。(ジャネの法則
  2. 未来時間を意識した際の感覚時間は、この先の時間の中でやるべき、やりたいと考えている事柄の総量に反比例し、その内ある時間の中で実施し切れる(と想定される)量に比例する

時間の捉え方が短くなったとか、長く感じる場合は、1.だけでなく、2.の要素も複合的絡み合っての結果なのかなと思います。
年を重ねて時間が無いと思うようになるのは、ある時間の中で得られる経験値が、自分の人生の中での占める割合が少しづつ減っていく為、だけでなく、いろんな考え方ができるようになったぶん、やりたいことが増えたため、または立場も変わっていく中でやるべきことが増えていったためという側面もあります。

そう考えると、時間が短く感じるのは「歳とったな〜。。」と嘆くためのネタになるだけでなく、この先の人生の中で自分はどういった事をやりたいのか、それに向けてどの程度のことが成し遂げられそうか、を意識するための丁度よい指標のような物になるのかもしれません。
長期的にやりたいこと、近い将来やるべきことなど、時にはそういったことから一旦頭を切り離し、ゆったりとした時間を過ごしたり、目の前の物事に取り組んで充実感を感じるというのが一番精神衛生上健康的な時間の過ごし方なのかなーと思いました。
常に、「時間が短い」と感じる場合は意識的にそういう時間を設けるのも必要かもしれません。

時間を意識しながらの方が効率的に動けることもありますが、いつも時間を意識するのも辛いですからね。

今日はそんな事を考えました。
明日からまた頑張っていこうと思います。

pythonでping監視 〜〜その2〜〜

前回、あるNWセグメント内のクライアントにpingを送って常時監視することを試みました。

kittagon.hateblo.jp

その別バージョンです。xargsで並列処理をしています。

# ping_xargs.py

import subprocess, requests, sys

def is_connectable(host):
    # ping = subprocess.Popen(["ping", "3", "-c", "1", host], stdin=subprocess.PIPE, stdout=subprocess.PIPE)
    ping = subprocess.Popen(["ping", "-W", "1", "-c", "1", host], stdin=subprocess.PIPE, stdout=subprocess.PIPE)
    ping.communicate()
    return ping.returncode == 0

ipaddress = sys.argv[2]
num = sys.argv[1]
result = is_connectable(ipaddress)
print(ipaddress)
if result is not True:
    url = "https://my-line-bot-url.com/alert" + num
    requests.get(url)
$ while true; do sed -e 's/,/ /g' IPAdressTable.csv | xargs -L 1 -P20 python ping_xargs.py; done;

特に効率的になったわけではないですが。

しかも今回監視をしたいと思っていたNW設計を聞いたら一番上のL3SWではポート間のICMPを禁止しているらしい。。。
L3SWにぶら下がってるHUB間で全然疎通しないからなんでかなと思っていたら、そもそもpingで監視が出来るNW環境じゃなかった。。

pythonでping監視

はじめに

  • あるプライベートネットワークの中に存在するクライアントにpingを打ち、常時監視するツールが欲しかった。
  • メールでの通知とかも分かりづらいのでいろんな通知の方法が使いたかった(今回はLine Botで通知)。
  • 良さげなツールが無かったので自分でプログラムを書いて実現することにした

PythonPingを打つ

pythonではsubprocessを使えばpingを打てるらしいです。

qiita.com

この関数をお借りしてpingを打ちます。

import subprocess
def is_connectable(host):
    ping = subprocess.Popen(["ping", "-w", "3", "-c", "1", host], stdin=subprocess.PIPE, stdout=subprocess.PIPE)
    ping.communicate()
    return ping.returncode == 0

今回監視したい対象は、あるセグメントの中で固定IPを振られて存在しているクライアントが対象なので、クライアントのIPアドレスcsvファイルから読み込みます。

取得したIPアドレスのリストをforループで回して全てにpingを打ち、NGだった場合は通知を送ります。
(今回はLine Botに通知させたいのでBotサーバのURLにリクエストしています。)

1,192.168.101.1
2,192.168.101.2
3,192.168.101.3
4,192.168.101.4
:
:
import csv, subprocess, requests

def is_connectable(host):
    ping = subprocess.Popen(["ping", "-W", "3", "-c", "1", host], stdin=subprocess.PIPE, stdout=subprocess.PIPE)
    ping.communicate()
    return ping.returncode == 0

while True:
    f = open('IPAdressTable.csv', 'r')
    reader = csv.reader(f)
    for index, row in enumerate(reader):
        print(row)
        result = is_connectable(row[1])
        if result is not True:
            url = "https://my-line-bot-url.com/alert" + row[0]
            requests.get(url)
    f.close()

これで、まずは機能としては完成ですが、同セグメント内に存在する100個近くのクライアントにpingを打っていると、一周するのも時間がかかってしまい、あまり常時監視している感じがしなくなってしまいます。

そこで並列処理を導入して、複数スレッドで処理させようと思います。

joblibで並列処理

qiita.com

pythonの並列処理にはjoblibが便利みたいです。

ここを参考にループ処理の部分を書き換えます。

import csv, subprocess, requests
from joblib import Parallel, delayed

def is_connectable(host):
    ping = subprocess.Popen(["ping", "-W", "3", "-c", "1", host], stdin=subprocess.PIPE, stdout=subprocess.PIPE)
    ping.communicate()
    return ping.returncode == 0

def send_connection_status(host, number):
    print(host)
    result = is_connectable(host)
    if result is not True:
        url = "https://my-line-bot-url.com/alert" + row[0]
        requests.get(url)

while True:
    f = open('IPAdressTable.csv', 'r')
    reader = csv.reader(f)
    header = next(reader)
    Parallel(n_jobs=-1)( [delayed(send_connection_status)(row[1], row[0]) for index, row in enumerate(reader)] )
    f.close()

これで複数スレッド立てて処理が出来るようです。
とりあえず実装しただけで細かいことはまだよくわかりませんが。

時間があるときにもう少し勉強したいと思います。

Flask-AskでAlexaのカスタムスキルを作成

はじめに

昨年末にAmazon Echo Dotを購入したので、カスタムスキルを作ろうと思い、その方法を備忘としてまとめます。

Alexaのカスタムスキルは以前作ったことがあり、そのときに使用したFlask-Askを使用して実装したいと思います。

Flask-Askをインストールする際の注意点はこちらの記事にまとめています。

kittagon.hateblo.jp

環境構築

DockerでPython環境の構築

今回は作ったプログラムと環境をHerokuにデプロイするので、コンテナをそのままデプロイ出来るようDockerで環境を作ります。

まずは簡単に以下のようなDockerfileを作ります。

FROM python:3.7.1-alpine3.8
ADD . /app
WORKDIR /app
RUN apk update && apk add git vim

以下コマンドでDockerコンテナを起動します。

$ docker build . -t alexa_env
$ docker run -it -v $PWD:/app -p 8000:8000 alexa_env /bin/ash

今回はalpineのDocker imageを使用しているので、/bin/bashではなくbin/ashであることに注意。

qiita.com

docs.docker.jp

Dockerコンテナに入ったら入ったら、flaskgunicornインストールします。

/app $ pip install flask gunicorn

動作確認のためのapp.pyを作成します。

from flask import Flask
app = Flask(__name__)

@app.route("/")
def hello():
    return "Hello World!"

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

gunicornを起動します。

/app $ gunicorn app:app -b 0.0.0.0:8000

ブラウザからhttp://localhost:8000を開いて"Hello World!"が表示されればOKです。

Herokuにデプロイ

続いてDockerfileを編集してHerokuにデプロイ出来る環境を作っていきます。
事前にコンテナ内でpip freeze > requirements.txtを実行しrequirement.txtを作成しておきます。
また前回の記事にて実施したFlask-Askのパッケージのローカルへのダウンロードと編集したパッケージを/src配下に準備するとともに、build-base,libffi-dev,openssl-devのインストールを追記しておきます。

最終的なDockerfileは以下の通り。

FROM python:3.7.2-alpine3.8
ADD . /app
WORKDIR /app
RUN apk update && \
    apk add git vim build-base libffi-dev openssl-dev && \
    pip install --upgrade pip && \
    pip install -r requirements.txt && \
    pip install src/Flask-Ask-0.9.8.tar.gz
EXPOSE 8000
ENV PORT 8000
CMD gunicorn app:app -b 0.0.0.0:$PORT

以前の記事に書いたように、以下コマンドでherokuにデプロイします。

$ heroku login # herokuにログイン
$ heroku container:login # Heroku 上の Container Registry へログイン
$ heroku create alexacustomskil # Heorkuアプリの作成
$ heroku container:push web --app yourappname
$ heroku container:release web --app yourappname

これでHerokuにデプロイされ、デプロイされたURLにアクセスし先ほどと同様"hello World!"が表示されればOKです。

スキル作成

いよいよここからはカスタムスキルを作成していきます。
基本的にはこちらの開発ブログに掲載されているチュートリアルに従って実装していきます。

Flask-Ask: A New Python Framework for Rapid Alexa Skills Kit Development : Alexa Blogs

こちらのチュートリアルでは、数字が3つ読み上げられるのでその数字を覚えて逆から読むというゲームを作っています。

Alexa Developer Console上での設定と、Flask-Askでのサーバ側の処理実装が必要になります。

Amazon Developper Consoleで設定

チュートリアルとは少し画面構成が違いますが、Console上で設定するものは以下のものです。

  • スキルの作成
    • スキルの名前を設定 「アレクサ、〇〇を開いて」のときの名前
    • スキルのインテントを設定 インテントとはユーザーがアレクサに対して応答する言葉のカテゴリのようなもの そのユーザー応答が何を意味するのかを定義します。
      今回登録するのは以下の2つ
      • YesIntent ゲーム開始時に「始めてもいいですか?」の問に対して、ユーザーがOKを場合の発話を定義
      • AnswerIntent 出された問題に対してユーザーが解答する場合の発話を定義
    • それぞれのインテントのスロットを設定 インテントにて定義する発話の中で使用する、変数の器のようなもの 今回のAnswerIntentでは発話の中で数字を読み上げるので、固定値で発話パターンを定義出来ないので、変数としてスロットを定義したうえで発話を定義します。

まずはAlexa Developer ConsoleにAmazonアカウントでログインします。
開発者アカウントにするために追加で情報入力が必要な際は入力します。

https://developer.amazon.com/alexa/console/ask

開発者コンソールが開いたらスキルの作成をクリックします。

f:id:ti_taka:20190203184434p:plain

新しいスキルの登録画面で、カスタムスキルを作成します。

f:id:ti_taka:20190203184439p:plain f:id:ti_taka:20190203213541p:plain

必要情報を入力すると、スキル編集画面にきます。

f:id:ti_taka:20190203184452p:plain

まずはインテントを登録します。

f:id:ti_taka:20190203184452p:plain

今回は上で説明したとおり、以下の2つを登録します。

  • YesIntent ゲーム開始時に「始めてもいいですか?」の問に対して、ユーザーがOKを場合の発話を定義
  • AnswerIntent 出された問題に対してユーザーが解答する場合の発話を定義

まずはYesIntentを登録します。

f:id:ti_taka:20190203184457p:plain

また、YesIntentのサンプルを登録し、このIntentがどういった発話パターンを取りうるのかAlexaに知ってもらいます。

f:id:ti_taka:20190203184502p:plain

次にAnswerIntentを登録します。

f:id:ti_taka:20190203184509p:plain

AnserIntentについては、発話パターンのサンプルを登録する前に、発話パターンの中で使用されるスロット(変数、器のようなもの)を登録します。

f:id:ti_taka:20190203184515p:plain

今回は3つの数字のスロットを使用しますので、それぞれfirst、second、thirdの3個を登録します。 スロットのタイプはAMAZON.numberとします。

f:id:ti_taka:20190203184522p:plain

次にこのスロットを使用してAnswerIntentの発話パターンを登録します。 今回は以下の2パターンを登録しました。

f:id:ti_taka:20190203184526p:plain

次に、このスキルを呼び出した際にそれを処理するサーバーのURLを設定します。 先程デプロイしたherokuのURLを設定します。

f:id:ti_taka:20190203184530p:plain

最後に今回のスキルを呼び出すためのスキル名を設定します。 設定を保存し、ビルドします。

f:id:ti_taka:20190203184536p:plain

これで開発コンソールでの設定は完了です。

左メニューのJSONエディター上は以下のような表示になります。
このJSONをそのまま編集してもOKです。

{
    "interactionModel": {
        "languageModel": {
            "invocationName": "メモリーゲーム",
            "intents": [
                {
                    "name": "AMAZON.CancelIntent",
                    "samples": []
                },
                {
                    "name": "AMAZON.HelpIntent",
                    "samples": []
                },
                {
                    "name": "AMAZON.StopIntent",
                    "samples": []
                },
                {
                    "name": "AMAZON.NavigateHomeIntent",
                    "samples": []
                },
                {
                    "name": "YesIntent",
                    "slots": [],
                    "samples": [
                        "うん",
                        "はい",
                        "もちろん",
                        "OKです",
                        "OK",
                        "了解"
                    ]
                },
                {
                    "name": "AnswerIntent",
                    "slots": [
                        {
                            "name": "first",
                            "type": "AMAZON.NUMBER"
                        },
                        {
                            "name": "second",
                            "type": "AMAZON.NUMBER"
                        },
                        {
                            "name": "third",
                            "type": "AMAZON.NUMBER"
                        }
                    ],
                    "samples": [
                        "{first} と {second} と {third}",
                        "{first} {second} {third}"
                    ]
                }
            ],
            "types": []
        }
    }
}

Flask-ASKで実装

サーバ側の処理はFlask-Askで実装していきます。

Welcome to Flask-Ask — Flask-Ask documentation

チュートリアルの通り、app.pyを以下のように書き換えます。

import logging
from random import randint
from flask import Flask, render_template
from flask_ask import Ask, statement, question, session

app = Flask(__name__)
ask = Ask(app, "/")
logging.getLogger("flask_ask").setLevel(logging.DEBUG)

@ask.launch
``def new_game():
    welcome_msg = render_template('welcome')
    return question(welcome_msg)

@ask.intent("YesIntent")
def next_round():
    numbers = [randint(0, 9) for _ in range(3)]
    round_msg = render_template('round', numbers=numbers)
    session.attributes['numbers'] = numbers[::-1]  # reverse
    return question(round_msg)

@ask.intent("AnswerIntent", convert={'first': int, 'second': int, 'third': int})
def answer(first, second, third):
    winning_numbers = session.attributes['numbers']
    if [first, second, third] == winning_numbers:
        msg = render_template('win')
    else:
        msg = render_template('lose')
    return statement(msg)

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

templateファイルの中にはAlexaが話すセリフを記載します。

welcome: メモリーゲームへようこそ! これから5つの数字を読み上げるので、アタナはその数字を逆の順番で答えてください。準備はいいですか?

round: 今回の数字は {{ numbers|join(", ") }} です。反対の順番で読み上げてください。

win: 正解です!

lose: 不正解です。

サーバー側の処理はこれでOKなので、再度Herokuにデプロイします。

そして開発用アカウントに紐づけたEcho端末や、アレクサシミュレーターで「アレクサ、メモリーゲームを開いて」と話しかけてゲームがスタートすれば成功です。

が、返事が返ってきません。

Herokuのログを確認すると以下のようなエラーが出ていました。

2019-01-24T15:02:16.836079+00:00 app[web.1]: File "/usr/local/lib/python3.7/site-packages/OpenSSL/crypto.py", line 740, in _subjectAltNameString
2019-01-24T15:02:16.836081+00:00 app[web.1]: method = _lib.X509V3_EXT_get(self._extension)
2019-01-24T15:02:16.836082+00:00 app[web.1]: AttributeError: module 'lib' has no attribute 'X509V3_EXT_get'

調べてみるとpyOpenSSLを再インストールする必要があるようです。

electricsheep.hatenadiary.jp

記事を参考にDockerfileを以下のように修正します。

FROM python:3.7.2-alpine3.8
ADD . /app
WORKDIR /app
RUN apk update && \
    apk add git vim build-base libffi-dev openssl-dev && \
    pip install --upgrade pip && \
    pip install -r requirements.txt && \
    pip install src/Flask-Ask-0.9.8.tar.gz && \
    pip uninstall --yes pyOpenSSL && \
    yes | pip install pyOpenSSL
EXPOSE 8000
ENV PORT 8000
CMD gunicorn app:app -b 0.0.0.0:$PORT

再度Herokuにデプロイすると無事動作するようになりました。

まとめ

前回の続きで、Flask-Askを使用してAlexaのカスタムスキルを作成してみました。
Flaskを使って簡単に実装できるので、アイデア次第でいろんなスキルが作れそうです。