Просмотр исходного кода

Merge branch 'dev-garmin' into dev

Pierre L 3 лет назад
Родитель
Сommit
988a8e2827
58 измененных файлов с 940 добавлено и 21 удалено
  1. BIN
      Dependencies/ios-armv7_arm64/ConnectIQ.framework/ConnectIQ
  2. 237 0
      Dependencies/ios-armv7_arm64/ConnectIQ.framework/Headers/ConnectIQ.h
  3. 34 0
      Dependencies/ios-armv7_arm64/ConnectIQ.framework/Headers/IQApp.h
  4. 20 0
      Dependencies/ios-armv7_arm64/ConnectIQ.framework/Headers/IQAppStatus.h
  5. 63 0
      Dependencies/ios-armv7_arm64/ConnectIQ.framework/Headers/IQConstants.h
  6. 61 0
      Dependencies/ios-armv7_arm64/ConnectIQ.framework/Headers/IQDevice.h
  7. BIN
      Dependencies/ios-armv7_arm64/ConnectIQ.framework/Info.plist
  8. 6 0
      Dependencies/ios-armv7_arm64/ConnectIQ.framework/Modules/module.modulemap
  9. BIN
      Dependencies/ios-armv7_arm64/ConnectIQ.framework/ar.lproj/IQLocalizable.strings
  10. BIN
      Dependencies/ios-armv7_arm64/ConnectIQ.framework/cs.lproj/IQLocalizable.strings
  11. BIN
      Dependencies/ios-armv7_arm64/ConnectIQ.framework/da.lproj/IQLocalizable.strings
  12. BIN
      Dependencies/ios-armv7_arm64/ConnectIQ.framework/de.lproj/IQLocalizable.strings
  13. BIN
      Dependencies/ios-armv7_arm64/ConnectIQ.framework/el.lproj/IQLocalizable.strings
  14. BIN
      Dependencies/ios-armv7_arm64/ConnectIQ.framework/en.lproj/IQLocalizable.strings
  15. BIN
      Dependencies/ios-armv7_arm64/ConnectIQ.framework/es.lproj/IQLocalizable.strings
  16. BIN
      Dependencies/ios-armv7_arm64/ConnectIQ.framework/fi.lproj/IQLocalizable.strings
  17. BIN
      Dependencies/ios-armv7_arm64/ConnectIQ.framework/fr.lproj/IQLocalizable.strings
  18. BIN
      Dependencies/ios-armv7_arm64/ConnectIQ.framework/he.lproj/IQLocalizable.strings
  19. BIN
      Dependencies/ios-armv7_arm64/ConnectIQ.framework/hr.lproj/IQLocalizable.strings
  20. BIN
      Dependencies/ios-armv7_arm64/ConnectIQ.framework/hu.lproj/IQLocalizable.strings
  21. BIN
      Dependencies/ios-armv7_arm64/ConnectIQ.framework/id.lproj/IQLocalizable.strings
  22. BIN
      Dependencies/ios-armv7_arm64/ConnectIQ.framework/it.lproj/IQLocalizable.strings
  23. BIN
      Dependencies/ios-armv7_arm64/ConnectIQ.framework/ja.lproj/IQLocalizable.strings
  24. BIN
      Dependencies/ios-armv7_arm64/ConnectIQ.framework/ko.lproj/IQLocalizable.strings
  25. BIN
      Dependencies/ios-armv7_arm64/ConnectIQ.framework/ms.lproj/IQLocalizable.strings
  26. BIN
      Dependencies/ios-armv7_arm64/ConnectIQ.framework/nb.lproj/IQLocalizable.strings
  27. BIN
      Dependencies/ios-armv7_arm64/ConnectIQ.framework/nl.lproj/IQLocalizable.strings
  28. BIN
      Dependencies/ios-armv7_arm64/ConnectIQ.framework/pl.lproj/IQLocalizable.strings
  29. BIN
      Dependencies/ios-armv7_arm64/ConnectIQ.framework/pt-PT.lproj/IQLocalizable.strings
  30. BIN
      Dependencies/ios-armv7_arm64/ConnectIQ.framework/pt.lproj/IQLocalizable.strings
  31. BIN
      Dependencies/ios-armv7_arm64/ConnectIQ.framework/ru.lproj/IQLocalizable.strings
  32. BIN
      Dependencies/ios-armv7_arm64/ConnectIQ.framework/sk.lproj/IQLocalizable.strings
  33. BIN
      Dependencies/ios-armv7_arm64/ConnectIQ.framework/sv.lproj/IQLocalizable.strings
  34. BIN
      Dependencies/ios-armv7_arm64/ConnectIQ.framework/th.lproj/IQLocalizable.strings
  35. BIN
      Dependencies/ios-armv7_arm64/ConnectIQ.framework/tr.lproj/IQLocalizable.strings
  36. BIN
      Dependencies/ios-armv7_arm64/ConnectIQ.framework/zh-Hans.lproj/IQLocalizable.strings
  37. BIN
      Dependencies/ios-armv7_arm64/ConnectIQ.framework/zh-Hant.lproj/IQLocalizable.strings
  38. 52 0
      FreeAPS.xcodeproj/project.pbxproj
  39. 6 3
      FreeAPS/Resources/Info.plist
  40. 2 1
      FreeAPS/Resources/json/defaults/freeaps/freeaps_settings.json
  41. 11 0
      FreeAPS/Sources/Application/FreeAPSApp.swift
  42. 1 0
      FreeAPS/Sources/Assemblies/ServiceAssembly.swift
  43. 7 0
      FreeAPS/Sources/Models/FreeAPSSettings.swift
  44. 0 1
      FreeAPS/Sources/Modules/AddCarbs/View/AddCarbsRootView.swift
  45. 1 1
      FreeAPS/Sources/Modules/Home/View/HomeRootView.swift
  46. 1 2
      FreeAPS/Sources/Modules/PreferencesEditor/PreferencesEditorStateModel.swift
  47. 0 2
      FreeAPS/Sources/Modules/PreferencesEditor/View/PreferencesEditorRootView.swift
  48. 1 0
      FreeAPS/Sources/Modules/Settings/View/SettingsRootView.swift
  49. 43 0
      FreeAPS/Sources/Modules/WatchConfig/View/WatchConfigRootView.swift
  50. 5 0
      FreeAPS/Sources/Modules/WatchConfig/WatchConfigDataFlow.swift
  51. 20 0
      FreeAPS/Sources/Modules/WatchConfig/WatchConfigProvider.swift
  52. 58 0
      FreeAPS/Sources/Modules/WatchConfig/WatchConfigStateModel.swift
  53. 3 0
      FreeAPS/Sources/Router/Screen.swift
  54. 208 0
      FreeAPS/Sources/Services/WatchManager/GarminManager.swift
  55. 22 3
      FreeAPS/Sources/Services/WatchManager/WatchManager.swift
  56. 5 1
      FreeAPSWatch WatchKit Extension/DataFlow.swift
  57. 64 5
      FreeAPSWatch WatchKit Extension/Views/MainView.swift
  58. 9 2
      FreeAPSWatch WatchKit Extension/WatchStateModel.swift

BIN
Dependencies/ios-armv7_arm64/ConnectIQ.framework/ConnectIQ


+ 237 - 0
Dependencies/ios-armv7_arm64/ConnectIQ.framework/Headers/ConnectIQ.h

