aboutsummaryrefslogtreecommitdiff
path: root/ios
diff options
context:
space:
mode:
authorAndrew Lehmer <alehmer@google.com>2017-04-26 14:58:59 -0700
committerAndrew Lehmer <alehmer@google.com>2017-04-26 14:58:59 -0700
commite76dcf96b0c451e46cddfa695de8feeb92533937 (patch)
treeed9a45d409f988f517e6c3f3a685cbf81ac45a5a /ios
parentbcf013dda8ffac9fd76937be6441b44bb9f3586f (diff)
downloadwalt-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')
-rw-r--r--ios/.gitignore3
-rw-r--r--ios/WALT.xcodeproj/project.pbxproj441
-rw-r--r--ios/WALT.xcodeproj/project.xcworkspace/contents.xcworkspacedata7
-rw-r--r--ios/WALT/Assets.xcassets/AppIcon.appiconset/Contents.json95
-rw-r--r--ios/WALT/Assets.xcassets/AppIcon.appiconset/icon@2x.pngbin0 -> 13465 bytes
-rw-r--r--ios/WALT/Assets.xcassets/AppIcon.appiconset/icon@3x.pngbin0 -> 20673 bytes
-rw-r--r--ios/WALT/Assets.xcassets/Contents.json6
-rw-r--r--ios/WALT/Assets.xcassets/ic_brightness_medium_24dp.imageset/Contents.json12
-rw-r--r--ios/WALT/Assets.xcassets/ic_brightness_medium_24dp.imageset/ic_brightness_medium_24dp.pdfbin0 -> 1021 bytes
-rw-r--r--ios/WALT/Assets.xcassets/ic_radio_button_checked_24dp.imageset/Contents.json12
-rw-r--r--ios/WALT/Assets.xcassets/ic_radio_button_checked_24dp.imageset/ic_radio_button_checked_24dp.pdf68
-rw-r--r--ios/WALT/Assets.xcassets/ic_receipt_black_24dp.imageset/Contents.json12
-rw-r--r--ios/WALT/Assets.xcassets/ic_receipt_black_24dp.imageset/ic_receipt_black_24dp.pdfbin0 -> 1060 bytes
-rw-r--r--ios/WALT/Assets.xcassets/ic_schedule_black_24dp.imageset/Contents.json12
-rw-r--r--ios/WALT/Assets.xcassets/ic_schedule_black_24dp.imageset/ic_schedule_black_24dp.pdf68
-rw-r--r--ios/WALT/Assets.xcassets/ic_settings_black_24dp.imageset/Contents.json12
-rw-r--r--ios/WALT/Assets.xcassets/ic_settings_black_24dp.imageset/ic_settings_black_24dp.pdfbin0 -> 1428 bytes
-rw-r--r--ios/WALT/Assets.xcassets/ic_swap_horiz_24dp.imageset/Contents.json12
-rw-r--r--ios/WALT/Assets.xcassets/ic_swap_horiz_24dp.imageset/ic_swap_horiz_24dp.pdfbin0 -> 993 bytes
-rw-r--r--ios/WALT/Assets.xcassets/ic_swap_vert_24dp.imageset/Contents.json12
-rw-r--r--ios/WALT/Assets.xcassets/ic_swap_vert_24dp.imageset/ic_swap_vert_24dp.pdf68
-rw-r--r--ios/WALT/Assets.xcassets/ic_timelapse_black_24dp.imageset/Contents.json12
-rw-r--r--ios/WALT/Assets.xcassets/ic_timelapse_black_24dp.imageset/ic_timelapse_black_24dp.pdf68
-rw-r--r--ios/WALT/Base.lproj/LaunchScreen.storyboard28
-rw-r--r--ios/WALT/Base.lproj/Main.storyboard502
-rw-r--r--ios/WALT/DebugLogController.h23
-rw-r--r--ios/WALT/DebugLogController.m33
-rw-r--r--ios/WALT/DragLatencyController.h27
-rw-r--r--ios/WALT/DragLatencyController.mm393
-rw-r--r--ios/WALT/Info.plist48
-rw-r--r--ios/WALT/MIDIClient.h73
-rw-r--r--ios/WALT/MIDIClient.m282
-rw-r--r--ios/WALT/MIDIEndpoint.h35
-rw-r--r--ios/WALT/MIDIEndpoint.m76
-rw-r--r--ios/WALT/MIDIMessage.h85
-rw-r--r--ios/WALT/MIDIMessage.m103
-rw-r--r--ios/WALT/MenuController.h24
-rw-r--r--ios/WALT/MenuController.m154
-rw-r--r--ios/WALT/NSArray+Extensions.h21
-rw-r--r--ios/WALT/NSArray+Extensions.m34
-rw-r--r--ios/WALT/ScreenResponseController.h26
-rw-r--r--ios/WALT/ScreenResponseController.m217
-rw-r--r--ios/WALT/SettingsController.h22
-rw-r--r--ios/WALT/SettingsController.m88
-rw-r--r--ios/WALT/TapLatencyController.h25
-rw-r--r--ios/WALT/TapLatencyController.m194
-rw-r--r--ios/WALT/UIAlertView+Extensions.h21
-rw-r--r--ios/WALT/UIAlertView+Extensions.m27
-rw-r--r--ios/WALT/WALTAppDelegate.h24
-rw-r--r--ios/WALT/WALTAppDelegate.m58
-rw-r--r--ios/WALT/WALTClient.h119
-rw-r--r--ios/WALT/WALTClient.m587
-rw-r--r--ios/WALT/WALTLogger.h28
-rw-r--r--ios/WALT/WALTLogger.m73
-rw-r--r--ios/WALT/WALTTouch.h28
-rw-r--r--ios/WALT/WALTTouch.m37
-rw-r--r--ios/WALT/main.m25
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
new file mode 100644
index 0000000..cf5db2b
--- /dev/null
+++ b/ios/WALT/Assets.xcassets/AppIcon.appiconset/icon@2x.png
Binary files differ
diff --git a/ios/WALT/Assets.xcassets/AppIcon.appiconset/icon@3x.png b/ios/WALT/Assets.xcassets/AppIcon.appiconset/icon@3x.png
new file mode 100644
index 0000000..754e0b8
--- /dev/null
+++ b/ios/WALT/Assets.xcassets/AppIcon.appiconset/icon@3x.png
Binary files differ
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
new file mode 100644
index 0000000..2b34316
--- /dev/null
+++ b/ios/WALT/Assets.xcassets/ic_brightness_medium_24dp.imageset/ic_brightness_medium_24dp.pdf
Binary files differ
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
new file mode 100644
index 0000000..dc7033b
--- /dev/null
+++ b/ios/WALT/Assets.xcassets/ic_receipt_black_24dp.imageset/ic_receipt_black_24dp.pdf
Binary files differ
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]PA0 yAhZҥ P%eٱ=C^;w5 d\h 5Pb8 V1F|\X A92\P8sRβb Kgx59ѝ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
new file mode 100644
index 0000000..a20e463
--- /dev/null
+++ b/ios/WALT/Assets.xcassets/ic_settings_black_24dp.imageset/ic_settings_black_24dp.pdf
Binary files differ
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
new file mode 100644
index 0000000..50bfc96
--- /dev/null
+++ b/ios/WALT/Assets.xcassets/ic_swap_horiz_24dp.imageset/ic_swap_horiz_24dp.pdf
Binary files differ
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\М,M wzH=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??) f 830 ˍR DJ\*Jeҫ)兝A2gåt)6ކYG+LϲQZ96O|W u.ш[^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]));
+ }
+}