Garminウォッチフェイスの作成:デジタル時計2

Garminウォッチフェイスの作成:デジタル時計で作成したデジタル時計に以下の2機能を追加。

  • 日の出、日の入りの時刻を表示
  • 高電力モードで秒を表示

画面としては次のようにした。

日の出、日の入りの時刻

いろいろと検索していたら、Arduinoで計算をしているサイトがあったので、その式を参考にさせてもらった。一応自分の家近辺であれば結構いい感じの時間が求まった。

ラジオペンチ Arduinoで日の出・日の入り時刻を計算
モノいじりが好きなおじさんの探究日誌

上記からの変更点は、日の出・日の入りを求める関数を1つにまとめた、リターン値は0時0分からの分を配列に入れて返した点。
前者は処理時間の若干の低減、後者は、その後の比較や描画処理での処理のしやすさを見込んで。

メソッドは、timeSunSetRise(緯度・経度,時間)で呼び出せる。
緯度・経度は、Position.getInfo().position.toDegrees()の結果を渡している。
時間は、Time.Gregorian.info(Time.now(), Time.FORMAT_SHORT)の結果を渡している。

緯度・経度の求め方

Garminのフォーラムを覗いて見たら、Activity.getActivityInfo().currentLocation.positionで位置を求めるという記述が多かった。
この実装を使った場合、結果として位置情報を取得することはできなかった。

そこで、センサーの情報を取得する方向に変更。フォーラム内では電力を食うのでないかという指摘があったが、使えるのがこれしか無いようだったので使ってみた。

利用当初は、パーミッションがないと、例外が出てしまったので、プロジェクトの設定を以下の様にして、実行したら、GPSの情報が取得できた。


画面上は「配置」となっていたため意味が分からなかったのだが、ポップアップされたツールチップメッセージには、GPSを使うための許可と書いてあったので、これにチェックを入れてビルドを行った。

表示の仕方


心拍数(80表示されているところ)の右側に、次の日の出、日の入り時刻を表示、画面右際に、日が見えない時間を「白線」で表したゲージのようなものを表示し、赤い線で現在の時間を表示してみた。
このゲージの表示の仕方は、Data Loverというウォッチフェイスを参考にさせてもらった。

ただ、実機で見たらゲージはちょっと見づらかった。もう少し調整が必要かもしれない。

秒の表示

onEnterSleep()とonExitSleep()を利用して、高電力モードのみ、秒の表示をするようにした。

高電力モードには、「時計を見る」という動作をするとその状態に移行する。
移行後10秒ぐらいのち、低電力モードに移行するみたいだ。ちなみのこの秒数の設定方法はわからなかった。

ソースコード

using Toybox.WatchUi;
using Toybox.Graphics;
using Toybox.System;
using Toybox.Lang;
using Toybox.Time;
using Toybox.Position;

class DigitalView extends WatchUi.WatchFace {
    var center;
    var bmBluetooth;
    var bmNotify;
    
    var fnLarge;
    var fnNormal;
    
    var fnLargeSize;
    var fnNormalSize;
	var viewSecX;
	var sunTime;
	
	const mdays = [0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]; 

	var doViewSec = false;
    
    function onEnterSleep() {
    	doViewSec = false;
    }
    
    function onExitSleep() {
    	doViewSec = true;
    }
    
    function initialize() {
        WatchFace.initialize();
        
        bmBluetooth = WatchUi.loadResource(Rez.Drawables.BluetoothIcon);
        bmNotify = WatchUi.loadResource(Rez.Drawables.NotifyIcon);
        
        fnLarge = Graphics.FONT_NUMBER_MEDIUM;
        fnNormal = Graphics.FONT_LARGE;
    }
    
    function DEG(x) {
    	return ((x) * 180.0 / Math.PI);
    }
    
    function RAD(x) {
    	return ((x) * Math.PI / 180.0);
    }

	// 近似式で太陽赤緯を求める
	function dCalc(n) {
		var w = (n + 0.5) * 2.0 * Math.PI / 365.0;              // 日付をラジアンに変換
		var d = + 0.33281
		    - 22.984 * Math.cos(w) - 0.34990 * Math.cos(2.0 * w) - 0.13980 * Math.cos(3.0 * w)
		    + 3.7872 * Math.sin(w) + 0.03250 * Math.sin(2.0 * w) + 0.07187 * Math.sin(3.0 * w);
		return RAD(d);                               // 赤緯を返す(単位はラジアン)
	}
	 
	// 近似式で均時差を求める
	function eCalc(n) {
		var w = (n + 0.5) * 2 * Math.PI / 365.0;              // 日付をラジアンに換算
		var e = + 0.0072 * Math.cos(w) - 0.0528 * Math.cos(2.0 * w) - 0.0012 * Math.cos(3.0 * w)
		    - 0.1229 * Math.sin(w) - 0.1565 * Math.sin(2.0 * w) - 0.0041 * Math.sin(3.0 * w);
		return e;                                    // 均一時差を返す(単位は時)
	}
	
