├── .gitignore
├── monkey.jungle
├── resources
├── drawables
│ ├── logo.png
│ ├── launcher_icon.png
│ └── drawables.xml
├── layouts
│ └── layout.xml
└── strings
│ └── strings.xml
├── source
├── Version.mc
├── Config.mc.template
├── DownloadStatus.mc
├── Urls.mc
├── MessageView.mc
├── TaoGlanceView.mc
├── NetUtil.mc
├── GrantView.mc
├── DownloadView.mc
├── WorkoutView.mc
├── DeferredIntent.mc
├── RequestDelegate.mc
├── TrainAsONEApp.mc
├── MessageUtil.mc
├── Grant.mc
├── WorkoutFormatter.mc
├── DownloadRequest.mc
├── WorkoutDelegate.mc
└── TaoModel.mc
├── LICENSE
├── koko
├── manifest-downloadcapable.xml
├── README.md
├── manifest.xml
├── manifest-beta.xml
└── manifest-allwatches.xml
/.gitignore:
--------------------------------------------------------------------------------
1 | bin
2 | source/Config.mc
3 |
--------------------------------------------------------------------------------
/monkey.jungle:
--------------------------------------------------------------------------------
1 | project.manifest = manifest.xml
2 |
3 |
--------------------------------------------------------------------------------
/resources/drawables/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TrainAsONE/trainasone-connectiq/HEAD/resources/drawables/logo.png
--------------------------------------------------------------------------------
/resources/drawables/launcher_icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TrainAsONE/trainasone-connectiq/HEAD/resources/drawables/launcher_icon.png
--------------------------------------------------------------------------------
/source/Version.mc:
--------------------------------------------------------------------------------
1 | // Must be in the form X.Y.Z where X Y & Z are all digits. No letters or other characters
2 | const AppVersion = "0.42.0";
3 |
--------------------------------------------------------------------------------
/resources/layouts/layout.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
--------------------------------------------------------------------------------
/resources/drawables/drawables.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/source/Config.mc.template:
--------------------------------------------------------------------------------
1 | import Toybox.Lang;
2 |
3 | // Contact garmin@trainasone.com to ask for a ClientId and ClientSecret
4 | const ClientId = "YourClientId";
5 | const ClientSecret = "YourClientSecret";
6 | const ServerUrls = ["https://beta.trainasone.com"] as Array;
7 | const ExcludeViewStackWorkaroundPreMonkeyV3 = "58f5139d5df474fd8ba05cf6c60c85840199f04f"; // Enable for physical devices, disable for simulator with this uniqueIdentifier
--------------------------------------------------------------------------------
/source/DownloadStatus.mc:
--------------------------------------------------------------------------------
1 | (:glance)
2 | module DownloadStatus {
3 | enum {
4 | OK = 0,
5 | INSUFFICIENT_SUBSCRIPTION_CAPABILITIES = 1,
6 | DEVICE_DOES_NOT_SUPPORT_DOWNLOAD = 2,
7 | EXTERNAL_SCHEDULE = 3,
8 | NO_WORKOUT_AVAILABLE = 4,
9 | WORKOUT_NOT_DOWNLOAD_CAPABLE = 5,
10 | RESPONSE_MISSING_WORKOUT_DATA = 6,
11 | RESPONSE_CODE_ZERO = 7,
12 | DOWNLOAD_TIMEOUT = 8,
13 | NOT_YET_ATTEMPTED = 9,
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/source/Urls.mc:
--------------------------------------------------------------------------------
1 | module Urls {
2 | const ABOUT_URL = "https://iq.trainasone.com/clwd";
3 | const NOT_DOWNLOAD_CAPABLE = "https://iq.trainasone.com/ndc";
4 | const NOT_DOWNLOAD_NOT_SUPPORTED = "https://iq.trainasone.com/dns";
5 | const INSUFFICIENT_SUBSCRIPTION_CAPABILITIES =
6 | "https://iq.trainasone.com/isc";
7 | const CANNOT_LOAD_WORKOUT_DATA = "https://iq.trainasone.com/clwd";
8 | const DOWNLOAD_TIMEOUT = "https://iq.trainasone.com/dt";
9 | }
10 |
--------------------------------------------------------------------------------
/source/MessageView.mc:
--------------------------------------------------------------------------------
1 | import Toybox.Graphics;
2 | import Toybox.Lang;
3 | import Toybox.System;
4 | import Toybox.WatchUi;
5 |
6 | // Show error message
7 | class MessageView extends WatchUi.View {
8 | private var _message as String;
9 |
10 | function initialize(message as String) {
11 | _message = message;
12 | Application.getApp().log("message: " + _message);
13 | View.initialize();
14 | }
15 |
16 | // Should allow a menu/select to restart main loop
17 | function onLayout(dc as Graphics.Dc) as Void {
18 | setLayout(Rez.Layouts.StandardLayout(dc));
19 | (View.findDrawableById("message") as WatchUi.Text).setText(_message);
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/source/TaoGlanceView.mc:
--------------------------------------------------------------------------------
1 | import Toybox.Graphics;
2 | import Toybox.Lang;
3 | import Toybox.WatchUi;
4 |
5 | (:glance)
6 | class TaoGlanceView extends WatchUi.GlanceView {
7 | var _workoutText as String?;
8 |
9 | function initialize() {
10 | GlanceView.initialize();
11 | }
12 |
13 | function onLayout(dc as Graphics.Dc) as Void {
14 | // FIXME - adjust formatting to merge lines 2 & 3 for glance view
15 | _workoutText = new WorkoutFormatter().buildMessageFromWorkout();
16 | // Application.getApp().log("workoutText(" + _workoutText + ")");
17 | }
18 |
19 | function onUpdate(dc as Graphics.Dc) as Void {
20 | if (_workoutText != null) {
21 | dc.setColor(Graphics.COLOR_WHITE, Graphics.COLOR_BLACK);
22 | dc.drawText(
23 | 0,
24 | 0,
25 | Graphics.FONT_XTINY,
26 | _workoutText as String,
27 | Graphics.TEXT_JUSTIFY_LEFT
28 | );
29 | }
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/source/NetUtil.mc:
--------------------------------------------------------------------------------
1 | import Toybox.Application;
2 | import Toybox.Communications;
3 | import Toybox.Lang;
4 | import Toybox.System;
5 | import Toybox.WatchUi;
6 |
7 | class NetUtil {
8 | public static function extractResponseCode(
9 | responseCode as Number,
10 | data as Dictionary or String or Null
11 | ) as Number {
12 | // workaround non 200 response codes being flattened out by Garmin runtime
13 | if (responseCode == 200 && data != null && data["responseCode"] != null) {
14 | return data["responseCode"] as Number;
15 | }
16 | return responseCode;
17 | }
18 |
19 | public static function deviceParams() as Dictionary {
20 | var deviceSettings = System.getDeviceSettings();
21 | var appVersion = Application.getApp().appVersion();
22 | var device =
23 | deviceSettings.partNumber +
24 | Lang.format("/$1$.$2$.$3$", deviceSettings.monkeyVersion);
25 |
26 | return {
27 | "appVersion" => appVersion,
28 | "device" => device,
29 | };
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/source/GrantView.mc:
--------------------------------------------------------------------------------
1 | import Toybox.Graphics;
2 | import Toybox.Lang;
3 | import Toybox.WatchUi;
4 |
5 | class GrantDelegate extends WatchUi.BehaviorDelegate {
6 | function initialize() {
7 | BehaviorDelegate.initialize();
8 | }
9 | }
10 |
11 | class GrantView extends WatchUi.View {
12 | private var _request as GrantRequest?;
13 | private var _reAuth as Boolean;
14 | private var _clearAuth as Boolean;
15 |
16 | function initialize(reAuth as Boolean, clearAuth as Boolean) {
17 | View.initialize();
18 | _reAuth = reAuth;
19 | _clearAuth = clearAuth;
20 | }
21 |
22 | function onLayout(dc as Graphics.Dc) as Void {
23 | setLayout(Rez.Layouts.StandardLayout(dc));
24 | var message =
25 | WatchUi.loadResource(
26 | _reAuth ? Rez.Strings.grantReAuthString : Rez.Strings.grantString
27 | ) as String;
28 | var view = View.findDrawableById("message") as WatchUi.Text;
29 | view.setFont(Graphics.FONT_SMALL);
30 | view.setText(message);
31 | }
32 |
33 | function onShow() as Void {
34 | if (_request == null) {
35 | _request = new GrantRequest(
36 | new GrantRequestDelegate(_clearAuth),
37 | _clearAuth
38 | );
39 | _request.start();
40 | }
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/source/DownloadView.mc:
--------------------------------------------------------------------------------
1 | import Toybox.Graphics;
2 | import Toybox.Lang;
3 | import Toybox.WatchUi;
4 |
5 | class DownloadDelegate extends WatchUi.BehaviorDelegate {
6 | function initialize() {
7 | BehaviorDelegate.initialize();
8 | }
9 | }
10 |
11 | class DownloadView extends WatchUi.View {
12 | private var _message as String?;
13 | private var _stateText as String = "";
14 | private var _request as DownloadRequest?;
15 |
16 | function initialize(message as String?) {
17 | View.initialize();
18 | _message = message != null ? message : "Checking workout";
19 | }
20 |
21 | function onLayout(dc as Graphics.Dc) as Void {
22 | setLayout(Rez.Layouts.StandardLayout(dc));
23 | }
24 |
25 | function onShow() as Void {
26 | if (_request == null) {
27 | updateState("connecting");
28 | _request = new DownloadRequest(self);
29 | _request.start();
30 | }
31 | }
32 |
33 | function updateState(stateText as String) as Void {
34 | _stateText = stateText;
35 | Application.getApp().log("download: " + _message + ": " + _stateText);
36 | (View.findDrawableById("message") as WatchUi.Text).setText(
37 | _message + "\n(" + _stateText + ")"
38 | );
39 | WatchUi.requestUpdate();
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/source/WorkoutView.mc:
--------------------------------------------------------------------------------
1 | import Toybox.Application;
2 | import Toybox.Lang;
3 | import Toybox.Graphics;
4 | import Toybox.WatchUi;
5 |
6 | class WorkoutView extends WatchUi.View {
7 | function initialize() {
8 | View.initialize();
9 | }
10 |
11 | function onLayout(dc as Graphics.Dc) as Void {
12 | setLayout(Rez.Layouts.StandardLayout(dc));
13 | // Graphics.Dc.getHeight() fails with "Could not find symbol mHeight", presumably as we have not displayed yet
14 | var deviceSettings = System.getDeviceSettings();
15 | var height = deviceSettings.screenHeight;
16 | var width = deviceSettings.screenWidth;
17 | var centre = width / 2;
18 | // Application.getApp().log("display: " + width + "x" + height);
19 | var view = View.findDrawableById("message") as WatchUi.Text;
20 |
21 | // Start text higher on vivoactive's shorter screen
22 | view.setLocation(centre, height <= 148 ? 62 : 74); // vivoactive
23 |
24 | // MEDIUM font works better on devices with 215 wide screens (235, 630, 735xt, etc)
25 | // as the SMALL font is much harder to read
26 | view.setFont(width == 215 ? Graphics.FONT_MEDIUM : Graphics.FONT_SMALL);
27 |
28 | var workoutFormatter = new WorkoutFormatter();
29 | view.setText(workoutFormatter.buildMessageFromWorkout());
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/source/DeferredIntent.mc:
--------------------------------------------------------------------------------
1 | // Code from Garmin to workaround System.exitTo(intent) issue in 645 firmware (SDK 3.0.3)
2 |
3 | import Toybox.System;
4 | import Toybox.Timer;
5 | import Toybox.WatchUi;
6 |
7 | class DeferredIntent {
8 | private var _data as Intent;
9 | private var _controller as WorkoutMenuDelegate;
10 | private var _timer as Timer.Timer;
11 |
12 | function initialize(controller as WorkoutMenuDelegate, data as Intent) {
13 | _controller = controller;
14 | _data = data;
15 | _timer = new Timer.Timer();
16 | _timer.start(method(:onTimer), 200, false);
17 | }
18 |
19 | function onTimer() as Void {
20 | _controller.handleDeferredIntent(_data);
21 | }
22 | }
23 |
24 | /*
25 | // Calling example
26 | public function handleDownloadResponse( data ) {
27 | // Clear the confirmation from the page stack
28 | WatchUi.popView( WatchUi.SLIDE_IMMEDIATE );
29 | // Kick off the kludge transaction
30 | _activeTransaction = new self.DeferredIntent( self, data );
31 | }
32 |
33 | // This is fired by the kludge transaction
34 | public function handleIntent( data ) {
35 | _activeTransaction = null;
36 | if ( data != null ) {
37 | System.exitTo( data.toIntent() );
38 | } else {
39 | handleError( -1000 );
40 | }
41 | }
42 | */
43 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | BSD 2-Clause License
2 |
3 | Copyright (c) 2017, TrainAsONE
4 | All rights reserved.
5 |
6 | Redistribution and use in source and binary forms, with or without
7 | modification, are permitted provided that the following conditions are met:
8 |
9 | * Redistributions of source code must retain the above copyright notice, this
10 | list of conditions and the following disclaimer.
11 |
12 | * Redistributions in binary form must reproduce the above copyright notice,
13 | this list of conditions and the following disclaimer in the documentation
14 | and/or other materials provided with the distribution.
15 |
16 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
17 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
18 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
19 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
20 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
21 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
22 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
23 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
24 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
25 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
26 |
--------------------------------------------------------------------------------
/source/RequestDelegate.mc:
--------------------------------------------------------------------------------
1 | import Toybox.Communications;
2 | import Toybox.Lang;
3 | import Toybox.WatchUi;
4 |
5 | class RequestDelegate {
6 | function handleErrorResponseCode(
7 | action as String,
8 | responseCode as Number
9 | ) as Void {
10 | switch (responseCode) {
11 | case Communications.BLE_ERROR:
12 | case Communications.BLE_HOST_TIMEOUT:
13 | case Communications.BLE_SERVER_TIMEOUT:
14 | case Communications.BLE_NO_DATA:
15 | case Communications.BLE_REQUEST_CANCELLED:
16 | case Communications.BLE_QUEUE_FULL:
17 | case Communications.BLE_REQUEST_TOO_LARGE:
18 | case Communications.BLE_UNKNOWN_SEND_ERROR:
19 | MessageUtil.showErrorMessage(
20 | WatchUi.loadResource(Rez.Strings.errorPhoneConnection) +
21 | " " +
22 | responseCode +
23 | " " +
24 | action
25 | );
26 | break;
27 | case Communications.BLE_CONNECTION_UNAVAILABLE:
28 | MessageUtil.showErrorResource(Rez.Strings.errorPleaseConnectPhone);
29 | break;
30 | case Communications.NETWORK_REQUEST_TIMED_OUT:
31 | MessageUtil.showErrorResource(Rez.Strings.errorNetworkRequestTimedOut);
32 | case 0: // no data - may be full, or empty FIT returned
33 | MessageUtil.showErrorResource(Rez.Strings.errorNoData);
34 | break;
35 | case 401: // Unauthorized
36 | WatchUi.switchToView(
37 | new GrantView(true, false),
38 | new GrantDelegate(),
39 | WatchUi.SLIDE_IMMEDIATE
40 | );
41 | break;
42 | case 404: // not found
43 | MessageUtil.showErrorResource(Rez.Strings.errorNotFound);
44 | break;
45 | case 418: // service alternately unavailable
46 | case 503: // service unavailable
47 | MessageUtil.showErrorResource(Rez.Strings.errorServiceUnavailable);
48 | break;
49 | default:
50 | MessageUtil.showErrorMessage(
51 | WatchUi.loadResource(Rez.Strings.serverError) +
52 | " " +
53 | responseCode +
54 | " " +
55 | action
56 | );
57 | break;
58 | }
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/source/TrainAsONEApp.mc:
--------------------------------------------------------------------------------
1 | import Toybox.Application;
2 | import Toybox.Lang;
3 | import Toybox.System;
4 | import Toybox.WatchUi;
5 |
6 | (:glance)
7 | class TrainAsONEApp extends Application.AppBase {
8 | var model as TaoModel;
9 |
10 | function initialize() {
11 | AppBase.initialize();
12 | log("Starting TrainAsONE " + appVersion());
13 | model = new TaoModel();
14 | }
15 |
16 | function appVersion() as String {
17 | var appVersion = $.AppVersion;
18 | if (Toybox has :PersistedContent) {
19 | appVersion += "+d"; // Download capable
20 | if (PersistedContent has :getAppWorkouts) {
21 | appVersion += ",o"; // Can manage own set of workouts
22 | }
23 | }
24 | return appVersion;
25 | }
26 |
27 | // onStart() is called on application start up
28 | function onStart(state) as Void {}
29 |
30 | // onStop() is called when your application is exiting
31 | function onStop(state) as Void {}
32 |
33 | function getGlanceView() as [ GlanceView ] or [ GlanceView, GlanceViewDelegate ] or Null {
34 | return [new TaoGlanceView()] as [ GlanceView ];
35 | }
36 |
37 | // Return the initial view of your application here
38 | function getInitialView() as [ Views ] or [ Views, InputDelegates ] {
39 | if (!System.getDeviceSettings().phoneConnected) {
40 | return (
41 | [
42 | new MessageView(
43 | WatchUi.loadResource(Rez.Strings.errorPleaseConnectPhone)
44 | ),
45 | new MessageDelegate(null),
46 | ]
47 | );
48 | } else if (model.accessToken == null) {
49 | return ([new GrantView(false, false), new GrantDelegate()]);
50 | } else {
51 | return ([new DownloadView(null), new DownloadDelegate()]);
52 | }
53 | }
54 |
55 | function log(message as String) as Void {
56 | var now = System.getClockTime();
57 | System.print(
58 | now.hour.format("%02d") +
59 | ":" +
60 | now.min.format("%02d") +
61 | ":" +
62 | now.sec.format("%02d") +
63 | " "
64 | );
65 | var array = message.toCharArray();
66 | for (var i = 0; i < array.size(); ++i) {
67 | if (array[i] == '\n') {
68 | array[i] = ' ';
69 | }
70 | }
71 | System.println(StringUtil.charArrayToString(array));
72 | }
73 | }
74 |
--------------------------------------------------------------------------------
/source/MessageUtil.mc:
--------------------------------------------------------------------------------
1 | import Toybox.Application;
2 | import Toybox.Communications;
3 | import Toybox.Lang;
4 | import Toybox.WatchUi;
5 |
6 | /// All of these should be static, but Application.getApp().model does not work in
7 | /// a static method with Type Checking enabled
8 | class MessageUtil {
9 | public static function fullErrorMessage(message as String) as String {
10 | var mModel = Application.getApp().model;
11 | return (
12 | message +
13 | "\n" +
14 | WatchUi.loadResource(
15 | mModel.downloadIntent != null
16 | ? Rez.Strings.pressForSavedWorkout
17 | : Rez.Strings.pressForOptions
18 | )
19 | );
20 | }
21 |
22 | public static function showAbout() as Void {
23 | var message =
24 | WatchUi.loadResource(Rez.Strings.aboutApp) +
25 | Application.getApp().appVersion();
26 | showMessage(message, Urls.ABOUT_URL);
27 | }
28 |
29 | public static function showErrorResource(rez as ResourceId) as Void {
30 | showErrorResourceWithUrl(rez, null);
31 | }
32 |
33 | public static function showErrorResourceWithUrl(rez as ResourceId, url as String?) as Void {
34 | showErrorMessageWithUrl(WatchUi.loadResource(rez) as String, url);
35 | }
36 |
37 | public static function showErrorMessage(message as String) as Void {
38 | showErrorMessageWithUrl(message, null);
39 | }
40 |
41 | public static function showErrorMessageWithUrl(message as String, url as String?) as Void {
42 | showMessage(fullErrorMessage(message), url);
43 | }
44 |
45 | public static function showMessage(message as String, url as String?) as Void {
46 | WatchUi.switchToView(
47 | new MessageView(message),
48 | new MessageDelegate(url),
49 | WatchUi.SLIDE_IMMEDIATE
50 | );
51 | }
52 | }
53 |
54 | class MessageDelegate extends WatchUi.BehaviorDelegate {
55 | private var mModel as TaoModel;
56 | private var _url as String?;
57 |
58 | function initialize(url as String?) {
59 | BehaviorDelegate.initialize();
60 | mModel = Application.getApp().model;
61 | _url = url;
62 | }
63 |
64 | function onMenu() as Boolean {
65 | return showErrorMenu();
66 | }
67 |
68 | function onSelect() as Boolean {
69 | return showErrorMenu();
70 | }
71 |
72 | function showErrorMenu() as Boolean {
73 | var menu = new WatchUi.Menu();
74 | if (_url != null) {
75 | menu.addItem(loadStringResource(Rez.Strings.moreInfo), :moreInfo);
76 | }
77 | if (mModel.hasWorkout()) {
78 | menu.addItem(loadStringResource(Rez.Strings.menuShowSaved), :showSaved);
79 | }
80 | menu.addItem(
81 | loadStringResource(Rez.Strings.menuRetry),
82 | mModel.accessToken == null ? :switchUser : :refetchWorkout
83 | );
84 | mModel.addStandardMenuOptions(menu);
85 |
86 | WatchUi.pushView(menu, new WorkoutMenuDelegate(_url), WatchUi.SLIDE_UP);
87 | return true;
88 | }
89 |
90 | function loadStringResource(rez as ResourceId) as String {
91 | return WatchUi.loadResource(rez) as String;
92 | }
93 | }
94 |
--------------------------------------------------------------------------------
/koko:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | # koko is a very simple script to automate building targets listed in the manifest
4 | # For those that way inclined it runs fine under Windows 10's Linux Subsystem
5 | #
6 | # Free for any use -- abs@trainasone.com
7 |
8 | APP=trainasone-connectiq
9 | OUTPUTDIR=bin/binaries
10 | MANIFEST=manifest.xml
11 |
12 | while getopts ahlm:nrvy: f; do
13 | case $f in
14 | a) opt_a=1 ;;
15 | h) opt_h=1 ;;
16 | l) opt_l=1 ;;
17 | m) MANIFEST="$OPTARG" ;;
18 | n) opt_n=1 ;;
19 | r) opt_r=-r ;;
20 | y) KOKO_PRIVATE_KEY="$OPTARG" ;;
21 | v) opt_v=1 ;;
22 | esac
23 | done
24 | shift $(expr $OPTIND - 1)
25 |
26 | usage_and_exit()
27 | {
28 | echo "$@"
29 | cat <
2 | TrainAsONE
3 | TrainAsONE beta
4 | TrainAsONE lite
5 | Heart rate
6 | Heart rate for recovery
7 | Heart rate for slow
8 | Speed
9 | «{
10 | }»
11 | TrainAsONE Garmin\nworkout app\nv
12 | Temperature adjust
13 | Undulation adjust
14 | Download timeout
15 | Watch workout storage\nfull? Please remove\nworkouts and retry
16 | Sorry, Garmin do not\nsupport download\non your model
17 | Please upgrade to a\nfull TrainAsONE\nsubscription to\ndownload
18 | Workout download\ntimed out
19 | Cannot update\nIs your device full?
20 | This workout cannot\nbe downloaded
21 | Cannot start workouts\nfrom this point
22 | Server returned\n"not found"
23 | Phone connect error
24 | Please connect to\nyour phone to check\nyour next workout
25 | Network request\ntimed out
26 | Server error:\n
27 | Server not available\nplease retry shortly
28 | Unexpected workout\nupdate error
29 | Unexpected workout\ndownload error
30 | Unexpected download\nnot allowed error
31 | Please re-grant access\nto TrainAsONE\non your phone
32 | Please grant access\nto TrainAsONE\non your phone
33 |
34 | No workout space
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 | Show more info
45 | NO
46 | No data from server
47 | Unable to find\nworkout - Please\ncheck your plan
48 | ( > for options )
49 | ( > for saved workout )
50 | Server
51 | Step names
52 | Step target
53 | updated
54 | Updating\nworkout
55 | YES
56 |
57 | today
58 | tomorrow
59 | yesterday
60 |
61 | °C
62 | °F
63 | mm
64 | cm
65 | pc
66 | m
67 | ft
68 | km
69 | mi
70 | min
71 |
72 | $1$\n$2$\n$3$
73 | No workout available\nplease adjust your\nplan
74 | No workouts while\nyour plan is set to\nexternal schedule
75 | Select to connect\nto TrainAsONE
76 |
77 |
78 |
--------------------------------------------------------------------------------
/source/Grant.mc:
--------------------------------------------------------------------------------
1 | import Toybox.Application;
2 | import Toybox.Communications;
3 | import Toybox.Lang;
4 | import Toybox.System;
5 | import Toybox.WatchUi;
6 |
7 | const RedirectUri = "https://localhost";
8 | const Scope = "WORKOUT";
9 |
10 | const OAUTH_CODE = "code";
11 | const OAUTH_ERROR = "error";
12 | const OAUTH_ERROR_DESCRIPTION = "error_description";
13 | const HTTP_STATUS_OK = 200;
14 |
15 | // Obtain and store an Oauth2 token for API access
16 | class GrantRequest {
17 | private var mModel as TaoModel;
18 | private var _delegate as GrantRequestDelegate;
19 | private var _clearAuth as Boolean;
20 |
21 | function initialize(delegate as GrantRequestDelegate, clearAuth as Boolean) {
22 | mModel = Application.getApp().model;
23 | _delegate = delegate;
24 | _clearAuth = clearAuth;
25 | Communications.registerForOAuthMessages(method(:handleAccessCodeResult)); // May fire immediately
26 | // Application.getApp().log("Grant(" + clearAuth + ")");
27 | }
28 |
29 | function start() as Void {
30 | var requestUrl = mModel.serverUrl + "/oauth/authorise";
31 | var requestParams =
32 | ({
33 | "client_id" => $.ClientId,
34 | "response_type" => OAUTH_CODE,
35 | "scope" => $.Scope,
36 | "redirect_uri" => $.RedirectUri,
37 | "logout" => _clearAuth ? "1" : "0",
38 | }) as Dictionary;
39 | var resultUrl = $.RedirectUri;
40 | var resultType = Communications.OAUTH_RESULT_TYPE_URL;
41 | // Need to explicitly enumerate the parameters we want to take from the response
42 | var resultKeys =
43 | ({
44 | OAUTH_CODE => OAUTH_CODE,
45 | OAUTH_ERROR_DESCRIPTION => OAUTH_ERROR_DESCRIPTION,
46 | }) as Dictionary;
47 | Communications.makeOAuthRequest(
48 | requestUrl,
49 | requestParams,
50 | resultUrl,
51 | resultType,
52 | resultKeys
53 | );
54 | }
55 |
56 | // Callback from Grant attempt
57 | function handleAccessCodeResult(response as OAuthMessage) as Void {
58 | // Application.getApp().log("handleAccessCodeResult(" + response.data + ")");
59 |
60 | var error;
61 |
62 | // We cannot rely on responseCode here - as simulator gives 200 but at least 735xt real device gives 2!
63 | // So we just check to see if we have a valid code
64 | if (response.data != null) {
65 | var data = response.data as Dictionary;
66 | var code = data[OAUTH_CODE];
67 | if (code != null) {
68 | // Convert auth code to access token
69 | var url = mModel.serverUrl + "/api/oauth/token";
70 | var params =
71 | ({
72 | "client_id" => $.ClientId,
73 | "client_secret" => $.ClientSecret,
74 | "redirect_uri" => $.RedirectUri,
75 | "grant_type" => "authorization_code",
76 | OAUTH_CODE => code,
77 | "jsonErrors" => "true",
78 | }) as Dictionary