diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 00000000..7a394671 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +apps/ios/cleverrc/roslib.js linguist-vendored +apps/ios/cleverrc/BinUtils.swift linguist-vendored diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..960e17c6 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +/deploy/ros_lib/ +*.pyc diff --git a/README.md b/README.md index 23f94ae7..57978ead 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ Клевер ====== -Клевер +Клевер «Клевер» — это учебный конструктор программируемого квадрокоптера, состоящего из популярных открытых компонентов, а также набор необходимой документации и библиотек для работы с ним. @@ -11,4 +11,73 @@ Для того, чтобы научиться собирать, настраивать, пилотировать и программировать автономный дрон «Клевер», воспользуйтесь этим учебником. -Оглавление находится в файле [SUMMARY.md](/SUMMARY.md). \ No newline at end of file +Основная документация +--------------------- + +https://copterexpress.gitbooks.io/clever/content/ + +**Образ ОС** для RPi 3 с предустановленным и преднастроенным ПО можно скачать [здесь](https://copterexpress.gitbooks.io/clever/content/docs/microsd_images.html). + +Образ включает в себя: + +* Raspbian Stretch +* ROS Kinetic +* Настроенную работу с сетью +* OpenCV +* mavros +* Набор ПО для работы с Клевером + +[Описание API](https://copterexpress.gitbooks.io/clever/content/docs/simple_offboard.html) для автономных полетов. + +Ручная установка +--------- + +Установить ROS Kinetic согласно [инструкциям](http://wiki.ros.org/kinetic/Installation). + +Склонировать репозиторий в папку `/home/pi/catkin_ws/src/clever`: + +```bash +cd ~/catkin_ws/src +git clone https://github.com/CopterExpress/clever.git clever +``` + +Пересобрать ROS-пакеты: + +```bash +cd ~/catkin_ws +catkin_make -j1 +``` + +Включить сервис roscore (если он не включен): + +```bash +sudo systemctl enable /home/pi/catkin_ws/src/clever/deploy/roscore.service +sudo systemctl start roscore +``` + +Включить сервис clever: + +```bash +sudo systemctl enable /home/pi/catkin_ws/src/clever/deploy/clever.service +sudo systemctl start clever +``` + +Зависимости +----------- + +[ROS Kinetic](http://wiki.ros.org/kinetic). + +Необходимые для работы ROS-пакеты: + +* `opencv3` +* `mavros` +* `rosbridge_suite` +* `web_video_server` +* `cv_camera` +* `nodelet` +* `dynamic_reconfigure` +* `bondcpp`, ветка `master` +* `roslint` +* `rosserial` + +TODO: внести в package.xml 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/README.md b/apps/ios/README.md new file mode 100644 index 00000000..536d1dbd --- /dev/null +++ b/apps/ios/README.md @@ -0,0 +1,10 @@ +iOS-приложение для управления Клевером +-------------------------------------- + +Для установки зависимостей необходим [CocoaPods](https://cocoapods.org): + +```bash +pod install +``` + +Для разработки и сборки откройте в XCode файл `cleverrc.xcworkspace`. 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 00000000..ae590b79 Binary files /dev/null and b/apps/ios/cleverrc/Assets.xcassets/AppIcon.appiconset/cleverios180-1.png differ 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 00000000..ae590b79 Binary files /dev/null and b/apps/ios/cleverrc/Assets.xcassets/AppIcon.appiconset/cleverios180.png differ 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/README.md b/apps/ios/cleverrc/README.md new file mode 100644 index 00000000..536d1dbd --- /dev/null +++ b/apps/ios/cleverrc/README.md @@ -0,0 +1,10 @@ +iOS-приложение для управления Клевером +-------------------------------------- + +Для установки зависимостей необходим [CocoaPods](https://cocoapods.org): + +```bash +pod install +``` + +Для разработки и сборки откройте в XCode файл `cleverrc.xcworkspace`. 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/aruco_pose/CMakeLists.txt b/aruco_pose/CMakeLists.txt new file mode 100644 index 00000000..90403cf5 --- /dev/null +++ b/aruco_pose/CMakeLists.txt @@ -0,0 +1,210 @@ +cmake_minimum_required(VERSION 2.8.3) +project(aruco_pose) + +add_definitions(-std=c++11 -Wall -g) + +## Compile as C++11, supported in ROS Kinetic and newer +add_compile_options(-std=c++11) + +## Find catkin macros and libraries +## if COMPONENTS list like find_package(catkin REQUIRED COMPONENTS xyz) +## is used, also find other catkin packages +find_package(catkin REQUIRED COMPONENTS + nodelet + pluginlib + roscpp + image_transport + cv_bridge + tf + #tf2 + #tf2_ros + #aruco_msgs +) + +## System dependencies are found with CMake's conventions +# find_package(Boost REQUIRED COMPONENTS system) + + +## Uncomment this if the package has a setup.py. This macro ensures +## modules and global scripts declared therein get installed +## See http://ros.org/doc/api/catkin/html/user_guide/setup_dot_py.html +# catkin_python_setup() + +################################################ +## Declare ROS messages, services and actions ## +################################################ + +## To declare and build messages, services or actions from within this +## package, follow these steps: +## * Let MSG_DEP_SET be the set of packages whose message types you use in +## your messages/services/actions (e.g. std_msgs, actionlib_msgs, ...). +## * In the file package.xml: +## * add a build_depend tag for "message_generation" +## * add a build_depend and a run_depend tag for each package in MSG_DEP_SET +## * If MSG_DEP_SET isn't empty the following dependency has been pulled in +## but can be declared for certainty nonetheless: +## * add a run_depend tag for "message_runtime" +## * In this file (CMakeLists.txt): +## * add "message_generation" and every package in MSG_DEP_SET to +## find_package(catkin REQUIRED COMPONENTS ...) +## * add "message_runtime" and every package in MSG_DEP_SET to +## catkin_package(CATKIN_DEPENDS ...) +## * uncomment the add_*_files sections below as needed +## and list every .msg/.srv/.action file to be processed +## * uncomment the generate_messages entry below +## * add every package in MSG_DEP_SET to generate_messages(DEPENDENCIES ...) + +## Generate messages in the 'msg' folder +#add_message_files( +# FILES +# Marker.msg +# MarkerArray.msg +#) + +## Generate services in the 'srv' folder +# add_service_files( +# FILES +# Service1.srv +# Service2.srv +# ) + +## Generate actions in the 'action' folder +# add_action_files( +# FILES +# Action1.action +# Action2.action +# ) + +## Generate added messages and services with any dependencies listed here +#generate_messages( +# DEPENDENCIES +# std_msgs # Or other packages containing msgs +#) + +################################################ +## Declare ROS dynamic reconfigure parameters ## +################################################ + +## To declare and build dynamic reconfigure parameters within this +## package, follow these steps: +## * In the file package.xml: +## * add a build_depend and a run_depend tag for "dynamic_reconfigure" +## * In this file (CMakeLists.txt): +## * add "dynamic_reconfigure" to +## find_package(catkin REQUIRED COMPONENTS ...) +## * uncomment the "generate_dynamic_reconfigure_options" section below +## and list every .cfg file to be processed + +## Generate dynamic reconfigure parameters in the 'cfg' folder +# generate_dynamic_reconfigure_options( +# cfg/DynReconf1.cfg +# cfg/DynReconf2.cfg +# ) + +################################### +## catkin specific configuration ## +################################### +## The catkin_package macro generates cmake config files for your package +## Declare things to be passed to dependent projects +## INCLUDE_DIRS: uncomment this if you package contains header files +## LIBRARIES: libraries you create in this project that dependent projects also need +## CATKIN_DEPENDS: catkin_packages dependent projects also need +## DEPENDS: system dependencies of this project that dependent projects also need +catkin_package( +# INCLUDE_DIRS include + LIBRARIES aruco_pose +# CATKIN_DEPENDS other_catkin_pkg +# DEPENDS system_lib +) + +########### +## Build ## +########### + +## Specify additional locations of header files +## Your package locations should be listed before other locations +include_directories( +# include + ${catkin_INCLUDE_DIRS} +) + +## Declare a C++ library +add_library(${PROJECT_NAME} + src/aruco_pose.cpp +) + +## Add cmake target dependencies of the library +## as an example, code may need to be generated before libraries +## either from message generation or dynamic reconfigure +# add_dependencies(${PROJECT_NAME} ${${PROJECT_NAME}_EXPORTED_TARGETS} ${catkin_EXPORTED_TARGETS}) + +## 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/aruco_pose_node.cpp) + +## Rename C++ executable without prefix +## The above recommended prefix causes long target names, the following renames the +## target back to the shorter version for ease of user use +## e.g. "rosrun someones_pkg node" instead of "rosrun someones_pkg someones_pkg_node" +# set_target_properties(${PROJECT_NAME}_node PROPERTIES OUTPUT_NAME node PREFIX "") + +## Add cmake target dependencies of the executable +## same as for the library above +# add_dependencies(${PROJECT_NAME}_node ${${PROJECT_NAME}_EXPORTED_TARGETS} ${catkin_EXPORTED_TARGETS}) + +## Specify libraries to link a library or executable target against +link_directories(/opt/ros/kinetic/lib) + +target_link_libraries(${PROJECT_NAME} + ${catkin_LIBRARIES} + "/opt/ros/kinetic/lib/libopencv_aruco3.so" # TODO: fix launch fails with .so loading +) + +############# +## Install ## +############# + +# all install targets should use catkin DESTINATION variables +# See http://ros.org/doc/api/catkin/html/adv_user_guide/variables.html + +## Mark executable scripts (Python etc.) for installation + ## in contrast to setup.py, you can choose the destination +# install(PROGRAMS +# scripts/my_python_script +# DESTINATION ${CATKIN_PACKAGE_BIN_DESTINATION} +# ) + +## Mark executables and/or libraries for installation +# install(TARGETS ${PROJECT_NAME} ${PROJECT_NAME}_node +# ARCHIVE DESTINATION ${CATKIN_PACKAGE_LIB_DESTINATION} +# LIBRARY DESTINATION ${CATKIN_PACKAGE_LIB_DESTINATION} +# RUNTIME DESTINATION ${CATKIN_PACKAGE_BIN_DESTINATION} +# ) + +## Mark cpp header files for installation +# install(DIRECTORY include/${PROJECT_NAME}/ +# DESTINATION ${CATKIN_PACKAGE_INCLUDE_DESTINATION} +# FILES_MATCHING PATTERN "*.h" +# PATTERN ".svn" EXCLUDE +# ) + +## Mark other files for installation (e.g. launch and bag files, etc.) +# install(FILES +# # myfile1 +# # myfile2 +# DESTINATION ${CATKIN_PACKAGE_SHARE_DESTINATION} +# ) + +############# +## Testing ## +############# + +## Add gtest based cpp test target and link libraries +# catkin_add_gtest(${PROJECT_NAME}-test test/test_aruco_pose.cpp) +# if(TARGET ${PROJECT_NAME}-test) +# target_link_libraries(${PROJECT_NAME}-test ${PROJECT_NAME}) +# endif() + +## Add folders to be run by python nosetests +# catkin_add_nosetests(test) diff --git a/aruco_pose/nodelet_plugins.xml b/aruco_pose/nodelet_plugins.xml new file mode 100644 index 00000000..79d5ddad --- /dev/null +++ b/aruco_pose/nodelet_plugins.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/aruco_pose/package.xml b/aruco_pose/package.xml new file mode 100644 index 00000000..b4da81c3 --- /dev/null +++ b/aruco_pose/package.xml @@ -0,0 +1,62 @@ + + + aruco_pose + 0.0.0 + ArUco maps precise pose estimation nodelet + + + + + Oleg Kalachev + + + + + + TODO + + + + + + + + + + + + + + + + + + + + + + + + + + catkin + + nodelet + roscpp + image_transport + cv_bridge + tf + + nodelet + roscpp + image_transport + cv_bridge + tf + + + + + + + + diff --git a/aruco_pose/src/aruco_pose.cpp b/aruco_pose/src/aruco_pose.cpp new file mode 100644 index 00000000..d6f4853f --- /dev/null +++ b/aruco_pose/src/aruco_pose.cpp @@ -0,0 +1,292 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace aruco_pose { + +class ArucoPose : public nodelet::Nodelet { + tf::TransformBroadcaster br; + cv::Ptr dictionary; + cv::Ptr parameters; + cv::Ptr board; + std::string frame_id_; + image_transport::CameraSubscriber img_sub; + image_transport::Publisher img_pub; + ros::Publisher marker_pub; + ros::Publisher pose_pub; + ros::NodeHandle nh_, nh_priv_; + + virtual void onInit(); + void createBoard(); + cv::Point3f getObjPointsCenter(cv::Mat objPoints); + void detect(const sensor_msgs::ImageConstPtr&, const sensor_msgs::CameraInfoConstPtr&); + void parseCameraInfo(const sensor_msgs::CameraInfoConstPtr&, cv::Mat&, cv::Mat&); + tf::Transform aruco2tf(cv::Mat rvec, cv::Mat tvec); +}; + +void ArucoPose::onInit() { + ROS_INFO("Initializing aruco_pose"); + nh_ = getNodeHandle(); + nh_priv_ = getPrivateNodeHandle(); + + nh_priv_.param("frame_id", frame_id_, std::string("aruco_map")); + + dictionary = cv::aruco::getPredefinedDictionary(cv::aruco::DICT_4X4_1000); + parameters = cv::aruco::DetectorParameters::create(); + + try + { + createBoard(); + } + catch (const std::exception &exc) + { + std::cerr << exc.what(); + exit(0); + } + + image_transport::ImageTransport it(nh_); + img_sub = it.subscribeCamera("image", 1, &ArucoPose::detect, this); + + image_transport::ImageTransport it_priv(nh_priv_); + img_pub = it_priv.advertise("debug", 1); + + pose_pub = nh_priv_.advertise("pose", 1); + + ROS_INFO("aruco_pose nodelet inited"); +} + +cv::Ptr createCustomGridBoard(int markersX, int markersY, float markerLength, float markerSeparationX, float markerSeparationY, + const cv::Ptr &dictionary, std::vector ids) { + + CV_Assert(markersX > 0 && markersY > 0 && markerLength > 0 && markerSeparationX > 0 && markerSeparationY > 0); + + cv::Ptr res = cv::makePtr(); + + res->dictionary = dictionary; + + size_t totalMarkers = (size_t) markersX * markersY; + res->ids = ids; + res->objPoints.reserve(totalMarkers); + + // calculate Board objPoints + float maxY = (float)markersY * markerLength + (markersY - 1) * markerSeparationY; + for(int y = 0; y < markersY; y++) { + for(int x = 0; x < markersX; x++) { + std::vector< cv::Point3f > corners; + corners.resize(4); + corners[0] = cv::Point3f(x * (markerLength + markerSeparationX), + maxY - y * (markerLength + markerSeparationY), 0); + corners[1] = corners[0] + cv::Point3f(markerLength, 0, 0); + corners[2] = corners[0] + cv::Point3f(markerLength, -markerLength, 0); + corners[3] = corners[0] + cv::Point3f(0, -markerLength, 0); + res->objPoints.push_back(corners); + } + } + + return res; +} + +#include "fix.cpp" + +void ArucoPose::createBoard() +{ + static auto map_image_pub = nh_priv_.advertise("map_image", 1, true); + cv_bridge::CvImage map_image_msg; + cv::Mat map_image; + + std::string type; + + nh_priv_.param("type", type, "gridboard"); + if (type == "gridboard") + { + ROS_INFO("Initialize gridboard"); + + int markers_x, markers_y, first_marker; + float markers_side, markers_sep_x, markers_sep_y; + std::vector marker_ids; + nh_priv_.param("markers_x", markers_x, 10); + nh_priv_.param("markers_y", markers_y, 10); + nh_priv_.param("first_marker", first_marker, 0); + + if (!nh_priv_.getParam("markers_side", markers_side)) + { + ROS_ERROR("gridboard: required parameter ~markers_side is not set."); + exit(1); + } + + if (!nh_priv_.getParam("markers_sep_x", markers_sep_x)) + { + if (!nh_priv_.getParam("markers_sep", markers_sep_x)) + { + ROS_ERROR("gridboard: ~markers_sep_x or ~markers_sep parameters are required"); + exit(1); + } + } + + if (!nh_priv_.getParam("markers_sep_y", markers_sep_y)) + { + if (!nh_priv_.getParam("markers_sep", markers_sep_y)) + { + ROS_ERROR("gridboard: ~markers_sep_y or ~markers_sep parameters are required"); + exit(1); + } + } + + if (nh_priv_.getParam("marker_ids", marker_ids)) + { + if (markers_x * markers_y != marker_ids.size()) + { + ROS_FATAL("~marker_ids length should be equal to ~markers_x * ~markers_y"); + exit(1); + } + } + else + { + // Fill marker_ids automatically + marker_ids.resize(markers_x * markers_y); + for(int i = 0; i < markers_x * markers_y; i++) + { + marker_ids.at(i) = first_marker++; + } + } + + // Create grid board + board = createCustomGridBoard(markers_x, markers_y, markers_side, markers_sep_x, markers_sep_y, dictionary, marker_ids); + + // Publish map image for debugging + _drawPlanarBoard(board, cv::Size(2000, 2000), map_image, 50, 1); + + cv::cvtColor(map_image, map_image, CV_GRAY2BGR); + + map_image_msg.encoding = sensor_msgs::image_encodings::BGR8; + map_image_msg.image = map_image; + map_image_pub.publish(map_image_msg.toImageMsg()); + } + else if (type == "custom") + { + // Not implemented yet + ROS_FATAL("Custom boards are not implemented yet."); + } + else + { + ROS_ERROR("Incorrect map type '%s'", type.c_str()); + } +} + +cv::Point3f ArucoPose::getObjPointsCenter(cv::Mat objPoints) { + float min_x = std::numeric_limits::max(); + float max_x = std::numeric_limits::min(); + float min_y = min_x, max_y = max_x; + for (int i = 0; i < objPoints.rows; i++) { + max_x = std::max(max_x, objPoints.at(i, 0)); + max_y = std::max(max_y, objPoints.at(i, 1)); + min_x = std::min(min_x, objPoints.at(i, 0)); + min_y = std::min(min_y, objPoints.at(i, 1)); + } + cv::Point3f res((min_x + max_x) / 2, (min_y + max_y) / 2, 0); + return res; +} + +void ArucoPose::detect(const sensor_msgs::ImageConstPtr& msg, const sensor_msgs::CameraInfoConstPtr &cinfo) { + cv::Mat image = cv_bridge::toCvShare(msg, "bgr8")->image; + + std::vector markerIds; + std::vector> markerCorners; + std::vector> rejectedCandidates; + + cv::aruco::detectMarkers(image, dictionary, markerCorners, markerIds, parameters, rejectedCandidates); + + cv::Mat cameraMatrix(3, 3, CV_64F); + cv::Mat distCoeffs(8, 1, CV_64F); + parseCameraInfo(cinfo, cameraMatrix, distCoeffs); + + if (markerIds.size() > 0) { + + cv::Mat rvec, tvec, objPoints; + int valid = _estimatePoseBoard(markerCorners, markerIds, board, cameraMatrix, distCoeffs, + rvec, tvec, false, objPoints); + + if (valid) { + // Send map transform + tf::StampedTransform transform(aruco2tf(rvec, tvec), msg->header.stamp, cinfo->header.frame_id, frame_id_); + br.sendTransform(transform); + + // Publish map pose + static geometry_msgs::PoseStamped ps; + ps.header.frame_id = frame_id_; + ps.header.stamp = msg->header.stamp; + ps.pose.orientation.w = 1; + pose_pub.publish(ps); + + // Send reference point + cv::Point3f ref = getObjPointsCenter(objPoints); + tf::Vector3 ref_vector3 = tf::Vector3(ref.x, ref.y, ref.z); + tf::Quaternion q(0, 0, 0); + static tf::StampedTransform ref_transform; + ref_transform.stamp_ = msg->header.stamp; + ref_transform.frame_id_ = frame_id_; + ref_transform.child_frame_id_ = "aruco_map_reference"; + ref_transform.setOrigin(ref_vector3); + ref_transform.setRotation(q); + br.sendTransform(ref_transform); + + if(img_pub.getNumSubscribers() > 0) + { + // Publish debug image + cv::aruco::drawDetectedMarkers(image, markerCorners, markerIds); + cv::aruco::drawAxis(image, cameraMatrix, distCoeffs, rvec, tvec, 0.3); + } + } + } + + if (img_pub.getNumSubscribers() > 0) + { + cv_bridge::CvImage out_msg; + out_msg.header.frame_id = msg->header.frame_id; + out_msg.header.stamp = msg->header.stamp; + out_msg.encoding = sensor_msgs::image_encodings::BGR8; + out_msg.image = image; + img_pub.publish(out_msg.toImageMsg()); + } +} + +void ArucoPose::parseCameraInfo(const sensor_msgs::CameraInfoConstPtr &cinfo, cv::Mat &cameraMat, cv::Mat &distCoeffs) { + for (int i = 0; i < 3; ++i) { + for (int j = 0; j < 3; ++j) { + cameraMat.at(i, j) = cinfo->K[3 * i + j]; + } + } + for (int k = 0; k < cinfo->D.size(); k++) { + distCoeffs.at(k) = cinfo->D[k]; + } +} + +tf::Transform ArucoPose::aruco2tf(cv::Mat rvec, cv::Mat tvec) { + + cv::Mat rot; + cv::Rodrigues(rvec, rot); + + tf::Matrix3x3 tf_rot(rot.at(0,0), rot.at(0,1), rot.at(0,2), + rot.at(1,0), rot.at(1,1), rot.at(1,2), + rot.at(2,0), rot.at(2,1), rot.at(2,2)); + tf::Vector3 tf_orig(tvec.at(0,0), tvec.at(1,0), tvec.at(2,0)); + return tf::Transform(tf_rot, tf_orig); +} + +PLUGINLIB_EXPORT_CLASS(ArucoPose, nodelet::Nodelet) + +} diff --git a/aruco_pose/src/fix.cpp b/aruco_pose/src/fix.cpp new file mode 100644 index 00000000..c15bf3b7 --- /dev/null +++ b/aruco_pose/src/fix.cpp @@ -0,0 +1,145 @@ +using namespace cv; +using namespace cv::aruco; + +// Temporal fix! +// TODO: remove +// fix strange bug in our OpenCV version + +void _getBoardObjectAndImagePoints(const Ptr &board, InputArrayOfArrays detectedCorners, + InputArray detectedIds, OutputArray objPoints, OutputArray imgPoints) { + + CV_Assert(board->ids.size() == board->objPoints.size()); + CV_Assert(detectedIds.total() == detectedCorners.total()); + + size_t nDetectedMarkers = detectedIds.total(); + + std::vector< Point3f > objPnts; + objPnts.reserve(nDetectedMarkers); + + std::vector< Point2f > imgPnts; + imgPnts.reserve(nDetectedMarkers); + + // look for detected markers that belong to the board and get their information + for(unsigned int i = 0; i < nDetectedMarkers; i++) { + int currentId = detectedIds.getMat().ptr< int >(0)[i]; + for(unsigned int j = 0; j < board->ids.size(); j++) { + if(currentId == board->ids[j]) { + for(int p = 0; p < 4; p++) { + objPnts.push_back(board->objPoints[j][p]); + imgPnts.push_back(detectedCorners.getMat(i).ptr< Point2f >(0)[p]); + } + } + } + } + + // create output + Mat(objPnts).copyTo(objPoints); + Mat(imgPnts).copyTo(imgPoints); +} + +int _estimatePoseBoard(InputArrayOfArrays _corners, InputArray _ids, const Ptr &board, + InputArray _cameraMatrix, InputArray _distCoeffs, OutputArray _rvec, + OutputArray _tvec, bool useExtrinsicGuess, Mat &objPoints) { + + CV_Assert(_corners.total() == _ids.total()); + + // get object and image points for the solvePnP function + Mat /*objPoints, */imgPoints; + _getBoardObjectAndImagePoints(board, _corners, _ids, objPoints, imgPoints); + + CV_Assert(imgPoints.total() == objPoints.total()); + + if(objPoints.total() == 0) // 0 of the detected markers in board + return 0; + +// std::cout << "objPoints: " << objPoints << std::endl; +// std::cout << "imgPoints: " << imgPoints << std::endl; + + solvePnP(objPoints, imgPoints, _cameraMatrix, _distCoeffs, _rvec, _tvec, useExtrinsicGuess); + + // divide by four since all the four corners are concatenated in the array for each marker + return (int)objPoints.total() / 4; +} + +void _drawPlanarBoard(Board *_board, Size outSize, OutputArray _img, int marginSize, + int borderBits) { + + CV_Assert(outSize.area() > 0); + CV_Assert(marginSize >= 0); + + _img.create(outSize, CV_8UC1); + Mat out = _img.getMat(); + out.setTo(Scalar::all(255)); + out.adjustROI(-marginSize, -marginSize, -marginSize, -marginSize); + + // calculate max and min values in XY plane + CV_Assert(_board->objPoints.size() > 0); + float minX, maxX, minY, maxY; + minX = maxX = _board->objPoints[0][0].x; + minY = maxY = _board->objPoints[0][0].y; + + for(unsigned int i = 0; i < _board->objPoints.size(); i++) { + for(int j = 0; j < 4; j++) { + minX = min(minX, _board->objPoints[i][j].x); + maxX = max(maxX, _board->objPoints[i][j].x); + minY = min(minY, _board->objPoints[i][j].y); + maxY = max(maxY, _board->objPoints[i][j].y); + } + } + + float sizeX = maxX - minX; + float sizeY = maxY - minY; + + // proportion transformations + float xReduction = sizeX / float(out.cols); + float yReduction = sizeY / float(out.rows); + + // determine the zone where the markers are placed + if(xReduction > yReduction) { + int nRows = int(sizeY / xReduction); + int rowsMargins = (out.rows - nRows) / 2; + out.adjustROI(-rowsMargins, -rowsMargins, 0, 0); + } else { + int nCols = int(sizeX / yReduction); + int colsMargins = (out.cols - nCols) / 2; + out.adjustROI(0, 0, -colsMargins, -colsMargins); + } + + // now paint each marker + Dictionary &dictionary = *(_board->dictionary); + Mat marker; + Point2f outCorners[3]; + Point2f inCorners[3]; + for(unsigned int m = 0; m < _board->objPoints.size(); m++) { + // transform corners to markerZone coordinates + for(int j = 0; j < 3; j++) { + Point2f pf = Point2f(_board->objPoints[m][j].x, _board->objPoints[m][j].y); + // move top left to 0, 0 + pf -= Point2f(minX, minY); + pf.x = pf.x / sizeX * float(out.cols); + pf.y = (1.0f - pf.y / sizeY) * float(out.rows); + outCorners[j] = pf; + } + + // get marker + Size dst_sz(outCorners[2] - outCorners[0]); // assuming CCW order + dst_sz.width = dst_sz.height = std::min(dst_sz.width, dst_sz.height); //marker should be square + dictionary.drawMarker(_board->ids[m], dst_sz.width, marker, borderBits); + + if((outCorners[0].y == outCorners[1].y) && (outCorners[1].x == outCorners[2].x)) { + // marker is aligned to image axes + marker.copyTo(out(Rect(outCorners[0], dst_sz))); + continue; + } + + // interpolate tiny marker to marker position in markerZone + inCorners[0] = Point2f(-0.5f, -0.5f); + inCorners[1] = Point2f(marker.cols - 0.5f, -0.5f); + inCorners[2] = Point2f(marker.cols - 0.5f, marker.rows - 0.5f); + + // remove perspective + Mat transformation = getAffineTransform(inCorners, outCorners); + warpAffine(marker, out, transformation, out.size(), INTER_LINEAR, + BORDER_TRANSPARENT); + } +} diff --git a/img/11_1.png b/assets/11_1.png similarity index 100% rename from img/11_1.png rename to assets/11_1.png diff --git a/img/11_2.png b/assets/11_2.png similarity index 100% rename from img/11_2.png rename to assets/11_2.png diff --git a/img/11_3.png b/assets/11_3.png similarity index 100% rename from img/11_3.png rename to assets/11_3.png diff --git a/img/11_4.png b/assets/11_4.png similarity index 100% rename from img/11_4.png rename to assets/11_4.png diff --git a/img/11_5.png b/assets/11_5.png similarity index 100% rename from img/11_5.png rename to assets/11_5.png diff --git a/img/13_1.png b/assets/13_1.png similarity index 100% rename from img/13_1.png rename to assets/13_1.png diff --git a/img/13_10.png b/assets/13_10.png similarity index 100% rename from img/13_10.png rename to assets/13_10.png diff --git a/img/13_11.png b/assets/13_11.png similarity index 100% rename from img/13_11.png rename to assets/13_11.png diff --git a/img/13_2.png b/assets/13_2.png similarity index 100% rename from img/13_2.png rename to assets/13_2.png diff --git a/img/13_3.jpg b/assets/13_3.jpg similarity index 100% rename from img/13_3.jpg rename to assets/13_3.jpg diff --git a/img/13_4.png b/assets/13_4.png similarity index 100% rename from img/13_4.png rename to assets/13_4.png diff --git a/img/13_5.png b/assets/13_5.png similarity index 100% rename from img/13_5.png rename to assets/13_5.png diff --git a/img/13_6.png b/assets/13_6.png similarity index 100% rename from img/13_6.png rename to assets/13_6.png diff --git a/img/13_7.png b/assets/13_7.png similarity index 100% rename from img/13_7.png rename to assets/13_7.png diff --git a/img/13_8.png b/assets/13_8.png similarity index 100% rename from img/13_8.png rename to assets/13_8.png diff --git a/img/13_9.png b/assets/13_9.png similarity index 100% rename from img/13_9.png rename to assets/13_9.png diff --git a/img/15_1.png b/assets/15_1.png similarity index 100% rename from img/15_1.png rename to assets/15_1.png diff --git a/img/15_2.png b/assets/15_2.png similarity index 100% rename from img/15_2.png rename to assets/15_2.png diff --git a/img/15_3.png b/assets/15_3.png similarity index 100% rename from img/15_3.png rename to assets/15_3.png diff --git a/img/15_4.png b/assets/15_4.png similarity index 100% rename from img/15_4.png rename to assets/15_4.png diff --git a/img/15_5.png b/assets/15_5.png similarity index 100% rename from img/15_5.png rename to assets/15_5.png diff --git a/img/15_6.png b/assets/15_6.png similarity index 100% rename from img/15_6.png rename to assets/15_6.png diff --git a/img/15_7.png b/assets/15_7.png similarity index 100% rename from img/15_7.png rename to assets/15_7.png diff --git a/img/16_1.png b/assets/16_1.png similarity index 100% rename from img/16_1.png rename to assets/16_1.png diff --git a/img/16_2.png b/assets/16_2.png similarity index 100% rename from img/16_2.png rename to assets/16_2.png diff --git a/img/16_3.png b/assets/16_3.png similarity index 100% rename from img/16_3.png rename to assets/16_3.png diff --git a/img/16_4.png b/assets/16_4.png similarity index 100% rename from img/16_4.png rename to assets/16_4.png diff --git a/img/1_1.png b/assets/1_1.png similarity index 100% rename from img/1_1.png rename to assets/1_1.png diff --git a/img/1_10.png b/assets/1_10.png similarity index 100% rename from img/1_10.png rename to assets/1_10.png diff --git a/img/1_11.png b/assets/1_11.png similarity index 100% rename from img/1_11.png rename to assets/1_11.png diff --git a/img/1_12.png b/assets/1_12.png similarity index 100% rename from img/1_12.png rename to assets/1_12.png diff --git a/img/1_13.png b/assets/1_13.png similarity index 100% rename from img/1_13.png rename to assets/1_13.png diff --git a/img/1_2.png b/assets/1_2.png similarity index 100% rename from img/1_2.png rename to assets/1_2.png diff --git a/img/1_3.png b/assets/1_3.png similarity index 100% rename from img/1_3.png rename to assets/1_3.png diff --git a/img/1_4.png b/assets/1_4.png similarity index 100% rename from img/1_4.png rename to assets/1_4.png diff --git a/img/1_5.png b/assets/1_5.png similarity index 100% rename from img/1_5.png rename to assets/1_5.png diff --git a/img/1_6.png b/assets/1_6.png similarity index 100% rename from img/1_6.png rename to assets/1_6.png diff --git a/img/1_7.png b/assets/1_7.png similarity index 100% rename from img/1_7.png rename to assets/1_7.png diff --git a/img/1_8.png b/assets/1_8.png similarity index 100% rename from img/1_8.png rename to assets/1_8.png diff --git a/img/1_9.png b/assets/1_9.png similarity index 100% rename from img/1_9.png rename to assets/1_9.png diff --git a/img/2_1.png b/assets/2_1.png similarity index 100% rename from img/2_1.png rename to assets/2_1.png diff --git a/img/2_2.png b/assets/2_2.png similarity index 100% rename from img/2_2.png rename to assets/2_2.png diff --git a/img/2_3.png b/assets/2_3.png similarity index 100% rename from img/2_3.png rename to assets/2_3.png diff --git a/img/2_4.png b/assets/2_4.png similarity index 100% rename from img/2_4.png rename to assets/2_4.png diff --git a/img/2_5.png b/assets/2_5.png similarity index 100% rename from img/2_5.png rename to assets/2_5.png diff --git a/img/2_6.png b/assets/2_6.png similarity index 100% rename from img/2_6.png rename to assets/2_6.png diff --git a/img/2_7.png b/assets/2_7.png similarity index 100% rename from img/2_7.png rename to assets/2_7.png diff --git a/img/2_8.png b/assets/2_8.png similarity index 100% rename from img/2_8.png rename to assets/2_8.png diff --git a/img/2_9.png b/assets/2_9.png similarity index 100% rename from img/2_9.png rename to assets/2_9.png diff --git a/img/4_1.png b/assets/4_1.png similarity index 100% rename from img/4_1.png rename to assets/4_1.png diff --git a/img/4_2.png b/assets/4_2.png similarity index 100% rename from img/4_2.png rename to assets/4_2.png diff --git a/img/4_3.png b/assets/4_3.png similarity index 100% rename from img/4_3.png rename to assets/4_3.png diff --git a/img/4_4.png b/assets/4_4.png similarity index 100% rename from img/4_4.png rename to assets/4_4.png diff --git a/img/4_5.png b/assets/4_5.png similarity index 100% rename from img/4_5.png rename to assets/4_5.png diff --git a/img/4_6.png b/assets/4_6.png similarity index 100% rename from img/4_6.png rename to assets/4_6.png diff --git a/img/7_1.png b/assets/7_1.png similarity index 100% rename from img/7_1.png rename to assets/7_1.png diff --git a/img/7_2.png b/assets/7_2.png similarity index 100% rename from img/7_2.png rename to assets/7_2.png diff --git a/img/7_3.png b/assets/7_3.png similarity index 100% rename from img/7_3.png rename to assets/7_3.png diff --git a/img/7_4.png b/assets/7_4.png similarity index 100% rename from img/7_4.png rename to assets/7_4.png diff --git a/img/8_1.png b/assets/8_1.png similarity index 100% rename from img/8_1.png rename to assets/8_1.png diff --git a/img/8_2.png b/assets/8_2.png similarity index 100% rename from img/8_2.png rename to assets/8_2.png diff --git a/img/8_3.png b/assets/8_3.png similarity index 100% rename from img/8_3.png rename to assets/8_3.png diff --git a/img/8_4.png b/assets/8_4.png similarity index 100% rename from img/8_4.png rename to assets/8_4.png diff --git a/img/8_5.png b/assets/8_5.png similarity index 100% rename from img/8_5.png rename to assets/8_5.png diff --git a/img/8_6.png b/assets/8_6.png similarity index 100% rename from img/8_6.png rename to assets/8_6.png diff --git a/img/9_1.png b/assets/9_1.png similarity index 100% rename from img/9_1.png rename to assets/9_1.png diff --git a/img/9_2.png b/assets/9_2.png similarity index 100% rename from img/9_2.png rename to assets/9_2.png diff --git a/img/Clever main.png b/assets/Clever main.png similarity index 100% rename from img/Clever main.png rename to assets/Clever main.png diff --git a/img/Clevermain.png b/assets/Clevermain.png similarity index 100% rename from img/Clevermain.png rename to assets/Clevermain.png diff --git a/img/addEqipment.jpg b/assets/addEqipment.jpg similarity index 100% rename from img/addEqipment.jpg rename to assets/addEqipment.jpg diff --git a/img/airframeSetup.jpg b/assets/airframeSetup.jpg similarity index 100% rename from img/airframeSetup.jpg rename to assets/airframeSetup.jpg diff --git a/img/allElements.png b/assets/allElements.png similarity index 100% rename from img/allElements.png rename to assets/allElements.png diff --git a/img/attentionSave.jpg b/assets/attentionSave.jpg similarity index 100% rename from img/attentionSave.jpg rename to assets/attentionSave.jpg diff --git a/img/brrc2205.png b/assets/brrc2205.png similarity index 100% rename from img/brrc2205.png rename to assets/brrc2205.png diff --git a/img/brrc2205on.png b/assets/brrc2205on.png similarity index 100% rename from img/brrc2205on.png rename to assets/brrc2205on.png diff --git a/img/brrc2205ondeck.png b/assets/brrc2205ondeck.png similarity index 100% rename from img/brrc2205ondeck.png rename to assets/brrc2205ondeck.png diff --git a/img/calibrateESC.jpg b/assets/calibrateESC.jpg similarity index 100% rename from img/calibrateESC.jpg rename to assets/calibrateESC.jpg diff --git a/img/calibratePIDparams.jpg b/assets/calibratePIDparams.jpg similarity index 100% rename from img/calibratePIDparams.jpg rename to assets/calibratePIDparams.jpg diff --git a/img/calibrateView.jpg b/assets/calibrateView.jpg similarity index 100% rename from img/calibrateView.jpg rename to assets/calibrateView.jpg diff --git a/img/calibrateViewStart.jpg b/assets/calibrateViewStart.jpg similarity index 100% rename from img/calibrateViewStart.jpg rename to assets/calibrateViewStart.jpg diff --git a/img/calibrateaxcel.jpg b/assets/calibrateaxcel.jpg similarity index 100% rename from img/calibrateaxcel.jpg rename to assets/calibrateaxcel.jpg diff --git a/img/calibrateaxcelstart.jpg b/assets/calibrateaxcelstart.jpg similarity index 100% rename from img/calibrateaxcelstart.jpg rename to assets/calibrateaxcelstart.jpg diff --git a/img/calibratecompass.jpg b/assets/calibratecompass.jpg similarity index 100% rename from img/calibratecompass.jpg rename to assets/calibratecompass.jpg diff --git a/img/calibrategyro.jpg b/assets/calibrategyro.jpg similarity index 100% rename from img/calibrategyro.jpg rename to assets/calibrategyro.jpg diff --git a/img/casebattery.png b/assets/casebattery.png similarity index 100% rename from img/casebattery.png rename to assets/casebattery.png diff --git a/img/chooseSwitch.jpg b/assets/chooseSwitch.jpg similarity index 100% rename from img/chooseSwitch.jpg rename to assets/chooseSwitch.jpg diff --git a/img/clever.jpg b/assets/clever.jpg similarity index 100% rename from img/clever.jpg rename to assets/clever.jpg diff --git a/img/connectBattery.png b/assets/connectBattery.png similarity index 100% rename from img/connectBattery.png rename to assets/connectBattery.png diff --git a/img/connectingRadio.png b/assets/connectingRadio.png similarity index 100% rename from img/connectingRadio.png rename to assets/connectingRadio.png diff --git a/img/connectionESCtoReceiver.png b/assets/connectionESCtoReceiver.png similarity index 100% rename from img/connectionESCtoReceiver.png rename to assets/connectionESCtoReceiver.png diff --git a/img/connectionLost.jpg b/assets/connectionLost.jpg similarity index 100% rename from img/connectionLost.jpg rename to assets/connectionLost.jpg diff --git a/img/connectionOK.jpg b/assets/connectionOK.jpg similarity index 100% rename from img/connectionOK.jpg rename to assets/connectionOK.jpg diff --git a/img/connectionPixhawk.png b/assets/connectionPixhawk.png similarity index 100% rename from img/connectionPixhawk.png rename to assets/connectionPixhawk.png diff --git a/img/consistofTransmitter.jpg b/assets/consistofTransmitter.jpg similarity index 100% rename from img/consistofTransmitter.jpg rename to assets/consistofTransmitter.jpg diff --git a/img/cutwire14AWG.jpg b/assets/cutwire14AWG.jpg similarity index 100% rename from img/cutwire14AWG.jpg rename to assets/cutwire14AWG.jpg diff --git a/img/escDYSzap.png b/assets/escDYSzap.png similarity index 100% rename from img/escDYSzap.png rename to assets/escDYSzap.png diff --git a/img/escWires.png b/assets/escWires.png similarity index 100% rename from img/escWires.png rename to assets/escWires.png diff --git a/img/explosion.png b/assets/explosion.png similarity index 100% rename from img/explosion.png rename to assets/explosion.png diff --git a/img/firmwarePX4.jpg b/assets/firmwarePX4.jpg similarity index 100% rename from img/firmwarePX4.jpg rename to assets/firmwarePX4.jpg diff --git a/img/flightModes.jpg b/assets/flightModes.jpg similarity index 100% rename from img/flightModes.jpg rename to assets/flightModes.jpg diff --git a/img/helphand.jpg b/assets/helphand.jpg similarity index 100% rename from img/helphand.jpg rename to assets/helphand.jpg diff --git a/img/holderLegs.png b/assets/holderLegs.png similarity index 100% rename from img/holderLegs.png rename to assets/holderLegs.png diff --git a/img/isoViewmountHolder.png b/assets/isoViewmountHolder.png similarity index 100% rename from img/isoViewmountHolder.png rename to assets/isoViewmountHolder.png diff --git a/img/jumper.png b/assets/jumper.png similarity index 100% rename from img/jumper.png rename to assets/jumper.png diff --git a/img/keep.png b/assets/keep.png similarity index 100% rename from img/keep.png rename to assets/keep.png diff --git a/img/lockradio.jpg b/assets/lockradio.jpg similarity index 100% rename from img/lockradio.jpg rename to assets/lockradio.jpg diff --git a/img/lockradio.png b/assets/lockradio.png similarity index 100% rename from img/lockradio.png rename to assets/lockradio.png diff --git a/img/lowsafeDeck.png b/assets/lowsafeDeck.png similarity index 100% rename from img/lowsafeDeck.png rename to assets/lowsafeDeck.png diff --git a/img/mainWindow.jpg b/assets/mainWindow.jpg similarity index 100% rename from img/mainWindow.jpg rename to assets/mainWindow.jpg diff --git a/img/motorsTopview.png b/assets/motorsTopview.png similarity index 100% rename from img/motorsTopview.png rename to assets/motorsTopview.png diff --git a/img/mount5vconnector.png b/assets/mount5vconnector.png similarity index 100% rename from img/mount5vconnector.png rename to assets/mount5vconnector.png diff --git a/img/mountAntenna.png b/assets/mountAntenna.png similarity index 100% rename from img/mountAntenna.png rename to assets/mountAntenna.png diff --git a/img/mountBeams.png b/assets/mountBeams.png similarity index 100% rename from img/mountBeams.png rename to assets/mountBeams.png diff --git a/img/mountBottomDeck.png b/assets/mountBottomDeck.png similarity index 100% rename from img/mountBottomDeck.png rename to assets/mountBottomDeck.png diff --git a/img/mountHolder.png b/assets/mountHolder.png similarity index 100% rename from img/mountHolder.png rename to assets/mountHolder.png diff --git a/img/mountPDB.png b/assets/mountPDB.png similarity index 100% rename from img/mountPDB.png rename to assets/mountPDB.png diff --git a/img/mountReceiverDeck.png b/assets/mountReceiverDeck.png similarity index 100% rename from img/mountReceiverDeck.png rename to assets/mountReceiverDeck.png diff --git a/img/mountReceiverStud.png b/assets/mountReceiverStud.png similarity index 100% rename from img/mountReceiverStud.png rename to assets/mountReceiverStud.png diff --git a/img/mountxt60pinsocket.png b/assets/mountxt60pinsocket.png similarity index 100% rename from img/mountxt60pinsocket.png rename to assets/mountxt60pinsocket.png diff --git a/img/notmoveslider.jpg b/assets/notmoveslider.jpg similarity index 100% rename from img/notmoveslider.jpg rename to assets/notmoveslider.jpg diff --git a/img/pixhawk.png b/assets/pixhawk.png similarity index 100% rename from img/pixhawk.png rename to assets/pixhawk.png diff --git a/img/radioTransmitter.png b/assets/radioTransmitter.png similarity index 100% rename from img/radioTransmitter.png rename to assets/radioTransmitter.png diff --git a/img/readyBatteryholder.png b/assets/readyBatteryholder.png similarity index 100% rename from img/readyBatteryholder.png rename to assets/readyBatteryholder.png diff --git a/img/receiver5V.png b/assets/receiver5V.png similarity index 100% rename from img/receiver5V.png rename to assets/receiver5V.png diff --git a/img/receiverPPM.png b/assets/receiverPPM.png similarity index 100% rename from img/receiverPPM.png rename to assets/receiverPPM.png diff --git a/img/resolderingESC.png b/assets/resolderingESC.png similarity index 100% rename from img/resolderingESC.png rename to assets/resolderingESC.png diff --git a/img/safeLegs.png b/assets/safeLegs.png similarity index 100% rename from img/safeLegs.png rename to assets/safeLegs.png diff --git a/img/safehighRadial.png b/assets/safehighRadial.png similarity index 100% rename from img/safehighRadial.png rename to assets/safehighRadial.png diff --git a/img/safelowRadial.png b/assets/safelowRadial.png similarity index 100% rename from img/safelowRadial.png rename to assets/safelowRadial.png diff --git a/img/safetyINflight.png b/assets/safetyINflight.png similarity index 100% rename from img/safetyINflight.png rename to assets/safetyINflight.png diff --git a/img/safetyPower.png b/assets/safetyPower.png similarity index 100% rename from img/safetyPower.png rename to assets/safetyPower.png diff --git a/img/safetyPreflight.png b/assets/safetyPreflight.png similarity index 100% rename from img/safetyPreflight.png rename to assets/safetyPreflight.png diff --git a/img/safetybyassem.png b/assets/safetybyassem.png similarity index 100% rename from img/safetybyassem.png rename to assets/safetybyassem.png diff --git a/img/soldering5VTOpdb.png b/assets/soldering5VTOpdb.png similarity index 100% rename from img/soldering5VTOpdb.png rename to assets/soldering5VTOpdb.png diff --git a/img/solderingBrrc2205ondeckTOescDYSzap.png b/assets/solderingBrrc2205ondeckTOescDYSzap.png similarity index 100% rename from img/solderingBrrc2205ondeckTOescDYSzap.png rename to assets/solderingBrrc2205ondeckTOescDYSzap.png diff --git a/img/solderingPowerwires.png b/assets/solderingPowerwires.png similarity index 100% rename from img/solderingPowerwires.png rename to assets/solderingPowerwires.png diff --git a/img/solderingxt60socketTOpdb.png b/assets/solderingxt60socketTOpdb.png similarity index 100% rename from img/solderingxt60socketTOpdb.png rename to assets/solderingxt60socketTOpdb.png diff --git a/img/stand.jpg b/assets/stand.jpg similarity index 100% rename from img/stand.jpg rename to assets/stand.jpg diff --git a/img/startPDBtest.jpg b/assets/startPDBtest.jpg similarity index 100% rename from img/startPDBtest.jpg rename to assets/startPDBtest.jpg diff --git a/img/testMotors.png b/assets/testMotors.png similarity index 100% rename from img/testMotors.png rename to assets/testMotors.png diff --git a/img/topESCcaseview.png b/assets/topESCcaseview.png similarity index 100% rename from img/topESCcaseview.png rename to assets/topESCcaseview.png diff --git a/img/topPreview.png b/assets/topPreview.png similarity index 100% rename from img/topPreview.png rename to assets/topPreview.png diff --git a/img/topviewmountPDB.png b/assets/topviewmountPDB.png similarity index 100% rename from img/topviewmountPDB.png rename to assets/topviewmountPDB.png diff --git a/img/topviewpixhawk.png b/assets/topviewpixhawk.png similarity index 100% rename from img/topviewpixhawk.png rename to assets/topviewpixhawk.png diff --git a/img/turnoffSafetyswitch.jpg b/assets/turnoffSafetyswitch.jpg similarity index 100% rename from img/turnoffSafetyswitch.jpg rename to assets/turnoffSafetyswitch.jpg diff --git a/img/xt60pinsocket.jpg b/assets/xt60pinsocket.jpg similarity index 100% rename from img/xt60pinsocket.jpg rename to assets/xt60pinsocket.jpg diff --git a/img/zap.jpg b/assets/zap.jpg similarity index 100% rename from img/zap.jpg rename to assets/zap.jpg diff --git a/img/zapPDBtest.jpg b/assets/zapPDBtest.jpg similarity index 100% rename from img/zapPDBtest.jpg rename to assets/zapPDBtest.jpg diff --git a/clever/CMakeLists.txt b/clever/CMakeLists.txt new file mode 100644 index 00000000..96c44158 --- /dev/null +++ b/clever/CMakeLists.txt @@ -0,0 +1,231 @@ +cmake_minimum_required(VERSION 2.8.3) +project(clever) + +## Compile as C++11, supported in ROS Kinetic and newer +add_compile_options(-std=c++11) + +## Find catkin macros and libraries +## if COMPONENTS list like find_package(catkin REQUIRED COMPONENTS xyz) +## is used, also find other catkin packages +find_package(catkin REQUIRED COMPONENTS + nodelet + pluginlib + roscpp + genmsg + rospy + std_msgs + message_generation + geometry_msgs + sensor_msgs + geographic_msgs + tf + tf2 + tf2_geometry_msgs +) + + +## System dependencies are found with CMake's conventions +# find_package(Boost REQUIRED COMPONENTS system) + + +## Uncomment this if the package has a setup.py. This macro ensures +## modules and global scripts declared therein get installed +## See http://ros.org/doc/api/catkin/html/user_guide/setup_dot_py.html +# catkin_python_setup() + +################################################ +## Declare ROS messages, services and actions ## +################################################ + +## To declare and build messages, services or actions from within this +## package, follow these steps: +## * Let MSG_DEP_SET be the set of packages whose message types you use in +## your messages/services/actions (e.g. std_msgs, actionlib_msgs, ...). +## * In the file package.xml: +## * add a build_depend tag for "message_generation" +## * add a build_depend and a run_depend tag for each package in MSG_DEP_SET +## * If MSG_DEP_SET isn't empty the following dependency has been pulled in +## but can be declared for certainty nonetheless: +## * add a run_depend tag for "message_runtime" +## * In this file (CMakeLists.txt): +## * add "message_generation" and every package in MSG_DEP_SET to +## find_package(catkin REQUIRED COMPONENTS ...) +## * add "message_runtime" and every package in MSG_DEP_SET to +## catkin_package(CATKIN_DEPENDS ...) +## * uncomment the add_*_files sections below as needed +## and list every .msg/.srv/.action file to be processed +## * uncomment the generate_messages entry below +## * add every package in MSG_DEP_SET to generate_messages(DEPENDENCIES ...) + +## Generate messages in the 'msg' folder +# add_message_files( +# FILES +# Message1.msg +# Message2.msg +# ) + +## Generate services in the 'srv' folder +add_service_files( + FILES + GetTelemetry.srv + Navigate.srv + SetPosition.srv + SetPositionYawRate.srv + SetPositionGlobal.srv + SetPositionGlobalYawRate.srv + SetVelocity.srv + SetVelocityYawRate.srv + SetAttitude.srv + SetAttitudeYawRate.srv + SetRatesYaw.srv + SetRates.srv +) + +## Generate actions in the 'action' folder +# add_action_files( +# FILES +# Action1.action +# Action2.action +# ) + +## Generate added messages and services with any dependencies listed here +generate_messages( + DEPENDENCIES + std_msgs # Or other packages containing msgs +) + +################################################ +## Declare ROS dynamic reconfigure parameters ## +################################################ + +## To declare and build dynamic reconfigure parameters within this +## package, follow these steps: +## * In the file package.xml: +## * add a build_depend and a run_depend tag for "dynamic_reconfigure" +## * In this file (CMakeLists.txt): +## * add "dynamic_reconfigure" to +## find_package(catkin REQUIRED COMPONENTS ...) +## * uncomment the "generate_dynamic_reconfigure_options" section below +## and list every .cfg file to be processed + +## Generate dynamic reconfigure parameters in the 'cfg' folder +# generate_dynamic_reconfigure_options( +# cfg/DynReconf1.cfg +# cfg/DynReconf2.cfg +# ) + +################################### +## catkin specific configuration ## +################################### +## The catkin_package macro generates cmake config files for your package +## Declare things to be passed to dependent projects +## INCLUDE_DIRS: uncomment this if you package contains header files +## LIBRARIES: libraries you create in this project that dependent projects also need +## CATKIN_DEPENDS: catkin_packages dependent projects also need +## DEPENDS: system dependencies of this project that dependent projects also need +catkin_package( +# INCLUDE_DIRS include +# LIBRARIES clever +# CATKIN_DEPENDS other_catkin_pkg +# DEPENDS system_lib +) + +########### +## Build ## +########### + +## Specify additional locations of header files +## Your package locations should be listed before other locations +include_directories( +# include + ${catkin_INCLUDE_DIRS} +) + +## Declare a C++ library +add_library(fcu_horiz + src/fcu_horiz.cpp +) + +add_library(aruco_vpe + src/aruco_vpe.cpp +) + +## Add cmake target dependencies of the library +## as an example, code may need to be generated before libraries +## either from message generation or dynamic reconfigure +# add_dependencies(${PROJECT_NAME} ${${PROJECT_NAME}_EXPORTED_TARGETS} ${catkin_EXPORTED_TARGETS}) + +## 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(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 +## target back to the shorter version for ease of user use +## e.g. "rosrun someones_pkg node" instead of "rosrun someones_pkg someones_pkg_node" +# set_target_properties(${PROJECT_NAME}_node PROPERTIES OUTPUT_NAME node PREFIX "") + +## Add cmake target dependencies of the executable +## same as for the library above +# add_dependencies(${PROJECT_NAME}_node ${${PROJECT_NAME}_EXPORTED_TARGETS} ${catkin_EXPORTED_TARGETS}) + +## Specify libraries to link a library or executable target against +target_link_libraries(fcu_horiz + ${catkin_LIBRARIES} + "/opt/ros/kinetic/lib/libtf2_ros.so" +) + +target_link_libraries(aruco_vpe + ${catkin_LIBRARIES} +) + +############# +## Install ## +############# + +# all install targets should use catkin DESTINATION variables +# See http://ros.org/doc/api/catkin/html/adv_user_guide/variables.html + +## Mark executable scripts (Python etc.) for installation +## in contrast to setup.py, you can choose the destination +# install(PROGRAMS +# scripts/my_python_script +# DESTINATION ${CATKIN_PACKAGE_BIN_DESTINATION} +# ) + +## Mark executables and/or libraries for installation +# install(TARGETS ${PROJECT_NAME} ${PROJECT_NAME}_node +# ARCHIVE DESTINATION ${CATKIN_PACKAGE_LIB_DESTINATION} +# LIBRARY DESTINATION ${CATKIN_PACKAGE_LIB_DESTINATION} +# RUNTIME DESTINATION ${CATKIN_PACKAGE_BIN_DESTINATION} +# ) + +## Mark cpp header files for installation +# install(DIRECTORY include/${PROJECT_NAME}/ +# DESTINATION ${CATKIN_PACKAGE_INCLUDE_DESTINATION} +# FILES_MATCHING PATTERN "*.h" +# PATTERN ".svn" EXCLUDE +# ) + +## Mark other files for installation (e.g. launch and bag files, etc.) +# install(FILES +# # myfile1 +# # myfile2 +# DESTINATION ${CATKIN_PACKAGE_SHARE_DESTINATION} +# ) + +############# +## Testing ## +############# + +## Add gtest based cpp test target and link libraries +# catkin_add_gtest(${PROJECT_NAME}-test test/test_clever.cpp) +# if(TARGET ${PROJECT_NAME}-test) +# target_link_libraries(${PROJECT_NAME}-test ${PROJECT_NAME}) +# endif() + +## Add folders to be run by python nosetests +# catkin_add_nosetests(test) diff --git a/clever/camera_info/fisheye_cam_320.yaml b/clever/camera_info/fisheye_cam_320.yaml new file mode 100644 index 00000000..76fcd8c2 --- /dev/null +++ b/clever/camera_info/fisheye_cam_320.yaml @@ -0,0 +1,45 @@ +image_width: 320 +image_height: 240 +distortion_model: plumb_bob +camera_name: raspicam +camera_matrix: + rows: 3 + cols: 3 + data: + - 166.23942373073172 + - 0. + - 162.19011246829268 + - 0. + - 166.5880923974026 + - 109.82227735714285 + - 0. + - 0. + - 1. +distortion_coefficients: + rows: 1 + cols: 8 + data: [ 2.15356885e-01, -1.17472846e-01, -3.06197672e-04, + -1.09444025e-04, -4.53657258e-03, 5.73090623e-01, + -1.27574577e-01, -2.86125589e-02, 0.00000000e+00, + 0.00000000e+00, 0.00000000e+00, 0.00000000e+00, + 0.00000000e+00, 0.00000000e+00] +rectification_matrix: + rows: 3 + cols: 3 + data: [1, 0, 0, 0, 1, 0, 0, 0, 1] +projection_matrix: + rows: 3 + cols: 4 + data: + - 166.23942373073172 + - 0. + - 162.19011246829268 + - 0. + - 0. + - 166.5880923974026 + - 109.82227735714285 + - 0. + - 0. + - 0. + - 1. + - 0. diff --git a/clever/camera_info/fisheye_cam_640.yaml b/clever/camera_info/fisheye_cam_640.yaml new file mode 100644 index 00000000..f664bb7c --- /dev/null +++ b/clever/camera_info/fisheye_cam_640.yaml @@ -0,0 +1,45 @@ +image_width: 640 +image_height: 480 +distortion_model: plumb_bob +camera_name: raspicam +camera_matrix: + rows: 3 + cols: 3 + data: + - 332.47884746146343 + - 0. + - 324.38022493658536 + - 0. + - 333.1761847948052 + - 219.6445547142857 + - 0. + - 0. + - 1. +distortion_coefficients: + rows: 1 + cols: 8 + data: [ 2.15356885e-01, -1.17472846e-01, -3.06197672e-04, + -1.09444025e-04, -4.53657258e-03, 5.73090623e-01, + -1.27574577e-01, -2.86125589e-02, 0.00000000e+00, + 0.00000000e+00, 0.00000000e+00, 0.00000000e+00, + 0.00000000e+00, 0.00000000e+00] +rectification_matrix: + rows: 3 + cols: 3 + data: [1, 0, 0, 0, 1, 0, 0, 0, 1] +projection_matrix: + rows: 3 + cols: 4 + data: + - 332.47884746146343 + - 0. + - 324.38022493658536 + - 0. + - 0. + - 333.1761847948052 + - 219.6445547142857 + - 0. + - 0. + - 0. + - 1. + - 0. diff --git a/clever/launch/arduino.launch b/clever/launch/arduino.launch new file mode 100644 index 00000000..593f38d2 --- /dev/null +++ b/clever/launch/arduino.launch @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/clever/launch/aruco.launch b/clever/launch/aruco.launch new file mode 100644 index 00000000..f9dc88c5 --- /dev/null +++ b/clever/launch/aruco.launch @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/clever/launch/clever.launch b/clever/launch/clever.launch new file mode 100644 index 00000000..6a582cfd --- /dev/null +++ b/clever/launch/clever.launch @@ -0,0 +1,61 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/clever/launch/copter_visualization.launch b/clever/launch/copter_visualization.launch new file mode 100644 index 00000000..24006baf --- /dev/null +++ b/clever/launch/copter_visualization.launch @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/clever/launch/fpv_camera.launch b/clever/launch/fpv_camera.launch new file mode 100644 index 00000000..399d403d --- /dev/null +++ b/clever/launch/fpv_camera.launch @@ -0,0 +1,6 @@ + + + + + + diff --git a/clever/launch/main_camera.launch b/clever/launch/main_camera.launch new file mode 100644 index 00000000..480ba38f --- /dev/null +++ b/clever/launch/main_camera.launch @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/clever/launch/mavros.launch b/clever/launch/mavros.launch new file mode 100644 index 00000000..d8a190f1 --- /dev/null +++ b/clever/launch/mavros.launch @@ -0,0 +1,52 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - safety_area + - image_pub + - vibration + - distance_sensor + - rangefinder + - 3dr_radio + - actuator_control + - hil_controls + - vfr_hud + - px4flow + - vision_speed_estimate + - fake_gps + - cam_imu_sync + - hil + - adsb + + + + + + diff --git a/clever/launch/sitl.launch b/clever/launch/sitl.launch new file mode 100644 index 00000000..1e48052f --- /dev/null +++ b/clever/launch/sitl.launch @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/clever/launch/web_server.launch b/clever/launch/web_server.launch new file mode 100644 index 00000000..9d8944ac --- /dev/null +++ b/clever/launch/web_server.launch @@ -0,0 +1,5 @@ + + + + + diff --git a/clever/nodelet_plugins.xml b/clever/nodelet_plugins.xml new file mode 100644 index 00000000..9e1c2c67 --- /dev/null +++ b/clever/nodelet_plugins.xml @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/clever/package.xml b/clever/package.xml new file mode 100644 index 00000000..5759150f --- /dev/null +++ b/clever/package.xml @@ -0,0 +1,56 @@ + + + clever + 0.0.1 + The clever package + + + + + Oleg Kalachev + + + + + + TODO + + + + + + + + + + + + + + + + + + + + + + + + + + catkin + + nodelet + roscpp + + nodelet + roscpp + + + + + + + + diff --git a/clever/requirements.txt b/clever/requirements.txt new file mode 100644 index 00000000..6cb6a548 --- /dev/null +++ b/clever/requirements.txt @@ -0,0 +1,2 @@ +flask==0.12.2 +geopy==1.11.0 diff --git a/clever/src/aruco_vpe.cpp b/clever/src/aruco_vpe.cpp new file mode 100644 index 00000000..122297c1 --- /dev/null +++ b/clever/src/aruco_vpe.cpp @@ -0,0 +1,133 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "util.h" + +using namespace tf2_ros; +using geometry_msgs::PoseStamped; +using geometry_msgs::TransformStamped; +using std::string; + +class ArucoVPE : public nodelet::Nodelet +{ +public: + ArucoVPE() : + last_published_(0), + lookup_timeout_(0.05) + {} + +private: + ros::Time last_published_; + ros::Duration lookup_timeout_; + ros::Publisher vision_position_pub_; + ros::Timer dummy_vision_timer_; + string aruco_orientation_; + bool reset_vpe_; + + void onInit() + { + ros::NodeHandle& nh = getNodeHandle(); + ros::NodeHandle& nh_priv = getPrivateNodeHandle(); + + nh_priv.param("aruco_orientation", aruco_orientation_, "local_origin"); + bool use_mocap; + nh_priv.param("use_mocap", use_mocap, false); + nh_priv.param("reset_vpe", reset_vpe_, !use_mocap); + + static ros::Subscriber pose_sub = nh.subscribe("mavros/local_position/pose", 1, &ArucoVPE::handlePose, this); + static ros::Subscriber aruco_pose_sub = nh.subscribe("aruco_pose/pose", 1, &ArucoVPE::handleArucoPose, this); + + vision_position_pub_ = nh.advertise(use_mocap ? "mavros/mocap/pose" : "mavros/vision_pose/pose", 1); + + ROS_INFO("aruco orientation frame: %s", aruco_orientation_.c_str()); + + dummy_vision_timer_ = nh.createTimer(ros::Duration(0.5), &ArucoVPE::publishDummy, this); + + ROS_INFO("Aruco VPE initialized"); + } + + void publishDummy(const ros::TimerEvent&) + { + // This is published to init FCU's position estimator + static PoseStamped ps; + ps.header.stamp = ros::Time::now(); + ps.pose.orientation.w = 1; + vision_position_pub_.publish(ps); + } + + void handlePose(const geometry_msgs::PoseStampedConstPtr& pose) + { + // local position is inited, stop posting dummy position + ROS_INFO_ONCE("Got local position, stop publishing zeroes"); + dummy_vision_timer_.stop(); + } + + void handleArucoPose(const geometry_msgs::PoseStampedConstPtr& pose) + { + static TransformBroadcaster br; + static Buffer tf_buffer; + static TransformListener tfListener(tf_buffer); + static StaticTransformBroadcaster static_br; + static PoseStamped ps, vpe_raw, vpe; + TransformStamped t; + + ros::Time stamp = pose->header.stamp; + double roll, pitch, yaw; + + try + { + // Refine aruco map pose + // Reference in local origin + t = tf_buffer.lookupTransform(aruco_orientation_, "aruco_map_reference", stamp, lookup_timeout_); + quaternionToEuler(t.transform.rotation, roll, pitch, yaw); + eulerToQuaternion(t.transform.rotation, 0, 0, yaw); + t.child_frame_id = "aruco_map_reference_horiz"; + br.sendTransform(t); + + // Aruco map in reference + t = tf_buffer.lookupTransform("aruco_map_reference", "aruco_map_raw", stamp, lookup_timeout_); + t.header.frame_id = "aruco_map_reference_horiz"; + t.child_frame_id = "aruco_map_vision"; + br.sendTransform(t); + + if (last_published_.toSec() == 0 || // no vpe has been posted + (reset_vpe_ && (ros::Time::now() - last_published_ > ros::Duration(2)))) // vpe origin outdated + { + ROS_INFO("Reset VPE"); + t = tf_buffer.lookupTransform("local_origin", "aruco_map_vision", stamp, lookup_timeout_); + t.child_frame_id = "aruco_map"; + static_br.sendTransform(t); + } + + // Calculate VPE + ps.header.frame_id = "fcu_horiz"; + ps.header.stamp = stamp; + ps.pose.orientation.w = 1; + + tf_buffer.transform(ps, vpe_raw, "aruco_map_vision", lookup_timeout_); + + vpe_raw.header.frame_id = "aruco_map"; + tf_buffer.transform(vpe_raw, vpe, "local_origin", lookup_timeout_); + + vision_position_pub_.publish(vpe); + + last_published_ = stamp; + dummy_vision_timer_.stop(); + } + catch (const tf2::TransformException& e) + { + ROS_WARN_THROTTLE(10, "Aruco VPE: failed to transform: %s", e.what()); + } + } +}; + +PLUGINLIB_EXPORT_CLASS(ArucoVPE, nodelet::Nodelet) diff --git a/clever/src/fcu_horiz.cpp b/clever/src/fcu_horiz.cpp new file mode 100644 index 00000000..aa17c1e3 --- /dev/null +++ b/clever/src/fcu_horiz.cpp @@ -0,0 +1,40 @@ +#include +#include +#include +#include +#include + +#include "util.h" + +class FcuHoriz : public nodelet::Nodelet +{ + geometry_msgs::TransformStamped t_; + + void handlePose(const geometry_msgs::PoseStampedConstPtr& msg) + { + static tf2_ros::TransformBroadcaster br; + double roll, pitch, yaw; + + t_.header.stamp = msg->header.stamp; + t_.header.frame_id = msg->header.frame_id; + t_.transform.translation.x = msg->pose.position.x; + t_.transform.translation.y = msg->pose.position.y; + t_.transform.translation.z = msg->pose.position.z; + + // Warning: this is not thead-safe + quaternionToEuler(msg->pose.orientation, roll, pitch, yaw); + eulerToQuaternion(t_.transform.rotation, 0, 0, yaw); + + br.sendTransform(t_); + } + + void onInit() + { + t_.child_frame_id = "fcu_horiz"; + t_.transform.rotation.w = 1; + static ros::Subscriber pose_sub = getNodeHandle().subscribe("mavros/local_position/pose", 1, &FcuHoriz::handlePose, this); + ROS_INFO("fcu_horiz initialized"); + } +}; + +PLUGINLIB_EXPORT_CLASS(FcuHoriz, nodelet::Nodelet) diff --git a/clever/src/fpv_camera b/clever/src/fpv_camera new file mode 100755 index 00000000..7ca4981d --- /dev/null +++ b/clever/src/fpv_camera @@ -0,0 +1,8 @@ +#!/usr/bin/env bash + +# Usage +# fpv_camera + +echo "Starting FPV camera $1 on :$2" +cd /home/pi/mjpg-streamer/mjpg-streamer-experimental +./mjpg_streamer -i "./input_uvc.so -d $1 -r 320x240 -f 30" -o "./output_http.so -w ./www -p $2" diff --git a/clever/src/global_local.py b/clever/src/global_local.py new file mode 100644 index 00000000..a4f8f047 --- /dev/null +++ b/clever/src/global_local.py @@ -0,0 +1,36 @@ +import rospy +import math +import geopy +from geometry_msgs.msg import PoseStamped +from geopy.distance import VincentyDistance, vincenty +from sensor_msgs.msg import NavSatFix + + +def global_to_local(lat, lon): + # TODO: refactor + + position_global = rospy.wait_for_message('mavros/global_position/global', NavSatFix, timeout=5) + pose = rospy.wait_for_message('mavros/local_position/pose', PoseStamped, timeout=5) + + d = math.hypot(pose.pose.position.x, pose.pose.position.y) + + bearing = math.degrees(math.atan2(-pose.pose.position.x, -pose.pose.position.y)) + if bearing < 0: + bearing += 360 + + cur = geopy.Point(position_global.latitude, position_global.longitude) + origin = VincentyDistance(meters=d).destination(cur, bearing) + + _origin = origin.latitude, origin.longitude + olat_tlon = origin.latitude, lon + tlat_olon = lat, origin.longitude + + N = vincenty(_origin, tlat_olon) + if lat < origin.latitude: + N = -N + + E = vincenty(_origin, olat_tlon) + if lon < origin.longitude: + E = -E + + return E.meters, N.meters 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(); +} diff --git a/clever/src/simple_offboard.py b/clever/src/simple_offboard.py new file mode 100755 index 00000000..a84b9367 --- /dev/null +++ b/clever/src/simple_offboard.py @@ -0,0 +1,466 @@ +#!/usr/bin/env python +from __future__ import division + +import rospy +from geometry_msgs.msg import TransformStamped, PoseStamped, Point, PointStamped, Vector3, Vector3Stamped, TwistStamped, QuaternionStamped +from sensor_msgs.msg import NavSatFix, BatteryState +import tf2_ros +import tf2_geometry_msgs +from mavros_msgs.msg import PositionTarget, AttitudeTarget, State +from mavros_msgs.srv import CommandBool, SetMode +from threading import Lock +import math + +from global_local import global_to_local +from util import euler_from_orientation, vector3_from_point, orientation_from_euler +from std_srvs.srv import Trigger +from clever import srv + + +rospy.init_node('simple_offboard') + + +# TF2 stuff +tf_broadcaster = tf2_ros.TransformBroadcaster() +static_tf_broadcaster = tf2_ros.StaticTransformBroadcaster() + +tf_buffer = tf2_ros.Buffer() +tf_listener = tf2_ros.TransformListener(tf_buffer) + + +position_pub = rospy.Publisher('/mavros/setpoint_raw/local', PositionTarget, queue_size=1) +attitude_pub = rospy.Publisher('/mavros/setpoint_raw/attitude', AttitudeTarget, queue_size=1) +target_pub = rospy.Publisher('~target', PoseStamped, queue_size=1) + +arming = rospy.ServiceProxy('/mavros/cmd/arming', CommandBool, persistent=True) +set_mode = rospy.ServiceProxy('/mavros/set_mode', SetMode, persistent=True) + + +pose = None +global_position = None +velocity = None +state = None +battery = None + + +def pose_update(data): + global pose + pose = data + + +def global_position_update(data): + global global_position + global_position = data + + +def velocity_update(data): + global velocity + velocity = data + + +def state_update(data): + global state + state = data + + +def battery_update(data): + global battery + battery = data + + +rospy.Subscriber('/mavros/state', State, state_update) +rospy.Subscriber('/mavros/local_position/pose', PoseStamped, pose_update) +rospy.Subscriber('/mavros/local_position/velocity', TwistStamped, velocity_update) +rospy.Subscriber('/mavros/global_position/global', NavSatFix, global_position_update) +rospy.Subscriber('/mavros/battery', BatteryState, battery_update) + + +AUTO_OFFBOARD = rospy.get_param('~auto_offboard', True) +AUTO_ARM = AUTO_OFFBOARD and rospy.get_param('~auto_arm', True) +OFFBOARD_TIMEOUT = rospy.Duration(rospy.get_param('~offboard_timeout', 3)) +ARM_TIMEOUT = rospy.Duration(rospy.get_param('~arm_timeout', 5)) +TRANSFORM_TIMEOUT = rospy.Duration(rospy.get_param('~transform_timeout', 3)) +SETPOINT_RATE = rospy.get_param('~setpoint_rate', 30) + + +def offboard_and_arm(): + if AUTO_OFFBOARD and state.mode != 'OFFBOARD': + rospy.sleep(.3) + rospy.loginfo('Switch mode to OFFBOARD') + res = set_mode(base_mode=0, custom_mode='OFFBOARD') + + start = rospy.get_rostime() + while True: + if state.mode == 'OFFBOARD': + break + if rospy.get_rostime() - start > OFFBOARD_TIMEOUT: + raise Exception('OFFBOARD request timed out') + + if AUTO_ARM and not state.armed: + rospy.loginfo('Arming') + res = arming(True) + + start = rospy.get_rostime() + while True: + if state.armed: + return True + + if rospy.get_rostime() - start > ARM_TIMEOUT: + raise Exception('Arming timed out') + + +ps = PoseStamped() +vs = Vector3Stamped() + + +BRAKE_TIME = rospy.Duration(0) + + +def get_navigate_setpoint(stamp, start, finish, start_stamp, speed): + distance = math.sqrt((finish.z - start.z)**2 + (finish.x - start.x)**2 + (finish.y - start.y)**2) + time = rospy.Duration(distance / speed) + k = (stamp - start_stamp) / time + time_left = start_stamp + time - stamp + + if BRAKE_TIME and time_left < BRAKE_TIME: + # time to brake + time_before_braking = time - BRAKE_TIME + brake_time_passed = (stamp - start_stamp - time_before_braking) + + if brake_time_passed > 2 * BRAKE_TIME: + # finish + k = 1 + else: + # brake! + k_before_braking = time_before_braking / time + k_after_braking = (speed * brake_time_passed.to_sec() - brake_time_passed.to_sec() ** 2 * speed / 4 / BRAKE_TIME.to_sec()) / distance + k = k_before_braking + k_after_braking + + k = min(k, 1) + + p = Point() + p.x = start.x + (finish.x - start.x) * k + p.y = start.y + (finish.y - start.y) * k + p.z = start.z + (finish.z - start.z) * k + return p + + +def get_publisher_and_message(req, stamp, continued=True, update_frame=True): + ps.header.stamp = stamp + vs.header.stamp = stamp + + if isinstance(req, srv.NavigateRequest): + global current_nav_start, current_nav_start_stamp, current_nav_finish + + if update_frame: + ps.header.frame_id = req.frame_id or 'local_origin' + ps.pose.position = Point(req.x, req.y, req.z) + ps.pose.orientation = orientation_from_euler(0, 0, req.yaw) + current_nav_finish = tf_buffer.transform(ps, 'local_origin', TRANSFORM_TIMEOUT) + + if not continued: + current_nav_start = pose.pose.position + current_nav_start_stamp = stamp + + setpoint = get_navigate_setpoint(stamp, current_nav_start, current_nav_finish.pose.position, + current_nav_start_stamp, req.speed) + + msg = PositionTarget(coordinate_frame=PositionTarget.FRAME_LOCAL_NED, + type_mask=PositionTarget.IGNORE_VX + PositionTarget.IGNORE_VY + PositionTarget.IGNORE_VZ + + PositionTarget.IGNORE_AFX + PositionTarget.IGNORE_AFY + PositionTarget.IGNORE_AFZ + + PositionTarget.IGNORE_YAW_RATE, + position=setpoint, + yaw=euler_from_orientation(current_nav_finish.pose.orientation)[2] - math.pi / 2) + return position_pub, msg + + elif isinstance(req, srv.SetPositionRequest): + ps.header.frame_id = req.frame_id or 'local_origin' + ps.pose.position = Point(req.x, req.y, req.z) + ps.pose.orientation = orientation_from_euler(0, 0, req.yaw) + pose_local = tf_buffer.transform(ps, 'local_origin', TRANSFORM_TIMEOUT) + + msg = PositionTarget(coordinate_frame=PositionTarget.FRAME_LOCAL_NED, + type_mask=PositionTarget.IGNORE_VX + PositionTarget.IGNORE_VY + PositionTarget.IGNORE_VZ + + PositionTarget.IGNORE_AFX + PositionTarget.IGNORE_AFY + PositionTarget.IGNORE_AFZ + + PositionTarget.IGNORE_YAW_RATE, + position=pose_local.pose.position, + yaw=euler_from_orientation(pose_local.pose.orientation)[2] - math.pi / 2) + return position_pub, msg + + elif isinstance(req, srv.SetPositionYawRateRequest): + ps.header.frame_id = req.frame_id or 'local_origin' + ps.pose.position = Point(req.x, req.y, req.z) + pose_local = tf_buffer.transform(ps, 'local_origin', TRANSFORM_TIMEOUT) + msg = PositionTarget(coordinate_frame=PositionTarget.FRAME_LOCAL_NED, + type_mask=PositionTarget.IGNORE_VX + PositionTarget.IGNORE_VY + PositionTarget.IGNORE_VZ + + PositionTarget.IGNORE_AFX + PositionTarget.IGNORE_AFY + PositionTarget.IGNORE_AFZ + + PositionTarget.IGNORE_YAW, + position=pose_local.pose.position, + yaw_rate=req.yaw_rate) + return position_pub, msg + + elif isinstance(req, srv.SetPositionGlobalRequest): + x, y = global_to_local(req.lat, req.lon) + + ps.header.frame_id = req.frame_id or 'local_origin' + ps.pose.position = Point(0, 0, req.z) + ps.pose.orientation = orientation_from_euler(0, 0, req.yaw) + pose_local = tf_buffer.transform(ps, 'local_origin', TRANSFORM_TIMEOUT) + pose_local.pose.position.x = x + pose_local.pose.position.y = y + + msg = PositionTarget(coordinate_frame=PositionTarget.FRAME_LOCAL_NED, + type_mask=PositionTarget.IGNORE_VX + PositionTarget.IGNORE_VY + PositionTarget.IGNORE_VZ + + PositionTarget.IGNORE_AFX + PositionTarget.IGNORE_AFY + PositionTarget.IGNORE_AFZ + + PositionTarget.IGNORE_YAW_RATE, + position=pose_local.pose.position, + yaw=euler_from_orientation(pose_local.pose.orientation)[2] - math.pi / 2) + return position_pub, msg + + elif isinstance(req, srv.SetPositionGlobalYawRateRequest): + x, y = global_to_local(req.lat, req.lon) + + ps.header.frame_id = req.frame_id or 'local_origin' + ps.pose.position = Point(0, 0, req.z) + pose_local = tf_buffer.transform(ps, 'local_origin', TRANSFORM_TIMEOUT) + pose_local.pose.position.x = x + pose_local.pose.position.y = y + + msg = PositionTarget(coordinate_frame=PositionTarget.FRAME_LOCAL_NED, + type_mask=PositionTarget.IGNORE_VX + PositionTarget.IGNORE_VY + PositionTarget.IGNORE_VZ + + PositionTarget.IGNORE_AFX + PositionTarget.IGNORE_AFY + PositionTarget.IGNORE_AFZ + + PositionTarget.IGNORE_YAW, + position=pose_local.pose.position, + yaw_rate=req.yaw_rate) + return position_pub, msg + + elif isinstance(req, srv.SetVelocityRequest): + vs.vector = Vector3(req.vx, req.vy, req.vz) + vs.header.frame_id = req.frame_id or 'local_origin' + ps.header.frame_id = req.frame_id or 'local_origin' + ps.pose.orientation = orientation_from_euler(0, 0, req.yaw) + pose_local = tf_buffer.transform(ps, 'local_origin', TRANSFORM_TIMEOUT) + vector_local = tf_buffer.transform(vs, 'local_origin', TRANSFORM_TIMEOUT) + msg = PositionTarget(coordinate_frame=PositionTarget.FRAME_LOCAL_NED, + type_mask=PositionTarget.IGNORE_PX + PositionTarget.IGNORE_PY + PositionTarget.IGNORE_PZ + + PositionTarget.IGNORE_AFX + PositionTarget.IGNORE_AFY + PositionTarget.IGNORE_AFZ + + PositionTarget.IGNORE_YAW_RATE, + velocity=vector_local.vector, + yaw=euler_from_orientation(pose_local.pose.orientation)[2] - math.pi / 2) + return position_pub, msg + + elif isinstance(req, srv.SetVelocityYawRateRequest): + vs.vector = Vector3(req.vx, req.vy, req.vz) + vs.header.frame_id = req.frame_id or 'local_origin' + vector_local = tf_buffer.transform(vs, 'local_origin', TRANSFORM_TIMEOUT) + msg = PositionTarget(coordinate_frame=PositionTarget.FRAME_LOCAL_NED, + type_mask=PositionTarget.IGNORE_PX + PositionTarget.IGNORE_PY + PositionTarget.IGNORE_PZ + + PositionTarget.IGNORE_AFX + PositionTarget.IGNORE_AFY + PositionTarget.IGNORE_AFZ + + PositionTarget.IGNORE_YAW, + velocity=vector_local.vector, + yaw_rate=req.yaw_rate) + return position_pub, msg + + elif isinstance(req, srv.SetAttitudeRequest): + ps.header.frame_id = req.frame_id or 'local_origin' + ps.pose.orientation = orientation_from_euler(req.roll, req.pitch, req.yaw) + pose_local = tf_buffer.transform(ps, 'local_origin', TRANSFORM_TIMEOUT) + msg = AttitudeTarget(orientation=pose_local.pose.orientation, + thrust=req.thrust, + type_mask=AttitudeTarget.IGNORE_YAW_RATE + AttitudeTarget.IGNORE_PITCH_RATE + + AttitudeTarget.IGNORE_ROLL_RATE) + return attitude_pub, msg + + elif isinstance(req, srv.SetAttitudeYawRateRequest): + msg = AttitudeTarget(orientation=orientation_from_euler(req.roll, req.pitch, 0), + thrust=req.thrust, + type_mask=AttitudeTarget.IGNORE_PITCH_RATE + AttitudeTarget.IGNORE_ROLL_RATE) + msg.body_rate.z = req.yaw_rate + return attitude_pub, msg + + elif isinstance(req, srv.SetRatesYawRequest): + ps.header.frame_id = req.frame_id or 'local_origin' + ps.pose.orientation = orientation_from_euler(0, 0, req.yaw) + pose_local = tf_buffer.transform(ps, 'local_origin', TRANSFORM_TIMEOUT) + msg = AttitudeTarget(orientation=pose_local.pose.orientation, + thrust=req.thrust, + type_mask=AttitudeTarget.IGNORE_YAW_RATE, + body_rate=Vector3(req.roll_rate, req.pitch_rate, 0)) + return attitude_pub, msg + + elif isinstance(req, srv.SetRatesRequest): + msg = AttitudeTarget(thrust=req.thrust, + type_mask=AttitudeTarget.IGNORE_ATTITUDE, + body_rate=Vector3(req.roll_rate, req.pitch_rate, req.yaw_rate)) + return attitude_pub, msg + + +current_pub = None +current_msg = None +current_req = None +current_nav_start = None +current_nav_finish = None +current_nav_start_stamp = None +handle_lock = Lock() + + +def handle(req): + global current_pub, current_msg, current_req + + if not state or not state.connected: + rospy.logwarn('No connection to the FCU') + return {'message': 'No connection to the FCU'} + + if isinstance(req, srv.NavigateRequest) and req.speed <= 0: + rospy.logwarn('Navigate speed must be greater than zero, %s passed') + return {'message': 'Navigate speed must be greater than zero, %s passed' % req.speed} + + try: + with handle_lock: + stamp = rospy.get_rostime() + current_req = req + current_pub, current_msg = get_publisher_and_message(req, stamp, False) + rospy.loginfo('Topic: %s, message: %s', current_pub.name, current_msg) + + current_msg.header.stamp = stamp + current_pub.publish(current_msg) + + if req.auto_arm: + offboard_and_arm() + else: + if state.mode != 'OFFBOARD': + return {'message': 'Copter is not in OFFBOARD mode, use auto_arm?'} + if not state.armed: + return {'message': 'Copter is not armed, use auto_arm?'} + + return {'success': True} + + except Exception as e: + rospy.logerr(str(e)) + return {'success': False, 'message': str(e)} + + +def release(req): + global current_pub + current_pub = None + rospy.loginfo('simple_offboard: release') + return {'success': True} + + +rospy.Service('navigate', srv.Navigate, handle) +rospy.Service('set_position', srv.SetPosition, handle) +rospy.Service('set_position/yaw_rate', srv.SetPositionYawRate, handle) +rospy.Service('set_position_global', srv.SetPositionGlobal, handle) +rospy.Service('set_position_global/yaw_rate', srv.SetPositionGlobalYawRate, handle) +rospy.Service('set_velocity', srv.SetVelocity, handle) +rospy.Service('set_velocity/yaw_rate', srv.SetVelocityYawRate, handle) +rospy.Service('set_attitude', srv.SetAttitude, handle) +rospy.Service('set_attitude/yaw_rate', srv.SetAttitudeYawRate, handle) +rospy.Service('set_rates', srv.SetRates, handle) +rospy.Service('set_rates/yaw', srv.SetRatesYaw, handle) +rospy.Service('release', Trigger, release) + + +def get_telemetry(req): + res = { + 'frame_id': req.frame_id or 'local_origin', + 'x': float('nan'), + 'y': float('nan'), + 'z': float('nan'), + 'lat': float('nan'), + 'lon': float('nan'), + 'vx': float('nan'), + 'vy': float('nan'), + 'vz': float('nan'), + 'pitch': float('nan'), + 'roll': float('nan'), + 'yaw': float('nan'), + 'pitch_rate': float('nan'), + 'roll_rate': float('nan'), + 'yaw_rate': float('nan'), + 'voltage': float('nan'), + 'cell_voltage': float('nan') + } + frame_id = req.frame_id or 'local_origin' + stamp = rospy.get_rostime() + + if pose: + p = tf_buffer.transform(pose, frame_id, TRANSFORM_TIMEOUT) + res['x'] = p.pose.position.x + res['y'] = p.pose.position.y + res['z'] = p.pose.position.z + # Get yaw in the request's frame_in + _, _, res['yaw'] = euler_from_orientation(p.pose.orientation) + # Calculate pitch and roll as angles between the pose and fcu_horiz + attitude_pose = tf_buffer.transform(pose, 'fcu_horiz', TRANSFORM_TIMEOUT) + res['roll'], res['pitch'], _ = euler_from_orientation(attitude_pose.pose.orientation) + + if velocity: + v = Vector3Stamped() + v.header.stamp = velocity.header.stamp + v.header.frame_id = velocity.header.frame_id + v.vector = velocity.twist.linear + linear = tf_buffer.transform(v, frame_id, TRANSFORM_TIMEOUT) + res['vx'] = linear.vector.x + res['vy'] = linear.vector.y + res['vz'] = linear.vector.z + # TODO pitch_rate, roll_rate, yaw_rate + + if global_position and stamp - global_position.header.stamp < rospy.Duration(5): + res['lat'] = global_position.latitude + res['lon'] = global_position.longitude + + if state: + res['connected'] = state.connected + res['armed'] = state.armed + res['mode'] = state.mode + + if battery: + res['voltage'] = battery.voltage + try: + res['cell_voltage'] = battery.cell_voltage[0] + except: + pass + + return res + + +rospy.Service('get_telemetry', srv.GetTelemetry, get_telemetry) + + +rospy.loginfo('simple_offboard inited') + + +def start_loop(): + global current_pub, current_msg, current_req + r = rospy.Rate(SETPOINT_RATE) + + while not rospy.is_shutdown(): + with handle_lock: + if current_pub is not None: + try: + stamp = rospy.get_rostime() + + if getattr(current_req, 'update_frame', False) or isinstance(current_req, srv.NavigateRequest): + current_pub, current_msg = get_publisher_and_message(current_req, stamp, True, + getattr(current_req, 'update_frame', False)) + + current_msg.header.stamp = stamp + current_pub.publish(current_msg) + + # For monitoring + if isinstance(current_msg, PositionTarget): + p = PoseStamped() + p.header.frame_id = 'local_origin' + p.header.stamp = stamp + p.pose.position = current_msg.position + p.pose.orientation = orientation_from_euler(0, 0, current_msg.yaw + math.pi / 2) + target_pub.publish(p) + + except Exception as e: + rospy.logwarn_throttle(10, str(e)) + + r.sleep() + + +start_loop() diff --git a/clever/src/util.h b/clever/src/util.h new file mode 100644 index 00000000..0eb7f1e9 --- /dev/null +++ b/clever/src/util.h @@ -0,0 +1,17 @@ +#pragma once + +#include +#include + +inline void quaternionToEuler(geometry_msgs::Quaternion q, double& roll, double& pitch, double& yaw) +{ + tf::Quaternion tfq(q.x, q.y, q.z, q.w); + tf::Matrix3x3 m(tfq); + m.getRPY(roll, pitch, yaw); +} + +inline void eulerToQuaternion(geometry_msgs::Quaternion& q, double roll, double pitch, double yaw) +{ + tf::Quaternion tfq(roll, pitch, yaw); + quaternionTFToMsg(tfq, q); +} diff --git a/clever/src/util.py b/clever/src/util.py new file mode 100644 index 00000000..eafa72fd --- /dev/null +++ b/clever/src/util.py @@ -0,0 +1,28 @@ +from geometry_msgs.msg import Quaternion, Vector3, Point +import tf.transformations as t + + +def orientation_from_quaternion(q): + return Quaternion(*q) + + +def orientation_from_euler(roll, pitch, yaw): + q = t.quaternion_from_euler(roll, pitch, yaw) + return orientation_from_quaternion(q) + + +def quaternion_from_orientation(o): + return o.x, o.y, o.z, o.w + + +def euler_from_orientation(o): + q = quaternion_from_orientation(o) + return t.euler_from_quaternion(q) + + +def vector3_from_point(p): + return Vector3(p.x, p.y, p.z) + + +def point_from_vector3(v): + return Point(v.x, v.y, v.z) diff --git a/clever/src/web_server.py b/clever/src/web_server.py new file mode 100755 index 00000000..86874760 --- /dev/null +++ b/clever/src/web_server.py @@ -0,0 +1,60 @@ +#!/usr/bin/env python + +import rospy +import subprocess +import re +from flask import Flask, send_from_directory, send_file, request, jsonify + +rospy.init_node('web_server', disable_signals=True) + +port = rospy.get_param('~port', 7070) +host = rospy.get_param('~host', '0.0.0.0') +serve_path = rospy.get_param('~path') +app = Flask(__name__) + + +@app.route('/') +def serve_index(): + return send_from_directory(serve_path, 'index.html') + + +@app.route('/') +def serve_static(path): + print serve_path, path + return send_from_directory(serve_path, path) + + +@app.route('/wifi_data/') +def get_wifi_data(): + cur_ip = request.remote_addr + ip_signal = get_ip_signal() + return jsonify({'ip': cur_ip, 'signal': ip_signal[cur_ip]}), 200 + + +def get_ip_signal(): + wlan_interface = 'wlan0' + # Getting info about wifi client connected to access point. From here we know MAC and signal level + iwl = subprocess.check_output(['sudo', 'iw', 'dev', 'wlan0', 'station', 'dump']).splitlines() + mac_signal = {} + cur_client = '' + for line in iwl: + if line.find('Station') != -1: + cur_client = re.search(r'([0-9A-F]{2}[:-]){5}([0-9A-F]{2})', line, re.I).group() + if line.find('signal') != -1: + sg = re.search(r'(\[-?\d*\])', line, re.I).group() + mac_signal[cur_client] = re.sub(r'[\[\]]', '', sg) + ip_signal = {} + # Getting ip-mac mapping + ip_mac = subprocess.check_output(['arp', '-i', wlan_interface]).splitlines() + for line in ip_mac: + mac = re.search(r'([0-9A-F]{2}[:-]){5}([0-9A-F]{2})', line, re.I) + if mac is not None: + mac = mac.group() + if mac in mac_signal: + ips = re.search(r'((2[0-5]|1[0-9]|[0-9])?[0-9]\.){3}((2[0-5]|1[0-9]|[0-9])?[0-9])', line, re.I).group() + ip_signal[ips] = mac_signal[mac] + return ip_signal + + +rospy.loginfo('Serving on %s:%s', host, port) +app.run(host=host, port=port, threaded=True) diff --git a/clever/srv/GetTelemetry.srv b/clever/srv/GetTelemetry.srv new file mode 100644 index 00000000..b815ce89 --- /dev/null +++ b/clever/srv/GetTelemetry.srv @@ -0,0 +1,22 @@ +string frame_id +--- +string frame_id +bool connected +bool armed +string mode +float32 x +float32 y +float32 z +float32 lat +float32 lon +float32 vx +float32 vy +float32 vz +float32 pitch +float32 roll +float32 yaw +float32 pitch_rate +float32 roll_rate +float32 yaw_rate +float32 voltage +float32 cell_voltage diff --git a/clever/srv/Navigate.srv b/clever/srv/Navigate.srv new file mode 100644 index 00000000..e368f284 --- /dev/null +++ b/clever/srv/Navigate.srv @@ -0,0 +1,11 @@ +float32 x +float32 y +float32 z +float32 yaw +float32 speed +string frame_id +bool update_frame +bool auto_arm +--- +bool success +string message diff --git a/clever/srv/SetAttitude.srv b/clever/srv/SetAttitude.srv new file mode 100644 index 00000000..05ba13db --- /dev/null +++ b/clever/srv/SetAttitude.srv @@ -0,0 +1,10 @@ +float32 pitch +float32 roll +float32 yaw +float32 thrust +string frame_id +bool update_frame +bool auto_arm +--- +bool success +string message diff --git a/clever/srv/SetAttitudeYawRate.srv b/clever/srv/SetAttitudeYawRate.srv new file mode 100644 index 00000000..e914cf7e --- /dev/null +++ b/clever/srv/SetAttitudeYawRate.srv @@ -0,0 +1,8 @@ +float32 roll +float32 pitch +float32 yaw_rate +float32 thrust +bool auto_arm +--- +bool success +string message diff --git a/clever/srv/SetPosition.srv b/clever/srv/SetPosition.srv new file mode 100644 index 00000000..421c4931 --- /dev/null +++ b/clever/srv/SetPosition.srv @@ -0,0 +1,10 @@ +float32 x +float32 y +float32 z +float32 yaw +string frame_id +bool update_frame +bool auto_arm +--- +bool success +string message diff --git a/clever/srv/SetPositionGlobal.srv b/clever/srv/SetPositionGlobal.srv new file mode 100644 index 00000000..742f4d95 --- /dev/null +++ b/clever/srv/SetPositionGlobal.srv @@ -0,0 +1,10 @@ +float32 lat +float32 lon +float32 z +float32 yaw +string frame_id +bool update_frame +bool auto_arm +--- +bool success +string message diff --git a/clever/srv/SetPositionGlobalYawRate.srv b/clever/srv/SetPositionGlobalYawRate.srv new file mode 100644 index 00000000..3fc42931 --- /dev/null +++ b/clever/srv/SetPositionGlobalYawRate.srv @@ -0,0 +1,10 @@ +float32 lat +float32 lon +float32 z +float32 yaw_rate +string frame_id +bool update_frame +bool auto_arm +--- +bool success +string message diff --git a/clever/srv/SetPositionYawRate.srv b/clever/srv/SetPositionYawRate.srv new file mode 100644 index 00000000..419621ca --- /dev/null +++ b/clever/srv/SetPositionYawRate.srv @@ -0,0 +1,10 @@ +float32 x +float32 y +float32 z +float32 yaw_rate +string frame_id +bool update_frame +bool auto_arm +--- +bool success +string message diff --git a/clever/srv/SetRates.srv b/clever/srv/SetRates.srv new file mode 100644 index 00000000..f6ebddf9 --- /dev/null +++ b/clever/srv/SetRates.srv @@ -0,0 +1,8 @@ +float32 pitch_rate +float32 roll_rate +float32 yaw_rate +float32 thrust +bool auto_arm +--- +bool success +string message diff --git a/clever/srv/SetRatesYaw.srv b/clever/srv/SetRatesYaw.srv new file mode 100644 index 00000000..df8950ac --- /dev/null +++ b/clever/srv/SetRatesYaw.srv @@ -0,0 +1,10 @@ +float32 pitch_rate +float32 roll_rate +float32 yaw +float32 thrust +string frame_id +bool update_frame +bool auto_arm +--- +bool success +string message diff --git a/clever/srv/SetVelocity.srv b/clever/srv/SetVelocity.srv new file mode 100644 index 00000000..01b818b2 --- /dev/null +++ b/clever/srv/SetVelocity.srv @@ -0,0 +1,10 @@ +float32 vx +float32 vy +float32 vz +float32 yaw +string frame_id +bool update_frame +bool auto_arm +--- +bool success +string message diff --git a/clever/srv/SetVelocityYawRate.srv b/clever/srv/SetVelocityYawRate.srv new file mode 100644 index 00000000..3155644e --- /dev/null +++ b/clever/srv/SetVelocityYawRate.srv @@ -0,0 +1,10 @@ +float32 vx +float32 vy +float32 vz +float32 yaw_rate +string frame_id +bool update_frame +bool auto_arm +--- +bool success +string message diff --git a/deploy/clever.service b/deploy/clever.service new file mode 100644 index 00000000..282ab0af --- /dev/null +++ b/deploy/clever.service @@ -0,0 +1,12 @@ +[Unit] +Description=Clever ROS package +Requires=roscore.service +After=roscore.service + +[Service] +EnvironmentFile=/home/pi/catkin_ws/src/clever/deploy/roscore.env +ExecStart=/opt/ros/kinetic/bin/roslaunch clever clever.launch --wait +Restart=on-abort + +[Install] +WantedBy=multi-user.target diff --git a/deploy/clever_arudino.tar.gz b/deploy/clever_arudino.tar.gz new file mode 100644 index 00000000..df92e77a Binary files /dev/null and b/deploy/clever_arudino.tar.gz differ diff --git a/deploy/generate_ros_lib b/deploy/generate_ros_lib new file mode 100644 index 00000000..b72c5602 --- /dev/null +++ b/deploy/generate_ros_lib @@ -0,0 +1,9 @@ +#!/usr/bin/env bash + +# This script generates ros_lib library for Arduino for using with rosseial_arduino: +# http://wiki.ros.org/rosserial_arduino/Tutorials +# https://copterexpress.gitbooks.io/clever/content/docs/arduino.html + +rm -rf ros_lib +rosrun rosserial_arduino make_libraries.py . +tar czf clever_arudino.tar.gz ros_lib diff --git a/deploy/roscore.env b/deploy/roscore.env new file mode 100644 index 00000000..f794e05b --- /dev/null +++ b/deploy/roscore.env @@ -0,0 +1,10 @@ +ROS_ROOT=/opt/ros/kinetic/share/ros +ROS_DISTRO=kinetic +ROS_PACKAGE_PATH=/home/pi/catkin_ws/src:/opt/ros/kinetic/share +ROS_PORT=11311 +ROS_MASTER_URI=http://localhost:11311 +CMAKE_PREFIX_PATH=/home/pi/catkin_ws/devel:/opt/ros/kinetic +PATH=/opt/ros/kinetic/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games:/snap/bin +LD_LIBRARY_PATH=/opt/ros/kinetic/lib +PYTHONPATH=/home/pi/catkin_ws/devel/lib/python2.7/dist-packages:/opt/ros/kinetic/lib/python2.7/dist-packages +ROS_IP=192.168.11.1 \ No newline at end of file diff --git a/deploy/roscore.service b/deploy/roscore.service new file mode 100644 index 00000000..56f949ac --- /dev/null +++ b/deploy/roscore.service @@ -0,0 +1,11 @@ +[Unit] +Description=Launcher for the ROS master, parameter server and rosout logging node +After=network.target + +[Service] +EnvironmentFile=/home/pi/catkin_ws/src/clever/deploy/roscore.env +ExecStart=/opt/ros/kinetic/bin/roscore +Restart=on-abort + +[Install] +WantedBy=multi-user.target diff --git a/docs/3g.md b/docs/3g.md index 1fe51dfe..a7972619 100644 --- a/docs/3g.md +++ b/docs/3g.md @@ -1,4 +1,4 @@ Использование внешнего 3G-модема === -TODO \ No newline at end of file +TODO diff --git a/docs/assemble.md b/docs/assemble.md index c8c6028e..8314be74 100644 --- a/docs/assemble.md +++ b/docs/assemble.md @@ -1,12 +1,12 @@ Инструкция по сборке конструктора Клевер 2 ========================================== -![Clever](../img/Clevermain.png) +![Clever](../assets/Clevermain.png) Состав конструктора ------------------- -![Explosion](../img/explosion.png) +![Explosion](../assets/explosion.png) * Рама центральная x2. * Рама дополнительная х4. @@ -76,7 +76,7 @@ 16. Ручка A (VrA). 17. Ручка B (VrB). -![radio Transmitter](../img/radioTransmitter.png) +![radio Transmitter](../assets/radioTransmitter.png) Дополнительное оборудование @@ -92,7 +92,7 @@ 7. Канцелярский нож 8. Мультиметр -![Дополнительное оборудование](../img/addEqipment.jpg) +![Дополнительное оборудование](../assets/addEqipment.jpg) ![Техника безопасности при пайке](../docs/tb.md) @@ -104,7 +104,7 @@ * Распаковать моторы. Используя плоскогубцы, укоротить провода на моторах, обрезать половину длины (оставив 25 мм). -![Мотор brrc2205](../img/brrc2205.png) +![Мотор brrc2205](../assets/brrc2205.png) Зачистить * снять 2мм термоизоляции с конца провода не повредив медные жилы. @@ -115,18 +115,18 @@ * Нанести флюс на оголенную часть провода. * Покрыть припоем, используя пинцет. -![Лужение](../img/zap.jpg) +![Лужение](../assets/zap.jpg) #### Закрепить мотор на луче * Установить мотор на сторону луча с гравировкой. * Прикрепить моторы к лучам винтами М3х8, используя отвертку. -![Закрепить мотор на луче](../img/brrc2205on.png) +![Закрепить мотор на луче](../assets/brrc2205on.png) * Лучи с моторами необходимо расположить согласно схеме. Стрелками указано направление вращения моторов. -![Вращение моторов](../img/brrc2205ondeck.png) +![Вращение моторов](../assets/brrc2205ondeck.png) //указать стрелками направление вращения на моторах #### Залудить три контактные площадки регулятора @@ -135,13 +135,13 @@ Чтобы припой аккуратно заполнил всю площадку, необходимо прогреть площадку регулятора. Для этого нужно удерживать жало паяльника на контактной плащадке в течение 2 сек (или больше, если потребуется) -![Лужение контактных площадок регуляторов](../img/escDYSzap.png) +![Лужение контактных площадок регуляторов](../assets/escDYSzap.png) * Повторить данную операцию для оставшихся трех регуляторов #### Припаять провода моторов к регуляторам Припаять ранее приготовленные провода моторов к контактным площадкам регуляторов. -![Припаять провода моторов к регуляторам](../img/solderingBrrc2205ondeckTOescDYSzap.png) +![Припаять провода моторов к регуляторам](../assets/solderingBrrc2205ondeckTOescDYSzap.png) * Повторить данную операцию для оставшихся трех регуляторов @@ -155,13 +155,13 @@ * Длина 7 см (Для силового разъема XT60 pin) - 1 красный, 1 черный * Длина 9 см (Для силового разъема XT60 socket) - 1 красный, 1 черный -![Подготовка проводов для силового разъема](../img/cutwire14AWG.jpg) +![Подготовка проводов для силового разъема](../assets/cutwire14AWG.jpg) #### Подготовка силовых разъемов питания XT60 pin и XT60 socket ![Статья про силовые разъемы и их обозначения](../notes/powerConnection.md) -![Силовой разъем XT60](../img/xt60pinsocket.jpg) +![Силовой разъем XT60](../assets/xt60pinsocket.jpg) 1. Под разъем XT60 pin залудить два силовых провода красный и чёрный 14AWG длиной 7 см. 2. Залудить контактные площадки разъема XT60 pin. @@ -171,7 +171,7 @@ 6. Надеть термоусадку ф5 на провода так, чтобы она закрывала контактные площадки проводов с XT60 . 7. Усадить термоусадку феном. -![Монтаж разъемов XT60](../img/mountxt60pinsocket.png) +![Монтаж разъемов XT60](../assets/mountxt60pinsocket.png) 8. Повторить процедуру для разъема XT60 socket. @@ -183,7 +183,7 @@ 3. Убрать 3-й (оранжевый) провод из разъема, за ненадобностью. 4. Длина оставшихся черного и красного проводов 10-12 см. -![Монтаж разъема 5В](../img/mount5vconnector.png) *было бы круто, если делать такие картинки и в формате гифки +![Монтаж разъема 5В](../assets/mount5vconnector.png) *было бы круто, если делать такие картинки и в формате гифки ### Монтаж платы распределения питания @@ -193,7 +193,7 @@ ![Статья про прозвонку](/notes/testConnection.md) -![Предпаячная проверка](../img/startPDBtest.jpg) +![Предпаячная проверка](../assets/startPDBtest.jpg) Прозвонить следующие цепи на НЕЗАМКНУТОСТЬ (отсутствие звукового сигнала мультиметра): * “BAT+” и “BAT-” @@ -208,14 +208,14 @@ 1. ![Залудить*](../notes/zap.md) контактные площадки платы питания. 2. С помощью мультиметра проверить отсутствие контактного замыкания на плате (прозвонить) -![Постпаячная проверка](../img/zapPDBtest.jpg) +![Постпаячная проверка](../assets/zapPDBtest.jpg) Чтобы припой аккуратно заполнил всю площадку, необходимо её прогреть. Для этого нужно удерживать жало паяльника на контактной плащадке в течение 2 сек (или больше, если потребуется) #### Пайка силового разъема питания XT60 Припаять разъем для АКБ, соблюдая полярность на контактных площадках. -![Пайка XT60 на PDB](../img/solderingxt60socketTOpdb.png) +![Пайка XT60 на PDB](../assets/solderingxt60socketTOpdb.png) ВАЖНО о полярности * красный провод - это “+” @@ -225,12 +225,12 @@ Припаять разъем 5В, соблюдая полярность на контактных площадках. (на изображении: красный провод - это питание “+”) -![Пайка 5В на PDB](../img/soldering5VTOpdb.png) +![Пайка 5В на PDB](../assets/soldering5VTOpdb.png) ### Монтаж отсека АКБ #### Подготовка перемычек (3 шт.) -![Перемычка](../img/jumper.png) +![Перемычка](../assets/jumper.png) * Отрезать силовой провод длиной 2 см. * Зачистить с обеих сторон. @@ -240,7 +240,7 @@ * Прозвонить мультиметром. В случае необходимости зачистить наждачной бумагой. #### Подготовка отсека АКБ -![Подготовка отсека АКБ](../img/casebattery.png) +![Подготовка отсека АКБ](../assets/casebattery.png) * Приклеить наклейки с разметкой внутрь отсека АКБ, в соответствии с полярностью. * Приклеить ленту из скотча на дно отсека. @@ -248,34 +248,34 @@ ### Монтаж платы распределения питания * Установить плату питания на раму винтами М3х8 и пластиковыми гайками. -![Установка платы PDB](../img/mountPDB.png) +![Установка платы PDB](../assets/mountPDB.png) * ВАЖНО Стрелочка на плате направлена в сторону носового выреза -![Установка платы PDB](../img/topviewmountPDB.png) +![Установка платы PDB](../assets/topviewmountPDB.png) #### Монтаж элементов 1. Установить гайки в пластиковые держатели -![Монтаж пластиковых держателей](../img/holderLegs.png) +![Монтаж пластиковых держателей](../assets/holderLegs.png) 2. Установить лучи на раму винтами М3х16 *Лучи устанавливаются поверх рамы *Пластиковые держатели устанавливаются снизу рамы -![Монтаж лучей](../img/mountBeams.png) +![Монтаж лучей](../assets/mountBeams.png) 3. Расположение моторов Проверить расположение моторов (моторы с черной гайкой в левом верхнем углу и в правом нижнем). -![Расположение моторов](../img/motorsTopview.png) +![Расположение моторов](../assets/motorsTopview.png) 4. Продеть силовые провода регуляторов в отверстия. -![силовые провода моторов](../img/escWires.png) +![силовые провода моторов](../assets/escWires.png) #### Пайка силовой цепи платы питания Припаять силовые провода регуляторов к плате питания, соблюдая полярность. -![Пайка силовых проводов на PDB](../img/solderingPowerwires.png) +![Пайка силовых проводов на PDB](../assets/solderingPowerwires.png) ВАЖНО о полярности *красный провод - это “+” @@ -284,14 +284,14 @@ ### Сопряжение приемника и пульта 1. Подключить радиоприемник к разъему 5В. В любой разъем, GND внизу. На схеме питание обозначено как 5V -![Подключение питания приемника](../img/receiver5V.png) +![Подключение питания приемника](../assets/receiver5V.png) 3. Подключить АКБ. Светодиод на радиоприемнике должен мигать. -![Подключение АКБ](../img/connectBattery.png) +![Подключение АКБ](../assets/connectBattery.png) #### БЕЗОПАСНОСТЬ при работе с АКБ -![БЕЗОПАСНОСТЬ при работе с АКБ](../img/safetyPower.png) +![БЕЗОПАСНОСТЬ при работе с АКБ](../assets/safetyPower.png) #### Включение радиопульта 1. На пульте зажать кнопку BIND KEY. @@ -300,45 +300,45 @@ 4. Отсоединить джампер. 5. Светодиод горит непрерывно. -![Подключение питания приемника](../img/connectingRadio.png) +![Подключение питания приемника](../assets/connectingRadio.png) ![Мануал по неисправностям](../docs/radioerrors1.md) ### Проверка направления вращения моторов 1. Наклеить наклейки на АКБ 18650. 2. Установить 18650 в отсек АКБ, соблюдая полярность. -![Готовность отсека АКБ](../img/readyBatteryholder.png) +![Готовность отсека АКБ](../assets/readyBatteryholder.png) 3. Проверить, что разъем питания 5В подключен к приемнику по схеме. 4. Подключить регулятор мотора к 3 каналу приемника CH3 по схеме. -![Подключение регулятора к приемнику](../img/connectionESCtoReceiver.png) +![Подключение регулятора к приемнику](../assets/connectionESCtoReceiver.png) 5. Подключить внешнее питание (АКБ). 6. Включить пульт. 7. Подать левым стиком газ (throttle) на 10%. 8. Проверить направления вращения мотора по схеме. -![Проверка вращения моторов](../img/testMotors.png) +![Проверка вращения моторов](../assets/testMotors.png) 9. Если необходимо изменить направление вращения, то меняем любые два фазных провода мотора (нужно перепаять). -![Перепайка фазных проводов](../img/resolderingESC.png) +![Перепайка фазных проводов](../assets/resolderingESC.png) ### Монтаж радиоприемника 1. Установить пластиковые стойки 30 мм на раму винтами М3х8. 2. Разъем питания 5В продеть в прорезь. -![Установка стоек и прорезь](../img/mountReceiverStud.png) +![Установка стоек и прорезь](../assets/mountReceiverStud.png) 3. Приемник прикрепить к ![нижней дополнительной раме*](../notes/deck.md), используя двухсторонний скотч и ориентируясь на гравировку. Антенны направлены вперед. - ![Установка радиоприемника на деку](../img/mountReceiverDeck.png) + ![Установка радиоприемника на деку](../assets/mountReceiverDeck.png) 4. Установить 3х проводной шлейф в канал PPM / CH1. -![Подключение радиоприемника](../img/receiverPPM.png) +![Подключение радиоприемника](../assets/receiverPPM.png) 5. Продеть в прорезь к разъему 5 В. 6. Прикрутить нижнюю дополнительную раму к стойкам на центральной раме винтами М3х8. -![Установка нижней деки](../img/mountBottomDeck.png) +![Установка нижней деки](../assets/mountBottomDeck.png) ##### ВАЖНО Направление стрелок на плате питания и на раме дополнительной совпадают @@ -346,11 +346,11 @@ ### Монтаж полетного контроллера #### Переворачиваем сборку -![Переворачиваем сборку](../img/topPreview.png) +![Переворачиваем сборку](../assets/topPreview.png) #### Установка полетного контроллера Pixhawk 1. Клеим 2х сторонний скотч по углам полетного контроллера - ![Полетный контроллер](../img/pixhawk.png) + ![Полетный контроллер](../assets/pixhawk.png) ##### ВАЖНО При работе моторов возникают вибрации, отрицательно влияющие на показания датчиков полетного контроллера Pixhawk. Чтобы избежать этого эффекта, количество слоев двустороннего скотча @@ -358,7 +358,7 @@ 2. Установить полетный контроллер в центр рамы - ![Полетный контроллер](../img/topviewpixhawk.png) + ![Полетный контроллер](../assets/topviewpixhawk.png) ##### ВАЖНО Стрелки на раме и pixhawk должны быть сонаправлены @@ -368,29 +368,29 @@ 2. Моторы к 1,2,3,4 портам MAIN OUT, согласно схеме 3. Питание от PDB (5В/VCC) в любой порт, кроме SB (SBUS) - ![Подключение полетного контроллера](../img/connectionPixhawk.png) + ![Подключение полетного контроллера](../assets/connectionPixhawk.png) ### Сборка регуляторов 1. Клеим 2х сторонний скотч на основание защитного бокса регуляторов -![Скотч на бокс регулей](../img/escCase.png) +![Скотч на бокс регулей](../assets/escCase.png) 2. Укладываем регуляторы в защитные боксы. Крепим полученную сборку к лучам рамы. -![Вид сверху с боксами для регулей](../img/topESCcaseview.png) +![Вид сверху с боксами для регулей](../assets/topESCcaseview.png) ### Установка защиты 1. Закрепить нижнюю защиту винтами М3х16 на лучах рамы -![Установка лучевой защиты](../img/lowsafeDeck.png) +![Установка лучевой защиты](../assets/lowsafeDeck.png) 2. Закрепить ножки к пластиковым держателям винтами М3х16 -![Установка ножек](../img/safeLegs.png) +![Установка ножек](../assets/safeLegs.png) 3. Закрепить стойки 30 мм в отверстия нижней защиты винтами М3х12 -![Установка нижней радиальной защиты](../img/safelowRadial.png) +![Установка нижней радиальной защиты](../assets/safelowRadial.png) 4. Закрепить верхнюю защиту винтами М3х12 -![Установка верхней радиальной защиты](../img/safehighRadial.png) +![Установка верхней радиальной защиты](../assets/safehighRadial.png) ### Монтаж отсека АКБ @@ -402,11 +402,11 @@ * Батарейный отсек (1 шт) 1. Прикрепить батарейный отсек на верхнюю дополнительную раму винтами М3х12 и гайками. -![Монтаж отсека АКБ](../img/mountHolder.png) +![Монтаж отсека АКБ](../assets/mountHolder.png) 2. Прикрепить верхнюю дополнительную раму на стойки винтами М3х8. -![Монтаж отсека АКБ](../img/isoViewmountHolder.png) +![Монтаж отсека АКБ](../assets/isoViewmountHolder.png) 3. Установить АКБ в отсек. @@ -415,7 +415,7 @@ 1. Крепим антенны на 2х сторонний скотч или изоленту, а усики продеваем в передние отверстия верхней дополнительной рамы. -![Монтаж отсека АКБ](../img/mountAntenna.png) +![Монтаж отсека АКБ](../assets/mountAntenna.png) Коптер готов к настройке! @@ -430,7 +430,7 @@ 3. Позвать на помощь “Если при выполнении работ возникли какие-либо проблемы, необходимо обратиться к преподавателю или учителю, а не пытаться решить проблему самостоятельно.” -![Безопасность при сборке](../img/safetybyassem.png) +![Безопасность при сборке](../assets/safetybyassem.png) ## Безопасность при работе с Li-ion аккумуляторами 18650 diff --git a/notes/deck.md b/docs/deck.md similarity index 85% rename from notes/deck.md rename to docs/deck.md index 2b79739f..5f5a62d6 100644 --- a/notes/deck.md +++ b/docs/deck.md @@ -2,4 +2,4 @@ Они абсолютно одинаковые. Поэтому для дальнейшего удобства понимания инструкции условно разделим их на верхнюю и нижнюю дополнительные рамы -![Общая раскладка](../img/allElements.png) +![Общая раскладка](../assets/allElements.png) diff --git a/docs/etcher.md b/docs/etcher.md index 178a19da..089d567b 100644 --- a/docs/etcher.md +++ b/docs/etcher.md @@ -1,4 +1,4 @@ Использование программы Etcher === -TODO \ No newline at end of file +TODO diff --git a/docs/frames.md b/docs/frames.md index eca8ba7b..9dbd0458 100644 --- a/docs/frames.md +++ b/docs/frames.md @@ -9,4 +9,4 @@ * `fcu` — координаты относительно квадрокоптера: схематичное изображение квадрокоптера на иллюстрации; * `fcu_horiz` — координаты относительно квадрокоптера без учета наклонов по тангажу и рысканью: красная, синия и зеленая линии на иллюстрации. -Более наглядно 3D визуализацию систем координат можно наблдюдать, используя [rviz](/docs/rviz.md). \ No newline at end of file +Более наглядно 3D визуализацию систем координат можно наблдюдать, используя [rviz](/docs/rviz.md). diff --git a/docs/les1.md b/docs/les1.md index ec9af942..45e8fb18 100644 --- a/docs/les1.md +++ b/docs/les1.md @@ -8,19 +8,19 @@ В 1899 году на выставке в Мэдисон-Сквер-Гарден инженер и изобретатель Никола Тесла продемонстрировал миниатюрное радиоуправляемое судно. Несмотря на то, что общественность в первую очередь заинтересовало военное применение его изобретения, сам Тесла указывал на потенциально гораздо более широкое применение дистанционного управления (названного изобретателем «телеавтоматикой»), например, в человекоподобных автоматонах. -![Tesla](../img/1_1.png) +![Tesla](../assets/1_1.png) ### «Жук» Кеттеринга Экспериментальная беспилотная «воздушная торпеда», один из первых проектов предшественников современных крылатых ракет. Разработана изобретателем Чарльзом Кеттерингом по заказу Армии США в 1917 году. Предназначалась для обстрела с дистанции до 120 км городов, крупных промышленных центров и мест сосредоточения войск противника. Отличался простотой конструкции и очень низкой ценой. Хотя аппарат сравнительно успешно прошёл испытания, Первая мировая война закончилась и снаряд так и остался опытным. -![Bug](../img/1_2.png) +![Bug](../assets/1_2.png) ### Вертолёт Ботезата Многовинтовые вертолёты разрабатывались ещё в первые годы вертолётостроения. Один из первых квадрокоптеров (англ. quadcopter, четырёхроторный вертолет), который реально оторвался от земли и мог держаться в воздухе, был создан Георгием Ботезатом и испытан в 1922 году. Недостатком этих аппаратов была сложная трансмиссия, передававшая вращение одного мотора на несколько винтов. Изобретение хвостового винта и автомата перекоса положило конец этим попыткам. Новые разработки начались в 1950-е годы, но дальше прототипов дело не продвинулось. -![Helicopter](../img/1_3.png) +![Helicopter](../assets/1_3.png) Применение коптеров ------------------- @@ -56,14 +56,14 @@ один из лучей направлен вперед, у «х»-платформ основное направление находится между двумя соседними лучами. -![Types](../img/1_4.png) +![Types](../assets/1_4.png) Управление ---------- Управление коптером осуществляется при помощи пульта управления, который передает команды радиоприемнику. Пульт питается от батареек, а радиоприемник получает питание от Полетного контроллера. Связь зачастую односторонняя, только от пульта к приемнику. Приемник подключается к полетному контроллеру минимум пятью проводами, по которым передаются сигналы поворота вокруг трех осей, команда «газа» и полетный режим. -![Control](../img/1_5.png) +![Control](../assets/1_5.png) **Throttle** — переводится как «дроссель», «тяга» или «газ» в обиходе. Газ мультикоптера — среднее арифметическое между скоростями вращения всех моторов. Чем больше газ, тем больше суммарная тяга моторов, и тем сильнее они тащат коптер вверх (проще говоря «Тапок в пол» здесь означает наискорейший подъем). Обычно измеряется в процентах: 0% — моторы остановлены, 100% — вращаются с максимальной скоростью. Газ висения — минимальный уровень газа, который необходим, чтобы коптер не терял высоту. @@ -71,15 +71,15 @@ **Yaw** – «рыскание». Поворот носа мультикоптера. условно - вращение вправо влево. -![Yaw](../img/1_6.png) +![Yaw](../assets/1_6.png) **Pitch** - «тангаж». В коптерах манипуляции с этим моментом силы позволяет коптеру двигаться вперед или назад за счет наклона носа в соответствующем направлении. -![Pitch](../img/1_7.png) +![Pitch](../assets/1_7.png) **Roll** - «крен». Наклон мультикоптера влево вправо. Коптер за счет крена может двигаться боком в соответствующую сторону. -![Roll](../img/1_8.png) +![Roll](../assets/1_8.png) Газ, тангаж, крен, рыскание — если вы можете управлять этими четырьмя параметрами, значит вы можете управлять квадрокоптером. Их еще иногда называют каналами управления. Режимов полета существует много. Используется и GPS, и барометр, и дальномер, так же режим стабилизации (stab, stabilize, летать в «стабе»), в котором квадрокоптер держит те углы, которые ему задаются с пульта не зависимо от внешних факторов. В этом режиме при отсутствии ветра квадрокоптер может висеть почти на месте. Ветер же придется компенсировать пилоту. Направление вращения винтов выбирается не случайно. Если бы все моторы вращались в одну сторону, квадрокоптер вращался бы в противоположную из-за создаваемых моментов. Поэтому одна пара противостоящих моторов всегда вращается в одну сторону, а другая пара — в другую. Эффект возникновения моментов вращения используется, чтобы изменять угол рыскания: одна пара моторов начинает вращаться чуть быстрее другой, и вот уже квадрокоптер медленно поворачивается к нам лицом: @@ -92,7 +92,7 @@ RBW — right back clockwise rotation (правый задний, вращение по часовой стрелке) - ![Parts](../img/1_9.png) + ![Parts](../assets/1_9.png) Элементы коптера ---------------- @@ -101,22 +101,22 @@ Именно здесь в игру вступают Полетные контроллеры. -![Flightctr](../img/1_10.png) +![Flightctr](../assets/1_10.png) **Полетный контроллер** - самая важная часть. Стабильность полета и управляемость на девяносто процентов зависит от способностей полетного контроллера. Задача полетного контроллера — переводить команды от пульта управления в сигналы задающие обороты двигателя. Также в нем установлены инерциальные измерительные датчики, позволяющие следить за текущим положением платформы и выполнять автоматические регулировки. -![Flightctr](../img/1_11.png) +![Flightctr](../assets/1_11.png) **ESC** — это регуляторы оборотов электродвигателей. Дело в том, что в мультикоптерах используют специальные бесколлекторные электродвигатели, которые способны работать на очень больших оборотах. Для управления этими двигателями необходимо формировать трехфазное напряжение и относительно большие токи, чем и занимаются регуляторы оборотов. Для каждого двигателя необходим свой регулятор оборотов. Все регулятора оборотов подключаются к полетному контроллеру. Питаются регуляторы непосредственно от аккумулятора. Каждый двигатель подключен к своему регулятору оборотов тремя проводами. Последовательность подключения проводов определяет направление вращения двигателя. -![Esc](../img/1_12.png) +![Esc](../assets/1_12.png) **Электродвигатель** В конструкциях коптеров используются бесколлекторные электродвигатели. Они обладают выдающимися характеристиками и живучестью в связи с отсутствием трущихся узлов (щеток) посредством которых передается ток. В отличие от обычного электродвигателя, у которого имеется подвижная часть - ротор и неподвижная - статор, у бесколлекторного двигателя подвижной частью является как раз статор с постоянными магнитами, а неподвижной частью - ротор с обмотками трех фаз. Для того, чтобы заставить вращаться такую систему, необходимо осуществлять в определенном порядке смену направления магнитного поля в обмотках ротора - тогда постоянные магниты статора будут взаимодействовать с магнитными полями ротора и подвижный статор прийдет в движение. Это движение основано на свойстве магнитов с одноименными полюсами полюсами отталкиваться, а с противоположными - притягиваться. -![Engine](../img/1_13.png) +![Engine](../assets/1_13.png) **Аппаратура радиоуправления** diff --git a/docs/les11.md b/docs/les11.md index a59a9361..dba134a1 100644 --- a/docs/les11.md +++ b/docs/les11.md @@ -10,7 +10,7 @@ Во время работы постоянно происходят химические реакции между компонентами электродных пластин с заполняющим их веществом — электролитом. Принципиальную схему устройства аккумулятора можно представить рисунком упрощенного вида, когда в корпус сосуда вставлены две пластины из разнородных металлов с выводами для обеспечения электрических контактов. Между пластинами залит электролит. -![bat](../img/11_1.png) +![bat](../assets/11_1.png) Работа аккумулятора при разряде ------------------------------- @@ -19,7 +19,7 @@ Этот процесс условно показан на схеме с никель-кадмиевой конструкцией электродов. -![bat](../img/11_2.png) +![bat](../assets/11_2.png) Здесь в качестве материала положительного электрода используют окислы никеля с добавками графита, которые повышают электрическую проводимость. Металлом отрицательного электрода работает губчатый кадмий. Во время разряда частицы активного кислорода из окислов никеля выделяются в электролит и направляются на отрицательные пластины, где окисляют кадмий. @@ -45,7 +45,7 @@ Двигатели квадрокоптера в зависимости от размера могут потреблять значительные токи. Основным требованием к аккумуляторам является высокая токоотдача. И наилучшими характеристиками с этой точки зрения обладают литий-полимерные аккумуляторы. -![bat](../img/11_3.png) +![bat](../assets/11_3.png) ### Характеристики LiPo аккумуляторов @@ -61,7 +61,7 @@ Для зарядки используется специальное зарядное устройство. Практически все модели питаются не от сети, а от постоянного напряжения 12В. -![bat](../img/11_4.png) +![bat](../assets/11_4.png) Особенность этого зарядного устройства в том, что он умеет делать балансировку ячеек аккумулятора. То есть аккумулятор подключается к нему не только силовым разъемом, но и дополнительным балансировочным разъемом на который выведены все ячейки по отдельности. Это дает возможность заряжать все ячейки равномерно, что дает одинаковое распределение нагрузки на банки аккумулятора в процессе эксплуатации. @@ -70,7 +70,7 @@ Для подключения аккумуляторов используют специальные коннекторы. Диаметр пистонов в них 4мм и они дополнительно подпружинены для обеспечения большой площади контакта. Еще для подключения используют специальные провода в силиконовой изоляции, которая способна выдерживать высокие температуры. -![bat](../img/11_5.png) +![bat](../assets/11_5.png) Меры предосторожности --------------------- diff --git a/docs/les13.md b/docs/les13.md index 177f0c8a..8bb16db1 100644 --- a/docs/les13.md +++ b/docs/les13.md @@ -6,7 +6,7 @@ Отличие мультикоптера от других подобных роботов (ездящих, плавающих), заключается в том, что пилот не управляет напрямую мощностью мотора. С помощью джойстика, он передает сигнал полётному контроллеру, который делает расчеты и передает необходимую мощность на моторы. -![Speed](../img/13_1.png) +![Speed](../assets/13_1.png) Рис. Скорость вращения моторов, в зависимости от команды пилота. @@ -17,7 +17,7 @@ Полётный контроллер - устройство, обеспечивающее полёт квадрокоптера, за счет управления газом, углами крена, тангажа и рысканья (throttle, pitch, roll, yaw). Это своеобразные "мозги" мультикоптера. Обычно он содержит несколько датчиков (гироскопы, акселерометр, магнитометр, GPS датчик) и микроконтроллер, который производит расчеты. Именно полётный контроллер отвечает за то, чтобы при среднем положении всех стиков джойстика квадрокоптер стабилизировался, висел в воздухе не отклоняясь ни в одну из сторон. -![FC](../img/13_2.png) +![FC](../assets/13_2.png) Полётный контроллер несколько десятков раз в секунду выполняет цикл управления в который входит: считывание показаний датчиков, считывание каналов управления, обработка информации и выдача управляющих сигналов моторам, чтобы выполнять команды пилота. @@ -35,7 +35,7 @@ Полётный контроллер выдает ШИМ-импульсы (PWM) на регуляторы оборотов (ESC), в зависимости от команды стика джойстика, либо программы. Например, чтобы дать команду мотору вращаться с максимальной скоростью контроллер должен отправлять импульсы длительностью 2 миллисекунды, перемежающиеся логическим нулем длительностью 10 — 20 миллисекунд. Длительности импульса в 1 миллисекунду соответствует остановка мотора, 1.1 мс — 10% от максимальной скорости, 1.2 мс — 20% и т.п. Длительность нуля не играет никакой роли, важна только длительность самого импульса. -![shim](../img/13_3.jpg) +![shim](../assets/13_3.jpg) Но все не так просто, полетные контроллеры бывают разные с разными настройками, регуляторы бывают разные, минимум (1 мс) и максимум (2 мс) — не универсальны. В зависимости от множества факторов диапазон 1-2 мс может на деле оказаться 1.1 — 1.9 мс, либо другим. Чтобы регулятор и контроллер говорили абсолютно на одном языке существует процедура калибровки регуляторов. @@ -48,7 +48,7 @@ Для многих устройств, использующих ПИД-регуляторы, существуют инструкции по настройке. Но чтобы легче ориентироваться в этом многообразии полезно понимать, как же внутри устроены эти регуляторы. Предлагаю вместе со мной самим заново «изобрести» и «на пальцах» понять формулу ПИД-регулятора. Будем рассматривать квадрокоптер в двумерном пространстве, где у него есть только один угол — угол крена, и два мотора: левый и правый. -![FC](../img/13_4.png) +![FC](../assets/13_4.png) В полетный контроллер непрерывно поступают команды с земли: «крен 30 градусов», «крен -10 градусов», «крен 0 градусов (держать горизонт)»; его задача — как можно быстрее и точнее их выполнять с помощью моторов с учетом: ветра, неравномерного распределения веса квадрокоптера, неравномерного износа моторов, инерции квадрокоптера и т.п. Таким образом, полетный контроллер должен непрерывно решать задачу, какую скорость вращения подавать на каждый мотор с учетом текущего значения угла крена и требуемого @@ -61,7 +61,7 @@ right = throttle - force,*** Представим ситуацию: поступает команда «держать горизонт» (***tar get_roll*** = 0), а квадрокоптер имеет крен влево: -![FC](../img/13_5.png) +![FC](../assets/13_5.png) Рис. Двухмерный квадрокоптер с креном влево. ***error*** — разность (ошибка) между ***tar get_roll*** и ***roll***, которую контроллер стремится минимизировать. @@ -82,15 +82,15 @@ D — настраиваемый коэффициент: чем он больш Скорость изменения любой величины — производная этой величины по времени: -![math](../img/13_6.png) +![math](../assets/13_6.png) И вот пропорциональный регулятор превращается в пропорционально-дифференциальный (пропорциональное слагаемое и дифференциальное): -![math](../img/13_7.png) +![math](../assets/13_7.png) Ошибку ***error*** вычислить легко, ведь на каждой итерации мы знаем ***roll*** и ***tar get_roll***; P и D — настраиваемые перед запуском параметры. Для вычисления производной (скорости изменения ***error***) необходимо хранить предыдущее значение ***error***, знать текущее значение ***error*** и знать время, которое прошло между измерениями (период регулирования). И вот она — физика шестого класса школы (скорость = расстояние / время): -![math](../img/13_8.png) +![math](../assets/13_8.png) ***dt*** — период регулирования; ***error previous*** — значение ошибки с предыдущей итерации цикла регуляции. Кстати, эта формула — простейший способ численного дифференцирования, и он нам здесь вполне подойдет. @@ -100,17 +100,17 @@ D — настраиваемый коэффициент: чем он больш Тут есть нюанс. Предположим ***error*** равна 1 градусу, цикл регулирования — 0.1с. Тогда за одну секунду сумма ошибок примет значение 10 градусов. А если цикл обработки — 0.01с, то сумма наберет аж 100 градусов. Чтобы за одно и тоже время интегральное слагаемое набирало одно и тоже значение при разных периодах регулирования, полученную сумму будем умножать на сам период регулирования. Легко посчитать, что в обоих случаях из примера получается сумма в 1 градус. Вот оно — интегральное слагаемое (пока без настраиваемого коэффициента): -![math](../img/13_9.png) +![math](../assets/13_9.png) Эта формула — не что иное, как численный интеграл по времени функции ***error*** в интервале от нуля до текущего момента. Именно поэтому слагаемое называется интегральным: -![math](../img/13_10.png) +![math](../assets/13_10.png) где T — текущий момент времени. Пришло время записать окончательную формулу пропорционально-интергрально-дифференциального регулятора: -![math](../img/13_11.png) +![math](../assets/13_11.png) где ***I*** — один из настраиваемых параметров, которых теперь трое: ***P,I,D***. ПИД регуляторы - важная часть полётного контроллера, без их использования квадрокоптер летал бы непредсказуемо. Они настраиваются индивидуально для каждого квадрокоптера. diff --git a/docs/les15.md b/docs/les15.md index 19dd74f5..e5931cf4 100644 --- a/docs/les15.md +++ b/docs/les15.md @@ -6,26 +6,26 @@ Радиосвязь - наиболее распространенный способ передачи информации на расстояние. Сотовые телефоны, спутниковая связь, телевиденье - все это работает на основе передачи сигналов через электромагнитные колебания определенной частоты. -![Radio](../img/15_1.png) +![Radio](../assets/15_1.png) В передатчике формируются высокочастотные колебания определенной частоты (несущий сигнал). На него накладывается сигнал, который нужно передать, это называется модуляция полезным сигналом. Сформированный таким образом высокочастотный сигнал излучается антенной в виде радиоволн. Этот сигнал воспринимается антенной приёмника, проходит через систему фильтров, которая выделяет из множества наведенных в антенне токов от различных передатчиков сигнал с нужной несущей частотой, а детектор выделяет из него модулирующий полезный сигнал. В зависимости от несущей частоты передатчика, излучаемый сигнал обладает разными характеристиками относительно дальности распространения, рассеивания, способности отражаться и огибать препятствия. Радиоволны распространяются в пустоте и в атмосфере; земная твердь и вода для них непрозрачны. Однако, благодаря эффектам дифракции и отражения, возможна связь между точками земной поверхности, не имеющими прямой видимости (в частности, находящимися на большом расстоянии). Примером модуляции сигнала может служить AM и FM. Это частные виды аналоговой модуляции в которых полезный сигнал передаётся либо за счет амплитуды волны, либо частоты. -![Radio](../img/15_2.png) +![Radio](../assets/15_2.png) ### Принцип работы радиоаппаратуры управления. Управление подвижными моделями основано на взаимодействии человека и модели, в нашем случае квадрокоптера. Аппаратура радиоуправления состоит из передатчика, который находится у пилота, и размещенных на модели приемника и полётного контроллера, который и управляет квадрокоптером через регуляторы мощности. О полётном контроллере и регуляторах мощности мы уже рассказывали, теперь рассмотрим Приёмник и передатчик. -![Radio](../img/15_3.png) +![Radio](../assets/15_3.png) ### Передатчик Различают 2 основных вида пультов - джойстиковые и пистолетные. Для квадрокоптеров используют джойстиковый пульт. -![Radio](../img/15_4.png) +![Radio](../assets/15_4.png) Для управления движущимися моделями требуется воздействие одновременно на несколько функций, поэтому передатчики радиоуправления делают многоканальными. Для квадрокоптеров минимальное количество каналов - 4: управление газом, угол крена, угол тангажа, угол рысканья. Положение каждого из стиков пульта кодируется при помощи ШИМ импульса. @@ -35,7 +35,7 @@ На выходе с пульта сигнал модулируется, чтобы передать данные на квадрокоптер. Модуляция сигнала позволяет наложить полезный сигнал на излучаемые волны, для этого все каналы уплотняются в один посредством кодирования. В основном для этого используется фазово-импульсная модуляция, обозначаемая буквами РРМ (Pulse Position Modulation), она распространена из-за единого стандарта на всем оборудовании. Пульт и приемник разных производителей могут работать вместе, привязка их друг к другу осуществляется за счет пары кварцевых резонаторов. -![Radio](../img/15_5.png) +![Radio](../assets/15_5.png) РРМ сигнал имеет фиксированную длину периода Т=20мс. Это означает, что информация о положениях ручек управления на передатчике попадает на модель 50 раз в секунду, что определяет быстродействие аппаратуры управления. Как правило, этого хватает, поскольку скорость реакции пилота на поведение модели намного меньше. Все каналы пронумерованы и передаются по порядку номеров. Значение сигнала в канале определяется величиною временного промежутка между первым и вторым импульсом - для первого канала, между вторым и третьим - для второго канала и т.д. @@ -45,13 +45,13 @@ Приёмник - устройство, служащее для осуществления радиоприёма, т.е . для выделения сигналов из радиоизлучения. Приёмник устанавливается на квадрокоптере, принимает сигнал с пульта и передаёт его в полётный контроллер. -![Radio](../img/15_6.png) +![Radio](../assets/15_6.png) ### Принципиальная схема работы приемника Сигналы, принятые антенной, подаются на колебательный контур, в котором работает приёмник. Этот контур является преселектором. После преселектора сигнал попадает в усилитель высокой частоты, а затем, уже усиленный сигнал подается на смеситель. На смеситель так же подается сигнал с гетеродина ( высокочастотного генератора). Антенна воспринимает сигналы со всех передатчиков, находящихся рядом, в смесителе происходит процесс первичной фильтрации сигнала. Из смесителя отфильтрованные сигналы поступают на селектор промежуточной частоты, который должен выбрать из всех полученных, сигнал "своего" приёмника и подавить остальные. После этого сигнал проходит через усилитель промежуточной частоты и попадает на демодулятор-дискримнатор, где сигнал проходит обратную модуляции процедуру, когда принятого сигнала выделяется полезный сигнал. Затем он попадает на триггер Шмидта, который формирует необходимую амплитуду и крутизну PPM сигнала, а затем подается на декодер полётного контроллера. -![Radio](../img/15_7.png) +![Radio](../assets/15_7.png) Чтобы было понятно, рассмотрим на примере. Наш передатчик и приемник осуществляют связь по 50 каналу, 40,665 МГц. Частота гетеродина, чтобы определить "свой" сигнал обычно отличается на 455 кГц (0,455 МГц), соответственно она равна 40,665 МГц - 0,455 МГц = 40,210 МГц. Например, антенна приняла сигналы 40,665 МГц и 40,805 МГц, что соответствует 50 и 80 каналам. Они усилились при помощи УВЧ и попали в смеситель. Чтобы найти "свой" сигнал в смесителе происходит сложение и вычитание частот полученных сигналов и гетеродина. Получим: diff --git a/docs/les16.md b/docs/les16.md index 2d1a3654..74a12a9a 100644 --- a/docs/les16.md +++ b/docs/les16.md @@ -8,14 +8,14 @@ ### Принцип работы аналоговых видеокамер -![Analog](../img/16_1.png) +![Analog](../assets/16_1.png) Он заключается в том, что световой поток, проходя сквозь линзы объектива, попадает на матрицу ПЗС, где он преобразуется в видеосигнал. Рынок аналоговых видеокамер обширный, и каждый из нас сможет сделать оптимальный для себя выбор, в соответствии с необходимыми характеристиками и вкусом. Большим преимуществом камер аналогового типа является их взаимосовместимость, даже от разных производителей, а также простота монтажа и настройки через соответствующие меню. ### Работа цифровых камер -![analogCam](../img/16_2.png) +![analogCam](../assets/16_2.png) Цифровые видеокамеры (IP камеры), вошли в нашу жизнь совсем недавно, но с позиций их использования – на практике зарекомендовали себя достаточно хорошо с позиций существенного улучшения качества. Конечно же, они пока что, немного дороговаты по сравнению с камерами аналогового типа, но в перспективе развития цифровых технологий – альтернативы им нет. @@ -61,11 +61,11 @@ Летать можно 2-мя способами. С помощью монитора или в очках. -![resolution](../img/16_3.png) +![resolution](../assets/16_3.png) Современный рабочий вариант очков имеет разрешение 800x600, чего более чем достаточно. -![camera](../img/16_4.png) +![camera](../assets/16_4.png) Даже на отличной камере детализация низковата. Атмосферу не чувствуешь, и какие-то шумы присутствуют. Насладиться красотой пейзажей можно потом, в записи со второй цифровой HD-камеры, а не в процессе полета. diff --git a/docs/les2.md b/docs/les2.md index 439a730e..947160d5 100644 --- a/docs/les2.md +++ b/docs/les2.md @@ -12,7 +12,7 @@ Так вот, эта разница потенциалов (+) и (-), есть электродвижущая сила (далее ЭДС), то есть электрическое напряжение. -![current](../img/2_1.png) +![current](../assets/2_1.png) Итак, источник электроэнергии обладает разностью потенциалов, заряженные частицы которых, стремятся друг к другу. А так - же есть такие, которые ограничивают их движение. @@ -28,12 +28,12 @@ Электрический ток будет равен тому, что мы получим, когда поделим разность потенциалов участка (величина напряжения) на сопротивляемость этого участка (сопротивление). Обозначаем: I - электрический ток; U - напряжение; R - сопротивление; -![current](../img/2_2.png) +![current](../assets/2_2.png) С помощью треугольника, работая с формулой закона Ома, легко написать формулу для любой входящей величины. -![current](../img/2_3.png) +![current](../assets/2_3.png) Нужно закрыть ту величину, которую необходимо определить. Если две оставшиеся величины находятся на одном уровне – значит надо их перемножить. @@ -73,12 +73,12 @@ I=2 A **I = I1+I2+I3+I4;** -![current](../img/2_4.png) +![current](../assets/2_4.png) Cумма токов, подходящих к узловой точке электрической цепи, равна сумме токов, уходящих от этого узла. -![current](../img/2_5.png) +![current](../assets/2_5.png) При параллельном соединении резисторов ток проходит по четырем направлениям, что уменьшает общее сопротивление или увеличивает общую проводимость цепи, которая равна сумме проводимостей ветвей. @@ -107,7 +107,7 @@ Cумма токов, подходящих к узловой точке элек Из этого равенства найдем сопротивление R, которым можно заменить два параллельно соединенных резистора: -![current](../img/2_6.png) +![current](../assets/2_6.png) Полученное выражение имеет большое практическое применение. Благодаря этому закону производятся расчёты электрических цепей. @@ -131,7 +131,7 @@ Cумма токов, подходящих к узловой точке элек **Е = Е1—Е2.** -![current](../img/2_7.png) +![current](../assets/2_7.png) Закон Джоуля-Ленца ------------------ @@ -151,11 +151,11 @@ Cумма токов, подходящих к узловой точке элек Если обозначить количество теплоты, создаваемое током, буквой Q (Дж), ток, протекающий по проводнику - I, сопротивление проводника - R и время, в течение которого ток протекал по проводнику - t, то закону Ленца-Джоуля можно придать следующее выражение: -![current](../img/2_8.png) +![current](../assets/2_8.png) Решим пример задачи: -![current](../img/2_9.png) +![current](../assets/2_9.png) ### Контрольные вопросы: 1) Что такое электродвижущая сила? diff --git a/docs/les4.md b/docs/les4.md index 6a7473fc..5163a94d 100644 --- a/docs/les4.md +++ b/docs/les4.md @@ -23,7 +23,7 @@ Для следующих операций понадобится уже специальный электронагревательный инструмент: паяльник, футорка или паяльная горелка. Паять в домашних условиях чаще всего приходится электропаяльником с медным луженым жалом. -![scheme](../img/4_1.png) +![scheme](../assets/4_1.png) Лудить необходимо следующим образом: @@ -54,7 +54,7 @@ * Простая последовательная скрутка, т. наз. прямая британская, или просто британка, применима для соединения токоведущих проводов гибких кабелей сечением до 1,4 кв. мм, не испытывающих регулярных больших механических нагрузок, напр. электрических удлинителей или времянок. -![scheme](../img/4_2.png) +![scheme](../assets/4_2.png) Электрические провода, испытывающие регулярные и/или постоянные механические нагрузки, должны быть обязательно многожильными. Скручивают их, как показано внизу на рис: концы разметливают, «метлы» вдвигают друг в друга и скручивают по-британски. Паяют легкоплавким припоем повышенной прочности, напр. ПОСК-50 (см. ниже) с активированным флюсом, не требующим удаления остатков, также см. ниже. Параллельные (тупиковые) скрутки проводов сечением свыше 0,7 кв. мм желательно паять погружением в расплавленный припой, см. далее. В противном случае придется греть или долго, или слишком мощным паяльником, отчего изоляция ползет, а флюс преждевременно выкипает. @@ -77,7 +77,7 @@ Второе – хлористый цинк в активированных флюс-пастах часто заменяют тераборнокислым натрием – бурой. Соляная кислота – высокотоксичное химически агрессивное летучее вещество; хлорид цинка также токсичен, а при нагреве сублимирует, т.е. улетучивается не плавясь. Бура безопасна, но при нагреве выделяет большое количество кристаллизационной воды, что немного ухудшает качество пайки. -![flux](../img/4_3.png) +![flux](../assets/4_3.png) Пайки от СКФ нужно обязательно промывать: в состав канифоли входит янтарная кислота, при длительном контакте разрушающая металл. Кроме того, случайно пролитый СКФ мгновенно растекается по большой площади и превращается в очень долго сохнущую чрезвычайно липкую гадость, пятна от которой ничем не сводятся ни с одежды, ни с мебели, ни с пола со стенами. В общем СКФ для пайки хороший флюс, но не для ротозеев с растяпами. Полноценный заменитель СКФ, но не такой противный при небрежном обращении – флюс ТАГС. Стальные детали более массивные, чем допустимо для пайки паяльной кислотой, и более прочно, паяют флюсом Ф38. Универсальным флюсом можно паять практически любые металлы в любых сочетаниях, в т.ч. алюминий, но прочность спая с ним не нормируется. К пайке алюминия мы еще вернемся. @@ -85,7 +85,7 @@ Любители мастерить также часто паяют сухим паяльником с бронзовым нелуженым жалом, т. наз. паяльным карандашом, поз. 1 на рис. Он хорош там, где недопустимо растекание припоя вне зоны пайки: в ювелирных изделиях, витражах, паяных предметах прикладного искусства. Иногда всухую паяют и микрочипы, монтируемые на поверхность, с шагом расположения выводов 1,25 или 0,625 мм, но это дело рискованное и для опытных специалистов: плохой тепловой контакт требует избыточной мощности паяльника и длительного нагрева, а обеспечить стабильность прогрева при ручной пайке невозможно. Для сухой пайки применяют гарпиус из ПОСК-40, 45 или 50 и флюс-пасты, не требующие удаления остатков. -![scheme](../img/4_4.png) +![scheme](../assets/4_4.png) ### Мелкая пайка @@ -102,7 +102,7 @@ эффект: спихнул нечаянно плату-«каракатицу» на пол – 1-2 или более дорожек отслоились. Не дожидаясь и первой перепайки. -![soldering](../img/4_5.png) +![soldering](../assets/4_5.png) Паечные наплывы на печатных платах должны быть округлыми гладкими высотой не более 0,7 диаметра монтажной площадки, см. справа на рис. Кончики выводов должны немного выступать из наплывов. Кстати, плата полностью самодельная. Есть способ в домашних условиях сделать печатный монтаж таким же точным и четким, как фабричный, да еще и вывести там надписи, какие хочется. Белые пятнышки – блики от лака при фотосъемке. Наплывы вогнутые и тем более сморщенные – тоже брак. Просто вогнутый наплыв значит, что припоя недостаточно, а морщинистый, кроме того, что в пайку проник воздух. Если собранное устройство не работает и есть подозрение на непропай, смотрите в первую очередь такие места. @@ -114,7 +114,7 @@ Ах да, подставки для паяльников. Классическая, слева на рис., пригодна для любых стержневых. Где на ней быть ванночкам для припоя и канифоли – дело ваше, какой-либо регламентации нет. Для маломощных паяльников с фартуком пригодны упрощенные подставки-скобы, в центре. -![soldering](../img/4_6.png) +![soldering](../assets/4_6.png) Паяльные станции комплектуются преимущественно пружинными или трубчатыми ложементами-гнездами для паяльников. В них вся горячая часть инструмента недоступна для прикосновения, но и промазать паяльником мимо них, сосредоточившись на пайке мелкой «россыпи», вероятнее. Но чего уж точно не надо делать, и что прямо запрещено ТБ – это подставку из подручных материалов, в которой паяльник лежит на ванночках для расходных материалов, справа на рис. diff --git a/docs/les7.md b/docs/les7.md index e982f388..4d81a373 100644 --- a/docs/les7.md +++ b/docs/les7.md @@ -8,12 +8,12 @@ Винт вращается на месте. При этом масса воздуха перемещается вертикально сверху вниз. Это один из режимов так называемого осевого обдува винта. На одной из лопастей выделены два небольших участка: один – «А» – ближе к оси вращения, другой – «Б» – у конца лопасти. В процессе вращения винта оба участка будут описывать концентрические окружности. Понятно, что длина окружности, описываемой элементом «Б», а значит, и его скорость относительно воздуха, будет больше, чем элемента «А». Иными словами, скорость элемента лопасти относительно воздуха зависит от того, на каком расстоянии он расположен от оси вращения. Чем это расстояние больше, тем большую скорость имеет элемент. Понятно, что на оси вращения скорость будет равна нулю, а на конце лопасти она будет наибольшей. -![rotation](../img/7_1.png) +![rotation](../assets/7_1.png) Поперечное сечение лопасти на этом участке имеет вид обтекаемого профиля. При обтекании этого профиля потоком воздуха под углом атаки возникают подъемная сила Y и сила сопротивления X, которые вычисляются по специальным формулам. Разбивая лопасть на множество мелких участков можно определить их подъемные силы и силы сопротивления, и, сложив вместе соответствующие силы по всем участкам, определить подъемную силу и силу лобового сопротивления одной лопасти. (С математической точки зрения такая операция именуется интегрированием по размаху лопасти). Подъемная сила (или сила тяги) всего винта получается умножением подъемной силы одной лопасти на число лопастей. Концевой эффект. Величина силы тяги винта определяется описанным выше методом с некоторой ошибкой, обусловленной несколькими причинами. Одна их них состоит в не учете явления так называемого концевого эффекта. Концевой эффект проявляется в стремлении воздуха к выравниванию давлений над лопастью и под лопастью путем перетекания через края лопасти. -![rotation](../img/7_2.png) +![rotation](../assets/7_2.png) В данном случае перетекание происходит как на внешнем, так и на внутреннем краях лопасти. А так как подъемная сила появляется вследствие разности давлений на верхней и нижней поверхностях лопасти, то любое выравнивание этих давлений вызывает потери подъемной силы. @@ -32,7 +32,7 @@ Для постройки квадрокоптера нужно две пары разнонаправленных винтов, для гексакоптера – три пары и т.д. -![rotation](../img/7_3.png) +![rotation](../assets/7_3.png) 5. **Направление вращения винтов** - классическое - против часовой стрелки 2 винта, по часовой стрелке другие 2 винта на квадрокоптерах. 6. **Качество изготовления пропеллеров** тоже важно. На практике это означает, что обязательно нужно балансировать пропеллеры, чтобы минимизировать вибрацию, которая разрушает механические части (постепенно), а также сводит с ума гироскопы, ухудшая полетные свойства мультикоптера. @@ -58,7 +58,7 @@ Для примера возьмем мотор X2204S 2300kv компании SunnySky. Заходим на сайт производителя и находим наш мотор. В описании имеется табличка, с помощью которой можно подобрать пропеллер (prop). -![rotation](../img/7_4.png) +![rotation](../assets/7_4.png) ### Контрольные вопросы diff --git a/docs/les8.md b/docs/les8.md index 30a90f54..cb374628 100644 --- a/docs/les8.md +++ b/docs/les8.md @@ -8,7 +8,7 @@ **Зако́н Ампе́ра** — закон взаимодействия электрических токов. Впервые был установлен Андре Мари Ампером в 1820 для постоянного тока. Из закона Ампера следует, что параллельные проводники с электрическими токами, текущими в одном направлении, притягиваются, а в противоположных — отталкиваются. -![low](../img/8_1.png) +![low](../assets/8_1.png) Закон Ома --------- @@ -26,13 +26,13 @@ Каждый электродвигатель обладает некоторыми отличительными свойствами, которые обуславливают его область применения, в которой он наиболее выгоден. Синхронные, асинхронные, постоянного тока, коллекторные, бесколлекторные, вентильно-индукторные, шаговые… -![engine](../img/8_2.png) +![engine](../assets/8_2.png) ### Двигатель постоянного тока (ДПТ) Именно этот тип двигателя стоит в большинстве старых игрушек. Батарейка, два проводка на контакты. Внутри такого двигателя на валу установлен контактный узел – коллектор, переключающий обмотки на роторе в зависимости от положения ротора. Постоянный ток, подводимый к двигателю, протекает то по одним, то по другим частям обмотки, создавая вращающий момент. -![engine](../img/8_3.png) +![engine](../assets/8_3.png) Двигатели постоянного тока бывают как очень маленького размера («вибра» в телефоне), так и довольно большого – обычно до мегаватта. Например, на фото ниже показан тяговый электродвигатель электровоза мощностью 810кВт и напряжением 1500В. @@ -40,7 +40,7 @@ Как ни странно, это самый распространенный в быту электродвигатель, название которого наименее известно. Почему так получилось? Его конструкция и характеристики такие же, как у двигателя постоянного тока, поэтому упоминание о нем в учебниках по приводу обычно помещается в самый конец главы. -![engine](../img/8_4.png) +![engine](../assets/8_4.png) Этот тип двигателей наиболее широко распространен в бытовой технике, где требуется регулировать частоту вращения: дрели, стиральные машины (не с «прямым приводом»), пылесосы и т.п. Почему именно он так популярен? Из-за простоты регулирования. Как и в ДПТ, его можно регулировать уровнем напряжения, что для сети переменного тока делается симистором (двунаправленным тиристором). Схема регулирования может быть так проста, что помещается, например, прямо в «курке» электроинструмента и не требует ни микроконтроллера, ни ШИМ, ни датчика положения ротора. @@ -52,14 +52,14 @@ Синхронных приводов бывает несколько подвидов – с магнитами (PMSM) и без (с обмоткой возбуждения и контактными кольцами), с синусоидальной ЭДС или с трапецеидальной (бесколлекторные двигатели постоянного тока, BLDC). Сюда же можно отнести некоторые шаговые двигатели. До эры силовой полупроводниковой электроники уделом синхронных машин было применение в качестве генераторов (почти все генераторы всех электростанций – синхронные машины), а также в качестве мощных приводов для какой-либо серьезной нагрузки в промышленности. -![engine](../img/8_5.png) +![engine](../assets/8_5.png) ### Сравнение коллекторного и бесколлекторного двигателя У радиоуправляемых моделей с электродвигателями бывают коллекторные и бесколлекторные двигатели. Краткое сравнение типов двигателей: коллекторные развивают меньшую скорость. Бесколлекторные двигатели способны развить большую скорость, а также более износостойкие. -![engine](../img/8_6.png) +![engine](../assets/8_6.png) ### Коллекторный двигатель diff --git a/docs/les9.md b/docs/les9.md index 89e61dd9..cfddf1d1 100644 --- a/docs/les9.md +++ b/docs/les9.md @@ -25,13 +25,13 @@ Трехфазные бесколлекторные двигатели приобрели наибольшее распространение. Но они могут быть и одно, двух, трех и более фазными. Чем больше фаз, тем более плавное вращение магнитного поля, но и сложнее система управления двигателем. 3-х фазная система наиболее оптимальна по соотношению эффективность/сложность, поэтому и получила столь широкое распространение. Далее будет рассматриваться только трехфазная схема, как наиболее распространенная. Фактически фазы – это обмотки двигателя. Поэтому если сказать “трехобмоточный”, думаю, это тоже будет правильно. Три обмотки соединяются по схеме “звезда” или “треугольник”. Трехфазный бесколлекторный двигатель имеет три провода – выводы обмоток. -![winding](../img/9_1.png) +![winding](../assets/9_1.png) Двигатели с датчиками имеют дополнительных 5 проводов (2-питание датчиков положения, и 3 сигналы от датчиков). В трехфазной системе в каждый момент времени напряжение подается на две из трех обмоток. Таким образом, есть 6 вариантов подачи постоянного напряжения на обмотки двигателя, как показано на рисунке ниже. -![engine](../img/9_2.png) +![engine](../assets/9_2.png) Это позволяет создать вращающееся магнитное поле, которое будет проворачиваться “шагами” на 60 градусов при каждом переключении. diff --git a/docs/modes.md b/docs/modes.md index 4fb8588b..7219b89e 100644 --- a/docs/modes.md +++ b/docs/modes.md @@ -45,4 +45,4 @@ PX4 * [SET_POSITION_TARGET_LOCAL_NED](https://pixhawk.ethz.ch/mavlink/#SET_POSITION_TARGET_LOCAL_NED) * [SET_ATTITUDE_TARGET](https://pixhawk.ethz.ch/mavlink/#SET_ATTITUDE_TARGET) -См.: [автономные полеты коптера в режиме OFFBOARD](simple_offboard.md). \ No newline at end of file +См.: [автономные полеты коптера в режиме OFFBOARD](simple_offboard.md). diff --git a/notes/powerConnection.md b/docs/powerConnection.md similarity index 97% rename from notes/powerConnection.md rename to docs/powerConnection.md index 814407db..2e41c467 100644 --- a/notes/powerConnection.md +++ b/docs/powerConnection.md @@ -11,7 +11,7 @@ Говоря простым языком, силовые цепи - это электрические цепи, которые предназначены для передачи большого количества энергии (тока), для обеспечения работоспособности всех систем. В нашем случае в качестве силовой цепи будут выступать провода, соединяющие аккумулятор с платой распределения питания, а также красные и черные провода идущие на регуляторы оборотов. -![Схема силовой цепи](../img/powerConnect.jpg) +![Схема силовой цепи](../assets/powerConnect.jpg) Вся энергия аккумулятора (АКБ) будет распределяться между регуляторами оборотов моторов. Чтобы моторы смогли поднять в воздух коптер, им необходимо много энергии. В качестве энергии выступает ток, который приходит с АКБ. Т.к. энергии нужно много, значит по проводам будет идти большой ток. Чтобы провода смогли выдержать такую нагрузку, необходимы провода большего диаметра. Для наших задач подойдут провода 18AWG, 16AWG, 14AWG. diff --git a/docs/radioerrors.md b/docs/radioerrors.md index 0d1519e3..9f9bbc74 100644 --- a/docs/radioerrors.md +++ b/docs/radioerrors.md @@ -12,7 +12,7 @@ 2. Переключатели A, B, C, D (2) в положение “От Себя”. 3. Правый стик (3) в центре. -![Заблокированный пульт](../img/lockradio.jpg) +![Заблокированный пульт](../assets/lockradio.jpg) Нет связи с приемником @@ -21,11 +21,11 @@ 1. Соединение с приемником отсутствует -![Нет соединения с приемником](../img/connectionLost.jpg) +![Нет соединения с приемником](../assets/connectionLost.jpg) 2. Соединение с приемником установлено -![Есть соединения с приемником](../img/connectionOK.jpg) +![Есть соединения с приемником](../assets/connectionOK.jpg) Если соединение отсутствует, то 1. Проверьте, что приемник включен (моргает красный светодиод) @@ -37,7 +37,7 @@ -------------- Если нет связи с полетным контроллером, то на экране монитора компьютера в окне Channel Monitor не будут отображаться изменения положения слайдеров при перемещении стиками пульта. -![Нет связи с полетным контроллером](../img/notmoveslider.jpg) +![Нет связи с полетным контроллером](../assets/notmoveslider.jpg) 1. Зайдите в МЕНЮ (удерживаем нажатой кнопку “ОК”) 2. Выберите меню “System setup” (Кнопки Up/Down - для навигации, кнопка “ОК” - подтверждение выбора diff --git a/docs/radioerrors1.md b/docs/radioerrors1.md index 0d1519e3..9f9bbc74 100644 --- a/docs/radioerrors1.md +++ b/docs/radioerrors1.md @@ -12,7 +12,7 @@ 2. Переключатели A, B, C, D (2) в положение “От Себя”. 3. Правый стик (3) в центре. -![Заблокированный пульт](../img/lockradio.jpg) +![Заблокированный пульт](../assets/lockradio.jpg) Нет связи с приемником @@ -21,11 +21,11 @@ 1. Соединение с приемником отсутствует -![Нет соединения с приемником](../img/connectionLost.jpg) +![Нет соединения с приемником](../assets/connectionLost.jpg) 2. Соединение с приемником установлено -![Есть соединения с приемником](../img/connectionOK.jpg) +![Есть соединения с приемником](../assets/connectionOK.jpg) Если соединение отсутствует, то 1. Проверьте, что приемник включен (моргает красный светодиод) @@ -37,7 +37,7 @@ -------------- Если нет связи с полетным контроллером, то на экране монитора компьютера в окне Channel Monitor не будут отображаться изменения положения слайдеров при перемещении стиками пульта. -![Нет связи с полетным контроллером](../img/notmoveslider.jpg) +![Нет связи с полетным контроллером](../assets/notmoveslider.jpg) 1. Зайдите в МЕНЮ (удерживаем нажатой кнопку “ОК”) 2. Выберите меню “System setup” (Кнопки Up/Down - для навигации, кнопка “ОК” - подтверждение выбора diff --git a/docs/safety.md b/docs/safety.md index 1250269e..9d6b5e2b 100644 --- a/docs/safety.md +++ b/docs/safety.md @@ -30,7 +30,7 @@ Паяльник можно держать только за ручку. Если кто-то утверждает обратное — не верьте, вас вводят в заблуждение :) -![Паяльник состав](../img/solderConsist.jpg) +![Паяльник состав](../assets/solderConsist.jpg) ![Подробнее...->](../docs/tb.md) diff --git a/docs/setup.md b/docs/setup.md index 2fac8777..911f22c5 100644 --- a/docs/setup.md +++ b/docs/setup.md @@ -3,7 +3,7 @@ ### 1. Состав пульта FLYSKY i6 --------------------- -![Состав пульта](../img/consistofTransmitter.jpg) +![Состав пульта](../assets/consistofTransmitter.jpg) ### 2. Установка QGroundCongtrol @@ -20,7 +20,7 @@ ### 4. Обновление прошивки Pixhawk -![Обновление прошивки](../img/firmwarePX4.jpg) +![Обновление прошивки](../assets/firmwarePX4.jpg) 1. Заходим в Vehicle Setup. 2. Выбираем Firmware. @@ -33,7 +33,7 @@ ### 5. Настройка Pixhawk -![Главное окно](../img/mainWindow.jpg) +![Главное окно](../assets/mainWindow.jpg) 1. Системы, нуждающиеся в настройке: Airframe, Radio, Sensors, Flight Mode 2. Текущая прошивка контроллера. @@ -42,14 +42,14 @@ ### 6. Выбор рамы -![ Выбор рамы](../img/airframeSetup.jpg) +![ Выбор рамы](../assets/airframeSetup.jpg) 1. Заходим в меню Airframe. 2. Выбираем тип рамы Quadrotor X. 3. Выбираем тип навесных элементов Generic Quadrotor X config 4. Перемещаемся в начало списка и сохраняем настройки Apply and Restart -![Внимание!](../img/attentionSave.jpg) +![Внимание!](../assets/attentionSave.jpg) 5. Повторно подтверждаем Apply 6. Ждем, пока Pixhawk выполнит сохранение и перезагрузится @@ -65,7 +65,7 @@ 3. Далее необходимо убедиться, что связь с приемником установлена: * На ЖК Экране пульта высвечивается индикация - ![ Индикация пульта](../img/unblockView.jpg) + ![ Индикация пульта](../assets/unblockView.jpg) * Светодиод на приемнике горит непрерывно красным @@ -78,7 +78,7 @@ На канал CH5 назначаем 3-х позиционный переключатель SwC - будем изменять полетные режимы На канал CH6 назначаем 2-х позиционный переключатель SwA - аварийная остановка моторов -![Используемые переключатели](../img/chooseSwitch.jpg) +![Используемые переключатели](../assets/chooseSwitch.jpg) #### Чтобы переназначить переключатели, выполните следующие действия: @@ -90,7 +90,7 @@ * Channel 6 - SwA 5. Сохраните изменения (удерживаем нажатой кнопку “CANCEL”) -![Выбор каналов](../img/setupSwitch.jpg) +![Выбор каналов](../assets/setupSwitch.jpg) ### 9. Калибровка пульта @@ -102,7 +102,7 @@ * Чтобы установить один из триммеров в 0, необходимо на пульте переместить указатель в центр до длительного звукового сигнала (писка) 3. Жмем ОК -![Калибровка пульта Начало](../img/calibrateViewStart.jpg) +![Калибровка пульта Начало](../assets/calibrateViewStart.jpg) 4. Переводим Левый стик (газ) (throttle) в минимум и кликаем Next 5. Калибровка каналов управления (throttle, yaw, pitch, roll). @@ -116,7 +116,7 @@ 8. Калибровка пульта завершена! -![Калибровка пульта](../img/calibrateView.jpg) +![Калибровка пульта](../assets/calibrateView.jpg) ### 10. Калибровка акселерометра @@ -125,12 +125,12 @@ 2. Поскольку направление Pixhawk совпадает с носом БПЛА, то выбираем Autopilot Orientation: ROTATION_NONE Кликаем OK - ![Калибровка акселерометра](../img/calibrateaxcelstart.jpg) + ![Калибровка акселерометра](../assets/calibrateaxcelstart.jpg) 3. Начинаем калибровку: Последовательно располагаем БПЛА как на картинках, когда Pixhawk захватит положение, вокруг картинки появится желтая рамка - удерживаем в этом положении БПЛА до переключения в зеленую рамку - ![Калибровка акселерометра процесс](../img/calibrateaxcel.jpg) + ![Калибровка акселерометра процесс](../assets/calibrateaxcel.jpg) ### 11. Калибровка компаса @@ -142,7 +142,7 @@ * Выбираем ориентацию БПЛА как на картинке и ждем, когда Pixhawk определит положение БПЛА, появится желтая рамка и надпись "Rotate" * Вращаем БПЛА как на картинке до появления зеленой рамки - Pixhawk откалибровал компас по данной оси. -![Калибровка компаса](../img/calibratecompass.jpg) +![Калибровка компаса](../assets/calibratecompass.jpg) ### 12. Калибровка гироскопа @@ -151,7 +151,7 @@ 2. Устанавливаем БПЛА на ровную поверхность и кликаем OK Ждем окончания калибровки. -![Калибровка компаса](../img/calibrategyro.jpg) +![Калибровка компаса](../assets/calibrategyro.jpg) ##### Во время калибровки БПЛА не должен менять своего положения, шататься и т.д. @@ -170,7 +170,7 @@ 6. Аварийное отключение моторов ставим на переключатель SwA (Channel 6). Kill switch - Channel 6 -![Полетные режимы](../img/flightModes.jpg) +![Полетные режимы](../assets/flightModes.jpg) ### 14. Отключение Safety Switch @@ -183,7 +183,7 @@ 3. Сохраняем значения, кликая по кнопке Save 4. Повторяем установку максимальных значений для всех параметров, кроме CBRK_RATE_CTRL и CBRK_VELPOSERR -![Отключение кнопки безопасности](../img/turnoffSafetyswitch.jpg) +![Отключение кнопки безопасности](../assets/turnoffSafetyswitch.jpg) ### 15. Калибровка регуляторов @@ -199,7 +199,7 @@ 4. Проверяем, что АКБ не подключена и пропеллеры сняты Нажимаем Calibrate -![Калибровка регуляторов](../img/calibrateESC.jpg) +![Калибровка регуляторов](../assets/calibrateESC.jpg) ### 16. Настройка PID - регулятора @@ -218,7 +218,7 @@ * MC_ROLLRATE_I: 0.050 * MC_ROLLRATE_D: 0.0025 -![Коэффициенты ПИД-регулятора](../img/calibratePIDparams.jpg) +![Коэффициенты ПИД-регулятора](../assets/calibratePIDparams.jpg) ## ИНСТРУКЦИЯ ПО БЕЗОПАСНОСТИ diff --git a/docs/simple_offboard.md b/docs/simple_offboard.md index be37dde8..2695b192 100644 --- a/docs/simple_offboard.md +++ b/docs/simple_offboard.md @@ -111,25 +111,25 @@ rosservice call /get_telemetry "{frame_id: ''}" ```python # плавно взлететь на высоту 1.5 м со скоростью взлета 0.5 м/с -navigate(0, 0, 1.5, speed=0.5, frame_id='fcu_horiz', auto_arm=True) +navigate(x=0, y=0, z=1.5, speed=0.5, frame_id='fcu_horiz', auto_arm=True) ``` ```python # прилететь по прямой в точку 5:0 (высота 2) # в локальной системе координат со скоростью 0.8 м/с -navigate(5, 0, 3, speed=0.8) +navigate(x=5, y=0, z=3, speed=0.8) ``` ```python # пролететь вправо относительно коптера на 3 м -navigate(0, -1, 0, speed=1, frame_id='fcu_horiz') +navigate(x=0, y=-1, z=0, speed=1, frame_id='fcu_horiz') ``` ```python # прилететь в точку 3:2 (высота 2) в системе координат маркерного поля # со скоростью 1 м/с -navigate(3, 2, 2, speed=1, frame_id='aruco_map', update_frame=True) +navigate(x=3, y=2, z=2, speed=1, frame_id='aruco_map', update_frame=True) ``` Пример взлета на коптере на 2 метра из командной строки: @@ -253,4 +253,4 @@ set_mode = rospy.ServiceProxy('/mavros/set_mode', SetMode) # объявляем set_mode(base_mode=0, custom_mode='AUTO.LAND') # включаем режим посадки ``` -Для полетов в поле ArUco-макеров см. [навигация по ArUco](/docs/aruco.md). \ No newline at end of file +Для полетов в поле ArUco-макеров см. [навигация по ArUco](/docs/aruco.md). diff --git a/docs/tb.md b/docs/tb.md index 16d9e3c5..ea83326e 100644 --- a/docs/tb.md +++ b/docs/tb.md @@ -7,16 +7,16 @@ 1. Привести в порядок рабочее место, ничего не должно мешать процессу. Рабочее место должно быть хорошо освещено. 2. Паяльник, находящийся в рабочем состоянии, установить в зоне действия местной вытяжной вентиляции, в специальную подставку. -![stand](/img/stand.jpg) +![stand](/assets/stand.jpg) Во время пайки: 1. Паяльник следует держать только за ручку, так как жало имеет высокую температуру. -![keep](/img/keep.png) +![keep](/assets/keep.png) 2. Для перемещения изделий применять специальные инструменты (пинцеты, клещи или другие инструменты), обеспечивающие безопасность при пайке. 3. Во избежание ожогов расплавленным припоем при распайке не выдергивать резко с большим усилием паяемые провода. 4. При пайке мелких и подвижных изделий пользоваться специальным держателем. -![helphand](/img/helphand.jpg) +![helphand](/assets/helphand.jpg) 5. Паяльник переносить за корпус, а не за провод или рабочую часть. При перерывах в работе паяльник отключать от электросети. **При обнаружении неисправной работы паяльника или возникновении возгорания отключить его от питающей электросети.** diff --git a/notes/testConnection.md b/docs/testConnection.md similarity index 97% rename from notes/testConnection.md rename to docs/testConnection.md index a041a8f0..5e131548 100644 --- a/notes/testConnection.md +++ b/docs/testConnection.md @@ -12,7 +12,7 @@ * Проверить работу мультиметра путем замыкания щупов между собой. При корректной работе прибор издаст характерный звук. * Попарно красный щуп прикладывается к “+ ”контакту, черный к “-” / ”GND”. Если в цепи есть короткое замыкание, издается звук. -1[Режим прозвонки](../img/startPDBtest.jpg) +1[Режим прозвонки](../assets/startPDBtest.jpg) 1. Прозвонить следующие цепи на НЕЗАМКНУТОСТЬ (отсутствие звукового сигнала мультиметра): diff --git a/docs/wifi.md b/docs/wifi.md index fa68b1e4..2cd8f987 100644 --- a/docs/wifi.md +++ b/docs/wifi.md @@ -5,4 +5,4 @@ Пароль: `cleverwifi`. -TODO: иллюстрация. \ No newline at end of file +TODO: иллюстрация. diff --git a/notes/zap.md b/docs/zap.md similarity index 87% rename from notes/zap.md rename to docs/zap.md index 949920ba..61df30a9 100644 --- a/notes/zap.md +++ b/docs/zap.md @@ -10,7 +10,7 @@ 1. Нанести флюс на контактную площадку 2. Покрыть припоем контактную площадку -![Лужение площадок](../img/zapPDBtest.jpg) +![Лужение площадок](../assets/zapPDBtest.jpg) ## Лужение проводов @@ -22,4 +22,4 @@ 3. Нанести флюс на скрученные оголенные провода 4. Покрыть слоем припоя. -![Лужение проводов](../img/zap.jpg) +![Лужение проводов](../assets/zap.jpg) diff --git a/gpsmd.md b/gpsmd.md deleted file mode 100644 index 85e8c272..00000000 --- a/gpsmd.md +++ /dev/null @@ -1,3 +0,0 @@ -Установка GPS -=== - diff --git a/image/Jenkinsfile b/image/Jenkinsfile new file mode 100644 index 00000000..5ad1f845 --- /dev/null +++ b/image/Jenkinsfile @@ -0,0 +1,62 @@ +pipeline { + agent any + stages { + stage('Get image') { + agent any + environment { + RPI_DONWLOAD_URL = 'https://downloads.raspberrypi.org/raspbian_lite_latest' + RPI_ZIP_NAME = 'raspbian_lite_latest.zip' + RPI_IMAGE_NAME = '2017-11-29-raspbian-stretch-lite.img' + } + steps { + sh '$WORKSPACE/image/image-config.sh get_image $BUID_DIRECTORY $RPI_ZIP_NAME $RPI_DONWLOAD_URL $RPI_IMAGE_NAME $IMAGE_NAME' + } + } + stage('Resize FS') { + environment { + SIZE = '7G' + } + steps { + sh '$WORKSPACE/image/image-config.sh resize_fs $SIZE $BUID_DIRECTORY $IMAGE_NAME $DEV_ROOTFS' + } + } + stage('Configure interfaces') { + environment { + EXECUTE_FILE = 'iface.sh' + } + steps { + sh '$WORKSPACE/image/image-config.sh execute $BUID_DIRECTORY/$IMAGE_NAME $PREFIX_PATH $DEV_ROOTFS $DEV_BOOT $WORKSPACE/image/$EXECUTE_FILE' + } + } + stage('Install Apps') { + environment { + EXECUTE_FILE = 'apps.sh' + } + steps { + sh '$WORKSPACE/image/image-config.sh execute $BUID_DIRECTORY/$IMAGE_NAME $PREFIX_PATH $DEV_ROOTFS $DEV_BOOT $WORKSPACE/image/$EXECUTE_FILE' + } + } + stage('Install ROS') { + environment { + EXECUTE_FILE = 'ros.sh' + } + steps { + sh '$WORKSPACE/image/image-config.sh execute $BUID_DIRECTORY/$IMAGE_NAME $PREFIX_PATH $DEV_ROOTFS $DEV_BOOT $WORKSPACE/image/$EXECUTE_FILE' + } + } + stage('Publish image') { + environment { + CONFIG_FILE = 'coex-ci.conf' + } + steps { + sh '$WORKSPACE/image/image-config.sh publish_image $BUID_DIRECTORY $IMAGE_NAME $WORKSPACE $CONFIG_FILE $RELEASE_ID $RELEASE_BODY' + } + } + } + environment { + BUID_DIRECTORY = '/home/pi/clever_builder/temp' + PREFIX_PATH = '/mnt' + DEV_BOOT = '/dev/disk/by-uuid/CDD4-B453' + DEV_ROOTFS = '/dev/disk/by-uuid/72bfc10d-73ec-4d9e-a54a-1cc507ee7ed2' + } +} diff --git a/image/apps.sh b/image/apps.sh new file mode 100755 index 00000000..6a6139e6 --- /dev/null +++ b/image/apps.sh @@ -0,0 +1,100 @@ +#!/bin/bash + +################################################################################################################################## +# Установка необходимых программ +################################################################################################################################## + + +echo -e "\033[0;31m\033[1m$(date) | #1 apt update && apt upgrade\033[0m\033[0m" + +# install bootstrap tools +apt-get update +# && apt upgrade -y + + + +echo -e "\033[0;31m\033[1m$(date) | #2 Install programs\033[0m\033[0m" + +apt-get install --no-install-recommends -y \ + ipython \ + screen \ + byobu \ + nmap \ + lsof \ + python-pip \ + git \ + isc-dhcp-server \ + tmux \ + vim + + + + + +echo -e "\033[0;31m\033[1m$(date) | #3 Write to /etc/wpa_supplicant/wpa_supplicant.conf\033[0m\033[0m" + +echo " +network={ + ssid=\"CLEVER\" + mode=2 + key_mgmt=WPA-PSK + psk=\"cleverwifi\" + frequency=2437 +}" >> /etc/wpa_supplicant/wpa_supplicant.conf + + + + + +echo -e "\033[0;31m\033[1m$(date) | #4 Write STATIC to /etc/dhcpcd.conf\033[0m\033[0m" + +echo " +interface wlan0 +static ip_address=192.168.11.1/24" >> /etc/dhcpcd.conf + + + + +echo -e "\033[0;31m\033[1m$(date) | #5 Write iface to /etc/default/isc-dhcp-server\033[0m\033[0m" + +# https://www.shellhacks.com/ru/sed-find-replace-string-in-file/ +sed -i 's/INTERFACESv4=\"\"/INTERFACESv4=\"wlan0\"/' /etc/default/isc-dhcp-server + + +echo -e "\033[0;31m\033[1m$(date) | #6 Write dhcp declaration subnet to /etc/dhcp/dhcpd.conf\033[0m\033[0m" + + +echo "subnet 192.168.11.0 netmask 255.255.255.0 { + range 192.168.11.11 192.168.11.254; + #option domain-name-servers 8.8.8.8; + #option domain-name "rpi.local"; + option routers 192.168.11.1; + option broadcast-address 192.168.11.255; + default-lease-time 600; + max-lease-time 7200; +}" >> /etc/dhcp/dhcpd.conf + + + +echo -e "\033[0;31m\033[1m$(date) | #7 Write start script for dhcpd to /etc/network/if-up.d/isc-dhcp-server\033[0m\033[0m" + +echo "#!/bin/sh +if [ \"\$IFACE\" = \"--all\" ]; +then sleep 10 && systemctl start isc-dhcp-server.service & +fi +" > /etc/network/if-up.d/isc-dhcp-server \ + && chmod +x /etc/network/if-up.d/isc-dhcp-server + + + +echo -e "\033[0;31m\033[1m$(date) | #8 Write magic script for rename SSID to /etc/rc.local\033[0m\033[0m" + +RENAME_SSID="sudo sed -i.OLD \"s/CLEVER/CLEVER-\$(head -c 100 /dev/urandom | xxd -ps -c 100 | sed -e 's/[^0-9]//g' | cut -c 1-4)/g\" /etc/wpa_supplicant/wpa_supplicant.conf && sudo sed -i '/sudo sed/d' /etc/rc.local && sudo reboot" + + +sed -i "19a$RENAME_SSID" /etc/rc.local + + + +echo -e "\033[0;31m\033[1m$(date) | #9 End of install programs\033[0m\033[0m" + diff --git a/image/git_release.py b/image/git_release.py new file mode 100755 index 00000000..e434aba5 --- /dev/null +++ b/image/git_release.py @@ -0,0 +1,38 @@ +#!/usr/bin/env python + +# +# Simple github release body-editor +# Smirnov Artem @urpylka +# +# Use: +# python git_release.py CONFIG_FILE RELEASE_ID RELEASE_BODY +# + + +from ConfigParser import SafeConfigParser +import requests, sys, urllib + +def json_wrapper(image_name, image_link, image_size, old_text): + old_text = urllib.unquote_plus(old_text) + buffer = "### Download\n* [" + image_name + ".zip](" + image_link + ") (" + image_size + ")\n\n" + old_text + js = {} + js["body"] = buffer + return js + + +def main(): + + cfgParser = SafeConfigParser() + cfgParser.read(sys.argv[1]) + + js = json_wrapper(sys.argv[4], sys.argv[5], sys.argv[6], sys.argv[3]) + + r = requests.patch(cfgParser.get('github','url') + sys.argv[2], json=js, auth=(cfgParser.get('github','login'), cfgParser.get('github','password'))) + + if r.status_code == 200: + print("Message has been successfully added!") + else: + return 1 + +if __name__ == '__main__': + main() diff --git a/image/iface.sh b/image/iface.sh new file mode 100755 index 00000000..f4b1fb3b --- /dev/null +++ b/image/iface.sh @@ -0,0 +1,138 @@ +#!/bin/bash + +################################################################################################################################## +# Настройка интерфейсов +################################################################################################################################## + +# вот так все в принципе должно включиться +# /usr/bin/raspi-config nonint do_i2c 0 +# /usr/bin/raspi-config nonint do_spi 0 +# /usr/bin/raspi-config nonint do_camera 0 +# /usr/bin/raspi-config nonint do_rgpio 0 +# /usr/bin/raspi-config nonint do_ssh 0 + +# по идеи эти настройки должны проводиться до по другому как сделано в prepare_image.sh + +set_config_var() { + lua - "$1" "$2" "$3" < "$3.bak" +local key=assert(arg[1]) +local value=assert(arg[2]) +local fn=assert(arg[3]) +local file=assert(io.open(fn)) +local made_change=false +for line in file:lines() do + if line:match("^#?%s*"..key.."=.*$") then + line=key.."="..value + made_change=true + end + print(line) +end + +if not made_change then + print(key.."="..value) +end +EOF + mv "$3.bak" "$3" +} + +BLACKLIST=/etc/modprobe.d/raspi-blacklist.conf +CONFIG=/boot/config.txt + +# 2. Изменить необходимые настройки + +# 2.1. Включить sshd +echo -e "\033[0;31m\033[1m$(date) | #11 Turn on sshd\033[0m\033[0m" +touch /boot/ssh + +# 2.2. Включить GPIO +# Включено по умолчанию + +# 2.3. Включить I2C +echo -e "\033[0;31m\033[1m$(date) | #12 Turn on I2C\033[0m\033[0m" + +set_config_var dtparam=i2c_arm on $CONFIG && + if ! [ -e $BLACKLIST ]; then + touch $BLACKLIST + fi + sed $BLACKLIST -i -e "s/^\(blacklist[[:space:]]*i2c[-_]bcm2708\)/#\1/" + sed /etc/modules -i -e "s/^#[[:space:]]*\(i2c[-_]dev\)/\1/" + if ! grep -q "^i2c[-_]dev" /etc/modules; then + printf "i2c-dev\n" >> /etc/modules + fi + +# 2.4. Включить SPI +echo -e "\033[0;31m\033[1m$(date) | #13 Turn on SPI\033[0m\033[0m" + +set_config_var dtparam=spi on $CONFIG && + if ! [ -e $BLACKLIST ]; then + touch $BLACKLIST + fi + sed $BLACKLIST -i -e "s/^\(blacklist[[:space:]]*spi[-_]bcm2708\)/#\1/" + +# 2.5. Включить raspicam +echo -e "\033[0;31m\033[1m$(date) | #14 Turn on raspicam\033[0m\033[0m" + +get_config_var() { + lua - "$1" "$2" <> /etc/modules +if ! grep -q "^bcm2835-v4l2" /etc/modules; then + printf "bcm2835-v4l2\n" >> /etc/modules +fi + +# 2.6. Настроить AP wifi +# 2.7. Настроить сеть на wlan +# 2.8. Настроить DHCPd на wlan + + +echo -e "\033[0;31m\033[1m$(date) | #15 End of configuring interfaces\033[0m\033[0m" diff --git a/image/image-config.sh b/image/image-config.sh new file mode 100755 index 00000000..bc7df212 --- /dev/null +++ b/image/image-config.sh @@ -0,0 +1,491 @@ +#!/bin/bash +#!/bin/sh + +set -e + +# +# Script for image configure +# @smirart Smirnov Artem +# + + +# PREFIX_PATH=/mnt +# IMAGE=/home/pi/2017-11-29-raspbian-stretch-lite.img +# +# # blkid +# UUID_BOOT=CDD4-B453 +# UUID_ROOTFS=72bfc10d-73ec-4d9e-a54a-1cc507ee7ed2 +# +# # /dev/disk/by-label/boot +# DEV_BOOT=/dev/disk/by-uuid/$UUID_BOOT +# # /dev/disk/by-label$2 +# DEV_ROOTFS=/dev/disk/by-uuid/$UUID_ROOTFS + + +get_image() { + +# STATIC +# TEMPLATE: get_image $JENKINS_HOME $RPI_ZIP_NAME $RPI_DONWLOAD_URL $RPI_IMAGE_NAME $IMAGE_NAME + + echo 'Download RaspbianOS' + echo "$(date) | 1. Download raspbian lite" + if [ ! -e "$1/$2" ]; + then wget -nv -O $1/$2 $3 + fi + echo "$(date) | Downloading complete" + echo 'Unzip image' + echo "$(date) | 2. Unzip raspbian lite" + if [ ! -e "$1/$4" ]; + then unzip -uo $1/$2 -d $1 + fi + echo "$(date) | Unziping complete" + echo 'Duplicate image' + cp -f $1/$4 $1/$5 +} + +resize_fs() { + + # STATIC + # TEMPLATE: resize_fs $SIZE $JENKINS_HOME $IMAGE_NAME $DEV_ROOTFS + + set +e + + # https://ru.wikipedia.org/wiki/%D0%A0%D0%B0%D0%B7%D1%80%D0%B5%D0%B6%D1%91%D0%BD%D0%BD%D1%8B%D0%B9_%D1%84%D0%B0%D0%B9%D0%BB + + # https://raspberrypi.stackexchange.com/questions/13137/how-can-i-mount-a-raspberry-pi-linux-distro-image + # fdisk -l 2017-11-29-raspbian-stretch-lite.img + # https://www.stableit.ru/2011/05/losetup.html + # -f : losetup сам выбрал loop (минуя занятые) + # -P : losetup монтирует разделы в образе как отдельные подразделы, + # например /dev/loop0p1 и /dev/loop0p2 + # --show : печатает имя устройства, например /dev/loop4 + + # http://karelzak.blogspot.ru/2015/05/resize-by-sfdisk.html + # ", +" : расширяет раздел до размеров образа + # -N 2 : выбирает раздел 2 для работы + + echo -e "\033[0;31m\033[1mTruncate image\033[0m\033[0m" \ + && truncate -s$1 $2/$3 \ + && echo "Mount loop-image: $2/$3" \ + && local DEV_IMAGE=$(losetup -Pf $2/$3 --show) \ + && sleep 0.5 \ + && echo -e "\033[0;31m\033[1mMount loop-image: $1\033[0m\033[0m" \ + && echo ", +" | sfdisk -N 2 $DEV_IMAGE \ + && sleep 0.5 \ + && echo -e "\033[0;31m\033[1mCheck & repair filesystem after expand partition\033[0m\033[0m" \ + && e2fsck -fvy $4 \ + && echo -e "\033[0;31m\033[1mExpand filesystem\033[0m\033[0m" \ + && resize2fs $4 \ + && echo -e "\033[0;31m\033[1mUmount loop-image\033[0m\033[0m" \ + && losetup -d $DEV_IMAGE + + set -e +} + +publish_image() { + +# STATIC +# TEMPLATE: publish_image $JENKINS_HOME $IMAGE_NAME $WORKSPACE $CONFIG_FILE $RELEASE_ID $RELEASE_BODY + +# https://developer.github.com/v3/repos/releases/ +#RELEASE_BODY="### Changelog\n* Add /boot/cmdline.txt net.ifnames=0 https://www.freedesktop.org/wiki/Software/systemd/PredictableNetworkInterfaceNames/\n* Updated cophelper\n* Installed copstat" + + echo 'Zip image' \ + && zip $1/$2.zip $1/$2 \ + && echo 'Upload image' \ + && local IMAGE_LINK=$($3/image/yadisk.py $1/$4 $1/$2.zip) \ + && local IMAGE_SIZE=$(du -sh $1/$2.zip | awk '{ print $1 }') \ + && $3/image/git_release.py $1/$4 $5 $6 $2 $IMAGE_LINK $IMAGE_SIZE +} + +publish_image2() { + +# STATIC +# TEMPLATE: publish_image $JENKINS_HOME $IMAGE_NAME $WORKSPACE $CONFIG_FILE $RELEASE_ID $RELEASE_BODY + +# https://developer.github.com/v3/repos/releases/ +#RELEASE_BODY="### Changelog\n* Add /boot/cmdline.txt net.ifnames=0 https://www.freedesktop.org/wiki/Software/systemd/PredictableNetworkInterfaceNames/\n* Updated cophelper\n* Installed copstat" + + echo 'Zip image' \ + && zip $1/$2.zip $1/$2 \ + && echo 'Upload image' \ + && local IMAGE_LINK=$($3/image/yadisk.py $1/$4 $1/$2.zip) \ + && local IMAGE_SIZE=$(du -sh $1/$2.zip | awk '{ print $1 }') \ + && local NEW_RELEASE_BODY="### Download\n* [$2.zip]($IMAGE_LINK) ($IMAGE_SIZE)\n\n$6" \ + && local DATA="{ \"body\":\"$NEW_RELEASE_BODY\" }" \ + && curl -d "$(echo $DATA)" -u "LOGIN:PASS" --request PATCH https://api.github.com/repos/ONWER/REPO/releases/$5 +} + +burn_image() { + +# STATIC +# TEMPLATE: burn_image $IMAGE_PATH $MICROSD_DEV + + echo -e "\033[0;31m\033[1mBurn image\033[0m\033[0m" \ + && dd if=$1 of=$2 \ + && echo -e "\033[0;31m\033[1mBurn image finished!\033[0m\033[0m" +} + +burn_and_reboot() { + +# STATIC +# TEMPLATE: burn_and_reboot $IMAGE_PATH $MICROSD_DEV + + burn_image $1 $2 \ + && reboot +} + +mount_system() { + + # STATIC + # TEMPLATE: mount_system $IMAGE $PREFIX_PATH $DEV_ROOTFS $DEV_BOOT + + # https://www.stableit.ru/2011/05/losetup.html + # -f : losetup выбирает незанятое имя устройства, например /dev/loop2 + # -P : losetup монтирует разделы в образе как отдельные подразделы, + # например /dev/loop0p1 и /dev/loop0p2 + # --show : печатает имя устройства, например /dev/loop4 + + echo -e "\033[0;31m\033[1mMount loop-image: $1\033[0m\033[0m" + DEV_IMAGE=$(losetup -Pf $1 --show) + sleep 0.5 + + echo -e "\033[0;31m\033[1mMount dirs $2 & $2/boot\033[0m\033[0m" + mount $3 $2 + mount $4 $2/boot + + echo -e "\033[0;31m\033[1mBind system dirs\033[0m\033[0m" + # https://github.com/debian-pi/raspbian-ua-netinst/issues/314 + echo "Mounting /proc in chroot... " + if [ ! -d $2/proc ] ; then + mkdir -p $2/proc + echo "Created $2/proc" + fi + mount -t proc -o nosuid,noexec,nodev proc $2/proc + echo "OK" + + echo "Mounting /sys in chroot... " + if [ ! -d $2/sys ] ; then + mkdir -p $2/sys + echo "Created $2/sys" + fi + mount -t sysfs -o nosuid,noexec,nodev sysfs $2/sys + echo "OK" + + echo "Mounting /dev/ and /dev/pts in chroot... " + mkdir -p -m 755 $2/dev/pts + mount -t devtmpfs -o mode=0755,nosuid devtmpfs $2/dev + mount -t devpts -o gid=5,mode=620 devpts $2/dev/pts + # mount -t devpts none "$2/dev/pts" -o ptmxmode=0666,newinstance + # ln -fs "pts/ptmx" "$2/dev/ptmx" + echo "OK" + + + # mount -o bind /dev $2/dev + # mount -t proc proc $2/proc + # mount -t devpts devpts $2/dev/pts + + # mount -t proc proc $2/proc + # mount -t sysfs sys $2/sys + # mount --bind /dev $2/dev + + echo -e "\033[0;31m\033[1mCopy DNS records\033[0m\033[0m" + cp -L /etc/resolv.conf $2/etc/resolv.conf + + # https://wiki.archlinux.org/index.php/Change_root_(%D0%A0%D1%83%D1%81%D1%81%D0%BA%D0%B8%D0%B9) + # http://www.unix-lab.org/posts/chroot/ + # https://habrahabr.ru/post/141012/ + # https://losst.ru/vosstanovlenie-grub2 + # http://unixteam.ru/content/virtualizaciya-ili-zapuskaem-prilozhenie-v-chroot-okruzhenii-razmyshleniya + # http://help.ubuntu.ru/wiki/%D0%B2%D0%BE%D1%81%D1%81%D1%82%D0%B0%D0%BD%D0%BE%D0%B2%D0%BB%D0%B5%D0%BD%D0%B8%D0%B5_grub + echo -e "\033[0;31m\033[1mEnter chroot\033[0m\033[0m" + chroot $2 /bin/bash +} + +mount_system2() { + + # STATIC + # TEMPLATE: mount_system2 $IMAGE $PREFIX_PATH $DEV_ROOTFS $DEV_BOOT $EXECUTE_FILE + + echo -e "\033[0;31m\033[1mMount loop-image: $1\033[0m\033[0m" + DEV_IMAGE=$(losetup -Pf $1 --show) + sleep 0.5 + + echo -e "\033[0;31m\033[1mMount dirs $2 & $2/boot\033[0m\033[0m" + mount $3 $2 + mount $4 $2/boot + + echo -e "\033[0;31m\033[1mBind system dirs\033[0m\033[0m" + echo "Mounting /proc in chroot... " + if [ ! -d $2/proc ] ; then + mkdir -p $2/proc + echo "Created $2/proc" + fi + mount -t proc -o nosuid,noexec,nodev proc $2/proc + echo "OK" + + echo "Mounting /sys in chroot... " + if [ ! -d $2/sys ] ; then + mkdir -p $2/sys + echo "Created $2/sys" + fi + mount -t sysfs -o nosuid,noexec,nodev sysfs $2/sys + echo "OK" + + echo "Mounting /dev/ and /dev/pts in chroot... " + mkdir -p -m 755 $2/dev/pts + mount -t devtmpfs -o mode=0755,nosuid devtmpfs $2/dev + mount -t devpts -o gid=5,mode=620 devpts $2/dev/pts + echo "OK" + + echo -e "\033[0;31m\033[1mCopy DNS records\033[0m\033[0m" + cp -L /etc/resolv.conf $2/etc/resolv.conf + + echo -e "\033[0;31m\033[1m$(date) | Enter chroot\033[0m\033[0m" + chroot $2 /bin/bash -c "$5" +} + +umount_system() { + + # STATIC + # TEMPLATE: umount_system $PREFIX_PATH + + echo -e "\033[0;31m\033[1m$(date) | Umount recursive dirs: $1\033[0m\033[0m" + umount -fR $1 + echo -e "\033[0;31m\033[1m$(date) | Umount loop-image\033[0m\033[0m" + losetup -d $DEV_IMAGE +} + +umount_system2() { + + # STATIC + # TEMPLATE: umount_system $PREFIX_PATH + + echo -e "\033[0;31m\033[1m$(date) | Umount recursive dirs: $1\033[0m\033[0m" + umount -fR $1 + echo -e "\033[0;31m\033[1m$(date) | Umount loop-image\033[0m\033[0m" + losetup -D +} + +set_config_var() { + lua - "$1" "$2" "$3" < "$3.bak" +local key=assert(arg[1]) +local value=assert(arg[2]) +local fn=assert(arg[3]) +local file=assert(io.open(fn)) +local made_change=false +for line in file:lines() do + if line:match("^#?%s*"..key.."=.*$") then + line=key.."="..value + made_change=true + end + print(line) +end + +if not made_change then + print(key.."="..value) +end +EOF + mv "$3.bak" "$3" +} + +configure_system() { + + # STATIC + # TEMPLATE: configure_system $IMAGE $PREFIX_PATH $DEV_ROOTFS $DEV_BOOT + + local BLACKLIST=/etc/modprobe.d/raspi-blacklist.conf + local CONFIG=/boot/config.txt + + BLACKLIST=$2$BLACKLIST + CONFIG=$2$CONFIG + + # 1. Примонитровать образ + + # https://raspberrypi.stackexchange.com/questions/13137/how-can-i-mount-a-raspberry-pi-linux-distro-image + # mount -v -o offset=48234496 -t ext4 2017-11-29-raspbian-stretch-lite.img $PREFIX_PATH + # mount -v -o offset=4194304,sizelimit=29360128 -t vfat 2017-11-29-raspbian-stretch-lite.img $PREFIX_PATH/boot + # + # fdisk -l 2017-11-29-raspbian-stretch-lite.img + # https://www.stableit.ru/2011/05/losetup.html + # -f : losetup сам выбрал loop (минуя занятые) + # -P : losetup монтирует разделы в образе как отдельные подразделы, + # например /dev/loop0p1 и /dev/loop0p2 + # --show : печатает имя устройства, например /dev/loop4 + echo -e "\033[0;31m\033[1mMount loop-image: $1\033[0m\033[0m" + DEV_IMAGE=$(losetup -Pf $1 --show) + sleep 0.5 + + echo -e "\033[0;31m\033[1mMount dirs $2 & $2/boot\033[0m\033[0m" + mount $3 $2 + mount $4 $2/boot + + # 2. Изменить необходимые настройки + + # 2.1. Включить sshd + echo -e "\033[0;31m\033[1mTurn on sshd\033[0m\033[0m" + touch $2/boot/ssh + + # 2.2. Включить GPIO + # Включено по умолчанию + + # 2.3. Включить I2C + echo -e "\033[0;31m\033[1mTurn on I2C\033[0m\033[0m" + + set_config_var dtparam=i2c_arm on $CONFIG && + if ! [ -e $BLACKLIST ]; then + touch $BLACKLIST + fi + sed $BLACKLIST -i -e "s/^\(blacklist[[:space:]]*i2c[-_]bcm2708\)/#\1/" + sed $2/etc/modules -i -e "s/^#[[:space:]]*\(i2c[-_]dev\)/\1/" + if ! grep -q "^i2c[-_]dev" $2/etc/modules; then + printf "i2c-dev\n" >> $2/etc/modules + fi + + # 2.4. Включить SPI + echo -e "\033[0;31m\033[1mTurn on SPI\033[0m\033[0m" + + set_config_var dtparam=spi on $CONFIG && + if ! [ -e $BLACKLIST ]; then + touch $BLACKLIST + fi + sed $BLACKLIST -i -e "s/^\(blacklist[[:space:]]*spi[-_]bcm2708\)/#\1/" + + # 2.5. Включить raspicam + # Включена по умолчанию вроде как + + # 2.6. Настроить AP wifi + # 2.7. Настроить сеть на wlan + # 2.8. Настроить DHCPd на wlan + + # Отмонтировать образ + umount_system $2 +} + + +prepare_fs() { + + # STATIC + # TEMPLATE: prepare_fs $IMAGE $SIZE + + date + # Удаляем старый образ + # -f : не выводить ошибки, если файла нет + rm -f $1 + # Копируем origin образ + # --progress : Вывод прогресс-бара + rsync --progress -av $1.orig $1 + expand_image $1 $2G + date +} + +install_docker() { + + # STATIC + # TEMPLATE: install_docker $IMAGE $PREFIX_PATH $DEV_ROOTFS $DEV_BOOT + + # https://askubuntu.com/questions/485567/unexpected-end-of-file + mount_system $1 $2 $3 $4 << EOF +#!/bin/bash +# https://www.raspberrypi.org/blog/docker-comes-to-raspberry-pi/ +curl -sSL https://get.docker.com | sh +usermod -aG docker pi +systemctl enable docker +service docker start +docker pull smirart/rpi-ros:sshd +docker run -di --restart unless-stopped -p 192.168.0.121:2202:22 -t smirart/rpi-ros:sshd +EOF + umount_system $2 +} + +test_docker() { + + # STATIC + # TEMPLATE: test_docker $IMAGE $PREFIX_PATH $DEV_ROOTFS $DEV_BOOT + + mount_system $1 $2 $3 $4 << EOF +#!/bin/bash +# https://www.raspberrypi.org/blog/docker-comes-to-raspberry-pi/ +service docker start +sleep 1 +docker images +docker ps -a +EOF + umount_system $2 +} + +enter() { + + # STATIC + # TEMPLATE: enter $IMAGE $PREFIX_PATH $DEV_ROOTFS $DEV_BOOT + + mount_system $1 $2 $3 $4 + umount_system $2 +} + +execute() { + + # STATIC + # TEMPLATE: execute $IMAGE $PREFIX_PATH $DEV_ROOTFS $DEV_BOOT $EXECUTE_FILE + + mount_system2 $1 $2 $3 $4 "$(cat $5)" + umount_system2 $2 +} + + +# очистить history +# https://askubuntu.com/questions/191999/how-to-clear-bash-history-completely +# cat /dev/null > ~/.bash_history && history -c && exit +# +# screen in chroot +# getty tty +# https://stackoverflow.com/questions/19104894/screen-must-be-connected-to-a-terminal/25646444 +# +# docker in chroot +# service docker start +# https://forums.docker.com/t/cannot-connect-to-the-docker-daemon-is-the-docker-daemon-running-on-this-host/8925/17 + + +if [ $(whoami) != "root" ]; +then echo "" \ + && echo "********************************************************************" \ + && echo "******************** This should be run as root ********************" \ + && echo "********************************************************************" \ + && echo "" \ + && exit 1 +fi + + +echo "\$#: $#" +echo "\$1: $1" +echo "\$2: $2" +echo "\$3: $3" +echo "\$4: $4" +echo "\$5: $5" +echo "\$6: $6" + + +# test_docker +# install_docker +# prepare_fs +# configure_system + +case "$1" in + enter) # enter $IMAGE $PREFIX_PATH $DEV_ROOTFS $DEV_BOOT + enter $2 $3 $4 $5;; + + get_image) # get_image $JENKINS_HOME $RPI_ZIP_NAME $RPI_DONWLOAD_URL $RPI_IMAGE_NAME $IMAGE_NAME + get_image $2 $3 $4 $5 $6;; + + resize_fs) # resize_fs $SIZE $JENKINS_HOME $IMAGE_NAME $DEV_ROOTFS + resize_fs $2 $3 $4 $5;; + + publish_image) # publish_image $JENKINS_HOME $IMAGE_NAME $WORKSPACE $CONFIG_FILE $RELEASE_ID $RELEASE_BODY + publish_image $2 $3 $4 $5 $6 $7;; + + execute) # execute $IMAGE $PREFIX_PATH $DEV_ROOTFS $DEV_BOOT $EXECUTE_FILE + execute $2 $3 $4 $5 $6;; + + *) + echo "Enter one of: enter, get_image, resize_fs, publish_image, execute";; +esac diff --git a/image/ros.sh b/image/ros.sh new file mode 100755 index 00000000..2848fdd3 --- /dev/null +++ b/image/ros.sh @@ -0,0 +1,191 @@ +#!/bin/bash + +################################################################################################################################## +# ROS for user pi +################################################################################################################################## + +# ros http://wiki.ros.org/action/fullsearch/ROSberryPi/Installing%20ROS%20Kinetic%20on%20the%20Raspberry%20Pi +# maintainer @urpylka + +echo -e "\033[0;31m\033[1m$(date) | #0 Install ROS\033[0m\033[0m" + + + + +echo -e "\033[0;31m\033[1m$(date) | #1 Install dirmngr & add key to apt-key\033[0m\033[0m" + +# по умолчанию dirmngr отсуствует на образе и требуется для установки ключа +# http://wpblogger.su/tags/apt/ +apt-get install dirmngr +# setup keys +apt-key adv --keyserver hkp://ha.pool.sks-keyservers.net:80 --recv-key 421C365BD9FF1F717815A3895523BAEEB01FA116 + +# setup sources.list +echo "deb http://packages.ros.org/ros/ubuntu stretch main" > /etc/apt/sources.list.d/ros-latest.list + + + + +echo -e "\033[0;31m\033[1m$(date) | #2 apt update && apt upgrade\033[0m\033[0m" + +# install bootstrap tools +apt-get update +# && apt upgrade -y + + + + +echo -e "\033[0;31m\033[1m$(date) | #3 Install wget, unzip, python-rosdep, python-rosinstall-generator, python-wstool, python-rosinstall, build-essential, cmake\033[0m\033[0m" + +apt-get install --no-install-recommends -y \ + wget \ + unzip \ + python-rosdep \ + python-rosinstall-generator \ + python-wstool \ + python-rosinstall \ + build-essential \ + cmake \ + libjpeg8-dev + + + +echo -e "\033[0;31m\033[1m$(date) | #4 rosdep init && rosdep update\033[0m\033[0m" + +# bootstrap rosdep +rosdep init && rosdep update + + + + +echo -e "\033[0;31m\033[1m$(date) | #5 Prepare ros_comm packages to kinetic-ros_comm-wet.rosinstall\033[0m\033[0m" + +# create catkin workspace +mkdir -p /home/pi/ros_catkin_ws && cd /home/pi/ros_catkin_ws \ + && rosinstall_generator ros_comm --rosdistro kinetic --deps --wet-only --tar > kinetic-ros_comm-wet.rosinstall \ + && wstool init src kinetic-ros_comm-wet.rosinstall + + + +echo -e "\033[0;31m\033[1m$(date) | #6 Install assimp-3.1.1 to /home/pi/ros_catkin_ws/external_src\033[0m\033[0m" + +# Unavailable Dependencies +mkdir -p /home/pi/ros_catkin_ws/external_src \ + && cd /home/pi/ros_catkin_ws/external_src \ + && wget http://sourceforge.net/projects/assimp/files/assimp-3.1/assimp-3.1.1_no_test_models.zip/download -O assimp-3.1.1_no_test_models.zip \ + && unzip assimp-3.1.1_no_test_models.zip \ + && cd assimp-3.1.1 \ + && cmake . \ + && make \ + && make install + + + + +echo -e "\033[0;31m\033[1m$(date) | #7 Prepare other ROS-packages to kinetic-custom_ros.rosinstall\033[0m\033[0m" + +cd /home/pi/ros_catkin_ws \ + && rosinstall_generator \ + actionlib actionlib_msgs angles async_web_server_cpp bond bond_core bondcpp bondpy camera_calibration_parsers camera_info_manager catkin class_loader cmake_modules cpp_common cv_bridge cv_camera diagnostic_msgs diagnostic_updater dynamic_reconfigure eigen_conversions gencpp geneus genlisp genmsg gennodejs genpy geographic_msgs geometry_msgs geometry2 image_transport libmavconn mavlink mavros_msgs message_filters message_generation message_runtime mk nav_msgs nodelet orocos_kdl pluginlib python_orocos_kdl ros ros_comm rosapi rosauth rosbag rosbag_migration_rule rosbag_storage rosbash rosboost_cfg rosbridge_library rosbridge_server rosbridge_suite rosbuild rosclean rosconsole rosconsole_bridge roscpp roscpp_serialization roscpp_traits roscreate rosgraph rosgraph_msgs roslang roslaunch roslib roslint roslisp roslz4 rosmake rosmaster rosmsg rosnode rosout rospack rosparam rospy rospy_tutorials rosserial rosserial_client rosserial_msgs rosserial_python rosservice rostest rostime rostopic rosunit roswtf sensor_msgs smclib std_msgs std_srvs stereo_msgs tf tf2 tf2_bullet tf2_eigen tf2_geometry_msgs tf2_kdl tf2_msgs tf2_py tf2_ros tf2_sensor_msgs tf2_tools topic_tools trajectory_msgs urdf urdf_parser_plugin usb_cam uuid_msgs visualization_msgs web_video_server xmlrpcpp mavros opencv3 mavros_extras \ + --rosdistro kinetic --deps --wet-only --tar > kinetic-custom_ros.rosinstall \ + && wstool merge -t src kinetic-custom_ros.rosinstall \ + && wstool update -t src + + + + +echo -e "\033[0;31m\033[1m$(date) | #8 Install dependencies apps with rosdep\033[0m\033[0m" + +# как я понял установка apt-get всяких зависимостей для ros-пакетов +# Resolving Dependencies with rosdep +cd /home/pi/ros_catkin_ws \ + && rosdep install -y --from-paths src --ignore-src --rosdistro kinetic -r --os=debian:stretch + + + + +echo -e "\033[0;31m\033[1m$(date) | #9 Refactor usb_cam in SRC\033[0m\033[0m" + +# добавление префикса с помощью двух define +# #define PIX_FMT_RGB24 AV_PIX_FMT_RGB24 +# #define PIX_FMT_YUV422P AV_PIX_FMT_YUV422P + +sed -i '/#define __STDC_CONSTANT_MACROS/a\#define PIX_FMT_RGB24 AV_PIX_FMT_RGB24\n#define PIX_FMT_YUV422P AV_PIX_FMT_YUV422P' /home/pi/ros_catkin_ws/src/usb_cam/src/usb_cam.cpp + + + +echo -e "\033[0;31m\033[1m$(date) | #10 Install GeographicLib datasets\033[0m\033[0m" + +/home/pi/ros_catkin_ws/src/mavros/mavros/scripts/install_geographiclib_datasets.sh + + + + +echo -e "\033[0;31m\033[1m$(date) | #11 Build light packages on 2 threads\033[0m\033[0m" + +# Building the catkin Workspace +cd /home/pi/ros_catkin_ws && ./src/catkin/bin/catkin_make_isolated --install -DCMAKE_BUILD_TYPE=Release --install-space /opt/ros/kinetic -j2 --pkg actionlib actionlib_msgs angles async_web_server_cpp bond bond_core bondcpp bondpy camera_calibration_parsers camera_info_manager catkin class_loader cmake_modules cpp_common diagnostic_msgs diagnostic_updater dynamic_reconfigure eigen_conversions gencpp geneus genlisp genmsg gennodejs genpy geographic_msgs geometry_msgs geometry2 image_transport libmavconn mavlink mavros_msgs message_filters message_generation message_runtime mk nav_msgs nodelet orocos_kdl pluginlib python_orocos_kdl ros ros_comm rosapi rosauth rosbag rosbag_migration_rule rosbag_storage rosbash rosboost_cfg rosbridge_library rosbridge_server rosbridge_suite rosbuild rosclean rosconsole rosconsole_bridge roscpp roscpp_serialization roscpp_traits roscreate rosgraph rosgraph_msgs roslang roslaunch roslib roslint roslisp roslz4 rosmake rosmaster rosmsg rosnode rosout rospack rosparam rospy rospy_tutorials rosserial rosserial_client rosserial_msgs rosserial_python rosservice rostest rostime rostopic rosunit roswtf sensor_msgs smclib std_msgs std_srvs stereo_msgs tf tf2 tf2_bullet tf2_eigen tf2_geometry_msgs tf2_kdl tf2_msgs tf2_py tf2_ros tf2_sensor_msgs tf2_tools topic_tools trajectory_msgs urdf urdf_parser_plugin usb_cam uuid_msgs visualization_msgs xmlrpcpp + + + + +echo -e "\033[0;31m\033[1m$(date) | #12 Build heavy packages\033[0m\033[0m" + +# Building the catkin Workspace +cd /home/pi/ros_catkin_ws && ./src/catkin/bin/catkin_make_isolated --install -DCMAKE_BUILD_TYPE=Release --install-space /opt/ros/kinetic -j1 --pkg mavros opencv3 cv_bridge cv_camera mavros_extras web_video_server + + + +echo -e "\033[0;31m\033[1m$(date) | #13 Create catkin_ws\033[0m\033[0m" + +mkdir -p /home/pi/catkin_ws/src \ + && cd /home/pi/catkin_ws \ + && source /opt/ros/kinetic/setup.bash \ + && catkin init \ + && wstool init /home/pi/catkin_ws/src + + + + +echo -e "\033[0;31m\033[1m$(date) | #14 Install CLEVER-BUNDLE\033[0m\033[0m" + +cd /home/pi/catkin_ws/src \ + && git clone https://github.com/CopterExpress/clever.git clever \ + && pip install wheel \ + && pip install -r /home/pi/catkin_ws/src/clever/clever/requirements.txt \ + && cd /home/pi/catkin_ws \ + && source /opt/ros/kinetic/setup.bash \ + && catkin_make -j1 \ + && systemctl enable /home/pi/catkin_ws/src/clever/deploy/roscore.service \ + && systemctl enable /home/pi/catkin_ws/src/clever/deploy/clever.service + + + +echo -e "\033[0;31m\033[1m$(date) | #15 Add mjpg-streamer at /home/pi\033[0m\033[0m" + +# https://github.com/jacksonliam/mjpg-streamer + +cd /home/pi \ + && git clone https://github.com/jacksonliam/mjpg-streamer.git \ + && cd /home/pi/mjpg-streamer/mjpg-streamer-experimental \ + && make \ + && make install + + + +echo -e "\033[0;31m\033[1m$(date) | #16 Add ENV vars\033[0m\033[0m" + +# setup environment +echo "LANG=C.UTF-8" >> /home/pi/.bashrc +echo "LC_ALL=C.UTF-8" >> /home/pi/.bashrc +echo "ROS_DISTRO=kinetic" >> /home/pi/.bashrc +echo "export ROS_IP=192.168.11.1" >> /home/pi/.bashrc + +echo "source /opt/ros/kinetic/setup.bash" >> /home/pi/.bashrc \ + && echo "source /home/pi/catkin_ws/devel/setup.bash" >> /home/pi/.bashrc + +chown -Rf pi:pi /home/pi + + + +echo -e "\033[0;31m\033[1m$(date) | #17 END of ROS INSTALLATION\033[0m\033[0m" diff --git a/image/yadisk.py b/image/yadisk.py new file mode 100755 index 00000000..b42c4c8c --- /dev/null +++ b/image/yadisk.py @@ -0,0 +1,55 @@ +#!/usr/bin/env python + +# +# Simple python uploader to YaDisk +# Smirnov Artem @urpylka +# +# Use: +# python yadisk.py login password file server_dir +# + +from YaDiskClient.YaDiskClient import YaDisk +import os.path, sys + +def upload(_login, _password, _server_dir, _file): + if os.path.isfile(_file): + disk = YaDisk(_login, _password) + disk.upload(_file, _server_dir + '/' + os.path.basename(_file)) + link = disk.publish_doc(_server_dir + '/' + os.path.basename(_file)) + print link + else: + print "Error: file-path is bad" + return 1 + +def main(): + if (len(sys.argv) == 5): + print "login: " + sys.argv[1] + print "password: " + sys.argv[2] + print "server_dir: " + sys.argv[3] + print "file: " + sys.argv[4] + + upload(sys.argv[1], sys.argv[2], sys.argv[3], sys.argv[4]) + + elif (len(sys.argv) == 3): + # print "config: " + sys.argv[1] + # print "file: " + sys.argv[2] + + if os.path.isfile(sys.argv[1]) and os.path.isfile(sys.argv[2]): + + from ConfigParser import SafeConfigParser + cfgParser = SafeConfigParser() + cfgParser.read(sys.argv[1]) + # print "login: " + cfgParser.get('yadisk','login') + # print "password: " + cfgParser.get('yadisk','password') + # print "server_dir: " + cfgParser.get('yadisk','server_dir') + + upload(cfgParser.get('yadisk','login'), cfgParser.get('yadisk','password'), cfgParser.get('yadisk','server_dir'), sys.argv[2]) + else: + print "Error: file-path or config-path is bad" + return 1 + else: + print "Error: amount of args is incorrect" + return 1 + +if __name__ == '__main__': + main() diff --git a/primeri-programm.md b/primeri-programm.md deleted file mode 100644 index e69de29b..00000000 diff --git a/sborka.md b/sborka.md deleted file mode 100644 index e69de29b..00000000 diff --git a/sitl.md b/sitl.md deleted file mode 100644 index e69de29b..00000000