	function from11Days(n) {
		var sumDays = n.day - 1;
		for (var i = 0; i < n.month; i++) {
			sumDays += mdays[i];
		}
		if (n.month > 2 && (n.year % 4) == 0 && (n.year % 100) != 0) {
			sumDays++;
		}
		return sumDays;
	}
	
	// 日の出時刻を求める関数
	function timeSunSetRise(xy, n) {
		var y = RAD(xy[0]);
		var d = dCalc(from11Days(n));
		var e = eCalc(from11Days(n));
		// 太陽の時角幅を求める(視半径、大気差などを補正 (-0.899度) )
		var t = DEG(Math.acos( (Math.sin(RAD(-0.899)) - Math.sin(d) * Math.sin(y)) / (Math.cos(d) * Math.cos(y)) ) );
		return [((( -t + 180.0 - xy[1] + 135.0) / 15.0 - e) * 60).toLong(), ((( t + 180.0 - xy[1] + 135.0) / 15.0 - e) * 60).toLong()]; // 日の出, 日の入り時刻を返す
	}

    // Load your resources here
    function onLayout(dc) {
        center = [dc.getWidth()/2, dc.getHeight()/2];
        fnLargeSize = [Graphics.getFontAscent(fnLarge), dc.getFontHeight(fnLarge)];
        fnNormalSize = [Graphics.getFontAscent(fnNormal), dc.getFontHeight(fnNormal)];

		var widthLarge = dc.getTextWidthInPixels("00:00", fnLarge) + 2;
		var widthNormal = dc.getTextWidthInPixels("00", fnNormal) + 2;
		viewSecX = center[0] + (widthLarge + widthNormal) / 2.0 - widthNormal;
    }

    // 時間・日付を描画する
    function drawTime(dc, timeM, timeS) {
    	var y = center[1] - fnLargeSize[0];
    	if (doViewSec) {
	    	dc.drawText(viewSecX - 2, y,
	    		fnLarge, timeS.hour + ":" + timeS.min.format("%02d"), Graphics.TEXT_JUSTIFY_RIGHT);
	    	dc.drawText(viewSecX + 2, center[1] - fnNormalSize[0],
	    		fnNormal, timeS.sec.format("%02d"), Graphics.TEXT_JUSTIFY_LEFT);
    	}
    	else {
	    	dc.drawText(center[0], y,
	    		fnLarge, timeS.hour + ":" + timeS.min.format("%02d"), Graphics.TEXT_JUSTIFY_CENTER);
    	}
    	dc.drawText(center[0], y - fnNormalSize[0],
    		fnNormal, timeS.month + "/" + timeS.day + " " + timeM.day_of_week,
    		Graphics.TEXT_JUSTIFY_CENTER);
    }
    
    // バッテリーパーセンテージを表示
    function drawBattery(dc) {
        dc.setColor(Graphics.COLOR_GREEN, Graphics.COLOR_BLACK);
    	dc.drawText(center[0], dc.getHeight() - fnNormalSize[1],
    		fnNormal, System.getSystemStats().battery.format("%3.0f") + "%",
    		Graphics.TEXT_JUSTIFY_CENTER);
    }

	// 心拍数を求める
    private function retrieveHeartrateText() {
		var currentHeartrate = ActivityMonitor.getHeartRateHistory(1, true).next().heartRate;
		if (currentHeartrate != ActivityMonitor.INVALID_HR_SAMPLE) {
			return currentHeartrate.format("%d");
		}
		else {
			return "---";
		}
    }    
    
    // 歩数・心拍数などの情報を表示
    function drawInformation(dc) {
        var info = ActivityMonitor.getInfo();
        // 歩数
    	dc.drawText(center[0], center[1],
    		fnNormal, info.stepGoal.format("%5d") + "/" + info.steps.format("%5d"),
    		Graphics.TEXT_JUSTIFY_CENTER);
		// 心拍数
    	dc.drawText(center[0] - 5, center[1] + fnNormalSize[1],
    		fnNormal, retrieveHeartrateText(),
    		Graphics.TEXT_JUSTIFY_RIGHT);
    }
    
