domingo, 17 de enero de 2016

Open source Connect IQ face for Garmin watches - Analog & Bars

Download Analog & Bars [source code | prg for vivoactive | Garmin store]
Download Analog & Skies [source code | prg for vivoactive | Garmin store]

Garmin empowers their watches by providing the Connect IQ SDK so that we can program our own apps, faces, etc. However, there's little documentation, and even almost no Open Source apps. So, let me publish (under GNU GPL v3.0) a watch face I wrote: Analog & Bars [source code | prg for vivoactive | Garmin store]. Maybe you can improve it and publish your own.



  • Note on the Garmin Vivoactive device: It is a thin and comfortable watch designed for tracking sport activities and displaying some basic information. Do not expect anything similar to an Android operating system. It is very basic and very well optimized for its purposes. The battery lasts for weeks. The only drawback it might have is the bluetooth connection with your phone. I never managed to connect it to my own Galaxy Note 3 phone, which is among the supported ones (I factory reset it to make sure, plus it works with other bluetooth devices). The Garmin tech service was very patient but all they could say is they don't know why a supported phone doesn't work with it. I asked them what happens when Android phones get their OS updated: does Garmin check if the connection to that device continues to work? They don't. I have to say, my Garmin watch did connect to many other phones, including some which are not officially supported by Garmin. If your phone is an unlucky one, each time you charge it, it will transmit the tracking data to your Garmin account. Unless you only have Linux at home, for which Garmin does not provide support.

The app may be available on the Garmin Connect Apps site, uploaded by myself or by someone else with their own modifications. I will not maintain it and I will not fix bugs, I will not add new functionalities, and I will not compile it for different devices.

I tested it on Garmin Vivoactive. You should be able to compile it for other square watches but the background is stored as a PNG with the Vivoactive screen size . This should not be a big issue, as I provide Matlab/Octave code for generating the background image. It is transparent, which is pretty convenient if you want to display something behind the minute ticks. Just display the background in last place, as I do in my app.

% vivoactive watch face background generation. Matlab / Octave
% Boyan Bonev, January 2016
function watchface()

im = uint8(zeros(148,205));


%hours
cx = 205/2; cy = 148/2+0.5;
r1 = 67.5; r2 = 150;
%im = ticks(im,12, r1, r2, cy, cx, [-0.01 -0.005 0 0.005 0.01 ]);
im = ticks(im,12, r1, r2, [cy cy-1 cy+1], [cx cx-1 cx+1], 0);
im(3:end-2,cx-1:cx+1) = 0;
m=16;
im(1+m:148-m,1+m:205-m) = 0;
imhours = im;

%minutes
r1 = 66.5; r2 = 150;
im = uint8(zeros(148,205));
im = ticks(im,12*5, r1, r2, cy, cx, [-0.006 -0.005 0 0.005 0.006]);
m=10;
im(1+m:148-m,1+m:205-m) = 0;
r1 = 100; r2 = 150;
im = ticks(im,2, r1, r2, cy, cx, [-0.005, 0, 0.005]+pi/6/5*9);
im = ticks(im,2, r1, r2, cy, cx, [-0.005, 0, 0.005]+pi/6/5*21);
im(3:end-2,cx-1:cx+1) = 0;
immins = im;

imr = imhours;
img = imhours;
imb = imhours;
imr(immins==255) = 255;

rgb = cat(3,imr,img,imb);
imwrite(rgb,'face.png');

im2 = immins/255;
im2(imhours==255) = 2;
%imagesc(im2);
cmap = [0 0 0; 1 0 0; 1 1 1];
imwrite(im2, cmap, 'face2.png', 'png', 'Transparency', 0);

end % of function

function imout = ticks(im,n,r1,r2, cys, cxs, offsets)
    for offset = offsets
        for i=0:n-1
            angle = 2*pi/n*i + offset-pi/2;

            for r=r1:r2
                x0 = cos(angle)*r;
                y0 = sin(angle)*r;
                for cy = cys
                    for cx = cxs
                        x = round(x0+cx);
                        y = round(y0+cy);
                        if y>0 && x>0 && y<=148 && x<=205
                            im(y,x) = 255;
                        end
                    end
                end
            end
        end
    end
    imout = im;
end % of function
You don't need to run this code if you are going to use the Vivoactive device. Just download the result:





