Golang(Gobot+Firmata+Arduino)でアナログメーターを作ってみる

# Golang(Gobot+Firmata+Arduino)でアナログメーターを作ってみる

こんにちは、最近は色々検索していて、ブラウザのタブを開きすぎて、フリーズしがちな毎日です。
今回は PC の状態を視覚的に素早く認識できる CPU,Memory,Disk の使用率を可視化するアナログメーターを前から気になっていた Gobot 等を使って実践してみました。

成果物

https://youtu.be/yzfc8TC2CWM

https://github.com/Iovesophy/analog-meter-go

# Gobot セットアップ

Gobot とは

https://gobot.io/

> Next generation robotics/IoT framework with support for 35 different platforms

次世代 robotics/IoT フレームワークですね、
35のプラットフォームをサポートしている強力なライブラリです。

1. Golang をインストールする

https://golang.org/doc/install

2. Gobot をインストールする

```Go
$ go get -d -u gobot.io/x/gobot/...
```

具体的な使い方としては、`import`部分で各種使用したい`Gobot`のライブラリを読み込ませると良いです。

```Go
package main

import (
        "gobot.io/x/gobot"
        "gobot.io/x/gobot/drivers/gpio"
        "gobot.io/x/gobot/platforms/firmata"
)
```

3. Firmata を Arduino にインストール

Firmata とは

>FirmataはPC - マイコン間でやり取りするためのプロトコルです。 汎用入出力の値の取得・書き込みその他の操作をPC側からArduinoのようなマイコンに対して行う為に使用されます。
https://gist.github.com/hiroeorz/7868628

とあるように PC -マイコン間のやりとりに関して難しいことを考えることなく Golang から操作できます。

インストール方法はとても簡単で Arduino IDE のスケッチ例から選択して書き込むだけです。

![スクリーンショット 2021-10-07 11.20.24.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1009976/3fac6d3b-bb87-ac84-1d4f-e447ed12736b.png)

今回は `StandardFirmata` を使用します。

詳しくは本家のサイトで確認してください。

> Firmata is a generic protocol for communicating with microcontrollers from software on a host computer. It is intended to work with any host computer software package. To download a host software package, please click on the following link to open the list of Firmata client libraries in your default browser.

https://github.com/firmata/arduino#firmata-client-libraries

4. Gobotを使ってプログラムを書く

今回はメーターの駆動部分にサーボモーターを使います。
またArduinoの種類についても詳しくはGitHubに準備した成果物中に記載しているので確認してみてください。

また今回PCのCPU,MEM,DISKの使用状況をモニタリングするため`gopsutil`というライブラリを使用しています。

Goの公式documentにも記載があります。

https://pkg.go.dev/github.com/shirou/gopsutil

> https://github.com/Iovesophy/analog-meter-go/blob/master/hardware/Parts.md

```Go
package main

import (
	"fmt"
	"log"
	"time"

	"github.com/shirou/gopsutil/v3/cpu"
	"github.com/shirou/gopsutil/v3/disk"
	"github.com/shirou/gopsutil/v3/mem"
	"gobot.io/x/gobot"
	"gobot.io/x/gobot/drivers/gpio"
	"gobot.io/x/gobot/platforms/firmata"
	"gobot.io/x/gobot/platforms/keyboard"
)

```

まずは必要なライブラリを`import`します。

次にリテラルで持っておきたい値を`const`で定義しておきます。

```Go
const (
	MeterMax  uint8         = 165
	MeterMin                = 15
)
```

今回はサーボモーターの駆動範囲には`±15`度遊びを持たせています。

最大角度をMeterMaxとしてリテラルに`165`を設定します。
最小角度をMeterMinとしてリテラルに`15`を設定します。

次に構造体として使用する電子パーツ等を宣言しておきます。
せっかくGoで書くので今回はパーツとパーツの制御に関わるコンポーネントを`device`という構造体に持たせて利用することにしました。

このようにデバイスとして括ってあげることで`*loop`関数内で見通しがつくやすくなります。

