diff --git a/iOS/Hexiwear.xcodeproj/project.pbxproj b/iOS/Hexiwear.xcodeproj/project.pbxproj new file mode 100644 index 0000000..09f2bdd --- /dev/null +++ b/iOS/Hexiwear.xcodeproj/project.pbxproj @@ -0,0 +1,522 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 46; + objects = { + +/* Begin PBXBuildFile section */ + B3AB921D1D40B9C600368526 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3AB921C1D40B9C600368526 /* AppDelegate.swift */; }; + B3AB92221D40B9C600368526 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = B3AB92201D40B9C600368526 /* Main.storyboard */; }; + B3AB92241D40B9C600368526 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = B3AB92231D40B9C600368526 /* Assets.xcassets */; }; + B3AB92E51D40BAD700368526 /* AcceleratorTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3AB92E11D40BAD700368526 /* AcceleratorTableViewCell.swift */; }; + B3AB92E61D40BAD700368526 /* GyroTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3AB92E21D40BAD700368526 /* GyroTableViewCell.swift */; }; + B3AB92E71D40BAD700368526 /* HexiwearTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3AB92E31D40BAD700368526 /* HexiwearTableViewCell.swift */; }; + B3AB92E81D40BAD700368526 /* WeatherTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3AB92E41D40BAD700368526 /* WeatherTableViewCell.swift */; }; + B3AB92F11D40BB1000368526 /* Account.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3AB92EB1D40BB1000368526 /* Account.swift */; }; + B3AB92F21D40BB1000368526 /* Device.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3AB92EC1D40BB1000368526 /* Device.swift */; }; + B3AB92F31D40BB1000368526 /* Feed.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3AB92ED1D40BB1000368526 /* Feed.swift */; }; + B3AB92F41D40BB1000368526 /* Hexiwear.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3AB92EE1D40BB1000368526 /* Hexiwear.swift */; }; + B3AB92F51D40BB1000368526 /* TrackingDevice.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3AB92EF1D40BB1000368526 /* TrackingDevice.swift */; }; + B3AB92F61D40BB1000368526 /* UserCredentials.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3AB92F01D40BB1000368526 /* UserCredentials.swift */; }; + B3AB92FB1D40BB3F00368526 /* FirmwareSelectionTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3AB92F91D40BB3F00368526 /* FirmwareSelectionTableViewController.swift */; }; + B3AB92FC1D40BB3F00368526 /* FWUpgradeViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3AB92FA1D40BB3F00368526 /* FWUpgradeViewController.swift */; }; + B3AB93001D40BB5B00368526 /* ChangePasswordTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3AB92FE1D40BB5B00368526 /* ChangePasswordTableViewController.swift */; }; + B3AB93011D40BB5B00368526 /* SettingsTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3AB92FF1D40BB5B00368526 /* SettingsTableViewController.swift */; }; + B3AB93041D40BB8100368526 /* HexiwearTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3AB93031D40BB8100368526 /* HexiwearTableViewController.swift */; }; + B3AB93091D40BBAE00368526 /* ActivateDeviceViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3AB93061D40BBAE00368526 /* ActivateDeviceViewController.swift */; }; + B3AB930A1D40BBAE00368526 /* DetectHexiwearTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3AB93071D40BBAE00368526 /* DetectHexiwearTableViewController.swift */; }; + B3AB930B1D40BBAE00368526 /* ShowDevicesTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3AB93081D40BBAE00368526 /* ShowDevicesTableViewController.swift */; }; + B3AB930D1D40BBC300368526 /* BaseNavigationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3AB930C1D40BBC300368526 /* BaseNavigationController.swift */; }; + B3AB93131D40BBE100368526 /* CreateAccountTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3AB930F1D40BBE100368526 /* CreateAccountTableViewController.swift */; }; + B3AB93141D40BBE100368526 /* ForgotPasswordViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3AB93101D40BBE100368526 /* ForgotPasswordViewController.swift */; }; + B3AB93151D40BBE100368526 /* LoginViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3AB93111D40BBE100368526 /* LoginViewController.swift */; }; + B3AB93161D40BBE100368526 /* StaticViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3AB93121D40BBE100368526 /* StaticViewController.swift */; }; + B3AB931B1D40BC2800368526 /* DataStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3AB93181D40BC2800368526 /* DataStore.swift */; }; + B3AB931C1D40BC2800368526 /* MQTTAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3AB93191D40BC2800368526 /* MQTTAPI.swift */; }; + B3AB931D1D40BC2800368526 /* WebAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3AB931A1D40BC2800368526 /* WebAPI.swift */; }; + B3AB93221D40BC4600368526 /* HEXIWEAR_KW40_factory_settings.img in Resources */ = {isa = PBXBuildFile; fileRef = B3AB931F1D40BC4600368526 /* HEXIWEAR_KW40_factory_settings.img */; }; + B3AB93231D40BC4600368526 /* HEXIWEAR_MK64_factory_settings.img in Resources */ = {isa = PBXBuildFile; fileRef = B3AB93201D40BC4600368526 /* HEXIWEAR_MK64_factory_settings.img */; }; + B3AB93241D40BC4600368526 /* Util.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3AB93211D40BC4600368526 /* Util.swift */; }; + B3AB93301D40BC5000368526 /* ca.cer in Resources */ = {isa = PBXBuildFile; fileRef = B3AB93261D40BC5000368526 /* ca.cer */; }; + B3AB93311D40BC5000368526 /* CocoaMQTT.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3AB93281D40BC5000368526 /* CocoaMQTT.swift */; }; + B3AB93321D40BC5000368526 /* CocoaMQTTFrame.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3AB93291D40BC5000368526 /* CocoaMQTTFrame.swift */; }; + B3AB93331D40BC5000368526 /* CocoaMQTTMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3AB932A1D40BC5000368526 /* CocoaMQTTMessage.swift */; }; + B3AB93341D40BC5000368526 /* GCDAsyncSocket.m in Sources */ = {isa = PBXBuildFile; fileRef = B3AB932C1D40BC5000368526 /* GCDAsyncSocket.m */; }; + B3AB93351D40BC5000368526 /* LICENSE in Resources */ = {isa = PBXBuildFile; fileRef = B3AB932D1D40BC5000368526 /* LICENSE */; }; + B3AB93361D40BC5000368526 /* MSWeakTimer.m in Sources */ = {isa = PBXBuildFile; fileRef = B3AB932F1D40BC5000368526 /* MSWeakTimer.m */; }; +/* End PBXBuildFile section */ + +/* Begin PBXFileReference section */ + B3AB92191D40B9C600368526 /* Hexiwear.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Hexiwear.app; sourceTree = BUILT_PRODUCTS_DIR; }; + B3AB921C1D40B9C600368526 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + B3AB92211D40B9C600368526 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; + B3AB92231D40B9C600368526 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + B3AB92281D40B9C600368526 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + B3AB92E11D40BAD700368526 /* AcceleratorTableViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AcceleratorTableViewCell.swift; sourceTree = ""; }; + B3AB92E21D40BAD700368526 /* GyroTableViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GyroTableViewCell.swift; sourceTree = ""; }; + B3AB92E31D40BAD700368526 /* HexiwearTableViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HexiwearTableViewCell.swift; sourceTree = ""; }; + B3AB92E41D40BAD700368526 /* WeatherTableViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WeatherTableViewCell.swift; sourceTree = ""; }; + B3AB92EB1D40BB1000368526 /* Account.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Account.swift; sourceTree = ""; }; + B3AB92EC1D40BB1000368526 /* Device.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Device.swift; sourceTree = ""; }; + B3AB92ED1D40BB1000368526 /* Feed.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Feed.swift; sourceTree = ""; }; + B3AB92EE1D40BB1000368526 /* Hexiwear.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Hexiwear.swift; sourceTree = ""; }; + B3AB92EF1D40BB1000368526 /* TrackingDevice.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TrackingDevice.swift; sourceTree = ""; }; + B3AB92F01D40BB1000368526 /* UserCredentials.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UserCredentials.swift; sourceTree = ""; }; + B3AB92F91D40BB3F00368526 /* FirmwareSelectionTableViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FirmwareSelectionTableViewController.swift; sourceTree = ""; }; + B3AB92FA1D40BB3F00368526 /* FWUpgradeViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FWUpgradeViewController.swift; sourceTree = ""; }; + B3AB92FE1D40BB5B00368526 /* ChangePasswordTableViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChangePasswordTableViewController.swift; sourceTree = ""; }; + B3AB92FF1D40BB5B00368526 /* SettingsTableViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SettingsTableViewController.swift; sourceTree = ""; }; + B3AB93031D40BB8100368526 /* HexiwearTableViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HexiwearTableViewController.swift; sourceTree = ""; }; + B3AB93061D40BBAE00368526 /* ActivateDeviceViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ActivateDeviceViewController.swift; sourceTree = ""; }; + B3AB93071D40BBAE00368526 /* DetectHexiwearTableViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DetectHexiwearTableViewController.swift; sourceTree = ""; }; + B3AB93081D40BBAE00368526 /* ShowDevicesTableViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ShowDevicesTableViewController.swift; sourceTree = ""; }; + B3AB930C1D40BBC300368526 /* BaseNavigationController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BaseNavigationController.swift; sourceTree = ""; }; + B3AB930F1D40BBE100368526 /* CreateAccountTableViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CreateAccountTableViewController.swift; sourceTree = ""; }; + B3AB93101D40BBE100368526 /* ForgotPasswordViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ForgotPasswordViewController.swift; sourceTree = ""; }; + B3AB93111D40BBE100368526 /* LoginViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LoginViewController.swift; sourceTree = ""; }; + B3AB93121D40BBE100368526 /* StaticViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StaticViewController.swift; sourceTree = ""; }; + B3AB93181D40BC2800368526 /* DataStore.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DataStore.swift; sourceTree = ""; }; + B3AB93191D40BC2800368526 /* MQTTAPI.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MQTTAPI.swift; sourceTree = ""; }; + B3AB931A1D40BC2800368526 /* WebAPI.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WebAPI.swift; sourceTree = ""; }; + B3AB931F1D40BC4600368526 /* HEXIWEAR_KW40_factory_settings.img */ = {isa = PBXFileReference; lastKnownFileType = file; path = HEXIWEAR_KW40_factory_settings.img; sourceTree = ""; }; + B3AB93201D40BC4600368526 /* HEXIWEAR_MK64_factory_settings.img */ = {isa = PBXFileReference; lastKnownFileType = file; path = HEXIWEAR_MK64_factory_settings.img; sourceTree = ""; }; + B3AB93211D40BC4600368526 /* Util.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Util.swift; sourceTree = ""; }; + B3AB93261D40BC5000368526 /* ca.cer */ = {isa = PBXFileReference; lastKnownFileType = file; path = ca.cer; sourceTree = ""; }; + B3AB93271D40BC5000368526 /* CocoaMQTT.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = CocoaMQTT.h; sourceTree = ""; }; + B3AB93281D40BC5000368526 /* CocoaMQTT.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CocoaMQTT.swift; sourceTree = ""; }; + B3AB93291D40BC5000368526 /* CocoaMQTTFrame.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CocoaMQTTFrame.swift; sourceTree = ""; }; + B3AB932A1D40BC5000368526 /* CocoaMQTTMessage.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CocoaMQTTMessage.swift; sourceTree = ""; }; + B3AB932B1D40BC5000368526 /* GCDAsyncSocket.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = GCDAsyncSocket.h; sourceTree = ""; }; + B3AB932C1D40BC5000368526 /* GCDAsyncSocket.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GCDAsyncSocket.m; sourceTree = ""; }; + B3AB932D1D40BC5000368526 /* LICENSE */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = LICENSE; sourceTree = ""; }; + B3AB932E1D40BC5000368526 /* MSWeakTimer.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MSWeakTimer.h; sourceTree = ""; }; + B3AB932F1D40BC5000368526 /* MSWeakTimer.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MSWeakTimer.m; sourceTree = ""; }; + B3AB93371D40BC7500368526 /* Hexiwear-Bridging-Header.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "Hexiwear-Bridging-Header.h"; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + B3AB92161D40B9C600368526 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + B3AB92101D40B9C600368526 = { + isa = PBXGroup; + children = ( + B3AB921B1D40B9C600368526 /* Hexiwear */, + B3AB921A1D40B9C600368526 /* Products */, + ); + sourceTree = ""; + }; + B3AB921A1D40B9C600368526 /* Products */ = { + isa = PBXGroup; + children = ( + B3AB92191D40B9C600368526 /* Hexiwear.app */, + ); + name = Products; + sourceTree = ""; + }; + B3AB921B1D40B9C600368526 /* Hexiwear */ = { + isa = PBXGroup; + children = ( + B3AB93371D40BC7500368526 /* Hexiwear-Bridging-Header.h */, + B3AB921C1D40B9C600368526 /* AppDelegate.swift */, + B3AB92EA1D40BAEE00368526 /* Model */, + B3AB92E01D40BAAD00368526 /* View */, + B3AB92F71D40BB2600368526 /* Controller */, + B3AB93171D40BC0E00368526 /* Service */, + B3AB931E1D40BC3300368526 /* Util */, + B3AB93251D40BC5000368526 /* CocoaMQTT */, + B3AB92201D40B9C600368526 /* Main.storyboard */, + B3AB92231D40B9C600368526 /* Assets.xcassets */, + B3AB92281D40B9C600368526 /* Info.plist */, + ); + path = Hexiwear; + sourceTree = ""; + }; + B3AB92E01D40BAAD00368526 /* View */ = { + isa = PBXGroup; + children = ( + B3AB92E11D40BAD700368526 /* AcceleratorTableViewCell.swift */, + B3AB92E21D40BAD700368526 /* GyroTableViewCell.swift */, + B3AB92E31D40BAD700368526 /* HexiwearTableViewCell.swift */, + B3AB92E41D40BAD700368526 /* WeatherTableViewCell.swift */, + ); + name = View; + sourceTree = ""; + }; + B3AB92EA1D40BAEE00368526 /* Model */ = { + isa = PBXGroup; + children = ( + B3AB92EB1D40BB1000368526 /* Account.swift */, + B3AB92EC1D40BB1000368526 /* Device.swift */, + B3AB92ED1D40BB1000368526 /* Feed.swift */, + B3AB92EE1D40BB1000368526 /* Hexiwear.swift */, + B3AB92EF1D40BB1000368526 /* TrackingDevice.swift */, + B3AB92F01D40BB1000368526 /* UserCredentials.swift */, + ); + name = Model; + sourceTree = ""; + }; + B3AB92F71D40BB2600368526 /* Controller */ = { + isa = PBXGroup; + children = ( + B3AB930C1D40BBC300368526 /* BaseNavigationController.swift */, + B3AB930E1D40BBCA00368526 /* Login */, + B3AB93051D40BB9000368526 /* Detect Hexiwear */, + B3AB93021D40BB6A00368526 /* Hexiwear */, + B3AB92FD1D40BB4400368526 /* Settings */, + B3AB92F81D40BB2E00368526 /* OTAP */, + ); + name = Controller; + sourceTree = ""; + }; + B3AB92F81D40BB2E00368526 /* OTAP */ = { + isa = PBXGroup; + children = ( + B3AB92F91D40BB3F00368526 /* FirmwareSelectionTableViewController.swift */, + B3AB92FA1D40BB3F00368526 /* FWUpgradeViewController.swift */, + ); + name = OTAP; + sourceTree = ""; + }; + B3AB92FD1D40BB4400368526 /* Settings */ = { + isa = PBXGroup; + children = ( + B3AB92FE1D40BB5B00368526 /* ChangePasswordTableViewController.swift */, + B3AB92FF1D40BB5B00368526 /* SettingsTableViewController.swift */, + ); + name = Settings; + sourceTree = ""; + }; + B3AB93021D40BB6A00368526 /* Hexiwear */ = { + isa = PBXGroup; + children = ( + B3AB93031D40BB8100368526 /* HexiwearTableViewController.swift */, + ); + name = Hexiwear; + sourceTree = ""; + }; + B3AB93051D40BB9000368526 /* Detect Hexiwear */ = { + isa = PBXGroup; + children = ( + B3AB93061D40BBAE00368526 /* ActivateDeviceViewController.swift */, + B3AB93071D40BBAE00368526 /* DetectHexiwearTableViewController.swift */, + B3AB93081D40BBAE00368526 /* ShowDevicesTableViewController.swift */, + ); + name = "Detect Hexiwear"; + sourceTree = ""; + }; + B3AB930E1D40BBCA00368526 /* Login */ = { + isa = PBXGroup; + children = ( + B3AB930F1D40BBE100368526 /* CreateAccountTableViewController.swift */, + B3AB93101D40BBE100368526 /* ForgotPasswordViewController.swift */, + B3AB93111D40BBE100368526 /* LoginViewController.swift */, + B3AB93121D40BBE100368526 /* StaticViewController.swift */, + ); + name = Login; + sourceTree = ""; + }; + B3AB93171D40BC0E00368526 /* Service */ = { + isa = PBXGroup; + children = ( + B3AB93181D40BC2800368526 /* DataStore.swift */, + B3AB93191D40BC2800368526 /* MQTTAPI.swift */, + B3AB931A1D40BC2800368526 /* WebAPI.swift */, + ); + name = Service; + sourceTree = ""; + }; + B3AB931E1D40BC3300368526 /* Util */ = { + isa = PBXGroup; + children = ( + B3AB931F1D40BC4600368526 /* HEXIWEAR_KW40_factory_settings.img */, + B3AB93201D40BC4600368526 /* HEXIWEAR_MK64_factory_settings.img */, + B3AB93211D40BC4600368526 /* Util.swift */, + ); + name = Util; + sourceTree = ""; + }; + B3AB93251D40BC5000368526 /* CocoaMQTT */ = { + isa = PBXGroup; + children = ( + B3AB93261D40BC5000368526 /* ca.cer */, + B3AB93271D40BC5000368526 /* CocoaMQTT.h */, + B3AB93281D40BC5000368526 /* CocoaMQTT.swift */, + B3AB93291D40BC5000368526 /* CocoaMQTTFrame.swift */, + B3AB932A1D40BC5000368526 /* CocoaMQTTMessage.swift */, + B3AB932B1D40BC5000368526 /* GCDAsyncSocket.h */, + B3AB932C1D40BC5000368526 /* GCDAsyncSocket.m */, + B3AB932D1D40BC5000368526 /* LICENSE */, + B3AB932E1D40BC5000368526 /* MSWeakTimer.h */, + B3AB932F1D40BC5000368526 /* MSWeakTimer.m */, + ); + path = CocoaMQTT; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + B3AB92181D40B9C600368526 /* Hexiwear */ = { + isa = PBXNativeTarget; + buildConfigurationList = B3AB922B1D40B9C600368526 /* Build configuration list for PBXNativeTarget "Hexiwear" */; + buildPhases = ( + B3AB92151D40B9C600368526 /* Sources */, + B3AB92161D40B9C600368526 /* Frameworks */, + B3AB92171D40B9C600368526 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = Hexiwear; + productName = Hexiwear; + productReference = B3AB92191D40B9C600368526 /* Hexiwear.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + B3AB92111D40B9C600368526 /* Project object */ = { + isa = PBXProject; + attributes = { + LastSwiftUpdateCheck = 0730; + LastUpgradeCheck = 0730; + ORGANIZATIONNAME = WolkAbout; + TargetAttributes = { + B3AB92181D40B9C600368526 = { + CreatedOnToolsVersion = 7.3.1; + }; + }; + }; + buildConfigurationList = B3AB92141D40B9C600368526 /* Build configuration list for PBXProject "Hexiwear" */; + compatibilityVersion = "Xcode 3.2"; + developmentRegion = English; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = B3AB92101D40B9C600368526; + productRefGroup = B3AB921A1D40B9C600368526 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + B3AB92181D40B9C600368526 /* Hexiwear */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + B3AB92171D40B9C600368526 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + B3AB93351D40BC5000368526 /* LICENSE in Resources */, + B3AB93301D40BC5000368526 /* ca.cer in Resources */, + B3AB92241D40B9C600368526 /* Assets.xcassets in Resources */, + B3AB93221D40BC4600368526 /* HEXIWEAR_KW40_factory_settings.img in Resources */, + B3AB93231D40BC4600368526 /* HEXIWEAR_MK64_factory_settings.img in Resources */, + B3AB92221D40B9C600368526 /* Main.storyboard in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + B3AB92151D40B9C600368526 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + B3AB93341D40BC5000368526 /* GCDAsyncSocket.m in Sources */, + B3AB93041D40BB8100368526 /* HexiwearTableViewController.swift in Sources */, + B3AB93331D40BC5000368526 /* CocoaMQTTMessage.swift in Sources */, + B3AB92E81D40BAD700368526 /* WeatherTableViewCell.swift in Sources */, + B3AB93141D40BBE100368526 /* ForgotPasswordViewController.swift in Sources */, + B3AB93151D40BBE100368526 /* LoginViewController.swift in Sources */, + B3AB93161D40BBE100368526 /* StaticViewController.swift in Sources */, + B3AB92F61D40BB1000368526 /* UserCredentials.swift in Sources */, + B3AB93131D40BBE100368526 /* CreateAccountTableViewController.swift in Sources */, + B3AB93091D40BBAE00368526 /* ActivateDeviceViewController.swift in Sources */, + B3AB92F31D40BB1000368526 /* Feed.swift in Sources */, + B3AB930B1D40BBAE00368526 /* ShowDevicesTableViewController.swift in Sources */, + B3AB92FB1D40BB3F00368526 /* FirmwareSelectionTableViewController.swift in Sources */, + B3AB93241D40BC4600368526 /* Util.swift in Sources */, + B3AB92F51D40BB1000368526 /* TrackingDevice.swift in Sources */, + B3AB93311D40BC5000368526 /* CocoaMQTT.swift in Sources */, + B3AB930D1D40BBC300368526 /* BaseNavigationController.swift in Sources */, + B3AB931B1D40BC2800368526 /* DataStore.swift in Sources */, + B3AB921D1D40B9C600368526 /* AppDelegate.swift in Sources */, + B3AB93001D40BB5B00368526 /* ChangePasswordTableViewController.swift in Sources */, + B3AB930A1D40BBAE00368526 /* DetectHexiwearTableViewController.swift in Sources */, + B3AB92E61D40BAD700368526 /* GyroTableViewCell.swift in Sources */, + B3AB92E71D40BAD700368526 /* HexiwearTableViewCell.swift in Sources */, + B3AB92E51D40BAD700368526 /* AcceleratorTableViewCell.swift in Sources */, + B3AB931C1D40BC2800368526 /* MQTTAPI.swift in Sources */, + B3AB92FC1D40BB3F00368526 /* FWUpgradeViewController.swift in Sources */, + B3AB931D1D40BC2800368526 /* WebAPI.swift in Sources */, + B3AB93011D40BB5B00368526 /* SettingsTableViewController.swift in Sources */, + B3AB93361D40BC5000368526 /* MSWeakTimer.m in Sources */, + B3AB92F41D40BB1000368526 /* Hexiwear.swift in Sources */, + B3AB92F21D40BB1000368526 /* Device.swift in Sources */, + B3AB92F11D40BB1000368526 /* Account.swift in Sources */, + B3AB93321D40BC5000368526 /* CocoaMQTTFrame.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXVariantGroup section */ + B3AB92201D40B9C600368526 /* Main.storyboard */ = { + isa = PBXVariantGroup; + children = ( + B3AB92211D40B9C600368526 /* Base */, + ); + name = Main.storyboard; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + B3AB92291D40B9C600368526 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 9.3; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + SWIFT_OBJC_BRIDGING_HEADER = "Hexiwear/Hexiwear-Bridging-Header.h"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + B3AB922A1D40B9C600368526 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 9.3; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + SWIFT_OBJC_BRIDGING_HEADER = "Hexiwear/Hexiwear-Bridging-Header.h"; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + B3AB922C1D40B9C600368526 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + INFOPLIST_FILE = Hexiwear/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = com.wolkabout.Hexiwear; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Debug; + }; + B3AB922D1D40B9C600368526 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + INFOPLIST_FILE = Hexiwear/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = com.wolkabout.Hexiwear; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + B3AB92141D40B9C600368526 /* Build configuration list for PBXProject "Hexiwear" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + B3AB92291D40B9C600368526 /* Debug */, + B3AB922A1D40B9C600368526 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + B3AB922B1D40B9C600368526 /* Build configuration list for PBXNativeTarget "Hexiwear" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + B3AB922C1D40B9C600368526 /* Debug */, + B3AB922D1D40B9C600368526 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = B3AB92111D40B9C600368526 /* Project object */; +} diff --git a/iOS/Hexiwear.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/iOS/Hexiwear.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..97720fb --- /dev/null +++ b/iOS/Hexiwear.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/iOS/Hexiwear.xcodeproj/project.xcworkspace/xcuserdata/iholod.xcuserdatad/UserInterfaceState.xcuserstate b/iOS/Hexiwear.xcodeproj/project.xcworkspace/xcuserdata/iholod.xcuserdatad/UserInterfaceState.xcuserstate new file mode 100644 index 0000000..89ace58 Binary files /dev/null and b/iOS/Hexiwear.xcodeproj/project.xcworkspace/xcuserdata/iholod.xcuserdatad/UserInterfaceState.xcuserstate differ diff --git a/iOS/Hexiwear.xcodeproj/xcuserdata/iholod.xcuserdatad/xcschemes/Hexiwear.xcscheme b/iOS/Hexiwear.xcodeproj/xcuserdata/iholod.xcuserdatad/xcschemes/Hexiwear.xcscheme new file mode 100644 index 0000000..9550647 --- /dev/null +++ b/iOS/Hexiwear.xcodeproj/xcuserdata/iholod.xcuserdatad/xcschemes/Hexiwear.xcscheme @@ -0,0 +1,91 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/iOS/Hexiwear.xcodeproj/xcuserdata/iholod.xcuserdatad/xcschemes/xcschememanagement.plist b/iOS/Hexiwear.xcodeproj/xcuserdata/iholod.xcuserdatad/xcschemes/xcschememanagement.plist new file mode 100644 index 0000000..538974f --- /dev/null +++ b/iOS/Hexiwear.xcodeproj/xcuserdata/iholod.xcuserdatad/xcschemes/xcschememanagement.plist @@ -0,0 +1,22 @@ + + + + + SchemeUserState + + Hexiwear.xcscheme + + orderHint + 0 + + + SuppressBuildableAutocreation + + B3AB92181D40B9C600368526 + + primary + + + + + diff --git a/iOS/Hexiwear/AcceleratorTableViewCell.swift b/iOS/Hexiwear/AcceleratorTableViewCell.swift new file mode 100644 index 0000000..7451017 --- /dev/null +++ b/iOS/Hexiwear/AcceleratorTableViewCell.swift @@ -0,0 +1,68 @@ +// +// Hexiwear application is used to pair with Hexiwear BLE devices +// and send sensor readings to WolkSense sensor data cloud +// +// Copyright (C) 2016 WolkAbout Technology s.r.o. +// +// Hexiwear is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Hexiwear is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . +// +// +// AcceleratorTableViewCell.swift +// + +import UIKit + +class AcceleratorTableViewCell: UITableViewCell { + + @IBOutlet private weak var xLabel: UILabel! + @IBOutlet private weak var yLabel: UILabel! + @IBOutlet private weak var zLabel: UILabel! + + override func awakeFromNib() { + super.awakeFromNib() + // Initialization code + } + + override func setSelected(selected: Bool, animated: Bool) { + super.setSelected(selected, animated: animated) + } + + var xValue: String { + get { + return xLabel.text! + } + set (newX) { + xLabel.text = newX + } + } + + var yValue: String { + get { + return yLabel.text! + } + set (newY) { + yLabel.text = newY + } + } + + var zValue: String { + get { + return zLabel.text! + } + set (newZ) { + zLabel.text = newZ + } + } + +} diff --git a/iOS/Hexiwear/Account.swift b/iOS/Hexiwear/Account.swift new file mode 100644 index 0000000..05d3cd2 --- /dev/null +++ b/iOS/Hexiwear/Account.swift @@ -0,0 +1,38 @@ +// +// Hexiwear application is used to pair with Hexiwear BLE devices +// and send sensor readings to WolkSense sensor data cloud +// +// Copyright (C) 2016 WolkAbout Technology s.r.o. +// +// Hexiwear is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Hexiwear is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . +// +// +// Account.swift +// + +import Foundation + +class Account { + let firstName: String + let lastName: String + let email: String + let password: String + + init (firstName: String, lastName: String, email: String, password: String) { + self.firstName = firstName + self.lastName = lastName + self.email = email + self.password = password + } +} \ No newline at end of file diff --git a/iOS/Hexiwear/ActivateDeviceViewController.swift b/iOS/Hexiwear/ActivateDeviceViewController.swift new file mode 100644 index 0000000..e62fae2 --- /dev/null +++ b/iOS/Hexiwear/ActivateDeviceViewController.swift @@ -0,0 +1,254 @@ +// +// Hexiwear application is used to pair with Hexiwear BLE devices +// and send sensor readings to WolkSense sensor data cloud +// +// Copyright (C) 2016 WolkAbout Technology s.r.o. +// +// Hexiwear is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Hexiwear is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . +// +// +// ActivateDeviceViewController.swift +// + +import UIKit +import CoreBluetooth + +protocol DeviceActivationDelegate { + func didActivateDevice(pointId: Int, serials: SerialMapping) + func didSkipActivation() +} + +class SingleTextViewController: UIViewController { + + var skipButton: UIBarButtonItem! + var actionButton: UIBarButtonItem! + + var dataStore: DataStore! + var selectedPeripheral: CBPeripheral! + var selectedPeripheralSerial: String = "" + + override func viewDidLoad() { + super.viewDidLoad() + skipButton = UIBarButtonItem(title: "Skip", style: .Plain, target: self, action: #selector(SingleTextViewController.skipButtonAction)) + navigationItem.leftBarButtonItems = [skipButton] + actionButton = UIBarButtonItem(title: "Activate", style: .Plain, target: self, action: #selector(SingleTextViewController.actionButtonAction)) + actionButton.enabled = false + navigationItem.rightBarButtonItems = [actionButton] + + if selectedPeripheral != nil { + selectedPeripheralSerial = selectedPeripheral!.identifier.UUIDString + } + + } + + func toggleActivateButtonEnabled() { + print("Override in subclass") + } + + func actionButtonAction() { + print("Override in subclass") + } + + func skipButtonAction() { + print("Override in subclass") + } + + +} + +class ActivateDeviceViewController : SingleTextViewController { + + @IBOutlet weak var deviceName: UITextField! + @IBOutlet weak var errorLabel: UILabel! + @IBOutlet weak var registerWithUsedName: UIButton! + + var deviceActivationDelegate: DeviceActivationDelegate? + var selectableDevices: [Device] = [] + var selectedDeviceSerial: String = "" + + override func viewDidLoad() { + super.viewDidLoad() + skipButton.title = "Skip" + actionButton.title = "Activate" + deviceName.delegate = self + title = "Activate hexiwear" + errorLabel.hidden = true + + selectableDevices = dataStore.points + .filter { $0.owner.uppercaseString == "SELF" && $0.isHexiwear() } + .sort { $0.name.uppercaseString < $1.name.uppercaseString } + registerWithUsedName.enabled = selectableDevices.count > 0 + } + + override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) { + if segue.identifier == "toAlreadyRegisteredNames" { + if let vc = segue.destinationViewController as? ShowDevicesTableViewController { + vc.selectableDevices = selectableDevices + vc.activatedDeviceDelegate = self + } + } + } + + override func toggleActivateButtonEnabled() { + if let dn = deviceName.text where dn.isEmpty == false { + actionButton.enabled = true + } + else { + actionButton.enabled = false + } + } + + override func actionButtonAction() { + + guard selectedPeripheralSerial != "" else { + errorLabel.text = "Error activating HEXIWEAR. Missing serial number." + errorLabel.hidden = false + view.setNeedsDisplay() + return + } + + let progressHUD = JGProgressHUD(style: .Dark) + progressHUD.textLabel.text = "Activating..." + progressHUD.showInView(self.view, animated: true) + + dataStore.getSerial( + // get serial failure + { _ in self.activationErrorHandler(progressHUD) }, + + // get serial success + onSuccess: { serial in + let deviceName = self.deviceName.text! + print("ACTI -- device: " + deviceName) + + // If there is no device selected to continue with... + guard self.selectedDeviceSerial.characters.count > 0 else { + // ... just activate new device + print("ACTI -- no old serial, just activate new one") + self.activateDeviceWithSerial(serial, deviceName: deviceName, progressHUD: progressHUD) + return + } + + // check if already selected device serial is activated + if self.selectedDeviceSerial.characters.count > 0 { + self.dataStore.getActivationStatusForSerial( self.selectedDeviceSerial, + onFailure: { _ in self.activationErrorHandler(progressHUD) }, + onSuccess: { activationStatus in + dispatch_async(dispatch_get_main_queue()) { + + // ... if it is activated then first deactivate device with old serial number + if activationStatus == "ACTIVATED" { + print("ACTI -- old serial ACTIVATED") + self.dataStore.deactivateDevice(self.selectedDeviceSerial, + onFailure: { _ in self.activationErrorHandler(progressHUD) }, + onSuccess: { + print("ACTI -- old serial deactivated, proceeding with new serial activation") + + // ... and activate device with new serial number + self.activateDeviceWithSerial(serial, deviceName: deviceName, progressHUD: progressHUD) + } + ) + } + else { + // ... if it is not activated then activate device with new serial number + print("ACTI -- old serial NOT ACTIVATED, proceeding with new serial activation") + self.activateDeviceWithSerial(serial, deviceName: deviceName, progressHUD: progressHUD) + } + } + } + ) + } + } + ) + } + + private func activationErrorHandler(progressHUD: JGProgressHUD) { + dispatch_async(dispatch_get_main_queue()) { + progressHUD.dismiss() + self.errorLabel.text = "Error activating device!" + self.errorLabel.hidden = false + self.view.setNeedsDisplay() + } + } + + private func activateDeviceWithSerial(serial: String, deviceName: String, progressHUD: JGProgressHUD) { + dataStore.activateDeviceWithSerialAndName(serial, deviceName: deviceName, + // activate failure + onFailure: { reason in + dispatch_async(dispatch_get_main_queue()) { + progressHUD.dismiss() + var messageText = "" + switch reason { + case .NoSuccessStatusCode(let statusCode): + if statusCode == CONFLICT { + messageText = "Device name is already used! Try with different name or tap on 'Continue existing device'" + } + else { + messageText = "Error activating device!" + } + default: + messageText = "Error activating device!" + } + self.errorLabel.text = messageText + self.errorLabel.hidden = false + self.view.setNeedsDisplay() + } + }, + + // activate success + onSuccess: { pointId, password in + dispatch_async(dispatch_get_main_queue()) { + progressHUD.dismiss() + } + print("ACTI -- Activated \(serial) with pointId: \(pointId)") + let serialMapping = SerialMapping(hexiSerial: self.selectedPeripheralSerial, wolkSerial: serial, wolkPassword: password) + self.dataStore.fetchAll( + { _ in + self.deviceActivationDelegate?.didActivateDevice(pointId, serials: serialMapping) + }, + onSuccess: { + self.deviceActivationDelegate?.didActivateDevice(pointId, serials: serialMapping) + } + ) + } + ) + } + + override func skipButtonAction() { + deviceActivationDelegate?.didSkipActivation() + } + + @IBAction func deviceNameChanged(sender: UITextField) { + toggleActivateButtonEnabled() + selectedDeviceSerial = "" + errorLabel.hidden = true + self.view.setNeedsDisplay() + } +} + +extension ActivateDeviceViewController: UITextFieldDelegate { + func textFieldShouldReturn(textField: UITextField) -> Bool { + textField.resignFirstResponder() + return true + } +} + +extension ActivateDeviceViewController: ActivatedDeviceDelegate { + func didSelectAlreadyActivatedName(name: String, serial: String) { + navigationController?.popViewControllerAnimated(true) + deviceName.text = name + selectedDeviceSerial = serial + toggleActivateButtonEnabled() + } +} + diff --git a/iOS/Hexiwear/AppDelegate.swift b/iOS/Hexiwear/AppDelegate.swift new file mode 100644 index 0000000..9a69a65 --- /dev/null +++ b/iOS/Hexiwear/AppDelegate.swift @@ -0,0 +1,95 @@ +// +// Hexiwear application is used to pair with Hexiwear BLE devices +// and send sensor readings to WolkSense sensor data cloud +// +// Copyright (C) 2016 WolkAbout Technology s.r.o. +// +// Hexiwear is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Hexiwear is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . +// +// +// AppDelegate.swift +// + +import UIKit + +@UIApplicationMain +class AppDelegate: UIResponder, UIApplicationDelegate { + + var window: UIWindow? + var userCredentials: UserCredentials! + var webApi: WebAPI! + var dataStore: DataStore! + var device: TrackingDevice! + var mqttAPI: MQTTAPI! + + func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool { + + userCredentials = UserCredentials() + let userAccount = userCredentials.email ?? "" + device = TrackingDevice() + device.userAccount = userAccount + webApi = WebAPI.sharedWebAPI + dataStore = DataStore(webApi: webApi, userCredentials: userCredentials, trackingDevice: device) + mqttAPI = MQTTAPI() + + return true + } + + func application(application: UIApplication, handleOpenURL url: NSURL) -> Bool { + + if url.fileURL { + guard let privateDocsDir = getPrivateDocumentsDirectory() else { return false } + guard let _ = try? NSFileManager.defaultManager().contentsOfDirectoryAtPath(privateDocsDir) else { return false } + let fullFileName = (privateDocsDir as NSString).stringByAppendingPathComponent(url.lastPathComponent!) + + // write + guard let urlData = NSData(contentsOfURL: url) else { print("NOT valid URL data"); return false} + let filePath = fullFileName + + if !NSFileManager.defaultManager().createFileAtPath(filePath, contents: nil, attributes: nil) { print("Failure creating file"); return false } + + let fileWrapper = NSFileWrapper(regularFileWithContents: urlData) + + let fileURL = NSURL(fileURLWithPath: filePath) + + guard let _ = try? fileWrapper.writeToURL(fileURL, options: .Atomic, originalContentsURL: nil) else { print("Error writting to file"); return false } + } + return true + } + + func applicationWillResignActive(application: UIApplication) { + // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state. + // Use this method to pause ongoing tasks, disable timers, and throttle down OpenGL ES frame rates. Games should use this method to pause the game. + } + + func applicationDidEnterBackground(application: UIApplication) { + // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later. + // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits. + } + + func applicationWillEnterForeground(application: UIApplication) { + // Called as part of the transition from the background to the inactive state; here you can undo many of the changes made on entering the background. + } + + func applicationDidBecomeActive(application: UIApplication) { + // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface. + } + + func applicationWillTerminate(application: UIApplication) { + // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:. + } + + +} + diff --git a/iOS/Hexiwear/Assets.xcassets/AppIcon.appiconset/Contents.json b/iOS/Hexiwear/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..73b81f7 --- /dev/null +++ b/iOS/Hexiwear/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,86 @@ +{ + "images" : [ + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "apple-icon-58x58.png", + "scale" : "2x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "apple-icon-87x87.png", + "scale" : "3x" + }, + { + "size" : "40x40", + "idiom" : "iphone", + "filename" : "apple-icon-80x80.png", + "scale" : "2x" + }, + { + "size" : "40x40", + "idiom" : "iphone", + "filename" : "apple-icon-120x120.png", + "scale" : "3x" + }, + { + "size" : "60x60", + "idiom" : "iphone", + "filename" : "apple-icon-120x120-1.png", + "scale" : "2x" + }, + { + "size" : "60x60", + "idiom" : "iphone", + "filename" : "apple-icon-180x180.png", + "scale" : "3x" + }, + { + "size" : "29x29", + "idiom" : "ipad", + "filename" : "apple-icon-29x29.png", + "scale" : "1x" + }, + { + "size" : "29x29", + "idiom" : "ipad", + "filename" : "apple-icon-58x58-1.png", + "scale" : "2x" + }, + { + "size" : "40x40", + "idiom" : "ipad", + "filename" : "apple-icon-40x40.png", + "scale" : "1x" + }, + { + "size" : "40x40", + "idiom" : "ipad", + "filename" : "apple-icon-80x80-1.png", + "scale" : "2x" + }, + { + "size" : "76x76", + "idiom" : "ipad", + "filename" : "apple-icon-76x76.png", + "scale" : "1x" + }, + { + "size" : "76x76", + "idiom" : "ipad", + "filename" : "apple-icon-152x152.png", + "scale" : "2x" + }, + { + "size" : "83.5x83.5", + "idiom" : "ipad", + "filename" : "apple-icon-167x167.png", + "scale" : "2x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/iOS/Hexiwear/Assets.xcassets/AppIcon.appiconset/apple-icon-120x120-1.png b/iOS/Hexiwear/Assets.xcassets/AppIcon.appiconset/apple-icon-120x120-1.png new file mode 100644 index 0000000..3c215fb Binary files /dev/null and b/iOS/Hexiwear/Assets.xcassets/AppIcon.appiconset/apple-icon-120x120-1.png differ diff --git a/iOS/Hexiwear/Assets.xcassets/AppIcon.appiconset/apple-icon-120x120.png b/iOS/Hexiwear/Assets.xcassets/AppIcon.appiconset/apple-icon-120x120.png new file mode 100644 index 0000000..3c215fb Binary files /dev/null and b/iOS/Hexiwear/Assets.xcassets/AppIcon.appiconset/apple-icon-120x120.png differ diff --git a/iOS/Hexiwear/Assets.xcassets/AppIcon.appiconset/apple-icon-152x152.png b/iOS/Hexiwear/Assets.xcassets/AppIcon.appiconset/apple-icon-152x152.png new file mode 100644 index 0000000..9e2533a Binary files /dev/null and b/iOS/Hexiwear/Assets.xcassets/AppIcon.appiconset/apple-icon-152x152.png differ diff --git a/iOS/Hexiwear/Assets.xcassets/AppIcon.appiconset/apple-icon-167x167.png b/iOS/Hexiwear/Assets.xcassets/AppIcon.appiconset/apple-icon-167x167.png new file mode 100644 index 0000000..bfac48a Binary files /dev/null and b/iOS/Hexiwear/Assets.xcassets/AppIcon.appiconset/apple-icon-167x167.png differ diff --git a/iOS/Hexiwear/Assets.xcassets/AppIcon.appiconset/apple-icon-180x180.png b/iOS/Hexiwear/Assets.xcassets/AppIcon.appiconset/apple-icon-180x180.png new file mode 100644 index 0000000..5b8eac3 Binary files /dev/null and b/iOS/Hexiwear/Assets.xcassets/AppIcon.appiconset/apple-icon-180x180.png differ diff --git a/iOS/Hexiwear/Assets.xcassets/AppIcon.appiconset/apple-icon-29x29.png b/iOS/Hexiwear/Assets.xcassets/AppIcon.appiconset/apple-icon-29x29.png new file mode 100644 index 0000000..f9c0687 Binary files /dev/null and b/iOS/Hexiwear/Assets.xcassets/AppIcon.appiconset/apple-icon-29x29.png differ diff --git a/iOS/Hexiwear/Assets.xcassets/AppIcon.appiconset/apple-icon-40x40.png b/iOS/Hexiwear/Assets.xcassets/AppIcon.appiconset/apple-icon-40x40.png new file mode 100644 index 0000000..af71a20 Binary files /dev/null and b/iOS/Hexiwear/Assets.xcassets/AppIcon.appiconset/apple-icon-40x40.png differ diff --git a/iOS/Hexiwear/Assets.xcassets/AppIcon.appiconset/apple-icon-58x58-1.png b/iOS/Hexiwear/Assets.xcassets/AppIcon.appiconset/apple-icon-58x58-1.png new file mode 100644 index 0000000..9aa53db Binary files /dev/null and b/iOS/Hexiwear/Assets.xcassets/AppIcon.appiconset/apple-icon-58x58-1.png differ diff --git a/iOS/Hexiwear/Assets.xcassets/AppIcon.appiconset/apple-icon-58x58.png b/iOS/Hexiwear/Assets.xcassets/AppIcon.appiconset/apple-icon-58x58.png new file mode 100644 index 0000000..9aa53db Binary files /dev/null and b/iOS/Hexiwear/Assets.xcassets/AppIcon.appiconset/apple-icon-58x58.png differ diff --git a/iOS/Hexiwear/Assets.xcassets/AppIcon.appiconset/apple-icon-76x76.png b/iOS/Hexiwear/Assets.xcassets/AppIcon.appiconset/apple-icon-76x76.png new file mode 100644 index 0000000..f2f0857 Binary files /dev/null and b/iOS/Hexiwear/Assets.xcassets/AppIcon.appiconset/apple-icon-76x76.png differ diff --git a/iOS/Hexiwear/Assets.xcassets/AppIcon.appiconset/apple-icon-80x80-1.png b/iOS/Hexiwear/Assets.xcassets/AppIcon.appiconset/apple-icon-80x80-1.png new file mode 100644 index 0000000..22e02e9 Binary files /dev/null and b/iOS/Hexiwear/Assets.xcassets/AppIcon.appiconset/apple-icon-80x80-1.png differ diff --git a/iOS/Hexiwear/Assets.xcassets/AppIcon.appiconset/apple-icon-80x80.png b/iOS/Hexiwear/Assets.xcassets/AppIcon.appiconset/apple-icon-80x80.png new file mode 100644 index 0000000..22e02e9 Binary files /dev/null and b/iOS/Hexiwear/Assets.xcassets/AppIcon.appiconset/apple-icon-80x80.png differ diff --git a/iOS/Hexiwear/Assets.xcassets/AppIcon.appiconset/apple-icon-87x87.png b/iOS/Hexiwear/Assets.xcassets/AppIcon.appiconset/apple-icon-87x87.png new file mode 100644 index 0000000..b2d9c65 Binary files /dev/null and b/iOS/Hexiwear/Assets.xcassets/AppIcon.appiconset/apple-icon-87x87.png differ diff --git a/iOS/Hexiwear/Assets.xcassets/Brand Assets.launchimage/Contents.json b/iOS/Hexiwear/Assets.xcassets/Brand Assets.launchimage/Contents.json new file mode 100644 index 0000000..8a4947a --- /dev/null +++ b/iOS/Hexiwear/Assets.xcassets/Brand Assets.launchimage/Contents.json @@ -0,0 +1,35 @@ +{ + "images" : [ + { + "extent" : "full-screen", + "idiom" : "iphone", + "subtype" : "736h", + "filename" : "WS_Vertical_iPhone6S.png", + "minimum-system-version" : "8.0", + "orientation" : "portrait", + "scale" : "3x" + }, + { + "extent" : "full-screen", + "idiom" : "iphone", + "subtype" : "736h", + "filename" : "WS_Horizontal_iPhone6S.png", + "minimum-system-version" : "8.0", + "orientation" : "landscape", + "scale" : "3x" + }, + { + "extent" : "full-screen", + "idiom" : "iphone", + "subtype" : "667h", + "filename" : "WS_Vertical_iPhone6.png", + "minimum-system-version" : "8.0", + "orientation" : "portrait", + "scale" : "2x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/iOS/Hexiwear/Assets.xcassets/Brand Assets.launchimage/WS_Horizontal_iPhone6S.png b/iOS/Hexiwear/Assets.xcassets/Brand Assets.launchimage/WS_Horizontal_iPhone6S.png new file mode 100644 index 0000000..7c766d0 Binary files /dev/null and b/iOS/Hexiwear/Assets.xcassets/Brand Assets.launchimage/WS_Horizontal_iPhone6S.png differ diff --git a/iOS/Hexiwear/Assets.xcassets/Brand Assets.launchimage/WS_Vertical_iPhone6.png b/iOS/Hexiwear/Assets.xcassets/Brand Assets.launchimage/WS_Vertical_iPhone6.png new file mode 100644 index 0000000..b312fdd Binary files /dev/null and b/iOS/Hexiwear/Assets.xcassets/Brand Assets.launchimage/WS_Vertical_iPhone6.png differ diff --git a/iOS/Hexiwear/Assets.xcassets/Brand Assets.launchimage/WS_Vertical_iPhone6S.png b/iOS/Hexiwear/Assets.xcassets/Brand Assets.launchimage/WS_Vertical_iPhone6S.png new file mode 100644 index 0000000..146ad92 Binary files /dev/null and b/iOS/Hexiwear/Assets.xcassets/Brand Assets.launchimage/WS_Vertical_iPhone6S.png differ diff --git a/iOS/Hexiwear/Assets.xcassets/Contents.json b/iOS/Hexiwear/Assets.xcassets/Contents.json new file mode 100644 index 0000000..da4a164 --- /dev/null +++ b/iOS/Hexiwear/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/iOS/Hexiwear/Assets.xcassets/LaunchImage.launchimage/Contents.json b/iOS/Hexiwear/Assets.xcassets/LaunchImage.launchimage/Contents.json new file mode 100644 index 0000000..8a4947a --- /dev/null +++ b/iOS/Hexiwear/Assets.xcassets/LaunchImage.launchimage/Contents.json @@ -0,0 +1,35 @@ +{ + "images" : [ + { + "extent" : "full-screen", + "idiom" : "iphone", + "subtype" : "736h", + "filename" : "WS_Vertical_iPhone6S.png", + "minimum-system-version" : "8.0", + "orientation" : "portrait", + "scale" : "3x" + }, + { + "extent" : "full-screen", + "idiom" : "iphone", + "subtype" : "736h", + "filename" : "WS_Horizontal_iPhone6S.png", + "minimum-system-version" : "8.0", + "orientation" : "landscape", + "scale" : "3x" + }, + { + "extent" : "full-screen", + "idiom" : "iphone", + "subtype" : "667h", + "filename" : "WS_Vertical_iPhone6.png", + "minimum-system-version" : "8.0", + "orientation" : "portrait", + "scale" : "2x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/iOS/Hexiwear/Assets.xcassets/LaunchImage.launchimage/WS_Horizontal_iPhone6S.png b/iOS/Hexiwear/Assets.xcassets/LaunchImage.launchimage/WS_Horizontal_iPhone6S.png new file mode 100644 index 0000000..7c766d0 Binary files /dev/null and b/iOS/Hexiwear/Assets.xcassets/LaunchImage.launchimage/WS_Horizontal_iPhone6S.png differ diff --git a/iOS/Hexiwear/Assets.xcassets/LaunchImage.launchimage/WS_Vertical_iPhone6.png b/iOS/Hexiwear/Assets.xcassets/LaunchImage.launchimage/WS_Vertical_iPhone6.png new file mode 100644 index 0000000..b312fdd Binary files /dev/null and b/iOS/Hexiwear/Assets.xcassets/LaunchImage.launchimage/WS_Vertical_iPhone6.png differ diff --git a/iOS/Hexiwear/Assets.xcassets/LaunchImage.launchimage/WS_Vertical_iPhone6S.png b/iOS/Hexiwear/Assets.xcassets/LaunchImage.launchimage/WS_Vertical_iPhone6S.png new file mode 100644 index 0000000..146ad92 Binary files /dev/null and b/iOS/Hexiwear/Assets.xcassets/LaunchImage.launchimage/WS_Vertical_iPhone6S.png differ diff --git a/iOS/Hexiwear/Assets.xcassets/Splash.imageset/Contents.json b/iOS/Hexiwear/Assets.xcassets/Splash.imageset/Contents.json new file mode 100644 index 0000000..285c71e --- /dev/null +++ b/iOS/Hexiwear/Assets.xcassets/Splash.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "WS_Vertical_iPhone6S-2.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "WS_Vertical_iPhone6S-1.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "WS_Vertical_iPhone6S.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/iOS/Hexiwear/Assets.xcassets/Splash.imageset/WS_Vertical_iPhone6S-1.png b/iOS/Hexiwear/Assets.xcassets/Splash.imageset/WS_Vertical_iPhone6S-1.png new file mode 100644 index 0000000..146ad92 Binary files /dev/null and b/iOS/Hexiwear/Assets.xcassets/Splash.imageset/WS_Vertical_iPhone6S-1.png differ diff --git a/iOS/Hexiwear/Assets.xcassets/Splash.imageset/WS_Vertical_iPhone6S-2.png b/iOS/Hexiwear/Assets.xcassets/Splash.imageset/WS_Vertical_iPhone6S-2.png new file mode 100644 index 0000000..146ad92 Binary files /dev/null and b/iOS/Hexiwear/Assets.xcassets/Splash.imageset/WS_Vertical_iPhone6S-2.png differ diff --git a/iOS/Hexiwear/Assets.xcassets/Splash.imageset/WS_Vertical_iPhone6S.png b/iOS/Hexiwear/Assets.xcassets/Splash.imageset/WS_Vertical_iPhone6S.png new file mode 100644 index 0000000..146ad92 Binary files /dev/null and b/iOS/Hexiwear/Assets.xcassets/Splash.imageset/WS_Vertical_iPhone6S.png differ diff --git a/iOS/Hexiwear/Assets.xcassets/accelerometer.imageset/Contents.json b/iOS/Hexiwear/Assets.xcassets/accelerometer.imageset/Contents.json new file mode 100644 index 0000000..d021dc9 --- /dev/null +++ b/iOS/Hexiwear/Assets.xcassets/accelerometer.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "accelerometer.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "accelerometer-1.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "accelerometer-2.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/iOS/Hexiwear/Assets.xcassets/accelerometer.imageset/accelerometer-1.png b/iOS/Hexiwear/Assets.xcassets/accelerometer.imageset/accelerometer-1.png new file mode 100644 index 0000000..420a738 Binary files /dev/null and b/iOS/Hexiwear/Assets.xcassets/accelerometer.imageset/accelerometer-1.png differ diff --git a/iOS/Hexiwear/Assets.xcassets/accelerometer.imageset/accelerometer-2.png b/iOS/Hexiwear/Assets.xcassets/accelerometer.imageset/accelerometer-2.png new file mode 100644 index 0000000..420a738 Binary files /dev/null and b/iOS/Hexiwear/Assets.xcassets/accelerometer.imageset/accelerometer-2.png differ diff --git a/iOS/Hexiwear/Assets.xcassets/accelerometer.imageset/accelerometer.png b/iOS/Hexiwear/Assets.xcassets/accelerometer.imageset/accelerometer.png new file mode 100644 index 0000000..420a738 Binary files /dev/null and b/iOS/Hexiwear/Assets.xcassets/accelerometer.imageset/accelerometer.png differ diff --git a/iOS/Hexiwear/Assets.xcassets/battery.imageset/Contents.json b/iOS/Hexiwear/Assets.xcassets/battery.imageset/Contents.json new file mode 100644 index 0000000..039fe0c --- /dev/null +++ b/iOS/Hexiwear/Assets.xcassets/battery.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "battery.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "battery-1.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "battery-2.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/iOS/Hexiwear/Assets.xcassets/battery.imageset/battery-1.png b/iOS/Hexiwear/Assets.xcassets/battery.imageset/battery-1.png new file mode 100644 index 0000000..84b75ea Binary files /dev/null and b/iOS/Hexiwear/Assets.xcassets/battery.imageset/battery-1.png differ diff --git a/iOS/Hexiwear/Assets.xcassets/battery.imageset/battery-2.png b/iOS/Hexiwear/Assets.xcassets/battery.imageset/battery-2.png new file mode 100644 index 0000000..84b75ea Binary files /dev/null and b/iOS/Hexiwear/Assets.xcassets/battery.imageset/battery-2.png differ diff --git a/iOS/Hexiwear/Assets.xcassets/battery.imageset/battery.png b/iOS/Hexiwear/Assets.xcassets/battery.imageset/battery.png new file mode 100644 index 0000000..84b75ea Binary files /dev/null and b/iOS/Hexiwear/Assets.xcassets/battery.imageset/battery.png differ diff --git a/iOS/Hexiwear/Assets.xcassets/calories.imageset/Contents.json b/iOS/Hexiwear/Assets.xcassets/calories.imageset/Contents.json new file mode 100644 index 0000000..9991bbb --- /dev/null +++ b/iOS/Hexiwear/Assets.xcassets/calories.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "running.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "running2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "running3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/iOS/Hexiwear/Assets.xcassets/calories.imageset/running.png b/iOS/Hexiwear/Assets.xcassets/calories.imageset/running.png new file mode 100644 index 0000000..0d5c554 Binary files /dev/null and b/iOS/Hexiwear/Assets.xcassets/calories.imageset/running.png differ diff --git a/iOS/Hexiwear/Assets.xcassets/calories.imageset/running2x.png b/iOS/Hexiwear/Assets.xcassets/calories.imageset/running2x.png new file mode 100644 index 0000000..ea81430 Binary files /dev/null and b/iOS/Hexiwear/Assets.xcassets/calories.imageset/running2x.png differ diff --git a/iOS/Hexiwear/Assets.xcassets/calories.imageset/running3x.png b/iOS/Hexiwear/Assets.xcassets/calories.imageset/running3x.png new file mode 100644 index 0000000..c3c5a7a Binary files /dev/null and b/iOS/Hexiwear/Assets.xcassets/calories.imageset/running3x.png differ diff --git a/iOS/Hexiwear/Assets.xcassets/gyroscope.imageset/Contents.json b/iOS/Hexiwear/Assets.xcassets/gyroscope.imageset/Contents.json new file mode 100644 index 0000000..673151a --- /dev/null +++ b/iOS/Hexiwear/Assets.xcassets/gyroscope.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "gyroscope.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "gyroscope-1.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "gyroscope-2.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/iOS/Hexiwear/Assets.xcassets/gyroscope.imageset/gyroscope-1.png b/iOS/Hexiwear/Assets.xcassets/gyroscope.imageset/gyroscope-1.png new file mode 100644 index 0000000..fe5fdfa Binary files /dev/null and b/iOS/Hexiwear/Assets.xcassets/gyroscope.imageset/gyroscope-1.png differ diff --git a/iOS/Hexiwear/Assets.xcassets/gyroscope.imageset/gyroscope-2.png b/iOS/Hexiwear/Assets.xcassets/gyroscope.imageset/gyroscope-2.png new file mode 100644 index 0000000..fe5fdfa Binary files /dev/null and b/iOS/Hexiwear/Assets.xcassets/gyroscope.imageset/gyroscope-2.png differ diff --git a/iOS/Hexiwear/Assets.xcassets/gyroscope.imageset/gyroscope.png b/iOS/Hexiwear/Assets.xcassets/gyroscope.imageset/gyroscope.png new file mode 100644 index 0000000..fe5fdfa Binary files /dev/null and b/iOS/Hexiwear/Assets.xcassets/gyroscope.imageset/gyroscope.png differ diff --git a/iOS/Hexiwear/Assets.xcassets/heartbeat_icon.imageset/Contents.json b/iOS/Hexiwear/Assets.xcassets/heartbeat_icon.imageset/Contents.json new file mode 100644 index 0000000..4fe5fa9 --- /dev/null +++ b/iOS/Hexiwear/Assets.xcassets/heartbeat_icon.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "heartbeat_icon.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "heartbeat_icon-1.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "heartbeat_icon-2.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/iOS/Hexiwear/Assets.xcassets/heartbeat_icon.imageset/heartbeat_icon-1.png b/iOS/Hexiwear/Assets.xcassets/heartbeat_icon.imageset/heartbeat_icon-1.png new file mode 100644 index 0000000..b2e80de Binary files /dev/null and b/iOS/Hexiwear/Assets.xcassets/heartbeat_icon.imageset/heartbeat_icon-1.png differ diff --git a/iOS/Hexiwear/Assets.xcassets/heartbeat_icon.imageset/heartbeat_icon-2.png b/iOS/Hexiwear/Assets.xcassets/heartbeat_icon.imageset/heartbeat_icon-2.png new file mode 100644 index 0000000..b2e80de Binary files /dev/null and b/iOS/Hexiwear/Assets.xcassets/heartbeat_icon.imageset/heartbeat_icon-2.png differ diff --git a/iOS/Hexiwear/Assets.xcassets/heartbeat_icon.imageset/heartbeat_icon.png b/iOS/Hexiwear/Assets.xcassets/heartbeat_icon.imageset/heartbeat_icon.png new file mode 100644 index 0000000..b2e80de Binary files /dev/null and b/iOS/Hexiwear/Assets.xcassets/heartbeat_icon.imageset/heartbeat_icon.png differ diff --git a/iOS/Hexiwear/Assets.xcassets/hexi-icon.imageset/Contents.json b/iOS/Hexiwear/Assets.xcassets/hexi-icon.imageset/Contents.json new file mode 100644 index 0000000..fc421d5 --- /dev/null +++ b/iOS/Hexiwear/Assets.xcassets/hexi-icon.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "hexi-icon-big-24.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "hexi-icon-big-48.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "hexi-icon-big-72.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/iOS/Hexiwear/Assets.xcassets/hexi-icon.imageset/hexi-icon-big-24.png b/iOS/Hexiwear/Assets.xcassets/hexi-icon.imageset/hexi-icon-big-24.png new file mode 100644 index 0000000..c623014 Binary files /dev/null and b/iOS/Hexiwear/Assets.xcassets/hexi-icon.imageset/hexi-icon-big-24.png differ diff --git a/iOS/Hexiwear/Assets.xcassets/hexi-icon.imageset/hexi-icon-big-48.png b/iOS/Hexiwear/Assets.xcassets/hexi-icon.imageset/hexi-icon-big-48.png new file mode 100644 index 0000000..c334753 Binary files /dev/null and b/iOS/Hexiwear/Assets.xcassets/hexi-icon.imageset/hexi-icon-big-48.png differ diff --git a/iOS/Hexiwear/Assets.xcassets/hexi-icon.imageset/hexi-icon-big-72.png b/iOS/Hexiwear/Assets.xcassets/hexi-icon.imageset/hexi-icon-big-72.png new file mode 100644 index 0000000..55e2be9 Binary files /dev/null and b/iOS/Hexiwear/Assets.xcassets/hexi-icon.imageset/hexi-icon-big-72.png differ diff --git a/iOS/Hexiwear/Assets.xcassets/humidity.imageset/Contents.json b/iOS/Hexiwear/Assets.xcassets/humidity.imageset/Contents.json new file mode 100644 index 0000000..698c9ed --- /dev/null +++ b/iOS/Hexiwear/Assets.xcassets/humidity.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "humidity_icon-2.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "humidity_icon-1.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "humidity_icon.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/iOS/Hexiwear/Assets.xcassets/humidity.imageset/humidity_icon-1.png b/iOS/Hexiwear/Assets.xcassets/humidity.imageset/humidity_icon-1.png new file mode 100644 index 0000000..3f05529 Binary files /dev/null and b/iOS/Hexiwear/Assets.xcassets/humidity.imageset/humidity_icon-1.png differ diff --git a/iOS/Hexiwear/Assets.xcassets/humidity.imageset/humidity_icon-2.png b/iOS/Hexiwear/Assets.xcassets/humidity.imageset/humidity_icon-2.png new file mode 100644 index 0000000..3f05529 Binary files /dev/null and b/iOS/Hexiwear/Assets.xcassets/humidity.imageset/humidity_icon-2.png differ diff --git a/iOS/Hexiwear/Assets.xcassets/humidity.imageset/humidity_icon.png b/iOS/Hexiwear/Assets.xcassets/humidity.imageset/humidity_icon.png new file mode 100644 index 0000000..3f05529 Binary files /dev/null and b/iOS/Hexiwear/Assets.xcassets/humidity.imageset/humidity_icon.png differ diff --git a/iOS/Hexiwear/Assets.xcassets/icon_accel_black.imageset/Contents.json b/iOS/Hexiwear/Assets.xcassets/icon_accel_black.imageset/Contents.json new file mode 100644 index 0000000..ef84606 --- /dev/null +++ b/iOS/Hexiwear/Assets.xcassets/icon_accel_black.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "icon_accel_black2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "icon_accel_black-1.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/iOS/Hexiwear/Assets.xcassets/icon_accel_black.imageset/icon_accel_black-1.png b/iOS/Hexiwear/Assets.xcassets/icon_accel_black.imageset/icon_accel_black-1.png new file mode 100644 index 0000000..ad7c7ed Binary files /dev/null and b/iOS/Hexiwear/Assets.xcassets/icon_accel_black.imageset/icon_accel_black-1.png differ diff --git a/iOS/Hexiwear/Assets.xcassets/icon_accel_black.imageset/icon_accel_black2x.png b/iOS/Hexiwear/Assets.xcassets/icon_accel_black.imageset/icon_accel_black2x.png new file mode 100644 index 0000000..4411b72 Binary files /dev/null and b/iOS/Hexiwear/Assets.xcassets/icon_accel_black.imageset/icon_accel_black2x.png differ diff --git a/iOS/Hexiwear/Assets.xcassets/icon_gyro_black.imageset/Contents.json b/iOS/Hexiwear/Assets.xcassets/icon_gyro_black.imageset/Contents.json new file mode 100644 index 0000000..fa1aa44 --- /dev/null +++ b/iOS/Hexiwear/Assets.xcassets/icon_gyro_black.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "icon_gyro_black2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "icon_gyro_black-1.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/iOS/Hexiwear/Assets.xcassets/icon_gyro_black.imageset/icon_gyro_black-1.png b/iOS/Hexiwear/Assets.xcassets/icon_gyro_black.imageset/icon_gyro_black-1.png new file mode 100644 index 0000000..cb10cbc Binary files /dev/null and b/iOS/Hexiwear/Assets.xcassets/icon_gyro_black.imageset/icon_gyro_black-1.png differ diff --git a/iOS/Hexiwear/Assets.xcassets/icon_gyro_black.imageset/icon_gyro_black2x.png b/iOS/Hexiwear/Assets.xcassets/icon_gyro_black.imageset/icon_gyro_black2x.png new file mode 100644 index 0000000..4f3a084 Binary files /dev/null and b/iOS/Hexiwear/Assets.xcassets/icon_gyro_black.imageset/icon_gyro_black2x.png differ diff --git a/iOS/Hexiwear/Assets.xcassets/icon_temp_black.imageset/Contents.json b/iOS/Hexiwear/Assets.xcassets/icon_temp_black.imageset/Contents.json new file mode 100644 index 0000000..f45b9c8 --- /dev/null +++ b/iOS/Hexiwear/Assets.xcassets/icon_temp_black.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "icon_temp_black2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "icon_temp_black-1.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/iOS/Hexiwear/Assets.xcassets/icon_temp_black.imageset/icon_temp_black-1.png b/iOS/Hexiwear/Assets.xcassets/icon_temp_black.imageset/icon_temp_black-1.png new file mode 100644 index 0000000..6417fbf Binary files /dev/null and b/iOS/Hexiwear/Assets.xcassets/icon_temp_black.imageset/icon_temp_black-1.png differ diff --git a/iOS/Hexiwear/Assets.xcassets/icon_temp_black.imageset/icon_temp_black2x.png b/iOS/Hexiwear/Assets.xcassets/icon_temp_black.imageset/icon_temp_black2x.png new file mode 100644 index 0000000..1652448 Binary files /dev/null and b/iOS/Hexiwear/Assets.xcassets/icon_temp_black.imageset/icon_temp_black2x.png differ diff --git a/iOS/Hexiwear/Assets.xcassets/light_icon.imageset/Contents.json b/iOS/Hexiwear/Assets.xcassets/light_icon.imageset/Contents.json new file mode 100644 index 0000000..649d800 --- /dev/null +++ b/iOS/Hexiwear/Assets.xcassets/light_icon.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "light_icon.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "light_icon-1.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "light_icon-2.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/iOS/Hexiwear/Assets.xcassets/light_icon.imageset/light_icon-1.png b/iOS/Hexiwear/Assets.xcassets/light_icon.imageset/light_icon-1.png new file mode 100644 index 0000000..58f4fb5 Binary files /dev/null and b/iOS/Hexiwear/Assets.xcassets/light_icon.imageset/light_icon-1.png differ diff --git a/iOS/Hexiwear/Assets.xcassets/light_icon.imageset/light_icon-2.png b/iOS/Hexiwear/Assets.xcassets/light_icon.imageset/light_icon-2.png new file mode 100644 index 0000000..58f4fb5 Binary files /dev/null and b/iOS/Hexiwear/Assets.xcassets/light_icon.imageset/light_icon-2.png differ diff --git a/iOS/Hexiwear/Assets.xcassets/light_icon.imageset/light_icon.png b/iOS/Hexiwear/Assets.xcassets/light_icon.imageset/light_icon.png new file mode 100644 index 0000000..58f4fb5 Binary files /dev/null and b/iOS/Hexiwear/Assets.xcassets/light_icon.imageset/light_icon.png differ diff --git a/iOS/Hexiwear/Assets.xcassets/logo.imageset/Contents.json b/iOS/Hexiwear/Assets.xcassets/logo.imageset/Contents.json new file mode 100644 index 0000000..64d379c --- /dev/null +++ b/iOS/Hexiwear/Assets.xcassets/logo.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "logo@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/iOS/Hexiwear/Assets.xcassets/logo.imageset/logo@2x.png b/iOS/Hexiwear/Assets.xcassets/logo.imageset/logo@2x.png new file mode 100644 index 0000000..c6014bc Binary files /dev/null and b/iOS/Hexiwear/Assets.xcassets/logo.imageset/logo@2x.png differ diff --git a/iOS/Hexiwear/Assets.xcassets/magnet_icon.imageset/Contents.json b/iOS/Hexiwear/Assets.xcassets/magnet_icon.imageset/Contents.json new file mode 100644 index 0000000..35522dd --- /dev/null +++ b/iOS/Hexiwear/Assets.xcassets/magnet_icon.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "magnet_icon.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "magnet_icon-1.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "magnet_icon-2.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/iOS/Hexiwear/Assets.xcassets/magnet_icon.imageset/magnet_icon-1.png b/iOS/Hexiwear/Assets.xcassets/magnet_icon.imageset/magnet_icon-1.png new file mode 100644 index 0000000..44e11d1 Binary files /dev/null and b/iOS/Hexiwear/Assets.xcassets/magnet_icon.imageset/magnet_icon-1.png differ diff --git a/iOS/Hexiwear/Assets.xcassets/magnet_icon.imageset/magnet_icon-2.png b/iOS/Hexiwear/Assets.xcassets/magnet_icon.imageset/magnet_icon-2.png new file mode 100644 index 0000000..44e11d1 Binary files /dev/null and b/iOS/Hexiwear/Assets.xcassets/magnet_icon.imageset/magnet_icon-2.png differ diff --git a/iOS/Hexiwear/Assets.xcassets/magnet_icon.imageset/magnet_icon.png b/iOS/Hexiwear/Assets.xcassets/magnet_icon.imageset/magnet_icon.png new file mode 100644 index 0000000..44e11d1 Binary files /dev/null and b/iOS/Hexiwear/Assets.xcassets/magnet_icon.imageset/magnet_icon.png differ diff --git a/iOS/Hexiwear/Assets.xcassets/pressure.imageset/Contents.json b/iOS/Hexiwear/Assets.xcassets/pressure.imageset/Contents.json new file mode 100644 index 0000000..77eeac0 --- /dev/null +++ b/iOS/Hexiwear/Assets.xcassets/pressure.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "pressure_icon-2.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "pressure_icon-1.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "pressure_icon.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/iOS/Hexiwear/Assets.xcassets/pressure.imageset/pressure_icon-1.png b/iOS/Hexiwear/Assets.xcassets/pressure.imageset/pressure_icon-1.png new file mode 100644 index 0000000..03b9657 Binary files /dev/null and b/iOS/Hexiwear/Assets.xcassets/pressure.imageset/pressure_icon-1.png differ diff --git a/iOS/Hexiwear/Assets.xcassets/pressure.imageset/pressure_icon-2.png b/iOS/Hexiwear/Assets.xcassets/pressure.imageset/pressure_icon-2.png new file mode 100644 index 0000000..03b9657 Binary files /dev/null and b/iOS/Hexiwear/Assets.xcassets/pressure.imageset/pressure_icon-2.png differ diff --git a/iOS/Hexiwear/Assets.xcassets/pressure.imageset/pressure_icon.png b/iOS/Hexiwear/Assets.xcassets/pressure.imageset/pressure_icon.png new file mode 100644 index 0000000..03b9657 Binary files /dev/null and b/iOS/Hexiwear/Assets.xcassets/pressure.imageset/pressure_icon.png differ diff --git a/iOS/Hexiwear/Assets.xcassets/settings.imageset/Contents.json b/iOS/Hexiwear/Assets.xcassets/settings.imageset/Contents.json new file mode 100644 index 0000000..248ffe8 --- /dev/null +++ b/iOS/Hexiwear/Assets.xcassets/settings.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "settings.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "settings-1.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "settings-2.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/iOS/Hexiwear/Assets.xcassets/settings.imageset/settings-1.png b/iOS/Hexiwear/Assets.xcassets/settings.imageset/settings-1.png new file mode 100644 index 0000000..bd86f16 Binary files /dev/null and b/iOS/Hexiwear/Assets.xcassets/settings.imageset/settings-1.png differ diff --git a/iOS/Hexiwear/Assets.xcassets/settings.imageset/settings-2.png b/iOS/Hexiwear/Assets.xcassets/settings.imageset/settings-2.png new file mode 100644 index 0000000..bd86f16 Binary files /dev/null and b/iOS/Hexiwear/Assets.xcassets/settings.imageset/settings-2.png differ diff --git a/iOS/Hexiwear/Assets.xcassets/settings.imageset/settings.png b/iOS/Hexiwear/Assets.xcassets/settings.imageset/settings.png new file mode 100644 index 0000000..bd86f16 Binary files /dev/null and b/iOS/Hexiwear/Assets.xcassets/settings.imageset/settings.png differ diff --git a/iOS/Hexiwear/Assets.xcassets/steps_icon.imageset/Contents.json b/iOS/Hexiwear/Assets.xcassets/steps_icon.imageset/Contents.json new file mode 100644 index 0000000..1291640 --- /dev/null +++ b/iOS/Hexiwear/Assets.xcassets/steps_icon.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "steps_icon.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "steps_icon-1.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "steps_icon-2.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/iOS/Hexiwear/Assets.xcassets/steps_icon.imageset/steps_icon-1.png b/iOS/Hexiwear/Assets.xcassets/steps_icon.imageset/steps_icon-1.png new file mode 100644 index 0000000..d470efa Binary files /dev/null and b/iOS/Hexiwear/Assets.xcassets/steps_icon.imageset/steps_icon-1.png differ diff --git a/iOS/Hexiwear/Assets.xcassets/steps_icon.imageset/steps_icon-2.png b/iOS/Hexiwear/Assets.xcassets/steps_icon.imageset/steps_icon-2.png new file mode 100644 index 0000000..d470efa Binary files /dev/null and b/iOS/Hexiwear/Assets.xcassets/steps_icon.imageset/steps_icon-2.png differ diff --git a/iOS/Hexiwear/Assets.xcassets/steps_icon.imageset/steps_icon.png b/iOS/Hexiwear/Assets.xcassets/steps_icon.imageset/steps_icon.png new file mode 100644 index 0000000..d470efa Binary files /dev/null and b/iOS/Hexiwear/Assets.xcassets/steps_icon.imageset/steps_icon.png differ diff --git a/iOS/Hexiwear/Assets.xcassets/temperature.imageset/Contents.json b/iOS/Hexiwear/Assets.xcassets/temperature.imageset/Contents.json new file mode 100644 index 0000000..f040fb2 --- /dev/null +++ b/iOS/Hexiwear/Assets.xcassets/temperature.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "temperature.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "temperature-1.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "temperature-2.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/iOS/Hexiwear/Assets.xcassets/temperature.imageset/temperature-1.png b/iOS/Hexiwear/Assets.xcassets/temperature.imageset/temperature-1.png new file mode 100644 index 0000000..20d4414 Binary files /dev/null and b/iOS/Hexiwear/Assets.xcassets/temperature.imageset/temperature-1.png differ diff --git a/iOS/Hexiwear/Assets.xcassets/temperature.imageset/temperature-2.png b/iOS/Hexiwear/Assets.xcassets/temperature.imageset/temperature-2.png new file mode 100644 index 0000000..20d4414 Binary files /dev/null and b/iOS/Hexiwear/Assets.xcassets/temperature.imageset/temperature-2.png differ diff --git a/iOS/Hexiwear/Assets.xcassets/temperature.imageset/temperature.png b/iOS/Hexiwear/Assets.xcassets/temperature.imageset/temperature.png new file mode 100644 index 0000000..20d4414 Binary files /dev/null and b/iOS/Hexiwear/Assets.xcassets/temperature.imageset/temperature.png differ diff --git a/iOS/Hexiwear/Assets.xcassets/wolkabout.imageset/Contents.json b/iOS/Hexiwear/Assets.xcassets/wolkabout.imageset/Contents.json new file mode 100644 index 0000000..a5329d7 --- /dev/null +++ b/iOS/Hexiwear/Assets.xcassets/wolkabout.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "wolkabout.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "wolkabout-1.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "wolkabout-2.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/iOS/Hexiwear/Assets.xcassets/wolkabout.imageset/wolkabout-1.png b/iOS/Hexiwear/Assets.xcassets/wolkabout.imageset/wolkabout-1.png new file mode 100644 index 0000000..d83babd Binary files /dev/null and b/iOS/Hexiwear/Assets.xcassets/wolkabout.imageset/wolkabout-1.png differ diff --git a/iOS/Hexiwear/Assets.xcassets/wolkabout.imageset/wolkabout-2.png b/iOS/Hexiwear/Assets.xcassets/wolkabout.imageset/wolkabout-2.png new file mode 100644 index 0000000..d83babd Binary files /dev/null and b/iOS/Hexiwear/Assets.xcassets/wolkabout.imageset/wolkabout-2.png differ diff --git a/iOS/Hexiwear/Assets.xcassets/wolkabout.imageset/wolkabout.png b/iOS/Hexiwear/Assets.xcassets/wolkabout.imageset/wolkabout.png new file mode 100644 index 0000000..d83babd Binary files /dev/null and b/iOS/Hexiwear/Assets.xcassets/wolkabout.imageset/wolkabout.png differ diff --git a/iOS/Hexiwear/Base.lproj/Main.storyboard b/iOS/Hexiwear/Base.lproj/Main.storyboard new file mode 100644 index 0000000..0ca53d3 --- /dev/null +++ b/iOS/Hexiwear/Base.lproj/Main.storyboard @@ -0,0 +1,4127 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/iOS/Hexiwear/BaseNavigationController.swift b/iOS/Hexiwear/BaseNavigationController.swift new file mode 100644 index 0000000..00a874f --- /dev/null +++ b/iOS/Hexiwear/BaseNavigationController.swift @@ -0,0 +1,41 @@ +// +// Hexiwear application is used to pair with Hexiwear BLE devices +// and send sensor readings to WolkSense sensor data cloud +// +// Copyright (C) 2016 WolkAbout Technology s.r.o. +// +// Hexiwear is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Hexiwear is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . +// +// +// BaseNavigationController.swift +// + +import UIKit + +class BaseNavigationController: UINavigationController { + + override func viewDidLoad() { + super.viewDidLoad() + let layer = self.navigationBar.layer + self.navigationBar.barTintColor = wolkaboutBlueColor + self.navigationBar.tintColor = UIColor.whiteColor() + layer.cornerRadius = 3.0 + layer.shadowOffset = CGSizeMake(1.0, 2.0) + layer.shadowColor = UIColor.blackColor().CGColor + layer.shadowRadius = 3.0 + layer.shadowOpacity = 0.3 + navigationBar.tintColor = UIColor.whiteColor() + navigationBar.titleTextAttributes = [NSForegroundColorAttributeName: UIColor.whiteColor()] + } +} diff --git a/iOS/Hexiwear/ChangePasswordTableViewController.swift b/iOS/Hexiwear/ChangePasswordTableViewController.swift new file mode 100644 index 0000000..ea610b2 --- /dev/null +++ b/iOS/Hexiwear/ChangePasswordTableViewController.swift @@ -0,0 +1,124 @@ +// +// Hexiwear application is used to pair with Hexiwear BLE devices +// and send sensor readings to WolkSense sensor data cloud +// +// Copyright (C) 2016 WolkAbout Technology s.r.o. +// +// Hexiwear is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Hexiwear is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . +// +// +// ChangePasswordTableViewController.swift +// + +import UIKit + +class ChangePasswordTableViewController: UITableViewController { + + @IBOutlet weak var oldPassword: UITextField! + @IBOutlet weak var newPassword: UITextField! + @IBOutlet weak var confirmNewPassword: UITextField! + @IBOutlet weak var buttons: UIView! + @IBOutlet weak var activityIndicator: UIActivityIndicatorView! + + var dataStore: DataStore! + var userEmail: String = "" + + override func viewDidLoad() { + super.viewDidLoad() + oldPassword.delegate = self + newPassword.delegate = self + confirmNewPassword.delegate = self + title = "Change password" + } + + override func viewDidAppear(animated: Bool) { + if isMovingToParentViewController() { + oldPassword.becomeFirstResponder() + } + } + + @IBAction func closeAction(sender: AnyObject) { + dismissViewControllerAnimated(true, completion: nil) + } + + private func hideButtons() { + buttons.hidden = true + activityIndicator.startAnimating() + } + + private func showButtons() { + buttons.hidden = false + activityIndicator.stopAnimating() + } + + func checkAllFieldsSet() -> Bool { + guard let oldPass = oldPassword.text, newPass = newPassword.text, conf = confirmNewPassword.text else { + return false + } + return !oldPass.isEmpty && !newPass.isEmpty && !conf.isEmpty + + } + + @IBAction func changeAction(sender: AnyObject) { + hideButtons() + + if !checkAllFieldsSet() { + showSimpleAlertWithTitle(applicationTitle, message: "All fields are mandatory!", viewController: self) + showButtons() + return + } + + + let passwordsMatch = newPassword.text == confirmNewPassword.text + if !passwordsMatch { + showSimpleAlertWithTitle(applicationTitle, message: "Passwords do not match!", viewController: self) + showButtons() + return + } + + dataStore.changePasswordForUserEmail(userEmail, oldPassword: oldPassword.text ?? "", newPassword: newPassword.text ?? "", + onFailure:{ _ in + dispatch_async(dispatch_get_main_queue()) { + showSimpleAlertWithTitle(applicationTitle, message: "Error changing password!", viewController: self) + self.showButtons() + } + }, + onSuccess: { + dispatch_async(dispatch_get_main_queue()) { + showSimpleAlertWithTitle(applicationTitle, message: "Password has been changed!", viewController: self, OKhandler: { _ in + self.dismissViewControllerAnimated(true, completion: nil) }) + } + } + ) + + } + +} + +extension ChangePasswordTableViewController: UITextFieldDelegate { + func textFieldShouldReturn(textField: UITextField) -> Bool { + if textField == oldPassword { + newPassword.becomeFirstResponder() + return true + } + + if textField == newPassword { + confirmNewPassword.becomeFirstResponder() + return true + } + + textField.resignFirstResponder() + return true + } +} \ No newline at end of file diff --git a/iOS/Hexiwear/CocoaMQTT/CocoaMQTT.h b/iOS/Hexiwear/CocoaMQTT/CocoaMQTT.h new file mode 100644 index 0000000..8dda575 --- /dev/null +++ b/iOS/Hexiwear/CocoaMQTT/CocoaMQTT.h @@ -0,0 +1,20 @@ +// +// CocoaMQTT.h +// CocoaMQTT +// +// Created by CrazyWisdom on 15/12/11. +// Copyright © 2015年 emqtt.io. All rights reserved. +// + +#import + +//! Project version number for CocoaMQTT. +FOUNDATION_EXPORT double CocoaMQTTVersionNumber; + +//! Project version string for CocoaMQTT. +FOUNDATION_EXPORT const unsigned char CocoaMQTTVersionString[]; + +// In this header, you should import all the public headers of your framework using statements like #import + +#import "GCDAsyncSocket.h" +#import "MSWeakTimer.h" \ No newline at end of file diff --git a/iOS/Hexiwear/CocoaMQTT/CocoaMQTT.swift b/iOS/Hexiwear/CocoaMQTT/CocoaMQTT.swift new file mode 100644 index 0000000..41fd700 --- /dev/null +++ b/iOS/Hexiwear/CocoaMQTT/CocoaMQTT.swift @@ -0,0 +1,628 @@ +// +// CocoaMQTT.swift +// CocoaMQTT +// +// Created by Feng Lee on 14/8/3. +// Copyright (c) 2015 emqtt.io. All rights reserved. +// + +import Foundation + +/** + * MQTT Delegate + */ +public protocol CocoaMQTTDelegate : class { + + /** + * MQTT connected with server + */ + + func mqtt(mqtt: CocoaMQTT, didConnect host: String, port: Int) + + func mqtt(mqtt: CocoaMQTT, didConnectAck ack: CocoaMQTTConnAck) + + func mqtt(mqtt: CocoaMQTT, didPublishMessage message: CocoaMQTTMessage, id: UInt16) + + func mqtt(mqtt: CocoaMQTT, didReceiveMessage message: CocoaMQTTMessage, id: UInt16 ) + + func mqtt(mqtt: CocoaMQTT, didSubscribeTopic topic: String) + + func mqtt(mqtt: CocoaMQTT, didUnsubscribeTopic topic: String) + + func mqttDidPing(mqtt: CocoaMQTT) + + func mqttDidReceivePong(mqtt: CocoaMQTT) + + func mqttDidDisconnect(mqtt: CocoaMQTT, withError err: NSError?) + +} + +/** + * Blueprint of the MQTT client + */ +public protocol CocoaMQTTClient { + + var host: String { get set } + + var port: UInt16 { get set } + + var clientId: String { get } + + var username: String? {get set} + + var password: String? {get set} + + var secureMQTT: Bool {get set} + + var cleanSess: Bool {get set} + + var keepAlive: UInt16 {get set} + + var willMessage: CocoaMQTTWill? {get set} + + func connect() -> Bool + + func publish(topic: String, withString string: String, qos: CocoaMQTTQOS, retain: Bool, dup: Bool) -> UInt16 + + func publish(message: CocoaMQTTMessage) -> UInt16 + + func subscribe(topic: String, qos: CocoaMQTTQOS) -> UInt16 + + func unsubscribe(topic: String) -> UInt16 + + func ping() + + func disconnect() + +} + + +/** + * QOS + */ +public enum CocoaMQTTQOS: UInt8 { + + case QOS0 = 0 + + case QOS1 + + case QOS2 +} + +/** + * Connection State + */ +enum CocoaMQTTConnState: UInt8 { + + case INIT = 0 + + case CONNECTING + + case CONNECTED + + case DISCONNECTED +} + + +/** + * Conn Ack + */ +public enum CocoaMQTTConnAck: UInt8 { + + case ACCEPT = 0 + + case PROTO_VER + + case INVALID_ID + + case SERVER + + case CREDENTIALS + + case AUTH + +} + +/** + * asyncsocket read tag + */ +enum CocoaMQTTReadTag: Int { + + case TAG_HEADER = 0 + + case TAG_LENGTH + + case TAG_PAYLOAD + +} + +/** + * Main CocoaMQTT Class + * + * Notice: GCDAsyncSocket need delegate to extend NSObject + */ +public class CocoaMQTT: NSObject, CocoaMQTTClient, GCDAsyncSocketDelegate, CocoaMQTTReaderDelegate { + + //client variables + + public var host = "localhost" + + public var port: UInt16 = 1883 + + public var clientId: String + + public var username: String? + + public var password: String? + + public var secureMQTT: Bool = false + + public var cleanSess: Bool = true + + //keep alive + + public var keepAlive: UInt16 = 60 + + var aliveTimer: MSWeakTimer? + + //will message + + public var willMessage: CocoaMQTTWill? + + //delegate weak?? + + public weak var delegate: CocoaMQTTDelegate? + + //socket and connection + + var connState = CocoaMQTTConnState.INIT + + var socket: GCDAsyncSocket? + + var reader: CocoaMQTTReader? + + //global message id + + var gmid: UInt16 = 1 + + //subscribed topics + + var subscriptions = Dictionary() + + //published messages + + var messages = Dictionary() + + public init(clientId: String, host: String = "localhost", port: UInt16 = 1883) { + self.clientId = clientId + self.host = host + self.port = port + } + + //API Functions + + public func connect() -> Bool { + socket = GCDAsyncSocket(delegate: self, delegateQueue: dispatch_get_main_queue()) + reader = CocoaMQTTReader(socket: socket!, delegate: self) + do { + try socket!.connectToHost(self.host, onPort: self.port) + connState = CocoaMQTTConnState.CONNECTING + return true + } catch let error as NSError { + #if DEBUG + NSLog("CocoaMQTT: socket connect error: \(error.description)") + #endif + return false + } + } + + public func publish(topic: String, withString string: String, qos: CocoaMQTTQOS = .QOS1, retain: Bool = false, dup: Bool = false) -> UInt16 { + let message = CocoaMQTTMessage(topic: topic, string: string, qos: qos, retain: retain, dup: dup) + return publish(message) + } + + public func publish(message: CocoaMQTTMessage) -> UInt16 { + let msgId: UInt16 = _nextMessageId() + let frame = CocoaMQTTFramePublish(msgid: msgId, topic: message.topic, payload: message.payload) + frame.qos = message.qos.rawValue + frame.retain = message.retain + frame.dup = message.dup + send(frame, tag: Int(msgId)) + if message.qos != CocoaMQTTQOS.QOS0 { + messages[msgId] = message //cache + } else { + delegate?.mqtt(self, didPublishMessage: message, id: msgId) + } + return msgId + } + + public func subscribe(topic: String, qos: CocoaMQTTQOS = .QOS1) -> UInt16 { + let msgId = _nextMessageId() + let frame = CocoaMQTTFrameSubscribe(msgid: msgId, topic: topic, reqos: qos.rawValue) + send(frame, tag: Int(msgId)) + subscriptions[msgId] = topic //cache? + return msgId + } + + public func unsubscribe(topic: String) -> UInt16 { + let msgId = _nextMessageId() + let frame = CocoaMQTTFrameUnsubscribe(msgid: msgId, topic: topic) + subscriptions[msgId] = topic //cache + send(frame, tag: Int(msgId)) + return msgId + } + + public func ping() { + send(CocoaMQTTFrame(type: CocoaMQTTFrameType.PINGREQ), tag: -0xC0) + self.delegate?.mqttDidPing(self) + } + + public func disconnect() { + send(CocoaMQTTFrame(type: CocoaMQTTFrameType.DISCONNECT), tag: -0xE0) + socket!.disconnect() + } + + func send(frame: CocoaMQTTFrame, tag: Int = 0) { + let data = frame.data() + socket!.writeData(NSData(bytes: data, length: data.count), withTimeout: -1, tag: tag) + } + + //AsyncSocket Delegate + + public func socket(sock: GCDAsyncSocket!, didConnectToHost host: String!, port: UInt16) { + connState = CocoaMQTTConnState.CONNECTED + sock.startTLS(["GCDAsyncSocketManuallyEvaluateTrust": true]) + delegate?.mqtt(self, didConnect: host, port: Int(port)) + } + + public func socket(sock: GCDAsyncSocket!, didReceiveTrust trust: SecTrust!, completionHandler: ((Bool) -> Void)!) { + #if DEBUG + NSLog("CocoaMQTT: didReceiveTrust") + #endif + let certificateData = getCertificate() + + guard certificateData.isEmpty == false else { completionHandler(false); return } + + guard let trust = trust else { completionHandler(false); return } + + let isValidResult = isValid(trust, certificates: certificateData, domain: nil) + completionHandler(isValidResult) + } + + internal func getCertificate() -> [NSData] { + guard let certFilePath = NSBundle.mainBundle().pathForResource("ca", ofType: "cer") else { return [] } + guard let data = NSData(contentsOfFile: certFilePath) else { return [] } + return [data] + } + + func isValid(trust: SecTrust, certificates: [NSData], domain: String?) -> Bool { + + let policy = SecPolicyCreateSSL(true, domain) + + SecTrustSetPolicies(trust,policy) + + var collect = [SecCertificate]() + for cert in certificates { + collect.append(SecCertificateCreateWithData(nil,cert)!) + } + SecTrustSetAnchorCertificates(trust,collect) + var result: SecTrustResultType = 0 + SecTrustEvaluate(trust,&result) + let r = Int(result) + if r == kSecTrustResultUnspecified || r == kSecTrustResultProceed { + return true + } + return false + } + + func certificateChainForTrust(trust: SecTrustRef) -> [NSData] { + let certificates = (0.. [NSData] in + var certificates = certificates + let cert = SecTrustGetCertificateAtIndex(trust, index) + certificates.append(SecCertificateCopyData(cert!)) + return certificates + } + + return certificates + } + + + public func socketDidSecure(sock: GCDAsyncSocket!) { + #if DEBUG + NSLog("CocoaMQTT: socketDidSecure") + #endif + let frame = CocoaMQTTFrameConnect(client: self) + send(frame) + reader!.start() + } + + public func socket(sock: GCDAsyncSocket!, didWriteDataWithTag tag: Int) { + #if DEBUG + NSLog("CocoaMQTT: Socket write message with tag: \(tag)") + #endif + } + + public func socket(sock: GCDAsyncSocket!, didReadData data: NSData!, withTag tag: Int) { + let etag: CocoaMQTTReadTag = CocoaMQTTReadTag(rawValue: tag)! + var bytes = [UInt8]([0]) + switch etag { + case CocoaMQTTReadTag.TAG_HEADER: + data.getBytes(&bytes, length: 1) + reader!.headerReady(bytes[0]) + case CocoaMQTTReadTag.TAG_LENGTH: + data.getBytes(&bytes, length: 1) + reader!.lengthReady(bytes[0]) + case CocoaMQTTReadTag.TAG_PAYLOAD: + reader!.payloadReady(data) + } + } + + public func socketDidDisconnect(sock: GCDAsyncSocket!, withError err: NSError!) { + connState = CocoaMQTTConnState.DISCONNECTED + delegate?.mqttDidDisconnect(self, withError: err) + } + + //CocoaMQTTReader Delegate + + public func didReceiveConnAck(reader: CocoaMQTTReader, connack: UInt8) { + connState = CocoaMQTTConnState.CONNECTED + #if DEBUG + NSLog("CocoaMQTT: CONNACK Received: \(connack)") + #endif + + let ack = CocoaMQTTConnAck(rawValue: connack)! + delegate?.mqtt(self, didConnectAck: ack) + + //keep alive + if ack == CocoaMQTTConnAck.ACCEPT && keepAlive > 0 { + aliveTimer = MSWeakTimer.scheduledTimerWithTimeInterval( + NSTimeInterval(keepAlive), + target: self, + selector: #selector(CocoaMQTT._aliveTimerFired), + userInfo: nil, + repeats: true, + dispatchQueue: dispatch_get_main_queue()) + } + } + + func _aliveTimerFired() { + if connState == CocoaMQTTConnState.CONNECTED { + ping() + } else { + aliveTimer?.invalidate() + } + } + + func didReceivePublish(reader: CocoaMQTTReader, message: CocoaMQTTMessage, id: UInt16) { + #if DEBUG + NSLog("CocoaMQTT: PUBLISH Received from \(message.topic)") + #endif + delegate?.mqtt(self, didReceiveMessage: message, id: id) + if message.qos == CocoaMQTTQOS.QOS1 { + _puback(CocoaMQTTFrameType.PUBACK, msgid: id) + } else if message.qos == CocoaMQTTQOS.QOS2 { + _puback(CocoaMQTTFrameType.PUBREC, msgid: id) + } + } + + func _puback(type: CocoaMQTTFrameType, msgid: UInt16) { + var descr: String? + switch type { + case .PUBACK: descr = "PUBACK" + case .PUBREC: descr = "PUBREC" + case .PUBREL: descr = "PUBREL" + case .PUBCOMP: descr = "PUBCOMP" + default: assert(false) + } + #if DEBUG + if descr != nil { + NSLog("CocoaMQTT: Send \(descr!), msgid: \(msgid)") + } + #endif + send(CocoaMQTTFramePubAck(type: type, msgid: msgid)) + } + + func didReceivePubAck(reader: CocoaMQTTReader, msgid: UInt16) { + #if DEBUG + NSLog("CocoaMQTT: PUBACK Received: \(msgid)") + #endif + if let message = messages[msgid] { + messages.removeValueForKey(msgid) + delegate?.mqtt(self, didPublishMessage: message, id: msgid) + } + } + + func didReceivePubRec(reader: CocoaMQTTReader, msgid: UInt16) { + #if DEBUG + NSLog("CocoaMQTT: PUBREC Received: \(msgid)") + #endif + _puback(CocoaMQTTFrameType.PUBREL, msgid: msgid) + } + + func didReceivePubRel(reader: CocoaMQTTReader, msgid: UInt16) { + #if DEBUG + NSLog("CocoaMQTT: PUBREL Received: \(msgid)") + #endif + if let message = messages[msgid] { + messages.removeValueForKey(msgid) + delegate?.mqtt(self, didPublishMessage: message, id: msgid) + } + _puback(CocoaMQTTFrameType.PUBCOMP, msgid: msgid) + } + + func didReceivePubComp(reader: CocoaMQTTReader, msgid: UInt16) { + #if DEBUG + NSLog("CocoaMQTT: PUBCOMP Received: \(msgid)") + #endif + } + + func didReceiveSubAck(reader: CocoaMQTTReader, msgid: UInt16) { + #if DEBUG + NSLog("CocoaMQTT: SUBACK Received: \(msgid)") + #endif + if let topic = subscriptions.removeValueForKey(msgid) { + delegate?.mqtt(self, didSubscribeTopic: topic) + } + } + + func didReceiveUnsubAck(reader: CocoaMQTTReader, msgid: UInt16) { + #if DEBUG + NSLog("CocoaMQTT: UNSUBACK Received: \(msgid)") + #endif + if let topic = subscriptions.removeValueForKey(msgid) { + delegate?.mqtt(self, didUnsubscribeTopic: topic) + } + } + + func didReceivePong(reader: CocoaMQTTReader) { + #if DEBUG + NSLog("CocoaMQTT: PONG Received") + #endif + delegate?.mqttDidReceivePong(self) + } + + func _nextMessageId() -> UInt16 { + self.gmid += 1 + let id = self.gmid + if id >= UInt16.max { gmid = 1 } + return id + } + +} + +/** + * MQTT Reader Delegate + */ +protocol CocoaMQTTReaderDelegate { + + func didReceiveConnAck(reader: CocoaMQTTReader, connack: UInt8) + + func didReceivePublish(reader: CocoaMQTTReader, message: CocoaMQTTMessage, id: UInt16) + + func didReceivePubAck(reader: CocoaMQTTReader, msgid: UInt16) + + func didReceivePubRec(reader: CocoaMQTTReader, msgid: UInt16) + + func didReceivePubRel(reader: CocoaMQTTReader, msgid: UInt16) + + func didReceivePubComp(reader: CocoaMQTTReader, msgid: UInt16) + + func didReceiveSubAck(reader: CocoaMQTTReader, msgid: UInt16) + + func didReceiveUnsubAck(reader: CocoaMQTTReader, msgid: UInt16) + + func didReceivePong(reader: CocoaMQTTReader) + +} + +public class CocoaMQTTReader { + + var socket: GCDAsyncSocket + + var header: UInt8 = 0 + + var data: [UInt8] = [] + + var length: UInt = 0 + + var multiply: Int = 1 + + var delegate: CocoaMQTTReaderDelegate + + var timeout: Int = 30000 + + init(socket: GCDAsyncSocket, delegate: CocoaMQTTReaderDelegate) { + self.socket = socket + self.delegate = delegate + } + + func start() { readHeader() } + + func readHeader() { + _reset(); socket.readDataToLength(1, withTimeout: -1, tag: CocoaMQTTReadTag.TAG_HEADER.rawValue) + } + + func headerReady(header: UInt8) { + #if DEBUG + NSLog("CocoaMQTTReader: header ready: \(header) ") + #endif + self.header = header + readLength() + } + + func readLength() { + socket.readDataToLength(1, withTimeout: NSTimeInterval(timeout), tag: CocoaMQTTReadTag.TAG_LENGTH.rawValue) + } + + func lengthReady(byte: UInt8) { + length += (UInt)((Int)(byte & 127) * multiply) + if byte & 0x80 == 0 { //done + if length == 0 { + frameReady() + } else { + readPayload() + } + } else { //more + multiply *= 128 + readLength() + } + } + + func readPayload() { + socket.readDataToLength(length, withTimeout: NSTimeInterval(timeout), tag: CocoaMQTTReadTag.TAG_PAYLOAD.rawValue) + } + + func payloadReady(data: NSData) { + self.data = [UInt8](count: data.length, repeatedValue: 0) + data.getBytes(&(self.data), length: data.length) + frameReady() + } + + func frameReady() { + //handle frame + let frameType = CocoaMQTTFrameType(rawValue: UInt8(header & 0xF0))! + switch frameType { + case .CONNACK: + delegate.didReceiveConnAck(self, connack: data[1]) + case .PUBLISH: + let (msgId, message) = unpackPublish() + delegate.didReceivePublish(self, message: message, id: msgId) + case .PUBACK: + delegate.didReceivePubAck(self, msgid: _msgid(data)) + case .PUBREC: + delegate.didReceivePubRec(self, msgid: _msgid(data)) + case .PUBREL: + delegate.didReceivePubRel(self, msgid: _msgid(data)) + case .PUBCOMP: + delegate.didReceivePubComp(self, msgid: _msgid(data)) + case .SUBACK: + delegate.didReceiveSubAck(self, msgid: _msgid(data)) + case .UNSUBACK: + delegate.didReceiveUnsubAck(self, msgid: _msgid(data)) + case .PINGRESP: + delegate.didReceivePong(self) + default: + assert(false) + } + readHeader() + } + + func unpackPublish() -> (UInt16, CocoaMQTTMessage) { + let frame = CocoaMQTTFramePublish(header: header, data: data) + frame.unpack() + let msgId = frame.msgid! + let qos = CocoaMQTTQOS(rawValue: frame.qos)! + let message = CocoaMQTTMessage(topic: frame.topic!, payload: frame.payload, qos: qos, retain: frame.retain, dup: frame.dup) + return (msgId, message) + } + + func _msgid(bytes: [UInt8]) -> UInt16 { + if bytes.count < 2 { return 0 } + return UInt16(bytes[0]) << 8 + UInt16(bytes[1]) + } + + func _reset() { + length = 0; multiply = 1; header = 0; data = [] + } + +} diff --git a/iOS/Hexiwear/CocoaMQTT/CocoaMQTTFrame.swift b/iOS/Hexiwear/CocoaMQTT/CocoaMQTTFrame.swift new file mode 100644 index 0000000..d0b95c6 --- /dev/null +++ b/iOS/Hexiwear/CocoaMQTT/CocoaMQTTFrame.swift @@ -0,0 +1,411 @@ +// +// CocoaMQTTFrame.swift +// CocoaMQTT +// +// Created by Feng Lee on 14/8/3. +// Copyright (c) 2015 emqtt.io. All rights reserved. +// + +import Foundation + +/** + * Encode and Decode big-endian UInt16 + */ +extension UInt16 { + + //Most Significant Byte (MSB) + var highByte: UInt8 { return UInt8( (self & 0xFF00) >> 8) } + + //Least Significant Byte (LSB) + var lowByte: UInt8 { return UInt8(self & 0x00FF) } + + var hlBytes: [UInt8] { return [highByte, lowByte] } + +} + +/** + * String with two bytes length + */ +extension String { + + //ok? + var bytesWithLength: [UInt8] { return UInt16(utf8.count).hlBytes + utf8 } + +} + +/** + * Bool to bit + */ +extension Bool { + + var bit: UInt8 { return self ? 1 : 0} + + init(bit: UInt8) { + self = (bit == 0) ? false : true + } + +} + +/** + * read bit + */ +extension UInt8 { + + func bitAt(offset: UInt8) -> UInt8 { + return (self >> offset) & 0x01 + } + +} + +/** + * MQTT Frame Type + */ +enum CocoaMQTTFrameType: UInt8 { + + case RESERVED = 0x00 + + case CONNECT = 0x10 + + case CONNACK = 0x20 + + case PUBLISH = 0x30 + + case PUBACK = 0x40 + + case PUBREC = 0x50 + + case PUBREL = 0x60 + + case PUBCOMP = 0x70 + + case SUBSCRIBE = 0x80 + + case SUBACK = 0x90 + + case UNSUBSCRIBE = 0xA0 + + case UNSUBACK = 0xB0 + + case PINGREQ = 0xC0 + + case PINGRESP = 0xD0 + + case DISCONNECT = 0xE0 + +} + + +/** + * MQTT Frame + */ +class CocoaMQTTFrame { + + + /** + * |-------------------------------------- + * | 7 6 5 4 | 3 | 2 1 | 0 | + * | Type | DUP flag | QoS | RETAIN | + * |-------------------------------------- + */ + var header: UInt8 = 0 + + var type: UInt8 { return UInt8(header & 0xF0) } + + var dup: Bool { + + get { return ((header & 0x08) >> 3) == 0 ? false : true } + + set { header |= (newValue.bit << 3) } + + } + + var qos: UInt8 { + + //#define GETQOS(HDR) ((HDR & 0x06) >> 1) + get { return (header & 0x06) >> 1 } + + //#define SETQOS(HDR, Q) (HDR | ((Q) << 1)) + set { header |= (newValue << 1) } + + } + + var retain: Bool { + + get { return (header & 0x01) == 0 ? false : true } + + set { header |= newValue.bit } + + } + + /* + * Variable Header + */ + var variableHeader: [UInt8] = [] + + /* + * Payload + */ + var payload: [UInt8] = [] + + init(header: UInt8) { + self.header = header + } + + init(type: CocoaMQTTFrameType, payload: [UInt8] = []) { + self.header = type.rawValue + self.payload = payload + } + + func data() -> [UInt8] { + self.pack() + return [UInt8]([header]) + encodeLength() + variableHeader + payload + } + + func encodeLength() -> [UInt8] { + var bytes: [UInt8] = [] + var digit: UInt8 = 0 + var len: UInt32 = UInt32(variableHeader.count+payload.count) + repeat { + digit = UInt8(len % 128) + len = len / 128 + // if there are more digits to encode, set the top bit of this digit + if len > 0 { digit = digit | 0x80 } + bytes.append(digit) + } while len > 0 + return bytes + } + + func pack() { return; } //do nothing + +} + +/** + * MQTT CONNECT Frame + */ +class CocoaMQTTFrameConnect: CocoaMQTTFrame { + + let PROTOCOL_LEVEL = UInt8(4) + + let PROTOCOL_VERSION: String = "MQTT/3.1.1" + + let PROTOCOL_MAGIC: String = "MQTT" + + /** + * |---------------------------------------------------------------------------------- + * | 7 | 6 | 5 | 4 3 | 2 | 1 | 0 | + * | username | password | willretain | willqos | willflag | cleansession | reserved | + * |---------------------------------------------------------------------------------- + */ + var flags: UInt8 = 0 + + var flagUsername: Bool { + //#define FLAG_USERNAME(F, U) (F | ((U) << 7)) + get { return Bool(bit: (flags >> 7) & 0x01) } + + set { flags |= (newValue.bit << 7) } + } + + var flagPasswd: Bool { + //#define FLAG_PASSWD(F, P) (F | ((P) << 6)) + get { return Bool(bit:(flags >> 6) & 0x01) } + + set { flags |= (newValue.bit << 6) } + } + + var flagWillRetain: Bool { + //#define FLAG_WILLRETAIN(F, R) (F | ((R) << 5)) + get { return Bool(bit: (flags >> 5) & 0x01) } + + set { flags |= (newValue.bit << 5) } + } + + var flagWillQOS: UInt8 { + //#define FLAG_WILLQOS(F, Q) (F | ((Q) << 3)) + get { return (flags >> 3) & 0x03 } + + set { flags |= (newValue << 3) } + } + + var flagWill: Bool { + //#define FLAG_WILL(F, W) (F | ((W) << 2)) + get { return Bool(bit:(flags >> 2) & 0x01) } + + set { flags |= ((newValue.bit) << 2) } + } + + var flagCleanSess: Bool { + //#define FLAG_CLEANSESS(F, C) (F | ((C) << 1)) + get { return Bool(bit: (flags >> 1) & 0x01) } + + set { flags |= ((newValue.bit) << 1) } + } + + var client: CocoaMQTTClient + + init(client: CocoaMQTT) { + self.client = client + super.init(type: CocoaMQTTFrameType.CONNECT) + } + + override func pack() { + + //variable header + variableHeader += PROTOCOL_MAGIC.bytesWithLength + variableHeader.append(PROTOCOL_LEVEL) + + //payload + payload += client.clientId.bytesWithLength + + if let will = client.willMessage { + flagWill = true + flagWillQOS = will.qos.rawValue + flagWillRetain = will.retain + payload += will.topic.bytesWithLength + payload += will.payload + } + if let username = client.username { + flagUsername = true + payload += username.bytesWithLength + } + if let password = client.password { + flagPasswd = true + payload += password.bytesWithLength + } + + //flags + flagCleanSess = client.cleanSess + variableHeader.append(flags) + variableHeader += client.keepAlive.hlBytes + + } + +} + +/** + * MQTT PUBLISH Frame + */ +class CocoaMQTTFramePublish: CocoaMQTTFrame { + + var msgid: UInt16? + + var topic: String? + + var data: [UInt8]? + + init(msgid: UInt16, topic: String, payload: [UInt8]) { + super.init(type: CocoaMQTTFrameType.PUBLISH, payload: payload) + self.msgid = msgid + self.topic = topic + } + + init(header: UInt8, data: [UInt8]) { + super.init(header: header) + self.data = data + } + + func unpack() { + //topic + var msb = data![0], lsb = data![1] + let len = UInt16(msb) << 8 + UInt16(lsb) + var pos: Int = 2 + Int(len) + topic = NSString(bytes: [UInt8](data![2...(pos-1)]), length: Int(len), encoding: NSUTF8StringEncoding) as? String + + //msgid + if qos == 0 { + msgid = 0 + } else { + msb = data![pos]; lsb = data![pos+1] + msgid = UInt16(msb) << 8 + UInt16(lsb) + pos += 2 + } + + //payload + let end = data!.count - 1 + + if (end - pos >= 0) { + payload = [UInt8](data![pos...end]) + //receives an empty message + } else { + payload = [] + } + } + + override func pack() { + variableHeader += topic!.bytesWithLength + if qos > 0 { + variableHeader += msgid!.hlBytes + } + } + +} + +/** + * MQTT PUBACK Frame + */ +class CocoaMQTTFramePubAck: CocoaMQTTFrame { + + var msgid: UInt16? + + init(type: CocoaMQTTFrameType, msgid: UInt16) { + super.init(type: type) + if type == CocoaMQTTFrameType.PUBREL { + qos = CocoaMQTTQOS.QOS1.rawValue + } + self.msgid = msgid + } + + override func pack() { + variableHeader += msgid!.hlBytes + } + +} + +/** + * MQTT SUBSCRIBE Frame + */ +class CocoaMQTTFrameSubscribe: CocoaMQTTFrame { + + var msgid: UInt16? + + var topic: String? + + var reqos: UInt8 = CocoaMQTTQOS.QOS0.rawValue + + init(msgid: UInt16, topic: String, reqos: UInt8) { + super.init(type: CocoaMQTTFrameType.SUBSCRIBE) + self.msgid = msgid + self.topic = topic + self.reqos = reqos + self.qos = CocoaMQTTQOS.QOS1.rawValue + } + + override func pack() { + variableHeader += msgid!.hlBytes + payload += topic!.bytesWithLength + payload.append(reqos) + } + +} + +/** + * MQTT UNSUBSCRIBE Frame + */ +class CocoaMQTTFrameUnsubscribe: CocoaMQTTFrame { + + var msgid: UInt16? + + var topic: String? + + init(msgid: UInt16, topic: String) { + super.init(type: CocoaMQTTFrameType.UNSUBSCRIBE) + self.msgid = msgid + self.topic = topic + qos = CocoaMQTTQOS.QOS1.rawValue + } + + override func pack() { + variableHeader += msgid!.hlBytes + payload += topic!.bytesWithLength + } + +} diff --git a/iOS/Hexiwear/CocoaMQTT/CocoaMQTTMessage.swift b/iOS/Hexiwear/CocoaMQTT/CocoaMQTTMessage.swift new file mode 100644 index 0000000..834159f --- /dev/null +++ b/iOS/Hexiwear/CocoaMQTT/CocoaMQTTMessage.swift @@ -0,0 +1,60 @@ +// +// CocoaMQTTMessage.swift +// CocoaMQTT +// +// Created by Feng Lee on 14/8/3. +// Copyright (c) 2015 emqtt.io. All rights reserved. +// + +import Foundation + +/** + * MQTT Message + */ +public class CocoaMQTTMessage { + + public var topic: String + + public var payload: [UInt8] + + //utf8 bytes array to string + public var string: String? { + get { + return NSString(bytes: payload, length: payload.count, encoding: NSUTF8StringEncoding) as? String + } + } + + var qos: CocoaMQTTQOS = .QOS1 + + var retain: Bool = false + + var dup: Bool = false + + init(topic: String, string: String, qos: CocoaMQTTQOS = .QOS1, retain: Bool = false, dup: Bool = false) { + self.topic = topic + self.payload = [UInt8](string.utf8) + self.qos = qos + self.retain = retain + self.dup = dup + } + + init(topic: String, payload: [UInt8], qos: CocoaMQTTQOS = .QOS1, retain: Bool = false, dup: Bool = false) { + self.topic = topic + self.payload = payload + self.qos = qos + self.retain = retain + self.dup = dup + } + +} + +/** + * MQTT Will Message + */ +public class CocoaMQTTWill: CocoaMQTTMessage { + + public init(topic: String, message: String) { + super.init(topic: topic, payload: message.bytesWithLength) + } + +} diff --git a/iOS/Hexiwear/CocoaMQTT/GCDAsyncSocket.h b/iOS/Hexiwear/CocoaMQTT/GCDAsyncSocket.h new file mode 100644 index 0000000..60aa5bb --- /dev/null +++ b/iOS/Hexiwear/CocoaMQTT/GCDAsyncSocket.h @@ -0,0 +1,1197 @@ +// +// GCDAsyncSocket.h +// +// This class is in the public domain. +// Originally created by Robbie Hanson in Q3 2010. +// Updated and maintained by Deusty LLC and the Apple development community. +// +// https://github.com/robbiehanson/CocoaAsyncSocket +// + +#import +#import +#import +#import +#import + +#include // AF_INET, AF_INET6 + +@class GCDAsyncReadPacket; +@class GCDAsyncWritePacket; +@class GCDAsyncSocketPreBuffer; + +extern NSString *const GCDAsyncSocketException; +extern NSString *const GCDAsyncSocketErrorDomain; + +extern NSString *const GCDAsyncSocketQueueName; +extern NSString *const GCDAsyncSocketThreadName; + +extern NSString *const GCDAsyncSocketManuallyEvaluateTrust; +#if TARGET_OS_IPHONE +extern NSString *const GCDAsyncSocketUseCFStreamForTLS; +#endif +#define GCDAsyncSocketSSLPeerName (NSString *)kCFStreamSSLPeerName +#define GCDAsyncSocketSSLCertificates (NSString *)kCFStreamSSLCertificates +#define GCDAsyncSocketSSLIsServer (NSString *)kCFStreamSSLIsServer +extern NSString *const GCDAsyncSocketSSLPeerID; +extern NSString *const GCDAsyncSocketSSLProtocolVersionMin; +extern NSString *const GCDAsyncSocketSSLProtocolVersionMax; +extern NSString *const GCDAsyncSocketSSLSessionOptionFalseStart; +extern NSString *const GCDAsyncSocketSSLSessionOptionSendOneByteRecord; +extern NSString *const GCDAsyncSocketSSLCipherSuites; +#if !TARGET_OS_IPHONE +extern NSString *const GCDAsyncSocketSSLDiffieHellmanParameters; +#endif + +#define GCDAsyncSocketLoggingContext 65535 + + +typedef NS_ENUM(NSInteger, GCDAsyncSocketError) { + GCDAsyncSocketNoError = 0, // Never used + GCDAsyncSocketBadConfigError, // Invalid configuration + GCDAsyncSocketBadParamError, // Invalid parameter was passed + GCDAsyncSocketConnectTimeoutError, // A connect operation timed out + GCDAsyncSocketReadTimeoutError, // A read operation timed out + GCDAsyncSocketWriteTimeoutError, // A write operation timed out + GCDAsyncSocketReadMaxedOutError, // Reached set maxLength without completing + GCDAsyncSocketClosedError, // The remote peer closed the connection + GCDAsyncSocketOtherError, // Description provided in userInfo +}; + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +@interface GCDAsyncSocket : NSObject + +/** + * GCDAsyncSocket uses the standard delegate paradigm, + * but executes all delegate callbacks on a given delegate dispatch queue. + * This allows for maximum concurrency, while at the same time providing easy thread safety. + * + * You MUST set a delegate AND delegate dispatch queue before attempting to + * use the socket, or you will get an error. + * + * The socket queue is optional. + * If you pass NULL, GCDAsyncSocket will automatically create it's own socket queue. + * If you choose to provide a socket queue, the socket queue must not be a concurrent queue. + * If you choose to provide a socket queue, and the socket queue has a configured target queue, + * then please see the discussion for the method markSocketQueueTargetQueue. + * + * The delegate queue and socket queue can optionally be the same. + **/ +- (id)init; +- (id)initWithSocketQueue:(dispatch_queue_t)sq; +- (id)initWithDelegate:(id)aDelegate delegateQueue:(dispatch_queue_t)dq; +- (id)initWithDelegate:(id)aDelegate delegateQueue:(dispatch_queue_t)dq socketQueue:(dispatch_queue_t)sq; + +#pragma mark Configuration + +@property (atomic, weak, readwrite) id delegate; +#if OS_OBJECT_USE_OBJC +@property (atomic, strong, readwrite) dispatch_queue_t delegateQueue; +#else +@property (atomic, assign, readwrite) dispatch_queue_t delegateQueue; +#endif + +- (void)getDelegate:(id *)delegatePtr delegateQueue:(dispatch_queue_t *)delegateQueuePtr; +- (void)setDelegate:(id)delegate delegateQueue:(dispatch_queue_t)delegateQueue; + +/** + * If you are setting the delegate to nil within the delegate's dealloc method, + * you may need to use the synchronous versions below. + **/ +- (void)synchronouslySetDelegate:(id)delegate; +- (void)synchronouslySetDelegateQueue:(dispatch_queue_t)delegateQueue; +- (void)synchronouslySetDelegate:(id)delegate delegateQueue:(dispatch_queue_t)delegateQueue; + +/** + * By default, both IPv4 and IPv6 are enabled. + * + * For accepting incoming connections, this means GCDAsyncSocket automatically supports both protocols, + * and can simulataneously accept incoming connections on either protocol. + * + * For outgoing connections, this means GCDAsyncSocket can connect to remote hosts running either protocol. + * If a DNS lookup returns only IPv4 results, GCDAsyncSocket will automatically use IPv4. + * If a DNS lookup returns only IPv6 results, GCDAsyncSocket will automatically use IPv6. + * If a DNS lookup returns both IPv4 and IPv6 results, the preferred protocol will be chosen. + * By default, the preferred protocol is IPv4, but may be configured as desired. + **/ + +@property (atomic, assign, readwrite, getter=isIPv4Enabled) BOOL IPv4Enabled; +@property (atomic, assign, readwrite, getter=isIPv6Enabled) BOOL IPv6Enabled; + +@property (atomic, assign, readwrite, getter=isIPv4PreferredOverIPv6) BOOL IPv4PreferredOverIPv6; + +/** + * User data allows you to associate arbitrary information with the socket. + * This data is not used internally by socket in any way. + **/ +@property (atomic, strong, readwrite) id userData; + +#pragma mark Accepting + +/** + * Tells the socket to begin listening and accepting connections on the given port. + * When a connection is accepted, a new instance of GCDAsyncSocket will be spawned to handle it, + * and the socket:didAcceptNewSocket: delegate method will be invoked. + * + * The socket will listen on all available interfaces (e.g. wifi, ethernet, etc) + **/ +- (BOOL)acceptOnPort:(uint16_t)port error:(NSError **)errPtr; + +/** + * This method is the same as acceptOnPort:error: with the + * additional option of specifying which interface to listen on. + * + * For example, you could specify that the socket should only accept connections over ethernet, + * and not other interfaces such as wifi. + * + * The interface may be specified by name (e.g. "en1" or "lo0") or by IP address (e.g. "192.168.4.34"). + * You may also use the special strings "localhost" or "loopback" to specify that + * the socket only accept connections from the local machine. + * + * You can see the list of interfaces via the command line utility "ifconfig", + * or programmatically via the getifaddrs() function. + * + * To accept connections on any interface pass nil, or simply use the acceptOnPort:error: method. + **/ +- (BOOL)acceptOnInterface:(NSString *)interface port:(uint16_t)port error:(NSError **)errPtr; + +/** + * Tells the socket to begin listening and accepting connections on the unix domain at the given url. + * When a connection is accepted, a new instance of GCDAsyncSocket will be spawned to handle it, + * and the socket:didAcceptNewSocket: delegate method will be invoked. + * + * The socket will listen on all available interfaces (e.g. wifi, ethernet, etc) + **/ +- (BOOL)acceptOnUrl:(NSURL *)url error:(NSError **)errPtr; + +#pragma mark Connecting + +/** + * Connects to the given host and port. + * + * This method invokes connectToHost:onPort:viaInterface:withTimeout:error: + * and uses the default interface, and no timeout. + **/ +- (BOOL)connectToHost:(NSString *)host onPort:(uint16_t)port error:(NSError **)errPtr; + +/** + * Connects to the given host and port with an optional timeout. + * + * This method invokes connectToHost:onPort:viaInterface:withTimeout:error: and uses the default interface. + **/ +- (BOOL)connectToHost:(NSString *)host + onPort:(uint16_t)port + withTimeout:(NSTimeInterval)timeout + error:(NSError **)errPtr; + +/** + * Connects to the given host & port, via the optional interface, with an optional timeout. + * + * The host may be a domain name (e.g. "deusty.com") or an IP address string (e.g. "192.168.0.2"). + * The host may also be the special strings "localhost" or "loopback" to specify connecting + * to a service on the local machine. + * + * The interface may be a name (e.g. "en1" or "lo0") or the corresponding IP address (e.g. "192.168.4.35"). + * The interface may also be used to specify the local port (see below). + * + * To not time out use a negative time interval. + * + * This method will return NO if an error is detected, and set the error pointer (if one was given). + * Possible errors would be a nil host, invalid interface, or socket is already connected. + * + * If no errors are detected, this method will start a background connect operation and immediately return YES. + * The delegate callbacks are used to notify you when the socket connects, or if the host was unreachable. + * + * Since this class supports queued reads and writes, you can immediately start reading and/or writing. + * All read/write operations will be queued, and upon socket connection, + * the operations will be dequeued and processed in order. + * + * The interface may optionally contain a port number at the end of the string, separated by a colon. + * This allows you to specify the local port that should be used for the outgoing connection. (read paragraph to end) + * To specify both interface and local port: "en1:8082" or "192.168.4.35:2424". + * To specify only local port: ":8082". + * Please note this is an advanced feature, and is somewhat hidden on purpose. + * You should understand that 99.999% of the time you should NOT specify the local port for an outgoing connection. + * If you think you need to, there is a very good chance you have a fundamental misunderstanding somewhere. + * Local ports do NOT need to match remote ports. In fact, they almost never do. + * This feature is here for networking professionals using very advanced techniques. + **/ +- (BOOL)connectToHost:(NSString *)host + onPort:(uint16_t)port + viaInterface:(NSString *)interface + withTimeout:(NSTimeInterval)timeout + error:(NSError **)errPtr; + +/** + * Connects to the given address, specified as a sockaddr structure wrapped in a NSData object. + * For example, a NSData object returned from NSNetService's addresses method. + * + * If you have an existing struct sockaddr you can convert it to a NSData object like so: + * struct sockaddr sa -> NSData *dsa = [NSData dataWithBytes:&remoteAddr length:remoteAddr.sa_len]; + * struct sockaddr *sa -> NSData *dsa = [NSData dataWithBytes:remoteAddr length:remoteAddr->sa_len]; + * + * This method invokes connectToAdd + **/ +- (BOOL)connectToAddress:(NSData *)remoteAddr error:(NSError **)errPtr; + +/** + * This method is the same as connectToAddress:error: with an additional timeout option. + * To not time out use a negative time interval, or simply use the connectToAddress:error: method. + **/ +- (BOOL)connectToAddress:(NSData *)remoteAddr withTimeout:(NSTimeInterval)timeout error:(NSError **)errPtr; + +/** + * Connects to the given address, using the specified interface and timeout. + * + * The address is specified as a sockaddr structure wrapped in a NSData object. + * For example, a NSData object returned from NSNetService's addresses method. + * + * If you have an existing struct sockaddr you can convert it to a NSData object like so: + * struct sockaddr sa -> NSData *dsa = [NSData dataWithBytes:&remoteAddr length:remoteAddr.sa_len]; + * struct sockaddr *sa -> NSData *dsa = [NSData dataWithBytes:remoteAddr length:remoteAddr->sa_len]; + * + * The interface may be a name (e.g. "en1" or "lo0") or the corresponding IP address (e.g. "192.168.4.35"). + * The interface may also be used to specify the local port (see below). + * + * The timeout is optional. To not time out use a negative time interval. + * + * This method will return NO if an error is detected, and set the error pointer (if one was given). + * Possible errors would be a nil host, invalid interface, or socket is already connected. + * + * If no errors are detected, this method will start a background connect operation and immediately return YES. + * The delegate callbacks are used to notify you when the socket connects, or if the host was unreachable. + * + * Since this class supports queued reads and writes, you can immediately start reading and/or writing. + * All read/write operations will be queued, and upon socket connection, + * the operations will be dequeued and processed in order. + * + * The interface may optionally contain a port number at the end of the string, separated by a colon. + * This allows you to specify the local port that should be used for the outgoing connection. (read paragraph to end) + * To specify both interface and local port: "en1:8082" or "192.168.4.35:2424". + * To specify only local port: ":8082". + * Please note this is an advanced feature, and is somewhat hidden on purpose. + * You should understand that 99.999% of the time you should NOT specify the local port for an outgoing connection. + * If you think you need to, there is a very good chance you have a fundamental misunderstanding somewhere. + * Local ports do NOT need to match remote ports. In fact, they almost never do. + * This feature is here for networking professionals using very advanced techniques. + **/ +- (BOOL)connectToAddress:(NSData *)remoteAddr + viaInterface:(NSString *)interface + withTimeout:(NSTimeInterval)timeout + error:(NSError **)errPtr; +/** + * Connects to the unix domain socket at the given url, using the specified timeout. + */ +- (BOOL)connectToUrl:(NSURL *)url withTimeout:(NSTimeInterval)timeout error:(NSError **)errPtr; + +#pragma mark Disconnecting + +/** + * Disconnects immediately (synchronously). Any pending reads or writes are dropped. + * + * If the socket is not already disconnected, an invocation to the socketDidDisconnect:withError: delegate method + * will be queued onto the delegateQueue asynchronously (behind any previously queued delegate methods). + * In other words, the disconnected delegate method will be invoked sometime shortly after this method returns. + * + * Please note the recommended way of releasing a GCDAsyncSocket instance (e.g. in a dealloc method) + * [asyncSocket setDelegate:nil]; + * [asyncSocket disconnect]; + * [asyncSocket release]; + * + * If you plan on disconnecting the socket, and then immediately asking it to connect again, + * you'll likely want to do so like this: + * [asyncSocket setDelegate:nil]; + * [asyncSocket disconnect]; + * [asyncSocket setDelegate:self]; + * [asyncSocket connect...]; + **/ +- (void)disconnect; + +/** + * Disconnects after all pending reads have completed. + * After calling this, the read and write methods will do nothing. + * The socket will disconnect even if there are still pending writes. + **/ +- (void)disconnectAfterReading; + +/** + * Disconnects after all pending writes have completed. + * After calling this, the read and write methods will do nothing. + * The socket will disconnect even if there are still pending reads. + **/ +- (void)disconnectAfterWriting; + +/** + * Disconnects after all pending reads and writes have completed. + * After calling this, the read and write methods will do nothing. + **/ +- (void)disconnectAfterReadingAndWriting; + +#pragma mark Diagnostics + +/** + * Returns whether the socket is disconnected or connected. + * + * A disconnected socket may be recycled. + * That is, it can used again for connecting or listening. + * + * If a socket is in the process of connecting, it may be neither disconnected nor connected. + **/ +@property (atomic, readonly) BOOL isDisconnected; +@property (atomic, readonly) BOOL isConnected; + +/** + * Returns the local or remote host and port to which this socket is connected, or nil and 0 if not connected. + * The host will be an IP address. + **/ +@property (atomic, readonly) NSString *connectedHost; +@property (atomic, readonly) uint16_t connectedPort; +@property (atomic, readonly) NSURL *connectedUrl; + +@property (atomic, readonly) NSString *localHost; +@property (atomic, readonly) uint16_t localPort; + +/** + * Returns the local or remote address to which this socket is connected, + * specified as a sockaddr structure wrapped in a NSData object. + * + * @seealso connectedHost + * @seealso connectedPort + * @seealso localHost + * @seealso localPort + **/ +@property (atomic, readonly) NSData *connectedAddress; +@property (atomic, readonly) NSData *localAddress; + +/** + * Returns whether the socket is IPv4 or IPv6. + * An accepting socket may be both. + **/ +@property (atomic, readonly) BOOL isIPv4; +@property (atomic, readonly) BOOL isIPv6; + +/** + * Returns whether or not the socket has been secured via SSL/TLS. + * + * See also the startTLS method. + **/ +@property (atomic, readonly) BOOL isSecure; + +#pragma mark Reading + +// The readData and writeData methods won't block (they are asynchronous). +// +// When a read is complete the socket:didReadData:withTag: delegate method is dispatched on the delegateQueue. +// When a write is complete the socket:didWriteDataWithTag: delegate method is dispatched on the delegateQueue. +// +// You may optionally set a timeout for any read/write operation. (To not timeout, use a negative time interval.) +// If a read/write opertion times out, the corresponding "socket:shouldTimeout..." delegate method +// is called to optionally allow you to extend the timeout. +// Upon a timeout, the "socket:didDisconnectWithError:" method is called +// +// The tag is for your convenience. +// You can use it as an array index, step number, state id, pointer, etc. + +/** + * Reads the first available bytes that become available on the socket. + * + * If the timeout value is negative, the read operation will not use a timeout. + **/ +- (void)readDataWithTimeout:(NSTimeInterval)timeout tag:(long)tag; + +/** + * Reads the first available bytes that become available on the socket. + * The bytes will be appended to the given byte buffer starting at the given offset. + * The given buffer will automatically be increased in size if needed. + * + * If the timeout value is negative, the read operation will not use a timeout. + * If the buffer if nil, the socket will create a buffer for you. + * + * If the bufferOffset is greater than the length of the given buffer, + * the method will do nothing, and the delegate will not be called. + * + * If you pass a buffer, you must not alter it in any way while the socket is using it. + * After completion, the data returned in socket:didReadData:withTag: will be a subset of the given buffer. + * That is, it will reference the bytes that were appended to the given buffer via + * the method [NSData dataWithBytesNoCopy:length:freeWhenDone:NO]. + **/ +- (void)readDataWithTimeout:(NSTimeInterval)timeout + buffer:(NSMutableData *)buffer + bufferOffset:(NSUInteger)offset + tag:(long)tag; + +/** + * Reads the first available bytes that become available on the socket. + * The bytes will be appended to the given byte buffer starting at the given offset. + * The given buffer will automatically be increased in size if needed. + * A maximum of length bytes will be read. + * + * If the timeout value is negative, the read operation will not use a timeout. + * If the buffer if nil, a buffer will automatically be created for you. + * If maxLength is zero, no length restriction is enforced. + * + * If the bufferOffset is greater than the length of the given buffer, + * the method will do nothing, and the delegate will not be called. + * + * If you pass a buffer, you must not alter it in any way while the socket is using it. + * After completion, the data returned in socket:didReadData:withTag: will be a subset of the given buffer. + * That is, it will reference the bytes that were appended to the given buffer via + * the method [NSData dataWithBytesNoCopy:length:freeWhenDone:NO]. + **/ +- (void)readDataWithTimeout:(NSTimeInterval)timeout + buffer:(NSMutableData *)buffer + bufferOffset:(NSUInteger)offset + maxLength:(NSUInteger)length + tag:(long)tag; + +/** + * Reads the given number of bytes. + * + * If the timeout value is negative, the read operation will not use a timeout. + * + * If the length is 0, this method does nothing and the delegate is not called. + **/ +- (void)readDataToLength:(NSUInteger)length withTimeout:(NSTimeInterval)timeout tag:(long)tag; + +/** + * Reads the given number of bytes. + * The bytes will be appended to the given byte buffer starting at the given offset. + * The given buffer will automatically be increased in size if needed. + * + * If the timeout value is negative, the read operation will not use a timeout. + * If the buffer if nil, a buffer will automatically be created for you. + * + * If the length is 0, this method does nothing and the delegate is not called. + * If the bufferOffset is greater than the length of the given buffer, + * the method will do nothing, and the delegate will not be called. + * + * If you pass a buffer, you must not alter it in any way while AsyncSocket is using it. + * After completion, the data returned in socket:didReadData:withTag: will be a subset of the given buffer. + * That is, it will reference the bytes that were appended to the given buffer via + * the method [NSData dataWithBytesNoCopy:length:freeWhenDone:NO]. + **/ +- (void)readDataToLength:(NSUInteger)length + withTimeout:(NSTimeInterval)timeout + buffer:(NSMutableData *)buffer + bufferOffset:(NSUInteger)offset + tag:(long)tag; + +/** + * Reads bytes until (and including) the passed "data" parameter, which acts as a separator. + * + * If the timeout value is negative, the read operation will not use a timeout. + * + * If you pass nil or zero-length data as the "data" parameter, + * the method will do nothing (except maybe print a warning), and the delegate will not be called. + * + * To read a line from the socket, use the line separator (e.g. CRLF for HTTP, see below) as the "data" parameter. + * If you're developing your own custom protocol, be sure your separator can not occur naturally as + * part of the data between separators. + * For example, imagine you want to send several small documents over a socket. + * Using CRLF as a separator is likely unwise, as a CRLF could easily exist within the documents. + * In this particular example, it would be better to use a protocol similar to HTTP with + * a header that includes the length of the document. + * Also be careful that your separator cannot occur naturally as part of the encoding for a character. + * + * The given data (separator) parameter should be immutable. + * For performance reasons, the socket will retain it, not copy it. + * So if it is immutable, don't modify it while the socket is using it. + **/ +- (void)readDataToData:(NSData *)data withTimeout:(NSTimeInterval)timeout tag:(long)tag; + +/** + * Reads bytes until (and including) the passed "data" parameter, which acts as a separator. + * The bytes will be appended to the given byte buffer starting at the given offset. + * The given buffer will automatically be increased in size if needed. + * + * If the timeout value is negative, the read operation will not use a timeout. + * If the buffer if nil, a buffer will automatically be created for you. + * + * If the bufferOffset is greater than the length of the given buffer, + * the method will do nothing (except maybe print a warning), and the delegate will not be called. + * + * If you pass a buffer, you must not alter it in any way while the socket is using it. + * After completion, the data returned in socket:didReadData:withTag: will be a subset of the given buffer. + * That is, it will reference the bytes that were appended to the given buffer via + * the method [NSData dataWithBytesNoCopy:length:freeWhenDone:NO]. + * + * To read a line from the socket, use the line separator (e.g. CRLF for HTTP, see below) as the "data" parameter. + * If you're developing your own custom protocol, be sure your separator can not occur naturally as + * part of the data between separators. + * For example, imagine you want to send several small documents over a socket. + * Using CRLF as a separator is likely unwise, as a CRLF could easily exist within the documents. + * In this particular example, it would be better to use a protocol similar to HTTP with + * a header that includes the length of the document. + * Also be careful that your separator cannot occur naturally as part of the encoding for a character. + * + * The given data (separator) parameter should be immutable. + * For performance reasons, the socket will retain it, not copy it. + * So if it is immutable, don't modify it while the socket is using it. + **/ +- (void)readDataToData:(NSData *)data + withTimeout:(NSTimeInterval)timeout + buffer:(NSMutableData *)buffer + bufferOffset:(NSUInteger)offset + tag:(long)tag; + +/** + * Reads bytes until (and including) the passed "data" parameter, which acts as a separator. + * + * If the timeout value is negative, the read operation will not use a timeout. + * + * If maxLength is zero, no length restriction is enforced. + * Otherwise if maxLength bytes are read without completing the read, + * it is treated similarly to a timeout - the socket is closed with a GCDAsyncSocketReadMaxedOutError. + * The read will complete successfully if exactly maxLength bytes are read and the given data is found at the end. + * + * If you pass nil or zero-length data as the "data" parameter, + * the method will do nothing (except maybe print a warning), and the delegate will not be called. + * If you pass a maxLength parameter that is less than the length of the data parameter, + * the method will do nothing (except maybe print a warning), and the delegate will not be called. + * + * To read a line from the socket, use the line separator (e.g. CRLF for HTTP, see below) as the "data" parameter. + * If you're developing your own custom protocol, be sure your separator can not occur naturally as + * part of the data between separators. + * For example, imagine you want to send several small documents over a socket. + * Using CRLF as a separator is likely unwise, as a CRLF could easily exist within the documents. + * In this particular example, it would be better to use a protocol similar to HTTP with + * a header that includes the length of the document. + * Also be careful that your separator cannot occur naturally as part of the encoding for a character. + * + * The given data (separator) parameter should be immutable. + * For performance reasons, the socket will retain it, not copy it. + * So if it is immutable, don't modify it while the socket is using it. + **/ +- (void)readDataToData:(NSData *)data withTimeout:(NSTimeInterval)timeout maxLength:(NSUInteger)length tag:(long)tag; + +/** + * Reads bytes until (and including) the passed "data" parameter, which acts as a separator. + * The bytes will be appended to the given byte buffer starting at the given offset. + * The given buffer will automatically be increased in size if needed. + * + * If the timeout value is negative, the read operation will not use a timeout. + * If the buffer if nil, a buffer will automatically be created for you. + * + * If maxLength is zero, no length restriction is enforced. + * Otherwise if maxLength bytes are read without completing the read, + * it is treated similarly to a timeout - the socket is closed with a GCDAsyncSocketReadMaxedOutError. + * The read will complete successfully if exactly maxLength bytes are read and the given data is found at the end. + * + * If you pass a maxLength parameter that is less than the length of the data (separator) parameter, + * the method will do nothing (except maybe print a warning), and the delegate will not be called. + * If the bufferOffset is greater than the length of the given buffer, + * the method will do nothing (except maybe print a warning), and the delegate will not be called. + * + * If you pass a buffer, you must not alter it in any way while the socket is using it. + * After completion, the data returned in socket:didReadData:withTag: will be a subset of the given buffer. + * That is, it will reference the bytes that were appended to the given buffer via + * the method [NSData dataWithBytesNoCopy:length:freeWhenDone:NO]. + * + * To read a line from the socket, use the line separator (e.g. CRLF for HTTP, see below) as the "data" parameter. + * If you're developing your own custom protocol, be sure your separator can not occur naturally as + * part of the data between separators. + * For example, imagine you want to send several small documents over a socket. + * Using CRLF as a separator is likely unwise, as a CRLF could easily exist within the documents. + * In this particular example, it would be better to use a protocol similar to HTTP with + * a header that includes the length of the document. + * Also be careful that your separator cannot occur naturally as part of the encoding for a character. + * + * The given data (separator) parameter should be immutable. + * For performance reasons, the socket will retain it, not copy it. + * So if it is immutable, don't modify it while the socket is using it. + **/ +- (void)readDataToData:(NSData *)data + withTimeout:(NSTimeInterval)timeout + buffer:(NSMutableData *)buffer + bufferOffset:(NSUInteger)offset + maxLength:(NSUInteger)length + tag:(long)tag; + +/** + * Returns progress of the current read, from 0.0 to 1.0, or NaN if no current read (use isnan() to check). + * The parameters "tag", "done" and "total" will be filled in if they aren't NULL. + **/ +- (float)progressOfReadReturningTag:(long *)tagPtr bytesDone:(NSUInteger *)donePtr total:(NSUInteger *)totalPtr; + +#pragma mark Writing + +/** + * Writes data to the socket, and calls the delegate when finished. + * + * If you pass in nil or zero-length data, this method does nothing and the delegate will not be called. + * If the timeout value is negative, the write operation will not use a timeout. + * + * Thread-Safety Note: + * If the given data parameter is mutable (NSMutableData) then you MUST NOT alter the data while + * the socket is writing it. In other words, it's not safe to alter the data until after the delegate method + * socket:didWriteDataWithTag: is invoked signifying that this particular write operation has completed. + * This is due to the fact that GCDAsyncSocket does NOT copy the data. It simply retains it. + * This is for performance reasons. Often times, if NSMutableData is passed, it is because + * a request/response was built up in memory. Copying this data adds an unwanted/unneeded overhead. + * If you need to write data from an immutable buffer, and you need to alter the buffer before the socket + * completes writing the bytes (which is NOT immediately after this method returns, but rather at a later time + * when the delegate method notifies you), then you should first copy the bytes, and pass the copy to this method. + **/ +- (void)writeData:(NSData *)data withTimeout:(NSTimeInterval)timeout tag:(long)tag; + +/** + * Returns progress of the current write, from 0.0 to 1.0, or NaN if no current write (use isnan() to check). + * The parameters "tag", "done" and "total" will be filled in if they aren't NULL. + **/ +- (float)progressOfWriteReturningTag:(long *)tagPtr bytesDone:(NSUInteger *)donePtr total:(NSUInteger *)totalPtr; + +#pragma mark Security + +/** + * Secures the connection using SSL/TLS. + * + * This method may be called at any time, and the TLS handshake will occur after all pending reads and writes + * are finished. This allows one the option of sending a protocol dependent StartTLS message, and queuing + * the upgrade to TLS at the same time, without having to wait for the write to finish. + * Any reads or writes scheduled after this method is called will occur over the secured connection. + * + * ==== The available TOP-LEVEL KEYS are: + * + * - GCDAsyncSocketManuallyEvaluateTrust + * The value must be of type NSNumber, encapsulating a BOOL value. + * If you set this to YES, then the underlying SecureTransport system will not evaluate the SecTrustRef of the peer. + * Instead it will pause at the moment evaulation would typically occur, + * and allow us to handle the security evaluation however we see fit. + * So GCDAsyncSocket will invoke the delegate method socket:shouldTrustPeer: passing the SecTrustRef. + * + * Note that if you set this option, then all other configuration keys are ignored. + * Evaluation will be completely up to you during the socket:didReceiveTrust:completionHandler: delegate method. + * + * For more information on trust evaluation see: + * Apple's Technical Note TN2232 - HTTPS Server Trust Evaluation + * https://developer.apple.com/library/ios/technotes/tn2232/_index.html + * + * If unspecified, the default value is NO. + * + * - GCDAsyncSocketUseCFStreamForTLS (iOS only) + * The value must be of type NSNumber, encapsulating a BOOL value. + * By default GCDAsyncSocket will use the SecureTransport layer to perform encryption. + * This gives us more control over the security protocol (many more configuration options), + * plus it allows us to optimize things like sys calls and buffer allocation. + * + * However, if you absolutely must, you can instruct GCDAsyncSocket to use the old-fashioned encryption + * technique by going through the CFStream instead. So instead of using SecureTransport, GCDAsyncSocket + * will instead setup a CFRead/CFWriteStream. And then set the kCFStreamPropertySSLSettings property + * (via CFReadStreamSetProperty / CFWriteStreamSetProperty) and will pass the given options to this method. + * + * Thus all the other keys in the given dictionary will be ignored by GCDAsyncSocket, + * and will passed directly CFReadStreamSetProperty / CFWriteStreamSetProperty. + * For more infomation on these keys, please see the documentation for kCFStreamPropertySSLSettings. + * + * If unspecified, the default value is NO. + * + * ==== The available CONFIGURATION KEYS are: + * + * - kCFStreamSSLPeerName + * The value must be of type NSString. + * It should match the name in the X.509 certificate given by the remote party. + * See Apple's documentation for SSLSetPeerDomainName. + * + * - kCFStreamSSLCertificates + * The value must be of type NSArray. + * See Apple's documentation for SSLSetCertificate. + * + * - kCFStreamSSLIsServer + * The value must be of type NSNumber, encapsulationg a BOOL value. + * See Apple's documentation for SSLCreateContext for iOS. + * This is optional for iOS. If not supplied, a NO value is the default. + * This is not needed for Mac OS X, and the value is ignored. + * + * - GCDAsyncSocketSSLPeerID + * The value must be of type NSData. + * You must set this value if you want to use TLS session resumption. + * See Apple's documentation for SSLSetPeerID. + * + * - GCDAsyncSocketSSLProtocolVersionMin + * - GCDAsyncSocketSSLProtocolVersionMax + * The value(s) must be of type NSNumber, encapsulting a SSLProtocol value. + * See Apple's documentation for SSLSetProtocolVersionMin & SSLSetProtocolVersionMax. + * See also the SSLProtocol typedef. + * + * - GCDAsyncSocketSSLSessionOptionFalseStart + * The value must be of type NSNumber, encapsulating a BOOL value. + * See Apple's documentation for kSSLSessionOptionFalseStart. + * + * - GCDAsyncSocketSSLSessionOptionSendOneByteRecord + * The value must be of type NSNumber, encapsulating a BOOL value. + * See Apple's documentation for kSSLSessionOptionSendOneByteRecord. + * + * - GCDAsyncSocketSSLCipherSuites + * The values must be of type NSArray. + * Each item within the array must be a NSNumber, encapsulating + * See Apple's documentation for SSLSetEnabledCiphers. + * See also the SSLCipherSuite typedef. + * + * - GCDAsyncSocketSSLDiffieHellmanParameters (Mac OS X only) + * The value must be of type NSData. + * See Apple's documentation for SSLSetDiffieHellmanParams. + * + * ==== The following UNAVAILABLE KEYS are: (with throw an exception) + * + * - kCFStreamSSLAllowsAnyRoot (UNAVAILABLE) + * You MUST use manual trust evaluation instead (see GCDAsyncSocketManuallyEvaluateTrust). + * Corresponding deprecated method: SSLSetAllowsAnyRoot + * + * - kCFStreamSSLAllowsExpiredRoots (UNAVAILABLE) + * You MUST use manual trust evaluation instead (see GCDAsyncSocketManuallyEvaluateTrust). + * Corresponding deprecated method: SSLSetAllowsExpiredRoots + * + * - kCFStreamSSLAllowsExpiredCertificates (UNAVAILABLE) + * You MUST use manual trust evaluation instead (see GCDAsyncSocketManuallyEvaluateTrust). + * Corresponding deprecated method: SSLSetAllowsExpiredCerts + * + * - kCFStreamSSLValidatesCertificateChain (UNAVAILABLE) + * You MUST use manual trust evaluation instead (see GCDAsyncSocketManuallyEvaluateTrust). + * Corresponding deprecated method: SSLSetEnableCertVerify + * + * - kCFStreamSSLLevel (UNAVAILABLE) + * You MUST use GCDAsyncSocketSSLProtocolVersionMin & GCDAsyncSocketSSLProtocolVersionMin instead. + * Corresponding deprecated method: SSLSetProtocolVersionEnabled + * + * + * Please refer to Apple's documentation for corresponding SSLFunctions. + * + * If you pass in nil or an empty dictionary, the default settings will be used. + * + * IMPORTANT SECURITY NOTE: + * The default settings will check to make sure the remote party's certificate is signed by a + * trusted 3rd party certificate agency (e.g. verisign) and that the certificate is not expired. + * However it will not verify the name on the certificate unless you + * give it a name to verify against via the kCFStreamSSLPeerName key. + * The security implications of this are important to understand. + * Imagine you are attempting to create a secure connection to MySecureServer.com, + * but your socket gets directed to MaliciousServer.com because of a hacked DNS server. + * If you simply use the default settings, and MaliciousServer.com has a valid certificate, + * the default settings will not detect any problems since the certificate is valid. + * To properly secure your connection in this particular scenario you + * should set the kCFStreamSSLPeerName property to "MySecureServer.com". + * + * You can also perform additional validation in socketDidSecure. + **/ +- (void)startTLS:(NSDictionary *)tlsSettings; + +#pragma mark Advanced + +/** + * Traditionally sockets are not closed until the conversation is over. + * However, it is technically possible for the remote enpoint to close its write stream. + * Our socket would then be notified that there is no more data to be read, + * but our socket would still be writeable and the remote endpoint could continue to receive our data. + * + * The argument for this confusing functionality stems from the idea that a client could shut down its + * write stream after sending a request to the server, thus notifying the server there are to be no further requests. + * In practice, however, this technique did little to help server developers. + * + * To make matters worse, from a TCP perspective there is no way to tell the difference from a read stream close + * and a full socket close. They both result in the TCP stack receiving a FIN packet. The only way to tell + * is by continuing to write to the socket. If it was only a read stream close, then writes will continue to work. + * Otherwise an error will be occur shortly (when the remote end sends us a RST packet). + * + * In addition to the technical challenges and confusion, many high level socket/stream API's provide + * no support for dealing with the problem. If the read stream is closed, the API immediately declares the + * socket to be closed, and shuts down the write stream as well. In fact, this is what Apple's CFStream API does. + * It might sound like poor design at first, but in fact it simplifies development. + * + * The vast majority of the time if the read stream is closed it's because the remote endpoint closed its socket. + * Thus it actually makes sense to close the socket at this point. + * And in fact this is what most networking developers want and expect to happen. + * However, if you are writing a server that interacts with a plethora of clients, + * you might encounter a client that uses the discouraged technique of shutting down its write stream. + * If this is the case, you can set this property to NO, + * and make use of the socketDidCloseReadStream delegate method. + * + * The default value is YES. + **/ +@property (atomic, assign, readwrite) BOOL autoDisconnectOnClosedReadStream; + +/** + * GCDAsyncSocket maintains thread safety by using an internal serial dispatch_queue. + * In most cases, the instance creates this queue itself. + * However, to allow for maximum flexibility, the internal queue may be passed in the init method. + * This allows for some advanced options such as controlling socket priority via target queues. + * However, when one begins to use target queues like this, they open the door to some specific deadlock issues. + * + * For example, imagine there are 2 queues: + * dispatch_queue_t socketQueue; + * dispatch_queue_t socketTargetQueue; + * + * If you do this (pseudo-code): + * socketQueue.targetQueue = socketTargetQueue; + * + * Then all socketQueue operations will actually get run on the given socketTargetQueue. + * This is fine and works great in most situations. + * But if you run code directly from within the socketTargetQueue that accesses the socket, + * you could potentially get deadlock. Imagine the following code: + * + * - (BOOL)socketHasSomething + * { + * __block BOOL result = NO; + * dispatch_block_t block = ^{ + * result = [self someInternalMethodToBeRunOnlyOnSocketQueue]; + * } + * if (is_executing_on_queue(socketQueue)) + * block(); + * else + * dispatch_sync(socketQueue, block); + * + * return result; + * } + * + * What happens if you call this method from the socketTargetQueue? The result is deadlock. + * This is because the GCD API offers no mechanism to discover a queue's targetQueue. + * Thus we have no idea if our socketQueue is configured with a targetQueue. + * If we had this information, we could easily avoid deadlock. + * But, since these API's are missing or unfeasible, you'll have to explicitly set it. + * + * IF you pass a socketQueue via the init method, + * AND you've configured the passed socketQueue with a targetQueue, + * THEN you should pass the end queue in the target hierarchy. + * + * For example, consider the following queue hierarchy: + * socketQueue -> ipQueue -> moduleQueue + * + * This example demonstrates priority shaping within some server. + * All incoming client connections from the same IP address are executed on the same target queue. + * And all connections for a particular module are executed on the same target queue. + * Thus, the priority of all networking for the entire module can be changed on the fly. + * Additionally, networking traffic from a single IP cannot monopolize the module. + * + * Here's how you would accomplish something like that: + * - (dispatch_queue_t)newSocketQueueForConnectionFromAddress:(NSData *)address onSocket:(GCDAsyncSocket *)sock + * { + * dispatch_queue_t socketQueue = dispatch_queue_create("", NULL); + * dispatch_queue_t ipQueue = [self ipQueueForAddress:address]; + * + * dispatch_set_target_queue(socketQueue, ipQueue); + * dispatch_set_target_queue(iqQueue, moduleQueue); + * + * return socketQueue; + * } + * - (void)socket:(GCDAsyncSocket *)sock didAcceptNewSocket:(GCDAsyncSocket *)newSocket + * { + * [clientConnections addObject:newSocket]; + * [newSocket markSocketQueueTargetQueue:moduleQueue]; + * } + * + * Note: This workaround is ONLY needed if you intend to execute code directly on the ipQueue or moduleQueue. + * This is often NOT the case, as such queues are used solely for execution shaping. + **/ +- (void)markSocketQueueTargetQueue:(dispatch_queue_t)socketQueuesPreConfiguredTargetQueue; +- (void)unmarkSocketQueueTargetQueue:(dispatch_queue_t)socketQueuesPreviouslyConfiguredTargetQueue; + +/** + * It's not thread-safe to access certain variables from outside the socket's internal queue. + * + * For example, the socket file descriptor. + * File descriptors are simply integers which reference an index in the per-process file table. + * However, when one requests a new file descriptor (by opening a file or socket), + * the file descriptor returned is guaranteed to be the lowest numbered unused descriptor. + * So if we're not careful, the following could be possible: + * + * - Thread A invokes a method which returns the socket's file descriptor. + * - The socket is closed via the socket's internal queue on thread B. + * - Thread C opens a file, and subsequently receives the file descriptor that was previously the socket's FD. + * - Thread A is now accessing/altering the file instead of the socket. + * + * In addition to this, other variables are not actually objects, + * and thus cannot be retained/released or even autoreleased. + * An example is the sslContext, of type SSLContextRef, which is actually a malloc'd struct. + * + * Although there are internal variables that make it difficult to maintain thread-safety, + * it is important to provide access to these variables + * to ensure this class can be used in a wide array of environments. + * This method helps to accomplish this by invoking the current block on the socket's internal queue. + * The methods below can be invoked from within the block to access + * those generally thread-unsafe internal variables in a thread-safe manner. + * The given block will be invoked synchronously on the socket's internal queue. + * + * If you save references to any protected variables and use them outside the block, you do so at your own peril. + **/ +- (void)performBlock:(dispatch_block_t)block; + +/** + * These methods are only available from within the context of a performBlock: invocation. + * See the documentation for the performBlock: method above. + * + * Provides access to the socket's file descriptor(s). + * If the socket is a server socket (is accepting incoming connections), + * it might actually have multiple internal socket file descriptors - one for IPv4 and one for IPv6. + **/ +- (int)socketFD; +- (int)socket4FD; +- (int)socket6FD; + +#if TARGET_OS_IPHONE + +/** + * These methods are only available from within the context of a performBlock: invocation. + * See the documentation for the performBlock: method above. + * + * Provides access to the socket's internal CFReadStream/CFWriteStream. + * + * These streams are only used as workarounds for specific iOS shortcomings: + * + * - Apple has decided to keep the SecureTransport framework private is iOS. + * This means the only supplied way to do SSL/TLS is via CFStream or some other API layered on top of it. + * Thus, in order to provide SSL/TLS support on iOS we are forced to rely on CFStream, + * instead of the preferred and faster and more powerful SecureTransport. + * + * - If a socket doesn't have backgrounding enabled, and that socket is closed while the app is backgrounded, + * Apple only bothers to notify us via the CFStream API. + * The faster and more powerful GCD API isn't notified properly in this case. + * + * See also: (BOOL)enableBackgroundingOnSocket + **/ +- (CFReadStreamRef)readStream; +- (CFWriteStreamRef)writeStream; + +/** + * This method is only available from within the context of a performBlock: invocation. + * See the documentation for the performBlock: method above. + * + * Configures the socket to allow it to operate when the iOS application has been backgrounded. + * In other words, this method creates a read & write stream, and invokes: + * + * CFReadStreamSetProperty(readStream, kCFStreamNetworkServiceType, kCFStreamNetworkServiceTypeVoIP); + * CFWriteStreamSetProperty(writeStream, kCFStreamNetworkServiceType, kCFStreamNetworkServiceTypeVoIP); + * + * Returns YES if successful, NO otherwise. + * + * Note: Apple does not officially support backgrounding server sockets. + * That is, if your socket is accepting incoming connections, Apple does not officially support + * allowing iOS applications to accept incoming connections while an app is backgrounded. + * + * Example usage: + * + * - (void)socket:(GCDAsyncSocket *)sock didConnectToHost:(NSString *)host port:(uint16_t)port + * { + * [asyncSocket performBlock:^{ + * [asyncSocket enableBackgroundingOnSocket]; + * }]; + * } + **/ +- (BOOL)enableBackgroundingOnSocket; + +#endif + +/** + * This method is only available from within the context of a performBlock: invocation. + * See the documentation for the performBlock: method above. + * + * Provides access to the socket's SSLContext, if SSL/TLS has been started on the socket. + **/ +- (SSLContextRef)sslContext; + +#pragma mark Utilities + +/** + * The address lookup utility used by the class. + * This method is synchronous, so it's recommended you use it on a background thread/queue. + * + * The special strings "localhost" and "loopback" return the loopback address for IPv4 and IPv6. + * + * @returns + * A mutable array with all IPv4 and IPv6 addresses returned by getaddrinfo. + * The addresses are specifically for TCP connections. + * You can filter the addresses, if needed, using the other utility methods provided by the class. + **/ ++ (NSMutableArray *)lookupHost:(NSString *)host port:(uint16_t)port error:(NSError **)errPtr; + +/** + * Extracting host and port information from raw address data. + **/ + ++ (NSString *)hostFromAddress:(NSData *)address; ++ (uint16_t)portFromAddress:(NSData *)address; + ++ (BOOL)isIPv4Address:(NSData *)address; ++ (BOOL)isIPv6Address:(NSData *)address; + ++ (BOOL)getHost:(NSString **)hostPtr port:(uint16_t *)portPtr fromAddress:(NSData *)address; + ++ (BOOL)getHost:(NSString **)hostPtr port:(uint16_t *)portPtr family:(sa_family_t *)afPtr fromAddress:(NSData *)address; + +/** + * A few common line separators, for use with the readDataToData:... methods. + **/ ++ (NSData *)CRLFData; // 0x0D0A ++ (NSData *)CRData; // 0x0D ++ (NSData *)LFData; // 0x0A ++ (NSData *)ZeroData; // 0x00 + +@end + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +@protocol GCDAsyncSocketDelegate +@optional + +/** + * This method is called immediately prior to socket:didAcceptNewSocket:. + * It optionally allows a listening socket to specify the socketQueue for a new accepted socket. + * If this method is not implemented, or returns NULL, the new accepted socket will create its own default queue. + * + * Since you cannot autorelease a dispatch_queue, + * this method uses the "new" prefix in its name to specify that the returned queue has been retained. + * + * Thus you could do something like this in the implementation: + * return dispatch_queue_create("MyQueue", NULL); + * + * If you are placing multiple sockets on the same queue, + * then care should be taken to increment the retain count each time this method is invoked. + * + * For example, your implementation might look something like this: + * dispatch_retain(myExistingQueue); + * return myExistingQueue; + **/ +- (dispatch_queue_t)newSocketQueueForConnectionFromAddress:(NSData *)address onSocket:(GCDAsyncSocket *)sock; + +/** + * Called when a socket accepts a connection. + * Another socket is automatically spawned to handle it. + * + * You must retain the newSocket if you wish to handle the connection. + * Otherwise the newSocket instance will be released and the spawned connection will be closed. + * + * By default the new socket will have the same delegate and delegateQueue. + * You may, of course, change this at any time. + **/ +- (void)socket:(GCDAsyncSocket *)sock didAcceptNewSocket:(GCDAsyncSocket *)newSocket; + +/** + * Called when a socket connects and is ready for reading and writing. + * The host parameter will be an IP address, not a DNS name. + **/ +- (void)socket:(GCDAsyncSocket *)sock didConnectToHost:(NSString *)host port:(uint16_t)port; + +/** + * Called when a socket connects and is ready for reading and writing. + * The host parameter will be an IP address, not a DNS name. + **/ +- (void)socket:(GCDAsyncSocket *)sock didConnectToUrl:(NSURL *)url; + +/** + * Called when a socket has completed reading the requested data into memory. + * Not called if there is an error. + **/ +- (void)socket:(GCDAsyncSocket *)sock didReadData:(NSData *)data withTag:(long)tag; + +/** + * Called when a socket has read in data, but has not yet completed the read. + * This would occur if using readToData: or readToLength: methods. + * It may be used to for things such as updating progress bars. + **/ +- (void)socket:(GCDAsyncSocket *)sock didReadPartialDataOfLength:(NSUInteger)partialLength tag:(long)tag; + +/** + * Called when a socket has completed writing the requested data. Not called if there is an error. + **/ +- (void)socket:(GCDAsyncSocket *)sock didWriteDataWithTag:(long)tag; + +/** + * Called when a socket has written some data, but has not yet completed the entire write. + * It may be used to for things such as updating progress bars. + **/ +- (void)socket:(GCDAsyncSocket *)sock didWritePartialDataOfLength:(NSUInteger)partialLength tag:(long)tag; + +/** + * Called if a read operation has reached its timeout without completing. + * This method allows you to optionally extend the timeout. + * If you return a positive time interval (> 0) the read's timeout will be extended by the given amount. + * If you don't implement this method, or return a non-positive time interval (<= 0) the read will timeout as usual. + * + * The elapsed parameter is the sum of the original timeout, plus any additions previously added via this method. + * The length parameter is the number of bytes that have been read so far for the read operation. + * + * Note that this method may be called multiple times for a single read if you return positive numbers. + **/ +- (NSTimeInterval)socket:(GCDAsyncSocket *)sock shouldTimeoutReadWithTag:(long)tag + elapsed:(NSTimeInterval)elapsed + bytesDone:(NSUInteger)length; + +/** + * Called if a write operation has reached its timeout without completing. + * This method allows you to optionally extend the timeout. + * If you return a positive time interval (> 0) the write's timeout will be extended by the given amount. + * If you don't implement this method, or return a non-positive time interval (<= 0) the write will timeout as usual. + * + * The elapsed parameter is the sum of the original timeout, plus any additions previously added via this method. + * The length parameter is the number of bytes that have been written so far for the write operation. + * + * Note that this method may be called multiple times for a single write if you return positive numbers. + **/ +- (NSTimeInterval)socket:(GCDAsyncSocket *)sock shouldTimeoutWriteWithTag:(long)tag + elapsed:(NSTimeInterval)elapsed + bytesDone:(NSUInteger)length; + +/** + * Conditionally called if the read stream closes, but the write stream may still be writeable. + * + * This delegate method is only called if autoDisconnectOnClosedReadStream has been set to NO. + * See the discussion on the autoDisconnectOnClosedReadStream method for more information. + **/ +- (void)socketDidCloseReadStream:(GCDAsyncSocket *)sock; + +/** + * Called when a socket disconnects with or without error. + * + * If you call the disconnect method, and the socket wasn't already disconnected, + * then an invocation of this delegate method will be enqueued on the delegateQueue + * before the disconnect method returns. + * + * Note: If the GCDAsyncSocket instance is deallocated while it is still connected, + * and the delegate is not also deallocated, then this method will be invoked, + * but the sock parameter will be nil. (It must necessarily be nil since it is no longer available.) + * This is a generally rare, but is possible if one writes code like this: + * + * asyncSocket = nil; // I'm implicitly disconnecting the socket + * + * In this case it may preferrable to nil the delegate beforehand, like this: + * + * asyncSocket.delegate = nil; // Don't invoke my delegate method + * asyncSocket = nil; // I'm implicitly disconnecting the socket + * + * Of course, this depends on how your state machine is configured. + **/ +- (void)socketDidDisconnect:(GCDAsyncSocket *)sock withError:(NSError *)err; + +/** + * Called after the socket has successfully completed SSL/TLS negotiation. + * This method is not called unless you use the provided startTLS method. + * + * If a SSL/TLS negotiation fails (invalid certificate, etc) then the socket will immediately close, + * and the socketDidDisconnect:withError: delegate method will be called with the specific SSL error code. + **/ +- (void)socketDidSecure:(GCDAsyncSocket *)sock; + +/** + * Allows a socket delegate to hook into the TLS handshake and manually validate the peer it's connecting to. + * + * This is only called if startTLS is invoked with options that include: + * - GCDAsyncSocketManuallyEvaluateTrust == YES + * + * Typically the delegate will use SecTrustEvaluate (and related functions) to properly validate the peer. + * + * Note from Apple's documentation: + * Because [SecTrustEvaluate] might look on the network for certificates in the certificate chain, + * [it] might block while attempting network access. You should never call it from your main thread; + * call it only from within a function running on a dispatch queue or on a separate thread. + * + * Thus this method uses a completionHandler block rather than a normal return value. + * The completionHandler block is thread-safe, and may be invoked from a background queue/thread. + * It is safe to invoke the completionHandler block even if the socket has been closed. + **/ +- (void)socket:(GCDAsyncSocket *)sock didReceiveTrust:(SecTrustRef)trust +completionHandler:(void (^)(BOOL shouldTrustPeer))completionHandler; + +@end diff --git a/iOS/Hexiwear/CocoaMQTT/GCDAsyncSocket.m b/iOS/Hexiwear/CocoaMQTT/GCDAsyncSocket.m new file mode 100644 index 0000000..0ae5ec4 --- /dev/null +++ b/iOS/Hexiwear/CocoaMQTT/GCDAsyncSocket.m @@ -0,0 +1,8239 @@ +// +// GCDAsyncSocket.m +// +// This class is in the public domain. +// Originally created by Robbie Hanson in Q4 2010. +// Updated and maintained by Deusty LLC and the Apple development community. +// +// https://github.com/robbiehanson/CocoaAsyncSocket +// + +#import "GCDAsyncSocket.h" + +#if TARGET_OS_IPHONE +#import +#endif + +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import + +#if ! __has_feature(objc_arc) +#warning This file must be compiled with ARC. Use -fobjc-arc flag (or convert project to ARC). +// For more information see: https://github.com/robbiehanson/CocoaAsyncSocket/wiki/ARC +#endif + + +#ifndef GCDAsyncSocketLoggingEnabled +#define GCDAsyncSocketLoggingEnabled 0 +#endif + +#if GCDAsyncSocketLoggingEnabled + +// Logging Enabled - See log level below + +// Logging uses the CocoaLumberjack framework (which is also GCD based). +// https://github.com/robbiehanson/CocoaLumberjack +// +// It allows us to do a lot of logging without significantly slowing down the code. +#import "DDLog.h" + +#define LogAsync YES +#define LogContext GCDAsyncSocketLoggingContext + +#define LogObjc(flg, frmt, ...) LOG_OBJC_MAYBE(LogAsync, logLevel, flg, LogContext, frmt, ##__VA_ARGS__) +#define LogC(flg, frmt, ...) LOG_C_MAYBE(LogAsync, logLevel, flg, LogContext, frmt, ##__VA_ARGS__) + +#define LogError(frmt, ...) LogObjc(LOG_FLAG_ERROR, (@"%@: " frmt), THIS_FILE, ##__VA_ARGS__) +#define LogWarn(frmt, ...) LogObjc(LOG_FLAG_WARN, (@"%@: " frmt), THIS_FILE, ##__VA_ARGS__) +#define LogInfo(frmt, ...) LogObjc(LOG_FLAG_INFO, (@"%@: " frmt), THIS_FILE, ##__VA_ARGS__) +#define LogVerbose(frmt, ...) LogObjc(LOG_FLAG_VERBOSE, (@"%@: " frmt), THIS_FILE, ##__VA_ARGS__) + +#define LogCError(frmt, ...) LogC(LOG_FLAG_ERROR, (@"%@: " frmt), THIS_FILE, ##__VA_ARGS__) +#define LogCWarn(frmt, ...) LogC(LOG_FLAG_WARN, (@"%@: " frmt), THIS_FILE, ##__VA_ARGS__) +#define LogCInfo(frmt, ...) LogC(LOG_FLAG_INFO, (@"%@: " frmt), THIS_FILE, ##__VA_ARGS__) +#define LogCVerbose(frmt, ...) LogC(LOG_FLAG_VERBOSE, (@"%@: " frmt), THIS_FILE, ##__VA_ARGS__) + +#define LogTrace() LogObjc(LOG_FLAG_VERBOSE, @"%@: %@", THIS_FILE, THIS_METHOD) +#define LogCTrace() LogC(LOG_FLAG_VERBOSE, @"%@: %s", THIS_FILE, __FUNCTION__) + +#ifndef GCDAsyncSocketLogLevel +#define GCDAsyncSocketLogLevel LOG_LEVEL_VERBOSE +#endif + +// Log levels : off, error, warn, info, verbose +static const int logLevel = GCDAsyncSocketLogLevel; + +#else + +// Logging Disabled + +#define LogError(frmt, ...) {} +#define LogWarn(frmt, ...) {} +#define LogInfo(frmt, ...) {} +#define LogVerbose(frmt, ...) {} + +#define LogCError(frmt, ...) {} +#define LogCWarn(frmt, ...) {} +#define LogCInfo(frmt, ...) {} +#define LogCVerbose(frmt, ...) {} + +#define LogTrace() {} +#define LogCTrace(frmt, ...) {} + +#endif + +/** + * Seeing a return statements within an inner block + * can sometimes be mistaken for a return point of the enclosing method. + * This makes inline blocks a bit easier to read. + **/ +#define return_from_block return + +/** + * A socket file descriptor is really just an integer. + * It represents the index of the socket within the kernel. + * This makes invalid file descriptor comparisons easier to read. + **/ +#define SOCKET_NULL -1 + + +NSString *const GCDAsyncSocketException = @"GCDAsyncSocketException"; +NSString *const GCDAsyncSocketErrorDomain = @"GCDAsyncSocketErrorDomain"; + +NSString *const GCDAsyncSocketQueueName = @"GCDAsyncSocket"; +NSString *const GCDAsyncSocketThreadName = @"GCDAsyncSocket-CFStream"; + +NSString *const GCDAsyncSocketManuallyEvaluateTrust = @"GCDAsyncSocketManuallyEvaluateTrust"; +#if TARGET_OS_IPHONE +NSString *const GCDAsyncSocketUseCFStreamForTLS = @"GCDAsyncSocketUseCFStreamForTLS"; +#endif +NSString *const GCDAsyncSocketSSLPeerID = @"GCDAsyncSocketSSLPeerID"; +NSString *const GCDAsyncSocketSSLProtocolVersionMin = @"GCDAsyncSocketSSLProtocolVersionMin"; +NSString *const GCDAsyncSocketSSLProtocolVersionMax = @"GCDAsyncSocketSSLProtocolVersionMax"; +NSString *const GCDAsyncSocketSSLSessionOptionFalseStart = @"GCDAsyncSocketSSLSessionOptionFalseStart"; +NSString *const GCDAsyncSocketSSLSessionOptionSendOneByteRecord = @"GCDAsyncSocketSSLSessionOptionSendOneByteRecord"; +NSString *const GCDAsyncSocketSSLCipherSuites = @"GCDAsyncSocketSSLCipherSuites"; +#if !TARGET_OS_IPHONE +NSString *const GCDAsyncSocketSSLDiffieHellmanParameters = @"GCDAsyncSocketSSLDiffieHellmanParameters"; +#endif + +enum GCDAsyncSocketFlags +{ + kSocketStarted = 1 << 0, // If set, socket has been started (accepting/connecting) + kConnected = 1 << 1, // If set, the socket is connected + kForbidReadsWrites = 1 << 2, // If set, no new reads or writes are allowed + kReadsPaused = 1 << 3, // If set, reads are paused due to possible timeout + kWritesPaused = 1 << 4, // If set, writes are paused due to possible timeout + kDisconnectAfterReads = 1 << 5, // If set, disconnect after no more reads are queued + kDisconnectAfterWrites = 1 << 6, // If set, disconnect after no more writes are queued + kSocketCanAcceptBytes = 1 << 7, // If set, we know socket can accept bytes. If unset, it's unknown. + kReadSourceSuspended = 1 << 8, // If set, the read source is suspended + kWriteSourceSuspended = 1 << 9, // If set, the write source is suspended + kQueuedTLS = 1 << 10, // If set, we've queued an upgrade to TLS + kStartingReadTLS = 1 << 11, // If set, we're waiting for TLS negotiation to complete + kStartingWriteTLS = 1 << 12, // If set, we're waiting for TLS negotiation to complete + kSocketSecure = 1 << 13, // If set, socket is using secure communication via SSL/TLS + kSocketHasReadEOF = 1 << 14, // If set, we have read EOF from socket + kReadStreamClosed = 1 << 15, // If set, we've read EOF plus prebuffer has been drained + kDealloc = 1 << 16, // If set, the socket is being deallocated +#if TARGET_OS_IPHONE + kAddedStreamsToRunLoop = 1 << 17, // If set, CFStreams have been added to listener thread + kUsingCFStreamForTLS = 1 << 18, // If set, we're forced to use CFStream instead of SecureTransport + kSecureSocketHasBytesAvailable = 1 << 19, // If set, CFReadStream has notified us of bytes available +#endif +}; + +enum GCDAsyncSocketConfig +{ + kIPv4Disabled = 1 << 0, // If set, IPv4 is disabled + kIPv6Disabled = 1 << 1, // If set, IPv6 is disabled + kPreferIPv6 = 1 << 2, // If set, IPv6 is preferred over IPv4 + kAllowHalfDuplexConnection = 1 << 3, // If set, the socket will stay open even if the read stream closes +}; + +#if TARGET_OS_IPHONE +static NSThread *cfstreamThread; // Used for CFStreams + + +static uint64_t cfstreamThreadRetainCount; // setup & teardown +static dispatch_queue_t cfstreamThreadSetupQueue; // setup & teardown +#endif + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +/** + * A PreBuffer is used when there is more data available on the socket + * than is being requested by current read request. + * In this case we slurp up all data from the socket (to minimize sys calls), + * and store additional yet unread data in a "prebuffer". + * + * The prebuffer is entirely drained before we read from the socket again. + * In other words, a large chunk of data is written is written to the prebuffer. + * The prebuffer is then drained via a series of one or more reads (for subsequent read request(s)). + * + * A ring buffer was once used for this purpose. + * But a ring buffer takes up twice as much memory as needed (double the size for mirroring). + * In fact, it generally takes up more than twice the needed size as everything has to be rounded up to vm_page_size. + * And since the prebuffer is always completely drained after being written to, a full ring buffer isn't needed. + * + * The current design is very simple and straight-forward, while also keeping memory requirements lower. + **/ + +@interface GCDAsyncSocketPreBuffer : NSObject +{ + uint8_t *preBuffer; + size_t preBufferSize; + + uint8_t *readPointer; + uint8_t *writePointer; +} + +- (id)initWithCapacity:(size_t)numBytes; + +- (void)ensureCapacityForWrite:(size_t)numBytes; + +- (size_t)availableBytes; +- (uint8_t *)readBuffer; + +- (void)getReadBuffer:(uint8_t **)bufferPtr availableBytes:(size_t *)availableBytesPtr; + +- (size_t)availableSpace; +- (uint8_t *)writeBuffer; + +- (void)getWriteBuffer:(uint8_t **)bufferPtr availableSpace:(size_t *)availableSpacePtr; + +- (void)didRead:(size_t)bytesRead; +- (void)didWrite:(size_t)bytesWritten; + +- (void)reset; + +@end + +@implementation GCDAsyncSocketPreBuffer + +- (id)initWithCapacity:(size_t)numBytes +{ + if ((self = [super init])) + { + preBufferSize = numBytes; + preBuffer = malloc(preBufferSize); + + readPointer = preBuffer; + writePointer = preBuffer; + } + return self; +} + +- (void)dealloc +{ + if (preBuffer) + free(preBuffer); +} + +- (void)ensureCapacityForWrite:(size_t)numBytes +{ + size_t availableSpace = [self availableSpace]; + + if (numBytes > availableSpace) + { + size_t additionalBytes = numBytes - availableSpace; + + size_t newPreBufferSize = preBufferSize + additionalBytes; + uint8_t *newPreBuffer = realloc(preBuffer, newPreBufferSize); + + size_t readPointerOffset = readPointer - preBuffer; + size_t writePointerOffset = writePointer - preBuffer; + + preBuffer = newPreBuffer; + preBufferSize = newPreBufferSize; + + readPointer = preBuffer + readPointerOffset; + writePointer = preBuffer + writePointerOffset; + } +} + +- (size_t)availableBytes +{ + return writePointer - readPointer; +} + +- (uint8_t *)readBuffer +{ + return readPointer; +} + +- (void)getReadBuffer:(uint8_t **)bufferPtr availableBytes:(size_t *)availableBytesPtr +{ + if (bufferPtr) *bufferPtr = readPointer; + if (availableBytesPtr) *availableBytesPtr = [self availableBytes]; +} + +- (void)didRead:(size_t)bytesRead +{ + readPointer += bytesRead; + + if (readPointer == writePointer) + { + // The prebuffer has been drained. Reset pointers. + readPointer = preBuffer; + writePointer = preBuffer; + } +} + +- (size_t)availableSpace +{ + return preBufferSize - (writePointer - preBuffer); +} + +- (uint8_t *)writeBuffer +{ + return writePointer; +} + +- (void)getWriteBuffer:(uint8_t **)bufferPtr availableSpace:(size_t *)availableSpacePtr +{ + if (bufferPtr) *bufferPtr = writePointer; + if (availableSpacePtr) *availableSpacePtr = [self availableSpace]; +} + +- (void)didWrite:(size_t)bytesWritten +{ + writePointer += bytesWritten; +} + +- (void)reset +{ + readPointer = preBuffer; + writePointer = preBuffer; +} + +@end + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +/** + * The GCDAsyncReadPacket encompasses the instructions for any given read. + * The content of a read packet allows the code to determine if we're: + * - reading to a certain length + * - reading to a certain separator + * - or simply reading the first chunk of available data + **/ +@interface GCDAsyncReadPacket : NSObject +{ +@public + NSMutableData *buffer; + NSUInteger startOffset; + NSUInteger bytesDone; + NSUInteger maxLength; + NSTimeInterval timeout; + NSUInteger readLength; + NSData *term; + BOOL bufferOwner; + NSUInteger originalBufferLength; + long tag; +} +- (id)initWithData:(NSMutableData *)d + startOffset:(NSUInteger)s + maxLength:(NSUInteger)m + timeout:(NSTimeInterval)t + readLength:(NSUInteger)l + terminator:(NSData *)e + tag:(long)i; + +- (void)ensureCapacityForAdditionalDataOfLength:(NSUInteger)bytesToRead; + +- (NSUInteger)optimalReadLengthWithDefault:(NSUInteger)defaultValue shouldPreBuffer:(BOOL *)shouldPreBufferPtr; + +- (NSUInteger)readLengthForNonTermWithHint:(NSUInteger)bytesAvailable; +- (NSUInteger)readLengthForTermWithHint:(NSUInteger)bytesAvailable shouldPreBuffer:(BOOL *)shouldPreBufferPtr; +- (NSUInteger)readLengthForTermWithPreBuffer:(GCDAsyncSocketPreBuffer *)preBuffer found:(BOOL *)foundPtr; + +- (NSInteger)searchForTermAfterPreBuffering:(ssize_t)numBytes; + +@end + +@implementation GCDAsyncReadPacket + +- (id)initWithData:(NSMutableData *)d + startOffset:(NSUInteger)s + maxLength:(NSUInteger)m + timeout:(NSTimeInterval)t + readLength:(NSUInteger)l + terminator:(NSData *)e + tag:(long)i +{ + if((self = [super init])) + { + bytesDone = 0; + maxLength = m; + timeout = t; + readLength = l; + term = [e copy]; + tag = i; + + if (d) + { + buffer = d; + startOffset = s; + bufferOwner = NO; + originalBufferLength = [d length]; + } + else + { + if (readLength > 0) + buffer = [[NSMutableData alloc] initWithLength:readLength]; + else + buffer = [[NSMutableData alloc] initWithLength:0]; + + startOffset = 0; + bufferOwner = YES; + originalBufferLength = 0; + } + } + return self; +} + +/** + * Increases the length of the buffer (if needed) to ensure a read of the given size will fit. + **/ +- (void)ensureCapacityForAdditionalDataOfLength:(NSUInteger)bytesToRead +{ + NSUInteger buffSize = [buffer length]; + NSUInteger buffUsed = startOffset + bytesDone; + + NSUInteger buffSpace = buffSize - buffUsed; + + if (bytesToRead > buffSpace) + { + NSUInteger buffInc = bytesToRead - buffSpace; + + [buffer increaseLengthBy:buffInc]; + } +} + +/** + * This method is used when we do NOT know how much data is available to be read from the socket. + * This method returns the default value unless it exceeds the specified readLength or maxLength. + * + * Furthermore, the shouldPreBuffer decision is based upon the packet type, + * and whether the returned value would fit in the current buffer without requiring a resize of the buffer. + **/ +- (NSUInteger)optimalReadLengthWithDefault:(NSUInteger)defaultValue shouldPreBuffer:(BOOL *)shouldPreBufferPtr +{ + NSUInteger result; + + if (readLength > 0) + { + // Read a specific length of data + + result = MIN(defaultValue, (readLength - bytesDone)); + + // There is no need to prebuffer since we know exactly how much data we need to read. + // Even if the buffer isn't currently big enough to fit this amount of data, + // it would have to be resized eventually anyway. + + if (shouldPreBufferPtr) + *shouldPreBufferPtr = NO; + } + else + { + // Either reading until we find a specified terminator, + // or we're simply reading all available data. + // + // In other words, one of: + // + // - readDataToData packet + // - readDataWithTimeout packet + + if (maxLength > 0) + result = MIN(defaultValue, (maxLength - bytesDone)); + else + result = defaultValue; + + // Since we don't know the size of the read in advance, + // the shouldPreBuffer decision is based upon whether the returned value would fit + // in the current buffer without requiring a resize of the buffer. + // + // This is because, in all likelyhood, the amount read from the socket will be less than the default value. + // Thus we should avoid over-allocating the read buffer when we can simply use the pre-buffer instead. + + if (shouldPreBufferPtr) + { + NSUInteger buffSize = [buffer length]; + NSUInteger buffUsed = startOffset + bytesDone; + + NSUInteger buffSpace = buffSize - buffUsed; + + if (buffSpace >= result) + *shouldPreBufferPtr = NO; + else + *shouldPreBufferPtr = YES; + } + } + + return result; +} + +/** + * For read packets without a set terminator, returns the amount of data + * that can be read without exceeding the readLength or maxLength. + * + * The given parameter indicates the number of bytes estimated to be available on the socket, + * which is taken into consideration during the calculation. + * + * The given hint MUST be greater than zero. + **/ +- (NSUInteger)readLengthForNonTermWithHint:(NSUInteger)bytesAvailable +{ + NSAssert(term == nil, @"This method does not apply to term reads"); + NSAssert(bytesAvailable > 0, @"Invalid parameter: bytesAvailable"); + + if (readLength > 0) + { + // Read a specific length of data + + return MIN(bytesAvailable, (readLength - bytesDone)); + + // No need to avoid resizing the buffer. + // If the user provided their own buffer, + // and told us to read a certain length of data that exceeds the size of the buffer, + // then it is clear that our code will resize the buffer during the read operation. + // + // This method does not actually do any resizing. + // The resizing will happen elsewhere if needed. + } + else + { + // Read all available data + + NSUInteger result = bytesAvailable; + + if (maxLength > 0) + { + result = MIN(result, (maxLength - bytesDone)); + } + + // No need to avoid resizing the buffer. + // If the user provided their own buffer, + // and told us to read all available data without giving us a maxLength, + // then it is clear that our code might resize the buffer during the read operation. + // + // This method does not actually do any resizing. + // The resizing will happen elsewhere if needed. + + return result; + } +} + +/** + * For read packets with a set terminator, returns the amount of data + * that can be read without exceeding the maxLength. + * + * The given parameter indicates the number of bytes estimated to be available on the socket, + * which is taken into consideration during the calculation. + * + * To optimize memory allocations, mem copies, and mem moves + * the shouldPreBuffer boolean value will indicate if the data should be read into a prebuffer first, + * or if the data can be read directly into the read packet's buffer. + **/ +- (NSUInteger)readLengthForTermWithHint:(NSUInteger)bytesAvailable shouldPreBuffer:(BOOL *)shouldPreBufferPtr +{ + NSAssert(term != nil, @"This method does not apply to non-term reads"); + NSAssert(bytesAvailable > 0, @"Invalid parameter: bytesAvailable"); + + + NSUInteger result = bytesAvailable; + + if (maxLength > 0) + { + result = MIN(result, (maxLength - bytesDone)); + } + + // Should the data be read into the read packet's buffer, or into a pre-buffer first? + // + // One would imagine the preferred option is the faster one. + // So which one is faster? + // + // Reading directly into the packet's buffer requires: + // 1. Possibly resizing packet buffer (malloc/realloc) + // 2. Filling buffer (read) + // 3. Searching for term (memcmp) + // 4. Possibly copying overflow into prebuffer (malloc/realloc, memcpy) + // + // Reading into prebuffer first: + // 1. Possibly resizing prebuffer (malloc/realloc) + // 2. Filling buffer (read) + // 3. Searching for term (memcmp) + // 4. Copying underflow into packet buffer (malloc/realloc, memcpy) + // 5. Removing underflow from prebuffer (memmove) + // + // Comparing the performance of the two we can see that reading + // data into the prebuffer first is slower due to the extra memove. + // + // However: + // The implementation of NSMutableData is open source via core foundation's CFMutableData. + // Decreasing the length of a mutable data object doesn't cause a realloc. + // In other words, the capacity of a mutable data object can grow, but doesn't shrink. + // + // This means the prebuffer will rarely need a realloc. + // The packet buffer, on the other hand, may often need a realloc. + // This is especially true if we are the buffer owner. + // Furthermore, if we are constantly realloc'ing the packet buffer, + // and then moving the overflow into the prebuffer, + // then we're consistently over-allocating memory for each term read. + // And now we get into a bit of a tradeoff between speed and memory utilization. + // + // The end result is that the two perform very similarly. + // And we can answer the original question very simply by another means. + // + // If we can read all the data directly into the packet's buffer without resizing it first, + // then we do so. Otherwise we use the prebuffer. + + if (shouldPreBufferPtr) + { + NSUInteger buffSize = [buffer length]; + NSUInteger buffUsed = startOffset + bytesDone; + + if ((buffSize - buffUsed) >= result) + *shouldPreBufferPtr = NO; + else + *shouldPreBufferPtr = YES; + } + + return result; +} + +/** + * For read packets with a set terminator, + * returns the amount of data that can be read from the given preBuffer, + * without going over a terminator or the maxLength. + * + * It is assumed the terminator has not already been read. + **/ +- (NSUInteger)readLengthForTermWithPreBuffer:(GCDAsyncSocketPreBuffer *)preBuffer found:(BOOL *)foundPtr +{ + NSAssert(term != nil, @"This method does not apply to non-term reads"); + NSAssert([preBuffer availableBytes] > 0, @"Invoked with empty pre buffer!"); + + // We know that the terminator, as a whole, doesn't exist in our own buffer. + // But it is possible that a _portion_ of it exists in our buffer. + // So we're going to look for the terminator starting with a portion of our own buffer. + // + // Example: + // + // term length = 3 bytes + // bytesDone = 5 bytes + // preBuffer length = 5 bytes + // + // If we append the preBuffer to our buffer, + // it would look like this: + // + // --------------------- + // |B|B|B|B|B|P|P|P|P|P| + // --------------------- + // + // So we start our search here: + // + // --------------------- + // |B|B|B|B|B|P|P|P|P|P| + // -------^-^-^--------- + // + // And move forwards... + // + // --------------------- + // |B|B|B|B|B|P|P|P|P|P| + // ---------^-^-^------- + // + // Until we find the terminator or reach the end. + // + // --------------------- + // |B|B|B|B|B|P|P|P|P|P| + // ---------------^-^-^- + + BOOL found = NO; + + NSUInteger termLength = [term length]; + NSUInteger preBufferLength = [preBuffer availableBytes]; + + if ((bytesDone + preBufferLength) < termLength) + { + // Not enough data for a full term sequence yet + return preBufferLength; + } + + NSUInteger maxPreBufferLength; + if (maxLength > 0) { + maxPreBufferLength = MIN(preBufferLength, (maxLength - bytesDone)); + + // Note: maxLength >= termLength + } + else { + maxPreBufferLength = preBufferLength; + } + + uint8_t seq[termLength]; + const void *termBuf = [term bytes]; + + NSUInteger bufLen = MIN(bytesDone, (termLength - 1)); + uint8_t *buf = (uint8_t *)[buffer mutableBytes] + startOffset + bytesDone - bufLen; + + NSUInteger preLen = termLength - bufLen; + const uint8_t *pre = [preBuffer readBuffer]; + + NSUInteger loopCount = bufLen + maxPreBufferLength - termLength + 1; // Plus one. See example above. + + NSUInteger result = maxPreBufferLength; + + NSUInteger i; + for (i = 0; i < loopCount; i++) + { + if (bufLen > 0) + { + // Combining bytes from buffer and preBuffer + + memcpy(seq, buf, bufLen); + memcpy(seq + bufLen, pre, preLen); + + if (memcmp(seq, termBuf, termLength) == 0) + { + result = preLen; + found = YES; + break; + } + + buf++; + bufLen--; + preLen++; + } + else + { + // Comparing directly from preBuffer + + if (memcmp(pre, termBuf, termLength) == 0) + { + NSUInteger preOffset = pre - [preBuffer readBuffer]; // pointer arithmetic + + result = preOffset + termLength; + found = YES; + break; + } + + pre++; + } + } + + // There is no need to avoid resizing the buffer in this particular situation. + + if (foundPtr) *foundPtr = found; + return result; +} + +/** + * For read packets with a set terminator, scans the packet buffer for the term. + * It is assumed the terminator had not been fully read prior to the new bytes. + * + * If the term is found, the number of excess bytes after the term are returned. + * If the term is not found, this method will return -1. + * + * Note: A return value of zero means the term was found at the very end. + * + * Prerequisites: + * The given number of bytes have been added to the end of our buffer. + * Our bytesDone variable has NOT been changed due to the prebuffered bytes. + **/ +- (NSInteger)searchForTermAfterPreBuffering:(ssize_t)numBytes +{ + NSAssert(term != nil, @"This method does not apply to non-term reads"); + + // The implementation of this method is very similar to the above method. + // See the above method for a discussion of the algorithm used here. + + uint8_t *buff = [buffer mutableBytes]; + NSUInteger buffLength = bytesDone + numBytes; + + const void *termBuff = [term bytes]; + NSUInteger termLength = [term length]; + + // Note: We are dealing with unsigned integers, + // so make sure the math doesn't go below zero. + + NSUInteger i = ((buffLength - numBytes) >= termLength) ? (buffLength - numBytes - termLength + 1) : 0; + + while (i + termLength <= buffLength) + { + uint8_t *subBuffer = buff + startOffset + i; + + if (memcmp(subBuffer, termBuff, termLength) == 0) + { + return buffLength - (i + termLength); + } + + i++; + } + + return -1; +} + + +@end + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +/** + * The GCDAsyncWritePacket encompasses the instructions for any given write. + **/ +@interface GCDAsyncWritePacket : NSObject +{ +@public + NSData *buffer; + NSUInteger bytesDone; + long tag; + NSTimeInterval timeout; +} +- (id)initWithData:(NSData *)d timeout:(NSTimeInterval)t tag:(long)i; +@end + +@implementation GCDAsyncWritePacket + +- (id)initWithData:(NSData *)d timeout:(NSTimeInterval)t tag:(long)i +{ + if((self = [super init])) + { + buffer = d; // Retain not copy. For performance as documented in header file. + bytesDone = 0; + timeout = t; + tag = i; + } + return self; +} + + +@end + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +/** + * The GCDAsyncSpecialPacket encompasses special instructions for interruptions in the read/write queues. + * This class my be altered to support more than just TLS in the future. + **/ +@interface GCDAsyncSpecialPacket : NSObject +{ +@public + NSDictionary *tlsSettings; +} +- (id)initWithTLSSettings:(NSDictionary *)settings; +@end + +@implementation GCDAsyncSpecialPacket + +- (id)initWithTLSSettings:(NSDictionary *)settings +{ + if((self = [super init])) + { + tlsSettings = [settings copy]; + } + return self; +} + + +@end + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +@implementation GCDAsyncSocket +{ + uint32_t flags; + uint16_t config; + + __weak id delegate; + dispatch_queue_t delegateQueue; + + int socket4FD; + int socket6FD; + int socketUN; + NSURL *socketUrl; + int stateIndex; + NSData * connectInterface4; + NSData * connectInterface6; + NSData * connectInterfaceUN; + + dispatch_queue_t socketQueue; + + dispatch_source_t accept4Source; + dispatch_source_t accept6Source; + dispatch_source_t acceptUNSource; + dispatch_source_t connectTimer; + dispatch_source_t readSource; + dispatch_source_t writeSource; + dispatch_source_t readTimer; + dispatch_source_t writeTimer; + + NSMutableArray *readQueue; + NSMutableArray *writeQueue; + + GCDAsyncReadPacket *currentRead; + GCDAsyncWritePacket *currentWrite; + + unsigned long socketFDBytesAvailable; + + GCDAsyncSocketPreBuffer *preBuffer; + +#if TARGET_OS_IPHONE + CFStreamClientContext streamContext; + CFReadStreamRef readStream; + CFWriteStreamRef writeStream; +#endif + SSLContextRef sslContext; + GCDAsyncSocketPreBuffer *sslPreBuffer; + size_t sslWriteCachedLength; + OSStatus sslErrCode; + OSStatus lastSSLHandshakeError; + + void *IsOnSocketQueueOrTargetQueueKey; + + id userData; +} + +- (id)init +{ + return [self initWithDelegate:nil delegateQueue:NULL socketQueue:NULL]; +} + +- (id)initWithSocketQueue:(dispatch_queue_t)sq +{ + return [self initWithDelegate:nil delegateQueue:NULL socketQueue:sq]; +} + +- (id)initWithDelegate:(id)aDelegate delegateQueue:(dispatch_queue_t)dq +{ + return [self initWithDelegate:aDelegate delegateQueue:dq socketQueue:NULL]; +} + +- (id)initWithDelegate:(id)aDelegate delegateQueue:(dispatch_queue_t)dq socketQueue:(dispatch_queue_t)sq +{ + if((self = [super init])) + { + delegate = aDelegate; + delegateQueue = dq; + +#if !OS_OBJECT_USE_OBJC + if (dq) dispatch_retain(dq); +#endif + + socket4FD = SOCKET_NULL; + socket6FD = SOCKET_NULL; + socketUN = SOCKET_NULL; + socketUrl = nil; + stateIndex = 0; + + if (sq) + { + NSAssert(sq != dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0), + @"The given socketQueue parameter must not be a concurrent queue."); + NSAssert(sq != dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), + @"The given socketQueue parameter must not be a concurrent queue."); + NSAssert(sq != dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), + @"The given socketQueue parameter must not be a concurrent queue."); + + socketQueue = sq; +#if !OS_OBJECT_USE_OBJC + dispatch_retain(sq); +#endif + } + else + { + socketQueue = dispatch_queue_create([GCDAsyncSocketQueueName UTF8String], NULL); + } + + // The dispatch_queue_set_specific() and dispatch_get_specific() functions take a "void *key" parameter. + // From the documentation: + // + // > Keys are only compared as pointers and are never dereferenced. + // > Thus, you can use a pointer to a static variable for a specific subsystem or + // > any other value that allows you to identify the value uniquely. + // + // We're just going to use the memory address of an ivar. + // Specifically an ivar that is explicitly named for our purpose to make the code more readable. + // + // However, it feels tedious (and less readable) to include the "&" all the time: + // dispatch_get_specific(&IsOnSocketQueueOrTargetQueueKey) + // + // So we're going to make it so it doesn't matter if we use the '&' or not, + // by assigning the value of the ivar to the address of the ivar. + // Thus: IsOnSocketQueueOrTargetQueueKey == &IsOnSocketQueueOrTargetQueueKey; + + IsOnSocketQueueOrTargetQueueKey = &IsOnSocketQueueOrTargetQueueKey; + + void *nonNullUnusedPointer = (__bridge void *)self; + dispatch_queue_set_specific(socketQueue, IsOnSocketQueueOrTargetQueueKey, nonNullUnusedPointer, NULL); + + readQueue = [[NSMutableArray alloc] initWithCapacity:5]; + currentRead = nil; + + writeQueue = [[NSMutableArray alloc] initWithCapacity:5]; + currentWrite = nil; + + preBuffer = [[GCDAsyncSocketPreBuffer alloc] initWithCapacity:(1024 * 4)]; + } + return self; +} + +- (void)dealloc +{ + LogInfo(@"%@ - %@ (start)", THIS_METHOD, self); + + // Set dealloc flag. + // This is used by closeWithError to ensure we don't accidentally retain ourself. + flags |= kDealloc; + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + { + [self closeWithError:nil]; + } + else + { + dispatch_sync(socketQueue, ^{ + [self closeWithError:nil]; + }); + } + + delegate = nil; + +#if !OS_OBJECT_USE_OBJC + if (delegateQueue) dispatch_release(delegateQueue); +#endif + delegateQueue = NULL; + +#if !OS_OBJECT_USE_OBJC + if (socketQueue) dispatch_release(socketQueue); +#endif + socketQueue = NULL; + + LogInfo(@"%@ - %@ (finish)", THIS_METHOD, self); +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Configuration +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (id)delegate +{ + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + { + return delegate; + } + else + { + __block id result; + + dispatch_sync(socketQueue, ^{ + result = delegate; + }); + + return result; + } +} + +- (void)setDelegate:(id)newDelegate synchronously:(BOOL)synchronously +{ + dispatch_block_t block = ^{ + delegate = newDelegate; + }; + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) { + block(); + } + else { + if (synchronously) + dispatch_sync(socketQueue, block); + else + dispatch_async(socketQueue, block); + } +} + +- (void)setDelegate:(id)newDelegate +{ + [self setDelegate:newDelegate synchronously:NO]; +} + +- (void)synchronouslySetDelegate:(id)newDelegate +{ + [self setDelegate:newDelegate synchronously:YES]; +} + +- (dispatch_queue_t)delegateQueue +{ + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + { + return delegateQueue; + } + else + { + __block dispatch_queue_t result; + + dispatch_sync(socketQueue, ^{ + result = delegateQueue; + }); + + return result; + } +} + +- (void)setDelegateQueue:(dispatch_queue_t)newDelegateQueue synchronously:(BOOL)synchronously +{ + dispatch_block_t block = ^{ + +#if !OS_OBJECT_USE_OBJC + if (delegateQueue) dispatch_release(delegateQueue); + if (newDelegateQueue) dispatch_retain(newDelegateQueue); +#endif + + delegateQueue = newDelegateQueue; + }; + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) { + block(); + } + else { + if (synchronously) + dispatch_sync(socketQueue, block); + else + dispatch_async(socketQueue, block); + } +} + +- (void)setDelegateQueue:(dispatch_queue_t)newDelegateQueue +{ + [self setDelegateQueue:newDelegateQueue synchronously:NO]; +} + +- (void)synchronouslySetDelegateQueue:(dispatch_queue_t)newDelegateQueue +{ + [self setDelegateQueue:newDelegateQueue synchronously:YES]; +} + +- (void)getDelegate:(id *)delegatePtr delegateQueue:(dispatch_queue_t *)delegateQueuePtr +{ + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + { + if (delegatePtr) *delegatePtr = delegate; + if (delegateQueuePtr) *delegateQueuePtr = delegateQueue; + } + else + { + __block id dPtr = NULL; + __block dispatch_queue_t dqPtr = NULL; + + dispatch_sync(socketQueue, ^{ + dPtr = delegate; + dqPtr = delegateQueue; + }); + + if (delegatePtr) *delegatePtr = dPtr; + if (delegateQueuePtr) *delegateQueuePtr = dqPtr; + } +} + +- (void)setDelegate:(id)newDelegate delegateQueue:(dispatch_queue_t)newDelegateQueue synchronously:(BOOL)synchronously +{ + dispatch_block_t block = ^{ + + delegate = newDelegate; + +#if !OS_OBJECT_USE_OBJC + if (delegateQueue) dispatch_release(delegateQueue); + if (newDelegateQueue) dispatch_retain(newDelegateQueue); +#endif + + delegateQueue = newDelegateQueue; + }; + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) { + block(); + } + else { + if (synchronously) + dispatch_sync(socketQueue, block); + else + dispatch_async(socketQueue, block); + } +} + +- (void)setDelegate:(id)newDelegate delegateQueue:(dispatch_queue_t)newDelegateQueue +{ + [self setDelegate:newDelegate delegateQueue:newDelegateQueue synchronously:NO]; +} + +- (void)synchronouslySetDelegate:(id)newDelegate delegateQueue:(dispatch_queue_t)newDelegateQueue +{ + [self setDelegate:newDelegate delegateQueue:newDelegateQueue synchronously:YES]; +} + +- (BOOL)isIPv4Enabled +{ + // Note: YES means kIPv4Disabled is OFF + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + { + return ((config & kIPv4Disabled) == 0); + } + else + { + __block BOOL result; + + dispatch_sync(socketQueue, ^{ + result = ((config & kIPv4Disabled) == 0); + }); + + return result; + } +} + +- (void)setIPv4Enabled:(BOOL)flag +{ + // Note: YES means kIPv4Disabled is OFF + + dispatch_block_t block = ^{ + + if (flag) + config &= ~kIPv4Disabled; + else + config |= kIPv4Disabled; + }; + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + block(); + else + dispatch_async(socketQueue, block); +} + +- (BOOL)isIPv6Enabled +{ + // Note: YES means kIPv6Disabled is OFF + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + { + return ((config & kIPv6Disabled) == 0); + } + else + { + __block BOOL result; + + dispatch_sync(socketQueue, ^{ + result = ((config & kIPv6Disabled) == 0); + }); + + return result; + } +} + +- (void)setIPv6Enabled:(BOOL)flag +{ + // Note: YES means kIPv6Disabled is OFF + + dispatch_block_t block = ^{ + + if (flag) + config &= ~kIPv6Disabled; + else + config |= kIPv6Disabled; + }; + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + block(); + else + dispatch_async(socketQueue, block); +} + +- (BOOL)isIPv4PreferredOverIPv6 +{ + // Note: YES means kPreferIPv6 is OFF + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + { + return ((config & kPreferIPv6) == 0); + } + else + { + __block BOOL result; + + dispatch_sync(socketQueue, ^{ + result = ((config & kPreferIPv6) == 0); + }); + + return result; + } +} + +- (void)setIPv4PreferredOverIPv6:(BOOL)flag +{ + // Note: YES means kPreferIPv6 is OFF + + dispatch_block_t block = ^{ + + if (flag) + config &= ~kPreferIPv6; + else + config |= kPreferIPv6; + }; + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + block(); + else + dispatch_async(socketQueue, block); +} + +- (id)userData +{ + __block id result = nil; + + dispatch_block_t block = ^{ + + result = userData; + }; + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + block(); + else + dispatch_sync(socketQueue, block); + + return result; +} + +- (void)setUserData:(id)arbitraryUserData +{ + dispatch_block_t block = ^{ + + if (userData != arbitraryUserData) + { + userData = arbitraryUserData; + } + }; + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + block(); + else + dispatch_async(socketQueue, block); +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Accepting +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (BOOL)acceptOnPort:(uint16_t)port error:(NSError **)errPtr +{ + return [self acceptOnInterface:nil port:port error:errPtr]; +} + +- (BOOL)acceptOnInterface:(NSString *)inInterface port:(uint16_t)port error:(NSError **)errPtr +{ + LogTrace(); + + // Just in-case interface parameter is immutable. + NSString *interface = [inInterface copy]; + + __block BOOL result = NO; + __block NSError *err = nil; + + // CreateSocket Block + // This block will be invoked within the dispatch block below. + + int(^createSocket)(int, NSData*) = ^int (int domain, NSData *interfaceAddr) { + + int socketFD = socket(domain, SOCK_STREAM, 0); + + if (socketFD == SOCKET_NULL) + { + NSString *reason = @"Error in socket() function"; + err = [self errnoErrorWithReason:reason]; + + return SOCKET_NULL; + } + + int status; + + // Set socket options + + status = fcntl(socketFD, F_SETFL, O_NONBLOCK); + if (status == -1) + { + NSString *reason = @"Error enabling non-blocking IO on socket (fcntl)"; + err = [self errnoErrorWithReason:reason]; + + LogVerbose(@"close(socketFD)"); + close(socketFD); + return SOCKET_NULL; + } + + int reuseOn = 1; + status = setsockopt(socketFD, SOL_SOCKET, SO_REUSEADDR, &reuseOn, sizeof(reuseOn)); + if (status == -1) + { + NSString *reason = @"Error enabling address reuse (setsockopt)"; + err = [self errnoErrorWithReason:reason]; + + LogVerbose(@"close(socketFD)"); + close(socketFD); + return SOCKET_NULL; + } + + // Bind socket + + status = bind(socketFD, (const struct sockaddr *)[interfaceAddr bytes], (socklen_t)[interfaceAddr length]); + if (status == -1) + { + NSString *reason = @"Error in bind() function"; + err = [self errnoErrorWithReason:reason]; + + LogVerbose(@"close(socketFD)"); + close(socketFD); + return SOCKET_NULL; + } + + // Listen + + status = listen(socketFD, 1024); + if (status == -1) + { + NSString *reason = @"Error in listen() function"; + err = [self errnoErrorWithReason:reason]; + + LogVerbose(@"close(socketFD)"); + close(socketFD); + return SOCKET_NULL; + } + + return socketFD; + }; + + // Create dispatch block and run on socketQueue + + dispatch_block_t block = ^{ @autoreleasepool { + + if (delegate == nil) // Must have delegate set + { + NSString *msg = @"Attempting to accept without a delegate. Set a delegate first."; + err = [self badConfigError:msg]; + + return_from_block; + } + + if (delegateQueue == NULL) // Must have delegate queue set + { + NSString *msg = @"Attempting to accept without a delegate queue. Set a delegate queue first."; + err = [self badConfigError:msg]; + + return_from_block; + } + + BOOL isIPv4Disabled = (config & kIPv4Disabled) ? YES : NO; + BOOL isIPv6Disabled = (config & kIPv6Disabled) ? YES : NO; + + if (isIPv4Disabled && isIPv6Disabled) // Must have IPv4 or IPv6 enabled + { + NSString *msg = @"Both IPv4 and IPv6 have been disabled. Must enable at least one protocol first."; + err = [self badConfigError:msg]; + + return_from_block; + } + + if (![self isDisconnected]) // Must be disconnected + { + NSString *msg = @"Attempting to accept while connected or accepting connections. Disconnect first."; + err = [self badConfigError:msg]; + + return_from_block; + } + + // Clear queues (spurious read/write requests post disconnect) + [readQueue removeAllObjects]; + [writeQueue removeAllObjects]; + + // Resolve interface from description + + NSMutableData *interface4 = nil; + NSMutableData *interface6 = nil; + + [self getInterfaceAddress4:&interface4 address6:&interface6 fromDescription:interface port:port]; + + if ((interface4 == nil) && (interface6 == nil)) + { + NSString *msg = @"Unknown interface. Specify valid interface by name (e.g. \"en1\") or IP address."; + err = [self badParamError:msg]; + + return_from_block; + } + + if (isIPv4Disabled && (interface6 == nil)) + { + NSString *msg = @"IPv4 has been disabled and specified interface doesn't support IPv6."; + err = [self badParamError:msg]; + + return_from_block; + } + + if (isIPv6Disabled && (interface4 == nil)) + { + NSString *msg = @"IPv6 has been disabled and specified interface doesn't support IPv4."; + err = [self badParamError:msg]; + + return_from_block; + } + + BOOL enableIPv4 = !isIPv4Disabled && (interface4 != nil); + BOOL enableIPv6 = !isIPv6Disabled && (interface6 != nil); + + // Create sockets, configure, bind, and listen + + if (enableIPv4) + { + LogVerbose(@"Creating IPv4 socket"); + socket4FD = createSocket(AF_INET, interface4); + + if (socket4FD == SOCKET_NULL) + { + return_from_block; + } + } + + if (enableIPv6) + { + LogVerbose(@"Creating IPv6 socket"); + + if (enableIPv4 && (port == 0)) + { + // No specific port was specified, so we allowed the OS to pick an available port for us. + // Now we need to make sure the IPv6 socket listens on the same port as the IPv4 socket. + + struct sockaddr_in6 *addr6 = (struct sockaddr_in6 *)[interface6 mutableBytes]; + addr6->sin6_port = htons([self localPort4]); + } + + socket6FD = createSocket(AF_INET6, interface6); + + if (socket6FD == SOCKET_NULL) + { + if (socket4FD != SOCKET_NULL) + { + LogVerbose(@"close(socket4FD)"); + close(socket4FD); + } + + return_from_block; + } + } + + // Create accept sources + + if (enableIPv4) + { + accept4Source = dispatch_source_create(DISPATCH_SOURCE_TYPE_READ, socket4FD, 0, socketQueue); + + int socketFD = socket4FD; + dispatch_source_t acceptSource = accept4Source; + + __weak GCDAsyncSocket *weakSelf = self; + + dispatch_source_set_event_handler(accept4Source, ^{ @autoreleasepool { +#pragma clang diagnostic push +#pragma clang diagnostic warning "-Wimplicit-retain-self" + + __strong GCDAsyncSocket *strongSelf = weakSelf; + if (strongSelf == nil) return_from_block; + + LogVerbose(@"event4Block"); + + unsigned long i = 0; + unsigned long numPendingConnections = dispatch_source_get_data(acceptSource); + + LogVerbose(@"numPendingConnections: %lu", numPendingConnections); + + while ([strongSelf doAccept:socketFD] && (++i < numPendingConnections)); + +#pragma clang diagnostic pop + }}); + + + dispatch_source_set_cancel_handler(accept4Source, ^{ +#pragma clang diagnostic push +#pragma clang diagnostic warning "-Wimplicit-retain-self" + +#if !OS_OBJECT_USE_OBJC + LogVerbose(@"dispatch_release(accept4Source)"); + dispatch_release(acceptSource); +#endif + + LogVerbose(@"close(socket4FD)"); + close(socketFD); + +#pragma clang diagnostic pop + }); + + LogVerbose(@"dispatch_resume(accept4Source)"); + dispatch_resume(accept4Source); + } + + if (enableIPv6) + { + accept6Source = dispatch_source_create(DISPATCH_SOURCE_TYPE_READ, socket6FD, 0, socketQueue); + + int socketFD = socket6FD; + dispatch_source_t acceptSource = accept6Source; + + __weak GCDAsyncSocket *weakSelf = self; + + dispatch_source_set_event_handler(accept6Source, ^{ @autoreleasepool { +#pragma clang diagnostic push +#pragma clang diagnostic warning "-Wimplicit-retain-self" + + __strong GCDAsyncSocket *strongSelf = weakSelf; + if (strongSelf == nil) return_from_block; + + LogVerbose(@"event6Block"); + + unsigned long i = 0; + unsigned long numPendingConnections = dispatch_source_get_data(acceptSource); + + LogVerbose(@"numPendingConnections: %lu", numPendingConnections); + + while ([strongSelf doAccept:socketFD] && (++i < numPendingConnections)); + +#pragma clang diagnostic pop + }}); + + dispatch_source_set_cancel_handler(accept6Source, ^{ +#pragma clang diagnostic push +#pragma clang diagnostic warning "-Wimplicit-retain-self" + +#if !OS_OBJECT_USE_OBJC + LogVerbose(@"dispatch_release(accept6Source)"); + dispatch_release(acceptSource); +#endif + + LogVerbose(@"close(socket6FD)"); + close(socketFD); + +#pragma clang diagnostic pop + }); + + LogVerbose(@"dispatch_resume(accept6Source)"); + dispatch_resume(accept6Source); + } + + flags |= kSocketStarted; + + result = YES; + }}; + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + block(); + else + dispatch_sync(socketQueue, block); + + if (result == NO) + { + LogInfo(@"Error in accept: %@", err); + + if (errPtr) + *errPtr = err; + } + + return result; +} + +- (BOOL)acceptOnUrl:(NSURL *)url error:(NSError **)errPtr; +{ + LogTrace(); + + __block BOOL result = NO; + __block NSError *err = nil; + + // CreateSocket Block + // This block will be invoked within the dispatch block below. + + int(^createSocket)(int, NSData*) = ^int (int domain, NSData *interfaceAddr) { + + int socketFD = socket(domain, SOCK_STREAM, 0); + + if (socketFD == SOCKET_NULL) + { + NSString *reason = @"Error in socket() function"; + err = [self errnoErrorWithReason:reason]; + + return SOCKET_NULL; + } + + int status; + + // Set socket options + + status = fcntl(socketFD, F_SETFL, O_NONBLOCK); + if (status == -1) + { + NSString *reason = @"Error enabling non-blocking IO on socket (fcntl)"; + err = [self errnoErrorWithReason:reason]; + + LogVerbose(@"close(socketFD)"); + close(socketFD); + return SOCKET_NULL; + } + + int reuseOn = 1; + status = setsockopt(socketFD, SOL_SOCKET, SO_REUSEADDR, &reuseOn, sizeof(reuseOn)); + if (status == -1) + { + NSString *reason = @"Error enabling address reuse (setsockopt)"; + err = [self errnoErrorWithReason:reason]; + + LogVerbose(@"close(socketFD)"); + close(socketFD); + return SOCKET_NULL; + } + + // Bind socket + + status = bind(socketFD, (const struct sockaddr *)[interfaceAddr bytes], (socklen_t)[interfaceAddr length]); + if (status == -1) + { + NSString *reason = @"Error in bind() function"; + err = [self errnoErrorWithReason:reason]; + + LogVerbose(@"close(socketFD)"); + close(socketFD); + return SOCKET_NULL; + } + + // Listen + + status = listen(socketFD, 1024); + if (status == -1) + { + NSString *reason = @"Error in listen() function"; + err = [self errnoErrorWithReason:reason]; + + LogVerbose(@"close(socketFD)"); + close(socketFD); + return SOCKET_NULL; + } + + return socketFD; + }; + + // Create dispatch block and run on socketQueue + + dispatch_block_t block = ^{ @autoreleasepool { + + if (delegate == nil) // Must have delegate set + { + NSString *msg = @"Attempting to accept without a delegate. Set a delegate first."; + err = [self badConfigError:msg]; + + return_from_block; + } + + if (delegateQueue == NULL) // Must have delegate queue set + { + NSString *msg = @"Attempting to accept without a delegate queue. Set a delegate queue first."; + err = [self badConfigError:msg]; + + return_from_block; + } + + if (![self isDisconnected]) // Must be disconnected + { + NSString *msg = @"Attempting to accept while connected or accepting connections. Disconnect first."; + err = [self badConfigError:msg]; + + return_from_block; + } + + // Clear queues (spurious read/write requests post disconnect) + [readQueue removeAllObjects]; + [writeQueue removeAllObjects]; + + // Remove a previous socket + + NSError *error = nil; + NSFileManager *fileManager = [NSFileManager defaultManager]; + if ([fileManager fileExistsAtPath:url.path]) { + if (![[NSFileManager defaultManager] removeItemAtURL:url error:&error]) { + NSString *msg = @"Could not remove previous unix domain socket at given url."; + err = [self otherError:msg]; + + return_from_block; + } + } + + // Resolve interface from description + + NSData *interface = [self getInterfaceAddressFromUrl:url]; + + if (interface == nil) + { + NSString *msg = @"Invalid unix domain url. Specify a valid file url that does not exist (e.g. \"file:///tmp/socket\")"; + err = [self badParamError:msg]; + + return_from_block; + } + + // Create sockets, configure, bind, and listen + + LogVerbose(@"Creating unix domain socket"); + socketUN = createSocket(AF_UNIX, interface); + + if (socketUN == SOCKET_NULL) + { + return_from_block; + } + + socketUrl = url; + + // Create accept sources + + acceptUNSource = dispatch_source_create(DISPATCH_SOURCE_TYPE_READ, socketUN, 0, socketQueue); + + int socketFD = socketUN; + dispatch_source_t acceptSource = acceptUNSource; + + dispatch_source_set_event_handler(acceptUNSource, ^{ @autoreleasepool { + + LogVerbose(@"eventUNBlock"); + + unsigned long i = 0; + unsigned long numPendingConnections = dispatch_source_get_data(acceptSource); + + LogVerbose(@"numPendingConnections: %lu", numPendingConnections); + + while ([self doAccept:socketFD] && (++i < numPendingConnections)); + }}); + + dispatch_source_set_cancel_handler(acceptUNSource, ^{ + +#if NEEDS_DISPATCH_RETAIN_RELEASE + LogVerbose(@"dispatch_release(accept4Source)"); + dispatch_release(acceptSource); +#endif + + LogVerbose(@"close(socket4FD)"); + close(socketFD); + }); + + LogVerbose(@"dispatch_resume(accept4Source)"); + dispatch_resume(acceptUNSource); + + flags |= kSocketStarted; + + result = YES; + }}; + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + block(); + else + dispatch_sync(socketQueue, block); + + if (result == NO) + { + LogInfo(@"Error in accept: %@", err); + + if (errPtr) + *errPtr = err; + } + + return result; +} + +- (BOOL)doAccept:(int)parentSocketFD +{ + LogTrace(); + + int socketType; + int childSocketFD; + NSData *childSocketAddress; + + if (parentSocketFD == socket4FD) + { + socketType = 0; + + struct sockaddr_in addr; + socklen_t addrLen = sizeof(addr); + + childSocketFD = accept(parentSocketFD, (struct sockaddr *)&addr, &addrLen); + + if (childSocketFD == -1) + { + LogWarn(@"Accept failed with error: %@", [self errnoError]); + return NO; + } + + childSocketAddress = [NSData dataWithBytes:&addr length:addrLen]; + } + else if (parentSocketFD == socket6FD) + { + socketType = 1; + + struct sockaddr_in6 addr; + socklen_t addrLen = sizeof(addr); + + childSocketFD = accept(parentSocketFD, (struct sockaddr *)&addr, &addrLen); + + if (childSocketFD == -1) + { + LogWarn(@"Accept failed with error: %@", [self errnoError]); + return NO; + } + + childSocketAddress = [NSData dataWithBytes:&addr length:addrLen]; + } + else // if (parentSocketFD == socketUN) + { + socketType = 2; + + struct sockaddr_un addr; + socklen_t addrLen = sizeof(addr); + + childSocketFD = accept(parentSocketFD, (struct sockaddr *)&addr, &addrLen); + + if (childSocketFD == -1) + { + LogWarn(@"Accept failed with error: %@", [self errnoError]); + return NO; + } + + childSocketAddress = [NSData dataWithBytes:&addr length:addrLen]; + } + + // Enable non-blocking IO on the socket + + int result = fcntl(childSocketFD, F_SETFL, O_NONBLOCK); + if (result == -1) + { + LogWarn(@"Error enabling non-blocking IO on accepted socket (fcntl)"); + return NO; + } + + // Prevent SIGPIPE signals + + int nosigpipe = 1; + setsockopt(childSocketFD, SOL_SOCKET, SO_NOSIGPIPE, &nosigpipe, sizeof(nosigpipe)); + + // Notify delegate + + if (delegateQueue) + { + __strong id theDelegate = delegate; + + dispatch_async(delegateQueue, ^{ @autoreleasepool { + + // Query delegate for custom socket queue + + dispatch_queue_t childSocketQueue = NULL; + + if ([theDelegate respondsToSelector:@selector(newSocketQueueForConnectionFromAddress:onSocket:)]) + { + childSocketQueue = [theDelegate newSocketQueueForConnectionFromAddress:childSocketAddress + onSocket:self]; + } + + // Create GCDAsyncSocket instance for accepted socket + + GCDAsyncSocket *acceptedSocket = [[[self class] alloc] initWithDelegate:theDelegate + delegateQueue:delegateQueue + socketQueue:childSocketQueue]; + + if (socketType == 0) + acceptedSocket->socket4FD = childSocketFD; + else if (socketType == 1) + acceptedSocket->socket6FD = childSocketFD; + else + acceptedSocket->socketUN = childSocketFD; + + acceptedSocket->flags = (kSocketStarted | kConnected); + + // Setup read and write sources for accepted socket + + dispatch_async(acceptedSocket->socketQueue, ^{ @autoreleasepool { + + [acceptedSocket setupReadAndWriteSourcesForNewlyConnectedSocket:childSocketFD]; + }}); + + // Notify delegate + + if ([theDelegate respondsToSelector:@selector(socket:didAcceptNewSocket:)]) + { + [theDelegate socket:self didAcceptNewSocket:acceptedSocket]; + } + + // Release the socket queue returned from the delegate (it was retained by acceptedSocket) +#if !OS_OBJECT_USE_OBJC + if (childSocketQueue) dispatch_release(childSocketQueue); +#endif + + // The accepted socket should have been retained by the delegate. + // Otherwise it gets properly released when exiting the block. + }}); + } + + return YES; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Connecting +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +/** + * This method runs through the various checks required prior to a connection attempt. + * It is shared between the connectToHost and connectToAddress methods. + * + **/ +- (BOOL)preConnectWithInterface:(NSString *)interface error:(NSError **)errPtr +{ + NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue"); + + if (delegate == nil) // Must have delegate set + { + if (errPtr) + { + NSString *msg = @"Attempting to connect without a delegate. Set a delegate first."; + *errPtr = [self badConfigError:msg]; + } + return NO; + } + + if (delegateQueue == NULL) // Must have delegate queue set + { + if (errPtr) + { + NSString *msg = @"Attempting to connect without a delegate queue. Set a delegate queue first."; + *errPtr = [self badConfigError:msg]; + } + return NO; + } + + if (![self isDisconnected]) // Must be disconnected + { + if (errPtr) + { + NSString *msg = @"Attempting to connect while connected or accepting connections. Disconnect first."; + *errPtr = [self badConfigError:msg]; + } + return NO; + } + + BOOL isIPv4Disabled = (config & kIPv4Disabled) ? YES : NO; + BOOL isIPv6Disabled = (config & kIPv6Disabled) ? YES : NO; + + if (isIPv4Disabled && isIPv6Disabled) // Must have IPv4 or IPv6 enabled + { + if (errPtr) + { + NSString *msg = @"Both IPv4 and IPv6 have been disabled. Must enable at least one protocol first."; + *errPtr = [self badConfigError:msg]; + } + return NO; + } + + if (interface) + { + NSMutableData *interface4 = nil; + NSMutableData *interface6 = nil; + + [self getInterfaceAddress4:&interface4 address6:&interface6 fromDescription:interface port:0]; + + if ((interface4 == nil) && (interface6 == nil)) + { + if (errPtr) + { + NSString *msg = @"Unknown interface. Specify valid interface by name (e.g. \"en1\") or IP address."; + *errPtr = [self badParamError:msg]; + } + return NO; + } + + if (isIPv4Disabled && (interface6 == nil)) + { + if (errPtr) + { + NSString *msg = @"IPv4 has been disabled and specified interface doesn't support IPv6."; + *errPtr = [self badParamError:msg]; + } + return NO; + } + + if (isIPv6Disabled && (interface4 == nil)) + { + if (errPtr) + { + NSString *msg = @"IPv6 has been disabled and specified interface doesn't support IPv4."; + *errPtr = [self badParamError:msg]; + } + return NO; + } + + connectInterface4 = interface4; + connectInterface6 = interface6; + } + + // Clear queues (spurious read/write requests post disconnect) + [readQueue removeAllObjects]; + [writeQueue removeAllObjects]; + + return YES; +} + +- (BOOL)preConnectWithUrl:(NSURL *)url error:(NSError **)errPtr +{ + NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue"); + + if (delegate == nil) // Must have delegate set + { + if (errPtr) + { + NSString *msg = @"Attempting to connect without a delegate. Set a delegate first."; + *errPtr = [self badConfigError:msg]; + } + return NO; + } + + if (delegateQueue == NULL) // Must have delegate queue set + { + if (errPtr) + { + NSString *msg = @"Attempting to connect without a delegate queue. Set a delegate queue first."; + *errPtr = [self badConfigError:msg]; + } + return NO; + } + + if (![self isDisconnected]) // Must be disconnected + { + if (errPtr) + { + NSString *msg = @"Attempting to connect while connected or accepting connections. Disconnect first."; + *errPtr = [self badConfigError:msg]; + } + return NO; + } + + NSData *interface = [self getInterfaceAddressFromUrl:url]; + + if (interface == nil) + { + if (errPtr) + { + NSString *msg = @"Unknown interface. Specify valid interface by name (e.g. \"en1\") or IP address."; + *errPtr = [self badParamError:msg]; + } + return NO; + } + + connectInterfaceUN = interface; + + // Clear queues (spurious read/write requests post disconnect) + [readQueue removeAllObjects]; + [writeQueue removeAllObjects]; + + return YES; +} + +- (BOOL)connectToHost:(NSString*)host onPort:(uint16_t)port error:(NSError **)errPtr +{ + return [self connectToHost:host onPort:port withTimeout:-1 error:errPtr]; +} + +- (BOOL)connectToHost:(NSString *)host + onPort:(uint16_t)port + withTimeout:(NSTimeInterval)timeout + error:(NSError **)errPtr +{ + return [self connectToHost:host onPort:port viaInterface:nil withTimeout:timeout error:errPtr]; +} + +- (BOOL)connectToHost:(NSString *)inHost + onPort:(uint16_t)port + viaInterface:(NSString *)inInterface + withTimeout:(NSTimeInterval)timeout + error:(NSError **)errPtr +{ + LogTrace(); + + // Just in case immutable objects were passed + NSString *host = [inHost copy]; + NSString *interface = [inInterface copy]; + + __block BOOL result = NO; + __block NSError *preConnectErr = nil; + + dispatch_block_t block = ^{ @autoreleasepool { + + // Check for problems with host parameter + + if ([host length] == 0) + { + NSString *msg = @"Invalid host parameter (nil or \"\"). Should be a domain name or IP address string."; + preConnectErr = [self badParamError:msg]; + + return_from_block; + } + + // Run through standard pre-connect checks + + if (![self preConnectWithInterface:interface error:&preConnectErr]) + { + return_from_block; + } + + // We've made it past all the checks. + // It's time to start the connection process. + + flags |= kSocketStarted; + + LogVerbose(@"Dispatching DNS lookup..."); + + // It's possible that the given host parameter is actually a NSMutableString. + // So we want to copy it now, within this block that will be executed synchronously. + // This way the asynchronous lookup block below doesn't have to worry about it changing. + + NSString *hostCpy = [host copy]; + + int aStateIndex = stateIndex; + __weak GCDAsyncSocket *weakSelf = self; + + dispatch_queue_t globalConcurrentQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); + dispatch_async(globalConcurrentQueue, ^{ @autoreleasepool { +#pragma clang diagnostic push +#pragma clang diagnostic warning "-Wimplicit-retain-self" + + NSError *lookupErr = nil; + NSMutableArray *addresses = [GCDAsyncSocket lookupHost:hostCpy port:port error:&lookupErr]; + + __strong GCDAsyncSocket *strongSelf = weakSelf; + if (strongSelf == nil) return_from_block; + + if (lookupErr) + { + dispatch_async(strongSelf->socketQueue, ^{ @autoreleasepool { + + [strongSelf lookup:aStateIndex didFail:lookupErr]; + }}); + } + else + { + NSData *address4 = nil; + NSData *address6 = nil; + + for (NSData *address in addresses) + { + if (!address4 && [GCDAsyncSocket isIPv4Address:address]) + { + address4 = address; + } + else if (!address6 && [GCDAsyncSocket isIPv6Address:address]) + { + address6 = address; + } + } + + dispatch_async(strongSelf->socketQueue, ^{ @autoreleasepool { + + [strongSelf lookup:aStateIndex didSucceedWithAddress4:address4 address6:address6]; + }}); + } + +#pragma clang diagnostic pop + }}); + + [self startConnectTimeout:timeout]; + + result = YES; + }}; + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + block(); + else + dispatch_sync(socketQueue, block); + + + if (errPtr) *errPtr = preConnectErr; + return result; +} + +- (BOOL)connectToAddress:(NSData *)remoteAddr error:(NSError **)errPtr +{ + return [self connectToAddress:remoteAddr viaInterface:nil withTimeout:-1 error:errPtr]; +} + +- (BOOL)connectToAddress:(NSData *)remoteAddr withTimeout:(NSTimeInterval)timeout error:(NSError **)errPtr +{ + return [self connectToAddress:remoteAddr viaInterface:nil withTimeout:timeout error:errPtr]; +} + +- (BOOL)connectToAddress:(NSData *)inRemoteAddr + viaInterface:(NSString *)inInterface + withTimeout:(NSTimeInterval)timeout + error:(NSError **)errPtr +{ + LogTrace(); + + // Just in case immutable objects were passed + NSData *remoteAddr = [inRemoteAddr copy]; + NSString *interface = [inInterface copy]; + + __block BOOL result = NO; + __block NSError *err = nil; + + dispatch_block_t block = ^{ @autoreleasepool { + + // Check for problems with remoteAddr parameter + + NSData *address4 = nil; + NSData *address6 = nil; + + if ([remoteAddr length] >= sizeof(struct sockaddr)) + { + const struct sockaddr *sockaddr = (const struct sockaddr *)[remoteAddr bytes]; + + if (sockaddr->sa_family == AF_INET) + { + if ([remoteAddr length] == sizeof(struct sockaddr_in)) + { + address4 = remoteAddr; + } + } + else if (sockaddr->sa_family == AF_INET6) + { + if ([remoteAddr length] == sizeof(struct sockaddr_in6)) + { + address6 = remoteAddr; + } + } + } + + if ((address4 == nil) && (address6 == nil)) + { + NSString *msg = @"A valid IPv4 or IPv6 address was not given"; + err = [self badParamError:msg]; + + return_from_block; + } + + BOOL isIPv4Disabled = (config & kIPv4Disabled) ? YES : NO; + BOOL isIPv6Disabled = (config & kIPv6Disabled) ? YES : NO; + + if (isIPv4Disabled && (address4 != nil)) + { + NSString *msg = @"IPv4 has been disabled and an IPv4 address was passed."; + err = [self badParamError:msg]; + + return_from_block; + } + + if (isIPv6Disabled && (address6 != nil)) + { + NSString *msg = @"IPv6 has been disabled and an IPv6 address was passed."; + err = [self badParamError:msg]; + + return_from_block; + } + + // Run through standard pre-connect checks + + if (![self preConnectWithInterface:interface error:&err]) + { + return_from_block; + } + + // We've made it past all the checks. + // It's time to start the connection process. + + if (![self connectWithAddress4:address4 address6:address6 error:&err]) + { + return_from_block; + } + + flags |= kSocketStarted; + + [self startConnectTimeout:timeout]; + + result = YES; + }}; + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + block(); + else + dispatch_sync(socketQueue, block); + + if (result == NO) + { + if (errPtr) + *errPtr = err; + } + + return result; +} + +- (BOOL)connectToUrl:(NSURL *)url withTimeout:(NSTimeInterval)timeout error:(NSError **)errPtr; +{ + LogTrace(); + + __block BOOL result = NO; + __block NSError *err = nil; + + dispatch_block_t block = ^{ @autoreleasepool { + + // Check for problems with host parameter + + if ([url.path length] == 0) + { + NSString *msg = @"Invalid unix domain socket url."; + err = [self badParamError:msg]; + + return_from_block; + } + + // Run through standard pre-connect checks + + if (![self preConnectWithUrl:url error:&err]) + { + return_from_block; + } + + // We've made it past all the checks. + // It's time to start the connection process. + + flags |= kSocketStarted; + + // Start the normal connection process + + NSError *err = nil; + if (![self connectWithAddressUN:connectInterfaceUN error:&err]) + { + [self closeWithError:err]; + + return_from_block; + } + + [self startConnectTimeout:timeout]; + + result = YES; + }}; + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + block(); + else + dispatch_sync(socketQueue, block); + + if (result == NO) + { + if (errPtr) + *errPtr = err; + } + + return result; +} + +- (void)lookup:(int)aStateIndex didSucceedWithAddress4:(NSData *)address4 address6:(NSData *)address6 +{ + LogTrace(); + + NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue"); + NSAssert(address4 || address6, @"Expected at least one valid address"); + + if (aStateIndex != stateIndex) + { + LogInfo(@"Ignoring lookupDidSucceed, already disconnected"); + + // The connect operation has been cancelled. + // That is, socket was disconnected, or connection has already timed out. + return; + } + + // Check for problems + + BOOL isIPv4Disabled = (config & kIPv4Disabled) ? YES : NO; + BOOL isIPv6Disabled = (config & kIPv6Disabled) ? YES : NO; + + if (isIPv4Disabled && (address6 == nil)) + { + NSString *msg = @"IPv4 has been disabled and DNS lookup found no IPv6 address."; + + [self closeWithError:[self otherError:msg]]; + return; + } + + if (isIPv6Disabled && (address4 == nil)) + { + NSString *msg = @"IPv6 has been disabled and DNS lookup found no IPv4 address."; + + [self closeWithError:[self otherError:msg]]; + return; + } + + // Start the normal connection process + + NSError *err = nil; + if (![self connectWithAddress4:address4 address6:address6 error:&err]) + { + [self closeWithError:err]; + } +} + +/** + * This method is called if the DNS lookup fails. + * This method is executed on the socketQueue. + * + * Since the DNS lookup executed synchronously on a global concurrent queue, + * the original connection request may have already been cancelled or timed-out by the time this method is invoked. + * The lookupIndex tells us whether the lookup is still valid or not. + **/ +- (void)lookup:(int)aStateIndex didFail:(NSError *)error +{ + LogTrace(); + + NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue"); + + + if (aStateIndex != stateIndex) + { + LogInfo(@"Ignoring lookup:didFail: - already disconnected"); + + // The connect operation has been cancelled. + // That is, socket was disconnected, or connection has already timed out. + return; + } + + [self endConnectTimeout]; + [self closeWithError:error]; +} + +- (BOOL)connectWithAddress4:(NSData *)address4 address6:(NSData *)address6 error:(NSError **)errPtr +{ + LogTrace(); + + NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue"); + + LogVerbose(@"IPv4: %@:%hu", [[self class] hostFromAddress:address4], [[self class] portFromAddress:address4]); + LogVerbose(@"IPv6: %@:%hu", [[self class] hostFromAddress:address6], [[self class] portFromAddress:address6]); + + // Determine socket type + + BOOL preferIPv6 = (config & kPreferIPv6) ? YES : NO; + + BOOL useIPv6 = ((preferIPv6 && address6) || (address4 == nil)); + + // Create the socket + + int socketFD; + NSData *address; + NSData *connectInterface; + + if (useIPv6) + { + LogVerbose(@"Creating IPv6 socket"); + + socket6FD = socket(AF_INET6, SOCK_STREAM, 0); + + socketFD = socket6FD; + address = address6; + connectInterface = connectInterface6; + } + else + { + LogVerbose(@"Creating IPv4 socket"); + + socket4FD = socket(AF_INET, SOCK_STREAM, 0); + + socketFD = socket4FD; + address = address4; + connectInterface = connectInterface4; + } + + if (socketFD == SOCKET_NULL) + { + if (errPtr) + *errPtr = [self errnoErrorWithReason:@"Error in socket() function"]; + + return NO; + } + + // Bind the socket to the desired interface (if needed) + + if (connectInterface) + { + LogVerbose(@"Binding socket..."); + + if ([[self class] portFromAddress:connectInterface] > 0) + { + // Since we're going to be binding to a specific port, + // we should turn on reuseaddr to allow us to override sockets in time_wait. + + int reuseOn = 1; + setsockopt(socketFD, SOL_SOCKET, SO_REUSEADDR, &reuseOn, sizeof(reuseOn)); + } + + const struct sockaddr *interfaceAddr = (const struct sockaddr *)[connectInterface bytes]; + + int result = bind(socketFD, interfaceAddr, (socklen_t)[connectInterface length]); + if (result != 0) + { + if (errPtr) + *errPtr = [self errnoErrorWithReason:@"Error in bind() function"]; + + return NO; + } + } + + // Prevent SIGPIPE signals + + int nosigpipe = 1; + setsockopt(socketFD, SOL_SOCKET, SO_NOSIGPIPE, &nosigpipe, sizeof(nosigpipe)); + + // Start the connection process in a background queue + + int aStateIndex = stateIndex; + __weak GCDAsyncSocket *weakSelf = self; + + dispatch_queue_t globalConcurrentQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); + dispatch_async(globalConcurrentQueue, ^{ +#pragma clang diagnostic push +#pragma clang diagnostic warning "-Wimplicit-retain-self" + + int result = connect(socketFD, (const struct sockaddr *)[address bytes], (socklen_t)[address length]); + + __strong GCDAsyncSocket *strongSelf = weakSelf; + if (strongSelf == nil) return_from_block; + + if (result == 0) + { + dispatch_async(strongSelf->socketQueue, ^{ @autoreleasepool { + + [strongSelf didConnect:aStateIndex]; + }}); + } + else + { + NSError *error = [strongSelf errnoErrorWithReason:@"Error in connect() function"]; + + dispatch_async(strongSelf->socketQueue, ^{ @autoreleasepool { + + [strongSelf didNotConnect:aStateIndex error:error]; + }}); + } + +#pragma clang diagnostic pop + }); + + LogVerbose(@"Connecting..."); + + return YES; +} + +- (BOOL)connectWithAddressUN:(NSData *)address error:(NSError **)errPtr +{ + LogTrace(); + + NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue"); + + // Create the socket + + int socketFD; + + LogVerbose(@"Creating unix domain socket"); + + socketUN = socket(AF_UNIX, SOCK_STREAM, 0); + + socketFD = socketUN; + + if (socketFD == SOCKET_NULL) + { + if (errPtr) + *errPtr = [self errnoErrorWithReason:@"Error in socket() function"]; + + return NO; + } + + // Bind the socket to the desired interface (if needed) + + LogVerbose(@"Binding socket..."); + + int reuseOn = 1; + setsockopt(socketFD, SOL_SOCKET, SO_REUSEADDR, &reuseOn, sizeof(reuseOn)); + + // const struct sockaddr *interfaceAddr = (const struct sockaddr *)[address bytes]; + // + // int result = bind(socketFD, interfaceAddr, (socklen_t)[address length]); + // if (result != 0) + // { + // if (errPtr) + // *errPtr = [self errnoErrorWithReason:@"Error in bind() function"]; + // + // return NO; + // } + + // Prevent SIGPIPE signals + + int nosigpipe = 1; + setsockopt(socketFD, SOL_SOCKET, SO_NOSIGPIPE, &nosigpipe, sizeof(nosigpipe)); + + // Start the connection process in a background queue + + int aStateIndex = stateIndex; + + dispatch_queue_t globalConcurrentQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); + dispatch_async(globalConcurrentQueue, ^{ + + const struct sockaddr *addr = (const struct sockaddr *)[address bytes]; + int result = connect(socketFD, addr, addr->sa_len); + if (result == 0) + { + dispatch_async(socketQueue, ^{ @autoreleasepool { + + [self didConnect:aStateIndex]; + }}); + } + else + { + // TODO: Bad file descriptor + perror("connect"); + NSError *error = [self errnoErrorWithReason:@"Error in connect() function"]; + + dispatch_async(socketQueue, ^{ @autoreleasepool { + + [self didNotConnect:aStateIndex error:error]; + }}); + } + }); + + LogVerbose(@"Connecting..."); + + return YES; +} + +- (void)didConnect:(int)aStateIndex +{ + LogTrace(); + + NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue"); + + + if (aStateIndex != stateIndex) + { + LogInfo(@"Ignoring didConnect, already disconnected"); + + // The connect operation has been cancelled. + // That is, socket was disconnected, or connection has already timed out. + return; + } + + flags |= kConnected; + + [self endConnectTimeout]; + +#if TARGET_OS_IPHONE + // The endConnectTimeout method executed above incremented the stateIndex. + aStateIndex = stateIndex; +#endif + + // Setup read/write streams (as workaround for specific shortcomings in the iOS platform) + // + // Note: + // There may be configuration options that must be set by the delegate before opening the streams. + // The primary example is the kCFStreamNetworkServiceTypeVoIP flag, which only works on an unopened stream. + // + // Thus we wait until after the socket:didConnectToHost:port: delegate method has completed. + // This gives the delegate time to properly configure the streams if needed. + + dispatch_block_t SetupStreamsPart1 = ^{ +#if TARGET_OS_IPHONE + + if (![self createReadAndWriteStream]) + { + [self closeWithError:[self otherError:@"Error creating CFStreams"]]; + return; + } + + if (![self registerForStreamCallbacksIncludingReadWrite:NO]) + { + [self closeWithError:[self otherError:@"Error in CFStreamSetClient"]]; + return; + } + +#endif + }; + dispatch_block_t SetupStreamsPart2 = ^{ +#if TARGET_OS_IPHONE + + if (aStateIndex != stateIndex) + { + // The socket has been disconnected. + return; + } + + if (![self addStreamsToRunLoop]) + { + [self closeWithError:[self otherError:@"Error in CFStreamScheduleWithRunLoop"]]; + return; + } + + if (![self openStreams]) + { + [self closeWithError:[self otherError:@"Error creating CFStreams"]]; + return; + } + +#endif + }; + + // Notify delegate + + NSString *host = [self connectedHost]; + uint16_t port = [self connectedPort]; + NSURL *url = [self connectedUrl]; + + __strong id theDelegate = delegate; + + if (delegateQueue && host != nil && [theDelegate respondsToSelector:@selector(socket:didConnectToHost:port:)]) + { + SetupStreamsPart1(); + + dispatch_async(delegateQueue, ^{ @autoreleasepool { + + [theDelegate socket:self didConnectToHost:host port:port]; + + dispatch_async(socketQueue, ^{ @autoreleasepool { + + SetupStreamsPart2(); + }}); + }}); + } + else if (delegateQueue && url != nil && [theDelegate respondsToSelector:@selector(socket:didConnectToUrl:)]) + { + SetupStreamsPart1(); + + dispatch_async(delegateQueue, ^{ @autoreleasepool { + + [theDelegate socket:self didConnectToUrl:url]; + + dispatch_async(socketQueue, ^{ @autoreleasepool { + + SetupStreamsPart2(); + }}); + }}); + } + else + { + SetupStreamsPart1(); + SetupStreamsPart2(); + } + + // Get the connected socket + + int socketFD = (socket4FD != SOCKET_NULL) ? socket4FD : (socket6FD != SOCKET_NULL) ? socket6FD : socketUN; + + // Enable non-blocking IO on the socket + + int result = fcntl(socketFD, F_SETFL, O_NONBLOCK); + if (result == -1) + { + NSString *errMsg = @"Error enabling non-blocking IO on socket (fcntl)"; + [self closeWithError:[self otherError:errMsg]]; + + return; + } + + // Setup our read/write sources + + [self setupReadAndWriteSourcesForNewlyConnectedSocket:socketFD]; + + // Dequeue any pending read/write requests + + [self maybeDequeueRead]; + [self maybeDequeueWrite]; +} + +- (void)didNotConnect:(int)aStateIndex error:(NSError *)error +{ + LogTrace(); + + NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue"); + + + if (aStateIndex != stateIndex) + { + LogInfo(@"Ignoring didNotConnect, already disconnected"); + + // The connect operation has been cancelled. + // That is, socket was disconnected, or connection has already timed out. + return; + } + + [self closeWithError:error]; +} + +- (void)startConnectTimeout:(NSTimeInterval)timeout +{ + if (timeout >= 0.0) + { + connectTimer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, socketQueue); + + __weak GCDAsyncSocket *weakSelf = self; + + dispatch_source_set_event_handler(connectTimer, ^{ @autoreleasepool { +#pragma clang diagnostic push +#pragma clang diagnostic warning "-Wimplicit-retain-self" + + __strong GCDAsyncSocket *strongSelf = weakSelf; + if (strongSelf == nil) return_from_block; + + [strongSelf doConnectTimeout]; + +#pragma clang diagnostic pop + }}); + +#if !OS_OBJECT_USE_OBJC + dispatch_source_t theConnectTimer = connectTimer; + dispatch_source_set_cancel_handler(connectTimer, ^{ +#pragma clang diagnostic push +#pragma clang diagnostic warning "-Wimplicit-retain-self" + + LogVerbose(@"dispatch_release(connectTimer)"); + dispatch_release(theConnectTimer); + +#pragma clang diagnostic pop + }); +#endif + + dispatch_time_t tt = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(timeout * NSEC_PER_SEC)); + dispatch_source_set_timer(connectTimer, tt, DISPATCH_TIME_FOREVER, 0); + + dispatch_resume(connectTimer); + } +} + +- (void)endConnectTimeout +{ + LogTrace(); + + if (connectTimer) + { + dispatch_source_cancel(connectTimer); + connectTimer = NULL; + } + + // Increment stateIndex. + // This will prevent us from processing results from any related background asynchronous operations. + // + // Note: This should be called from close method even if connectTimer is NULL. + // This is because one might disconnect a socket prior to a successful connection which had no timeout. + + stateIndex++; + + if (connectInterface4) + { + connectInterface4 = nil; + } + if (connectInterface6) + { + connectInterface6 = nil; + } +} + +- (void)doConnectTimeout +{ + LogTrace(); + + [self endConnectTimeout]; + [self closeWithError:[self connectTimeoutError]]; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Disconnecting +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (void)closeWithError:(NSError *)error +{ + LogTrace(); + NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue"); + + [self endConnectTimeout]; + + if (currentRead != nil) [self endCurrentRead]; + if (currentWrite != nil) [self endCurrentWrite]; + + [readQueue removeAllObjects]; + [writeQueue removeAllObjects]; + + [preBuffer reset]; + +#if TARGET_OS_IPHONE + { + if (readStream || writeStream) + { + [self removeStreamsFromRunLoop]; + + if (readStream) + { + CFReadStreamSetClient(readStream, kCFStreamEventNone, NULL, NULL); + CFReadStreamClose(readStream); + CFRelease(readStream); + readStream = NULL; + } + if (writeStream) + { + CFWriteStreamSetClient(writeStream, kCFStreamEventNone, NULL, NULL); + CFWriteStreamClose(writeStream); + CFRelease(writeStream); + writeStream = NULL; + } + } + } +#endif + + [sslPreBuffer reset]; + sslErrCode = lastSSLHandshakeError = noErr; + + if (sslContext) + { + // Getting a linker error here about the SSLx() functions? + // You need to add the Security Framework to your application. + + SSLClose(sslContext); + +#if TARGET_OS_IPHONE || (__MAC_OS_X_VERSION_MIN_REQUIRED >= 1080) + CFRelease(sslContext); +#else + SSLDisposeContext(sslContext); +#endif + + sslContext = NULL; + } + + // For some crazy reason (in my opinion), cancelling a dispatch source doesn't + // invoke the cancel handler if the dispatch source is paused. + // So we have to unpause the source if needed. + // This allows the cancel handler to be run, which in turn releases the source and closes the socket. + + if (!accept4Source && !accept6Source && !acceptUNSource && !readSource && !writeSource) + { + LogVerbose(@"manually closing close"); + + if (socket4FD != SOCKET_NULL) + { + LogVerbose(@"close(socket4FD)"); + close(socket4FD); + socket4FD = SOCKET_NULL; + } + + if (socket6FD != SOCKET_NULL) + { + LogVerbose(@"close(socket6FD)"); + close(socket6FD); + socket6FD = SOCKET_NULL; + } + + if (socketUN != SOCKET_NULL) + { + LogVerbose(@"close(socketUN)"); + close(socketUN); + socketUN = SOCKET_NULL; + unlink(socketUrl.path.fileSystemRepresentation); + socketUrl = nil; + } + } + else + { + if (accept4Source) + { + LogVerbose(@"dispatch_source_cancel(accept4Source)"); + dispatch_source_cancel(accept4Source); + + // We never suspend accept4Source + + accept4Source = NULL; + } + + if (accept6Source) + { + LogVerbose(@"dispatch_source_cancel(accept6Source)"); + dispatch_source_cancel(accept6Source); + + // We never suspend accept6Source + + accept6Source = NULL; + } + + if (acceptUNSource) + { + LogVerbose(@"dispatch_source_cancel(acceptUNSource)"); + dispatch_source_cancel(acceptUNSource); + + // We never suspend acceptUNSource + + acceptUNSource = NULL; + } + + if (readSource) + { + LogVerbose(@"dispatch_source_cancel(readSource)"); + dispatch_source_cancel(readSource); + + [self resumeReadSource]; + + readSource = NULL; + } + + if (writeSource) + { + LogVerbose(@"dispatch_source_cancel(writeSource)"); + dispatch_source_cancel(writeSource); + + [self resumeWriteSource]; + + writeSource = NULL; + } + + // The sockets will be closed by the cancel handlers of the corresponding source + + socket4FD = SOCKET_NULL; + socket6FD = SOCKET_NULL; + socketUN = SOCKET_NULL; + } + + // If the client has passed the connect/accept method, then the connection has at least begun. + // Notify delegate that it is now ending. + BOOL shouldCallDelegate = (flags & kSocketStarted) ? YES : NO; + BOOL isDeallocating = (flags & kDealloc) ? YES : NO; + + // Clear stored socket info and all flags (config remains as is) + socketFDBytesAvailable = 0; + flags = 0; + sslWriteCachedLength = 0; + + if (shouldCallDelegate) + { + __strong id theDelegate = delegate; + __strong id theSelf = isDeallocating ? nil : self; + + if (delegateQueue && [theDelegate respondsToSelector: @selector(socketDidDisconnect:withError:)]) + { + dispatch_async(delegateQueue, ^{ @autoreleasepool { + + [theDelegate socketDidDisconnect:theSelf withError:error]; + }}); + } + } +} + +- (void)disconnect +{ + dispatch_block_t block = ^{ @autoreleasepool { + + if (flags & kSocketStarted) + { + [self closeWithError:nil]; + } + }}; + + // Synchronous disconnection, as documented in the header file + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + block(); + else + dispatch_sync(socketQueue, block); +} + +- (void)disconnectAfterReading +{ + dispatch_async(socketQueue, ^{ @autoreleasepool { + + if (flags & kSocketStarted) + { + flags |= (kForbidReadsWrites | kDisconnectAfterReads); + [self maybeClose]; + } + }}); +} + +- (void)disconnectAfterWriting +{ + dispatch_async(socketQueue, ^{ @autoreleasepool { + + if (flags & kSocketStarted) + { + flags |= (kForbidReadsWrites | kDisconnectAfterWrites); + [self maybeClose]; + } + }}); +} + +- (void)disconnectAfterReadingAndWriting +{ + dispatch_async(socketQueue, ^{ @autoreleasepool { + + if (flags & kSocketStarted) + { + flags |= (kForbidReadsWrites | kDisconnectAfterReads | kDisconnectAfterWrites); + [self maybeClose]; + } + }}); +} + +/** + * Closes the socket if possible. + * That is, if all writes have completed, and we're set to disconnect after writing, + * or if all reads have completed, and we're set to disconnect after reading. + **/ +- (void)maybeClose +{ + NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue"); + + BOOL shouldClose = NO; + + if (flags & kDisconnectAfterReads) + { + if (([readQueue count] == 0) && (currentRead == nil)) + { + if (flags & kDisconnectAfterWrites) + { + if (([writeQueue count] == 0) && (currentWrite == nil)) + { + shouldClose = YES; + } + } + else + { + shouldClose = YES; + } + } + } + else if (flags & kDisconnectAfterWrites) + { + if (([writeQueue count] == 0) && (currentWrite == nil)) + { + shouldClose = YES; + } + } + + if (shouldClose) + { + [self closeWithError:nil]; + } +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Errors +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (NSError *)badConfigError:(NSString *)errMsg +{ + NSDictionary *userInfo = [NSDictionary dictionaryWithObject:errMsg forKey:NSLocalizedDescriptionKey]; + + return [NSError errorWithDomain:GCDAsyncSocketErrorDomain code:GCDAsyncSocketBadConfigError userInfo:userInfo]; +} + +- (NSError *)badParamError:(NSString *)errMsg +{ + NSDictionary *userInfo = [NSDictionary dictionaryWithObject:errMsg forKey:NSLocalizedDescriptionKey]; + + return [NSError errorWithDomain:GCDAsyncSocketErrorDomain code:GCDAsyncSocketBadParamError userInfo:userInfo]; +} + ++ (NSError *)gaiError:(int)gai_error +{ + NSString *errMsg = [NSString stringWithCString:gai_strerror(gai_error) encoding:NSASCIIStringEncoding]; + NSDictionary *userInfo = [NSDictionary dictionaryWithObject:errMsg forKey:NSLocalizedDescriptionKey]; + + return [NSError errorWithDomain:@"kCFStreamErrorDomainNetDB" code:gai_error userInfo:userInfo]; +} + +- (NSError *)errnoErrorWithReason:(NSString *)reason +{ + NSString *errMsg = [NSString stringWithUTF8String:strerror(errno)]; + NSDictionary *userInfo = [NSDictionary dictionaryWithObjectsAndKeys:errMsg, NSLocalizedDescriptionKey, + reason, NSLocalizedFailureReasonErrorKey, nil]; + + return [NSError errorWithDomain:NSPOSIXErrorDomain code:errno userInfo:userInfo]; +} + +- (NSError *)errnoError +{ + NSString *errMsg = [NSString stringWithUTF8String:strerror(errno)]; + NSDictionary *userInfo = [NSDictionary dictionaryWithObject:errMsg forKey:NSLocalizedDescriptionKey]; + + return [NSError errorWithDomain:NSPOSIXErrorDomain code:errno userInfo:userInfo]; +} + +- (NSError *)sslError:(OSStatus)ssl_error +{ + NSString *msg = @"Error code definition can be found in Apple's SecureTransport.h"; + NSDictionary *userInfo = [NSDictionary dictionaryWithObject:msg forKey:NSLocalizedRecoverySuggestionErrorKey]; + + return [NSError errorWithDomain:@"kCFStreamErrorDomainSSL" code:ssl_error userInfo:userInfo]; +} + +- (NSError *)connectTimeoutError +{ + NSString *errMsg = NSLocalizedStringWithDefaultValue(@"GCDAsyncSocketConnectTimeoutError", + @"GCDAsyncSocket", [NSBundle mainBundle], + @"Attempt to connect to host timed out", nil); + + NSDictionary *userInfo = [NSDictionary dictionaryWithObject:errMsg forKey:NSLocalizedDescriptionKey]; + + return [NSError errorWithDomain:GCDAsyncSocketErrorDomain code:GCDAsyncSocketConnectTimeoutError userInfo:userInfo]; +} + +/** + * Returns a standard AsyncSocket maxed out error. + **/ +- (NSError *)readMaxedOutError +{ + NSString *errMsg = NSLocalizedStringWithDefaultValue(@"GCDAsyncSocketReadMaxedOutError", + @"GCDAsyncSocket", [NSBundle mainBundle], + @"Read operation reached set maximum length", nil); + + NSDictionary *info = [NSDictionary dictionaryWithObject:errMsg forKey:NSLocalizedDescriptionKey]; + + return [NSError errorWithDomain:GCDAsyncSocketErrorDomain code:GCDAsyncSocketReadMaxedOutError userInfo:info]; +} + +/** + * Returns a standard AsyncSocket write timeout error. + **/ +- (NSError *)readTimeoutError +{ + NSString *errMsg = NSLocalizedStringWithDefaultValue(@"GCDAsyncSocketReadTimeoutError", + @"GCDAsyncSocket", [NSBundle mainBundle], + @"Read operation timed out", nil); + + NSDictionary *userInfo = [NSDictionary dictionaryWithObject:errMsg forKey:NSLocalizedDescriptionKey]; + + return [NSError errorWithDomain:GCDAsyncSocketErrorDomain code:GCDAsyncSocketReadTimeoutError userInfo:userInfo]; +} + +/** + * Returns a standard AsyncSocket write timeout error. + **/ +- (NSError *)writeTimeoutError +{ + NSString *errMsg = NSLocalizedStringWithDefaultValue(@"GCDAsyncSocketWriteTimeoutError", + @"GCDAsyncSocket", [NSBundle mainBundle], + @"Write operation timed out", nil); + + NSDictionary *userInfo = [NSDictionary dictionaryWithObject:errMsg forKey:NSLocalizedDescriptionKey]; + + return [NSError errorWithDomain:GCDAsyncSocketErrorDomain code:GCDAsyncSocketWriteTimeoutError userInfo:userInfo]; +} + +- (NSError *)connectionClosedError +{ + NSString *errMsg = NSLocalizedStringWithDefaultValue(@"GCDAsyncSocketClosedError", + @"GCDAsyncSocket", [NSBundle mainBundle], + @"Socket closed by remote peer", nil); + + NSDictionary *userInfo = [NSDictionary dictionaryWithObject:errMsg forKey:NSLocalizedDescriptionKey]; + + return [NSError errorWithDomain:GCDAsyncSocketErrorDomain code:GCDAsyncSocketClosedError userInfo:userInfo]; +} + +- (NSError *)otherError:(NSString *)errMsg +{ + NSDictionary *userInfo = [NSDictionary dictionaryWithObject:errMsg forKey:NSLocalizedDescriptionKey]; + + return [NSError errorWithDomain:GCDAsyncSocketErrorDomain code:GCDAsyncSocketOtherError userInfo:userInfo]; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Diagnostics +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (BOOL)isDisconnected +{ + __block BOOL result = NO; + + dispatch_block_t block = ^{ + result = (flags & kSocketStarted) ? NO : YES; + }; + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + block(); + else + dispatch_sync(socketQueue, block); + + return result; +} + +- (BOOL)isConnected +{ + __block BOOL result = NO; + + dispatch_block_t block = ^{ + result = (flags & kConnected) ? YES : NO; + }; + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + block(); + else + dispatch_sync(socketQueue, block); + + return result; +} + +- (NSString *)connectedHost +{ + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + { + if (socket4FD != SOCKET_NULL) + return [self connectedHostFromSocket4:socket4FD]; + if (socket6FD != SOCKET_NULL) + return [self connectedHostFromSocket6:socket6FD]; + + return nil; + } + else + { + __block NSString *result = nil; + + dispatch_sync(socketQueue, ^{ @autoreleasepool { + + if (socket4FD != SOCKET_NULL) + result = [self connectedHostFromSocket4:socket4FD]; + else if (socket6FD != SOCKET_NULL) + result = [self connectedHostFromSocket6:socket6FD]; + }}); + + return result; + } +} + +- (uint16_t)connectedPort +{ + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + { + if (socket4FD != SOCKET_NULL) + return [self connectedPortFromSocket4:socket4FD]; + if (socket6FD != SOCKET_NULL) + return [self connectedPortFromSocket6:socket6FD]; + + return 0; + } + else + { + __block uint16_t result = 0; + + dispatch_sync(socketQueue, ^{ + // No need for autorelease pool + + if (socket4FD != SOCKET_NULL) + result = [self connectedPortFromSocket4:socket4FD]; + else if (socket6FD != SOCKET_NULL) + result = [self connectedPortFromSocket6:socket6FD]; + }); + + return result; + } +} + +- (NSURL *)connectedUrl +{ + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + { + if (socketUN != SOCKET_NULL) + return [self connectedUrlFromSocketUN:socketUN]; + + return nil; + } + else + { + __block NSURL *result = nil; + + dispatch_sync(socketQueue, ^{ @autoreleasepool { + + if (socketUN != SOCKET_NULL) + result = [self connectedUrlFromSocketUN:socketUN]; + }}); + + return result; + } +} + +- (NSString *)localHost +{ + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + { + if (socket4FD != SOCKET_NULL) + return [self localHostFromSocket4:socket4FD]; + if (socket6FD != SOCKET_NULL) + return [self localHostFromSocket6:socket6FD]; + + return nil; + } + else + { + __block NSString *result = nil; + + dispatch_sync(socketQueue, ^{ @autoreleasepool { + + if (socket4FD != SOCKET_NULL) + result = [self localHostFromSocket4:socket4FD]; + else if (socket6FD != SOCKET_NULL) + result = [self localHostFromSocket6:socket6FD]; + }}); + + return result; + } +} + +- (uint16_t)localPort +{ + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + { + if (socket4FD != SOCKET_NULL) + return [self localPortFromSocket4:socket4FD]; + if (socket6FD != SOCKET_NULL) + return [self localPortFromSocket6:socket6FD]; + + return 0; + } + else + { + __block uint16_t result = 0; + + dispatch_sync(socketQueue, ^{ + // No need for autorelease pool + + if (socket4FD != SOCKET_NULL) + result = [self localPortFromSocket4:socket4FD]; + else if (socket6FD != SOCKET_NULL) + result = [self localPortFromSocket6:socket6FD]; + }); + + return result; + } +} + +- (NSString *)connectedHost4 +{ + if (socket4FD != SOCKET_NULL) + return [self connectedHostFromSocket4:socket4FD]; + + return nil; +} + +- (NSString *)connectedHost6 +{ + if (socket6FD != SOCKET_NULL) + return [self connectedHostFromSocket6:socket6FD]; + + return nil; +} + +- (uint16_t)connectedPort4 +{ + if (socket4FD != SOCKET_NULL) + return [self connectedPortFromSocket4:socket4FD]; + + return 0; +} + +- (uint16_t)connectedPort6 +{ + if (socket6FD != SOCKET_NULL) + return [self connectedPortFromSocket6:socket6FD]; + + return 0; +} + +- (NSString *)localHost4 +{ + if (socket4FD != SOCKET_NULL) + return [self localHostFromSocket4:socket4FD]; + + return nil; +} + +- (NSString *)localHost6 +{ + if (socket6FD != SOCKET_NULL) + return [self localHostFromSocket6:socket6FD]; + + return nil; +} + +- (uint16_t)localPort4 +{ + if (socket4FD != SOCKET_NULL) + return [self localPortFromSocket4:socket4FD]; + + return 0; +} + +- (uint16_t)localPort6 +{ + if (socket6FD != SOCKET_NULL) + return [self localPortFromSocket6:socket6FD]; + + return 0; +} + +- (NSString *)connectedHostFromSocket4:(int)socketFD +{ + struct sockaddr_in sockaddr4; + socklen_t sockaddr4len = sizeof(sockaddr4); + + if (getpeername(socketFD, (struct sockaddr *)&sockaddr4, &sockaddr4len) < 0) + { + return nil; + } + return [[self class] hostFromSockaddr4:&sockaddr4]; +} + +- (NSString *)connectedHostFromSocket6:(int)socketFD +{ + struct sockaddr_in6 sockaddr6; + socklen_t sockaddr6len = sizeof(sockaddr6); + + if (getpeername(socketFD, (struct sockaddr *)&sockaddr6, &sockaddr6len) < 0) + { + return nil; + } + return [[self class] hostFromSockaddr6:&sockaddr6]; +} + +- (uint16_t)connectedPortFromSocket4:(int)socketFD +{ + struct sockaddr_in sockaddr4; + socklen_t sockaddr4len = sizeof(sockaddr4); + + if (getpeername(socketFD, (struct sockaddr *)&sockaddr4, &sockaddr4len) < 0) + { + return 0; + } + return [[self class] portFromSockaddr4:&sockaddr4]; +} + +- (uint16_t)connectedPortFromSocket6:(int)socketFD +{ + struct sockaddr_in6 sockaddr6; + socklen_t sockaddr6len = sizeof(sockaddr6); + + if (getpeername(socketFD, (struct sockaddr *)&sockaddr6, &sockaddr6len) < 0) + { + return 0; + } + return [[self class] portFromSockaddr6:&sockaddr6]; +} + +- (NSURL *)connectedUrlFromSocketUN:(int)socketFD +{ + struct sockaddr_un sockaddr; + socklen_t sockaddrlen = sizeof(sockaddr); + + if (getpeername(socketFD, (struct sockaddr *)&sockaddr, &sockaddrlen) < 0) + { + return 0; + } + return [[self class] urlFromSockaddrUN:&sockaddr]; +} + +- (NSString *)localHostFromSocket4:(int)socketFD +{ + struct sockaddr_in sockaddr4; + socklen_t sockaddr4len = sizeof(sockaddr4); + + if (getsockname(socketFD, (struct sockaddr *)&sockaddr4, &sockaddr4len) < 0) + { + return nil; + } + return [[self class] hostFromSockaddr4:&sockaddr4]; +} + +- (NSString *)localHostFromSocket6:(int)socketFD +{ + struct sockaddr_in6 sockaddr6; + socklen_t sockaddr6len = sizeof(sockaddr6); + + if (getsockname(socketFD, (struct sockaddr *)&sockaddr6, &sockaddr6len) < 0) + { + return nil; + } + return [[self class] hostFromSockaddr6:&sockaddr6]; +} + +- (uint16_t)localPortFromSocket4:(int)socketFD +{ + struct sockaddr_in sockaddr4; + socklen_t sockaddr4len = sizeof(sockaddr4); + + if (getsockname(socketFD, (struct sockaddr *)&sockaddr4, &sockaddr4len) < 0) + { + return 0; + } + return [[self class] portFromSockaddr4:&sockaddr4]; +} + +- (uint16_t)localPortFromSocket6:(int)socketFD +{ + struct sockaddr_in6 sockaddr6; + socklen_t sockaddr6len = sizeof(sockaddr6); + + if (getsockname(socketFD, (struct sockaddr *)&sockaddr6, &sockaddr6len) < 0) + { + return 0; + } + return [[self class] portFromSockaddr6:&sockaddr6]; +} + +- (NSData *)connectedAddress +{ + __block NSData *result = nil; + + dispatch_block_t block = ^{ + if (socket4FD != SOCKET_NULL) + { + struct sockaddr_in sockaddr4; + socklen_t sockaddr4len = sizeof(sockaddr4); + + if (getpeername(socket4FD, (struct sockaddr *)&sockaddr4, &sockaddr4len) == 0) + { + result = [[NSData alloc] initWithBytes:&sockaddr4 length:sockaddr4len]; + } + } + + if (socket6FD != SOCKET_NULL) + { + struct sockaddr_in6 sockaddr6; + socklen_t sockaddr6len = sizeof(sockaddr6); + + if (getpeername(socket6FD, (struct sockaddr *)&sockaddr6, &sockaddr6len) == 0) + { + result = [[NSData alloc] initWithBytes:&sockaddr6 length:sockaddr6len]; + } + } + }; + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + block(); + else + dispatch_sync(socketQueue, block); + + return result; +} + +- (NSData *)localAddress +{ + __block NSData *result = nil; + + dispatch_block_t block = ^{ + if (socket4FD != SOCKET_NULL) + { + struct sockaddr_in sockaddr4; + socklen_t sockaddr4len = sizeof(sockaddr4); + + if (getsockname(socket4FD, (struct sockaddr *)&sockaddr4, &sockaddr4len) == 0) + { + result = [[NSData alloc] initWithBytes:&sockaddr4 length:sockaddr4len]; + } + } + + if (socket6FD != SOCKET_NULL) + { + struct sockaddr_in6 sockaddr6; + socklen_t sockaddr6len = sizeof(sockaddr6); + + if (getsockname(socket6FD, (struct sockaddr *)&sockaddr6, &sockaddr6len) == 0) + { + result = [[NSData alloc] initWithBytes:&sockaddr6 length:sockaddr6len]; + } + } + }; + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + block(); + else + dispatch_sync(socketQueue, block); + + return result; +} + +- (BOOL)isIPv4 +{ + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + { + return (socket4FD != SOCKET_NULL); + } + else + { + __block BOOL result = NO; + + dispatch_sync(socketQueue, ^{ + result = (socket4FD != SOCKET_NULL); + }); + + return result; + } +} + +- (BOOL)isIPv6 +{ + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + { + return (socket6FD != SOCKET_NULL); + } + else + { + __block BOOL result = NO; + + dispatch_sync(socketQueue, ^{ + result = (socket6FD != SOCKET_NULL); + }); + + return result; + } +} + +- (BOOL)isSecure +{ + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + { + return (flags & kSocketSecure) ? YES : NO; + } + else + { + __block BOOL result; + + dispatch_sync(socketQueue, ^{ + result = (flags & kSocketSecure) ? YES : NO; + }); + + return result; + } +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Utilities +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +/** + * Finds the address of an interface description. + * An inteface description may be an interface name (en0, en1, lo0) or corresponding IP (192.168.4.34). + * + * The interface description may optionally contain a port number at the end, separated by a colon. + * If a non-zero port parameter is provided, any port number in the interface description is ignored. + * + * The returned value is a 'struct sockaddr' wrapped in an NSMutableData object. + **/ +- (void)getInterfaceAddress4:(NSMutableData **)interfaceAddr4Ptr + address6:(NSMutableData **)interfaceAddr6Ptr + fromDescription:(NSString *)interfaceDescription + port:(uint16_t)port +{ + NSMutableData *addr4 = nil; + NSMutableData *addr6 = nil; + + NSString *interface = nil; + + NSArray *components = [interfaceDescription componentsSeparatedByString:@":"]; + if ([components count] > 0) + { + NSString *temp = [components objectAtIndex:0]; + if ([temp length] > 0) + { + interface = temp; + } + } + if ([components count] > 1 && port == 0) + { + long portL = strtol([[components objectAtIndex:1] UTF8String], NULL, 10); + + if (portL > 0 && portL <= UINT16_MAX) + { + port = (uint16_t)portL; + } + } + + if (interface == nil) + { + // ANY address + + struct sockaddr_in sockaddr4; + memset(&sockaddr4, 0, sizeof(sockaddr4)); + + sockaddr4.sin_len = sizeof(sockaddr4); + sockaddr4.sin_family = AF_INET; + sockaddr4.sin_port = htons(port); + sockaddr4.sin_addr.s_addr = htonl(INADDR_ANY); + + struct sockaddr_in6 sockaddr6; + memset(&sockaddr6, 0, sizeof(sockaddr6)); + + sockaddr6.sin6_len = sizeof(sockaddr6); + sockaddr6.sin6_family = AF_INET6; + sockaddr6.sin6_port = htons(port); + sockaddr6.sin6_addr = in6addr_any; + + addr4 = [NSMutableData dataWithBytes:&sockaddr4 length:sizeof(sockaddr4)]; + addr6 = [NSMutableData dataWithBytes:&sockaddr6 length:sizeof(sockaddr6)]; + } + else if ([interface isEqualToString:@"localhost"] || [interface isEqualToString:@"loopback"]) + { + // LOOPBACK address + + struct sockaddr_in sockaddr4; + memset(&sockaddr4, 0, sizeof(sockaddr4)); + + sockaddr4.sin_len = sizeof(sockaddr4); + sockaddr4.sin_family = AF_INET; + sockaddr4.sin_port = htons(port); + sockaddr4.sin_addr.s_addr = htonl(INADDR_LOOPBACK); + + struct sockaddr_in6 sockaddr6; + memset(&sockaddr6, 0, sizeof(sockaddr6)); + + sockaddr6.sin6_len = sizeof(sockaddr6); + sockaddr6.sin6_family = AF_INET6; + sockaddr6.sin6_port = htons(port); + sockaddr6.sin6_addr = in6addr_loopback; + + addr4 = [NSMutableData dataWithBytes:&sockaddr4 length:sizeof(sockaddr4)]; + addr6 = [NSMutableData dataWithBytes:&sockaddr6 length:sizeof(sockaddr6)]; + } + else + { + const char *iface = [interface UTF8String]; + + struct ifaddrs *addrs; + const struct ifaddrs *cursor; + + if ((getifaddrs(&addrs) == 0)) + { + cursor = addrs; + while (cursor != NULL) + { + if ((addr4 == nil) && (cursor->ifa_addr->sa_family == AF_INET)) + { + // IPv4 + + struct sockaddr_in nativeAddr4; + memcpy(&nativeAddr4, cursor->ifa_addr, sizeof(nativeAddr4)); + + if (strcmp(cursor->ifa_name, iface) == 0) + { + // Name match + + nativeAddr4.sin_port = htons(port); + + addr4 = [NSMutableData dataWithBytes:&nativeAddr4 length:sizeof(nativeAddr4)]; + } + else + { + char ip[INET_ADDRSTRLEN]; + + const char *conversion = inet_ntop(AF_INET, &nativeAddr4.sin_addr, ip, sizeof(ip)); + + if ((conversion != NULL) && (strcmp(ip, iface) == 0)) + { + // IP match + + nativeAddr4.sin_port = htons(port); + + addr4 = [NSMutableData dataWithBytes:&nativeAddr4 length:sizeof(nativeAddr4)]; + } + } + } + else if ((addr6 == nil) && (cursor->ifa_addr->sa_family == AF_INET6)) + { + // IPv6 + + struct sockaddr_in6 nativeAddr6; + memcpy(&nativeAddr6, cursor->ifa_addr, sizeof(nativeAddr6)); + + if (strcmp(cursor->ifa_name, iface) == 0) + { + // Name match + + nativeAddr6.sin6_port = htons(port); + + addr6 = [NSMutableData dataWithBytes:&nativeAddr6 length:sizeof(nativeAddr6)]; + } + else + { + char ip[INET6_ADDRSTRLEN]; + + const char *conversion = inet_ntop(AF_INET6, &nativeAddr6.sin6_addr, ip, sizeof(ip)); + + if ((conversion != NULL) && (strcmp(ip, iface) == 0)) + { + // IP match + + nativeAddr6.sin6_port = htons(port); + + addr6 = [NSMutableData dataWithBytes:&nativeAddr6 length:sizeof(nativeAddr6)]; + } + } + } + + cursor = cursor->ifa_next; + } + + freeifaddrs(addrs); + } + } + + if (interfaceAddr4Ptr) *interfaceAddr4Ptr = addr4; + if (interfaceAddr6Ptr) *interfaceAddr6Ptr = addr6; +} + +- (NSData *)getInterfaceAddressFromUrl:(NSURL *)url; +{ + NSString *path = url.path; + if (path.length == 0) { + return nil; + } + + struct sockaddr_un nativeAddr; + nativeAddr.sun_family = AF_UNIX; + strlcpy(nativeAddr.sun_path, path.fileSystemRepresentation, sizeof(nativeAddr.sun_path)); + nativeAddr.sun_len = SUN_LEN(&nativeAddr); + NSData *interface = [NSData dataWithBytes:&nativeAddr length:sizeof(struct sockaddr_un)]; + + return interface; +} + +- (void)setupReadAndWriteSourcesForNewlyConnectedSocket:(int)socketFD +{ + readSource = dispatch_source_create(DISPATCH_SOURCE_TYPE_READ, socketFD, 0, socketQueue); + writeSource = dispatch_source_create(DISPATCH_SOURCE_TYPE_WRITE, socketFD, 0, socketQueue); + + // Setup event handlers + + __weak GCDAsyncSocket *weakSelf = self; + + dispatch_source_set_event_handler(readSource, ^{ @autoreleasepool { +#pragma clang diagnostic push +#pragma clang diagnostic warning "-Wimplicit-retain-self" + + __strong GCDAsyncSocket *strongSelf = weakSelf; + if (strongSelf == nil) return_from_block; + + LogVerbose(@"readEventBlock"); + + strongSelf->socketFDBytesAvailable = dispatch_source_get_data(strongSelf->readSource); + LogVerbose(@"socketFDBytesAvailable: %lu", strongSelf->socketFDBytesAvailable); + + if (strongSelf->socketFDBytesAvailable > 0) + [strongSelf doReadData]; + else + [strongSelf doReadEOF]; + +#pragma clang diagnostic pop + }}); + + dispatch_source_set_event_handler(writeSource, ^{ @autoreleasepool { +#pragma clang diagnostic push +#pragma clang diagnostic warning "-Wimplicit-retain-self" + + __strong GCDAsyncSocket *strongSelf = weakSelf; + if (strongSelf == nil) return_from_block; + + LogVerbose(@"writeEventBlock"); + + strongSelf->flags |= kSocketCanAcceptBytes; + [strongSelf doWriteData]; + +#pragma clang diagnostic pop + }}); + + // Setup cancel handlers + + __block int socketFDRefCount = 2; + +#if !OS_OBJECT_USE_OBJC + dispatch_source_t theReadSource = readSource; + dispatch_source_t theWriteSource = writeSource; +#endif + + dispatch_source_set_cancel_handler(readSource, ^{ +#pragma clang diagnostic push +#pragma clang diagnostic warning "-Wimplicit-retain-self" + + LogVerbose(@"readCancelBlock"); + +#if !OS_OBJECT_USE_OBJC + LogVerbose(@"dispatch_release(readSource)"); + dispatch_release(theReadSource); +#endif + + if (--socketFDRefCount == 0) + { + LogVerbose(@"close(socketFD)"); + close(socketFD); + } + +#pragma clang diagnostic pop + }); + + dispatch_source_set_cancel_handler(writeSource, ^{ +#pragma clang diagnostic push +#pragma clang diagnostic warning "-Wimplicit-retain-self" + + LogVerbose(@"writeCancelBlock"); + +#if !OS_OBJECT_USE_OBJC + LogVerbose(@"dispatch_release(writeSource)"); + dispatch_release(theWriteSource); +#endif + + if (--socketFDRefCount == 0) + { + LogVerbose(@"close(socketFD)"); + close(socketFD); + } + +#pragma clang diagnostic pop + }); + + // We will not be able to read until data arrives. + // But we should be able to write immediately. + + socketFDBytesAvailable = 0; + flags &= ~kReadSourceSuspended; + + LogVerbose(@"dispatch_resume(readSource)"); + dispatch_resume(readSource); + + flags |= kSocketCanAcceptBytes; + flags |= kWriteSourceSuspended; +} + +- (BOOL)usingCFStreamForTLS +{ +#if TARGET_OS_IPHONE + + if ((flags & kSocketSecure) && (flags & kUsingCFStreamForTLS)) + { + // The startTLS method was given the GCDAsyncSocketUseCFStreamForTLS flag. + + return YES; + } + +#endif + + return NO; +} + +- (BOOL)usingSecureTransportForTLS +{ + // Invoking this method is equivalent to ![self usingCFStreamForTLS] (just more readable) + +#if TARGET_OS_IPHONE + + if ((flags & kSocketSecure) && (flags & kUsingCFStreamForTLS)) + { + // The startTLS method was given the GCDAsyncSocketUseCFStreamForTLS flag. + + return NO; + } + +#endif + + return YES; +} + +- (void)suspendReadSource +{ + if (!(flags & kReadSourceSuspended)) + { + LogVerbose(@"dispatch_suspend(readSource)"); + + dispatch_suspend(readSource); + flags |= kReadSourceSuspended; + } +} + +- (void)resumeReadSource +{ + if (flags & kReadSourceSuspended) + { + LogVerbose(@"dispatch_resume(readSource)"); + + dispatch_resume(readSource); + flags &= ~kReadSourceSuspended; + } +} + +- (void)suspendWriteSource +{ + if (!(flags & kWriteSourceSuspended)) + { + LogVerbose(@"dispatch_suspend(writeSource)"); + + dispatch_suspend(writeSource); + flags |= kWriteSourceSuspended; + } +} + +- (void)resumeWriteSource +{ + if (flags & kWriteSourceSuspended) + { + LogVerbose(@"dispatch_resume(writeSource)"); + + dispatch_resume(writeSource); + flags &= ~kWriteSourceSuspended; + } +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Reading +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (void)readDataWithTimeout:(NSTimeInterval)timeout tag:(long)tag +{ + [self readDataWithTimeout:timeout buffer:nil bufferOffset:0 maxLength:0 tag:tag]; +} + +- (void)readDataWithTimeout:(NSTimeInterval)timeout + buffer:(NSMutableData *)buffer + bufferOffset:(NSUInteger)offset + tag:(long)tag +{ + [self readDataWithTimeout:timeout buffer:buffer bufferOffset:offset maxLength:0 tag:tag]; +} + +- (void)readDataWithTimeout:(NSTimeInterval)timeout + buffer:(NSMutableData *)buffer + bufferOffset:(NSUInteger)offset + maxLength:(NSUInteger)length + tag:(long)tag +{ + if (offset > [buffer length]) { + LogWarn(@"Cannot read: offset > [buffer length]"); + return; + } + + GCDAsyncReadPacket *packet = [[GCDAsyncReadPacket alloc] initWithData:buffer + startOffset:offset + maxLength:length + timeout:timeout + readLength:0 + terminator:nil + tag:tag]; + + dispatch_async(socketQueue, ^{ @autoreleasepool { + + LogTrace(); + + if ((flags & kSocketStarted) && !(flags & kForbidReadsWrites)) + { + [readQueue addObject:packet]; + [self maybeDequeueRead]; + } + }}); + + // Do not rely on the block being run in order to release the packet, + // as the queue might get released without the block completing. +} + +- (void)readDataToLength:(NSUInteger)length withTimeout:(NSTimeInterval)timeout tag:(long)tag +{ + [self readDataToLength:length withTimeout:timeout buffer:nil bufferOffset:0 tag:tag]; +} + +- (void)readDataToLength:(NSUInteger)length + withTimeout:(NSTimeInterval)timeout + buffer:(NSMutableData *)buffer + bufferOffset:(NSUInteger)offset + tag:(long)tag +{ + if (length == 0) { + LogWarn(@"Cannot read: length == 0"); + return; + } + if (offset > [buffer length]) { + LogWarn(@"Cannot read: offset > [buffer length]"); + return; + } + + GCDAsyncReadPacket *packet = [[GCDAsyncReadPacket alloc] initWithData:buffer + startOffset:offset + maxLength:0 + timeout:timeout + readLength:length + terminator:nil + tag:tag]; + + dispatch_async(socketQueue, ^{ @autoreleasepool { + + LogTrace(); + + if ((flags & kSocketStarted) && !(flags & kForbidReadsWrites)) + { + [readQueue addObject:packet]; + [self maybeDequeueRead]; + } + }}); + + // Do not rely on the block being run in order to release the packet, + // as the queue might get released without the block completing. +} + +- (void)readDataToData:(NSData *)data withTimeout:(NSTimeInterval)timeout tag:(long)tag +{ + [self readDataToData:data withTimeout:timeout buffer:nil bufferOffset:0 maxLength:0 tag:tag]; +} + +- (void)readDataToData:(NSData *)data + withTimeout:(NSTimeInterval)timeout + buffer:(NSMutableData *)buffer + bufferOffset:(NSUInteger)offset + tag:(long)tag +{ + [self readDataToData:data withTimeout:timeout buffer:buffer bufferOffset:offset maxLength:0 tag:tag]; +} + +- (void)readDataToData:(NSData *)data withTimeout:(NSTimeInterval)timeout maxLength:(NSUInteger)length tag:(long)tag +{ + [self readDataToData:data withTimeout:timeout buffer:nil bufferOffset:0 maxLength:length tag:tag]; +} + +- (void)readDataToData:(NSData *)data + withTimeout:(NSTimeInterval)timeout + buffer:(NSMutableData *)buffer + bufferOffset:(NSUInteger)offset + maxLength:(NSUInteger)maxLength + tag:(long)tag +{ + if ([data length] == 0) { + LogWarn(@"Cannot read: [data length] == 0"); + return; + } + if (offset > [buffer length]) { + LogWarn(@"Cannot read: offset > [buffer length]"); + return; + } + if (maxLength > 0 && maxLength < [data length]) { + LogWarn(@"Cannot read: maxLength > 0 && maxLength < [data length]"); + return; + } + + GCDAsyncReadPacket *packet = [[GCDAsyncReadPacket alloc] initWithData:buffer + startOffset:offset + maxLength:maxLength + timeout:timeout + readLength:0 + terminator:data + tag:tag]; + + dispatch_async(socketQueue, ^{ @autoreleasepool { + + LogTrace(); + + if ((flags & kSocketStarted) && !(flags & kForbidReadsWrites)) + { + [readQueue addObject:packet]; + [self maybeDequeueRead]; + } + }}); + + // Do not rely on the block being run in order to release the packet, + // as the queue might get released without the block completing. +} + +- (float)progressOfReadReturningTag:(long *)tagPtr bytesDone:(NSUInteger *)donePtr total:(NSUInteger *)totalPtr +{ + __block float result = 0.0F; + + dispatch_block_t block = ^{ + + if (!currentRead || ![currentRead isKindOfClass:[GCDAsyncReadPacket class]]) + { + // We're not reading anything right now. + + if (tagPtr != NULL) *tagPtr = 0; + if (donePtr != NULL) *donePtr = 0; + if (totalPtr != NULL) *totalPtr = 0; + + result = NAN; + } + else + { + // It's only possible to know the progress of our read if we're reading to a certain length. + // If we're reading to data, we of course have no idea when the data will arrive. + // If we're reading to timeout, then we have no idea when the next chunk of data will arrive. + + NSUInteger done = currentRead->bytesDone; + NSUInteger total = currentRead->readLength; + + if (tagPtr != NULL) *tagPtr = currentRead->tag; + if (donePtr != NULL) *donePtr = done; + if (totalPtr != NULL) *totalPtr = total; + + if (total > 0) + result = (float)done / (float)total; + else + result = 1.0F; + } + }; + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + block(); + else + dispatch_sync(socketQueue, block); + + return result; +} + +/** + * This method starts a new read, if needed. + * + * It is called when: + * - a user requests a read + * - after a read request has finished (to handle the next request) + * - immediately after the socket opens to handle any pending requests + * + * This method also handles auto-disconnect post read/write completion. + **/ +- (void)maybeDequeueRead +{ + LogTrace(); + NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue"); + + // If we're not currently processing a read AND we have an available read stream + if ((currentRead == nil) && (flags & kConnected)) + { + if ([readQueue count] > 0) + { + // Dequeue the next object in the write queue + currentRead = [readQueue objectAtIndex:0]; + [readQueue removeObjectAtIndex:0]; + + + if ([currentRead isKindOfClass:[GCDAsyncSpecialPacket class]]) + { + LogVerbose(@"Dequeued GCDAsyncSpecialPacket"); + + // Attempt to start TLS + flags |= kStartingReadTLS; + + // This method won't do anything unless both kStartingReadTLS and kStartingWriteTLS are set + [self maybeStartTLS]; + } + else + { + LogVerbose(@"Dequeued GCDAsyncReadPacket"); + + // Setup read timer (if needed) + [self setupReadTimerWithTimeout:currentRead->timeout]; + + // Immediately read, if possible + [self doReadData]; + } + } + else if (flags & kDisconnectAfterReads) + { + if (flags & kDisconnectAfterWrites) + { + if (([writeQueue count] == 0) && (currentWrite == nil)) + { + [self closeWithError:nil]; + } + } + else + { + [self closeWithError:nil]; + } + } + else if (flags & kSocketSecure) + { + [self flushSSLBuffers]; + + // Edge case: + // + // We just drained all data from the ssl buffers, + // and all known data from the socket (socketFDBytesAvailable). + // + // If we didn't get any data from this process, + // then we may have reached the end of the TCP stream. + // + // Be sure callbacks are enabled so we're notified about a disconnection. + + if ([preBuffer availableBytes] == 0) + { + if ([self usingCFStreamForTLS]) { + // Callbacks never disabled + } + else { + [self resumeReadSource]; + } + } + } + } +} + +- (void)flushSSLBuffers +{ + LogTrace(); + + NSAssert((flags & kSocketSecure), @"Cannot flush ssl buffers on non-secure socket"); + + if ([preBuffer availableBytes] > 0) + { + // Only flush the ssl buffers if the prebuffer is empty. + // This is to avoid growing the prebuffer inifinitely large. + + return; + } + +#if TARGET_OS_IPHONE + + if ([self usingCFStreamForTLS]) + { + if ((flags & kSecureSocketHasBytesAvailable) && CFReadStreamHasBytesAvailable(readStream)) + { + LogVerbose(@"%@ - Flushing ssl buffers into prebuffer...", THIS_METHOD); + + CFIndex defaultBytesToRead = (1024 * 4); + + [preBuffer ensureCapacityForWrite:defaultBytesToRead]; + + uint8_t *buffer = [preBuffer writeBuffer]; + + CFIndex result = CFReadStreamRead(readStream, buffer, defaultBytesToRead); + LogVerbose(@"%@ - CFReadStreamRead(): result = %i", THIS_METHOD, (int)result); + + if (result > 0) + { + [preBuffer didWrite:result]; + } + + flags &= ~kSecureSocketHasBytesAvailable; + } + + return; + } + +#endif + + __block NSUInteger estimatedBytesAvailable = 0; + + dispatch_block_t updateEstimatedBytesAvailable = ^{ + + // Figure out if there is any data available to be read + // + // socketFDBytesAvailable <- Number of encrypted bytes we haven't read from the bsd socket + // [sslPreBuffer availableBytes] <- Number of encrypted bytes we've buffered from bsd socket + // sslInternalBufSize <- Number of decrypted bytes SecureTransport has buffered + // + // We call the variable "estimated" because we don't know how many decrypted bytes we'll get + // from the encrypted bytes in the sslPreBuffer. + // However, we do know this is an upper bound on the estimation. + + estimatedBytesAvailable = socketFDBytesAvailable + [sslPreBuffer availableBytes]; + + size_t sslInternalBufSize = 0; + SSLGetBufferedReadSize(sslContext, &sslInternalBufSize); + + estimatedBytesAvailable += sslInternalBufSize; + }; + + updateEstimatedBytesAvailable(); + + if (estimatedBytesAvailable > 0) + { + LogVerbose(@"%@ - Flushing ssl buffers into prebuffer...", THIS_METHOD); + + BOOL done = NO; + do + { + LogVerbose(@"%@ - estimatedBytesAvailable = %lu", THIS_METHOD, (unsigned long)estimatedBytesAvailable); + + // Make sure there's enough room in the prebuffer + + [preBuffer ensureCapacityForWrite:estimatedBytesAvailable]; + + // Read data into prebuffer + + uint8_t *buffer = [preBuffer writeBuffer]; + size_t bytesRead = 0; + + OSStatus result = SSLRead(sslContext, buffer, (size_t)estimatedBytesAvailable, &bytesRead); + LogVerbose(@"%@ - read from secure socket = %u", THIS_METHOD, (unsigned)bytesRead); + + if (bytesRead > 0) + { + [preBuffer didWrite:bytesRead]; + } + + LogVerbose(@"%@ - prebuffer.length = %zu", THIS_METHOD, [preBuffer availableBytes]); + + if (result != noErr) + { + done = YES; + } + else + { + updateEstimatedBytesAvailable(); + } + + } while (!done && estimatedBytesAvailable > 0); + } +} + +- (void)doReadData +{ + LogTrace(); + + // This method is called on the socketQueue. + // It might be called directly, or via the readSource when data is available to be read. + + if ((currentRead == nil) || (flags & kReadsPaused)) + { + LogVerbose(@"No currentRead or kReadsPaused"); + + // Unable to read at this time + + if (flags & kSocketSecure) + { + // Here's the situation: + // + // We have an established secure connection. + // There may not be a currentRead, but there might be encrypted data sitting around for us. + // When the user does get around to issuing a read, that encrypted data will need to be decrypted. + // + // So why make the user wait? + // We might as well get a head start on decrypting some data now. + // + // The other reason we do this has to do with detecting a socket disconnection. + // The SSL/TLS protocol has it's own disconnection handshake. + // So when a secure socket is closed, a "goodbye" packet comes across the wire. + // We want to make sure we read the "goodbye" packet so we can properly detect the TCP disconnection. + + [self flushSSLBuffers]; + } + + if ([self usingCFStreamForTLS]) + { + // CFReadStream only fires once when there is available data. + // It won't fire again until we've invoked CFReadStreamRead. + } + else + { + // If the readSource is firing, we need to pause it + // or else it will continue to fire over and over again. + // + // If the readSource is not firing, + // we want it to continue monitoring the socket. + + if (socketFDBytesAvailable > 0) + { + [self suspendReadSource]; + } + } + return; + } + + BOOL hasBytesAvailable = NO; + unsigned long estimatedBytesAvailable = 0; + + if ([self usingCFStreamForTLS]) + { +#if TARGET_OS_IPHONE + + // Requested CFStream, rather than SecureTransport, for TLS (via GCDAsyncSocketUseCFStreamForTLS) + + estimatedBytesAvailable = 0; + if ((flags & kSecureSocketHasBytesAvailable) && CFReadStreamHasBytesAvailable(readStream)) + hasBytesAvailable = YES; + else + hasBytesAvailable = NO; + +#endif + } + else + { + estimatedBytesAvailable = socketFDBytesAvailable; + + if (flags & kSocketSecure) + { + // There are 2 buffers to be aware of here. + // + // We are using SecureTransport, a TLS/SSL security layer which sits atop TCP. + // We issue a read to the SecureTranport API, which in turn issues a read to our SSLReadFunction. + // Our SSLReadFunction then reads from the BSD socket and returns the encrypted data to SecureTransport. + // SecureTransport then decrypts the data, and finally returns the decrypted data back to us. + // + // The first buffer is one we create. + // SecureTransport often requests small amounts of data. + // This has to do with the encypted packets that are coming across the TCP stream. + // But it's non-optimal to do a bunch of small reads from the BSD socket. + // So our SSLReadFunction reads all available data from the socket (optimizing the sys call) + // and may store excess in the sslPreBuffer. + + estimatedBytesAvailable += [sslPreBuffer availableBytes]; + + // The second buffer is within SecureTransport. + // As mentioned earlier, there are encrypted packets coming across the TCP stream. + // SecureTransport needs the entire packet to decrypt it. + // But if the entire packet produces X bytes of decrypted data, + // and we only asked SecureTransport for X/2 bytes of data, + // it must store the extra X/2 bytes of decrypted data for the next read. + // + // The SSLGetBufferedReadSize function will tell us the size of this internal buffer. + // From the documentation: + // + // "This function does not block or cause any low-level read operations to occur." + + size_t sslInternalBufSize = 0; + SSLGetBufferedReadSize(sslContext, &sslInternalBufSize); + + estimatedBytesAvailable += sslInternalBufSize; + } + + hasBytesAvailable = (estimatedBytesAvailable > 0); + } + + if ((hasBytesAvailable == NO) && ([preBuffer availableBytes] == 0)) + { + LogVerbose(@"No data available to read..."); + + // No data available to read. + + if (![self usingCFStreamForTLS]) + { + // Need to wait for readSource to fire and notify us of + // available data in the socket's internal read buffer. + + [self resumeReadSource]; + } + return; + } + + if (flags & kStartingReadTLS) + { + LogVerbose(@"Waiting for SSL/TLS handshake to complete"); + + // The readQueue is waiting for SSL/TLS handshake to complete. + + if (flags & kStartingWriteTLS) + { + if ([self usingSecureTransportForTLS] && lastSSLHandshakeError == errSSLWouldBlock) + { + // We are in the process of a SSL Handshake. + // We were waiting for incoming data which has just arrived. + + [self ssl_continueSSLHandshake]; + } + } + else + { + // We are still waiting for the writeQueue to drain and start the SSL/TLS process. + // We now know data is available to read. + + if (![self usingCFStreamForTLS]) + { + // Suspend the read source or else it will continue to fire nonstop. + + [self suspendReadSource]; + } + } + + return; + } + + BOOL done = NO; // Completed read operation + NSError *error = nil; // Error occurred + + NSUInteger totalBytesReadForCurrentRead = 0; + + // + // STEP 1 - READ FROM PREBUFFER + // + + if ([preBuffer availableBytes] > 0) + { + // There are 3 types of read packets: + // + // 1) Read all available data. + // 2) Read a specific length of data. + // 3) Read up to a particular terminator. + + NSUInteger bytesToCopy; + + if (currentRead->term != nil) + { + // Read type #3 - read up to a terminator + + bytesToCopy = [currentRead readLengthForTermWithPreBuffer:preBuffer found:&done]; + } + else + { + // Read type #1 or #2 + + bytesToCopy = [currentRead readLengthForNonTermWithHint:[preBuffer availableBytes]]; + } + + // Make sure we have enough room in the buffer for our read. + + [currentRead ensureCapacityForAdditionalDataOfLength:bytesToCopy]; + + // Copy bytes from prebuffer into packet buffer + + uint8_t *buffer = (uint8_t *)[currentRead->buffer mutableBytes] + currentRead->startOffset + + currentRead->bytesDone; + + memcpy(buffer, [preBuffer readBuffer], bytesToCopy); + + // Remove the copied bytes from the preBuffer + [preBuffer didRead:bytesToCopy]; + + LogVerbose(@"copied(%lu) preBufferLength(%zu)", (unsigned long)bytesToCopy, [preBuffer availableBytes]); + + // Update totals + + currentRead->bytesDone += bytesToCopy; + totalBytesReadForCurrentRead += bytesToCopy; + + // Check to see if the read operation is done + + if (currentRead->readLength > 0) + { + // Read type #2 - read a specific length of data + + done = (currentRead->bytesDone == currentRead->readLength); + } + else if (currentRead->term != nil) + { + // Read type #3 - read up to a terminator + + // Our 'done' variable was updated via the readLengthForTermWithPreBuffer:found: method + + if (!done && currentRead->maxLength > 0) + { + // We're not done and there's a set maxLength. + // Have we reached that maxLength yet? + + if (currentRead->bytesDone >= currentRead->maxLength) + { + error = [self readMaxedOutError]; + } + } + } + else + { + // Read type #1 - read all available data + // + // We're done as soon as + // - we've read all available data (in prebuffer and socket) + // - we've read the maxLength of read packet. + + done = ((currentRead->maxLength > 0) && (currentRead->bytesDone == currentRead->maxLength)); + } + + } + + // + // STEP 2 - READ FROM SOCKET + // + + BOOL socketEOF = (flags & kSocketHasReadEOF) ? YES : NO; // Nothing more to read via socket (end of file) + BOOL waiting = !done && !error && !socketEOF && !hasBytesAvailable; // Ran out of data, waiting for more + + if (!done && !error && !socketEOF && hasBytesAvailable) + { + NSAssert(([preBuffer availableBytes] == 0), @"Invalid logic"); + + BOOL readIntoPreBuffer = NO; + uint8_t *buffer = NULL; + size_t bytesRead = 0; + + if (flags & kSocketSecure) + { + if ([self usingCFStreamForTLS]) + { +#if TARGET_OS_IPHONE + + // Using CFStream, rather than SecureTransport, for TLS + + NSUInteger defaultReadLength = (1024 * 32); + + NSUInteger bytesToRead = [currentRead optimalReadLengthWithDefault:defaultReadLength + shouldPreBuffer:&readIntoPreBuffer]; + + // Make sure we have enough room in the buffer for our read. + // + // We are either reading directly into the currentRead->buffer, + // or we're reading into the temporary preBuffer. + + if (readIntoPreBuffer) + { + [preBuffer ensureCapacityForWrite:bytesToRead]; + + buffer = [preBuffer writeBuffer]; + } + else + { + [currentRead ensureCapacityForAdditionalDataOfLength:bytesToRead]; + + buffer = (uint8_t *)[currentRead->buffer mutableBytes] + + currentRead->startOffset + + currentRead->bytesDone; + } + + // Read data into buffer + + CFIndex result = CFReadStreamRead(readStream, buffer, (CFIndex)bytesToRead); + LogVerbose(@"CFReadStreamRead(): result = %i", (int)result); + + if (result < 0) + { + error = (__bridge_transfer NSError *)CFReadStreamCopyError(readStream); + } + else if (result == 0) + { + socketEOF = YES; + } + else + { + waiting = YES; + bytesRead = (size_t)result; + } + + // We only know how many decrypted bytes were read. + // The actual number of bytes read was likely more due to the overhead of the encryption. + // So we reset our flag, and rely on the next callback to alert us of more data. + flags &= ~kSecureSocketHasBytesAvailable; + +#endif + } + else + { + // Using SecureTransport for TLS + // + // We know: + // - how many bytes are available on the socket + // - how many encrypted bytes are sitting in the sslPreBuffer + // - how many decypted bytes are sitting in the sslContext + // + // But we do NOT know: + // - how many encypted bytes are sitting in the sslContext + // + // So we play the regular game of using an upper bound instead. + + NSUInteger defaultReadLength = (1024 * 32); + + if (defaultReadLength < estimatedBytesAvailable) { + defaultReadLength = estimatedBytesAvailable + (1024 * 16); + } + + NSUInteger bytesToRead = [currentRead optimalReadLengthWithDefault:defaultReadLength + shouldPreBuffer:&readIntoPreBuffer]; + + if (bytesToRead > SIZE_MAX) { // NSUInteger may be bigger than size_t + bytesToRead = SIZE_MAX; + } + + // Make sure we have enough room in the buffer for our read. + // + // We are either reading directly into the currentRead->buffer, + // or we're reading into the temporary preBuffer. + + if (readIntoPreBuffer) + { + [preBuffer ensureCapacityForWrite:bytesToRead]; + + buffer = [preBuffer writeBuffer]; + } + else + { + [currentRead ensureCapacityForAdditionalDataOfLength:bytesToRead]; + + buffer = (uint8_t *)[currentRead->buffer mutableBytes] + + currentRead->startOffset + + currentRead->bytesDone; + } + + // The documentation from Apple states: + // + // "a read operation might return errSSLWouldBlock, + // indicating that less data than requested was actually transferred" + // + // However, starting around 10.7, the function will sometimes return noErr, + // even if it didn't read as much data as requested. So we need to watch out for that. + + OSStatus result; + do + { + void *loop_buffer = buffer + bytesRead; + size_t loop_bytesToRead = (size_t)bytesToRead - bytesRead; + size_t loop_bytesRead = 0; + + result = SSLRead(sslContext, loop_buffer, loop_bytesToRead, &loop_bytesRead); + LogVerbose(@"read from secure socket = %u", (unsigned)loop_bytesRead); + + bytesRead += loop_bytesRead; + + } while ((result == noErr) && (bytesRead < bytesToRead)); + + + if (result != noErr) + { + if (result == errSSLWouldBlock) + waiting = YES; + else + { + if (result == errSSLClosedGraceful || result == errSSLClosedAbort) + { + // We've reached the end of the stream. + // Handle this the same way we would an EOF from the socket. + socketEOF = YES; + sslErrCode = result; + } + else + { + error = [self sslError:result]; + } + } + // It's possible that bytesRead > 0, even if the result was errSSLWouldBlock. + // This happens when the SSLRead function is able to read some data, + // but not the entire amount we requested. + + if (bytesRead <= 0) + { + bytesRead = 0; + } + } + + // Do not modify socketFDBytesAvailable. + // It will be updated via the SSLReadFunction(). + } + } + else + { + // Normal socket operation + + NSUInteger bytesToRead; + + // There are 3 types of read packets: + // + // 1) Read all available data. + // 2) Read a specific length of data. + // 3) Read up to a particular terminator. + + if (currentRead->term != nil) + { + // Read type #3 - read up to a terminator + + bytesToRead = [currentRead readLengthForTermWithHint:estimatedBytesAvailable + shouldPreBuffer:&readIntoPreBuffer]; + } + else + { + // Read type #1 or #2 + + bytesToRead = [currentRead readLengthForNonTermWithHint:estimatedBytesAvailable]; + } + + if (bytesToRead > SIZE_MAX) { // NSUInteger may be bigger than size_t (read param 3) + bytesToRead = SIZE_MAX; + } + + // Make sure we have enough room in the buffer for our read. + // + // We are either reading directly into the currentRead->buffer, + // or we're reading into the temporary preBuffer. + + if (readIntoPreBuffer) + { + [preBuffer ensureCapacityForWrite:bytesToRead]; + + buffer = [preBuffer writeBuffer]; + } + else + { + [currentRead ensureCapacityForAdditionalDataOfLength:bytesToRead]; + + buffer = (uint8_t *)[currentRead->buffer mutableBytes] + + currentRead->startOffset + + currentRead->bytesDone; + } + + // Read data into buffer + + int socketFD = (socket4FD != SOCKET_NULL) ? socket4FD : (socket6FD != SOCKET_NULL) ? socket6FD : socketUN; + + ssize_t result = read(socketFD, buffer, (size_t)bytesToRead); + LogVerbose(@"read from socket = %i", (int)result); + + if (result < 0) + { + if (errno == EWOULDBLOCK) + waiting = YES; + else + error = [self errnoErrorWithReason:@"Error in read() function"]; + + socketFDBytesAvailable = 0; + } + else if (result == 0) + { + socketEOF = YES; + socketFDBytesAvailable = 0; + } + else + { + bytesRead = result; + + if (bytesRead < bytesToRead) + { + // The read returned less data than requested. + // This means socketFDBytesAvailable was a bit off due to timing, + // because we read from the socket right when the readSource event was firing. + socketFDBytesAvailable = 0; + } + else + { + if (socketFDBytesAvailable <= bytesRead) + socketFDBytesAvailable = 0; + else + socketFDBytesAvailable -= bytesRead; + } + + if (socketFDBytesAvailable == 0) + { + waiting = YES; + } + } + } + + if (bytesRead > 0) + { + // Check to see if the read operation is done + + if (currentRead->readLength > 0) + { + // Read type #2 - read a specific length of data + // + // Note: We should never be using a prebuffer when we're reading a specific length of data. + + NSAssert(readIntoPreBuffer == NO, @"Invalid logic"); + + currentRead->bytesDone += bytesRead; + totalBytesReadForCurrentRead += bytesRead; + + done = (currentRead->bytesDone == currentRead->readLength); + } + else if (currentRead->term != nil) + { + // Read type #3 - read up to a terminator + + if (readIntoPreBuffer) + { + // We just read a big chunk of data into the preBuffer + + [preBuffer didWrite:bytesRead]; + LogVerbose(@"read data into preBuffer - preBuffer.length = %zu", [preBuffer availableBytes]); + + // Search for the terminating sequence + + NSUInteger bytesToCopy = [currentRead readLengthForTermWithPreBuffer:preBuffer found:&done]; + LogVerbose(@"copying %lu bytes from preBuffer", (unsigned long)bytesToCopy); + + // Ensure there's room on the read packet's buffer + + [currentRead ensureCapacityForAdditionalDataOfLength:bytesToCopy]; + + // Copy bytes from prebuffer into read buffer + + uint8_t *readBuf = (uint8_t *)[currentRead->buffer mutableBytes] + currentRead->startOffset + + currentRead->bytesDone; + + memcpy(readBuf, [preBuffer readBuffer], bytesToCopy); + + // Remove the copied bytes from the prebuffer + [preBuffer didRead:bytesToCopy]; + LogVerbose(@"preBuffer.length = %zu", [preBuffer availableBytes]); + + // Update totals + currentRead->bytesDone += bytesToCopy; + totalBytesReadForCurrentRead += bytesToCopy; + + // Our 'done' variable was updated via the readLengthForTermWithPreBuffer:found: method above + } + else + { + // We just read a big chunk of data directly into the packet's buffer. + // We need to move any overflow into the prebuffer. + + NSInteger overflow = [currentRead searchForTermAfterPreBuffering:bytesRead]; + + if (overflow == 0) + { + // Perfect match! + // Every byte we read stays in the read buffer, + // and the last byte we read was the last byte of the term. + + currentRead->bytesDone += bytesRead; + totalBytesReadForCurrentRead += bytesRead; + done = YES; + } + else if (overflow > 0) + { + // The term was found within the data that we read, + // and there are extra bytes that extend past the end of the term. + // We need to move these excess bytes out of the read packet and into the prebuffer. + + NSInteger underflow = bytesRead - overflow; + + // Copy excess data into preBuffer + + LogVerbose(@"copying %ld overflow bytes into preBuffer", (long)overflow); + [preBuffer ensureCapacityForWrite:overflow]; + + uint8_t *overflowBuffer = buffer + underflow; + memcpy([preBuffer writeBuffer], overflowBuffer, overflow); + + [preBuffer didWrite:overflow]; + LogVerbose(@"preBuffer.length = %zu", [preBuffer availableBytes]); + + // Note: The completeCurrentRead method will trim the buffer for us. + + currentRead->bytesDone += underflow; + totalBytesReadForCurrentRead += underflow; + done = YES; + } + else + { + // The term was not found within the data that we read. + + currentRead->bytesDone += bytesRead; + totalBytesReadForCurrentRead += bytesRead; + done = NO; + } + } + + if (!done && currentRead->maxLength > 0) + { + // We're not done and there's a set maxLength. + // Have we reached that maxLength yet? + + if (currentRead->bytesDone >= currentRead->maxLength) + { + error = [self readMaxedOutError]; + } + } + } + else + { + // Read type #1 - read all available data + + if (readIntoPreBuffer) + { + // We just read a chunk of data into the preBuffer + + [preBuffer didWrite:bytesRead]; + + // Now copy the data into the read packet. + // + // Recall that we didn't read directly into the packet's buffer to avoid + // over-allocating memory since we had no clue how much data was available to be read. + // + // Ensure there's room on the read packet's buffer + + [currentRead ensureCapacityForAdditionalDataOfLength:bytesRead]; + + // Copy bytes from prebuffer into read buffer + + uint8_t *readBuf = (uint8_t *)[currentRead->buffer mutableBytes] + currentRead->startOffset + + currentRead->bytesDone; + + memcpy(readBuf, [preBuffer readBuffer], bytesRead); + + // Remove the copied bytes from the prebuffer + [preBuffer didRead:bytesRead]; + + // Update totals + currentRead->bytesDone += bytesRead; + totalBytesReadForCurrentRead += bytesRead; + } + else + { + currentRead->bytesDone += bytesRead; + totalBytesReadForCurrentRead += bytesRead; + } + + done = YES; + } + + } // if (bytesRead > 0) + + } // if (!done && !error && !socketEOF && hasBytesAvailable) + + + if (!done && currentRead->readLength == 0 && currentRead->term == nil) + { + // Read type #1 - read all available data + // + // We might arrive here if we read data from the prebuffer but not from the socket. + + done = (totalBytesReadForCurrentRead > 0); + } + + // Check to see if we're done, or if we've made progress + + if (done) + { + [self completeCurrentRead]; + + if (!error && (!socketEOF || [preBuffer availableBytes] > 0)) + { + [self maybeDequeueRead]; + } + } + else if (totalBytesReadForCurrentRead > 0) + { + // We're not done read type #2 or #3 yet, but we have read in some bytes + + __strong id theDelegate = delegate; + + if (delegateQueue && [theDelegate respondsToSelector:@selector(socket:didReadPartialDataOfLength:tag:)]) + { + long theReadTag = currentRead->tag; + + dispatch_async(delegateQueue, ^{ @autoreleasepool { + + [theDelegate socket:self didReadPartialDataOfLength:totalBytesReadForCurrentRead tag:theReadTag]; + }}); + } + } + + // Check for errors + + if (error) + { + [self closeWithError:error]; + } + else if (socketEOF) + { + [self doReadEOF]; + } + else if (waiting) + { + if (![self usingCFStreamForTLS]) + { + // Monitor the socket for readability (if we're not already doing so) + [self resumeReadSource]; + } + } + + // Do not add any code here without first adding return statements in the error cases above. +} + +- (void)doReadEOF +{ + LogTrace(); + + // This method may be called more than once. + // If the EOF is read while there is still data in the preBuffer, + // then this method may be called continually after invocations of doReadData to see if it's time to disconnect. + + flags |= kSocketHasReadEOF; + + if (flags & kSocketSecure) + { + // If the SSL layer has any buffered data, flush it into the preBuffer now. + + [self flushSSLBuffers]; + } + + BOOL shouldDisconnect = NO; + NSError *error = nil; + + if ((flags & kStartingReadTLS) || (flags & kStartingWriteTLS)) + { + // We received an EOF during or prior to startTLS. + // The SSL/TLS handshake is now impossible, so this is an unrecoverable situation. + + shouldDisconnect = YES; + + if ([self usingSecureTransportForTLS]) + { + error = [self sslError:errSSLClosedAbort]; + } + } + else if (flags & kReadStreamClosed) + { + // The preBuffer has already been drained. + // The config allows half-duplex connections. + // We've previously checked the socket, and it appeared writeable. + // So we marked the read stream as closed and notified the delegate. + // + // As per the half-duplex contract, the socket will be closed when a write fails, + // or when the socket is manually closed. + + shouldDisconnect = NO; + } + else if ([preBuffer availableBytes] > 0) + { + LogVerbose(@"Socket reached EOF, but there is still data available in prebuffer"); + + // Although we won't be able to read any more data from the socket, + // there is existing data that has been prebuffered that we can read. + + shouldDisconnect = NO; + } + else if (config & kAllowHalfDuplexConnection) + { + // We just received an EOF (end of file) from the socket's read stream. + // This means the remote end of the socket (the peer we're connected to) + // has explicitly stated that it will not be sending us any more data. + // + // Query the socket to see if it is still writeable. (Perhaps the peer will continue reading data from us) + + int socketFD = (socket4FD != SOCKET_NULL) ? socket4FD : (socket6FD != SOCKET_NULL) ? socket6FD : socketUN; + + struct pollfd pfd[1]; + pfd[0].fd = socketFD; + pfd[0].events = POLLOUT; + pfd[0].revents = 0; + + poll(pfd, 1, 0); + + if (pfd[0].revents & POLLOUT) + { + // Socket appears to still be writeable + + shouldDisconnect = NO; + flags |= kReadStreamClosed; + + // Notify the delegate that we're going half-duplex + + __strong id theDelegate = delegate; + + if (delegateQueue && [theDelegate respondsToSelector:@selector(socketDidCloseReadStream:)]) + { + dispatch_async(delegateQueue, ^{ @autoreleasepool { + + [theDelegate socketDidCloseReadStream:self]; + }}); + } + } + else + { + shouldDisconnect = YES; + } + } + else + { + shouldDisconnect = YES; + } + + + if (shouldDisconnect) + { + if (error == nil) + { + if ([self usingSecureTransportForTLS]) + { + if (sslErrCode != noErr && sslErrCode != errSSLClosedGraceful) + { + error = [self sslError:sslErrCode]; + } + else + { + error = [self connectionClosedError]; + } + } + else + { + error = [self connectionClosedError]; + } + } + [self closeWithError:error]; + } + else + { + if (![self usingCFStreamForTLS]) + { + // Suspend the read source (if needed) + + [self suspendReadSource]; + } + } +} + +- (void)completeCurrentRead +{ + LogTrace(); + + NSAssert(currentRead, @"Trying to complete current read when there is no current read."); + + + NSData *result = nil; + + if (currentRead->bufferOwner) + { + // We created the buffer on behalf of the user. + // Trim our buffer to be the proper size. + [currentRead->buffer setLength:currentRead->bytesDone]; + + result = currentRead->buffer; + } + else + { + // We did NOT create the buffer. + // The buffer is owned by the caller. + // Only trim the buffer if we had to increase its size. + + if ([currentRead->buffer length] > currentRead->originalBufferLength) + { + NSUInteger readSize = currentRead->startOffset + currentRead->bytesDone; + NSUInteger origSize = currentRead->originalBufferLength; + + NSUInteger buffSize = MAX(readSize, origSize); + + [currentRead->buffer setLength:buffSize]; + } + + uint8_t *buffer = (uint8_t *)[currentRead->buffer mutableBytes] + currentRead->startOffset; + + result = [NSData dataWithBytesNoCopy:buffer length:currentRead->bytesDone freeWhenDone:NO]; + } + + __strong id theDelegate = delegate; + + if (delegateQueue && [theDelegate respondsToSelector:@selector(socket:didReadData:withTag:)]) + { + GCDAsyncReadPacket *theRead = currentRead; // Ensure currentRead retained since result may not own buffer + + dispatch_async(delegateQueue, ^{ @autoreleasepool { + + [theDelegate socket:self didReadData:result withTag:theRead->tag]; + }}); + } + + [self endCurrentRead]; +} + +- (void)endCurrentRead +{ + if (readTimer) + { + dispatch_source_cancel(readTimer); + readTimer = NULL; + } + + currentRead = nil; +} + +- (void)setupReadTimerWithTimeout:(NSTimeInterval)timeout +{ + if (timeout >= 0.0) + { + readTimer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, socketQueue); + + __weak GCDAsyncSocket *weakSelf = self; + + dispatch_source_set_event_handler(readTimer, ^{ @autoreleasepool { +#pragma clang diagnostic push +#pragma clang diagnostic warning "-Wimplicit-retain-self" + + __strong GCDAsyncSocket *strongSelf = weakSelf; + if (strongSelf == nil) return_from_block; + + [strongSelf doReadTimeout]; + +#pragma clang diagnostic pop + }}); + +#if !OS_OBJECT_USE_OBJC + dispatch_source_t theReadTimer = readTimer; + dispatch_source_set_cancel_handler(readTimer, ^{ +#pragma clang diagnostic push +#pragma clang diagnostic warning "-Wimplicit-retain-self" + + LogVerbose(@"dispatch_release(readTimer)"); + dispatch_release(theReadTimer); + +#pragma clang diagnostic pop + }); +#endif + + dispatch_time_t tt = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(timeout * NSEC_PER_SEC)); + + dispatch_source_set_timer(readTimer, tt, DISPATCH_TIME_FOREVER, 0); + dispatch_resume(readTimer); + } +} + +- (void)doReadTimeout +{ + // This is a little bit tricky. + // Ideally we'd like to synchronously query the delegate about a timeout extension. + // But if we do so synchronously we risk a possible deadlock. + // So instead we have to do so asynchronously, and callback to ourselves from within the delegate block. + + flags |= kReadsPaused; + + __strong id theDelegate = delegate; + + if (delegateQueue && [theDelegate respondsToSelector:@selector(socket:shouldTimeoutReadWithTag:elapsed:bytesDone:)]) + { + GCDAsyncReadPacket *theRead = currentRead; + + dispatch_async(delegateQueue, ^{ @autoreleasepool { + + NSTimeInterval timeoutExtension = 0.0; + + timeoutExtension = [theDelegate socket:self shouldTimeoutReadWithTag:theRead->tag + elapsed:theRead->timeout + bytesDone:theRead->bytesDone]; + + dispatch_async(socketQueue, ^{ @autoreleasepool { + + [self doReadTimeoutWithExtension:timeoutExtension]; + }}); + }}); + } + else + { + [self doReadTimeoutWithExtension:0.0]; + } +} + +- (void)doReadTimeoutWithExtension:(NSTimeInterval)timeoutExtension +{ + if (currentRead) + { + if (timeoutExtension > 0.0) + { + currentRead->timeout += timeoutExtension; + + // Reschedule the timer + dispatch_time_t tt = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(timeoutExtension * NSEC_PER_SEC)); + dispatch_source_set_timer(readTimer, tt, DISPATCH_TIME_FOREVER, 0); + + // Unpause reads, and continue + flags &= ~kReadsPaused; + [self doReadData]; + } + else + { + LogVerbose(@"ReadTimeout"); + + [self closeWithError:[self readTimeoutError]]; + } + } +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Writing +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (void)writeData:(NSData *)data withTimeout:(NSTimeInterval)timeout tag:(long)tag +{ + if ([data length] == 0) return; + + GCDAsyncWritePacket *packet = [[GCDAsyncWritePacket alloc] initWithData:data timeout:timeout tag:tag]; + + dispatch_async(socketQueue, ^{ @autoreleasepool { + + LogTrace(); + + if ((flags & kSocketStarted) && !(flags & kForbidReadsWrites)) + { + [writeQueue addObject:packet]; + [self maybeDequeueWrite]; + } + }}); + + // Do not rely on the block being run in order to release the packet, + // as the queue might get released without the block completing. +} + +- (float)progressOfWriteReturningTag:(long *)tagPtr bytesDone:(NSUInteger *)donePtr total:(NSUInteger *)totalPtr +{ + __block float result = 0.0F; + + dispatch_block_t block = ^{ + + if (!currentWrite || ![currentWrite isKindOfClass:[GCDAsyncWritePacket class]]) + { + // We're not writing anything right now. + + if (tagPtr != NULL) *tagPtr = 0; + if (donePtr != NULL) *donePtr = 0; + if (totalPtr != NULL) *totalPtr = 0; + + result = NAN; + } + else + { + NSUInteger done = currentWrite->bytesDone; + NSUInteger total = [currentWrite->buffer length]; + + if (tagPtr != NULL) *tagPtr = currentWrite->tag; + if (donePtr != NULL) *donePtr = done; + if (totalPtr != NULL) *totalPtr = total; + + result = (float)done / (float)total; + } + }; + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + block(); + else + dispatch_sync(socketQueue, block); + + return result; +} + +/** + * Conditionally starts a new write. + * + * It is called when: + * - a user requests a write + * - after a write request has finished (to handle the next request) + * - immediately after the socket opens to handle any pending requests + * + * This method also handles auto-disconnect post read/write completion. + **/ +- (void)maybeDequeueWrite +{ + LogTrace(); + NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue"); + + + // If we're not currently processing a write AND we have an available write stream + if ((currentWrite == nil) && (flags & kConnected)) + { + if ([writeQueue count] > 0) + { + // Dequeue the next object in the write queue + currentWrite = [writeQueue objectAtIndex:0]; + [writeQueue removeObjectAtIndex:0]; + + + if ([currentWrite isKindOfClass:[GCDAsyncSpecialPacket class]]) + { + LogVerbose(@"Dequeued GCDAsyncSpecialPacket"); + + // Attempt to start TLS + flags |= kStartingWriteTLS; + + // This method won't do anything unless both kStartingReadTLS and kStartingWriteTLS are set + [self maybeStartTLS]; + } + else + { + LogVerbose(@"Dequeued GCDAsyncWritePacket"); + + // Setup write timer (if needed) + [self setupWriteTimerWithTimeout:currentWrite->timeout]; + + // Immediately write, if possible + [self doWriteData]; + } + } + else if (flags & kDisconnectAfterWrites) + { + if (flags & kDisconnectAfterReads) + { + if (([readQueue count] == 0) && (currentRead == nil)) + { + [self closeWithError:nil]; + } + } + else + { + [self closeWithError:nil]; + } + } + } +} + +- (void)doWriteData +{ + LogTrace(); + + // This method is called by the writeSource via the socketQueue + + if ((currentWrite == nil) || (flags & kWritesPaused)) + { + LogVerbose(@"No currentWrite or kWritesPaused"); + + // Unable to write at this time + + if ([self usingCFStreamForTLS]) + { + // CFWriteStream only fires once when there is available data. + // It won't fire again until we've invoked CFWriteStreamWrite. + } + else + { + // If the writeSource is firing, we need to pause it + // or else it will continue to fire over and over again. + + if (flags & kSocketCanAcceptBytes) + { + [self suspendWriteSource]; + } + } + return; + } + + if (!(flags & kSocketCanAcceptBytes)) + { + LogVerbose(@"No space available to write..."); + + // No space available to write. + + if (![self usingCFStreamForTLS]) + { + // Need to wait for writeSource to fire and notify us of + // available space in the socket's internal write buffer. + + [self resumeWriteSource]; + } + return; + } + + if (flags & kStartingWriteTLS) + { + LogVerbose(@"Waiting for SSL/TLS handshake to complete"); + + // The writeQueue is waiting for SSL/TLS handshake to complete. + + if (flags & kStartingReadTLS) + { + if ([self usingSecureTransportForTLS] && lastSSLHandshakeError == errSSLWouldBlock) + { + // We are in the process of a SSL Handshake. + // We were waiting for available space in the socket's internal OS buffer to continue writing. + + [self ssl_continueSSLHandshake]; + } + } + else + { + // We are still waiting for the readQueue to drain and start the SSL/TLS process. + // We now know we can write to the socket. + + if (![self usingCFStreamForTLS]) + { + // Suspend the write source or else it will continue to fire nonstop. + + [self suspendWriteSource]; + } + } + + return; + } + + // Note: This method is not called if currentWrite is a GCDAsyncSpecialPacket (startTLS packet) + + BOOL waiting = NO; + NSError *error = nil; + size_t bytesWritten = 0; + + if (flags & kSocketSecure) + { + if ([self usingCFStreamForTLS]) + { +#if TARGET_OS_IPHONE + + // + // Writing data using CFStream (over internal TLS) + // + + const uint8_t *buffer = (const uint8_t *)[currentWrite->buffer bytes] + currentWrite->bytesDone; + + NSUInteger bytesToWrite = [currentWrite->buffer length] - currentWrite->bytesDone; + + if (bytesToWrite > SIZE_MAX) // NSUInteger may be bigger than size_t (write param 3) + { + bytesToWrite = SIZE_MAX; + } + + CFIndex result = CFWriteStreamWrite(writeStream, buffer, (CFIndex)bytesToWrite); + LogVerbose(@"CFWriteStreamWrite(%lu) = %li", (unsigned long)bytesToWrite, result); + + if (result < 0) + { + error = (__bridge_transfer NSError *)CFWriteStreamCopyError(writeStream); + } + else + { + bytesWritten = (size_t)result; + + // We always set waiting to true in this scenario. + // CFStream may have altered our underlying socket to non-blocking. + // Thus if we attempt to write without a callback, we may end up blocking our queue. + waiting = YES; + } + +#endif + } + else + { + // We're going to use the SSLWrite function. + // + // OSStatus SSLWrite(SSLContextRef context, const void *data, size_t dataLength, size_t *processed) + // + // Parameters: + // context - An SSL session context reference. + // data - A pointer to the buffer of data to write. + // dataLength - The amount, in bytes, of data to write. + // processed - On return, the length, in bytes, of the data actually written. + // + // It sounds pretty straight-forward, + // but there are a few caveats you should be aware of. + // + // The SSLWrite method operates in a non-obvious (and rather annoying) manner. + // According to the documentation: + // + // Because you may configure the underlying connection to operate in a non-blocking manner, + // a write operation might return errSSLWouldBlock, indicating that less data than requested + // was actually transferred. In this case, you should repeat the call to SSLWrite until some + // other result is returned. + // + // This sounds perfect, but when our SSLWriteFunction returns errSSLWouldBlock, + // then the SSLWrite method returns (with the proper errSSLWouldBlock return value), + // but it sets processed to dataLength !! + // + // In other words, if the SSLWrite function doesn't completely write all the data we tell it to, + // then it doesn't tell us how many bytes were actually written. So, for example, if we tell it to + // write 256 bytes then it might actually write 128 bytes, but then report 0 bytes written. + // + // You might be wondering: + // If the SSLWrite function doesn't tell us how many bytes were written, + // then how in the world are we supposed to update our parameters (buffer & bytesToWrite) + // for the next time we invoke SSLWrite? + // + // The answer is that SSLWrite cached all the data we told it to write, + // and it will push out that data next time we call SSLWrite. + // If we call SSLWrite with new data, it will push out the cached data first, and then the new data. + // If we call SSLWrite with empty data, then it will simply push out the cached data. + // + // For this purpose we're going to break large writes into a series of smaller writes. + // This allows us to report progress back to the delegate. + + OSStatus result; + + BOOL hasCachedDataToWrite = (sslWriteCachedLength > 0); + BOOL hasNewDataToWrite = YES; + + if (hasCachedDataToWrite) + { + size_t processed = 0; + + result = SSLWrite(sslContext, NULL, 0, &processed); + + if (result == noErr) + { + bytesWritten = sslWriteCachedLength; + sslWriteCachedLength = 0; + + if ([currentWrite->buffer length] == (currentWrite->bytesDone + bytesWritten)) + { + // We've written all data for the current write. + hasNewDataToWrite = NO; + } + } + else + { + if (result == errSSLWouldBlock) + { + waiting = YES; + } + else + { + error = [self sslError:result]; + } + + // Can't write any new data since we were unable to write the cached data. + hasNewDataToWrite = NO; + } + } + + if (hasNewDataToWrite) + { + const uint8_t *buffer = (const uint8_t *)[currentWrite->buffer bytes] + + currentWrite->bytesDone + + bytesWritten; + + NSUInteger bytesToWrite = [currentWrite->buffer length] - currentWrite->bytesDone - bytesWritten; + + if (bytesToWrite > SIZE_MAX) // NSUInteger may be bigger than size_t (write param 3) + { + bytesToWrite = SIZE_MAX; + } + + size_t bytesRemaining = bytesToWrite; + + BOOL keepLooping = YES; + while (keepLooping) + { + const size_t sslMaxBytesToWrite = 32768; + size_t sslBytesToWrite = MIN(bytesRemaining, sslMaxBytesToWrite); + size_t sslBytesWritten = 0; + + result = SSLWrite(sslContext, buffer, sslBytesToWrite, &sslBytesWritten); + + if (result == noErr) + { + buffer += sslBytesWritten; + bytesWritten += sslBytesWritten; + bytesRemaining -= sslBytesWritten; + + keepLooping = (bytesRemaining > 0); + } + else + { + if (result == errSSLWouldBlock) + { + waiting = YES; + sslWriteCachedLength = sslBytesToWrite; + } + else + { + error = [self sslError:result]; + } + + keepLooping = NO; + } + + } // while (keepLooping) + + } // if (hasNewDataToWrite) + } + } + else + { + // + // Writing data directly over raw socket + // + + int socketFD = (socket4FD != SOCKET_NULL) ? socket4FD : (socket6FD != SOCKET_NULL) ? socket6FD : socketUN; + + const uint8_t *buffer = (const uint8_t *)[currentWrite->buffer bytes] + currentWrite->bytesDone; + + NSUInteger bytesToWrite = [currentWrite->buffer length] - currentWrite->bytesDone; + + if (bytesToWrite > SIZE_MAX) // NSUInteger may be bigger than size_t (write param 3) + { + bytesToWrite = SIZE_MAX; + } + + ssize_t result = write(socketFD, buffer, (size_t)bytesToWrite); + LogVerbose(@"wrote to socket = %zd", result); + + // Check results + if (result < 0) + { + if (errno == EWOULDBLOCK) + { + waiting = YES; + } + else + { + error = [self errnoErrorWithReason:@"Error in write() function"]; + } + } + else + { + bytesWritten = result; + } + } + + // We're done with our writing. + // If we explictly ran into a situation where the socket told us there was no room in the buffer, + // then we immediately resume listening for notifications. + // + // We must do this before we dequeue another write, + // as that may in turn invoke this method again. + // + // Note that if CFStream is involved, it may have maliciously put our socket in blocking mode. + + if (waiting) + { + flags &= ~kSocketCanAcceptBytes; + + if (![self usingCFStreamForTLS]) + { + [self resumeWriteSource]; + } + } + + // Check our results + + BOOL done = NO; + + if (bytesWritten > 0) + { + // Update total amount read for the current write + currentWrite->bytesDone += bytesWritten; + LogVerbose(@"currentWrite->bytesDone = %lu", (unsigned long)currentWrite->bytesDone); + + // Is packet done? + done = (currentWrite->bytesDone == [currentWrite->buffer length]); + } + + if (done) + { + [self completeCurrentWrite]; + + if (!error) + { + dispatch_async(socketQueue, ^{ @autoreleasepool{ + + [self maybeDequeueWrite]; + }}); + } + } + else + { + // We were unable to finish writing the data, + // so we're waiting for another callback to notify us of available space in the lower-level output buffer. + + if (!waiting && !error) + { + // This would be the case if our write was able to accept some data, but not all of it. + + flags &= ~kSocketCanAcceptBytes; + + if (![self usingCFStreamForTLS]) + { + [self resumeWriteSource]; + } + } + + if (bytesWritten > 0) + { + // We're not done with the entire write, but we have written some bytes + + __strong id theDelegate = delegate; + + if (delegateQueue && [theDelegate respondsToSelector:@selector(socket:didWritePartialDataOfLength:tag:)]) + { + long theWriteTag = currentWrite->tag; + + dispatch_async(delegateQueue, ^{ @autoreleasepool { + + [theDelegate socket:self didWritePartialDataOfLength:bytesWritten tag:theWriteTag]; + }}); + } + } + } + + // Check for errors + + if (error) + { + [self closeWithError:[self errnoErrorWithReason:@"Error in write() function"]]; + } + + // Do not add any code here without first adding a return statement in the error case above. +} + +- (void)completeCurrentWrite +{ + LogTrace(); + + NSAssert(currentWrite, @"Trying to complete current write when there is no current write."); + + + __strong id theDelegate = delegate; + + if (delegateQueue && [theDelegate respondsToSelector:@selector(socket:didWriteDataWithTag:)]) + { + long theWriteTag = currentWrite->tag; + + dispatch_async(delegateQueue, ^{ @autoreleasepool { + + [theDelegate socket:self didWriteDataWithTag:theWriteTag]; + }}); + } + + [self endCurrentWrite]; +} + +- (void)endCurrentWrite +{ + if (writeTimer) + { + dispatch_source_cancel(writeTimer); + writeTimer = NULL; + } + + currentWrite = nil; +} + +- (void)setupWriteTimerWithTimeout:(NSTimeInterval)timeout +{ + if (timeout >= 0.0) + { + writeTimer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, socketQueue); + + __weak GCDAsyncSocket *weakSelf = self; + + dispatch_source_set_event_handler(writeTimer, ^{ @autoreleasepool { +#pragma clang diagnostic push +#pragma clang diagnostic warning "-Wimplicit-retain-self" + + __strong GCDAsyncSocket *strongSelf = weakSelf; + if (strongSelf == nil) return_from_block; + + [strongSelf doWriteTimeout]; + +#pragma clang diagnostic pop + }}); + +#if !OS_OBJECT_USE_OBJC + dispatch_source_t theWriteTimer = writeTimer; + dispatch_source_set_cancel_handler(writeTimer, ^{ +#pragma clang diagnostic push +#pragma clang diagnostic warning "-Wimplicit-retain-self" + + LogVerbose(@"dispatch_release(writeTimer)"); + dispatch_release(theWriteTimer); + +#pragma clang diagnostic pop + }); +#endif + + dispatch_time_t tt = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(timeout * NSEC_PER_SEC)); + + dispatch_source_set_timer(writeTimer, tt, DISPATCH_TIME_FOREVER, 0); + dispatch_resume(writeTimer); + } +} + +- (void)doWriteTimeout +{ + // This is a little bit tricky. + // Ideally we'd like to synchronously query the delegate about a timeout extension. + // But if we do so synchronously we risk a possible deadlock. + // So instead we have to do so asynchronously, and callback to ourselves from within the delegate block. + + flags |= kWritesPaused; + + __strong id theDelegate = delegate; + + if (delegateQueue && [theDelegate respondsToSelector:@selector(socket:shouldTimeoutWriteWithTag:elapsed:bytesDone:)]) + { + GCDAsyncWritePacket *theWrite = currentWrite; + + dispatch_async(delegateQueue, ^{ @autoreleasepool { + + NSTimeInterval timeoutExtension = 0.0; + + timeoutExtension = [theDelegate socket:self shouldTimeoutWriteWithTag:theWrite->tag + elapsed:theWrite->timeout + bytesDone:theWrite->bytesDone]; + + dispatch_async(socketQueue, ^{ @autoreleasepool { + + [self doWriteTimeoutWithExtension:timeoutExtension]; + }}); + }}); + } + else + { + [self doWriteTimeoutWithExtension:0.0]; + } +} + +- (void)doWriteTimeoutWithExtension:(NSTimeInterval)timeoutExtension +{ + if (currentWrite) + { + if (timeoutExtension > 0.0) + { + currentWrite->timeout += timeoutExtension; + + // Reschedule the timer + dispatch_time_t tt = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(timeoutExtension * NSEC_PER_SEC)); + dispatch_source_set_timer(writeTimer, tt, DISPATCH_TIME_FOREVER, 0); + + // Unpause writes, and continue + flags &= ~kWritesPaused; + [self doWriteData]; + } + else + { + LogVerbose(@"WriteTimeout"); + + [self closeWithError:[self writeTimeoutError]]; + } + } +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Security +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (void)startTLS:(NSDictionary *)tlsSettings +{ + LogTrace(); + + if (tlsSettings == nil) + { + // Passing nil/NULL to CFReadStreamSetProperty will appear to work the same as passing an empty dictionary, + // but causes problems if we later try to fetch the remote host's certificate. + // + // To be exact, it causes the following to return NULL instead of the normal result: + // CFReadStreamCopyProperty(readStream, kCFStreamPropertySSLPeerCertificates) + // + // So we use an empty dictionary instead, which works perfectly. + + tlsSettings = [NSDictionary dictionary]; + } + + GCDAsyncSpecialPacket *packet = [[GCDAsyncSpecialPacket alloc] initWithTLSSettings:tlsSettings]; + + dispatch_async(socketQueue, ^{ @autoreleasepool { + + if ((flags & kSocketStarted) && !(flags & kQueuedTLS) && !(flags & kForbidReadsWrites)) + { + [readQueue addObject:packet]; + [writeQueue addObject:packet]; + + flags |= kQueuedTLS; + + [self maybeDequeueRead]; + [self maybeDequeueWrite]; + } + }}); + +} + +- (void)maybeStartTLS +{ + // We can't start TLS until: + // - All queued reads prior to the user calling startTLS are complete + // - All queued writes prior to the user calling startTLS are complete + // + // We'll know these conditions are met when both kStartingReadTLS and kStartingWriteTLS are set + + if ((flags & kStartingReadTLS) && (flags & kStartingWriteTLS)) + { + BOOL useSecureTransport = YES; + +#if TARGET_OS_IPHONE + { + GCDAsyncSpecialPacket *tlsPacket = (GCDAsyncSpecialPacket *)currentRead; + NSDictionary *tlsSettings = tlsPacket->tlsSettings; + + NSNumber *value = [tlsSettings objectForKey:GCDAsyncSocketUseCFStreamForTLS]; + if (value && [value boolValue]) + useSecureTransport = NO; + } +#endif + + if (useSecureTransport) + { + [self ssl_startTLS]; + } + else + { +#if TARGET_OS_IPHONE + [self cf_startTLS]; +#endif + } + } +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Security via SecureTransport +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (OSStatus)sslReadWithBuffer:(void *)buffer length:(size_t *)bufferLength +{ + LogVerbose(@"sslReadWithBuffer:%p length:%lu", buffer, (unsigned long)*bufferLength); + + if ((socketFDBytesAvailable == 0) && ([sslPreBuffer availableBytes] == 0)) + { + LogVerbose(@"%@ - No data available to read...", THIS_METHOD); + + // No data available to read. + // + // Need to wait for readSource to fire and notify us of + // available data in the socket's internal read buffer. + + [self resumeReadSource]; + + *bufferLength = 0; + return errSSLWouldBlock; + } + + size_t totalBytesRead = 0; + size_t totalBytesLeftToBeRead = *bufferLength; + + BOOL done = NO; + BOOL socketError = NO; + + // + // STEP 1 : READ FROM SSL PRE BUFFER + // + + size_t sslPreBufferLength = [sslPreBuffer availableBytes]; + + if (sslPreBufferLength > 0) + { + LogVerbose(@"%@: Reading from SSL pre buffer...", THIS_METHOD); + + size_t bytesToCopy; + if (sslPreBufferLength > totalBytesLeftToBeRead) + bytesToCopy = totalBytesLeftToBeRead; + else + bytesToCopy = sslPreBufferLength; + + LogVerbose(@"%@: Copying %zu bytes from sslPreBuffer", THIS_METHOD, bytesToCopy); + + memcpy(buffer, [sslPreBuffer readBuffer], bytesToCopy); + [sslPreBuffer didRead:bytesToCopy]; + + LogVerbose(@"%@: sslPreBuffer.length = %zu", THIS_METHOD, [sslPreBuffer availableBytes]); + + totalBytesRead += bytesToCopy; + totalBytesLeftToBeRead -= bytesToCopy; + + done = (totalBytesLeftToBeRead == 0); + + if (done) LogVerbose(@"%@: Complete", THIS_METHOD); + } + + // + // STEP 2 : READ FROM SOCKET + // + + if (!done && (socketFDBytesAvailable > 0)) + { + LogVerbose(@"%@: Reading from socket...", THIS_METHOD); + + int socketFD = (socket4FD != SOCKET_NULL) ? socket4FD : (socket6FD != SOCKET_NULL) ? socket6FD : socketUN; + + BOOL readIntoPreBuffer; + size_t bytesToRead; + uint8_t *buf; + + if (socketFDBytesAvailable > totalBytesLeftToBeRead) + { + // Read all available data from socket into sslPreBuffer. + // Then copy requested amount into dataBuffer. + + LogVerbose(@"%@: Reading into sslPreBuffer...", THIS_METHOD); + + [sslPreBuffer ensureCapacityForWrite:socketFDBytesAvailable]; + + readIntoPreBuffer = YES; + bytesToRead = (size_t)socketFDBytesAvailable; + buf = [sslPreBuffer writeBuffer]; + } + else + { + // Read available data from socket directly into dataBuffer. + + LogVerbose(@"%@: Reading directly into dataBuffer...", THIS_METHOD); + + readIntoPreBuffer = NO; + bytesToRead = totalBytesLeftToBeRead; + buf = (uint8_t *)buffer + totalBytesRead; + } + + ssize_t result = read(socketFD, buf, bytesToRead); + LogVerbose(@"%@: read from socket = %zd", THIS_METHOD, result); + + if (result < 0) + { + LogVerbose(@"%@: read errno = %i", THIS_METHOD, errno); + + if (errno != EWOULDBLOCK) + { + socketError = YES; + } + + socketFDBytesAvailable = 0; + } + else if (result == 0) + { + LogVerbose(@"%@: read EOF", THIS_METHOD); + + socketError = YES; + socketFDBytesAvailable = 0; + } + else + { + size_t bytesReadFromSocket = result; + + if (socketFDBytesAvailable > bytesReadFromSocket) + socketFDBytesAvailable -= bytesReadFromSocket; + else + socketFDBytesAvailable = 0; + + if (readIntoPreBuffer) + { + [sslPreBuffer didWrite:bytesReadFromSocket]; + + size_t bytesToCopy = MIN(totalBytesLeftToBeRead, bytesReadFromSocket); + + LogVerbose(@"%@: Copying %zu bytes out of sslPreBuffer", THIS_METHOD, bytesToCopy); + + memcpy((uint8_t *)buffer + totalBytesRead, [sslPreBuffer readBuffer], bytesToCopy); + [sslPreBuffer didRead:bytesToCopy]; + + totalBytesRead += bytesToCopy; + totalBytesLeftToBeRead -= bytesToCopy; + + LogVerbose(@"%@: sslPreBuffer.length = %zu", THIS_METHOD, [sslPreBuffer availableBytes]); + } + else + { + totalBytesRead += bytesReadFromSocket; + totalBytesLeftToBeRead -= bytesReadFromSocket; + } + + done = (totalBytesLeftToBeRead == 0); + + if (done) LogVerbose(@"%@: Complete", THIS_METHOD); + } + } + + *bufferLength = totalBytesRead; + + if (done) + return noErr; + + if (socketError) + return errSSLClosedAbort; + + return errSSLWouldBlock; +} + +- (OSStatus)sslWriteWithBuffer:(const void *)buffer length:(size_t *)bufferLength +{ + if (!(flags & kSocketCanAcceptBytes)) + { + // Unable to write. + // + // Need to wait for writeSource to fire and notify us of + // available space in the socket's internal write buffer. + + [self resumeWriteSource]; + + *bufferLength = 0; + return errSSLWouldBlock; + } + + size_t bytesToWrite = *bufferLength; + size_t bytesWritten = 0; + + BOOL done = NO; + BOOL socketError = NO; + + int socketFD = (socket4FD != SOCKET_NULL) ? socket4FD : (socket6FD != SOCKET_NULL) ? socket6FD : socketUN; + + ssize_t result = write(socketFD, buffer, bytesToWrite); + + if (result < 0) + { + if (errno != EWOULDBLOCK) + { + socketError = YES; + } + + flags &= ~kSocketCanAcceptBytes; + } + else if (result == 0) + { + flags &= ~kSocketCanAcceptBytes; + } + else + { + bytesWritten = result; + + done = (bytesWritten == bytesToWrite); + } + + *bufferLength = bytesWritten; + + if (done) + return noErr; + + if (socketError) + return errSSLClosedAbort; + + return errSSLWouldBlock; +} + +static OSStatus SSLReadFunction(SSLConnectionRef connection, void *data, size_t *dataLength) +{ + GCDAsyncSocket *asyncSocket = (__bridge GCDAsyncSocket *)connection; + + NSCAssert(dispatch_get_specific(asyncSocket->IsOnSocketQueueOrTargetQueueKey), @"What the deuce?"); + + return [asyncSocket sslReadWithBuffer:data length:dataLength]; +} + +static OSStatus SSLWriteFunction(SSLConnectionRef connection, const void *data, size_t *dataLength) +{ + GCDAsyncSocket *asyncSocket = (__bridge GCDAsyncSocket *)connection; + + NSCAssert(dispatch_get_specific(asyncSocket->IsOnSocketQueueOrTargetQueueKey), @"What the deuce?"); + + return [asyncSocket sslWriteWithBuffer:data length:dataLength]; +} + +- (void)ssl_startTLS +{ + LogTrace(); + + LogVerbose(@"Starting TLS (via SecureTransport)..."); + + OSStatus status; + + GCDAsyncSpecialPacket *tlsPacket = (GCDAsyncSpecialPacket *)currentRead; + if (tlsPacket == nil) // Code to quiet the analyzer + { + NSAssert(NO, @"Logic error"); + + [self closeWithError:[self otherError:@"Logic error"]]; + return; + } + NSDictionary *tlsSettings = tlsPacket->tlsSettings; + + // Create SSLContext, and setup IO callbacks and connection ref + + BOOL isServer = [[tlsSettings objectForKey:(__bridge NSString *)kCFStreamSSLIsServer] boolValue]; + +#if TARGET_OS_IPHONE || (__MAC_OS_X_VERSION_MIN_REQUIRED >= 1080) + { + if (isServer) + sslContext = SSLCreateContext(kCFAllocatorDefault, kSSLServerSide, kSSLStreamType); + else + sslContext = SSLCreateContext(kCFAllocatorDefault, kSSLClientSide, kSSLStreamType); + + if (sslContext == NULL) + { + [self closeWithError:[self otherError:@"Error in SSLCreateContext"]]; + return; + } + } +#else // (__MAC_OS_X_VERSION_MIN_REQUIRED < 1080) + { + status = SSLNewContext(isServer, &sslContext); + if (status != noErr) + { + [self closeWithError:[self otherError:@"Error in SSLNewContext"]]; + return; + } + } +#endif + + status = SSLSetIOFuncs(sslContext, &SSLReadFunction, &SSLWriteFunction); + if (status != noErr) + { + [self closeWithError:[self otherError:@"Error in SSLSetIOFuncs"]]; + return; + } + + status = SSLSetConnection(sslContext, (__bridge SSLConnectionRef)self); + if (status != noErr) + { + [self closeWithError:[self otherError:@"Error in SSLSetConnection"]]; + return; + } + + + BOOL shouldManuallyEvaluateTrust = [[tlsSettings objectForKey:GCDAsyncSocketManuallyEvaluateTrust] boolValue]; + if (shouldManuallyEvaluateTrust) + { + if (isServer) + { + [self closeWithError:[self otherError:@"Manual trust validation is not supported for server sockets"]]; + return; + } + + status = SSLSetSessionOption(sslContext, kSSLSessionOptionBreakOnServerAuth, true); + if (status != noErr) + { + [self closeWithError:[self otherError:@"Error in SSLSetSessionOption"]]; + return; + } + +#if !TARGET_OS_IPHONE && (__MAC_OS_X_VERSION_MIN_REQUIRED < 1080) + + // Note from Apple's documentation: + // + // It is only necessary to call SSLSetEnableCertVerify on the Mac prior to OS X 10.8. + // On OS X 10.8 and later setting kSSLSessionOptionBreakOnServerAuth always disables the + // built-in trust evaluation. All versions of iOS behave like OS X 10.8 and thus + // SSLSetEnableCertVerify is not available on that platform at all. + + status = SSLSetEnableCertVerify(sslContext, NO); + if (status != noErr) + { + [self closeWithError:[self otherError:@"Error in SSLSetEnableCertVerify"]]; + return; + } + +#endif + } + + // Configure SSLContext from given settings + // + // Checklist: + // 1. kCFStreamSSLPeerName + // 2. kCFStreamSSLCertificates + // 3. GCDAsyncSocketSSLPeerID + // 4. GCDAsyncSocketSSLProtocolVersionMin + // 5. GCDAsyncSocketSSLProtocolVersionMax + // 6. GCDAsyncSocketSSLSessionOptionFalseStart + // 7. GCDAsyncSocketSSLSessionOptionSendOneByteRecord + // 8. GCDAsyncSocketSSLCipherSuites + // 9. GCDAsyncSocketSSLDiffieHellmanParameters (Mac) + // + // Deprecated (throw error): + // 10. kCFStreamSSLAllowsAnyRoot + // 11. kCFStreamSSLAllowsExpiredRoots + // 12. kCFStreamSSLAllowsExpiredCertificates + // 13. kCFStreamSSLValidatesCertificateChain + // 14. kCFStreamSSLLevel + + id value; + + // 1. kCFStreamSSLPeerName + + value = [tlsSettings objectForKey:(__bridge NSString *)kCFStreamSSLPeerName]; + if ([value isKindOfClass:[NSString class]]) + { + NSString *peerName = (NSString *)value; + + const char *peer = [peerName UTF8String]; + size_t peerLen = strlen(peer); + + status = SSLSetPeerDomainName(sslContext, peer, peerLen); + if (status != noErr) + { + [self closeWithError:[self otherError:@"Error in SSLSetPeerDomainName"]]; + return; + } + } + else if (value) + { + NSAssert(NO, @"Invalid value for kCFStreamSSLPeerName. Value must be of type NSString."); + + [self closeWithError:[self otherError:@"Invalid value for kCFStreamSSLPeerName."]]; + return; + } + + // 2. kCFStreamSSLCertificates + + value = [tlsSettings objectForKey:(__bridge NSString *)kCFStreamSSLCertificates]; + if ([value isKindOfClass:[NSArray class]]) + { + CFArrayRef certs = (__bridge CFArrayRef)value; + + status = SSLSetCertificate(sslContext, certs); + if (status != noErr) + { + [self closeWithError:[self otherError:@"Error in SSLSetCertificate"]]; + return; + } + } + else if (value) + { + NSAssert(NO, @"Invalid value for kCFStreamSSLCertificates. Value must be of type NSArray."); + + [self closeWithError:[self otherError:@"Invalid value for kCFStreamSSLCertificates."]]; + return; + } + + // 3. GCDAsyncSocketSSLPeerID + + value = [tlsSettings objectForKey:GCDAsyncSocketSSLPeerID]; + if ([value isKindOfClass:[NSData class]]) + { + NSData *peerIdData = (NSData *)value; + + status = SSLSetPeerID(sslContext, [peerIdData bytes], [peerIdData length]); + if (status != noErr) + { + [self closeWithError:[self otherError:@"Error in SSLSetPeerID"]]; + return; + } + } + else if (value) + { + NSAssert(NO, @"Invalid value for GCDAsyncSocketSSLPeerID. Value must be of type NSData." + @" (You can convert strings to data using a method like" + @" [string dataUsingEncoding:NSUTF8StringEncoding])"); + + [self closeWithError:[self otherError:@"Invalid value for GCDAsyncSocketSSLPeerID."]]; + return; + } + + // 4. GCDAsyncSocketSSLProtocolVersionMin + + value = [tlsSettings objectForKey:GCDAsyncSocketSSLProtocolVersionMin]; + if ([value isKindOfClass:[NSNumber class]]) + { + SSLProtocol minProtocol = (SSLProtocol)[(NSNumber *)value intValue]; + if (minProtocol != kSSLProtocolUnknown) + { + status = SSLSetProtocolVersionMin(sslContext, minProtocol); + if (status != noErr) + { + [self closeWithError:[self otherError:@"Error in SSLSetProtocolVersionMin"]]; + return; + } + } + } + else if (value) + { + NSAssert(NO, @"Invalid value for GCDAsyncSocketSSLProtocolVersionMin. Value must be of type NSNumber."); + + [self closeWithError:[self otherError:@"Invalid value for GCDAsyncSocketSSLProtocolVersionMin."]]; + return; + } + + // 5. GCDAsyncSocketSSLProtocolVersionMax + + value = [tlsSettings objectForKey:GCDAsyncSocketSSLProtocolVersionMax]; + if ([value isKindOfClass:[NSNumber class]]) + { + SSLProtocol maxProtocol = (SSLProtocol)[(NSNumber *)value intValue]; + if (maxProtocol != kSSLProtocolUnknown) + { + status = SSLSetProtocolVersionMax(sslContext, maxProtocol); + if (status != noErr) + { + [self closeWithError:[self otherError:@"Error in SSLSetProtocolVersionMax"]]; + return; + } + } + } + else if (value) + { + NSAssert(NO, @"Invalid value for GCDAsyncSocketSSLProtocolVersionMax. Value must be of type NSNumber."); + + [self closeWithError:[self otherError:@"Invalid value for GCDAsyncSocketSSLProtocolVersionMax."]]; + return; + } + + // 6. GCDAsyncSocketSSLSessionOptionFalseStart + + value = [tlsSettings objectForKey:GCDAsyncSocketSSLSessionOptionFalseStart]; + if ([value isKindOfClass:[NSNumber class]]) + { + status = SSLSetSessionOption(sslContext, kSSLSessionOptionFalseStart, [value boolValue]); + if (status != noErr) + { + [self closeWithError:[self otherError:@"Error in SSLSetSessionOption (kSSLSessionOptionFalseStart)"]]; + return; + } + } + else if (value) + { + NSAssert(NO, @"Invalid value for GCDAsyncSocketSSLSessionOptionFalseStart. Value must be of type NSNumber."); + + [self closeWithError:[self otherError:@"Invalid value for GCDAsyncSocketSSLSessionOptionFalseStart."]]; + return; + } + + // 7. GCDAsyncSocketSSLSessionOptionSendOneByteRecord + + value = [tlsSettings objectForKey:GCDAsyncSocketSSLSessionOptionSendOneByteRecord]; + if ([value isKindOfClass:[NSNumber class]]) + { + status = SSLSetSessionOption(sslContext, kSSLSessionOptionSendOneByteRecord, [value boolValue]); + if (status != noErr) + { + [self closeWithError: + [self otherError:@"Error in SSLSetSessionOption (kSSLSessionOptionSendOneByteRecord)"]]; + return; + } + } + else if (value) + { + NSAssert(NO, @"Invalid value for GCDAsyncSocketSSLSessionOptionSendOneByteRecord." + @" Value must be of type NSNumber."); + + [self closeWithError:[self otherError:@"Invalid value for GCDAsyncSocketSSLSessionOptionSendOneByteRecord."]]; + return; + } + + // 8. GCDAsyncSocketSSLCipherSuites + + value = [tlsSettings objectForKey:GCDAsyncSocketSSLCipherSuites]; + if ([value isKindOfClass:[NSArray class]]) + { + NSArray *cipherSuites = (NSArray *)value; + NSUInteger numberCiphers = [cipherSuites count]; + SSLCipherSuite ciphers[numberCiphers]; + + NSUInteger cipherIndex; + for (cipherIndex = 0; cipherIndex < numberCiphers; cipherIndex++) + { + NSNumber *cipherObject = [cipherSuites objectAtIndex:cipherIndex]; + ciphers[cipherIndex] = [cipherObject shortValue]; + } + + status = SSLSetEnabledCiphers(sslContext, ciphers, numberCiphers); + if (status != noErr) + { + [self closeWithError:[self otherError:@"Error in SSLSetEnabledCiphers"]]; + return; + } + } + else if (value) + { + NSAssert(NO, @"Invalid value for GCDAsyncSocketSSLCipherSuites. Value must be of type NSArray."); + + [self closeWithError:[self otherError:@"Invalid value for GCDAsyncSocketSSLCipherSuites."]]; + return; + } + + // 9. GCDAsyncSocketSSLDiffieHellmanParameters + +#if !TARGET_OS_IPHONE + value = [tlsSettings objectForKey:GCDAsyncSocketSSLDiffieHellmanParameters]; + if ([value isKindOfClass:[NSData class]]) + { + NSData *diffieHellmanData = (NSData *)value; + + status = SSLSetDiffieHellmanParams(sslContext, [diffieHellmanData bytes], [diffieHellmanData length]); + if (status != noErr) + { + [self closeWithError:[self otherError:@"Error in SSLSetDiffieHellmanParams"]]; + return; + } + } + else if (value) + { + NSAssert(NO, @"Invalid value for GCDAsyncSocketSSLDiffieHellmanParameters. Value must be of type NSData."); + + [self closeWithError:[self otherError:@"Invalid value for GCDAsyncSocketSSLDiffieHellmanParameters."]]; + return; + } +#endif + + // DEPRECATED checks + + // 10. kCFStreamSSLAllowsAnyRoot + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" + value = [tlsSettings objectForKey:(__bridge NSString *)kCFStreamSSLAllowsAnyRoot]; +#pragma clang diagnostic pop + if (value) + { + NSAssert(NO, @"Security option unavailable - kCFStreamSSLAllowsAnyRoot" + @" - You must use manual trust evaluation"); + + [self closeWithError:[self otherError:@"Security option unavailable - kCFStreamSSLAllowsAnyRoot"]]; + return; + } + + // 11. kCFStreamSSLAllowsExpiredRoots + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" + value = [tlsSettings objectForKey:(__bridge NSString *)kCFStreamSSLAllowsExpiredRoots]; +#pragma clang diagnostic pop + if (value) + { + NSAssert(NO, @"Security option unavailable - kCFStreamSSLAllowsExpiredRoots" + @" - You must use manual trust evaluation"); + + [self closeWithError:[self otherError:@"Security option unavailable - kCFStreamSSLAllowsExpiredRoots"]]; + return; + } + + // 12. kCFStreamSSLValidatesCertificateChain + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" + value = [tlsSettings objectForKey:(__bridge NSString *)kCFStreamSSLValidatesCertificateChain]; +#pragma clang diagnostic pop + if (value) + { + NSAssert(NO, @"Security option unavailable - kCFStreamSSLValidatesCertificateChain" + @" - You must use manual trust evaluation"); + + [self closeWithError:[self otherError:@"Security option unavailable - kCFStreamSSLValidatesCertificateChain"]]; + return; + } + + // 13. kCFStreamSSLAllowsExpiredCertificates + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" + value = [tlsSettings objectForKey:(__bridge NSString *)kCFStreamSSLAllowsExpiredCertificates]; +#pragma clang diagnostic pop + if (value) + { + NSAssert(NO, @"Security option unavailable - kCFStreamSSLAllowsExpiredCertificates" + @" - You must use manual trust evaluation"); + + [self closeWithError:[self otherError:@"Security option unavailable - kCFStreamSSLAllowsExpiredCertificates"]]; + return; + } + + // 14. kCFStreamSSLLevel + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" + value = [tlsSettings objectForKey:(__bridge NSString *)kCFStreamSSLLevel]; +#pragma clang diagnostic pop + if (value) + { + NSAssert(NO, @"Security option unavailable - kCFStreamSSLLevel" + @" - You must use GCDAsyncSocketSSLProtocolVersionMin & GCDAsyncSocketSSLProtocolVersionMax"); + + [self closeWithError:[self otherError:@"Security option unavailable - kCFStreamSSLLevel"]]; + return; + } + + // Setup the sslPreBuffer + // + // Any data in the preBuffer needs to be moved into the sslPreBuffer, + // as this data is now part of the secure read stream. + + sslPreBuffer = [[GCDAsyncSocketPreBuffer alloc] initWithCapacity:(1024 * 4)]; + + size_t preBufferLength = [preBuffer availableBytes]; + + if (preBufferLength > 0) + { + [sslPreBuffer ensureCapacityForWrite:preBufferLength]; + + memcpy([sslPreBuffer writeBuffer], [preBuffer readBuffer], preBufferLength); + [preBuffer didRead:preBufferLength]; + [sslPreBuffer didWrite:preBufferLength]; + } + + sslErrCode = lastSSLHandshakeError = noErr; + + // Start the SSL Handshake process + + [self ssl_continueSSLHandshake]; +} + +- (void)ssl_continueSSLHandshake +{ + LogTrace(); + + // If the return value is noErr, the session is ready for normal secure communication. + // If the return value is errSSLWouldBlock, the SSLHandshake function must be called again. + // If the return value is errSSLServerAuthCompleted, we ask delegate if we should trust the + // server and then call SSLHandshake again to resume the handshake or close the connection + // errSSLPeerBadCert SSL error. + // Otherwise, the return value indicates an error code. + + OSStatus status = SSLHandshake(sslContext); + lastSSLHandshakeError = status; + + if (status == noErr) + { + LogVerbose(@"SSLHandshake complete"); + + flags &= ~kStartingReadTLS; + flags &= ~kStartingWriteTLS; + + flags |= kSocketSecure; + + __strong id theDelegate = delegate; + + if (delegateQueue && [theDelegate respondsToSelector:@selector(socketDidSecure:)]) + { + dispatch_async(delegateQueue, ^{ @autoreleasepool { + + [theDelegate socketDidSecure:self]; + }}); + } + + [self endCurrentRead]; + [self endCurrentWrite]; + + [self maybeDequeueRead]; + [self maybeDequeueWrite]; + } + else if (status == errSSLPeerAuthCompleted) + { + LogVerbose(@"SSLHandshake peerAuthCompleted - awaiting delegate approval"); + + __block SecTrustRef trust = NULL; + status = SSLCopyPeerTrust(sslContext, &trust); + if (status != noErr) + { + [self closeWithError:[self sslError:status]]; + return; + } + + int aStateIndex = stateIndex; + dispatch_queue_t theSocketQueue = socketQueue; + + __weak GCDAsyncSocket *weakSelf = self; + + void (^comletionHandler)(BOOL) = ^(BOOL shouldTrust){ @autoreleasepool { +#pragma clang diagnostic push +#pragma clang diagnostic warning "-Wimplicit-retain-self" + + dispatch_async(theSocketQueue, ^{ @autoreleasepool { + + if (trust) { + CFRelease(trust); + trust = NULL; + } + + __strong GCDAsyncSocket *strongSelf = weakSelf; + if (strongSelf) + { + [strongSelf ssl_shouldTrustPeer:shouldTrust stateIndex:aStateIndex]; + } + }}); + +#pragma clang diagnostic pop + }}; + + __strong id theDelegate = delegate; + + if (delegateQueue && [theDelegate respondsToSelector:@selector(socket:didReceiveTrust:completionHandler:)]) + { + dispatch_async(delegateQueue, ^{ @autoreleasepool { + + [theDelegate socket:self didReceiveTrust:trust completionHandler:comletionHandler]; + }}); + } + else + { + if (trust) { + CFRelease(trust); + trust = NULL; + } + + NSString *msg = @"GCDAsyncSocketManuallyEvaluateTrust specified in tlsSettings," + @" but delegate doesn't implement socket:shouldTrustPeer:"; + + [self closeWithError:[self otherError:msg]]; + return; + } + } + else if (status == errSSLWouldBlock) + { + LogVerbose(@"SSLHandshake continues..."); + + // Handshake continues... + // + // This method will be called again from doReadData or doWriteData. + } + else + { + [self closeWithError:[self sslError:status]]; + } +} + +- (void)ssl_shouldTrustPeer:(BOOL)shouldTrust stateIndex:(int)aStateIndex +{ + LogTrace(); + + if (aStateIndex != stateIndex) + { + LogInfo(@"Ignoring ssl_shouldTrustPeer - invalid state (maybe disconnected)"); + + // One of the following is true + // - the socket was disconnected + // - the startTLS operation timed out + // - the completionHandler was already invoked once + + return; + } + + // Increment stateIndex to ensure completionHandler can only be called once. + stateIndex++; + + if (shouldTrust) + { + NSAssert(lastSSLHandshakeError == errSSLPeerAuthCompleted, @"ssl_shouldTrustPeer called when last error is %d and not errSSLPeerAuthCompleted", (int)lastSSLHandshakeError); + [self ssl_continueSSLHandshake]; + } + else + { + [self closeWithError:[self sslError:errSSLPeerBadCert]]; + } +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Security via CFStream +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +#if TARGET_OS_IPHONE + +- (void)cf_finishSSLHandshake +{ + LogTrace(); + + if ((flags & kStartingReadTLS) && (flags & kStartingWriteTLS)) + { + flags &= ~kStartingReadTLS; + flags &= ~kStartingWriteTLS; + + flags |= kSocketSecure; + + __strong id theDelegate = delegate; + + if (delegateQueue && [theDelegate respondsToSelector:@selector(socketDidSecure:)]) + { + dispatch_async(delegateQueue, ^{ @autoreleasepool { + + [theDelegate socketDidSecure:self]; + }}); + } + + [self endCurrentRead]; + [self endCurrentWrite]; + + [self maybeDequeueRead]; + [self maybeDequeueWrite]; + } +} + +- (void)cf_abortSSLHandshake:(NSError *)error +{ + LogTrace(); + + if ((flags & kStartingReadTLS) && (flags & kStartingWriteTLS)) + { + flags &= ~kStartingReadTLS; + flags &= ~kStartingWriteTLS; + + [self closeWithError:error]; + } +} + +- (void)cf_startTLS +{ + LogTrace(); + + LogVerbose(@"Starting TLS (via CFStream)..."); + + if ([preBuffer availableBytes] > 0) + { + NSString *msg = @"Invalid TLS transition. Handshake has already been read from socket."; + + [self closeWithError:[self otherError:msg]]; + return; + } + + [self suspendReadSource]; + [self suspendWriteSource]; + + socketFDBytesAvailable = 0; + flags &= ~kSocketCanAcceptBytes; + flags &= ~kSecureSocketHasBytesAvailable; + + flags |= kUsingCFStreamForTLS; + + if (![self createReadAndWriteStream]) + { + [self closeWithError:[self otherError:@"Error in CFStreamCreatePairWithSocket"]]; + return; + } + + if (![self registerForStreamCallbacksIncludingReadWrite:YES]) + { + [self closeWithError:[self otherError:@"Error in CFStreamSetClient"]]; + return; + } + + if (![self addStreamsToRunLoop]) + { + [self closeWithError:[self otherError:@"Error in CFStreamScheduleWithRunLoop"]]; + return; + } + + NSAssert([currentRead isKindOfClass:[GCDAsyncSpecialPacket class]], @"Invalid read packet for startTLS"); + NSAssert([currentWrite isKindOfClass:[GCDAsyncSpecialPacket class]], @"Invalid write packet for startTLS"); + + GCDAsyncSpecialPacket *tlsPacket = (GCDAsyncSpecialPacket *)currentRead; + CFDictionaryRef tlsSettings = (__bridge CFDictionaryRef)tlsPacket->tlsSettings; + + // Getting an error concerning kCFStreamPropertySSLSettings ? + // You need to add the CFNetwork framework to your iOS application. + + BOOL r1 = CFReadStreamSetProperty(readStream, kCFStreamPropertySSLSettings, tlsSettings); + BOOL r2 = CFWriteStreamSetProperty(writeStream, kCFStreamPropertySSLSettings, tlsSettings); + + // For some reason, starting around the time of iOS 4.3, + // the first call to set the kCFStreamPropertySSLSettings will return true, + // but the second will return false. + // + // Order doesn't seem to matter. + // So you could call CFReadStreamSetProperty and then CFWriteStreamSetProperty, or you could reverse the order. + // Either way, the first call will return true, and the second returns false. + // + // Interestingly, this doesn't seem to affect anything. + // Which is not altogether unusual, as the documentation seems to suggest that (for many settings) + // setting it on one side of the stream automatically sets it for the other side of the stream. + // + // Although there isn't anything in the documentation to suggest that the second attempt would fail. + // + // Furthermore, this only seems to affect streams that are negotiating a security upgrade. + // In other words, the socket gets connected, there is some back-and-forth communication over the unsecure + // connection, and then a startTLS is issued. + // So this mostly affects newer protocols (XMPP, IMAP) as opposed to older protocols (HTTPS). + + if (!r1 && !r2) // Yes, the && is correct - workaround for apple bug. + { + [self closeWithError:[self otherError:@"Error in CFStreamSetProperty"]]; + return; + } + + if (![self openStreams]) + { + [self closeWithError:[self otherError:@"Error in CFStreamOpen"]]; + return; + } + + LogVerbose(@"Waiting for SSL Handshake to complete..."); +} + +#endif + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark CFStream +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +#if TARGET_OS_IPHONE + ++ (void)ignore:(id)_ +{} + ++ (void)startCFStreamThreadIfNeeded +{ + LogTrace(); + + static dispatch_once_t predicate; + dispatch_once(&predicate, ^{ + + cfstreamThreadRetainCount = 0; + cfstreamThreadSetupQueue = dispatch_queue_create("GCDAsyncSocket-CFStreamThreadSetup", DISPATCH_QUEUE_SERIAL); + }); + + dispatch_sync(cfstreamThreadSetupQueue, ^{ @autoreleasepool { + + if (++cfstreamThreadRetainCount == 1) + { + cfstreamThread = [[NSThread alloc] initWithTarget:self + selector:@selector(cfstreamThread) + object:nil]; + [cfstreamThread start]; + } + }}); +} + ++ (void)stopCFStreamThreadIfNeeded +{ + LogTrace(); + + // The creation of the cfstreamThread is relatively expensive. + // So we'd like to keep it available for recycling. + // However, there's a tradeoff here, because it shouldn't remain alive forever. + // So what we're going to do is use a little delay before taking it down. + // This way it can be reused properly in situations where multiple sockets are continually in flux. + + int delayInSeconds = 30; + dispatch_time_t when = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delayInSeconds * NSEC_PER_SEC)); + dispatch_after(when, cfstreamThreadSetupQueue, ^{ @autoreleasepool { +#pragma clang diagnostic push +#pragma clang diagnostic warning "-Wimplicit-retain-self" + + if (cfstreamThreadRetainCount == 0) + { + LogWarn(@"Logic error concerning cfstreamThread start / stop"); + return_from_block; + } + + if (--cfstreamThreadRetainCount == 0) + { + [cfstreamThread cancel]; // set isCancelled flag + + // wake up the thread + [GCDAsyncSocket performSelector:@selector(ignore:) + onThread:cfstreamThread + withObject:[NSNull null] + waitUntilDone:NO]; + + cfstreamThread = nil; + } + +#pragma clang diagnostic pop + }}); +} + ++ (void)cfstreamThread { @autoreleasepool + { + [[NSThread currentThread] setName:GCDAsyncSocketThreadName]; + + LogInfo(@"CFStreamThread: Started"); + + // We can't run the run loop unless it has an associated input source or a timer. + // So we'll just create a timer that will never fire - unless the server runs for decades. + [NSTimer scheduledTimerWithTimeInterval:[[NSDate distantFuture] timeIntervalSinceNow] + target:self + selector:@selector(ignore:) + userInfo:nil + repeats:YES]; + + NSThread *currentThread = [NSThread currentThread]; + NSRunLoop *currentRunLoop = [NSRunLoop currentRunLoop]; + + BOOL isCancelled = [currentThread isCancelled]; + + while (!isCancelled && [currentRunLoop runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]]) + { + isCancelled = [currentThread isCancelled]; + } + + LogInfo(@"CFStreamThread: Stopped"); + }} + ++ (void)scheduleCFStreams:(GCDAsyncSocket *)asyncSocket +{ + LogTrace(); + NSAssert([NSThread currentThread] == cfstreamThread, @"Invoked on wrong thread"); + + CFRunLoopRef runLoop = CFRunLoopGetCurrent(); + + if (asyncSocket->readStream) + CFReadStreamScheduleWithRunLoop(asyncSocket->readStream, runLoop, kCFRunLoopDefaultMode); + + if (asyncSocket->writeStream) + CFWriteStreamScheduleWithRunLoop(asyncSocket->writeStream, runLoop, kCFRunLoopDefaultMode); +} + ++ (void)unscheduleCFStreams:(GCDAsyncSocket *)asyncSocket +{ + LogTrace(); + NSAssert([NSThread currentThread] == cfstreamThread, @"Invoked on wrong thread"); + + CFRunLoopRef runLoop = CFRunLoopGetCurrent(); + + if (asyncSocket->readStream) + CFReadStreamUnscheduleFromRunLoop(asyncSocket->readStream, runLoop, kCFRunLoopDefaultMode); + + if (asyncSocket->writeStream) + CFWriteStreamUnscheduleFromRunLoop(asyncSocket->writeStream, runLoop, kCFRunLoopDefaultMode); +} + +static void CFReadStreamCallback (CFReadStreamRef stream, CFStreamEventType type, void *pInfo) +{ + GCDAsyncSocket *asyncSocket = (__bridge GCDAsyncSocket *)pInfo; + + switch(type) + { + case kCFStreamEventHasBytesAvailable: + { + dispatch_async(asyncSocket->socketQueue, ^{ @autoreleasepool { + + LogCVerbose(@"CFReadStreamCallback - HasBytesAvailable"); + + if (asyncSocket->readStream != stream) + return_from_block; + + if ((asyncSocket->flags & kStartingReadTLS) && (asyncSocket->flags & kStartingWriteTLS)) + { + // If we set kCFStreamPropertySSLSettings before we opened the streams, this might be a lie. + // (A callback related to the tcp stream, but not to the SSL layer). + + if (CFReadStreamHasBytesAvailable(asyncSocket->readStream)) + { + asyncSocket->flags |= kSecureSocketHasBytesAvailable; + [asyncSocket cf_finishSSLHandshake]; + } + } + else + { + asyncSocket->flags |= kSecureSocketHasBytesAvailable; + [asyncSocket doReadData]; + } + }}); + + break; + } + default: + { + NSError *error = (__bridge_transfer NSError *)CFReadStreamCopyError(stream); + + if (error == nil && type == kCFStreamEventEndEncountered) + { + error = [asyncSocket connectionClosedError]; + } + + dispatch_async(asyncSocket->socketQueue, ^{ @autoreleasepool { + + LogCVerbose(@"CFReadStreamCallback - Other"); + + if (asyncSocket->readStream != stream) + return_from_block; + + if ((asyncSocket->flags & kStartingReadTLS) && (asyncSocket->flags & kStartingWriteTLS)) + { + [asyncSocket cf_abortSSLHandshake:error]; + } + else + { + [asyncSocket closeWithError:error]; + } + }}); + + break; + } + } + +} + +static void CFWriteStreamCallback (CFWriteStreamRef stream, CFStreamEventType type, void *pInfo) +{ + GCDAsyncSocket *asyncSocket = (__bridge GCDAsyncSocket *)pInfo; + + switch(type) + { + case kCFStreamEventCanAcceptBytes: + { + dispatch_async(asyncSocket->socketQueue, ^{ @autoreleasepool { + + LogCVerbose(@"CFWriteStreamCallback - CanAcceptBytes"); + + if (asyncSocket->writeStream != stream) + return_from_block; + + if ((asyncSocket->flags & kStartingReadTLS) && (asyncSocket->flags & kStartingWriteTLS)) + { + // If we set kCFStreamPropertySSLSettings before we opened the streams, this might be a lie. + // (A callback related to the tcp stream, but not to the SSL layer). + + if (CFWriteStreamCanAcceptBytes(asyncSocket->writeStream)) + { + asyncSocket->flags |= kSocketCanAcceptBytes; + [asyncSocket cf_finishSSLHandshake]; + } + } + else + { + asyncSocket->flags |= kSocketCanAcceptBytes; + [asyncSocket doWriteData]; + } + }}); + + break; + } + default: + { + NSError *error = (__bridge_transfer NSError *)CFWriteStreamCopyError(stream); + + if (error == nil && type == kCFStreamEventEndEncountered) + { + error = [asyncSocket connectionClosedError]; + } + + dispatch_async(asyncSocket->socketQueue, ^{ @autoreleasepool { + + LogCVerbose(@"CFWriteStreamCallback - Other"); + + if (asyncSocket->writeStream != stream) + return_from_block; + + if ((asyncSocket->flags & kStartingReadTLS) && (asyncSocket->flags & kStartingWriteTLS)) + { + [asyncSocket cf_abortSSLHandshake:error]; + } + else + { + [asyncSocket closeWithError:error]; + } + }}); + + break; + } + } + +} + +- (BOOL)createReadAndWriteStream +{ + LogTrace(); + + NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue"); + + + if (readStream || writeStream) + { + // Streams already created + return YES; + } + + int socketFD = (socket4FD != SOCKET_NULL) ? socket4FD : (socket6FD != SOCKET_NULL) ? socket6FD : socketUN; + + if (socketFD == SOCKET_NULL) + { + // Cannot create streams without a file descriptor + return NO; + } + + if (![self isConnected]) + { + // Cannot create streams until file descriptor is connected + return NO; + } + + LogVerbose(@"Creating read and write stream..."); + + CFStreamCreatePairWithSocket(NULL, (CFSocketNativeHandle)socketFD, &readStream, &writeStream); + + // The kCFStreamPropertyShouldCloseNativeSocket property should be false by default (for our case). + // But let's not take any chances. + + if (readStream) + CFReadStreamSetProperty(readStream, kCFStreamPropertyShouldCloseNativeSocket, kCFBooleanFalse); + if (writeStream) + CFWriteStreamSetProperty(writeStream, kCFStreamPropertyShouldCloseNativeSocket, kCFBooleanFalse); + + if ((readStream == NULL) || (writeStream == NULL)) + { + LogWarn(@"Unable to create read and write stream..."); + + if (readStream) + { + CFReadStreamClose(readStream); + CFRelease(readStream); + readStream = NULL; + } + if (writeStream) + { + CFWriteStreamClose(writeStream); + CFRelease(writeStream); + writeStream = NULL; + } + + return NO; + } + + return YES; +} + +- (BOOL)registerForStreamCallbacksIncludingReadWrite:(BOOL)includeReadWrite +{ + LogVerbose(@"%@ %@", THIS_METHOD, (includeReadWrite ? @"YES" : @"NO")); + + NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue"); + NSAssert((readStream != NULL && writeStream != NULL), @"Read/Write stream is null"); + + streamContext.version = 0; + streamContext.info = (__bridge void *)(self); + streamContext.retain = nil; + streamContext.release = nil; + streamContext.copyDescription = nil; + + CFOptionFlags readStreamEvents = kCFStreamEventErrorOccurred | kCFStreamEventEndEncountered; + if (includeReadWrite) + readStreamEvents |= kCFStreamEventHasBytesAvailable; + + if (!CFReadStreamSetClient(readStream, readStreamEvents, &CFReadStreamCallback, &streamContext)) + { + return NO; + } + + CFOptionFlags writeStreamEvents = kCFStreamEventErrorOccurred | kCFStreamEventEndEncountered; + if (includeReadWrite) + writeStreamEvents |= kCFStreamEventCanAcceptBytes; + + if (!CFWriteStreamSetClient(writeStream, writeStreamEvents, &CFWriteStreamCallback, &streamContext)) + { + return NO; + } + + return YES; +} + +- (BOOL)addStreamsToRunLoop +{ + LogTrace(); + + NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue"); + NSAssert((readStream != NULL && writeStream != NULL), @"Read/Write stream is null"); + + if (!(flags & kAddedStreamsToRunLoop)) + { + LogVerbose(@"Adding streams to runloop..."); + + [[self class] startCFStreamThreadIfNeeded]; + [[self class] performSelector:@selector(scheduleCFStreams:) + onThread:cfstreamThread + withObject:self + waitUntilDone:YES]; + + flags |= kAddedStreamsToRunLoop; + } + + return YES; +} + +- (void)removeStreamsFromRunLoop +{ + LogTrace(); + + NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue"); + NSAssert((readStream != NULL && writeStream != NULL), @"Read/Write stream is null"); + + if (flags & kAddedStreamsToRunLoop) + { + LogVerbose(@"Removing streams from runloop..."); + + [[self class] performSelector:@selector(unscheduleCFStreams:) + onThread:cfstreamThread + withObject:self + waitUntilDone:YES]; + [[self class] stopCFStreamThreadIfNeeded]; + + flags &= ~kAddedStreamsToRunLoop; + } +} + +- (BOOL)openStreams +{ + LogTrace(); + + NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue"); + NSAssert((readStream != NULL && writeStream != NULL), @"Read/Write stream is null"); + + CFStreamStatus readStatus = CFReadStreamGetStatus(readStream); + CFStreamStatus writeStatus = CFWriteStreamGetStatus(writeStream); + + if ((readStatus == kCFStreamStatusNotOpen) || (writeStatus == kCFStreamStatusNotOpen)) + { + LogVerbose(@"Opening read and write stream..."); + + BOOL r1 = CFReadStreamOpen(readStream); + BOOL r2 = CFWriteStreamOpen(writeStream); + + if (!r1 || !r2) + { + LogError(@"Error in CFStreamOpen"); + return NO; + } + } + + return YES; +} + +#endif + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Advanced +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +/** + * See header file for big discussion of this method. + **/ +- (BOOL)autoDisconnectOnClosedReadStream +{ + // Note: YES means kAllowHalfDuplexConnection is OFF + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + { + return ((config & kAllowHalfDuplexConnection) == 0); + } + else + { + __block BOOL result; + + dispatch_sync(socketQueue, ^{ + result = ((config & kAllowHalfDuplexConnection) == 0); + }); + + return result; + } +} + +/** + * See header file for big discussion of this method. + **/ +- (void)setAutoDisconnectOnClosedReadStream:(BOOL)flag +{ + // Note: YES means kAllowHalfDuplexConnection is OFF + + dispatch_block_t block = ^{ + + if (flag) + config &= ~kAllowHalfDuplexConnection; + else + config |= kAllowHalfDuplexConnection; + }; + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + block(); + else + dispatch_async(socketQueue, block); +} + + +/** + * See header file for big discussion of this method. + **/ +- (void)markSocketQueueTargetQueue:(dispatch_queue_t)socketNewTargetQueue +{ + void *nonNullUnusedPointer = (__bridge void *)self; + dispatch_queue_set_specific(socketNewTargetQueue, IsOnSocketQueueOrTargetQueueKey, nonNullUnusedPointer, NULL); +} + +/** + * See header file for big discussion of this method. + **/ +- (void)unmarkSocketQueueTargetQueue:(dispatch_queue_t)socketOldTargetQueue +{ + dispatch_queue_set_specific(socketOldTargetQueue, IsOnSocketQueueOrTargetQueueKey, NULL, NULL); +} + +/** + * See header file for big discussion of this method. + **/ +- (void)performBlock:(dispatch_block_t)block +{ + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + block(); + else + dispatch_sync(socketQueue, block); +} + +/** + * Questions? Have you read the header file? + **/ +- (int)socketFD +{ + if (!dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + { + LogWarn(@"%@ - Method only available from within the context of a performBlock: invocation", THIS_METHOD); + return SOCKET_NULL; + } + + if (socket4FD != SOCKET_NULL) + return socket4FD; + else + return socket6FD; +} + +/** + * Questions? Have you read the header file? + **/ +- (int)socket4FD +{ + if (!dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + { + LogWarn(@"%@ - Method only available from within the context of a performBlock: invocation", THIS_METHOD); + return SOCKET_NULL; + } + + return socket4FD; +} + +/** + * Questions? Have you read the header file? + **/ +- (int)socket6FD +{ + if (!dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + { + LogWarn(@"%@ - Method only available from within the context of a performBlock: invocation", THIS_METHOD); + return SOCKET_NULL; + } + + return socket6FD; +} + +#if TARGET_OS_IPHONE + +/** + * Questions? Have you read the header file? + **/ +- (CFReadStreamRef)readStream +{ + if (!dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + { + LogWarn(@"%@ - Method only available from within the context of a performBlock: invocation", THIS_METHOD); + return NULL; + } + + if (readStream == NULL) + [self createReadAndWriteStream]; + + return readStream; +} + +/** + * Questions? Have you read the header file? + **/ +- (CFWriteStreamRef)writeStream +{ + if (!dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + { + LogWarn(@"%@ - Method only available from within the context of a performBlock: invocation", THIS_METHOD); + return NULL; + } + + if (writeStream == NULL) + [self createReadAndWriteStream]; + + return writeStream; +} + +- (BOOL)enableBackgroundingOnSocketWithCaveat:(BOOL)caveat +{ + if (![self createReadAndWriteStream]) + { + // Error occurred creating streams (perhaps socket isn't open) + return NO; + } + + BOOL r1, r2; + + LogVerbose(@"Enabling backgrouding on socket"); + + r1 = CFReadStreamSetProperty(readStream, kCFStreamNetworkServiceType, kCFStreamNetworkServiceTypeVoIP); + r2 = CFWriteStreamSetProperty(writeStream, kCFStreamNetworkServiceType, kCFStreamNetworkServiceTypeVoIP); + + if (!r1 || !r2) + { + return NO; + } + + if (!caveat) + { + if (![self openStreams]) + { + return NO; + } + } + + return YES; +} + +/** + * Questions? Have you read the header file? + **/ +- (BOOL)enableBackgroundingOnSocket +{ + LogTrace(); + + if (!dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + { + LogWarn(@"%@ - Method only available from within the context of a performBlock: invocation", THIS_METHOD); + return NO; + } + + return [self enableBackgroundingOnSocketWithCaveat:NO]; +} + +- (BOOL)enableBackgroundingOnSocketWithCaveat // Deprecated in iOS 4.??? +{ + // This method was created as a workaround for a bug in iOS. + // Apple has since fixed this bug. + // I'm not entirely sure which version of iOS they fixed it in... + + LogTrace(); + + if (!dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + { + LogWarn(@"%@ - Method only available from within the context of a performBlock: invocation", THIS_METHOD); + return NO; + } + + return [self enableBackgroundingOnSocketWithCaveat:YES]; +} + +#endif + +- (SSLContextRef)sslContext +{ + if (!dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + { + LogWarn(@"%@ - Method only available from within the context of a performBlock: invocation", THIS_METHOD); + return NULL; + } + + return sslContext; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Class Utilities +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + ++ (NSMutableArray *)lookupHost:(NSString *)host port:(uint16_t)port error:(NSError **)errPtr +{ + LogTrace(); + + NSMutableArray *addresses = nil; + NSError *error = nil; + + if ([host isEqualToString:@"localhost"] || [host isEqualToString:@"loopback"]) + { + // Use LOOPBACK address + struct sockaddr_in nativeAddr4; + nativeAddr4.sin_len = sizeof(struct sockaddr_in); + nativeAddr4.sin_family = AF_INET; + nativeAddr4.sin_port = htons(port); + nativeAddr4.sin_addr.s_addr = htonl(INADDR_LOOPBACK); + memset(&(nativeAddr4.sin_zero), 0, sizeof(nativeAddr4.sin_zero)); + + struct sockaddr_in6 nativeAddr6; + nativeAddr6.sin6_len = sizeof(struct sockaddr_in6); + nativeAddr6.sin6_family = AF_INET6; + nativeAddr6.sin6_port = htons(port); + nativeAddr6.sin6_flowinfo = 0; + nativeAddr6.sin6_addr = in6addr_loopback; + nativeAddr6.sin6_scope_id = 0; + + // Wrap the native address structures + + NSData *address4 = [NSData dataWithBytes:&nativeAddr4 length:sizeof(nativeAddr4)]; + NSData *address6 = [NSData dataWithBytes:&nativeAddr6 length:sizeof(nativeAddr6)]; + + addresses = [NSMutableArray arrayWithCapacity:2]; + [addresses addObject:address4]; + [addresses addObject:address6]; + } + else + { + NSString *portStr = [NSString stringWithFormat:@"%hu", port]; + + struct addrinfo hints, *res, *res0; + + memset(&hints, 0, sizeof(hints)); + hints.ai_family = PF_UNSPEC; + hints.ai_socktype = SOCK_STREAM; + hints.ai_protocol = IPPROTO_TCP; + + int gai_error = getaddrinfo([host UTF8String], [portStr UTF8String], &hints, &res0); + + if (gai_error) + { + error = [self gaiError:gai_error]; + } + else + { + NSUInteger capacity = 0; + for (res = res0; res; res = res->ai_next) + { + if (res->ai_family == AF_INET || res->ai_family == AF_INET6) { + capacity++; + } + } + + addresses = [NSMutableArray arrayWithCapacity:capacity]; + + for (res = res0; res; res = res->ai_next) + { + if (res->ai_family == AF_INET) + { + // Found IPv4 address. + // Wrap the native address structure, and add to results. + + NSData *address4 = [NSData dataWithBytes:res->ai_addr length:res->ai_addrlen]; + [addresses addObject:address4]; + } + else if (res->ai_family == AF_INET6) + { + // Found IPv6 address. + // Wrap the native address structure, and add to results. + + NSData *address6 = [NSData dataWithBytes:res->ai_addr length:res->ai_addrlen]; + [addresses addObject:address6]; + } + } + freeaddrinfo(res0); + + if ([addresses count] == 0) + { + error = [self gaiError:EAI_FAIL]; + } + } + } + + if (errPtr) *errPtr = error; + return addresses; +} + ++ (NSString *)hostFromSockaddr4:(const struct sockaddr_in *)pSockaddr4 +{ + char addrBuf[INET_ADDRSTRLEN]; + + if (inet_ntop(AF_INET, &pSockaddr4->sin_addr, addrBuf, (socklen_t)sizeof(addrBuf)) == NULL) + { + addrBuf[0] = '\0'; + } + + return [NSString stringWithCString:addrBuf encoding:NSASCIIStringEncoding]; +} + ++ (NSString *)hostFromSockaddr6:(const struct sockaddr_in6 *)pSockaddr6 +{ + char addrBuf[INET6_ADDRSTRLEN]; + + if (inet_ntop(AF_INET6, &pSockaddr6->sin6_addr, addrBuf, (socklen_t)sizeof(addrBuf)) == NULL) + { + addrBuf[0] = '\0'; + } + + return [NSString stringWithCString:addrBuf encoding:NSASCIIStringEncoding]; +} + ++ (uint16_t)portFromSockaddr4:(const struct sockaddr_in *)pSockaddr4 +{ + return ntohs(pSockaddr4->sin_port); +} + ++ (uint16_t)portFromSockaddr6:(const struct sockaddr_in6 *)pSockaddr6 +{ + return ntohs(pSockaddr6->sin6_port); +} + ++ (NSURL *)urlFromSockaddrUN:(const struct sockaddr_un *)pSockaddr +{ + NSString *path = [NSString stringWithUTF8String:pSockaddr->sun_path]; + return [NSURL fileURLWithPath:path]; +} + ++ (NSString *)hostFromAddress:(NSData *)address +{ + NSString *host; + + if ([self getHost:&host port:NULL fromAddress:address]) + return host; + else + return nil; +} + ++ (uint16_t)portFromAddress:(NSData *)address +{ + uint16_t port; + + if ([self getHost:NULL port:&port fromAddress:address]) + return port; + else + return 0; +} + ++ (BOOL)isIPv4Address:(NSData *)address +{ + if ([address length] >= sizeof(struct sockaddr)) + { + const struct sockaddr *sockaddrX = [address bytes]; + + if (sockaddrX->sa_family == AF_INET) { + return YES; + } + } + + return NO; +} + ++ (BOOL)isIPv6Address:(NSData *)address +{ + if ([address length] >= sizeof(struct sockaddr)) + { + const struct sockaddr *sockaddrX = [address bytes]; + + if (sockaddrX->sa_family == AF_INET6) { + return YES; + } + } + + return NO; +} + ++ (BOOL)getHost:(NSString **)hostPtr port:(uint16_t *)portPtr fromAddress:(NSData *)address +{ + return [self getHost:hostPtr port:portPtr family:NULL fromAddress:address]; +} + ++ (BOOL)getHost:(NSString **)hostPtr port:(uint16_t *)portPtr family:(sa_family_t *)afPtr fromAddress:(NSData *)address +{ + if ([address length] >= sizeof(struct sockaddr)) + { + const struct sockaddr *sockaddrX = [address bytes]; + + if (sockaddrX->sa_family == AF_INET) + { + if ([address length] >= sizeof(struct sockaddr_in)) + { + struct sockaddr_in sockaddr4; + memcpy(&sockaddr4, sockaddrX, sizeof(sockaddr4)); + + if (hostPtr) *hostPtr = [self hostFromSockaddr4:&sockaddr4]; + if (portPtr) *portPtr = [self portFromSockaddr4:&sockaddr4]; + if (afPtr) *afPtr = AF_INET; + + return YES; + } + } + else if (sockaddrX->sa_family == AF_INET6) + { + if ([address length] >= sizeof(struct sockaddr_in6)) + { + struct sockaddr_in6 sockaddr6; + memcpy(&sockaddr6, sockaddrX, sizeof(sockaddr6)); + + if (hostPtr) *hostPtr = [self hostFromSockaddr6:&sockaddr6]; + if (portPtr) *portPtr = [self portFromSockaddr6:&sockaddr6]; + if (afPtr) *afPtr = AF_INET6; + + return YES; + } + } + } + + return NO; +} + ++ (NSData *)CRLFData +{ + return [NSData dataWithBytes:"\x0D\x0A" length:2]; +} + ++ (NSData *)CRData +{ + return [NSData dataWithBytes:"\x0D" length:1]; +} + ++ (NSData *)LFData +{ + return [NSData dataWithBytes:"\x0A" length:1]; +} + ++ (NSData *)ZeroData +{ + return [NSData dataWithBytes:"" length:1]; +} + +@end diff --git a/iOS/Hexiwear/CocoaMQTT/LICENSE b/iOS/Hexiwear/CocoaMQTT/LICENSE new file mode 100644 index 0000000..3efe404 --- /dev/null +++ b/iOS/Hexiwear/CocoaMQTT/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2014 emqtt + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/iOS/Hexiwear/CocoaMQTT/MSWeakTimer.h b/iOS/Hexiwear/CocoaMQTT/MSWeakTimer.h new file mode 100644 index 0000000..b644991 --- /dev/null +++ b/iOS/Hexiwear/CocoaMQTT/MSWeakTimer.h @@ -0,0 +1,75 @@ +// +// MSWeakTimer.h +// MindSnacks +// +// Created by Javier Soto on 1/23/13. +// +// + +#import + +/** + `MSWeakTimer` behaves similar to an `NSTimer` but doesn't retain the target. + This timer is implemented using GCD, so you can schedule and unschedule it on arbitrary queues (unlike regular NSTimers!) + */ +@interface MSWeakTimer : NSObject + +/** + * Creates a timer with the specified parameters and waits for a call to `-schedule` to start ticking. + * @note It's safe to retain the returned timer by the object that is also the target. + * or the provided `dispatchQueue`. + * @param timeInterval how frequently `selector` will be invoked on `target`. If the timer doens't repeat, it will only be invoked once, approximately `timeInterval` seconds from the time you call this method. + * @param repeats if `YES`, `selector` will be invoked on `target` until the `MSWeakTimer` object is deallocated or until you call `invalidate`. If `NO`, it will only be invoked once. + * @param dispatchQueue the queue where the delegate method will be dispatched. It can be either a serial or concurrent queue. + * @see `invalidate`. + */ +- (id)initWithTimeInterval:(NSTimeInterval)timeInterval + target:(id)target + selector:(SEL)selector + userInfo:(id)userInfo + repeats:(BOOL)repeats + dispatchQueue:(dispatch_queue_t)dispatchQueue; + +/** + * Creates an `MSWeakTimer` object and schedules it to start ticking inmediately. + */ ++ (instancetype)scheduledTimerWithTimeInterval:(NSTimeInterval)timeInterval + target:(id)target + selector:(SEL)selector + userInfo:(id)userInfo + repeats:(BOOL)repeats + dispatchQueue:(dispatch_queue_t)dispatchQueue; + +/** + * Starts the timer if it hadn't been schedule yet. + * @warning calling this method on an already scheduled timer results in undefined behavior. + */ +- (void)schedule; + +/** + * Sets the amount of time after the scheduled fire date that the timer may fire to the given interval. + * @discussion Setting a tolerance for a timer allows it to fire later than the scheduled fire date, improving the ability of the system to optimize for increased power savings and responsiveness. The timer may fire at any time between its scheduled fire date and the scheduled fire date plus the tolerance. The timer will not fire before the scheduled fire date. For repeating timers, the next fire date is calculated from the original fire date regardless of tolerance applied at individual fire times, to avoid drift. The default value is zero, which means no additional tolerance is applied. The system reserves the right to apply a small amount of tolerance to certain timers regardless of the value of this property. + As the user of the timer, you will have the best idea of what an appropriate tolerance for a timer may be. A general rule of thumb, though, is to set the tolerance to at least 10% of the interval, for a repeating timer. Even a small amount of tolerance will have a significant positive impact on the power usage of your application. The system may put a maximum value of the tolerance. + */ +@property (atomic, assign) NSTimeInterval tolerance; + +/** + * Causes the timer to be fired synchronously manually on the queue from which you call this method. + * You can use this method to fire a repeating timer without interrupting its regular firing schedule. + * If the timer is non-repeating, it is automatically invalidated after firing, even if its scheduled fire date has not arrived. + */ +- (void)fire; + +/** + * You can call this method on repeatable timers in order to stop it from running and trying + * to call the delegate method. + * @note `MSWeakTimer` won't invoke the `selector` on `target` again after calling this method. + * You can call this method from any queue, it doesn't have to be the queue from where you scheduled it. + * Since it doesn't retain the delegate, unlike a regular `NSTimer`, your `dealloc` method will actually be called + * and it's easier to place the `invalidate` call there, instead of figuring out a safe place to do it. + */ +- (void)invalidate; + +- (id)userInfo; + +@end diff --git a/iOS/Hexiwear/CocoaMQTT/MSWeakTimer.m b/iOS/Hexiwear/CocoaMQTT/MSWeakTimer.m new file mode 100644 index 0000000..78efe97 --- /dev/null +++ b/iOS/Hexiwear/CocoaMQTT/MSWeakTimer.m @@ -0,0 +1,217 @@ +// +// MSWeakTimer.m +// MindSnacks +// +// Created by Javier Soto on 1/23/13. +// +// + +#import "MSWeakTimer.h" + +#import + +#if !__has_feature(objc_arc) + #error MSWeakTimer is ARC only. Either turn on ARC for the project or use -fobjc-arc flag +#endif + +#if OS_OBJECT_USE_OBJC + #define ms_gcd_property_qualifier strong + #define ms_release_gcd_object(object) +#else + #define ms_gcd_property_qualifier assign + #define ms_release_gcd_object(object) dispatch_release(object) +#endif + +@interface MSWeakTimer () +{ + struct + { + uint32_t timerIsInvalidated; + } _timerFlags; +} + +@property (nonatomic, assign) NSTimeInterval timeInterval; +@property (nonatomic, weak) id target; +@property (nonatomic, assign) SEL selector; +@property (nonatomic, strong) id userInfo; +@property (nonatomic, assign) BOOL repeats; + +@property (nonatomic, ms_gcd_property_qualifier) dispatch_queue_t privateSerialQueue; + +@property (nonatomic, ms_gcd_property_qualifier) dispatch_source_t timer; + +@end + +@implementation MSWeakTimer + +@synthesize tolerance = _tolerance; + +- (id)initWithTimeInterval:(NSTimeInterval)timeInterval + target:(id)target + selector:(SEL)selector + userInfo:(id)userInfo + repeats:(BOOL)repeats + dispatchQueue:(dispatch_queue_t)dispatchQueue +{ + NSParameterAssert(target); + NSParameterAssert(selector); + NSParameterAssert(dispatchQueue); + + if ((self = [super init])) + { + self.timeInterval = timeInterval; + self.target = target; + self.selector = selector; + self.userInfo = userInfo; + self.repeats = repeats; + + NSString *privateQueueName = [NSString stringWithFormat:@"com.mindsnacks.msweaktimer.%p", self]; + self.privateSerialQueue = dispatch_queue_create([privateQueueName cStringUsingEncoding:NSASCIIStringEncoding], DISPATCH_QUEUE_SERIAL); + dispatch_set_target_queue(self.privateSerialQueue, dispatchQueue); + + self.timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, + 0, + 0, + self.privateSerialQueue); + } + + return self; +} + +- (id)init +{ + return [self initWithTimeInterval:0 + target:nil + selector:NULL + userInfo:nil + repeats:NO + dispatchQueue:nil]; +} + ++ (instancetype)scheduledTimerWithTimeInterval:(NSTimeInterval)timeInterval + target:(id)target + selector:(SEL)selector + userInfo:(id)userInfo + repeats:(BOOL)repeats + dispatchQueue:(dispatch_queue_t)dispatchQueue +{ + MSWeakTimer *timer = [[self alloc] initWithTimeInterval:timeInterval + target:target + selector:selector + userInfo:userInfo + repeats:repeats + dispatchQueue:dispatchQueue]; + + [timer schedule]; + + return timer; +} + +- (void)dealloc +{ + [self invalidate]; + + ms_release_gcd_object(_privateSerialQueue); +} + +- (NSString *)description +{ + return [NSString stringWithFormat:@"<%@ %p> time_interval=%f target=%@ selector=%@ userInfo=%@ repeats=%d timer=%@", + NSStringFromClass([self class]), + self, + self.timeInterval, + self.target, + NSStringFromSelector(self.selector), + self.userInfo, + self.repeats, + self.timer]; +} + +#pragma mark - + +- (void)setTolerance:(NSTimeInterval)tolerance +{ + @synchronized(self) + { + if (tolerance != _tolerance) + { + _tolerance = tolerance; + + [self resetTimerProperties]; + } + } +} + +- (NSTimeInterval)tolerance +{ + @synchronized(self) + { + return _tolerance; + } +} + +- (void)resetTimerProperties +{ + int64_t intervalInNanoseconds = (int64_t)(self.timeInterval * NSEC_PER_SEC); + int64_t toleranceInNanoseconds = (int64_t)(self.tolerance * NSEC_PER_SEC); + + dispatch_source_set_timer(self.timer, + dispatch_time(DISPATCH_TIME_NOW, intervalInNanoseconds), + (uint64_t)intervalInNanoseconds, + toleranceInNanoseconds + ); +} + +- (void)schedule +{ + [self resetTimerProperties]; + + __weak MSWeakTimer *weakSelf = self; + + dispatch_source_set_event_handler(self.timer, ^{ + [weakSelf timerFired]; + }); + + dispatch_resume(self.timer); +} + +- (void)fire +{ + [self timerFired]; +} + +- (void)invalidate +{ + // We check with an atomic operation if it has already been invalidated. Ideally we would synchronize this on the private queue, + // but since we can't know the context from which this method will be called, dispatch_sync might cause a deadlock. + if (!OSAtomicTestAndSetBarrier(7, &_timerFlags.timerIsInvalidated)) + { + dispatch_source_t timer = self.timer; + dispatch_async(self.privateSerialQueue, ^{ + dispatch_source_cancel(timer); + ms_release_gcd_object(timer); + }); + } +} + +- (void)timerFired +{ + // Checking attomatically if the timer has already been invalidated. + if (OSAtomicAnd32OrigBarrier(1, &_timerFlags.timerIsInvalidated)) + { + return; + } + + // We're not worried about this warning because the selector we're calling doesn't return a +1 object. + #pragma clang diagnostic push + #pragma clang diagnostic ignored "-Warc-performSelector-leaks" + [self.target performSelector:self.selector withObject:self]; + #pragma clang diagnostic pop + + if (!self.repeats) + { + [self invalidate]; + } +} + +@end diff --git a/iOS/Hexiwear/CocoaMQTT/ca.cer b/iOS/Hexiwear/CocoaMQTT/ca.cer new file mode 100644 index 0000000..b52afe2 Binary files /dev/null and b/iOS/Hexiwear/CocoaMQTT/ca.cer differ diff --git a/iOS/Hexiwear/CreateAccountTableViewController.swift b/iOS/Hexiwear/CreateAccountTableViewController.swift new file mode 100644 index 0000000..16557fa --- /dev/null +++ b/iOS/Hexiwear/CreateAccountTableViewController.swift @@ -0,0 +1,196 @@ +// +// Hexiwear application is used to pair with Hexiwear BLE devices +// and send sensor readings to WolkSense sensor data cloud +// +// Copyright (C) 2016 WolkAbout Technology s.r.o. +// +// Hexiwear is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Hexiwear is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . +// +// +// CreateAccountTableViewController.swift +// + +import UIKit + +class CreateAccountTableViewController: UITableViewController { + + @IBOutlet weak var firstName: UITextField! + @IBOutlet weak var lastName: UITextField! + @IBOutlet weak var email: UITextField! + @IBOutlet weak var password: UITextField! + @IBOutlet weak var confirmPassword: UITextField! + @IBOutlet weak var buttons: UIView! + @IBOutlet weak var activityIndicator: UIActivityIndicatorView! + @IBOutlet weak var policySwitch: UISwitch! + @IBOutlet weak var signUpButton: UIButton! + + + var dataStore: DataStore! + var userCredentials: UserCredentials! + + override func viewDidLoad() { + super.viewDidLoad() + + firstName.delegate = self + lastName.delegate = self + email.delegate = self + password.delegate = self + confirmPassword.delegate = self + signUpButton.enabled = false + } + + func didActivateSuccessfully() { + showSimpleAlertWithTitle(applicationTitle, message: "Activation successfull.", viewController: self) + } + + func didFailActivation() { + showSimpleAlertWithTitle(applicationTitle, message: "Activation failed.", viewController: self) + } + + @IBAction func cancel(sender: UIButton) { + self.dismissViewControllerAnimated(true, completion: nil) + } + + private func hideButtons() { + buttons.hidden = true + activityIndicator.startAnimating() + } + + private func showButtons() { + buttons.hidden = false + activityIndicator.stopAnimating() + } + + @IBAction func switchChanged(sender: UISwitch) { + signUpButton.enabled = policySwitch.on + } + + func checkAllFieldsSet() -> Bool { + guard let fn = firstName.text, ln = lastName.text, em = email.text, pass = password.text, conf = confirmPassword.text else { + return false + } + return !fn.isEmpty && !ln.isEmpty && !em.isEmpty && !pass.isEmpty && !conf.isEmpty + } + + @IBAction func createAccount(sender: UIButton) { + hideButtons() + + if !checkAllFieldsSet() { + showSimpleAlertWithTitle(applicationTitle, message: "All fields are mandatory!", viewController: self) + showButtons() + return + } + + if let em = email.text where !isValidEmailAddress(em) { + showSimpleAlertWithTitle(applicationTitle, message: "Invalid email!", viewController: self) + showButtons() + return + } + + let passwordsMatch = password.text == confirmPassword.text + if !passwordsMatch { + showSimpleAlertWithTitle(applicationTitle, message: "Passwords do not match!", viewController: self) + showButtons() + return + } + + guard let pass = password.text, conf = confirmPassword.text where (pass.characters.count >= 8) && (conf.characters.count >= 8) else { + showSimpleAlertWithTitle(applicationTitle, message: "Password should be at least 8 characters long!", viewController: self) + showButtons() + return + } + + if !policySwitch.on { + showSimpleAlertWithTitle(applicationTitle, message: "Please agree with privacy policy and terms and conditions", viewController: self) + showButtons() + return + } + + + let account = Account(firstName: firstName.text ?? "", lastName: lastName.text ?? "", email: email.text ?? "", password: password.text ?? "") + dataStore.signUpWithAccount(account, + onFailure: { _ in + dispatch_async(dispatch_get_main_queue()) { + showSimpleAlertWithTitle(applicationTitle, message: "Error creating account!", viewController: self) + self.showButtons() + } + + }, + onSuccess: { + dispatch_async(dispatch_get_main_queue()) { + showSimpleAlertWithTitle(applicationTitle, message: "Thank you for signing up! A verification email has been sent to your email address. Please check your Inbox and follow the instructions.", viewController: self, OKhandler: { _ in + self.dismissViewControllerAnimated(true, completion: nil) }) + self.userCredentials.email = self.email.text + + } + } + ) + } + + override func tableView(tableView: UITableView, shouldHighlightRowAtIndexPath indexPath: NSIndexPath) -> Bool { + return false + } + + override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) { + if segue.identifier == "toTerms" { + if let vc = segue.destinationViewController as? BaseNavigationController, + termsVC = vc.topViewController as? StaticViewController { + termsVC.staticDelegate = self + termsVC.url = NSURL(string: termsAndConditionsURL) + } + } + else if segue.identifier == "toPolicy" { + if let vc = segue.destinationViewController as? BaseNavigationController, + termsVC = vc.topViewController as? StaticViewController { + termsVC.staticDelegate = self + termsVC.url = NSURL(string: privacyPolicyURL) + } + } + } +} + +extension CreateAccountTableViewController: UITextFieldDelegate { + func textFieldShouldReturn(textField: UITextField) -> Bool { + if textField == firstName { + lastName.becomeFirstResponder() + return true + } + + if textField == lastName { + email.becomeFirstResponder() + return true + } + + if textField == email { + password.becomeFirstResponder() + return true + } + + if textField == password { + confirmPassword.becomeFirstResponder() + return true + } + + textField.resignFirstResponder() + return true + } + +} + +extension CreateAccountTableViewController: StaticViewDelegate { + func willClose() { + dismissViewControllerAnimated(true, completion: nil) + } +} + diff --git a/iOS/Hexiwear/DataStore.swift b/iOS/Hexiwear/DataStore.swift new file mode 100644 index 0000000..2eec70d --- /dev/null +++ b/iOS/Hexiwear/DataStore.swift @@ -0,0 +1,196 @@ +// +// Hexiwear application is used to pair with Hexiwear BLE devices +// and send sensor readings to WolkSense sensor data cloud +// +// Copyright (C) 2016 WolkAbout Technology s.r.o. +// +// Hexiwear is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Hexiwear is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . +// +// +// DataStore.swift +// + +import Foundation + +class DataStore { + + let webApi: WebAPI + private let userCredentials: UserCredentials + private let trackingDevice: TrackingDevice + + let concurrentDataStoreQueue = dispatch_queue_create("com.wolkabout.DataStoreQueue", DISPATCH_QUEUE_SERIAL) // synchronizes access to DataStore properties + + init(webApi: WebAPI, userCredentials: UserCredentials, trackingDevice: TrackingDevice) { + self.webApi = webApi + self.userCredentials = userCredentials + self.trackingDevice = trackingDevice + } + + func clearDataStore() { + self.pointFeeds = [] + self.points = [] + } + +//MARK:- Properties + + private var _points: [Device] = [] + var points: [Device] { + get { + var pointsCopy: [Device]! + dispatch_sync(concurrentDataStoreQueue) { + pointsCopy = self._points + } + return pointsCopy + } + + set (newPoints) { + dispatch_barrier_sync(self.concurrentDataStoreQueue) { + + // devices + self._points = newPoints + + // feeds + var feeds = [Feed]() + for point in newPoints { + if let enabledFeeds = point.enabledFeeds() { + feeds += enabledFeeds + } + } + self._pointFeeds = feeds + } + } + } + + // CURRENT DEVICE POINT + private var _currentDevicePoint: Device? + var currentDevicePoint: Device? { + get { + var pointCopy: Device? + dispatch_sync(concurrentDataStoreQueue) { + pointCopy = self._currentDevicePoint + } + return pointCopy + } + } + + // FEEDS + private var _pointFeeds: [Feed] = [] + var pointFeeds: [Feed] { + get { + var pointFeedsCopy: [Feed] = [] + dispatch_sync(concurrentDataStoreQueue) { + pointFeedsCopy = self._pointFeeds + } + return pointFeedsCopy + } + + set (newPointFeeds) { + dispatch_barrier_sync(self.concurrentDataStoreQueue) { + self._pointFeeds = newPointFeeds + } + } + } + + func getDeviceNameForSerial(serial: String) -> String? { + for point in self.points { + if point.deviceSerial == serial { + return point.name + } + } + return nil + } + +} + + + +//MARK:- Fetch points and fetch all (points + messages) +extension DataStore { + + // FETCH ALL + internal func fetchAll(onFailure:(Reason) -> (), onSuccess:() -> ()) { + // POINTS + webApi.fetchPoints(onFailure) { dev in + self.points = dev + onSuccess() + } + } +} + +//MARK:- Device management +extension DataStore { + // GET ACTIVATION STATUS for serial + func getActivationStatusForSerial(serial: String, onFailure:(Reason) -> (), onSuccess: (activationStatus: String) -> ()) { + // Check precondition + guard !serial.isEmpty else { + onSuccess(activationStatus: "NOT_ACTIVATED") + return + } + webApi.getDeviceActivationStatus(serial, onFailure: onFailure, onSuccess: onSuccess) + } + + + // GET RANDOM SERIAL + func getSerial(onFailure:(Reason) -> (), onSuccess: (serial: String) -> ()) { + webApi.getRandomSerial(onFailure, onSuccess:onSuccess) + } + + // ACTIVATE + internal func activateDeviceWithSerialAndName(deviceSerial: String, deviceName: String, onFailure: (Reason) -> (), onSuccess: (pointId: Int, password: String) -> ()) { + // Check precondition + guard !deviceSerial.isEmpty && !deviceName.isEmpty else { + onFailure(.NoData) + return + } + + webApi.activateDevice(deviceSerial, deviceName: deviceName, onFailure: onFailure, onSuccess: onSuccess) + } + + // DEACTIVATE + internal func deactivateDevice(serialNumber: String, onFailure: (Reason) -> (), onSuccess: () -> ()) { + // Check precondition + guard !serialNumber.isEmpty else { + onFailure(.NoData) + return + } + + webApi.deactivateDevice(serialNumber, onFailure: onFailure, onSuccess: onSuccess) + } + +} + +//MARK:- User account management +extension DataStore { + + // SIGN UP + internal func signUpWithAccount(account: Account, onFailure: (Reason) -> (), onSuccess: () -> ()) { + webApi.signUp(account.firstName, lastName: account.lastName, email: account.email, password: account.password, onFailure: onFailure, onSuccess: onSuccess) + } + + // SIGN IN + internal func signInWithUsername(username: String, password: String, onFailure: (Reason) -> (), onSuccess: () -> ()) { + webApi.signIn(username, password: password, onFailure: onFailure, onSuccess: onSuccess) + } + + // RESET PASSWORD + internal func resetPasswordForUserEmail(userEmail: String, onFailure: (Reason) -> (), onSuccess: () -> ()) { + webApi.resetPassword(userEmail, onFailure: onFailure, onSuccess: onSuccess) + } + + // CHANGE PASSWORD + internal func changePasswordForUserEmail(userEmail: String, oldPassword: String, newPassword:String, onFailure: (Reason) -> (), onSuccess: () -> ()) { + webApi.changePassword(userEmail, oldPassword: oldPassword, newPassword: newPassword, onFailure: onFailure, onSuccess: onSuccess) + } + +} diff --git a/iOS/Hexiwear/DetectHexiwearTableViewController.swift b/iOS/Hexiwear/DetectHexiwearTableViewController.swift new file mode 100644 index 0000000..3135375 --- /dev/null +++ b/iOS/Hexiwear/DetectHexiwearTableViewController.swift @@ -0,0 +1,551 @@ +// +// Hexiwear application is used to pair with Hexiwear BLE devices +// and send sensor readings to WolkSense sensor data cloud +// +// Copyright (C) 2016 WolkAbout Technology s.r.o. +// +// Hexiwear is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Hexiwear is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . +// +// +// DetectHexiwearTableViewController.swift +// + +import UIKit +import CoreBluetooth + +struct HexiwearPeripheral { + let p: CBPeripheral + let isOTAP: Bool + let deviceName: String + let rssi: NSNumber? +} + +protocol HexiwearReconnection { + func didReconnectPeripheral(peripheral: CBPeripheral) + func didDisconnectPeripheral() +} + +class DetectHexiwearTableViewController: UITableViewController { + + // BLE + var centralManager : CBCentralManager! + var hexiwearPeripherals: [HexiwearPeripheral] = [] + var selectedPeripheral: CBPeripheral! + var isHEXIOTAP: Bool = false + + var dataStore: DataStore! + var userCredentials: UserCredentials! + var device: TrackingDevice! + var mqttAPI: MQTTAPI! + var titleForReadings: String? + + var skipButton: UIBarButtonItem! + var refreshCont: UIRefreshControl! + + var disconnectOnSignOut: Bool = false + var hexiwearReconnection: HexiwearReconnection? + let progressHUD = JGProgressHUD(style: .Dark) + + + override func viewDidLoad() { + super.viewDidLoad() + self.navigationController!.navigationBar.titleTextAttributes = [NSForegroundColorAttributeName: UIColor.whiteColor()] + + // Initialize central manager + centralManager = CBCentralManager(delegate: self, queue: nil) + + self.refreshCont = UIRefreshControl() + self.refreshCont.addTarget(self, action: #selector(DetectHexiwearTableViewController.refresh(_:)), forControlEvents: UIControlEvents.ValueChanged) + self.tableView.addSubview(refreshCont) + + title = "Detect" + + } + + override func viewDidAppear(animated: Bool) { + scanPeripherals() + } + + func refresh(sender:AnyObject) { + scanPeripherals() + refreshCont.endRefreshing() + } + + private func scanPeripherals() { + hexiwearPeripherals = retrieveConnectedHexiwearPeripherals() + tableView.reloadData() + centralManager.scanForPeripheralsWithServices(nil, options: [CBCentralManagerScanOptionAllowDuplicatesKey:true]) + } + + private func retrieveConnectedHexiwearPeripherals() -> [HexiwearPeripheral] { + return + centralManager + .retrieveConnectedPeripheralsWithServices([DIServiceUUID]) + .filter({$0.name == "HEXIWEAR"}) + .map { peri -> HexiwearPeripheral in + let deviceName = getDeviceNameForHexiSerial(peri.identifier.UUIDString) + return HexiwearPeripheral(p: peri, isOTAP: false, deviceName: deviceName, rssi: nil) + } + } + + private func getDeviceNameForHexiSerial(hexiSerial: String) -> String { + let serialMappings = getSerialMappings() + + guard let wolkSerialForHexiSerial = device.findHexiAndWolkCombination(hexiSerial, hexiAndWolkSerials: serialMappings) else { return "" } + + return dataStore.getDeviceNameForSerial(wolkSerialForHexiSerial) ?? "" + + } + + // MARK: - Table view data source + + override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + return hexiwearPeripherals.count + } + + + override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell { + let cell = tableView.dequeueReusableCellWithIdentifier("hexiwearCellNew", forIndexPath: indexPath) as! HexiwearTableViewCell + + guard indexPath.row < hexiwearPeripherals.count else { + cell.titleLabel.text = "" + cell.detailLabel.text = "" + cell.signalLabel.text = "" + return cell + } + + // Configure the cell... + let peri = hexiwearPeripherals[indexPath.row] + if peri.isOTAP { + cell.titleLabel?.text = peri.deviceName == "" ? "New HEXIWEAR OTAP" : peri.deviceName + " -- OTAP" + } + else { + cell.titleLabel?.text = peri.deviceName == "" ? "New HEXIWEAR" : peri.deviceName + } + + cell.detailLabel?.text = peri.p.identifier.UUIDString + + if let rssi = peri.rssi where rssi.doubleValue < 0.0 { + cell.signalLabel?.text = getSignalLevel(rssi) + } + else { + cell.signalLabel?.text = "" + } + + return cell + } + + func getSignalLevel(rssi: NSNumber) -> String { + + if rssi.doubleValue > -50.0 { + return "●●●●" + } + else if rssi.doubleValue > -60.0 { + return "●●●○" + } + else if rssi.doubleValue > -70.0 { + return "●●○○" + } + else if rssi.doubleValue > -80.0 { + return "●○○○" + } + else { + return "○○○○" + } + } + + func failureHandler(failureReason: Reason) { + delay(0.0) { + self.progressHUD.dismiss() + switch failureReason { + case .Other(let err): + print("Other error \(err.description)") + default: + print("Default error handler") + } + } + } + + override func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) { + guard indexPath.row != hexiwearPeripherals.count else { return } + + centralManager.stopScan() + + let peri = hexiwearPeripherals[indexPath.row] + selectedPeripheral = peri.p + isHEXIOTAP = peri.isOTAP + titleForReadings = peri.deviceName + centralManager.connectPeripheral(selectedPeripheral, options: nil) + + progressHUD.textLabel.text = "Connecting to \(peri.deviceName)" + progressHUD.showInView(self.view, animated: true) + + } + + override func tableView(tableView: UITableView, heightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat { + return 60.0 + } + + override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) { + progressHUD.dismiss() + + if segue.identifier == "toHexiwearDeviceTable" { + if let vc = segue.destinationViewController as? HexiwearTableViewController { + vc.hexiwearPeripheral = selectedPeripheral + vc.dataStore = self.dataStore + if userCredentials.isDemoUser() { + vc.title = "DEMO" + vc.isDemoAccount = true + } + else { + vc.isDemoAccount = false + let onlineTitle = device.trackingIsOff ? " (cloud OFF)" : " (cloud ON)" + if let title = titleForReadings { + vc.title = title + onlineTitle + } + else { + vc.title = title + } + } + + vc.trackingDevice = self.device + vc.mqttAPI = self.mqttAPI + vc.hexiwearDelegate = self + hexiwearReconnection = vc + + let serialMappings = getSerialMappings() + + let hexiSerial = selectedPeripheral.identifier.UUIDString + + let (wolkSerialForHexiSerial, wolkPasswordForHexiSerial) = device.findWolkCredentials(hexiSerial, hexiAndWolkSerials: serialMappings) + vc.wolkSerialForHexiserial = wolkSerialForHexiSerial + vc.wolkPasswordForHexiserial = wolkPasswordForHexiSerial + + centralManager.stopScan() + } + } + else if segue.identifier == "toOTAP" { + if let vc = segue.destinationViewController as? FirmwareSelectionTableViewController { + vc.peri = selectedPeripheral + vc.hexiwearDelegate = self + vc.otapDelegate = self + centralManager.stopScan() + } + } + else if segue.identifier == "toActivateDevice" { + if let vc = segue.destinationViewController as? ActivateDeviceViewController { + vc.dataStore = self.dataStore + vc.selectedPeripheral = selectedPeripheral + vc.deviceActivationDelegate = self + } + } + else if segue.identifier == "toSettingsBase" { + if let nc = segue.destinationViewController as? BaseNavigationController, + vc = nc.topViewController as? SettingaBaseTableViewController { + vc.title = "Cloud settings" + vc.dataStore = self.dataStore + vc.trackingDevice = self.device + vc.delegate = self + vc.isDemoUser = userCredentials.isDemoUser() + } + } + } + + private func showAlertWithText (header : String = "Warning", message : String) { + let alert = UIAlertController(title: header, message: message, preferredStyle: UIAlertControllerStyle.Alert) + alert.addAction(UIAlertAction(title: "Ok", style: UIAlertActionStyle.Destructive, handler: nil)) + self.presentViewController(alert, animated: true, completion: nil) + } + +} + + +//MARK:- CBCentralManagerDelegate +extension DetectHexiwearTableViewController: CBCentralManagerDelegate { + + func centralManagerDidUpdateState(central: CBCentralManager) { + if central.state == .PoweredOn { + scanPeripherals() + } + else { + showAlertWithText("Error", message: "Bluetooth not initialized") + } + } + + private func foundHexiwearIndex(p: CBPeripheral) -> (Bool, Int) { + guard hexiwearPeripherals.count > 0 else { return (false, 0) } + + for i in 0..= 0.0 else { return } // do not reload table while it is refreshing + hexiwearPeripherals.replaceRange(index...index, with: [newHexiwearPeripheral]) + tableView.reloadData() + } + } + + func centralManager(central: CBCentralManager, didConnectPeripheral peripheral: CBPeripheral) { + + selectedPeripheral = peripheral + + // If device is reconnected while on readings screen, just drop any checking + guard navigationController?.topMostViewController() == self else { + progressHUD.dismiss() + hexiwearReconnection?.didReconnectPeripheral(peripheral) + return + } + + // if user is logged in with demo account, skip cloud features (activation) + guard userCredentials.isDemoUser() == false else { + if self.isHEXIOTAP { + self.performSegueWithIdentifier("toOTAP", sender: self) + } + else { + self.performSegueWithIdentifier("toHexiwearDeviceTable", sender: self) + } + return + } + + + // Check device activation status... + let serialMappings = getSerialMappings() + + let hexiSerial = peripheral.identifier.UUIDString + + // Activate device if there is no serial for selected hexiwear + guard let wolkSerialForHexiSerial = device.findHexiAndWolkCombination(hexiSerial, hexiAndWolkSerials: serialMappings) else { + self.dataStore.fetchAll(self.failureHandler) { + dispatch_async(dispatch_get_main_queue()) { + // ... if it is not activated proceed to activation screen + self.performSegueWithIdentifier("toActivateDevice", sender: self) + } + } + return + } + + // If cloud is OFF + guard device.trackingIsOff == false else { + if self.isHEXIOTAP { + self.performSegueWithIdentifier("toOTAP", sender: self) + } + else { + self.performSegueWithIdentifier("toHexiwearDeviceTable", sender: self) + } + return + } + + // If cloud is ON + self.dataStore.getActivationStatusForSerial(wolkSerialForHexiSerial, onFailure: self.failureHandler) { activationStatus in + dispatch_async(dispatch_get_main_queue()) { + // ... if it is activated proceed to main screen + if activationStatus == "ACTIVATED" { + dispatch_async(dispatch_get_main_queue()) { + if self.isHEXIOTAP { + self.performSegueWithIdentifier("toOTAP", sender: self) + } + else { + self.performSegueWithIdentifier("toHexiwearDeviceTable", sender: self) + } + } + return + } + + self.dataStore.fetchAll(self.failureHandler) { + dispatch_async(dispatch_get_main_queue()) { + // ... if it is not activated proceed to activation screen + self.performSegueWithIdentifier("toActivateDevice", sender: self) + } + } + } + } + } + + func centralManager(central: CBCentralManager, didDisconnectPeripheral peripheral: CBPeripheral, error: NSError?) { + + if let error = error { print("didDisconnectPeripheral error: \(error)") } + + // If authentication is lost, drop readings processing + if let err = error where err.domain == CBErrorDomain { + let connectionFailedError: Int = CBError.ConnectionFailed.rawValue + if err.code == connectionFailedError { + print("connection failed") + } + } + + guard !disconnectOnSignOut else { return } + + if let _ = navigationController?.topMostViewController() as? FWUpgradeViewController { + showSimpleAlertWithTitle(applicationTitle, message: "Hexiwear disconnected!", viewController: self, OKhandler: { _ in + self.navigationController?.popToViewController(self, animated: true) + }) + } + else if let _ = navigationController?.topMostViewController() as? FirmwareSelectionTableViewController { + showSimpleAlertWithTitle(applicationTitle, message: "Hexiwear disconnected!", viewController: self, OKhandler: { _ in + self.navigationController?.popToViewController(self, animated: true) + }) + } + else if navigationController?.topMostViewController() != self { // reconnect + hexiwearReconnection?.didDisconnectPeripheral() + central.connectPeripheral(peripheral, options: nil) + } + + } + + private func getSerialMappings() -> [SerialMapping] { + let hexiAndWolkSerials = device.hexiAndWolkSerials + + let serialMappings = device.serialsStringToHexiAndWolkCombination(hexiAndWolkSerials) + return serialMappings + } +} + + +// MARK: - DeviceActivationDelegate +extension DetectHexiwearTableViewController: DeviceActivationDelegate { + func didActivateDevice(pointId: Int, serials: SerialMapping) { + print("DETECT HEXI -- didActivateDevice with serials: \(serials) and pointId: \(pointId)") + + // Get hexi and wolk serial mappings + let serialMappings = getSerialMappings() + + // Filter out mapping new hexi serial (if there is one) + var serialMappingsFiltered = serialMappings.filter { return $0.hexiSerial != serials.hexiSerial } + + // Add new hexi serial mapping + serialMappingsFiltered.append(serials) + + // Save new mappings + device.hexiAndWolkSerials = device.hexiAndWolkCombinationToString(serialMappingsFiltered) + + proceedToMainScreen() + } + + func didSkipActivation() { + navigationController?.popViewControllerAnimated(true) + restartScanning() + } + + func proceedToMainScreen() { + dispatch_async(dispatch_get_main_queue()) { + self.navigationController?.popViewControllerAnimated(false) + let segueToPerform = self.isHEXIOTAP ? "toOTAP" : "toHexiwearDeviceTable" + self.performSegueWithIdentifier(segueToPerform, sender: self) + } + } + + func restartScanning() { + navigationController?.popToViewController(self, animated: true) + if selectedPeripheral != nil { + centralManager.cancelPeripheralConnection(selectedPeripheral) + selectedPeripheral.delegate = nil + selectedPeripheral = nil + } + mqttAPI.setAuthorisationOptions("", password: "") + hexiwearPeripherals = [] + tableView.reloadData() + scanPeripherals() + } +} + +extension DetectHexiwearTableViewController: HexiwearPeripheralDelegate { + func didUnwind() { + restartScanning() + } + + func didLoseBonding() { + let message = "Lost bonding with HEXIWEAR. Click OK to open Bluetooth settings and choose forget HEXIWEAR and try again." + + showOKAndCancelAlertWithTitle(applicationTitle, message: message, viewController: self, OKhandler: {_ in + UIApplication.sharedApplication().openURL(NSURL(string:UIApplicationOpenSettingsURLString)!); + }) + if selectedPeripheral != nil { + centralManager.cancelPeripheralConnection(selectedPeripheral) + selectedPeripheral.delegate = nil + selectedPeripheral = nil + } + mqttAPI.setAuthorisationOptions("", password: "") + hexiwearPeripherals = [] + tableView.reloadData() + scanPeripherals() + self.navigationController?.popToViewController(self, animated: true) + } + + func willDisconnectOnSignOut() { + if selectedPeripheral != nil { + disconnectOnSignOut = true + centralManager.cancelPeripheralConnection(selectedPeripheral) + selectedPeripheral.delegate = nil + selectedPeripheral = nil + } + mqttAPI.setAuthorisationOptions("", password: "") + NSNotificationCenter.defaultCenter().postNotificationName(HexiwearDidSignOut, object: nil) + + } +} + +extension DetectHexiwearTableViewController: OTAPDelegate { + func didCancelOTAP() { + restartScanning() + } + + func didFailedOTAP() { + showSimpleAlertWithTitle(applicationTitle, message: "OTAP failed!", viewController: self, OKhandler: { _ in + self.navigationController?.popToViewController(self, animated: true) + self.restartScanning() + }) + } +} + +extension DetectHexiwearTableViewController : HexiwearSettingsDelegate { + func didSignOut() { + willDisconnectOnSignOut() + } + + func didSetTime() { + print("n/a") + } +} + diff --git a/iOS/Hexiwear/Device.swift b/iOS/Hexiwear/Device.swift new file mode 100644 index 0000000..27ef2c9 --- /dev/null +++ b/iOS/Hexiwear/Device.swift @@ -0,0 +1,105 @@ +// +// Hexiwear application is used to pair with Hexiwear BLE devices +// and send sensor readings to WolkSense sensor data cloud +// +// Copyright (C) 2016 WolkAbout Technology s.r.o. +// +// Hexiwear is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Hexiwear is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . +// +// +// Device.swift +// + +import Foundation +import UIKit + +public struct Device { + let id: Int + var name: String = "" + let deviceSerial: String + let activationTimestamp: Double + let deviceState: String + let batteryState: Int + var heartbeat: Int = 0 + let lastReportTimestamp: Double + let owner: String + var feeds: [Feed]? + + init (id: Int, name: String, deviceSerial: String, activationTimestamp: Double, deviceState: String, batteryState: Int, heartbeat: Int, lastReportTimestamp: Double, owner: String, feeds: [Feed]?) { + self.id = id + self.name = name + self.deviceSerial = deviceSerial + self.activationTimestamp = activationTimestamp + self.deviceState = deviceState + self.batteryState = batteryState + self.heartbeat = heartbeat + self.lastReportTimestamp = lastReportTimestamp + self.owner = owner + self.feeds = feeds + } + + static func parseDeviceJSON(deviceJson: [String:AnyObject]) -> Device? { + var device: Device? + if let id = deviceJson["id"] as? Int, + name = deviceJson["name"] as? String, + deviceSerial = deviceJson["deviceSerial"] as? String, + activationTimestamp = deviceJson["activationTimestamp"] as? Double, + deviceState = deviceJson["deviceState"] as? String, + batteryState = deviceJson["batteryState"] as? Int, + heartbeat = deviceJson["heartbeat"] as? Int, + lastReportTimestamp = deviceJson["lastReportTimestamp"] as? Double, + owner = deviceJson["owner"] as? String { + + device = Device(id: id, name: name, deviceSerial: deviceSerial, activationTimestamp: activationTimestamp / 1000.0, deviceState: deviceState, batteryState: batteryState, heartbeat: heartbeat, lastReportTimestamp: lastReportTimestamp / 1000.0, owner: owner, feeds: nil) + + // feeds + if let feeds = deviceJson["feeds"] as? [[String:AnyObject]] { + var feedArray: [Feed] = [] + feedArray = feeds.reduce([]) { (accum, elem) in + var accum = accum + if let feed = Feed.parseFeedJSON(elem, device: device!) { + accum.append(feed) + } + return accum + } + device!.feeds = feedArray + } + } + + return device + } + + func isHexiwear() -> Bool { + guard deviceSerial.characters.count == 16 else { return false } + + let range = deviceSerial.startIndex.advancedBy(4) ..< deviceSerial.startIndex.advancedBy(6) + let substring = deviceSerial[range] + + return substring == "HX" + } + +} + +extension Device: CustomStringConvertible { + public var description: String { + return "DEVICE {\n\t id:\(id),\n\t name:\(name),\n\t activationTimestamp:\(activationTimestamp),\n\t deviceState:\(deviceState),\n\t batteryState:\(batteryState),\n\t heartbeat:\(heartbeat),\n\t lastReportTimestamp:\(lastReportTimestamp),\n\t owner:\(owner),\n\t feeds:\(feeds?.count ?? 0)\n}" + } +} + +extension Device { + func enabledFeeds() -> [Feed]? { + return self.feeds + } +} + diff --git a/iOS/Hexiwear/FWUpgradeViewController.swift b/iOS/Hexiwear/FWUpgradeViewController.swift new file mode 100644 index 0000000..5c813e7 --- /dev/null +++ b/iOS/Hexiwear/FWUpgradeViewController.swift @@ -0,0 +1,319 @@ +// +// Hexiwear application is used to pair with Hexiwear BLE devices +// and send sensor readings to WolkSense sensor data cloud +// +// Copyright (C) 2016 WolkAbout Technology s.r.o. +// +// Hexiwear is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Hexiwear is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . +// +// +// FWUpgradeViewController.swift +// + +import UIKit +import CoreBluetooth + +protocol OTAPDelegate { + func didCancelOTAP() + func didFailedOTAP() +} + +class FWUpgradeViewController: UIViewController { + + @IBOutlet weak var progressView: UIProgressView! + @IBOutlet weak var firmwareLabel: UILabel! + @IBOutlet weak var panelContainer: UIView! + @IBOutlet weak var filenameLabel: UILabel! + @IBOutlet weak var versionLabel: UILabel! + @IBOutlet weak var fwType: UILabel! + + var fwPath: String = "" + var fwFileName: String = "" + var fwTypeString: String = "" + var fwVersion: String = "" + var hexiwearPeripheral : CBPeripheral! + var hexiwearDelegate: HexiwearPeripheralDelegate? + var otapDelegate: OTAPDelegate? + + + // OTAP vars + var isOTAPEnabled = true // if hexiware is in OTAP mode this will be true + var hexiwearFW: [UInt8] = [] // array of bytes of hexiwear firmware + var hexiwearFWLength: Int = 0 + var otapIsRunning = false + + + var otapControlPointCharacteristic: CBCharacteristic? + var otapDataCharacteristic: CBCharacteristic? + var otapStateCharacteristic: CBCharacteristic? + + var isOtapStateAvailable = false // first read which otap state (i.e. which firmware file to send) and then proceed with otap + + let formatter = NSNumberFormatter() + + override func viewDidLoad() { + super.viewDidLoad() + + hexiwearPeripheral.delegate = self + hexiwearPeripheral.discoverServices(nil) + + // Progress % formatter + formatter.maximumFractionDigits = 1 + formatter.minimumFractionDigits = 1 + formatter.minimumIntegerDigits = 1 + + title = "Firmware upgrade" + + progressView.progress = 0.0 + filenameLabel.text = "" + versionLabel.text = "" + fwType.text = "" + } + + override func viewWillAppear(animated: Bool) { + filenameLabel.text = "FILE: \(fwFileName)" + versionLabel.text = "VERSION: \(fwVersion)" + fwType.text = "TYPE: \(fwTypeString)" + } + + override func viewWillDisappear(animated: Bool) { + if isMovingFromParentViewController() && otapIsRunning { + otapDelegate?.didCancelOTAP() + } + } +} + + +//MARK:- CBPeripheralDelegate +extension FWUpgradeViewController: CBPeripheralDelegate { + + func peripheral(peripheral: CBPeripheral, didDiscoverServices error: NSError?) { + if let error = error { print("didDiscoverServices error: \(error)") } + + guard let services = peripheral.services else { return } + + for service in services { + let thisService = service as CBService + if Hexiwear.validService(thisService, isOTAPEnabled: isOTAPEnabled) { + peripheral.discoverCharacteristics(nil, forService: thisService) + } + } + } + + // Enable notification for each characteristic of valid service + func peripheral(peripheral: CBPeripheral, didDiscoverCharacteristicsForService service: CBService, error: NSError?) { + if let error = error { print("didDiscoverCharacteristicsForService error: \(error)") } + + guard let characteristics = service.characteristics else { return } + + for charateristic in characteristics { + let thisCharacteristic = charateristic as CBCharacteristic + if Hexiwear.validDataCharacteristic(thisCharacteristic, isOTAPEnabled: isOTAPEnabled) { + if thisCharacteristic.UUID == OTAPControlPointUUID { + otapControlPointCharacteristic = thisCharacteristic + } + else if thisCharacteristic.UUID == OTAPDataUUID { + otapDataCharacteristic = thisCharacteristic + } + else if thisCharacteristic.UUID == OTAPStateUUID { + otapStateCharacteristic = thisCharacteristic + peripheral.readValueForCharacteristic(otapStateCharacteristic!) + } + } + } + } + + // Get data values when they are updated + private func setLabelsHidden(hidden: Bool) { + panelContainer.hidden = hidden + } + + private func setInfo(infoText: String) { + firmwareLabel.text = infoText + } + + func peripheral(peripheral: CBPeripheral, didUpdateValueForCharacteristic characteristic: CBCharacteristic, error: NSError?) { + + // If authentication is lost, drop OTAP + if let err = error where err.domain == CBATTErrorDomain { + let authenticationError: Int = CBATTError.InsufficientAuthentication.rawValue + if err.code == authenticationError { + hexiwearDelegate?.didLoseBonding() + return + } + } + + setLabelsHidden(false) + + if characteristic.UUID == OTAPStateUUID { + isOtapStateAvailable = true + let otapState = Hexiwear.getOtapState(characteristic.value) + print("otapState: \(otapState)") + if let otap = otapControlPointCharacteristic { + peripheral.setNotifyValue(true, forCharacteristic: otap) + } + } + else if characteristic.UUID == OTAPControlPointUUID { + otapIsRunning = true + let rawData = characteristic.value + print("Control point notified with \(characteristic.value)") + // Get command + let otapCommand = getOTAPCommand(rawData) + + switch otapCommand { + case .NewImageInfoRequest: + + // Parse received NewImageInfoRequest command + guard let _ = OTAP_CMD_NewImgInfoReq(data: rawData) else { + setInfo("Error initiating firmware upgrade. Wrong image info request received.") + return + } + print("New image info request parsed!") + + guard let data = NSData(contentsOfFile: fwPath) else { + setInfo("Error reading firmware file data. Upgrade aborted.") + return + } + + var dataBytes: [UInt8] = [UInt8](count: data.length, repeatedValue: 0x00) + let length = data.length + data.getBytes(&dataBytes, length: length) + hexiwearFW = dataBytes + hexiwearFWLength = length + + // Parse FW file header + guard let otapImageFileHeader = OTAImageFileHeader(data: data) else { + setInfo("Error initiating firmware upgrade. Wrong image info request received.") + return + } + + + // Make NewImageInfoResponse command and send to OTAP Client + let newImageInfoResponse = OTAP_CMD_NewImgInfoRes(imageId: uint16ToLittleEndianBytesArray(otapImageFileHeader.imageId), imageVersion: otapImageFileHeader.imageVersion, imageFileSize: otapImageFileHeader.totalImageFileSize) + let newImageResponseBinary = newImageInfoResponse.convertToBinary() + let newImageResponseData = NSData(bytes: newImageResponseBinary, length: newImageResponseBinary.count) + print("write image info response binary: \(newImageResponseBinary) data: \(newImageResponseData)") + peripheral.writeValue(newImageResponseData, forCharacteristic: otapControlPointCharacteristic!, type: CBCharacteristicWriteType.WithResponse) + print("New image info response sent!") + + case .ImageBlockRequest: + + // Parse received Image block request command + guard let imageBlockRequest = OTAP_CMD_ImgBlockReq(data: rawData) else { + setInfo("Error upgrading firmware. Wrong image block request received.") + return + } + + // Set OTAP transfer parameters + let startPosition = imageBlockRequest.startPosition + let blockSize = imageBlockRequest.blockSize + let chunkSize = imageBlockRequest.chunkSize + + self.sendImageChunks(peripheral, startPosition: startPosition, blockSize: blockSize, chunkSize: chunkSize, sequenceNumber: 0, amountOfChunksToSend: 4) + + case .ErrorNotification: + + otapIsRunning = false + // Parse received Error notification command + guard let errorNotification = OTAP_CMD_ErrNotification(data: rawData) else { + setInfo("Error upgrading firmware. Error notification received.") + return + } + + if let errorStatus = OTAP_Status(rawValue: errorNotification.errStatus) { + setInfo("Error upgrading firmware. Error status = \(errorStatus).") + print(errorStatus) + } + else { + setInfo("Error upgrading firmware.") + print("Unknown error status") + } + otapDelegate?.didFailedOTAP() + + case .ImageTransferComplete: + otapIsRunning = false + // Parse image transfer complete + guard let imageTransferComplete = OTAP_CMD_ImgTransferComplete(data: rawData) else { + setInfo("Error upgrading firmware. Image transfer failed.") + return + } + + if imageTransferComplete.status == OTAP_Status.StatusSuccess { + setInfo("Firmware upgrade completed successfully.") + print("Image transfer complete with success !") + } + else { + setInfo("Firmware upgrade failed.") + print("Image transfer failed !") + print(imageTransferComplete.status) + } + + default: + print("Command not recognized") + } + } + } + + func sendImageChunks(peripheral: CBPeripheral, startPosition: UInt32, blockSize: UInt32, chunkSize: UInt16, sequenceNumber: UInt8, amountOfChunksToSend: UInt8) { + + guard otapIsRunning else { return } + + var sequenceNumber = sequenceNumber + + for i: UInt8 in 0 ..< amountOfChunksToSend { + sendChunk(peripheral, startPosition:startPosition, blockSize: blockSize, chunkSize: chunkSize, sequenceNumber: sequenceNumber + i) + } + + // Print progress + let start = startPosition / UInt32(chunkSize) + let end = UInt32(hexiwearFWLength) / UInt32(chunkSize) + 1 + let progress = start + UInt32(sequenceNumber) + UInt32(amountOfChunksToSend) + + let progressPercentage = Float(progress)/Float(end) + progressView.progress = progressPercentage + + if progress <= end { + setInfo("Uploaded \(progress) of \(end) chunks (\(formatter.stringFromNumber(Float(100 * progressPercentage))!))%") + } + + if sequenceNumber < 0xFF - amountOfChunksToSend + 1 { + // Continue with next chunks + sequenceNumber += amountOfChunksToSend + + delay((GAP_PPCP_connectionInterval + GAP_PPCP_packetLeeway) * Double(amountOfChunksToSend)) { + self.sendImageChunks(peripheral, startPosition: startPosition, blockSize: blockSize, chunkSize: chunkSize, sequenceNumber: sequenceNumber, amountOfChunksToSend: amountOfChunksToSend) + } + } + } + + func sendChunk(peripheral: CBPeripheral, startPosition: UInt32, blockSize: UInt32, chunkSize: UInt16, sequenceNumber: UInt8) { + guard otapIsRunning else { return } + + let currentChunkStart = startPosition + UInt32(UInt16(sequenceNumber) * chunkSize) + let proposedChunkEnd = currentChunkStart + UInt32(chunkSize) - 1 + let isProposedChunkEndBeyondFileSize = proposedChunkEnd >= UInt32(hexiwearFWLength) + let currentChunkEnd = isProposedChunkEndBeyondFileSize ? hexiwearFWLength - 1 : Int(proposedChunkEnd) + + guard UInt32(hexiwearFWLength) > currentChunkStart else { return } + + let data: [UInt8] = [UInt8](hexiwearFW[Int(currentChunkStart)...Int(currentChunkEnd)]) + let imageChunkCmd = OTAP_CMD_ImgChunkAtt(seqNumber: sequenceNumber, data: data) + let imageChunkBinary = imageChunkCmd.convertToBinary() + let imageChunkData = NSData(bytes: imageChunkBinary, length: imageChunkBinary.count) + + peripheral.writeValue(imageChunkData, forCharacteristic: otapDataCharacteristic!, type: CBCharacteristicWriteType.WithoutResponse) + } + +} + diff --git a/iOS/Hexiwear/Feed.swift b/iOS/Hexiwear/Feed.swift new file mode 100644 index 0000000..7dcb9e6 --- /dev/null +++ b/iOS/Hexiwear/Feed.swift @@ -0,0 +1,114 @@ +// +// Hexiwear application is used to pair with Hexiwear BLE devices +// and send sensor readings to WolkSense sensor data cloud +// +// Copyright (C) 2016 WolkAbout Technology s.r.o. +// +// Hexiwear is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Hexiwear is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . +// +// +// Feed.swift +// + +import UIKit +import MapKit + +public enum ReadingType: String { + case TEMPERATURE = "T" + case PRESSURE = "P" + case HUMIDITY = "H" + case GPS = "G" + case BATTERY = "B" + case MOTION = "M" + case CONNECTION = "C" + case ALARM = "A" + case LIGHT = "LT" + case ACCELEROMETER = "ACL" + case MAGNETOMETER = "MAG" + case GYROSCOPE = "GYR" + case STEPS = "STP" + case HEARTRATE = "BPM" +} + +extension ReadingType: CustomStringConvertible { + public var description: String { + return self.rawValue + } +} + +struct Feed { + let device: Device + let id: Int + let readingType: ReadingType + let currentValue: String + let trend: String + var enabled: Bool + let order: Int + let alarmState: String + var alarmHigh: String + var alarmHighEnabled: Bool + var alarmLow: String + var alarmLowEnabled: Bool + + init? (device: Device, id: Int, readingType: String, currentValue: String, trend: String, enabled: Bool, order: Int, alarmState: String, alarmHigh: String, alarmHighEnabled: Bool, alarmLow: String, alarmLowEnabled: Bool) { + + guard let feedType = ReadingType(rawValue: readingType) else { return nil } + + self.device = device + self.id = id + self.readingType = feedType + self.currentValue = currentValue + self.trend = trend + self.enabled = enabled + self.order = order + self.alarmState = alarmState + self.alarmHigh = alarmHigh + self.alarmHighEnabled = alarmHighEnabled + self.alarmLow = alarmLow + self.alarmLowEnabled = alarmLowEnabled + } + + static func parseFeedJSON(feedJson: [String:AnyObject], device: Device) -> Feed? { + if let id = feedJson["id"] as? Int, + readingType = feedJson["readingType"] as? String, + currentValue = feedJson["currentValue"] as? String, + trend = feedJson["trend"] as? String, + enabled = feedJson["enabled"] as? Bool, + order = feedJson["order"] as? Int, + alarmState = feedJson["alarmState"] as? String, + alarmHigh = feedJson["alarmHigh"] as? String, + alarmHighEnabled = feedJson["alarmHighEnabled"] as? Bool, + alarmLow = feedJson["alarmLow"] as? String, + alarmLowEnabled = feedJson["alarmLowEnabled"] as? Bool { + return Feed(device: device, id: id, readingType: readingType, currentValue: currentValue, trend: trend, enabled: enabled, order: order, alarmState: alarmState, alarmHigh: alarmHigh, alarmHighEnabled: alarmHighEnabled, alarmLow: alarmLow, alarmLowEnabled: alarmLowEnabled) + } + + return nil + } +} + +extension Feed: CustomStringConvertible { + internal var description: String { + return "FEED {\n\t id:\(id),\n\t enabled:\(enabled),\n\t name:\(device.name),\n\t \n\t currentValue:\(currentValue)\n}\n" + } +} + + +func ==(lhs: Feed, rhs: Feed) -> Bool { + return lhs.id == rhs.id +} + +extension Feed: Hashable { + var hashValue: Int { return id } +} diff --git a/iOS/Hexiwear/FirmwareSelectionTableViewController.swift b/iOS/Hexiwear/FirmwareSelectionTableViewController.swift new file mode 100644 index 0000000..adab1f4 --- /dev/null +++ b/iOS/Hexiwear/FirmwareSelectionTableViewController.swift @@ -0,0 +1,282 @@ +// +// Hexiwear application is used to pair with Hexiwear BLE devices +// and send sensor readings to WolkSense sensor data cloud +// +// Copyright (C) 2016 WolkAbout Technology s.r.o. +// +// Hexiwear is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Hexiwear is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . +// +// +// FirmwareSelectionTableViewController.swift +// + +import UIKit +import CoreBluetooth + + +struct FirmwareFileItem { + let fileName: String + let fileVersion: String + let factory: Bool +} + +class FirmwareSelectionTableViewController: UITableViewController { + + var firmwareFilesKW40: [FirmwareFileItem] = [] + var firmwareFilesMK64: [FirmwareFileItem] = [] + var peri: CBPeripheral! + var selectedFile: String = "" + var selectedFileName: String = "" + var selectedType: String = "" + var selectedFileVersion: String = "" + var privateDocsDir: String? + var refreshCont: UIRefreshControl! + var hexiwearDelegate: HexiwearPeripheralDelegate? + var otapDelegate: OTAPDelegate? + + + override func viewDidLoad() { + super.viewDidLoad() + + self.refreshCont = UIRefreshControl() + self.refreshCont.addTarget(self, action: #selector(FirmwareSelectionTableViewController.refresh(_:)), forControlEvents: UIControlEvents.ValueChanged) + self.tableView.addSubview(refreshCont) + + title = "Select firmware file" + } + + func refresh(sender:AnyObject) { + // Code to refresh table view + getFirmwareFiles() + tableView.reloadData() + self.refreshCont.endRefreshing() + } + + override func viewWillAppear(animated: Bool) { + getFirmwareFiles() + } + + override func viewWillDisappear(animated: Bool) { + if isMovingFromParentViewController() { + hexiwearDelegate?.didUnwind() + } + } + + private func getFactorySettingsFirmwareFiles() { + firmwareFilesKW40 = [] + firmwareFilesMK64 = [] + + guard let _ = NSBundle.mainBundle().pathForResource("HEXIWEAR_KW40_factory_settings", ofType: "img") else { return } + let kw40FW = FirmwareFileItem(fileName: "HEXIWEAR_KW40_factory_settings.img", fileVersion: "version: 1.0.0", factory: true) + firmwareFilesKW40.append(kw40FW) + + guard let _ = NSBundle.mainBundle().pathForResource("HEXIWEAR_MK64_factory_settings", ofType: "img") else { return } + let mk64FW = FirmwareFileItem(fileName: "HEXIWEAR_MK64_factory_settings.img", fileVersion: "version: 1.0.0", factory: true) + firmwareFilesMK64.append(mk64FW) + } + + private func getFirmwareFiles() { + firmwareFilesKW40 = [] + firmwareFilesMK64 = [] + + getFactorySettingsFirmwareFiles() + privateDocsDir = getPrivateDocumentsDirectory() + let fileManager = NSFileManager.defaultManager() + + do { + let fwFilesPaths = try fileManager.contentsOfDirectoryAtPath(privateDocsDir!) + for file in fwFilesPaths { + let fullFileName = (self.privateDocsDir! as NSString).stringByAppendingPathComponent(file) + + if let data = NSData(contentsOfFile: fullFileName) { + var dataBytes: [UInt8] = [UInt8](count: data.length, repeatedValue: 0x00) + let length = data.length + data.getBytes(&dataBytes, length: length) + + // Parse FW file header + if let otapImageFileHeader = OTAImageFileHeader(data: data) { + selectedFileVersion = "version: \(otapImageFileHeader.imageVersionAsString())" + let firmwareFile = FirmwareFileItem(fileName: file, fileVersion: selectedFileVersion, factory: false) + + if otapImageFileHeader.imageId == 1 { + firmwareFilesKW40.append(firmwareFile) + } + else if otapImageFileHeader.imageId == 2 { + firmwareFilesMK64.append(firmwareFile) + } + } + } + } + } catch { + print(error) + } + } + + override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) { + if segue.identifier == "toFWUpgrade" { + if let vc = segue.destinationViewController as? FWUpgradeViewController { + vc.fwPath = selectedFile + vc.fwFileName = selectedFileName + vc.fwTypeString = selectedType + vc.fwVersion = selectedFileVersion + vc.hexiwearPeripheral = peri + vc.hexiwearDelegate = self.hexiwearDelegate + vc.otapDelegate = self.otapDelegate + } + } + } + + // MARK: - Table view data source + + override func numberOfSectionsInTableView(tableView: UITableView) -> Int { + return 2 + } + + override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + return section == 0 ? firmwareFilesKW40.count : firmwareFilesMK64.count + } + + override func tableView(tableView: UITableView, titleForHeaderInSection section: Int) -> String? { + if section == 0 { + return "KW40" + } + return "MK64" + } + + override func tableView(tableView: UITableView, willDisplayHeaderView view: UIView, forSection section: Int) { + if let headerView = view as? UITableViewHeaderFooterView { + headerView.contentView.backgroundColor = UIColor(red: 127.0/255.0, green: 147.0/255.0, blue: 0.0, alpha: 0.6) + headerView.alpha = 0.95 + } + } + + + + override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell { + let cell = tableView.dequeueReusableCellWithIdentifier("firmwareCell", forIndexPath: indexPath) + + if indexPath.section == 0 { + let firmwareFile = firmwareFilesKW40[indexPath.row] + cell.textLabel?.text = firmwareFile.fileName + cell.detailTextLabel?.text = firmwareFile.fileVersion + } + else { + let firmwareFile = firmwareFilesMK64[indexPath.row] + cell.textLabel?.text = firmwareFile.fileName + cell.detailTextLabel?.text = firmwareFile.fileVersion + } + return cell + } + + + override func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) { + + // If the first item of any section is tapped, that is factory file which is embedded in bundle + // File path is different than for other FW files + + if indexPath.section == 0 && indexPath.row == 0 { // factory KW40 + guard let strPathKW40 = NSBundle.mainBundle().pathForResource("HEXIWEAR_KW40_factory_settings", ofType: "img") else { return } + selectedType = "KW40" + selectedFileVersion = "1.0.0" + selectedFileName = "HEXIWEAR_KW40_factory_settings.img" + selectedFile = strPathKW40 + performSegueWithIdentifier("toFWUpgrade", sender: nil) + + return + } + + if indexPath.section == 1 && indexPath.row == 0 { // factory MK64 + guard let strPathMK64 = NSBundle.mainBundle().pathForResource("HEXIWEAR_MK64_factory_settings", ofType: "img") else { return } + selectedType = "MK64" + selectedFileVersion = "1.0.0" + selectedFileName = "HEXIWEAR_MK64_factory_settings.img" + selectedFile = strPathMK64 + performSegueWithIdentifier("toFWUpgrade", sender: nil) + + return + } + + + guard let selectedFWFile = getFileForIndexPath(indexPath) else { return } + selectedFileName = selectedFWFile + let fullFileName = (self.privateDocsDir! as NSString).stringByAppendingPathComponent(selectedFWFile) + selectedFile = fullFileName + if let data = NSData(contentsOfFile: fullFileName) { + var dataBytes: [UInt8] = [UInt8](count: data.length, repeatedValue: 0x00) + let length = data.length + data.getBytes(&dataBytes, length: length) + + // Parse FW file header + if let otapImageFileHeader = OTAImageFileHeader(data: data) { + if otapImageFileHeader.imageId == 1 { + selectedType = "KW40" + } + else if otapImageFileHeader.imageId == 2 { + selectedType = "MK64" + } + selectedFileVersion = otapImageFileHeader.imageVersionAsString() + } + } + + performSegueWithIdentifier("toFWUpgrade", sender: nil) + + } + + override func tableView(tableView: UITableView, canEditRowAtIndexPath indexPath: NSIndexPath) -> Bool { + // let the controller to know that able to edit tableView's row + return true + } + + + override func tableView(tableView: UITableView, editActionsForRowAtIndexPath indexPath: NSIndexPath) -> [UITableViewRowAction]? { + let deleteAction = UITableViewRowAction(style: .Default, title: "Delete", handler: { (action , indexPath) -> Void in + + let item = indexPath.section == 0 ? self.firmwareFilesKW40[indexPath.row] : self.firmwareFilesMK64[indexPath.row] + + let fileManager = NSFileManager.defaultManager() + + do { + let fullFileName = (self.privateDocsDir! as NSString).stringByAppendingPathComponent(item.fileName) + try fileManager.removeItemAtPath(fullFileName) + dispatch_async(dispatch_get_main_queue()) { + if indexPath.section == 0 { + self.firmwareFilesKW40.removeAtIndex(indexPath.row) + } + else { + self.firmwareFilesMK64.removeAtIndex(indexPath.row) + } + self.tableView.deleteRowsAtIndexPaths([indexPath], withRowAnimation: .Fade) + } + } catch { + print(error) + } + }) + + deleteAction.backgroundColor = UIColor.redColor() + + return [deleteAction] + } + + override func tableView(tableView: UITableView, heightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat { + return 60.0 + } + + private func getFileForIndexPath(indexPath: NSIndexPath) -> String? { + if indexPath.section == 0 { + return indexPath.row >= firmwareFilesKW40.count ? nil : firmwareFilesKW40[indexPath.row].fileName + } + + return indexPath.row >= firmwareFilesMK64.count ? nil : firmwareFilesMK64[indexPath.row].fileName + } +} diff --git a/iOS/Hexiwear/ForgotPasswordViewController.swift b/iOS/Hexiwear/ForgotPasswordViewController.swift new file mode 100644 index 0000000..9d3b307 --- /dev/null +++ b/iOS/Hexiwear/ForgotPasswordViewController.swift @@ -0,0 +1,88 @@ +// +// Hexiwear application is used to pair with Hexiwear BLE devices +// and send sensor readings to WolkSense sensor data cloud +// +// Copyright (C) 2016 WolkAbout Technology s.r.o. +// +// Hexiwear is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Hexiwear is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . +// +// +// ForgotPasswordViewController.swift +// + +import UIKit + +protocol ForgotPasswordDelegate { + func didFinishResettingPassword() +} + +class ForgotPasswordViewController: SingleTextViewController { + + var forgotPasswordDelegate: ForgotPasswordDelegate? + @IBOutlet weak var emailText: UITextField! + @IBOutlet weak var errorLabel: UILabel! + + + override func viewDidLoad() { + super.viewDidLoad() + + skipButton.title = "Done" + actionButton.title = "Reset" + emailText.delegate = self + emailText.text = "" + title = "Reset password" + errorLabel.hidden = true + } + + override func toggleActivateButtonEnabled() { + if let em = emailText.text where isValidEmailAddress(em) { + actionButton.enabled = true + } + else { + actionButton.enabled = false + } + } + + override func actionButtonAction() { + dataStore.resetPasswordForUserEmail(emailText.text!, + onFailure: { _ in + dispatch_async(dispatch_get_main_queue()) { + self.errorLabel.hidden = false + self.view.setNeedsDisplay() + } + }, + onSuccess: { + showSimpleAlertWithTitle(applicationTitle, message: "Check your email for new password.", viewController: self, OKhandler: { _ in self.forgotPasswordDelegate?.didFinishResettingPassword() + }) + } + ) + } + + override func skipButtonAction() { + forgotPasswordDelegate?.didFinishResettingPassword() + } + + @IBAction func emailChanged(sender: UITextField) { + toggleActivateButtonEnabled() + errorLabel.hidden = true + self.view.setNeedsDisplay() + } +} + +extension ForgotPasswordViewController: UITextFieldDelegate { + func textFieldShouldReturn(textField: UITextField) -> Bool { + textField.resignFirstResponder() + return true + } +} diff --git a/iOS/Hexiwear/GyroTableViewCell.swift b/iOS/Hexiwear/GyroTableViewCell.swift new file mode 100644 index 0000000..61e8dc6 --- /dev/null +++ b/iOS/Hexiwear/GyroTableViewCell.swift @@ -0,0 +1,68 @@ +// +// Hexiwear application is used to pair with Hexiwear BLE devices +// and send sensor readings to WolkSense sensor data cloud +// +// Copyright (C) 2016 WolkAbout Technology s.r.o. +// +// Hexiwear is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Hexiwear is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . +// +// +// GyroTableViewCell.swift +// + +import UIKit + +class GyroTableViewCell: UITableViewCell { + + @IBOutlet private weak var xLabel: UILabel! + @IBOutlet private weak var yLabel: UILabel! + @IBOutlet private weak var zLabel: UILabel! + + override func awakeFromNib() { + super.awakeFromNib() + // Initialization code + } + + override func setSelected(selected: Bool, animated: Bool) { + super.setSelected(selected, animated: animated) + } + + var xValue: String { + get { + return xLabel.text! + } + set (newX) { + xLabel.text = newX + } + } + + var yValue: String { + get { + return yLabel.text! + } + set (newY) { + yLabel.text = newY + } + } + + var zValue: String { + get { + return zLabel.text! + } + set (newZ) { + zLabel.text = newZ + } + } + +} diff --git a/iOS/Hexiwear/HEXIWEAR_KW40_factory_settings.img b/iOS/Hexiwear/HEXIWEAR_KW40_factory_settings.img new file mode 100644 index 0000000..31d0611 Binary files /dev/null and b/iOS/Hexiwear/HEXIWEAR_KW40_factory_settings.img differ diff --git a/iOS/Hexiwear/HEXIWEAR_MK64_factory_settings.img b/iOS/Hexiwear/HEXIWEAR_MK64_factory_settings.img new file mode 100644 index 0000000..7744860 Binary files /dev/null and b/iOS/Hexiwear/HEXIWEAR_MK64_factory_settings.img differ diff --git a/iOS/Hexiwear/Hexiwear-Bridging-Header.h b/iOS/Hexiwear/Hexiwear-Bridging-Header.h new file mode 100644 index 0000000..b4b71fc --- /dev/null +++ b/iOS/Hexiwear/Hexiwear-Bridging-Header.h @@ -0,0 +1,27 @@ +// +// Hexiwear application is used to pair with Hexiwear BLE devices +// and send sensor readings to WolkSense sensor data cloud +// +// Copyright (C) 2016 WolkAbout Technology s.r.o. +// +// Hexiwear is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Hexiwear is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . +// +// +// Use this file to import your target's public headers that you would like to expose to Swift. +// + +#import "JGProgressHUD/JGProgressHUD.h" +#import "GCDAsyncSocket.h" +#import "MSWeakTimer.h" + diff --git a/iOS/Hexiwear/Hexiwear.swift b/iOS/Hexiwear/Hexiwear.swift new file mode 100644 index 0000000..b371126 --- /dev/null +++ b/iOS/Hexiwear/Hexiwear.swift @@ -0,0 +1,1021 @@ +// +// Hexiwear application is used to pair with Hexiwear BLE devices +// and send sensor readings to WolkSense sensor data cloud +// +// Copyright (C) 2016 WolkAbout Technology s.r.o. +// +// Hexiwear is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Hexiwear is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . +// +// +// Hexiwear.swift +// + +import Foundation +import CoreBluetooth + +let deviceName = "HEXIWEAR" +let deviceNameLong = "HEXIWEAR" +let deviceNameOtap = "HEXIOTAP" +let deviceNameLongOtap = "HEXIOTAP" + +// Service UUIDs +let GAServiceUUID = CBUUID(string: "1800") // Generic Access +let DIServiceUUID = CBUUID(string: "180A") // Device Information +let BatteryServiceUUID = CBUUID(string: "180F") // Battery service + +// HEXIWEAR Custom Service UUIDs +let MotionServiceUUID = CBUUID(string: "2000") +let WeatherServiceUUID = CBUUID(string: "2010") +let HealthServiceUUID = CBUUID(string: "2020") +let AlertServiceUUID = CBUUID(string: "2030") +let HexiwearModeServiceUUID = CBUUID(string: "2040") +let OTAPServiceUUID = CBUUID(string: "01FF5550-BA5E-F4EE-5CA1-EB1E5E4B1CE0") + +// Characteristic UUIDs + +// General Access +let GADeviceNameUUID = CBUUID(string: "2A00") // UTF8 string +let GAAppearanceUUID = CBUUID(string: "2A01") // 16bit? +let GAPPConnParamsUUID = CBUUID(string: "2A04") // uint16[4] - some suitable values + +let SerialNumberUUID = CBUUID(string: "2A25") + + +// Device information +let DIManufacturerUUID = CBUUID(string: "2A29") // UTF8 string +let DIHWRevisionUUID = CBUUID(string: "2A27") // UTF8 string +let DIFWRevisionUUID = CBUUID(string: "2A26") // UTF8 string + +// Battery level +let BatteryLevelUUID = CBUUID(string: "2A19") // uint8 - battery level in % + +// Motion +let MotionAccelerometerUUID = CBUUID(string: "2001") // int16[3] x, y, z in +/- 4g. Multiplied by 100 +let MotionMagnetometerUUID = CBUUID(string: "2003") // int16[3] x, y, z magnet in uT. Multiplied by 100 +let MotionGyroUUID = CBUUID(string: "2002") // int16[3] x, y, z +/- 256 deg/sec. Multiplied by 100 + +// Weather +let WeatherLightUUID = CBUUID(string: "2011") // uint8 light level in % +let WeatherTemperatureUUID = CBUUID(string: "2012") // int16 temperature in Celsius. Multiplied by 100 +let WeatherHumidityUUID = CBUUID(string: "2013") // uint16 relative humidity in %. Multiplied by 100 +let WeatherPressureUUID = CBUUID(string: "2014") // uint16 in Pascals. Multiplied by 100 + +// Health +let HealthHeartRateUUID = CBUUID(string: "2021") // uint8 beats per minute +let HealthStepsUUID = CBUUID(string: "2022") // uint16 number of steps +let HealthCaloriesUUID = CBUUID(string: "2023") // uint16 value of kcal + +// Alert +let AlertINUUID = CBUUID(string: "2031") // uint8[20] Alerts and commands from Phone TO Watch +let AlertOUTUUID = CBUUID(string: "2032") // uint8[20] Alerts and commands from Watch TO Phone + +// Hexiwear mode +let HexiwearModeUUID = CBUUID(string: "2041") // UInt8 + + +// OTAP +let OTAPControlPointUUID = CBUUID(string: "01FF5551-BA5E-F4EE-5CA1-EB1E5E4B1CE0") +let OTAPDataUUID = CBUUID(string: "01FF5552-BA5E-F4EE-5CA1-EB1E5E4B1CE0") +let OTAPStateUUID = CBUUID(string: "01FF5553-BA5E-F4EE-5CA1-EB1E5E4B1CE0") // 0 - no otap, 1 - KW40, 2 - MK64 + + +//MARK:- Watch readings +struct DeviceInfo { + var manufacturer: String + var firmwareRevision: String + + init() { + manufacturer = "" + firmwareRevision = "" + } +} + +public enum HexiwearMode: Int { + case IDLE = 0 // All sensors off + case WATCH = 1 // temp and battery + case SENSOR_TAG = 2 // All sensors on + case WEATHER_STATION = 3 // Temperature, humidity, pressure data available + case MOTION_CONTROL = 4 // Accel + case HEARTRATE = 5 // heart rate data available + case PEDOMETER = 6 // Pedometer data available + case COMPASS = 7 // ? + + static func getReadingsForMode(mode: HexiwearMode) -> [HexiwearReading] { + switch mode { + case .SENSOR_TAG: return [.BATTERY, .ACCELEROMETER, .MAGNETOMETER, .GYRO, .TEMPERATURE, .HUMIDITY, .PRESSURE, .LIGHT] + case .PEDOMETER: return [.PEDOMETER, .CALORIES] + case .HEARTRATE: return [.HEARTRATE] + default: return [] + } + } +} + +public enum HexiwearReading: Int { + case BATTERY = 0 + case ACCELEROMETER = 1 + case MAGNETOMETER = 2 + case GYRO = 3 + case TEMPERATURE = 4 + case HUMIDITY = 5 + case PRESSURE = 6 + case LIGHT = 7 + case PEDOMETER = 8 + case HEARTRATE = 9 + case CALORIES = 10 +} + +public enum OtapState: UInt8 { + case NO_OTAP = 0 + case KW40 = 1 + case MK64 = 2 +} + +struct HexiwearReadings { + var batteryLevel: Double? + var motionAccelX: Double? + var motionAccelY: Double? + var motionAccelZ: Double? + var motionMagnetX: Double? + var motionMagnetY: Double? + var motionMagnetZ: Double? + var motionGyroX: Double? + var motionGyroY: Double? + var motionGyroZ: Double? + var ambientLight: Double? + var ambientTemperature: Double? + var relativeHumidity: Double? + var airPressure: Double? + var heartRate: Int? + var steps: Int? + var calories: Int? + var lastWeatherDate: NSDate? + var lastGyroDate: NSDate? + var lastAccelDate: NSDate? + var lastMagnetDate: NSDate? + var lastStepsDate: NSDate? + var lastCaloriesDate: NSDate? + var lastHeartRateDate: NSDate? + var hexiwearMode: HexiwearMode + + private let nf = NSNumberFormatter() + private let mqttNF = NSNumberFormatter() + private let mqttValueNF = NSNumberFormatter() + + init() { + nf.numberStyle = .NoStyle + nf.minimumFractionDigits = 1 + nf.maximumFractionDigits = 1 + nf.minimumIntegerDigits = 1 + + mqttNF.numberStyle = .NoStyle + mqttNF.minimumFractionDigits = 0 + mqttNF.maximumFractionDigits = 0 + mqttNF.minimumIntegerDigits = 1 + + + mqttValueNF.numberStyle = .NoStyle + mqttValueNF.minimumFractionDigits = 1 + mqttValueNF.maximumFractionDigits = 1 + mqttValueNF.minimumIntegerDigits = 1 + mqttValueNF.minusSign = "" + mqttValueNF.decimalSeparator = "" + + hexiwearMode = .IDLE + } + + init(batteryLevel: Double?, temperature: Double?, pressure: Double?, humidity: Double?, accelX: Double?, accelY: Double?, accelZ: Double?, gyroX: Double?, gyroY: Double?, gyroZ: Double?, magnetX: Double?, magnetY: Double?, magnetZ: Double?, steps: Int?, calories: Int?, heartRate: Int?, ambientLight: Double?, hexiwearMode: HexiwearMode) { + self.init() + self.batteryLevel = batteryLevel + self.ambientTemperature = temperature + self.airPressure = pressure + self.relativeHumidity = humidity + self.motionAccelX = accelX + self.motionAccelY = accelY + self.motionAccelZ = accelZ + self.motionGyroX = gyroX + self.motionGyroY = gyroY + self.motionGyroZ = gyroZ + self.motionMagnetX = magnetX + self.motionMagnetY = magnetY + self.motionMagnetZ = magnetZ + self.steps = steps + self.calories = calories + self.heartRate = heartRate + self.ambientLight = ambientLight + self.hexiwearMode = hexiwearMode + } + + func batteryLevelAsString() -> String { + return stringFromReading(batteryLevel) + } + + func motionAccelXAsString() -> String { + return stringFromReading(motionAccelX) + } + + func motionAccelYAsString() -> String { + return stringFromReading(motionAccelY) + } + + func motionAccelZAsString() -> String { + return stringFromReading(motionAccelZ) + } + + func motionMagnetXAsString() -> String { + return stringFromReading(motionMagnetX) + } + + func motionMagnetYAsString() -> String { + return stringFromReading(motionMagnetY) + } + + func motionMagnetZAsString() -> String { + return stringFromReading(motionMagnetZ) + } + + func motionGyroXAsString() -> String { + return stringFromReading(motionGyroX) + } + + func motionGyroYAsString() -> String { + return stringFromReading(motionGyroY) + } + + func motionGyroZAsString() -> String { + return stringFromReading(motionGyroZ) + } + + func ambientLightAsString() -> String { + return stringFromReading(ambientLight) + } + + func ambientTemperatureAsString() -> String { + return stringFromReading(ambientTemperature) + } + + func relativeHumidityAsString() -> String { + return stringFromReading(relativeHumidity) + } + + func airPressureAsString() -> String { + return stringFromReading(airPressure) + } + + func heartRateAsString() -> String { + return stringFromReading(heartRate) + } + + func stepsAsString() -> String { + return stringFromReading(steps) + } + + func caloriesAsString() -> String { + return stringFromReading(calories) + } + + //MARK:- Readings private helpers + + private func stringFromReading(readingValue: Double?) -> String { + guard let readingValue = readingValue else { return "--" } + return nf.stringFromNumber(readingValue) ?? "--" + } + + private func stringFromReading(readingValue: Int?) -> String { + guard let readingValue = readingValue else { return "--" } + return mqttNF.stringFromNumber(readingValue) ?? "--" + } + + private func ambientTemperatureAsMQTTString() -> String? { + guard let temperature = ambientTemperature else { return nil } + if let temp = mqttNF.stringFromNumber(temperature * 10.0) { + return "T:\(temp)" + } + return nil + } + + private func relativeHumidityAsMQTTString() -> String? { + guard let humidity = relativeHumidity else { return nil } + if let temp = mqttNF.stringFromNumber(humidity * 10.0) { + return "H:\(temp)" + } + return nil + } + + private func airPressureAsMQTTString() -> String? { + guard let pressure = airPressure else { return nil } + if let temp = mqttNF.stringFromNumber(pressure * 10.0) { + return "P:\(temp)" + } + return nil + } + + + private func MQTTValue(doubleValue: Double) -> String? { + let sign = doubleValue >= 0.0 ? "+" : "-" + guard let stringValue = mqttValueNF.stringFromNumber(doubleValue) else { + return nil + } + return sign + stringValue + } + + private func ambientLightAsMQTTString() -> String? { + guard let light = ambientLight else { return nil } + + guard let lightValue = MQTTValue(light) else { return nil } + + return "LT:\(lightValue)" + } + + private func accelAsMQTTString() -> String? { + guard let x = motionAccelX, + y = motionAccelY, + z = motionAccelZ else { return nil } + guard let ax = MQTTValue(x) else { return nil } + guard let ay = MQTTValue(y) else { return nil } + guard let az = MQTTValue(z) else { return nil } + return "ACL:\(ax)\(ay)\(az)" + } + + private func gyroAsMQTTString() -> String? { + guard let x = motionGyroX, + y = motionGyroY, + z = motionGyroZ else { return nil } + guard let gx = MQTTValue(x) else { return nil } + guard let gy = MQTTValue(y) else { return nil } + guard let gz = MQTTValue(z) else { return nil } + return "GYR:\(gx)\(gy)\(gz)" + } + + private func magnetAsMQTTString() -> String? { + guard let x = motionMagnetX, + y = motionMagnetY, + z = motionMagnetZ else { return nil } + guard let mx = MQTTValue(x) else { return nil } + guard let my = MQTTValue(y) else { return nil } + guard let mz = MQTTValue(z) else { return nil } + return "MAG:\(mx)\(my)\(mz)" + } + + private func stepsAsMQTTString() -> String? { + guard let s = steps else { return nil } + + return "STP:\(s)" + } + + private func caloriesAsMQTTString() -> String? { + guard let c = calories else { return nil } + + return "KCAL:\(c)" + } + + private func heartRateAsMQTTString() -> String? { + guard let h = heartRate else { return nil } + + return "BPM:\(h)" + } + + private func arrayOfReadingsToMQTT(readings: [String]) -> String? { + guard readings.count > 0 else { return nil } + let rtcTimestamp = Int64(NSDate().timeIntervalSince1970) + let readingsMQTT = "RTC \(rtcTimestamp);READINGS " + + var readingsMQTTTail = "" + + for item in readings { + readingsMQTTTail += (item + ",") + } + + if readingsMQTTTail.isEmpty { + return nil + } + + return readingsMQTT + "R:\(rtcTimestamp)," + String(readingsMQTTTail.characters.dropLast()) + ";" + } + + + + func asMQTTMessage() -> String? { + let mqttPressure = airPressureAsMQTTString() + let mqttTemperature = ambientTemperatureAsMQTTString() + let mqttHumidity = relativeHumidityAsMQTTString() + let mqttLight = ambientLightAsMQTTString() + let mqttAccel = accelAsMQTTString() + let mqttGyro = gyroAsMQTTString() + let mqttMagnet = magnetAsMQTTString() + let mqttSteps = stepsAsMQTTString() + let mqttCalories = caloriesAsMQTTString() + let mqttHeartRate = heartRateAsMQTTString() + + let readings = [mqttPressure, mqttTemperature, mqttHumidity, mqttLight, mqttAccel, mqttGyro, mqttMagnet, mqttSteps, mqttCalories, mqttHeartRate] + let filteredReadings = readings.filter { return $0 != nil }.map { return $0!} + + return arrayOfReadingsToMQTT(filteredReadings) + } + +} + +class Hexiwear { + + + //MARK:- BLE checks and validations + // Check name of device from advertisement data + class func hexiwearFound (advertisementData: [NSObject : AnyObject]!) -> Bool { + let nameOfDeviceFound = (advertisementData as NSDictionary).objectForKey(CBAdvertisementDataLocalNameKey) as? NSString + return (nameOfDeviceFound == deviceName || nameOfDeviceFound == deviceNameLong) + } + + + // Check name of device from advertisement data + class func hexiotapFound (advertisementData: [NSObject : AnyObject]!) -> Bool { + let otapServices = (advertisementData as NSDictionary).objectForKey(CBAdvertisementDataServiceUUIDsKey) as? [CBUUID] + + if let otapServiceAdvertised = otapServices { + return otapServiceAdvertised.contains(OTAPServiceUUID) + } + return false + } + + // Check if the service has a valid UUID + class func validService (service : CBService, isOTAPEnabled: Bool) -> Bool { + if isOTAPEnabled { + return service.UUID == OTAPServiceUUID + } + + return service.UUID == BatteryServiceUUID + || service.UUID == MotionServiceUUID + || service.UUID == WeatherServiceUUID + || service.UUID == HealthServiceUUID + || service.UUID == OTAPServiceUUID + || service.UUID == DIServiceUUID + || service.UUID == HexiwearModeServiceUUID + || service.UUID == AlertServiceUUID + } + + // Check if the characteristic has a valid data UUID + class func validDataCharacteristic (characteristic : CBCharacteristic, isOTAPEnabled: Bool) -> Bool { + + if isOTAPEnabled { + return characteristic.UUID == OTAPControlPointUUID + || characteristic.UUID == OTAPDataUUID + || characteristic.UUID == OTAPStateUUID + } + + return characteristic.UUID == BatteryLevelUUID + || characteristic.UUID == MotionAccelerometerUUID + || characteristic.UUID == MotionMagnetometerUUID + || characteristic.UUID == MotionGyroUUID + || characteristic.UUID == WeatherLightUUID + || characteristic.UUID == WeatherTemperatureUUID + || characteristic.UUID == WeatherHumidityUUID + || characteristic.UUID == WeatherPressureUUID + || characteristic.UUID == HealthHeartRateUUID + || characteristic.UUID == HealthStepsUUID + || characteristic.UUID == HealthCaloriesUUID + || characteristic.UUID == OTAPControlPointUUID + || characteristic.UUID == OTAPDataUUID + || characteristic.UUID == SerialNumberUUID + || characteristic.UUID == DIManufacturerUUID + || characteristic.UUID == DIHWRevisionUUID + || characteristic.UUID == DIFWRevisionUUID + || characteristic.UUID == HexiwearModeUUID + || characteristic.UUID == AlertINUUID + + } + + //MARK:- Characteristics value parsing + + // Get battery level value + class func getBatteryLevel(value : NSData?) -> Double? { + guard let value = value else { return nil } + + let dataFromSensor: [UInt8] = dataToIntegerArray(value) + let batteryLevel = Double(dataFromSensor[0]) + return batteryLevel + } + + // Get motion accelerometer values + class func getMotionAccelerometerValues(value : NSData?) -> (x: Double, y: Double, z: Double)? { + return getXYZValues(value, divideBy: 100.0) + } + + // Get motion magnetometer values + class func getMotionMagnetometerValues(value : NSData?) -> (x: Double, y: Double, z: Double)? { + return getXYZValues(value, divideBy: 100.0) + } + + // Get motion gyro values + class func getMotionGyroValues(value : NSData?) -> (x: Double, y: Double, z: Double)? { + return getXYZValues(value, divideBy: 1.0) + } + + // Get ambient light value + class func getAmbientLight(value : NSData?) -> Double? { + guard let value = value else { return nil } + + let dataFromSensor: [UInt8] = dataToIntegerArray(value) + let ambientLight = Double(dataFromSensor[0]) + return ambientLight + } + + // Get ambient temperature value + class func getAmbientTemperature(value : NSData?) -> Double? { + guard let value = value else { return nil } + + let dataFromSensor: [Int16] = dataToIntegerArray(value) + let ambientTemperature = Double(dataFromSensor[0])/100 + return ambientTemperature + } + + // Get relative Humidity + class func getRelativeHumidity(value: NSData?) -> Double? { + guard let value = value else { return nil } + + let dataFromSensor: [UInt16] = dataToIntegerArray(value) + let humidity = Double(dataFromSensor[0])/100 + + return humidity + } + + // Get pressure value + class func getPressureData(value: NSData?) -> Double? { + guard let value = value else { return nil } + let dataFromSensor: [UInt16] = dataToIntegerArray(value) + let pressure = Double(dataFromSensor[0])/10 + + return pressure + } + + // Get heart rate + class func getHeartRate(value: NSData?) -> Int? { + guard let value = value else { return nil } + let dataFromSensor: [UInt8] = dataToIntegerArray(value) + let heartRate = Int(dataFromSensor[0]) + return heartRate + } + + // Get steps + class func getSteps(value: NSData?) -> Int? { + guard let value = value else { return nil } + let dataFromSensor: [UInt16] = dataToIntegerArray(value) + let steps = Int(dataFromSensor[0]) + return steps + } + + // Get calories + class func getCalories(value: NSData?) -> Int? { + guard let value = value else { return nil } + let dataFromSensor: [UInt16] = dataToIntegerArray(value) + let calories = Int(dataFromSensor[0]) + return calories + } + + // Get hexiwear mode + class func getHexiwearMode(value: NSData?) -> Int? { + guard let value = value else { return nil } + let dataFromSensor: [UInt8] = dataToIntegerArray(value) + let mode = Int(dataFromSensor[0]) + return mode + } + + // Get otap status + class func getOtapState(value: NSData?) -> OtapState { + guard let value = value else { return OtapState.NO_OTAP } + let dataFromSensor: [UInt8] = dataToIntegerArray(value) + guard dataFromSensor.count > 0 else { return .NO_OTAP } + let state = OtapState(rawValue: dataFromSensor[0]) ?? OtapState.NO_OTAP + return state + } + + // Get Manufacturer + class func getManufacturer(value: NSData?) -> String? { + return getStringFromNSData(value) + } + + // Get Firmware revision + class func getFirmwareRevision(value: NSData?) -> String? { + return getStringFromNSData(value) + } + + // Get Current time + class func getCurrentTimestampForHexiwear(isTimeZoneOffsetIncluded: Bool) -> [UInt8] { + let timezone = NSTimeZone.localTimeZone() + let currentTimestamp = NSDate() + let currentTimestampAsUInt32 = UInt32(currentTimestamp.timeIntervalSince1970) + + let timezoneOffsetInSecs = timezone.secondsFromGMT + + let timezoneOffset = isTimeZoneOffsetIncluded ? UInt32(abs(timezoneOffsetInSecs)) : UInt32(0) + + let correctedTimestampForHexiwear = timezoneOffsetInSecs > 0 ? currentTimestampAsUInt32 + timezoneOffset : currentTimestampAsUInt32 - timezoneOffset + + var returnArray: [UInt8] = [3, 4] // 3 - AlertIn command id for setting time, 4 - lenght of value in bytes + returnArray += uint32ToLittleEndianBytesArray(correctedTimestampForHexiwear) + while returnArray.count < 20 { + returnArray.append(0x00) // append remaining bytes (to 20) with zeroes + } + return returnArray + } + + private class func getStringFromNSData(value: NSData?) -> String? { + guard let value = value else { return nil } + let str = String(data: value, encoding: NSUTF8StringEncoding) + return str + } + + //MARK:- Hexiwear private helpers + private class func getXYZValues(value : NSData?, divideBy: Double) -> (x: Double, y: Double, z: Double)? { + guard let value = value else { return nil } + + let dataFromSensor: [Int16] = dataToIntegerArray(value) + let valueX = Double(dataFromSensor[0]) / divideBy + let valueY = Double(dataFromSensor[1]) / divideBy + let valueZ = Double(dataFromSensor[2]) / divideBy + return (valueX, valueY, valueZ) + } +} + +//MARK:- OTAP + +// BLE OTAP Protocol definitions +let OTAP_CmdIdFieldSize: UInt16 = 1 +let OTAP_ImageIdFieldSize: UInt16 = 2 +let OTAP_ImageVersionFieldSize: UInt16 = 8 +let OTAP_ChunkSeqNumberSize: UInt16 = 1 +let OTAP_MaxChunksPerBlock: UInt16 = 256 +let OTAP_StartPositionSize: UInt16 = 4 +let OTAP_BlockSize: UInt16 = 4 +let OTAP_ChunkSize: UInt16 = 2 +let OTAP_TransferMethodSize: UInt16 = 1 +let OTAP_TransferChannelSize: UInt16 = 2 +let OTAP_ImageFileHeaderLength: UInt16 = 58 +let OTAP_CmdErrorStatusSize: UInt16 = 1 + + +// ATT_MTU +let OTAP_ImageChunkDataSizeAtt: UInt16 = 20 - OTAP_CmdIdFieldSize - OTAP_ChunkSeqNumberSize +let OTAP_TransferMethodAtt: UInt8 = 0x00 +let OTAP_TransferChannelAtt: UInt16 = 0x0004 + +let GAP_PPCP_connectionInterval: Double = 0.050 // 50.0ms +let GAP_PPCP_packetLeeway: Double = 0.0025 // 2.5ms per packet + +// Protocol +protocol ConvertableToBinary { + func convertToBinary() -> [UInt8] +} + +// OTA Image Header +struct OTAImageFileHeader: ConvertableToBinary { + let fileIdentifier: UInt32 + let headerVersion: UInt16 + let headerLength: UInt16 + let headerFieldControl: UInt16 + let companyId: UInt16 + let imageId: UInt16 // here will be 1 - KW40, 2 - MK64 as is in 5553 characteristics + let imageVersion: [UInt8] // length = OTAP_ImageVersionFieldSize - this info will be visible in DIS Firmware version string upon OTAP upload and restart + let headerString: [UInt8] // length = BLE_OTAP_HeaderStrLength + let totalImageFileSize: UInt32 + + init?(data: NSData?) { + // There should be some data + guard let data = data else { return nil } + + let length = Int(OTAP_ImageFileHeaderLength) // header size is 58 bytes + + var dataBytes: [UInt8] = [UInt8](count: length, repeatedValue: 0x00) + data.getBytes(&dataBytes, length: length) + + fileIdentifier = uint32FromFourBytes(dataBytes[0], lohiByte: dataBytes[1], hiloByte: dataBytes[2], hihiByte: dataBytes[3]) + headerVersion = uint16FromTwoBytes(dataBytes[4], hiByte: dataBytes[5]) + headerLength = uint16FromTwoBytes(dataBytes[6], hiByte: dataBytes[7]) + headerFieldControl = uint16FromTwoBytes(dataBytes[8], hiByte: dataBytes[9]) + companyId = uint16FromTwoBytes(dataBytes[10], hiByte: dataBytes[11]) + imageId = uint16FromTwoBytes(dataBytes[12], hiByte: dataBytes[13]) + imageVersion = Array(dataBytes[14...21]) + headerString = Array(dataBytes[22...53]) + totalImageFileSize = uint32FromFourBytes(dataBytes[54], lohiByte: dataBytes[55], hiloByte: dataBytes[56], hihiByte: dataBytes[57]) + print(self) + } + + func convertToBinary() -> [UInt8] { + var bytesArray:[UInt8] = [] + bytesArray.appendContentsOf(uint32ToLittleEndianBytesArray(fileIdentifier)) + bytesArray.appendContentsOf(uint16ToLittleEndianBytesArray(headerVersion)) + bytesArray.appendContentsOf(uint16ToLittleEndianBytesArray(headerLength)) + bytesArray.appendContentsOf(uint16ToLittleEndianBytesArray(headerFieldControl)) + bytesArray.appendContentsOf(uint16ToLittleEndianBytesArray(companyId)) + bytesArray.appendContentsOf(imageVersion) + bytesArray.appendContentsOf(headerString) + bytesArray.appendContentsOf(uint32ToLittleEndianBytesArray(totalImageFileSize)) + return bytesArray + } + + func imageVersionAsString() -> String { + guard imageVersion.count >= 3 else { return "" } + + let major = imageVersion[0] + let minor = imageVersion[1] + let build = imageVersion[2] + + return "\(major).\(minor).\(build)" + } +} + +// BLE OTAP Protocol statuses +enum OTAP_Status: UInt8 { + case StatusSuccess = 0x00 /*!< The operation was successful. */ + case StatusImageDataNotExpected = 0x01 /*!< The OTAP Server tried to send an image data chunk to the OTAP Client but the Client was not expecting it. */ + case StatusUnexpectedTransferMethod = 0x02 /*!< The OTAP Server tried to send an image data chunk using a transfer method the OTAP Client does not support/expect. */ + case StatusUnexpectedCmdOnDataChannel = 0x03 /*!< The OTAP Server tried to send an unexpected command (different from a data chunk) on a data Channel (ATT or CoC) */ + case StatusUnexpectedL2capChannelOrPsm = 0x04 /*!< The selected channel or PSM is not valid for the selected transfer method (ATT or CoC). */ + case StatusUnexpectedOtapPeer = 0x05 /*!< A command was received from an unexpected OTAP Server or Client device. */ + case StatusUnexpectedCommand = 0x06 /*!< The command sent from the OTAP peer device is not expected in the current state. */ + case StatusUnknownCommand = 0x07 /*!< The command sent from the OTAP peer device is not known. */ + case StatusInvalidCommandLength = 0x08 /*!< Invalid command length. */ + case StatusInvalidCommandParameter = 0x09 /*!< A parameter of the command was not valid. */ + case StatusFailedImageIntegrityCheck = 0x0A /*!< The image integrity check has failed. */ + case StatusUnexpectedSequenceNumber = 0x0B /*!< A chunk with an unexpected sequence number has been received. */ + case StatusImageSizeTooLarge = 0x0C /*!< The upgrade image size is too large for the OTAP Client. */ + case StatusUnexpectedDataLength = 0x0D /*!< The length of a Data Chunk was not expected. */ + case StatusUnknownFileIdentifier = 0x0E /*!< The image file identifier is not recognized. */ + case StatusUnknownHeaderVersion = 0x0F /*!< The image file header version is not recognized. */ + case StatusUnexpectedHeaderLength = 0x10 /*!< The image file header length is not expected for the current header version. */ + case StatusUnexpectedHeaderFieldControl = 0x11 /*!< The image file header field control is not expected for the current header version. */ + case StatusUnknownCompanyId = 0x12 /*!< The image file header company identifier is not recognized. */ + case StatusUnexpectedImageId = 0x13 /*!< The image file header image identifier is not as expected. */ + case StatusUnexpectedImageVersion = 0x14 /*!< The image file header image version is not as expected. */ + case StatusUnexpectedImageFileSize = 0x15 /*!< The image file header image file size is not as expected. */ + case StatusInvalidSubElementLength = 0x16 /*!< One of the sub-elements has an invalid length. */ + case StatusImageStorageError = 0x17 /*!< An image storage error has occurred. */ + case StatusInvalidImageCrc = 0x18 /*!< The computed CRC does not match the received CRC. */ + case StatusInvalidImageFileSize = 0x19 /*!< The image file size is not valid. */ + case StatusInvalidL2capPsm = 0x1A /*!< A block transfer request has been made via the L2CAP CoC method but the specified Psm is not known. */ + case StatusNoL2capPsmConnection = 0x1B /*!< A block transfer request has been made via the L2CAP CoC method but there is no valid PSM connection. */ + case NumberOfStatuses = 0x1C +} + +// OTAP Protocol Commands +enum OTAP_Command: UInt8 { + case NoCommand = 0x00 /*!< No command. */ + case NewImageNotification = 0x01 /*!< OTAP Server -> OTAP Client - A new image is available on the OTAP Server */ + case NewImageInfoRequest = 0x02 /*!< OTAP Client -> OTAP Server - The OTAP Client requests image information from the OTAP Server */ + case NewImageInfoResponse = 0x03 /*!< OTAP Server -> OTAP Client - The OTAP Server sends requested image information to the OTAP Client */ + case ImageBlockRequest = 0x04 /*!< OTAP Client -> OTAP Server - The OTAP Client requests an image block from the OTAP Server */ + case ImageChunk = 0x05 /*!< OTAP Server -> OTAP Client - The OTAP Server sends an image chunk to the OTAP Client */ + case ImageTransferComplete = 0x06 /*!< OTAP Client -> OTAP Server - The OTAP Client notifies the OTAP Server that an image transfer was completed*/ + case ErrorNotification = 0x07 /*!< Bidirectional - An error has occurred */ + case StopImageTransfer = 0x08 /*!< OTAP Client -> OTAP Server - The OTAP Client request the OTAP Server to stop an image transfer. */ +} + + +// OTAP New Image Notification Command +struct OTAP_CMD_NewImgNotification: ConvertableToBinary { + let command: OTAP_Command = OTAP_Command.NewImageNotification + let imageId: [UInt8] // length = OTAP_ImageIdFieldSize + let imageVersion: [UInt8] // length = OTAP_ImageVersionFieldSize + let imageFileSize: UInt32 + + func convertToBinary() -> [UInt8] { + var bytesArray:[UInt8] = [] + bytesArray.append(command.rawValue) + bytesArray.appendContentsOf(imageId) + bytesArray.appendContentsOf(imageVersion) + bytesArray.appendContentsOf(uint32ToLittleEndianBytesArray(imageFileSize)) + return bytesArray + } +} + +// OTAP New Image Info Request Command +struct OTAP_CMD_NewImgInfoReq { + let command: OTAP_Command = OTAP_Command.NewImageInfoRequest + let currentImageId: [UInt8] // length = OTAP_ImageIdFieldSize + let currentImageVersion: [UInt8] // length = OTAP_ImageVersionFieldSize + + init?(data: NSData?) { + // There should be some data + guard let data = data else { return nil } + + var dataBytes: [UInt8] = [UInt8](count: data.length, repeatedValue: 0x00) + let length = data.length + data.getBytes(&dataBytes, length: length) + + // Data length should be CmdId + ImageId + ImageVersion = 1 + 2 + 8 = 11bytes + guard length == Int(OTAP_CmdIdFieldSize + OTAP_ImageIdFieldSize + OTAP_ImageVersionFieldSize) else { return nil } + + currentImageId = Array(dataBytes[1...2]) + currentImageVersion = Array(dataBytes[3...dataBytes.count-1]) + print(self) + } + +} + +// OTAP New Image Info Response Command +struct OTAP_CMD_NewImgInfoRes: ConvertableToBinary { + let command: OTAP_Command = OTAP_Command.NewImageInfoResponse + let imageId: [UInt8] // length = OTAP_ImageIdFieldSize + let imageVersion: [UInt8] // length = OTAP_ImageVersionFieldSize + let imageFileSize: UInt32 + + init(imageId: [UInt8], imageVersion: [UInt8], imageFileSize: UInt32) { + self.imageId = imageId + self.imageVersion = imageVersion + self.imageFileSize = imageFileSize + print(self) + } + + func convertToBinary() -> [UInt8] { + var bytesArray:[UInt8] = [] + bytesArray.append(command.rawValue) + bytesArray.appendContentsOf(imageId) + bytesArray.appendContentsOf(imageVersion) + bytesArray.appendContentsOf(uint32ToLittleEndianBytesArray(imageFileSize)) + return bytesArray + } +} + +// OTAP Image Block Request Command +struct OTAP_CMD_ImgBlockReq { + let command: OTAP_Command = OTAP_Command.ImageBlockRequest + let imageId: [UInt8] // length = OTAP_ImageIdFieldSize + let startPosition: UInt32 + let blockSize: UInt32 + let chunkSize: UInt16 + let transferMethod: UInt8 // should be OTAP_TransferMethodAtt = 0x00 + let l2capChannelOrPsm: UInt16 // should be OTAP_TransferChannelAtt = 0x0004 + + init?(data: NSData?) { + // There should be some data + guard let data = data else { return nil } + + var dataBytes: [UInt8] = [UInt8](count: data.length, repeatedValue: 0x00) + let length = data.length + data.getBytes(&dataBytes, length: length) + + // Data length should be CmdId + ImageId + StartPosition + BlockSize + ChunkSize + TransferMethodSize + TransferChannelSize = 1 + 2 + 4 + 4 + 2 + 1 + 2 = 16bytes + guard length == Int(OTAP_CmdIdFieldSize + OTAP_ImageIdFieldSize + OTAP_StartPositionSize + OTAP_BlockSize + OTAP_ChunkSize + OTAP_TransferMethodSize + OTAP_TransferChannelSize) else { return nil } + + imageId = Array(dataBytes[1...2]) + startPosition = uint32FromFourBytes(dataBytes[3], lohiByte: dataBytes[4], hiloByte: dataBytes[5], hihiByte: dataBytes[6]) + blockSize = uint32FromFourBytes(dataBytes[7], lohiByte: dataBytes[8], hiloByte: dataBytes[9], hihiByte: dataBytes[10]) + chunkSize = uint16FromTwoBytes(dataBytes[11], hiByte: dataBytes[12]) + transferMethod = dataBytes[13] + l2capChannelOrPsm = uint16FromTwoBytes(dataBytes[14], hiByte: dataBytes[15]) + print(self) + } +} + +// OTAP Image Chunk Command - for ATT transfer method only +struct OTAP_CMD_ImgChunkAtt: ConvertableToBinary { + let command: OTAP_Command = OTAP_Command.ImageChunk + let seqNumber: UInt8 // Max 256 chunks per block. */ + let data: [UInt8] // length = OTAP_ImageChunkDataSizeAtt + + init(seqNumber: UInt8, data: [UInt8]) { + self.seqNumber = seqNumber + self.data = data + } + + func convertToBinary() -> [UInt8] { + var bytesArray:[UInt8] = [] + bytesArray.append(command.rawValue) + bytesArray.append(seqNumber) + bytesArray.appendContentsOf(data) + return bytesArray + } +} + +// OTAP Image Transfer Complete Command +struct OTAP_CMD_ImgTransferComplete { + let command: OTAP_Command = OTAP_Command.ImageTransferComplete + let imageId: [UInt8] // length = OTAP_ImageIdFieldSize + let status: OTAP_Status // length = OTAP_CmdErrorStatusSize + + init?(data: NSData?) { + // There should be some data + guard let data = data else { return nil } + + var dataBytes: [UInt8] = [UInt8](count: data.length, repeatedValue: 0x00) + let length = data.length + data.getBytes(&dataBytes, length: length) + + // Data length should be CmdId + ImageId + ErrorStatus = 1 + 2 + 1 = 4bytes + guard length == Int(OTAP_CmdIdFieldSize + OTAP_ImageIdFieldSize + OTAP_CmdErrorStatusSize) else { return nil } + + imageId = Array(dataBytes[1...2]) + if let s = OTAP_Status(rawValue: dataBytes[3]) { + status = s + } + else { + status = OTAP_Status.NumberOfStatuses + } + print(self) + } +} + +// OTAP Error Notification Command +struct OTAP_CMD_ErrNotification: ConvertableToBinary { + let command: OTAP_Command = OTAP_Command.ErrorNotification + let cmdId: UInt8 // The command which caused the error + let errStatus: UInt8 // Actually OTAP_Status code + + init?(data: NSData?) { + // There should be some data + guard let data = data else { return nil } + + var dataBytes: [UInt8] = [UInt8](count: data.length, repeatedValue: 0x00) + let length = data.length + data.getBytes(&dataBytes, length: length) + + // Data length should be CmdId + CmdId + ErrorStatus = 1 + 1 + 1 = 3bytes + guard length == Int(OTAP_CmdIdFieldSize + OTAP_CmdIdFieldSize + OTAP_CmdErrorStatusSize) else { return nil } + + cmdId = dataBytes[1] + errStatus = dataBytes[2] + print(self) + } + + func convertToBinary() -> [UInt8] { + var bytesArray:[UInt8] = [] + bytesArray.append(command.rawValue) + bytesArray.append(cmdId) + bytesArray.append(errStatus) + return bytesArray + } +} + +// OTAP Stop Image Transfer Command +struct OTAP_CMD_StopImgTransfer { + let command: OTAP_Command + let imageId: [UInt8] // length = OTAP_ImageIdFieldSize +} + +// Extract command id from NSData +func getOTAPCommand(data: NSData?) -> OTAP_Command { + // There should be some data + guard let data = data else { return OTAP_Command.NoCommand } + + var dataBytes: [UInt8] = [UInt8](count: data.length, repeatedValue: 0x00) + let length = data.length + data.getBytes(&dataBytes, length: length) + + let cmdId: UInt8 = dataBytes[0] + return OTAP_Command(rawValue: cmdId) ?? OTAP_Command.NoCommand +} + +// Conversion helpers +func uint32ToLittleEndianBytesArray(uint32: UInt32) -> [UInt8] { + var littleEndian = uint32.littleEndian + let bytePtr = withUnsafePointer(&littleEndian) { + UnsafeBufferPointer(start: UnsafePointer($0), count: sizeofValue(littleEndian)) + } + let byteArray = Array(bytePtr) + return byteArray +} + +func uint16ToLittleEndianBytesArray(uint16: UInt16) -> [UInt8] { + var littleEndian = uint16.littleEndian + let bytePtr = withUnsafePointer(&littleEndian) { + UnsafeBufferPointer(start: UnsafePointer($0), count: sizeofValue(littleEndian)) + } + let byteArray = Array(bytePtr) + return byteArray +} + +func uint16FromTwoBytes(loByte: UInt8, hiByte: UInt8) -> UInt16 { + let lowByte = UInt16(loByte & 0xFF) + let highByte = UInt16(hiByte & 0xFF) << 8 + return highByte | lowByte +} + +func int16FromTwoBytes(loByte: UInt8, hiByte: UInt8) -> Int16 { + let lowByte = Int16(loByte & 0xFF) + let highByte = Int16(hiByte & 0xFF) << 8 + return highByte | lowByte +} + +func uint32FromFourBytes(loloByte: UInt8, lohiByte: UInt8, hiloByte: UInt8, hihiByte: UInt8) -> UInt32 { + let lowLowByte = UInt32(loloByte & 0xFF) + let lowHighByte = UInt32(lohiByte & 0xFF) << 8 + let highLowByte = UInt32(hiloByte & 0xFF) << 16 + let highHighByte = UInt32(hihiByte & 0xFF) << 24 + return highHighByte | highLowByte | lowHighByte | lowLowByte +} + +func dataToIntegerArray(value: NSData) -> [T] { + let dataLength = value.length + let count = dataLength / sizeof(T) + var array = [T](count: count, repeatedValue: 0x00) + value.getBytes(&array, length:dataLength) + return array +} diff --git a/iOS/Hexiwear/HexiwearTableViewCell.swift b/iOS/Hexiwear/HexiwearTableViewCell.swift new file mode 100644 index 0000000..62b6d2f --- /dev/null +++ b/iOS/Hexiwear/HexiwearTableViewCell.swift @@ -0,0 +1,32 @@ +// +// Hexiwear application is used to pair with Hexiwear BLE devices +// and send sensor readings to WolkSense sensor data cloud +// +// Copyright (C) 2016 WolkAbout Technology s.r.o. +// +// Hexiwear is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Hexiwear is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . +// +// +// HexiwearTableViewCell.swift +// + +import UIKit + +class HexiwearTableViewCell: UITableViewCell { + + @IBOutlet weak var titleLabel: UILabel! + @IBOutlet weak var detailLabel: UILabel! + @IBOutlet weak var signalLabel: UILabel! + +} diff --git a/iOS/Hexiwear/HexiwearTableViewController.swift b/iOS/Hexiwear/HexiwearTableViewController.swift new file mode 100644 index 0000000..35e9872 --- /dev/null +++ b/iOS/Hexiwear/HexiwearTableViewController.swift @@ -0,0 +1,715 @@ +// +// Hexiwear application is used to pair with Hexiwear BLE devices +// and send sensor readings to WolkSense sensor data cloud +// +// Copyright (C) 2016 WolkAbout Technology s.r.o. +// +// Hexiwear is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Hexiwear is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . +// +// +// HexiwearTableViewController.swift +// + +import UIKit +import CoreBluetooth + +protocol HexiwearPeripheralDelegate { + func didUnwind() + func didLoseBonding() + func willDisconnectOnSignOut() +} + +protocol HexiwearTimeSettingDelegate { + func didStartSettingTime() + func didFinishSettingTime(success: Bool) +} + +class HexiwearTableViewController: UITableViewController { + + @IBOutlet weak var modeCell: WeatherTableViewCell! + @IBOutlet weak var disconnectedCell: WeatherTableViewCell! + @IBOutlet weak var batteryCell: WeatherTableViewCell! + @IBOutlet weak var temperatureCell: WeatherTableViewCell! + @IBOutlet weak var humidityCell: WeatherTableViewCell! + @IBOutlet weak var pressureCell: WeatherTableViewCell! + @IBOutlet weak var acceleratorCell: AcceleratorTableViewCell! + @IBOutlet weak var gyroCell: GyroTableViewCell! + @IBOutlet weak var magnetometerCell: AcceleratorTableViewCell! + @IBOutlet weak var stepsCell: WeatherTableViewCell! + @IBOutlet weak var caloriesCell: WeatherTableViewCell! + @IBOutlet weak var heartRateCell: WeatherTableViewCell! + @IBOutlet weak var lightCell: WeatherTableViewCell! + + var dataStore: DataStore! + + // BLE + var hexiwearPeripheral : CBPeripheral! + var hexiwearReadings = HexiwearReadings() + var availableReadings: [HexiwearReading] = [] + var deviceInfo = DeviceInfo() + + var batteryCharacteristics: CBCharacteristic? + var motionAccelCharacteristics: CBCharacteristic? + var motionMagnetCharacteristics: CBCharacteristic? + var motionGyroCharacteristics: CBCharacteristic? + var weatherLightCharacteristics: CBCharacteristic? + var weatherTemperatureCharacteristics: CBCharacteristic? + var weatherHumidityCharacteristics: CBCharacteristic? + var weatherPressureCharacteristics: CBCharacteristic? + var healthHeartRateCharacteristics: CBCharacteristic? + var healthStepsCharacteristics: CBCharacteristic? + var healthCaloriesCharacteristics: CBCharacteristic? + + var diManufacturerCharacteristics: CBCharacteristic? + var diHWRevisionCharacteristics: CBCharacteristic? + var diFWRevisionCharacteristics: CBCharacteristic? + + var serialNumberCharacteristic: CBCharacteristic? + var hexiwearModeCharacteristic: CBCharacteristic? + + var alertInCharacteristic: CBCharacteristic? + + var readTimer: NSTimer! + var isBatteryRead = false + var shouldPublishToMQTT = false + + var isDemoAccount = true + + // OTAP vars + var isOTAPEnabled = false // if hexiware is in OTAP mode this will be true + + // Tracking device + var trackingDevice: TrackingDevice! + var mqttAPI: MQTTAPI! + var wolkSerialForHexiserial: String? + var wolkPasswordForHexiserial: String? + + var timeSettingDelegate: HexiwearTimeSettingDelegate? + + var batteryIsOn = true + var temperatureIsOn = true + var humidityIsOn = true + var pressureIsOn = true + var lightIsOn = true + var acceleratorIsOn = true + var magnetometerIsOn = true + var gyroIsOn = true + var stepsIsOn = true + var caloriesIsOn = true + var heartrateIsOn = true + var sendToCloudIsOn = true + + var shouldDiscoverServices = true + var isConnected = true + + // Delegate + var hexiwearDelegate: HexiwearPeripheralDelegate? + + override func viewDidLoad() { + super.viewDidLoad() + + // Know when MQTT has published data + mqttAPI.setAuthorisationOptions(wolkSerialForHexiserial ?? "", password: wolkPasswordForHexiserial ?? "") + mqttAPI.mqttDelegate = self + + // Setup polling timer + readTimer = NSTimer.scheduledTimerWithTimeInterval(0.5, target: self, selector: #selector(HexiwearTableViewController.readCharacteristics), userInfo: nil, repeats: true) + } + + override func viewWillAppear(animated: Bool) { + batteryIsOn = !trackingDevice.isBatteryOff ?? true + temperatureIsOn = !trackingDevice.isTemperatureOff ?? true + humidityIsOn = !trackingDevice.isHumidityOff ?? true + pressureIsOn = !trackingDevice.isPressureOff ?? true + lightIsOn = !trackingDevice.isLightOff ?? true + acceleratorIsOn = !trackingDevice.isAcceleratorOff ?? true + magnetometerIsOn = !trackingDevice.isMagnetometerOff ?? true + gyroIsOn = !trackingDevice.isGyroOff ?? true + stepsIsOn = !trackingDevice.isStepsOff ?? true + caloriesIsOn = !trackingDevice.isCaloriesOff ?? true + heartrateIsOn = !trackingDevice.isHeartRateOff ?? true + sendToCloudIsOn = !trackingDevice.trackingIsOff ?? true + + tableView.reloadData() + + // Discover hexiwear services + discoverHexiwearServices() + } + + private func discoverHexiwearServices() { + if shouldDiscoverServices && hexiwearPeripheral != nil { + hexiwearPeripheral.delegate = self + hexiwearPeripheral.discoverServices(nil) + shouldDiscoverServices = false + } + } + + override func viewWillDisappear(animated: Bool) { + if isMovingFromParentViewController() { + stopTheTimer() + hexiwearDelegate?.didUnwind() + } + } + + private func stopTheTimer() { + readTimer.invalidate() + readTimer = nil + } + + @IBAction func goToSettings(sender: UIBarButtonItem) { + performSegueWithIdentifier("toSettings", sender: nil) + } + + override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) { + if segue.identifier == "toSettings" { + if let nc = segue.destinationViewController as? BaseNavigationController, + vc = nc.topViewController as? HexiSettingsTableViewController { + vc.title = "Hexiwear settings" + vc.dataStore = self.dataStore + vc.trackingDevice = self.trackingDevice + vc.deviceInfo = self.deviceInfo + vc.hexiwearMode = self.hexiwearReadings.hexiwearMode + vc.delegate = self + self.timeSettingDelegate = vc + } + } + } + + override func tableView(tableView: UITableView, heightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat { + let cellRow = indexPath.row + let cellHeight: CGFloat = 160.0 + + if cellRow == 0 { // mode cell + return isConnected && hexiwearReadings.hexiwearMode == .IDLE ? cellHeight : 0.0 + } + else if cellRow == 1 { // disconnectedCell + return isConnected == false ? cellHeight : 0.0 + } + else if cellRow == 2 { // battery + return isConnected && batteryIsOn && availableReadings.contains(.BATTERY) ? cellHeight : 0.0 + } + else if cellRow == 3 { // temperature + return isConnected && temperatureIsOn && availableReadings.contains(.TEMPERATURE) ? cellHeight : 0.0 + } + else if cellRow == 4 { // humidity + return isConnected && humidityIsOn && availableReadings.contains(.HUMIDITY) ? cellHeight : 0.0 + } + else if cellRow == 5 { // pressure + return isConnected && pressureIsOn && availableReadings.contains(.PRESSURE) ? cellHeight : 0.0 + } + else if cellRow == 6 { // light + return isConnected && lightIsOn && availableReadings.contains(.LIGHT) ? cellHeight : 0.0 + } + else if cellRow == 7 { // accel + return isConnected && acceleratorIsOn && availableReadings.contains(.ACCELEROMETER) ? cellHeight : 0.0 + } + else if cellRow == 8 { // magnet + return isConnected && magnetometerIsOn && availableReadings.contains(.MAGNETOMETER) ? cellHeight : 0.0 + } + else if cellRow == 9 { // gyro + return isConnected && gyroIsOn && availableReadings.contains(.GYRO) ? cellHeight : 0.0 + } + else if cellRow == 10 { // steps + return isConnected && stepsIsOn && availableReadings.contains(.PEDOMETER) ? cellHeight : 0.0 + } + else if cellRow == 11 { // calories + return isConnected && caloriesIsOn && availableReadings.contains(.CALORIES) ? cellHeight : 0.0 + } + else { // heartrate + return isConnected && heartrateIsOn && availableReadings.contains(.HEARTRATE) ? cellHeight : 0.0 + } + } +} + +//MARK:- CBPeripheralDelegate +extension HexiwearTableViewController: CBPeripheralDelegate { + + func peripheral(peripheral: CBPeripheral, didDiscoverServices error: NSError?) { + guard error == Optional.None else { + print("didDiscoverServices error: \(error)") + return + } + + guard let services = peripheral.services else { return } + + let progressHUD = JGProgressHUD(style: .Dark) + progressHUD.textLabel.text = "Exploring services..." + progressHUD.showInView(self.view, animated: true) + + for service in services { + let thisService = service as CBService + if Hexiwear.validService(thisService, isOTAPEnabled: isOTAPEnabled) { + peripheral.discoverCharacteristics(nil, forService: thisService) + } + } + progressHUD.dismiss() + } + + // Enable notification and sensor for each characteristic of valid service + func peripheral(peripheral: CBPeripheral, didDiscoverCharacteristicsForService service: CBService, error: NSError?) { + guard error == Optional.None else { + print("didDiscoverCharacteristicsForService error: \(error)") + return + } + + guard let characteristics = service.characteristics else { return } + + let progressHUD = JGProgressHUD(style: .Dark) + progressHUD.textLabel.text = "Discovering characteristics..." + progressHUD.showInView(self.view, animated: true) + + for charateristic in characteristics { + let thisCharacteristic = charateristic as CBCharacteristic + + if Hexiwear.validDataCharacteristic(thisCharacteristic, isOTAPEnabled: isOTAPEnabled) { + + if thisCharacteristic.UUID == WeatherTemperatureUUID { + weatherTemperatureCharacteristics = thisCharacteristic + } + else if thisCharacteristic.UUID == WeatherHumidityUUID { + weatherHumidityCharacteristics = thisCharacteristic + } + else if thisCharacteristic.UUID == WeatherPressureUUID { + weatherPressureCharacteristics = thisCharacteristic + } + else if thisCharacteristic.UUID == WeatherLightUUID { + weatherLightCharacteristics = thisCharacteristic + } + else if thisCharacteristic.UUID == BatteryLevelUUID { + batteryCharacteristics = thisCharacteristic + peripheral.readValueForCharacteristic(batteryCharacteristics!) + peripheral.setNotifyValue(true, forCharacteristic: batteryCharacteristics!) + } + else if thisCharacteristic.UUID == MotionAccelerometerUUID { + motionAccelCharacteristics = thisCharacteristic + } + else if thisCharacteristic.UUID == MotionMagnetometerUUID { + motionMagnetCharacteristics = thisCharacteristic + } + else if thisCharacteristic.UUID == MotionGyroUUID { + motionGyroCharacteristics = thisCharacteristic + } + else if thisCharacteristic.UUID == HealthHeartRateUUID { + healthHeartRateCharacteristics = thisCharacteristic + } + else if thisCharacteristic.UUID == HealthStepsUUID { + healthStepsCharacteristics = thisCharacteristic + } + else if thisCharacteristic.UUID == HealthCaloriesUUID { + healthCaloriesCharacteristics = thisCharacteristic + } + else if thisCharacteristic.UUID == SerialNumberUUID { + serialNumberCharacteristic = thisCharacteristic + } + else if thisCharacteristic.UUID == DIManufacturerUUID { + diManufacturerCharacteristics = thisCharacteristic + peripheral.readValueForCharacteristic(diManufacturerCharacteristics!) + } + else if thisCharacteristic.UUID == DIHWRevisionUUID { + diHWRevisionCharacteristics = thisCharacteristic + peripheral.readValueForCharacteristic(diHWRevisionCharacteristics!) + } + else if thisCharacteristic.UUID == DIFWRevisionUUID { + diFWRevisionCharacteristics = thisCharacteristic + peripheral.readValueForCharacteristic(diFWRevisionCharacteristics!) + } + else if thisCharacteristic.UUID == HexiwearModeUUID { + hexiwearModeCharacteristic = thisCharacteristic + peripheral.readValueForCharacteristic(hexiwearModeCharacteristic!) + } + else if thisCharacteristic.UUID == AlertINUUID { + alertInCharacteristic = thisCharacteristic + setTime() + } + } + } + progressHUD.dismiss() + + } + + // Get data values when they are updated + func peripheral(peripheral: CBPeripheral, didUpdateValueForCharacteristic characteristic: CBCharacteristic, error: NSError?) { + + guard error == Optional.None else { + print("didUpdateValueForCharacteristic error: \(error)") + // If authentication is lost, drop readings processing + if error!.domain == CBATTErrorDomain { + let authenticationError: Int = CBATTError.InsufficientAuthentication.rawValue + if error!.code == authenticationError { + hexiwearDelegate?.didLoseBonding() + } + } + return + } + + let lastSensorReadingDate = NSDate() + + if characteristic.UUID == WeatherTemperatureUUID { + hexiwearReadings.ambientTemperature = Hexiwear.getAmbientTemperature(characteristic.value) + hexiwearReadings.lastWeatherDate = lastSensorReadingDate + trackingDevice.lastSensorReadingDate = lastSensorReadingDate + trackingDevice.lastTemperature = hexiwearReadings.ambientTemperature + shouldPublishToMQTT = true + } + else if characteristic.UUID == WeatherHumidityUUID { + hexiwearReadings.relativeHumidity = Hexiwear.getRelativeHumidity(characteristic.value) + hexiwearReadings.lastWeatherDate = lastSensorReadingDate + trackingDevice.lastSensorReadingDate = lastSensorReadingDate + trackingDevice.lastAirHumidity = hexiwearReadings.relativeHumidity + shouldPublishToMQTT = true + } + else if characteristic.UUID == WeatherPressureUUID { + hexiwearReadings.airPressure = Hexiwear.getPressureData(characteristic.value) + hexiwearReadings.lastWeatherDate = lastSensorReadingDate + trackingDevice.lastSensorReadingDate = lastSensorReadingDate + trackingDevice.lastAirPressure = hexiwearReadings.airPressure + shouldPublishToMQTT = true + } + else if characteristic.UUID == WeatherLightUUID { + hexiwearReadings.ambientLight = Hexiwear.getAmbientLight(characteristic.value) + hexiwearReadings.lastWeatherDate = lastSensorReadingDate + trackingDevice.lastLight = hexiwearReadings.ambientLight + trackingDevice.lastSensorReadingDate = lastSensorReadingDate + shouldPublishToMQTT = true + } + else if characteristic.UUID == BatteryLevelUUID { + isBatteryRead = true + hexiwearReadings.batteryLevel = Hexiwear.getBatteryLevel(characteristic.value) + } + else if characteristic.UUID == MotionAccelerometerUUID { + if let accel = Hexiwear.getMotionAccelerometerValues(characteristic.value) { + hexiwearReadings.motionAccelX = accel.x + hexiwearReadings.motionAccelY = accel.y + hexiwearReadings.motionAccelZ = accel.z + hexiwearReadings.lastAccelDate = lastSensorReadingDate + trackingDevice.lastAccelerationX = accel.x + trackingDevice.lastAccelerationY = accel.y + trackingDevice.lastAccelerationZ = accel.z + trackingDevice.lastSensorReadingDate = lastSensorReadingDate + shouldPublishToMQTT = true + } + } + else if characteristic.UUID == MotionMagnetometerUUID { + if let magnet = Hexiwear.getMotionMagnetometerValues(characteristic.value) { + hexiwearReadings.motionMagnetX = magnet.x + hexiwearReadings.motionMagnetY = magnet.y + hexiwearReadings.motionMagnetZ = magnet.z + hexiwearReadings.lastMagnetDate = lastSensorReadingDate + trackingDevice.lastMagnetX = magnet.x + trackingDevice.lastMagnetY = magnet.y + trackingDevice.lastMagnetZ = magnet.z + trackingDevice.lastSensorReadingDate = lastSensorReadingDate + shouldPublishToMQTT = true + } + } + else if characteristic.UUID == MotionGyroUUID { + if let gyro = Hexiwear.getMotionGyroValues(characteristic.value) { + hexiwearReadings.motionGyroX = gyro.x + hexiwearReadings.motionGyroY = gyro.y + hexiwearReadings.motionGyroZ = gyro.z + hexiwearReadings.lastGyroDate = lastSensorReadingDate + trackingDevice.lastGyroX = gyro.x + trackingDevice.lastGyroY = gyro.y + trackingDevice.lastGyroZ = gyro.z + trackingDevice.lastSensorReadingDate = lastSensorReadingDate + shouldPublishToMQTT = true + } + } + else if characteristic.UUID == HealthHeartRateUUID { + hexiwearReadings.heartRate = Hexiwear.getHeartRate(characteristic.value) + hexiwearReadings.lastHeartRateDate = lastSensorReadingDate + trackingDevice.lastHeartRate = hexiwearReadings.heartRate + trackingDevice.lastSensorReadingDate = lastSensorReadingDate + shouldPublishToMQTT = true + } + else if characteristic.UUID == HealthStepsUUID { + hexiwearReadings.steps = Hexiwear.getSteps(characteristic.value) + hexiwearReadings.lastStepsDate = lastSensorReadingDate + trackingDevice.lastSteps = hexiwearReadings.steps + trackingDevice.lastSensorReadingDate = lastSensorReadingDate + shouldPublishToMQTT = true + } + else if characteristic.UUID == HealthCaloriesUUID { + hexiwearReadings.calories = Hexiwear.getCalories(characteristic.value) + hexiwearReadings.lastCaloriesDate = lastSensorReadingDate + trackingDevice.lastCalories = hexiwearReadings.calories + trackingDevice.lastSensorReadingDate = lastSensorReadingDate + shouldPublishToMQTT = true + } + + else if characteristic.UUID == DIManufacturerUUID { + deviceInfo.manufacturer = Hexiwear.getManufacturer(characteristic.value) ?? "" + } + else if characteristic.UUID == DIFWRevisionUUID { + deviceInfo.firmwareRevision = Hexiwear.getFirmwareRevision(characteristic.value) ?? "" + } + else if characteristic.UUID == HexiwearModeUUID { + if let rawMode = Hexiwear.getHexiwearMode(characteristic.value), + mode = HexiwearMode(rawValue: rawMode) { + hexiwearReadings.hexiwearMode = mode + } + else { + hexiwearReadings.hexiwearMode = .IDLE + } + availableReadings = HexiwearMode.getReadingsForMode(hexiwearReadings.hexiwearMode) + trackingDevice.lastHexiwearMode = hexiwearReadings.hexiwearMode.rawValue + tableView.reloadData() + } + + + refreshReadingsLabels(hexiwearReadings) + } + + func peripheral(peripheral: CBPeripheral, didWriteValueForCharacteristic characteristic: CBCharacteristic, error: NSError?) { + + if let error = error { print("didWriteValueForCharacteristic error: \(error)") } + + if characteristic.UUID == AlertINUUID { + let success = error == nil + timeSettingDelegate?.didFinishSettingTime(success) + + if !success { + let message = "Insufficient authorisation error occured. Click OK to open Bluetooth settings and choose forget HEXIWEAR and try again." + guard let topVC = UIApplication.sharedApplication().keyWindow?.rootViewController?.topMostViewController() else { return } + showOKAndCancelAlertWithTitle(applicationTitle, message: message, viewController: topVC, OKhandler: {_ in + UIApplication.sharedApplication().openURL(NSURL(string:UIApplicationOpenSettingsURLString)!); + }) + } + } + } + + + private func refreshReadingsLabels(readings: HexiwearReadings) { + + // battery cell + batteryCell.topText = "" + batteryCell.middleText = readings.batteryLevelAsString() + batteryCell.bottomText = "" + + // temperature cell + temperatureCell.topText = "" + temperatureCell.middleText = readings.ambientTemperatureAsString() + temperatureCell.bottomText = "" + + // humidity cell + humidityCell.topText = "" + humidityCell.middleText = readings.relativeHumidityAsString() + humidityCell.bottomText = "" + + // pressure cell + pressureCell.topText = "" + pressureCell.middleText = readings.airPressureAsString() + pressureCell.bottomText = "" + + // Light cell + lightCell.topText = "" + lightCell.middleText = readings.ambientLightAsString() + lightCell.bottomText = "" + + // Acceleration cell + acceleratorCell.xValue = readings.motionAccelXAsString() + acceleratorCell.yValue = readings.motionAccelYAsString() + acceleratorCell.zValue = readings.motionAccelZAsString() + + // Gyro cell + gyroCell.xValue = readings.motionGyroXAsString() + gyroCell.yValue = readings.motionGyroYAsString() + gyroCell.zValue = readings.motionGyroZAsString() + + // Magnetometer cell + magnetometerCell.xValue = readings.motionMagnetXAsString() + magnetometerCell.yValue = readings.motionMagnetYAsString() + magnetometerCell.zValue = readings.motionMagnetZAsString() + + // Steps cell + stepsCell.topText = "" + stepsCell.middleText = readings.stepsAsString() + stepsCell.bottomText = "" + + // Calories cell + caloriesCell.topText = "" + caloriesCell.middleText = readings.caloriesAsString() + caloriesCell.bottomText = "" + + // Heart rate cell + heartRateCell.topText = "" + heartRateCell.middleText = readings.heartRateAsString() + heartRateCell.bottomText = "" + } + + func readCharacteristics() { + + guard let hexiwearPeripheral = self.hexiwearPeripheral else { return } + + if let batteryChar = batteryCharacteristics where !isBatteryRead && batteryIsOn && availableReadings.contains(.BATTERY) { + hexiwearPeripheral.readValueForCharacteristic(batteryChar) + } + + if let motionAccel = motionAccelCharacteristics where acceleratorIsOn && availableReadings.contains(.ACCELEROMETER) { + hexiwearPeripheral.readValueForCharacteristic(motionAccel) + } + + if let motionMagnet = motionMagnetCharacteristics where magnetometerIsOn && availableReadings.contains(.MAGNETOMETER) { + hexiwearPeripheral.readValueForCharacteristic(motionMagnet) + } + + if let motionGyro = motionGyroCharacteristics where gyroIsOn && availableReadings.contains(.GYRO){ + hexiwearPeripheral.readValueForCharacteristic(motionGyro) + } + + if let weatherLight = weatherLightCharacteristics where lightIsOn && availableReadings.contains(.LIGHT) { + hexiwearPeripheral.readValueForCharacteristic(weatherLight) + } + + if let weatherTemperature = weatherTemperatureCharacteristics where temperatureIsOn && availableReadings.contains(.TEMPERATURE){ + hexiwearPeripheral.readValueForCharacteristic(weatherTemperature) + } + + if let weatherHumidity = weatherHumidityCharacteristics where humidityIsOn && availableReadings.contains(.HUMIDITY) { + hexiwearPeripheral.readValueForCharacteristic(weatherHumidity) + } + + if let weatherPressure = weatherPressureCharacteristics where pressureIsOn && availableReadings.contains(.PRESSURE) { + hexiwearPeripheral.readValueForCharacteristic(weatherPressure) + } + + if let healthHeartRate = healthHeartRateCharacteristics where heartrateIsOn && availableReadings.contains(.HEARTRATE) { + hexiwearPeripheral.readValueForCharacteristic(healthHeartRate) + } + + if let healthSteps = healthStepsCharacteristics where stepsIsOn && availableReadings.contains(.PEDOMETER) { + hexiwearPeripheral.readValueForCharacteristic(healthSteps) + } + + if let healthCalories = healthCaloriesCharacteristics where caloriesIsOn && availableReadings.contains(.CALORIES) { + hexiwearPeripheral.readValueForCharacteristic(healthCalories) + } + + if let hexiwearMode = hexiwearModeCharacteristic { + hexiwearPeripheral.readValueForCharacteristic(hexiwearMode) + } + + if sendToCloudIsOn && shouldPublishToMQTT { publishToMQTT() } + } + + func publishToMQTT() { + // Drop any mqtt for demo user + guard isDemoAccount == false else { return } + + // Transmit to MQTT server if necessary + let shouldTransmit = sendToCloudIsOn + let heartbeatElapsed = trackingDevice.isHeartbeatIntervalReached() + if shouldTransmit && heartbeatElapsed { + let lastTemperature = trackingDevice.lastTemperature + let lastPressure = trackingDevice.lastAirPressure + let lastHumidity = trackingDevice.lastAirHumidity + let lastAccelX = trackingDevice.lastAccelerationX + let lastAccelY = trackingDevice.lastAccelerationY + let lastAccelZ = trackingDevice.lastAccelerationZ + let lastGyroX = trackingDevice.lastGyroX + let lastGyroY = trackingDevice.lastGyroY + let lastGyroZ = trackingDevice.lastGyroZ + let lastMagnetX = trackingDevice.lastMagnetX + let lastMagnetY = trackingDevice.lastMagnetY + let lastMagnetZ = trackingDevice.lastMagnetZ + let lastSteps = trackingDevice.lastSteps + let lastCalories = trackingDevice.lastCalories + let lastHeartRate = trackingDevice.lastHeartRate + let lastLight = trackingDevice.lastLight + + let lastHexiwearMode: HexiwearMode + if let mode = trackingDevice.lastHexiwearMode { + lastHexiwearMode = HexiwearMode(rawValue: mode) ?? .IDLE + } + else { + lastHexiwearMode = .IDLE + } + + let hexiwearReadings = HexiwearReadings(batteryLevel: nil, temperature: lastTemperature, pressure: lastPressure, humidity: lastHumidity, accelX: lastAccelX, accelY: lastAccelY, accelZ: lastAccelZ, gyroX: lastGyroX, gyroY: lastGyroY, gyroZ: lastGyroZ, magnetX: lastMagnetX, magnetY: lastMagnetY, magnetZ: lastMagnetZ, steps: lastSteps, calories: lastCalories, heartRate: lastHeartRate, ambientLight: lastLight, hexiwearMode: lastHexiwearMode) + + mqttAPI.publishHexiwearReadings(hexiwearReadings, forSerial: wolkSerialForHexiserial ?? "") + } + } +} + +extension HexiwearTableViewController : MQTTAPIProtocol { + + func didPublishReadings() { + dispatch_async(dispatch_get_main_queue()) { + self.trackingDevice.lastPublished = NSDate() + self.trackingDevice.lastTemperature = nil + self.trackingDevice.lastAirPressure = nil + self.trackingDevice.lastAirHumidity = nil + self.trackingDevice.lastAccelerationX = nil + self.trackingDevice.lastAccelerationY = nil + self.trackingDevice.lastAccelerationZ = nil + self.trackingDevice.lastGyroX = nil + self.trackingDevice.lastGyroY = nil + self.trackingDevice.lastGyroZ = nil + self.trackingDevice.lastMagnetX = nil + self.trackingDevice.lastMagnetY = nil + self.trackingDevice.lastMagnetZ = nil + self.trackingDevice.lastSteps = nil + self.trackingDevice.lastCalories = nil + self.trackingDevice.lastHeartRate = nil + self.trackingDevice.lastLight = nil + } + } +} + +extension HexiwearTableViewController : HexiwearSettingsDelegate { + func didSignOut() { + stopTheTimer() + hexiwearDelegate?.willDisconnectOnSignOut() + } + + func didSetTime() { + timeSettingDelegate?.didStartSettingTime() + guard let _ = alertInCharacteristic else { timeSettingDelegate?.didFinishSettingTime(false); return } + + setTime() + } + + func setTime() { + guard let alertIn = alertInCharacteristic, hexiwearPeripheral = self.hexiwearPeripheral else { return } + let newTimeBinary = Hexiwear.getCurrentTimestampForHexiwear(true) + let newTimeData = NSData(bytes: newTimeBinary, length: newTimeBinary.count) + hexiwearPeripheral.writeValue(newTimeData, forCharacteristic: alertIn, type: CBCharacteristicWriteType.WithResponse) + } +} + +extension HexiwearTableViewController : HexiwearReconnection { + func didReconnectPeripheral(peripheral: CBPeripheral) { + isConnected = true + let progressHUD = JGProgressHUD(style: .Dark) + progressHUD.textLabel.text = "Reconnecting..." + progressHUD.showInView(self.view, animated: true) + progressHUD.dismissAfterDelay(1.0, animated: true) + tableView.reloadData() + + hexiwearPeripheral = peripheral + shouldDiscoverServices = true + discoverHexiwearServices() + } + + func didDisconnectPeripheral() { + isConnected = false + let progressHUD = JGProgressHUD(style: .Dark) + progressHUD.textLabel.text = "Hexiwear disconnected!" + progressHUD.showInView(self.view, animated: true) + progressHUD.dismissAfterDelay(2.0, animated: true) + tableView.reloadData() + } +} + diff --git a/iOS/Hexiwear/Info.plist b/iOS/Hexiwear/Info.plist new file mode 100644 index 0000000..a0867f0 --- /dev/null +++ b/iOS/Hexiwear/Info.plist @@ -0,0 +1,64 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleDocumentTypes + + + CFBundleTypeIconFiles + + CFBundleTypeName + Hexiwear Firmware + LSItemContentTypes + + public.data + + + + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + APPL + CFBundleShortVersionString + 1.0.0 + CFBundleSignature + ???? + CFBundleVersion + 1 + LSRequiresIPhoneOS + + NSAppTransportSecurity + + UIBackgroundModes + + bluetooth-central + + UILaunchStoryboardName + HexiwearLaunchScreen + UIMainStoryboardFile + Main + UIRequiredDeviceCapabilities + + armv7 + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + + diff --git a/iOS/Hexiwear/LoginViewController.swift b/iOS/Hexiwear/LoginViewController.swift new file mode 100644 index 0000000..9dadd79 --- /dev/null +++ b/iOS/Hexiwear/LoginViewController.swift @@ -0,0 +1,317 @@ +// +// Hexiwear application is used to pair with Hexiwear BLE devices +// and send sensor readings to WolkSense sensor data cloud +// +// Copyright (C) 2016 WolkAbout Technology s.r.o. +// +// Hexiwear is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Hexiwear is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . +// +// +// LoginViewController.swift +// + +import UIKit + +class LoginViewController: UIViewController { + + @IBOutlet weak var username: UITextField! + @IBOutlet weak var userPassword: UITextField! + @IBOutlet weak var signInElementsView: UIView! + @IBOutlet weak var signInProgressView: UIView! + @IBOutlet weak var connectivityProblemView: UIView! + @IBOutlet weak var indicatorView: UIActivityIndicatorView! + @IBOutlet weak var scrollView: UIScrollView! + @IBOutlet weak var forgotPasswordAndSignup: UIView! + + var responseStatusCode = 0 + var userCredentials: UserCredentials! + var dataStore: DataStore! + var device: TrackingDevice! + var mqttAPI: MQTTAPI! + + var isAlreadyPresented = false + + + //MARK:- Init + init?(_ coder: NSCoder? = nil) { + + if let coder = coder { + super.init(coder: coder) + } else { + super.init(nibName: nil, bundle:nil) + } + } + + required convenience init?(coder: NSCoder) { + self.init(coder) + } + + //MARK:- Other methods + override func viewDidLoad() { + // Init properties + let appDelegate = UIApplication.sharedApplication().delegate as! AppDelegate + + userCredentials = appDelegate.userCredentials + device = appDelegate.device + dataStore = appDelegate.dataStore + mqttAPI = appDelegate.mqttAPI + + username.delegate = self + userPassword.delegate = self + + NSNotificationCenter.defaultCenter().addObserver(self, selector: #selector(LoginViewController.didSignOut), name: HexiwearDidSignOut, object: nil) + + let tapBackground = UITapGestureRecognizer(target: self, action: #selector(LoginViewController.resignLogin)) + tapBackground.numberOfTapsRequired = 1 + view.addGestureRecognizer(tapBackground) + + let scrollContentSize = CGSizeMake(320, 345); + self.scrollView.contentSize = scrollContentSize; + } + + + func resignLogin() { + view.endEditing(true) + } + + func keyboardWillShow(n: NSNotification) { + if let userInfo = n.userInfo { + if let keyboardSize = userInfo[UIKeyboardFrameBeginUserInfoKey]?.CGRectValue.size { + let contentInsets = UIEdgeInsetsMake(-100.0, 0.0, keyboardSize.height, 0.0) + scrollView.contentInset = contentInsets + scrollView.scrollIndicatorInsets = contentInsets + } + } + } + + func keyboardWillHide(n: NSNotification) { + let contentInsets = UIEdgeInsetsZero; + scrollView.contentInset = contentInsets; + scrollView.scrollIndicatorInsets = contentInsets; + } + + func didSignOut() { + dispatch_async(dispatch_get_main_queue()) { + self.userCredentials.clearCredentials() + self.dataStore.clearDataStore() + self.isAlreadyPresented = false + self.showSignIn() + self.presentedViewController?.dismissViewControllerAnimated(false, completion: nil) + self.presentedViewController?.dismissViewControllerAnimated(true, completion: nil) + } + } + + override func viewWillAppear(animated: Bool) { + NSNotificationCenter.defaultCenter().addObserver(self, selector: #selector(LoginViewController.keyboardWillShow(_:)), name: UIKeyboardWillShowNotification, object: self.view.window) + NSNotificationCenter.defaultCenter().addObserver(self, selector: #selector(LoginViewController.keyboardWillHide(_:)), name: UIKeyboardWillHideNotification, object: self.view.window) + + self.view.endEditing(true) + self.username.text = userCredentials.email ?? "" + if self.username.text == demoAccount { + self.username.text = "" + } + self.userPassword.text = "" + + setTrackingDeviceOwner() + + if isAlreadyPresented { return } + isAlreadyPresented = true + + if userCredentials.accessToken != nil && userCredentials.accessTokenExpires != nil { + fetchDataAndProceedToDetectionScreen() + } + else { + showSignIn() + userCredentials.clearCredentials() + } + } + + override func viewWillDisappear(animated: Bool) { + NSNotificationCenter.defaultCenter().removeObserver(self, name: UIKeyboardWillShowNotification, object: nil) + NSNotificationCenter.defaultCenter().removeObserver(self, name: UIKeyboardWillHideNotification, object: nil) + } + + override func viewDidAppear(animated: Bool) { + self.view.endEditing(true) + } + +//MARK: - Actions + @IBAction func signIn(sender: UIButton) { + UIView.transitionFromView(signInElementsView, toView: signInProgressView, duration: 0.5, options: .ShowHideTransitionViews, completion: nil) + showSigningIn() + + // if user is logging with demo account, skip cloud connection + guard let usr = username.text, pass = userPassword.text where usr != demoAccount || pass != demoAccount else { + userCredentials.clearCredentials() + userCredentials.email = demoAccount + detectHexiwear() + return + } + + dataStore.signInWithUsername(username.text ?? "", password: userPassword.text ?? "", + onFailure: { reason in + switch reason { + case .AccessTokenExpired: + dispatch_async(dispatch_get_main_queue()) { + showSimpleAlertWithTitle(applicationTitle, message: "Email/password authentication failed. Please try again.", viewController: self) + self.showSignIn() + } + case .NoSuccessStatusCode(let statusCode): + if statusCode == FORBIDDEN || statusCode == NOT_AUTHORIZED { + dispatch_async(dispatch_get_main_queue()) { + showSimpleAlertWithTitle(applicationTitle, message: "Sign in failed. Email/password not recognized.", viewController: self) + self.showSignIn() + } + } + default: + dispatch_async(dispatch_get_main_queue()) { + self.showConnectivityProblem() + } + } + + }, + onSuccess: { + if self.userCredentials.accessToken != nil && !self.userCredentials.accessToken!.isEmpty { + self.fetchDataAndProceedToDetectionScreen() + } + } + ) + } + + @IBAction func tryLoginAgain(sender: UIButton) { + showSigningIn() + fetchDataAndProceedToDetectionScreen() + } + + func loginFailureHandler(failureReason: Reason) { + switch failureReason { + case .AccessTokenExpired, .NoSuccessStatusCode(_): + dispatch_async(dispatch_get_main_queue()) { [unowned self] in self.showSignIn() } + case .Other(let err): + print("Other error \(err.description)") + default: + dispatch_async(dispatch_get_main_queue()) { [unowned self] in self.showConnectivityProblem() } + } + } + + private func fetchDataAndProceedToDetectionScreen() { + showSigningIn() + + if !indicatorView.isAnimating() { + dispatch_async(dispatch_get_main_queue()) { self.indicatorView.startAnimating() } + } + + // Connect currently logged in user with the device + setTrackingDeviceOwner() + + dataStore.fetchAll(loginFailureHandler) { + self.detectHexiwear() + return + } + } + + private func detectHexiwear() { + dispatch_async(dispatch_get_main_queue()) { + self.performSegueWithIdentifier("toDetectHexiwear", sender: self) + } + } + + private func showSignIn() { + indicatorView.stopAnimating() + signInProgressView.hidden = true + signInElementsView.hidden = false + connectivityProblemView.hidden = true + forgotPasswordAndSignup.hidden = false + username.becomeFirstResponder() + } + + private func showSigningIn() { + signInProgressView.hidden = false + signInElementsView.hidden = true + connectivityProblemView.hidden = true + forgotPasswordAndSignup.hidden = true + indicatorView.startAnimating() + } + + private func showConnectivityProblem() { + indicatorView.stopAnimating() + signInProgressView.hidden = true + signInElementsView.hidden = true + forgotPasswordAndSignup.hidden = true + connectivityProblemView.hidden = false + } + + func setTrackingDeviceOwner() { + let userAccount = userCredentials.email ?? "" + self.device.userAccount = userAccount + } + + override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) { + // Link device with the currnet loged in user (on the client side) + setTrackingDeviceOwner() + + if segue.identifier == "toDetectHexiwear" { + NSNotificationCenter.defaultCenter().removeObserver(self, name: UIKeyboardWillShowNotification, object: nil) + NSNotificationCenter.defaultCenter().removeObserver(self, name: UIKeyboardWillHideNotification, object: nil) + + if let vc = segue.destinationViewController as? BaseNavigationController, + detectHexiwearTableViewController = vc.topViewController as? DetectHexiwearTableViewController { + detectHexiwearTableViewController.dataStore = self.dataStore + detectHexiwearTableViewController.userCredentials = self.userCredentials + detectHexiwearTableViewController.device = self.device + detectHexiwearTableViewController.mqttAPI = self.mqttAPI + } + } + else if segue.identifier == "toSignUp" { + NSNotificationCenter.defaultCenter().removeObserver(self, name: UIKeyboardWillShowNotification, object: nil) + NSNotificationCenter.defaultCenter().removeObserver(self, name: UIKeyboardWillHideNotification, object: nil) + + if let vc = segue.destinationViewController as? CreateAccountTableViewController { + vc.dataStore = self.dataStore + vc.userCredentials = self.userCredentials + } + } + else if segue.identifier == "toForgotPassword" { + NSNotificationCenter.defaultCenter().removeObserver(self, name: UIKeyboardWillShowNotification, object: nil) + NSNotificationCenter.defaultCenter().removeObserver(self, name: UIKeyboardWillHideNotification, object: nil) + + if let vc = segue.destinationViewController as? BaseNavigationController, + fpc = vc.topViewController as? ForgotPasswordViewController { + fpc.dataStore = self.dataStore + fpc.forgotPasswordDelegate = self + } + } + } +} + +// MARK: - UITextFieldDelegate +extension LoginViewController: UITextFieldDelegate { + func textFieldShouldReturn(textField: UITextField) -> Bool { + if textField == username { + userPassword.becomeFirstResponder() + return true + } + + textField.resignFirstResponder() + signIn(UIButton()) + return true + } +} + +// MARK: - ForgotPasswordDelegate +extension LoginViewController: ForgotPasswordDelegate { + func didFinishResettingPassword() { + presentedViewController?.dismissViewControllerAnimated(true, completion: nil) + } +} diff --git a/iOS/Hexiwear/MQTTAPI.swift b/iOS/Hexiwear/MQTTAPI.swift new file mode 100644 index 0000000..f2e2b19 --- /dev/null +++ b/iOS/Hexiwear/MQTTAPI.swift @@ -0,0 +1,202 @@ +// +// Hexiwear application is used to pair with Hexiwear BLE devices +// and send sensor readings to WolkSense sensor data cloud +// +// Copyright (C) 2016 WolkAbout Technology s.r.o. +// +// Hexiwear is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Hexiwear is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . +// +// +// TrackingAPI.swift +// + +import Foundation + +protocol MQTTAPIProtocol { + func didPublishReadings() +} + +class MQTTAPI { + + let clientId = "HexiweariOSClient" + var mqttClient: CocoaMQTT! + var mqttPayload: String = "" + var serial: String = "" + var mqttDelegate: MQTTAPIProtocol? + + let MQTThost = "wolksense.com" + let MQTTport: UInt16 = 8883 + var MQTTQoS: CocoaMQTTQOS = .QOS0 + + //MARK: - Properties + lazy var mqttRequestQueue: NSOperationQueue = { + var queue = NSOperationQueue() + queue.name = "com.wolkabout.Hexiwear.mqttApiQueue" + queue.maxConcurrentOperationCount = 1 // just one running request allowed + return queue + }() // used to start and cancel mqtt client + + private var subscribeTopic: String { + get { + return "config/\(serial)" + } + } + + private var publishTopic: String { + get { + return "sensors/\(serial)" + } + } + + let responseSemaphore: NSCondition = NSCondition() + var responseReceived = false + + + //MARK: - Methods + init(delegate: MQTTAPIProtocol? = nil, QoS: CocoaMQTTQOS = .QOS0) { + self.mqttDelegate = delegate + self.MQTTQoS = QoS + + mqttClient = CocoaMQTT(clientId: clientId, host: MQTThost, port: MQTTport) + if let mqtt = mqttClient { + mqtt.keepAlive = 60 + mqtt.secureMQTT = true + mqtt.delegate = self + } + } + + func setAuthorisationOptions(username: String, password: String) { + guard mqttClient != nil else { return } + + mqttClient.username = username + mqttClient.password = password + print("MQTT -- user:\(username) pass:\(password)") + } + + private func signalResponseReceived() { + self.responseSemaphore.lock() + self.responseReceived = true + self.responseSemaphore.signal() + self.responseSemaphore.unlock() + } + + private func tryDisconnect() { + if mqttClient != nil && mqttClient.connState == .CONNECTED { + mqttClient.disconnect() + } + } + +} + +extension MQTTAPI: CocoaMQTTDelegate { + func mqtt(mqtt: CocoaMQTT, didConnect host: String, port: Int) { + print("MQTT -- didConnect \(host):\(port)") + } + + func mqtt(mqtt: CocoaMQTT, didConnectAck ack: CocoaMQTTConnAck) { + if ack == .ACCEPT { + self.mqttClient.publish(self.publishTopic, withString: mqttPayload, qos: self.MQTTQoS, retain: false, dup: false) + return + } + + } + + func mqtt(mqtt: CocoaMQTT, didPublishMessage message: CocoaMQTTMessage, id: UInt16) { + print("MQTT -- didPublishMessage with id: \(id) and message: \(message.string)") + tryDisconnect() + } + + func mqtt(mqtt: CocoaMQTT, didPublishAck id: UInt16) { + print("MQTT -- didPublishAck with id: \(id)") + } + + func mqtt(mqtt: CocoaMQTT, didReceiveMessage message: CocoaMQTTMessage, id: UInt16 ) { + print("MQTT -- didReceivedMessage: \(message.string) with id \(id)") + } + + func mqtt(mqtt: CocoaMQTT, didSubscribeTopic topic: String) { + print("MQTT -- didSubscribeTopic to \(topic)") + } + + func mqtt(mqtt: CocoaMQTT, didUnsubscribeTopic topic: String) { + print("MQTT -- didUnsubscribeTopic to \(topic)") + } + + func mqttDidPing(mqtt: CocoaMQTT) { + print("MQTT -- didPing") + } + + func mqttDidReceivePong(mqtt: CocoaMQTT) { + print("MQTT -- didReceivePong") + } + + func mqttDidDisconnect(mqtt: CocoaMQTT, withError err: NSError?) { + if let error = err { print("MQTT -- didDisconnect with error: \(error)") } + + signalResponseReceived() + mqttDelegate?.didPublishReadings() + print("MQTT -- didDisconnect") + } + +} + +extension MQTTAPI { + + func publishHexiwearReadings(readings: HexiwearReadings, forSerial: String) { + guard mqttRequestQueue.operationCount == 0 else { + print("MQTT -- DROPPED PUBLISH as ALREADY one is RUNNING!!!!!") + return + } + + guard let mqttPayload = readings.asMQTTMessage() else { + print("MQTT -- no valid MQTT message from hexiwear readings") + return + } + + let publish = PublishReadings(trackingAPI: self, mqttPayload: mqttPayload, serial: forSerial) + mqttRequestQueue.addOperation(publish) + } + + class PublishReadings: NSOperation { + private unowned let trackingAPI: MQTTAPI + private let mqttPayload: String + private let serial: String + + init (trackingAPI: MQTTAPI, mqttPayload: String, serial: String) { + self.trackingAPI = trackingAPI + self.mqttPayload = mqttPayload + self.serial = serial + } + + override func main() { + if self.cancelled { + print("MQTT -- Cancelled publish locations") + return + } + + // create new MQTT Connection + trackingAPI.mqttPayload = mqttPayload + trackingAPI.serial = serial + trackingAPI.mqttClient.connect() + + self.trackingAPI.responseSemaphore.lock() + while !self.trackingAPI.responseReceived { + self.trackingAPI.responseSemaphore.wait() + } + self.trackingAPI.responseReceived = false + self.trackingAPI.responseSemaphore.unlock() + } + } +} + diff --git a/iOS/Hexiwear/SettingsTableViewController.swift b/iOS/Hexiwear/SettingsTableViewController.swift new file mode 100644 index 0000000..dd23821 --- /dev/null +++ b/iOS/Hexiwear/SettingsTableViewController.swift @@ -0,0 +1,368 @@ +// +// Hexiwear application is used to pair with Hexiwear BLE devices +// and send sensor readings to WolkSense sensor data cloud +// +// Copyright (C) 2016 WolkAbout Technology s.r.o. +// +// Hexiwear is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Hexiwear is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . +// +// +// SettingsTableViewController.swift +// + +import UIKit + +protocol HexiwearSettingsDelegate { + func didSignOut() + func didSetTime() +} + +class SettingaBaseTableViewController: UITableViewController { + @IBOutlet weak var userAccountLabel: UILabel! + @IBOutlet weak var sendToCloudSwitch: UISwitch! + @IBOutlet weak var sendEvery: UISegmentedControl! + + var delegate: HexiwearSettingsDelegate? + var trackingDevice: TrackingDevice! + var dataStore: DataStore! + let progressHUD = JGProgressHUD(style: .Dark) + var isDemoUser = false + + override func viewWillAppear(animated: Bool) { + userAccountLabel.text = trackingDevice.userAccount + sendToCloudSwitch.on = !trackingDevice.trackingIsOff ?? true + + let heartbeat = trackingDevice.heartbeat + if heartbeat == 5 { + sendEvery.selectedSegmentIndex = 0 + } + else if heartbeat == 60 { + sendEvery.selectedSegmentIndex = 1 + } + else if heartbeat == 300 { + sendEvery.selectedSegmentIndex = 2 + } + else { + sendEvery.selectedSegmentIndex = 0 + } + + } + + private func logOut() { + let msg: String = "Sign out?" + let alert = UIAlertController(title: applicationTitle, message: msg, preferredStyle: UIAlertControllerStyle.Alert) + + let remove = UIAlertAction(title: "Sign out", style: UIAlertActionStyle.Default) { action in + self.delegate?.didSignOut() + } + + let cancel = UIAlertAction(title: "Cancel", style: UIAlertActionStyle.Default) { action in + alert.dismissViewControllerAnimated(true, completion: nil) + } + + alert.addAction(remove) + alert.addAction(cancel) + + presentViewController(alert, animated: true, completion: nil) + } + + @IBAction func sendToCloud(sender: AnyObject) { + trackingDevice.trackingIsOff = !sendToCloudSwitch.on + tableView.reloadData() + } + + @IBAction func sendEveryChanged(sender: UISegmentedControl) { + switch sendEvery.selectedSegmentIndex { + case 0: trackingDevice.heartbeat = 5 + case 1: trackingDevice.heartbeat = 60 + case 2: trackingDevice.heartbeat = 300 + default: trackingDevice.heartbeat = 5 + } + } + + @IBAction func doneAction(sender: UIBarButtonItem) { + dismissViewControllerAnimated(true, completion: nil) + } + + override func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) { + + tableView.deselectRowAtIndexPath(indexPath, animated: true) + if indexPath.section == 0 && indexPath.row == 0 { // log out + logOut() + } + else if indexPath.section == 0 && indexPath.row == 1 { // change password + performSegueWithIdentifier("toChangePasswordBase", sender: nil) + } + } + + override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) { + if segue.identifier == "toChangePasswordBase" { + if let vc = segue.destinationViewController as? ChangePasswordTableViewController { + vc.dataStore = self.dataStore + vc.userEmail = trackingDevice.userAccount + } + } + } + + override func tableView(tableView: UITableView, heightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat { + // only show user account for demo user + if isDemoUser { + if indexPath.section == 0 && indexPath.row == 0 { + return 44.0 + } + return 0.0 + } + + + if indexPath.section == 1 && indexPath.row == 1 { // send every + return trackingDevice.trackingIsOff ? 0.0 : 44.0 + } + + return 44.0 + } + + override func tableView(tableView: UITableView, titleForHeaderInSection section: Int) -> String? { + if section == 0 { + return "Signed in" + } + else if section == 1 { + return isDemoUser ? "" : "Cloud" + } + return nil + } + + override func tableView(tableView: UITableView, titleForFooterInSection section: Int) -> String? { + if section == 0 { + return "Tap on email to sign out" + } + else if section == 1 { + return isDemoUser ? "" : "When ON, readings will be sent to the cloud" + } + return nil + } + +} + +class HexiSettingsTableViewController: UITableViewController { + + @IBOutlet weak var batterySwitch: UISwitch! + @IBOutlet weak var temperatureSwitch: UISwitch! + @IBOutlet weak var humiditySwitch: UISwitch! + @IBOutlet weak var pressureSwitch: UISwitch! + @IBOutlet weak var lightSwitch: UISwitch! + @IBOutlet weak var acceleratorSwitch: UISwitch! + @IBOutlet weak var gyroSwitch: UISwitch! + @IBOutlet weak var magnetometerSwitch: UISwitch! + @IBOutlet weak var stepsSwitch: UISwitch! + @IBOutlet weak var caloriesSwitch: UISwitch! + @IBOutlet weak var heartrateSwitch: UISwitch! + @IBOutlet weak var manufacturerLabel: UILabel! + @IBOutlet weak var firmwareRevision: UILabel! + + @IBOutlet weak var batteryCell: UITableViewCell! + @IBOutlet weak var temperatureCell: UITableViewCell! + @IBOutlet weak var humidityCell: UITableViewCell! + @IBOutlet weak var pressureCell: UITableViewCell! + @IBOutlet weak var lightCell: UITableViewCell! + @IBOutlet weak var accCell: UITableViewCell! + @IBOutlet weak var magnetCell: UITableViewCell! + @IBOutlet weak var gyroCell: UITableViewCell! + @IBOutlet weak var stepsCell: UITableViewCell! + @IBOutlet weak var caloriesCell: UITableViewCell! + @IBOutlet weak var heartrateCell: UITableViewCell! + + + + @IBOutlet weak var batteryCellImageView: UIImageView! + @IBOutlet weak var temperatureCellImageView: UIImageView! + @IBOutlet weak var humidityCellImageView: UIImageView! + @IBOutlet weak var pressureCellImageView: UIImageView! + @IBOutlet weak var lightCellImageView: UIImageView! + @IBOutlet weak var accCellImageView: UIImageView! + @IBOutlet weak var magnetCellImageView: UIImageView! + @IBOutlet weak var gyroCellImageView: UIImageView! + @IBOutlet weak var stepsCellImageView: UIImageView! + @IBOutlet weak var caloriesImageView: UIImageView! + @IBOutlet weak var heartrateImageView: UIImageView! + + + var deviceInfo: DeviceInfo! + var hexiwearMode: HexiwearMode! + var availableReadings: [HexiwearReading] = [] + + var delegate: HexiwearSettingsDelegate? + var trackingDevice: TrackingDevice! + var dataStore: DataStore! + let progressHUD = JGProgressHUD(style: .Dark) + + override func viewWillAppear(animated: Bool) { + + super.viewWillAppear(animated) + + batterySwitch.on = !trackingDevice.isBatteryOff ?? true + temperatureSwitch.on = !trackingDevice.isTemperatureOff ?? true + humiditySwitch.on = !trackingDevice.isHumidityOff ?? true + pressureSwitch.on = !trackingDevice.isPressureOff ?? true + lightSwitch.on = !trackingDevice.isLightOff ?? true + acceleratorSwitch.on = !trackingDevice.isAcceleratorOff ?? true + magnetometerSwitch.on = !trackingDevice.isMagnetometerOff ?? true + gyroSwitch.on = !trackingDevice.isGyroOff ?? true + stepsSwitch.on = !trackingDevice.isStepsOff ?? true + caloriesSwitch.on = !trackingDevice.isCaloriesOff ?? true + heartrateSwitch.on = !trackingDevice.isHeartRateOff ?? true + manufacturerLabel.text = deviceInfo.manufacturer + firmwareRevision.text = deviceInfo.firmwareRevision + + guard let mode = hexiwearMode else { return } + + availableReadings = HexiwearMode.getReadingsForMode(mode) + + batterySwitch.enabled = availableReadings.contains(.BATTERY) + temperatureSwitch.enabled = availableReadings.contains(.TEMPERATURE) + humiditySwitch.enabled = availableReadings.contains(.HUMIDITY) + pressureSwitch.enabled = availableReadings.contains(.PRESSURE) + lightSwitch.enabled = availableReadings.contains(.LIGHT) + acceleratorSwitch.enabled = availableReadings.contains(.ACCELEROMETER) + magnetometerSwitch.enabled = availableReadings.contains(.MAGNETOMETER) + gyroSwitch.enabled = availableReadings.contains(.GYRO) + stepsSwitch.enabled = availableReadings.contains(.PEDOMETER) + caloriesSwitch.enabled = availableReadings.contains(.CALORIES) + heartrateSwitch.enabled = availableReadings.contains(.HEARTRATE) + + batteryCellImageView?.image = UIImage(named: "battery") + batteryCellImageView?.alpha = 0.6 + batteryCellImageView?.contentMode = .ScaleAspectFit + temperatureCellImageView?.image = UIImage(named: "temperature") + temperatureCellImageView?.alpha = 0.6 + temperatureCellImageView?.contentMode = .ScaleAspectFit + humidityCellImageView?.image = UIImage(named: "humidity") + humidityCellImageView?.alpha = 0.6 + humidityCellImageView?.contentMode = .ScaleAspectFit + pressureCellImageView?.image = UIImage(named: "pressure") + pressureCellImageView?.alpha = 0.6 + pressureCellImageView?.contentMode = .ScaleAspectFit + lightCellImageView?.image = UIImage(named: "light_icon") + lightCellImageView?.alpha = 0.6 + lightCellImageView?.contentMode = .ScaleAspectFit + accCellImageView?.image = UIImage(named: "accelerometer") + accCellImageView?.alpha = 0.6 + accCellImageView?.contentMode = .ScaleAspectFit + magnetCellImageView?.image = UIImage(named: "magnet_icon") + magnetCellImageView?.alpha = 0.6 + magnetCellImageView?.contentMode = .ScaleAspectFit + gyroCellImageView?.image = UIImage(named: "gyroscope") + gyroCellImageView?.alpha = 0.6 + gyroCellImageView?.contentMode = .ScaleAspectFit + stepsCellImageView?.image = UIImage(named: "steps_icon") + stepsCellImageView?.alpha = 0.6 + stepsCellImageView?.contentMode = .ScaleAspectFit + caloriesImageView.image = UIImage(named: "calories") + caloriesImageView.alpha = 0.6 + caloriesImageView.contentMode = .ScaleAspectFit + heartrateImageView.image = UIImage(named: "heartbeat_icon") + heartrateImageView.alpha = 0.6 + heartrateImageView.contentMode = .ScaleAspectFit + + tableView.reloadData() + } + + @IBAction func doneAction(sender: UIBarButtonItem) { + dismissViewControllerAnimated(true, completion: nil) + } + + @IBAction func batterySwitchAction(sender: AnyObject) { + trackingDevice.isBatteryOff = !batterySwitch.on + } + + @IBAction func weatherSwitchAction(sender: AnyObject) { + trackingDevice.isTemperatureOff = !temperatureSwitch.on + } + + @IBAction func humiditySwitchAction(sender: UISwitch) { + trackingDevice.isHumidityOff = !humiditySwitch.on + } + + @IBAction func pressureSwitchAction(sender: UISwitch) { + trackingDevice.isPressureOff = !pressureSwitch.on + } + + @IBAction func lightSwitchAction(sender: UISwitch) { + trackingDevice.isLightOff = !lightSwitch.on + } + + @IBAction func acceleratorSwitchAction(sender: AnyObject) { + trackingDevice.isAcceleratorOff = !acceleratorSwitch.on + } + + @IBAction func magnetometerSwitchAction(sender: AnyObject) { + trackingDevice.isMagnetometerOff = !magnetometerSwitch.on + } + + @IBAction func gyroSwitchAction(sender: AnyObject) { + trackingDevice.isGyroOff = !gyroSwitch.on + } + + @IBAction func stepsSwitchAction(sender: AnyObject) { + trackingDevice.isStepsOff = !stepsSwitch.on + } + + @IBAction func caloriesSwitchAction(sender: UISwitch) { + trackingDevice.isCaloriesOff = !caloriesSwitch.on + } + + @IBAction func heartrateSwitchAction(sender: UISwitch) { + trackingDevice.isHeartRateOff = !heartrateSwitch.on + } + + override func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) { + tableView.deselectRowAtIndexPath(indexPath, animated: true) + if indexPath.section == 1 && indexPath.row == 0 { // set time + setTime() + } + } + + private func setTime() { + delegate?.didSetTime() + } + +} + +extension HexiSettingsTableViewController: HexiwearTimeSettingDelegate { + func didStartSettingTime() { + let time = NSDate() + let formatter = NSDateFormatter() + formatter.dateStyle = .MediumStyle + formatter.timeStyle = .ShortStyle + + let datetimestring = formatter.stringFromDate(time) + progressHUD.textLabel.text = "Setting time on HEXIWEAR to \(datetimestring)" + progressHUD.showInView(self.view, animated: true) + + } + + func didFinishSettingTime(success: Bool) { + delay(1.0) { + if success { + self.progressHUD.textLabel.text = "Time set successfully!" + } + else { + self.progressHUD.textLabel.text = "Failure setting time!" + } + } + delay(2.0) { + self.progressHUD.dismiss() + } + + } +} diff --git a/iOS/Hexiwear/ShowDevicesTableViewController.swift b/iOS/Hexiwear/ShowDevicesTableViewController.swift new file mode 100644 index 0000000..38c9ff6 --- /dev/null +++ b/iOS/Hexiwear/ShowDevicesTableViewController.swift @@ -0,0 +1,64 @@ +// +// Hexiwear application is used to pair with Hexiwear BLE devices +// and send sensor readings to WolkSense sensor data cloud +// +// Copyright (C) 2016 WolkAbout Technology s.r.o. +// +// Hexiwear is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Hexiwear is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . +// +// +// ShowDevicesTableViewController.swift +// + +import UIKit + +protocol ActivatedDeviceDelegate { + func didSelectAlreadyActivatedName(name: String, serial: String) +} + +class ShowDevicesTableViewController: UITableViewController { + var activatedDeviceDelegate: ActivatedDeviceDelegate? + + var selectableDevices: [Device] = [] + + override func viewDidLoad() { + super.viewDidLoad() + title = "Continue device" + } + + // MARK: - Table view data source + override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + return selectableDevices.count + } + + override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell { + let cell = tableView.dequeueReusableCellWithIdentifier("deviceCell", forIndexPath: indexPath) + let device = selectableDevices[indexPath.row] + cell.textLabel?.text = device.name + cell.detailTextLabel?.text = device.deviceSerial + cell.accessoryType = .None + + return cell + } + + override func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) { + let device = selectableDevices[indexPath.row] + activatedDeviceDelegate?.didSelectAlreadyActivatedName(device.name, serial: device.deviceSerial) + } + + override func tableView(tableView: UITableView, heightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat { + return 60.0 + } + +} diff --git a/iOS/Hexiwear/StaticViewController.swift b/iOS/Hexiwear/StaticViewController.swift new file mode 100644 index 0000000..1d8ded7 --- /dev/null +++ b/iOS/Hexiwear/StaticViewController.swift @@ -0,0 +1,45 @@ +// +// Hexiwear application is used to pair with Hexiwear BLE devices +// and send sensor readings to WolkSense sensor data cloud +// +// Copyright (C) 2016 WolkAbout Technology s.r.o. +// +// Hexiwear is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Hexiwear is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . +// +// +// StaticViewController.swift +// + +import UIKit + +protocol StaticViewDelegate { + func willClose() +} + +class StaticViewController: UIViewController { + + var staticDelegate: StaticViewDelegate? + + @IBOutlet weak var webView: UIWebView! + var url: NSURL! + + override func viewWillAppear(animated: Bool) { + webView.loadRequest(NSURLRequest(URL: url)) + } + + @IBAction func closeAction(sender: AnyObject) { + staticDelegate?.willClose() + } + +} diff --git a/iOS/Hexiwear/TrackingDevice.swift b/iOS/Hexiwear/TrackingDevice.swift new file mode 100644 index 0000000..eb3d4c0 --- /dev/null +++ b/iOS/Hexiwear/TrackingDevice.swift @@ -0,0 +1,516 @@ +// +// Hexiwear application is used to pair with Hexiwear BLE devices +// and send sensor readings to WolkSense sensor data cloud +// +// Copyright (C) 2016 WolkAbout Technology s.r.o. +// +// Hexiwear is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Hexiwear is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . +// +// +// TrackingDevice.swift +// + +import Foundation +import CoreLocation + +class TrackingDevice { + + let preferences = NSUserDefaults.standardUserDefaults() + + + // Account specific properties + var userAccount: String = "" { + didSet { + print("Tracking device account: \(userAccount)") + } + } + + //example: "hexi1,wolk1|hexi2,wolk2|hexi3,wolk3|hexi4,wolk4" + var hexiAndWolkSerials: String { + get { + if let hi = preferences.objectForKey("\(userAccount)-hexiAndWolkSerials") as? String { + return hi + } + return "" + } + set (newHI) { + preferences.setObject(newHI, forKey: "\(userAccount)-hexiAndWolkSerials") + preferences.synchronize() + } + } + + // Account agnostic properties + + var trackingIsOff: Bool { + get { + return preferences.boolForKey("trackingIsOff") + } + set (newTracking) { + preferences.setBool(newTracking, forKey: "trackingIsOff") + preferences.synchronize() + } + } + + var lastPublished: NSDate? { + get { + if let lp = preferences.objectForKey("deviceLastPublished") as? NSDate { + return lp + } + return nil + } + set (newLP) { + preferences.setObject(newLP, forKey: "deviceLastPublished") + preferences.synchronize() + } + } + +/* HEXI start*/ + var lastSensorReadingDate: NSDate? { + get { + if let lp = preferences.objectForKey("lastSensorReadingDate") as? NSDate { + return lp + } + return nil + } + set (newLP) { + preferences.setObject(newLP, forKey: "lastSensorReadingDate") + preferences.synchronize() + } + } + + var lastTemperature: Double? { + get { + if let lp = preferences.objectForKey("lastTemperature") as? Double { + return lp + } + return nil + } + set (newLT) { + preferences.setObject(newLT, forKey: "lastTemperature") + preferences.synchronize() + } + } + + var lastAirPressure: Double? { + get { + if let lp = preferences.objectForKey("lastAirPressure") as? Double { + return lp + } + return nil + } + set (newLP) { + preferences.setObject(newLP, forKey: "lastAirPressure") + preferences.synchronize() + } + } + + var lastAirHumidity: Double? { + get { + if let lp = preferences.objectForKey("lastAirHumidity") as? Double { + return lp + } + return nil + } + set (newLAH) { + preferences.setObject(newLAH, forKey: "lastAirHumidity") + preferences.synchronize() + } + } + + var lastLight: Double? { + get { + if let lp = preferences.objectForKey("lastLight") as? Double { + return lp + } + return nil + } + set (newLAH) { + preferences.setObject(newLAH, forKey: "lastLight") + preferences.synchronize() + } + } + + var lastAccelerationX: Double? { + get { + if let lp = preferences.objectForKey("lastAccelerationX") as? Double { + return lp + } + return nil + } + set (newLAC) { + preferences.setObject(newLAC, forKey: "lastAccelerationX") + preferences.synchronize() + } + } + + var lastAccelerationY: Double? { + get { + if let lp = preferences.objectForKey("lastAccelerationY") as? Double { + return lp + } + return nil + } + set (newLAC) { + preferences.setObject(newLAC, forKey: "lastAccelerationY") + preferences.synchronize() + } + } + + var lastAccelerationZ: Double? { + get { + if let lp = preferences.objectForKey("lastAccelerationZ") as? Double { + return lp + } + return nil + } + set (newLAC) { + preferences.setObject(newLAC, forKey: "lastAccelerationZ") + preferences.synchronize() + } + } + + var lastGyroX: Double? { + get { + if let lp = preferences.objectForKey("lastGyroX") as? Double { + return lp + } + return nil + } + set (newLAG) { + preferences.setObject(newLAG, forKey: "lastGyroX") + preferences.synchronize() + } + } + + var lastGyroY: Double? { + get { + if let lp = preferences.objectForKey("lastGyroY") as? Double { + return lp + } + return nil + } + set (newLAG) { + preferences.setObject(newLAG, forKey: "lastGyroY") + preferences.synchronize() + } + } + + var lastGyroZ: Double? { + get { + if let lp = preferences.objectForKey("lastGyroZ") as? Double { + return lp + } + return nil + } + set (newLAG) { + preferences.setObject(newLAG, forKey: "lastGyroZ") + preferences.synchronize() + } + } + + var lastMagnetX: Double? { + get { + if let lp = preferences.objectForKey("lastMagnetX") as? Double { + return lp + } + return nil + } + set (newMAG) { + preferences.setObject(newMAG, forKey: "lastMagnetX") + preferences.synchronize() + } + } + + var lastMagnetY: Double? { + get { + if let lp = preferences.objectForKey("lastMagnetY") as? Double { + return lp + } + return nil + } + set (newMAG) { + preferences.setObject(newMAG, forKey: "lastMagnetY") + preferences.synchronize() + } + } + + var lastMagnetZ: Double? { + get { + if let lp = preferences.objectForKey("lastMagnetZ") as? Double { + return lp + } + return nil + } + set (newMAG) { + preferences.setObject(newMAG, forKey: "lastMagnetZ") + preferences.synchronize() + } + } + + var lastSteps: Int? { + get { + if let lp = preferences.objectForKey("lastSteps") as? Int { + return lp + } + return nil + } + set (newSteps) { + preferences.setObject(newSteps, forKey: "lastSteps") + preferences.synchronize() + } + } + + var lastCalories: Int? { + get { + if let lp = preferences.objectForKey("lastCalories") as? Int { + return lp + } + return nil + } + set (newCalories) { + preferences.setObject(newCalories, forKey: "lastCalories") + preferences.synchronize() + } + } + + var lastHeartRate: Int? { + get { + if let lp = preferences.objectForKey("lastHeartRate") as? Int { + return lp + } + return nil + } + set (newHR) { + preferences.setObject(newHR, forKey: "lastHeartRate") + preferences.synchronize() + } + } + + var lastHexiwearMode: Int? { + get { + if let lp = preferences.objectForKey("lastHexiwearMode") as? Int { + return lp + } + return nil + } + set (newHR) { + preferences.setObject(newHR, forKey: "lastHexiwearMode") + preferences.synchronize() + } + } + + + var isBatteryOff: Bool { + get { + return preferences.boolForKey("isBatteryOff") + } + set (newValue) { + preferences.setBool(newValue, forKey: "isBatteryOff") + preferences.synchronize() + } + } + + var isTemperatureOff: Bool { + get { + return preferences.boolForKey("isTemperatureOff") + } + set (newValue) { + preferences.setBool(newValue, forKey: "isTemperatureOff") + preferences.synchronize() + } + } + + var isHumidityOff: Bool { + get { + return preferences.boolForKey("isHumidityOff") + } + set (newValue) { + preferences.setBool(newValue, forKey: "isHumidityOff") + preferences.synchronize() + } + } + + var isPressureOff: Bool { + get { + return preferences.boolForKey("isPressureOff") + } + set (newValue) { + preferences.setBool(newValue, forKey: "isPressureOff") + preferences.synchronize() + } + } + + var isLightOff: Bool { + get { + return preferences.boolForKey("isLightOff") + } + set (newValue) { + preferences.setBool(newValue, forKey: "isLightOff") + preferences.synchronize() + } + } + + var isAcceleratorOff: Bool { + get { + return preferences.boolForKey("isAcceleratorOff") + } + set (newValue) { + preferences.setBool(newValue, forKey: "isAcceleratorOff") + preferences.synchronize() + } + } + + var isMagnetometerOff: Bool { + get { + return preferences.boolForKey("isMagnetometerOff") + } + set (newValue) { + preferences.setBool(newValue, forKey: "isMagnetometerOff") + preferences.synchronize() + } + } + + var isGyroOff: Bool { + get { + return preferences.boolForKey("isGyroOff") + } + set (newValue) { + preferences.setBool(newValue, forKey: "isGyroOff") + preferences.synchronize() + } + } + + var isStepsOff: Bool { + get { + return preferences.boolForKey("isStepsOff") + } + set (newValue) { + preferences.setBool(newValue, forKey: "isStepsOff") + preferences.synchronize() + } + } + + var isCaloriesOff: Bool { + get { + return preferences.boolForKey("isCaloriesOff") + } + set (newValue) { + preferences.setBool(newValue, forKey: "isCaloriesOff") + preferences.synchronize() + } + } + + var isHeartRateOff: Bool { + get { + return preferences.boolForKey("isHeartRateOff") + } + set (newValue) { + preferences.setBool(newValue, forKey: "isHeartRateOff") + preferences.synchronize() + } + } + + var heartbeat: Int { + get { + return preferences.integerForKey("heartbeat") + } + set (newValue) { + preferences.setInteger(newValue, forKey: "heartbeat") + preferences.synchronize() + } + } + + + /* HEXI end*/ + + func clear() { + lastPublished = nil + lastSensorReadingDate = nil + trackingIsOff = true + preferences.synchronize() + } + + + func isHeartbeatIntervalReached() -> Bool { + guard let lastPublished = lastPublished else { return true } + + let lastPublishedTimeInterval = lastPublished.timeIntervalSinceNow + let timeSinceUpdate = (-1 * lastPublishedTimeInterval) + var timeTrigger = Double(heartbeat) + + if timeTrigger < 5.0 { + timeTrigger = 5.0 + } + + return timeSinceUpdate >= timeTrigger + + } + + + // Convert string of mappings to array of mappings + //example: serialsString = "hexi1,wolk1|hexi2,wolk2|hexi3,wolk3|hexi4,wolk4" + // to [{hexi1,wolk1},{hexi2,wolk2}...] + func serialsStringToHexiAndWolkCombination(serialsString: String) -> [SerialMapping] { + guard serialsString.characters.count > 0 else { return [] } + + let serials = serialsString.componentsSeparatedByString("|") + let serialsMapping = serials.map { serialsJoined -> SerialMapping in + let twoSerialsAndPassword = serialsJoined.componentsSeparatedByString(",") + return SerialMapping(hexiSerial: twoSerialsAndPassword[0], wolkSerial: twoSerialsAndPassword[1], wolkPassword: twoSerialsAndPassword[2]) + } + + return serialsMapping + } + + // Convert array of mappings to string of mappings + func hexiAndWolkCombinationToString(hexiAndWolk: [SerialMapping]) -> String { + guard hexiAndWolk.count > 0 else { return "" } + + let start = "" + let serialsArrayAsString = hexiAndWolk.reduce(start) { $0 + "\($1.hexiSerial),\($1.wolkSerial),\($1.wolkPassword)|" } + if serialsArrayAsString.characters.count > 0 { + let finalArray = String(serialsArrayAsString.characters.dropLast()) + return finalArray + } + return "" + } + + // Find hexi serial in list of mappings and return wolk serial + func findHexiAndWolkCombination(hexiToFind: String, hexiAndWolkSerials: [SerialMapping]) -> String? { + guard hexiToFind.characters.count > 0 else { print("no hexi"); return nil } + + for mapping in hexiAndWolkSerials { + if hexiToFind == mapping.hexiSerial { return mapping.wolkSerial } + } + return nil + } + + // Find hexi serial in list of mappings and return wolk serial and password + func findWolkCredentials(hexiToFind: String, hexiAndWolkSerials: [SerialMapping]) -> (String?, String?) { + guard hexiToFind.characters.count > 0 else { print("no hexi"); return (nil,nil) } + + for mapping in hexiAndWolkSerials { + if hexiToFind == mapping.hexiSerial { return (mapping.wolkSerial, mapping.wolkPassword) } + } + return (nil, nil) + } + +} + +// Mapping structure +struct SerialMapping { + let hexiSerial: String + let wolkSerial: String + let wolkPassword: String +} diff --git a/iOS/Hexiwear/UserCredentials.swift b/iOS/Hexiwear/UserCredentials.swift new file mode 100644 index 0000000..c739b18 --- /dev/null +++ b/iOS/Hexiwear/UserCredentials.swift @@ -0,0 +1,103 @@ +// +// Hexiwear application is used to pair with Hexiwear BLE devices +// and send sensor readings to WolkSense sensor data cloud +// +// Copyright (C) 2016 WolkAbout Technology s.r.o. +// +// Hexiwear is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Hexiwear is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . +// +// +// UserCredentials.swift +// + +import Foundation + +class UserCredentials { + + let preferences = NSUserDefaults.standardUserDefaults() + + var accessTokenExpires: NSDate? { + get { + let token = preferences.objectForKey("accessTokenExpires") as? NSDate + return token + } + set (newToken) { + preferences.setObject(newToken, forKey: "accessTokenExpires") + preferences.synchronize() + } + } + + var accessToken: String? { + get { + let token = preferences.objectForKey("accessToken") as? String + return token + } + set (newToken) { + preferences.setObject(newToken, forKey: "accessToken") + preferences.synchronize() + } + } + + var refreshToken: String? { + get { + let token = preferences.objectForKey("refreshToken") as? String + return token + } + set (newToken) { + preferences.setObject(newToken, forKey: "refreshToken") + preferences.synchronize() + } + } + + var email: String? { + get { + let email = preferences.objectForKey("email") as? String + return email + } + set (newToken) { + preferences.setObject(newToken, forKey: "email") + preferences.synchronize() + } + } + + func isDemoUser() -> Bool { + if let userEmail = self.email where userEmail == demoAccount { + return true + } + return false + } + + func clearCredentials() { + accessTokenExpires = nil + accessToken = nil + refreshToken = nil + email = nil + preferences.synchronize() + } + + internal func storeCredentials(credentials: NSDictionary) { + if let accessToken = credentials["accessToken"] as? String, + accessTokenExpires = credentials["accessTokenExpires"] as? String, + refreshToken = credentials["refreshToken"] as? String, + email = credentials["email"] as? String { + print("store credentials accessToken: \(accessToken), email: \(email)") + self.accessToken = accessToken + let dateFormat = NSDateFormatter() + dateFormat.dateFormat = "yyyy-MM-dd'T'HH:mm:ssZ" + self.accessTokenExpires = dateFormat.dateFromString(accessTokenExpires) + self.refreshToken = refreshToken + self.email = email + } + } +} \ No newline at end of file diff --git a/iOS/Hexiwear/Util.swift b/iOS/Hexiwear/Util.swift new file mode 100644 index 0000000..a125756 --- /dev/null +++ b/iOS/Hexiwear/Util.swift @@ -0,0 +1,134 @@ +// +// Hexiwear application is used to pair with Hexiwear BLE devices +// and send sensor readings to WolkSense sensor data cloud +// +// Copyright (C) 2016 WolkAbout Technology s.r.o. +// +// Hexiwear is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Hexiwear is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . +// +// +// Util.swift +// + +import UIKit +import MapKit + +// HEXIWEAR CONSTANTS + +let signUpURL = "https://wolksense.com" +let privacyPolicyURL = "https://wolksense.com/documents/privacypolicy.html" +let termsAndConditionsURL = "https://wolksense.com/documents/termsofservice.html" +let NOT_AUTHORIZED = 401 +let FORBIDDEN = 403 +let NOT_FOUND = 404 +let CONFLICT = 409 + +let applicationTitle = "Hexiwear" +let demoAccount = "demo" +let wolkaboutBlueColor = UIColor(red: 0.0, green: 128.0/255.0, blue: 1.0, alpha: 1.0) + +// NOTIFICATIONS +let HexiwearDidSignOut = "HexiwearDidSignOut" + +// TopMostViewController extensions +extension UIViewController { + func topMostViewController() -> UIViewController { + // Handling Modal views + if let presentedViewController = self.presentedViewController { + return presentedViewController.topMostViewController() + } + + // Handling UIViewController's added as subviews to some other views. + for view in self.view.subviews + { + // Key property which most of us are unaware of / rarely use. + if let subViewController = view.nextResponder() { + if subViewController is UIViewController { + let viewController = subViewController as! UIViewController + return viewController.topMostViewController() + } + } + } + return self + } +} + +extension UITabBarController { + override func topMostViewController() -> UIViewController { + return self.selectedViewController!.topMostViewController() + } +} + +extension UINavigationController { + override func topMostViewController() -> UIViewController { + return self.visibleViewController!.topMostViewController() + } +} + + + +// Global misc functions + +func isValidEmailAddress(email: String) -> Bool { + let regExPattern = "[A-Z0-9a-z\\._%+-]+@([A-Za-z0-9-]+\\.)+[A-Za-z]{2,4}" + do { + let regEx = try NSRegularExpression(pattern: regExPattern, options: .CaseInsensitive) + let regExMatches = regEx.numberOfMatchesInString(email, options: [], range: NSMakeRange(0, email.characters.count)) + return regExMatches == 0 ? false : true + } + catch { + return false + } +} + +func getPrivateDocumentsDirectory() -> String? { + + let paths = NSSearchPathForDirectoriesInDomains(.DocumentDirectory, .UserDomainMask, true) + if paths.count == 0 { print("No private directory"); return nil } + var documentsDirectory = paths[0] + documentsDirectory = (documentsDirectory as NSString).stringByAppendingPathComponent("Private Documents") + if let _ = try? NSFileManager.defaultManager().createDirectoryAtPath(documentsDirectory, withIntermediateDirectories: true, attributes: nil) { + return documentsDirectory + } + return nil +} + +func delay(delay:Double, closure:()->()) { + dispatch_after( + dispatch_time( + DISPATCH_TIME_NOW, + Int64(delay * Double(NSEC_PER_SEC)) + ), + dispatch_get_main_queue(), closure) +} + +func showSimpleAlertWithTitle(title: String!, message: String, viewController: UIViewController, OKhandler: ((UIAlertAction!) -> Void)? = nil) { + dispatch_async(dispatch_get_main_queue()) { + let alert = UIAlertController(title: title, message: message, preferredStyle: .Alert) + let action = UIAlertAction(title: "OK", style: .Cancel, handler: OKhandler) + alert.addAction(action) + viewController.presentViewController(alert, animated: true, completion: nil) + } +} + +func showOKAndCancelAlertWithTitle(title: String!, message: String, viewController: UIViewController, OKhandler: ((UIAlertAction!) -> Void)? = nil) { + dispatch_async(dispatch_get_main_queue()) { + let alert = UIAlertController(title: title, message: message, preferredStyle: .Alert) + let OKaction = UIAlertAction(title: "OK", style: .Default, handler: OKhandler) + alert.addAction(OKaction) + let CancelAction = UIAlertAction(title: "Cancel", style: .Cancel, handler: nil) + alert.addAction(CancelAction) + viewController.presentViewController(alert, animated: true, completion: nil) + } +} \ No newline at end of file diff --git a/iOS/Hexiwear/WeatherTableViewCell.swift b/iOS/Hexiwear/WeatherTableViewCell.swift new file mode 100644 index 0000000..c5483d4 --- /dev/null +++ b/iOS/Hexiwear/WeatherTableViewCell.swift @@ -0,0 +1,67 @@ +// +// Hexiwear application is used to pair with Hexiwear BLE devices +// and send sensor readings to WolkSense sensor data cloud +// +// Copyright (C) 2016 WolkAbout Technology s.r.o. +// +// Hexiwear is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Hexiwear is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . +// +// +// WeatherTableViewCell.swift +// + +import UIKit + +class WeatherTableViewCell: UITableViewCell { + + @IBOutlet private weak var topLabel: UILabel! + @IBOutlet private weak var middleLabel: UILabel! + @IBOutlet private weak var bottomLabel: UILabel! + + override func awakeFromNib() { + super.awakeFromNib() + // Initialization code + } + + override func setSelected(selected: Bool, animated: Bool) { + super.setSelected(selected, animated: animated) + } + + var topText: String { + get { + return topLabel.text! + } + set (newText) { + topLabel.text = newText + } + } + + var middleText: String { + get { + return middleLabel.text! + } + set (newText) { + middleLabel.text = newText + } + } + + var bottomText: String { + get { + return bottomLabel.text! + } + set (newText) { + bottomLabel.text = newText + } + } +} diff --git a/iOS/Hexiwear/WebAPI.swift b/iOS/Hexiwear/WebAPI.swift new file mode 100644 index 0000000..1f29f07 --- /dev/null +++ b/iOS/Hexiwear/WebAPI.swift @@ -0,0 +1,703 @@ +// +// Hexiwear application is used to pair with Hexiwear BLE devices +// and send sensor readings to WolkSense sensor data cloud +// +// Copyright (C) 2016 WolkAbout Technology s.r.o. +// +// Hexiwear is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Hexiwear is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . +// +// +// WebAPI.swift +// + +import Foundation +import UIKit +import CoreLocation + + +public enum Reason { + case InvalidRequest + case AccessTokenExpired + case Cancelled + case CouldNotParseResponse + case NoData + case NoSuccessStatusCode(statusCode: Int) + case Other(NSError) +} + +func defaultSimpleFailureHandler(failureReason: Reason) { + switch failureReason { + case .InvalidRequest: + print("Invalid request") + case .AccessTokenExpired: + print("Access token expired") + case .Cancelled: + print("Request cancelled") + case .CouldNotParseResponse: + print("Could not parse JSON") + case .NoData: + print("No data") + case .NoSuccessStatusCode(let statusCode): + print("No success status code: \(statusCode)") + case .Other(let err): + print("Other error \(err.description)") + } +} + +func setNetworkActivityIndicatorVisible(visible: Bool) { + dispatch_async(dispatch_get_main_queue()) { + UIApplication.sharedApplication().networkActivityIndicatorVisible = visible + } +} + +public class WebAPIConfiguration { + let session: NSURLSession + let refreshTokenTimeout: NSTimeInterval + let pageSize: Int + let isNetworkActivityIndicated: Bool + let userCredentials: UserCredentials + let wolkaboutURL = "https://api.wolksense.com/api" + + init(session: NSURLSession = NSURLSession(configuration: NSURLSessionConfiguration.defaultSessionConfiguration()), + refreshTokenTimeout: NSTimeInterval = 20 * 60, // 20 minutes + pageSize: Int = 20, + isNetworkActivityIndicated: Bool = true, + userCredentials: UserCredentials = UserCredentials()) + { + self.session = session + self.refreshTokenTimeout = refreshTokenTimeout + self.pageSize = pageSize + self.isNetworkActivityIndicated = isNetworkActivityIndicated + self.userCredentials = userCredentials + } +} + +public class WebAPI { + + public static let sharedWebAPI = WebAPI() + + var defaultFailureHandler: (Reason) -> () { + get { + return defaultSimpleFailureHandler + } + } + + private let HTTP_OK = 200 + private let configuration: WebAPIConfiguration + + private lazy var requestQueue: NSOperationQueue = { + var queue = NSOperationQueue() + queue.name = "com.wolkabout.Hexiwear.webAPIQueue" + queue.maxConcurrentOperationCount = 1 // 1 running request allowed + return queue + }() + + init(webAPIConfiguration: WebAPIConfiguration = WebAPIConfiguration()) { + self.configuration = webAPIConfiguration + } + + deinit { + cancelRequests() + } + + func cancelRequests() { + if configuration.isNetworkActivityIndicated { setNetworkActivityIndicatorVisible(false) } + requestQueue.cancelAllOperations() + } + + + //MARK: - Device management functions + + // GET ACTIVATION STATUS + func getDeviceActivationStatus(deviceSerialNumber: String, onFailure:(Reason) -> (), onSuccess: (activationStatus: String) -> ()) { + + guard !deviceSerialNumber.isEmpty else { onFailure(.InvalidRequest); return } + + let requestURL = configuration.wolkaboutURL + "/v2/devices/\(deviceSerialNumber)/activation_status" + + let onSuccessConverter = { [unowned self] (let responseData: NSData?) in + + // Response data can be deserialized to String + guard let responseData = responseData, + dataString = NSString(data: responseData, encoding: NSUTF8StringEncoding) else { + onFailure(.CouldNotParseResponse) + return + } + + var responseString: String = String(dataString).stringByReplacingOccurrencesOfString("\r", withString: "") + responseString = responseString.stringByReplacingOccurrencesOfString("\n", withString: "") + + let status = self.getActivationStatusFromString(responseString) + status.isEmpty ? onFailure(.NoData) + : onSuccess(activationStatus: status) + } + + let getDeviceStatusOperation = WolkAboutWebRequest(webApi: self, requestURL: requestURL, queryString: nil, requestData: nil, method: "GET", sendAuthorisationToken: true, isResponseDataExpected: true, isNetworkActivityIndicated: configuration.isNetworkActivityIndicated, onFailure: onFailure, onSuccess: onSuccessConverter) + requestQueue.addOperation(getDeviceStatusOperation) + } + + // GET RANDOM SERIAL + func getRandomSerial(onFailure:(Reason) -> (), onSuccess: (serial: String) -> ()) { + let requestURL = configuration.wolkaboutURL + "/v3/devices/random_serial" + let queryString = "?type=HEXIWEAR" + + let onSuccessConverter = { [unowned self] (let responseData: NSData?) in + + // Response data can be deserialized to String + guard let responseData = responseData, + dataString = NSString(data: responseData, encoding: NSUTF8StringEncoding) else { + onFailure(.CouldNotParseResponse) + return + } + + var responseString: String = String(dataString).stringByReplacingOccurrencesOfString("\r", withString: "") + responseString = responseString.stringByReplacingOccurrencesOfString("\n", withString: "") + + let serialNumber = self.getSerialFromString(responseString) + serialNumber.isEmpty ? onFailure(.NoData) + : onSuccess(serial: serialNumber) + } + + let getRandomSerialOperation = WolkAboutWebRequest(webApi: self, requestURL: requestURL, queryString: queryString, requestData: nil, method: "GET", sendAuthorisationToken: true, isResponseDataExpected: true, isNetworkActivityIndicated: configuration.isNetworkActivityIndicated, onFailure: onFailure, onSuccess: onSuccessConverter) + requestQueue.addOperation(getRandomSerialOperation) + } + + // ACTIVATE + func activateDevice(deviceSerialNumber: String, deviceName: String, onFailure:(Reason) -> (), onSuccess: (pointId: Int, password: String) -> ()) { + + guard !deviceSerialNumber.isEmpty && !deviceName.isEmpty else { onFailure(.InvalidRequest); return } + + let requestURL = configuration.wolkaboutURL + "/v2/devices/\(deviceSerialNumber)" + + let requestJson: [String:AnyObject] = ["name": deviceName] + guard let requestData = try? NSJSONSerialization.dataWithJSONObject(requestJson, options: NSJSONWritingOptions.PrettyPrinted) else { + onFailure(.InvalidRequest) + return + } + + let onSuccessConverter = { [unowned self] (let responseData: NSData?) in + + // Response data can be deserialized to String + guard let responseData = responseData, + dataString = NSString(data: responseData, encoding: NSUTF8StringEncoding) else { + onFailure(.CouldNotParseResponse) + return + } + + var responseString: String = String(dataString).stringByReplacingOccurrencesOfString("\r", withString: "") + responseString = responseString.stringByReplacingOccurrencesOfString("\n", withString: "") + + if let pointNumber = self.getPointIdFromString(responseString) { + let password = self.getPasswordFromString(responseString) + onSuccess(pointId: pointNumber, password: password) + } + else { + onFailure(.NoData) + } + } + + let activateDeviceOperation = WolkAboutWebRequest(webApi: self, requestURL: requestURL, queryString: nil, requestData: requestData, method: "POST", sendAuthorisationToken: true, isResponseDataExpected: true, isNetworkActivityIndicated: configuration.isNetworkActivityIndicated, onFailure: onFailure, onSuccess: onSuccessConverter) + requestQueue.addOperation(activateDeviceOperation) + } + + + // DEACTIVATE + func deactivateDevice(deviceSerialNumber: String, onFailure:(Reason) -> (), onSuccess: () -> ()) { + guard !deviceSerialNumber.isEmpty else { onFailure(.InvalidRequest); return } + + let requestURL = configuration.wolkaboutURL + "/v2/devices/\(deviceSerialNumber)" + + let onSuccessConverter = { (_: NSData?) in onSuccess() } + + let deactivateOperation = WolkAboutWebRequest(webApi: self, requestURL: requestURL, queryString: nil, requestData: nil, method: "DELETE", sendAuthorisationToken: true, isResponseDataExpected: false, isNetworkActivityIndicated: configuration.isNetworkActivityIndicated, onFailure: onFailure, onSuccess: onSuccessConverter) + requestQueue.addOperation(deactivateOperation) + } + + + // FETCH POINTS + func fetchPoints(onFailure:(Reason) -> (), onSuccess: ([Device]) -> ()) { + let requestURL = configuration.wolkaboutURL + "/v3/points" + + let onSuccessConverter = { (let responseData: NSData?) in + // Response data can be deserialized to JSON + guard let responseData = responseData, jsonArrayOfDict = try? NSJSONSerialization.JSONObjectWithData(responseData, options: []) as! [[String:AnyObject]] else { + onFailure(.CouldNotParseResponse) + return + } + + // Generate result from JSON + var result = [Device]() + result = jsonArrayOfDict.reduce([]) { (accum, elem) in + var accum = accum + if let item = Device.parseDeviceJSON(elem) { + accum.append(item) + } + return accum + } + onSuccess(result) + } + + let fetchOperation = WolkAboutWebRequest(webApi: self, requestURL: requestURL, queryString: nil, requestData: nil, method: "GET", sendAuthorisationToken: true, isResponseDataExpected: true, isNetworkActivityIndicated: configuration.isNetworkActivityIndicated, onFailure: onFailure, onSuccess: onSuccessConverter) + requestQueue.addOperation(fetchOperation) + } + + + //MARK: - User account management functions + + // SIGN UP + func signUp(firstName: String, lastName: String, email: String, password: String, onFailure:(Reason) -> (), onSuccess: () -> ()) { + guard !firstName.isEmpty && !lastName.isEmpty && !email.isEmpty && !password.isEmpty else { onFailure(.InvalidRequest); return } + + let requestURL = configuration.wolkaboutURL + "/v2/signUp" + let requestJson: [String:AnyObject] = ["firstName": firstName, "lastName": lastName, "email": email, "password": password] + print(requestJson) + guard let requestData = try? NSJSONSerialization.dataWithJSONObject(requestJson, options: NSJSONWritingOptions.PrettyPrinted) else { + onFailure(.InvalidRequest) + return + } + + let onSuccessConverter = { (_: NSData?) in onSuccess() } + + let signUpOperation = WolkAboutWebRequest(webApi: self, requestURL: requestURL, queryString: nil, requestData: requestData, method: "POST", sendAuthorisationToken: false, isResponseDataExpected: false, isNetworkActivityIndicated: configuration.isNetworkActivityIndicated, onFailure: onFailure, onSuccess: onSuccessConverter) + requestQueue.addOperation(signUpOperation) + } + + + //SIGN IN + func signIn(username: String, password: String, onFailure:(Reason) -> (), onSuccess: () -> ()) { + guard !username.isEmpty && !password.isEmpty else { onFailure(.InvalidRequest); return } + + let requestURL = configuration.wolkaboutURL + "/signIn" + let requestJson: [String:AnyObject] = ["email": username, "password": password] + guard let requestData = try? NSJSONSerialization.dataWithJSONObject(requestJson, options: NSJSONWritingOptions.PrettyPrinted) else { + onFailure(.InvalidRequest) + return + } + + + let onSuccessConverter = { (let responseData: NSData?) in + self.configuration.userCredentials.accessToken = "" + // Response data can be deserialized to JSON + guard let responseData = responseData, credentials = try? NSJSONSerialization.JSONObjectWithData(responseData, options: []) as! [String:String] else { + onFailure(.CouldNotParseResponse) + return + } + + self.configuration.userCredentials.storeCredentials(credentials) + onSuccess() + } + + let signInOperation = WolkAboutWebRequest(webApi: self, requestURL: requestURL, queryString: nil, requestData: requestData, method: "POST", sendAuthorisationToken:false, isResponseDataExpected: true, isNetworkActivityIndicated: configuration.isNetworkActivityIndicated, onFailure: onFailure, onSuccess: onSuccessConverter) + requestQueue.addOperation(signInOperation) + } + + // RESET PASSWORD + func resetPassword(userEmail: String, onFailure:(Reason) -> (), onSuccess: () -> ()) { + guard !userEmail.isEmpty else { onFailure(.InvalidRequest); return } + + let requestURL = configuration.wolkaboutURL + "/reset-password" + let requestJson: [String:AnyObject] = ["email": userEmail] + guard let requestData = try? NSJSONSerialization.dataWithJSONObject(requestJson, options: NSJSONWritingOptions.PrettyPrinted) else { + onFailure(.InvalidRequest) + return + } + + + let onSuccessConverter = { (_: NSData?) in onSuccess() } + + let verifyOperation = WolkAboutWebRequest(webApi: self, requestURL: requestURL, queryString: nil, requestData: requestData, method: "POST", sendAuthorisationToken: false, isResponseDataExpected: false, isNetworkActivityIndicated: configuration.isNetworkActivityIndicated, onFailure: onFailure, onSuccess: onSuccessConverter) + requestQueue.addOperation(verifyOperation) + } + + // CHANGE PASSWORD + func changePassword(userEmail: String, oldPassword: String, newPassword: String, onFailure:(Reason) -> (), onSuccess: () -> ()) { + guard !oldPassword.isEmpty && !newPassword.isEmpty && !userEmail.isEmpty else { onFailure(.InvalidRequest); return } + + let requestURL = configuration.wolkaboutURL + "/change-password" + let requestJson: [String:AnyObject] = ["email": userEmail, "oldPassword": oldPassword, "newPassword": newPassword] + guard let requestData = try? NSJSONSerialization.dataWithJSONObject(requestJson, options: NSJSONWritingOptions.PrettyPrinted) else { + onFailure(.InvalidRequest) + return + } + + + let onSuccessConverter = { (_: NSData?) in onSuccess() } + + let verifyOperation = WolkAboutWebRequest(webApi: self, requestURL: requestURL, queryString: nil, requestData: requestData, method: "POST", sendAuthorisationToken: true, isResponseDataExpected: false, isNetworkActivityIndicated: configuration.isNetworkActivityIndicated, onFailure: onFailure, onSuccess: onSuccessConverter) + requestQueue.addOperation(verifyOperation) + } + +} + +//MARK: - Private functions +extension WebAPI { + + //MARK:- WolkAboutWebRequest + private class WolkAboutWebRequest: NSOperation { + private unowned let webApi: WebAPI + private let requestURL: String + private let queryString: String? + private let requestData: NSData? + private let method: String + private let sendAuthorisationToken: Bool + private let isResponseDataExpected: Bool + private let isNetworkActivityIndicated: Bool + private let failure: (Reason) -> () + private let success: (NSData?) -> () + + // Request/response vars + private let requestURLPath: String + private var request: NSMutableURLRequest! + private var failureReason: Reason? + private var result: NSData? + + // Async task management + private var responseSemaphore: NSCondition! + private var responseReceived = false + + init (webApi: WebAPI, requestURL: String, queryString: String?, requestData: NSData?, method: String, sendAuthorisationToken: Bool, isResponseDataExpected: Bool, isNetworkActivityIndicated: Bool, onFailure:(Reason) -> (), onSuccess: (NSData?) -> ()) { + self.webApi = webApi + self.requestURL = requestURL + self.queryString = queryString + self.requestData = requestData + self.method = method + self.sendAuthorisationToken = sendAuthorisationToken + self.isResponseDataExpected = isResponseDataExpected + self.isNetworkActivityIndicated = isNetworkActivityIndicated + self.failure = onFailure + self.success = onSuccess + + self.requestURLPath = requestURL + (queryString ?? "") + } + + override func main() { + + // Bounce if cancelled + guard !cancelled else { + failure(Reason.Cancelled) + return + } + + // Bounce if access token expired + if sendAuthorisationToken { + guard webApi.checkAccessToken() else { + failure(.AccessTokenExpired) + return + } + } + + // Bounce if cancelled + guard !cancelled else { + failure(Reason.Cancelled) + return + } + request = webApi.createWebAPIRequest(requestURLPath, method: method, jsonData: requestData, sendAuthorisationToken: sendAuthorisationToken) + + // Bounce if cancelled + guard !cancelled else { + failure(Reason.Cancelled) + return + } + responseSemaphore = NSCondition() + + if isNetworkActivityIndicated { setNetworkActivityIndicatorVisible(true) } + + let task = webApi.configuration.session.dataTaskWithRequest(request) { data, response, error in + + // Bounce if cancelled + guard !self.cancelled else { + self.failureReason = .Cancelled + self.signalResponseReceived() + return + } + + // There is no error + guard error == nil else { + self.failureReason = .Other(error!) + self.signalResponseReceived() + return + } + + // Bounce if cancelled + guard !self.cancelled else { + self.failureReason = .Cancelled + self.signalResponseReceived() + return + } + + // Response is valid + guard let httpResponse = response as? NSHTTPURLResponse else { + self.failureReason = .NoData + self.signalResponseReceived() + return + } + + // Bounce if cancelled + guard !self.cancelled else { + self.failureReason = .Cancelled + self.signalResponseReceived() + return + } + + // Response status code is successful + guard httpResponse.statusCode == self.webApi.HTTP_OK else { + self.failureReason = .NoSuccessStatusCode(statusCode: httpResponse.statusCode) + self.signalResponseReceived() + return + } + + // Bounce if cancelled + guard !self.cancelled else { + self.failureReason = .Cancelled + self.signalResponseReceived() + return + } + + // Is response data expected (i.e. exit if only statusCode is needed) + guard self.isResponseDataExpected else { + self.failureReason = nil + self.result = nil + self.signalResponseReceived() + return + } + + // Bounce if cancelled + guard !self.cancelled else { + self.failureReason = .Cancelled + self.signalResponseReceived() + return + } + + // There is response data + guard let responseData = data else { + self.failureReason = .NoData + self.signalResponseReceived() + return + } + + // Bounce if cancelled + guard !self.cancelled else { + self.failureReason = .Cancelled + self.signalResponseReceived() + return + } + + // signal that data task is done + self.result = responseData + self.signalResponseReceived() + + } + task.resume() + + // wait for async data task to end + responseSemaphore.lock() + while !responseReceived { + responseSemaphore.wait() + } + responseSemaphore.unlock() + + if isNetworkActivityIndicated { setNetworkActivityIndicatorVisible(false) } + + // Callback success or failure + if let reason = failureReason { + failure(reason) + } + else { + success(result) + } + } + + private func signalResponseReceived() { + // signal that data task is done + responseSemaphore.lock() + responseReceived = true + responseSemaphore.signal() + responseSemaphore.unlock() + } + } + + // Create REQUEST + private func createWebAPIRequest(requestURLPath: String, method: String = "GET", jsonData: NSData? = nil, sendAuthorisationToken: Bool = true) -> NSMutableURLRequest { + let requestURLPathEscaped = requestURLPath.stringByAddingPercentEncodingWithAllowedCharacters(NSCharacterSet.URLQueryAllowedCharacterSet()) + + let requestURL: NSURL = NSURL(string: requestURLPathEscaped!)! + let request = NSMutableURLRequest(URL: requestURL) + request.HTTPMethod = method + + if let data = jsonData { + let postDataLengthString = "\(data.length)" + request.setValue(postDataLengthString, forHTTPHeaderField: "Content-Length") + request.HTTPBody = data + request.setValue("application/json", forHTTPHeaderField: "Content-Type") + } + + if let accessToken = configuration.userCredentials.accessToken where sendAuthorisationToken { + request.setValue(accessToken, forHTTPHeaderField: "Authorization") + } + + request.cachePolicy = NSURLRequestCachePolicy.ReloadIgnoringLocalCacheData + print(request) + return request + } + + private func makeQueryString(dict: [String:String]) -> String { + var keyValuePairs = "" + for (k, v) in dict { + keyValuePairs += "\(k)=\(v)&" + } + return keyValuePairs == "" ? "" : "?\(keyValuePairs[keyValuePairs.startIndex.. Bool { + + if !(configuration.userCredentials.accessTokenExpires != nil && configuration.userCredentials.accessTokenExpires!.timeIntervalSinceNow < configuration.refreshTokenTimeout) { + return true + } + + let responseSemaphore: NSCondition = NSCondition() + var responseReceived = false + var accessTokenOk = false + + if configuration.userCredentials.accessTokenExpires != nil && configuration.userCredentials.accessTokenExpires!.timeIntervalSinceNow < configuration.refreshTokenTimeout { + let content = String("{\"refreshToken\":\"\(configuration.userCredentials.refreshToken!)\"}"); + guard let jsonData = content.dataUsingEncoding(NSUTF8StringEncoding) else { + configuration.userCredentials.clearCredentials() + return false + } + + if configuration.isNetworkActivityIndicated { setNetworkActivityIndicatorVisible(true) } + + let refreshRequest = createWebAPIRequest(configuration.wolkaboutURL + "/refreshToken", jsonData: jsonData, method: "POST") + configuration.session.dataTaskWithRequest(refreshRequest) { [unowned self] (let data, let response, let error) in + // Response is valid + guard let httpResponse = response as? NSHTTPURLResponse else { + self.configuration.userCredentials.clearCredentials() + + // signal that data task is done + responseSemaphore.lock() + responseReceived = true + responseSemaphore.signal() + responseSemaphore.unlock() + return + } + + // Response status code is successful + guard httpResponse.statusCode == self.HTTP_OK else { + self.configuration.userCredentials.clearCredentials() + + // signal that data task is done + responseSemaphore.lock() + responseReceived = true + responseSemaphore.signal() + responseSemaphore.unlock() + return + } + + // There is response data + guard let responseData = data else { + self.configuration.userCredentials.clearCredentials() + + // signal that data task is done + responseSemaphore.lock() + responseReceived = true + responseSemaphore.signal() + responseSemaphore.unlock() + return + } + + guard let credentials = try? NSJSONSerialization.JSONObjectWithData(responseData, options: [.MutableContainers]) as! [String:String] else { + self.configuration.userCredentials.clearCredentials() + + // signal that data task is done + responseSemaphore.lock() + responseReceived = true + responseSemaphore.signal() + responseSemaphore.unlock() + return + } + + + self.configuration.userCredentials.storeCredentials(credentials) + accessTokenOk = true + + // signal that data task is done + responseSemaphore.lock() + responseReceived = true + responseSemaphore.signal() + responseSemaphore.unlock() + + }.resume() + + // wait for async data task to end + responseSemaphore.lock() + while !responseReceived { + responseSemaphore.wait() + } + responseSemaphore.unlock() + + if configuration.isNetworkActivityIndicated { setNetworkActivityIndicatorVisible(false) } + + } + return accessTokenOk + } + + private func getActivationStatusFromString(jsonString: String) -> String { + return getStringFromJSON(jsonString, key: "activationStatus") + } + + private func getSerialFromString(jsonString: String) -> String { + return getStringFromJSON(jsonString, key: "serial") + } + + private func getPointIdFromString(jsonString: String) -> Int? { + return getIntFromJSON(jsonString, key: "pointId") + } + + private func getPasswordFromString(jsonString: String) -> String { + return getStringFromJSON(jsonString, key: "password") + } + + private func getStringFromJSON(jsonString: String, key: String) -> String { + do { + if let data = jsonString.dataUsingEncoding(NSUTF8StringEncoding), + authenticationResponse = try NSJSONSerialization.JSONObjectWithData(data, options: [.MutableContainers]) as? NSDictionary, + stringValue = authenticationResponse[key] as? String + { + return stringValue + } + } + catch { + return "" + } + + return "" + } + + private func getIntFromJSON(jsonString: String, key: String) -> Int? { + do { + if let data = jsonString.dataUsingEncoding(NSUTF8StringEncoding), + authenticationResponse = try NSJSONSerialization.JSONObjectWithData(data, options: [.MutableContainers]) as? NSDictionary, + intValue = authenticationResponse[key] as? Int + { + return intValue + } + + } + catch { + return nil + } + + return nil + } +} diff --git a/iOS/Hexiwear/commodo-ca.crt b/iOS/Hexiwear/commodo-ca.crt new file mode 100644 index 0000000..be8dc43 --- /dev/null +++ b/iOS/Hexiwear/commodo-ca.crt @@ -0,0 +1,69 @@ +-----BEGIN CERTIFICATE----- +MIIGCDCCA/CgAwIBAgIQKy5u6tl1NmwUim7bo3yMBzANBgkqhkiG9w0BAQwFADCB +hTELMAkGA1UEBhMCR0IxGzAZBgNVBAgTEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4G +A1UEBxMHU2FsZm9yZDEaMBgGA1UEChMRQ09NT0RPIENBIExpbWl0ZWQxKzApBgNV +BAMTIkNPTU9ETyBSU0EgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMTQwMjEy +MDAwMDAwWhcNMjkwMjExMjM1OTU5WjCBkDELMAkGA1UEBhMCR0IxGzAZBgNVBAgT +EkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4GA1UEBxMHU2FsZm9yZDEaMBgGA1UEChMR +Q09NT0RPIENBIExpbWl0ZWQxNjA0BgNVBAMTLUNPTU9ETyBSU0EgRG9tYWluIFZh +bGlkYXRpb24gU2VjdXJlIFNlcnZlciBDQTCCASIwDQYJKoZIhvcNAQEBBQADggEP +ADCCAQoCggEBAI7CAhnhoFmk6zg1jSz9AdDTScBkxwtiBUUWOqigwAwCfx3M28Sh +bXcDow+G+eMGnD4LgYqbSRutA776S9uMIO3Vzl5ljj4Nr0zCsLdFXlIvNN5IJGS0 +Qa4Al/e+Z96e0HqnU4A7fK31llVvl0cKfIWLIpeNs4TgllfQcBhglo/uLQeTnaG6 +ytHNe+nEKpooIZFNb5JPJaXyejXdJtxGpdCsWTWM/06RQ1A/WZMebFEh7lgUq/51 +UHg+TLAchhP6a5i84DuUHoVS3AOTJBhuyydRReZw3iVDpA3hSqXttn7IzW3uLh0n +c13cRTCAquOyQQuvvUSH2rnlG51/ruWFgqUCAwEAAaOCAWUwggFhMB8GA1UdIwQY +MBaAFLuvfgI9+qbxPISOre44mOzZMjLUMB0GA1UdDgQWBBSQr2o6lFoL2JDqElZz +30O0Oija5zAOBgNVHQ8BAf8EBAMCAYYwEgYDVR0TAQH/BAgwBgEB/wIBADAdBgNV +HSUEFjAUBggrBgEFBQcDAQYIKwYBBQUHAwIwGwYDVR0gBBQwEjAGBgRVHSAAMAgG +BmeBDAECATBMBgNVHR8ERTBDMEGgP6A9hjtodHRwOi8vY3JsLmNvbW9kb2NhLmNv +bS9DT01PRE9SU0FDZXJ0aWZpY2F0aW9uQXV0aG9yaXR5LmNybDBxBggrBgEFBQcB +AQRlMGMwOwYIKwYBBQUHMAKGL2h0dHA6Ly9jcnQuY29tb2RvY2EuY29tL0NPTU9E +T1JTQUFkZFRydXN0Q0EuY3J0MCQGCCsGAQUFBzABhhhodHRwOi8vb2NzcC5jb21v +ZG9jYS5jb20wDQYJKoZIhvcNAQEMBQADggIBAE4rdk+SHGI2ibp3wScF9BzWRJ2p +mj6q1WZmAT7qSeaiNbz69t2Vjpk1mA42GHWx3d1Qcnyu3HeIzg/3kCDKo2cuH1Z/ +e+FE6kKVxF0NAVBGFfKBiVlsit2M8RKhjTpCipj4SzR7JzsItG8kO3KdY3RYPBps +P0/HEZrIqPW1N+8QRcZs2eBelSaz662jue5/DJpmNXMyYE7l3YphLG5SEXdoltMY +dVEVABt0iN3hxzgEQyjpFv3ZBdRdRydg1vs4O2xyopT4Qhrf7W8GjEXCBgCq5Ojc +2bXhc3js9iPc0d1sjhqPpepUfJa3w/5Vjo1JXvxku88+vZbrac2/4EjxYoIQ5QxG +V/Iz2tDIY+3GH5QFlkoakdH368+PUq4NCNk+qKBR6cGHdNXJ93SrLlP7u3r7l+L4 +HyaPs9Kg4DdbKDsx5Q5XLVq4rXmsXiBmGqW5prU5wfWYQ//u+aen/e7KJD2AFsQX +j4rBYKEMrltDR5FL1ZoXX/nUh8HCjLfn4g8wGTeGrODcQgPmlKidrv0PJFGUzpII +0fxQ8ANAe4hZ7Q7drNJ3gjTcBpUC2JD5Leo31Rpg0Gcg19hCC0Wvgmje3WYkN5Ap +lBlGGSW4gNfL1IYoakRwJiNiqZ+Gb7+6kHDSVneFeO/qJakXzlByjAA6quPbYzSf ++AZxAeKCINT+b72x +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIF2DCCA8CgAwIBAgIQTKr5yttjb+Af907YWwOGnTANBgkqhkiG9w0BAQwFADCB +hTELMAkGA1UEBhMCR0IxGzAZBgNVBAgTEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4G +A1UEBxMHU2FsZm9yZDEaMBgGA1UEChMRQ09NT0RPIENBIExpbWl0ZWQxKzApBgNV +BAMTIkNPTU9ETyBSU0EgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMTAwMTE5 +MDAwMDAwWhcNMzgwMTE4MjM1OTU5WjCBhTELMAkGA1UEBhMCR0IxGzAZBgNVBAgT +EkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4GA1UEBxMHU2FsZm9yZDEaMBgGA1UEChMR +Q09NT0RPIENBIExpbWl0ZWQxKzApBgNVBAMTIkNPTU9ETyBSU0EgQ2VydGlmaWNh +dGlvbiBBdXRob3JpdHkwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCR +6FSS0gpWsawNJN3Fz0RndJkrN6N9I3AAcbxT38T6KhKPS38QVr2fcHK3YX/JSw8X +pz3jsARh7v8Rl8f0hj4K+j5c+ZPmNHrZFGvnnLOFoIJ6dq9xkNfs/Q36nGz637CC +9BR++b7Epi9Pf5l/tfxnQ3K9DADWietrLNPtj5gcFKt+5eNu/Nio5JIk2kNrYrhV +/erBvGy2i/MOjZrkm2xpmfh4SDBF1a3hDTxFYPwyllEnvGfDyi62a+pGx8cgoLEf +Zd5ICLqkTqnyg0Y3hOvozIFIQ2dOciqbXL1MGyiKXCJ7tKuY2e7gUYPDCUZObT6Z ++pUX2nwzV0E8jVHtC7ZcryxjGt9XyD+86V3Em69FmeKjWiS0uqlWPc9vqv9JWL7w +qP/0uK3pN/u6uPQLOvnoQ0IeidiEyxPx2bvhiWC4jChWrBQdnArncevPDt09qZah +SL0896+1DSJMwBGB7FY79tOi4lu3sgQiUpWAk2nojkxl8ZEDLXB0AuqLZxUpaVIC +u9ffUGpVRr+goyhhf3DQw6KqLCGqR84onAZFdr+CGCe01a60y1Dma/RMhnEw6abf +Fobg2P9A3fvQQoh/ozM6LlweQRGBY84YcWsr7KaKtzFcOmpH4MN5WdYgGq/yapiq +crxXStJLnbsQ/LBMQeXtHT1eKJ2czL+zUdqnR+WEUwIDAQABo0IwQDAdBgNVHQ4E +FgQUu69+Aj36pvE8hI6t7jiY7NkyMtQwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB +/wQFMAMBAf8wDQYJKoZIhvcNAQEMBQADggIBAArx1UaEt65Ru2yyTUEUAJNMnMvl +wFTPoCWOAvn9sKIN9SCYPBMtrFaisNZ+EZLpLrqeLppysb0ZRGxhNaKatBYSaVqM +4dc+pBroLwP0rmEdEBsqpIt6xf4FpuHA1sj+nq6PK7o9mfjYcwlYRm6mnPTXJ9OV +2jeDchzTc+CiR5kDOF3VSXkAKRzH7JsgHAckaVd4sjn8OoSgtZx8jb8uk2Intzna +FxiuvTwJaP+EmzzV1gsD41eeFPfR60/IvYcjt7ZJQ3mFXLrrkguhxuhoqEwWsRqZ +CuhTLJK7oQkYdQxlqHvLI7cawiiFwxv/0Cti76R7CZGYZ4wUAc1oBmpjIXUDgIiK +boHGhfKppC3n9KUkEEeDys30jXlYsQab5xoq2Z0B15R97QNKyvDb6KkBPvVWmcke +jkk9u+UJueBPSZI9FoJAzMxZxuY67RIuaTxslbH9qh17f4a+Hg4yRvv7E491f0yL +S0Zj/gA0QHDBw7mh3aZw4gSzQbzpgJHqZJx64SIDqZxubw5lT2yHh17zbqD5daWb +QOhTsiedSrnAdyGN/4fy3ryM7xfft0kL0fJuMAsaDk527RH89elWsn2/x20Kk4yl +0MC2Hb46TpSi125sC8KKfPog88Tk5c0NqMuRkrF8hey1FGlmDoLnzc7ILaZRfyHB +NVOFBkpdn627G190 +-----END CERTIFICATE----- diff --git a/iOS/Podfile b/iOS/Podfile new file mode 100644 index 0000000..1d0d799 --- /dev/null +++ b/iOS/Podfile @@ -0,0 +1,6 @@ +use_frameworks! + +target "Hexiwear" do + pod 'JGProgressHUD' +end +