Due to the transparency, you may not see the white hour ticks. Fill a black rectangle of the size of the watch screen before you display it, and you'll see the hours and minutes ticks like in the app's screenshot on the top.


In this watch face, the bars represent the steps history for the last week. The ones in red are weekend days. The bar in gray is the goal, and the gray digits on its right are the number of steps the goal bar represents. The blue symbols represent the memory, battery, sound, vibration, phone connection, alarms, and the ones you don't see here are the ones for notifications and for sleep mode. They were all included in a font which I generated for the purpose. See the Connect IQ documentation for generating fonts. Here is how the app's fonts look like:

 

They come with a text file indicating the coordinates of each character. Bitmap Font Generator is a freeware that generates them, recommended by Garmin's Connect IQ guide. My antivirus complained about it, but I believe that was a false positive. I was able to run it in Linux, with Wine. (I didn't try to run the Connect IQ SDK on Linux). Generate the bitmaps with 128x128 size. You don't need to smooth them, as the Garmin device will display only a few colors anyway. Generate their descriptor as a text file. Finally, don't forget to include the fonts in the resources.xml file of your project. I used the filter option because I don't use all the symbols.

A good programming practice would have been to extract all the strings to the strings.xml file. It makes it easier to keep the app multilanguage.

And here goes the code. I've zipped the whole project here: source code. By the way, code is code both in singular and plural. Unless you mean "access codes".



// Boyan Bonev, January 2016

//

// This app needs the Connect IQ SDK to be compiled.

// It is designed for the Vivoactive watch by Garmin.

// It may work for other platforms, but you need to

// compile it yourself.

// I will NOT maintain it and I will NOT respond to inquiries

// for upgrades or bug fixes. However, feel free to

// modify it and distribute it, as long as you keep it Free Software.

// You may also acknowledge the original author if you feel like doing it.

//

// The present code is licensed under the GNU GPL V3.0 license http://www.gnu.org/licenses/gpl.txt

// This doesn't apply to the fonts and other resources.

//

// If you have questions about Connect IQ and C Monkey, please,

// make them public via the Garmin Forum.

//





using Toybox.WatchUi as Ui;

using Toybox.Graphics as Gfx;

using Toybox.System as Sys;

using Toybox.Lang as Lang;

using Toybox.Sensor as Sens;

using Toybox.ActivityMonitor as Act;

using Toybox.Attention as Att;

using Toybox.Math as Math;

using Toybox.Time as Time;

using Toybox.Time.Gregorian as Greg;



class AnalogBarsView extends Ui.WatchFace {



       var font, font2, fontnumbers, fsyms;

       var h, w, h2, w2;



    function initialize() {

        WatchFace.initialize();

        font2 = Ui.loadResource(Rez.Fonts.sawade20);

        fontnumbers = Ui.loadResource(Rez.Fonts.sawade35);

        fsyms = Ui.loadResource(Rez.Fonts.typicons22);

    }



    //! Load your resources here

    function onLayout(dc) {

       // setLayout(Rez.Layouts.WatchFace(dc));

    }



    //! Called when this View is brought to the foreground. Restore

    //! the state of this View and prepare it to be shown. This includes

    //! loading resources into memory.

    function onShow() {

    }



    //! Update the view

