Friday, 2 December 2016

Garmin ConnectIQ - First Attempt at Using the SDK

The Garmin Forerunner 910XT that I have previously blogged about using died a death a couple of weeks ago so I decided to buy a Garmin Fenix 3 which has roughly the same feature set.  The big selling point for a geek like me was the Connect IQ capability that means you can write apps for the watch.  Geektastic!

Overall ConnectIQ let's you write:

  • Fully blown apps
  • Watch faces
  • Widgets
  • Data fields

...so really customising your Garmin product.

Some resources I used to get myself setup:


In particular I spent a long time going through the Getting Started section of the Programmer's Guide.  I'm generally pretty gung ho and like to go it alone but it was worth going through this slowly step-by-step.  I chose to use Eclipse (Luna) and followed the tutorial to produce a simple watch face and load it into the emulator. I won't repeat the steps here as the ones Garmin provide are extremely good.

I then decided to modify the watch face in some way to learn more about ConnectIQ.  My idea was to provide a "countdown until parkrun" watch face.  Keen readers will know I like a bit of parkrun so I decided to do a watch face that both shows you the current time and, as time passes, counts down the days, minutes, hours and seconds until parkrun.

To modify the watch face I just had to change two files within the Eclipse project.  These are selected on the Project Explorer view below.

In simple terms, layout.xml defines the layout of the watch face and the xxxView.mc file contains the code required to modify aspects of the watch face.

For the layout I decided to have a simple one of:

  • The current time at the top
  • Then a count down until parkrun
  • Then some text to say "until parkrun"
  • Then some form of "motivational" slogan

The layout.xml for this looks like:

<layout id="WatchFace">
    <label id="TimeLabel" x="center" y="50" font="Gfx.FONT_LARGE" justification="Gfx.TEXT_JUSTIFY_CENTER" color="Gfx.COLOR_BLUE" />
    <label id="TimeToParkrunLabel" x="center" y ="100" font="Gfx.FONT_SMALL" justification="Gfx.TEXT_JUSTIFY_CENTER" color="Gfx.COLOR_RED" />
    <label id="ParkrunTextLabel" x="center" y ="125" font="Gfx.FONT_SMALL" justification="Gfx.TEXT_JUSTIFY_CENTER" color="Gfx.COLOR_RED" /> 
    <label id="SloganTextLabel" x="center" y ="160" font="Gfx.FONT_SMALL" justification="Gfx.TEXT_JUSTIFY_CENTER" color="Gfx.COLOR_GREEN" />
</layout>

Here we see four labels with their name, position on screen, font size and colour defined.

Without changing anything in the view.mc file, this function references the layout file and tells ConnectIQ what to draw on screen:

function initialize() {
        WatchFace.initialize();
    }

    // Load your resources here
    function onLayout(dc) {
        setLayout(Rez.Layouts.WatchFace(dc));
    } 

Where the WatchFace reference links to the layout.xml contents.

The first function I changed was the "onUpdate" one.  Here it is:

    // Update the view
    function onUpdate(dc) {
        // Get and show the current time
        var clockTime = Sys.getClockTime();
        var timeString = Lang.format("$1$:$2$:$3$", [clockTime.hour, clockTime.min.format("%02d"),clockTime.sec.format("%02d")]);
        var view = View.findDrawableById("TimeLabel");
        view.setText(timeString);
        
        //Time to park run
        var TimeToParkrunStr = CalcTimeToParkrun();
        var ParkrunTimeView = View.findDrawableById("TimeToParkrunLabel");
        ParkrunTimeView.setText(TimeToParkrunStr);
        
        //Added by Paul Weeks
        var ParkrunLabelStr = "...until next parkrun!";
        var ParkrunLabelView = View.findDrawableById("ParkrunTextLabel");
        ParkrunLabelView.setText(ParkrunLabelStr);
        
        //This is a slogan at the bottom.  Could change dynamically in future
        var SloganStr = "#DFYB";
        var SloganLabelView = View.findDrawableById("SloganTextLabel");
        SloganLabelView.setText(SloganStr);
        
        // Call the parent onUpdate function to redraw the layout
        View.onUpdate(dc);
    }

Here we can see some simple concepts that show how to update the screen and show the time.  Breaking it down:

1)Create a variable to hold the current time:
var clockTime = Sys.getClockTime();

2)Turn this into a string.  The Lang.format method can be used for this.  Here we see it used to create a string with three components (referenced as $1$:$2$:$3$) formed from the hour, second and min part of the time.  The %02d part simply puts a leading zero on to pad numbers less than 10:

var timeString = Lang.format("$1$:$2$:$3$", [clockTime.hour, clockTime.min.format("%02d"),clockTime.sec.format("%02d")]);

3)Creating a "view variable" that links to layout.xml and then updating this with the time text:
var view = View.findDrawableById("TimeLabel");
view.setText(timeString);

But all the heavy lifting I did was related to the "countdown until parkrun" part.  Here you can see I call out to another function called "CalcTimeToParkrun".  It was really slow going writing this!

Unlike with other languages like Python there's not a mass of examples an tutorials on the internet or entries on Stack Overflow.  Instead it was a case of using the API reference and trial and error to get things working.  I learnt a lot!

Here's the function:

