Garminウォッチフェイスの作成:デジタル時計で作成したデジタル時計に以下の2機能を追加。
- 日の出、日の入りの時刻を表示
- 高電力モードで秒を表示
画面としては次のようにした。
- 2024/04/06 追記
GPS位置情報の取り出し方法と日の出日の入り情報の計算の仕方について補足を追記。
日の出、日の入りの時刻
いろいろと検索していたら、Arduinoで計算をしているサイトがあったので、その式を参考にさせてもらった。一応自分の家近辺であれば結構いい感じの時間が求まった。
上記からの変更点は、日の出・日の入りを求める関数を1つにまとめた、リターン値は0時0分からの分を配列に入れて返した点。
前者は処理時間の若干の低減、後者は、その後の比較や描画処理での処理のしやすさを見込んで。
メソッドは、timeSunSetRise(緯度・経度,時間)で呼び出せる。
緯度・経度は、Position.getInfo().position.toDegrees()
の結果を渡している。
時間は、Time.Gregorian.info(Time.now(), Time.FORMAT_SHORT)
の結果を渡している。
- 2024/04/06 追記
日の出・日の入り時刻の取得は、Toybox.Weatherライブラリに追加された。
Toybox.Weather.CurrentConditions.getSunriseとgetSunsetメソッド。
それぞれ、位置と日付を与えることで計算結果が出力される。
必要なAPI Levelは3.3.0。
緯度・経度の求め方
Garminのフォーラムを覗いて見たら、Activity.getActivityInfo().currentLocation.position
で位置を求めるという記述が多かった。
この実装を使った場合、結果として位置情報を取得することはできなかった。
そこで、センサーの情報を取得する方向に変更。フォーラム内では電力を食うのでないかという指摘があったが、使えるのがこれしか無いようだったので使ってみた。
利用当初は、パーミッションがないと、例外が出てしまったので、プロジェクトの設定を以下の様にして、実行したら、GPSの情報が取得できた。
画面上は「配置」となっていたため意味が分からなかったのだが、ポップアップされたツールチップメッセージには、GPSを使うための許可と書いてあったので、これにチェックを入れてビルドを行った。
- 2024/04/06 補足
Best way to get the current location?に質問事項があった。
これによると以下の様な感じになるらしい。- Toybox.Position.getInfo(利用可能な場合)
最後にGPSにアクセスした際の位置 - Toybox.Activity.getActivityInfo().currentLocation(それ以外の場合)
最後のアクティビティの位置 - Toybox.Weather.getCurrentConditions().weather.observationLocationPosition
携帯(スマホ)の位置
WeatherライブラリはAPI Levelの縛りがあるので、注意が必要とのこと。
- Toybox.Position.getInfo(利用可能な場合)
表示の仕方
心拍数(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で逆だった。恥ずかしい。
ソースコードは面倒なので、修正しない予定。
コメント