Przeglądaj źródła

add garmin management

add garmin management in iAPS. Requires to use data or watch faces
Pierre L 3 lat temu
rodzic
commit
9015374098
50 zmienionych plików z 771 dodań i 5 usunięć
  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. 10 0
      FreeAPS/Sources/Application/FreeAPSApp.swift
  41. 1 0
      FreeAPS/Sources/Assemblies/ServiceAssembly.swift
  42. 5 0
      FreeAPS/Sources/Modules/GarminConfig/GarminConfigDataFlow.swift
  43. 3 0
      FreeAPS/Sources/Modules/GarminConfig/GarminConfigProvider.swift
  44. 20 0
      FreeAPS/Sources/Modules/GarminConfig/GarminConfigStateModel.swift
  45. 30 0
      FreeAPS/Sources/Modules/GarminConfig/View/GarminConfigRootView.swift
  46. 1 0
      FreeAPS/Sources/Modules/Settings/View/SettingsRootView.swift
  47. 3 0
      FreeAPS/Sources/Router/Screen.swift
  48. 194 0
      FreeAPS/Sources/Services/WatchManager/GarminManager.swift
  49. 21 2
      FreeAPS/Sources/Services/WatchManager/WatchManager.swift
  50. 4 0
      FreeAPSWatch WatchKit Extension/DataFlow.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

@@ -318,6 +318,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 /* GarminConfigDataFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE94597F29E9E3BD0047C9C6 /* GarminConfigDataFlow.swift */; };
+		CE94598229E9E3D30047C9C6 /* GarminConfigProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE94598129E9E3D30047C9C6 /* GarminConfigProvider.swift */; };
+		CE94598429E9E3E60047C9C6 /* GarminConfigStateModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE94598329E9E3E60047C9C6 /* GarminConfigStateModel.swift */; };
+		CE94598729E9E4110047C9C6 /* GarminConfigRootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE94598629E9E4110047C9C6 /* GarminConfigRootView.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 */,
 				CE79502F29980E5800FA576E /* ShareClientUI.framework in Embed Frameworks */,
 				CE79502C29980CB500FA576E /* G7SensorKitUI.framework in Embed Frameworks */,
 				CE79502B29980CAF00FA576E /* CGMBLEKitUI.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 /* GarminConfigDataFlow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GarminConfigDataFlow.swift; sourceTree = "<group>"; };
+		CE94598129E9E3D30047C9C6 /* GarminConfigProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GarminConfigProvider.swift; sourceTree = "<group>"; };
+		CE94598329E9E3E60047C9C6 /* GarminConfigStateModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GarminConfigStateModel.swift; sourceTree = "<group>"; };
+		CE94598629E9E4110047C9C6 /* GarminConfigRootView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GarminConfigRootView.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; };
@@ -838,6 +852,7 @@
 			isa = PBXFrameworksBuildPhase;
 			buildActionMask = 2147483647;
 			files = (
+				CE94597A29E9DF7B0047C9C6 /* ConnectIQ.framework in Frameworks */,
 				CE79502E29980E4D00FA576E /* ShareClientUI.framework in Frameworks */,
 				CE79502A29980C9F00FA576E /* G7SensorKitUI.framework in Frameworks */,
 				CE79502829980C9600FA576E /* CGMBLEKitUI.framework in Frameworks */,
