non vorrei lavorare

2020年度からの小学校プログラミング教育の必修化を親として迎えるブロガーの書く、子供との日常

macOSのCPU温度をgRPCを使ってElectronで作ったデスクトップマスコットに表示させた

おはようございます。最近子供達はライフル銃のような物を紙工作しています。@kjunichiです。

背景

gopheronでraspberry piのCPU温度の表示が既に出来ていた。

ふと昔のQiitaの投稿

qiita.com

を見つけてから、Swiftが出てからこの辺りのAPI何か変化ないか調べたら、 特に変化はないものの、

github.com

を見つけた。

これで、これまでのRaspberry Piと同様のgRPCのクラサバ方式で gopheronでCPU温度を表示できるはず。

メインの処理はすぐ出来た

gopheron-raspberrypi同様に外部コマンドを起動して結果を取得するだけなので、

というツイートの通り、ファミレスドヤリング駆動であっさり実装できた。

Macらしいアイコンを表示したい!

macOSと分かるアイコンを表示したいと思い、これを実現するのが 険しい道のりだった。

もっとも、アイコンデータを吸い出して、Raspberry PiのCPU温度の表示のするクライアントと同様に クライアント側にアイコンデータを埋め込めば全然難易度低いが、 Githubに公開して場合にアイコンのライセンスが非常に気になる。

f:id:kjw_junichi:20180723082301j:plain

何は無くともアイコンデータ

以下の記事に助けられ、

/System/Library/CoreServices/CoreTypes.bundle/Contents/Resources/FinderIcon.icns

のファインダーのアイコンを使えば良さそうだと発見。

このアイコンの画像データをgRPCでクライアント側にimgタグにData URI形式して埋め込んで、 クライアント側にHTMLデータとして送信して、gopheron側で、HTMLをレンダリングすれば、 macOSを示すアイコンが表示されるはず。

icnsファイルをどうやって読み込むか

libicnsなるUNIX系なライブラリを発見するも、ハタと考えた。 今回はgRPCサーバ側の処理はmacOS限定なので、Cocoa APIで手軽にできそうなら そちらで実装した方がスッキリ出来そう。

そんな訳でcgoでCocoa APIを使う方法に続く。

cgoからCocoa APIを使う

数年前に、Cocoa APIをcgoで使う以下の記事を読んでいたので、Objective-Cと連携させれば出来そうな事が分かってはいたが、 今回改めて調べたら、以下が見つかった。

http://unknownplace.org/archives/cgo-and-eventloop.html

ポイントは、CFLAGSでの指定と、LDFLAGSで依存するフレームワークを指定するところ。

#cgo CFLAGS: -x objective-c
#cgo LDFLAGS: -framework Foundation -framework AppKit

Cocoa APIの冒険

ここで、一旦Cocoa APIを使ってicnsファイルを読み込んで、PNG画像データをData URI形式の 文字列を返却するObcjective-Cのコードを書いておく。

icnsファイルをCocoa APIで読み込むには

を見つけたが、試行錯誤したものの、どうもダメ。 以下のコードが自分の環境では動いた。

NSBitmapImageRepにimageRepsWithContentsOfFileなるファイルパスを指定すると配列に読み込んでくれるメソッド?が 見つかった。

配列からは、本来は、使いたい画像サイズから適切な画像要素を取得するべきなのだろうが、メンドくさいので、 2番目を決め打ちで指定している。

NSString *path = @"/System/Library/CoreServices/CoreTypes.bundle/Contents/Resources/FinderIcon.icns";
NSArray * imageReps = [NSBitmapImageRep imageRepsWithContentsOfFile:path];
NSImageRep *imageRep = [imageReps objectAtIndex: 1];

PNGのバイナリデータを取得するには

NSBitmapImageRep型になっていれば、そこから指定した画像フォーマットを取得するのは簡単だった。 でもpropertiesを明示的に指定しないと落ちたのでこの点は要注意。

NSData *data = [imageRep representationUsingType:NSPNGFileType properties : nil];

NSData型のデータをBase64エンコードするには

base64EncodedStringWithOptionsを使えば出来る。昔はbase64EncodedStringもあったが、現在は base64EncodedStringWithOptionsが推奨されてる模様。

NSString *pngstr = [data base64EncodedStringWithOptions:0 ];

NSString型をchar*型に変換するには

cgoでGo側に連携するためにC.String型に変換する必要がある。 以下のコードで行えた。

int len = [pngstr length];
char *buf = (char*)malloc(sizeof(char)*(len+1));
strcpy(buf, [pngstr UTF8String]);

cgoでObjective-Cの結果を扱う

Objective-Cで作った関数を呼ぶには

CやC++同様にC.関数名で呼び出せる。

tmpStr := C.getFinderIconStr()

cgoでCString型をGoのstring型に変換するには

pngDataUrl := C.GoString(tmpStr)

cgoでCStringの領域を解放するには

C.free(unsafe.Pointer(tmpStr))

成果物

動かし方

macOS側で

go get -u github.com/kjunichi/gopheron-macos
cd $GOPATH/src/github.com/kjunichi/gopheron-macos
go run server/main.go

gopheronを動かすPCで

go get -u github.com/kjunichi/gopheron
cd $GOPATH/src/github.com/kjunichi/gopheron
go build gopheron.go
./gopheron &
node macosClient.js hostname.local

まとめ

Cocoa APIを調べるとiOS専用っぽいAPIが出てきたり、SwiftのAPIだったり、cgoで使える型式で すぐには見つからないこともあるようだ。

今回はgoroutineを使ってないから、あんまり問題なさそうだが、Cocoa APIを特定のスレッドから 呼び出す必要があったりする模様。

参考資料

関連記事

Cocoaつながり

14年前の記事

8年前の記事

3年前の記事

2年前の記事