@@ -0,0 +1,237 @@
+//
+//  ConnectIQ.h
+//  ConnectIQ
+//
+//  Copyright (c) 2014 Garmin. All rights reserved.
+//
+
+#import <Foundation/Foundation.h>
+#import "IQConstants.h"
+#import "IQDevice.h"
+#import "IQApp.h"
+
+// --------------------------------------------------------------------------------
+#pragma mark - PUBLIC TYPES
+// --------------------------------------------------------------------------------
+
+/// @brief  SendMessage progress callback block
+///
+/// @param  sentBytes  The number of bytes that have been successfully transferred
+///                    to the device so far for this connection.
+/// @param  totalBytes The total number of bytes to transfer for this connection.
+typedef void (^IQSendMessageProgress)(uint32_t sentBytes, uint32_t totalBytes);
+
+/// @brief  SendMessage completion callback block
+///
+/// @param  result The result of the SendMessage operation.
+typedef void (^IQSendMessageCompletion)(IQSendMessageResult result);
+
+/// @brief  Conforming to the IQUIOverrideDelegate protocol indicates that an
+///         object handles one or more events triggered by the ConnectIQ SDK that
+///         require user input.
+@protocol IQUIOverrideDelegate <NSObject>
+@optional
+/// @brief  Called by the ConnectIQ SDK when an action has been requested that
+///         requires Garmin Connect Mobile to be installed.
+///
+///         The receiver should choose whether or not to launch the Apple App
+///         Store page for GCM, ideally by presenting the user with a choice.
+///
+///         If the receiver of this message decides to install GCM, it must call
+///         showAppStoreForConnectMobile.
+- (void)needsToInstallConnectMobile;
+@end
+
+/// @brief  Conforming to the IQDeviceEventDelegate protocol indicates that an
+///         object handles ConnectIQ device status events.
+@protocol IQDeviceEventDelegate <NSObject>
+@optional
+/// @brief  Called by the ConnectIQ SDK when an IQDevice's connection status has
+///         changed.
+///
+/// @param  device The IQDevice whose status changed.
+/// @param  status The new status of the device.
+- (void)deviceStatusChanged:(IQDevice *)device status:(IQDeviceStatus)status;
+@end
+
+/// @brief  Conforming to the IQAppMessageDelegate protocol indicates that an
+///         object handles messages from ConnectIQ apps on compatible devices.
+@protocol IQAppMessageDelegate <NSObject>
+@optional
+/// @brief  Called by the ConnectIQ SDK when a message is received from a device.
+///
+/// @param  message The message that was received.
+/// @param  app     The device app that sent the message.
+- (void)receivedMessage:(id)message fromApp:(IQApp *)app;
+@end
+
+// --------------------------------------------------------------------------------
+#pragma mark - CLASS DEFINITION
+// --------------------------------------------------------------------------------
+
+/// @brief  The root of the ConnectIQ SDK API.
+@interface ConnectIQ : NSObject
+
++ (instancetype)new NS_UNAVAILABLE;
+- (instancetype)init NS_UNAVAILABLE;
+
+// --------------------------------------------------------------------------------
+#pragma mark - SINGLETON ACCESS
+// --------------------------------------------------------------------------------
+
+/// @brief  Exposes the single static instance of the ConnectIQ class.
+///
+/// @return The single status instance of the ConnectIQ class.
++ (ConnectIQ *)sharedInstance;
+
+// --------------------------------------------------------------------------------
+#pragma mark - INITIALIZATION
+// --------------------------------------------------------------------------------
+
+/// @brief  Initializes the ConnectIQ SDK with startup parameters necessary for
+///         its operation.
+///
+/// @param  urlScheme The URL scheme for this companion app. When Garmin Connect
+///                   Mobile is launched, it will return to the companion app by
+///                   launching a URL with this scheme.
+/// @param  delegate  The delegate that the SDK will use for notifying the
+///                   companion app about events that require user input. If this
+///                   is nil, the SDK's default UI will be used.
+- (void)initializeWithUrlScheme:(NSString *)urlScheme uiOverrideDelegate:(id<IQUIOverrideDelegate>)delegate;
+
+// --------------------------------------------------------------------------------
+#pragma mark - EXTERNAL LAUNCHING
+// --------------------------------------------------------------------------------
+
+/// @brief  Launches the Apple App Store page for the Garmin Connect Mobile app.
+///         The companion app should only call this in response to a
+///         needsToInstallConnectMobile event that gets triggered on the
+///         IQUIOverrideDelegate.
+- (void)showAppStoreForConnectMobile;
+
+/// @brief  Launches Garmin Connect Mobile for the purpose of retrieving a list of
+///         ConnectIQ-compatible devices.
+///
+///         Once the user has chosen which ConnectIQ devices to share with the
+///         companion app, GCM will return those devices to the companion app by
+///         opening a URL with the scheme registered in
+///         initializeWithUrlScheme:uiOverrideDelegate:.
+///
+///         The companion app should handle this URL by passing it in to the
+///         parseDeviceSelectionResponseFromURL: method to get the list of devices
+///         that the user permitted the companion app to communicate with.
+- (void)showConnectIQDeviceSelection;
+
+/// @brief  Parses a URL opened from Garmin Connect Mobile into a list of devices.
+///
+/// @param  url The URL to parse.
+///
+/// @return An array of IQDevice objects representing the ConnectIQ-compatible
+///         devices that the user allowed GCM to share with the companion app.
+///
+/// @seealso showConnectIQDeviceSelection
+- (NSArray *)parseDeviceSelectionResponseFromURL:(NSURL *)url;
+
+/// @brief  Launches Garmin Connect Mobile and shows the ConnectIQ app store page
+///         for the given app.
+///
+///         The companion app should call this if the user would like to manage
+///         the app on the device, such as to install, upgrade, uninstall, or
+///         modify settings.
+///
+/// @param  app The app to show the ConnectIQ app store page for.
+- (void)showConnectIQStoreForApp:(IQApp *)app;
+
+// --------------------------------------------------------------------------------
+#pragma mark - DEVICE MANAGEMENT
+// --------------------------------------------------------------------------------
+
+/// @brief  Registers an object as a listener for ConnectIQ device status events.
+///
+///         A device may have multiple device event listeners if this method is
+///         called more than once.
+///
+/// @param  device   A device to listen for status events from.
+/// @param  delegate The listener which will receive status events for this device.
+- (void)registerForDeviceEvents:(IQDevice *)device delegate:(id<IQDeviceEventDelegate>)delegate;
+
+/// @brief  Unregisters a listener for a specific device.
+///
+/// @param  device The device to unregister the listener for.
+/// @param  delegate The listener to remove from the device.
+- (void)unregisterForDeviceEvents:(IQDevice *)device delegate:(id<IQDeviceEventDelegate>)delegate;
+
+/// @brief  Unregisters the specified listener from all devices for which it had
+///         previously been registered.
+///
+/// @param  delegate The listener to unregister.
+- (void)unregisterForAllDeviceEvents:(id<IQDeviceEventDelegate>)delegate;
+
+/// @brief  Gets the current connection status of a device.
+///
+///         The device must have been registered for event notifications by
+///         calling registerForDeviceEvents:delegate: or this method will return
+///         IQDeviceStatus_InvalidDevice.
+///
+/// @param  device The device to get the status for.
+///
+/// @return The device's current connection status.
+- (IQDeviceStatus)getDeviceStatus:(IQDevice *)device;
+
+// --------------------------------------------------------------------------------
+#pragma mark - APP MANAGEMENT
+// --------------------------------------------------------------------------------
+
+/// @brief  Begins getting the status of an app on a device. This method returns
+///         immediately.
+///
+/// @param  app        The IQApp to get the status for.
+/// @param  completion The completion block that will be triggered when the device
+///                    status operation is complete.
+- (void)getAppStatus:(IQApp *)app completion:(void(^)(IQAppStatus *appStatus))completion;
+
+/// @brief  Registers an object as a listener for ConnectIQ messages from an app
+///         on a device.
+///
+///         An app may have multiple message listeners if this method is called
+///         more than once.
+///
+/// @param  app      The app to listen for messages from.
+/// @param  delegate The listener which will receive messages for this app.
+- (void)registerForAppMessages:(IQApp *)app delegate:(id<IQAppMessageDelegate>)delegate;
+
+/// @brief  Unregisters a listener for a specific app.
+///
+/// @param  app      The app to unregister a listener for.
+/// @param  delegate The listener to remove from the app.
+- (void)unregisterForAppMessages:(IQApp *)app delegate:(id<IQAppMessageDelegate>)delegate;
+
+/// @brief  Unregisters all previously registered apps for a specific listener.
+///
+/// @param  delegate The listener to unregister.
+- (void)unregisterForAllAppMessages:(id<IQAppMessageDelegate>)delegate;
+
+/// @brief  Begins sending a message to an app. This method returns immediately.
+///
+/// @param  message    The message to send to the app. This message must be one of
+///                    the following types: NSString, NSNumber, NSNull, NSArray,
+///                    or NSDictionary. Arrays and dictionaries may be nested.
+/// @param  app        The app to send the message to.
+/// @param  progress   A progress block that will be triggered periodically
+///                    throughout the transfer. This is guaranteed to be triggered
+///                    at least once.
+/// @param  completion A completion block that will be triggered when the send
+///                    message operation is complete.
+- (void)sendMessage:(id)message toApp:(IQApp *)app progress:(IQSendMessageProgress)progress completion:(IQSendMessageCompletion)completion;
+
+/// @brief  Sends an open app request message request to the device. This method returns immediately.
+///
+/// @param  app        The app to open.
+/// @param  completion A completion block that will be triggered when the send
+///                    message operation is complete.
+- (void)openAppRequest:(IQApp *)app completion:(IQSendMessageCompletion)completion;
+
+// TODO *** Holding off on documenting this until this method actually works.
+- (void)sendImage:(NSData *)bitmap toApp:(IQApp *)app progress:(IQSendMessageProgress)progress completion:(IQSendMessageCompletion)completion;
+
+@end

+ 34 - 0
Dependencies/ios-armv7_arm64/ConnectIQ.framework/Headers/IQApp.h