また、`Gobot`の大変素晴らしい点なのですが、`LedDriver`や`ServoDriver`などドライバーが標準で用意されているので、こちらを宣言するだけで、簡単にデバイスを制御できます。

```Go
type device struct {
	keys       *keyboard.Driver
	keyflag    string
	ledGreen   *gpio.LedDriver
	ledBlue    *gpio.LedDriver
	ledYellow  *gpio.LedDriver
	servoMotor *gpio.ServoDriver
	angleBuf   uint8
}
```

次にデバイスの設定を行います。
ここではレシーバとして`c *device`を定義して先程宣言した`device`に設定値を読み込ませていきます。
`firmataAdaptor`と`PIN番号`を各種Gobotの`gpio.New*`関数に渡してあげることで設定完了です。

```Go

func (c *device) deviceSettings(firmataAdaptor *firmata.Adaptor) {
	c.servoMotor = gpio.NewServoDriver(firmataAdaptor, "5")
	c.ledYellow = gpio.NewLedDriver(firmataAdaptor, "3")
	c.ledGreen = gpio.NewLedDriver(firmataAdaptor, "11")
	c.ledBlue = gpio.NewLedDriver(firmataAdaptor, "13")
}

```

次に起動時やキー入力受信時などの制御内容を`initMotion`として記述します。
`Toggle`というのはトグルスイッチのイメージでこのような形で`Gobot`を利用することによってより読みやすく記述でき、大変直感的にデバイス操作を実現できます。

```Go
func (c *device) initMotion() {
	c.angleBuf = MeterMax
	c.servoMotor.Move(c.angleBuf)
	for i := 0; i < 5; i++ {
		c.ledBlue.Toggle()
		c.ledYellow.Toggle()
		c.ledGreen.Toggle()
		time.Sleep(time.Millisecond * 100)
	}
}
```

次にせっかくGoを使っているのでマルチスレッドにシングルコアしか持たないArduinoを操作してみましょう。
`mainLoop`関数と`subLoop`関数を準備してそれぞれの` *Loop`をマルチスレッドに実行させることでキー入力の割り込み処理を実現します。

`subLoop関数`

```Go
func (c *device) subLoop() {
	for {
		time.Sleep(time.Second)
		if c.keyflag == "cpu" {
			p, err := cpu.Percent(0, false)
			if err != nil {
				log.Fatal(err)
			}
			angleRaw := calcAngleRaw(p[0])
			angle := MeterMax - angleRaw
			if c.angleBuf <= angle {
				for i := c.angleBuf; i < angle; i++ {
					c.servoMotor.Move(i)
					time.Sleep(time.Millisecond * 50)
				}
			} else if c.angleBuf >= angle {
				for i := c.angleBuf; i > angle; i-- {
					c.servoMotor.Move(i)
					time.Sleep(time.Millisecond * 50)
				}
			}
			c.angleBuf = angle
		} else if c.keyflag == "mem" {
			m, err := mem.VirtualMemory()
			if err != nil {
				log.Fatal(err)
			}
			angleRaw := calcAngleRaw(m.UsedPercent)
			c.servoMotor.Move(MeterMax - angleRaw)
		} else if c.keyflag == "disk" {
			d, err := disk.Usage("/Volumes")
			if err != nil {
				log.Fatal(err)
			}
			angleRaw := calcAngleRaw(d.UsedPercent)
			c.servoMotor.Move(MeterMax - angleRaw)
		}
	}
}

func calcAngleRaw(d float64) uint8 {
	return uint8(d / 100 * float64(MeterMax-MeterMin))
}
```

`gopsutil`では次のように例えばCPU使用率を取得したい場合

```Go
cpu.Percent(0, false)
```

と記述すればCPU使用率をパーセントで取得できます。
これらの値をサーボモータで設定している駆動領域に正規化させるために`calcAngleRaw`関数を定義して計算しています。

```Go
func calcAngleRaw(d float64) uint8 {
	return uint8(d / 100 * float64(MeterMax-MeterMin))
}
```