//Calculate time until parkrun
//Algorithm may be a little clunky.  Refine over time
function CalcTimeToParkrun() {
  //Some constants
  var parkrunDay = 7;     //Parkrun on Saturday.  Could be setting in future
  var parkrunTime = 9;    //Parkrun at 0900.  Could be setting in future
        
  //Need to calculate the next parkrun day at parkrun time and then work out the difference between then and now.  
  var now = Time.now();
  var info = Calendar.info(now, Time.FORMAT_SHORT);
        
  //Format_short means day of week is a number.  1 for Sunday, 2 for Monday etc.
  var dayStr = Lang.format("$1$", [info.day_of_week]);   
  var hourStr = Lang.format("$1$", [info.hour]);
        
  //Might be useful, shows how to format a date
  //var dateStr = Lang.format("$1$ $2$ $3$", [info.day_of_week, info.month, info.day]);
        
  //Have a look at the day of week.  The actual day is a special day as parkrun might either be that day or next week.  
  //Saturday is day 7.
  var dayNum = dayStr.toNumber();   //Turn day to an actual number
  var hourNum = hourStr.toNumber(); //Turn hour into actual number
        
  //What we need to do is calculate how many days to parkrun.  Saturday is the key case, i.e. assessing before or after
  //parkrun time.
  var daysToPR = -1;
  if ((dayNum < parkrunDay) || ((dayNum == parkrunDay) && (hourNum < parkrunTime))){
     daysToPR = 7 - dayNum;
     }
  else {
     daysToPR = 7;
     }
          
  //Create a moment that represents midnight today
  var todayDict = {:day => info.day.toNumber(), :month => info.month.toNumber(), :year => info.year.toNumber()};
  var todayMoment = Calendar.moment(todayDict);
          
  //Create a duration of the number of days until parkrun + hours until parkrun
  var durDict = {:days => daysToPR, :hours => parkrunTime};          
  var myDuration = Calendar.duration(durDict);
          

  //Add the number of days and hours to midnight today to get a moment that represents parkrun start time
  var parkrunMoment = myDuration.add(todayMoment);
         
  //Subtract now from when parkrun is to get a duration until parkrun.  .value turns it into seconds
  var durationTillParkrun = parkrunMoment.subtract(now).value(); 
          
  //So now we have seconds until parkrun. Need to turn into days, hours and mins
  //This seems like hard yards but can't find a better way...
  var daysTillParkrun = durationTillParkrun / 86400;
  var hoursTillParkrun = (durationTillParkrun - (daysTillParkrun * 86400)) / 3600; 
  var minsTillParkrun = (durationTillParkrun - ((daysTillParkrun * 86400)+(hoursTillParkrun * 3600))) / 60;
  var secsTillParkrun = (durationTillParkrun - ((daysTillParkrun * 86400)+(hoursTillParkrun * 3600)+(minsTillParkrun * 60)));
                                        
  //Return 
  return daysTillParkrun.toString() + " days " + hoursTillParkrun.toString() + ":" + minsTillParkrun.toString() + ":" + secsTillParkrun.toString();  
}

The algorithm is pretty simple.  It's as follows:

  • Get a number associated with the day of week.  So Sunday = 1, Monday = 2 etc.
  • Calculate "days until parkrun".  In general this is 7 - Day of week number.  However there's an extra decision to make on parkrun day as to whether it's before or after parkrun (and so mere minutes to go or several days).
  • Calculate a duration* which is from midnight today plus the number of full days until parkrun plus the number of hours to wait on parkrun day.
  • Calculate a moment** which is the actual date and time of parkrun.
  • Calculate the difference between now and the date and time of parkrun.

*A duration is a period of time in Garmin Connect IQ.  So these two lines of code define a duration:
var durDict = {:days => daysToPR, :hours => parkrunTime};          
var myDuration = Calendar.duration(durDict);

So the dictionary defines the number of days and hours for the duration, then the duration is calculated.

**A moment is a moment in time in Garmin Connect IQ.  These two lines of code define a moment:
  var todayDict = {:day => info.day.toNumber(), :month => info.month.toNumber(), :year => info.year.toNumber()};
  var todayMoment = Calendar.moment(todayDict); 

Again the dictionary defines the parameters for the moment and then the moment is created.

You can then do calculations based upon durations and moments:
var parkrunMoment = myDuration.add(todayMoment);

..and so the big reveal, how does it look in the Eclipse simulator?


...hmmm, yes you're right, somewhere between awful and terrible!

Playing with the layout file I changed it to be:

<layout id="WatchFace">
  <label id="TimeLabel" x="center" y="15" font="Gfx.FONT_NUMBER_THAI_HOT" justification="Gfx.TEXT_JUSTIFY_CENTER" color="Gfx.COLOR_WHITE" />
  <label id="TimeToParkrunLabel" x="center" y ="115" font="Gfx.FONT_MEDIUM" justification="Gfx.TEXT_JUSTIFY_CENTER" color="Gfx.COLOR_WHITE" />
  <label id="ParkrunTextLabel" x="center" y ="140" font="Gfx.FONT_MEDIUM" justification="Gfx.TEXT_JUSTIFY_CENTER" color="Gfx.COLOR_WHITE" /> 
  <label id="SloganTextLabel" x="center" y ="175" font="Gfx.FONT_SMALL" justification="Gfx.TEXT_JUSTIFY_CENTER" color="Gfx.COLOR_WHITE" />
</layout>

So all white text and bigger fonts.  The "THAI_HOT" font is a built in Garmin font.  I also played with the simulator settings to specify that a Fenix 3 simulator be used:


...and now it looks a lot better in the simulator:


...and it looks vaguely OK on my wrist after side-loading the application!


So what have I learnt:
  • Real estate is at a premium on a Garmin watch.  Just 218 x 218 pixels to play with!
  • You need to think very carefully as to what to put on the very small screen.
  • I need to explore sleep mode as the watch counts down the seconds for about 10 seconds then stops and just updates once a minute.  Lots of wrist waggling is required to get it to start counting down again.  I know this is to save battery but the default watch faces both have a seconds component so it must be possible to get a continuous countdown.





No comments:

Post a Comment