@@ -0,0 +1,34 @@
+//
+//  IQApp.h
+//  ConnectIQ
+//
+//  Copyright (c) 2014 Garmin. All rights reserved.
+//
+
+#import <Foundation/Foundation.h>
+#import "IQDevice.h"
+#import "IQAppStatus.h"
+
+/// @brief  Represents an instance of a ConnectIQ app that is installed on a
+///         Garmin device.
+@interface IQApp : NSObject <NSSecureCoding>
+
+/// @brief  The unique identifier for this app.
+@property (nonatomic, readonly) NSUUID *uuid;
+
+/// @brief  The unique identifier for this app in the store.
+@property (nonatomic, readonly) NSUUID *storeUuid;
+
+/// @brief  The device that this app is installed on.
+@property (nonatomic, readonly) IQDevice *device;
+
+/// @brief  Creates a new app instance.
+///
+/// @param  uuid        The UUID of the app to create.
+/// @param  storeUuid   The store UUID of the app to create.
+/// @param  device      The device the app to create is installed on.
+///
+/// @return A new IQApp instance with the appropriate values set.
++ (IQApp *)appWithUUID:(NSUUID *)uuid storeUuid:(NSUUID *)storeUuid device:(IQDevice *)device;
+
+@end

+ 20 - 0
Dependencies/ios-armv7_arm64/ConnectIQ.framework/Headers/IQAppStatus.h

@@ -0,0 +1,20 @@
+//
+//  IQAppStatus.h
+//  ConnectIQ
+//
+//  Copyright (c) 2014 Garmin. All rights reserved.
+//
+
+#import <Foundation/Foundation.h>
+
+/// @brief  Represents the current status of an app on a Garmin device.
+@interface IQAppStatus : NSObject
+
+/// @brief  YES if the app is installed on the device, NO if it isn't.
+@property (nonatomic, readonly) BOOL isInstalled;
+
+/// @brief  The version of the app that is currently installed on the device. If
+///         the app is not installed, this value is unused.
+@property (nonatomic, readonly) uint16_t version;
+
+@end

+ 63 - 0
Dependencies/ios-armv7_arm64/ConnectIQ.framework/Headers/IQConstants.h

@@ -0,0 +1,63 @@
+//
+//  IQConstants.h
+//  ConnectIQ
+//
+//  Copyright (c) 2014 Garmin. All rights reserved.
+//
+
+#import <Foundation/Foundation.h>
+
+/// @brief  The current version of the ConnectIQ SDK.
+extern int const IQSDKVersion;
+
+/// @brief  The bundle identifier for the Garmin Connect Mobile app.
+extern NSString * const IQGCMBundle;
+
+/// @brief  The result of a SendMessage operation
+typedef NS_ENUM(NSInteger, IQSendMessageResult){
+    ///! @brief  The message was sent successfully.
+    IQSendMessageResult_Success,
+
+    /// @brief  The message failed to send due to an unknown error.
+    IQSendMessageResult_Failure_Unknown,
+
+    /// @brief  The message failed to send. There was an error within the SDK or
+    ///         on the device.
+    IQSendMessageResult_Failure_InternalError,
+
+    /// @brief  The message failed to send. The device is not available right now.
+    IQSendMessageResult_Failure_DeviceNotAvailable,
+
+    /// @brief  The message failed to send. The app is not installed on the
+    ///         device.
+    IQSendMessageResult_Failure_AppNotFound,
+
+    /// @brief  The message failed to send. The device is busy and cannot receive
+    ///         the message right now.
+    IQSendMessageResult_Failure_DeviceIsBusy,
+
+    /// @brief  The message failed to send. The message contained an unsupported
+    ///         type.
+    IQSendMessageResult_Failure_UnsupportedType,
+
+    /// @brief  The message failed to send. The device does not have enough memory
+    ///         to receive the message.
+    IQSendMessageResult_Failure_InsufficientMemory,
+
+    /// @brief  The message failed to send. The connection timed out while sending
+    ///         the message.
+    IQSendMessageResult_Failure_Timeout,
+
+    /// @brief  The message failed to send and was retried, but could not complete
+    ///         after a number of tries.
+    IQSendMessageResult_Failure_MaxRetries,
+
+    /// @brief  The message was received by the device but it chose not to display
+    ///         a message prompt, ignoring the message.
+    IQSendMessageResult_Failure_PromptNotDisplayed,
+
+    /// @brief  The message was received by the device but the app to open
+    ///         was already running on the device.
+    IQSendMessageResult_Failure_AppAlreadyRunning,
+};
+NSString *NSStringFromSendMessageResult(IQSendMessageResult value);

+ 61 - 0
Dependencies/ios-armv7_arm64/ConnectIQ.framework/Headers/IQDevice.h

@@ -0,0 +1,61 @@
+//
+//  IQDevice.h
+//  ConnectIQ
+//
+//  Copyright (c) 2014 Garmin. All rights reserved.
+//
+
+#import <Foundation/Foundation.h>
+#import <CoreBluetooth/CoreBluetooth.h>
+
+/// @brief  The current status of an IQDevice.
+typedef NS_ENUM(NSInteger, IQDeviceStatus){
+    /// @brief  No device with this UUID has been registered for status events
+    ///         the SDK.
+    IQDeviceStatus_InvalidDevice,
+
+    /// @brief  Bluetooth is either powered off or resetting.
+    IQDeviceStatus_BluetoothNotReady,
+
+    /// @brief  This device could not be found by iOS. Perhaps the user removed
+    ///         the device?
+    IQDeviceStatus_NotFound,
+
+    /// @brief  The device is recognized by iOS, but it is not currently
+    ///         connected.
+    IQDeviceStatus_NotConnected,
+
+    /// @brief  The device is connected and ready to communicate.
+    IQDeviceStatus_Connected,
+};
+
+/// @brief  Represents a ConnectIQ-compatible Garmin device.
+@interface IQDevice : NSObject <NSSecureCoding>
+
+/// @brief  The unique identifier for this device.
+@property (nonatomic, readonly) NSUUID *uuid;
+
+/// @brief  The model name of the device provided by Garmin Connect Mobile.
+@property (nonatomic, readonly) NSString *modelName;
+
+/// @brief  The friendly name of the device, set by the user and provided by
+///         Garmin Connect Mobile.
+@property (nonatomic, readonly) NSString *friendlyName;
+
+/// @brief  Creates a new device instance.
+///
+/// @param  uuid         The UUID of the device to create.
+/// @param  modelName    The model name of the device to create.
+/// @param  friendlyName The friendly name of the device to create.
+///
+/// @return A new IQDevice instance with the appropriate values set.
++ (IQDevice *)deviceWithId:(NSUUID *)uuid modelName:(NSString *)modelName friendlyName:(NSString *)friendlyName;
+
+/// @brief  Creates a new device instance by copying another device's values.
+///
+/// @param  device The device to copy values from.
+///
+/// @return A new IQDevice instance with all values copied.
+- (instancetype)initWithDevice:(IQDevice *)device;
+
+@end

BIN
Dependencies/ios-armv7_arm64/ConnectIQ.framework/Info.plist


+ 6 - 0
Dependencies/ios-armv7_arm64/ConnectIQ.framework/Modules/module.modulemap

@@ -0,0 +1,6 @@
+framework module ConnectIQ {
+  umbrella header "ConnectIQ.h"
+
+  export *
+  module * { export * }
+}

BIN
Dependencies/ios-armv7_arm64/ConnectIQ.framework/ar.lproj/IQLocalizable.strings


BIN
Dependencies/ios-armv7_arm64/ConnectIQ.framework/cs.lproj/IQLocalizable.strings


BIN
Dependencies/ios-armv7_arm64/ConnectIQ.framework/da.lproj/IQLocalizable.strings


BIN
Dependencies/ios-armv7_arm64/ConnectIQ.framework/de.lproj/IQLocalizable.strings


BIN
Dependencies/ios-armv7_arm64/ConnectIQ.framework/el.lproj/IQLocalizable.strings


BIN
Dependencies/ios-armv7_arm64/ConnectIQ.framework/en.lproj/IQLocalizable.strings


BIN
Dependencies/ios-armv7_arm64/ConnectIQ.framework/es.lproj/IQLocalizable.strings


BIN
Dependencies/ios-armv7_arm64/ConnectIQ.framework/fi.lproj/IQLocalizable.strings


BIN
Dependencies/ios-armv7_arm64/ConnectIQ.framework/fr.lproj/IQLocalizable.strings


BIN
Dependencies/ios-armv7_arm64/ConnectIQ.framework/he.lproj/IQLocalizable.strings


BIN
Dependencies/ios-armv7_arm64/ConnectIQ.framework/hr.lproj/IQLocalizable.strings


BIN
Dependencies/ios-armv7_arm64/ConnectIQ.framework/hu.lproj/IQLocalizable.strings


