📻

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

2021/10/07に公開

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

  1. Gobot をインストールする
$ go get -d -u gobot.io/x/gobot/...

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

package main

import (
        "gobot.io/x/gobot"
        "gobot.io/x/gobot/drivers/gpio"
        "gobot.io/x/gobot/platforms/firmata"
)
  1. Firmata を Arduino にインストール

Firmata とは

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

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

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

スクリーンショット 2021-10-07 11.20.24.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

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

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で定義しておきます。

const (
	MeterMax  uint8         = 165
	MeterMin                = 15
)

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

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

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

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

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

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に設定値を読み込ませていきます。
firmataAdaptorPIN番号を各種Gobotのgpio.New*関数に渡してあげることで設定完了です。


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を利用することによってより読みやすく記述でき、大変直感的にデバイス操作を実現できます。

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関数

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使用率を取得したい場合

cpu.Percent(0, false)

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

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

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

		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関数

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にバインドさせて、GobotNewRobot関数に渡します。

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

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

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

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

ケースを取り付けた様子

今回はケースと文字盤土台、針は自らモデリングし、3Dプリンターで印刷しました。

所感

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

Next generation robotics/IoT framework

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

参考

GitHubで編集を提案

Discussion