    function onUpdate(dc) {

              w = dc.getWidth();

              h = dc.getHeight();

              h2 = h/2;

              w2 = w/2;

              var n;



             

              var dateinfo = Greg.info(Time.now(), Time.FORMAT_SHORT);

              //Sys.println(dateinfo.day_of_week);

              var daynames = ["U","M","T","W","R","F","S"];

             

             

              // Clear the screen

              dc.setColor(Gfx.COLOR_BLACK, Gfx.COLOR_WHITE);

              dc.fillRectangle(0,0,w,h);





              var settings = Sys.getDeviceSettings();

              var systemStats = Sys.getSystemStats();

              var actinfo = Act.getInfo();



              // Plot Bars

             

              var stepsnorm = actinfo.stepGoal * 3;

              if (stepsnorm <= 0){

                     stepsnorm = 10000;

              }

              var steps = actinfo.steps;

              if (steps > stepsnorm) {

                     stepsnorm = steps;

              }



              // steps history

              var hist = Act.getHistory();

              var color;

              var dn;

              for (n=0;n<hist.size();n++){

                     dn = (dateinfo.day_of_week+5-n) % 7;

                     if (dn == 6 || dn == 0) { color = Gfx.COLOR_RED;}

                     else { color = Gfx.COLOR_GREEN;}

                     bar(dc, 13-n, 1.0*hist[n].steps / stepsnorm,

                     color, daynames[dn]);

              }

             

              // steps today

              dn = dateinfo.day_of_week-1;

              if (dn == 6 || dn == 0) { color = Gfx.COLOR_RED;}

                     else { color = Gfx.COLOR_GREEN;}

              bar(dc, 14, 1.0*steps / stepsnorm,

              color, daynames[dn], true);

             

              // goal

              bar(dc, 15, 1.0*actinfo.stepGoal / stepsnorm,

              Gfx.COLOR_DK_GRAY, "g");

              var s = actinfo.stepGoal;

              var sn;

              var pos = -1;

              for (n=10000;n>0;n=n/10) {

                     sn = s/n;

                     if (pos == -1) {

                           if (sn > 0) { // start printing

                                  pos = 82;

                           } else {

                                  continue;

                           }

                     }

                     dc.drawText(w-23, pos, font2, ""+sn, Gfx.TEXT_JUSTIFY_CENTER);

                     s = s - sn*n;

                     pos = pos + 11;

              }





              // write the date

              var months = ["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"];

              var weekdays = ["Sun","Mon","Tue","Wed","Thu","Fri","Sat"];

              dc.setColor(Gfx.COLOR_GREEN, Gfx.COLOR_TRANSPARENT);

              dc.drawText(35, 20, font2,

              weekdays[dateinfo.day_of_week-1],

              Gfx.TEXT_JUSTIFY_CENTER);

              dc.drawText(35, 32, font2,

              months[dateinfo.month-1]+ " "+ dateinfo.day,

              Gfx.TEXT_JUSTIFY_CENTER);

              dc.drawText(35, 44, font2,

              ""+ dateinfo.year,

              Gfx.TEXT_JUSTIFY_CENTER);









              // Plot Symbols



              //a:sun b:battery c:battery d:battery e:battery f:bell g:vibrate h:vibrate

              //i:bulb j:gps k:bubble l:bubbles m:notes n:notes o:phone p:phone q:chrono

              //r:thumbsdown s:thumbsup t:crossed u:user v:sound w:nosound

              //Check the pictures in resources/fonts/typicons22_0.png

              //Be aware that the characters loading filter is set in resource.xml

       

              var flags = "";

              if (settings.tonesOn) {flags = flags + "v";} else {flags = flags + "w";}

              if (settings.vibrateOn) {flags = flags + "h";}

              if (settings.phoneConnected) {flags = flags + "p";}

              if (settings.notificationCount>0) {flags = flags + "l";}

              if (settings.alarmCount>0) {flags = flags + "f";}

              if (actinfo.moveBarLevel>Act.MOVE_BAR_LEVEL_MIN) {flags = flags + "q";}

              if (actinfo.isSleepMode) {flags = flags + "i";}

       

              dc.setColor(Gfx.COLOR_BLUE, Gfx.COLOR_TRANSPARENT);

              dc.drawText(17, h-37, fsyms, flags,  Gfx.TEXT_JUSTIFY_LEFT);

             

              // Battery

              var battery = systemStats.battery;

              dc.drawText(17, h-53, fsyms, "d",  Gfx.TEXT_JUSTIFY_LEFT);

              dc.fillRectangle(17+3,h-44, 11.0*battery/100, 7);

              dc.drawText(17+19, h-52, font2, battery.format("%d") + "%", Gfx.TEXT_JUSTIFY_LEFT);



              // Memory

              //memory usage

              var mem = 100.0*systemStats.usedMemory/systemStats.totalMemory;

              dc.drawText(17, h-68, fsyms, "e",  Gfx.TEXT_JUSTIFY_LEFT);

              dc.drawText(17+19, h-66, font2, mem.format("%d") + "%", Gfx.TEXT_JUSTIFY_LEFT);



       

              // Write calories, distance

              var dist1 = actinfo.distance/160934.0;

              var dist2 = actinfo.distance/100000.0;

              dc.drawText(w-20, 20, font2, steps + " steps", Gfx.TEXT_JUSTIFY_RIGHT);

              dc.drawText(w-20, 32, font2, dist1.format("%0.1f") +" mi / "+ dist2.format("%0.1f") + " km", Gfx.TEXT_JUSTIFY_RIGHT);

              dc.drawText(w-20, 44, font2, actinfo.calories + " kcal", Gfx.TEXT_JUSTIFY_RIGHT);

       

       

              // plot the background with transparency

              dc.drawBitmap(0, 0, Ui.loadResource(Rez.Drawables.background));





       

              // write the clock numbers

              dc.setColor(Gfx.COLOR_WHITE, Gfx.COLOR_TRANSPARENT);

              dc.drawText(w2+1, -6, fontnumbers, ""+12, Gfx.TEXT_JUSTIFY_CENTER);

              dc.drawText(w2, h-30, fontnumbers, ""+6, Gfx.TEXT_JUSTIFY_CENTER);

              dc.drawText(17, h2-17, fontnumbers, ""+9, Gfx.TEXT_JUSTIFY_LEFT);

              dc.drawText(w-17, h2-17, fontnumbers, ""+3, Gfx.TEXT_JUSTIFY_RIGHT);

       

       

              // Get and show the current time

              var clockTime = Sys.getClockTime();


 
              // Draw hands

              var alpha, r, r2, hand;

             

              // hours

              alpha = Math.PI/6*(1.0*clockTime.hour+clockTime.min/60.0);

              r = 50;

              r2 = 12;

              hand =         [[w2,h2],

                                         [w2+r2*Math.sin(alpha-0.4),h2-r2*Math.cos(alpha-0.4)],

                                         [w2+r*Math.sin(alpha),h2-r*Math.cos(alpha)],

                                         [w2+r2*Math.sin(alpha+0.4),h2-r2*Math.cos(alpha+0.4)]   ];

              dc.setColor(Gfx.COLOR_WHITE, Gfx.COLOR_TRANSPARENT);

              dc.fillPolygon(hand);

              dc.setColor(Gfx.COLOR_BLACK, Gfx.COLOR_TRANSPARENT);

              for (n=0; n<3; n++) {

                     dc.drawLine(hand[n][0], hand[n][1], hand[n+1][0], hand[n+1][1]);

              }

              dc.drawLine(hand[n][0], hand[n][1], hand[0][0], hand[0][1]);



              // minutes

              alpha = Math.PI/30.0*clockTime.min;

              r = 90;

              r2 = 20;

              hand =         [[w2,h2],

                                         [w2+r2*Math.sin(alpha-0.15),h2-r2*Math.cos(alpha-0.15)],

                                         [w2+r*Math.sin(alpha),h2-r*Math.cos(alpha)],

                                         [w2+r2*Math.sin(alpha+0.15),h2-r2*Math.cos(alpha+0.15)]   ];

              dc.setColor(Gfx.COLOR_WHITE, Gfx.COLOR_TRANSPARENT);

              dc.fillPolygon(hand);

              dc.setColor(Gfx.COLOR_BLACK, Gfx.COLOR_TRANSPARENT);

              for (n=0; n<3; n++) {

                     dc.drawLine(hand[n][0], hand[n][1], hand[n+1][0], hand[n+1][1]);

              }

              dc.drawLine(hand[n][0], hand[n][1], hand[0][0], hand[0][1]);





    }

   



   

    function bar(dc, position, percentage, color1, text, strong) {

        var len = (h - 21) * (percentage);

        len = len.toNumber();

       dc.setColor(color1, Gfx.COLOR_TRANSPARENT);

              dc.fillRectangle(18+position*10, h-len-10, 8, len+1);

              dc.drawText(22+position*10, h-len-28, font2, text, Gfx.TEXT_JUSTIFY_CENTER);

              if(strong){

                     dc.drawText(22+position*10-1, h-len-28-1, font2, text, Gfx.TEXT_JUSTIFY_CENTER);

                     dc.drawText(22+position*10-1, h-len-28, font2, text, Gfx.TEXT_JUSTIFY_CENTER);

                     dc.drawText(22+position*10, h-len-28-1, font2, text, Gfx.TEXT_JUSTIFY_CENTER);

              }

    }

   

   

    //! Called when this View is removed from the screen. Save the

    //! state of this View here. This includes freeing resources from

    //! memory.

    function onHide() {

    }



    //! The user has just looked at their watch. Timers and animations may be started here.

    function onExitSleep() {

    }



    //! Terminate any active timers and prepare for slow updates.

    function onEnterSleep() {

    }



}