BIN
Dependencies/ios-armv7_arm64/ConnectIQ.framework/id.lproj/IQLocalizable.strings


BIN
Dependencies/ios-armv7_arm64/ConnectIQ.framework/it.lproj/IQLocalizable.strings


BIN
Dependencies/ios-armv7_arm64/ConnectIQ.framework/ja.lproj/IQLocalizable.strings


BIN
Dependencies/ios-armv7_arm64/ConnectIQ.framework/ko.lproj/IQLocalizable.strings


BIN
Dependencies/ios-armv7_arm64/ConnectIQ.framework/ms.lproj/IQLocalizable.strings


BIN
Dependencies/ios-armv7_arm64/ConnectIQ.framework/nb.lproj/IQLocalizable.strings


BIN
Dependencies/ios-armv7_arm64/ConnectIQ.framework/nl.lproj/IQLocalizable.strings


BIN
Dependencies/ios-armv7_arm64/ConnectIQ.framework/pl.lproj/IQLocalizable.strings


BIN
Dependencies/ios-armv7_arm64/ConnectIQ.framework/pt-PT.lproj/IQLocalizable.strings


BIN
Dependencies/ios-armv7_arm64/ConnectIQ.framework/pt.lproj/IQLocalizable.strings


BIN
Dependencies/ios-armv7_arm64/ConnectIQ.framework/ru.lproj/IQLocalizable.strings


BIN
Dependencies/ios-armv7_arm64/ConnectIQ.framework/sk.lproj/IQLocalizable.strings


BIN
Dependencies/ios-armv7_arm64/ConnectIQ.framework/sv.lproj/IQLocalizable.strings


BIN
Dependencies/ios-armv7_arm64/ConnectIQ.framework/th.lproj/IQLocalizable.strings


BIN
Dependencies/ios-armv7_arm64/ConnectIQ.framework/tr.lproj/IQLocalizable.strings


BIN
Dependencies/ios-armv7_arm64/ConnectIQ.framework/zh-Hans.lproj/IQLocalizable.strings


BIN
Dependencies/ios-armv7_arm64/ConnectIQ.framework/zh-Hant.lproj/IQLocalizable.strings


+ 52 - 0
FreeAPS.xcodeproj/project.pbxproj

@@ -310,6 +310,13 @@
 		CE79502F29980E5800FA576E /* ShareClientUI.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = CE79502D29980E4D00FA576E /* ShareClientUI.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
 		CE82E02528E867BA00473A9C /* AlertStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE82E02428E867BA00473A9C /* AlertStorage.swift */; };
 		CE82E02728E869DF00473A9C /* AlertEntry.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE82E02628E869DF00473A9C /* AlertEntry.swift */; };
+		CE94597A29E9DF7B0047C9C6 /* ConnectIQ.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CE94597929E9DF7B0047C9C6 /* ConnectIQ.framework */; };
+		CE94597B29E9DFA90047C9C6 /* ConnectIQ.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = CE94597929E9DF7B0047C9C6 /* ConnectIQ.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
+		CE94597E29E9E1EE0047C9C6 /* GarminManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE94597D29E9E1EE0047C9C6 /* GarminManager.swift */; };
+		CE94598029E9E3BD0047C9C6 /* WatchConfigDataFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE94597F29E9E3BD0047C9C6 /* WatchConfigDataFlow.swift */; };
+		CE94598229E9E3D30047C9C6 /* WatchConfigProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE94598129E9E3D30047C9C6 /* WatchConfigProvider.swift */; };
+		CE94598429E9E3E60047C9C6 /* WatchConfigStateModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE94598329E9E3E60047C9C6 /* WatchConfigStateModel.swift */; };
+		CE94598729E9E4110047C9C6 /* WatchConfigRootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE94598629E9E4110047C9C6 /* WatchConfigRootView.swift */; };
 		CEA4F62329BE10F70011ADF7 /* SavitzkyGolayFilter.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEA4F62229BE10F70011ADF7 /* SavitzkyGolayFilter.swift */; };
 		CEB434DC28B8F5B900B70274 /* MKRingProgressView.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CEB434DB28B8F5B900B70274 /* MKRingProgressView.framework */; };
 		CEB434DD28B8F5B900B70274 /* MKRingProgressView.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = CEB434DB28B8F5B900B70274 /* MKRingProgressView.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
@@ -401,6 +408,7 @@
 			dstPath = "";
 			dstSubfolderSpec = 10;
 			files = (
+				CE94597B29E9DFA90047C9C6 /* ConnectIQ.framework in Embed Frameworks */,
 				CEC751DB29D88280006E9D24 /* MinimedKit.framework in Embed Frameworks */,
 				CEC751DC29D88280006E9D24 /* MinimedKitUI.framework in Embed Frameworks */,
 				CEC751D929D8827A006E9D24 /* OmniKit.framework in Embed Frameworks */,
@@ -789,6 +797,12 @@
 		CE79502D29980E4D00FA576E /* ShareClientUI.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = ShareClientUI.framework; sourceTree = BUILT_PRODUCTS_DIR; };
 		CE82E02428E867BA00473A9C /* AlertStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlertStorage.swift; sourceTree = "<group>"; };
 		CE82E02628E869DF00473A9C /* AlertEntry.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlertEntry.swift; sourceTree = "<group>"; };
+		CE94597929E9DF7B0047C9C6 /* ConnectIQ.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = ConnectIQ.framework; path = "Dependencies/ios-armv7_arm64/ConnectIQ.framework"; sourceTree = "<group>"; };
+		CE94597D29E9E1EE0047C9C6 /* GarminManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GarminManager.swift; sourceTree = "<group>"; };
+		CE94597F29E9E3BD0047C9C6 /* WatchConfigDataFlow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WatchConfigDataFlow.swift; sourceTree = "<group>"; };
+		CE94598129E9E3D30047C9C6 /* WatchConfigProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WatchConfigProvider.swift; sourceTree = "<group>"; };
+		CE94598329E9E3E60047C9C6 /* WatchConfigStateModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WatchConfigStateModel.swift; sourceTree = "<group>"; };
+		CE94598629E9E4110047C9C6 /* WatchConfigRootView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WatchConfigRootView.swift; sourceTree = "<group>"; };
 		CEA4F62229BE10F70011ADF7 /* SavitzkyGolayFilter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SavitzkyGolayFilter.swift; sourceTree = "<group>"; };
 		CEB434DB28B8F5B900B70274 /* MKRingProgressView.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = MKRingProgressView.framework; sourceTree = BUILT_PRODUCTS_DIR; };
 		CEB434DE28B8F5C400B70274 /* OmniBLE.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = OmniBLE.framework; sourceTree = BUILT_PRODUCTS_DIR; };
@@ -842,6 +856,7 @@
 			isa = PBXFrameworksBuildPhase;
 			buildActionMask = 2147483647;
 			files = (
+				CE94597A29E9DF7B0047C9C6 /* ConnectIQ.framework in Frameworks */,
 				CEC751D829D88262006E9D24 /* MinimedKitUI.framework in Frameworks */,
 				CEC751D629D88262006E9D24 /* MinimedKit.framework in Frameworks */,
 				CEC751D429D88257006E9D24 /* OmniKitUI.framework in Frameworks */,