また、CPU使用率に関しては変動が大きいので、サーボモーターの駆動音がうるさい場合があります。
これを解消するために1度ずつその時点でのCPU使用率の割合までwaitをかけながら制御しています。

```Go
		p, err := cpu.Percent(0, false)
			if err != nil {
				log.Fatal(err)
			}
			angleRaw := calcAngleRaw(p[0])
			angle := MeterMax - angleRaw
			if c.angleBuf <= angle {
				for i := c.angleBuf; i < angle; i++ {
					c.servoMotor.Move(i)
					time.Sleep(time.Millisecond * 50)
				}
			} else if c.angleBuf >= angle {
				for i := c.angleBuf; i > angle; i-- {
					c.servoMotor.Move(i)
					time.Sleep(time.Millisecond * 50)
				}
			}
			c.angleBuf = angle
```

`mainLoop関数`

```Go
func (c *device) mainLoop() {
	firmataAdaptor := firmata.NewAdaptor("/dev/tty.usbmodem142101")
	c.deviceSettings(firmataAdaptor)
	c.keys = keyboard.NewDriver()
	c.keys.On(keyboard.Key, func(keydata interface{}) {
		key := keydata.(keyboard.KeyEvent)
		c.initMotion()
		if key.Key == keyboard.P {
			c.keyflag = "cpu"
			c.ledBlue.Off()
			c.ledYellow.Off()
			c.ledGreen.On()
		} else if key.Key == keyboard.M {
			c.keyflag = "mem"
			c.ledBlue.Off()
			c.ledYellow.On()
			c.ledGreen.Off()
		} else if key.Key == keyboard.D {
			c.keyflag = "disk"
			c.ledBlue.On()
			c.ledYellow.Off()
			c.ledGreen.Off()
		} else if key.Key >= 97 && key.Key <= 122 {
			c.keyflag = "unknownkey"
			c.ledBlue.Off()
			c.ledYellow.Off()
			c.ledGreen.Off()
			c.servoMotor.Move(165)
		}
		fmt.Println(c.keyflag)
	})
	robot := gobot.NewRobot("bot",
		[]gobot.Connection{firmataAdaptor},
		[]gobot.Device{
			c.keys,
			c.servoMotor,
			c.ledBlue,
			c.ledYellow,
			c.ledGreen,
		},
		c.keys, //set workspace
	)
	robot.Start()
}
```

`mainLoop`関数では、キー入力を受け付ける部分を書いています。
また、ハンドラにキー入力と`subLoop`で使用するフラグを定義するために制御内容を記述して`c.keys`にバインドさせて、`Gobot`の`NewRobot`関数に渡します。

その際、必ず`subLoop`で使用するデバイスに関しての設定等も`[]gobot.Device`に渡しておきます。

最後に変数`robot`にバインドさせていた`NewRobot`関数を`robot.Start()`で実行します。

最後に`main`関数で`subLoop`を`go routine`で実行したのちに、`mainLoop`を実行します。
レシーバを有効にするため`device`構造体を`main`関数内で宣言し、各種` *Loop`関数をレシーバ経由で呼び出します。

```Go
func main() {
	c := device{}
	c.keyflag = "init"
	go c.subLoop()
	c.mainLoop()
}
```

# 所感

今回はアナログメーターを作るだけの簡単な例でした、
`Gobot`という名称からもイメージできると思いますがロボティクス開発やIoT開発において利用されることが想定されています。
> Next generation robotics/IoT framework

今後も積極的にお家IoT開発等に使っていきたいなと思いました。

# 参考
- Gobot: https://gobot.io/
- gopstil: https://pkg.go.dev/github.com/shirou/gopsutil
- 「CPUやメモリなどの情報を取得するgopsutilのご紹介」 http://tdoc.info/blog/2015/12/16/gopsutil.html
- Arduino: https://store-usa.arduino.cc/products/arduino-uno-rev3/?selectedStore=us
- ServoMotor: http://www.kumantech.com/kuman-mg90s-9g-size-metal-gear-micro-servo-for-plane-helicopter-boat-car-robot-ky61_p0228.html