Garmin アプリ開発(7) 不具合解析
アプリを公開していると、「Can’t use」とか「エラーが出ます」のような問い合わせメールがいくつか届きました。
「おかしいなぁ? シミュレーターではちゃんと動いているんだけど」
と思いつつ、こういうケースの調査方法です
ログを手動で送ってもらう
手元で問題が再現するなら、自分のデバイスのクラッシュログを確認します。
通常、/GARMIN/APPS/LOGS/CIQ_LOG.YML に入っています。
再現しない場合は
Thank you for your feedback.
If you also forward your crash logs I can take a look, the following file will contain what I needCIQ_LOG.YML or .TXT found within the /GARMIN/APPS/LOGS directory within the watch filesystem.
BR
などとメールして、 を送ってもらいます。
Monkey C: Open ERA Viewer
これはいろんなユーザーのところでエラーが発生したログを見ることができるツールです。個別にログを送ってもらう必要はないので便利ですね。
ただしこれはユーザーがスマホとSyncしないと見えないで、じっくり待つ必要があります。問い合わせが来てもすぐに見えるとは限らないです。
Garminの開発環境をセットアップした VS Codeから、Ctrl+Shift+p でコマンドパレットを開き、「Monkey C: Open ERA Viewer」を選ぶことでERA Viwerが開きます。
Core Topics (garmin.com) などを見ながら
Developer IDと
For example, if the URL is https://apps.garmin.com/en-US/developer/e1683f31–54fe–45d9–9aa8-cba39fe7fd1f/apps then the developer ID is e1683f31–54fe–45d9–9aa8-cba39fe7fd1f.
developer keyを設定します。
実際にクラッシュログが集まるとこのように見えます。
この場合は
AdditionalTimeStopwathView ファイルの backfunction 関数の 121行目が怪しいとなります。
今回該当するコードは
View.findDrawableById("back").setBitmap(_transparentIcon);
ですが、怪しいところは無いんだよね~
シミュレーターでは全機種OKだし、手元のForeAthlete55でも問題なし・・・
一つ怪しいのは _transparentIconで指定している画像データが 1x1 サイズの透明PNGファイルなので、特定機種では描画できないのかもしれない(仮説)
なのでこのアイコンを8x8の黒アイコンに変更してリリースして様子を見てみます。
(続くかも)
Garmin Data Fields作ってみた(1)
GarminのData Fields を作ってみました。
Data Fieldsとは
Data Fieldsとは、アプリケーションのタイプの一つで、ガーミンアクティビティーアプリ(ランとかバイク等)にpluginして画面を拡張できるアプリタイプです。
Data Fields と Simple Data Fieldsの2種類があります。
- Simple Data Fieldsは単一の数値や文字列を表示する機能が実現できます。Simpleとあるように画面レイアウトなどを考えなくてよい利点があります。
- Data Fieldsは複数のデータを表示できます。画面レイアウトなどを自分で実装する必要がある分だけ、すこし複雑です。
作るアプリ
横浜マラソン2022の関門(チェックポイント)を表示するData Fieldsを作ってみます。
表示したいデータは3種類あるのですが、Simple Data Filedsを3つ作ることで簡単に済ませましょう(※注:これはあとで痛い目を・・・)
- 次の関門までの距離
- 次の関門までの時間
- 次の関門の通過予定の余裕時間(先行/遅れ)
コード
WatchUi.SimpleDataFieldを派生したクラスを作ります。
computeメソッドで表示したい文字列を返します
class CheckpointYokohama2022View1 extends WatchUi.SimpleDataField {private var _yokohama as Yokohama2022;
// Set the label of the data field here.function initialize() {SimpleDataField.initialize();label = "CP to go";_yokohama = new Yokohama2022();}
// The given info object contains all the current workout// information. Calculate a value and return it in this method.// Note that compute() and onUpdate() are asynchronous, and there is no// guarantee that compute() will be called before onUpdate().function compute(info as Activity.Info) as Numeric or Duration or String or Null {// See Activity.Info in the documentation for available information.var distance = _yokohama.RemainDistanceToCP((info.elapsedDistance != null) ? info.elapsedDistance : 0.0);return _yokohama.FormatDistance(distance);}}
これと似たメソッドを計3個準備して、別のアプリとしてデバイスにインストールします。
これで準備完了
本体設定
Garminの本体取説を見ながら Data Fieldsを設定します。
3つのData Fieldsを設定します・・・・
- 次の関門までの距離 → OK
- 次の関門までの時間 → OK
- 次の関門の通過予定の余裕時間 → NG! あれ?
「Connect IQデータ項目設定を削除するか、入れ替えてください」
なん、、だと、、!
意味が分からないので調べてみると、どうもCustom Data Fieldsは2個までしか使えないらしい!
終わった・・・続く
Garmin アプリ開発(6) テスト設計
統合環境をセットアップしただけでユニットテストが準備済みなのはすばらしいですね。時代の流れを感じます。
公式:https://developer.garmin.com/connect-iq/core-topics/unit-testing/
test runner
テストを実行するには、「(:test)」アノテーションをstaticな関数につけます。
VS ccode上で、 CTRL+Shift+p でコマンドパレットを開き、Monkey C : Run Testを実行すると呼び出されます。
(:test)
function HandlerIsActive_Test1(logger as Logger) as Boolean {
print("AppBase_test1 start");
var view = new ViewMock();
....
}
逆にRun Testを実行しても、通常初めに実行されるAppBase派生クラスは実行されません。クラスの生成などはすべてこのテストランナー内で行う必要がありますので注意しましょう。
mockクラス
mock定義も同様に「(:test)」で良いみたいです(ドキュメントには書いて無さそうですが)。こちらは自動で実行されませんが、テストランナーから利用することができます。
(:test)
class ViewMock extends AdditionalTimeStopwatchView{
function initialize(){AdditionalTimeStopwatchView.initialize(); //親クラスを初期化
}
...
}
テスト設計
主に自作の2つのクラス「AdditionalTimeHandler」「StopWatch」をテストします。そのためにtestフォルダー内に2つのテストランナー、2つのクラスを作りました。
Hander_test, StopWatch_testはテストランナーです。シンプルなテストケースを多数並べておけば、順次呼び出されます。
Garmin アプリ開発(5) クラス構造 - mnob Tech blog では他にSDK派生クラスが3個(App, 入力, View)ありましたが、こいつらはテストしません。
SDK派生クラスであることからSDK依存部分がテスト困難なこと、最初っからそれを見越してシンプルに作ることで自動テストを不要にする考え方です。
View_Mockクラス
AdditionalTimeHandlerはAdditionalTimeView(SDK派生)クラスに依存しています。AdditioanlTimeViewクラスはSDK依存部分がテストしにくいので、そのままではテストできません。
そこでViewクラスを派生して、View_Mockクラスを準備します。
View_MockはGarmin SDKの呼び出しを行わないように、AdditionalTimeHandlerから呼ばれる外部APIを全部printfに置き換えてテストが動けばOKという方針です
public function setClock(clockTime){
printf("view.setClock: $1$:$2$", [clock2HourMin(clockTime), clock2Sec(clockTime)]);
}
public function setFreerun(elapsed){
printf("view.setFreerun: $1$", [elapsed2MinSec(elapsed)]);
}
....
StopWatch_testSubclass クラス
StopWatchクラスは現在のシステム時刻を取得するため、SDKのSystemに依存しています。
protected function getNow() as Number{
return System.getTimer();
}
System.getTimer()でシステムのミリ秒を取得して計算していますが、このままではテストのたびに違う値になりテストできません。
そこでt-wadaさんのこの記事を参考に対策します。現在時刻が関わるユニットテストから、テスト容易性設計を学ぶ - t-wadaのブログ
今回は「アプローチ4: 対象クラスのテスト用サブクラスをテスト内で作成 (Test-Specific Subclass)」の戦略を採用しました。
(:test)
class StopWatch_TestSubclass extends StopWatch{
private var nowStub = 0 as Number;
function initialize() {
StopWatch.initialize();
}
//override
public function getNow() as Number{
return nowStub;}
public function setNow(now as Number){
nowStub = now;
}
}
システムに依存していた getNow() メソッドをoverrideして、setNow()でセットされた値を使うサブクラスを作成しました。このサブクラスをテスト対象にすることで、この2つのメソッド以外は本体のStopWatchクラスのテストが可能になりました。
テストケースの例
(:test)
function StopWatch_Test4(logger as Logger) as Boolean {
var stw = new StopWatch_TestSubclass();
stw.setNow(1000); // 現在時刻を1秒にセット
stw.StartStop(); // Start/Stopボタンを押す
stw.setNow(2000); // 現在時刻を2秒にセット
stw.StartStop(); // Start/Stopボタンを押す
stw.setNow(3500); // 現在時刻を3.5秒にセット
return (stw.getElapsed() == 1000); // 経過時間が1秒かどうかテスト
}
このように、現在時刻をセットしてストップウォッチのテストができます
ソースコード:https://github.com/masahiro-nobori-964a-d32a/Timers11Plus/tree/main/test
(続く:ネタ一覧 Vibration, Storage, 多機種展開, 49日問題, Picker, パフォーマンスチューニング, DebugPrintf)
Garmin アプリ開発(5) クラス構造
公式のサンプル
おおよそこういうクラス構造でした。上に並んでいるSDKのクラスを派生するSampleXXX という名前の4つのクラスで出来ています。
Menuが要らなければ、最小3つのクラスでアプリが作れます。
公式サンプルの課題
作りたいアルゴリズム(いわゆるドメイン知識とかビジネスロジック)が、主に「SampleView」に入り込んでいる構造です。簡単なうちはこれでもいいですが、少し複雑なものは別のクラスに出した方がいいです。
自作アプリ
ということで、2つクラスを足して5個のクラスでアプリを作ります。
AdditionalTimeStopwathApp アプリケーション本体、全体を生成する
AdditionalTimeDelegate キー入力
AdditionalTimeView 画面表示
これらの3つのクラスはサンプルと同じくGarminのSDKの派生クラスになってます。この派生クラスはテストしにくいので、なるべくシンプルに保つように作っていきます
AdditionalTimeHandler 自作の入力と表示をハンドリングするクラス
StopWatch ストップウォッチクラス
真ん中下段にあるこの2つのクラスを足します。自分が作りたい複雑なロジックはここに入れていくことにします。
AdditionalTimeStopwathApp
SDKのAppBaseの派生クラスで、アプリが起動したときに最初に実行されます。source code
公式: https://developer.garmin.com/connect-iq/core-topics/the-application-object/
詳細なライフサイクルは公式の解説を参照するとして、ここから自分のクラスを生成する必要があります。
ここでは、自作クラスAdditionalTimeHandlerの生成を追加し、依存関係にあるクラスをDependency Injection(依存関係の外部からの注入)の考え方で、引数で与えています。
AdditionalTimeHandler(view); ←HandlerはViewに依存している
AdditionalTimeDelegate(handler): ←Delegateはhandlerに依存している
AddtionalTimeView
レイアウトされたUIパーツに文字列を設定したり、表示を切り替えたりするクラスです。source code
公式: https://developer.garmin.com/connect-iq/core-topics/user-interface/
詳細なViewクラスのライフサイクルは公式参照ですが、主なものはこちらです
View.onShow()
: 表示されたとき
View.onLayout(dc)
: レイアウトがロードされたとき
View.onUpdate()
: 表示の更新
View.onHide()
: 表示が終わった時
アプリの表示が終わるなどの重要イベントに関してはアプリ全体の動きが変わるのでAddtionalTimeHandlerに伝える必要があります。
そのためHandler → View間でcallbackの仕組みで、依存関係の逆転(DIP: dependency inversion principle)を行ってます
Handler側
//! Constructor
public function initialize(view as AdditionalTimeView) {
self._view = view;
self._stopwatch = new StopWatch();
self._viewtimer = new Timer.Timer();
view.Register(method(:onShow), method(:onHide)); // ← コールバック登録
self._lastvibrated = System.getTimer();
}
View側
//コールバック呼び出し
function onHide() as Void {
if(self._onHideCallback){
self._onHideCallback.invoke();
}
}
//コールバック登録
public function Register(onShowCallback as Method, onHideCallback as Method){
self._onShowCallback = onShowCallback;
self._onHideCallback = onHideCallback;
}
これで依存関係とは反対方向の呼び出しが可能になります
AddtionalTimeDelegate
キー入力をハンドリングするクラスです source code
公式: https://developer.garmin.com/connect-iq/core-topics/input-handling/
onKey()
onKeyPressed()
onKeyReleased()
このアプリでは上記3つだけハンドリングしています。
Pressed/Releasedはボタン長押し判定が必要な場合にハンドリングします
Garmin アプリ開発(4) 画面レイアウトとデザインを考える
プログラムのサンプルの理解ができてきたので、画面レイアウトを考えていきます。
要素
今回画面に必要な要素は
・ストップウォッチ2個
なので1画面にレイアウトできそうです。
Reference Guides にあるように、3field layoutでレイアウトするのが良さそうです。
色使い
ただしデフォルトの3 layer layoutそのままだと、白黒の寂しい画面になるので色を使ってデザインしようと思います。
ではこの機種forerunner 55(forathlete 55)はどんな描画ができるか、同じくReference Guides で調べます。
Screen Shape round
Screen Size 208 x 208
Display Colors 8
丸くて、208x208ドットで、なんと 8色 でした! (買う前に見たカタログに書いて無いよ!)
デジタル8色とは.... 1980年代パソコンみたいなデザインが必要ですね!
8色(3bit)カラーいろいろ
左が元画像、右は8色カラー化した画像。フルカラーの8色化はざらざら(誤差拡散)かのっぺり(誤差拡散なし)になってしまいます。
これだとフルカラーの高級時計などをモチーフにしたデザインは難しそうです
2020年代のデザインは単色アイコンも流行ってますし、プロのデザイナーなら8色でもかっこよく作るんでしょう
80年代ゲームの色使いでプログラミング+デザインで感心するのはザナドゥの色使い
動くキャラは2bit(白、黒、赤、透明)、背景はなんと1ビット(黄/青)の、3bit(8色を5色に減色)カラーで、これだけのデザインを実現してます!
重ね合わせ処理を省略して高速な描画を実現するため、減色覚悟で2bit / 1bit使い分け。
制約がありながらのデザイン力が素晴らしく背景1bitなのに、神殿、通過可能、乗れるブロックなどが一目で判別できます。
こういうデザインも8 colorで出来ますね。こういう色使いで行きます
画像リソース作成
お好きなビットマップエディタ(GIMP - GNU Image Manipulation Programなど)で、pngファイルを作成します。
背景画像や、アイコンを作ります。
レイアウト指定
https://developer.garmin.com/connect-iq/core-topics/layouts/
このリファレンスマニュアルを参考に、レイアウトをxmlファイルで指定します。
<layout id="MainLayout">
<bitmap id="background" x="center" y="center" filename="../../resources-round-240x240/images/timers11_240x240.png" />
<label id="StopWatchMS" x="210" y="21" font="Graphics.FONT_NUMBER_THAI_HOT" justification="Graphics.TEXT_JUSTIFY_RIGHT" color="Graphics.COLOR_WHITE" />
<label id="StopWatchCenti" x="186" y="95" font="Graphics.FONT_NUMBER_MILD" justification="Graphics.TEXT_JUSTIFY_LEFT" color="Graphics.COLOR_WHITE" />
<label id="FreeRun" x="210" y="116" font="Graphics.FONT_NUMBER_THAI_HOT" justification="Graphics.TEXT_JUSTIFY_RIGHT" color="Graphics.COLOR_WHITE" />
<label id="AdditionalTime" x="68" y="95" font="Graphics.FONT_NUMBER_MILD" justification="Graphics.TEXT_JUSTIFY_LEFT" color="Graphics.COLOR_YELLOW" />
<bitmap id="back" x="148" y="199" filename="../../resources-round-240x240/images/exit_240x240.png"/>
<bitmap id="up" x="5" y="106" filename="../../resources-round-240x240/images/setup_240x240.png"/>
</layout>
bitmap要素で背景やアイコン、label要素でストップウォッチなどの文字列の座標とフォントサイズ、配置、色を指定します。
サンプルプログラムにこれらのリソースを上書きして、↓のようなコードで label に文字列をセットすると、UIが大体できます。
_DrawableStopWatchMS = View.findDrawableById("StopWatchMS");
_DrawableStopWatchMS.setText(”88:88");
文字列はみ出さないように、サイズや座標を微調整してレイアウトの完成です
まだ全く動きませんが、UIのガワができると続きを作るやる気が出てきます
Garmin アプリ開発(3) 初めてのアプリ
公式サイトに初めてのアプリを作るための手引き↓が準備されています。
Connect IQ Basics:your first app
今回は"Sample"という名前で、「Watch App」を作る手順をなぞってみました。
Venu SQという製品をサポートリストに入れてシミュレーターを動かしてみます。
なんか動きましたね!
function onMenu() as Boolean {return true;}
Menuボタンを押すと pushViewで画面がメニューに切り替わるようです
function onMenuItem(item as Symbol) as Void {if (item == :item_1) {System.println("item 1");} else if (item == :item_2) {System.println("item 2");}}
で、MenuItenを選択すると println でデバッグコンソールにメッセージが出ました
sourceフォルダーの中身はこんな感じ
構造を見てみるとこのような関係になっていました。
SampleApp: アプリケーションの生成と、初期クラスの生成
SampleView 表示用のクラス
SampleDelegate キー入力ハンドラ
SampleManuDelegate メニュー選択の時のハンドラ
ということでアプリの基本的な形がわかってきました。
Garmin アプリ開発(2) 開発環境
開発環境の準備
アプリ開発にあたり、開発環境を準備します。
本家のサイトからSDKをダウンロードします。
Connect IQ SDK | Garmin Developers
ここにある、
の手順に従って
- The Connect IQ SDK Manager
- The Monkey C Visual Studio Code Extension
- Generating a Developer Key
を実施します
Visual Studio Code Monkey C Extensionを使うために、事前にマイクロソフトのサイトからvscodeもインストールしておきます
Visual Studio Code – コード エディター | Microsoft Azure
SDK Manager使用上の注意は、最新のSDKとして「Beta版を選んではいけない」です。
↑のキャプチャーでいう 「4.1.0 Beta」 を「Use as SDK」してはいけません。
Beta版ではあとから行う公開用コマンド「Export Project」が「ERROR: App Exporting Not Supported」というエラーで失敗します。
開発言語
開発言語は「Monkey C」という独自言語です。独自とはいえ説明によると
「Monkey Cは、他のポピュラーな言語から大きく影響を受けています。C、Java™、JavaScript、Python™、Lua、Ruby、PHPのすべてが、Monkey Cの設計に影響を与えています。これらの言語に慣れ親しんでいる方なら、Monkey Cは簡単に手に取ることができるでしょう。」
とのことで、普通のオブジェクト指向言語です。
調べながら分かった注意点は↓です。
型について
Monkey Cでは、型は実行時に解決しています。だた、コンパイル時にエラーを検出できるように as 演算子で型を指定することもできます
- 関数の戻り値の方を指定 function doSomething() as String {
- 変数の型を指定 var globalX as Lang.Number = 0;
- 変数の型の候補を指定 var globalX as Lang.Number or Lang.String = 0;
配列の書き方
var array = new [size];
var array = [1, 2, 3, 4, 5];
var array = [ [1,2], [3,4] ];
Dictionaries : 連想配列?の書き方
var dict = { "a" => 1, "b" => 2 }; // Creates a dictionary
System.println( dict["a"] ); // Prints "1"
System.println( dict["b"] ); // Prints "2"
System.println( dict["c"] ); // Prints "null"
文字列整形:formatの書き方
Lang.format("$1$:$2$", [clockTime.hour.format("%d"), clockTime.min.format("%02d")]);