テクノロジの無駄づかい

日々の「ステュディオス」を求めて遠回りしがちなエンジニアの記録

Gitbook の環境を Dockerで作った

しばらくきちんとしたドキュメンテーションから遠ざかっていたのだけど、仕様書なるものを書く事になった。

基本的に文章を書くのは Markdown形式に寄せていくようにしているので Gitbook を使うことにした。最終的にPDFにすることも簡単であろうと。

しかし、PDF出力でハマった。その記録。

要件

目的はシステムの仕様書を書くということを踏まえ次の要件で環境を作ることにした。

  1. Markdownで記述できること
  2. UMLも記載したい
    • 最低でも、クラス図、シーケンス図は書きたい
  3. PDFで出力できること

実現方法の検討

1. Markdownで記述できること

「1. Markdownで記述できること」はGitbookを採用した時点で満たせる。
最初からGitbookを使うつもりだったので、検討するまでもない。 ただ、ドキュメントの量が大きくなってくるとビルドに時間がかかりそうなのが気がかりだったので代替プロダクトを一応探してみた。
mdBookというRust製の Gitbookクローンを見つけたのだけど、mdBook自体も言語から発展途上感が強かったので、今回は見送った。

で、当初の方針通りGitbookを採用して進めることとした。

2. UMLも記載したい

これについては Gitbookのプラグインで解決できそうなので、どれを使うかということで探した。

mermaidを利用したものがお手軽で良さそうだったのだけど、クラス図をサポートできるものが見つからなかったので PlantUMLを利用したもので検討した。

PlantUMLを利用するプラグインはいくつもある。ググってすぐ出てきたのは、plantumlなのだけど、それを改良したっぽい umlを使って見ることにした。

動作はするのだけど、gitbook serveで使っていると、UMLの出力が遅くて快適に使えない。PlantUMLでUMLレンダリングする際にJavaでできたPlantUMLを動かすためにJavaVMの起動から始まるのが遅い原因だと思われる。

そこで、PlantUMLサーバをつかってUMLレンダリングするタイプのplantuml-cloudを使ってみることにした。狙い通りUMLレンダリングは速くなった。 速度改善と同時にPlantUMLに必要な graphvizのインストールも不要になった。 オフライン時も使えるようにしたいので ローカルでPlantUMLのサーバである plantuml-serviceを動かすことにした。Dockerイメージも提供されているので、次のDockerコンテナを動かすDocker Composeファイルを作った。

  • plantuml-service
  • Nginx
  • Giitbook

Nginxはplantuml-serviceをリバースプロキシするために入れている。というのもplantuml-serviceの待受ポート番号(1608)とplantuml-cloudの接続ポート番号(80) が一致しておらず、変更することもできなかったのだ。それで仕方なくリバプロしてGitbookからのリクエストを受けられるようにした。

docker-compose.ymlは次の通り。

version: "2.2"
services:
  plantuml:
    image: 'bitjourney/plantuml-service:1.3.3'

  nginx:
    image: nginx:alpine
    volumes:
      - ./nginx/default.conf:/etc/nginx/conf.d/default.conf
    depends_on:
      - plantuml

  gitbook:
    image: hero/docker-gitbook:latest
    volumes:
      - ./gitbook:/gitbook
      - ./dest:/gitbook_dest
    ports:
      - 4000:4000
    depends_on:
      - nginx
    command: 'serve'

2018/11/08追記 plantuml-cloudに送っていたプルリクがマージされ、ポート番号が指定できるようになった。これに伴いnginxによるリバプロは不要になり、次の docker-compose.yml のようにplantumlとgitbookだけでよくなった。

version: "2.2"
services:
  plantuml:
    image: 'bitjourney/plantuml-service:1.3.3'

  gitbook:
    image: hero/docker-gitbook:latest
    volumes:
      - ./gitbook:/gitbook
      - ./dest:/gitbook_dest
    ports:
      - 4000:4000
    depends_on:
      - plantuml
    command: 'serve'

この環境でplantuml-cloudを使うには次のようにbook.jsonでplantuml-cloudを設定する

{
    "plugins": ["plantuml-cloud"],
    "pluginsConfig": {
      "plantuml-cloud": {
        "protocol": "http",
        "host": "plantuml",
        "port": 1608
      }
    }
}

3. PDFで出力できること

GitbookのPDF出力は calibreに依存している。 calibre をインストールしている他のDockerイメージを参考にしながら次のようなDcokerfileを作った。

FROM node:10.12.0-alpine
LABEL maintainer="HeRoMo"

