From 431b97a01119cc8a396218aca4904f3cfd20be6b Mon Sep 17 00:00:00 2001 From: Oleg Kalachev Date: Mon, 19 Feb 2018 06:38:24 +0300 Subject: [PATCH] Add mobile remote control for iOS --- apps/ios/.gitignore | 17 + apps/ios/Podfile | 14 + apps/ios/Podfile.lock | 21 + apps/ios/cleverrc.xcodeproj/project.pbxproj | 444 ++ .../contents.xcworkspacedata | 7 + .../contents.xcworkspacedata | 10 + apps/ios/cleverrc/AppDelegate.swift | 46 + .../AppIcon.appiconset/Contents.json | 100 + .../AppIcon.appiconset/cleverios180-1.png | Bin 0 -> 3344 bytes .../AppIcon.appiconset/cleverios180.png | Bin 0 -> 3344 bytes .../cleverrc/Assets.xcassets/Contents.json | 6 + .../Image.imageset/Contents.json | 20 + .../Base.lproj/LaunchScreen.storyboard | 29 + apps/ios/cleverrc/Base.lproj/Main.storyboard | 46 + apps/ios/cleverrc/BinUtils.swift | 453 ++ apps/ios/cleverrc/Info.plist | 46 + apps/ios/cleverrc/ViewController.swift | 70 + apps/ios/cleverrc/clever.svg | 84 + apps/ios/cleverrc/index.html | 23 + apps/ios/cleverrc/main.css | 91 + apps/ios/cleverrc/main.js | 126 + apps/ios/cleverrc/roslib.js | 3693 +++++++++++++++++ apps/ios/cleverrc/telemetry.js | 85 + clever/CMakeLists.txt | 5 +- clever/launch/clever.launch | 3 + clever/launch/mavros.launch | 1 - clever/src/rc.cpp | 118 + 27 files changed, 5556 insertions(+), 2 deletions(-) create mode 100644 apps/ios/.gitignore create mode 100644 apps/ios/Podfile create mode 100644 apps/ios/Podfile.lock create mode 100644 apps/ios/cleverrc.xcodeproj/project.pbxproj create mode 100644 apps/ios/cleverrc.xcodeproj/project.xcworkspace/contents.xcworkspacedata create mode 100644 apps/ios/cleverrc.xcworkspace/contents.xcworkspacedata create mode 100644 apps/ios/cleverrc/AppDelegate.swift create mode 100644 apps/ios/cleverrc/Assets.xcassets/AppIcon.appiconset/Contents.json create mode 100644 apps/ios/cleverrc/Assets.xcassets/AppIcon.appiconset/cleverios180-1.png create mode 100644 apps/ios/cleverrc/Assets.xcassets/AppIcon.appiconset/cleverios180.png create mode 100644 apps/ios/cleverrc/Assets.xcassets/Contents.json create mode 100644 apps/ios/cleverrc/Assets.xcassets/Image.imageset/Contents.json create mode 100644 apps/ios/cleverrc/Base.lproj/LaunchScreen.storyboard create mode 100644 apps/ios/cleverrc/Base.lproj/Main.storyboard create mode 100644 apps/ios/cleverrc/BinUtils.swift create mode 100644 apps/ios/cleverrc/Info.plist create mode 100644 apps/ios/cleverrc/ViewController.swift create mode 100644 apps/ios/cleverrc/clever.svg create mode 100644 apps/ios/cleverrc/index.html create mode 100644 apps/ios/cleverrc/main.css create mode 100644 apps/ios/cleverrc/main.js create mode 100644 apps/ios/cleverrc/roslib.js create mode 100644 apps/ios/cleverrc/telemetry.js create mode 100644 clever/src/rc.cpp diff --git a/apps/ios/.gitignore b/apps/ios/.gitignore new file mode 100644 index 00000000..db36ad06 --- /dev/null +++ b/apps/ios/.gitignore @@ -0,0 +1,17 @@ +# Xcode +.DS_Store +build/ +*.pbxuser +!default.pbxuser +*.mode1v3 +!default.mode1v3 +*.mode2v3 +!default.mode2v3 +*.perspectivev3 +!default.perspectivev3 +!default.xcworkspace +xcuserdata +profile +*.moved-aside +DerivedData +Pods/ diff --git a/apps/ios/Podfile b/apps/ios/Podfile new file mode 100644 index 00000000..2bffe4c7 --- /dev/null +++ b/apps/ios/Podfile @@ -0,0 +1,14 @@ +project 'cleverrc.xcodeproj/' + +# Uncomment the next line to define a global platform for your project +# platform :ios, '9.0' + +target 'cleverrc' do + # Comment the next line if you're not using Swift and don't want to use dynamic frameworks + use_frameworks! + + # Pods for cleverrc + pod 'SwiftSocket', '~> 2.0' + pod 'NotificationBannerSwift' + +end diff --git a/apps/ios/Podfile.lock b/apps/ios/Podfile.lock new file mode 100644 index 00000000..23c9ad41 --- /dev/null +++ b/apps/ios/Podfile.lock @@ -0,0 +1,21 @@ +PODS: + - MarqueeLabel/Swift (3.1.4) + - NotificationBannerSwift (1.5.4): + - MarqueeLabel/Swift + - SnapKit (~> 4.0) + - SnapKit (4.0.0) + - SwiftSocket (2.0.2) + +DEPENDENCIES: + - NotificationBannerSwift + - SwiftSocket (~> 2.0) + +SPEC CHECKSUMS: + MarqueeLabel: bf768455fe88d427f71476ebb23f9092b660f40b + NotificationBannerSwift: 4f6666c8421dcf11be0812dd1093d932c15921af + SnapKit: a42d492c16e80209130a3379f73596c3454b7694 + SwiftSocket: 6f4c9c63fbc5c1d61188936bb3c599fd546f40ae + +PODFILE CHECKSUM: fd5199f69c3ee8c1fbc0dd582477d890c8b2a24f + +COCOAPODS: 1.4.0 diff --git a/apps/ios/cleverrc.xcodeproj/project.pbxproj b/apps/ios/cleverrc.xcodeproj/project.pbxproj new file mode 100644 index 00000000..4fa29e7c --- /dev/null +++ b/apps/ios/cleverrc.xcodeproj/project.pbxproj @@ -0,0 +1,444 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 48; + objects = { + +/* Begin PBXBuildFile section */ + 7C0AB7AB202A744400BAED27 /* BinUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C0AB7AA202A744400BAED27 /* BinUtils.swift */; }; + 7C51654120139237004D1F4D /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C51654020139237004D1F4D /* AppDelegate.swift */; }; + 7C51654320139237004D1F4D /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C51654220139237004D1F4D /* ViewController.swift */; }; + 7C51654620139237004D1F4D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 7C51654420139237004D1F4D /* Main.storyboard */; }; + 7C51654820139237004D1F4D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 7C51654720139237004D1F4D /* Assets.xcassets */; }; + 7C51654B20139237004D1F4D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 7C51654920139237004D1F4D /* LaunchScreen.storyboard */; }; + 7C516553201526BA004D1F4D /* index.html in Resources */ = {isa = PBXBuildFile; fileRef = 7C516552201526BA004D1F4D /* index.html */; }; + 7C51655520153180004D1F4D /* main.js in Resources */ = {isa = PBXBuildFile; fileRef = 7C51655420153180004D1F4D /* main.js */; }; + 7CA401E22033CE17009FAA3B /* main.css in Resources */ = {isa = PBXBuildFile; fileRef = 7CA401E12033CE17009FAA3B /* main.css */; }; + 7CA401E42033FA34009FAA3B /* telemetry.js in Resources */ = {isa = PBXBuildFile; fileRef = 7CA401E32033FA34009FAA3B /* telemetry.js */; }; + 7CA401E6203471D9009FAA3B /* clever.svg in Resources */ = {isa = PBXBuildFile; fileRef = 7CA401E5203471D8009FAA3B /* clever.svg */; }; + C25141CAF1A7125F3CE29DDC /* Pods_cleverrc.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C38C04523251039FF13DDCD /* Pods_cleverrc.framework */; }; +/* End PBXBuildFile section */ + +/* Begin PBXFileReference section */ + 5C38C04523251039FF13DDCD /* Pods_cleverrc.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_cleverrc.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 7C0AB7AA202A744400BAED27 /* BinUtils.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BinUtils.swift; sourceTree = ""; }; + 7C45DCE9203A75A2009C73F5 /* roslib.js */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.javascript; path = roslib.js; sourceTree = ""; }; + 7C51653D20139237004D1F4D /* cleverrc.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = cleverrc.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 7C51654020139237004D1F4D /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 7C51654220139237004D1F4D /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; + 7C51654520139237004D1F4D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; + 7C51654720139237004D1F4D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 7C51654A20139237004D1F4D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; + 7C51654C20139237004D1F4D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 7C516552201526BA004D1F4D /* index.html */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.html; path = index.html; sourceTree = ""; }; + 7C51655420153180004D1F4D /* main.js */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.javascript; path = main.js; sourceTree = ""; }; + 7CA401E12033CE17009FAA3B /* main.css */ = {isa = PBXFileReference; lastKnownFileType = text.css; path = main.css; sourceTree = ""; }; + 7CA401E32033FA34009FAA3B /* telemetry.js */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.javascript; path = telemetry.js; sourceTree = ""; }; + 7CA401E5203471D8009FAA3B /* clever.svg */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xml; path = clever.svg; sourceTree = ""; }; + AAC9195BF3A9BF6942EF4D0B /* Pods-cleverrc.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-cleverrc.release.xcconfig"; path = "Pods/Target Support Files/Pods-cleverrc/Pods-cleverrc.release.xcconfig"; sourceTree = ""; }; + CB200F4B933204EA97E0E2E4 /* Pods-cleverrc.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-cleverrc.debug.xcconfig"; path = "Pods/Target Support Files/Pods-cleverrc/Pods-cleverrc.debug.xcconfig"; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 7C51653A20139237004D1F4D /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + C25141CAF1A7125F3CE29DDC /* Pods_cleverrc.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 4FA3968F2242239E15A656D2 /* Pods */ = { + isa = PBXGroup; + children = ( + CB200F4B933204EA97E0E2E4 /* Pods-cleverrc.debug.xcconfig */, + AAC9195BF3A9BF6942EF4D0B /* Pods-cleverrc.release.xcconfig */, + ); + name = Pods; + sourceTree = ""; + }; + 66C638F0021EBE07741B26F3 /* Frameworks */ = { + isa = PBXGroup; + children = ( + 5C38C04523251039FF13DDCD /* Pods_cleverrc.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; + 7C51653420139237004D1F4D = { + isa = PBXGroup; + children = ( + 66C638F0021EBE07741B26F3 /* Frameworks */, + 4FA3968F2242239E15A656D2 /* Pods */, + 7C51653E20139237004D1F4D /* Products */, + 7C51653F20139237004D1F4D /* cleverrc */, + ); + sourceTree = ""; + }; + 7C51653E20139237004D1F4D /* Products */ = { + isa = PBXGroup; + children = ( + 7C51653D20139237004D1F4D /* cleverrc.app */, + ); + name = Products; + sourceTree = ""; + }; + 7C51653F20139237004D1F4D /* cleverrc */ = { + isa = PBXGroup; + children = ( + 7C45DCE9203A75A2009C73F5 /* roslib.js */, + 7C51654020139237004D1F4D /* AppDelegate.swift */, + 7C51654720139237004D1F4D /* Assets.xcassets */, + 7C0AB7AA202A744400BAED27 /* BinUtils.swift */, + 7C51654C20139237004D1F4D /* Info.plist */, + 7C51654920139237004D1F4D /* LaunchScreen.storyboard */, + 7C51654420139237004D1F4D /* Main.storyboard */, + 7C51654220139237004D1F4D /* ViewController.swift */, + 7CA401E5203471D8009FAA3B /* clever.svg */, + 7C516552201526BA004D1F4D /* index.html */, + 7CA401E12033CE17009FAA3B /* main.css */, + 7C51655420153180004D1F4D /* main.js */, + 7CA401E32033FA34009FAA3B /* telemetry.js */, + ); + path = cleverrc; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 7C51653C20139237004D1F4D /* cleverrc */ = { + isa = PBXNativeTarget; + buildConfigurationList = 7C51654F20139237004D1F4D /* Build configuration list for PBXNativeTarget "cleverrc" */; + buildPhases = ( + 9F096121C4A02BCE9D4FD1B9 /* [CP] Check Pods Manifest.lock */, + 7C51653920139237004D1F4D /* Sources */, + 7C51653A20139237004D1F4D /* Frameworks */, + 7C51653B20139237004D1F4D /* Resources */, + A37DBBAD5E44E632F8A8A204 /* [CP] Embed Pods Frameworks */, + 9BAB41D26FC0095C7C86B9DE /* [CP] Copy Pods Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = cleverrc; + productName = cleverrc; + productReference = 7C51653D20139237004D1F4D /* cleverrc.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 7C51653520139237004D1F4D /* Project object */ = { + isa = PBXProject; + attributes = { + LastSwiftUpdateCheck = 0920; + LastUpgradeCheck = 0920; + ORGANIZATIONNAME = "Copter Express"; + TargetAttributes = { + 7C51653C20139237004D1F4D = { + CreatedOnToolsVersion = 9.2; + ProvisioningStyle = Automatic; + }; + }; + }; + buildConfigurationList = 7C51653820139237004D1F4D /* Build configuration list for PBXProject "cleverrc" */; + compatibilityVersion = "Xcode 8.0"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 7C51653420139237004D1F4D; + productRefGroup = 7C51653E20139237004D1F4D /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 7C51653C20139237004D1F4D /* cleverrc */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 7C51653B20139237004D1F4D /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 7C51654B20139237004D1F4D /* LaunchScreen.storyboard in Resources */, + 7CA401E6203471D9009FAA3B /* clever.svg in Resources */, + 7CA401E42033FA34009FAA3B /* telemetry.js in Resources */, + 7C516553201526BA004D1F4D /* index.html in Resources */, + 7C51654820139237004D1F4D /* Assets.xcassets in Resources */, + 7CA401E22033CE17009FAA3B /* main.css in Resources */, + 7C51654620139237004D1F4D /* Main.storyboard in Resources */, + 7C51655520153180004D1F4D /* main.js in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 9BAB41D26FC0095C7C86B9DE /* [CP] Copy Pods Resources */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "[CP] Copy Pods Resources"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-cleverrc/Pods-cleverrc-resources.sh\"\n"; + showEnvVarsInLog = 0; + }; + 9F096121C4A02BCE9D4FD1B9 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-cleverrc-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; + A37DBBAD5E44E632F8A8A204 /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "${SRCROOT}/Pods/Target Support Files/Pods-cleverrc/Pods-cleverrc-frameworks.sh", + "${BUILT_PRODUCTS_DIR}/MarqueeLabel/MarqueeLabel.framework", + "${BUILT_PRODUCTS_DIR}/NotificationBannerSwift/NotificationBannerSwift.framework", + "${BUILT_PRODUCTS_DIR}/SnapKit/SnapKit.framework", + "${BUILT_PRODUCTS_DIR}/SwiftSocket/SwiftSocket.framework", + ); + name = "[CP] Embed Pods Frameworks"; + outputPaths = ( + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/MarqueeLabel.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/NotificationBannerSwift.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/SnapKit.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/SwiftSocket.framework", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-cleverrc/Pods-cleverrc-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 7C51653920139237004D1F4D /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 7C51654320139237004D1F4D /* ViewController.swift in Sources */, + 7C51654120139237004D1F4D /* AppDelegate.swift in Sources */, + 7C0AB7AB202A744400BAED27 /* BinUtils.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXVariantGroup section */ + 7C51654420139237004D1F4D /* Main.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 7C51654520139237004D1F4D /* Base */, + ); + name = Main.storyboard; + path = .; + sourceTree = ""; + }; + 7C51654920139237004D1F4D /* LaunchScreen.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 7C51654A20139237004D1F4D /* Base */, + ); + name = LaunchScreen.storyboard; + path = .; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 7C51654D20139237004D1F4D /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + CODE_SIGN_IDENTITY = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + 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 = 11.2; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + 7C51654E20139237004D1F4D /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + CODE_SIGN_IDENTITY = "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 = gnu11; + 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 = 11.2; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 7C51655020139237004D1F4D /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = CB200F4B933204EA97E0E2E4 /* Pods-cleverrc.debug.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_TEAM = 7QY6KJ2672; + INFOPLIST_FILE = cleverrc/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = coex.cleverrc; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 4.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 7C51655120139237004D1F4D /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = AAC9195BF3A9BF6942EF4D0B /* Pods-cleverrc.release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_TEAM = 7QY6KJ2672; + INFOPLIST_FILE = cleverrc/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = coex.cleverrc; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 4.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 7C51653820139237004D1F4D /* Build configuration list for PBXProject "cleverrc" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 7C51654D20139237004D1F4D /* Debug */, + 7C51654E20139237004D1F4D /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 7C51654F20139237004D1F4D /* Build configuration list for PBXNativeTarget "cleverrc" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 7C51655020139237004D1F4D /* Debug */, + 7C51655120139237004D1F4D /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 7C51653520139237004D1F4D /* Project object */; +} diff --git a/apps/ios/cleverrc.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/apps/ios/cleverrc.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 00000000..a82027a8 --- /dev/null +++ b/apps/ios/cleverrc.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/apps/ios/cleverrc.xcworkspace/contents.xcworkspacedata b/apps/ios/cleverrc.xcworkspace/contents.xcworkspacedata new file mode 100644 index 00000000..e6d7f535 --- /dev/null +++ b/apps/ios/cleverrc.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,10 @@ + + + + + + + diff --git a/apps/ios/cleverrc/AppDelegate.swift b/apps/ios/cleverrc/AppDelegate.swift new file mode 100644 index 00000000..981c369a --- /dev/null +++ b/apps/ios/cleverrc/AppDelegate.swift @@ -0,0 +1,46 @@ +// +// AppDelegate.swift +// cleverrc +// +// Created by Oleg Kalachev on 20.01.2018. +// Copyright © 2018 Copter Express. All rights reserved. +// + +import UIKit + +@UIApplicationMain +class AppDelegate: UIResponder, UIApplicationDelegate { + + var window: UIWindow? + + + func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool { + // Override point for customization after application launch. + return true + } + + func applicationWillResignActive(_ application: UIApplication) { + // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state. + // Use this method to pause ongoing tasks, disable timers, and invalidate graphics rendering callbacks. Games should use this method to pause the game. + } + + func applicationDidEnterBackground(_ application: UIApplication) { + // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later. + // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits. + } + + func applicationWillEnterForeground(_ application: UIApplication) { + // Called as part of the transition from the background to the active state; here you can undo many of the changes made on entering the background. + } + + func applicationDidBecomeActive(_ application: UIApplication) { + // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface. + } + + func applicationWillTerminate(_ application: UIApplication) { + // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:. + } + + +} + diff --git a/apps/ios/cleverrc/Assets.xcassets/AppIcon.appiconset/Contents.json b/apps/ios/cleverrc/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 00000000..dbbe908d --- /dev/null +++ b/apps/ios/cleverrc/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,100 @@ +{ + "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" : "cleverios180.png", + "scale" : "2x" + }, + { + "size" : "60x60", + "idiom" : "iphone", + "filename" : "cleverios180-1.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" + }, + { + "idiom" : "ios-marketing", + "size" : "1024x1024", + "scale" : "1x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/apps/ios/cleverrc/Assets.xcassets/AppIcon.appiconset/cleverios180-1.png b/apps/ios/cleverrc/Assets.xcassets/AppIcon.appiconset/cleverios180-1.png new file mode 100644 index 0000000000000000000000000000000000000000..ae590b79e3ed6c0b512d0dc88fc9ea4bf7687021 GIT binary patch literal 3344 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 TNAli895P)kxF%ZN@!7utxT#&9 literal 0 HcmV?d00001 diff --git a/apps/ios/cleverrc/Assets.xcassets/AppIcon.appiconset/cleverios180.png b/apps/ios/cleverrc/Assets.xcassets/AppIcon.appiconset/cleverios180.png new file mode 100644 index 0000000000000000000000000000000000000000..ae590b79e3ed6c0b512d0dc88fc9ea4bf7687021 GIT binary patch literal 3344 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 TNAli895P)kxF%ZN@!7utxT#&9 literal 0 HcmV?d00001 diff --git a/apps/ios/cleverrc/Assets.xcassets/Contents.json b/apps/ios/cleverrc/Assets.xcassets/Contents.json new file mode 100644 index 00000000..da4a164c --- /dev/null +++ b/apps/ios/cleverrc/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/apps/ios/cleverrc/Assets.xcassets/Image.imageset/Contents.json b/apps/ios/cleverrc/Assets.xcassets/Image.imageset/Contents.json new file mode 100644 index 00000000..f8f827e4 --- /dev/null +++ b/apps/ios/cleverrc/Assets.xcassets/Image.imageset/Contents.json @@ -0,0 +1,20 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/apps/ios/cleverrc/Base.lproj/LaunchScreen.storyboard b/apps/ios/cleverrc/Base.lproj/LaunchScreen.storyboard new file mode 100644 index 00000000..19d54bf0 --- /dev/null +++ b/apps/ios/cleverrc/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/ios/cleverrc/Base.lproj/Main.storyboard b/apps/ios/cleverrc/Base.lproj/Main.storyboard new file mode 100644 index 00000000..0afff7f1 --- /dev/null +++ b/apps/ios/cleverrc/Base.lproj/Main.storyboard @@ -0,0 +1,46 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/ios/cleverrc/BinUtils.swift b/apps/ios/cleverrc/BinUtils.swift new file mode 100644 index 00000000..422cd7c3 --- /dev/null +++ b/apps/ios/cleverrc/BinUtils.swift @@ -0,0 +1,453 @@ +// +// BinUtils.swift +// BinUtils +// +// Created by Nicolas Seriot on 12/03/16. +// Copyright © 2016 Nicolas Seriot. All rights reserved. +// + +import Foundation +import CoreFoundation + +// MARK: protocol UnpackedType + +public protocol Unpackable {} + +extension NSString: Unpackable {} +extension Bool: Unpackable {} +extension Int: Unpackable {} +extension Double: Unpackable {} + +// MARK: protocol DataConvertible + +protocol DataConvertible {} + +extension DataConvertible { + + init?(data: Data) { + guard data.count == MemoryLayout.size else { return nil } + self = data.withUnsafeBytes { $0.pointee } + } + + init?(bytes: [UInt8]) { + let data = Data(bytes:bytes) + self.init(data:data) + } + + var data: Data { + var value = self + return Data(buffer: UnsafeBufferPointer(start: &value, count: 1)) + } +} + +extension Bool : DataConvertible { } + +extension Int8 : DataConvertible { } +extension Int16 : DataConvertible { } +extension Int32 : DataConvertible { } +extension Int64 : DataConvertible { } + +extension UInt8 : DataConvertible { } +extension UInt16 : DataConvertible { } +extension UInt32 : DataConvertible { } +extension UInt64 : DataConvertible { } + +extension Float32 : DataConvertible { } +extension Float64 : DataConvertible { } + +// MARK: String extension + +extension String { + subscript (from:Int, to:Int) -> String { + return NSString(string: self).substring(with: NSMakeRange(from, to-from)) + } +} + +// MARK: Data extension + +extension Data { + var bytes : [UInt8] { + return self.withUnsafeBytes { + [UInt8](UnsafeBufferPointer(start: $0, count: self.count)) + } + } +} + +// MARK: functions + +public func hexlify(_ data:Data) -> String { + + // similar to hexlify() in Python's binascii module + // https://docs.python.org/2/library/binascii.html + + var s = String() + var byte: UInt8 = 0 + + for i in 0 ..< data.count { + NSData(data: data).getBytes(&byte, range: NSMakeRange(i, 1)) + s = s.appendingFormat("%02x", byte) + } + + return s as String +} + +public func unhexlify(_ string:String) -> Data? { + + // similar to unhexlify() in Python's binascii module + // https://docs.python.org/2/library/binascii.html + + let s = string.uppercased().replacingOccurrences(of: " ", with: "") + + let nonHexCharacterSet = CharacterSet(charactersIn: "0123456789ABCDEF").inverted + if let range = s.rangeOfCharacter(from: nonHexCharacterSet) { + print("-- found non hex character at range \(range)") + return nil + } + + var data = Data(capacity: s.count / 2) + + for i in stride(from: 0, to:s.count, by:2) { + let byteString = s[i, i+2] + let byte = UInt8(byteString.withCString { strtoul($0, nil, 16) }) + data.append([byte] as [UInt8], count: 1) + } + + return data +} + +func readIntegerType(_ type:T.Type, bytes:[UInt8], loc:inout Int) -> T { + let size = MemoryLayout.size + let sub = Array(bytes[loc..<(loc+size)]) + loc += size + return T(bytes: sub)! +} + +func readFloatingPointType(_ type:T.Type, bytes:[UInt8], loc:inout Int, isBigEndian:Bool) -> T { + let size = MemoryLayout.size + let sub = Array(bytes[loc..<(loc+size)]) + loc += size + let sub_ = isBigEndian ? sub.reversed() : sub + return T(bytes: sub_)! +} + +func isBigEndianFromMandatoryByteOrderFirstCharacter(_ format:String) -> Bool { + + guard let firstChar = format.first else { assertionFailure("empty format"); return false } + + let s = NSString(string: String(firstChar)) + let c = s.substring(to: 1) + + if c == "@" { assertionFailure("native size and alignment is unsupported") } + + if c == "=" || c == "<" { return false } + if c == ">" || c == "!" { return true } + + assertionFailure("format '\(format)' first character must be among '=<>!'") + + return false +} + +// akin to struct.calcsize(fmt) +func numberOfBytesInFormat(_ format:String) -> Int { + + var numberOfBytes = 0 + + var n = 0 // repeat counter + + var mutableFormat = format + + while !mutableFormat.isEmpty { + + let c = mutableFormat.remove(at: mutableFormat.startIndex) + + if let i = Int(String(c)) , 0...9 ~= i { + if n > 0 { n *= 10 } + n += i + continue + } + + if c == "s" { + numberOfBytes += max(n,1) + n = 0 + continue + } + + for _ in 0..", "!", " ": + () + case "c", "b", "B", "x", "?": + numberOfBytes += 1 + case "h", "H": + numberOfBytes += 2 + case "i", "l", "I", "L", "f": + numberOfBytes += 4 + case "q", "Q", "d": + numberOfBytes += 8 + case "P": + numberOfBytes += MemoryLayout.size + default: + assertionFailure("-- unsupported format \(c)") + } + } + + n = 0 + } + + return numberOfBytes +} + +func formatDoesMatchDataLength(_ format:String, data:Data) -> Bool { + let sizeAccordingToFormat = numberOfBytesInFormat(format) + let dataLength = data.count + if sizeAccordingToFormat != dataLength { + print("format \"\(format)\" expects \(sizeAccordingToFormat) bytes but data is \(dataLength) bytes") + return false + } + + return true +} + +/* + pack() and unpack() should behave as Python's struct module https://docs.python.org/2/library/struct.html BUT: + - native size and alignment '@' is not supported + - as a consequence, the byte order specifier character is mandatory and must be among "=<>!" + - native byte order '=' assumes a little-endian system (eg. Intel x86) + - Pascal strings 'p' and native pointers 'P' are not supported + */ + +public enum BinUtilsError: Error { + case formatDoesMatchDataLength(format:String, dataSize:Int) + case unsupportedFormat(character:Character) +} + +public func pack(_ format:String, _ objects:[Any], _ stringEncoding:String.Encoding=String.Encoding.windowsCP1252) -> Data { + + var objectsQueue = objects + + var mutableFormat = format + + var mutableData = Data() + + var isBigEndian = false + + let firstCharacter = mutableFormat.remove(at: mutableFormat.startIndex) + + switch(firstCharacter) { + case "<", "=": + isBigEndian = false + case ">", "!": + isBigEndian = true + case "@": + assertionFailure("native size and alignment '@' is unsupported'") + default: + assertionFailure("unsupported format chacracter'") + } + + var n = 0 // repeat counter + + while !mutableFormat.isEmpty { + + let c = mutableFormat.remove(at: mutableFormat.startIndex) + + if let i = Int(String(c)) , 0...9 ~= i { + if n > 0 { n *= 10 } + n += i + continue + } + + var o : Any = 0 + + if c == "s" { + o = objectsQueue.remove(at: 0) + + guard let stringData = (o as! String).data(using: .utf8) else { assertionFailure(); return Data() } + var bytes = stringData.bytes + + let expectedSize = max(1, n) + + // pad ... + while bytes.count < expectedSize { bytes.append(0x00) } + + // ... or trunk + if bytes.count > expectedSize { bytes = Array(bytes[0.. [Unpackable] { + + assert(CFByteOrderGetCurrent() == 1 /* CFByteOrderLittleEndian */, "\(#file) assumes little endian, but host is big endian") + + let isBigEndian = isBigEndianFromMandatoryByteOrderFirstCharacter(format) + + if formatDoesMatchDataLength(format, data: data) == false { + throw BinUtilsError.formatDoesMatchDataLength(format:format, dataSize:data.count) + } + + var a : [Unpackable] = [] + + var loc = 0 + + let bytes = data.bytes + + var n = 0 // repeat counter + + var mutableFormat = format + + mutableFormat.remove(at: mutableFormat.startIndex) // consume byte-order specifier + + while !mutableFormat.isEmpty { + + let c = mutableFormat.remove(at: mutableFormat.startIndex) + + if let i = Int(String(c)) , 0...9 ~= i { + if n > 0 { n *= 10 } + n += i + continue + } + + if c == "s" { + let length = max(n,1) + let sub = Array(bytes[loc.. + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleDisplayName + Clever RC + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + APPL + CFBundleShortVersionString + 1.0 + CFBundleVersion + 1 + LSRequiresIPhoneOS + + UILaunchStoryboardName + LaunchScreen + UIMainStoryboardFile + Main + UIRequiredDeviceCapabilities + + armv7 + + UISupportedInterfaceOrientations + + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + + diff --git a/apps/ios/cleverrc/ViewController.swift b/apps/ios/cleverrc/ViewController.swift new file mode 100644 index 00000000..5eae6b3f --- /dev/null +++ b/apps/ios/cleverrc/ViewController.swift @@ -0,0 +1,70 @@ +// +// ViewController.swift +// cleverrc +// +// Created by Oleg Kalachev on 20.01.2018. +// Copyright © 2018 Copter Express. All rights reserved. +// + +import UIKit +import WebKit +import SwiftSocket +import NotificationBannerSwift + +class ViewController: UIViewController, WKScriptMessageHandler { + @IBOutlet weak var webView: WKWebView! + let impactGenerator = UIImpactFeedbackGenerator(style: .medium) + let notificationGenerator = UINotificationFeedbackGenerator() + let udpSocket = UDPClient(address:"255.255.255.255", port: 35602) + + override func viewDidLoad() { + super.viewDidLoad() + + // Don't lock screen + UIApplication.shared.isIdleTimerDisabled = true + + // Setup webview event handlers + webView.configuration.userContentController.add(self, name: "control") + webView.configuration.userContentController.add(self, name: "controlStart") + webView.configuration.userContentController.add(self, name: "lowBattery") + webView.configuration.userContentController.add(self, name: "notification") + + // Load the main page + let url = Bundle.main.url(forResource: "index", withExtension: "html") + let requestObj = URLRequest(url: url!) + webView.load(requestObj) + + // Setup UDP broadcasting + udpSocket.enableBroadcast() + } + + override func didReceiveMemoryWarning() { + super.didReceiveMemoryWarning() + // Dispose of any resources that can be recreated. + } + + func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) { + if (message.name == "control") { + // Send UDP control message + let m = message.body as! NSDictionary; + let d = pack(" + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/ios/cleverrc/index.html b/apps/ios/cleverrc/index.html new file mode 100644 index 00000000..391ef631 --- /dev/null +++ b/apps/ios/cleverrc/index.html @@ -0,0 +1,23 @@ + + + + + + + + +
DISCONNECTED
+
+ +
+
+
+
+
+
+
+
+ + + + diff --git a/apps/ios/cleverrc/main.css b/apps/ios/cleverrc/main.css new file mode 100644 index 00000000..c9a6dc11 --- /dev/null +++ b/apps/ios/cleverrc/main.css @@ -0,0 +1,91 @@ +html, body { + margin: 0; + padding: 0; + user-select: none; + font-family: sans-serif; + background: #212121; + color: rgba(255, 255, 255, 0.9); +} + +.stick { + border-radius: 50%; + width: 5cm; + height: 5cm; + position: relative; + transform: translateZ(0); + border: 4px solid rgba(255,255,255,.4); + box-shadow: 0 0 0 1px rgba(0,0,0,.2), inset 0 0 0 1px rgba(0,0,0,.2); +} + +.stick-pointer { + position: absolute; + border-radius: 50%; + background-color: rgba(255,255,255,.25); + box-shadow: 0 0 10px rgba(0,0,0,.3); + width: 3cm; + height: 3cm; + margin-left: -1.5cm; + margin-top: -1.5cm; + top: 2.5cm; + left: 2.5cm; + pointer-events: none; + transform: translateZ(0); +} + +.container { + display: flex; + justify-content: space-around; + align-items: center; + width: 100%; + height: 100%; +} + +.telemetry { + position: absolute; + text-align: center; + width: 100%; + top: 30px; + font-size: 20px; + user-select: none; + pointer-events: none; +} + +body.armed .telemetry .mode { + font-weight: bold; +} + +@keyframes scale { + 0% { transform: scale(1.0); } + 50% { transform: scale(1.2); } + 100% { transform: scale(1.0); } +} + +.battery { + position: absolute; + text-align: center; + width: 100%; + bottom: 30px; + font-size: 20px; + user-select: none; + pointer-events: none; +} + +body.low-battery .battery { + color: #ff554b; + animation: scale 0.3s 1 ease-in-out} + +.logo { + position: absolute; + background: url(clever.svg); + -webkit-background-size: 50px; + background-size: 50px; + width: 50px; + height: 50px; + top: 50%; + left: 50%; + margin-top: -25px; + margin-left: -25px; + font-size: 20px; + user-select: none; + pointer-events: none; +} diff --git a/apps/ios/cleverrc/main.js b/apps/ios/cleverrc/main.js new file mode 100644 index 00000000..75c94506 --- /dev/null +++ b/apps/ios/cleverrc/main.js @@ -0,0 +1,126 @@ +function throttle(func, ms) { + var isThrottled = false, + savedArgs, + savedThis; + + function wrapper() { + if (isThrottled) { + savedArgs = arguments; + savedThis = this; + return; + } + func.apply(this, arguments); + isThrottled = true; + setTimeout(function() { + isThrottled = false; + if (savedArgs) { + wrapper.apply(savedThis, savedArgs); + savedArgs = savedThis = null; + } + }, ms); + } + return wrapper; +} + +function callNativeApp(name, msg) { + try { + webkit.messageHandlers[name].postMessage(msg); + return true; + } catch(err) { + console.warn('The native context does not exist yet'); + return false; + } +} + +var rcLastPublish = null; + +function rcPublish() { + callNativeApp('control', controlMessage); + rcLastPublish = new Date(); +} + +rcPublishThrottled = throttle(rcPublish, 30); + +setInterval(function() { + if (rcLastPublish !== null && new Date() - rcLastPublish > 800) { + rcPublishThrottled(); + } +}, 50); + +var body = document.querySelector('body'); +var stickLeft = document.querySelector('.stick-left'); +var stickRight = document.querySelector('.stick-right'); + +var controlMessage = { x: 0, y: 0, z: 0, r: 0 }; + +function onStickTouchMove(touch) { + var target = touch.target; + var targetRect = target.getBoundingClientRect(); + var stickPointer = target.querySelector('.stick-pointer'); + + var offsetX = touch.clientX - targetRect.left; + var offsetY = touch.clientY - targetRect.top; + + var x = 2 * offsetX / targetRect.width; + var y = 2 * offsetY / targetRect.height; + + x = Math.max(0, x); + x = Math.min(2, x); + y = Math.max(0, y); + y = Math.min(2, y); + + stickPointer.style.left = (x * 50) + '%'; + stickPointer.style.top = (y * 50) + '%'; + + x -= 1; + y = 1 - y; + + if (target.matches('.stick-left')) { + controlMessage.z = Math.round((y + 1) * 500); + controlMessage.r = Math.round(x * 1000); + } else if (target.matches('.stick-right')) { + controlMessage.x = Math.round(y * 1000); + controlMessage.y = Math.round(x * 1000); + } +} + +body.addEventListener('touchmove', function (e) { + e.preventDefault(); +}); + +function stickTouchStart(e) { + setControlMode(); + callNativeApp('controlStart'); + onStickTouchMove(e.changedTouches[0]); + rcPublishThrottled(); + e.stopPropagation(); + e.preventDefault(); +} + +function stickTouchMove(e) { + onStickTouchMove(e.changedTouches[0]); + rcPublishThrottled(); + e.stopPropagation(); + e.preventDefault(); +} + +function stickTouchEnd(e) { + var pointer = e.target.querySelector('.stick-pointer'); + if (e.target.matches('.stick-left')) { + controlMessage.r = 0; + pointer.style.left = '50%'; + } else if (e.target.matches('.stick-right')) { + controlMessage.x = 0; + controlMessage.y = 0; + pointer.style.left = '50%'; + pointer.style.top = '50%'; + } + rcPublishThrottled(); +} + +stickLeft.addEventListener('touchmove', stickTouchMove); +stickRight.addEventListener('touchmove', stickTouchMove); +stickLeft.addEventListener('touchstart', stickTouchStart); +stickRight.addEventListener('touchstart', stickTouchStart); +stickLeft.addEventListener('touchend', stickTouchEnd); +stickRight.addEventListener('touchend', stickTouchEnd); diff --git a/apps/ios/cleverrc/roslib.js b/apps/ios/cleverrc/roslib.js new file mode 100644 index 00000000..2193da84 --- /dev/null +++ b/apps/ios/cleverrc/roslib.js @@ -0,0 +1,3693 @@ +(function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o 0 && + tree._listeners.length > this._events.maxListeners + ) { + tree._listeners.warned = true; + logPossibleMemoryLeak.call(this, tree._listeners.length, name); + } + } + return true; + } + name = type.shift(); + } + return true; + } + + // By default EventEmitters will print a warning if more than + // 10 listeners are added to it. This is a useful default which + // helps finding memory leaks. + // + // Obviously not all Emitters should be limited to 10. This function allows + // that to be increased. Set to zero for unlimited. + + EventEmitter.prototype.delimiter = '.'; + + EventEmitter.prototype.setMaxListeners = function(n) { + if (n !== undefined) { + this._events || init.call(this); + this._events.maxListeners = n; + if (!this._conf) this._conf = {}; + this._conf.maxListeners = n; + } + }; + + EventEmitter.prototype.event = ''; + + EventEmitter.prototype.once = function(event, fn) { + this.many(event, 1, fn); + return this; + }; + + EventEmitter.prototype.many = function(event, ttl, fn) { + var self = this; + + if (typeof fn !== 'function') { + throw new Error('many only accepts instances of Function'); + } + + function listener() { + if (--ttl === 0) { + self.off(event, listener); + } + fn.apply(this, arguments); + } + + listener._origin = fn; + + this.on(event, listener); + + return self; + }; + + EventEmitter.prototype.emit = function() { + + this._events || init.call(this); + + var type = arguments[0]; + + if (type === 'newListener' && !this.newListener) { + if (!this._events.newListener) { + return false; + } + } + + var al = arguments.length; + var args,l,i,j; + var handler; + + if (this._all && this._all.length) { + handler = this._all.slice(); + if (al > 3) { + args = new Array(al); + for (j = 0; j < al; j++) args[j] = arguments[j]; + } + + for (i = 0, l = handler.length; i < l; i++) { + this.event = type; + switch (al) { + case 1: + handler[i].call(this, type); + break; + case 2: + handler[i].call(this, type, arguments[1]); + break; + case 3: + handler[i].call(this, type, arguments[1], arguments[2]); + break; + default: + handler[i].apply(this, args); + } + } + } + + if (this.wildcard) { + handler = []; + var ns = typeof type === 'string' ? type.split(this.delimiter) : type.slice(); + searchListenerTree.call(this, handler, ns, this.listenerTree, 0); + } else { + handler = this._events[type]; + if (typeof handler === 'function') { + this.event = type; + switch (al) { + case 1: + handler.call(this); + break; + case 2: + handler.call(this, arguments[1]); + break; + case 3: + handler.call(this, arguments[1], arguments[2]); + break; + default: + args = new Array(al - 1); + for (j = 1; j < al; j++) args[j - 1] = arguments[j]; + handler.apply(this, args); + } + return true; + } else if (handler) { + // need to make copy of handlers because list can change in the middle + // of emit call + handler = handler.slice(); + } + } + + if (handler && handler.length) { + if (al > 3) { + args = new Array(al - 1); + for (j = 1; j < al; j++) args[j - 1] = arguments[j]; + } + for (i = 0, l = handler.length; i < l; i++) { + this.event = type; + switch (al) { + case 1: + handler[i].call(this); + break; + case 2: + handler[i].call(this, arguments[1]); + break; + case 3: + handler[i].call(this, arguments[1], arguments[2]); + break; + default: + handler[i].apply(this, args); + } + } + return true; + } else if (!this._all && type === 'error') { + if (arguments[1] instanceof Error) { + throw arguments[1]; // Unhandled 'error' event + } else { + throw new Error("Uncaught, unspecified 'error' event."); + } + return false; + } + + return !!this._all; + }; + + EventEmitter.prototype.emitAsync = function() { + + this._events || init.call(this); + + var type = arguments[0]; + + if (type === 'newListener' && !this.newListener) { + if (!this._events.newListener) { return Promise.resolve([false]); } + } + + var promises= []; + + var al = arguments.length; + var args,l,i,j; + var handler; + + if (this._all) { + if (al > 3) { + args = new Array(al); + for (j = 1; j < al; j++) args[j] = arguments[j]; + } + for (i = 0, l = this._all.length; i < l; i++) { + this.event = type; + switch (al) { + case 1: + promises.push(this._all[i].call(this, type)); + break; + case 2: + promises.push(this._all[i].call(this, type, arguments[1])); + break; + case 3: + promises.push(this._all[i].call(this, type, arguments[1], arguments[2])); + break; + default: + promises.push(this._all[i].apply(this, args)); + } + } + } + + if (this.wildcard) { + handler = []; + var ns = typeof type === 'string' ? type.split(this.delimiter) : type.slice(); + searchListenerTree.call(this, handler, ns, this.listenerTree, 0); + } else { + handler = this._events[type]; + } + + if (typeof handler === 'function') { + this.event = type; + switch (al) { + case 1: + promises.push(handler.call(this)); + break; + case 2: + promises.push(handler.call(this, arguments[1])); + break; + case 3: + promises.push(handler.call(this, arguments[1], arguments[2])); + break; + default: + args = new Array(al - 1); + for (j = 1; j < al; j++) args[j - 1] = arguments[j]; + promises.push(handler.apply(this, args)); + } + } else if (handler && handler.length) { + if (al > 3) { + args = new Array(al - 1); + for (j = 1; j < al; j++) args[j - 1] = arguments[j]; + } + for (i = 0, l = handler.length; i < l; i++) { + this.event = type; + switch (al) { + case 1: + promises.push(handler[i].call(this)); + break; + case 2: + promises.push(handler[i].call(this, arguments[1])); + break; + case 3: + promises.push(handler[i].call(this, arguments[1], arguments[2])); + break; + default: + promises.push(handler[i].apply(this, args)); + } + } + } else if (!this._all && type === 'error') { + if (arguments[1] instanceof Error) { + return Promise.reject(arguments[1]); // Unhandled 'error' event + } else { + return Promise.reject("Uncaught, unspecified 'error' event."); + } + } + + return Promise.all(promises); + }; + + EventEmitter.prototype.on = function(type, listener) { + if (typeof type === 'function') { + this.onAny(type); + return this; + } + + if (typeof listener !== 'function') { + throw new Error('on only accepts instances of Function'); + } + this._events || init.call(this); + + // To avoid recursion in the case that type == "newListeners"! Before + // adding it to the listeners, first emit "newListeners". + this.emit('newListener', type, listener); + + if (this.wildcard) { + growListenerTree.call(this, type, listener); + return this; + } + + if (!this._events[type]) { + // Optimize the case of one listener. Don't need the extra array object. + this._events[type] = listener; + } + else { + if (typeof this._events[type] === 'function') { + // Change to array. + this._events[type] = [this._events[type]]; + } + + // If we've already got an array, just append. + this._events[type].push(listener); + + // Check for listener leak + if ( + !this._events[type].warned && + this._events.maxListeners > 0 && + this._events[type].length > this._events.maxListeners + ) { + this._events[type].warned = true; + logPossibleMemoryLeak.call(this, this._events[type].length, type); + } + } + + return this; + }; + + EventEmitter.prototype.onAny = function(fn) { + if (typeof fn !== 'function') { + throw new Error('onAny only accepts instances of Function'); + } + + if (!this._all) { + this._all = []; + } + + // Add the function to the event listener collection. + this._all.push(fn); + return this; + }; + + EventEmitter.prototype.addListener = EventEmitter.prototype.on; + + EventEmitter.prototype.off = function(type, listener) { + if (typeof listener !== 'function') { + throw new Error('removeListener only takes instances of Function'); + } + + var handlers,leafs=[]; + + if(this.wildcard) { + var ns = typeof type === 'string' ? type.split(this.delimiter) : type.slice(); + leafs = searchListenerTree.call(this, null, ns, this.listenerTree, 0); + } + else { + // does not use listeners(), so no side effect of creating _events[type] + if (!this._events[type]) return this; + handlers = this._events[type]; + leafs.push({_listeners:handlers}); + } + + for (var iLeaf=0; iLeaf 0) { + recursivelyGarbageCollect(root[key]); + } + if (Object.keys(obj).length === 0) { + delete root[key]; + } + } + } + recursivelyGarbageCollect(this.listenerTree); + + return this; + }; + + EventEmitter.prototype.offAny = function(fn) { + var i = 0, l = 0, fns; + if (fn && this._all && this._all.length > 0) { + fns = this._all; + for(i = 0, l = fns.length; i < l; i++) { + if(fn === fns[i]) { + fns.splice(i, 1); + this.emit("removeListenerAny", fn); + return this; + } + } + } else { + fns = this._all; + for(i = 0, l = fns.length; i < l; i++) + this.emit("removeListenerAny", fns[i]); + this._all = []; + } + return this; + }; + + EventEmitter.prototype.removeListener = EventEmitter.prototype.off; + + EventEmitter.prototype.removeAllListeners = function(type) { + if (arguments.length === 0) { + !this._events || init.call(this); + return this; + } + + if (this.wildcard) { + var ns = typeof type === 'string' ? type.split(this.delimiter) : type.slice(); + var leafs = searchListenerTree.call(this, null, ns, this.listenerTree, 0); + + for (var iLeaf=0; iLeaf t2.secs) { + return false; + } else if(t1.secs < t2.secs) { + return true; + } else if(t1.nsecs < t2.nsecs) { + return true; + } else { + return false; + } + }; + + // TODO: this may be more complicated than necessary, since I'm + // not sure if the callbacks can ever wind up with a scenario + // where we've been preempted by a next goal, it hasn't finished + // processing, and then we get a cancel message + cancelListener.subscribe(function(cancelMessage) { + + // cancel ALL goals if both empty + if(cancelMessage.stamp.secs === 0 && cancelMessage.stamp.secs === 0 && cancelMessage.id === '') { + that.nextGoal = null; + if(that.currentGoal) { + that.emit('cancel'); + } + } else { // treat id and stamp independently + if(that.currentGoal && cancelMessage.id === that.currentGoal.goal_id.id) { + that.emit('cancel'); + } else if(that.nextGoal && cancelMessage.id === that.nextGoal.goal_id.id) { + that.nextGoal = null; + } + + if(that.nextGoal && isEarlier(that.nextGoal.goal_id.stamp, + cancelMessage.stamp)) { + that.nextGoal = null; + } + if(that.currentGoal && isEarlier(that.currentGoal.goal_id.stamp, + cancelMessage.stamp)) { + + that.emit('cancel'); + } + } + }); + + // publish status at pseudo-fixed rate; required for clients to know they've connected + var statusInterval = setInterval( function() { + var currentTime = new Date(); + var secs = Math.floor(currentTime.getTime()/1000); + var nsecs = Math.round(1000000000*(currentTime.getTime()/1000-secs)); + that.statusMessage.header.stamp.secs = secs; + that.statusMessage.header.stamp.nsecs = nsecs; + statusPublisher.publish(that.statusMessage); + }, 500); // publish every 500ms + +} + +SimpleActionServer.prototype.__proto__ = EventEmitter2.prototype; + +/** +* Set action state to succeeded and return to client +*/ + +SimpleActionServer.prototype.setSucceeded = function(result2) { + + + var resultMessage = new Message({ + status : {goal_id : this.currentGoal.goal_id, status : 3}, + result : result2 + }); + this.resultPublisher.publish(resultMessage); + + this.statusMessage.status_list = []; + if(this.nextGoal) { + this.currentGoal = this.nextGoal; + this.nextGoal = null; + this.emit('goal', this.currentGoal.goal); + } else { + this.currentGoal = null; + } +}; + +/** +* Function to send feedback +*/ + +SimpleActionServer.prototype.sendFeedback = function(feedback2) { + + var feedbackMessage = new Message({ + status : {goal_id : this.currentGoal.goal_id, status : 1}, + feedback : feedback2 + }); + this.feedbackPublisher.publish(feedbackMessage); +}; + +/** +* Handle case where client requests preemption +*/ + +SimpleActionServer.prototype.setPreempted = function() { + + this.statusMessage.status_list = []; + var resultMessage = new Message({ + status : {goal_id : this.currentGoal.goal_id, status : 2}, + }); + this.resultPublisher.publish(resultMessage); + + if(this.nextGoal) { + this.currentGoal = this.nextGoal; + this.nextGoal = null; + this.emit('goal', this.currentGoal.goal); + } else { + this.currentGoal = null; + } +}; + +module.exports = SimpleActionServer; +},{"../core/Message":10,"../core/Topic":17,"eventemitter2":1}],9:[function(require,module,exports){ +var Ros = require('../core/Ros'); +var mixin = require('../mixin'); + +var action = module.exports = { + ActionClient: require('./ActionClient'), + ActionListener: require('./ActionListener'), + Goal: require('./Goal'), + SimpleActionServer: require('./SimpleActionServer') +}; + +mixin(Ros, ['ActionClient', 'SimpleActionServer'], action); + +},{"../core/Ros":12,"../mixin":24,"./ActionClient":5,"./ActionListener":6,"./Goal":7,"./SimpleActionServer":8}],10:[function(require,module,exports){ +/** + * @fileoverview + * @author Brandon Alexander - baalexander@gmail.com + */ + +var assign = require('object-assign'); + +/** + * Message objects are used for publishing and subscribing to and from topics. + * + * @constructor + * @param values - object matching the fields defined in the .msg definition file + */ +function Message(values) { + assign(this, values); +} + +module.exports = Message; +},{"object-assign":2}],11:[function(require,module,exports){ +/** + * @fileoverview + * @author Brandon Alexander - baalexander@gmail.com + */ + +var Service = require('./Service'); +var ServiceRequest = require('./ServiceRequest'); + +/** + * A ROS parameter. + * + * @constructor + * @param options - possible keys include: + * * ros - the ROSLIB.Ros connection handle + * * name - the param name, like max_vel_x + */ +function Param(options) { + options = options || {}; + this.ros = options.ros; + this.name = options.name; +} + +/** + * Fetches the value of the param. + * + * @param callback - function with the following params: + * * value - the value of the param from ROS. + */ +Param.prototype.get = function(callback) { + var paramClient = new Service({ + ros : this.ros, + name : '/rosapi/get_param', + serviceType : 'rosapi/GetParam' + }); + + var request = new ServiceRequest({ + name : this.name + }); + + paramClient.callService(request, function(result) { + var value = JSON.parse(result.value); + callback(value); + }); +}; + +/** + * Sets the value of the param in ROS. + * + * @param value - value to set param to. + */ +Param.prototype.set = function(value, callback) { + var paramClient = new Service({ + ros : this.ros, + name : '/rosapi/set_param', + serviceType : 'rosapi/SetParam' + }); + + var request = new ServiceRequest({ + name : this.name, + value : JSON.stringify(value) + }); + + paramClient.callService(request, callback); +}; + +/** + * Delete this parameter on the ROS server. + */ +Param.prototype.delete = function(callback) { + var paramClient = new Service({ + ros : this.ros, + name : '/rosapi/delete_param', + serviceType : 'rosapi/DeleteParam' + }); + + var request = new ServiceRequest({ + name : this.name + }); + + paramClient.callService(request, callback); +}; + +module.exports = Param; +},{"./Service":13,"./ServiceRequest":14}],12:[function(require,module,exports){ +/** + * @fileoverview + * @author Brandon Alexander - baalexander@gmail.com + */ + +var WebSocket = require('ws'); +var socketAdapter = require('./SocketAdapter.js'); + +var Service = require('./Service'); +var ServiceRequest = require('./ServiceRequest'); + +var assign = require('object-assign'); +var EventEmitter2 = require('eventemitter2').EventEmitter2; + +/** + * Manages connection to the server and all interactions with ROS. + * + * Emits the following events: + * * 'error' - there was an error with ROS + * * 'connection' - connected to the WebSocket server + * * 'close' - disconnected to the WebSocket server + * * - a message came from rosbridge with the given topic name + * * - a service response came from rosbridge with the given ID + * + * @constructor + * @param options - possible keys include:
+ * * url (optional) - (can be specified later with `connect`) the WebSocket URL for rosbridge or the node server url to connect using socket.io (if socket.io exists in the page)
+ * * groovyCompatibility - don't use interfaces that changed after the last groovy release or rosbridge_suite and related tools (defaults to true) + * * transportLibrary (optional) - one of 'websocket' (default), 'socket.io' or RTCPeerConnection instance controlling how the connection is created in `connect`. + * * transportOptions (optional) - the options to use use when creating a connection. Currently only used if `transportLibrary` is RTCPeerConnection. + */ +function Ros(options) { + options = options || {}; + this.socket = null; + this.idCounter = 0; + this.isConnected = false; + this.transportLibrary = options.transportLibrary || 'websocket'; + this.transportOptions = options.transportOptions || {}; + + if (typeof options.groovyCompatibility === 'undefined') { + this.groovyCompatibility = true; + } + else { + this.groovyCompatibility = options.groovyCompatibility; + } + + // Sets unlimited event listeners. + this.setMaxListeners(0); + + // begin by checking if a URL was given + if (options.url) { + this.connect(options.url); + } +} + +Ros.prototype.__proto__ = EventEmitter2.prototype; + +/** + * Connect to the specified WebSocket. + * + * @param url - WebSocket URL or RTCDataChannel label for Rosbridge + */ +Ros.prototype.connect = function(url) { + if (this.transportLibrary === 'socket.io') { + this.socket = assign(io(url, {'force new connection': true}), socketAdapter(this)); + this.socket.on('connect', this.socket.onopen); + this.socket.on('data', this.socket.onmessage); + this.socket.on('close', this.socket.onclose); + this.socket.on('error', this.socket.onerror); + } else if (this.transportLibrary.constructor.name === 'RTCPeerConnection') { + this.socket = assign(this.transportLibrary.createDataChannel(url, this.transportOptions), socketAdapter(this)); + }else { + this.socket = assign(new WebSocket(url), socketAdapter(this)); + } + +}; + +/** + * Disconnect from the WebSocket server. + */ +Ros.prototype.close = function() { + if (this.socket) { + this.socket.close(); + } +}; + +/** + * Sends an authorization request to the server. + * + * @param mac - MAC (hash) string given by the trusted source. + * @param client - IP of the client. + * @param dest - IP of the destination. + * @param rand - Random string given by the trusted source. + * @param t - Time of the authorization request. + * @param level - User level as a string given by the client. + * @param end - End time of the client's session. + */ +Ros.prototype.authenticate = function(mac, client, dest, rand, t, level, end) { + // create the request + var auth = { + op : 'auth', + mac : mac, + client : client, + dest : dest, + rand : rand, + t : t, + level : level, + end : end + }; + // send the request + this.callOnConnection(auth); +}; + +/** + * Sends the message over the WebSocket, but queues the message up if not yet + * connected. + */ +Ros.prototype.callOnConnection = function(message) { + var that = this; + var messageJson = JSON.stringify(message); + var emitter = null; + if (this.transportLibrary === 'socket.io') { + emitter = function(msg){that.socket.emit('operation', msg);}; + } else { + emitter = function(msg){that.socket.send(msg);}; + } + + if (!this.isConnected) { + that.once('connection', function() { + emitter(messageJson); + }); + } else { + emitter(messageJson); + } +}; + +/** + * Sends a set_level request to the server + * + * @param level - Status level (none, error, warning, info) + * @param id - Optional: Operation ID to change status level on + */ +Ros.prototype.setStatusLevel = function(level, id){ + var levelMsg = { + op: 'set_level', + level: level, + id: id + }; + + this.callOnConnection(levelMsg); +}; + +/** + * Retrieves Action Servers in ROS as an array of string + * + * * actionservers - Array of action server names + */ +Ros.prototype.getActionServers = function(callback, failedCallback) { + var getActionServers = new Service({ + ros : this, + name : '/rosapi/action_servers', + serviceType : 'rosapi/GetActionServers' + }); + + var request = new ServiceRequest({}); + if (typeof failedCallback === 'function'){ + getActionServers.callService(request, + function(result) { + callback(result.action_servers); + }, + function(message){ + failedCallback(message); + } + ); + }else{ + getActionServers.callService(request, function(result) { + callback(result.action_servers); + }); + } +}; + +/** + * Retrieves list of topics in ROS as an array. + * + * @param callback function with params: + * * topics - Array of topic names + */ +Ros.prototype.getTopics = function(callback, failedCallback) { + var topicsClient = new Service({ + ros : this, + name : '/rosapi/topics', + serviceType : 'rosapi/Topics' + }); + + var request = new ServiceRequest(); + if (typeof failedCallback === 'function'){ + topicsClient.callService(request, + function(result) { + callback(result); + }, + function(message){ + failedCallback(message); + } + ); + }else{ + topicsClient.callService(request, function(result) { + callback(result); + }); + } +}; + +/** + * Retrieves Topics in ROS as an array as specific type + * + * @param topicType topic type to find: + * @param callback function with params: + * * topics - Array of topic names + */ +Ros.prototype.getTopicsForType = function(topicType, callback, failedCallback) { + var topicsForTypeClient = new Service({ + ros : this, + name : '/rosapi/topics_for_type', + serviceType : 'rosapi/TopicsForType' + }); + + var request = new ServiceRequest({ + type: topicType + }); + if (typeof failedCallback === 'function'){ + topicsForTypeClient.callService(request, + function(result) { + callback(result.topics); + }, + function(message){ + failedCallback(message); + } + ); + }else{ + topicsForTypeClient.callService(request, function(result) { + callback(result.topics); + }); + } +}; + +/** + * Retrieves list of active service names in ROS. + * + * @param callback - function with the following params: + * * services - array of service names + */ +Ros.prototype.getServices = function(callback, failedCallback) { + var servicesClient = new Service({ + ros : this, + name : '/rosapi/services', + serviceType : 'rosapi/Services' + }); + + var request = new ServiceRequest(); + if (typeof failedCallback === 'function'){ + servicesClient.callService(request, + function(result) { + callback(result.services); + }, + function(message) { + failedCallback(message); + } + ); + }else{ + servicesClient.callService(request, function(result) { + callback(result.services); + }); + } +}; + +/** + * Retrieves list of services in ROS as an array as specific type + * + * @param serviceType service type to find: + * @param callback function with params: + * * topics - Array of service names + */ +Ros.prototype.getServicesForType = function(serviceType, callback, failedCallback) { + var servicesForTypeClient = new Service({ + ros : this, + name : '/rosapi/services_for_type', + serviceType : 'rosapi/ServicesForType' + }); + + var request = new ServiceRequest({ + type: serviceType + }); + if (typeof failedCallback === 'function'){ + servicesForTypeClient.callService(request, + function(result) { + callback(result.services); + }, + function(message) { + failedCallback(message); + } + ); + }else{ + servicesForTypeClient.callService(request, function(result) { + callback(result.services); + }); + } +}; + +/** + * Retrieves a detail of ROS service request. + * + * @param service name of service: + * @param callback - function with params: + * * type - String of the service type + */ +Ros.prototype.getServiceRequestDetails = function(type, callback, failedCallback) { + var serviceTypeClient = new Service({ + ros : this, + name : '/rosapi/service_request_details', + serviceType : 'rosapi/ServiceRequestDetails' + }); + var request = new ServiceRequest({ + type: type + }); + + if (typeof failedCallback === 'function'){ + serviceTypeClient.callService(request, + function(result) { + callback(result); + }, + function(message){ + failedCallback(message); + } + ); + }else{ + serviceTypeClient.callService(request, function(result) { + callback(result); + }); + } +}; + +/** + * Retrieves a detail of ROS service request. + * + * @param service name of service: + * @param callback - function with params: + * * type - String of the service type + */ +Ros.prototype.getServiceResponseDetails = function(type, callback, failedCallback) { + var serviceTypeClient = new Service({ + ros : this, + name : '/rosapi/service_response_details', + serviceType : 'rosapi/ServiceResponseDetails' + }); + var request = new ServiceRequest({ + type: type + }); + + if (typeof failedCallback === 'function'){ + serviceTypeClient.callService(request, + function(result) { + callback(result); + }, + function(message){ + failedCallback(message); + } + ); + }else{ + serviceTypeClient.callService(request, function(result) { + callback(result); + }); + } +}; + +/** + * Retrieves list of active node names in ROS. + * + * @param callback - function with the following params: + * * nodes - array of node names + */ +Ros.prototype.getNodes = function(callback, failedCallback) { + var nodesClient = new Service({ + ros : this, + name : '/rosapi/nodes', + serviceType : 'rosapi/Nodes' + }); + + var request = new ServiceRequest(); + if (typeof failedCallback === 'function'){ + nodesClient.callService(request, + function(result) { + callback(result.nodes); + }, + function(message) { + failedCallback(message); + } + ); + }else{ + nodesClient.callService(request, function(result) { + callback(result.nodes); + }); + } +}; + +/** + * Retrieves list subscribed topics, publishing topics and services of a specific node + * + * @param node name of the node: + * @param callback - function with params: + * * publications - array of published topic names + * * subscriptions - array of subscribed topic names + * * services - array of service names hosted + */ +Ros.prototype.getNodeDetails = function(node, callback, failedCallback) { + var nodesClient = new Service({ + ros : this, + name : '/rosapi/node_details', + serviceType : 'rosapi/NodeDetails' + }); + + var request = new ServiceRequest({ + node: node + }); + if (typeof failedCallback === 'function'){ + nodesClient.callService(request, + function(result) { + callback(result.subscribing, result.publishing, result.services); + }, + function(message) { + failedCallback(message); + } + ); + } else { + nodesClient.callService(request, function(result) { + callback(result); + }); + } +}; + +/** + * Retrieves list of param names from the ROS Parameter Server. + * + * @param callback function with params: + * * params - array of param names. + */ +Ros.prototype.getParams = function(callback, failedCallback) { + var paramsClient = new Service({ + ros : this, + name : '/rosapi/get_param_names', + serviceType : 'rosapi/GetParamNames' + }); + var request = new ServiceRequest(); + if (typeof failedCallback === 'function'){ + paramsClient.callService(request, + function(result) { + callback(result.names); + }, + function(message){ + failedCallback(message); + } + ); + }else{ + paramsClient.callService(request, function(result) { + callback(result.names); + }); + } +}; + +/** + * Retrieves a type of ROS topic. + * + * @param topic name of the topic: + * @param callback - function with params: + * * type - String of the topic type + */ +Ros.prototype.getTopicType = function(topic, callback, failedCallback) { + var topicTypeClient = new Service({ + ros : this, + name : '/rosapi/topic_type', + serviceType : 'rosapi/TopicType' + }); + var request = new ServiceRequest({ + topic: topic + }); + + if (typeof failedCallback === 'function'){ + topicTypeClient.callService(request, + function(result) { + callback(result.type); + }, + function(message){ + failedCallback(message); + } + ); + }else{ + topicTypeClient.callService(request, function(result) { + callback(result.type); + }); + } +}; + +/** + * Retrieves a type of ROS service. + * + * @param service name of service: + * @param callback - function with params: + * * type - String of the service type + */ +Ros.prototype.getServiceType = function(service, callback, failedCallback) { + var serviceTypeClient = new Service({ + ros : this, + name : '/rosapi/service_type', + serviceType : 'rosapi/ServiceType' + }); + var request = new ServiceRequest({ + service: service + }); + + if (typeof failedCallback === 'function'){ + serviceTypeClient.callService(request, + function(result) { + callback(result.type); + }, + function(message){ + failedCallback(message); + } + ); + }else{ + serviceTypeClient.callService(request, function(result) { + callback(result.type); + }); + } +}; + +/** + * Retrieves a detail of ROS message. + * + * @param callback - function with params: + * * details - Array of the message detail + * @param message - String of a topic type + */ +Ros.prototype.getMessageDetails = function(message, callback, failedCallback) { + var messageDetailClient = new Service({ + ros : this, + name : '/rosapi/message_details', + serviceType : 'rosapi/MessageDetails' + }); + var request = new ServiceRequest({ + type: message + }); + + if (typeof failedCallback === 'function'){ + messageDetailClient.callService(request, + function(result) { + callback(result.typedefs); + }, + function(message){ + failedCallback(message); + } + ); + }else{ + messageDetailClient.callService(request, function(result) { + callback(result.typedefs); + }); + } +}; + +/** + * Decode a typedefs into a dictionary like `rosmsg show foo/bar` + * + * @param defs - array of type_def dictionary + */ +Ros.prototype.decodeTypeDefs = function(defs) { + var that = this; + + // calls itself recursively to resolve type definition using hints. + var decodeTypeDefsRec = function(theType, hints) { + var typeDefDict = {}; + for (var i = 0; i < theType.fieldnames.length; i++) { + var arrayLen = theType.fieldarraylen[i]; + var fieldName = theType.fieldnames[i]; + var fieldType = theType.fieldtypes[i]; + if (fieldType.indexOf('/') === -1) { // check the fieldType includes '/' or not + if (arrayLen === -1) { + typeDefDict[fieldName] = fieldType; + } + else { + typeDefDict[fieldName] = [fieldType]; + } + } + else { + // lookup the name + var sub = false; + for (var j = 0; j < hints.length; j++) { + if (hints[j].type.toString() === fieldType.toString()) { + sub = hints[j]; + break; + } + } + if (sub) { + var subResult = decodeTypeDefsRec(sub, hints); + if (arrayLen === -1) { + } + else { + typeDefDict[fieldName] = [subResult]; + } + } + else { + that.emit('error', 'Cannot find ' + fieldType + ' in decodeTypeDefs'); + } + } + } + return typeDefDict; + }; + + return decodeTypeDefsRec(defs[0], defs); +}; + + +module.exports = Ros; + +},{"./Service":13,"./ServiceRequest":14,"./SocketAdapter.js":16,"eventemitter2":1,"object-assign":2,"ws":39}],13:[function(require,module,exports){ +/** + * @fileoverview + * @author Brandon Alexander - baalexander@gmail.com + */ + +var ServiceResponse = require('./ServiceResponse'); +var ServiceRequest = require('./ServiceRequest'); +var EventEmitter2 = require('eventemitter2').EventEmitter2; + +/** + * A ROS service client. + * + * @constructor + * @params options - possible keys include: + * * ros - the ROSLIB.Ros connection handle + * * name - the service name, like /add_two_ints + * * serviceType - the service type, like 'rospy_tutorials/AddTwoInts' + */ +function Service(options) { + options = options || {}; + this.ros = options.ros; + this.name = options.name; + this.serviceType = options.serviceType; + this.isAdvertised = false; + + this._serviceCallback = null; +} +Service.prototype.__proto__ = EventEmitter2.prototype; +/** + * Calls the service. Returns the service response in the callback. + * + * @param request - the ROSLIB.ServiceRequest to send + * @param callback - function with params: + * * response - the response from the service request + * @param failedCallback - the callback function when the service call failed (optional). Params: + * * error - the error message reported by ROS + */ +Service.prototype.callService = function(request, callback, failedCallback) { + if (this.isAdvertised) { + return; + } + + var serviceCallId = 'call_service:' + this.name + ':' + (++this.ros.idCounter); + + if (callback || failedCallback) { + this.ros.once(serviceCallId, function(message) { + if (message.result !== undefined && message.result === false) { + if (typeof failedCallback === 'function') { + failedCallback(message.values); + } + } else if (typeof callback === 'function') { + callback(new ServiceResponse(message.values)); + } + }); + } + + var call = { + op : 'call_service', + id : serviceCallId, + service : this.name, + args : request + }; + this.ros.callOnConnection(call); +}; + +/** + * Every time a message is published for the given topic, the callback + * will be called with the message object. + * + * @param callback - function with the following params: + * * message - the published message + */ +Service.prototype.advertise = function(callback) { + if (this.isAdvertised || typeof callback !== 'function') { + return; + } + + this._serviceCallback = callback; + this.ros.on(this.name, this._serviceResponse.bind(this)); + this.ros.callOnConnection({ + op: 'advertise_service', + type: this.serviceType, + service: this.name + }); + this.isAdvertised = true; +}; + +Service.prototype.unadvertise = function() { + if (!this.isAdvertised) { + return; + } + this.ros.callOnConnection({ + op: 'unadvertise_service', + service: this.name + }); + this.isAdvertised = false; +}; + +Service.prototype._serviceResponse = function(rosbridgeRequest) { + var response = {}; + var success = this._serviceCallback(rosbridgeRequest.args, response); + + var call = { + op: 'service_response', + service: this.name, + values: new ServiceResponse(response), + result: success + }; + + if (rosbridgeRequest.id) { + call.id = rosbridgeRequest.id; + } + + this.ros.callOnConnection(call); +}; + +module.exports = Service; +},{"./ServiceRequest":14,"./ServiceResponse":15,"eventemitter2":1}],14:[function(require,module,exports){ +/** + * @fileoverview + * @author Brandon Alexander - balexander@willowgarage.com + */ + +var assign = require('object-assign'); + +/** + * A ServiceRequest is passed into the service call. + * + * @constructor + * @param values - object matching the fields defined in the .srv definition file + */ +function ServiceRequest(values) { + assign(this, values); +} + +module.exports = ServiceRequest; +},{"object-assign":2}],15:[function(require,module,exports){ +/** + * @fileoverview + * @author Brandon Alexander - balexander@willowgarage.com + */ + +var assign = require('object-assign'); + +/** + * A ServiceResponse is returned from the service call. + * + * @constructor + * @param values - object matching the fields defined in the .srv definition file + */ +function ServiceResponse(values) { + assign(this, values); +} + +module.exports = ServiceResponse; +},{"object-assign":2}],16:[function(require,module,exports){ +/** + * Socket event handling utilities for handling events on either + * WebSocket and TCP sockets + * + * Note to anyone reviewing this code: these functions are called + * in the context of their parent object, unless bound + * @fileOverview + */ +'use strict'; + +var decompressPng = require('../util/decompressPng'); +var WebSocket = require('ws'); +var BSON = null; +if(typeof bson !== 'undefined'){ + BSON = bson().BSON; +} + +/** + * Events listeners for a WebSocket or TCP socket to a JavaScript + * ROS Client. Sets up Messages for a given topic to trigger an + * event on the ROS client. + * + * @namespace SocketAdapter + * @private + */ +function SocketAdapter(client) { + function handleMessage(message) { + if (message.op === 'publish') { + client.emit(message.topic, message.msg); + } else if (message.op === 'service_response') { + client.emit(message.id, message); + } else if (message.op === 'call_service') { + client.emit(message.service, message); + } else if(message.op === 'status'){ + if(message.id){ + client.emit('status:'+message.id, message); + } else { + client.emit('status', message); + } + } + } + + function handlePng(message, callback) { + if (message.op === 'png') { + decompressPng(message.data, callback); + } else { + callback(message); + } + } + + function decodeBSON(data, callback) { + if (!BSON) { + throw 'Cannot process BSON encoded message without BSON header.'; + } + var reader = new FileReader(); + reader.onload = function() { + var uint8Array = new Uint8Array(this.result); + var msg = BSON.deserialize(uint8Array); + callback(msg); + }; + reader.readAsArrayBuffer(data); + } + + return { + /** + * Emits a 'connection' event on WebSocket connection. + * + * @param event - the argument to emit with the event. + * @memberof SocketAdapter + */ + onopen: function onOpen(event) { + client.isConnected = true; + client.emit('connection', event); + }, + + /** + * Emits a 'close' event on WebSocket disconnection. + * + * @param event - the argument to emit with the event. + * @memberof SocketAdapter + */ + onclose: function onClose(event) { + client.isConnected = false; + client.emit('close', event); + }, + + /** + * Emits an 'error' event whenever there was an error. + * + * @param event - the argument to emit with the event. + * @memberof SocketAdapter + */ + onerror: function onError(event) { + client.emit('error', event); + }, + + /** + * Parses message responses from rosbridge and sends to the appropriate + * topic, service, or param. + * + * @param message - the raw JSON message from rosbridge. + * @memberof SocketAdapter + */ + onmessage: function onMessage(data) { + if (typeof Blob !== 'undefined' && data.data instanceof Blob) { + decodeBSON(data.data, function (message) { + handlePng(message, handleMessage); + }); + } else { + var message = JSON.parse(typeof data === 'string' ? data : data.data); + handlePng(message, handleMessage); + } + } + }; +} + +module.exports = SocketAdapter; + +},{"../util/decompressPng":41,"ws":39}],17:[function(require,module,exports){ +/** + * @fileoverview + * @author Brandon Alexander - baalexander@gmail.com + */ + +var EventEmitter2 = require('eventemitter2').EventEmitter2; +var Message = require('./Message'); + +/** + * Publish and/or subscribe to a topic in ROS. + * + * Emits the following events: + * * 'warning' - if there are any warning during the Topic creation + * * 'message' - the message data from rosbridge + * + * @constructor + * @param options - object with following keys: + * * ros - the ROSLIB.Ros connection handle + * * name - the topic name, like /cmd_vel + * * messageType - the message type, like 'std_msgs/String' + * * compression - the type of compression to use, like 'png' + * * throttle_rate - the rate (in ms in between messages) at which to throttle the topics + * * queue_size - the queue created at bridge side for re-publishing webtopics (defaults to 100) + * * latch - latch the topic when publishing + * * queue_length - the queue length at bridge side used when subscribing (defaults to 0, no queueing). + * * reconnect_on_close - the flag to enable resubscription and readvertisement on close event(defaults to true). + */ +function Topic(options) { + options = options || {}; + this.ros = options.ros; + this.name = options.name; + this.messageType = options.messageType; + this.isAdvertised = false; + this.compression = options.compression || 'none'; + this.throttle_rate = options.throttle_rate || 0; + this.latch = options.latch || false; + this.queue_size = options.queue_size || 100; + this.queue_length = options.queue_length || 0; + this.reconnect_on_close = options.reconnect_on_close || true; + + // Check for valid compression types + if (this.compression && this.compression !== 'png' && + this.compression !== 'none') { + this.emit('warning', this.compression + + ' compression is not supported. No compression will be used.'); + } + + // Check if throttle rate is negative + if (this.throttle_rate < 0) { + this.emit('warning', this.throttle_rate + ' is not allowed. Set to 0'); + this.throttle_rate = 0; + } + + var that = this; + if (this.reconnect_on_close) { + this.callForSubscribeAndAdvertise = function(message) { + that.ros.callOnConnection(message); + + that.waitForReconnect = false; + that.reconnectFunc = function() { + if(!that.waitForReconnect) { + that.waitForReconnect = true; + that.ros.callOnConnection(message); + that.ros.once('connection', function() { + that.waitForReconnect = false; + }); + } + }; + that.ros.on('close', that.reconnectFunc); + }; + } + else { + this.callForSubscribeAndAdvertise = this.ros.callOnConnection; + } + + this._messageCallback = function(data) { + that.emit('message', new Message(data)); + }; +} +Topic.prototype.__proto__ = EventEmitter2.prototype; + +/** + * Every time a message is published for the given topic, the callback + * will be called with the message object. + * + * @param callback - function with the following params: + * * message - the published message + */ +Topic.prototype.subscribe = function(callback) { + if (typeof callback === 'function') { + this.on('message', callback); + } + + if (this.subscribeId) { return; } + this.ros.on(this.name, this._messageCallback); + this.subscribeId = 'subscribe:' + this.name + ':' + (++this.ros.idCounter); + + this.callForSubscribeAndAdvertise({ + op: 'subscribe', + id: this.subscribeId, + type: this.messageType, + topic: this.name, + compression: this.compression, + throttle_rate: this.throttle_rate, + queue_length: this.queue_length + }); +}; + +/** + * Unregisters as a subscriber for the topic. Unsubscribing stop remove + * all subscribe callbacks. To remove a call back, you must explicitly + * pass the callback function in. + * + * @param callback - the optional callback to unregister, if + * * provided and other listeners are registered the topic won't + * * unsubscribe, just stop emitting to the passed listener + */ +Topic.prototype.unsubscribe = function(callback) { + if (callback) { + this.off('message', callback); + // If there is any other callbacks still subscribed don't unsubscribe + if (this.listeners('message').length) { return; } + } + if (!this.subscribeId) { return; } + // Note: Don't call this.removeAllListeners, allow client to handle that themselves + this.ros.off(this.name, this._messageCallback); + if(this.reconnect_on_close) { + this.ros.off('close', this.reconnectFunc); + } + this.emit('unsubscribe'); + this.ros.callOnConnection({ + op: 'unsubscribe', + id: this.subscribeId, + topic: this.name + }); + this.subscribeId = null; +}; + + +/** + * Registers as a publisher for the topic. + */ +Topic.prototype.advertise = function() { + if (this.isAdvertised) { + return; + } + this.advertiseId = 'advertise:' + this.name + ':' + (++this.ros.idCounter); + this.callForSubscribeAndAdvertise({ + op: 'advertise', + id: this.advertiseId, + type: this.messageType, + topic: this.name, + latch: this.latch, + queue_size: this.queue_size + }); + this.isAdvertised = true; + + if(!this.reconnect_on_close) { + var that = this; + this.ros.on('close', function() { + that.isAdvertised = false; + }); + } +}; + +/** + * Unregisters as a publisher for the topic. + */ +Topic.prototype.unadvertise = function() { + if (!this.isAdvertised) { + return; + } + if(this.reconnect_on_close) { + this.ros.off('close', this.reconnectFunc); + } + this.emit('unadvertise'); + this.ros.callOnConnection({ + op: 'unadvertise', + id: this.advertiseId, + topic: this.name + }); + this.isAdvertised = false; +}; + +/** + * Publish the message. + * + * @param message - A ROSLIB.Message object. + */ +Topic.prototype.publish = function(message) { + if (!this.isAdvertised) { + this.advertise(); + } + + this.ros.idCounter++; + var call = { + op: 'publish', + id: 'publish:' + this.name + ':' + this.ros.idCounter, + topic: this.name, + msg: message, + latch: this.latch + }; + this.ros.callOnConnection(call); +}; + +module.exports = Topic; + +},{"./Message":10,"eventemitter2":1}],18:[function(require,module,exports){ +var mixin = require('../mixin'); + +var core = module.exports = { + Ros: require('./Ros'), + Topic: require('./Topic'), + Message: require('./Message'), + Param: require('./Param'), + Service: require('./Service'), + ServiceRequest: require('./ServiceRequest'), + ServiceResponse: require('./ServiceResponse') +}; + +mixin(core.Ros, ['Param', 'Service', 'Topic'], core); + +},{"../mixin":24,"./Message":10,"./Param":11,"./Ros":12,"./Service":13,"./ServiceRequest":14,"./ServiceResponse":15,"./Topic":17}],19:[function(require,module,exports){ +/** + * @fileoverview + * @author David Gossow - dgossow@willowgarage.com + */ + +var Vector3 = require('./Vector3'); +var Quaternion = require('./Quaternion'); + +/** + * A Pose in 3D space. Values are copied into this object. + * + * @constructor + * @param options - object with following keys: + * * position - the Vector3 describing the position + * * orientation - the ROSLIB.Quaternion describing the orientation + */ +function Pose(options) { + options = options || {}; + // copy the values into this object if they exist + this.position = new Vector3(options.position); + this.orientation = new Quaternion(options.orientation); +} + +/** + * Apply a transform against this pose. + * + * @param tf the transform + */ +Pose.prototype.applyTransform = function(tf) { + this.position.multiplyQuaternion(tf.rotation); + this.position.add(tf.translation); + var tmp = tf.rotation.clone(); + tmp.multiply(this.orientation); + this.orientation = tmp; +}; + +/** + * Clone a copy of this pose. + * + * @returns the cloned pose + */ +Pose.prototype.clone = function() { + return new Pose(this); +}; + +module.exports = Pose; +},{"./Quaternion":20,"./Vector3":22}],20:[function(require,module,exports){ +/** + * @fileoverview + * @author David Gossow - dgossow@willowgarage.com + */ + +/** + * A Quaternion. + * + * @constructor + * @param options - object with following keys: + * * x - the x value + * * y - the y value + * * z - the z value + * * w - the w value + */ +function Quaternion(options) { + options = options || {}; + this.x = options.x || 0; + this.y = options.y || 0; + this.z = options.z || 0; + this.w = (typeof options.w === 'number') ? options.w : 1; +} + +/** + * Perform a conjugation on this quaternion. + */ +Quaternion.prototype.conjugate = function() { + this.x *= -1; + this.y *= -1; + this.z *= -1; +}; + +/** + * Return the norm of this quaternion. + */ +Quaternion.prototype.norm = function() { + return Math.sqrt(this.x * this.x + this.y * this.y + this.z * this.z + this.w * this.w); +}; + +/** + * Perform a normalization on this quaternion. + */ +Quaternion.prototype.normalize = function() { + var l = Math.sqrt(this.x * this.x + this.y * this.y + this.z * this.z + this.w * this.w); + if (l === 0) { + this.x = 0; + this.y = 0; + this.z = 0; + this.w = 1; + } else { + l = 1 / l; + this.x = this.x * l; + this.y = this.y * l; + this.z = this.z * l; + this.w = this.w * l; + } +}; + +/** + * Convert this quaternion into its inverse. + */ +Quaternion.prototype.invert = function() { + this.conjugate(); + this.normalize(); +}; + +/** + * Set the values of this quaternion to the product of itself and the given quaternion. + * + * @param q the quaternion to multiply with + */ +Quaternion.prototype.multiply = function(q) { + var newX = this.x * q.w + this.y * q.z - this.z * q.y + this.w * q.x; + var newY = -this.x * q.z + this.y * q.w + this.z * q.x + this.w * q.y; + var newZ = this.x * q.y - this.y * q.x + this.z * q.w + this.w * q.z; + var newW = -this.x * q.x - this.y * q.y - this.z * q.z + this.w * q.w; + this.x = newX; + this.y = newY; + this.z = newZ; + this.w = newW; +}; + +/** + * Clone a copy of this quaternion. + * + * @returns the cloned quaternion + */ +Quaternion.prototype.clone = function() { + return new Quaternion(this); +}; + +module.exports = Quaternion; + +},{}],21:[function(require,module,exports){ +/** + * @fileoverview + * @author David Gossow - dgossow@willowgarage.com + */ + +var Vector3 = require('./Vector3'); +var Quaternion = require('./Quaternion'); + +/** + * A Transform in 3-space. Values are copied into this object. + * + * @constructor + * @param options - object with following keys: + * * translation - the Vector3 describing the translation + * * rotation - the ROSLIB.Quaternion describing the rotation + */ +function Transform(options) { + options = options || {}; + // Copy the values into this object if they exist + this.translation = new Vector3(options.translation); + this.rotation = new Quaternion(options.rotation); +} + +/** + * Clone a copy of this transform. + * + * @returns the cloned transform + */ +Transform.prototype.clone = function() { + return new Transform(this); +}; + +module.exports = Transform; +},{"./Quaternion":20,"./Vector3":22}],22:[function(require,module,exports){ +/** + * @fileoverview + * @author David Gossow - dgossow@willowgarage.com + */ + +/** + * A 3D vector. + * + * @constructor + * @param options - object with following keys: + * * x - the x value + * * y - the y value + * * z - the z value + */ +function Vector3(options) { + options = options || {}; + this.x = options.x || 0; + this.y = options.y || 0; + this.z = options.z || 0; +} + +/** + * Set the values of this vector to the sum of itself and the given vector. + * + * @param v the vector to add with + */ +Vector3.prototype.add = function(v) { + this.x += v.x; + this.y += v.y; + this.z += v.z; +}; + +/** + * Set the values of this vector to the difference of itself and the given vector. + * + * @param v the vector to subtract with + */ +Vector3.prototype.subtract = function(v) { + this.x -= v.x; + this.y -= v.y; + this.z -= v.z; +}; + +/** + * Multiply the given Quaternion with this vector. + * + * @param q - the quaternion to multiply with + */ +Vector3.prototype.multiplyQuaternion = function(q) { + var ix = q.w * this.x + q.y * this.z - q.z * this.y; + var iy = q.w * this.y + q.z * this.x - q.x * this.z; + var iz = q.w * this.z + q.x * this.y - q.y * this.x; + var iw = -q.x * this.x - q.y * this.y - q.z * this.z; + this.x = ix * q.w + iw * -q.x + iy * -q.z - iz * -q.y; + this.y = iy * q.w + iw * -q.y + iz * -q.x - ix * -q.z; + this.z = iz * q.w + iw * -q.z + ix * -q.y - iy * -q.x; +}; + +/** + * Clone a copy of this vector. + * + * @returns the cloned vector + */ +Vector3.prototype.clone = function() { + return new Vector3(this); +}; + +module.exports = Vector3; +},{}],23:[function(require,module,exports){ +module.exports = { + Pose: require('./Pose'), + Quaternion: require('./Quaternion'), + Transform: require('./Transform'), + Vector3: require('./Vector3') +}; + +},{"./Pose":19,"./Quaternion":20,"./Transform":21,"./Vector3":22}],24:[function(require,module,exports){ +/** + * Mixin a feature to the core/Ros prototype. + * For example, mixin(Ros, ['Topic'], {Topic: }) + * will add a topic bound to any Ros instances so a user + * can call `var topic = ros.Topic({name: '/foo'});` + * + * @author Graeme Yeates - github.com/megawac + */ +module.exports = function(Ros, classes, features) { + classes.forEach(function(className) { + var Class = features[className]; + Ros.prototype[className] = function(options) { + options.ros = this; + return new Class(options); + }; + }); +}; + +},{}],25:[function(require,module,exports){ +/** + * @fileoverview + * @author David Gossow - dgossow@willowgarage.com + */ + +var ActionClient = require('../actionlib/ActionClient'); +var Goal = require('../actionlib/Goal'); + +var Service = require('../core/Service.js'); +var ServiceRequest = require('../core/ServiceRequest.js'); + +var Transform = require('../math/Transform'); + +/** + * A TF Client that listens to TFs from tf2_web_republisher. + * + * @constructor + * @param options - object with following keys: + * * ros - the ROSLIB.Ros connection handle + * * fixedFrame - the fixed frame, like /base_link + * * angularThres - the angular threshold for the TF republisher + * * transThres - the translation threshold for the TF republisher + * * rate - the rate for the TF republisher + * * updateDelay - the time (in ms) to wait after a new subscription + * to update the TF republisher's list of TFs + * * topicTimeout - the timeout parameter for the TF republisher + * * serverName (optional) - the name of the tf2_web_republisher server + * * repubServiceName (optional) - the name of the republish_tfs service (non groovy compatibility mode only) + * default: '/republish_tfs' + */ +function TFClient(options) { + options = options || {}; + this.ros = options.ros; + this.fixedFrame = options.fixedFrame || '/base_link'; + this.angularThres = options.angularThres || 2.0; + this.transThres = options.transThres || 0.01; + this.rate = options.rate || 10.0; + this.updateDelay = options.updateDelay || 50; + var seconds = options.topicTimeout || 2.0; + var secs = Math.floor(seconds); + var nsecs = Math.floor((seconds - secs) * 1000000000); + this.topicTimeout = { + secs: secs, + nsecs: nsecs + }; + this.serverName = options.serverName || '/tf2_web_republisher'; + this.repubServiceName = options.repubServiceName || '/republish_tfs'; + + this.currentGoal = false; + this.currentTopic = false; + this.frameInfos = {}; + this.republisherUpdateRequested = false; + + // Create an Action client + this.actionClient = this.ros.ActionClient({ + serverName : this.serverName, + actionName : 'tf2_web_republisher/TFSubscriptionAction', + omitStatus : true, + omitResult : true + }); + + // Create a Service client + this.serviceClient = this.ros.Service({ + name: this.repubServiceName, + serviceType: 'tf2_web_republisher/RepublishTFs' + }); +} + +/** + * Process the incoming TF message and send them out using the callback + * functions. + * + * @param tf - the TF message from the server + */ +TFClient.prototype.processTFArray = function(tf) { + var that = this; + tf.transforms.forEach(function(transform) { + var frameID = transform.child_frame_id; + if (frameID[0] === '/') + { + frameID = frameID.substring(1); + } + var info = this.frameInfos[frameID]; + if (info) { + info.transform = new Transform({ + translation : transform.transform.translation, + rotation : transform.transform.rotation + }); + info.cbs.forEach(function(cb) { + cb(info.transform); + }); + } + }, this); +}; + +/** + * Create and send a new goal (or service request) to the tf2_web_republisher + * based on the current list of TFs. + */ +TFClient.prototype.updateGoal = function() { + var goalMessage = { + source_frames : Object.keys(this.frameInfos), + target_frame : this.fixedFrame, + angular_thres : this.angularThres, + trans_thres : this.transThres, + rate : this.rate + }; + + // if we're running in groovy compatibility mode (the default) + // then use the action interface to tf2_web_republisher + if(this.ros.groovyCompatibility) { + if (this.currentGoal) { + this.currentGoal.cancel(); + } + this.currentGoal = new Goal({ + actionClient : this.actionClient, + goalMessage : goalMessage + }); + + this.currentGoal.on('feedback', this.processTFArray.bind(this)); + this.currentGoal.send(); + } + else { + // otherwise, use the service interface + // The service interface has the same parameters as the action, + // plus the timeout + goalMessage.timeout = this.topicTimeout; + var request = new ServiceRequest(goalMessage); + + this.serviceClient.callService(request, this.processResponse.bind(this)); + } + + this.republisherUpdateRequested = false; +}; + +/** + * Process the service response and subscribe to the tf republisher + * topic + * + * @param response the service response containing the topic name + */ +TFClient.prototype.processResponse = function(response) { + // if we subscribed to a topic before, unsubscribe so + // the republisher stops publishing it + if (this.currentTopic) { + this.currentTopic.unsubscribe(); + } + + this.currentTopic = this.ros.Topic({ + name: response.topic_name, + messageType: 'tf2_web_republisher/TFArray' + }); + this.currentTopic.subscribe(this.processTFArray.bind(this)); +}; + +/** + * Subscribe to the given TF frame. + * + * @param frameID - the TF frame to subscribe to + * @param callback - function with params: + * * transform - the transform data + */ +TFClient.prototype.subscribe = function(frameID, callback) { + // remove leading slash, if it's there + if (frameID[0] === '/') + { + frameID = frameID.substring(1); + } + // if there is no callback registered for the given frame, create emtpy callback list + if (!this.frameInfos[frameID]) { + this.frameInfos[frameID] = { + cbs: [] + }; + if (!this.republisherUpdateRequested) { + setTimeout(this.updateGoal.bind(this), this.updateDelay); + this.republisherUpdateRequested = true; + } + } + // if we already have a transform, call back immediately + else if (this.frameInfos[frameID].transform) { + callback(this.frameInfos[frameID].transform); + } + this.frameInfos[frameID].cbs.push(callback); +}; + +/** + * Unsubscribe from the given TF frame. + * + * @param frameID - the TF frame to unsubscribe from + * @param callback - the callback function to remove + */ +TFClient.prototype.unsubscribe = function(frameID, callback) { + // remove leading slash, if it's there + if (frameID[0] === '/') + { + frameID = frameID.substring(1); + } + var info = this.frameInfos[frameID]; + for (var cbs = info && info.cbs || [], idx = cbs.length; idx--;) { + if (cbs[idx] === callback) { + cbs.splice(idx, 1); + } + } + if (!callback || cbs.length === 0) { + delete this.frameInfos[frameID]; + } +}; + +/** + * Unsubscribe and unadvertise all topics associated with this TFClient. + */ +TFClient.prototype.dispose = function() { + this.actionClient.dispose(); + if (this.currentTopic) { + this.currentTopic.unsubscribe(); + } +}; + +module.exports = TFClient; + +},{"../actionlib/ActionClient":5,"../actionlib/Goal":7,"../core/Service.js":13,"../core/ServiceRequest.js":14,"../math/Transform":21}],26:[function(require,module,exports){ +var Ros = require('../core/Ros'); +var mixin = require('../mixin'); + +var tf = module.exports = { + TFClient: require('./TFClient') +}; + +mixin(Ros, ['TFClient'], tf); +},{"../core/Ros":12,"../mixin":24,"./TFClient":25}],27:[function(require,module,exports){ +/** + * @fileOverview + * @author Benjamin Pitzer - ben.pitzer@gmail.com + * @author Russell Toris - rctoris@wpi.edu + */ + +var Vector3 = require('../math/Vector3'); +var UrdfTypes = require('./UrdfTypes'); + +/** + * A Box element in a URDF. + * + * @constructor + * @param options - object with following keys: + * * xml - the XML element to parse + */ +function UrdfBox(options) { + this.dimension = null; + this.type = UrdfTypes.URDF_BOX; + + // Parse the xml string + var xyz = options.xml.getAttribute('size').split(' '); + this.dimension = new Vector3({ + x : parseFloat(xyz[0]), + y : parseFloat(xyz[1]), + z : parseFloat(xyz[2]) + }); +} + +module.exports = UrdfBox; +},{"../math/Vector3":22,"./UrdfTypes":36}],28:[function(require,module,exports){ +/** + * @fileOverview + * @author Benjamin Pitzer - ben.pitzer@gmail.com + * @author Russell Toris - rctoris@wpi.edu + */ + +/** + * A Color element in a URDF. + * + * @constructor + * @param options - object with following keys: + * * xml - the XML element to parse + */ +function UrdfColor(options) { + // Parse the xml string + var rgba = options.xml.getAttribute('rgba').split(' '); + this.r = parseFloat(rgba[0]); + this.g = parseFloat(rgba[1]); + this.b = parseFloat(rgba[2]); + this.a = parseFloat(rgba[3]); +} + +module.exports = UrdfColor; +},{}],29:[function(require,module,exports){ +/** + * @fileOverview + * @author Benjamin Pitzer - ben.pitzer@gmail.com + * @author Russell Toris - rctoris@wpi.edu + */ + +var UrdfTypes = require('./UrdfTypes'); + +/** + * A Cylinder element in a URDF. + * + * @constructor + * @param options - object with following keys: + * * xml - the XML element to parse + */ +function UrdfCylinder(options) { + this.type = UrdfTypes.URDF_CYLINDER; + this.length = parseFloat(options.xml.getAttribute('length')); + this.radius = parseFloat(options.xml.getAttribute('radius')); +} + +module.exports = UrdfCylinder; +},{"./UrdfTypes":36}],30:[function(require,module,exports){ +/** + * @fileOverview + * @author David V. Lu!! davidvlu@gmail.com + */ + +/** + * A Joint element in a URDF. + * + * @constructor + * @param options - object with following keys: + * * xml - the XML element to parse + */ +function UrdfJoint(options) { + this.name = options.xml.getAttribute('name'); + this.type = options.xml.getAttribute('type'); + + var parents = options.xml.getElementsByTagName('parent'); + if(parents.length > 0) { + this.parent = parents[0].getAttribute('link'); + } + + var children = options.xml.getElementsByTagName('child'); + if(children.length > 0) { + this.child = children[0].getAttribute('link'); + } + + var limits = options.xml.getElementsByTagName('limit'); + if (limits.length > 0) { + this.minval = parseFloat( limits[0].getAttribute('lower') ); + this.maxval = parseFloat( limits[0].getAttribute('upper') ); + } +} + +module.exports = UrdfJoint; + +},{}],31:[function(require,module,exports){ +/** + * @fileOverview + * @author Benjamin Pitzer - ben.pitzer@gmail.com + * @author Russell Toris - rctoris@wpi.edu + */ + +var UrdfVisual = require('./UrdfVisual'); + +/** + * A Link element in a URDF. + * + * @constructor + * @param options - object with following keys: + * * xml - the XML element to parse + */ +function UrdfLink(options) { + this.name = options.xml.getAttribute('name'); + this.visuals = []; + var visuals = options.xml.getElementsByTagName('visual'); + + for( var i=0; i 0) { + this.textureFilename = textures[0].getAttribute('filename'); + } + + // Color + var colors = options.xml.getElementsByTagName('color'); + if (colors.length > 0) { + // Parse the RBGA string + this.color = new UrdfColor({ + xml : colors[0] + }); + } +} + +UrdfMaterial.prototype.isLink = function() { + return this.color === null && this.textureFilename === null; +}; + +var assign = require('object-assign'); + +UrdfMaterial.prototype.assign = function(obj) { + return assign(this, obj); +}; + +module.exports = UrdfMaterial; + +},{"./UrdfColor":28,"object-assign":2}],33:[function(require,module,exports){ +/** + * @fileOverview + * @author Benjamin Pitzer - ben.pitzer@gmail.com + * @author Russell Toris - rctoris@wpi.edu + */ + +var Vector3 = require('../math/Vector3'); +var UrdfTypes = require('./UrdfTypes'); + +/** + * A Mesh element in a URDF. + * + * @constructor + * @param options - object with following keys: + * * xml - the XML element to parse + */ +function UrdfMesh(options) { + this.scale = null; + + this.type = UrdfTypes.URDF_MESH; + this.filename = options.xml.getAttribute('filename'); + + // Check for a scale + var scale = options.xml.getAttribute('scale'); + if (scale) { + // Get the XYZ + var xyz = scale.split(' '); + this.scale = new Vector3({ + x : parseFloat(xyz[0]), + y : parseFloat(xyz[1]), + z : parseFloat(xyz[2]) + }); + } +} + +module.exports = UrdfMesh; +},{"../math/Vector3":22,"./UrdfTypes":36}],34:[function(require,module,exports){ +/** + * @fileOverview + * @author Benjamin Pitzer - ben.pitzer@gmail.com + * @author Russell Toris - rctoris@wpi.edu + */ + +var UrdfMaterial = require('./UrdfMaterial'); +var UrdfLink = require('./UrdfLink'); +var UrdfJoint = require('./UrdfJoint'); +var DOMParser = require('xmldom').DOMParser; + +// See https://developer.mozilla.org/docs/XPathResult#Constants +var XPATH_FIRST_ORDERED_NODE_TYPE = 9; + +/** + * A URDF Model can be used to parse a given URDF into the appropriate elements. + * + * @constructor + * @param options - object with following keys: + * * xml - the XML element to parse + * * string - the XML element to parse as a string + */ +function UrdfModel(options) { + options = options || {}; + var xmlDoc = options.xml; + var string = options.string; + this.materials = {}; + this.links = {}; + this.joints = {}; + + // Check if we are using a string or an XML element + if (string) { + // Parse the string + var parser = new DOMParser(); + xmlDoc = parser.parseFromString(string, 'text/xml'); + } + + // Initialize the model with the given XML node. + // Get the robot tag + var robotXml = xmlDoc.documentElement; + + // Get the robot name + this.name = robotXml.getAttribute('name'); + + // Parse all the visual elements we need + for (var nodes = robotXml.childNodes, i = 0; i < nodes.length; i++) { + var node = nodes[i]; + if (node.tagName === 'material') { + var material = new UrdfMaterial({ + xml : node + }); + // Make sure this is unique + if (this.materials[material.name] !== void 0) { + if( this.materials[material.name].isLink() ) { + this.materials[material.name].assign( material ); + } else { + console.warn('Material ' + material.name + 'is not unique.'); + } + } else { + this.materials[material.name] = material; + } + } else if (node.tagName === 'link') { + var link = new UrdfLink({ + xml : node + }); + // Make sure this is unique + if (this.links[link.name] !== void 0) { + console.warn('Link ' + link.name + ' is not unique.'); + } else { + // Check for a material + for( var j=0; j 0) { + var geom = geoms[0]; + var shape = null; + // Check for the shape + for (var i = 0; i < geom.childNodes.length; i++) { + var node = geom.childNodes[i]; + if (node.nodeType === 1) { + shape = node; + break; + } + } + // Check the type + var type = shape.nodeName; + if (type === 'sphere') { + this.geometry = new UrdfSphere({ + xml : shape + }); + } else if (type === 'box') { + this.geometry = new UrdfBox({ + xml : shape + }); + } else if (type === 'cylinder') { + this.geometry = new UrdfCylinder({ + xml : shape + }); + } else if (type === 'mesh') { + this.geometry = new UrdfMesh({ + xml : shape + }); + } else { + console.warn('Unknown geometry type ' + type); + } + } + + // Material + var materials = xml.getElementsByTagName('material'); + if (materials.length > 0) { + this.material = new UrdfMaterial({ + xml : materials[0] + }); + } +} + +module.exports = UrdfVisual; +},{"../math/Pose":19,"../math/Quaternion":20,"../math/Vector3":22,"./UrdfBox":27,"./UrdfCylinder":29,"./UrdfMaterial":32,"./UrdfMesh":33,"./UrdfSphere":35}],38:[function(require,module,exports){ +module.exports = require('object-assign')({ + UrdfBox: require('./UrdfBox'), + UrdfColor: require('./UrdfColor'), + UrdfCylinder: require('./UrdfCylinder'), + UrdfLink: require('./UrdfLink'), + UrdfMaterial: require('./UrdfMaterial'), + UrdfMesh: require('./UrdfMesh'), + UrdfModel: require('./UrdfModel'), + UrdfSphere: require('./UrdfSphere'), + UrdfVisual: require('./UrdfVisual') +}, require('./UrdfTypes')); + +},{"./UrdfBox":27,"./UrdfColor":28,"./UrdfCylinder":29,"./UrdfLink":31,"./UrdfMaterial":32,"./UrdfMesh":33,"./UrdfModel":34,"./UrdfSphere":35,"./UrdfTypes":36,"./UrdfVisual":37,"object-assign":2}],39:[function(require,module,exports){ +(function (global){ +module.exports = global.WebSocket; +}).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {}) +},{}],40:[function(require,module,exports){ +/* global document */ +module.exports = function Canvas() { + return document.createElement('canvas'); +}; +},{}],41:[function(require,module,exports){ +(function (global){ +/** + * @fileOverview + * @author Graeme Yeates - github.com/megawac + */ + +'use strict'; + +var Canvas = require('canvas'); +var Image = Canvas.Image || global.Image; + +/** + * If a message was compressed as a PNG image (a compression hack since + * gzipping over WebSockets * is not supported yet), this function places the + * "image" in a canvas element then decodes the * "image" as a Base64 string. + * + * @private + * @param data - object containing the PNG data. + * @param callback - function with params: + * * data - the uncompressed data + */ +function decompressPng(data, callback) { + // Uncompresses the data before sending it through (use image/canvas to do so). + var image = new Image(); + // When the image loads, extracts the raw data (JSON message). + image.onload = function() { + // Creates a local canvas to draw on. + var canvas = new Canvas(); + var context = canvas.getContext('2d'); + + // Sets width and height. + canvas.width = image.width; + canvas.height = image.height; + + // Prevents anti-aliasing and loosing data + context.imageSmoothingEnabled = false; + context.webkitImageSmoothingEnabled = false; + context.mozImageSmoothingEnabled = false; + + // Puts the data into the image. + context.drawImage(image, 0, 0); + // Grabs the raw, uncompressed data. + var imageData = context.getImageData(0, 0, image.width, image.height).data; + + // Constructs the JSON. + var jsonData = ''; + for (var i = 0; i < imageData.length; i += 4) { + // RGB + jsonData += String.fromCharCode(imageData[i], imageData[i + 1], imageData[i + 2]); + } + callback(JSON.parse(jsonData)); + }; + // Sends the image data to load. + image.src = 'data:image/png;base64,' + data; +} + +module.exports = decompressPng; +}).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {}) +},{"canvas":40}],42:[function(require,module,exports){ +(function (global){ +exports.DOMImplementation = global.DOMImplementation; +exports.XMLSerializer = global.XMLSerializer; +exports.DOMParser = global.DOMParser; +}).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {}) +},{}]},{},[4]); diff --git a/apps/ios/cleverrc/telemetry.js b/apps/ios/cleverrc/telemetry.js new file mode 100644 index 00000000..47e1f589 --- /dev/null +++ b/apps/ios/cleverrc/telemetry.js @@ -0,0 +1,85 @@ +var url = 'ws://192.168.11.1:9090'; +var modeEl = document.querySelector('.telemetry .mode'); +var batteryEl = document.querySelector('.battery'); + +var ros = new ROSLIB.Ros({ url: url }); + +ros.on('connection', function () { + body.classList.add('connected'); +}); + +ros.on('close', function () { + body.classList.remove('connected'); + modeEl.classList.remove('armed'); + modeEl.innerHTML = 'DISCONNECTED'; + batteryEl.innerHTML = ''; + setTimeout(function() { + modeEl.innerHTML = 'RECONNECTING'; + ros.connect(url); + }, 2000); +}); + +var fcuState; + +new ROSLIB.Topic({ + ros: ros, + name: '/state_latched', + messageType: 'mavros_msgs/State' +}).subscribe(function(message) { + body.classList.toggle('fcu-disconnected', !message.connected); + body.classList.toggle('armed', message.armed); + fcuState = message; + modeEl.classList.toggle('armed', fcuState.armed); + modeEl.innerHTML = message.connected ? fcuState.mode : 'DISCONNECTED FROM FCU'; + console.log('state', message); +}); + +function notifyLowBattery() { + callNativeApp('lowBattery'); +} + +notifyLowBatteryThrottled = throttle(notifyLowBattery, 10000); + +new ROSLIB.Topic({ + ros: ros, + name: '/mavros/battery', + messageType: 'sensor_msgs/BatteryState', + throttle_rate: 5000 +}).subscribe(function(message) { + var LOW_BATTERY = 3.8; + batteryEl.innerHTML = (message.cell_voltage[0].toFixed(2) + ' V') || ''; + + if (message.cell_voltage[0] < LOW_BATTERY) { + console.log('low battery'); + callNativeApp('lowBattery'); + body.classList.remove('low-battery'); + void body.offsetWidth; // trick for repeating animation + body.classList.add('low-battery'); + } else { + body.classList.remove('low-battery'); + } +}); + +new ROSLIB.Topic({ + ros: ros, + name: '/rosout_agg', + messageType: 'rosgraph_msgs/Log' +}).subscribe(function(message) { + if(message.level >= 4) { + if (message.msg.startsWith('CMD: ')) { + return; + } + callNativeApp('notification', message); + } +}); + +var setMode = new ROSLIB.Service({ + ros: ros, + name : '/mavros/set_mode', + serviceType : 'mavros_msgs/SetMode' +}); + +function setControlMode() { + var CONTROL_MODE = 'STABILIZED'; + setMode.callService(new ROSLIB.ServiceRequest({ custom_mode: CONTROL_MODE })); +} diff --git a/clever/CMakeLists.txt b/clever/CMakeLists.txt index a32e4da9..96c44158 100644 --- a/clever/CMakeLists.txt +++ b/clever/CMakeLists.txt @@ -11,6 +11,7 @@ find_package(catkin REQUIRED COMPONENTS nodelet pluginlib roscpp + genmsg rospy std_msgs message_generation @@ -157,7 +158,9 @@ add_library(aruco_vpe ## Declare a C++ executable ## With catkin_make all packages are built within a single CMake context ## The recommended prefix ensures that target names across packages don't collide -# add_executable(${PROJECT_NAME}_node src/clever_node.cpp) +add_executable(rc src/rc.cpp) + +target_link_libraries(rc ${catkin_LIBRARIES}) ## Rename C++ executable without prefix ## The above recommended prefix causes long target names, the following renames the diff --git a/clever/launch/clever.launch b/clever/launch/clever.launch index c89f32f1..6a582cfd 100644 --- a/clever/launch/clever.launch +++ b/clever/launch/clever.launch @@ -48,6 +48,9 @@ + + + diff --git a/clever/launch/mavros.launch b/clever/launch/mavros.launch index ae2a1c8c..d8a190f1 100644 --- a/clever/launch/mavros.launch +++ b/clever/launch/mavros.launch @@ -37,7 +37,6 @@ - 3dr_radio - actuator_control - hil_controls - - manual_control - vfr_hud - px4flow - vision_speed_estimate diff --git a/clever/src/rc.cpp b/clever/src/rc.cpp new file mode 100644 index 00000000..69f5a09f --- /dev/null +++ b/clever/src/rc.cpp @@ -0,0 +1,118 @@ +// CLEVER mobile remote control support: +// * Send ManualControl messages through UDP +// * `latched_state` topic + +#include +#include +#include +#include +#include "ros/ros.h" +#include "std_msgs/String.h" +#include "mavros_msgs/State.h" +#include "mavros_msgs/ManualControl.h" + +struct ControlMessage +{ + int16_t x, y, z, r; +} __attribute__((packed)); + +class RC +{ +public: + RC(): + nh(), + nh_priv("~") + { + // Create socket thread + std::thread t(&RC::socketThread, this); + t.detach(); + + initLatchedState(); + } + +private: + ros::NodeHandle nh, nh_priv; + ros::Subscriber state_sub; + ros::Publisher state_pub; + mavros_msgs::StateConstPtr state_msg; + + void handleState(const mavros_msgs::StateConstPtr& state) + { + if (!state_msg || + state->connected != state_msg->connected || + state->mode != state_msg->mode || + state->armed != state_msg->armed) { + state_msg = state; + state_pub.publish(state_msg); + } + } + + void initLatchedState() + { + state_sub = nh.subscribe("mavros/state", 1, &RC::handleState, this); + state_pub = nh.advertise("state_latched", 1, true); + } + + int createSocket(int port) + { + int sockfd = socket(AF_INET, SOCK_DGRAM, 0); + + sockaddr_in sin; + sin.sin_family = AF_INET; + sin.sin_addr.s_addr = htonl(INADDR_ANY); + sin.sin_port = htons(port); + + if (bind(sockfd, (sockaddr *)&sin, sizeof(sin)) < 0) { + ROS_FATAL("socket bind error: %s", strerror(errno)); + close(sockfd); + ros::shutdown(); + } + + return sockfd; + } + + void socketThread() + { + int port; + nh_priv.param("port", port, 35602); + int sockfd = createSocket(port); + + char buff[9999]; + + ros::Publisher manual_control_pub = nh.advertise("mavros/manual_control/send", 1); + mavros_msgs::ManualControl manual_control_msg; + + sockaddr_in client_addr; + socklen_t client_addr_size = sizeof(client_addr); + + ROS_INFO("UDP RC initialized on port %d", port); + + while (true) { + // read next UDP packet + int bsize = recvfrom(sockfd, &buff[0], sizeof(buff) - 1, 0, (sockaddr *) &client_addr, &client_addr_size); + + if (bsize < 0) { + ROS_ERROR("recvfrom() error: %s", strerror(errno)); + } else if (bsize != sizeof(ControlMessage)) { + ROS_ERROR_THROTTLE(30, "Wrong UDP packet size: %d", bsize); + } + + // unpack message + // warning: ignore endianness, so the code is platform-dependent + ControlMessage *msg = (ControlMessage *)buff; + + manual_control_msg.x = msg->x; + manual_control_msg.y = msg->y; + manual_control_msg.z = msg->z; + manual_control_msg.r = msg->r; + manual_control_pub.publish(manual_control_msg); + } + } +}; + +int main(int argc, char **argv) +{ + ros::init(argc, argv, "rc"); + RC rc; + ros::spin(); +}