概要
- 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です。「なにこれなにこれ、すごーい!」となったのは言うまでもありません。対応プラットフォームが非常に豊富なことも魅力ですし、簡単な実装例が多数整備されている点も素晴らしい。自分は「ArduinoもRaspberry Piも名前ぐらいしか知らない」という人間でしたが、これなら簡単にモックシステムが作れるはずだと確信しました。
購入したもの
よくわからなかったので、Amazonのベストセラーと「よく一緒に購入されている商品」をまとめ買いです。躊躇などありません٩( 'ω' )و
- Raspberry Pi3 Model B ボード&ケースセット 3ple Decker対応 (Element14版, Clear)
- Raspberry Pi用電源セット(5V 3.0A)-Pi3フル負荷検証済
- Samsung microSDHCカード 32GB EVO Plus Class10 UHS-I対応 MB-MC32GA/ECO
- Arduino エントリーキット(Uno版)
- Arduinoケース
開発
Arduinoのセンサー値を読み取ろう
準備
LinuxやmacOS環境であればUSBケーブルでArduinoをPCにさすだけで認識すると思います。認識しない場合はArduino IDEをインストールすれば同時にドライバがインストールされます。
私の開発環境はWindowsなので、COM3
で認識されています。Raspberry PiなどのLinux環境では/dev/ttyACM0
として認識されることが多いと思いますが、違っていて分からない場合にはこちらの記事を参照して探してください。
Arduino IDEなどを使って標準的なFirmataをArduinoに書き込んでおきます。手順は「ProcessingとArduinoを接続する」の記事が画面つきで詳しいです。
回路を組む
今回のサンプルでは、センサー部に高精度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月初旬の京都の夜は、ちょいと涼しい温度でした )
REST APIサービスをつくろう
そもそもなぜAPIサービスにすべきか
IoT
を実現するのに何故急にAPIサービス
やREST APIサービス
といった単語が出てくるのか分からない、という方もいらっしゃるかもしれないので少しだけ説明いたします。
IoT
はInternet of Things
の略称で、「モノをネットワークにつなぐ」などと表現されます。自分なりの理解ですが、ネットワークに接続したい動機は主に 他のシステムからその機器の情報を使いたい からだと思います。では、どのようにすれば他のシステムから利用しやすい形で機器の情報を公開できるでしょうか?
既にデータや機能を公開している多くのサービスが採用しているように、RESTの概念を取り入れたAPIサービスを用いることで、この要求を簡単に達成することができます。また、IoT機器ではAPIサービスによりデータと機能
のみを公開する極力小さな仕組みに留めて、その情報を活用するWebシステムやアプリは別途開発する方が後々のメリットが大きくなります。これはマイクロサービスアーキテクチャ
の考えに通じてゆきますが、活用する側のシステムとAPI側とが分離され独立になっていることにより、新たな機器のAPIも組み合わせたシステムの開発が容易になったり、各々のシステムで全く異なる技術や言語を選定することが可能になります。(つまり、他のシステムの影響を受けず、そのシステムに最も適した技術を選定して開発ができる)
より詳細について
上記でも既に喋りすぎ感があるので、後は素晴らしい書籍をご覧いただければと思います。
RESTについてもAPIについても様々な記事がありますが、私は以下の書籍をオススメいたします。
この記事で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": [
{
"number": 1,
"name": "Kitchen",
"temp_c": 25.86
}
]
}
{
"number": 1,
"name": "Kitchen",
"temp_c": 25.86
}
{
"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では一般的に使われるPUT
やDELETE
などの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.RWMutex
のtempLock
で値の更新時に排他制御をしたのと、ログ出力を削除しました。
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でアクセスしてみると、ご覧のようにセンサー値がとれています٩( 'ω' )و
curlコマンドでPUTしてみましょう。
curl -X PUT -d @name.json http://localhost:5000/api/v1/sensors/1
こちらも成功しました。PUT
メソッドで送ったJSONデータにより、name
が変更されました。
デプロイ
プログラムができましたので、Raspberry Pi用にビルドしてデプロイしましょう。
Raspberry PiのセットアップとSSH有効化
ラズパイ同梱のGetting Started(紙)にあるように、QuickStartGuideの通りに作業します。OSセットアップが終われば、DebianをベースにしたRaspbianがインストールできます。
後々バイナリをデプロイするのに便利なので、以下の記事も参照して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をご覧ください。
起動!
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