    // Bluetoothとイベント通知アイコンの表示
    function drawNotify(dc) {
    	var info = System.getDeviceSettings();
    	if (info.notificationCount > 0) {
    		// メールアイコンの表示
    		dc.drawBitmap(5, center[1] + fnNormalSize[1] * 0.5 - bmNotify.getHeight() / 2 + 2, bmNotify);
    	}
    	if (info.phoneConnected) {
    		// BLEアイコンの表示
	    	var y = center[1] + fnNormalSize[1] * 1.5;
    		var wh = [bmBluetooth.getWidth() / 2, bmBluetooth.getHeight() / 2];
    		var tl = [20, y - wh[1]];
    		dc.setColor(Graphics.COLOR_DK_BLUE, Graphics.COLOR_BLACK);
    		dc.fillEllipse(tl[0] + 12, y, wh[0] - 1, wh[1] - 1);
    		dc.drawBitmap(tl[0], tl[1], bmBluetooth);
    	}
    }
    
    // 日の出・日の入りに関する情報を表示する
    function drawSun(dc, time) {        
        var info = Position.getInfo();
        if (info != null && info has :position) {
        	var work = info.position.toDegrees();
        	System.println(work[0] + "," + work[1]);
        	var sunTime = timeSunSetRise(work, time);
	        
	        // 日が出ていない時間を白の円弧で表示
	        dc.setPenWidth(2);
	        dc.setColor(Graphics.COLOR_WHITE, Graphics.COLOR_BLACK);
	        var radius = center[0] - 1;
	        var start = 60.0;
	        dc.drawArc(center[0], center[1], radius, Graphics.ARC_CLOCKWISE,
	        	start, start - 5.0 * sunTime[0] / 60.0);
	    	dc.drawArc(center[0], center[1], radius, Graphics.ARC_CLOCKWISE,
	    		start - 5.0 * sunTime[1] / 60.0, 300.0);
			// 3時間の分割位置を表示
			var arrowAngle = (30.0 + 5 * (time.hour + time.min / 60.0)) / 180.0 * Math.PI;
	        radius = center[0] - 6;
	        dc.setPenWidth(1);
	        dc.setColor(Graphics.COLOR_GREEN, Graphics.COLOR_BLACK);
	        for (var i = 0; i < 9; i++) {
				var arrowAngle = (30.0 + 15.0 * i) / 180.0 * Math.PI;
				dc.drawLine(center[0] + Math.sin(arrowAngle) * center[0], center[1] - Math.cos(arrowAngle) * center[0],
					center[0] + Math.sin(arrowAngle) * radius, center[1] - Math.cos(arrowAngle) * radius);
	        }
	        // 現在時刻を示す針を表示
	        radius = center[0] - 1;
	        dc.setPenWidth(3);
	        dc.setColor(Graphics.COLOR_RED, Graphics.COLOR_BLACK);
			dc.drawLine(center[0] + Math.sin(arrowAngle) * (radius - 5), center[1] - Math.cos(arrowAngle) * (radius - 5),
				center[0] + Math.sin(arrowAngle) * radius, center[1] - Math.cos(arrowAngle) * radius);
	        dc.setPenWidth(1);
	        
	        // 次の日の出・日の入りの時刻を表示
	        dc.setColor(Graphics.COLOR_WHITE, Graphics.COLOR_BLACK);
	        var now = time.hour * 60 + time.min;
	        var str = "";
	        if (now < sunTime[0] || sunTime[1] <= now) {
	        	// 今は夜なので次の日の出を表示(にしたいが今は今日の日の出になっている)
	        	time.day++;
	        	sunTime = timeSunSetRise(work, time);
	        	str = (sunTime[0] / 60).format("%2d") + ":" + (sunTime[0] % 60).format("%2d");
	        }
	        else {
	        	// 今は日中なので次の日の入りを表示
	        	str = (sunTime[1] / 60).format("%2d") + ":" + (sunTime[1] % 60).format("%2d");
	        }
	    	dc.drawText(center[0] + 20, center[1] + fnNormalSize[1] * 2.0 - dc.getFontHeight(Graphics.FONT_SMALL),
	    		Graphics.FONT_SMALL, str, Graphics.TEXT_JUSTIFY_LEFT);
		}
    }

    // Update the view
    function onUpdate(dc) {
        dc.setColor(Graphics.COLOR_BLACK, Graphics.COLOR_BLACK);
    	dc.clear();
    	
    	var now = Time.now();
        var nowM = Time.Gregorian.info(now, Time.FORMAT_MEDIUM);
        var nowS = Time.Gregorian.info(now, Time.FORMAT_SHORT);
        dc.setColor(Graphics.COLOR_WHITE, Graphics.COLOR_BLACK);
        drawTime(dc, nowM, nowS);        
        drawInformation(dc);
        drawNotify(dc);		// 色の変更有
        
        drawSun(dc, nowS);	// 色・幅の変更有
        
        drawBattery(dc);	// 色の変更有
    }

    function onPartialUpdate( dc ) {
    	// 計測したい処理を実行する
    	onUpdate(dc);
    }
}

追記(1/14)

sin/cosの使用位置がx,yで逆だった。恥ずかしい。
ソースコードは面倒なので、修正しない予定。

コメント

タイトルとURLをコピーしました