ENV GLIBC_VERSION 2.28-r0
RUN apk add --update curl && \
    curl -Lo /etc/apk/keys/sgerrand.rsa.pub https://alpine-pkgs.sgerrand.com/sgerrand.rsa.pub && \
    curl -Lo glibc.apk "https://github.com/sgerrand/alpine-pkg-glibc/releases/download/${GLIBC_VERSION}/glibc-${GLIBC_VERSION}.apk" && \
    curl -Lo glibc-bin.apk "https://github.com/sgerrand/alpine-pkg-glibc/releases/download/${GLIBC_VERSION}/glibc-bin-${GLIBC_VERSION}.apk" && \
    apk add glibc-bin.apk glibc.apk && \
    /usr/glibc-compat/sbin/ldconfig /lib /usr/glibc-compat/lib && \
    echo 'hosts: files mdns4_minimal [NOTFOUND=return] dns mdns4' >> /etc/nsswitch.conf && \
    apk del curl && \
    rm -rf glibc.apk glibc-bin.apk /var/cache/apk/*

ENV LD_LIBRARY_PATH $LD_LIBRARY_PATH:/opt/calibre/lib
ENV PATH $PATH:/opt/calibre/bin
ENV CALIBRE_INSTALLER_SOURCE_CODE_URL https://raw.githubusercontent.com/kovidgoyal/calibre/master/setup/linux-installer.py
RUN apk update && \
    apk add --no-cache --upgrade \
    bash \
    ca-certificates \
    gcc \
    mesa-gl \
    python \
    qt5-qtbase-x11 \
    wget \
    xdg-utils \
    libxcomposite \
    xz && \
    wget -O- ${CALIBRE_INSTALLER_SOURCE_CODE_URL} | python -c "import sys; main=lambda:sys.stderr.write('Download failed\n'); exec(sys.stdin.read()); main(install_dir='/opt', isolated=True)" && \
    rm -rf /tmp/calibre-installer-cache

RUN yarn global add gitbook-cli svgexport

COPY ./start.sh /usr/bin/
RUN chmod 755 /usr/bin/start.sh

ENTRYPOINT ["start.sh"]

さて、このイメージを使ってPDFを作って見ると、PDFはできるにはできるが、UMLの図が表示できない。

plantuml-serviceではSVGUMLレンダリングしているので、PNGならどうだろうかと試そうにもplantuml-serviceはSVGのみサポートなので使えない。PDF作成時のみ ローカルで PlantUMLを動作するようにして試してみたがダメだった。

イメージを小さくしようとalpineベースのイメージをベースにしていたが、そのかわり calibreやそれが依存しているglibcを自前でビルドするようにしていたがそこに何らかの不足があるようだ。

試しに Debianベースのイメージをベースに作り直した。それでは calibreもパッケージマネージャでインストールできる。UMLを含むPDFの出力も問題なく出力できる。

最終的にgitbookのDockerfileは次のようになった。

FROM node:10.13.0-slim
LABEL maintainer="HeRoMo"

# install apt packages
RUN apt-get update -y && \
    apt-get install -y \
      bzip2 \
      calibre && \
    apt-get autoremove -y && \
    apt-get clean && \
    rm -rf /var/cache/apt/archives/* /var/lib/apt/lists/*

RUN yarn global add gitbook-cli svgexport

COPY ./start.sh /usr/bin/
RUN chmod 755 /usr/bin/start.sh

ENTRYPOINT ["start.sh"]

シンプルになった。イメージのサイズは 約900MBと少々大きくなったけど。

リポジトリは次の通り。

github.com

Dockerhubはこちら

https://hub.docker.com/r/hero/gitbook/

GAS を Typescript でリファクタした

以前、GASをWebpackとBabelでモダンに実装した話を書いた。

hero.hatenablog.jp

このエントリを要約すると次の通り。

  • Togglで記録した作業時間をRedmineの作業時間に登録するGoogle スプレドッドシートアドオンを作った。
  • google\/clasp を利用して、ローカルでコーディング、バージョン管理した。
  • ES6 でJSを書いて webpack + babel でGAS用にビルドするようにした。

Claspが Typescriptをサポートした

まあまあモダンなJSでGASを実装できる構成を確立したことで、今後はやりやすくなるぞと思ってから、半年ほど。

Claspで驚くアップデートがあった。v1.5.0でTypescriptをサポートしたのだ。

qiita.com

clasp pushコマンドを実行すると、Javascriptのコードを対応するGASプロジェクトファイルにアップロードしてくれるのだが、Typescriptのコードも自動的にコンパイルしてアップロードしてくれるようになったのだ。

もちろん、HTTP通信にはUrlFetchApp.fetchを使わないといけないとかGASプラットフォームの制約はあるものの、コードのシンタックスはTypescriptのものが使える。

つまり、次が使える。

型以外はwebpack+babelの構成でも実現していたので、それほど目新しさはないが、webpack も babel も必要なく、これまで通りのコマンドを使うだけでトランスパイルしてアップロードしてくれる。

Toggl2rm を移行してみた

これは便利そうだということで、以前作ったToggl2rmを移行してみた。

移行前後のコードは次の通り。

依存モジュールが激減

移行前はこんな数のモジュールに依存していた。

  • @google/clasp
  • babel-core
  • babel-loader
  • babel-preset-env
  • babel-preset-gas
  • eslint
  • eslint-config-airbnb-base
  • eslint-plugin-googleappsscript
  • eslint-plugin-import
  • file-loader
  • gas-webpack-plugin
  • html-webpack-plugin
  • webpack

それが、移行後は たったの3つ。

トランスパイルに必要な一切を @google/claspが担保してくれるので、 どんどんアップデートされるwebpackやbabelを追いかける必要もない。 その他必要なモジュールの組み合わせにも悩まされることもない。

@types/google-apps-scriptによってGAS固有のクラス群にも型チェックできるようになるし、VSCodeならばコード補完が使えるのでドキュメントを確認する回数も減るだろう。

いいことしかない。

コードの書き換え

もともとのコードがES6だったこともありコードの書き換えも少なかった。

  • 拡張子を*.tsに変更する
  • 各関数の引数と戻り値に型アノテーションを追加
  • tslintのエラー箇所を修正
  • 変数globalを削除

ほぼこれだけで移行完了だった。

結構、関数コメントに引数や戻り値について書き込んでいたけれど、やはりコードで定義できるほうが確実だと思った。 そして、コード補完が感動的に便利。

いいことしかない。

まとめ

claspでTypescriptを使うのは簡単。

趣味レベルで使っているGASのコードは作った後はそれほど頻繁にはいじらないと思う。そういうコードほど型アノテーションは恩恵があると思う。 また、ビルドの依存モジュールも少なくてコード以外のもののメンテナンスが最小化される。

GASの実装でTypescriptを使わない理由が見当たらない。

Alexaスキルをリリースしてみた

ここしばらく、仕事でAlexaスキルを作っていたのだけど、 スキルアワードもあるし、自分でもなんか作って公開してみようと思っていた。

ただ、ネタがない。

アワードに応募するなら、なにか思いついてももう作っている時間がないなぁと思っていたところ 思いついて、構想30分、実装数時間で作って公開したのがこれ。

発想の扉

発想の扉

「アレクサ、発想の扉を開いて」でEchoに話しかけると オズボーンのチェックリストをベースにした、視点を変え発想を促す(と思う)問いかけしてくるだけのスキル。

「アレクサ、発想の扉を開いて」で始めると、連続問いかけモードで「ほかにもいりますか?」などと次を促してくるので、「はい」とか「次の頂戴」とか言うと連続して問いかけさせることができる。

「アレクサ、発想の扉で問いかけて」というと、単発問いかけモードとなり、一つだけ問いかけてすぐに終了する。

一応、連続問いかけモードモードの場合には、同じことを何度も言わないようにしている。 工夫といえばそれくらいで、対話しているようでしてなくて一方的にしゃべるだけのスキルなので作りもシンプル、簡単。

でも、アイコンが...

ただ、困ったのは、スキルのアイコン。
スキルの公開には必須だのだけど、絵心のない私にはコードを書くよりハードル高かった1

Pixabayで見つけた CC0の画像をちょっといじって使わせてもらった。

リジェクト喰らった

サクッと作って、サクッと申請して、サクッと公開!と思いきや、一度リジェクトされてしまった。

Amazonによるスキル審査でリジェクトされた場合、理由と再現手順が記載された丁寧なメールが届く。 どこをどう直せばよいのかがわかりやすかった。

Alexaの審査チームからの指摘は3点、うち2点は指摘の箇所が違うだけで同じ理由。

サンプル発話が少なすぎる

カスタムインテントの中で、サンプル発話を2つだけしか設定していないものがあった。 想定はしていたが、直接定義していなかった発話も拡張して拾ってくれていたので 実機テストをして問題なしと思っていたが、だめらしい。

ユーザーによる発話 | Amazon Alexa Voice Design Guideには「1インテント当たりのサンプル発話数は30個以上を目安としてください。」と書かれているが、18個に増やしたら審査をパスできた2

セッションオープンなら必ず次の発話を促すこと

これは2箇所で指摘された。一つはヘルプインテントを呼ばれた場合にスキルを終了せずに続けるようにしていたのだが、 次を促す台詞で終わっていなかった。これは単純にリファクタした際に落としてしまっており私のミス。

もう一箇所が、連続問いかけモードで問いかけの後で、次を促す台詞がないという指摘。
連続問いかけモードでは問いかけの後に「ほかにもいりますか?」と次を促す。 ただ、疑問文2連続となり不自然でうざいと思ったので 最初の3回は使い方を知ってもらうために「ほかにもいりますか?」と話すが、4回目以降は言わないようにしていた。

申請時に記載するスキルのテスト方法にもその旨記載していたのだけど、セッションをクローズしないなら、次の発話を促す台詞を入れることというのは絶対らしい。

審査は速かった

修正自体はちょっとした変更で済んだので、修正して再申請。今度はサクッと通過して公開となった。 スキルアワードの締切間近だったので、混んでいるかと思ったが、最初の申請から2営業日目でリジェクト、 その日のうちに修正して再申請して翌日の公開となった。

Amazonの審査チームの皆さんの迅速で丁寧な審査に感謝。

最後に

スキルの呼び出しフレーズが「〜を開いて」であることから、思いつきで作ったたわいのないスキルなのだが、 ブレストなどで煮詰まったときの気分転換程度に使ってみてほしい。

その結果、このスキルの問いかけが役立っても立たなくても、「発想の扉」が開いてアイデアが出れば幸いである。


  1. Amazon側もそこがつまずきポイントになっていると思っているのか最近になって謹製のアイコン作成ツールAlexa Icon Builderを出してきた。

  2. Alexaハンズオンセミナーでは「1インテントに対して最低6つ、理想は30」と教わった。2個ではやっぱ少なすぎた。

GAS を Webpack + Babel でやってみた

久々に、Google Sheets の Add-on で欲しいものができたので作った。 良い機会なので、Webpack + Babel でビルドする構成を試してみた。 また、Google Apps Script (GAS)周りでもいくつか変化があったので取り入れてみた。

その記録。

作ったもの

会社で開発に使った時間を資産管理の関係で申請する必要があるのだが、Redmineのチケットに時間を記録すればOKとなっている。 一方、私は Togglで日々の時間管理をしている。

チケット終了ごとにRedmineに登録したり、締め日にTogglを見ながら手作業でRedmineに登録したりしてみたのだけど、どちらも面倒でやってられなかった。 やはり、普段の時間管理はTogglだけにしたい。 ということで、TogglからRedmineにデータ移行できるツールを作ることにした1

コマンドラインツールでも良かったのだけど、Redmineに登録する前にちょっと確認したり、調整したりできたほうが便利かと考えた。 となると Google Sheetsに一旦書き出して、それを登録できるものがよかろう、それならばAdd-onとして実装するのが最も便利に使えそうだと考えた。

それで作ったのは toggl2rm 。 次の機能を備えている。

以下に、toggl2rmを作る過程でキーとなった部分や新たに調べたことを記述する。

Webpack + Babelの設定

ざっと、TogglとRedmineAPIを叩く部分を GASのWebエディタで検証がてら実装した後、gappsコマンド(node-google-apps-script)を使って、ローカルにソースを持ってきて、Webkit + Babelでビルドする設定を行った。

webpack.config.babel.jsは次の通り設定した。

import GasPlugin from 'gas-webpack-plugin';
import HtmlWebpackPlugin from 'html-webpack-plugin';
import path from 'path';

export default {
  context: path.resolve(__dirname, 'src'),
  entry: {
    code: './code',
    props: './props',
    utils: './utils',
  },
  output: {
    path: path.resolve(__dirname, 'dest'),
    filename: '[name].js',
  },
  module: {
    rules: [
      {
        test: /\.js$/,
        exclude: /node_modules/,
        use: 'babel-loader',
      },
      {
        test: /\.json$/,
        exclude: /node_modules/,
        use: {
          loader: 'file-loader',
          options: {
            name: './[name].[ext]',
          },
        },
      },
    ],
  },
  plugins: [
    new GasPlugin(),
    new HtmlWebpackPlugin({
      filename: 'sidebar.html',
      template: 'sidebar.html',
      inject: false,
    }),
    new HtmlWebpackPlugin({
      filename: 'setting_dialog.html',
      template: 'setting_dialog.html',
      inject: false,
    }),
  ],
};

.babelrcは次の通り。

{
  "presets": ["env", "gas"]
}

この設定では次のプラグインを利用している。

gas-webpack-pluginbabel-preset-gasでES6なJSをいい感じのGAS向けコードにトランスパイルしてくれる。

html-webpack-pluginは結局 srcディレクトリからdestディレクトリにHTMLファイルをコピーするのにしか使っていない。 本当はHTMLに含まれるCSSとJSを別ファイルで実装し、ビルドによって一つのHTMLにまとめたかった。 html-webpack-pluginプラグインを更にインストールしていろいろ試したが、どうもうまくいかなかった。 最初からdestディレクトリにHTMLだけ置いたり、Webpack使わずコピーしても良かったのだけど、ソースとビルド成果物をはっきり分けたい+設定はwebpack.config.babel.jsに集約したいのでこうした。

また、JSを一つのファイルにビルドしてまとめても良かったのだけど、なんとなく大きな1ファイルを作りたくないなぁと思って、3つに分けた。 しかし、一つにしたほうが、グローバルに晒したくない関数を隠せるので今後見直すかもしれない。

ESLint

ESLintも導入してみた。 ベースの設定は eslint-config-airbnb-baseを利用した。 これだけだとGAS特有のオブジェクトの参照でエラーが出るので、それはeslint-plugin-googleappsscriptで解決した。

自分で実装したグローバルオブジェクトとconsole.error()等のログ出力系(後述のStackdriver Loggingのため)を個別に許可設定した。

私はAtomエディタを使っているので、エディタのLinterが実装時にリアルタイムでチェックしてくれ、便利だった。

上記を設定したのものはこんな感じ。

extends: airbnb-base

plugins:
  - googleappsscript

env:
  googleappsscript/googleappsscript: true

globals:
  Props: false
  Utils: false

rules:
  no-console: ["error", { allow: ["info", "warn", "error"] }]

clasp

最初は gappsコマンド(node-google-apps-script)を使っていた。 しかし、複数のGASのプロジェクトがある場合、個別にGoogle Drive APIの認証が必要なのにその認証情報の管理は~/.gapps 1ファイルで行われ、切り替えるのが不便だった。

汎用的な不満だと思うので認証情報ファイルをコマンド実行時に指定する方法があるのではと、リポジトリを久々に見るとディスコンしていた。 代わりにGoogle謹製の google/claspを使えと書かれていた。

claspを調べてみてgappsと比べ以下が優れていると思ったので乗り換えた。

  • GASプロジェクト毎の認証設定が不要(1回のログインですべてのGASプロジェクトの管理が可能)
  • GASスクリプトの作成も可能(gappsは先にGASスクリプトを作っておく必要がある)
  • ドキュメントに紐づくスクリプトの管理も可能(gappsでは不可)
  • Google謹製という安心感

使い方はこちらを参照 => GAS のGoogle謹製CLIツール clasp - Qiita

Stackdriver Logging

確か以前は console.log()はGASでは使えなかったと思うのだが、今は使える。 GASはサーバサイドで実行されるスクリプトなので、ブラウザのDev Toolには何も出力されない。 どこに出力されるかというと、Stackdriver というGoogle Cloud上のログサービスに出力される。

どうやら、去年の夏頃こうなったようだ3。 以前からGASにちゃんとしたロギングがほしいと思っていた。 Logger.log()では最後の実行分しか見れないので、どうしても必要な場合には スプレッドシートに書いたりしたものだ。 しかし、これからはStackdriver Loggingが使える。 DEBUG、INFO、WARN、ERRORという一般的なログレベルを設定して出力できるのも嬉しい。

Stackdriverは無料のサービスではないが、毎月無料枠があり、個人ユースではそれで足りるのではないかと思う。 今回はエラーログの記録に利用している。

ざっと調べてみた結果はこちら => GAS でStackdriver Loggingを使う - Qiita

最後に

久々に GASでの開発をやってみて、自分なりのモダンな開発構成を確立できたと思う。 gas-webpack-pluginがWebpack4に対応できていないのかどうもうまくトランスパイルできなかったので、実は今回はWebpack3を使っている。 そのうち対応されればアップデートしようと思う。

しばらく見ないうちにGASを取り巻く環境も色々変わっていた。 以前は自分にだけ自作Add-onをインストールする方法があったのだが、それがなくっている。代わりにテスト設定を作成してそこから起動すれば動かせる様に変更されようだ。少々めんどくさくなった。 その一方で、謹製CLIツールが用意されたり、ログ周りが強化されていた。
更に、GAS専用のマネジメントコンソールもできた。Google Driveでは他のドキュメントとごちゃまぜの管理だけど、こちらだとドキュメントに埋め込まれたGASも含め、すべてのGASのみを管理できる。 まだ、あまり調べていないが、バージョン管理と公開周りも開発版と公開版を別々に動かせるようにするためにちょっと変わったようだ。 このあたりの変更はChromeアプリのプラットフォームとしてGASを使っていくためであろうか?

何れにせよ、以前より開発しやすくなったと思う。 便利なサーバレス環境なので今後も活用していこうと思う。


  1. 個人的には全員でTogglに移行したほうが効率的なのではと思うものの、今、回っている仕組みを私の我儘トリガーで変更するほどでもないし、Togglも有料プランが必要になりそうで費用もいくらか発生してしまうので、個人的に解決する道を選択した。

  2. CSVでダウンロードする機能をTogglは提供している。しかし有料プランの機能。同等のデータはAPIを使えば取得でき、それを利用している。無料で有料機能相当を提供していることに気を使ってライセンスは CC-BY-NC-4.0 としている。

  3. G Suite Developers Blog: Stackdriver Logging for Google Apps Script is now available

Google Homeでリモコン操作をやってみた。その2

hero.hatenablog.jp

上に示したポストで Google Home -> Node-RED までを書いたけど、これはその続き。 Node-RED でMQTTのメッセージを受けた後、赤外線を発するまでを書く。

irMagicianの準備

Node-REDのフローのフローを作成する前に、その要素として必要になる赤外線リモコン irMagicianの準備をしておく。

f:id:HeRo:20180108140216p:plain

irMagician は大宮技研製の赤外線リモコンモジュール。 Raspiとの組み合わせでよく使われるものらしく、Amazonでも購入できる。 USB接続なので、電子工作苦手な私にはありがたい。

最初はirmagicianを使ってリモコン信号を記録しようとMacBookに接続して、動かしてみた。 その時点では、少々不安定なものの動いており、それらしいデータも取れていた。

しかし、そのデータを書き込んで動作確認するも、ターゲットのシーリングライトは全く反応しない。 キャプチャしたリモコン信号データが、Macではうまく書き出せないのかなと思い、 Raspi上で試すと今度は書き出したデータを書き込もうとするとエラーが発生する。 こりゃダメだと思い、仕方なくJSのライブラリの利用は諦めた。

ググると、pythonを使っている人が多そうだった。 で、netbuffalo/irmcli: irMagician command line utilityを試してみた。

irmcliのインストール

インストールと言っても irmcli自体は単なる puthon のスクリプトなので、github から cloneするだけ。

ただ、素のraspiだと、git や pip がないのと、pyserial に依存しているのでそれらをインストールする必要がある。 手順は次の通り。

sudo apt-get update
sudo apt-get install git python-pip
sudo pip install pyserial
git clone https://github.com/netbuffalo/irmcli.git

リモコンデータの作成

リモコン信号のキャプチャするには次のコマンドを実行し、リモコン受光部に実際のリモコンの赤外線を浴びせる。

$ python irmcli/irmcli.py -c

キャプチャした信号は irMagician に保持されているので、次のコマンドで、発信することができる。 irMagicianを操作したい機器に向ければそれで操作できる。

$ python irmcli/irmcli.py -p

リモコン信号をファイルに書き出すには次のコマンド。

$ python irmcli/irmcli.py -s -f [書き出し先ファイルパス]

書き出したファイルを読ませてリモコンを発信するには次のコマンドを使う。

$ python irmcli/irmcli.py -p -f [読み込ませるファイルパス]

一通りのやりたい操作をRaspi上で試して問題なく動作したので、 ターゲットのシーリングライトのリモコンのON/OFFの信号をキャプチャしてファイルに保存しておく。 irMagicianが保持できるリモコン信号は1つで、しかも揮発性で電源落とせば消えてしまう。 したがって、onとoffのそれぞれの信号をデータとして保存しておいて、発信するたびに読み込ませる必要があるのだ。

事前準備はここまで。

Node-REDのフローの登録

いよいよNode-RED上のフローを登録する。 次のようなフローにした。

f:id:HeRo:20180108100726p:plain

mqtt in ノードは その1 に書いたとおりの設定。 それ以降の設定を順に説明する。

JSON ノード

文字通り、受けたメッセージをJSONに変換する。 その1で書いたように、IFTTTからBeebotteには次のようなJSONを送信している。

{ "data": {"room": "bedroom","device": "light","action":"{{TextField}}"} }

これが、次の様にmag.payloadstringで入れられて送られて来る。

"{"data":{"room":"bedroom","device":"light","action":"オン"},"ispublic":true,"ts":1515375632470}"

これを後の処理で使いやすいようにJSONをオブジェクトに変換する。

{"data":{"room":"bedroom","device":"light","action":"オフ"},"ispublic":true,"ts":1515375632470}

Change ノード

Changeノードを制すものがNode-REDを制すほど使い勝手のあるノードらしい。

ここでは次の3つの設定で値を書き換えてある。

f:id:HeRo:20180108104648p:plain

1つ目と2つ目は オンオフをそれぞれ、onoffに書き換えている。 3つ目で、msg.payload.datamsg.payloadにセットして必要な値のみ後続の処理に送るようにしている。

Switchノード

Switchノードは条件分岐を行うノード。 ここでは、msg.payload.actionの値 on|offにより、点灯するのか消灯するのかを制御する。 次の様に設定した。

f:id:HeRo:20180108105715p:plain

Exec ノード

前述の様に node様のJSのライブラリはうまく動かなかったので、それを内部で利用する node-red-contrib-irmagician を使うことはできなさそうということで、pythonCLIツールirmcliを Execノードで実行することにした。 まあ、Exec ノードがあればどんな言語で実装されたものでも、コマンドラインで実行できればなんとかなる。

次の様に設定した。 次のキャプチャは 点灯用だけど、同様に消灯用も登録する。違うのは読み込ませるjsonファイルのみ。 f:id:HeRo:20180108110841p:plain

ついにシーリングライトのON/OFF

これですべての設定が完了した。

今回、利用したRaspiは 昔に買って、使い道なく放置していた RASPBERRY PI 1 MODEL B+。 Raspiの中でスペックは最も低いが、代わりに消費電力はPi3より小さいし、Node-REDを動かすだけなら問題ない。 ただ、起動して動作するまでは少々時間がかかるが、ずっと稼働させておくものなので気にならない。

f:id:HeRo:20180108141028p:plain

実際にやってみると、irMagicianの赤外線が方向にシビアできちんと対象の方向に向けていなければ赤外線が届かない。 それで、irMagicianを繋いでいるUSBケーブルに針金を巻き付け方向を保っている。

Google Homeに「OK Google、寝室ライトオン」と話しかければシーリングライトが点灯するし、 「OK Google、寝室ライトオフ」と話しかければ消灯する。 これまで、夜明かりをつけるときには暗闇でリモコンを探していたので思った以上に便利だ。

本当は、寝室のエアコンも操作したかったところだが、irMagicianの指向性が思った以上に強く、一つでいろんな家電を操作するには使いづらい。 LED拡散キャップも試してみたが、今度が距離的に厳しくなりすぎる。 ラズベリー・パイ専用 学習リモコン基板 だと部屋に1個で事足りるようになるだろうか。

まあ、しばらくは寝室の明かりだけかな。

あ、部屋を片付けてたら、大昔に買った PC-OP-RS1が出てきた。これまだ動くかなぁ。

Google Homeでリモコン操作をやってみた。その1

Amazon Echoの招待がなかなか届かないので*1Google Home を買ってみた。 使ってみて、ちょっとしたことを声だけで操作できるのは思った以上に便利だと感じる今日このごろ。

音楽を流したり、ちょっとした質問に答えてくれるのも便利だし、なぞなぞとか早口言葉をやってくれるのも面白いのだが、 やはり家電機器の操作もやってみたい。

とりあえず、最初は定番、電灯のON/OFF。我が家のシーリングライトは赤外線リモコン操作なので、リモコン信号を操作できれば実現できるだろう。

最初から連携するデバイスを購入してしまえばもっと簡単なのかもしれない。 しかしデバイスメーカーのサーバサイドサービスに依存するとサービスが終了した場合に機器ごと使えなくなってしまいかねないので、なるべく自作できるものの組み合わせで実現したい。

で、以下の組み合わせでやってみた。

うーん、どうもやりたいことのシンプルさの割に実現する仕掛けが複雑すぎるように思う。 しかし、2018年正月の時点ではインターネットに晒す自前サーバの運用なし構成ではこれでもシンプルな方かと思う。

以下にそれぞれの連携方法について書く。

Google Assistant -> IFTTT -> beebotte

IFTTTにGoogle Assistant のトリガーが用意されている。 そのトリガーとWebhookを利用して beebotte にメッセージを送る。 ここまでで、Google Assistantへの音声入力をプログラムから扱える形のメッセージに変換することになる。 そこまでを順に説明する。

Beebotte の設定

IFTTTのアプレット登録に、REST APIのエンドポイントが必要になるので先にBeebotteの設定を行う。

Beebotte はIoT向けのクラウドサービス。REST API や MQTTをサポートして、接続された機器にリアルタイムにメッセージを通知することができる。今回はこのサービスをMQTTブローカーとして利用する1。 50,000メッセージ/日までならフリープランで利用できる。個人ユースにはそれで十分であろう。

まずはアカウントを登録する。フリープランならユーザ名、メアドとパスワードだけで登録できる。 アカウントを作成したら、チャネルとリソースを登録するよう促される。 特に迷うところはないと思う。 登録したらこんな感じ。

f:id:HeRo:20180103182319p:plain

Channel名とresource名で REST API や MQTTのエンドポイントが決まる。 また Channel Token が発行されるが、これは後にIFTTTのアプレット登録やNode-REDから接続する場合に必要となる。

IFTTTでのアプレット登録

Google Assistant トリガーは4種類ある。用途に応じて使えば良いが、今回は Say a phrase with a text ingredient を利用する。これは音声入力したテキストをアウトプットに渡せる。今回は電灯の操作なので、オンオフを渡して制御する。

入力項目は次の通り。

設定項目 設定値
What do you want to say? 寝室ライト $
What's another way to say it? (オプションなのでよしなに)
And another way? (これもオプションなのでよしなに)
What do you want the Assistant to say in response? 寝室ライト $ しました。
Language Japanese を選択

What do you want to say? の設定が肝となる。 寝室ライト $と設定したが、「寝室ライト」の部分が 「OK Google,」の後に続くコマンドワードで何をしたいのかを示すキーワードとなる。 「$」はコマンドワードのパラメータとして設定できる音声入力のプレースホルダーで、ここでは「オン」あるいは「オフ」を想定している。 つまり、「OK Google, 寝室ライト オン」とGoogle Homeに話しかければ、このアプレットが起動して、$=オン がセットされるということとなる。

What do you want the Assistant to say in response? は音声コマンドを復唱させることにより、入力が正しいかどうか確認するために設定する。

続いて、アプレットの後半、アウトプットの設定。 このアプレットの目的は beebotte にメッセージをパブリッシュすること。パブリッシュの手段としてはbeebotteのREST APIを利用する。 そのため、アウトプットには Webhookを利用する。

設定は次の通り。

設定項目 設定値
URL http://api.beebotte.com/v1/data/publish/<Channel名>/<resource名>?token=
Method POST を選択
Content Type application/json を選択
Body {"data": [{"room": "bedroom","device": "light","action":"{{TextField}}"}]}

URL にはbeebotteのメッセージパブリッシュのRESR APIを指定する。先に登録した チャネルとリソースをパスにはめこんで、認証のためクエリパラメータtokenChennel token を設定する。 前述の スクリーンキャプチャに合わせると http://api.beebotte.com/v1/data/publish/MyIoT/light_at_bedroomというのがAPIのURLとなる。

Body には、APIに送信するデータを設定する。ここにはJSON形式で、後にNode-REDで処理する時に必要になりそうなパラメータを送信できるようにする。{{TextField}}には $ にセットされたテキスト、すなわち「オン」または「オフ」が入ることになる。

beebotte に Node−REDで接続

前節までで、Google Homeへの音声コマンドが、beebotte 上のメッセージに変換されるところまでができている。 あとは、Node-REDでそのメッセージを受け、処理するだけ。

Raspberry Pi やその上で動くNode-REDの準備は次に書いてあるとおり。

Node-REDではデフォルトで MQTTがサポートされている。 beebotte と連携するには mqtt in ノードを利用する。

mqtt in ノードの設定はこんな感じ。 トピックには [Channel名]/[resource名]を設定する

f:id:HeRo:20180107191427p:plain

mqtt in ノードのサーバ設定は別フォームが開くので次のように設定する。 セキュリティタブで、ユーザ名に token:[Channel Tokem] を設定するのが肝。

f:id:HeRo:20180107190740p:plain

設定項目 設定値
サーバ mqtt.beebotte.com
ポート 8883
SSL/TLS接続を使用 チェックする。TLSの設定は不要
ユーザ名 token:[Channel Tokem]

設定が正しく、beebotte に接続できる場合にはフローのデプロイ後、次のようにノードに 接続済 と表示される。

f:id:HeRo:20180107192117p:plain

動作確認

ここまでの作業を確認すために次のようなストリームを作って、Google Home => Node-REDに届くメッセージを確認する。

f:id:HeRo:20180108000451p:plain

OK Google, 寝室ライト オン」とGoogle Homeに話しかけて、 Debugノードのデバッグ出力に送ったメッセージが出力されれば成功。

さて、

この先は Node−REDでフローを作って、赤外線を発するまでだけど、ここまでで結構長くなったので、続きはまたあとで。

その2に続く。

hero.hatenablog.jp


  1. IFTTTはまあ大丈夫だろうけど、デバイスメーカーのディスコンを嫌う割にbeebotteなどというイマイチ聞いたことないサービスを使って大丈夫か?とも思ったが、いざとなればHerokuでCloudMQTTを動かすという代替策があるので採用した。ただし、CloudMQTTは無料では同時接続デバイスが10と少々少ない。

*1:今はやっと手元に届いたところ。

Severlessをやってみた

AWSでちょっとした処理を実行するのに Lambdaはとても便利。

これまで、ちょっとしたことをLambdaで実装したことはあったのだが、 ちょっとしたこと以上のコードをAWSコンソール上で書くのはなかなかつらい。

そこで、Serverless Frameworkを使ってみようと思った。

当初、Serverlessを使うメリットとして考えていたのは次のとおり。

  • ローカルの使い慣れたエディタでLambdaを実装できる。
  • Webpack + Babel プラグインを使えば、モダンなJavascriptの機能を使える。
  • デプロイも楽になりそう

試しに簡単なアプリケーションを書いてみた。

サンプルアプリのシナリオ

試してみる課題シナリオは次のとおりとした。

  1. Githubにプルリクを作ったり、プルリクにプッシュするとCodeBuildでビルドを実行する。
  2. プルリクには Github の Status APIで状態を通知する
  3. ビルドの開始をSlackにも通知する
  4. CodeBuild の処理終了をCloudWatch Eventで受け、その結果をGithub の Status APIで更新する
  5. 結果をSlackにも通知する。

作ってみたコードはこちら => HeRoMo/ServerlessSample: My First Serverless Application Sample

やってみて

やってみてどうだったのか?感想を以下に述べる。

関連リソースも一緒に定義可能

予備知識少なめで取り掛かったのでドキュメントを読みながら進めた。 最初はトリガーとなるAmazon SNS等は別に登録して用意しないといけないのかな?と思っていたのだが、 SNSを始め、API Gatewayなど、Lambdaをトリガーできるリソースは serverless.ymlの関数定義で一緒に定義できる。 これはとても便利。 また、Lambdaから他のAWSのリソース・サービスにアクセスするのに必要な権限もserverless.ymlでLambdaの実行ロールに追加できる。 とにかく、Lambdaの実行に必要なものがserverless.ymlで一元管理できるのが管理しやすくてよかった。 最初にデプロイした時、裏でCloud Formationのスタックが作成、実行されてちょっと驚いたが、こういうことなら納得。 Cloud Formationの定義記法で、Lambdaとは直接やり取りしない設定も定義できそうなのでCloud Formationも覚えねばと思った。

やはり async/await は便利

Webpack プラグインとbabelを追加して、ES2015 からNode6.10相当にトランスパイルするように設定した。 Lambdaを使う動機としてはAWSのサービス間の連携を開発するためのグルーコードとして便利というのもあると思う。 実際、AWSSDKは組み込みで動作するので適切な権限がLambdaの実行ロールに付与されていれば、別サービスにアクセスするのは それほど難しくない。 しかし、AWSの実態はHTTPベースのAPIなので、呼び出しはどうしても非同期となる。非同期を多用する必要がある場合には もう、async/awaitが使えないと辛い体になってしまっているので、これは助かった。 AWS SDKの関数は現状、コールバック式なので、早くPromiseを返すようになればと思う。

全般的な感想

前述したメリットはすべて享受できたと思う。 私はJSを書くときにはAtomエディタを使うことが多いが、やはり使い慣れたエディタだと実装しやすい。 webpackでビルドできると、babelでトラスアイルできるだけでなく、自由にnodeモジュールを使うこともできるので、 Github APIやSlack APIのライブラリを利用することができ、その点でも効率的に実装することができた。 デプロイもsls deployワン・コマンドで済むのはとても楽だった。

テンプレートも作った

今後別のアプリケーションを実装するのに便利なように、webpack+babelを設定したアプリケーションの雛形を作った。 ServerlessにはServerlessアプリをgithubから取ってきてそれをひな形に別のアプリを作成する機能が備わっている。 そこもよく考えられているなぁと感心した。

テンプレートはこちら。=> HeRoMo/sls-template: Serverless Framework application template