Rust で書いたコードを AWS Lambda で動かす

Mac でクロスコンパイルしたバイナリを Lambda で動かすという記事はあったのですが、Linux でビルドして Lambda で動かすという記事が見当たらなかったのと、それなりに躓いたのでメモを残しておきます。

Rust 自体はインストールできてるとして、大まかな手順としては、

  1. rustup で target x86_64-linux-unknown-musl を追加する
  2. musl-gcc をインストールする
  3. cargo でビルド
  4. Lambda にデプロイ

ではそれぞれ詳細な手順をば。

1. rustup で target x86_64-linux-unknown-musl を追加する

これは以下のコマンドを実行するだけですね。

rustup target add x86_64-linux-unknown-musl

2. musl-gcc をインストールする

Mac だと brew で、Ubuntu だと apt で入れられるっぽいのですが、私の使っている Amazon Linux だと yum で入れる方法がわからなかったのでソースからビルドしました。

GitHub - richfelker/musl-cross-make: Simple makefile-based build for musl cross compiler

こちらのリポジトリをクローンしてきて、

TARGET=x86_64-linux-musl make

これにはしばらく時間がかかります。

ビルドが完了したら

make install

これで output ディレクトリ以下に成果物ができます。

成果物を適当なディレクトリにコピーします。

sudo cp -f output/* /usr

cargo でビルド

AWS Lambda で動く Rust のコードは AWS のオフィシャルなブログでサンプルコードが公開 されています。

まずはこちらのコマンドを実行して雛形を作っていきます。

cargo new my_lambda_function --bin

記事に書かれているように Cargo.toml[dependencies] セクションの内容を追加していきます。

[package] セクションに autobins = false を追加するのを忘れずに。

[[bin]] セクションも追加します。

.cargo ディレクトリを作成し、その下に config というファイルを作って以下のような内容を書きます

[target.x86_64-unknown-linux-musl]
linker = "x86_64-linux-musl-gcc"

そしてこれをビルドします。

CC_x86_64_unknown_linux_musl="x86_64-linux-musl-gcc" cargo build --release --target x86_64-unknown-linux-musl

ビルドが成功すると、以下の場所にファイルができています。

$ file target/x86_64-unknown-linux-musl/release/bootstrap
target/x86_64-unknown-linux-musl/release/bootstrap: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically linked, not stripped

static link されていますね。

これを zip します。

zip -j rust.zip ./target/x86_64-unknown-linux-musl/release/bootstrap

4. Lambda にデプロイ

AWS の管理コンソールにログインして Lambda の関数を新しく作ります。

Auth from scratch を選択して Runtime は Provide your own bootstrap を選択します。

Function code で Upload a .zip file を選択して先程作成した rust.zip をアップロードします。

Save を押します。

以下のようなテストデータを作成してテストを実行してみましょう。

{
  "firstName": "Takashi"
}

実行が成功して以下のようなレスポンスが得られたら成功です!

{
  "message": "Hello, Takashi!"
}

Go 製 WebAssembly ホスト環境パッケージ wax のご紹介

みなさんこんにちは。

これは Go2 Advent Calendar 2019 の 12 月 24 日のための記事です。(少しフライング気味に投稿しておきます)

今日は個人的な趣味で作っている wax というライブラリのご紹介をしたいと思います。

3 行で

  1. WebAssembly のホスト環境(実行環境、ランタイムともいう)を Go で書いてみました。
  2. なので、WebAssembly だけど Web ブラウザは出てきません。フィボナッチ数の計算とかはできます。
  3. エミュレータを作ってるみたいで楽しいけど、一人で開発するのは辛いので仲間募集中です!

WebAssembly のホスト環境って?

WebAssembly というと、Go や Rust などの言語で書いたプログラムを Web ブラウザの上で実行させることができるということを思い浮かべられる方が多いと思います。それは WebAssembly の本来の目的というか、典型的な使い方だと思います。私はそういうメジャー路線にはいつも乗りそこねてしまうタイプであり、このところの WebAssembly のブームに関しても私はそこまで興味を持っていなかったというのが本当のところでした(「asm.js がバイナリになったんでしょー」(←多分違うw)とか「ブラウザで 3D ゴリゴリなゲームとかプレイしたい/作りたい人のためのものでしょー」くらいの認識でした)。

ところがある日、何かの拍子に WebAssembly の仕様書を拾ってしまったのです。

https://webassembly.github.io/spec/core/_download/WebAssembly.pdf

ちょっと目次などを眺めてみたらこれがまた面白そう。何が面白そうかって .wasm ファイルのバイナリフォーマットが定義されているんです。

バイナリを見るとパースせずにはいられない私としては、実物の .wasm ファイルをパースしてみたくなりました。そんなわけでいろいろな言語で簡単なプログラムを wasm に出力してみて、仕様書とにらめっこしながらバイナリをパースするコードを書き、.wasm ファイルの内容を JSON でダンプするツールなんか書いているうちに、仕様書に命令セットの定義とかが出てきてワクワクしてきてしまい、コードの実行もしたい欲が高まって、気づいたら wax という名前まで考えていました。

wax の wa は WebAssembly、x は execution から取っています。

ググラビリティの低さは懸念ですが、後悔はしていません。

少し話がそれましたが、WebAssembly とはスタックベースの仮想マシンの命令セットとその実行環境・実行方法(セマンティクス)などを定めた仕様ですが、そのバイナリフォーマットは比較的軽量(命令コードは1バイト固定、オペランドはたとえば整数リテラルが可変長なので、小さな整数は 1 バイトで表せるなど)であり、セキュリティを意識した仕様 -- たとえば明示的にスタックを操作する命令がなかったりスタックのメモリ空間がプログラムからさわれる領域にはなかったり(というかそもそもスタックにはメモリのアドレスなどの概念がない)などスタックオーバーフローを利用した攻撃が難しそうなどなど -- になっていたりと、まさに Web のために作られたものである印象を受けました。

で、Web ブラウザ上で実行することはもちろん WebAssembly の主目的だと思うのですが、WebAssembly の仕様書の冒頭には以下のような文章もありました。

1.1 Introduction

WebAssembly (abbreviated Wasm) is a safe, portable, low-level code format designed for efficient execution and compact representation. Its main goal is to enable high performance applications on the Web, but it does not make any Web-specific assumptions or provide Web-specific features, so it can be employed in other environments as well.

訳すると、

「1.1 イントロダクション

WebAssembly(Wasm と省略される)は安全、ポータブル、低レベルのコードフォーマットであり、効率的な実行とコンパクトな表現のためにデザインされた。そのメインのゴールは Web 上でハイパフォーマンスなアプリケーションを可能にすることだが、いかなる Web 固有の仮定をすることもなく Web 固有の機能を提供することもない。そのため他の環境でも同様に用いることができる。」

仕様書の冒頭のこの部分を読んだ時点で、私の WebAssembly に関するそれまでの狭い理解はぶち壊されました。

WebAssembly を自分のプログラムでホストする、すなわち WebAssembly の実行環境を自分のプログラムに組み込むことで、自分のプログラムでプラグインのような任意のサブプログラムを実行できるようになるということだと理解したのです。

たとえば Wireshark では独自の Dissector を Lua で書いて組み込むことができますが、Lua という言語を知らない人には敷居が高くなってしまいます。(Lua はシンプルな言語なので簡単に使えるようになるとは思いますが、それをメンテナンスしたりすることを考えると、たとえばチーム内でよく使われている言語を使いたくなったりすると思います。)

また、もし自分のプログラムのプラグイン機能を提供するとした場合、ユーザーにある特定の言語を利用することを強要するような感じになってしまうのは気が引けるというか、できればユーザーが自分の好きな言語でプラグインを書くことができるととてもよいと思いませんか?

それ(ユーザーが好きな言語でプラグインを書けること)を可能にするのが WebAssembly のもたらすもののうちの一つだと考えています。

WebAssembly の実行環境をホストすることで、自分のプログラムにユーザーのコードを安全に組み込むことができ、かつユーザーは自由な言語を利用できるようになる。

これまでその2つ(安全性の確保と自由な言語の利用)を両立させようとするとおそらく実質的には何らかのコンテナ技術を使うしかなかったと思うのですが、WebAssembly の登場によってより軽量に、さまざまな環境でそれを実現することができるようになると思います。

拙訳:

面白い事実:WebAssembly は Web でもなければアセンブリでもない。ブラウザの外でも実行できる。それは WASI (WebAssembly System Interface) と呼ばれる。とてもクレイジーであると同時にとてもパワフルである。Solomon Hykes (訳注:Docker の創業者兼 CTO)が言っていた:もし 2008 年に WASM と WASI が存在していたら、我々は Docker を作る必要はなかったかも🤯

実際、Envoy Proxy では WebAssembly で書かれた Extension をサポートするというニュース がつい最近ありました。

また CDN の Fastly は CDN の Edge でユーザーの任意のコードを実行できる仕組みとして WebAssembly を実験的に採用しました。(そのエンジンの Lucet は最近 Bytecode Alliance(WebAssembly の規格化団体)に移管されたようです)

今後、そのように、ユーザーのコードを実行することでプラットフォームやソフトウェアの機能拡張をするような場面では WebAssembly の採用がどんどん進んでいくのではないかと思います。

どうやって WebAssembly をホストするの?

wax のパッケージ(Go 言語の意味でのパッケージ)は WebAssembly ホスト環境のライブラリですが、そのパッケージを利用したその名も wax というコマンドも用意しています。

wax コマンドは .wasm ファイル(の中の指定された関数)を実行して結果を表示するプログラムですが、これは wax パッケージの具体的な利用例にもなっています。

基本的には wax.ParseBinaryModule() で .wasm ファイルをパースし、wax.NewRuntime() でランタイムを生成、runtime の FindFuncAddr() で関数を見つけ、同じく runtime の InvokeFunc() でその関数を実行するだけです。

InvokeFunc() の戻り値には、実行された wasm の関数の戻り値が含まれています。

なので、ホスト環境が Go 言語で書かれたプログラムであれば、wax を使うことで簡単に WebAssembly をホストすることができます。

現時点でできること・できないこと

できること:

  • 算術演算(整数型・浮動小数点数型)- 単項演算、二項演算、比較、変換、など
  • 関数呼び出し
  • 分岐(条件分岐、ジャンプ、ループなど)

できないこと:

  • ホスト環境の関数などのインポート
  • WASI などの ABI のサポート
  • 配列や文字列など、プリミティブ型(整数型、浮動小数点数型)以外を引数や戻り値に取る関数の実行 → これはそもそも仕様上無理かもしれません。ホスト環境のメモリ領域を Import してそれを介してやり取りする感じになるのが正解なのかも。というかその方法を定めてるのが WASI とかだと思うのでもっと勉強します

などなど

感覚的には、標準で用意されているテスト の正常系のうち、だいたい 7〜8 割くらいは通ってるかな、という感じです。

なので、まだまだできないことだらけではありますしおそらくバグとかも多いのですが、仕様書と格闘しながら少しずつできることを増やしていってる段階です。

プログラムを wasm 向けにコンパイルして実行してみよう

最近は いろいろな言語が wasm の出力をサポート しています。 今回は、その中から Go と Rust で書いたプログラムを wasm にコンパイルし、それを wax を使って実行してみたいと思います。

Go

みなさんの大好きな Go はもちろん wasm を出力できます。 しかし普通にやると GC や goroutine を含めた Runtime 全体が wasm になってしまい、出力されるバイナリのサイズが全然小さくなく wax で動かせる自身が全くないので、というかそもそも Go が吐き出す wasm のバイナリは JS 環境(ブラウザ)で使われることを前提としたものしか出力できなさそうなので wax ではきっと動かないので、今回の例では TinyGo を使って小さなバイナリを吐き出してみたいと思います。

TinyGo は Go のサブセットに対応したコンパイラで、組み込み向けの軽量なバイナリを出力します。

以下のようなコードを書いて、add.go というファイル名で保存してください。

package main

func main() {
}

//go:export add
func add(a, b int) int {
    return a + b
}

main 関数が空ですが気にしないでください。 普通の Go には無い //go:export という指定があります。 これはこの関数を add という名前で export するという指定です。

ではこのソースを TinyGo でビルドします。今回は Docker で tinygo のコンテナの tinygo コマンドを使います。 add.go をおいたディレクトリで、以下のコマンドを実行します。

docker run --rm -v "$(pwd)":/src tinygo/tinygo:latest tinygo build -target wasm -o /src/add.wasm /src/add.go

これで add.wasm というファイルができるはずですので、次はこれを wax を使って実行します。

go install github.com/bearmini/wax/cmd/wax
wax -f "add" -a "i32:1" -a "i32:2" add.wasm

go instal ~~ で wax コマンドをインストールしています。したがってこれは一度だけ実行すればよく、それ以降は wax コマンドを使えるようになります。($GOPATH/bin に PATH を通しておく必要があります)

wax コマンドの引数は -f で実行する関数の名前を指定しています。 -a は引数の指定です。型(i32)と値( 12)をコロン : でつなげて指定します。負の数や 16 進数でも指定できます。 最後に .wasm ファイル名を指定しています。

これを実行し、結果として以下のように表示されれば成功です!

0:i32:0x00000003 3 3

これは、関数の 0 番目の戻り値1i32 型で、0x00000003 という値だったことを示します。 その後の 2 つの 3 は、0x00000003 を 10 進数で表したもので、符号なしと符号付きでの解釈をそれぞれ示しています。

wax リポジトリには、.wasm ファイルを実行する wax コマンドの他に、.wasm ファイルの内容を JSON 形式で dump する wadump というツールや、.wasm ファイルに含まれるコードを逆アセンブルする wadisasm というツール、デバッグシンボルの情報などを削ぎ落として .wasm ファイルを軽量化 (strip) する wastrip というツールも含まれています。 これらも実行してみましょう。

まずは wadump から。

go install github.com/bearmini/wax/cmd/wadump
wadump add.wasm

すると以下のような結果が表示されるはずです。

{"Preamble":{"MagicNumber":1836278016,"Version":1},"Sections":[{"ID":1,"Size":30,"Content":"BmAAAX9gA39/fwF/YAAAYAF/AGACf38AYAJ/fwF/","FuncTypes":[{"ParamTypes":"","ReturnTypes":"fw=="},{"ParamTypes":"f39/","ReturnTypes":"fw=="},{"ParamTypes":"","ReturnTypes":""},{"ParamTypes":"fw==","ReturnTypes":""},{"ParamTypes":"f38=","ReturnTypes":""},{"ParamTypes":"f38=","ReturnTypes":"fw=="}]},{"ID":2,"Size":42,"Content":"AgNlbnYNaW9fZ2V0X3N0ZG91dAAAA2Vudg5yZXNvdXJjZV93cml0ZQAB","Imports":[{"Mod":"env","Nm":"io_get_stdout","DescType":0,"Desc":0},{"Mod":"env","Nm":"resource_write","DescType":0,"Desc":1}]},{"ID":3,"Size":17,"Content":"EAICAgICAgECAgMEAgUFBQU=","Types":[2,2,2,2,2,2,1,2,2,3,4,2,5,5,5,5]},
...(省略)

続いて wadisasm

go install github.com/bearmini/wax/cmd/wadisasm
wadisasm add.wasm

以下のような逆アセンブル結果が表示されます

func:0
0b end


func:1
10 84 80 80 80 00 call funcidx:00000004
0b                end


func:2
3f 00                   memory.size 0x00
41 10                   i32.const 00000010
74                      i32.shl
41 c6 80 84 80 00       i32.const 00010046
...(省略)

アセンブルリストの中に以下のような関数を見つけることができるでしょうか。

func:12
20 01 local.get 00000001
20 00 local.get 00000000
6a    i32.add
0b    end

func のあとの番号は使った TinyGo のコンパイラのバージョンなどによって変わってくるかもしれませんが、命令列の中に i32.add とありますね。この命令はスタックに積まれた 2 つの 32bit 整数を取り出し、加算を行って結果をスタックに戻す命令です。そしてこの関数こそが Go のソースコードadd() 関数から生成されたWebAssembly のコードです。

wastrip も同様に実行してみてください。実行前と実行後で、ファイルサイズや wadump の結果を比べるとその差が一目瞭然でしょう。

他にも、リポジトリ の examples/go ディレクトリの下にはフィボナッチ数を計算するサンプルコードなどがありますので実行したり逆アセンブルしてみてください。

Rust

最近人気のある Rust も WebAssembly の出力をサポートしています。

以下のようなコードを書いて add.rs というファイル名で保存します。

#[no_mangle]
pub extern fn add(a: u32, b: u32) -> u32 {
  a + b
}

これをコンパイルします。

rustc --crate-type=cdylib --target wasm32-unknown-unknown -O add.rs

これで add.wasm というファイルができるはずです。

実行方法等は Go の方で説明したのと同じですので割愛します。

ほかにも Emscripten を使うと C/C++ から wasm を出力することができると思いますし、他の言語も wasm をサポートするものが増えていると思います。 皆様もいろいろな言語からコンパイルして生成した wasm を実行してみてください。

他のホスト環境との比較

私の知る限りでは、以下のようなホスト環境がすでに世の中に存在するようです。

これらの有名所の実行環境はだいたい LLVM などをバックエンドに使って wasm を JIT もしくは AOT してネイティブコードとして実行するものが多そうです。

wax は今の所 JIT に対応する予定はありません。パフォーマンス的には JIT する実装に比べて見劣りすると思いますが、Pure Go なことで組み込みやすいとか、何かしら wax らしい特徴を出していけたらいいな、と思っています。

それから、GitHub - appcypher/awesome-wasm-runtimes: A list of webassemby runtimes にはその他の実装もたくさん列挙されています。いつかここに wax も載ることができるとうれしいですね。

お仲間募集中

一人でコツコツ開発するのは気ままでいいのですが、仕様をどうやって解釈したらいいのかがわからなさすぎるときなどにモチベーションの維持が難しかったりするのでそういうときに相談できたりするお相手がいるとうれしいな、という気持ちです。 あと、MVP という最小限の仕様はまだ独力でもなんとかなりそうなのですが、スレッドや SIMD のサポートなど MVP 以上の実装をしようと思うとやはり一人では限界がありそうと今から感じています。

もし wax の開発に興味のある方は Twitter @bearmini にぜひお声がけいただければと思います。 Github 上での Issue や PR も大歓迎です~!

ということで、メリークリスマス!🎅


  1. 0 番目の戻り値、ということは1番目・2番め・・・の戻り値もあるのか、ということですが、現時点の WASM の MVP (Minimal Viable Product) では関数の戻り値は1つまで、ということになっています。将来のバージョンでは複数の戻り値を返す関数もサポートされる可能性があります。

SORACOM LTE-M Button powered by AWS(通称 #あのボタン)を使って EC2 のインスタンス代を節約した話

この記事は SORACOM Advent Calendar 2019 ふたつめ の 12 月 3日分です。

はじまり

最近、開発用の PC の調子が悪かったのもあって EC2 インスタンス上にいろいろな開発環境を構築してみています。 そうするとどの PC を使っても同じ開発環境で作業を継続できると思ったからです。

近頃は VS Code のリモート接続拡張を使うことで、あたかもローカルにファイルがあるかのようにリモートホスト上のファイルが編集でき、統合ターミナルを使って自然にリモートホスト上でコマンドを実行できますので、リモート接続していることを忘れてしまうときがあるほどなので、これさえあればだいぶ行けるだろうと思ったのです。

最初はお試しと思って t3.micro インスタンスで始めたものの、自分の書いたプログラムを Fuzzing したいと思ってぶっ通しでプログラムを実行しようとしたら CPU クレジット不足になってしまったので c5.large にインスタンスタイプを変更し、Fuzzing が終わって今度はいろいろテストを実行していたらメモリ不足になってしまったので r5.large に更に変更し、という感じで今に至っています。

ソラコムでは社員の福利厚生(?)として AWS のサービス使い放題というのがあるのですが、貧乏性の私はさすがに r5.large インスタンスを起動しっぱなしはもったいないなぁと感じてしまい、弊社の リーダーシップステートメント にも Avoid Muda とあるように少しでもコストを削減しようと思い、開発業務をしていない間はインスタンスをちゃんと停止しようと考えました。

r5.large インスタンスは Tokyo リージョンでは $0.152/hour (2019年12月現在)ですので、使っていない時間が1日の半分(12時間)あったとして1ヶ月(30日)で $0.152 * 12 * 30 = $54.72 のムダが生じていることになります。(実際には休日などもありますし、1日12時間も働くことはまれですし、もっとムダが生じてしまうことになるはずです。)

しかしながら生来のめんどくさがりである私は、インスタンスを起動したり停止したりするために AWS コンソールにログインして EC2 のページから自分のインスタンスを検索して起動したり停止したりするのは非常に面倒であると思いました。 aws コマンドを使ってコマンドラインで実行するともう少し手間は少ないかもしれませんが。

というわけで、ここはテクノロジーの力で解決しよう!(大げさ)と思って取り出したのがこちらの商品です。

soracom.jp

そう、「AWS ボタン」とか「#あのボタン」と呼ばれているものです。

設計(というほどのものでもない、ただの思いつき)

目論見はこうです。

  1. 仕事を始めるタイミングでボタンを押す。インスタンスが起動する。
  2. 仕事を終えるタイミングでボタンを押す。インスタンスが停止する。

これだけです。 究極にシンプルです。

AWS ボタンは、1回押し(シングルクリック)、2回押し(ダブルクリック)、長押し(ロングクリック)の3タイプの押し方ができますので、これをそれぞれ以下のような機能に割り当てることにしました。

  • シングルクリック:現在のインスタンスの状況(稼働中か、停止中かなど)を Slack のチャンネルに報告
  • ダブルクリック:インスタンスが停止していたら起動。Slack のチャンネルに報告。
  • ロングクリック:インスタンスが起動していたら停止。Slack のチャンネルに報告。

実装

ではここから実装に入っていきます。

AWS IoT 1-Click の設定

まずは AWS IoT 1-Click で AWS ボタンを Claim(登録)して有効化します。 手順などはこちらを参考にしてください。

dev.soracom.io

Lambda で起動される関数を先に作成

今回は Go で Lambda 関数を作成しました。 Gist にコードを上げておきました。これをビルドして ZIP してアップロードします。

https://gist.github.com/bearmini/99e99ead08ed0e8aff39c6379f5aeeae

AWS IoT 1-Click に戻り、プロジェクトを作成

AWS IoT 1-Click で Project と Device template を作成します。

f:id:bearmini:20191203124552p:plain
このような設定をしました

起動する Lambda は先程作ったものを指定します。

プレイスメントの設定

そのボタンで起動/停止したいインスタンスインスタンス ID、リージョン、通知先の Slack の Incoming Webhook URL をプレイスメントで指定します。

実行!

ボタンを押してみます。

まずは恐る恐るシングルクリック。

ボタンの LED がオレンジ色に点滅して10秒ほど待つと緑色に光ります。 すると程なくして Slack に通知がくるはずです。

f:id:bearmini:20191203125817p:plain
シングルクリックした際の通知。現在の状態(running)が通知された。

インスタンスを停止する際は長押しします。

f:id:bearmini:20191203125931p:plain
インスタンス停止時の通知

インスタンスを起動する際はダブルクリックです。

f:id:bearmini:20191203130041p:plain
インスタンス起動時の通知

インスタンスを停止すると、瞬間的に VS Code は画面が暗くなって再接続を試み始めます。

インスタンスを起動すると VS Code は再接続に成功します。画面を一度リロードする必要がありますが、それ以降は何事もなかったかのように作業を継続できます。(開いていたターミナルなどは閉じてしまいますが、screen コマンドなどでセッションを維持するようにすればそれも問題なく継続できるでしょう)

おわりに

というわけで、ちょっとの投資とちょっとの工夫、そしてちょっとの実装でこんなに簡単に節約ができました。

みなさんも、何かアイディアを思いついたら、Just Do It! してみましょう。

WSL から "通知" を出す方法

PowerShell を Administrator 権限で起動して以下のコマンドを実行

Install-Module BurntToast
Set-ExecutionPolicy RemoteSigned
Import-Module BurntTost

WSL から

powershell.exe -command BurntToastNotification -Text "Test"

Hyper の Extension の作り始め方 - とくにデバッグの仕方

Windows に乗り換えてからターミナルソフトとして何を使うかをずっと悩んでいます。 Mac のときはずっと iTerm2 一択で特に不自由なく使えていたのですが、Windows 用となると Windows Terminal なんかも気になりますが、現時点(2019年6月中旬時点)では GitHub の README

The Windows Terminal is in the very early alpha stage, and not ready for the general public quite yet.

と書かれているので、興味はありますが仕事で使うにはちょっとまだやめておいたほうが良さそうです。

ということで、ConEmu、ConsoleZ、MobaXterm をインストールしてしばらく試用してみました。それらの中では MobaXterm が一番しっくり来たのですが、一つだけどうしても欲しい機能が見つけられませんでした。 その機能は iTerm2 にはあってとても便利につかっていたのですが、それはコンソールに出力された文字列のパターンに反応して任意のコマンドを実行する、というものです。

そんな機能何に使うの!?というツッコミは甘んじて受けますが、これがないと仕事の効率がめちゃめちゃ下がるんです。

なのでもし拡張機能とかが自分で作れるなら、そのくらい作るんだけどなーと思っていました。

そんなときにふと同僚が Hyper を使っているのを見て、私も Mac 版は少し触ったことはあったのですが Windows 版もあるということを知って少し試してみました。

Hyper は標準のままでは私の欲しい上記の機能は存在していませんし、既存の Extension にもそのようなものは存在しないようですが、Extension を自分で作れるという点が他の Terminal と違うところです。

というわけで Extension を作ってみました。

前置きが長くなりましたが、例によっていろんな罠にハマりましたのでその時の試行錯誤の記録を残しておきます。

1. 準備

まず当然 Hyper をインストールします。インストール方法は割愛(公式からダウンロードするだけです)。

自作のプラグインはいきなり npm パッケージにして公開するわけにも行きませんから、Local plugin という形で開発しますので、その準備をします。

Windows の場合は %USERPROFILE%\AppData\Roaming\Hyper\.hyper_plugins\local の下に好きな名前でフォルダをひとつ作ります。 その下に index.js と package.json を作ります。

index.js にはプラグインのコード本体を書いていきます。まずは以下のような内容のボイラープレートを書いておきましょう。

console.log('hello');
exports.middleware = (store) => (next) => (action) => {
  console.log(action);
  next(action);
}

package.json は普通に name とか version とかを書いておきます。

hyper の Preferences を編集します。メニューから Edit > Preferences と選択するか、Ctrl+, を押すか、%USERPROFILE%\AppData\Roaming\Hyper\.hyper.js を直接編集して、一番下の方にある localPlugins の配列に先程作ったフォルダの名前(フォルダ名のみ)を指定して保存します。

2. Hyper から Hyper を起動してプラグインを開発・デバッグ

Hyper プラグインデバッグの仕方を誰も書いてくれていなかったのでかなり試行錯誤しましたが、プラグイン開発時は以下のようにすると良さそうです。

  1. まずは普通に Hyper を起動します。(スタートメニューとかから)
  2. 起動した Hyper のターミナルから、hyper コマンドでもう一つ Hyper を起動します。予め %USERPROFILE%\AppData\Local\hyper\app-x.y.z\resources\bin (x.y.z は適宜環境に合わせてください)に PATH を通しておく必要があるかもしれません。 最初に起動した方の Hyper を親 Hyper、親から起動した方を子 Hyper と呼ぶことにします。
  3. 親 Hyper にはプラグインがロードされたとかのメッセージが表示されます。これは子 Hyper 側でロードされたプラグインの情報です。プラグインに syntax error があったりするとここに表示されます。index.js に書いた、console.log('hello'); もここに出力されます。
  4. 子 Hyper 側で Developer tools を開きます。Ctrl+Shift+I で開くか、メニューから選択します。Hyper から呼ばれるプラグインの関数で console.log() するとこちらのコンソールに出力されます。console.log(action); の内容はこちらに出力されます。
  5. index.js を書き換えたら、子 Hyper で Ctrl+Shift+U を押して Plugin を更新します。親 Hyper にエラーなどが出力されていないことを確認したら、子 Hyper で Ctrl+Shift+R を押してリロードします。これで開発中のプラグインの新しいバージョンがロードされた状態になりますので動作確認等を行うことができます。

3. あとは自分の作りたい機能をバリバリ実装

見た目を変えたい場合は decorateXxx 系の関数を実装して export、コンソール上でやり取りされる文字列のデータが欲しいときは middleware() を実装して export という感じかと思います。 middleware() 内では基本的には 引数の action.type に応じて switch していろいろな処理を実装することになると思います。 表示する文字列をどうこうしたいときは SESSION_ADD_DATA など。 プラグイン固有の設定は .hyper.js の config 直下に プラグイン名 のキーを作ってそこに入れておき、ソース側では middleware() の action.type が CONFIG_LOAD もしくは CONFIG_RELOAD のときに action.config から読み取っておくと良いようです。 基本的には middleware() 関数を抜ける前に next(action) を呼び出します。state をいじってべつの action を起動することもできそう。

ここから先は検索すればいろいろ情報が出てきますし、公開されているプラグインなどのコードを参考にすれば大体のことはわかるのではないでしょうか。

我ながら説明が雑ですねw

WSL で Apex を使って Go の Lambda を作ったらエラーになった件とその対策

最近仕事用のメインマシンを数年ぶりに Windows にしたので WSL を使っていろいろとハマっています。

この記事の件もそのうちの一つです。

表題の通り、WSL 環境で apex を使って Go の Lambda をデプロイしたところ、デプロイ自体は問題なくできたのですが、その Lambda を実行すると以下のようなエラーが起こるようになってしまいました。

  ⨯ Error: function response: fork/exec /var/task/main: no such file or directory

どうやらデプロイされた実行ファイル(/var/task/main)が見当たらないというエラーのようです。 しかし、apex build で生成された ZIP ファイルの中身を見てみるとちゃんとファイルが存在しています。

上記のエラーメッセージをもとにネットを検索してみたところ、どうやら dynamic link されたバイナリだと、ビルド時の環境によっては実行できないことがあるようです。

解決策としては、static link するようにするとよいということです。

apex を使っているとコンパイルオプションなど気にしたことがありませんでしたが、project.json (もしくは function.json)に以下のような設定を追加することで static link 用のコンパイルオプションを指定できます。

  "hooks": {
    "build": "GOOS=linux GOARCH=amd64 go build -a -tags netgo -installsuffix netgo -ldflags='-extldflags=\"-static\"' -o main",
    "clean": "rm -f main"
  }

go build の後ろの -a -tags netgo -installsuffix netgo -ldflags='-extldflags=\"-static\"' が全て static link のために必要なオプションです。

WSL に限らず、alpine などでビルドしたバイナリも同様の問題が起こるようですので、これで直るのではないかと思います。

なお、Mac など従来このオプション指定なしでビルドしても動いていたような環境で上記オプションをつけてビルドしても問題なく static link になるようなので、私の手元では全て上記オプション指定に変更しました。

Slack アプリのフォントを変更する方法

[2019.07.22 追記] Slack 4.0.0 以降、ファイルの配置と適用方法が変更になりました。 以下の手順の実行には、Node.js と npx、asar というパッケージが必要です。 Node.js がインストールされている環境であれば npm i -g npx asar でインストールできます。

  1. Slack を閉じる

  2. %LocalAppData%\slack\app-x.y.z\resources\ ディレクトリに移動

  3. sudo npx asar extract app.asar app.asar.unpacked コマンドでアプリケーションパッケージを展開

  4. app.asar.unpacked\dist\ssb-interop.bundle.js というファイルを編集し、以下の内容を末尾に追加

document.addEventListener('DOMContentLoaded', function() {
        let s = document.createElement('style');
        s.type = 'text/css';
        s.innerHTML = '*{font-family:"Slack-Lato", "BIZ UDPGothic", appleLogo !important;}';
        document.head.appendChild(s);
});
  1. sudo npx asar pack app.asar.unpacked app.asar コマンドでアプリケーションパッケージを作成し直し

  2. Slack を起動

[2019.07.22 追記終わり]

Windows 10 の英語版を使っていると、Slack のアプリのフォントがいわゆる「中華フォント」状態になってしまいます。 Slack アプリの設定にはフォントを変更する機能は無いようなのですが、Slack アプリは Electron 製ということで CSS を無理やり適用することができるようです。

%LocalAppData%\slack\app-x.y.z\resources\app.asar.unpacked\src\static\ssb-interop.js を編集して、末尾に以下のような内容を追加します。

onload = function() {
        $("<style></style>").appendTo("head").html("*{font-family:'Slack-Lato', 'BIZ UDPGothic', appleLogo !important;}")
};

Slack-Lato は Slack の標準のフォントです。英数字などは標準のままのほうが違和感が少ないため、Slack-Lato をまず指定します。

次に指定しているのは、日本語表示用のフォントです。Windows に最初から入っていたフォントをいろいろ試してみて一番しっくり来たものを指定しました。

アプリではなくブラウザで Slack にアクセスした場合のフォント設定は font-family: NotoSansJP,Slack-Lato,appleLogo,sans-serif; となっているようですので、こちらに従ってみるのも良いかもしれません。

Slack のアプリのバージョンが上がるとデフォルトに戻ってしまいそうなので、自分用のメモでした。

別ホストの Linux の Docker を使う方法

私は仮想マシン上の Windows 10 の WSL から Ubuntu Linux 18.04 のホスト上で動いている Docker を使うように設定しましたのでこのあとの記事の書き方はその環境が前提の書き方になっていますが、Windows仮想マシンである必要はなく、同じネットワーク内の別ホストであれば Windows である必要すらなく、Mac や他の Linux などからも使うことができると思います。

また、WSL の場合は WSL 自体に直接 Docker を入れることもできるようなのですが、ネット上で見つけたいくつかの方法は私が試したところいずれもうまく行きませんでした。Windows をそもそも仮想環境で動作させているためかもしれません。というわけで WSL に Docker を入れるのは諦めました。

Docker for Windows を入れてみても良かったのですが、これもやはりもともと仮想環境な Windows 上に更に仮想環境を作ることになりそうだったのでうまく動くかどうかわからなかったことと、せっかくホスト OS が Linux なのでそこでコンテナを動かせばよいではないかと思い、その方法を調べたところ非常に簡単だったのですが、将来の自分のためにメモしておこうという感じです。

大まかな手順は以下のとおりです。

  1. ホスト OS (Ubuntu Linux) 側で Docker の起動時オプションを変更して、TCP でもリモート API サーバーをホストする
  2. ゲスト OS (Windows 10 WSL) 側で DOCKER_HOST 環境変数を使ってホスト OS の Docker を使うようにする

これだけです。(実際には WSL に docker コマンドをインストールしたりしたかもしれません。)

注意していただきたいのは、この方法だとホストの Docker がネットワークに公開されることになりますので、セキュリティ面では十分に気をつける必要があります。 (信頼できるプライベートなネットワークにしか接続されないようにするとか、もしくはネットワークアクセス制御などを適切に適用して意図しないアクセスを受け付けないようにするなどの対策を講じる必要があります)

1. ホスト OS (Ubuntu Linux) 側で Docker の起動時オプションを変更して、TCP でも API サーバーをホストするようにする

まずはホスト OS 側で以下のコマンドを実行して現在の設定を取得します。Ubuntu 18.04 では以下のファイルでしたが、他のバージョンでは違う場所にファイルがあるかもしれません。

sudo grep ExecStart /lib/systemd/system/docker.service

私の環境では以下のように表示されました。

ExecStart=/usr/bin/dockerd -H fd:// --containerd=/run/containerd/containerd.sock

この表示された内容をコピーしておきます。

次に、以下のコマンドを実行します。これは systemd の既存の unit ファイルに対して適用するための差分のファイルを生成するコマンドです。

sudo systemctl edit docker

エディタが起動しますので、先程コピーした内容を貼り付け、さらにその行に -H tcp://0.0.0.0:2375 を追加します。(Docker の API サーバーを TCP で指定のアドレス/ポートでホストするというオプションです。既存の別の -H オプションが存在しますが、その後ろあたりに追加すると良いでしょう) また、その行の前に以下のように2行追加します。

[Service]
ExecStart=
ExecStart=/usr/bin/dockerd -H fd:// -H tcp://0.0.0.0:2375 --containerd=/run/containerd/containerd.sock

真ん中の行の ExecStart= だけの行は決して間違いではありません。 このように書くことで、既存の ExecStart の設定を上書きすることができます。

保存して閉じたら、以下のコマンドを実行して Docker を再起動し、設定の変更を適用します。

sudo systemctl restart docker

ここまででホスト側の準備は完了です。

2. ゲスト OS (Windows 10 WSL) 側で DOCKER_HOST 環境変数を使ってホスト OS の Docker を使うようにする

ゲスト OS (Windows 10 WSL) で以下のコマンドを実行してみます。

DOCKER_HOST=tcp://192.168.1.234:2375 docker ps -a

192.168.1.234 の部分はホスト OS の IP アドレスに置き換えてください。

これでエラーにならずにホスト上で実行されているはずのコンテナの一覧が表示されたら成功です! (事前にホスト上で docker run hello-world などを実行しておくとわかりやすいかもしれません)

毎回 DOCKER_HOST を指定するのが面倒なので .bashrc あたりに以下のような設定を書いておくと良いでしょう。

export DOCKER_HOST=tcp://192.168.1.234:2375

なおポート番号の 2375 は Docker デーモンの暗号化されていない API サーバーで慣習的に使われているポート番号のようですが、他の値に変えても大丈夫です。

Docker

Docker

AWS アカウントから IAM の SSL サーバー証明書を撲滅した話

HTTPS でアクセス可能な API などの Web サービスを構築する場合、サーバー証明書が必要になります。

AWS の場合は現在は ACM (AWS Certificate Manager) を使うことで、自動更新される証明書を無料で発行できますので完全にそちらを使っているのですが、ACM 登場以前は他社から購入した証明書を AWS の IAM に登録してそれを利用しなければなりませんでした。 (EC2 で自前の Web サーバーを用意する場合は IAM に登録する必要はありませんが、ここではサービスのスケールアウトを考慮して CloudFront や ELB などを使う場合を想定しています。)

しかしたいていの証明書には有効期限というものがあり、しかしながら IAM に登録した証明書は ACM の証明書のように自動更新というわけにはいかないので、手動で更新していく必要があります。(更新するついでに ACM の証明書にしてしまうのがいいですね。)

IAM に登録した証明書は、CloudFront と Elastic Load Balancer (ELB) で利用可能ですが、たとえば API Gateway でカスタムドメインを使うと CloudFront 経由で証明書が利用できたり、Elastic Beanstalk も Load balancer を使う設定にしていると ELB 経由で証明書を利用する設定にできます。

私が把握しているのはこの 4 つのサービス(CloudFront、ELB、API Gateway、EB)のみですが、もしかしたら他にも IAM の証明書を使うことができるサービスがあるかもしれません。

さらに、一つのアカウントの中で複数のリージョンにまたがってそれらのサービスを使用して IAM の証明書を用いているという場合も考えられますし、場合によっては複数のアカウントを使いこなしているかもしれません。

そのような場合に、どこで IAM の証明書を使っているかをすべて手作業で列挙するのはなかなか難しいと思います。

そこでスクリプトを書いて aws コマンドを駆使して IAM の証明書を見つけ出してみました。

Detects if any IAM cert is used in the specified A ...

実行方法ですが、

$ ./iam-cert-check.sh <profile> ...

という具合に、aws コマンドで使うプロファイル名を指定します。(複数指定可能です)

これでいろいろな環境の IAM 証明書の使用の有無を一度にチェックできます。(もちろんそのプロファイルに十分な権限を与えておく必要はあります)

もしかしたらスクリプトにバグがあって IAM 証明書の存在を見逃している可能性も否定できませんので、ご利用は自己責任でお願いします。 また、今回チェック対象にした 4 つ以外のサービスでも IAM 証明書が使われている可能性がある場合はお知らせいただけると嬉しいです。

このスクリプトを利用して検出したすべての IAM の証明書を ACM の証明書に切り替えたら、あとは IAM の証明書を削除します。

aws iam delete-server-certificate --server-certificate-name <name> --profile <profile>

このコマンド、証明書をどこかで利用中でも容赦なく削除できてしまうそうなので十分な注意が必要です。 (AWS コマンドのリファレンスにその旨の警告があり、それで怖くなって調べていたらいつの間にか今回のスクリプトが出来上がっていましたw)

いったん ACM の証明書に切り替え終わると、後は条件を満たしていれば自動更新されるので安心ですね。

なお、ACM の証明書でも、自動更新の条件を満たしていない証明書があったときのことを考え、弊社では毎日一回 ACM の証明書の有効期限を自動でチェックして Slack に結果を通知しています。未使用の証明書などを削除するきっかけになるのでこれも役に立っています。 機会があればそちらのコード(Lambda 上で動く Go のプログラム)もご紹介しようかと思います。

Go 1.11 のバグと DNS の障害の合わせ技でハマった件

Go で書いたプログラムで、HTTP(S) による通信を行うとなぜか10秒間プログラム (goroutine) がストールして、その後正常に通信が行えるという謎な現象に悩まされ、無事解決できたのでここにメモしておきます。

といっても、解決のためにやったことといえば、Go のバージョンを 1.11.1(バグが修正されたバージョン)に上げただけなのですが😅

「同様の問題にハマっている人が多いみたいなので知見を共有すればよいのでは」という同僚の id:moznion のすすめもあって書いてみることにしました。

TL;DR

問題の内容と、解決に至るまでの経緯を時系列順に書き下してみます。

まず問題に気づいたのは、私が関わっているある Web サイトのログインなどの処理がやけにもっさりしているという報告を受け、それを私も確認したタイミングです。 エラーになるわけではないのですが、特定の API を呼ぶタイミングで、毎回 10 秒くらい待たされてしまうようになっていました。

ブラウザでデベロッパーツールを開いてみると、問題の API が毎回 10 秒ちょうどくらいでレスポンスを返してきていることに気づきました。正常時は数十 msec でレスポンスが返ってきますので、これは明らかにおかしいと思いました。しかも、「10秒かかるときもあれば15秒くらいなときもあれば8秒くらいなときもある」というような、バックエンドに負荷がかかったりしていることに起因しているような感じではなくて、常に 10.xxx 秒のレスポンスタイムでした。

その不調になっている API のサーバーは私が Go で構築して AWS 上で運用していたものでしたので、新しいインスタンスを立ち上げてそちらに最新版をデプロイし切り替えた時点でユーザー様から見える問題はすぐに回復しました。(ログインなど API を呼ぶ部分で 10 秒もかからなくなりました)

しかし原因究明をしないと不安だったので、不調だった方の API サーバーはサービスから切り離しましたがそのままアクセスはできる状態で残しておきました。

当初は、サービスの負荷が原因かと思ったのですが、サービスから切り離して負荷がほぼゼロの状況でも API 呼び出しに 10 秒かかるという問題は解決しませんでした。 この時点で、負荷が原因という説は否定されました。

API サーバーのログを確認すると、Redis (Elasticache) に接続しに行く部分の前後でタイムスタンプに 10 秒のラグがあるようでした。

そこで Elasticache もしくは AWS 上のネットワークの不調などを疑ったのですが、API サーバー上のシェルから curl コマンドなどで Redis に接続しにいってもまったく問題なく一瞬で繋がりますし、redis-cli でのコマンドのレスポンスにも全く問題がありませんでした。

Elasticache のイベントログ等を見ても、何日間もまったく問題なく動いているようでした。

Redis クラスターの DNS 名の解決も、dig コマンドなどで一瞬で解決できます。

ということで、どこで時間がかかっているのか、まったくわからなくなってしまいました。

こうなると、怪しいのは Go のプログラム自体です。

Redis のクライアントとしては redigo を使っていますが、こちらの redis.Dial() 関数の呼び出しで 10 秒かかっていることまではわかったのですが Issue などを探しても同様の問題は報告されていないようでした。

Dial() の内容としては基本的には名前解決をしてソケットを開いているだけだと思ったので、名前解決の部分がおかしいのかと思い、困り果てて golang dns slow みたいなやけっぱちな検索ワードで検索してみたところ奇跡的に引っかかったのがこちらの Go 本体の Issue でした:

github.com

この Issue 自体は 2017 年 9 月にレポートされたもので、 Go 1.8 とか 1.9 の問題だったので、Go 1.11 を使っていた私には関係ないか、、、と思いつつもスレッドを読んでいったところ、今年の8月29日に「Go 1.11 で DNS の名前解決が 10 秒かかる」という、私とまったく同じ症状を訴えている人がいました。

https://github.com/golang/go/issues/21906#issuecomment-416809879

dig など他のツールでは一瞬で名前解決できる、という点も私の症状と同じでした。

で、その Issue は

https://github.com/golang/go/issues/27525

に引き継がれ、以下のコミットで修正されたようでした。

https://github.com/golang/go/commit/94f48ddb96c4dfc919ae024f64df19d764f5fb5b

結論としては、Linux 上で /etc/resolv.conf に書かれた nameserver のうち壊れたものが一つでもあると 10 秒かかるようになってしまっている、ということのようでした。

問題が起こった環境を見てみると、 /etc/resolv.conf に書かれた nameserver が 3 つあり、そのうちの一つで障害が起きており、応答が返ってこない状態になってしまっていました。

また、問題発生時に新しくデプロイし直して復旧した方のバージョンは確実に 1.11.1 でビルドしたもので、同じ nameserver を参照しているにもかかわらず 10 秒の遅延は発生しておりませんでしたので、あとは問題が起こっている方のバイナリが 1.11 でビルドされたものであることが確認できれば、ほぼ確実にこのバグにより引き起こされた問題ということができると思いました。

ということで、問題が起きた方のバイナリをコンパイルした Go のバージョンを調べてみることにしました。

コンパイル済みのバイナリから、そのバイナリをコンパイルした Go のバージョンを調べるには、以下の Dave Cheney 氏の記事が参考になります。

How to find out which Go version built your binary | Dave Cheney

すなわち、gdb で実行中のプロセスにアタッチもしくは新規プロセスを起動して、runtime.buildVersion という定数の値を表示してみれば良いわけです。

障害が起こったほうでは

(gdb) p 'runtime.buildVersion'
$1 = {str = 0xa33954 "go1.11gophergrave;gsime;gsiml;gtcir;gtdot;hangupharrw;hcirc;headerhealthheartsheighthelliphiddenhoarr;iacuteicirc;iexcl;ignoreigraveiiint;iiota;ijlig;imacr;image;imath;imped;infin;iogon;ip+netiprod;iq"..., len = 6}

と表示されましたので、 len = 6 ということで先頭から6文字分を読みます。するとgo1.11 ですね。

新しくデプロイした方は

(gdb) p 'runtime.buildVersion'
$1 = {str = 0xa38fb9 "go1.11.1gtquest;gtrless;harrcir;hijackedhostnamehreflanghttp/1.1iam/infoidentityif-matchif-rangeinfinityintprod;invalid isindot;languagelarrbfs;larrsim;lbrksld;lbrkslu;ldrdhar;lesdoto;lessdot;lessgtr;"..., len = 8}

ということで go1.11.1 です。

どうやらビンゴのようです。

ということで、Go 1.11.1 でビルドしてデプロイした現在のバージョンはこのまま問題なく稼働できそうということがわかり一安心です。

ただ、Elasticache へのアクセスは明示的にタイムアウトを設定して、異常時にももうちょっと早めにキャッシュを利用しない処理にフォールバックできたほうがよいように思いましたのでその点は修正する必要がありそうです。

まとめ

たしかコードコンプリートあたりに「プログラマはすぐ『コンパイラのバグじゃないか?』と疑いがちだけど、99%以上の場合は自分のコードのバグだ」みたいなことが書いてあったと思いますし私もしょっちゅう処理系を疑ってはブーメランが飛んできて自分のコードが良くないことを発見するのですが、今回はほんとに処理系のバグでした。

あと、DNS の障害が引き金になって起こるというレアなケースだったので調査に苦労しましたが原因を見つけることができたときはとても痛快でした。

これだからプログラマーはやめられませんね。

CO2 濃度を測ってみた 〜SORACOM Harvest と Wio LTE ならとってもかんたん!〜

2018年4月26日に SORACOM Technology Camp が開催されました。⛺

ご来場いただいた方はお気づきになられたかもしれませんが、社員が SORACOM のサービスを使って作ったデモが並んでいるコーナーがあったと思います。

これは、「SORACOM を使うとこんなに簡単に IoT のシステムを組めるよ!」という例を見ていただくため、また、お客様はエンジニアの方が中心ということで、「自分も何か作ってみたい」という気持ちを少しでも刺激することができたら、そんな思いで用意したデモでした。

マーケティングチームからエンジニアチームに「デモを作って」という依頼が舞い込んだのが 4 月 9 日でした。

それを受けて、私は以前から個人的にやってみたいと思っていながらも時間がないことを理由にやらずにいた、CO2二酸化炭素)濃度の測定と見える化をしてみることにしました。

なぜ CO2 濃度を測ってみたくなったの?

以下のような記事をたまたま同時期にいくつか読んで、CO2 の濃度が生産性に大きな影響を与えるということを知ったので、まずは自宅の仕事部屋の CO2 濃度が気になったからです。

mirai.doda.jp

また、ソラコムは会議自体が少ない方なのですが、たまに大人数で会議をしていると時間が経つに連れてだんだん生産性が低下していっている気がしていました。(これはソラコムに限ったことではなく、過去の勤め先などでの会議でとても身に覚えのあることです)

これまでは、単に疲れて集中力が途切れるせいかと思っていましたが、実は会議スペースの CO2 濃度にも何か因果関係があるのではないだろうか、もしあるとしたら会議のときに CO2 濃度を測定しておいて濃度が高まってきたら早めに換気を促すなどの対策を取れば、多少は会議の生産性の低下に抵抗しうるのではないか、そんな思いが生じたからです。

なぜ買ってこないで作るの?

作りたかったからです。エンジニアの端くれなので、仕方ないですよねぇ。

というのは半分冗談として。

たとえば市販品で以下のような物があります。

探せばもっと安いものもあるかもしれませんが、1万円超えというのはちょっと試しに買ってみるには高い感じがします。

一方、ふと気づくと自分の手元にはすでに Wio LTE がある・・・(こちらも1万円超え*1ですが、これはいろいろな目的に使えるので、と自分に言い訳)

Wio LTE JP Version

Wio LTE JP Version

Wio LTE の Grove コネクタに接続できる CO2 センサーを買ってきてつなぎ、あとは SORACOM Harvest にデータを投げればサクッと目的が達成できるのではないか、なぜもっと早くやらなかったんだろうと後悔しました。

早速 Grove システム対応の CO2 センサーを検索して見つけたのがこちら:

www.seeedstudio.com

あれあれ?

・・・思ったより・・・高いぞ・・・😨

そんなことを社内の Slack でつぶやいた時に教えてもらったのがこのあたりの記事:

MH-Z19 という格安 CO2 センサを読んでみた | tech - 氾濫原

ArduinoでCO2濃度を計測する(MH-Z19) - はらぺこらいおん

ふむふむ。MH-Z19 というセンサーを Aliexpress で購入すればよいのか。

Aliexpress で探してみると、20 ドルくらいで買えそうなものを見つけました。 (ただし型番は MH-Z19 B と いうもの)

用意したもの

接続

まず MH-Z19B を動作させるための配線を行いましょう。

VIN には 5V を供給する必要があります。

しかし、ここでいきなりつまづきます。Wio LTE から Grove コネクタ経由で供給される電圧は 3.3V なのです。

仕方ないから昇圧させるか・・・とも思いましたが、ふと USB バスパワー(VBUS)があることに気づきました。

Wio LTE回路図 を見てみると、VBUS が TP として出ていそうです。

f:id:bearmini:20180427131058p:plain

TP13 は、Wio LTE の基板のこのあたりのようです:

f:id:bearmini:20180427162710p:plain

(マイクロ USB コネクタのすぐ裏の、4つ並んでいる金属の丸い端子のうち、上の写真の状態では一番下のもの)

ここからセンサーに電源を供給できるかなと思ってジャンパー線をはんだ付けしようとしたのですが、写真をご覧いただいておわかりかもしれませんが私の技量ではうまく行かず断念しました。

それで結局どうしたかというと、もう一本別の USB ケーブルを用意して、VBUS だけを直接取り出すことにしました。

ということで、「不要な USB ケーブル」の出番です。電源供給元に挿すのは USB A のオスの方なので、そうでない方のコネクタを切り落とします。

次に、切り落とした部分から1cmくらいをワイヤーストリッパーで外側の被覆を剥くと、中から線が4本出て来るはずです。 どの線が VBUS かをテスターなどで調べましょう。 私が剥いたケーブルはちゃんと VBUS が赤、GND が黒だったので分かりやすかったです。 ネットでは、安物のケーブルでは赤黒が逆だったケースなどが報告されているので気をつけましょう。

VBUS / GND それぞれのケーブルもさらに数mm分の被覆を剥いて、中の金属線をむき出しにします。 私の使ったケーブルは撚り線になっていたので、ブレッドボードに挿しやすいようにジャンパー線にはんだ付けしました。 VBUS と GND がショートすると危険なので必ず絶縁しておきましょう。

私の場合はこのようなケーブルが出来上がりました:

f:id:bearmini:20180427162832p:plain

(熱収縮チューブの径が合ってないのが残念なところ・・・)

この USB ケーブルと MH-Z19B を、ブレッドボードを使って接続します。

f:id:bearmini:20180427163510p:plain

次に Wio LTE と MH-Z19B を接続します。

f:id:bearmini:20180427164348p:plain

Wio LTE 側のコネクタは D38 と書かれたコネクタを使います。(D38 はスケッチで明示的に ON にしなくてもデフォルトで ON になるそうです)

Grove ケーブルの黄色い線を MH-Z19B の PWM に接続します。 Grove ケーブルの黒い線を GND につなぎます。 赤の線と白の線は使いません。

あとは Wio LTE にスケッチを書き込んで、SORACOM Harvest へデータを投げ込めばデモの完成です。 スケッチ例はこの記事の末尾に載せておきます。 やっていることは単純で、センサーから出力される PWM の ON の時間と OFF の時間(トータルで約1秒)を読み取って、比率に応じて ppm の値に換算、それを1分間の平均値を取って毎分 Harvest に UDP で送信するのみです。

なお今回のデモでは、このような 2 又の USB ケーブル を使って、1つのモバイルバッテリーから Wio LTE と MH-Z19B へ給電しました。

f:id:bearmini:20180427165129p:plain

当日の展示の様子

f:id:bearmini:20180427165301p:plain

まとめ

会場に設置当初は 450ppm 程度で推移していました。 息を吹きかけるとグラフは急峻な立ち上がりを見せ、しばらくすると元の水準にもどっていました。 セッションの合間の人が多い時間帯は少し ppm 値が高くなり、セッションが始まって人が少なくなると ppm 値も下がっていくという挙動を観測することができました。

今度は社員が集まるミーティングなどで実際に稼働させてみたいと思います。

また、他のエンジニアのみなさんはそれぞれ Wio LTE をケースに入れていたので私も何かのケースに収めてみたいなと思いました。

SORACOM Harvest は本当に簡単で、設定することと言えば SIM のグループで Harvest を ON にするだけです。 それで雑にデータを投げ込むとグラフにしてくれたりするという超絶スグレモノです。サーバーサイドの設定が不要で、これはもうチートと言っても過言ではないくらいプロトタイピングが加速されます。 デバイス側の工夫などに、より時間を使うこともできるようになりますよね。

しかも!SORACOM Harvest で蓄積したデータがあると、SORACOM Kaleidoscope を楽しむこともできるのです! これはまさに一石二鳥!

結論: SORACOM Harvest 最強すぎる

スケッチ

#include <WioLTEforArduino.h>
#include <stdio.h>

#define ARRAY_LENGTH(a) (sizeof(a) / sizeof(a[0]))

#define PWM_PIN    (WIOLTE_D38)

WioLTE Wio;

typedef struct hist {
  int v;
  unsigned long t;
} pwm_history;

pwm_history h[3];

float ppm_history[60];

unsigned long last_report_timestamp = 0;


void setup() {
  delay(200);
  Wio.LedSetRGB(16, 0, 0);

  SerialUSB.println("");
  SerialUSB.println("--- START ---------------------------------------------------");

  SerialUSB.println("### I/O Initialize.");
  Wio.Init();

  SerialUSB.println("### Power supply ON.");
  Wio.PowerSupplyLTE(true);
  delay(5000);

  Wio.LedSetRGB(16, 16, 0);

  SerialUSB.println("### Turn on or reset.");
  if (!Wio.TurnOnOrReset()) {
    SerialUSB.println("### ERROR! ###");
    return;
  }

  Wio.LedSetRGB(4, 4, 16);

  SerialUSB.println("### Connecting to \"soracom.io\".");
  delay(5000);
  if (!Wio.Activate("soracom.io", "sora", "sora")) {
    SerialUSB.println("### ERROR! ###");
    return;
  }

  Wio.LedSetRGB(0, 16, 0);

  delay(5000);

  Wio.LedSetRGB(16, 16, 16);

  pinMode(PWM_PIN, INPUT);
}

void loop() {
  int v = digitalRead(PWM_PIN);
  unsigned long t = micros();

  if (v != h[2].v) {
    pwm_push_history(v, t);
  }


  //SerialUSB.println("h[0]{ v = " + String(h[0].v) + "}, t = " + String(h[0].t) + ", h[1]{ v = " + String(h[1].v) + ", t = " + String(h[1].t) + "}, h[2]{ v = " + String(h[2].v) + ", t = " + String(h[2].t) + "}");

  if (pwm_one_cycle_completed()) {
    unsigned long th = h[2].t - h[1].t;
    unsigned long tl = h[1].t - h[0].t;
    pwm_init_history();

    float ppm = 5000 * (th - 0.002) / (th + tl - 0.004);
    ppm_push_history(ppm);

    SerialUSB.println("th = " + String(th) + ", tl = " + String(tl) + ", PPM = " + String(ppm));
  }  
}

void pwm_init_history() {
  for (int i = 0; i < ARRAY_LENGTH(h); i++) {
    h[i].v = HIGH;
    h[i].t = 0;
  }
}

bool pwm_one_cycle_completed() {
  return h[0].v == LOW && h[1].v == HIGH && h[2].v == LOW;
}

void pwm_push_history(int v, unsigned long t) {
  for (int i = 0; i < ARRAY_LENGTH(h) - 1; i++) {
    h[i].v = h[i+1].v;
    h[i].t = h[i+1].t;
  }
  h[ARRAY_LENGTH(h) - 1].v = v;
  h[ARRAY_LENGTH(h) - 1].t = t;
}

void ppm_push_history(float ppm) {
  for (int i = 0; i < ARRAY_LENGTH(ppm_history) - 1; i++) {
    ppm_history[i] = ppm_history[i + 1];
  }
  ppm_history[ARRAY_LENGTH(ppm_history) - 1] = ppm;

  unsigned long now = millis();
  if (last_report_timestamp + 60 * 1000 < now) {
    float avg = get_average_ppm();
    post_to_harvest(avg);
    last_report_timestamp = now;
  }
}

void post_to_harvest(float ppm) {
  SerialUSB.println("### Open.");
  int connectId = Wio.SocketOpen("harvest.soracom.io", 8514, WIOLTE_UDP);
  if (connectId < 0) {
    SerialUSB.println("### ERROR! ###");
    goto err;
  }

  SerialUSB.println("### Send.");
  if (!Wio.SocketSend(connectId, String(ppm).c_str())) {
    SerialUSB.println("### ERROR! ###");
    goto err;
  }

  SerialUSB.println("### Receive.");
  int length;
  char data[100];
  do {
    length = Wio.SocketReceive(connectId, data, sizeof (data));
    if (length < 0) {
      SerialUSB.println("### ERROR! ###");
      goto err;
    }
  } while (length == 0);
  SerialUSB.print("Receive:");
  SerialUSB.print(data);
  SerialUSB.println("");

  SerialUSB.println("### Close.");
  if (!Wio.SocketClose(connectId)) {
    SerialUSB.println("### ERROR! ###");
    goto err;
  }

err:
  return;
}

float get_average_ppm() {
  float sum = 0;
  for (int i = 0; i < ARRAY_LENGTH(ppm_history); i++) {
    sum += ppm_history[i];
  }
  return sum / ARRAY_LENGTH(ppm_history);
}

*1:SORACOM User Console では定価は 9,800 円ですが、税や送料を考えると1万円を超えます

SORACOM を使って学ぶ AT コマンド - 3. 実践編

準備編では ATI コマンドを実行してみました。

モデムを使ってダイヤルアップ接続をしてデータ通信を行うことができるようになるには、いくつかの AT コマンドを組み合わせて実行していく必要があります。

とはいっても、どこから手を付けたらいいかわからないので、まずは SORACOM が公開している、SORACOM Air でインターネットに接続するためのスクリプトを読み解いてみましょう。

SORACOM が実施しているハンズオンセミナーの資料が GitHub にて公開されています。 https://github.com/soracom/handson/blob/master/setup/setup.md#3-0

こちらの「接続スクリプトのダウンロード」というところからリンクされているスクリプトを見てみましょう。

途中に

Init1 = ATZ
Init2 = ATQ0 V1 E1 S0=0 &C1 &D2 +FCLASS=0
Init3 = AT+CGDCONT=1,"IP","$2"

というような部分が見つかると思います。

これは、wvdial の設定ファイルの内容ですが、ダイヤルアップ接続時にはここに記載されている AT コマンドを一つずつ実行していっているようなイメージとなります。

以下、各コマンドの説明をしていきますが、皆様も是非実際に手を動かしてコマンドを実行してみてください。

ATZ コマンド

では、まず最初の ATZ コマンドは何のためのコマンドでしょうか。 ITU-T Rec. V.250 6.1.1 によると、モデムの設定をデフォルト状態に戻すためのもののようです。

つまり、モデムをいったんまっさらな状態にしているわけですね。 もしモデムを使って接続中(データ通信中)に ATZ コマンドを実行するとその接続は切断されるようです。

ATQ コマンド

つづいては ATQ0 V1 E1 S0=0 &C1 &D2 +FCLASS=0 という行を見ていきます。 長いコマンドに見えますが、実はこれは半角スペースごとに別の AT コマンドが続いています。 つまり、ATQ0 V1 E1 S0=0 &C1 &D2 +FCLASS=0

ATQ0
ATV1
ATE1
ATS0=0
AT&C1
AT&D2
AT+FCLASS=0

という複数のコマンドを連続で実行したのと同じことになります。

ちなみに、基本コマンドは空白で区切らずに ATQ0V1E1S0=0&C1&D2 という感じにつなげて実行することもできます。

まずは最初の ATQ0 を見ていきましょう。 これは ITU-T Rec. V.250 6.2.5 によると、モデムが返す結果コード(OK とか ERROR とか)を抑制するかしないかを指定するコマンドです。

ATQ0 は抑制しない(結果コードが表示される)ようにします。 抑制したい場合は ATQ1 を実行します。

もし工場出荷時のデフォルトで結果コードを抑制する設定になっているようなモデムがあったとすると、コマンドの実行が成功しても失敗しても結果コード(OKERROR など)が何も表示されなくなってしまって問題の切り分け等が難しくなってしまうので、ATQ0 で確実に結果コードが表示されるようにしているのだと思います。

ATV コマンド

続いて V1 の部分です。 これは、ITU-T Rec. V.250 によると、モデムからのレスポンスのフォーマットを選択します。

ATV0 および ATV1 が実行可能です。

それぞれの意味は、コマンドの実行例を見てもらったほうが早いと思います。

まず ATV0 を実行してから ATI を実行した場合は、以下のような結果になります。

Manufacturer: huawei
Model: MS2131
Revision: 21.751.19.00.00
IMEI: 86610xxxxxxxxxx
+GCAP: +CGSM,+DS,+ES
0

ATV1 を実行してから ATI を実行すると、以下のようになります。

ATV1
OK
ATI
Manufacturer: huawei
Model: MS2131
Revision: 21.751.19.00.00
IMEI: 866109028769760
+GCAP: +CGSM,+DS,+ES

OK

ATV0 のときと ATV1 のときの違いは以下のような点です。

  • ATV0 のときはコマンドを入力した行に上書きされるような形で結果が表示される。ATV1 のときはコマンドを入力した次の行から結果が表示される
  • ATV0 のときは ATI コマンドの最後の結果コードが数値(0OK と同じ意味)。ATV1 のときは OK と文字列で表示される
  • ATV0 のときは結果の数値の前に空行が入らない。ATV1 のときは結果コードの前に1行空行がある。

通常は ATV1 を設定しておいたほうが(人間にとって)読みやすくて良いでしょう。

ATE コマンド

つづいては E1 です。

これは ITU-T Rec. V.250 6.2.4 によると、モデムに送った AT コマンドの文字列をモデム側からエコーバックするかどうかを決めるコマンドです。

ATE0 ではエコーバックしない、ATE1 ではエコーバックする設定です。

screen で AT コマンドを試してみる際にはエコーバックがないと辛いので ATE1 を設定しておいたほうが良いでしょう。

ATS0 コマンド

つづいては S0=0 です。

ITU-T Rec. V.250 6.3.8 によりますと、これは電話がかかってきた際に何回の呼び出し音の後に自動応答をするかを決めるコマンドです。

ATS0=0 は自動応答しない設定です。

AT&C コマンド

続いては &C1 です。

ITU-T Rec. V.250 6.2.8 によると、"Circuit 109" の挙動を設定します。

"Circuit 109" とは、接続先からのキャリア信号を受信したこと(≒接続先に接続できたこと)を、DCE(≒モデム)が DTE(≒ユーザープログラム等)に伝えるための信号線の名前です。"Received Line Signal Detector" とか "DCD" と呼ばれたりもします。

AT&C0 では、接続できているかどうかに関わらず Circuit 109 が "ON" 状態になります。 AT&C1 では、接続状態に応じて Circuit 109 の状態が変化します。

通常は &C1 に設定しておきます。

AT&D コマンド

続いては &D2 です。

ITU-T Rec. V.250 6.2.9 によりますと、"Circuit 108/2" が "ON" から "OFF" になったときのモデムの挙動を設定します。

"Circuit 108/2" とは、DTE(≒ユーザープログラム等)がデータ送受信の準備が整っているかどうかを DCE(≒モデム)に伝えるための信号線の名前です。"Data terminal ready" とか "DTR" と呼ばれたりもします。

つまり、"Circuit 108/2" が "ON" から "OFF" になったということは、ユーザープログラムが終了するなどしてデータ送受信できなくなったことを示し、その際にモデムがどのように振る舞えばよいかを設定することになります。

これには設定可能な値が 3 つあります。

AT&D0 の場合は、モデムは "Circuit 108/2" の変化を無視します。(つまりデータ送受信を継続しようとします) AT&D1 の場合は、モデムは通話は継続したままコマンドモードに戻ります。 AT&D2 の場合は、モデムは通話を終了します。未送信のデータが残っている場合の挙動は +ETBM で設定されたパラメータにしたがって処理されます(モデムが対応している場合)

意図せずプログラムが終了してしまった場合などに接続が継続されてしまうと思わぬ課金が発生したりしてしまうかもしれませんので、ここでは &D2 に設定しているのだと思われます。

AT+FCLASS コマンド

続いては +FCLASS=0 です。

これは入門編でもご紹介したように + で始まっているので拡張コマンドになります。 そして +F なので FAX 関連のコマンドということになりそうです。

ITU-T Rec. V.250 の Supplement 1 (ITU-T Rec. V.250 をダウンロードしたのと同じページからダウンロードできます)を見ると、+FCLASSITU-T Rec. T.31 の 8.2.1 などで規定されているようです。

ということで ITU-T Rec. T.31 をダウンロードして確認してみましょう。

これは "サービスクラス" というものを選択するコマンドのようです。

FAX の送受信は行わず、データ通信のみの場合は +FCLASS=0 を指定するようです。

なお、Huawei MS2131i-8 と L-05A は +FCLASS コマンドをサポートしていませんでした。 (実行すると結果コードが ERROR になります。)

+FCLASS コマンドをサポートしているかどうかは +GCAP コマンドを実行するとわかります。

AT+GCAP
+GCAP: +CGSM,+DS,+ES

OK

ここに +FCLASS が含まれていないので、このモデムは +FCLASS コマンドをサポートしていないようです。

AT+CGDCONT コマンド

さて、長かった ATQ0 V1 E1 S0=0 &C1 &D2 +FCLASS=0 の解説がやっと終わりました。

次は AT+CGDCONT=1,"IP","$2" です。

$2 の部分は、これはシェルスクリプトの関数に渡されてくる引数で、実際には "soracom.io" という文字列になっています。 ですので、このコマンドは実行されるときには AT+CGDCONT=1,"IP","soracom.io" というコマンドとして実行されます。

これも AT の直後が + なので拡張コマンドです。+C なので携帯電話回線の制御のコマンドということがわかります。

ITU-T Rec. V250 Supplement 1 を見ると、+CGDCONT は GSM07.60 という標準で定められているようです。

GSM 07.60 は一般公開はされていないようですが、3GPP TS 27.060 が GSM 07.60 と同じものなので 3GPP のほうを見ましょう。

27.060 の 12.1 には AT コマンドの概要が書かれていますが、詳細なリファレンスは 3GPP TS 27.007 に記述されている旨が記されています。

ということで今度は 3GPP TS 27.007 を参照しますと 10.1.1 に +CGDCONT の定義がありました。

これは "PDP コンテキスト" と呼ばれるものを設定するコマンドです。"PDP" は Packet Data Protocol の略で、要はデータ通信のことです。PDP をするために必要な情報を設定します。

ここでは 1, "IP", "soracom.io" という3つのパラメータを設定しています。 最初の 1 は、コンテキストの ID で、1からいくつかの数が使えます。PDP コンテキストを保存しておくスロットの番号のようなものです。あとでこの設定を参照するためにこの 1 という値を使います。

"IP"PDP タイプというもので、 SORACOM の場合は今のところ "IP" を固定で指定しておけば良いです。

"soracom.io" は APN 名です。

ここまで実行したら、設定が完了しました。

ATD コマンド

設定が完了したらダイヤルアップします。 ダイヤルアップするには ATD コマンドを使用します。

ITU-T Rec. V.250 の 6.3.1 に ATD コマンドの基本的な定義があり、3GPP TS 27.007 の 6.2 には D コマンドが携帯電話回線で使われるときの追加仕様や変更点について少し書かれています。

SORACOM SIM でダイヤルアップするには ATD*99***1# というコマンドを実行します。

これは *99***1# という番号にダイヤルしているのですが、この番号の末尾の # の一つ前にある 1 は、先ほどの +CGDCONT で指定したコンテキスト ID の 1 です。+CGDCONT で異なるコンテキスト ID で複数の設定を保存している場合、ここのダイヤル先番号を変えてどのコンテキストを使うかを選択します。

ATD*99***1# を実行すると CONNECT 21600000 のような応答が返ってくると思います。(モデムの機種によっては数字が表示されたりされなかったりするかもしれません)

この状態で、ダイヤルアップ接続をしてソラコムの認証サーバーとの通信が始まっています。

本当であればここでプロトコルに則ってユーザー名・パスワード(いずれも "sora")を送信したり、IP アドレス情報や DNS サーバー情報を受信したりしてセッション開始となるのですが、screen コマンドで手作業で AT コマンドを実行している今の状態ではそれらのプロトコルを処理することがさすがにできません。

Linux では wvdial コマンドがそのあたりをやってくれて、ppp0 インターフェースを作成してくれたりしてデータ通信ができるようになります。

自作のプログラム等であれば、これまで見てきたような AT コマンドを使って接続したあとにここで認証処理を実施すればデータ通信が行えるようになるはずです。

なお、この状態では screen の画面上で何かキーを打ったりしても反応が無くなります。抜け出すには screen ごと終了する必要があります。

機種依存コマンド

機種によっては、標準で定められていない独自拡張コマンドを使うことで、AT コマンドだけでデータ通信を行うことができるようになるものもあります。

たとえば SORACOM の販売している Quectel 社製の 3G 通信モジュール UC20-G や EC21-J には AT+Q で始まる独自コマンドを使うことによって AT コマンドのみでデータ通信ができるようになっています。 (以前の記事でちらっと解説しています)

そのような機種依存のコマンドについては、メーカーが AT コマンドのリファレンスを提供していると思いますのでそれを参照するのが良いでしょう。

AT+C系の興味深いコマンド

ここからはデータ通信ではないのですが、AT コマンドを使って面白いことができるということをご紹介していきたいと思います。

以下の情報は、機種によってはサポートしていなかったりしますが、使えると有益な情報が得られたりして非常に便利です。

AT+CGREG

これは PS (Packet Switched) ドメイン、すなわちデータ通信用ネットワークへの登録状況を取得するコマンドです。

SIM がモデムに入っている状態で AT+CGREG を実行すると、デフォルトのままだとおそらく以下のような結果が得られるのではないかと思います。

AT+CGREG?
+CGREG: 0,1

OK

という感じの結果が返ってきます。 これの何が面白いのかと思われるかもしれませんが、以下のようにモードを変更してから再度実行すると今度は何やら面白そうな値が取得できます。

AT+CGREG=2
OK
AT+CGREG?
+CGREG: 2,1,"00B0","04AF4173"

OK

このレスポンスを解説しましょう。

まず、AT+CGREG=2 では、この後の AT+CREG? コマンドのモードを変更しています。モードを 2 にすると、AT+CREG? コマンドは GPRS ネットワークへの登録状況の他に、位置情報(基地局の ID やエリアの ID)を返すようになります。

上記で得られたのは "00B0" がエリアコード lac: location area code"04AF4173"基地局 ID(cid: cell id) です。

この情報を、地図上の座標に変換してくれるサービスがあります。 GoogleFirefox なども API を提供しているようですが、ここでは OpenCellID.org というサイトを使ってみましょう。

www.opencellid.org

サインアップは簡単です。名前、メールアドレスを入力し、用途を選択して Submit したら、Token というパスワードのようなものが送られてきますのでそれを使ってサイトにログインします。

ログインしたら、以下のように MCC = 440, MNC = 10, LAC, CID を入力します。 MCC = 440 は日本を表す Mobile Country Code です。 MNC = 10 は日本の中で NTT ドコモを表す Mobile Network Code です。 SORACOM の SIM は NTT ドコモの電波を掴むのでこれらの値を設定します。 LAC と CID は先ほどの AT+CGREG? コマンドで得られた16進数の値を10進数に変換したものです。 (10進数への変換は、MacLinux の場合は printf %d "0x04AF4173" というように printf コマンドを使うと簡単でしょう)

f:id:bearmini:20171203092019p:plain

このときはソラコムの二子玉川オフィスで基地局情報を取得しましたが、オフィスの近くの場所が示されていることがわかると思います。

GPS を搭載していないデバイスでも、基地局の位置情報を使うことで、「だいたいどのあたりにいるか」という感じの大まかな位置情報が利用可能となります。

なお、デバイスや携帯電話会社によってはこの情報を取得できない場合もあるようです。

それから、SORACOM のグローバル SIM を使ってローミングしている場合にも、接続先の国やオペレーター(電話会社)によって取得できたりできなかったりすることもあるようですので、あくまでも「利用できたらラッキー」という位の気持ちで使っていただくと良いでしょう。

AT+CGREG によく似たコマンドで AT+CREGC の後ろに G が入っていない)というのもありますが、こちらは CS (Circuit Switched) ドメイン、いわゆる音声通話や SMS 送受信用のネットワークへの登録状況を取得します。 SIM の種類がデータ通信専用のものだと PS ドメインにしか接続しないので、SORACOM の SIM の場合はとりあえず C の後ろに G が入っている方の、 AT+CGREG を実行しておくと良いでしょう。SMS つきの SIM を使っている場合、 AT+CREG コマンドでも登録状況が取得できるはずです。 逆に他社の SIM でデータ通信をサポートしていない(音声通話のみの)SIM を入れると AT+CREG のみ結果が得られるはずです。

ちなみに、拡張コマンドの多くは AT+XXXX? で現在の値の取得、AT+XXXX=Y で値の設定、AT+XXXX=? で設定可能な値のリストの取得、というような書式になっているものが多いようです。

AT+CSQ コマンド

こちらは、電波の強度(Signal Quality)を取得するコマンドです。 実行してみると、以下のような結果が得られるはずです。

AT+CSQ
+CSQ: 17,99

OK

1799 の2つの数が見えます。

1つめの数は RSSI (Received Signal Strength Indicator) です。 RSSI の値の範囲は 0 〜 31 です。 RSSI の値が 0 の場合は -113 dBm かそれ以下、RSSI の値が 31 の場合は -51 dBm かそれ以上、RSSI の値が 1 から 30 の間の場合は、その値を n とすると、-113 + (n * 2) [dBm] となります。今回の場合 17 なので、-79 dBm という事になります。

2つめの数は BER (Bit Error Rate) です。 BER の値が 0〜7 のときは 3GPP TS 45.008 の 8.2.4 にある表にしたがって導出されるエラーレートであることがわかります。 BER の値が 99 の場合は、ビットエラーが起きたかどうか不明な場合か検出できなかった場合です。

おわりに

入門編準備編、そしてこの実践編と3記事に渡って AT コマンドをいろいろ見てきましたが、いかがでしたでしょうか。 少しでも AT コマンドへの理解が深まったとしたら幸いです。 ここまでの 3 つの記事の内容を理解していれば、大抵の AT コマンドは、ITU-T3GPP、そしてデバイス製造元の発行しているドキュメントを参照しつつ、理解したり使いこなしたりできるようになっているはずです。

今絶賛流行中の Wio LTE も、Quectel 社の EC21-J というモジュールを搭載していてそのモジュールに対して AT コマンドを実行したりできますので、ぜひ試してみてください。

本当は SMS の送受信とかも AT コマンドでできるのでそこまでご紹介したかったのですが、(そろそろ力尽きて来たので)それはまたの機会にとっておきたいと思います。

ちなみに SMS 関連の AT コマンドは 3GPP TS 27.005 に定義されていますので、興味のある読者はぜひ参照してみてください。

SORACOM を使って学ぶ AT コマンド - 2. 準備編

前回の記事では、AT コマンドの概要を学びました。

今回は、AT コマンドを実際に実行したりしていろいろ試してみるために、準備を行っていきます。

以下のものを用意しましょう。

  • USB タイプのモデム(Huawei MS2131i-8 もしくは Abit AK-020)
  • SIM カード

モデム Huawei MS2131i-8 は SORACOM のユーザーコンソールでカバレッジタイプをグローバルに変更してからメニューの「発注」ページで購入します。 f:id:bearmini:20171201192750p:plain

こちらの製品は価格が 50 米ドル、送料込で日本円に換算して約7,000〜8,000円程度です(いずれも2017年12月現在)

Abit AK-020 は同じく SORACOM のユーザーコンソールで購入できますが、こちらはカバレッジタイプを「日本」にしてから、メニューの「発注」ページで購入します。

もし SORACOM の SIM カードをまだ持っていない場合は、こちらの AK-020 に SIM カードとクーポン(従量課金のデータ通信量部分に適用可能)がセットになったスターターキットを購入されるとよいかもしれません。

3G USBドングル AK-020 SORACOMスターターキット

3G USBドングル AK-020 SORACOMスターターキット

もし、他の USB 接続タイプのモデムをすでに持っていらっしゃる場合はそちらを使用しても大丈夫かもしれませんが、この記事のこの後のコマンドの実行などの際に文字列を読み替えていただいたりする必要が出てくるかもしれません。

モデムを入手したら、お手元のコンピューター用のデバイスドライバーをダウンロードしてインストールしてください。

Huawei MS2131i-8 の場合はこちらのブログ記事が参考になるかと思います。 https://blog.soracom.jp/blog/2017/11/08/huawei-ms2131i-8/

AK-020 の場合は取扱説明書がついてきますのでその通りにインストールを行います。(Mac の場合は Web サイトから最新版をダウンロードして使う必要があります)

また、コンピューターの USB ポートにモデムを挿して、モデムに付属のダイヤルアップ用アプリケーションなどで正常に接続でき、データ通信ができることも確認しておきましょう。

ここまで確認できたら、AT コマンドを実行できるようにしていきましょう。

macOS の場合

私は macOS 10.12 (Sierra) で動作確認を行いました。

まず、ターミナルを起動して以下のコマンドを実行してみてください。

Huawei MS2131i-8 を使っている場合:

ls /dev/tty.HUAWEIMobile-*

Abit AK-020 を使っている場合:

ls /dev/tty.USB*

注意: 上記以外のモデムを使用している場合や、OS のバージョンが異なる場合はデバイスファイル名が変わるかもしれません。/dev 以下でそれらしい名前のデバイスファイルを探してみてください。 (私が所有している L-05A というモデムの場合は /dev/tty.usbmodem* という感じのデバイスファイル名でした。)

実行すると以下のような 3 つのデバイスファイルが見つかるはずです。

MS2131i-8 の場合:

/dev/tty.HUAWEIMobile-Diag
/dev/tty.HUAWEIMobile-Modem
/dev/tty.HUAWEIMobile-Pcui

AK-020 の場合:

/dev/tty.USB Application Port_
/dev/tty.USB Modem Port_
/dev/tty.Speech Port_

次に、以下のコマンドを実行してみてください。

MS2131i-8 の場合:

screen /dev/tty.HUAWEIMobile-Modem

AK-020 の場合:

screen /dev/tty.USB\ Modem\ Port_

これでモデムに接続できて、ターミナルの画面が真っ黒になった(プロンプトなどの表示が消えた)はずです。 (なお L-05A の場合は screen /dev/tty.usbmodem1411 で接続できました)

接続できたらAT と入力し、Enter を押してみてください。

AT
OK

というように OK というレスポンスが返ってきたら成功です。

つぎに、ATI と入力して Enter を押してみてください。

MS2131i-8 の場合は以下のようなレスポンスがありました。

ATI
Manufacturer: huawei
Model: MS2131
Revision: 21.751.19.00.00
IMEI: 86610xxxxxxxxxx
+GCAP: +CGSM,+DS,+ES

OK

(IMEI はハードウェアの固有の ID ですので念のためマスクしてあります。)

AK-020 の場合は以下のようなレスポンスがありました。

ATI
MTK2
AK-020_V05_20160517

OK

もし反応がない場合は、デバイスファイル名を、さきほど ls コマンドで見つけた他のものに変えて実行してみてください。

なお、screen コマンドを終了するには Ctrl-a k と入力します。(Ctrl-a を押した後、Ctrl キーを離して k を押します。なお、環境によってはコマンドを起動するためのキーの割り当てがデフォルトの Ctrl-a から変更されている場合もあります。その場合はそちらを使ってください)

ちなみに、この ATII という AT コマンドを実行したことになります。

I コマンドは ITU-T Rec. V.250 の 6.1.3 Request identification information に詳しく定義されていますが、モデムを識別する情報を表示するコマンドです。 メーカーや製品のモデル等によってどのような内容が表示されるかが変わります。

モデムによっては、ATI0 といったように、数値を続けて指定することができます。 Huawei MS2131i-8 も AK-020 もともに数値を指定してもレスポンスが変わりませんでしたが、メーカーや機種によっては指定した数値によってレスポンスの内容が変わる場合があるようです。

最後の OK は AT コマンド共通の結果コードで、コマンドの実行が成功したことを示します。 何らかのエラーが起こった場合は ERROR が返ります。

ここまでできたら準備完了です。

次の記事では、いよいよ実践的な AT コマンドを見ていきましょう。

Windows の場合

T.B.D.

Linux の場合

T.B.D.

SORACOM を使って学ぶ AT コマンド - 1.入門編

この記事は SORACOM Advent Calendar 2017 4日目の記事です。

SORACOM の SIM を使っていると、「AT(エーティー)コマンド」というものを取り扱わないといけない場面にたまに出会いますよね。

私は出会います。

AT+CGDCONT=1,"IP","soracom.io"

とかそんな感じのやつです。

この「AT コマンド」が「モデムを制御したり情報を取得したりするためのもの」というのはなんとなく知っているけれど、基本的にはネット上で見つけたのをあまり意味もわからずコピペして使ってたり、コマンドの意味を調べようと思ってもなかなかまとまった情報が見当たらなかったり、そもそも体系的に勉強しようとしてもどこから手を付けたらいいかわからない、、、というような感じの人が多いのではないでしょうか。

私はそうでした。

日本語で書かれた良い入門記事が Web 上に見当たらなかったので、「ないものは作ってしまえ」メソッドで、ちょっと簡単にまとめてみたいと思います。

ツッコミ大歓迎です。

概要

AT コマンドの起源は 1981 年にヘイズ コミュニケーションズ (Hayes Communications) 社の固定電話回線用のモデムに導入された、電話をかけたり切ったりといった電話回線の制御やモデムの設定変更等に使われたコマンドに端を発するそうです。

コマンドの文字列が、皆さんご存知のように “AT” で始まるので日本語ではよく「AT コマンド」と言うと思いますが、英語では「ヘイズ コマンド セット (Hayes command set)」とも言うようです。(英語版の Wikipedia で「AT commands」と検索すると、https://en.wikipedia.org/wiki/Hayes_command_set に転送されます)

その後、モデムを作っていた各社が Hayes の真似をして独自拡張などを入れながら AT コマンドがモデムの制御のためのデファクトスタンダードとして広まっていったようです。

現在は ITU-T 勧告 (Recommendation) V.250(以下、ITU-T Rec. V.250)にて AT コマンドの標準が定められています。

ITU-T Rec. V.250 では、用語の定義、DTE と DCE の間の物理的な接続方法の取り決め、データレート、AT コマンドのシンタックス、多数の基本的な AT コマンドの仕様などが定められています。

ITU-T Rec. V.250 は ITU のサイトから誰でも無料でダウンロードして読むことができます。 https://www.itu.int/rec/T-REC-V.250-200307-I/en

なお、ITU-T Rec. V.250 では AT コマンドは「AT command set」と呼ばれています。そして AT は attention の先頭の2文字である旨も示されています。

携帯電話回線のためのモデムを制御するための AT コマンドの仕様は、ITU-T Rec. V.250 をベースに、3GPP TS 27.007 および 27.005 で標準が定められています。

27.007 および 27.005 は 3GPP のサイトから誰でも無料でダウンロードして読むことができます。 https://portal.3gpp.org/desktopmodules/Specifications/SpecificationDetails.aspx?specificationId=1515 https://portal.3gpp.org/desktopmodules/Specifications/SpecificationDetails.aspx?specificationId=1514

ITU-T Rec. V.250 は PDF で配布されていますが、3GPP TS 27.007 および 27.005 は Microsoft Word 形式のファイルなので読むために対応ソフトが必要です。

この後、上記 3 つのドキュメントをちょくちょく参照することになると思いますのでダウンロードして手元で見られるようにしておくと良いかもしれません。

AT コマンドのシンタックス

ほとんどの AT コマンドは、AT の 2 文字で始まる文字列です。 AT 以外に、直前のコマンドを繰り返す A/ というコマンドもありますが、それ以外は基本的に AT で始まります。

AT コマンドの文字列を何らかの方法でモデムに送信すると、モデムはそのコマンド文字列を解釈して様々な動作を行います。 どうやってモデムにコマンド文字列を送るかについては、モデムにシリアルポートがある場合はそのシリアルポート経由で送信したり、Unix 系の OS では tty が割り当てられたりしますのでそこに送信する場合もあります。

AT コマンドは通常は大文字の AT で始まる文字列ですが、小文字の at も許されているようです。大文字小文字の混在(aT もしくは At)は ITU-T Rec. V.250 では明示的には許可されていないように見受けられましたが、混在した文字列を送信しても問題なく認識してくれるモデムが多いようです。 とはいえ、AT と大文字で書くのが一般的かと思いますので、この記事でも大文字を使います。

AT の次に、アルファベットもしくは記号が続きます。

たとえば ATD というコマンドは電話をかけるコマンドです。 ATD12345678 という AT コマンドを実行すると、12345678 という番号に電話をかけます。

SORACOM SIM を使ってダイアルアップ接続する際には ATD*99***1# というようなコマンドを実行しますが、これは *99***1# という(ちょっと不思議な)番号に電話をかけているということになります。 電話番号として使える文字は、 0 1 2 3 4 5 6 7 8 9 * # + A B C D のいずれかのようです。

*# は電話機のボタン等でも見たことがありますね。

+ は国際電話をかけるときに先頭につけます。

A B C D は何に使うのかよくわかりません😅

これらの他に , を入れることもできて、, のタイミングでポーズ(一時停止)させることができます。

たとえば ATD123,456 という AT コマンドを実行すると、123 に電話をかけ、しばらく待ってから 456 を入力するようなイメージです。

サポートセンターなどに電話をすると自動音声でメニューが読み上げられて番号を入力させるようなことがあると思いますが、そのような場合に使えるのかもしれません。

, でどのくらいの時間ポーズするかは ATS8 コマンドで設定することができます。

ATD は番号の末尾に ; で区切って電話に応答するための A コマンドをつなげることができます。(例) ATD123456789;A これは、ダイヤルアップしてからコールバックしてくるようなシステムで用いるようです。

だいぶ話がそれてしまいました。

ATDATS8 のように、AT のすぐあとにアルファベットがくるコマンドは「基本コマンド」といって、以下のような種類があります。

コマンド 意味
A かかってきた電話に出る
D 電話をかける
E 実行したコマンドをエコーバックするかどうかの設定
H Hook の制御
I 識別情報のリクエス
L モニタースピーカーの音量設定
M モニタースピーカーのモード設定
O オンラインデータ状態に戻る
P パルスダイヤリングを選択
Q 結果コードの抑制
S0 自動応答の設定
S10 自動切断ディレイ
S3 コマンド行終端文字
S4 レスポンスフォーマッティング文字
S5 コマンド行編集文字の設定
S6 ダイヤルトーン検出が無効の場合にダイヤル開始するまでの待ち時間の設定
S7 接続完了までのタイムアウト設定
S8 ダイヤル時に , でポーズする時間の設定
T トーンダイヤリングを選択
V DCE レスポンスフォーマット
X 結果コード選択およびコール進捗モニタリング制御
Z デフォルトコンフィグレーションへのリセット

他にも、AT のすぐあとに & が来る基本コマンドもいくつかあります。

コマンド 意味
&C Circuit 109 (Received line signal detector) の挙動
&D Circuit 108 (Data terminal ready) の挙動
&F 工場出荷時設定にリセット

AT のあとに + が来るのは拡張コマンドと呼ばれ、+ の次の文字で大まかなコマンドのグループが特定され、さらに続く文字列でコマンドとなります。

拡張コマンドの先頭文字 意味
+A 呼制御関連
+C デジタル携帯電話拡張
+D データ圧縮 (ITU-T Rec. V.42 bis)
+E エラー制御 (ITU-T Rec. V.42)
+F FAX (ITU-T Rec. T.30 など)
+G 一般的なコマンド(identity や capabilities など)
+I DTE-DCE インターフェース関連 (ITU-T Rec.v.24 など)
+M 変調方式 (ITU-T Rec. V.32 bis など)
+P PCM DCE コマンド (ITU-T Rec. V.92)
+S Switched or Simultaneous Data Types
+T テスト関連
+V 音声通話拡張
+W 無線拡張

たとえば AT+CGDCONT=1,"IP","soracom.io"+C なので携帯電話回線用の拡張コマンドで、PDP Context と呼ばれるものを定義するためのものです。詳しくは続編で解説することになると思います。

このあと AT コマンドを実際に実行したりして学んでいきたいと思いますが、その前に AT コマンドを実行してみることができる環境を準備することが必要です。

次の記事では、AT コマンド実行環境の準備をしていきます。

date コマンドつらい

タイトルどおりですが、date コマンド色々つらいですよね。 同じ名前の date コマンドが、OS や環境によって、似て非なるものであるという辛さがまず第一に挙げられるかと思います。

macOS 上でデフォルトで使える date コマンドは BSD 版の date です。 BSD 版かどうかは man date で確認できます。

DATE(1)                   BSD General Commands Manual                  DATE(1)

NAME
     date -- display or set date and time

   :

BSD という文字が見えますね。

一方、Ubuntu などの Linux 上では GNU coreutils 版の date コマンドが使えます。(Mac にも homebrew などで GNU coreutils をインストールすることもできますが、話がややこしくなるのでここでは一旦おいておきます)

こちらは date --help で確認してみましょう。

   :
Report date bugs to bug-coreutils@gnu.org
GNU coreutils home page: <http://www.gnu.org/software/coreutils/>
General help using GNU software: <http://www.gnu.org/gethelp/>
For complete documentation, run: info coreutils 'date invocation'

最後の方に GNU coreutils という文字列が見えるはずです。

これらの date コマンドのオプションは、共通なものもあればまったく異なるものもあり、時々ハマリます。 (たとえばローカルの Mac で動いてたシェルスクリプトLinux サーバー上で動かない等)

さらに、Linux の中でも Docker 用に最適化された Alpine などでは GNU coreutils 版ではなく BusyBox 版の date がインストールされています。 この BusyBox 版の date は GNU coreutils 版に似たオプションが使えますが、機能が削減されていたりして、これもまた別物と考えたほうがよさそうです。

BusyBox 版の date かどうかは以下のように確認できます。

$ date --help
BusyBox v1.26.2 (2017-02-21 17:41:44 GMT) multi-call binary.

  :

さて、たとえば単純に現在の日時を表示させるだけならどの date を使っていても、単に date と入力すれば良いだけです。

BSD date

$ date
Fri Jun 16 09:41:37 JST 2017

GNU coreutils date

$ date
Fri Jun 16 00:41:48 UTC 2017

BusyBox date

$ date
Fri Jun 16 00:42:05 UTC 2017

表示されるフォーマットも同じですね。

では、次に年月日だけを指定のフォーマットで表示させてみましょう。

BSD date

$ date +"%Y-%m-%d"
2017-06-16

GNU coreutils date

$ date +"%Y-%m-%d"
2017-06-16

BusyBox date

$ date +"%Y-%m-%d"
2017-06-16

これは同じ引数で同じ結果となります。

では、昨日(現在日時の1日前)の日付を表示させてみましょう。

BSD date

$ date -v-1d
Thu Jun 15 09:50:02 JST 2017

GNU coreutils date

$ date -d '1 day ago'
Thu Jun 15 00:53:07 UTC 2017

BusyBox date

$ date -d "1970.01.01-00:00:$(( $( date +%s ) - $(( 24 * 60 * 60 )) ))"
Thu Jun 15 00:54:03 UTC 2017

BSD 版と GNU coreutils 版で、オプション名(-v と -d)も違いますし、そこに指定する値についてもだいぶポリシーが違いそうに見えます。BusyBox 版は発狂しそうですね。

一応 BusyBox 版を解説してみましょう。 BusyBox 版の -d オプションは GNU coreutils 版のような便利な(human friendly な)指定ができず、自力で計算している感じです。 基本的には Unix time で現在時刻から 1 日分の秒数を引き、それをエポックタイムに足して、1970年1月1日0時0分1497574657秒、みたいな時刻を作ってそれを表示させている感じです。(あと、厳密なことを言うと、うるう秒が挿入された日、タイムゾーンによっては夏時間になった日や夏時間が終わった日あたりにこのコマンドを実行すると、タイミングによっては誤った日時を表示してしまうかもしれません)

他にもタイムゾーンの指定とか、フォーマットの変換のときなどにどの date を使っているのかを勘違いすると全然思ったように動かなかったり、複数の環境で動作するようなスクリプトを書こうとしたりするとこのあたりの統一感のなさの辛さを味わってしまう感じです。

  • 日付の計算(x 日後、とか)
  • 日付のフォーマットの変換

などの date コマンドの機能のサブセットに特化した小さなバイナリを Golang あたりで複数の環境向けに作ると幸せになれるのかな、、、などと考えているところです。

Gnu Coreutils: Core Gnu Utilities

Gnu Coreutils: Core Gnu Utilities

Using BusyBox (Digital Short Cut) (Prentice Hall Open Source Software Development Series)

Using BusyBox (Digital Short Cut) (Prentice Hall Open Source Software Development Series)