diff options
author | Andrew Lehmer <alehmer@google.com> | 2017-04-26 14:58:59 -0700 |
---|---|---|
committer | Andrew Lehmer <alehmer@google.com> | 2017-04-26 14:58:59 -0700 |
commit | e76dcf96b0c451e46cddfa695de8feeb92533937 (patch) | |
tree | ed9a45d409f988f517e6c3f3a685cbf81ac45a5a /ios | |
parent | bcf013dda8ffac9fd76937be6441b44bb9f3586f (diff) | |
download | walt-e76dcf96b0c451e46cddfa695de8feeb92533937.tar.gz |
Import google/walt
Cloned from https://github.com/google/walt.git without modification.
Bug: 36896528
Test: N/A
Diffstat (limited to 'ios')
57 files changed, 4460 insertions, 0 deletions
diff --git a/ios/.gitignore b/ios/.gitignore new file mode 100644 index 0000000..916c7c5 --- /dev/null +++ b/ios/.gitignore @@ -0,0 +1,3 @@ +.DS_Store +xcuserdata/ + diff --git a/ios/WALT.xcodeproj/project.pbxproj b/ios/WALT.xcodeproj/project.pbxproj new file mode 100644 index 0000000..e6458b3 --- /dev/null +++ b/ios/WALT.xcodeproj/project.pbxproj @@ -0,0 +1,441 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 46; + objects = { + +/* Begin PBXBuildFile section */ + 932918081D8376BE0029432C /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 932918071D8376BE0029432C /* main.m */; }; + 9329180B1D8376BE0029432C /* WALTAppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 9329180A1D8376BE0029432C /* WALTAppDelegate.m */; }; + 9329180E1D8376BE0029432C /* MenuController.m in Sources */ = {isa = PBXBuildFile; fileRef = 9329180D1D8376BE0029432C /* MenuController.m */; }; + 932918111D8376BE0029432C /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 9329180F1D8376BE0029432C /* Main.storyboard */; }; + 932918131D8376BE0029432C /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 932918121D8376BE0029432C /* Assets.xcassets */; }; + 9329181F1D837A3E0029432C /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 9329181D1D837A3E0029432C /* LaunchScreen.storyboard */; }; + 932B1C6A1D861108008F3025 /* TapLatencyController.m in Sources */ = {isa = PBXBuildFile; fileRef = 932B1C691D861108008F3025 /* TapLatencyController.m */; }; + 932B1C761D865AE2008F3025 /* WALTLogger.m in Sources */ = {isa = PBXBuildFile; fileRef = 932B1C751D865AE2008F3025 /* WALTLogger.m */; }; + 93B35DE31D8CA7D000BE2E58 /* DebugLogController.m in Sources */ = {isa = PBXBuildFile; fileRef = 93B35DE21D8CA7D000BE2E58 /* DebugLogController.m */; }; + 93B35DEC1D90639500BE2E58 /* CoreMIDI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 93B35DEB1D90639500BE2E58 /* CoreMIDI.framework */; }; + 93B35DEF1D90B4C500BE2E58 /* MIDIClient.m in Sources */ = {isa = PBXBuildFile; fileRef = 93B35DEE1D90B4C500BE2E58 /* MIDIClient.m */; }; + 93B35DF31D90B7C200BE2E58 /* MIDIEndpoint.m in Sources */ = {isa = PBXBuildFile; fileRef = 93B35DF21D90B7C200BE2E58 /* MIDIEndpoint.m */; }; + 93B35E081D91F1FE00BE2E58 /* MIDIMessage.m in Sources */ = {isa = PBXBuildFile; fileRef = 93B35E071D91F1FE00BE2E58 /* MIDIMessage.m */; }; + 93B35E0C1D92FA6200BE2E58 /* WALTClient.m in Sources */ = {isa = PBXBuildFile; fileRef = 93B35E0B1D92FA6200BE2E58 /* WALTClient.m */; }; + 93B44C781D9AF97600D9BEC9 /* WALTTouch.m in Sources */ = {isa = PBXBuildFile; fileRef = 93B44C771D9AF97600D9BEC9 /* WALTTouch.m */; }; + 93B44C7D1D9C433A00D9BEC9 /* SettingsController.m in Sources */ = {isa = PBXBuildFile; fileRef = 93B44C7C1D9C433A00D9BEC9 /* SettingsController.m */; }; + 93B44C801D9C4DE300D9BEC9 /* ScreenResponseController.m in Sources */ = {isa = PBXBuildFile; fileRef = 93B44C7F1D9C4DE300D9BEC9 /* ScreenResponseController.m */; }; + 93B44C831D9D9E1F00D9BEC9 /* NSArray+Extensions.m in Sources */ = {isa = PBXBuildFile; fileRef = 93B44C821D9D9E1F00D9BEC9 /* NSArray+Extensions.m */; }; + 93B44C881D9EE33400D9BEC9 /* UIAlertView+Extensions.m in Sources */ = {isa = PBXBuildFile; fileRef = 93B44C871D9EE33400D9BEC9 /* UIAlertView+Extensions.m */; }; + 93B44C8B1D9F1D3700D9BEC9 /* DragLatencyController.mm in Sources */ = {isa = PBXBuildFile; fileRef = 93B44C8A1D9F1D3700D9BEC9 /* DragLatencyController.mm */; }; + 93B44C901DA8562E00D9BEC9 /* UIKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 93B44C8F1DA8562E00D9BEC9 /* UIKit.framework */; }; + 93B44C921DA8563900D9BEC9 /* CoreGraphics.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 93B44C911DA8563900D9BEC9 /* CoreGraphics.framework */; }; +/* End PBXBuildFile section */ + +/* Begin PBXFileReference section */ + 932918031D8376BE0029432C /* WALT.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = WALT.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 932918071D8376BE0029432C /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = "<group>"; }; + 932918091D8376BE0029432C /* WALTAppDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = WALTAppDelegate.h; sourceTree = "<group>"; }; + 9329180A1D8376BE0029432C /* WALTAppDelegate.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = WALTAppDelegate.m; sourceTree = "<group>"; }; + 9329180C1D8376BE0029432C /* MenuController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MenuController.h; sourceTree = "<group>"; }; + 9329180D1D8376BE0029432C /* MenuController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MenuController.m; sourceTree = "<group>"; }; + 932918101D8376BE0029432C /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = "<group>"; }; + 932918121D8376BE0029432C /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; }; + 932918171D8376BE0029432C /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; }; + 9329181E1D837A3E0029432C /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; }; + 932B1C681D861108008F3025 /* TapLatencyController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TapLatencyController.h; sourceTree = "<group>"; }; + 932B1C691D861108008F3025 /* TapLatencyController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TapLatencyController.m; sourceTree = "<group>"; }; + 932B1C741D865AE2008F3025 /* WALTLogger.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = WALTLogger.h; sourceTree = "<group>"; }; + 932B1C751D865AE2008F3025 /* WALTLogger.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = WALTLogger.m; sourceTree = "<group>"; }; + 93B35DE11D8CA7D000BE2E58 /* DebugLogController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = DebugLogController.h; sourceTree = "<group>"; }; + 93B35DE21D8CA7D000BE2E58 /* DebugLogController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = DebugLogController.m; sourceTree = "<group>"; }; + 93B35DEB1D90639500BE2E58 /* CoreMIDI.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreMIDI.framework; path = System/Library/Frameworks/CoreMIDI.framework; sourceTree = SDKROOT; }; + 93B35DED1D90B4C500BE2E58 /* MIDIClient.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MIDIClient.h; sourceTree = "<group>"; }; + 93B35DEE1D90B4C500BE2E58 /* MIDIClient.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MIDIClient.m; sourceTree = "<group>"; }; + 93B35DF11D90B7C200BE2E58 /* MIDIEndpoint.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MIDIEndpoint.h; sourceTree = "<group>"; }; + 93B35DF21D90B7C200BE2E58 /* MIDIEndpoint.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MIDIEndpoint.m; sourceTree = "<group>"; }; + 93B35E061D91F1FE00BE2E58 /* MIDIMessage.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MIDIMessage.h; sourceTree = "<group>"; }; + 93B35E071D91F1FE00BE2E58 /* MIDIMessage.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MIDIMessage.m; sourceTree = "<group>"; }; + 93B35E0A1D92FA6200BE2E58 /* WALTClient.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = WALTClient.h; sourceTree = "<group>"; }; + 93B35E0B1D92FA6200BE2E58 /* WALTClient.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = WALTClient.m; sourceTree = "<group>"; }; + 93B44C761D9AF97600D9BEC9 /* WALTTouch.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = WALTTouch.h; sourceTree = "<group>"; }; + 93B44C771D9AF97600D9BEC9 /* WALTTouch.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = WALTTouch.m; sourceTree = "<group>"; }; + 93B44C7B1D9C433A00D9BEC9 /* SettingsController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SettingsController.h; sourceTree = "<group>"; }; + 93B44C7C1D9C433A00D9BEC9 /* SettingsController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SettingsController.m; sourceTree = "<group>"; }; + 93B44C7E1D9C4DE300D9BEC9 /* ScreenResponseController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ScreenResponseController.h; sourceTree = "<group>"; }; + 93B44C7F1D9C4DE300D9BEC9 /* ScreenResponseController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ScreenResponseController.m; sourceTree = "<group>"; }; + 93B44C811D9D9E1F00D9BEC9 /* NSArray+Extensions.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSArray+Extensions.h"; sourceTree = "<group>"; }; + 93B44C821D9D9E1F00D9BEC9 /* NSArray+Extensions.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSArray+Extensions.m"; sourceTree = "<group>"; }; + 93B44C861D9EE33400D9BEC9 /* UIAlertView+Extensions.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "UIAlertView+Extensions.h"; sourceTree = "<group>"; }; + 93B44C871D9EE33400D9BEC9 /* UIAlertView+Extensions.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "UIAlertView+Extensions.m"; sourceTree = "<group>"; }; + 93B44C891D9F1D3700D9BEC9 /* DragLatencyController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = DragLatencyController.h; sourceTree = "<group>"; }; + 93B44C8A1D9F1D3700D9BEC9 /* DragLatencyController.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = DragLatencyController.mm; sourceTree = "<group>"; }; + 93B44C8F1DA8562E00D9BEC9 /* UIKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = UIKit.framework; path = System/Library/Frameworks/UIKit.framework; sourceTree = SDKROOT; }; + 93B44C911DA8563900D9BEC9 /* CoreGraphics.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreGraphics.framework; path = System/Library/Frameworks/CoreGraphics.framework; sourceTree = SDKROOT; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 932918001D8376BD0029432C /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 93B44C921DA8563900D9BEC9 /* CoreGraphics.framework in Frameworks */, + 93B44C901DA8562E00D9BEC9 /* UIKit.framework in Frameworks */, + 93B35DEC1D90639500BE2E58 /* CoreMIDI.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 932917FA1D8376BD0029432C = { + isa = PBXGroup; + children = ( + 932918051D8376BE0029432C /* WALT */, + 932918041D8376BE0029432C /* Products */, + 932B1C211D83B14A008F3025 /* Frameworks */, + ); + sourceTree = "<group>"; + }; + 932918041D8376BE0029432C /* Products */ = { + isa = PBXGroup; + children = ( + 932918031D8376BE0029432C /* WALT.app */, + ); + name = Products; + sourceTree = "<group>"; + }; + 932918051D8376BE0029432C /* WALT */ = { + isa = PBXGroup; + children = ( + 932918091D8376BE0029432C /* WALTAppDelegate.h */, + 9329180A1D8376BE0029432C /* WALTAppDelegate.m */, + 932B1C741D865AE2008F3025 /* WALTLogger.h */, + 932B1C751D865AE2008F3025 /* WALTLogger.m */, + 93B44C761D9AF97600D9BEC9 /* WALTTouch.h */, + 93B44C771D9AF97600D9BEC9 /* WALTTouch.m */, + 93B44C811D9D9E1F00D9BEC9 /* NSArray+Extensions.h */, + 93B44C821D9D9E1F00D9BEC9 /* NSArray+Extensions.m */, + 93B44C861D9EE33400D9BEC9 /* UIAlertView+Extensions.h */, + 93B44C871D9EE33400D9BEC9 /* UIAlertView+Extensions.m */, + 932B1C6C1D861803008F3025 /* Connectivity */, + 932B1C6D1D861809008F3025 /* UI */, + 932918061D8376BE0029432C /* Supporting Files */, + ); + path = WALT; + sourceTree = "<group>"; + }; + 932918061D8376BE0029432C /* Supporting Files */ = { + isa = PBXGroup; + children = ( + 932918071D8376BE0029432C /* main.m */, + 932918171D8376BE0029432C /* Info.plist */, + ); + name = "Supporting Files"; + sourceTree = "<group>"; + }; + 932B1C211D83B14A008F3025 /* Frameworks */ = { + isa = PBXGroup; + children = ( + 93B44C911DA8563900D9BEC9 /* CoreGraphics.framework */, + 93B44C8F1DA8562E00D9BEC9 /* UIKit.framework */, + 93B35DEB1D90639500BE2E58 /* CoreMIDI.framework */, + ); + name = Frameworks; + sourceTree = "<group>"; + }; + 932B1C6C1D861803008F3025 /* Connectivity */ = { + isa = PBXGroup; + children = ( + 93B35DED1D90B4C500BE2E58 /* MIDIClient.h */, + 93B35DEE1D90B4C500BE2E58 /* MIDIClient.m */, + 93B35DF11D90B7C200BE2E58 /* MIDIEndpoint.h */, + 93B35DF21D90B7C200BE2E58 /* MIDIEndpoint.m */, + 93B35E061D91F1FE00BE2E58 /* MIDIMessage.h */, + 93B35E071D91F1FE00BE2E58 /* MIDIMessage.m */, + 93B35E0A1D92FA6200BE2E58 /* WALTClient.h */, + 93B35E0B1D92FA6200BE2E58 /* WALTClient.m */, + ); + name = Connectivity; + sourceTree = "<group>"; + }; + 932B1C6D1D861809008F3025 /* UI */ = { + isa = PBXGroup; + children = ( + 9329180C1D8376BE0029432C /* MenuController.h */, + 9329180D1D8376BE0029432C /* MenuController.m */, + 93B35DE11D8CA7D000BE2E58 /* DebugLogController.h */, + 93B35DE21D8CA7D000BE2E58 /* DebugLogController.m */, + 93B44C891D9F1D3700D9BEC9 /* DragLatencyController.h */, + 93B44C8A1D9F1D3700D9BEC9 /* DragLatencyController.mm */, + 93B44C7B1D9C433A00D9BEC9 /* SettingsController.h */, + 93B44C7C1D9C433A00D9BEC9 /* SettingsController.m */, + 93B44C7E1D9C4DE300D9BEC9 /* ScreenResponseController.h */, + 93B44C7F1D9C4DE300D9BEC9 /* ScreenResponseController.m */, + 932B1C681D861108008F3025 /* TapLatencyController.h */, + 932B1C691D861108008F3025 /* TapLatencyController.m */, + 9329180F1D8376BE0029432C /* Main.storyboard */, + 9329181D1D837A3E0029432C /* LaunchScreen.storyboard */, + 932918121D8376BE0029432C /* Assets.xcassets */, + ); + name = UI; + sourceTree = "<group>"; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 932918021D8376BD0029432C /* WALT */ = { + isa = PBXNativeTarget; + buildConfigurationList = 9329181A1D8376BE0029432C /* Build configuration list for PBXNativeTarget "WALT" */; + buildPhases = ( + 932917FF1D8376BD0029432C /* Sources */, + 932918001D8376BD0029432C /* Frameworks */, + 932918011D8376BD0029432C /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = WALT; + productName = WALT; + productReference = 932918031D8376BE0029432C /* WALT.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 932917FB1D8376BD0029432C /* Project object */ = { + isa = PBXProject; + attributes = { + LastUpgradeCheck = 0810; + ORGANIZATIONNAME = "Google Inc."; + TargetAttributes = { + 932918021D8376BD0029432C = { + CreatedOnToolsVersion = 7.3.1; + DevelopmentTeam = M624B4DA33; + SystemCapabilities = { + com.apple.BackgroundModes = { + enabled = 1; + }; + com.apple.WAC = { + enabled = 0; + }; + }; + }; + }; + }; + buildConfigurationList = 932917FE1D8376BD0029432C /* Build configuration list for PBXProject "WALT" */; + compatibilityVersion = "Xcode 3.2"; + developmentRegion = English; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 932917FA1D8376BD0029432C; + productRefGroup = 932918041D8376BE0029432C /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 932918021D8376BD0029432C /* WALT */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 932918011D8376BD0029432C /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 9329181F1D837A3E0029432C /* LaunchScreen.storyboard in Resources */, + 932918131D8376BE0029432C /* Assets.xcassets in Resources */, + 932918111D8376BE0029432C /* Main.storyboard in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 932917FF1D8376BD0029432C /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 93B44C7D1D9C433A00D9BEC9 /* SettingsController.m in Sources */, + 93B35E081D91F1FE00BE2E58 /* MIDIMessage.m in Sources */, + 93B44C881D9EE33400D9BEC9 /* UIAlertView+Extensions.m in Sources */, + 93B44C831D9D9E1F00D9BEC9 /* NSArray+Extensions.m in Sources */, + 93B44C8B1D9F1D3700D9BEC9 /* DragLatencyController.mm in Sources */, + 93B35DE31D8CA7D000BE2E58 /* DebugLogController.m in Sources */, + 9329180E1D8376BE0029432C /* MenuController.m in Sources */, + 93B35DEF1D90B4C500BE2E58 /* MIDIClient.m in Sources */, + 93B35DF31D90B7C200BE2E58 /* MIDIEndpoint.m in Sources */, + 9329180B1D8376BE0029432C /* WALTAppDelegate.m in Sources */, + 932B1C6A1D861108008F3025 /* TapLatencyController.m in Sources */, + 932918081D8376BE0029432C /* main.m in Sources */, + 93B44C801D9C4DE300D9BEC9 /* ScreenResponseController.m in Sources */, + 93B35E0C1D92FA6200BE2E58 /* WALTClient.m in Sources */, + 932B1C761D865AE2008F3025 /* WALTLogger.m in Sources */, + 93B44C781D9AF97600D9BEC9 /* WALTTouch.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXVariantGroup section */ + 9329180F1D8376BE0029432C /* Main.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 932918101D8376BE0029432C /* Base */, + ); + name = Main.storyboard; + sourceTree = "<group>"; + }; + 9329181D1D837A3E0029432C /* LaunchScreen.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 9329181E1D837A3E0029432C /* Base */, + ); + name = LaunchScreen.storyboard; + sourceTree = "<group>"; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 932918181D8376BE0029432C /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "c++0x"; + CLANG_CXX_LIBRARY = "libc++"; + 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_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + 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 = c11; + 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 = 8.0; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 932918191D8376BE0029432C /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "c++0x"; + CLANG_CXX_LIBRARY = "libc++"; + 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_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + 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 = c11; + 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 = 8.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 9329181B1D8376BE0029432C /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CODE_SIGN_IDENTITY = "iPhone Developer"; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + INFOPLIST_FILE = WALT/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 8.0; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = org.chromium.latency.WALT; + PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE = ""; + }; + name = Debug; + }; + 9329181C1D8376BE0029432C /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CODE_SIGN_IDENTITY = "iPhone Developer"; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + INFOPLIST_FILE = WALT/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 8.0; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = org.chromium.latency.WALT; + PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE = ""; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 932917FE1D8376BD0029432C /* Build configuration list for PBXProject "WALT" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 932918181D8376BE0029432C /* Debug */, + 932918191D8376BE0029432C /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 9329181A1D8376BE0029432C /* Build configuration list for PBXNativeTarget "WALT" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 9329181B1D8376BE0029432C /* Debug */, + 9329181C1D8376BE0029432C /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 932917FB1D8376BD0029432C /* Project object */; +} diff --git a/ios/WALT.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/ios/WALT.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..51432ed --- /dev/null +++ b/ios/WALT.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ +<?xml version="1.0" encoding="UTF-8"?> +<Workspace + version = "1.0"> + <FileRef + location = "self:WALT.xcodeproj"> + </FileRef> +</Workspace> diff --git a/ios/WALT/Assets.xcassets/AppIcon.appiconset/Contents.json b/ios/WALT/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..ba51812 --- /dev/null +++ b/ios/WALT/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,95 @@ +{ + "images" : [ + { + "idiom" : "iphone", + "size" : "20x20", + "scale" : "2x" + }, + { + "idiom" : "iphone", + "size" : "20x20", + "scale" : "3x" + }, + { + "idiom" : "iphone", + "size" : "29x29", + "scale" : "2x" + }, + { + "idiom" : "iphone", + "size" : "29x29", + "scale" : "3x" + }, + { + "idiom" : "iphone", + "size" : "40x40", + "scale" : "2x" + }, + { + "idiom" : "iphone", + "size" : "40x40", + "scale" : "3x" + }, + { + "size" : "60x60", + "idiom" : "iphone", + "filename" : "icon@2x.png", + "scale" : "2x" + }, + { + "size" : "60x60", + "idiom" : "iphone", + "filename" : "icon@3x.png", + "scale" : "3x" + }, + { + "idiom" : "ipad", + "size" : "20x20", + "scale" : "1x" + }, + { + "idiom" : "ipad", + "size" : "20x20", + "scale" : "2x" + }, + { + "idiom" : "ipad", + "size" : "29x29", + "scale" : "1x" + }, + { + "idiom" : "ipad", + "size" : "29x29", + "scale" : "2x" + }, + { + "idiom" : "ipad", + "size" : "40x40", + "scale" : "1x" + }, + { + "idiom" : "ipad", + "size" : "40x40", + "scale" : "2x" + }, + { + "idiom" : "ipad", + "size" : "76x76", + "scale" : "1x" + }, + { + "idiom" : "ipad", + "size" : "76x76", + "scale" : "2x" + }, + { + "idiom" : "ipad", + "size" : "83.5x83.5", + "scale" : "2x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +}
\ No newline at end of file diff --git a/ios/WALT/Assets.xcassets/AppIcon.appiconset/icon@2x.png b/ios/WALT/Assets.xcassets/AppIcon.appiconset/icon@2x.png Binary files differnew file mode 100644 index 0000000..cf5db2b --- /dev/null +++ b/ios/WALT/Assets.xcassets/AppIcon.appiconset/icon@2x.png diff --git a/ios/WALT/Assets.xcassets/AppIcon.appiconset/icon@3x.png b/ios/WALT/Assets.xcassets/AppIcon.appiconset/icon@3x.png Binary files differnew file mode 100644 index 0000000..754e0b8 --- /dev/null +++ b/ios/WALT/Assets.xcassets/AppIcon.appiconset/icon@3x.png diff --git a/ios/WALT/Assets.xcassets/Contents.json b/ios/WALT/Assets.xcassets/Contents.json new file mode 100644 index 0000000..da4a164 --- /dev/null +++ b/ios/WALT/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "version" : 1, + "author" : "xcode" + } +}
\ No newline at end of file diff --git a/ios/WALT/Assets.xcassets/ic_brightness_medium_24dp.imageset/Contents.json b/ios/WALT/Assets.xcassets/ic_brightness_medium_24dp.imageset/Contents.json new file mode 100644 index 0000000..175b130 --- /dev/null +++ b/ios/WALT/Assets.xcassets/ic_brightness_medium_24dp.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "ic_brightness_medium_24dp.pdf" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +}
\ No newline at end of file diff --git a/ios/WALT/Assets.xcassets/ic_brightness_medium_24dp.imageset/ic_brightness_medium_24dp.pdf b/ios/WALT/Assets.xcassets/ic_brightness_medium_24dp.imageset/ic_brightness_medium_24dp.pdf Binary files differnew file mode 100644 index 0000000..2b34316 --- /dev/null +++ b/ios/WALT/Assets.xcassets/ic_brightness_medium_24dp.imageset/ic_brightness_medium_24dp.pdf diff --git a/ios/WALT/Assets.xcassets/ic_radio_button_checked_24dp.imageset/Contents.json b/ios/WALT/Assets.xcassets/ic_radio_button_checked_24dp.imageset/Contents.json new file mode 100644 index 0000000..d2fdd6f --- /dev/null +++ b/ios/WALT/Assets.xcassets/ic_radio_button_checked_24dp.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "ic_radio_button_checked_24dp.pdf" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +}
\ No newline at end of file diff --git a/ios/WALT/Assets.xcassets/ic_radio_button_checked_24dp.imageset/ic_radio_button_checked_24dp.pdf b/ios/WALT/Assets.xcassets/ic_radio_button_checked_24dp.imageset/ic_radio_button_checked_24dp.pdf new file mode 100644 index 0000000..0dbad84 --- /dev/null +++ b/ios/WALT/Assets.xcassets/ic_radio_button_checked_24dp.imageset/ic_radio_button_checked_24dp.pdf @@ -0,0 +1,68 @@ +%PDF-1.4 +% +3 0 obj +<< /Length 4 0 R + /Filter /FlateDecode +>> +stream +xUOA %d=aBZwaƎ>.w*kdxԘ?DV'
v9ںaK8cmS(P?bE4L dlOrʛEE^%NZ;GTK/;͡yvƥ#Wq +endstream +endobj +4 0 obj + 183 +endobj +2 0 obj +<< + /ExtGState << + /a0 << /CA 1 /ca 1 >> + >> +>> +endobj +5 0 obj +<< /Type /Page + /Parent 1 0 R + /MediaBox [ 0 0 180 180 ] + /Contents 3 0 R + /Group << + /Type /Group + /S /Transparency + /I true + /CS /DeviceRGB + >> + /Resources 2 0 R +>> +endobj +1 0 obj +<< /Type /Pages + /Kids [ 5 0 R ] + /Count 1 +>> +endobj +6 0 obj +<< /Creator (cairo 1.14.6 (http://cairographics.org)) + /Producer (cairo 1.14.6 (http://cairographics.org)) +>> +endobj +7 0 obj +<< /Type /Catalog + /Pages 1 0 R +>> +endobj +xref +0 8 +0000000000 65535 f +0000000583 00000 n +0000000297 00000 n +0000000015 00000 n +0000000275 00000 n +0000000369 00000 n +0000000648 00000 n +0000000775 00000 n +trailer +<< /Size 8 + /Root 7 0 R + /Info 6 0 R +>> +startxref +827 +%%EOF diff --git a/ios/WALT/Assets.xcassets/ic_receipt_black_24dp.imageset/Contents.json b/ios/WALT/Assets.xcassets/ic_receipt_black_24dp.imageset/Contents.json new file mode 100644 index 0000000..10b20cc --- /dev/null +++ b/ios/WALT/Assets.xcassets/ic_receipt_black_24dp.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "ic_receipt_black_24dp.pdf" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +}
\ No newline at end of file diff --git a/ios/WALT/Assets.xcassets/ic_receipt_black_24dp.imageset/ic_receipt_black_24dp.pdf b/ios/WALT/Assets.xcassets/ic_receipt_black_24dp.imageset/ic_receipt_black_24dp.pdf Binary files differnew file mode 100644 index 0000000..dc7033b --- /dev/null +++ b/ios/WALT/Assets.xcassets/ic_receipt_black_24dp.imageset/ic_receipt_black_24dp.pdf diff --git a/ios/WALT/Assets.xcassets/ic_schedule_black_24dp.imageset/Contents.json b/ios/WALT/Assets.xcassets/ic_schedule_black_24dp.imageset/Contents.json new file mode 100644 index 0000000..ad6335c --- /dev/null +++ b/ios/WALT/Assets.xcassets/ic_schedule_black_24dp.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "ic_schedule_black_24dp.pdf" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +}
\ No newline at end of file diff --git a/ios/WALT/Assets.xcassets/ic_schedule_black_24dp.imageset/ic_schedule_black_24dp.pdf b/ios/WALT/Assets.xcassets/ic_schedule_black_24dp.imageset/ic_schedule_black_24dp.pdf new file mode 100644 index 0000000..ab3cca1 --- /dev/null +++ b/ios/WALT/Assets.xcassets/ic_schedule_black_24dp.imageset/ic_schedule_black_24dp.pdf @@ -0,0 +1,68 @@ +%PDF-1.4 +% +3 0 obj +<< /Length 4 0 R + /Filter /FlateDecode +>> +stream +x]PA0yAhZҥ P%eٱ=C^;w5 d\h5Pb8V1F|\X
A92\P8sRβbKgx59ѝX"ۦ
H%P4g~y#ŷp ($Ns +endstream +endobj +4 0 obj + 183 +endobj +2 0 obj +<< + /ExtGState << + /a0 << /CA 1 /ca 1 >> + >> +>> +endobj +5 0 obj +<< /Type /Page + /Parent 1 0 R + /MediaBox [ 0 0 180 180 ] + /Contents 3 0 R + /Group << + /Type /Group + /S /Transparency + /I true + /CS /DeviceRGB + >> + /Resources 2 0 R +>> +endobj +1 0 obj +<< /Type /Pages + /Kids [ 5 0 R ] + /Count 1 +>> +endobj +6 0 obj +<< /Creator (cairo 1.14.6 (http://cairographics.org)) + /Producer (cairo 1.14.6 (http://cairographics.org)) +>> +endobj +7 0 obj +<< /Type /Catalog + /Pages 1 0 R +>> +endobj +xref +0 8 +0000000000 65535 f +0000000583 00000 n +0000000297 00000 n +0000000015 00000 n +0000000275 00000 n +0000000369 00000 n +0000000648 00000 n +0000000775 00000 n +trailer +<< /Size 8 + /Root 7 0 R + /Info 6 0 R +>> +startxref +827 +%%EOF diff --git a/ios/WALT/Assets.xcassets/ic_settings_black_24dp.imageset/Contents.json b/ios/WALT/Assets.xcassets/ic_settings_black_24dp.imageset/Contents.json new file mode 100644 index 0000000..5087db5 --- /dev/null +++ b/ios/WALT/Assets.xcassets/ic_settings_black_24dp.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "ic_settings_black_24dp.pdf" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +}
\ No newline at end of file diff --git a/ios/WALT/Assets.xcassets/ic_settings_black_24dp.imageset/ic_settings_black_24dp.pdf b/ios/WALT/Assets.xcassets/ic_settings_black_24dp.imageset/ic_settings_black_24dp.pdf Binary files differnew file mode 100644 index 0000000..a20e463 --- /dev/null +++ b/ios/WALT/Assets.xcassets/ic_settings_black_24dp.imageset/ic_settings_black_24dp.pdf diff --git a/ios/WALT/Assets.xcassets/ic_swap_horiz_24dp.imageset/Contents.json b/ios/WALT/Assets.xcassets/ic_swap_horiz_24dp.imageset/Contents.json new file mode 100644 index 0000000..8161920 --- /dev/null +++ b/ios/WALT/Assets.xcassets/ic_swap_horiz_24dp.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "ic_swap_horiz_24dp.pdf" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +}
\ No newline at end of file diff --git a/ios/WALT/Assets.xcassets/ic_swap_horiz_24dp.imageset/ic_swap_horiz_24dp.pdf b/ios/WALT/Assets.xcassets/ic_swap_horiz_24dp.imageset/ic_swap_horiz_24dp.pdf Binary files differnew file mode 100644 index 0000000..50bfc96 --- /dev/null +++ b/ios/WALT/Assets.xcassets/ic_swap_horiz_24dp.imageset/ic_swap_horiz_24dp.pdf diff --git a/ios/WALT/Assets.xcassets/ic_swap_vert_24dp.imageset/Contents.json b/ios/WALT/Assets.xcassets/ic_swap_vert_24dp.imageset/Contents.json new file mode 100644 index 0000000..d7d49f0 --- /dev/null +++ b/ios/WALT/Assets.xcassets/ic_swap_vert_24dp.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "ic_swap_vert_24dp.pdf" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +}
\ No newline at end of file diff --git a/ios/WALT/Assets.xcassets/ic_swap_vert_24dp.imageset/ic_swap_vert_24dp.pdf b/ios/WALT/Assets.xcassets/ic_swap_vert_24dp.imageset/ic_swap_vert_24dp.pdf new file mode 100644 index 0000000..36647b4 --- /dev/null +++ b/ios/WALT/Assets.xcassets/ic_swap_vert_24dp.imageset/ic_swap_vert_24dp.pdf @@ -0,0 +1,68 @@ +%PDF-1.4 +% +3 0 obj +<< /Length 4 0 R + /Filter /FlateDecode +>> +stream +x]
PDT1(_KD0<x+1z`DQ \#t-hz:,|R6\М,MwzH=2A+ +endstream +endobj +4 0 obj + 106 +endobj +2 0 obj +<< + /ExtGState << + /a0 << /CA 1 /ca 1 >> + >> +>> +endobj +5 0 obj +<< /Type /Page + /Parent 1 0 R + /MediaBox [ 0 0 180 180 ] + /Contents 3 0 R + /Group << + /Type /Group + /S /Transparency + /I true + /CS /DeviceRGB + >> + /Resources 2 0 R +>> +endobj +1 0 obj +<< /Type /Pages + /Kids [ 5 0 R ] + /Count 1 +>> +endobj +6 0 obj +<< /Creator (cairo 1.14.6 (http://cairographics.org)) + /Producer (cairo 1.14.6 (http://cairographics.org)) +>> +endobj +7 0 obj +<< /Type /Catalog + /Pages 1 0 R +>> +endobj +xref +0 8 +0000000000 65535 f +0000000506 00000 n +0000000220 00000 n +0000000015 00000 n +0000000198 00000 n +0000000292 00000 n +0000000571 00000 n +0000000698 00000 n +trailer +<< /Size 8 + /Root 7 0 R + /Info 6 0 R +>> +startxref +750 +%%EOF diff --git a/ios/WALT/Assets.xcassets/ic_timelapse_black_24dp.imageset/Contents.json b/ios/WALT/Assets.xcassets/ic_timelapse_black_24dp.imageset/Contents.json new file mode 100644 index 0000000..4ec5a1a --- /dev/null +++ b/ios/WALT/Assets.xcassets/ic_timelapse_black_24dp.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "ic_timelapse_black_24dp.pdf" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +}
\ No newline at end of file diff --git a/ios/WALT/Assets.xcassets/ic_timelapse_black_24dp.imageset/ic_timelapse_black_24dp.pdf b/ios/WALT/Assets.xcassets/ic_timelapse_black_24dp.imageset/ic_timelapse_black_24dp.pdf new file mode 100644 index 0000000..eacbc18 --- /dev/null +++ b/ios/WALT/Assets.xcassets/ic_timelapse_black_24dp.imageset/ic_timelapse_black_24dp.pdf @@ -0,0 +1,68 @@ +%PDF-1.4 +% +3 0 obj +<< /Length 4 0 R + /Filter /FlateDecode +>> +stream +x]OQnD!O#lLh??) f830ˍRDJ\*Jeҫ)兝A2gåt)6ކYG+LϲQZ96O|Wu.ш[^b9vtu}iM:dB;
-^y41PJ%NFwز96nS8B+(?ZR +endstream +endobj +4 0 obj + 207 +endobj +2 0 obj +<< + /ExtGState << + /a0 << /CA 1 /ca 1 >> + >> +>> +endobj +5 0 obj +<< /Type /Page + /Parent 1 0 R + /MediaBox [ 0 0 180 180 ] + /Contents 3 0 R + /Group << + /Type /Group + /S /Transparency + /I true + /CS /DeviceRGB + >> + /Resources 2 0 R +>> +endobj +1 0 obj +<< /Type /Pages + /Kids [ 5 0 R ] + /Count 1 +>> +endobj +6 0 obj +<< /Creator (cairo 1.14.6 (http://cairographics.org)) + /Producer (cairo 1.14.6 (http://cairographics.org)) +>> +endobj +7 0 obj +<< /Type /Catalog + /Pages 1 0 R +>> +endobj +xref +0 8 +0000000000 65535 f +0000000607 00000 n +0000000321 00000 n +0000000015 00000 n +0000000299 00000 n +0000000393 00000 n +0000000672 00000 n +0000000799 00000 n +trailer +<< /Size 8 + /Root 7 0 R + /Info 6 0 R +>> +startxref +851 +%%EOF diff --git a/ios/WALT/Base.lproj/LaunchScreen.storyboard b/ios/WALT/Base.lproj/LaunchScreen.storyboard new file mode 100644 index 0000000..cc0fbd6 --- /dev/null +++ b/ios/WALT/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,28 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="11201" systemVersion="15G1004" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" colorMatched="YES" initialViewController="ZVE-EP-b8v"> + <dependencies> + <deployment identifier="iOS"/> + <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="11161"/> + <capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/> + </dependencies> + <scenes> + <!--View Controller--> + <scene sceneID="NNW-W4-Efl"> + <objects> + <viewController id="ZVE-EP-b8v" sceneMemberID="viewController"> + <layoutGuides> + <viewControllerLayoutGuide type="top" id="s0l-pW-AVn"/> + <viewControllerLayoutGuide type="bottom" id="e0H-b7-IdX"/> + </layoutGuides> + <view key="view" contentMode="scaleToFill" id="aTj-Tt-xEN"> + <rect key="frame" x="0.0" y="0.0" width="375" height="667"/> + <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/> + <color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/> + </view> + </viewController> + <placeholder placeholderIdentifier="IBFirstResponder" id="uIO-hR-lzl" userLabel="First Responder" sceneMemberID="firstResponder"/> + </objects> + <point key="canvasLocation" x="474" y="302"/> + </scene> + </scenes> +</document> diff --git a/ios/WALT/Base.lproj/Main.storyboard b/ios/WALT/Base.lproj/Main.storyboard new file mode 100644 index 0000000..0f44ef7 --- /dev/null +++ b/ios/WALT/Base.lproj/Main.storyboard @@ -0,0 +1,502 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="11201" systemVersion="15G1004" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" colorMatched="YES" initialViewController="eQU-I2-GD8"> + <dependencies> + <deployment identifier="iOS"/> + <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="11161"/> + <capability name="Constraints to layout margins" minToolsVersion="6.0"/> + <capability name="Navigation items with more than one left or right bar item" minToolsVersion="7.0"/> + <capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/> + </dependencies> + <scenes> + <!--Menu--> + <scene sceneID="xfn-MN-fso"> + <objects> + <tableViewController title="Menu" id="VPe-PS-rc7" customClass="MenuController" sceneMemberID="viewController"> + <tableView key="view" clipsSubviews="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" dataMode="static" style="grouped" separatorStyle="default" rowHeight="44" sectionHeaderHeight="18" sectionFooterHeight="18" id="Nnj-XQ-ITE"> + <rect key="frame" x="0.0" y="0.0" width="320" height="568"/> + <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/> + <color key="backgroundColor" cocoaTouchSystemColor="groupTableViewBackgroundColor"/> + <sections> + <tableViewSection headerTitle="Connection" id="5LD-wg-Xzb"> + <cells> + <tableViewCell clipsSubviews="YES" contentMode="scaleToFill" selectionStyle="default" accessoryType="checkmark" indentationWidth="10" id="Yz9-7O-DAz"> + <rect key="frame" x="0.0" y="120" width="320" height="44"/> + <autoresizingMask key="autoresizingMask"/> + <tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" tableViewCell="Yz9-7O-DAz" id="iAK-6i-7rL"> + <frame key="frameInset" width="281" height="44"/> + <autoresizingMask key="autoresizingMask"/> + <subviews> + <imageView userInteractionEnabled="NO" contentMode="scaleToFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" fixedFrame="YES" image="ic_schedule_black_24dp" translatesAutoresizingMaskIntoConstraints="NO" id="m9l-fT-TFO"> + <frame key="frameInset" minX="11" minY="4" width="34" height="34"/> + <autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/> + </imageView> + <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" fixedFrame="YES" text="Clock Sync" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="cQe-pS-WYx"> + <frame key="frameInset" minX="53" minY="11" width="289" height="21"/> + <autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/> + <fontDescription key="fontDescription" type="system" pointSize="17"/> + <color key="textColor" cocoaTouchSystemColor="darkTextColor"/> + <nil key="highlightedColor"/> + </label> + </subviews> + </tableViewCellContentView> + </tableViewCell> + </cells> + </tableViewSection> + <tableViewSection headerTitle="Measure Latency" id="Xz6-jL-dfU"> + <cells> + <tableViewCell clipsSubviews="YES" contentMode="scaleToFill" selectionStyle="default" accessoryType="disclosureIndicator" indentationWidth="10" id="JQX-fF-HrG"> + <rect key="frame" x="0.0" y="220" width="320" height="44"/> + <autoresizingMask key="autoresizingMask"/> + <tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" tableViewCell="JQX-fF-HrG" id="hvq-GP-UvE"> + <frame key="frameInset" width="287" height="44"/> + <autoresizingMask key="autoresizingMask"/> + <subviews> + <imageView userInteractionEnabled="NO" contentMode="scaleToFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" fixedFrame="YES" image="ic_radio_button_checked_24dp" translatesAutoresizingMaskIntoConstraints="NO" id="2Jk-he-kof"> + <frame key="frameInset" minX="11" minY="4" width="34" height="34"/> + <autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/> + </imageView> + <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" fixedFrame="YES" text="Tap Latency" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="Ksj-bS-5ef"> + <frame key="frameInset" minX="53" minY="11" width="289" height="21"/> + <autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/> + <fontDescription key="fontDescription" type="system" pointSize="17"/> + <color key="textColor" cocoaTouchSystemColor="darkTextColor"/> + <nil key="highlightedColor"/> + </label> + </subviews> + </tableViewCellContentView> + <connections> + <segue destination="h21-iB-JdX" kind="show" id="SB0-CR-bdX"/> + </connections> + </tableViewCell> + <tableViewCell clipsSubviews="YES" contentMode="scaleToFill" selectionStyle="default" accessoryType="disclosureIndicator" indentationWidth="10" id="imo-Vh-mdV"> + <rect key="frame" x="0.0" y="264" width="320" height="44"/> + <autoresizingMask key="autoresizingMask"/> + <tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" tableViewCell="imo-Vh-mdV" id="rjR-UK-RhN"> + <frame key="frameInset" width="287" height="44"/> + <autoresizingMask key="autoresizingMask"/> + <subviews> + <imageView userInteractionEnabled="NO" contentMode="scaleToFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" fixedFrame="YES" image="ic_swap_vert_24dp" translatesAutoresizingMaskIntoConstraints="NO" id="8pB-A2-BtL"> + <frame key="frameInset" minX="11" minY="4" width="34" height="34"/> + <autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/> + </imageView> + <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" fixedFrame="YES" text="Drag Latency" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="l4F-Iu-ak1"> + <frame key="frameInset" minX="53" minY="11" width="289" height="21"/> + <autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/> + <fontDescription key="fontDescription" type="system" pointSize="17"/> + <color key="textColor" cocoaTouchSystemColor="darkTextColor"/> + <nil key="highlightedColor"/> + </label> + </subviews> + </tableViewCellContentView> + <connections> + <segue destination="Iye-5w-RWd" kind="show" id="Cz1-x2-JfO"/> + </connections> + </tableViewCell> + <tableViewCell clipsSubviews="YES" contentMode="scaleToFill" selectionStyle="default" accessoryType="disclosureIndicator" indentationWidth="10" id="hEB-ci-IiA"> + <rect key="frame" x="0.0" y="308" width="320" height="44"/> + <autoresizingMask key="autoresizingMask"/> + <tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" tableViewCell="hEB-ci-IiA" id="JMe-la-M9R"> + <frame key="frameInset" width="287" height="44"/> + <autoresizingMask key="autoresizingMask"/> + <subviews> + <imageView userInteractionEnabled="NO" contentMode="scaleToFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" fixedFrame="YES" image="ic_brightness_medium_24dp" translatesAutoresizingMaskIntoConstraints="NO" id="Osy-BJ-byS"> + <frame key="frameInset" minX="11" minY="4" width="34" height="34"/> + <autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/> + </imageView> + <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" fixedFrame="YES" text="Screen Response" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="CDC-fL-NyR"> + <frame key="frameInset" minX="53" minY="11" width="289" height="21"/> + <autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/> + <fontDescription key="fontDescription" type="system" pointSize="17"/> + <color key="textColor" cocoaTouchSystemColor="darkTextColor"/> + <nil key="highlightedColor"/> + </label> + </subviews> + </tableViewCellContentView> + <connections> + <segue destination="Fkf-5e-wh6" kind="show" id="5k3-vY-CoW"/> + </connections> + </tableViewCell> + </cells> + </tableViewSection> + <tableViewSection headerTitle="Configuration" id="b19-yD-3Nr"> + <cells> + <tableViewCell clipsSubviews="YES" contentMode="scaleToFill" selectionStyle="default" accessoryType="disclosureIndicator" indentationWidth="10" id="WbU-am-NQX"> + <rect key="frame" x="0.0" y="408" width="320" height="44"/> + <autoresizingMask key="autoresizingMask"/> + <tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" tableViewCell="WbU-am-NQX" id="Poi-2P-Xzl"> + <frame key="frameInset" width="287" height="44"/> + <autoresizingMask key="autoresizingMask"/> + <subviews> + <imageView userInteractionEnabled="NO" contentMode="scaleToFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" fixedFrame="YES" image="ic_receipt_black_24dp" translatesAutoresizingMaskIntoConstraints="NO" id="oYC-Td-Xiq"> + <frame key="frameInset" minX="11" minY="4" width="34" height="34"/> + <autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/> + </imageView> + <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" fixedFrame="YES" text="Log" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="r9U-Nu-T4s"> + <frame key="frameInset" minX="53" minY="11" width="289" height="21"/> + <autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/> + <fontDescription key="fontDescription" type="system" pointSize="17"/> + <color key="textColor" cocoaTouchSystemColor="darkTextColor"/> + <nil key="highlightedColor"/> + </label> + </subviews> + </tableViewCellContentView> + <connections> + <segue destination="guL-f1-O7a" kind="show" id="HYc-2d-b1V"/> + </connections> + </tableViewCell> + <tableViewCell clipsSubviews="YES" contentMode="scaleToFill" selectionStyle="default" accessoryType="disclosureIndicator" indentationWidth="10" id="3zc-KF-CGB"> + <rect key="frame" x="0.0" y="452" width="320" height="44"/> + <autoresizingMask key="autoresizingMask"/> + <tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" tableViewCell="3zc-KF-CGB" id="lWb-4z-t2t"> + <frame key="frameInset" width="287" height="44"/> + <autoresizingMask key="autoresizingMask"/> + <subviews> + <imageView userInteractionEnabled="NO" contentMode="scaleToFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" fixedFrame="YES" image="ic_settings_black_24dp" translatesAutoresizingMaskIntoConstraints="NO" id="PFH-yZ-k7o"> + <frame key="frameInset" minX="11" minY="4" width="34" height="34"/> + <autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/> + </imageView> + <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" fixedFrame="YES" text="Settings" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="qKV-2v-Mof"> + <frame key="frameInset" minX="53" minY="11" width="289" height="21"/> + <autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/> + <fontDescription key="fontDescription" type="system" pointSize="17"/> + <color key="textColor" cocoaTouchSystemColor="darkTextColor"/> + <nil key="highlightedColor"/> + </label> + </subviews> + </tableViewCellContentView> + <connections> + <segue destination="KXp-FX-gyD" kind="show" id="zW3-IK-73l"/> + </connections> + </tableViewCell> + </cells> + </tableViewSection> + </sections> + <connections> + <outlet property="dataSource" destination="VPe-PS-rc7" id="zUh-Cn-OUV"/> + <outlet property="delegate" destination="VPe-PS-rc7" id="apU-y3-EWH"/> + </connections> + </tableView> + <navigationItem key="navigationItem" title="WALT" id="cMy-Nl-GuX"> + <barButtonItem key="rightBarButtonItem" systemItem="action" id="CNR-8L-zzo"> + <connections> + <action selector="shareLog:" destination="VPe-PS-rc7" id="uOw-PR-xfd"/> + </connections> + </barButtonItem> + </navigationItem> + <connections> + <outlet property="syncCell" destination="Yz9-7O-DAz" id="wd3-Q6-3f4"/> + </connections> + </tableViewController> + <placeholder placeholderIdentifier="IBFirstResponder" id="M86-GS-6VT" userLabel="First Responder" sceneMemberID="firstResponder"/> + </objects> + <point key="canvasLocation" x="297.5" y="451.5"/> + </scene> + <!--Tap Latency--> + <scene sceneID="3uc-Py-LUu"> + <objects> + <viewController id="h21-iB-JdX" customClass="TapLatencyController" sceneMemberID="viewController"> + <layoutGuides> + <viewControllerLayoutGuide type="top" id="Kje-tJ-2UI"/> + <viewControllerLayoutGuide type="bottom" id="8ba-If-sGm"/> + </layoutGuides> + <view key="view" contentMode="scaleToFill" id="sap-lR-8gs"> + <rect key="frame" x="0.0" y="0.0" width="320" height="568"/> + <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/> + <subviews> + <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="N ↓%d (%d) ↑%d (%d) ⇄ %d" textAlignment="right" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="CKp-gK-Prj"> + <fontDescription key="fontDescription" name="Menlo-Regular" family="Menlo" pointSize="12"/> + <color key="textColor" cocoaTouchSystemColor="darkTextColor"/> + <nil key="highlightedColor"/> + </label> + <textView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleToFill" editable="NO" text="Tap log" selectable="NO" translatesAutoresizingMaskIntoConstraints="NO" id="aVF-f1-M68"> + <color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/> + <fontDescription key="fontDescription" name="Menlo-Regular" family="Menlo" pointSize="12"/> + <textInputTraits key="textInputTraits" autocapitalizationType="sentences"/> + </textView> + </subviews> + <color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/> + <constraints> + <constraint firstItem="aVF-f1-M68" firstAttribute="leading" secondItem="sap-lR-8gs" secondAttribute="leadingMargin" id="GJR-qH-82B"/> + <constraint firstItem="aVF-f1-M68" firstAttribute="trailing" secondItem="sap-lR-8gs" secondAttribute="trailingMargin" id="H63-xH-0g1"/> + <constraint firstItem="CKp-gK-Prj" firstAttribute="trailing" secondItem="sap-lR-8gs" secondAttribute="trailingMargin" id="IJc-zq-skT"/> + <constraint firstItem="8ba-If-sGm" firstAttribute="top" secondItem="aVF-f1-M68" secondAttribute="bottom" constant="20" id="XK1-Td-yd9"/> + <constraint firstItem="aVF-f1-M68" firstAttribute="top" secondItem="CKp-gK-Prj" secondAttribute="bottom" constant="8" id="YQn-DQ-Crg"/> + <constraint firstItem="CKp-gK-Prj" firstAttribute="top" secondItem="Kje-tJ-2UI" secondAttribute="bottom" constant="8" id="Zk4-8b-9t6"/> + <constraint firstItem="CKp-gK-Prj" firstAttribute="leading" secondItem="sap-lR-8gs" secondAttribute="leadingMargin" id="cu4-I8-gXQ"/> + </constraints> + </view> + <navigationItem key="navigationItem" title="Tap Latency" id="tft-Mo-BGM"> + <rightBarButtonItems> + <barButtonItem systemItem="action" id="QRN-YN-FDB"> + <connections> + <action selector="computeStatistics:" destination="h21-iB-JdX" id="MTm-4g-4QE"/> + </connections> + </barButtonItem> + <barButtonItem systemItem="refresh" id="qgs-da-ABq"> + <connections> + <action selector="reset:" destination="h21-iB-JdX" id="APG-Ef-yHQ"/> + </connections> + </barButtonItem> + </rightBarButtonItems> + </navigationItem> + <connections> + <outlet property="countLabel" destination="CKp-gK-Prj" id="W9S-au-8R2"/> + <outlet property="logView" destination="aVF-f1-M68" id="Wyz-CU-4mT"/> + </connections> + </viewController> + <placeholder placeholderIdentifier="IBFirstResponder" id="W9i-HC-abY" userLabel="First Responder" sceneMemberID="firstResponder"/> + </objects> + <point key="canvasLocation" x="1123" y="-183"/> + </scene> + <!--Drag Latency--> + <scene sceneID="4bt-KJ-0Wm"> + <objects> + <viewController id="Iye-5w-RWd" customClass="DragLatencyController" sceneMemberID="viewController"> + <layoutGuides> + <viewControllerLayoutGuide type="top" id="fEb-9K-y6d"/> + <viewControllerLayoutGuide type="bottom" id="Enu-n5-RxV"/> + </layoutGuides> + <view key="view" contentMode="scaleToFill" id="0Ud-kj-BK3"> + <rect key="frame" x="0.0" y="0.0" width="320" height="568"/> + <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/> + <subviews> + <view hidden="YES" userInteractionEnabled="NO" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="cBY-fS-a2g"> + <color key="backgroundColor" red="0.050980392156862744" green="0.37254901960784315" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/> + <constraints> + <constraint firstAttribute="height" constant="1" id="DwU-Fu-L9W"/> + </constraints> + </view> + <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="N ✛ %d ⇄ %d" textAlignment="right" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="71e-I5-3db"> + <fontDescription key="fontDescription" name="Menlo-Regular" family="Menlo" pointSize="12"/> + <color key="textColor" cocoaTouchSystemColor="darkTextColor"/> + <nil key="highlightedColor"/> + </label> + <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="" textAlignment="right" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="GyK-xM-A9Q"> + <fontDescription key="fontDescription" name="Menlo-Regular" family="Menlo" pointSize="12"/> + <color key="textColor" cocoaTouchSystemColor="darkTextColor"/> + <nil key="highlightedColor"/> + </label> + </subviews> + <color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/> + <constraints> + <constraint firstItem="Enu-n5-RxV" firstAttribute="top" secondItem="cBY-fS-a2g" secondAttribute="bottom" constant="100" id="1vV-Pu-RuO"/> + <constraint firstItem="GyK-xM-A9Q" firstAttribute="top" secondItem="71e-I5-3db" secondAttribute="bottom" constant="4" id="Hju-xu-Q8g"/> + <constraint firstItem="GyK-xM-A9Q" firstAttribute="leading" secondItem="0Ud-kj-BK3" secondAttribute="leadingMargin" id="Pbs-9G-d6W"/> + <constraint firstAttribute="trailingMargin" secondItem="cBY-fS-a2g" secondAttribute="trailing" constant="48" id="WCL-lT-JAo"/> + <constraint firstItem="71e-I5-3db" firstAttribute="top" secondItem="fEb-9K-y6d" secondAttribute="bottom" constant="8" id="Whk-y2-L1A"/> + <constraint firstItem="cBY-fS-a2g" firstAttribute="leading" secondItem="0Ud-kj-BK3" secondAttribute="leadingMargin" constant="47" id="bNo-cM-ETZ"/> + <constraint firstItem="71e-I5-3db" firstAttribute="leading" secondItem="0Ud-kj-BK3" secondAttribute="leadingMargin" id="ez0-ku-0wH"/> + <constraint firstItem="71e-I5-3db" firstAttribute="trailing" secondItem="0Ud-kj-BK3" secondAttribute="trailingMargin" id="wvD-NG-Tmd"/> + <constraint firstItem="GyK-xM-A9Q" firstAttribute="trailing" secondItem="0Ud-kj-BK3" secondAttribute="trailingMargin" id="zH0-EP-Iao"/> + </constraints> + </view> + <navigationItem key="navigationItem" title="Drag Latency" id="nkK-4I-dD4"> + <rightBarButtonItems> + <barButtonItem systemItem="action" id="pwk-sy-UTR"> + <connections> + <action selector="computeStatistics:" destination="Iye-5w-RWd" id="XSc-C0-Ipw"/> + </connections> + </barButtonItem> + <barButtonItem systemItem="play" id="ox2-eT-993"> + <connections> + <action selector="start:" destination="Iye-5w-RWd" id="grt-Pk-vaH"/> + </connections> + </barButtonItem> + <barButtonItem systemItem="refresh" id="gjM-TD-UIa"> + <connections> + <action selector="reset:" destination="Iye-5w-RWd" id="XLB-Zo-bxB"/> + </connections> + </barButtonItem> + </rightBarButtonItems> + </navigationItem> + <connections> + <outlet property="countLabel" destination="71e-I5-3db" id="2Hd-cJ-mJq"/> + <outlet property="goalpostView" destination="cBY-fS-a2g" id="ye8-n4-scw"/> + <outlet property="statusLabel" destination="GyK-xM-A9Q" id="PUC-gN-nFr"/> + </connections> + </viewController> + <placeholder placeholderIdentifier="IBFirstResponder" id="xKv-dC-lRO" userLabel="First Responder" sceneMemberID="firstResponder"/> + </objects> + <point key="canvasLocation" x="1123" y="479"/> + </scene> + <!--Log--> + <scene sceneID="nPK-4w-VMQ"> + <objects> + <viewController id="guL-f1-O7a" customClass="DebugLogController" sceneMemberID="viewController"> + <layoutGuides> + <viewControllerLayoutGuide type="top" id="Eeq-Eg-acb"/> + <viewControllerLayoutGuide type="bottom" id="KWd-BQ-T6A"/> + </layoutGuides> + <view key="view" contentMode="scaleToFill" id="L9F-Pn-wlS"> + <rect key="frame" x="0.0" y="0.0" width="320" height="568"/> + <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/> + <subviews> + <textView clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" fixedFrame="YES" alwaysBounceVertical="YES" editable="NO" translatesAutoresizingMaskIntoConstraints="NO" id="Ieg-Za-vhc"> + <frame key="frameInset" width="320" height="568"/> + <autoresizingMask key="autoresizingMask"/> + <color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/> + <fontDescription key="fontDescription" name="Menlo-Regular" family="Menlo" pointSize="12"/> + <textInputTraits key="textInputTraits" autocorrectionType="no" spellCheckingType="no"/> + </textView> + </subviews> + <color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/> + </view> + <navigationItem key="navigationItem" title="Log" id="O0W-gg-5jB"> + <barButtonItem key="rightBarButtonItem" systemItem="refresh" id="Jkt-ZT-dbp"> + <connections> + <action selector="reset:" destination="guL-f1-O7a" id="iq1-NB-I55"/> + </connections> + </barButtonItem> + </navigationItem> + <connections> + <outlet property="textView" destination="Ieg-Za-vhc" id="cpW-FM-oGN"/> + </connections> + </viewController> + <placeholder placeholderIdentifier="IBFirstResponder" id="sVI-Tk-Oeq" userLabel="First Responder" sceneMemberID="firstResponder"/> + </objects> + <point key="canvasLocation" x="-486" y="1241"/> + </scene> + <!--Settings--> + <scene sceneID="AXy-Me-DP3"> + <objects> + <tableViewController id="KXp-FX-gyD" customClass="SettingsController" sceneMemberID="viewController"> + <tableView key="view" clipsSubviews="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" dataMode="static" style="grouped" separatorStyle="default" rowHeight="44" sectionHeaderHeight="18" sectionFooterHeight="18" id="UHk-hm-yk8"> + <rect key="frame" x="0.0" y="0.0" width="320" height="568"/> + <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/> + <color key="backgroundColor" cocoaTouchSystemColor="groupTableViewBackgroundColor"/> + <sections> + <tableViewSection headerTitle="Diagnostics" id="D5K-Gr-zPc"> + <cells> + <tableViewCell clipsSubviews="YES" contentMode="scaleToFill" selectionStyle="default" indentationWidth="10" id="VEI-kW-Bog"> + <rect key="frame" x="0.0" y="120" width="320" height="44"/> + <autoresizingMask key="autoresizingMask"/> + <tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" tableViewCell="VEI-kW-Bog" id="xAH-6e-SbQ"> + <frame key="frameInset" width="320" height="44"/> + <autoresizingMask key="autoresizingMask"/> + <subviews> + <imageView userInteractionEnabled="NO" contentMode="scaleToFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" fixedFrame="YES" image="ic_swap_horiz_24dp" translatesAutoresizingMaskIntoConstraints="NO" id="pi9-AT-sUn"> + <frame key="frameInset" minX="11" minY="4" width="34" height="34"/> + <autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/> + </imageView> + <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" fixedFrame="YES" text="Ping" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="a4d-MA-jEA"> + <frame key="frameInset" minX="53" minY="11" width="289" height="21"/> + <autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/> + <fontDescription key="fontDescription" type="system" pointSize="17"/> + <color key="textColor" cocoaTouchSystemColor="darkTextColor"/> + <nil key="highlightedColor"/> + </label> + </subviews> + </tableViewCellContentView> + </tableViewCell> + <tableViewCell clipsSubviews="YES" contentMode="scaleToFill" selectionStyle="default" indentationWidth="10" id="Gz9-j5-8xW"> + <rect key="frame" x="0.0" y="164" width="320" height="44"/> + <autoresizingMask key="autoresizingMask"/> + <tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" tableViewCell="Gz9-j5-8xW" id="r4F-Bs-7ud"> + <frame key="frameInset" width="320" height="44"/> + <autoresizingMask key="autoresizingMask"/> + <subviews> + <imageView userInteractionEnabled="NO" contentMode="scaleToFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" fixedFrame="YES" image="ic_timelapse_black_24dp" translatesAutoresizingMaskIntoConstraints="NO" id="bTu-Fo-JNE"> + <frame key="frameInset" minX="11" minY="4" width="34" height="34"/> + <autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/> + </imageView> + <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" fixedFrame="YES" text="Check Clock Drift" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="EEb-uw-LMh"> + <frame key="frameInset" minX="53" minY="11" width="289" height="21"/> + <autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/> + <fontDescription key="fontDescription" type="system" pointSize="17"/> + <color key="textColor" cocoaTouchSystemColor="darkTextColor"/> + <nil key="highlightedColor"/> + </label> + </subviews> + </tableViewCellContentView> + </tableViewCell> + </cells> + </tableViewSection> + </sections> + <connections> + <outlet property="dataSource" destination="KXp-FX-gyD" id="avQ-Qu-gD4"/> + <outlet property="delegate" destination="KXp-FX-gyD" id="e0U-YI-bn6"/> + </connections> + </tableView> + <navigationItem key="navigationItem" title="Settings" id="bOg-hs-cZX"/> + </tableViewController> + <placeholder placeholderIdentifier="IBFirstResponder" id="iTV-IX-dfn" userLabel="First Responder" sceneMemberID="firstResponder"/> + </objects> + <point key="canvasLocation" x="281" y="1240"/> + </scene> + <!--Screen Response--> + <scene sceneID="xRB-KS-fQF"> + <objects> + <viewController id="Fkf-5e-wh6" customClass="ScreenResponseController" sceneMemberID="viewController"> + <layoutGuides> + <viewControllerLayoutGuide type="top" id="fLM-tb-4JW"/> + <viewControllerLayoutGuide type="bottom" id="o3i-Ce-bdu"/> + </layoutGuides> + <view key="view" contentMode="scaleToFill" id="mUy-JP-aUq"> + <rect key="frame" x="0.0" y="0.0" width="320" height="568"/> + <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/> + <subviews> + <view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="A0k-c3-ite"> + <color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/> + </view> + <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="%2f s" textAlignment="right" lineBreakMode="tailTruncation" numberOfLines="0" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="sBN-Rv-s0v"> + <fontDescription key="fontDescription" name="Menlo-Regular" family="Menlo" pointSize="12"/> + <color key="textColor" cocoaTouchSystemColor="darkTextColor"/> + <nil key="highlightedColor"/> + </label> + </subviews> + <color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/> + <constraints> + <constraint firstAttribute="trailingMargin" secondItem="sBN-Rv-s0v" secondAttribute="trailing" constant="4" id="4r7-cz-Seh"/> + <constraint firstItem="o3i-Ce-bdu" firstAttribute="top" secondItem="sBN-Rv-s0v" secondAttribute="bottom" constant="20" id="6nT-mv-2OZ"/> + <constraint firstItem="A0k-c3-ite" firstAttribute="leading" secondItem="mUy-JP-aUq" secondAttribute="leading" id="95R-9H-9dg"/> + <constraint firstItem="A0k-c3-ite" firstAttribute="bottom" secondItem="o3i-Ce-bdu" secondAttribute="top" id="EO5-zp-wxl"/> + <constraint firstItem="sBN-Rv-s0v" firstAttribute="top" secondItem="fLM-tb-4JW" secondAttribute="bottom" constant="8" id="Okz-Er-W3e"/> + <constraint firstAttribute="trailing" secondItem="A0k-c3-ite" secondAttribute="trailing" id="TO4-EE-mpl"/> + <constraint firstItem="A0k-c3-ite" firstAttribute="top" secondItem="fLM-tb-4JW" secondAttribute="bottom" id="Yav-6g-gqR"/> + <constraint firstItem="sBN-Rv-s0v" firstAttribute="leading" secondItem="mUy-JP-aUq" secondAttribute="leadingMargin" constant="4" id="g05-ai-aW1"/> + </constraints> + </view> + <navigationItem key="navigationItem" title="Screen Response" id="CyH-dP-ZeE"> + <barButtonItem key="rightBarButtonItem" systemItem="play" id="ovk-ji-96i"> + <connections> + <action selector="start:" destination="Fkf-5e-wh6" id="Xzm-yg-vpv"/> + </connections> + </barButtonItem> + </navigationItem> + <connections> + <outlet property="flasherView" destination="A0k-c3-ite" id="Rzq-mg-51J"/> + <outlet property="responseLabel" destination="sBN-Rv-s0v" id="yl2-9u-Hho"/> + </connections> + </viewController> + <placeholder placeholderIdentifier="IBFirstResponder" id="2TJ-Zy-sEu" userLabel="First Responder" sceneMemberID="firstResponder"/> + </objects> + <point key="canvasLocation" x="1121" y="1240"/> + </scene> + <!--Navigation Controller--> + <scene sceneID="OTd-lv-DGm"> + <objects> + <navigationController id="eQU-I2-GD8" sceneMemberID="viewController"> + <navigationBar key="navigationBar" contentMode="scaleToFill" id="hSL-5Q-cVe"> + <rect key="frame" x="0.0" y="0.0" width="320" height="44"/> + <autoresizingMask key="autoresizingMask"/> + </navigationBar> + <connections> + <segue destination="VPe-PS-rc7" kind="relationship" relationship="rootViewController" id="Sex-vh-uAb"/> + </connections> + </navigationController> + <placeholder placeholderIdentifier="IBFirstResponder" id="cgc-Xe-8Tr" userLabel="First Responder" sceneMemberID="firstResponder"/> + </objects> + <point key="canvasLocation" x="-510" y="451"/> + </scene> + </scenes> + <resources> + <image name="ic_brightness_medium_24dp" width="180" height="180"/> + <image name="ic_radio_button_checked_24dp" width="180" height="180"/> + <image name="ic_receipt_black_24dp" width="180" height="180"/> + <image name="ic_schedule_black_24dp" width="180" height="180"/> + <image name="ic_settings_black_24dp" width="180" height="180"/> + <image name="ic_swap_horiz_24dp" width="180" height="180"/> + <image name="ic_swap_vert_24dp" width="180" height="180"/> + <image name="ic_timelapse_black_24dp" width="180" height="180"/> + </resources> +</document> diff --git a/ios/WALT/DebugLogController.h b/ios/WALT/DebugLogController.h new file mode 100644 index 0000000..4b2c30e --- /dev/null +++ b/ios/WALT/DebugLogController.h @@ -0,0 +1,23 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import <UIKit/UIKit.h> + +@interface DebugLogController : UIViewController +@property (assign) IBOutlet UITextView *textView; + +- (IBAction)reset:(id)sender; +@end diff --git a/ios/WALT/DebugLogController.m b/ios/WALT/DebugLogController.m new file mode 100644 index 0000000..400c262 --- /dev/null +++ b/ios/WALT/DebugLogController.m @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import "DebugLogController.h" + +#import "WALTLogger.h" + +@implementation DebugLogController +- (void)viewDidAppear:(BOOL)animated { + [super viewDidAppear:animated]; + + self.textView.text = [[WALTLogger sessionLogger] stringValue]; +} + +- (IBAction)reset:(id)sender { + [[WALTLogger sessionLogger] clear]; + self.textView.text = [[WALTLogger sessionLogger] stringValue]; + // TODO(pquinn): Reprint DEVICE information? +} +@end diff --git a/ios/WALT/DragLatencyController.h b/ios/WALT/DragLatencyController.h new file mode 100644 index 0000000..6454fe7 --- /dev/null +++ b/ios/WALT/DragLatencyController.h @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import <UIKit/UIKit.h> + +@interface DragLatencyController : UIViewController +@property (assign, nonatomic) IBOutlet UILabel *countLabel; +@property (assign, nonatomic) IBOutlet UILabel *statusLabel; +@property (assign, nonatomic) IBOutlet UIView *goalpostView; + +- (IBAction)start:(id)sender; +- (IBAction)reset:(id)sender; +- (IBAction)computeStatistics:(id)sender; +@end diff --git a/ios/WALT/DragLatencyController.mm b/ios/WALT/DragLatencyController.mm new file mode 100644 index 0000000..5b6b9b4 --- /dev/null +++ b/ios/WALT/DragLatencyController.mm @@ -0,0 +1,393 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import "DragLatencyController.h" + +#import <dispatch/dispatch.h> +#import <math.h> +#import <numeric> +#import <vector> + +#import "UIAlertView+Extensions.h" +#import "WALTAppDelegate.h" +#import "WALTClient.h" +#import "WALTLogger.h" +#import "WALTTouch.h" + +static const NSTimeInterval kGoalpostFrequency = 0.55; // TODO(pquinn): User-configurable settings. +static const NSUInteger kMinTouchEvents = 100; +static const NSUInteger kMinLaserEvents = 8; +static const char kWALTLaserTag = 'L'; + +@interface WALTLaserEvent : NSObject +@property (assign) NSTimeInterval t; +@property (assign) int value; +@end + +@implementation WALTLaserEvent +@end + +/** Linear interpolation between x0 and x1 at alpha. */ +template <typename T> +static T Lerp(const T& x0, const T& x1, double alpha) { + NSCAssert(alpha >= 0 && alpha <= 1, @"alpha must be between 0 and 1 (%f)", alpha); + return ((1 - alpha) * x0) + (alpha * x1); +} + +/** Linear interpolation of (xp, yp) at x. */ +template <typename S, typename T> +static std::vector<T> Interpolate(const std::vector<S>& x, + const std::vector<S>& xp, + const std::vector<T>& yp) { + NSCAssert(xp.size(), @"xp must contain at least one value."); + NSCAssert(xp.size() == yp.size(), @"xp and yp must have matching lengths."); + + std::vector<T> y; + y.reserve(x.size()); + + size_t i = 0; // Index into x. + + for (; i < x.size() && x[i] < xp.front(); ++i) { + y.push_back(yp.front()); // Pad out y with yp.front() for x values before xp.front(). + } + + size_t ip = 0; // Index into xp/yp. + + for (; ip < xp.size() && i < x.size(); ++i) { + while (ip < xp.size() && xp[ip] <= x[i]) { // Find an xp[ip] greater than x[i]. + ++ip; + } + if (ip >= xp.size()) { + break; // Ran out of values. + } + + const double alpha = (x[i] - xp[ip - 1]) / static_cast<double>(xp[ip] - xp[ip - 1]); + y.push_back(Lerp(yp[ip - 1], yp[ip], alpha)); + } + + for (; i < x.size(); ++i) { + y.push_back(yp.back()); // Pad out y with yp.back() for values after xp.back(). + } + + return y; +} + +/** Extracts the values of y where the corresponding value in x is equal to value. */ +template <typename S, typename T> +static std::vector<S> Extract(const std::vector<T>& x, const std::vector<S>& y, const T& value) { + NSCAssert(x.size() == y.size(), @"x and y must have matching lengths."); + std::vector<S> extracted; + + for (size_t i = 0; i < x.size(); ++i) { + if (x[i] == value) { + extracted.push_back(y[i]); + } + } + + return extracted; +} + +/** Returns the standard deviation of the values in x. */ +template <typename T> +static T StandardDeviation(const std::vector<T>& x) { + NSCAssert(x.size() > 0, @"x must have at least one value."); + const T sum = std::accumulate(x.begin(), x.end(), T{}); + const T mean = sum / x.size(); + const T ss = std::accumulate(x.begin(), x.end(), T{}, ^(T accum, T value){ + return accum + ((value - mean) * (value - mean)); + }); + return sqrt(ss / (x.size() - 1)); +} + +/** Returns the index of the smallest value in x. */ +template <typename T> +static size_t ArgMin(const std::vector<T>& x) { + NSCAssert(x.size() > 0, @"x must have at least one value."); + size_t imin = 0; + for (size_t i = 1; i < x.size(); ++i) { + if (x[i] < x[imin]) { + imin = i; + } + } + return imin; +} + +/** + * Finds a positive time value that shifting laserTs by will minimise the standard deviation of + * interpolated touchYs. + */ +static NSTimeInterval FindBestShift(const std::vector<NSTimeInterval>& laserTs, + const std::vector<NSTimeInterval>& touchTs, + const std::vector<CGFloat>& touchYs) { + NSCAssert(laserTs.size() > 0, @"laserTs must have at least one value."); + NSCAssert(touchTs.size() == touchYs.size(), @"touchTs and touchYs must have matching lengths."); + + const NSTimeInterval kSearchCoverage = 0.15; + const int kSteps = 1500; + const NSTimeInterval kShiftStep = kSearchCoverage / kSteps; + + std::vector<NSTimeInterval> deviations; + deviations.reserve(kSteps); + + std::vector<NSTimeInterval> ts(laserTs.size()); + for (int i = 0; i < kSteps; ++i) { + for (size_t j = 0; j < laserTs.size(); ++j) { + ts[j] = laserTs[j] + (kShiftStep * i); + } + + std::vector<CGFloat> laserYs = Interpolate(ts, touchTs, touchYs); + deviations.push_back(StandardDeviation(laserYs)); + } + + return ArgMin(deviations) * kShiftStep; +} + +@interface DragLatencyController () +- (void)updateCountDisplay; +- (void)processEvent:(UIEvent *)event; +- (void)receiveTriggers:(id)context; +- (void)stopReceiver; +@end + +@implementation DragLatencyController { + WALTClient *_client; + WALTLogger *_logger; + + NSMutableArray<WALTTouch *> *_touchEvents; + NSMutableArray<WALTLaserEvent *> *_laserEvents; + + NSThread *_triggerReceiver; + dispatch_semaphore_t _receiverComplete; +} + +- (void)dealloc { + [self stopReceiver]; +} + +- (void)viewDidLoad { + [super viewDidLoad]; + + _client = ((WALTAppDelegate *)[UIApplication sharedApplication].delegate).client; + _logger = [WALTLogger sessionLogger]; +} + +- (void)viewWillAppear:(BOOL)animated { + [super viewWillAppear:animated]; + + [self updateCountDisplay]; + + [_logger appendString:@"DRAGLATENCY\n"]; +} + +- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event { + [self processEvent:event]; +} + +- (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event { + [self processEvent:event]; +} + +- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event { + [self processEvent:event]; +} + +- (void)processEvent:(UIEvent *)event { + // TODO(pquinn): Pull out coalesced touches. + + WALTTouch *touch = [[WALTTouch alloc] initWithEvent:event]; + [_touchEvents addObject:touch]; + [_logger appendFormat:@"TOUCH\t%.3f\t%.2f\t%.2f\n", + touch.kernelTime, touch.location.x, touch.location.y]; + [self updateCountDisplay]; +} + +- (void)updateCountDisplay { + NSString *counts = [NSString stringWithFormat:@"N ✛ %lu ⇄ %lu", + (unsigned long)_laserEvents.count, (unsigned long)_touchEvents.count]; + self.countLabel.text = counts; +} + +- (IBAction)start:(id)sender { + [self reset:sender]; + + self.goalpostView.hidden = NO; + self.statusLabel.text = @""; + + [UIView beginAnimations:@"Goalpost" context:NULL]; + [UIView setAnimationDuration:kGoalpostFrequency]; + [UIView setAnimationBeginsFromCurrentState:NO]; + [UIView setAnimationRepeatCount:FLT_MAX]; + [UIView setAnimationRepeatAutoreverses:YES]; + + self.goalpostView.transform = + CGAffineTransformMakeTranslation(0.0, -CGRectGetHeight(self.view.frame) + 300); + + [UIView commitAnimations]; + + _receiverComplete = dispatch_semaphore_create(0); + _triggerReceiver = [[NSThread alloc] initWithTarget:self + selector:@selector(receiveTriggers:) + object:nil]; + [_triggerReceiver start]; +} + +- (IBAction)reset:(id)sender { + [self stopReceiver]; + + self.goalpostView.transform = CGAffineTransformMakeTranslation(0.0, 0.0); + self.goalpostView.hidden = YES; + + _touchEvents = [[NSMutableArray<WALTTouch *> alloc] init]; + _laserEvents = [[NSMutableArray<WALTLaserEvent *> alloc] init]; + + [self updateCountDisplay]; + + NSError *error = nil; + if (![_client syncClocksWithError:&error]) { + UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"WALT Connection Error" error:error]; + [alert show]; + } + + [_logger appendString:@"RESET\n"]; +} + +- (void)receiveTriggers:(id)context { + // Turn on laser change notifications. + NSError *error = nil; + if (![_client sendCommand:WALTLaserOnCommand error:&error]) { + UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"WALT Connection Error" error:error]; + [alert show]; + dispatch_semaphore_signal(_receiverComplete); + return; + } + + NSData *response = [_client readResponseWithTimeout:kWALTReadTimeout]; + if (![_client checkResponse:response forCommand:WALTLaserOnCommand]) { + UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"WALT Response Error" + message:@"Failed to start laser probe." + delegate:nil + cancelButtonTitle:@"Dismiss" + otherButtonTitles:nil]; + [alert show]; + dispatch_semaphore_signal(_receiverComplete); + return; + } + + while (!NSThread.currentThread.isCancelled) { + WALTTrigger response = [_client readTriggerWithTimeout:kWALTReadTimeout]; + if (response.tag == kWALTLaserTag) { + WALTLaserEvent *event = [[WALTLaserEvent alloc] init]; + event.t = response.t; + event.value = response.value; + [_laserEvents addObject:event]; + [_logger appendFormat:@"LASER\t%.3f\t%d\n", event.t, event.value]; + } else if (response.tag != '\0') { // Don't fail for timeout errors. + UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"WALT Response Error" + message:@"Failed to read laser probe." + delegate:nil + cancelButtonTitle:@"Dismiss" + otherButtonTitles:nil]; + [alert show]; + } + } + + // Turn off laser change notifications. + [_client sendCommand:WALTLaserOffCommand error:nil]; + [_client readResponseWithTimeout:kWALTReadTimeout]; + + dispatch_semaphore_signal(_receiverComplete); +} + +- (void)stopReceiver { + // TODO(pquinn): This will deadlock if called in rapid succession -- there is a small delay + // between dispatch_semaphore_signal() and -[NSThread isExecuting] changing. + // Unfortunately, NSThread is not joinable... + if (_triggerReceiver.isExecuting) { + [_triggerReceiver cancel]; + dispatch_semaphore_wait(_receiverComplete, DISPATCH_TIME_FOREVER); + } +} + +- (IBAction)computeStatistics:(id)sender { + if (_touchEvents.count < kMinTouchEvents) { + self.statusLabel.text = + [NSString stringWithFormat:@"Too few touch events (%lu/%lu).", + (unsigned long)_touchEvents.count, (unsigned long)kMinTouchEvents]; + [self reset:sender]; + return; + } + + // Timestamps are reset to be relative to t0 to make the output easier to read. + const NSTimeInterval t0 = _touchEvents.firstObject.kernelTime; + const NSTimeInterval tF = _touchEvents.lastObject.kernelTime; + + std::vector<NSTimeInterval> ft(_touchEvents.count); + std::vector<CGFloat> fy(_touchEvents.count); + for (NSUInteger i = 0; i < _touchEvents.count; ++i) { + ft[i] = _touchEvents[i].kernelTime - t0; + fy[i] = _touchEvents[i].location.y; + } + + // Remove laser events that have a timestamp outside [t0, tF]. + [_laserEvents filterUsingPredicate:[NSPredicate predicateWithBlock: + ^BOOL(WALTLaserEvent *evaluatedObject, NSDictionary<NSString *, id> *bindings) { + return evaluatedObject.t >= t0 && evaluatedObject.t <= tF; + }]]; + + if (_laserEvents.count < kMinLaserEvents) { + self.statusLabel.text = + [NSString stringWithFormat:@"Too few laser events (%lu/%lu).", + (unsigned long)_laserEvents.count, (unsigned long)kMinLaserEvents]; + [self reset:sender]; + return; + } + + if (_laserEvents.firstObject.value != 0) { + self.statusLabel.text = @"First laser crossing was not into the beam."; + [self reset:sender]; + return; + } + + std::vector<NSTimeInterval> lt(_laserEvents.count); + std::vector<int> lv(_laserEvents.count); + for (NSUInteger i = 0; i < _laserEvents.count; ++i) { + lt[i] = _laserEvents[i].t - t0; + lv[i] = _laserEvents[i].value; + } + + // Calculate interpolated touch y positions at each laser event. + std::vector<CGFloat> ly = Interpolate(lt, ft, fy); + + // Labels for each laser event to denote those above/below the beam. + // The actual side is irrelevant, but events on the same side should have the same label. The + // vector will look like [0, 1, 1, 0, 0, 1, 1, 0, 0, ...]. + std::vector<int> sideLabels(lt.size()); + for (size_t i = 0; i < lt.size(); ++i) { + sideLabels[i] = ((i + 1) / 2) % 2; + } + + NSTimeInterval averageBestShift = 0; + for (int side = 0; side < 2; ++side) { + std::vector<NSTimeInterval> lts = Extract(sideLabels, lt, side); + NSTimeInterval bestShift = FindBestShift(lts, ft, fy); + averageBestShift += bestShift / 2; + } + + self.statusLabel.text = [NSString stringWithFormat:@"%.3f s", averageBestShift]; + + [self reset:sender]; +} +@end diff --git a/ios/WALT/Info.plist b/ios/WALT/Info.plist new file mode 100644 index 0000000..459291f --- /dev/null +++ b/ios/WALT/Info.plist @@ -0,0 +1,48 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> +<plist version="1.0"> +<dict> + <key>CFBundleDevelopmentRegion</key> + <string>en</string> + <key>CFBundleExecutable</key> + <string>$(EXECUTABLE_NAME)</string> + <key>CFBundleIdentifier</key> + <string>$(PRODUCT_BUNDLE_IDENTIFIER)</string> + <key>CFBundleInfoDictionaryVersion</key> + <string>6.0</string> + <key>CFBundleName</key> + <string>$(PRODUCT_NAME)</string> + <key>CFBundlePackageType</key> + <string>APPL</string> + <key>CFBundleShortVersionString</key> + <string>1.0</string> + <key>CFBundleSignature</key> + <string>????</string> + <key>CFBundleVersion</key> + <string>1</string> + <key>LSRequiresIPhoneOS</key> + <true/> + <key>UIBackgroundModes</key> + <array> + <string>audio</string> + </array> + <key>UILaunchStoryboardName</key> + <string>LaunchScreen</string> + <key>UIMainStoryboardFile</key> + <string>Main</string> + <key>UIRequiredDeviceCapabilities</key> + <array> + <string>armv7</string> + </array> + <key>UIRequiresFullScreen</key> + <true/> + <key>UISupportedInterfaceOrientations</key> + <array> + <string>UIInterfaceOrientationPortrait</string> + </array> + <key>UISupportedInterfaceOrientations~ipad</key> + <array> + <string>UIInterfaceOrientationPortrait</string> + </array> +</dict> +</plist> diff --git a/ios/WALT/MIDIClient.h b/ios/WALT/MIDIClient.h new file mode 100644 index 0000000..b4e10e3 --- /dev/null +++ b/ios/WALT/MIDIClient.h @@ -0,0 +1,73 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import <Foundation/Foundation.h> + +@class MIDIClient; +@class MIDIDestination; +@class MIDISource; + +extern NSString * const MIDIClientErrorDomain; + +/** + * Callbacks for MIDIClient changes. + * + * Note that these methods may not be called on the main thread. + */ +@protocol MIDIClientDelegate <NSObject> +/** Called when a MIDIClient receives data from a connected source. */ +- (void)MIDIClient:(MIDIClient *)client receivedData:(NSData *)message; + +@optional +/** Called when a MIDI I/O error occurs on the client's endpoints. */ +- (void)MIDIClient:(MIDIClient *)client receivedError:(NSError *)error; + +/** Called when a MIDI endpoint has been added to the system. */ +- (void)MIDIClientEndpointAdded:(MIDIClient *)client; + +/** Called when a MIDI endpoint has been removed to the system. */ +- (void)MIDIClientEndpointRemoved:(MIDIClient *)client; + +/** Called when the configuration of a MIDI object attached to the system has changed. */ +- (void)MIDIClientConfigurationChanged:(MIDIClient *)client; +@end + +/** A MIDI client that can read data from a MIDI source and write data to a MIDI destination. */ +@interface MIDIClient : NSObject +/** The source attached by -connectToSource:error:. */ +@property (readonly, nonatomic) MIDISource *source; + +/** The destination attached by -connectToDestination:error:. */ +@property (readonly, nonatomic) MIDIDestination *destination; + +@property (nonatomic, weak) id<MIDIClientDelegate> delegate; + +/** + * Creates a new MIDI client with a friendly name. + * + * If an error occurs, nil is returned and the error is populated with a description of the issue. + */ +- (instancetype)initWithName:(NSString *)name error:(NSError **)error; + +/** Attaches an input source to the client. */ +- (BOOL)connectToSource:(MIDISource *)source error:(NSError **)error; + +/** Attaches an output destination to the client. */ +- (BOOL)connectToDestination:(MIDIDestination *)destination error:(NSError **)error; + +/** Sends a MIDI packet of data to the client's output destination. */ +- (BOOL)sendData:(NSData *)data error:(NSError **)error; +@end diff --git a/ios/WALT/MIDIClient.m b/ios/WALT/MIDIClient.m new file mode 100644 index 0000000..1c51f5f --- /dev/null +++ b/ios/WALT/MIDIClient.m @@ -0,0 +1,282 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import "MIDIClient.h" + +#include <CoreMIDI/CoreMIDI.h> + +#import "MIDIEndpoint.h" +#import "MIDIMessage.h" + +NSString * const MIDIClientErrorDomain = @"MIDIClientErrorDomain"; + +@interface MIDIClient () +@property (readwrite, nonatomic) MIDISource *source; +@property (readwrite, nonatomic) MIDIDestination *destination; +// Used by midiRead() for SysEx messages spanning multiple packets. +@property (readwrite, nonatomic) NSMutableData *sysExBuffer; + +/** Returns whether the client's source or destination is attached to a particular device. */ +- (BOOL)attachedToDevice:(MIDIDeviceRef)device; +@end + +// Note: These functions (midiStateChanged and midiRead) are not called on the main thread! +static void midiStateChanged(const MIDINotification *message, void *context) { + MIDIClient *client = (__bridge MIDIClient *)context; + + switch (message->messageID) { + case kMIDIMsgObjectAdded: { + const MIDIObjectAddRemoveNotification *notification = + (const MIDIObjectAddRemoveNotification *)message; + + @autoreleasepool { + if ((notification->childType & (kMIDIObjectType_Source|kMIDIObjectType_Destination)) != 0 && + [client.delegate respondsToSelector:@selector(MIDIClientEndpointAdded:)]) { + [client.delegate MIDIClientEndpointAdded:client]; + } + } + break; + } + + case kMIDIMsgObjectRemoved: { + const MIDIObjectAddRemoveNotification *notification = + (const MIDIObjectAddRemoveNotification *)message; + + @autoreleasepool { + if ((notification->childType & (kMIDIObjectType_Source|kMIDIObjectType_Destination)) != 0 && + [client.delegate respondsToSelector:@selector(MIDIClientEndpointRemoved:)]) { + [client.delegate MIDIClientEndpointRemoved:client]; + } + } + break; + } + + case kMIDIMsgSetupChanged: + case kMIDIMsgPropertyChanged: + case kMIDIMsgSerialPortOwnerChanged: + case kMIDIMsgThruConnectionsChanged: { + @autoreleasepool { + if ([client.delegate respondsToSelector:@selector(MIDIClientConfigurationChanged:)]) { + [client.delegate MIDIClientConfigurationChanged:client]; + } + } + break; + } + + case kMIDIMsgIOError: { + const MIDIIOErrorNotification *notification = (const MIDIIOErrorNotification *)message; + + if ([client attachedToDevice:notification->driverDevice]) { + @autoreleasepool { + NSError *error = [NSError errorWithDomain:NSOSStatusErrorDomain + code:notification->errorCode + userInfo:nil]; + if ([client.delegate respondsToSelector:@selector(MIDIClient:receivedError:)]) { + [client.delegate MIDIClient:client receivedError:error]; + } + } + } + break; + } + + default: { + NSLog(@"Unhandled MIDI state change: %d", (int)message->messageID); + } + } +} + +static void midiRead(const MIDIPacketList *packets, void *portContext, void *sourceContext) { + MIDIClient *client = (__bridge MIDIClient *)portContext; + + // Read the data out of each packet and forward it to the client's delegate. + // Each MIDIPacket will contain either some MIDI commands, or the start/continuation of a SysEx + // command. The start of a command is detected with a byte greater than or equal to 0x80 (all data + // must be 7-bit friendly). The end of a SysEx command is marked with 0x7F. + + // TODO(pquinn): Should something be done with the timestamp data? + + UInt32 packetCount = packets->numPackets; + const MIDIPacket *packet = &packets->packet[0]; + @autoreleasepool { + while (packetCount--) { + if (packet->length == 0) { + continue; + } + + const Byte firstByte = packet->data[0]; + const Byte lastByte = packet->data[packet->length - 1]; + + if (firstByte >= 0x80 && firstByte != MIDIMessageSysEx && firstByte != MIDIMessageSysExEnd) { + // Packet describes non-SysEx MIDI messages. + NSMutableData *data = nil; + for (UInt16 i = 0; i < packet->length; ++i) { + // Packets can contain multiple MIDI messages. + if (packet->data[i] >= 0x80) { + if (data.length > 0) { // Tell the delegate about the last extracted command. + [client.delegate MIDIClient:client receivedData:data]; + } + data = [[NSMutableData alloc] init]; + } + [data appendBytes:&packet->data[i] length:1]; + } + + if (data.length > 0) { + [client.delegate MIDIClient:client receivedData:data]; + } + } + + if (firstByte == MIDIMessageSysEx) { + // The start of a SysEx message; collect data into sysExBuffer. + client.sysExBuffer = [[NSMutableData alloc] initWithBytes:packet->data + length:packet->length]; + } else if (firstByte < 0x80 || firstByte == MIDIMessageSysExEnd) { + // Continuation or end of a SysEx message. + [client.sysExBuffer appendBytes:packet->data length:packet->length]; + } + + if (lastByte == MIDIMessageSysExEnd) { + // End of a SysEx message. + [client.delegate MIDIClient:client receivedData:client.sysExBuffer]; + client.sysExBuffer = nil; + } + + packet = MIDIPacketNext(packet); + } + } +} + +@implementation MIDIClient { + NSString *_name; + MIDIClientRef _client; + MIDIPortRef _input; + MIDIPortRef _output; +} + +- (instancetype)initWithName:(NSString *)name error:(NSError **)error { + if ((self = [super init])) { + _name = name; // Hold onto the name because MIDIClientCreate() doesn't retain it. + OSStatus result = MIDIClientCreate((__bridge CFStringRef)name, + midiStateChanged, + (__bridge void *)self, + &_client); + if (result != noErr) { + if (error) { + *error = [NSError errorWithDomain:NSOSStatusErrorDomain code:result userInfo:nil]; + } + self = nil; + } + } + return self; +} + +- (void)dealloc { + MIDIClientDispose(_client); // Automatically disposes of the ports too. +} + +- (BOOL)connectToSource:(MIDISource *)source error:(NSError **)error { + OSStatus result = noErr; + if (!_input) { // Lazily create the input port. + result = MIDIInputPortCreate(_client, + (__bridge CFStringRef)_name, + midiRead, + (__bridge void *)self, + &_input); + if (result != noErr) { + if (error) { + *error = [NSError errorWithDomain:NSOSStatusErrorDomain code:result userInfo:nil]; + } + return NO; + } + } + + // Connect the source to the port. + result = MIDIPortConnectSource(_input, source.endpoint, (__bridge void *)self); + if (result != noErr) { + if (error) { + *error = [NSError errorWithDomain:NSOSStatusErrorDomain code:result userInfo:nil]; + } + return NO; + } + + self.source = source; + return YES; +} + +- (BOOL)connectToDestination:(MIDIDestination *)destination error:(NSError **)error { + if (!_output) { // Lazily create the output port. + OSStatus result = MIDIOutputPortCreate(_client, + (__bridge CFStringRef)_name, + &_output); + if (result != noErr) { + if (error) { + *error = [NSError errorWithDomain:NSOSStatusErrorDomain code:result userInfo:nil]; + } + return NO; + } + } + + self.destination = destination; + return YES; +} + +- (BOOL)sendData:(NSData *)data error:(NSError **)error { + if (data.length > sizeof(((MIDIPacket *)0)->data)) { + // TODO(pquinn): Dynamically allocate a buffer. + if (error) { + *error = [NSError errorWithDomain:MIDIClientErrorDomain + code:0 + userInfo:@{NSLocalizedDescriptionKey: + @"Too much data for a basic MIDIPacket."}]; + } + return NO; + } + + MIDIPacketList packetList; + MIDIPacket *packet = MIDIPacketListInit(&packetList); + packet = MIDIPacketListAdd(&packetList, sizeof(packetList), packet, 0, data.length, data.bytes); + if (!packet) { + if (error) { + *error = [NSError errorWithDomain:MIDIClientErrorDomain + code:0 + userInfo:@{NSLocalizedDescriptionKey: + @"Packet too large for buffer."}]; + } + return NO; + } + + OSStatus result = MIDISend(_output, self.destination.endpoint, &packetList); + if (result != noErr) { + if (error) { + *error = [NSError errorWithDomain:NSOSStatusErrorDomain code:result userInfo:nil]; + } + return NO; + } + return YES; +} + +- (BOOL)attachedToDevice:(MIDIDeviceRef)device { + MIDIDeviceRef sourceDevice = 0, destinationDevice = 0; + MIDIEntityGetDevice(self.source.endpoint, &sourceDevice); + MIDIEntityGetDevice(self.destination.endpoint, &destinationDevice); + + SInt32 sourceID = 0, destinationID = 0, deviceID = 0; + MIDIObjectGetIntegerProperty(sourceDevice, kMIDIPropertyUniqueID, &sourceID); + MIDIObjectGetIntegerProperty(destinationDevice, kMIDIPropertyUniqueID, &destinationID); + MIDIObjectGetIntegerProperty(device, kMIDIPropertyUniqueID, &deviceID); + + return (deviceID == sourceID || deviceID == destinationID); +} +@end diff --git a/ios/WALT/MIDIEndpoint.h b/ios/WALT/MIDIEndpoint.h new file mode 100644 index 0000000..ec43d1c --- /dev/null +++ b/ios/WALT/MIDIEndpoint.h @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include <CoreMIDI/CoreMIDI.h> +#import <Foundation/Foundation.h> + +/** An abstract MIDI endpoint (input source or output destination). */ +@interface MIDIEndpoint : NSObject +@property (readonly, nonatomic) MIDIEndpointRef endpoint; +@property (readonly, nonatomic) NSString *name; +@property (readonly, nonatomic, getter=isOnline) BOOL online; +@end + +@interface MIDIDestination : MIDIEndpoint +/** Returns an NSArray of all MIDI output destinations currently available on the system. */ ++ (NSArray<MIDIDestination *> *)allDestinations; +@end + +@interface MIDISource : MIDIEndpoint +/** Returns an NSArray of all MIDI input sources currently available on the system. */ ++ (NSArray<MIDISource *> *)allSources; +@end diff --git a/ios/WALT/MIDIEndpoint.m b/ios/WALT/MIDIEndpoint.m new file mode 100644 index 0000000..9fe7163 --- /dev/null +++ b/ios/WALT/MIDIEndpoint.m @@ -0,0 +1,76 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import "MIDIEndpoint.h" + +@interface MIDIEndpoint () +@property (readwrite, nonatomic, assign) MIDIEndpointRef endpoint; +@end + +@implementation MIDIEndpoint +- (NSString *)name { + CFStringRef result = CFSTR(""); + MIDIObjectGetStringProperty(self.endpoint, kMIDIPropertyDisplayName, &result); + return CFBridgingRelease(result); +} + +- (BOOL)isOnline { + SInt32 result = 1; + MIDIObjectGetIntegerProperty(self.endpoint, kMIDIPropertyOffline, &result); + return (result == 0 ? YES : NO); +} +@end + +@implementation MIDIDestination ++ (NSArray *)allDestinations { + NSMutableArray<MIDIDestination *> *destinations = + [[NSMutableArray<MIDIDestination *> alloc] init]; + + ItemCount destinationCount = MIDIGetNumberOfDestinations(); + for (ItemCount i = 0; i < destinationCount; ++i) { + MIDIEndpointRef endpoint = MIDIGetDestination(i); + if (endpoint) { + MIDIDestination *destination = [[MIDIDestination alloc] init]; + destination.endpoint = endpoint; + [destinations addObject:destination]; + } else { + NSLog(@"Error getting destination at index %lud, skipping.", i); + } + } + + return destinations; +} +@end + +@implementation MIDISource ++ (NSArray *)allSources { + NSMutableArray<MIDISource *> *sources = [[NSMutableArray<MIDISource *> alloc] init]; + + ItemCount sourceCount = MIDIGetNumberOfSources(); + for (ItemCount i = 0; i < sourceCount; ++i) { + MIDIEndpointRef endpoint = MIDIGetSource(i); + if (endpoint) { + MIDISource *source = [[MIDISource alloc] init]; + source.endpoint = endpoint; + [sources addObject:source]; + } else { + NSLog(@"Error getting source at index %lud, skipping.", i); + } + } + + return sources; +} +@end diff --git a/ios/WALT/MIDIMessage.h b/ios/WALT/MIDIMessage.h new file mode 100644 index 0000000..2577ee6 --- /dev/null +++ b/ios/WALT/MIDIMessage.h @@ -0,0 +1,85 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import <Foundation/Foundation.h> + +/** + * A MIDI channel number. + * + * Note that the first channel is '1'. + */ +typedef uint8_t MIDIChannel; +typedef uint8_t MIDIByte; + +typedef NS_ENUM(MIDIByte, MIDIMessageType) { + // Channel messages + MIDIMessageNoteOff = 0x08, + MIDIMessageNoteOn = 0x09, + MIDIMessageKeyPressure = 0x0A, + MIDIMessageControlChange = 0x0B, + MIDIMessageProgramChange = 0x0C, + MIDIMessageChannelPressure = 0x0D, + MIDIMessagePitchBend = 0x0E, + + // System messages + MIDIMessageSysEx = 0xF0, + MIDIMessageQuarterFrame = 0xF1, + MIDIMessageSongPosition = 0xF2, + MIDIMessageSongSelect = 0xF3, + MIDIMessageTuneRequest = 0xF6, + MIDIMessageSysExEnd = 0xF7, + MIDIMessageTimingClock = 0xF8, + MIDIMessageStart = 0xFA, + MIDIMessageContinue = 0xFB, + MIDIMessageStop = 0xFC, + MIDIMessageActiveSensing = 0xFE, + MIDIMessageReset = 0xFF, +}; + +extern const MIDIChannel kMIDINoChannel; + +#pragma mark Message Parsing + +/** Returns the MIDIMessageType for a given status byte. */ +MIDIMessageType MIDIMessageTypeFromStatus(MIDIByte status); + +/** + * Returns the MIDIChannel for a given status byte, or kMIDINoChannel if the status byte does not + * describe a channel message. + */ +MIDIChannel MIDIChannelFromStatus(MIDIByte status); + +/** + * Returns the body portion from a complete MIDI message (i.e., without leading or trailing data). + */ +NSData *MIDIMessageBody(NSData *message); + +#pragma mark Message Building + +/** Returns the MIDI status byte for a message type sent to a particular channel. */ +MIDIByte MIDIStatusByte(MIDIMessageType type, MIDIChannel channel); + +/** Creates a complete MIDI message packet for a given message type, channel, and its body. */ +NSData *MIDIMessageCreate(MIDIMessageType type, MIDIChannel channel, NSData *body); + +/** Creates a complete MIDI message packet for a simple message containing one data byte. */ +NSData *MIDIMessageCreateSimple1(MIDIMessageType type, MIDIChannel channel, MIDIByte first); + +/** Creates a complete MIDI message packet for a simple message containing two data bytes. */ +NSData *MIDIMessageCreateSimple2(MIDIMessageType type, + MIDIChannel channel, + MIDIByte first, + MIDIByte second); diff --git a/ios/WALT/MIDIMessage.m b/ios/WALT/MIDIMessage.m new file mode 100644 index 0000000..75c6a3b --- /dev/null +++ b/ios/WALT/MIDIMessage.m @@ -0,0 +1,103 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import "MIDIMessage.h" + +const uint8_t kMIDINoChannel = -1; + +MIDIMessageType MIDIMessageTypeFromStatus(MIDIByte status) { + if (status < MIDIMessageSysEx) { + return (status & 0xF0) >> 4; + } else { + return status; + } +} + +MIDIChannel MIDIChannelFromStatus(MIDIByte status) { + if (status < MIDIMessageSysEx) { + return (status & 0x0F) + 1; + } else { + return -1; + } +} + +NSData *MIDIMessageBody(NSData *message) { + if (message.length == 0) { + return nil; + } + + const MIDIByte *bytes = (const MIDIByte *)message.bytes; + + // Slice off any header/trailer bytes. + if (MIDIMessageTypeFromStatus(bytes[0]) == MIDIMessageSysEx) { + NSCAssert(bytes[message.length - 1] == MIDIMessageSysExEnd, @"SysEx message without trailer."); + return [message subdataWithRange:NSMakeRange(1, message.length - 2)]; + } else { + return [message subdataWithRange:NSMakeRange(1, message.length - 1)]; + } +} + +MIDIByte MIDIStatusByte(MIDIMessageType type, MIDIChannel channel) { + if (type >= MIDIMessageSysEx) { + return type; + } else { + return (type << 4) | (channel - 1); + } +} + +NSData *MIDIChannelMessageCreate(MIDIMessageType type, MIDIChannel channel, NSData *body) { + NSMutableData *message = + [[NSMutableData alloc] initWithCapacity:body.length + 2]; // +2 for status and SysEx trailer + + const MIDIByte status = MIDIStatusByte(type, channel); + [message appendBytes:&status length:1]; + [message appendData:body]; + + if (type == MIDIMessageSysEx) { + const MIDIByte trailer = MIDIMessageSysEx; + [message appendBytes:&trailer length:1]; + } + + return message; +} + +NSData *MIDIMessageCreateSimple1(MIDIMessageType type, MIDIChannel channel, MIDIByte first) { + NSCAssert(type != MIDIMessageSysEx, @"MIDIMessageCreateSimple1 cannot create SysEx messages."); + + NSMutableData *message = [[NSMutableData alloc] initWithCapacity:2]; // Status + Data + + const MIDIByte status = MIDIStatusByte(type, channel); + [message appendBytes:&status length:1]; + [message appendBytes:&first length:1]; + + return message; +} + +NSData *MIDIMessageCreateSimple2(MIDIMessageType type, + MIDIChannel channel, + MIDIByte first, + MIDIByte second) { + NSCAssert(type != MIDIMessageSysEx, @"MIDIMessageCreateSimple2 cannot create SysEx messages."); + + NSMutableData *message = [[NSMutableData alloc] initWithCapacity:3]; // Status + Data + Data + + const MIDIByte status = MIDIStatusByte(type, channel); + [message appendBytes:&status length:1]; + [message appendBytes:&first length:1]; + [message appendBytes:&second length:1]; + + return message; +} diff --git a/ios/WALT/MenuController.h b/ios/WALT/MenuController.h new file mode 100644 index 0000000..559392f --- /dev/null +++ b/ios/WALT/MenuController.h @@ -0,0 +1,24 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import <UIKit/UIKit.h> + +@interface MenuController : UITableViewController +@property (nonatomic, assign) IBOutlet UITableViewCell *syncCell; + +- (IBAction)shareLog:(id)sender; +@end + diff --git a/ios/WALT/MenuController.m b/ios/WALT/MenuController.m new file mode 100644 index 0000000..98b84e9 --- /dev/null +++ b/ios/WALT/MenuController.m @@ -0,0 +1,154 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import "MenuController.h" + +#import "UIAlertView+Extensions.h" +#import "WALTLogger.h" +#import "WALTAppDelegate.h" +#import "WALTClient.h" + +@implementation MenuController { + WALTClient *_client; + UIActivityIndicatorView *_spinner; +} + +- (void)viewDidLoad { + [super viewDidLoad]; + + _spinner = + [[UIActivityIndicatorView alloc] + initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleGray]; + + _client = ((WALTAppDelegate *)[UIApplication sharedApplication].delegate).client; +} + +- (void)viewDidAppear:(BOOL)animated { + [super viewDidAppear:animated]; + + [_client addObserver:self + forKeyPath:@"connected" + options:NSKeyValueObservingOptionInitial + context:NULL]; +} + +- (void)dealloc { + [_client removeObserver:self forKeyPath:@"connected"]; +} + +- (void)observeValueForKeyPath:(NSString *)keyPath + ofObject:(id)object + change:(NSDictionary *)change + context:(void *)context { + if (_client.isConnected) { + [_spinner stopAnimating]; + self.syncCell.accessoryView = nil; // Display a checkmark. + [[WALTLogger sessionLogger] appendString:@"WALT\tCONNECTED\n"]; + [[WALTLogger sessionLogger] appendFormat:@"SYNC\t%lld\t%lld\n", + _client.minError, _client.maxError]; + } else { + self.syncCell.accessoryView = _spinner; + [_spinner startAnimating]; + [[WALTLogger sessionLogger] appendString:@"WALT\tDISCONNECTED\n"]; + + // Return to this view controller. + UINavigationController *navigationController = self.navigationController; + if (navigationController.visibleViewController != self) { + [navigationController popToRootViewControllerAnimated:YES]; + + UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"WALT Connection Error" + message:@"WALT disconnected." + delegate:nil + cancelButtonTitle:@"Dismiss" + otherButtonTitles:nil]; + [alert show]; + } + } + + [self.tableView reloadData]; // Update accessory types. +} + +- (void)shareLog:(id)sender { + NSFileManager *fileManager = [NSFileManager defaultManager]; + NSArray *urls = [fileManager URLsForDirectory:NSDocumentDirectory inDomains:NSUserDomainMask]; + + if (urls.count > 0) { + NSDateFormatter *formatter = [[NSDateFormatter alloc] init]; + formatter.dateFormat = @"yyyy-MM-dd'T'HH-mm-ss"; + + // Save the log to a file (which also allows it to be retrieved in iTunes/Xcode). + NSString *logName = [NSString stringWithFormat:@"walt_%@.log", + [formatter stringFromDate:[NSDate date]]]; + NSURL *logURL = [urls.firstObject URLByAppendingPathComponent:logName]; + + WALTLogger *logger = [WALTLogger sessionLogger]; + NSError *error = nil; + if ([logger writeToURL:logURL error:&error]) { + // Open a share sheet for the URL. + UIActivityViewController *activityController = + [[UIActivityViewController alloc] initWithActivityItems:@[logURL] + applicationActivities:nil]; + [self presentViewController:activityController animated:YES completion:NULL]; + } else { + UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"Log Write Error" + error:error]; + [alert show]; + } + } else { + UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"Log Write Error" + message:@"Could not locate document directory." + delegate:nil + cancelButtonTitle:@"Dismiss" + otherButtonTitles:nil]; + [alert show]; + } +} + +#pragma mark - UITableView Delegate + +- (void)tableView:(UITableView *)tableView + willDisplayCell:(UITableViewCell *)cell +forRowAtIndexPath:(NSIndexPath *)indexPath { + if (indexPath.section == 1) { + // Show/hide the disclosure indicator on the "Measure Latency" cells. + cell.accessoryType = (_client.isConnected ? + UITableViewCellAccessoryDisclosureIndicator : + UITableViewCellAccessoryNone); + } +} + +- (NSIndexPath *)tableView:(UITableView *)tableView + willSelectRowAtIndexPath:(NSIndexPath *)indexPath { + if (indexPath.section == 0 && indexPath.row == 0) { + // "Clock Sync" + NSError *error = nil; + if (![_client checkConnectionWithError:&error] || + ![_client syncClocksWithError:&error]) { + UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"WALT Connection Error" + error:error]; + [alert show]; + } + [[WALTLogger sessionLogger] appendFormat:@"SYNC\t%lld\t%lld\n", + _client.minError, _client.maxError]; + return nil; + } else if (indexPath.section == 1 && !_client.isConnected) { + // "Measure Latency" + return nil; + } + + return indexPath; +} +@end diff --git a/ios/WALT/NSArray+Extensions.h b/ios/WALT/NSArray+Extensions.h new file mode 100644 index 0000000..cb8a4d1 --- /dev/null +++ b/ios/WALT/NSArray+Extensions.h @@ -0,0 +1,21 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import <Foundation/Foundation.h> + +@interface NSArray (WALTExtensions) +- (NSNumber *)medianValue; +@end diff --git a/ios/WALT/NSArray+Extensions.m b/ios/WALT/NSArray+Extensions.m new file mode 100644 index 0000000..2217bc4 --- /dev/null +++ b/ios/WALT/NSArray+Extensions.m @@ -0,0 +1,34 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import "NSArray+Extensions.h" + +@implementation NSArray (WALTExtensions) +- (NSNumber *)medianValue { + NSArray<NSNumber *> *sorted = [self sortedArrayUsingSelector:@selector(compare:)]; + const NSUInteger count = sorted.count; + if (count == 0) { + return nil; + } + + if (count % 2) { + return [sorted objectAtIndex:count / 2]; + } else { + return [NSNumber numberWithDouble:0.5 * ([sorted objectAtIndex:count / 2].doubleValue + + [sorted objectAtIndex:count / 2 - 1].doubleValue)]; + } +} +@end diff --git a/ios/WALT/ScreenResponseController.h b/ios/WALT/ScreenResponseController.h new file mode 100644 index 0000000..33d36a1 --- /dev/null +++ b/ios/WALT/ScreenResponseController.h @@ -0,0 +1,26 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import <UIKit/UIKit.h> + +@interface ScreenResponseController : UIViewController +@property (nonatomic, assign) IBOutlet UIView *flasherView; +@property (nonatomic, assign) IBOutlet UILabel *responseLabel; + +- (IBAction)start:(id)sender; +- (IBAction)reset:(id)sender; +- (IBAction)computeStatistics:(id)sender; +@end diff --git a/ios/WALT/ScreenResponseController.m b/ios/WALT/ScreenResponseController.m new file mode 100644 index 0000000..c88236c --- /dev/null +++ b/ios/WALT/ScreenResponseController.m @@ -0,0 +1,217 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import "ScreenResponseController.h" + +#include <stdatomic.h> + +#import "NSArray+Extensions.h" +#import "UIAlertView+Extensions.h" +#import "WALTAppDelegate.h" +#import "WALTClient.h" +#import "WALTLogger.h" + +static const NSUInteger kMaxFlashes = 20; // TODO(pquinn): Make this user-configurable. +static const NSTimeInterval kFlashingInterval = 0.1; +static const char kWALTScreenTag = 'S'; + +@interface ScreenResponseController () +- (void)setFlashTimer; +- (void)flash:(NSTimer *)timer; +@end + +@implementation ScreenResponseController { + WALTClient *_client; + WALTLogger *_logger; + + NSTimer *_flashTimer; + NSOperationQueue *_readOperations; + + // Statistics + NSUInteger _initiatedFlashes; + NSUInteger _detectedFlashes; + + _Atomic NSTimeInterval _lastFlashTime; + NSMutableArray<NSNumber *> *_deltas; +} + +- (void)dealloc { + [_readOperations cancelAllOperations]; + [_flashTimer invalidate]; +} + +- (void)viewDidLoad { + [super viewDidLoad]; + + _client = ((WALTAppDelegate *)[UIApplication sharedApplication].delegate).client; + _logger = [WALTLogger sessionLogger]; +} + +- (void)viewWillAppear:(BOOL)animated { + [super viewWillAppear:animated]; + + [_logger appendString:@"SCREENRESPONSE\n"]; + [self reset:nil]; +} + +- (IBAction)start:(id)sender { + [self reset:nil]; + + // Clear the screen trigger on the WALT. + NSError *error = nil; + if (![_client sendCommand:WALTSendLastScreenCommand error:&error]) { + UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"WALT Connection Error" error:error]; + [alert show]; + return; + } + + WALTTrigger trigger = [_client readTriggerWithTimeout:kWALTReadTimeout]; + if (trigger.tag != kWALTScreenTag) { + UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"WALT Response Error" + message:@"Failed to read last screen trigger." + delegate:nil + cancelButtonTitle:@"Dismiss" + otherButtonTitles:nil]; + [alert show]; + return; + } + + // Create a queue for work blocks to read WALT trigger responses. + _readOperations = [[NSOperationQueue alloc] init]; + _readOperations.maxConcurrentOperationCount = 1; + + // Start the flash timer and spawn a thread to check for responses. + [self setFlashTimer]; +} + +- (void)setFlashTimer { + _flashTimer = [NSTimer scheduledTimerWithTimeInterval:kFlashingInterval + target:self + selector:@selector(flash:) + userInfo:nil + repeats:NO]; +} + +- (IBAction)computeStatistics:(id)sender { + self.flasherView.hidden = YES; + self.responseLabel.hidden = NO; + + NSMutableString *results = [[NSMutableString alloc] init]; + for (NSNumber *delta in _deltas) { + [results appendFormat:@"%.3f s\n", delta.doubleValue]; + } + + [results appendFormat:@"Median: %.3f s\n", [_deltas medianValue].doubleValue]; + self.responseLabel.text = results; +} + +- (IBAction)reset:(id)sender { + _initiatedFlashes = 0; + _detectedFlashes = 0; + _deltas = [[NSMutableArray<NSNumber *> alloc] init]; + + [_readOperations cancelAllOperations]; + [_flashTimer invalidate]; + + self.flasherView.hidden = NO; + self.flasherView.backgroundColor = [UIColor whiteColor]; + self.responseLabel.hidden = YES; + + NSError *error = nil; + if (![_client syncClocksWithError:&error]) { + UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"WALT Connection Error" error:error]; + [alert show]; + } + + [_logger appendString:@"RESET\n"]; +} + +- (void)flash:(NSTimer *)timer { + if (_initiatedFlashes == 0) { + // First flash. + // Turn on brightness change notifications. + NSError *error = nil; + if (![_client sendCommand:WALTScreenOnCommand error:&error]) { + UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"WALT Connection Error" error:error]; + [alert show]; + return; + } + + NSData *response = [_client readResponseWithTimeout:kWALTReadTimeout]; + if (![_client checkResponse:response forCommand:WALTScreenOnCommand]) { + UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"WALT Response Error" + message:@"Failed to start screen probe." + delegate:nil + cancelButtonTitle:@"Dismiss" + otherButtonTitles:nil]; + [alert show]; + return; + } + } + + if (_initiatedFlashes != kMaxFlashes) { + // Swap the background colour and record the time. + self.flasherView.backgroundColor = + ([self.flasherView.backgroundColor isEqual:[UIColor blackColor]] ? + [UIColor whiteColor] : + [UIColor blackColor]); + atomic_store(&_lastFlashTime, _client.currentTime); + ++_initiatedFlashes; + + // Queue an operation to read the trigger. + [_readOperations addOperationWithBlock:^{ + // NB: The timeout here should be much greater than the expected screen response time. + WALTTrigger response = [_client readTriggerWithTimeout:kWALTReadTimeout]; + if (response.tag == kWALTScreenTag) { + ++_detectedFlashes; + + // Record the delta between the trigger and the flash time. + NSTimeInterval lastFlash = atomic_load(&_lastFlashTime); + NSTimeInterval delta = response.t - lastFlash; + if (delta > 0) { // Sanity check + [_deltas addObject:[NSNumber numberWithDouble:delta]]; + [_logger appendFormat:@"O\t%f\n", delta]; + } else { + [_logger appendFormat:@"X\tbogus delta\t%f\t%f\n", lastFlash, response.t]; + } + + // Queue up another flash. + [self performSelectorOnMainThread:@selector(setFlashTimer) + withObject:nil + waitUntilDone:NO]; + } else { + UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"WALT Response Error" + message:@"Failed to read screen probe." + delegate:nil + cancelButtonTitle:@"Dismiss" + otherButtonTitles:nil]; + [alert show]; + } + }]; + } + + if (_initiatedFlashes == kMaxFlashes) { + // Queue an operation (after the read trigger above) to turn off brightness notifications. + [_readOperations addOperationWithBlock:^{ + [_client sendCommand:WALTScreenOffCommand error:nil]; + [_client readResponseWithTimeout:kWALTReadTimeout]; + [self performSelectorOnMainThread:@selector(computeStatistics:) + withObject:nil + waitUntilDone:NO]; + }]; + } +} +@end diff --git a/ios/WALT/SettingsController.h b/ios/WALT/SettingsController.h new file mode 100644 index 0000000..85f3e79 --- /dev/null +++ b/ios/WALT/SettingsController.h @@ -0,0 +1,22 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import <UIKit/UIKit.h> + +@interface SettingsController : UITableViewController +- (IBAction)ping:(id)sender; +- (IBAction)checkDrift:(id)sender; +@end diff --git a/ios/WALT/SettingsController.m b/ios/WALT/SettingsController.m new file mode 100644 index 0000000..2ed5afc --- /dev/null +++ b/ios/WALT/SettingsController.m @@ -0,0 +1,88 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import "SettingsController.h" + +#import "UIAlertView+Extensions.h" +#import "WALTAppDelegate.h" +#import "WALTClient.h" + +@implementation SettingsController { + WALTClient *_client; + NSString *_status; +} + +- (void)viewDidLoad { + [super viewDidLoad]; + + _client = ((WALTAppDelegate *)[UIApplication sharedApplication].delegate).client; +} + +- (void)viewWillAppear:(BOOL)animated { + [super viewWillAppear:animated]; + + _status = [NSString string]; + [self.tableView reloadData]; +} + +- (IBAction)ping:(id)sender { + NSTimeInterval start = _client.currentTime; + NSError *error = nil; + if (![_client sendCommand:WALTPingCommand error:&error]) { + UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"WALT Connection Error" error:error]; + [alert show]; + } else { + NSData *response = [_client readResponseWithTimeout:kWALTReadTimeout]; + if (!response) { + _status = @"Timed out waiting for ping response."; + } else { + NSTimeInterval delta = _client.currentTime - start; + _status = [NSString stringWithFormat:@"Ping response in %.2f ms.", delta * 1000]; + } + } + [self.tableView reloadData]; +} + +- (IBAction)checkDrift:(id)sender { + NSError *error = nil; + if (![_client updateSyncBoundsWithError:&error]) { + UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"WALT Connection Error" error:error]; + [alert show]; + } else { + _status = [NSString stringWithFormat:@"Remote clock delayed between %lld and %lld µs.", + _client.minError, _client.maxError]; + } + [self.tableView reloadData]; +} + +- (NSIndexPath *)tableView:(UITableView *)tableView + willSelectRowAtIndexPath:(NSIndexPath *)indexPath { + if (indexPath.section == 0 && indexPath.row == 0) { + // "Ping" + [self ping:tableView]; + return nil; + } else if (indexPath.section == 0 && indexPath.row == 1) { + // "Check Drift" + [self checkDrift:tableView]; + return nil; + } + return indexPath; +} + +- (NSString *)tableView:(UITableView *)tableView titleForFooterInSection:(NSInteger)section { + return (section == 0 ? _status : nil); +} +@end diff --git a/ios/WALT/TapLatencyController.h b/ios/WALT/TapLatencyController.h new file mode 100644 index 0000000..dea25a5 --- /dev/null +++ b/ios/WALT/TapLatencyController.h @@ -0,0 +1,25 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import <UIKit/UIKit.h> + +@interface TapLatencyController : UIViewController +@property(nonatomic, assign) IBOutlet UILabel* countLabel; +@property(nonatomic, assign) IBOutlet UITextView* logView; + +- (IBAction)reset:(id)sender; +- (IBAction)computeStatistics:(id)sender; +@end diff --git a/ios/WALT/TapLatencyController.m b/ios/WALT/TapLatencyController.m new file mode 100644 index 0000000..f4590b3 --- /dev/null +++ b/ios/WALT/TapLatencyController.m @@ -0,0 +1,194 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import "TapLatencyController.h" + +#import "NSArray+Extensions.h" +#import "UIAlertView+Extensions.h" +#import "WALTAppDelegate.h" +#import "WALTClient.h" +#import "WALTLogger.h" +#import "WALTTouch.h" + +@interface TapLatencyController () +- (void)updateCountDisplay; +- (void)processEvent:(UIEvent *)event; +- (void)appendToLogView:(NSString *)string; +- (void)computeStatisticsForPhase:(UITouchPhase)phase; +@end + +@implementation TapLatencyController { + WALTClient *_client; + WALTLogger *_logger; + + // Statistics + unsigned int _downCount; + unsigned int _downCountRecorded; + unsigned int _upCount; + unsigned int _upCountRecorded; + + NSMutableArray<WALTTouch *> *_touches; +} + +- (void)viewDidLoad { + [super viewDidLoad]; + + self.logView.selectable = YES; + self.logView.text = [NSString string]; + self.logView.selectable = NO; + + _logger = [WALTLogger sessionLogger]; + _client = ((WALTAppDelegate *)[UIApplication sharedApplication].delegate).client; +} + +- (void)viewWillAppear:(BOOL)animated { + [super viewWillAppear:animated]; + + [_logger appendString:@"TAPLATENCY\n"]; + [self reset:nil]; +} + +- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event { + [self processEvent:event]; + [self updateCountDisplay]; +} + +- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event { + [self processEvent:event]; + [self updateCountDisplay]; +} + +- (void)updateCountDisplay { + NSString *counts = [NSString stringWithFormat:@"N ↓%u (%u) ↑%u (%u)", + _downCountRecorded, _downCount, _upCountRecorded, _upCount]; + self.countLabel.text = counts; +} + +- (void)processEvent:(UIEvent *)event { + // TODO(pquinn): Pick first/last coalesced touch? + + NSTimeInterval kernelTime = event.timestamp; + NSTimeInterval callbackTime = _client.currentTime; + + NSError *error = nil; + NSTimeInterval physicalTime = [_client lastShockTimeWithError:&error]; + if (physicalTime == -1) { + UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"WALT Connection Error" error:error]; + [alert show]; + return; + } + + WALTTouch *touch = [[WALTTouch alloc] initWithEvent:event]; + touch.callbackTime = callbackTime; + touch.physicalTime = physicalTime; + + NSString *actionString = nil; + if (touch.phase == UITouchPhaseBegan) { + _downCount += 1; + actionString = @"ACTION_DOWN"; + } else { + _upCount += 1; + actionString = @"ACTION_UP"; + } + + if (physicalTime == 0) { + [_logger appendFormat:@"%@\tX\tno shock\n", actionString]; + [self appendToLogView:[NSString stringWithFormat:@"%@: No shock detected\n", actionString]]; + return; + } + + NSTimeInterval physicalToKernel = kernelTime - physicalTime; + NSTimeInterval kernelToCallback = callbackTime - kernelTime; + + if (physicalToKernel < 0 || physicalToKernel > 0.2) { + [_logger appendFormat:@"%@\tX\tbogus kernelTime\t%f\n", actionString, physicalToKernel]; + [self appendToLogView: + [NSString stringWithFormat:@"%@: Bogus P → K: %.3f s\n", actionString, physicalToKernel]]; + return; + } + + [_logger appendFormat:@"%@\tO\t%f\t%f\t%f\n", + actionString, _client.baseTime, physicalToKernel, kernelToCallback]; + + [self appendToLogView: + [NSString stringWithFormat:@"%@: P → K: %.3f s; K → C: %.3f s\n", + actionString, physicalToKernel, kernelToCallback]]; + + [_touches addObject:touch]; + if (touch.phase == UITouchPhaseBegan) { + _downCountRecorded += 1; + } else { + _upCountRecorded += 1; + } +} + +- (IBAction)reset:(id)sender { + _downCount = 0; + _downCountRecorded = 0; + _upCount = 0; + _upCountRecorded = 0; + [self updateCountDisplay]; + + _touches = [[NSMutableArray<WALTTouch *> alloc] init]; + + NSError *error = nil; + if (![_client syncClocksWithError:&error]) { + UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"WALT Connection Error" error:error]; + [alert show]; + } + + [_logger appendString:@"RESET\n"]; + [self appendToLogView:@"===========================================\n"]; +} + +- (IBAction)computeStatistics:(id)sender { + [self appendToLogView:@"-------------------------------------------\n"]; + [self appendToLogView:@"Medians:\n"]; + [self computeStatisticsForPhase:UITouchPhaseBegan]; + [self computeStatisticsForPhase:UITouchPhaseEnded]; + + [self reset:sender]; +} + +- (void)computeStatisticsForPhase:(UITouchPhase)phase { + NSMutableArray<NSNumber *> *p2k = [[NSMutableArray<NSNumber *> alloc] init]; + NSMutableArray<NSNumber *> *k2c = [[NSMutableArray<NSNumber *> alloc] init]; + + for (WALTTouch *touch in _touches) { + if (touch.phase != phase) { + continue; + } + + [p2k addObject:[NSNumber numberWithDouble:touch.kernelTime - touch.physicalTime]]; + [k2c addObject:[NSNumber numberWithDouble:touch.callbackTime - touch.kernelTime]]; + } + + NSNumber *p2kMedian = [p2k medianValue]; + NSNumber *k2cMedian = [k2c medianValue]; + + NSString *actionString = (phase == UITouchPhaseBegan ? @"ACTION_DOWN" : @"ACTION_UP"); + [self appendToLogView: + [NSString stringWithFormat:@"%@: P → K: %.3f s; K → C: %.3f s\n", + actionString, p2kMedian.doubleValue, k2cMedian.doubleValue]]; +} + +- (void)appendToLogView:(NSString*)string { + self.logView.selectable = YES; + self.logView.text = [self.logView.text stringByAppendingString:string]; + [self.logView scrollRangeToVisible:NSMakeRange(self.logView.text.length - 2, 1)]; + self.logView.selectable = NO; +} +@end diff --git a/ios/WALT/UIAlertView+Extensions.h b/ios/WALT/UIAlertView+Extensions.h new file mode 100644 index 0000000..03b7a7b --- /dev/null +++ b/ios/WALT/UIAlertView+Extensions.h @@ -0,0 +1,21 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import <UIKit/UIKit.h> + +@interface UIAlertView (WALTExtensions) +- (instancetype)initWithTitle:(NSString *)title error:(NSError *)error; +@end diff --git a/ios/WALT/UIAlertView+Extensions.m b/ios/WALT/UIAlertView+Extensions.m new file mode 100644 index 0000000..50ad605 --- /dev/null +++ b/ios/WALT/UIAlertView+Extensions.m @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import "UIAlertView+Extensions.h" + +@implementation UIAlertView (WALTExtensions) +- (instancetype)initWithTitle:(NSString *)title error:(NSError *)error { + return [self initWithTitle:title + message:error.localizedDescription + delegate:nil + cancelButtonTitle:@"Dismiss" + otherButtonTitles:nil]; +} +@end diff --git a/ios/WALT/WALTAppDelegate.h b/ios/WALT/WALTAppDelegate.h new file mode 100644 index 0000000..b34df0d --- /dev/null +++ b/ios/WALT/WALTAppDelegate.h @@ -0,0 +1,24 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import <UIKit/UIKit.h> + +@class WALTClient; + +@interface WALTAppDelegate : UIResponder <UIApplicationDelegate> +@property (nonatomic) UIWindow *window; +@property (readonly, nonatomic) WALTClient *client; +@end diff --git a/ios/WALT/WALTAppDelegate.m b/ios/WALT/WALTAppDelegate.m new file mode 100644 index 0000000..8756723 --- /dev/null +++ b/ios/WALT/WALTAppDelegate.m @@ -0,0 +1,58 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import "WALTAppDelegate.h" + +#include <sys/utsname.h> + +#import "UIAlertView+Extensions.h" +#import "WALTClient.h" +#import "WALTLogger.h" + +@interface WALTAppDelegate () +@property (readwrite, nonatomic) WALTClient *client; +@end + +@implementation WALTAppDelegate +- (BOOL)application:(UIApplication *)application + willFinishLaunchingWithOptions:(NSDictionary *)launchOptions { + struct utsname systemInfo; + if (uname(&systemInfo) != 0) { + UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"System Error" + message:@"Cannot identify system." + delegate:nil + cancelButtonTitle:@"Dismiss" + otherButtonTitles:nil]; + [alert show]; + } else { + [[WALTLogger sessionLogger] appendFormat:@"DEVICE\t%s\t%s\t%s\t%s\t%s\n", + systemInfo.machine, + systemInfo.sysname, + systemInfo.release, + systemInfo.nodename, + systemInfo.version]; + } + + NSError *error = nil; + self.client = [[WALTClient alloc] initWithError:&error]; + if (!self.client) { + UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"WALT Connection Error" error:error]; + [alert show]; + } + + return YES; +} +@end diff --git a/ios/WALT/WALTClient.h b/ios/WALT/WALTClient.h new file mode 100644 index 0000000..df4c1e0 --- /dev/null +++ b/ios/WALT/WALTClient.h @@ -0,0 +1,119 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import <Foundation/Foundation.h> + +#import "MIDIClient.h" +#import "MIDIMessage.h" + +extern NSString * const WALTClientErrorDomain; + +/** A reasonable timeout to use when reading from the WALT. */ +extern const NSTimeInterval kWALTReadTimeout; + +typedef NS_ENUM(MIDIByte, WALTCommand) { + WALTDelayedPingCommand = 'D', // Ping with a delay + WALTResetCommand = 'F', // Reset all vars + WALTSendSyncCommand = 'I', // Send some digits for clock sync + WALTPingCommand = 'P', // Ping with a single byte + WALTVersionCommand = 'V', // Determine WALT's firmware version + WALTReadoutSyncCommand = 'R', // Read out sync times + WALTGShockCommand = 'G', // Send last shock time and watch for another + WALTTimeCommand = 'T', // Current time + WALTZeroSyncCommand = 'Z', // Initial zero + WALTScreenOnCommand = 'C', // Send a message on screen color change + WALTScreenOffCommand = 'c', + WALTSendLastScreenCommand = 'E', // Send info about last screen color change + WALTBrightnessCurveCommand = 'U', // Probe screen for brightness vs time curve + WALTLaserOnCommand = 'L', // Send messages on state change of the laser + WALTLaserOffCommand = 'l', + WALTSendLastLaserCommand = 'J', + WALTAudioCommand = 'A', // Start watching for signal on audio out line + WALTBeepCommand = 'B', // Generate a tone into the mic and send timestamp + WALTBeepStopCommand = 'S', // Stop generating tone + WALTMIDICommand = 'M', // Start listening for a MIDI message + WALTNoteCommand = 'N', // Generate a MIDI NoteOn message +}; + +typedef struct { + char tag; + NSTimeInterval t; + int value; + unsigned int count; +} WALTTrigger; + +/** + * A client for a WALT device. + * + * The client will automatically try to connect to any available WALT device, and monitor the system + * for device connections/disconnections. Users should observe the "connected" key to be notified of + * connection changes. + * + * Most commands produce a corresponding response from the WALT. The -sendCommand:error: method + * should be used to send the command, and -readResponse used to collect the response. + */ +@interface WALTClient : NSObject <MIDIClientDelegate> +@property (readonly, nonatomic, getter=isConnected) BOOL connected; + +/** + * Returns the base time of the WALT device. + * + * The time value is an adjusted version of -currentTime. + */ +@property (readonly, nonatomic) NSTimeInterval baseTime; + +/** Returns the number of seconds the system has been awake since it was last restarted. */ +@property (readonly, nonatomic) NSTimeInterval currentTime; + +/** Initialises the client and attempts to connect to any available WALT device. */ +- (instancetype)initWithError:(NSError **)error; + +/** Sends a command to the WALT. */ +- (BOOL)sendCommand:(WALTCommand)command error:(NSError **)error; + +/** Reads a response from the WALT, blocking up to timeout until one becomes available. */ +- (NSData *)readResponseWithTimeout:(NSTimeInterval)timeout; + +/** + * Reads a trigger response from the WALT. + * + * If an error occurs, the trigger's tag will be '\0'. + */ +- (WALTTrigger)readTriggerWithTimeout:(NSTimeInterval)timeout; + +/** Returns YES if the response data contains a valid acknowledgement for a command. */ +- (BOOL)checkResponse:(NSData *)response forCommand:(WALTCommand)command; + +/** Forces a complete clock synchronisation with the WALT. */ +- (BOOL)syncClocksWithError:(NSError **)error; + +/** Refreshes the min/max error synchronisation bounds. */ +- (BOOL)updateSyncBoundsWithError:(NSError **)error; + +@property (readonly, nonatomic) int64_t minError; +@property (readonly, nonatomic) int64_t maxError; + +/** + * Confirms the connection with the WALT (by setting -isConnected). + * + * Note that this method will only return NO if there is an error in the connection process. The + * absence of a device is not such an error. + */ +- (BOOL)checkConnectionWithError:(NSError **)error; + +/** Returns the time of the last shock detected by the WALT. */ +- (NSTimeInterval)lastShockTimeWithError:(NSError **)error; +@end diff --git a/ios/WALT/WALTClient.m b/ios/WALT/WALTClient.m new file mode 100644 index 0000000..ed69349 --- /dev/null +++ b/ios/WALT/WALTClient.m @@ -0,0 +1,587 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import "WALTClient.h" + +#include <ctype.h> +#include <dispatch/dispatch.h> +#include <mach/clock.h> +#include <mach/mach.h> +#include <mach/mach_host.h> +#include <stdlib.h> +#include <time.h> + +#import "MIDIEndpoint.h" +#import "MIDIMessage.h" + +NSString * const WALTClientErrorDomain = @"WALTClientErrorDomain"; + +static NSString * const kWALTVersion = @"v 4"; + +static const MIDIChannel kWALTChannel = 1; +static const MIDIByte kWALTSerialOverMIDIProgram = 1; +static const MIDIMessageType kWALTCommandType = MIDIMessageChannelPressure; + +const NSTimeInterval kWALTReadTimeout = 0.2; +static const NSTimeInterval kWALTDuplicateTimeout = 0.01; + +static const int kWALTSyncIterations = 7; +#define kWALTSyncDigitMax 9 // #define to avoid variable length array warnings. + +/** Similar to atoll(), but only reads a maximum of n characters from s. */ +static unsigned long long antoull(const char *s, size_t n) { + unsigned long long result = 0; + while (s && n-- && *s && isdigit(*s)) { + result = result * 10 + (*s - '0'); + ++s; + } + return result; +} + +/** Converts a mach_timespec_t to its equivalent number of microseconds. */ +static int64_t TimespecToMicroseconds(const mach_timespec_t ts) { + return ((int64_t)ts.tv_sec) * USEC_PER_SEC + ts.tv_nsec / NSEC_PER_USEC; +} + +/** Returns the current time (in microseconds) on a clock. */ +static int64_t CurrentTime(clock_serv_t clock) { + mach_timespec_t time = {0}; + clock_get_time(clock, &time); + return TimespecToMicroseconds(time); +} + +/** Sleeps the current thread for us microseconds. */ +static void Sleep(int64_t us) { + const struct timespec ts = { + .tv_sec = (long)(us / USEC_PER_SEC), + .tv_nsec = (us % USEC_PER_SEC) * NSEC_PER_USEC, + }; + nanosleep(&ts, NULL); +} + +@interface WALTClient () +@property (readwrite, nonatomic, getter=isConnected) BOOL connected; + +- (void)drainResponseQueue; +- (BOOL)improveSyncBoundsWithError:(NSError **)error; +- (BOOL)improveMinBoundWithError:(NSError **)error; +- (BOOL)improveMaxBoundWithError:(NSError **)error; +- (BOOL)readRemoteTimestamps:(uint64_t[kWALTSyncDigitMax])times error:(NSError **)error; +- (WALTTrigger)readTrigger:(NSData *)response; +@end + +@implementation WALTClient { + MIDIClient *_client; + + // Responses from the MIDIClient are queued up here with a signal to the semaphore. + NSMutableArray<NSData *> *_responseQueue; // TODO(pquinn): Lock-free circular buffer? + dispatch_semaphore_t _responseSemaphore; + + BOOL _syncCompleted; + + clock_serv_t _clock; + + NSData *_lastData; + NSTimeInterval _lastDataTimestamp; + + struct { + // All microseconds. + int64_t base; + int64_t minError; + int64_t maxError; + } _sync; +} + +- (instancetype)initWithError:(NSError **)error { + if ((self = [super init])) { + _responseQueue = [[NSMutableArray<NSData *> alloc] init]; + _responseSemaphore = dispatch_semaphore_create(0); + + // NB: It's important that this is the same clock used as the base for UIEvent's -timestamp. + kern_return_t result = host_get_clock_service(mach_host_self(), SYSTEM_CLOCK, &_clock); + + if (result != KERN_SUCCESS || ![self checkConnectionWithError:error]) { + self = nil; + } + } + return self; +} + +- (void)dealloc { + [self drainResponseQueue]; + mach_port_deallocate(mach_task_self(), _clock); +} + +// Ensure only one KVO notification is sent when the connection state is changed. ++ (BOOL)automaticallyNotifiesObserversOfConnected { + return NO; +} + +- (void)setConnected:(BOOL)connected { + if (_connected != connected) { + [self willChangeValueForKey:@"connected"]; + _connected = connected; + [self didChangeValueForKey:@"connected"]; + } +} + +- (BOOL)checkConnectionWithError:(NSError **)error { + if (_client.source.isOnline && _client.destination.isOnline && _syncCompleted) { + self.connected = YES; + return YES; // Everything's fine. + } + + _syncCompleted = NO; // Reset the sync state. + [self drainResponseQueue]; + + // Create a new client. + // This probably isn't strictly necessary, but solves some of the flakiness on iOS. + _client.delegate = nil; + _client = [[MIDIClient alloc] initWithName:@"WALT" error:error]; + _client.delegate = self; + if (!_client) { + self.connected = NO; + return NO; + } + + if (!_client.source.isOnline) { + // Try to connect to the first available input source. + // TODO(pquinn): Make this user-configurable. + NSArray<MIDISource *> *sources = [MIDISource allSources]; + if (sources.count) { + if (![_client connectToSource:sources.firstObject error:error]) { + self.connected = NO; + return NO; + } + } + } + + if (!_client.destination.isOnline) { + // Try to connect to the first available input source. + // TODO(pquinn): Make this user-configurable. + NSArray<MIDIDestination *> *destinations = [MIDIDestination allDestinations]; + if (destinations.count) { + if (![_client connectToDestination:destinations.firstObject error:error]) { + self.connected = NO; + return NO; + } + } + + if (_client.destination.isOnline) { + // Switch to Serial-over-MIDI mode. + NSData *message = MIDIMessageCreateSimple1(MIDIMessageProgramChange, + kWALTChannel, + kWALTSerialOverMIDIProgram); + if (![_client sendData:message error:error]) { + self.connected = NO; + return NO; + } + + // Make sure it's using a known protocol version. + message = MIDIMessageCreateSimple1(kWALTCommandType, kWALTChannel, WALTVersionCommand); + if (![_client sendData:message error:error]) { + self.connected = NO; + return NO; + } + + NSData *response = [self readResponseWithTimeout:kWALTReadTimeout]; + NSString *versionString = [[NSString alloc] initWithData:response + encoding:NSASCIIStringEncoding]; + if (![versionString isEqualToString:kWALTVersion]) { + if (error) { + *error = [NSError errorWithDomain:WALTClientErrorDomain + code:0 + userInfo:@{NSLocalizedDescriptionKey: + [@"Unknown WALT version: " + stringByAppendingString:versionString]}]; + } + self.connected = NO; + return NO; + } + + if (![self syncClocksWithError:error]) { + self.connected = NO; + return NO; + } + + _syncCompleted = YES; + } + } + + self.connected = (_client.source.isOnline && _client.destination.isOnline && _syncCompleted); + return YES; +} + +#pragma mark - Clock Synchronisation + +- (BOOL)syncClocksWithError:(NSError **)error { + _sync.base = CurrentTime(_clock); + + if (![self sendCommand:WALTZeroSyncCommand error:error]) { + return NO; + } + + NSData *data = [self readResponseWithTimeout:kWALTReadTimeout]; + if (![self checkResponse:data forCommand:WALTZeroSyncCommand]) { + if (error) { + *error = [NSError errorWithDomain:WALTClientErrorDomain + code:0 + userInfo:@{NSLocalizedDescriptionKey: + [NSString stringWithFormat:@"Bad acknowledgement for WALTZeroSyncCommand: %@", data]}]; + } + return NO; + } + + _sync.maxError = CurrentTime(_clock) - _sync.base; + _sync.minError = 0; + + for (int i = 0; i < kWALTSyncIterations; ++i) { + if (![self improveSyncBoundsWithError:error]) { + return NO; + } + } + + // Shift the time base so minError == 0 + _sync.base += _sync.minError; + _sync.maxError -= _sync.minError; + _sync.minError = 0; + return YES; +} + +- (BOOL)updateSyncBoundsWithError:(NSError **)error { + // Reset the bounds to unrealistic values + _sync.minError = -1e7; + _sync.maxError = 1e7; + + for (int i = 0; i < kWALTSyncIterations; ++i) { + if (![self improveSyncBoundsWithError:error]) { + return NO; + } + } + + return YES; +} + +- (int64_t)minError { + return _sync.minError; +} + +- (int64_t)maxError { + return _sync.maxError; +} + +- (BOOL)improveSyncBoundsWithError:(NSError **)error { + return ([self improveMinBoundWithError:error] && [self improveMaxBoundWithError:error]); +} + +- (BOOL)improveMinBoundWithError:(NSError **)error { + if (![self sendCommand:WALTResetCommand error:error]) { + return NO; + } + + NSData *data = [self readResponseWithTimeout:kWALTReadTimeout]; + if (![self checkResponse:data forCommand:WALTResetCommand]) { + if (error) { + *error = [NSError errorWithDomain:WALTClientErrorDomain + code:0 + userInfo:@{NSLocalizedDescriptionKey: + [NSString stringWithFormat:@"Bad acknowledgement for WALTResetCommand: %@", data]}]; + } + return NO; + } + + const uint64_t kMaxSleepTime = 700; // µs + const uint64_t kMinSleepTime = 70; // µs + const uint64_t kSleepTimeDivider = 10; + + uint64_t sleepTime = (_sync.maxError - _sync.minError) / kSleepTimeDivider; + if (sleepTime > kMaxSleepTime) { sleepTime = kMaxSleepTime; } + if (sleepTime < kMinSleepTime) { sleepTime = kMinSleepTime; } + + struct { + uint64_t local[kWALTSyncDigitMax]; + uint64_t remote[kWALTSyncDigitMax]; + } digitTimes = {0}; + + // Send the digits 1 through 9 and record the times they were sent in digitTimes.local. + for (int i = 0; i < kWALTSyncDigitMax; ++i) { + digitTimes.local[i] = CurrentTime(_clock) - _sync.base; + + char c = '1' + i; + if (![self sendCommand:c error:error]) { + if (error) { + *error = [NSError errorWithDomain:WALTClientErrorDomain + code:0 + userInfo:@{NSLocalizedDescriptionKey: + [NSString stringWithFormat:@"Error sending digit %d", i + 1], + NSUnderlyingErrorKey: *error}]; + } + return NO; + } + // Sleep between digits + Sleep(sleepTime); + } + + if (![self readRemoteTimestamps:digitTimes.remote error:error]) { + return NO; + } + + // Adjust minError to be the largest delta between local and remote. + for (int i = 0; i < kWALTSyncDigitMax; ++i) { + int64_t delta = digitTimes.local[i] - digitTimes.remote[i]; + if (digitTimes.local[i] != 0 && digitTimes.remote[i] != 0 && delta > _sync.minError) { + _sync.minError = delta; + } + } + return YES; +} + +- (BOOL)improveMaxBoundWithError:(NSError **)error { + struct { + uint64_t local[kWALTSyncDigitMax]; + uint64_t remote[kWALTSyncDigitMax]; + } digitTimes = {0}; + + // Ask the WALT to send the digits 1 through 9, and record the times they are received in + // digitTimes.local. + if (![self sendCommand:WALTSendSyncCommand error:error]) { + return NO; + } + + for (int i = 0; i < kWALTSyncDigitMax; ++i) { + NSData *data = [self readResponseWithTimeout:kWALTReadTimeout]; + if (data.length != 1) { + if (error) { + *error = [NSError errorWithDomain:WALTClientErrorDomain + code:0 + userInfo:@{NSLocalizedDescriptionKey: + [NSString stringWithFormat:@"Error receiving digit %d: %@", i + 1, data]}]; + } + return NO; + } + + char c = ((const char *)data.bytes)[0]; + if (!isdigit(c)) { + if (error) { + *error = [NSError errorWithDomain:WALTClientErrorDomain + code:0 + userInfo:@{NSLocalizedDescriptionKey: + [NSString stringWithFormat:@"Error parsing digit response: %c", c]}]; + } + return NO; + } + + int digit = c - '0'; + digitTimes.local[digit - 1] = CurrentTime(_clock) - _sync.base; + } + + if (![self readRemoteTimestamps:digitTimes.remote error:error]) { + return NO; + } + + // Adjust maxError to be the smallest delta between local and remote + for (int i = 0; i < kWALTSyncDigitMax; ++i) { + int64_t delta = digitTimes.local[i] - digitTimes.remote[i]; + if (digitTimes.local[i] != 0 && digitTimes.remote[i] != 0 && delta < _sync.maxError) { + _sync.maxError = delta; + } + } + return YES; +} + +- (BOOL)readRemoteTimestamps:(uint64_t [9])times error:(NSError **)error { + for (int i = 0; i < kWALTSyncDigitMax; ++i) { + // Ask the WALT for each digit's recorded timestamp + if (![self sendCommand:WALTReadoutSyncCommand error:error]) { + return NO; + } + + NSData *data = [self readResponseWithTimeout:kWALTReadTimeout]; + if (data.length < 3) { + if (error) { + *error = [NSError errorWithDomain:WALTClientErrorDomain + code:0 + userInfo:@{NSLocalizedDescriptionKey: + [NSString stringWithFormat:@"Error receiving sync digit %d: %@", i + 1, data]}]; + } + return NO; + } + + // The reply data is formatted as n:xxxx, where n is a digit between 1 and 9, and xxxx + // is a microsecond timestamp. + int digit = (int)antoull(data.bytes, 1); + uint64_t timestamp = antoull(((const char *)data.bytes) + 2, data.length - 2); + + if (digit != (i + 1) || timestamp == 0) { + if (error) { + *error = [NSError errorWithDomain:WALTClientErrorDomain + code:0 + userInfo:@{NSLocalizedDescriptionKey: + [NSString stringWithFormat:@"Error parsing remote time response for %d: %@", i, data]}]; + } + return NO; + } + times[digit - 1] = timestamp; + } + return YES; +} + +#pragma mark - MIDIClient Delegate + +// TODO(pquinn): Errors from these callbacks aren't propoagated anywhere. + +- (void)MIDIClientEndpointAdded:(MIDIClient *)client { + [self performSelectorOnMainThread:@selector(checkConnectionWithError:) + withObject:nil + waitUntilDone:NO]; +} + +- (void)MIDIClientEndpointRemoved:(MIDIClient *)client { + [self performSelectorOnMainThread:@selector(checkConnectionWithError:) + withObject:nil + waitUntilDone:NO]; +} + +- (void)MIDIClientConfigurationChanged:(MIDIClient *)client { + [self performSelectorOnMainThread:@selector(checkConnectionWithError:) + withObject:nil + waitUntilDone:NO]; +} + +- (void)MIDIClient:(MIDIClient *)client receivedError:(NSError *)error { + // TODO(pquinn): What's the scope of these errors? + NSLog(@"WALTClient received unhandled error: %@", error); +} + +- (void)MIDIClient:(MIDIClient *)client receivedData:(NSData *)message { + NSData *body = MIDIMessageBody(message); + @synchronized (_responseQueue) { + // Sometimes a message will be received twice in quick succession. It's not clear where the bug + // is (the WALT, CoreMIDI, or this application), and it cannot be reliably reproduced. As a + // hack, simply ignore messages that appear to be duplicates and arrive within + // kWALTDuplicateTimeout seconds. + if (self.currentTime - _lastDataTimestamp <= kWALTDuplicateTimeout && + [body isEqualToData:_lastData]) { + NSLog(@"Ignoring duplicate response within kWALTDuplicateTimeout: %@", message); + return; + } + + [_responseQueue addObject:body]; + _lastData = body; + _lastDataTimestamp = self.currentTime; + } + dispatch_semaphore_signal(_responseSemaphore); +} + +#pragma mark - Send/Receive + +- (void)drainResponseQueue { + @synchronized (_responseQueue) { + // Drain out any stale responses or the semaphore destructor will assert. + while (_responseQueue.count) { + dispatch_semaphore_wait(_responseSemaphore, DISPATCH_TIME_FOREVER); + [_responseQueue removeObjectAtIndex:0]; + } + } +} + +- (NSData *)readResponseWithTimeout:(NSTimeInterval)timeout { + if (dispatch_semaphore_wait(_responseSemaphore, + dispatch_time(DISPATCH_TIME_NOW, timeout * NSEC_PER_SEC))) { + return nil; + } + + @synchronized (_responseQueue) { + NSAssert(_responseQueue.count > 0, @"_responseQueue is empty!"); + NSData *response = _responseQueue.firstObject; + [_responseQueue removeObjectAtIndex:0]; + return response; + } +} + +- (BOOL)sendCommand:(WALTCommand)command error:(NSError **)error { + NSData *message = MIDIMessageCreateSimple1(kWALTCommandType, kWALTChannel, command); + [self drainResponseQueue]; + return [_client sendData:message error:error]; +} + +- (BOOL)checkResponse:(NSData *)response forCommand:(WALTCommand)command { + const WALTCommand flipped = isupper(command) ? tolower(command) : toupper(command); + if (response.length < 1 || ((const char *)response.bytes)[0] != flipped) { + return NO; + } else { + return YES; + } +} + +#pragma mark - Specific Commands + +- (NSTimeInterval)lastShockTimeWithError:(NSError **)error { + if (![self sendCommand:WALTGShockCommand error:error]) { + return -1; + } + + NSData *response = [self readResponseWithTimeout:kWALTReadTimeout]; + if (!response) { + if (error) { + *error = [NSError errorWithDomain:WALTClientErrorDomain + code:0 + userInfo:@{NSLocalizedDescriptionKey: + @"Error receiving shock response."}]; + } + return -1; + } + + uint64_t microseconds = antoull(response.bytes, response.length); + return ((NSTimeInterval)microseconds + _sync.base) / USEC_PER_SEC; +} + +- (WALTTrigger)readTrigger:(NSData *)response { + NSString *responseString = + [[NSString alloc] initWithData:response encoding:NSASCIIStringEncoding]; + NSArray<NSString *> *components = [responseString componentsSeparatedByString:@" "]; + + WALTTrigger result = {0}; + + if (components.count != 5 || + ![[components objectAtIndex:0] isEqualToString:@"G"] || + [components objectAtIndex:1].length != 1) { + return result; + } + + result.tag = [[components objectAtIndex:1] characterAtIndex:0]; + + uint64_t microseconds = atoll([components objectAtIndex:2].UTF8String); + result.t = ((NSTimeInterval)microseconds + _sync.base) / USEC_PER_SEC; + result.value = (int)atoll([components objectAtIndex:3].UTF8String); + result.count = (unsigned int)atoll([components objectAtIndex:4].UTF8String); + return result; +} + +- (WALTTrigger)readTriggerWithTimeout:(NSTimeInterval)timeout { + return [self readTrigger:[self readResponseWithTimeout:timeout]]; +} + +#pragma mark - Time + +- (NSTimeInterval)baseTime { + return ((NSTimeInterval)_sync.base) / USEC_PER_SEC; +} + +- (NSTimeInterval)currentTime { + return ((NSTimeInterval)CurrentTime(_clock)) / USEC_PER_SEC; +} +@end diff --git a/ios/WALT/WALTLogger.h b/ios/WALT/WALTLogger.h new file mode 100644 index 0000000..436aed7 --- /dev/null +++ b/ios/WALT/WALTLogger.h @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import <Foundation/Foundation.h> + +/** A basic buffer for log text. */ +@interface WALTLogger : NSObject ++ (instancetype)sessionLogger; + +- (void)appendString:(NSString *)string; +- (void)appendFormat:(NSString *)format, ... NS_FORMAT_FUNCTION(1,2); +- (void)clear; +- (BOOL)writeToURL:(NSURL *)url error:(NSError **)error; +- (NSString *)stringValue; +@end diff --git a/ios/WALT/WALTLogger.m b/ios/WALT/WALTLogger.m new file mode 100644 index 0000000..28ecb30 --- /dev/null +++ b/ios/WALT/WALTLogger.m @@ -0,0 +1,73 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import "WALTLogger.h" + +@implementation WALTLogger { + NSMutableString *_buffer; +} + ++ (instancetype)sessionLogger { + static WALTLogger *sessionLogger = nil; + static dispatch_once_t token; + dispatch_once(&token, ^{ + sessionLogger = [[self alloc] init]; + }); + return sessionLogger; +} + +- (instancetype)init { + if ((self = [super init])) { + _buffer = [[NSMutableString alloc] init]; + } + return self; +} + +- (void)appendString:(NSString *)string { + @synchronized (_buffer) { + [_buffer appendString:string]; + } +} + +- (void)appendFormat:(NSString *)format, ... { + va_list args; + va_start(args, format); + NSString *formatted = [[NSString alloc] initWithFormat:format arguments:args]; + [_buffer appendString:formatted]; + va_end(args); +} + +- (void)clear { + @synchronized (_buffer) { + [_buffer setString:[NSString string]]; + } +} + +- (BOOL)writeToURL:(NSURL *)url error:(NSError **)error { + @synchronized (_buffer) { + return [_buffer writeToURL:url + atomically:YES + encoding:NSUTF8StringEncoding + error:error]; + } +} + +- (NSString *)stringValue { + @synchronized (_buffer) { + return [NSString stringWithString:_buffer]; + } +} +@end diff --git a/ios/WALT/WALTTouch.h b/ios/WALT/WALTTouch.h new file mode 100644 index 0000000..b780096 --- /dev/null +++ b/ios/WALT/WALTTouch.h @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import <Foundation/Foundation.h> +#import <UIKit/UIKit.h> + +@interface WALTTouch : NSObject +@property (nonatomic, assign) UITouchPhase phase; +@property (nonatomic, assign) NSTimeInterval physicalTime; +@property (nonatomic, assign) NSTimeInterval kernelTime; +@property (nonatomic, assign) NSTimeInterval callbackTime; +@property (nonatomic, assign) CGPoint location; + +- (instancetype)initWithEvent:(UIEvent *)event; +@end diff --git a/ios/WALT/WALTTouch.m b/ios/WALT/WALTTouch.m new file mode 100644 index 0000000..1f69686 --- /dev/null +++ b/ios/WALT/WALTTouch.m @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import "WALTTouch.h" + +@implementation WALTTouch +- (instancetype)initWithEvent:(UIEvent *)event { + if ((self = [super init])) { + if ([event allTouches].count > 1) { + NSLog(@"Multiple touches in event; taking any."); + } + self.kernelTime = event.timestamp; + + UITouch *touch = [[event allTouches] anyObject]; + self.phase = touch.phase; + if ([touch respondsToSelector:@selector(preciseLocationInView:)]) { // iOS 9.1+ + self.location = [touch preciseLocationInView:nil]; + } else { + self.location = [touch locationInView:nil]; + } + } + return self; +} +@end diff --git a/ios/WALT/main.m b/ios/WALT/main.m new file mode 100644 index 0000000..90429a8 --- /dev/null +++ b/ios/WALT/main.m @@ -0,0 +1,25 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import <UIKit/UIKit.h> + +#import "WALTAppDelegate.h" + +int main(int argc, char * argv[]) { + @autoreleasepool { + return UIApplicationMain(argc, argv, nil, NSStringFromClass([WALTAppDelegate class])); + } +} |