From 571c12061e1acc386809083379a4b28c0ad974b8 Mon Sep 17 00:00:00 2001 From: Oleg Kalachev Date: Mon, 19 Feb 2018 22:22:08 +0300 Subject: [PATCH] Track 411 files into repository. - untracked .gitattributes - untracked .gitignore - untracked apps/ios/.gitignore - untracked apps/ios/cleverrc.xcodeproj/project.pbxproj - untracked apps/ios/cleverrc.xcodeproj/project.xcworkspace/contents.xcworkspacedata - untracked apps/ios/cleverrc.xcworkspace/contents.xcworkspacedata - untracked apps/ios/cleverrc/AppDelegate.swift - untracked apps/ios/cleverrc/Assets.xcassets/AppIcon.appiconset/cleverios180-1.png - untracked apps/ios/cleverrc/Assets.xcassets/AppIcon.appiconset/cleverios180.png - untracked apps/ios/cleverrc/Assets.xcassets/AppIcon.appiconset/Contents.json - untracked apps/ios/cleverrc/Assets.xcassets/Contents.json - untracked apps/ios/cleverrc/Assets.xcassets/Image.imageset/Contents.json - untracked apps/ios/cleverrc/Base.lproj/LaunchScreen.storyboard - untracked apps/ios/cleverrc/Base.lproj/Main.storyboard - untracked apps/ios/cleverrc/BinUtils.swift - untracked apps/ios/cleverrc/clever.svg - untracked apps/ios/cleverrc/index.html - untracked apps/ios/cleverrc/Info.plist - untracked apps/ios/cleverrc/main.css - untracked apps/ios/cleverrc/main.js - untracked apps/ios/cleverrc/README.md - untracked apps/ios/cleverrc/roslib.js - untracked apps/ios/cleverrc/telemetry.js - untracked apps/ios/cleverrc/ViewController.swift - untracked apps/ios/Podfile - untracked apps/ios/Podfile.lock - untracked apps/ios/README.md - untracked aruco_pose/CMakeLists.txt - untracked aruco_pose/nodelet_plugins.xml - untracked aruco_pose/package.xml - untracked aruco_pose/src/aruco_pose.cpp - untracked aruco_pose/src/fix.cpp - untracked assets/11_1.png - untracked assets/11_2.png - untracked assets/11_3.png - untracked assets/11_4.png - untracked assets/11_5.png - untracked assets/13_1.png - untracked assets/13_10.png - untracked assets/13_11.png - untracked assets/13_2.png - untracked assets/13_3.jpg - untracked assets/13_4.png - untracked assets/13_5.png - untracked assets/13_6.png - untracked assets/13_7.png - untracked assets/13_8.png - untracked assets/13_9.png - untracked assets/15_1.png - untracked assets/15_2.png - untracked assets/15_3.png - untracked assets/15_4.png - untracked assets/15_5.png - untracked assets/15_6.png - untracked assets/15_7.png - untracked assets/16_1.png - untracked assets/16_2.png - untracked assets/16_3.png - untracked assets/16_4.png - untracked assets/1_1.png - untracked assets/1_10.png - untracked assets/1_11.png - untracked assets/1_12.png - untracked assets/1_13.png - untracked assets/1_2.png - untracked assets/1_3.png - untracked assets/1_4.png - untracked assets/1_5.png - untracked assets/1_6.png - untracked assets/1_7.png - untracked assets/1_8.png - untracked assets/1_9.png - untracked assets/2_1.png - untracked assets/2_2.png - untracked assets/2_3.png - untracked assets/2_4.png - untracked assets/2_5.png - untracked assets/2_6.png - untracked assets/2_7.png - untracked assets/2_8.png - untracked assets/2_9.png - untracked assets/4_1.png - untracked assets/4_2.png - untracked assets/4_3.png - untracked assets/4_4.png - untracked assets/4_5.png - untracked assets/4_6.png - untracked assets/7_1.png - untracked assets/7_2.png - untracked assets/7_3.png - untracked assets/7_4.png - untracked assets/8_1.png - untracked assets/8_2.png - untracked assets/8_3.png - untracked assets/8_4.png - untracked assets/8_5.png - untracked assets/8_6.png - untracked assets/9_1.png - untracked assets/9_2.png - untracked assets/addEqipment.jpg - untracked assets/airframeSetup.jpg - untracked assets/allElements.png - untracked assets/attentionSave.jpg - untracked assets/brrc2205.png - untracked assets/brrc2205on.png - untracked assets/brrc2205ondeck.png - untracked assets/calibrateaxcel.jpg - untracked assets/calibrateaxcelstart.jpg - untracked assets/calibratecompass.jpg - untracked assets/calibrateESC.jpg - untracked assets/calibrategyro.jpg - untracked assets/calibratePIDparams.jpg - untracked assets/calibrateView.jpg - untracked assets/calibrateViewStart.jpg - untracked assets/casebattery.png - untracked assets/chooseSwitch.jpg - untracked assets/Clever main.png - untracked assets/clever.jpg - untracked assets/Clevermain.png - untracked assets/connectBattery.png - untracked assets/connectingRadio.png - untracked assets/connectionESCtoReceiver.png - untracked assets/connectionLost.jpg - untracked assets/connectionOK.jpg - untracked assets/connectionPixhawk.png - untracked assets/consistofTransmitter.jpg - untracked assets/cutwire14AWG.jpg - untracked assets/escDYSzap.png - untracked assets/escWires.png - untracked assets/explosion.png - untracked assets/firmwarePX4.jpg - untracked assets/flightModes.jpg - untracked assets/helphand.jpg - untracked assets/holderLegs.png - untracked assets/isoViewmountHolder.png - untracked assets/jumper.png - untracked assets/keep.png - untracked assets/lockradio.jpg - untracked assets/lockradio.png - untracked assets/lowsafeDeck.png - untracked assets/mainWindow.jpg - untracked assets/motorsTopview.png - untracked assets/mount5vconnector.png - untracked assets/mountAntenna.png - untracked assets/mountBeams.png - untracked assets/mountBottomDeck.png - untracked assets/mountHolder.png - untracked assets/mountPDB.png - untracked assets/mountReceiverDeck.png - untracked assets/mountReceiverStud.png - untracked assets/mountxt60pinsocket.png - untracked assets/notmoveslider.jpg - untracked assets/pixhawk.png - untracked assets/radioTransmitter.png - untracked assets/readyBatteryholder.png - untracked assets/receiver5V.png - untracked assets/receiverPPM.png - untracked assets/resolderingESC.png - untracked assets/safehighRadial.png - untracked assets/safeLegs.png - untracked assets/safelowRadial.png - untracked assets/safetybyassem.png - untracked assets/safetyINflight.png - untracked assets/safetyPower.png - untracked assets/safetyPreflight.png - untracked assets/soldering5VTOpdb.png - untracked assets/solderingBrrc2205ondeckTOescDYSzap.png - untracked assets/solderingPowerwires.png - untracked assets/solderingxt60socketTOpdb.png - untracked assets/stand.jpg - untracked assets/startPDBtest.jpg - untracked assets/testMotors.png - untracked assets/topESCcaseview.png - untracked assets/topPreview.png - untracked assets/topviewmountPDB.png - untracked assets/topviewpixhawk.png - untracked assets/turnoffSafetyswitch.jpg - untracked assets/xt60pinsocket.jpg - untracked assets/zap.jpg - untracked assets/zapPDBtest.jpg - untracked clever/camera_info/fisheye_cam_320.yaml - untracked clever/camera_info/fisheye_cam_640.yaml - untracked clever/CMakeLists.txt - untracked clever/launch/arduino.launch - untracked clever/launch/aruco.launch - untracked clever/launch/clever.launch - untracked clever/launch/copter_visualization.launch - untracked clever/launch/fpv_camera.launch - untracked clever/launch/main_camera.launch - untracked clever/launch/mavros.launch - untracked clever/launch/sitl.launch - untracked clever/launch/web_server.launch - untracked clever/nodelet_plugins.xml - untracked clever/package.xml - untracked clever/requirements.txt - untracked clever/src/aruco_vpe.cpp - untracked clever/src/fcu_horiz.cpp - untracked clever/src/fpv_camera - untracked clever/src/global_local.py - untracked clever/src/rc.cpp - untracked clever/src/simple_offboard.py - untracked clever/src/util.h - untracked clever/src/util.py - untracked clever/src/web_server.py - untracked clever/srv/GetTelemetry.srv - untracked clever/srv/Navigate.srv - untracked clever/srv/SetAttitude.srv - untracked clever/srv/SetAttitudeYawRate.srv - untracked clever/srv/SetPosition.srv - untracked clever/srv/SetPositionGlobal.srv - untracked clever/srv/SetPositionGlobalYawRate.srv - untracked clever/srv/SetPositionYawRate.srv - untracked clever/srv/SetRates.srv - untracked clever/srv/SetRatesYaw.srv - untracked clever/srv/SetVelocity.srv - untracked clever/srv/SetVelocityYawRate.srv - untracked deploy/clever.service - untracked deploy/clever_arudino.tar.gz - untracked deploy/generate_ros_lib - untracked deploy/roscore.env - untracked deploy/roscore.service - modified docs/3g.md - modified docs/assemble.md - untracked docs/deck.md - modified docs/etcher.md - modified docs/frames.md - modified docs/les1.md - modified docs/les11.md - modified docs/les13.md - modified docs/les15.md - modified docs/les16.md - modified docs/les2.md - modified docs/les4.md - modified docs/les7.md - modified docs/les8.md - modified docs/les9.md - modified docs/modes.md - untracked docs/powerConnection.md - modified docs/radioerrors.md - modified docs/radioerrors1.md - modified docs/safety.md - modified docs/setup.md - modified docs/simple_offboard.md - modified docs/tb.md - untracked docs/testConnection.md - modified docs/wifi.md - untracked docs/zap.md - removed gpsmd.md - untracked image/apps.sh - untracked image/git_release.py - untracked image/iface.sh - untracked image/image-config.sh - untracked image/Jenkinsfile - untracked image/ros.sh - untracked image/yadisk.py - removed img/11_1.png - removed img/11_2.png - removed img/11_3.png - removed img/11_4.png - removed img/11_5.png - removed img/13_1.png - removed img/13_10.png - removed img/13_11.png - removed img/13_2.png - removed img/13_3.jpg - removed img/13_4.png - removed img/13_5.png - removed img/13_6.png - removed img/13_7.png - removed img/13_8.png - removed img/13_9.png - removed img/15_1.png - removed img/15_2.png - removed img/15_3.png - removed img/15_4.png - removed img/15_5.png - removed img/15_6.png - removed img/15_7.png - removed img/16_1.png - removed img/16_2.png - removed img/16_3.png - removed img/16_4.png - removed img/1_1.png - removed img/1_10.png - removed img/1_11.png - removed img/1_12.png - removed img/1_13.png - removed img/1_2.png - removed img/1_3.png - removed img/1_4.png - removed img/1_5.png - removed img/1_6.png - removed img/1_7.png - removed img/1_8.png - removed img/1_9.png - removed img/2_1.png - removed img/2_2.png - removed img/2_3.png - removed img/2_4.png - removed img/2_5.png - removed img/2_6.png - removed img/2_7.png - removed img/2_8.png - removed img/2_9.png - removed img/4_1.png - removed img/4_2.png - removed img/4_3.png - removed img/4_4.png - removed img/4_5.png - removed img/4_6.png - removed img/7_1.png - removed img/7_2.png - removed img/7_3.png - removed img/7_4.png - removed img/8_1.png - removed img/8_2.png - removed img/8_3.png - removed img/8_4.png - removed img/8_5.png - removed img/8_6.png - removed img/9_1.png - removed img/9_2.png - removed img/addEqipment.jpg - removed img/airframeSetup.jpg - removed img/allElements.png - removed img/attentionSave.jpg - removed img/brrc2205.png - removed img/brrc2205on.png - removed img/brrc2205ondeck.png - removed img/calibrateaxcel.jpg - removed img/calibrateaxcelstart.jpg - removed img/calibratecompass.jpg - removed img/calibrateESC.jpg - removed img/calibrategyro.jpg - removed img/calibratePIDparams.jpg - removed img/calibrateView.jpg - removed img/calibrateViewStart.jpg - removed img/casebattery.png - removed img/chooseSwitch.jpg - removed img/Clever main.png - removed img/clever.jpg - removed img/Clevermain.png - removed img/connectBattery.png - removed img/connectingRadio.png - removed img/connectionESCtoReceiver.png - removed img/connectionLost.jpg - removed img/connectionOK.jpg - removed img/connectionPixhawk.png - removed img/consistofTransmitter.jpg - removed img/cutwire14AWG.jpg - removed img/escDYSzap.png - removed img/escWires.png - removed img/explosion.png - removed img/firmwarePX4.jpg - removed img/flightModes.jpg - removed img/helphand.jpg - removed img/holderLegs.png - removed img/isoViewmountHolder.png - removed img/jumper.png - removed img/keep.png - removed img/lockradio.jpg - removed img/lockradio.png - removed img/lowsafeDeck.png - removed img/mainWindow.jpg - removed img/motorsTopview.png - removed img/mount5vconnector.png - removed img/mountAntenna.png - removed img/mountBeams.png - removed img/mountBottomDeck.png - removed img/mountHolder.png - removed img/mountPDB.png - removed img/mountReceiverDeck.png - removed img/mountReceiverStud.png - removed img/mountxt60pinsocket.png - removed img/notmoveslider.jpg - removed img/pixhawk.png - removed img/radioTransmitter.png - removed img/readyBatteryholder.png - removed img/receiver5V.png - removed img/receiverPPM.png - removed img/resolderingESC.png - removed img/safehighRadial.png - removed img/safeLegs.png - removed img/safelowRadial.png - removed img/safetybyassem.png - removed img/safetyINflight.png - removed img/safetyPower.png - removed img/safetyPreflight.png - removed img/soldering5VTOpdb.png - removed img/solderingBrrc2205ondeckTOescDYSzap.png - removed img/solderingPowerwires.png - removed img/solderingxt60socketTOpdb.png - removed img/stand.jpg - removed img/startPDBtest.jpg - removed img/testMotors.png - removed img/topESCcaseview.png - removed img/topPreview.png - removed img/topviewmountPDB.png - removed img/topviewpixhawk.png - removed img/turnoffSafetyswitch.jpg - removed img/xt60pinsocket.jpg - removed img/zap.jpg - removed img/zapPDBtest.jpg - removed notes/deck.md - removed notes/powerConnection.md - removed notes/testConnection.md - removed notes/zap.md - removed primeri-programm.md - modified README.md - removed sborka.md - removed sitl.md Auto commit by GitBook Editor --- .gitattributes | 2 + .gitignore | 2 + README.md | 73 +- apps/ios/.gitignore | 17 + apps/ios/Podfile | 14 + apps/ios/Podfile.lock | 21 + apps/ios/README.md | 10 + apps/ios/cleverrc.xcodeproj/project.pbxproj | 444 ++ .../contents.xcworkspacedata | 7 + .../contents.xcworkspacedata | 10 + apps/ios/cleverrc/AppDelegate.swift | 46 + .../AppIcon.appiconset/Contents.json | 100 + .../AppIcon.appiconset/cleverios180-1.png | Bin 0 -> 3344 bytes .../AppIcon.appiconset/cleverios180.png | Bin 0 -> 3344 bytes .../cleverrc/Assets.xcassets/Contents.json | 6 + .../Image.imageset/Contents.json | 20 + .../Base.lproj/LaunchScreen.storyboard | 29 + apps/ios/cleverrc/Base.lproj/Main.storyboard | 46 + apps/ios/cleverrc/BinUtils.swift | 453 ++ apps/ios/cleverrc/Info.plist | 46 + apps/ios/cleverrc/README.md | 10 + apps/ios/cleverrc/ViewController.swift | 70 + apps/ios/cleverrc/clever.svg | 84 + apps/ios/cleverrc/index.html | 23 + apps/ios/cleverrc/main.css | 91 + apps/ios/cleverrc/main.js | 126 + apps/ios/cleverrc/roslib.js | 3693 +++++++++++++++++ apps/ios/cleverrc/telemetry.js | 85 + aruco_pose/CMakeLists.txt | 210 + aruco_pose/nodelet_plugins.xml | 5 + aruco_pose/package.xml | 62 + aruco_pose/src/aruco_pose.cpp | 292 ++ aruco_pose/src/fix.cpp | 145 + {img => assets}/11_1.png | Bin {img => assets}/11_2.png | Bin {img => assets}/11_3.png | Bin {img => assets}/11_4.png | Bin {img => assets}/11_5.png | Bin {img => assets}/13_1.png | Bin {img => assets}/13_10.png | Bin {img => assets}/13_11.png | Bin {img => assets}/13_2.png | Bin {img => assets}/13_3.jpg | Bin {img => assets}/13_4.png | Bin {img => assets}/13_5.png | Bin {img => assets}/13_6.png | Bin {img => assets}/13_7.png | Bin {img => assets}/13_8.png | Bin {img => assets}/13_9.png | Bin {img => assets}/15_1.png | Bin {img => assets}/15_2.png | Bin {img => assets}/15_3.png | Bin {img => assets}/15_4.png | Bin {img => assets}/15_5.png | Bin {img => assets}/15_6.png | Bin {img => assets}/15_7.png | Bin {img => assets}/16_1.png | Bin {img => assets}/16_2.png | Bin {img => assets}/16_3.png | Bin {img => assets}/16_4.png | Bin {img => assets}/1_1.png | Bin {img => assets}/1_10.png | Bin {img => assets}/1_11.png | Bin {img => assets}/1_12.png | Bin {img => assets}/1_13.png | Bin {img => assets}/1_2.png | Bin {img => assets}/1_3.png | Bin {img => assets}/1_4.png | Bin {img => assets}/1_5.png | Bin {img => assets}/1_6.png | Bin {img => assets}/1_7.png | Bin {img => assets}/1_8.png | Bin {img => assets}/1_9.png | Bin {img => assets}/2_1.png | Bin {img => assets}/2_2.png | Bin {img => assets}/2_3.png | Bin {img => assets}/2_4.png | Bin {img => assets}/2_5.png | Bin {img => assets}/2_6.png | Bin {img => assets}/2_7.png | Bin {img => assets}/2_8.png | Bin {img => assets}/2_9.png | Bin {img => assets}/4_1.png | Bin {img => assets}/4_2.png | Bin {img => assets}/4_3.png | Bin {img => assets}/4_4.png | Bin {img => assets}/4_5.png | Bin {img => assets}/4_6.png | Bin {img => assets}/7_1.png | Bin {img => assets}/7_2.png | Bin {img => assets}/7_3.png | Bin {img => assets}/7_4.png | Bin {img => assets}/8_1.png | Bin {img => assets}/8_2.png | Bin {img => assets}/8_3.png | Bin {img => assets}/8_4.png | Bin {img => assets}/8_5.png | Bin {img => assets}/8_6.png | Bin {img => assets}/9_1.png | Bin {img => assets}/9_2.png | Bin {img => assets}/Clever main.png | Bin {img => assets}/Clevermain.png | Bin {img => assets}/addEqipment.jpg | Bin {img => assets}/airframeSetup.jpg | Bin {img => assets}/allElements.png | Bin {img => assets}/attentionSave.jpg | Bin {img => assets}/brrc2205.png | Bin {img => assets}/brrc2205on.png | Bin {img => assets}/brrc2205ondeck.png | Bin {img => assets}/calibrateESC.jpg | Bin {img => assets}/calibratePIDparams.jpg | Bin {img => assets}/calibrateView.jpg | Bin {img => assets}/calibrateViewStart.jpg | Bin {img => assets}/calibrateaxcel.jpg | Bin {img => assets}/calibrateaxcelstart.jpg | Bin {img => assets}/calibratecompass.jpg | Bin {img => assets}/calibrategyro.jpg | Bin {img => assets}/casebattery.png | Bin {img => assets}/chooseSwitch.jpg | Bin {img => assets}/clever.jpg | Bin {img => assets}/connectBattery.png | Bin {img => assets}/connectingRadio.png | Bin {img => assets}/connectionESCtoReceiver.png | Bin {img => assets}/connectionLost.jpg | Bin {img => assets}/connectionOK.jpg | Bin {img => assets}/connectionPixhawk.png | Bin {img => assets}/consistofTransmitter.jpg | Bin {img => assets}/cutwire14AWG.jpg | Bin {img => assets}/escDYSzap.png | Bin {img => assets}/escWires.png | Bin {img => assets}/explosion.png | Bin {img => assets}/firmwarePX4.jpg | Bin {img => assets}/flightModes.jpg | Bin {img => assets}/helphand.jpg | Bin {img => assets}/holderLegs.png | Bin {img => assets}/isoViewmountHolder.png | Bin {img => assets}/jumper.png | Bin {img => assets}/keep.png | Bin {img => assets}/lockradio.jpg | Bin {img => assets}/lockradio.png | Bin {img => assets}/lowsafeDeck.png | Bin {img => assets}/mainWindow.jpg | Bin {img => assets}/motorsTopview.png | Bin {img => assets}/mount5vconnector.png | Bin {img => assets}/mountAntenna.png | Bin {img => assets}/mountBeams.png | Bin {img => assets}/mountBottomDeck.png | Bin {img => assets}/mountHolder.png | Bin {img => assets}/mountPDB.png | Bin {img => assets}/mountReceiverDeck.png | Bin {img => assets}/mountReceiverStud.png | Bin {img => assets}/mountxt60pinsocket.png | Bin {img => assets}/notmoveslider.jpg | Bin {img => assets}/pixhawk.png | Bin {img => assets}/radioTransmitter.png | Bin {img => assets}/readyBatteryholder.png | Bin {img => assets}/receiver5V.png | Bin {img => assets}/receiverPPM.png | Bin {img => assets}/resolderingESC.png | Bin {img => assets}/safeLegs.png | Bin {img => assets}/safehighRadial.png | Bin {img => assets}/safelowRadial.png | Bin {img => assets}/safetyINflight.png | Bin {img => assets}/safetyPower.png | Bin {img => assets}/safetyPreflight.png | Bin {img => assets}/safetybyassem.png | Bin {img => assets}/soldering5VTOpdb.png | Bin .../solderingBrrc2205ondeckTOescDYSzap.png | Bin {img => assets}/solderingPowerwires.png | Bin {img => assets}/solderingxt60socketTOpdb.png | Bin {img => assets}/stand.jpg | Bin {img => assets}/startPDBtest.jpg | Bin {img => assets}/testMotors.png | Bin {img => assets}/topESCcaseview.png | Bin {img => assets}/topPreview.png | Bin {img => assets}/topviewmountPDB.png | Bin {img => assets}/topviewpixhawk.png | Bin {img => assets}/turnoffSafetyswitch.jpg | Bin {img => assets}/xt60pinsocket.jpg | Bin {img => assets}/zap.jpg | Bin {img => assets}/zapPDBtest.jpg | Bin clever/CMakeLists.txt | 231 ++ clever/camera_info/fisheye_cam_320.yaml | 45 + clever/camera_info/fisheye_cam_640.yaml | 45 + clever/launch/arduino.launch | 9 + clever/launch/aruco.launch | 24 + clever/launch/clever.launch | 61 + clever/launch/copter_visualization.launch | 12 + clever/launch/fpv_camera.launch | 6 + clever/launch/main_camera.launch | 23 + clever/launch/mavros.launch | 52 + clever/launch/sitl.launch | 17 + clever/launch/web_server.launch | 5 + clever/nodelet_plugins.xml | 10 + clever/package.xml | 56 + clever/requirements.txt | 2 + clever/src/aruco_vpe.cpp | 133 + clever/src/fcu_horiz.cpp | 40 + clever/src/fpv_camera | 8 + clever/src/global_local.py | 36 + clever/src/rc.cpp | 118 + clever/src/simple_offboard.py | 466 +++ clever/src/util.h | 17 + clever/src/util.py | 28 + clever/src/web_server.py | 60 + clever/srv/GetTelemetry.srv | 22 + clever/srv/Navigate.srv | 11 + clever/srv/SetAttitude.srv | 10 + clever/srv/SetAttitudeYawRate.srv | 8 + clever/srv/SetPosition.srv | 10 + clever/srv/SetPositionGlobal.srv | 10 + clever/srv/SetPositionGlobalYawRate.srv | 10 + clever/srv/SetPositionYawRate.srv | 10 + clever/srv/SetRates.srv | 8 + clever/srv/SetRatesYaw.srv | 10 + clever/srv/SetVelocity.srv | 10 + clever/srv/SetVelocityYawRate.srv | 10 + deploy/clever.service | 12 + deploy/clever_arudino.tar.gz | Bin 0 -> 186276 bytes deploy/generate_ros_lib | 9 + deploy/roscore.env | 10 + deploy/roscore.service | 11 + docs/3g.md | 2 +- docs/assemble.md | 106 +- {notes => docs}/deck.md | 2 +- docs/etcher.md | 2 +- docs/frames.md | 2 +- docs/les1.md | 26 +- docs/les11.md | 10 +- docs/les13.md | 22 +- docs/les15.md | 14 +- docs/les16.md | 8 +- docs/les2.md | 18 +- docs/les4.md | 12 +- docs/les7.md | 8 +- docs/les8.md | 12 +- docs/les9.md | 4 +- docs/modes.md | 2 +- {notes => docs}/powerConnection.md | 2 +- docs/radioerrors.md | 8 +- docs/radioerrors1.md | 8 +- docs/safety.md | 2 +- docs/setup.md | 36 +- docs/simple_offboard.md | 10 +- docs/tb.md | 6 +- {notes => docs}/testConnection.md | 2 +- docs/wifi.md | 2 +- {notes => docs}/zap.md | 4 +- gpsmd.md | 3 - image/Jenkinsfile | 62 + image/apps.sh | 100 + image/git_release.py | 38 + image/iface.sh | 138 + image/image-config.sh | 491 +++ image/ros.sh | 191 + image/yadisk.py | 55 + primeri-programm.md | 0 sborka.md | 0 sitl.md | 0 259 files changed, 9155 insertions(+), 170 deletions(-) create mode 100644 .gitattributes create mode 100644 .gitignore create mode 100644 apps/ios/.gitignore create mode 100644 apps/ios/Podfile create mode 100644 apps/ios/Podfile.lock create mode 100644 apps/ios/README.md create mode 100644 apps/ios/cleverrc.xcodeproj/project.pbxproj create mode 100644 apps/ios/cleverrc.xcodeproj/project.xcworkspace/contents.xcworkspacedata create mode 100644 apps/ios/cleverrc.xcworkspace/contents.xcworkspacedata create mode 100644 apps/ios/cleverrc/AppDelegate.swift create mode 100644 apps/ios/cleverrc/Assets.xcassets/AppIcon.appiconset/Contents.json create mode 100644 apps/ios/cleverrc/Assets.xcassets/AppIcon.appiconset/cleverios180-1.png create mode 100644 apps/ios/cleverrc/Assets.xcassets/AppIcon.appiconset/cleverios180.png create mode 100644 apps/ios/cleverrc/Assets.xcassets/Contents.json create mode 100644 apps/ios/cleverrc/Assets.xcassets/Image.imageset/Contents.json create mode 100644 apps/ios/cleverrc/Base.lproj/LaunchScreen.storyboard create mode 100644 apps/ios/cleverrc/Base.lproj/Main.storyboard create mode 100644 apps/ios/cleverrc/BinUtils.swift create mode 100644 apps/ios/cleverrc/Info.plist create mode 100644 apps/ios/cleverrc/README.md create mode 100644 apps/ios/cleverrc/ViewController.swift create mode 100644 apps/ios/cleverrc/clever.svg create mode 100644 apps/ios/cleverrc/index.html create mode 100644 apps/ios/cleverrc/main.css create mode 100644 apps/ios/cleverrc/main.js create mode 100644 apps/ios/cleverrc/roslib.js create mode 100644 apps/ios/cleverrc/telemetry.js create mode 100644 aruco_pose/CMakeLists.txt create mode 100644 aruco_pose/nodelet_plugins.xml create mode 100644 aruco_pose/package.xml create mode 100644 aruco_pose/src/aruco_pose.cpp create mode 100644 aruco_pose/src/fix.cpp rename {img => assets}/11_1.png (100%) rename {img => assets}/11_2.png (100%) rename {img => assets}/11_3.png (100%) rename {img => assets}/11_4.png (100%) rename {img => assets}/11_5.png (100%) rename {img => assets}/13_1.png (100%) rename {img => assets}/13_10.png (100%) rename {img => assets}/13_11.png (100%) rename {img => assets}/13_2.png (100%) rename {img => assets}/13_3.jpg (100%) rename {img => assets}/13_4.png (100%) rename {img => assets}/13_5.png (100%) rename {img => assets}/13_6.png (100%) rename {img => assets}/13_7.png (100%) rename {img => assets}/13_8.png (100%) rename {img => assets}/13_9.png (100%) rename {img => assets}/15_1.png (100%) rename {img => assets}/15_2.png (100%) rename {img => assets}/15_3.png (100%) rename {img => assets}/15_4.png (100%) rename {img => assets}/15_5.png (100%) rename {img => assets}/15_6.png (100%) rename {img => assets}/15_7.png (100%) rename {img => assets}/16_1.png (100%) rename {img => assets}/16_2.png (100%) rename {img => assets}/16_3.png (100%) rename {img => assets}/16_4.png (100%) rename {img => assets}/1_1.png (100%) rename {img => assets}/1_10.png (100%) rename {img => assets}/1_11.png (100%) rename {img => assets}/1_12.png (100%) rename {img => assets}/1_13.png (100%) rename {img => assets}/1_2.png (100%) rename {img => assets}/1_3.png (100%) rename {img => assets}/1_4.png (100%) rename {img => assets}/1_5.png (100%) rename {img => assets}/1_6.png (100%) rename {img => assets}/1_7.png (100%) rename {img => assets}/1_8.png (100%) rename {img => assets}/1_9.png (100%) rename {img => assets}/2_1.png (100%) rename {img => assets}/2_2.png (100%) rename {img => assets}/2_3.png (100%) rename {img => assets}/2_4.png (100%) rename {img => assets}/2_5.png (100%) rename {img => assets}/2_6.png (100%) rename {img => assets}/2_7.png (100%) rename {img => assets}/2_8.png (100%) rename {img => assets}/2_9.png (100%) rename {img => assets}/4_1.png (100%) rename {img => assets}/4_2.png (100%) rename {img => assets}/4_3.png (100%) rename {img => assets}/4_4.png (100%) rename {img => assets}/4_5.png (100%) rename {img => assets}/4_6.png (100%) rename {img => assets}/7_1.png (100%) rename {img => assets}/7_2.png (100%) rename {img => assets}/7_3.png (100%) rename {img => assets}/7_4.png (100%) rename {img => assets}/8_1.png (100%) rename {img => assets}/8_2.png (100%) rename {img => assets}/8_3.png (100%) rename {img => assets}/8_4.png (100%) rename {img => assets}/8_5.png (100%) rename {img => assets}/8_6.png (100%) rename {img => assets}/9_1.png (100%) rename {img => assets}/9_2.png (100%) rename {img => assets}/Clever main.png (100%) rename {img => assets}/Clevermain.png (100%) rename {img => assets}/addEqipment.jpg (100%) rename {img => assets}/airframeSetup.jpg (100%) rename {img => assets}/allElements.png (100%) rename {img => assets}/attentionSave.jpg (100%) rename {img => assets}/brrc2205.png (100%) rename {img => assets}/brrc2205on.png (100%) rename {img => assets}/brrc2205ondeck.png (100%) rename {img => assets}/calibrateESC.jpg (100%) rename {img => assets}/calibratePIDparams.jpg (100%) rename {img => assets}/calibrateView.jpg (100%) rename {img => assets}/calibrateViewStart.jpg (100%) rename {img => assets}/calibrateaxcel.jpg (100%) rename {img => assets}/calibrateaxcelstart.jpg (100%) rename {img => assets}/calibratecompass.jpg (100%) rename {img => assets}/calibrategyro.jpg (100%) rename {img => assets}/casebattery.png (100%) rename {img => assets}/chooseSwitch.jpg (100%) rename {img => assets}/clever.jpg (100%) rename {img => assets}/connectBattery.png (100%) rename {img => assets}/connectingRadio.png (100%) rename {img => assets}/connectionESCtoReceiver.png (100%) rename {img => assets}/connectionLost.jpg (100%) rename {img => assets}/connectionOK.jpg (100%) rename {img => assets}/connectionPixhawk.png (100%) rename {img => assets}/consistofTransmitter.jpg (100%) rename {img => assets}/cutwire14AWG.jpg (100%) rename {img => assets}/escDYSzap.png (100%) rename {img => assets}/escWires.png (100%) rename {img => assets}/explosion.png (100%) rename {img => assets}/firmwarePX4.jpg (100%) rename {img => assets}/flightModes.jpg (100%) rename {img => assets}/helphand.jpg (100%) rename {img => assets}/holderLegs.png (100%) rename {img => assets}/isoViewmountHolder.png (100%) rename {img => assets}/jumper.png (100%) rename {img => assets}/keep.png (100%) rename {img => assets}/lockradio.jpg (100%) rename {img => assets}/lockradio.png (100%) rename {img => assets}/lowsafeDeck.png (100%) rename {img => assets}/mainWindow.jpg (100%) rename {img => assets}/motorsTopview.png (100%) rename {img => assets}/mount5vconnector.png (100%) rename {img => assets}/mountAntenna.png (100%) rename {img => assets}/mountBeams.png (100%) rename {img => assets}/mountBottomDeck.png (100%) rename {img => assets}/mountHolder.png (100%) rename {img => assets}/mountPDB.png (100%) rename {img => assets}/mountReceiverDeck.png (100%) rename {img => assets}/mountReceiverStud.png (100%) rename {img => assets}/mountxt60pinsocket.png (100%) rename {img => assets}/notmoveslider.jpg (100%) rename {img => assets}/pixhawk.png (100%) rename {img => assets}/radioTransmitter.png (100%) rename {img => assets}/readyBatteryholder.png (100%) rename {img => assets}/receiver5V.png (100%) rename {img => assets}/receiverPPM.png (100%) rename {img => assets}/resolderingESC.png (100%) rename {img => assets}/safeLegs.png (100%) rename {img => assets}/safehighRadial.png (100%) rename {img => assets}/safelowRadial.png (100%) rename {img => assets}/safetyINflight.png (100%) rename {img => assets}/safetyPower.png (100%) rename {img => assets}/safetyPreflight.png (100%) rename {img => assets}/safetybyassem.png (100%) rename {img => assets}/soldering5VTOpdb.png (100%) rename {img => assets}/solderingBrrc2205ondeckTOescDYSzap.png (100%) rename {img => assets}/solderingPowerwires.png (100%) rename {img => assets}/solderingxt60socketTOpdb.png (100%) rename {img => assets}/stand.jpg (100%) rename {img => assets}/startPDBtest.jpg (100%) rename {img => assets}/testMotors.png (100%) rename {img => assets}/topESCcaseview.png (100%) rename {img => assets}/topPreview.png (100%) rename {img => assets}/topviewmountPDB.png (100%) rename {img => assets}/topviewpixhawk.png (100%) rename {img => assets}/turnoffSafetyswitch.jpg (100%) rename {img => assets}/xt60pinsocket.jpg (100%) rename {img => assets}/zap.jpg (100%) rename {img => assets}/zapPDBtest.jpg (100%) create mode 100644 clever/CMakeLists.txt create mode 100644 clever/camera_info/fisheye_cam_320.yaml create mode 100644 clever/camera_info/fisheye_cam_640.yaml create mode 100644 clever/launch/arduino.launch create mode 100644 clever/launch/aruco.launch create mode 100644 clever/launch/clever.launch create mode 100644 clever/launch/copter_visualization.launch create mode 100644 clever/launch/fpv_camera.launch create mode 100644 clever/launch/main_camera.launch create mode 100644 clever/launch/mavros.launch create mode 100644 clever/launch/sitl.launch create mode 100644 clever/launch/web_server.launch create mode 100644 clever/nodelet_plugins.xml create mode 100644 clever/package.xml create mode 100644 clever/requirements.txt create mode 100644 clever/src/aruco_vpe.cpp create mode 100644 clever/src/fcu_horiz.cpp create mode 100755 clever/src/fpv_camera create mode 100644 clever/src/global_local.py create mode 100644 clever/src/rc.cpp create mode 100755 clever/src/simple_offboard.py create mode 100644 clever/src/util.h create mode 100644 clever/src/util.py create mode 100755 clever/src/web_server.py create mode 100644 clever/srv/GetTelemetry.srv create mode 100644 clever/srv/Navigate.srv create mode 100644 clever/srv/SetAttitude.srv create mode 100644 clever/srv/SetAttitudeYawRate.srv create mode 100644 clever/srv/SetPosition.srv create mode 100644 clever/srv/SetPositionGlobal.srv create mode 100644 clever/srv/SetPositionGlobalYawRate.srv create mode 100644 clever/srv/SetPositionYawRate.srv create mode 100644 clever/srv/SetRates.srv create mode 100644 clever/srv/SetRatesYaw.srv create mode 100644 clever/srv/SetVelocity.srv create mode 100644 clever/srv/SetVelocityYawRate.srv create mode 100644 deploy/clever.service create mode 100644 deploy/clever_arudino.tar.gz create mode 100644 deploy/generate_ros_lib create mode 100644 deploy/roscore.env create mode 100644 deploy/roscore.service rename {notes => docs}/deck.md (85%) rename {notes => docs}/powerConnection.md (97%) rename {notes => docs}/testConnection.md (97%) rename {notes => docs}/zap.md (87%) delete mode 100644 gpsmd.md create mode 100644 image/Jenkinsfile create mode 100755 image/apps.sh create mode 100755 image/git_release.py create mode 100755 image/iface.sh create mode 100755 image/image-config.sh create mode 100755 image/ros.sh create mode 100755 image/yadisk.py delete mode 100644 primeri-programm.md delete mode 100644 sborka.md delete mode 100644 sitl.md 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 0000000000000000000000000000000000000000..ae590b79e3ed6c0b512d0dc88fc9ea4bf7687021 GIT binary patch literal 3344 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 TNAli895P)kxF%ZN@!7utxT#&9 literal 0 HcmV?d00001 diff --git a/apps/ios/cleverrc/Assets.xcassets/AppIcon.appiconset/cleverios180.png b/apps/ios/cleverrc/Assets.xcassets/AppIcon.appiconset/cleverios180.png new file mode 100644 index 0000000000000000000000000000000000000000..ae590b79e3ed6c0b512d0dc88fc9ea4bf7687021 GIT binary patch literal 3344 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 TNAli895P)kxF%ZN@!7utxT#&9 literal 0 HcmV?d00001 diff --git a/apps/ios/cleverrc/Assets.xcassets/Contents.json b/apps/ios/cleverrc/Assets.xcassets/Contents.json new file mode 100644 index 00000000..da4a164c --- /dev/null +++ b/apps/ios/cleverrc/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/apps/ios/cleverrc/Assets.xcassets/Image.imageset/Contents.json b/apps/ios/cleverrc/Assets.xcassets/Image.imageset/Contents.json new file mode 100644 index 00000000..f8f827e4 --- /dev/null +++ b/apps/ios/cleverrc/Assets.xcassets/Image.imageset/Contents.json @@ -0,0 +1,20 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/apps/ios/cleverrc/Base.lproj/LaunchScreen.storyboard b/apps/ios/cleverrc/Base.lproj/LaunchScreen.storyboard new file mode 100644 index 00000000..19d54bf0 --- /dev/null +++ b/apps/ios/cleverrc/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/ios/cleverrc/Base.lproj/Main.storyboard b/apps/ios/cleverrc/Base.lproj/Main.storyboard new file mode 100644 index 00000000..0afff7f1 --- /dev/null +++ b/apps/ios/cleverrc/Base.lproj/Main.storyboard @@ -0,0 +1,46 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/ios/cleverrc/BinUtils.swift b/apps/ios/cleverrc/BinUtils.swift new file mode 100644 index 00000000..422cd7c3 --- /dev/null +++ b/apps/ios/cleverrc/BinUtils.swift @@ -0,0 +1,453 @@ +// +// BinUtils.swift +// BinUtils +// +// Created by Nicolas Seriot on 12/03/16. +// Copyright © 2016 Nicolas Seriot. All rights reserved. +// + +import Foundation +import CoreFoundation + +// MARK: protocol UnpackedType + +public protocol Unpackable {} + +extension NSString: Unpackable {} +extension Bool: Unpackable {} +extension Int: Unpackable {} +extension Double: Unpackable {} + +// MARK: protocol DataConvertible + +protocol DataConvertible {} + +extension DataConvertible { + + init?(data: Data) { + guard data.count == MemoryLayout.size else { return nil } + self = data.withUnsafeBytes { $0.pointee } + } + + init?(bytes: [UInt8]) { + let data = Data(bytes:bytes) + self.init(data:data) + } + + var data: Data { + var value = self + return Data(buffer: UnsafeBufferPointer(start: &value, count: 1)) + } +} + +extension Bool : DataConvertible { } + +extension Int8 : DataConvertible { } +extension Int16 : DataConvertible { } +extension Int32 : DataConvertible { } +extension Int64 : DataConvertible { } + +extension UInt8 : DataConvertible { } +extension UInt16 : DataConvertible { } +extension UInt32 : DataConvertible { } +extension UInt64 : DataConvertible { } + +extension Float32 : DataConvertible { } +extension Float64 : DataConvertible { } + +// MARK: String extension + +extension String { + subscript (from:Int, to:Int) -> String { + return NSString(string: self).substring(with: NSMakeRange(from, to-from)) + } +} + +// MARK: Data extension + +extension Data { + var bytes : [UInt8] { + return self.withUnsafeBytes { + [UInt8](UnsafeBufferPointer(start: $0, count: self.count)) + } + } +} + +// MARK: functions + +public func hexlify(_ data:Data) -> String { + + // similar to hexlify() in Python's binascii module + // https://docs.python.org/2/library/binascii.html + + var s = String() + var byte: UInt8 = 0 + + for i in 0 ..< data.count { + NSData(data: data).getBytes(&byte, range: NSMakeRange(i, 1)) + s = s.appendingFormat("%02x", byte) + } + + return s as String +} + +public func unhexlify(_ string:String) -> Data? { + + // similar to unhexlify() in Python's binascii module + // https://docs.python.org/2/library/binascii.html + + let s = string.uppercased().replacingOccurrences(of: " ", with: "") + + let nonHexCharacterSet = CharacterSet(charactersIn: "0123456789ABCDEF").inverted + if let range = s.rangeOfCharacter(from: nonHexCharacterSet) { + print("-- found non hex character at range \(range)") + return nil + } + + var data = Data(capacity: s.count / 2) + + for i in stride(from: 0, to:s.count, by:2) { + let byteString = s[i, i+2] + let byte = UInt8(byteString.withCString { strtoul($0, nil, 16) }) + data.append([byte] as [UInt8], count: 1) + } + + return data +} + +func readIntegerType(_ type:T.Type, bytes:[UInt8], loc:inout Int) -> T { + let size = MemoryLayout.size + let sub = Array(bytes[loc..<(loc+size)]) + loc += size + return T(bytes: sub)! +} + +func readFloatingPointType(_ type:T.Type, bytes:[UInt8], loc:inout Int, isBigEndian:Bool) -> T { + let size = MemoryLayout.size + let sub = Array(bytes[loc..<(loc+size)]) + loc += size + let sub_ = isBigEndian ? sub.reversed() : sub + return T(bytes: sub_)! +} + +func isBigEndianFromMandatoryByteOrderFirstCharacter(_ format:String) -> Bool { + + guard let firstChar = format.first else { assertionFailure("empty format"); return false } + + let s = NSString(string: String(firstChar)) + let c = s.substring(to: 1) + + if c == "@" { assertionFailure("native size and alignment is unsupported") } + + if c == "=" || c == "<" { return false } + if c == ">" || c == "!" { return true } + + assertionFailure("format '\(format)' first character must be among '=<>!'") + + return false +} + +// akin to struct.calcsize(fmt) +func numberOfBytesInFormat(_ format:String) -> Int { + + var numberOfBytes = 0 + + var n = 0 // repeat counter + + var mutableFormat = format + + while !mutableFormat.isEmpty { + + let c = mutableFormat.remove(at: mutableFormat.startIndex) + + if let i = Int(String(c)) , 0...9 ~= i { + if n > 0 { n *= 10 } + n += i + continue + } + + if c == "s" { + numberOfBytes += max(n,1) + n = 0 + continue + } + + for _ in 0..", "!", " ": + () + case "c", "b", "B", "x", "?": + numberOfBytes += 1 + case "h", "H": + numberOfBytes += 2 + case "i", "l", "I", "L", "f": + numberOfBytes += 4 + case "q", "Q", "d": + numberOfBytes += 8 + case "P": + numberOfBytes += MemoryLayout.size + default: + assertionFailure("-- unsupported format \(c)") + } + } + + n = 0 + } + + return numberOfBytes +} + +func formatDoesMatchDataLength(_ format:String, data:Data) -> Bool { + let sizeAccordingToFormat = numberOfBytesInFormat(format) + let dataLength = data.count + if sizeAccordingToFormat != dataLength { + print("format \"\(format)\" expects \(sizeAccordingToFormat) bytes but data is \(dataLength) bytes") + return false + } + + return true +} + +/* + pack() and unpack() should behave as Python's struct module https://docs.python.org/2/library/struct.html BUT: + - native size and alignment '@' is not supported + - as a consequence, the byte order specifier character is mandatory and must be among "=<>!" + - native byte order '=' assumes a little-endian system (eg. Intel x86) + - Pascal strings 'p' and native pointers 'P' are not supported + */ + +public enum BinUtilsError: Error { + case formatDoesMatchDataLength(format:String, dataSize:Int) + case unsupportedFormat(character:Character) +} + +public func pack(_ format:String, _ objects:[Any], _ stringEncoding:String.Encoding=String.Encoding.windowsCP1252) -> Data { + + var objectsQueue = objects + + var mutableFormat = format + + var mutableData = Data() + + var isBigEndian = false + + let firstCharacter = mutableFormat.remove(at: mutableFormat.startIndex) + + switch(firstCharacter) { + case "<", "=": + isBigEndian = false + case ">", "!": + isBigEndian = true + case "@": + assertionFailure("native size and alignment '@' is unsupported'") + default: + assertionFailure("unsupported format chacracter'") + } + + var n = 0 // repeat counter + + while !mutableFormat.isEmpty { + + let c = mutableFormat.remove(at: mutableFormat.startIndex) + + if let i = Int(String(c)) , 0...9 ~= i { + if n > 0 { n *= 10 } + n += i + continue + } + + var o : Any = 0 + + if c == "s" { + o = objectsQueue.remove(at: 0) + + guard let stringData = (o as! String).data(using: .utf8) else { assertionFailure(); return Data() } + var bytes = stringData.bytes + + let expectedSize = max(1, n) + + // pad ... + while bytes.count < expectedSize { bytes.append(0x00) } + + // ... or trunk + if bytes.count > expectedSize { bytes = Array(bytes[0.. [Unpackable] { + + assert(CFByteOrderGetCurrent() == 1 /* CFByteOrderLittleEndian */, "\(#file) assumes little endian, but host is big endian") + + let isBigEndian = isBigEndianFromMandatoryByteOrderFirstCharacter(format) + + if formatDoesMatchDataLength(format, data: data) == false { + throw BinUtilsError.formatDoesMatchDataLength(format:format, dataSize:data.count) + } + + var a : [Unpackable] = [] + + var loc = 0 + + let bytes = data.bytes + + var n = 0 // repeat counter + + var mutableFormat = format + + mutableFormat.remove(at: mutableFormat.startIndex) // consume byte-order specifier + + while !mutableFormat.isEmpty { + + let c = mutableFormat.remove(at: mutableFormat.startIndex) + + if let i = Int(String(c)) , 0...9 ~= i { + if n > 0 { n *= 10 } + n += i + continue + } + + if c == "s" { + let length = max(n,1) + let sub = Array(bytes[loc.. + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleDisplayName + Clever RC + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + APPL + CFBundleShortVersionString + 1.0 + CFBundleVersion + 1 + LSRequiresIPhoneOS + + UILaunchStoryboardName + LaunchScreen + UIMainStoryboardFile + Main + UIRequiredDeviceCapabilities + + armv7 + + UISupportedInterfaceOrientations + + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + + diff --git a/apps/ios/cleverrc/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 0000000000000000000000000000000000000000..df92e77ae50dfa1b5cff36330914b33876126bc8 GIT binary patch literal 186276 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](../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