Gobotの招きにあひて、徒然なるままにArduinoとRaspberry PiでIoTっぽいことをやってみるなり

# 概要

![image.png](https://qiita-image-store.s3.amazonaws.com/0/166225/370f9a5f-fba2-aa5a-1834-44a86817ab6d.png)

* Arduinoに接続したセンサーを[Gobotライブラリ](https://gobot.io/documentation/platforms/arduino/)を用いて読み取る
* [gin-gonicライブラリ](https://github.com/gin-gonic/gin)を用いたREST APIサービスを作る
* センサー読み取り値をREST APIで公開する（JSON over HTTP）

上記の特徴をもったREST APIサービスをGo言語で作成し、Raspberry Pi上で動かしてみました。
なんだかIoTっぽい！٩( 'ω' )و

なお、この記事で紹介しているコードは、[KemoKemo/IoT-API-Sample](https://github.com/KemoKemo/IoT-API-Sample)にございます。こちらもあわせて見ていただくと、より一層内容を理解していただきやすくなると思います。

## 背景

最近お仕事で、以下のようなことを実現したくなったのがきっかけでした。

* センサー値をネットワーク経由で取得して使いたい
    * 以前の通信内容とは関係なく、今の数値がとれれば良い（**ステートレス**）
* センサーの属性値（名前とか）を外部からネットワーク経由で設定したい
* これらの機能を、いろんなシステムやアプリから使いたい

ちょうどGo言語をきっかけにWebサービスの作り方や仕組みを理解しつつあったので、「それならセンサー値さえ簡単に読み取れれば、後はGoでちゃちゃっとWebサービス書けばできそうだなぁ」と感じました。

そこで調べて出会ったのが、[Gobot](https://gobot.io/)です。「なにこれなにこれ、すごーい！」となったのは言うまでもありません。[対応プラットフォームが非常に豊富](https://gobot.io/documentation/platforms/)なことも魅力ですし、[簡単な実装例が多数](https://gobot.io/documentation/examples/)整備されている点も素晴らしい。自分は「[Arduino](https://gobot.io/documentation/platforms/arduino/)も[Raspberry Pi](https://gobot.io/documentation/platforms/raspi/)も名前ぐらいしか知らない」という人間でしたが、これなら簡単にモックシステムが作れるはずだと確信しました。

## 購入したもの

よくわからなかったので、Amazonのベストセラーと「よく一緒に購入されている商品」をまとめ買いです。躊躇などありません٩( 'ω' )و

* [Raspberry Pi3 Model B ボード＆ケースセット 3ple Decker対応 (Element14版, Clear)](https://www.amazon.co.jp/Raspberry-%E3%83%9C%E3%83%BC%E3%83%89%EF%BC%86%E3%82%B1%E3%83%BC%E3%82%B9%E3%82%BB%E3%83%83%E3%83%88-Element14%E7%89%88-Computing-Lab/dp/B01CSFZ4JG/ref=sr_1_1?ie=UTF8&qid=1498383728&sr=8-1&keywords=raspberry+pi)
* [Raspberry Pi用電源セット(5V 3.0A)－Pi3フル負荷検証済](https://www.amazon.co.jp/Physical-Computing-Lab-TSI-PI036-5V3A-3-0A-Pi3%E3%83%95%E3%83%AB%E8%B2%A0%E8%8D%B7%E6%A4%9C%E8%A8%BC%E6%B8%88/dp/B01N8ZIJL8/ref=pd_bxgy_147_2?_encoding=UTF8&psc=1&refRID=R2XDAE7V7W47GXGTD9Q1)
* [Samsung microSDHCカード 32GB EVO Plus Class10 UHS-I対応 MB-MC32GA/ECO](https://www.amazon.co.jp/dp/B06XSV23T1/ref=pd_bxgy_147_3?_encoding=UTF8&psc=1&refRID=R2XDAE7V7W47GXGTD9Q1)
* [Arduino エントリーキット(Uno版）](https://www.amazon.co.jp/gp/product/B008PQEWIW/ref=oh_aui_detailpage_o00_s00?ie=UTF8&psc=1)
* [Arduinoケース](https://www.amazon.co.jp/gp/product/B01KN1W8H8/ref=oh_aui_detailpage_o00_s00?ie=UTF8&psc=1)

# 開発

## Arduinoのセンサー値を読み取ろう

### 準備

LinuxやmacOS環境であればUSBケーブルでArduinoをPCにさすだけで認識すると思います。認識しない場合は[Arduino IDE](https://www.arduino.cc/en/main/software)をインストールすれば同時にドライバがインストールされます。

私の開発環境はWindowsなので、`COM3`で認識されています。Raspberry PiなどのLinux環境では`/dev/ttyACM0`として認識されることが多いと思いますが、違っていて分からない場合には[こちらの記事](http://qiita.com/yuiseki/items/bfa9ad984f7333377870)を参照して探してください。
![device_manager.png](https://qiita-image-store.s3.amazonaws.com/0/166225/b529adea-194a-17a0-a289-534edcfbceea.png)

Arduino IDEなどを使って標準的なFirmataをArduinoに書き込んでおきます。手順は「[ProcessingとArduinoを接続する](http://yoppa.org/tau_bmaw13/4772.html)」の記事が画面つきで詳しいです。

### 回路を組む

![LM35_temp_board.png](https://qiita-image-store.s3.amazonaws.com/0/166225/6a189021-0c3f-85e7-3132-5a24a5a325c3.png)

今回のサンプルでは、センサー部に[高精度IC温度センサ LM35DZ](http://akizukidenshi.com/catalog/g/gI-00116/)を用います。（[光センサー(CdSセル)](http://akizukidenshi.com/catalog/g/gI-00110/)とかでも面白いかも。どちらもArduinoエントリーキットに入ってます。）

温度センサの出力値を、Arduinoのアナログ0番`A0`に入力しています。

### Gobotを使って読み取る

このセンサー値を一定間隔で採取し、標準出力に出してみましょう。

```go
package main

import (
	"flag"
	"log"
	"os"
	"time"

	"gobot.io/x/gobot"
	"gobot.io/x/gobot/drivers/aio"
	"gobot.io/x/gobot/platforms/firmata"
)

const (
	exitCodeOK int = iota
	exitCodeFailed
)

var (
	port = flag.String("port", "/dev/ttyACM0", "the port of the Arduino")
)

func init() {
	flag.Parse()
}

func main() {
	os.Exit(run(os.Args))
}

func run(args []string) int {
	firmataAdaptor := firmata.NewAdaptor(*port)
	sensor := aio.NewAnalogSensorDriver(firmataAdaptor, "0")

	work := func() {
		gobot.Every(1*time.Second, func() {
			val, err := sensor.Read()
			if err != nil {
				log.Println("Failed to read", err)
				return
			}
			cel := (5.0 * float64(val) * 100.0) / 1024
			log.Printf("Raw-value:%v Celsius:%.2f", val, cel)
		})
	}

	robot := gobot.NewRobot("sensorBot",
		[]gobot.Connection{firmataAdaptor},
		[]gobot.Device{sensor},
		work,
	)

	err := robot.Start()
	if err != nil {
		log.Println("Failed to start a robot", err)
		return exitCodeFailed
	}
	return exitCodeOK
}
```

ポイントが2つあります。

1つ目は、`firmata.NewAdaptor`メソッドに指定するArduinoのポート情報を、実行時のオプション`-port`で変更できるようにしている点です。これにより、今回の例のように「開発時はWindows上で動作させ、完成品はRaspberry Piで動作させる」といった場合に、コードを変更することなくどちらの環境でも動作させることができます。

2つ目は、Arduinoのアナログピンのデータを扱う際に[aioパッケージ](https://github.com/hybridgroup/gobot/tree/master/drivers/aio)の`NewAnalogSensorDriver`を使っている点です。2016年12月20日の[こちらのコミット](https://github.com/hybridgroup/gobot/commit/c18663897365c614d61fde7fd16051146e318efc)で変更されているとおり、アナログデータを扱う部分が[GPIOパッケージ](https://github.com/hybridgroup/gobot/tree/master/drivers/gpio)から分離されています。過去のGobotを使った記事を参考にしていると躓くポイントとなりやすいのでご注意ください。[公式の豊富なサンプル](https://gobot.io/documentation/examples/)を参考にするのが良いです。

実際に動かしてみるとこんな感じです。（2017年9月初旬の京都の夜は、ちょいと涼しい温度でした :laughing: ）
![sensor-bot.gif](https://qiita-image-store.s3.amazonaws.com/0/166225/530b2fcd-6baf-ba10-1de5-3ba7c1d724b4.gif)

## REST APIサービスをつくろう

### そもそもなぜAPIサービスにすべきか

`IoT`を実現するのに何故急に`APIサービス`や`REST APIサービス`といった単語が出てくるのか分からない、という方もいらっしゃるかもしれないので少しだけ説明いたします。

`IoT`は`Internet of Things`の略称で、「モノをネットワークにつなぐ」などと表現されます。自分なりの理解ですが、ネットワークに接続したい動機は主に **他のシステムからその機器の情報を使いたい** からだと思います。では、どのようにすれば他のシステムから利用しやすい形で機器の情報を公開できるでしょうか？

既にデータや機能を公開している多くのサービスが採用しているように、RESTの概念を取り入れたAPIサービスを用いることで、この要求を簡単に達成することができます。また、IoT機器ではAPIサービスにより`データと機能`のみを公開する極力小さな仕組みに留めて、その情報を活用するWebシステムやアプリは別途開発する方が後々のメリットが大きくなります。これは`マイクロサービスアーキテクチャ`の考えに通じてゆきますが、活用する側のシステムとAPI側とが分離され独立になっていることにより、**新たな機器のAPIも組み合わせたシステムの開発が容易になったり、各々のシステムで全く異なる技術や言語を選定することが可能になります**。（つまり、他のシステムの影響を受けず、そのシステムに最も適した技術を選定して開発ができる）

#### より詳細について

上記でも既に喋りすぎ感があるので、後は素晴らしい書籍をご覧いただければと思います。

RESTについてもAPIについても様々な記事がありますが、私は以下の書籍をオススメいたします。

> [Web API: The Good Parts](https://www.amazon.co.jp/gp/product/4873116864/ref=oh_aui_detailpage_o00_s00?ie=UTF8&psc=1)

この記事で`APIサービス`と呼んでいるものは、上記書籍の表現を引用させていただくと`HTTPプロトコルを利用してネットワーク越しに呼び出すAPI`という意味です。APIを通じて、`センサーの値と機能（属性値を変更するなど）`をHTTP経由で利用可能にしたいと思います。

また、種々のWebシステムやアプリとIoT機器との連携を最適化する上でマイクロサービスアーキテクチャの考えはとても重要だと思います。以下の書籍が素晴らしいです。

> [マイクロサービスアーキテクチャ](https://www.amazon.co.jp/%E3%83%9E%E3%82%A4%E3%82%AF%E3%83%AD%E3%82%B5%E3%83%BC%E3%83%93%E3%82%B9%E3%82%A2%E3%83%BC%E3%82%AD%E3%83%86%E3%82%AF%E3%83%81%E3%83%A3-Sam-Newman/dp/4873117607)

### エンドポイントの設計

では、外部システムからアクセスしてもらう際のURIであるエンドポイントと、どういったHTTPメソッドでどんな機能が使用できるのかを設計しましょう。

今回は、温度センサーの属性値として設置場所などを設定できる`name`と温度センサー値から計算された`temp_c`をJSON形式でやりとりするような仕様にしてみました。

|エンドポイント|HTTPメソッド|概要|
|:---|:---|:---|
|/sensors|GET|以下の`sensor_list.json`のように、全てのセンサー情報がJSON形式で取得できる|
|/sensors/:sid|GET|以下の`sensor1.json`のように、`:sid`で指定した番号のセンサー情報がJSON形式で取得できる|
|/sensors/:sid|PUT|以下の`name.json`のようなJSONデータを送ることで、`:sid`で指定した番号のセンサーに属する`name`情報を更新できる|

```sensor_list.json
{
  "sensor_list": [
    {
      "number": 1,
      "name": "Kitchen",
      "temp_c": 25.86
    }
  ]
}
```

```sensor1.json
{
  "number": 1,
  "name": "Kitchen",
  "temp_c": 25.86
}
```

```name.json
{
  "name": "Living Room"
}
```

### gin-gonicを使った実装

まず、センサーの値をJSONで送受信できるよう構造体を定義します。

```go
type sensorData struct {
	SensorList []sensor `json:"sensor_list"`
}

type sensor struct {
	Number       int     `json:"number"`
	Name         string  `json:"name" binding:"required"`
	TemperatureC float64 `json:"temp_c"`
}
```

`binding:"required"`の部分は、`PUT`メソッドで受け取ったJSONデータを簡単に構造体に読み込むために用いる`gin.Context.BindJSON`メソッドのための宣言です。「あー、外部から受け取るパラメータにつければいいんだな」ぐらいに思ってもらえれば良いと思います。

```go
var tempData sensorData

func init() {
	tempData = sensorData{
		SensorList: []sensor{
			sensor{Number: 1, Name: "Kitchen", TemperatureC: 25.86},
		},
	}
}

func main() {
	os.Exit(run(os.Args))
}

func run(args []string) int {
	r := gin.Default()
	r.Use(cors.Default())
	v1 := r.Group("/api/v1")
	{
		v1.GET("/sensors", sensorsGetEndpoint)
		v1.GET("/sensors/:sid", sensorIDGetEndpoint)
		v1.PUT("/sensors/:sid", sensorIDPutEndpoint)
	}

	err := r.Run(":5000")
	if err != nil {
		log.Println("Failed to start", err)
		return exitCodeFailed
	}
	return exitCodeOK
}
```

`v1.GET("/sensors", sensorsGetEndpoint)`の部分をご覧ください。上述の[エンドポイントの設計](#エンドポイントの設計)で書いた表の1行目の内容をそのままコードにしたようなシンプルさでAPIが実装されています。実に素晴らしい٩( 'ω' )و

APIでは一般的に使われる`PUT`や`DELETE`などのHTTPメソッドですが、ブラウザ上で動作するJavascriptなどからリクエストを行う場合には通常`CORS(Cross-Origin Resource Sharing)`を適用するためにクライアント側のリクエスト時に[プリフライトリクエスト](https://developer.mozilla.org/ja/docs/Web/HTTP/HTTP_access_control#Preflighted_requests)が必要になります。詳細は先程も紹介した[Web API: The Good Parts](https://www.amazon.co.jp/gp/product/4873116864/ref=oh_aui_detailpage_o00_s00?ie=UTF8&psc=1)の「4.5 同一生成元ポリシーとクロスオリジンリソース共有」や[Real World HTTP ―歴史とコードに学ぶインターネットとウェブ技術](https://www.amazon.co.jp/Real-World-HTTP-%E2%80%95%E6%AD%B4%E5%8F%B2%E3%81%A8%E3%82%B3%E3%83%BC%E3%83%89%E3%81%AB%E5%AD%A6%E3%81%B6%E3%82%A4%E3%83%B3%E3%82%BF%E3%83%BC%E3%83%8D%E3%83%83%E3%83%88%E3%81%A8%E3%82%A6%E3%82%A7%E3%83%96%E6%8A%80%E8%A1%93-%E3%82%88%E3%81%97%E3%81%8D/dp/4873118042)の「10.3.6 クロスオリジンリソースシェアリング (CORS)」をご覧ください。この記事では簡単のため、gin-gonic用CORS制御ミドルウェアである[gin-contrib/cors](https://github.com/gin-contrib/cors)のDefault設定を使って、`r.Use(cors.Default())`という全部素通しの設定を使います。

次に、個々のエンドポイントで実行される`sensorsGetEndpoint`などの関数の内容もみてみましょう。

```go
func sensorsGetEndpoint(c *gin.Context) {
	c.JSON(http.StatusOK, tempData)
}

func sensorIDGetEndpoint(c *gin.Context) {
	sid := c.Param("sid")
	id, err := parseSensorID(sid)
	if err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"message": err.Error()})
		return
	}
	c.JSON(http.StatusOK, tempData.SensorList[id-1])
}

func sensorIDPutEndpoint(c *gin.Context) {
	sid := c.Param("sid")
	id, err := parseSensorID(sid)
	if err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"message": err.Error()})
		return
	}

	data := sensor{}
	err = c.BindJSON(&data)
	if err != nil {
		c.JSON(http.StatusUnsupportedMediaType, gin.H{"message": err.Error()})
		return
	}
	tempData.SensorList[id-1].Name = data.Name
	c.JSON(http.StatusOK, nil)
}

func parseSensorID(sid string) (int, error) {
	id, err := strconv.Atoi(sid)
	if err != nil {
		return id, err
	}
	if id > len(tempData.SensorList) {
		return id, fmt.Errorf("id '%v' does not exist", id)
	}
	return id, nil
}
```

いいですね。`c.JSON(http.StatusOK, tempData)`、たったこれだけでデータをJSON形式でリクエスト側に返せます。他の関数はURI中に登場する`sid`をパースする処理があるため若干処理が増えていますが、まぁこんなものでしょう。

#### 実はGobotにもAPI機能がありますが・・・

実はGobot自体にもRESTfulなAPI機能が備わっており、[こちら](https://gobot.io/documentation/core/api/)に実装のサンプルが、[こちら](https://github.com/hybridgroup/cppp-io/blob/master/specification.markdown)にAPI仕様があります。しかし、必要なAPI機能を過不足なく自由に実現するために敢えて、`gin-gonic`フレームワークを使ったAPIサービスと組み合わせる手法をとります。

## Gobotとgin-gonicをふゅーじょん！

これまでに作ったセンサープログラムとAPIサービスを融合しましょう。
gin-gonicのAPIサービスをgoroutineで起動しておいて、その後にgobotをStartします。

```go
func main() {
	os.Exit(run(os.Args))
}

func run(args []string) int {
	go runAPI(*addr)
	err := runRobot(*port)
	if err != nil {
		log.Println("Failed to start a robot", err)
		return exitCodeFailed
	}
	return exitCodeOK
}
```

`runAPI`にAPIサービスの実装が、`runRobot`にセンサー読み取りの実装が入っています。変えたのは以下の箇所で、`sync.RWMutex`の`tempLock`で値の更新時に排他制御をしたのと、ログ出力を削除しました。

```go
work := func() {
		gobot.Every(1*time.Second, func() {
			val, err := sensor.Read()
			if err != nil {
				log.Println("Failed to read", err)
				return
			}
			cel := (5.0 * float64(val) * 100.0) / 1024.0
			tempLock.Lock()
			tempData.SensorList[0].TemperatureC = cel
			tempLock.Unlock()
		})
	}
```

ためしにブラウザからアクセスしてみましょう。Firefoxでアクセスしてみると、ご覧のようにセンサー値がとれています٩( 'ω' )و

![image.png](https://qiita-image-store.s3.amazonaws.com/0/166225/d51dd250-6038-2dab-e0e5-47c46d5ff928.png)

curlコマンドでPUTしてみましょう。

```bash
curl -X PUT -d @name.json http://localhost:5000/api/v1/sensors/1
```

![sensor-api2.gif](https://qiita-image-store.s3.amazonaws.com/0/166225/c6e51fe3-9dbe-ad6d-8da0-6102a8e97a8f.gif)

こちらも成功しました。`PUT`メソッドで送ったJSONデータにより、`name`が変更されました。

# デプロイ

プログラムができましたので、Raspberry Pi用にビルドしてデプロイしましょう。

## Raspberry PiのセットアップとSSH有効化

ラズパイ同梱のGetting Started（紙）にあるように、QuickStartGuideの通りに作業します。OSセットアップが終われば、Debianをベースにした[Raspbian](https://www.raspberrypi.org/downloads/raspbian/)がインストールできます。

> [Raspberry Pi Quick Start Guide](https://www.raspberrypi.org/files/legacy/qsg.pdf)

後々バイナリをデプロイするのに便利なので、以下の記事も参照してSSHを有効化しておきましょう。

> [Raspberry Pi 3を初回起動してからSSH接続まで](http://ykubot.com/2017/01/17/raspberry-pi-3-initialize/)

## クロスコンパイルとデプロイ

Go言語はクロスコンパイル可能なので、**例えばWindows上でラズパイ用に簡単にビルドできます**。

```bash
$ GOARM=7 GOARCH=arm GOOS=linux go build
```

GOARMの設定はラズパイのバージョンによって以下のように設定します。

* Raspberry Pi A, A+, B, B+, Zero ならば `GOARM=6`
* Raspberry Pi 2, 3 ならば `GOARM=7`

ビルドの方法やscpコマンドを使ったデプロイに関する詳細は[How to Connect](https://gobot.io/documentation/platforms/raspi/#how-to-connect)をご覧ください。

## 起動！

![image.png](https://qiita-image-store.s3.amazonaws.com/0/166225/e8cd1289-0e8f-5770-057c-94237997ffe2.png)

`sensor-api`をRaspberry Pi上で起動して、Windows端末から`curl`で情報取得してみました。さきほどと同じように動作しています。これにて、手のひらサイズのミニコンピュータ「Raspberry Pi」上で「Arduinoに接続したセンサーの値と機能を、REST APIで公開するサービス」が稼働しました。やったね！(*´ω｀*)

なお、gin-gonicのログからμ秒オーダーで処理がなされていることが分かります。素晴らしい速度ですね！さすがGo言語、そして数あるGo製Webフレームワークの中でもフルスタックで爆速なgin-gonicのなせる技です！実にAwesome！

IoTもWebもたーのしー！٩( 'ω' )و

# さいごに

gobotならびにgin-gonicライブラリの関係者の皆様に、この場をお借りしてお礼申し上げます。素晴らしいライブラリをどうもありがとうございます！m(_ _)m

ArduinoもRaspberry Piも使い始めたばかりでIoT界隈の事情もまだ良くわかっていない若輩者ですので、もっともっと勉強したいと思います。「ここちゃうで」「もっとこうするといいよ」といったお気づきの点がございましたら、コメント欄でご指摘、ご指導いただけますと幸いです。どうぞよろしくお願いいたしますm(_ _)m