@@ -1044,6 +1059,7 @@
 		3811DE0325C9D31700A708ED /* Modules */ = {
 			isa = PBXGroup;
 			children = (
+				CE94597C29E9E1CD0047C9C6 /* WatchConfig */,
 				19E1F7E629D0828B005C8D20 /* IconConfig */,
 				19D466A129AA2B0A004D5F33 /* FPUConfig */,
 				F90692CD274B99850037068D /* HealthKit */,
@@ -1313,6 +1329,7 @@
 		3818AA48274C267000843DB3 /* Frameworks */ = {
 			isa = PBXGroup;
 			children = (
+				CE94597929E9DF7B0047C9C6 /* ConnectIQ.framework */,
 				CEC751D529D88262006E9D24 /* MinimedKit.framework */,
 				CEC751D729D88262006E9D24 /* MinimedKitUI.framework */,
 				CEC751D129D88257006E9D24 /* OmniKit.framework */,
@@ -1673,6 +1690,7 @@
 			isa = PBXGroup;
 			children = (
 				38E8754E275556FA00975559 /* WatchManager.swift */,
+				CE94597D29E9E1EE0047C9C6 /* GarminManager.swift */,
 			);
 			path = WatchManager;
 			sourceTree = "<group>";
@@ -1943,6 +1961,25 @@
 			path = Bolus;
 			sourceTree = "<group>";
 		};
+		CE94597C29E9E1CD0047C9C6 /* WatchConfig */ = {
+			isa = PBXGroup;
+			children = (
+				CE94598529E9E3FE0047C9C6 /* View */,
+				CE94597F29E9E3BD0047C9C6 /* WatchConfigDataFlow.swift */,
+				CE94598129E9E3D30047C9C6 /* WatchConfigProvider.swift */,
+				CE94598329E9E3E60047C9C6 /* WatchConfigStateModel.swift */,
+			);
+			path = WatchConfig;
+			sourceTree = "<group>";
+		};
+		CE94598529E9E3FE0047C9C6 /* View */ = {
+			isa = PBXGroup;
+			children = (
+				CE94598629E9E4110047C9C6 /* WatchConfigRootView.swift */,
+			);
+			path = View;
+			sourceTree = "<group>";
+		};
 		CEB434E128B8F9BC00B70274 /* Bluetooth */ = {
 			isa = PBXGroup;
 			children = (
@@ -2360,6 +2397,7 @@
 				3894873A2614928B004DF424 /* DispatchTimer.swift in Sources */,
 				3895E4C625B9E00D00214B37 /* Preferences.swift in Sources */,
 				386A124F271707F000DDC61C /* DexcomSourceG6.swift in Sources */,
+				CE94598429E9E3E60047C9C6 /* WatchConfigStateModel.swift in Sources */,
 				38DF1786276A73D400B3528F /* TagCloudView.swift in Sources */,
 				38B4F3CD25E5031100E76A18 /* Broadcaster.swift in Sources */,
 				383420D925FFEB3F002D46C1 /* Popup.swift in Sources */,
@@ -2372,9 +2410,11 @@
 				38A0364225ED069400FCBB52 /* TempBasal.swift in Sources */,
 				3811DE1725C9D40400A708ED /* Screen.swift in Sources */,
 				383948DA25CD64D500E91849 /* Glucose.swift in Sources */,
+				CE94598029E9E3BD0047C9C6 /* WatchConfigDataFlow.swift in Sources */,
 				388E596C25AD95110019842D /* OpenAPS.swift in Sources */,
 				E00EEC0527368630002FF094 /* StorageAssembly.swift in Sources */,
 				384E803825C388640086DB71 /* Script.swift in Sources */,
+				CE94597E29E9E1EE0047C9C6 /* GarminManager.swift in Sources */,
 				3883583425EEB38000E024B2 /* PumpSettings.swift in Sources */,
 				38DAB280260CBB7F00F74C1A /* PumpView.swift in Sources */,
 				3811DEB125C9D88300A708ED /* Keychain.swift in Sources */,
@@ -2390,6 +2430,7 @@
 				CEB434E328B8F9DB00B70274 /* BluetoothStateManager.swift in Sources */,
 				3811DE4225C9D4A100A708ED /* SettingsDataFlow.swift in Sources */,
 				3811DE2525C9D48300A708ED /* MainRootView.swift in Sources */,
+				CE94598229E9E3D30047C9C6 /* WatchConfigProvider.swift in Sources */,
 				38E44535274E411700EC9A94 /* Disk+Data.swift in Sources */,
 				3811DE3125C9D49500A708ED /* HomeProvider.swift in Sources */,
 				FE41E4D629463EE20047FD55 /* NightscoutPreferences.swift in Sources */,
@@ -2594,6 +2635,7 @@
 				BA00D96F7B2FF169A06FB530 /* CGMStateModel.swift in Sources */,
 				61962FCAF8A2D222553AC5A3 /* LibreConfigDataFlow.swift in Sources */,
 				6EADD581738D64431902AC0A /* LibreConfigProvider.swift in Sources */,
+				CE94598729E9E4110047C9C6 /* WatchConfigRootView.swift in Sources */,
 				903D18976088B09110BCBE29 /* LibreConfigStateModel.swift in Sources */,
 				9050F378F0063C064D7FFC86 /* LibreConfigRootView.swift in Sources */,
 				B7C465E9472624D8A2BE2A6A /* CalibrationsDataFlow.swift in Sources */,
@@ -2862,6 +2904,10 @@
 				DEVELOPMENT_ASSET_PATHS = "";
 				DEVELOPMENT_TEAM = "$(DEVELOPER_TEAM)";
 				ENABLE_PREVIEWS = YES;
+				FRAMEWORK_SEARCH_PATHS = (
+					"$(inherited)",
+					"$(PROJECT_DIR)/Dependencies/ios-armv7_arm64",
+				);
 				INFOPLIST_FILE = FreeAPS/Resources/Info.plist;
 				IPHONEOS_DEPLOYMENT_TARGET = 15.2;
 				LD_RUNPATH_SEARCH_PATHS = (
@@ -2876,6 +2922,7 @@
 				OTHER_LDFLAGS = (
 					"-weak_framework",
 					CoreNFC,
+					"-ObjC",
 				);
 				PRODUCT_BUNDLE_IDENTIFIER = "$(BUNDLE_IDENTIFIER)";
 				PRODUCT_NAME = "$(TARGET_NAME)";
@@ -2899,6 +2946,10 @@
 				DEVELOPMENT_ASSET_PATHS = "";
 				DEVELOPMENT_TEAM = "$(DEVELOPER_TEAM)";
 				ENABLE_PREVIEWS = YES;
+				FRAMEWORK_SEARCH_PATHS = (
+					"$(inherited)",
+					"$(PROJECT_DIR)/Dependencies/ios-armv7_arm64",
+				);
 				INFOPLIST_FILE = FreeAPS/Resources/Info.plist;
 				IPHONEOS_DEPLOYMENT_TARGET = 15.2;
 				LD_RUNPATH_SEARCH_PATHS = (
@@ -2913,6 +2964,7 @@
 				OTHER_LDFLAGS = (
 					"-weak_framework",
 					CoreNFC,
+					"-ObjC",
 				);
 				PRODUCT_BUNDLE_IDENTIFIER = "$(BUNDLE_IDENTIFIER)";
 				PRODUCT_NAME = "$(TARGET_NAME)";

+ 6 - 3
FreeAPS/Resources/Info.plist

@@ -10,6 +10,8 @@
 	</array>
 	<key>BuildBranch</key>
 	<string></string>
+	<key>CBBundleDisplayName</key>
+	<string>$(APP_DISPLAY_NAME)</string>
 	<key>CFBundleDevelopmentRegion</key>
 	<string>$(DEVELOPMENT_LANGUAGE)</string>
 	<key>CFBundleDisplayName</key>
@@ -30,7 +32,9 @@
 	<array>
 		<dict>
 			<key>CFBundleTypeRole</key>
-			<string>Editor</string>
+			<string>None</string>
+			<key>CFBundleURLName</key>
+			<string>com.artificial-pancreas-iaps</string>
 			<key>CFBundleURLSchemes</key>
 			<array>
 				<string>freeaps-x</string>
@@ -43,6 +47,7 @@
 	<false/>
 	<key>LSApplicationQueriesSchemes</key>
 	<array>
+		<string>gcm-ciq</string>
 		<string>dexcomg7</string>
 		<string>xdripswift</string>
 		<string>dexcomg6</string>
@@ -104,8 +109,6 @@
 		<string>UIInterfaceOrientationPortrait</string>
 		<string>UIInterfaceOrientationPortraitUpsideDown</string>
 	</array>
-	<key>CBBundleDisplayName</key>
-	<string>$(APP_DISPLAY_NAME)</string>
 	<key>UISupportedInterfaceOrientations~ipad</key>
 	<array>
 		<string>UIInterfaceOrientationPortrait</string>

+ 2 - 1
FreeAPS/Resources/json/defaults/freeaps/freeaps_settings.json

@@ -26,5 +26,6 @@
     "individualAdjustmentFactor": 0.5,
     "timeCap": 8,
     "minuteInterval": 30,
-    "delay": 60
+    "delay": 60,
+    "displayOnWatch": "BGTarget"
 }

+ 11 - 0
FreeAPS/Sources/Application/FreeAPSApp.swift

@@ -59,9 +59,20 @@ import Swinject
             Main.RootView(resolver: resolver)
                 .environment(\.managedObjectContext, dataController.persistentContainer.viewContext)
                 .environmentObject(Icons())
+                .onOpenURL(perform: handleURL)
         }
         .onChange(of: scenePhase) { newScenePhase in
             debug(.default, "APPLICATION PHASE: \(newScenePhase)")
         }
     }
+
+    private func handleURL(_ url: URL) {
+        let components = URLComponents(url: url, resolvingAgainstBaseURL: false)
+
+        switch components?.host {
+        case "device-select-resp":
+            resolver.resolve(NotificationCenter.self)!.post(name: .openFromGarminConnect, object: url)
+        default: break
+        }
+    }
 }

+ 1 - 0
FreeAPS/Sources/Assemblies/ServiceAssembly.swift

@@ -19,5 +19,6 @@ final class ServiceAssembly: Assembly {
         container.register(HealthKitManager.self) { r in BaseHealthKitManager(resolver: r) }
         container.register(UserNotificationsManager.self) { r in BaseUserNotificationsManager(resolver: r) }
         container.register(WatchManager.self) { r in BaseWatchManager(resolver: r) }
+        container.register(GarminManager.self) { r in BaseGarminManager(resolver: r) }
     }
 }

+ 7 - 0
FreeAPS/Sources/Models/FreeAPSSettings.swift

@@ -12,6 +12,7 @@ struct FreeAPSSettings: JSON, Equatable {
     var insulinReqPercentage: Decimal = 70
     var skipBolusScreenAfterCarbs: Bool = false
     var displayHR: Bool = false
+    var displayOnWatch: AwConfig = .BGTarget
     var cgm: CGMType = .nightscout
     var uploadGlucose: Bool = false
     var useCalendar: Bool = false
@@ -81,6 +82,12 @@ extension FreeAPSSettings: Decodable {
 
         if let displayHR = try? container.decode(Bool.self, forKey: .displayHR) {
             settings.displayHR = displayHR
+            // compatibility if displayOnWatch is not available in json files
+            settings.displayOnWatch = (displayHR == true) ? AwConfig.HR : AwConfig.BGTarget
+        }
+
+        if let displayOnWatch = try? container.decode(AwConfig.self, forKey: .displayOnWatch) {
+            settings.displayOnWatch = displayOnWatch
         }
 
         if let cgm = try? container.decode(CGMType.self, forKey: .cgm) {

+ 0 - 1
FreeAPS/Sources/Modules/AddCarbs/View/AddCarbsRootView.swift

@@ -185,7 +185,6 @@ extension AddCarbs {
 
                         state.removePresetFromNewMeal()
                         if state.carbs == 0, state.fat == 0, state.protein == 0 { state.summation = [] }
-
                     }
                     label: { Text("[ -1 ]") }
                         .disabled(

+ 1 - 1
FreeAPS/Sources/Modules/Home/View/HomeRootView.swift

@@ -242,7 +242,7 @@ extension Home {
                 }
 
                 Spacer()
-                
+
                 if let overrideString = overrideString {
                     Text(overrideString)
                         .font(.system(size: 12))

+ 1 - 2
FreeAPS/Sources/Modules/PreferencesEditor/PreferencesEditorStateModel.swift

@@ -7,7 +7,6 @@ extension PreferencesEditor {
         @Published var allowAnnouncements = false
         @Published var insulinReqPercentage: Decimal = 70
         @Published var skipBolusScreenAfterCarbs = false
-        @Published var displayHR = false
         @Published var displayStatistics = false
         @Published var sections: [FieldSection] = []
 
@@ -15,7 +14,7 @@ extension PreferencesEditor {
             preferences = provider.preferences
             subscribeSetting(\.allowAnnouncements, on: $allowAnnouncements) { allowAnnouncements = $0 }
             subscribeSetting(\.insulinReqPercentage, on: $insulinReqPercentage) { insulinReqPercentage = $0 }
-            subscribeSetting(\.displayHR, on: $displayHR) { displayHR = $0 }
+            subscribeSetting(\.insulinReqFraction, on: $insulinReqFraction) { insulinReqFraction = $0 }
             subscribeSetting(\.displayStatistics, on: $displayStatistics) { displayStatistics = $0 }
             subscribeSetting(\.skipBolusScreenAfterCarbs, on: $skipBolusScreenAfterCarbs) { skipBolusScreenAfterCarbs = $0 }
 

+ 0 - 2
FreeAPS/Sources/Modules/PreferencesEditor/View/PreferencesEditorRootView.swift

@@ -37,8 +37,6 @@ extension PreferencesEditor {
 
                     Toggle("Skip Bolus screen after carbs", isOn: $state.skipBolusScreenAfterCarbs)
 
-                    Toggle("Display HR on Watch", isOn: $state.displayHR)
-
                     Toggle("Display Statistics", isOn: $state.displayStatistics)
                 }
 

+ 1 - 0
FreeAPS/Sources/Modules/Settings/View/SettingsRootView.swift

@@ -21,6 +21,7 @@ extension Settings {
                 Section(header: Text("Devices")) {
                     Text("Pump").navigationLink(to: .pumpConfig, from: self)
                     Text("CGM").navigationLink(to: .cgm, from: self)
+                    Text("Watch Devices").navigationLink(to: .watch, from: self)
                 }
 
                 Section(header: Text("Services")) {

+ 43 - 0
FreeAPS/Sources/Modules/WatchConfig/View/WatchConfigRootView.swift

@@ -0,0 +1,43 @@
+import SwiftUI
+import Swinject
+
+extension WatchConfig {
+    struct RootView: BaseView {
+        let resolver: Resolver
+        @StateObject var state = StateModel()
+
+        var body: some View {
+            Form {
+                Section(header: Text("Apple Watch")) {
+                    Picker(
+                        selection: $state.selectedAwConfig,
+                        label: Text("Display on Watch")
+                    ) {
+                        ForEach(AwConfig.allCases) { v in
+                            Text(v.displayName).tag(v)
+                        }
+                    }
+                }
+                Section(header: Text("Garmin Watch")) {
+                    List {
+                        ForEach(state.devices, id: \.uuid) { device in
+                            Text(device.friendlyName)
+                        }
+                        .onDelete(perform: onDelete)
+                    }
+                    Button("Add devices") {
+                        state.selectGarminDevices()
+                    }
+                }
+            }
+            .onAppear(perform: configureView)
+            .navigationTitle("Watch Configuration")
+            .navigationBarTitleDisplayMode(.automatic)
+        }
+
+        private func onDelete(offsets: IndexSet) {
+            state.devices.remove(atOffsets: offsets)
+            state.deleteGarminDevice()
+        }
+    }
+}

+ 5 - 0
FreeAPS/Sources/Modules/WatchConfig/WatchConfigDataFlow.swift

@@ -0,0 +1,5 @@
+enum WatchConfig {
+    enum Config {}
+}
+
+protocol WatchConfigProvider {}

+ 20 - 0
FreeAPS/Sources/Modules/WatchConfig/WatchConfigProvider.swift

@@ -0,0 +1,20 @@
+import Foundation
+
+extension WatchConfig {
+    final class Provider: BaseProvider, WatchConfigProvider {
+        @Injected() private var settingsManager: SettingsManager!
+        private let processQueue = DispatchQueue(label: "WatchDeviceProvider.processQueue")
+
+        var preferences: Preferences {
+            settingsManager.preferences
+        }
+
+        func savePreferences(_ preferences: Preferences) {
+            processQueue.async {
+                var prefs = preferences
+                prefs.timestamp = Date()
+                self.storage.save(prefs, as: OpenAPS.Settings.preferences)
+            }
+        }
+    }
+}

+ 58 - 0
FreeAPS/Sources/Modules/WatchConfig/WatchConfigStateModel.swift

@@ -0,0 +1,58 @@
+import ConnectIQ
+import SwiftUI
+
+enum AwConfig: String, JSON, CaseIterable, Identifiable, Codable {
+    var id: String { rawValue }
+    case HR
+    case BGTarget
+    case steps
+
+    var displayName: String {
+        switch self {
+        case .BGTarget:
+            return "Glucose Target"
+        case .HR:
+            return "Heart Rate"
+        case .steps:
+            return "Steps"
+        }
+    }
+}
+
+extension WatchConfig {
+    final class StateModel: BaseStateModel<Provider> {
+        @Injected() private var garmin: GarminManager!
+        @Published var devices: [IQDevice] = []
+        @Published var selectedAwConfig: AwConfig = .HR
+
+        private(set) var preferences = Preferences()
+
+        override func subscribe() {
+            preferences = provider.preferences
+
+            subscribeSetting(\.displayOnWatch, on: $selectedAwConfig) { selectedAwConfig = $0 }
+            didSet: { [weak self] value in
+                // for compatibility with old displayHR
+                switch value {
+                case .HR:
+                    self?.settingsManager.settings.displayHR = true
+                default:
+                    self?.settingsManager.settings.displayHR = false
+                }
+            }
+
+            devices = garmin.devices
+        }
+
+        func selectGarminDevices() {
+            garmin.selectDevices()
+                .receive(on: DispatchQueue.main)
+                .weakAssign(to: \.devices, on: self)
+                .store(in: &lifetime)
+        }
+
+        func deleteGarminDevice() {
+            garmin.updateListDevices(devices: devices)
+        }
+    }
+}

+ 3 - 0
FreeAPS/Sources/Router/Screen.swift

@@ -29,6 +29,7 @@ enum Screen: Identifiable, Hashable {
     case iconConfig
     case overrideProfilesConfig
     case snooze
+    case watch
 
     var id: Int { String(reflecting: self).hashValue }
 }
@@ -93,6 +94,8 @@ extension Screen {
             OverrideProfilesConfig.RootView(resolver: resolver)
         case .snooze:
             Snooze.RootView(resolver: resolver)
+        case .watch:
+            WatchConfig.RootView(resolver: resolver)
         }
     }
 

+ 208 - 0
FreeAPS/Sources/Services/WatchManager/GarminManager.swift

@@ -0,0 +1,208 @@
+import Combine
+import ConnectIQ
+import Foundation
+import Swinject
+
+protocol GarminManager {
+    func selectDevices() -> AnyPublisher<[IQDevice], Never>
+    func updateListDevices(devices: [IQDevice])
+    var devices: [IQDevice] { get }
+    func sendState(_ data: Data)
+    var stateRequet: (() -> (Data))? { get set }
+}
+
+extension Notification.Name {
+    static let openFromGarminConnect = Notification.Name("Notification.Name.openFromGarminConnect")
+}
+
+final class BaseGarminManager: NSObject, GarminManager, Injectable {
+    private enum Config {
+        static let watchfaceUUID = UUID(uuidString: "EC3420F6-027D-49B3-B45F-D81D6D3ED90A")
+        static let watchdataUUID = UUID(uuidString: "71CF0982-CA41-42A5-8441-EA81D36056C3")
+    }
+
+    private let connectIQ = ConnectIQ.sharedInstance()
+
+    private let router = FreeAPSApp.resolver.resolve(Router.self)!
+
+    @Injected() private var notificationCenter: NotificationCenter!
+
+    @Persisted(key: "BaseGarminManager.persistedDevices") private var persistedDevices: [CodableDevice] = []
+
+    private var watchfaces: [IQApp] = []
+
+    var stateRequet: (() -> (Data))?
+
+    private let stateSubject = PassthroughSubject<NSDictionary, Never>()
+
+    private(set) var devices: [IQDevice] = [] {
+        didSet {
+            persistedDevices = devices.map(CodableDevice.init)
+            watchfaces = []
+            devices.forEach { device in
+                connectIQ?.register(forDeviceEvents: device, delegate: self)
+                let watchfaceApp = IQApp(
+                    uuid: Config.watchfaceUUID,
+                    store: UUID(),
+                    device: device
+                )
+                let watchDataFieldApp = IQApp(
+                    uuid: Config.watchdataUUID,
+                    store: UUID(),
+                    device: device
+                )
+                watchfaces.append(watchfaceApp!)
+                watchfaces.append(watchDataFieldApp!)
+                connectIQ?.register(forAppMessages: watchfaceApp, delegate: self)
+            }
+        }
+    }
+
+    private var lifetime = Lifetime()
+    private var selectPromise: Future<[IQDevice], Never>.Promise?
+
+    init(resolver: Resolver) {
+        super.init()
+        connectIQ?.initialize(withUrlScheme: "freeaps-x", uiOverrideDelegate: self)
+        injectServices(resolver)
+        restoreDevices()
+        subscribeToOpenFromGarminConnect()
+        setupApplications()
+        subscribeState()
+    }
+
+    private func subscribeToOpenFromGarminConnect() {
+        notificationCenter
+            .publisher(for: .openFromGarminConnect)
+            .sink { notification in
+                guard let url = notification.object as? URL else { return }
+                self.parseDevicesFor(url: url)
+            }
+            .store(in: &lifetime)
+    }
+
+    private func subscribeState() {
+        func sendToWatchface(state: NSDictionary) {
+            watchfaces.forEach { app in
+                connectIQ?.getAppStatus(app) { status in
+                    guard status?.isInstalled ?? false else {
+                        debug(.service, "Garmin: watchface app not installed")
+                        return
+                    }
+                    debug(.service, "Garmin: sending message to watchface")
+                    self.sendMessage(state, to: app)
+                }
+            }
+        }
+
+        stateSubject
+            .throttle(for: .seconds(10), scheduler: DispatchQueue.main, latest: true)
+            .sink { state in
+                sendToWatchface(state: state)
+            }
+            .store(in: &lifetime)
+    }
+
+    private func restoreDevices() {
+        devices = persistedDevices.map(\.iqDevice)
+    }
+
+    private func parseDevicesFor(url: URL) {
+        devices = connectIQ?.parseDeviceSelectionResponse(from: url) as? [IQDevice] ?? []
+        selectPromise?(.success(devices))
+        selectPromise = nil
+    }
+
+    private func setupApplications() {
+        devices.forEach { _ in
+        }
+    }
+
+    func selectDevices() -> AnyPublisher<[IQDevice], Never> {
+        Future { promise in
+            self.selectPromise = promise
+            self.connectIQ?.showDeviceSelection()
+        }
+        .timeout(120, scheduler: DispatchQueue.main)
+        .replaceEmpty(with: [])
+        .eraseToAnyPublisher()
+    }
+
+    func updateListDevices(devices: [IQDevice]) {
+        self.devices = devices
+    }
+
+    func sendState(_ data: Data) {
+        guard let object = try? JSONSerialization.jsonObject(with: data, options: []) as? NSDictionary else {
+            return
+        }
+        stateSubject.send(object)
+    }
+
+    private func sendMessage(_ msg: NSDictionary, to app: IQApp) {
+        connectIQ?.sendMessage(msg, to: app, progress: { _, _ in
+            // debug(.service, "Garmin: sending progress: \(Int(Double(sent) / Double(all) * 100)) %")
+        }, completion: { result in
+            if result == .success {
+                debug(.service, "Garmin: message sent")
+            } else {
+                debug(.service, "Garmin: message failed")
+            }
+        })
+    }
+}
+
+extension BaseGarminManager: IQUIOverrideDelegate {
+    func needsToInstallConnectMobile() {
+        debug(.apsManager, "Garmin is not available")
+        let messageCont = MessageContent(
+            content: "The app Garmin Connect must be installed to use for iAPS.\n Go to App Store to download it",
+            type: .warning
+        )
+        router.alertMessage.send(messageCont)
+    }
+}
+
+extension BaseGarminManager: IQDeviceEventDelegate {
+    func deviceStatusChanged(_ device: IQDevice, status: IQDeviceStatus) {
+        switch status {
+        case .invalidDevice:
+            debug(.service, "Garmin: invalidDevice, Device: \(device.uuid!)")
+        case .bluetoothNotReady:
+            debug(.service, "Garmin: bluetoothNotReady, Device: \(device.uuid!)")
+        case .notFound:
+            debug(.service, "Garmin: notFound, Device: \(device.uuid!)")
+        case .notConnected:
+            debug(.service, "Garmin: notConnected, Device: \(device.uuid!)")
+        case .connected:
+            debug(.service, "Garmin: connected, Device: \(device.uuid!)")
+        @unknown default:
+            debug(.service, "Garmin: unknown state, Device: \(device.uuid!)")
+        }
+    }
+}
+
+extension BaseGarminManager: IQAppMessageDelegate {
+    func receivedMessage(_ message: Any, from app: IQApp) {
+        print("ASDF: got message: \(message) from app: \(app.uuid!)")
+        if let status = message as? String, status == "status", let watchState = stateRequet?() {
+            sendState(watchState)
+        }
+    }
+}
+
+struct CodableDevice: Codable, Equatable {
+    let id: UUID
+    let modelName: String
+    let friendlyName: String
+
+    init(iqDevice: IQDevice) {
+        id = iqDevice.uuid
+        modelName = iqDevice.modelName
+        friendlyName = iqDevice.modelName
+    }
+
+    var iqDevice: IQDevice {
+        IQDevice(id: id, modelName: modelName, friendlyName: friendlyName)
+    }
+}

+ 22 - 3
FreeAPS/Sources/Services/WatchManager/WatchManager.swift

@@ -16,6 +16,7 @@ final class BaseWatchManager: NSObject, WatchManager, Injectable {
     @Injected() private var storage: FileStorage!
     @Injected() private var carbsStorage: CarbsStorage!
     @Injected() private var tempTargetsStorage: TempTargetsStorage!
+    @Injected() private var garmin: GarminManager!
 
     private var lifetime = Lifetime()
 
@@ -40,6 +41,13 @@ final class BaseWatchManager: NSObject, WatchManager, Injectable {
         broadcaster.register(EnactedSuggestionObserver.self, observer: self)
         broadcaster.register(PumpBatteryObserver.self, observer: self)
         broadcaster.register(PumpReservoirObserver.self, observer: self)
+        garmin.stateRequet = { [weak self] () -> Data in
+            guard let self = self, let data = try? JSONEncoder().encode(self.state) else {
+                warning(.service, "Cannot encode watch state")
+                return Data()
+            }
+            return data
+        }
 
         configureState()
     }
@@ -50,9 +58,15 @@ final class BaseWatchManager: NSObject, WatchManager, Injectable {
             self.state.glucose = glucoseValues.glucose
             self.state.trend = glucoseValues.trend
             self.state.delta = glucoseValues.delta
+            self.state.trendRaw = self.glucoseStorage.recent().last?.direction?.rawValue
             self.state.glucoseDate = self.glucoseStorage.recent().last?.dateString
+            self.state.glucoseDateInterval = self.state.glucoseDate.map { UInt64($0.timeIntervalSince1970) }
             self.state.lastLoopDate = self.enactedSuggestion?.recieved == true ? self.enactedSuggestion?.deliverAt : self
                 .apsManager.lastLoopDate
+            self.state.lastLoopDateInterval = self.state.lastLoopDate.map {
+                guard $0.timeIntervalSince1970 > 0 else { return 0 }
+                return UInt64($0.timeIntervalSince1970)
+            }
             self.state.bolusIncrement = self.settingsManager.preferences.bolusIncrement
             self.state.maxCOB = self.settingsManager.preferences.maxCOB
             self.state.maxBolus = self.settingsManager.pumpSettings.maxBolus
@@ -80,9 +94,11 @@ final class BaseWatchManager: NSObject, WatchManager, Injectable {
                 }
             self.state.bolusAfterCarbs = !self.settingsManager.settings.skipBolusScreenAfterCarbs
 
-            self.state.displayHR = self.settingsManager.settings.displayHR
+            self.state.displayOnWatch = self.settingsManager.settings.displayOnWatch
 
-            self.state.eventualBG = self.evetualBGStraing()
+            let eBG = self.evetualBGStraing()
+            self.state.eventualBG = eBG.map { "⇢ " + $0 }
+            self.state.eventualBGRaw = eBG
 
             self.sendState()
         }
@@ -94,6 +110,9 @@ final class BaseWatchManager: NSObject, WatchManager, Injectable {
             warning(.service, "Cannot encode watch state")
             return
         }
+
+        garmin.sendState(data)
+
         guard session.isReachable else { return }
         session.sendMessageData(data, replyHandler: nil) { error in
             warning(.service, "Cannot send message to watch", error: error)
@@ -148,7 +167,7 @@ final class BaseWatchManager: NSObject, WatchManager, Injectable {
             return nil
         }
         let units = settingsManager.settings.units
-        return "⇢ " + eventualFormatter.string(
+        return eventualFormatter.string(
             from: (units == .mmolL ? eventualBG.asMmolL : Decimal(eventualBG)) as NSNumber
         )!
     }

+ 5 - 1
FreeAPSWatch WatchKit Extension/DataFlow.swift

@@ -3,9 +3,12 @@ import Foundation
 struct WatchState: Codable {
     var glucose: String?
     var trend: String?
+    var trendRaw: String?
     var delta: String?
     var glucoseDate: Date?
+    var glucoseDateInterval: UInt64?
     var lastLoopDate: Date?
+    var lastLoopDateInterval: UInt64?
     var bolusIncrement: Decimal?
     var maxCOB: Decimal?
     var maxBolus: Decimal?
@@ -16,7 +19,8 @@ struct WatchState: Codable {
     var tempTargets: [TempTargetWatchPreset] = []
     var bolusAfterCarbs: Bool?
     var eventualBG: String?
-    var displayHR: Bool?
+    var eventualBGRaw: String?
+    var displayOnWatch: AwConfig?
 }
 
 struct TempTargetWatchPreset: Codable, Identifiable {

+ 64 - 5
FreeAPSWatch WatchKit Extension/Views/MainView.swift

@@ -13,6 +13,7 @@ struct MainView: View {
     @State var isTargetsActive = false
     @State var isBolusActive = false
     @State private var pulse = 0
+    @State private var steps = 0
 
     @GestureState var isDetectingLongPress = false
     @State var completedLongPress = false
@@ -118,7 +119,8 @@ struct MainView: View {
                     .scaledToFill()
                     .minimumScaleFactor(0.5)
 
-                if state.displayHR {
+                switch state.displayOnWatch {
+                case .HR:
                     Spacer()
                     HStack {
                         if completedLongPress {
@@ -146,14 +148,25 @@ struct MainView: View {
                             .gesture(longPress)
                         }
                     }
-
-                } else if let eventualBG = state.eventualBG.nonEmpty {
+                case .BGTarget:
+                    if let eventualBG = state.eventualBG.nonEmpty {
+                        Spacer()
+                        HStack {
+                            Text(eventualBG)
+                                .font(.caption2)
+                                .scaledToFill()
+                                .foregroundColor(.secondary)
+                                .minimumScaleFactor(0.5)
+                        }
+                    }
+                case .steps:
                     Spacer()
                     HStack {
-                        Text(eventualBG)
+                        Text("🦶" + " \(steps)")
+                            .fontWeight(.regular)
                             .font(.caption2)
                             .scaledToFill()
-                            .foregroundColor(.secondary)
+                            .foregroundColor(.white)
                             .minimumScaleFactor(0.5)
                     }
                 }
@@ -255,15 +268,61 @@ struct MainView: View {
     func start() {
         autorizeHealthKit()
         startHeartRateQuery(quantityTypeIdentifier: .heartRate)
+        startStepsQuery(quantityTypeIdentifier: .stepCount)
     }
 
     func autorizeHealthKit() {
         let healthKitTypes: Set = [
+            HKObjectType.quantityType(forIdentifier: HKQuantityTypeIdentifier.stepCount)!,
             HKObjectType.quantityType(forIdentifier: HKQuantityTypeIdentifier.heartRate)!
         ]
         healthStore.requestAuthorization(toShare: healthKitTypes, read: healthKitTypes) { _, _ in }
     }
 
+    private func startStepsQuery(quantityTypeIdentifier _: HKQuantityTypeIdentifier) {
+        let type = HKQuantityType.quantityType(forIdentifier: .stepCount)!
+        let now = Date()
+        let startOfDay = Calendar.current.startOfDay(for: now)
+        var interval = DateComponents()
+        interval.day = 1
+        let query = HKStatisticsCollectionQuery(
+            quantityType: type,
+            quantitySamplePredicate: nil,
+            options: [.cumulativeSum],
+            anchorDate: startOfDay,
+            intervalComponents: interval
+        )
+
+        query.initialResultsHandler = { _, result, _ in
+            var resultCount = 0.0
+            guard let result = result else {
+                self.steps = 0
+                return
+            }
+            result.enumerateStatistics(from: startOfDay, to: now) { statistics, _ in
+
+                if let sum = statistics.sumQuantity() {
+                    // Get steps (they are of double type)
+                    resultCount = sum.doubleValue(for: HKUnit.count())
+                } // end if
+                // Return
+                self.steps = Int(resultCount)
+            }
+        }
+
+        query.statisticsUpdateHandler = {
+            _, statistics, _, _ in
+
+            // If new statistics are available
+            if let sum = statistics?.sumQuantity() {
+                let resultCount = sum.doubleValue(for: HKUnit.count())
+                // Return
+                self.steps = Int(resultCount)
+            } // end if
+        }
+        healthStore.execute(query)
+    }
+
     private func startHeartRateQuery(quantityTypeIdentifier: HKQuantityTypeIdentifier) {
         let devicePredicate = HKQuery.predicateForObjects(from: [HKDevice.local()])
         let updateHandler: (HKAnchoredObjectQuery, [HKSample]?, [HKDeletedObject]?, HKQueryAnchor?, Error?) -> Void = {

+ 9 - 2
FreeAPSWatch WatchKit Extension/WatchStateModel.swift

@@ -3,6 +3,13 @@ import Foundation
 import SwiftUI
 import WatchConnectivity
 
+enum AwConfig: String, CaseIterable, Identifiable, Codable {
+    var id: String { rawValue }
+    case HR
+    case BGTarget
+    case steps
+}
+
 class WatchStateModel: NSObject, ObservableObject {
     var session: WCSession
 
@@ -23,7 +30,7 @@ class WatchStateModel: NSObject, ObservableObject {
     @Published var isCarbsViewActive = false
     @Published var isTempTargetViewActive = false
     @Published var isBolusViewActive = false
-    @Published var displayHR = false
+    @Published var displayOnWatch: AwConfig = .BGTarget
     @Published var eventualBG = ""
     @Published var isConfirmationViewActive = false {
         didSet {
@@ -160,7 +167,7 @@ class WatchStateModel: NSObject, ObservableObject {
         bolusAfterCarbs = state.bolusAfterCarbs ?? true
         lastUpdate = Date()
         eventualBG = state.eventualBG ?? ""
-        displayHR = state.displayHR ?? false
+        displayOnWatch = state.displayOnWatch ?? .BGTarget
     }
 }