@@ -1040,6 +1055,7 @@
 		3811DE0325C9D31700A708ED /* Modules */ = {
 			isa = PBXGroup;
 			children = (
+				CE94597C29E9E1CD0047C9C6 /* GarminConfig */,
 				19E1F7E629D0828B005C8D20 /* IconConfig */,
 				19D466A129AA2B0A004D5F33 /* FPUConfig */,
 				F90692CD274B99850037068D /* HealthKit */,
@@ -1309,6 +1325,7 @@
 		3818AA48274C267000843DB3 /* Frameworks */ = {
 			isa = PBXGroup;
 			children = (
+				CE94597929E9DF7B0047C9C6 /* ConnectIQ.framework */,
 				CE79502D29980E4D00FA576E /* ShareClientUI.framework */,
 				CE79502929980C9F00FA576E /* G7SensorKitUI.framework */,
 				CE79502729980C9600FA576E /* CGMBLEKitUI.framework */,
@@ -1665,6 +1682,7 @@
 			isa = PBXGroup;
 			children = (
 				38E8754E275556FA00975559 /* WatchManager.swift */,
+				CE94597D29E9E1EE0047C9C6 /* GarminManager.swift */,
 			);
 			path = WatchManager;
 			sourceTree = "<group>";
@@ -1935,6 +1953,25 @@
 			path = Bolus;
 			sourceTree = "<group>";
 		};
+		CE94597C29E9E1CD0047C9C6 /* GarminConfig */ = {
+			isa = PBXGroup;
+			children = (
+				CE94598529E9E3FE0047C9C6 /* View */,
+				CE94597F29E9E3BD0047C9C6 /* GarminConfigDataFlow.swift */,
+				CE94598129E9E3D30047C9C6 /* GarminConfigProvider.swift */,
+				CE94598329E9E3E60047C9C6 /* GarminConfigStateModel.swift */,
+			);
+			path = GarminConfig;
+			sourceTree = "<group>";
+		};
+		CE94598529E9E3FE0047C9C6 /* View */ = {
+			isa = PBXGroup;
+			children = (
+				CE94598629E9E4110047C9C6 /* GarminConfigRootView.swift */,
+			);
+			path = View;
+			sourceTree = "<group>";
+		};
 		CEB434E128B8F9BC00B70274 /* Bluetooth */ = {
 			isa = PBXGroup;
 			children = (
@@ -2352,6 +2389,7 @@
 				3894873A2614928B004DF424 /* DispatchTimer.swift in Sources */,
 				3895E4C625B9E00D00214B37 /* Preferences.swift in Sources */,
 				386A124F271707F000DDC61C /* DexcomSourceG6.swift in Sources */,
+				CE94598429E9E3E60047C9C6 /* GarminConfigStateModel.swift in Sources */,
 				38DF1786276A73D400B3528F /* TagCloudView.swift in Sources */,
 				38B4F3CD25E5031100E76A18 /* Broadcaster.swift in Sources */,
 				383420D925FFEB3F002D46C1 /* Popup.swift in Sources */,
@@ -2364,9 +2402,11 @@
 				38A0364225ED069400FCBB52 /* TempBasal.swift in Sources */,
 				3811DE1725C9D40400A708ED /* Screen.swift in Sources */,
 				383948DA25CD64D500E91849 /* Glucose.swift in Sources */,
+				CE94598029E9E3BD0047C9C6 /* GarminConfigDataFlow.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 */,
@@ -2382,6 +2422,7 @@
 				CEB434E328B8F9DB00B70274 /* BluetoothStateManager.swift in Sources */,
 				3811DE4225C9D4A100A708ED /* SettingsDataFlow.swift in Sources */,
 				3811DE2525C9D48300A708ED /* MainRootView.swift in Sources */,
+				CE94598229E9E3D30047C9C6 /* GarminConfigProvider.swift in Sources */,
 				38E44535274E411700EC9A94 /* Disk+Data.swift in Sources */,
 				3811DE3125C9D49500A708ED /* HomeProvider.swift in Sources */,
 				FE41E4D629463EE20047FD55 /* NightscoutPreferences.swift in Sources */,
@@ -2586,6 +2627,7 @@
 				BA00D96F7B2FF169A06FB530 /* CGMStateModel.swift in Sources */,
 				61962FCAF8A2D222553AC5A3 /* LibreConfigDataFlow.swift in Sources */,
 				6EADD581738D64431902AC0A /* LibreConfigProvider.swift in Sources */,
+				CE94598729E9E4110047C9C6 /* GarminConfigRootView.swift in Sources */,
 				903D18976088B09110BCBE29 /* LibreConfigStateModel.swift in Sources */,
 				9050F378F0063C064D7FFC86 /* LibreConfigRootView.swift in Sources */,
 				B7C465E9472624D8A2BE2A6A /* CalibrationsDataFlow.swift in Sources */,
@@ -2854,6 +2896,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.1;
 				LD_RUNPATH_SEARCH_PATHS = (
@@ -2868,6 +2914,7 @@
 				OTHER_LDFLAGS = (
 					"-weak_framework",
 					CoreNFC,
+					"-ObjC",
 				);
 				PRODUCT_BUNDLE_IDENTIFIER = "$(BUNDLE_IDENTIFIER)";
 				PRODUCT_NAME = "$(TARGET_NAME)";
@@ -2891,6 +2938,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.1;
 				LD_RUNPATH_SEARCH_PATHS = (
@@ -2905,6 +2956,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>

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

@@ -64,4 +64,14 @@ import Swinject
             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) }
     }
 }

+ 5 - 0
FreeAPS/Sources/Modules/GarminConfig/GarminConfigDataFlow.swift

@@ -0,0 +1,5 @@
+enum GarminConfig {
+    enum Config {}
+}
+
+protocol GarminConfigProvider {}

+ 3 - 0
FreeAPS/Sources/Modules/GarminConfig/GarminConfigProvider.swift

@@ -0,0 +1,3 @@
+extension GarminConfig {
+    final class Provider: BaseProvider, GarminConfigProvider {}
+}

+ 20 - 0
FreeAPS/Sources/Modules/GarminConfig/GarminConfigStateModel.swift

@@ -0,0 +1,20 @@
+import ConnectIQ
+import SwiftUI
+
+extension GarminConfig {
+    final class StateModel: BaseStateModel<Provider> {
+        @Injected() private var garmin: GarminManager!
+        @Published var devices: [IQDevice] = []
+
+        override func subscribe() {
+            devices = garmin.devices
+        }
+
+        func selectDevices() {
+            garmin.selectDevices()
+                .receive(on: DispatchQueue.main)
+                .weakAssign(to: \.devices, on: self)
+                .store(in: &lifetime)
+        }
+    }
+}

+ 30 - 0
FreeAPS/Sources/Modules/GarminConfig/View/GarminConfigRootView.swift

@@ -0,0 +1,30 @@
+import SwiftUI
+import Swinject
+
+extension GarminConfig {
+    struct RootView: BaseView {
+        let resolver: Resolver
+        @StateObject var state = StateModel()
+
+        var body: some View {
+            Form {
+                Section {
+                    Button("Select devices") {
+                        state.selectDevices()
+                    }
+                }
+
+                if state.devices.isNotEmpty {
+                    Section(header: Text("Connected devices")) {
+                        ForEach(state.devices, id: \.uuid) { device in
+                            Text(device.friendlyName)
+                        }
+                    }
+                }
+            }
+            .onAppear(perform: configureView)
+            .navigationTitle("Garmin Watch")
+            .navigationBarTitleDisplayMode(.automatic)
+        }
+    }
+}

+ 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("Garmin watch").navigationLink(to: .garmin, from: self)
                 }
 
                 Section(header: Text("Services")) {

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

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

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

@@ -0,0 +1,194 @@
+import Combine
+import ConnectIQ
+import Foundation
+import Swinject
+
+protocol GarminManager {
+    func selectDevices() -> AnyPublisher<[IQDevice], Never>
+    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()
+
+    @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 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: { sent, all 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() {}
+}
+
+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)
+    }
+}

+ 21 - 2
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
@@ -82,7 +96,9 @@ final class BaseWatchManager: NSObject, WatchManager, Injectable {
 
             self.state.displayHR = self.settingsManager.settings.displayHR
 
-            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
         )!
     }

+ 4 - 0
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,6 +19,7 @@ struct WatchState: Codable {
     var tempTargets: [TempTargetWatchPreset] = []
     var bolusAfterCarbs: Bool?
     var eventualBG: String?
+    var eventualBGRaw: String?
     var displayHR: Bool?
 }