Go
Arduino
RaspberryPi
IoT
Gobot

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

概要

image.png

  • Arduinoに接続したセンサーをGobotライブラリを用いて読み取る
  • gin-gonicライブラリを用いたREST APIサービスを作る
  • センサー読み取り値をREST APIで公開する(JSON over HTTP)

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

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

背景

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

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

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

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

購入したもの

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

開発

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

準備

LinuxやmacOS環境であればUSBケーブルでArduinoをPCにさすだけで認識すると思います。認識しない場合はArduino IDEをインストールすれば同時にドライバがインストールされます。

私の開発環境はWindowsなので、COM3で認識されています。Raspberry PiなどのLinux環境では/dev/ttyACM0として認識されることが多いと思いますが、違っていて分からない場合にはこちらの記事を参照して探してください。
device_manager.png

Arduino IDEなどを使って標準的なFirmataをArduinoに書き込んでおきます。手順は「ProcessingとArduinoを接続する」の記事が画面つきで詳しいです。

回路を組む

LM35_temp_board.png

今回のサンプルでは、センサー部に高精度IC温度センサ LM35DZを用います。(光センサー(CdSセル)とかでも面白いかも。どちらもArduinoエントリーキットに入ってます。)

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

Gobotを使って読み取る

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

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パッケージNewAnalogSensorDriverを使っている点です。2016年12月20日のこちらのコミットで変更されているとおり、アナログデータを扱う部分がGPIOパッケージから分離されています。過去のGobotを使った記事を参考にしていると躓くポイントとなりやすいのでご注意ください。公式の豊富なサンプルを参考にするのが良いです。

実際に動かしてみるとこんな感じです。(2017年9月初旬の京都の夜は、ちょいと涼しい温度でした :laughing:
sensor-bot.gif

REST APIサービスをつくろう

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

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

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

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

より詳細について

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

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

Web API: The Good Parts

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

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

マイクロサービスアーキテクチャ

エンドポイントの設計

では、外部システムからアクセスしてもらう際の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で送受信できるよう構造体を定義します。

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メソッドのための宣言です。「あー、外部から受け取るパラメータにつければいいんだな」ぐらいに思ってもらえれば良いと思います。

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では一般的に使われるPUTDELETEなどのHTTPメソッドですが、ブラウザ上で動作するJavascriptなどからリクエストを行う場合には通常CORS(Cross-Origin Resource Sharing)を適用するためにクライアント側のリクエスト時にプリフライトリクエストが必要になります。詳細は先程も紹介したWeb API: The Good Partsの「4.5 同一生成元ポリシーとクロスオリジンリソース共有」やReal World HTTP ―歴史とコードに学ぶインターネットとウェブ技術の「10.3.6 クロスオリジンリソースシェアリング (CORS)」をご覧ください。この記事では簡単のため、gin-gonic用CORS制御ミドルウェアであるgin-contrib/corsのDefault設定を使って、r.Use(cors.Default())という全部素通しの設定を使います。

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

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機能が備わっており、こちらに実装のサンプルが、こちらにAPI仕様があります。しかし、必要なAPI機能を過不足なく自由に実現するために敢えて、gin-gonicフレームワークを使ったAPIサービスと組み合わせる手法をとります。

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

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

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.RWMutextempLockで値の更新時に排他制御をしたのと、ログ出力を削除しました。

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

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

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

sensor-api2.gif

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

デプロイ

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

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

ラズパイ同梱のGetting Started(紙)にあるように、QuickStartGuideの通りに作業します。OSセットアップが終われば、DebianをベースにしたRaspbianがインストールできます。

Raspberry Pi Quick Start Guide

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

Raspberry Pi 3を初回起動してからSSH接続まで

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

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

$ 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をご覧ください。

起動!

image.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