diff --git a/.circleci/config.yml b/.circleci/config.yml new file mode 100644 index 000000000..825f26624 --- /dev/null +++ b/.circleci/config.yml @@ -0,0 +1,83 @@ +# CircleCI 2.0 configuration file for dronekit-python + +# Steps common to both Python 2 and Python 3 +common: &commonsteps + steps: + - checkout + + - run: + name: Install dronekit with dependencies + command: | + virtualenv venv + source venv/bin/activate + pip install -UI future + pip install -r requirements.txt -UI + pip install . -UI + + - run: + name: Run dronekit tests + environment: + SITL_SPEEDUP: 10 + SITL_RATE: 200 + command: | + source venv/bin/activate + nosetests -svx dronekit.test.unit + nosetests -svx dronekit.test.sitl + + - run: + name: Build documentation + command: | + source venv/bin/activate + cd docs; make clean; make html + + - store_artifacts: + path: test-reports + destination: test-reports + +# Jobs definition +version: 2 +jobs: + python2: + docker: + - image: circleci/python:2.7-stretch + working_directory: ~/dronekit-python2 + <<: *commonsteps + + python3: + docker: + - image: circleci/python:3.6-stretch + working_directory: ~/dronekit-python3 + <<: *commonsteps + + deploy: + name: Deploy to dronekit.io + working_directory: ~/deploy + machine: true + steps: + - checkout + - run: + name: update docs + command: ./scripts/update-docs.sh + +# Send a notification on Gitter +notify: + webhooks: + - url: https://webhooks.gitter.im/e/27b0e1a9fede99dbbe6c + +# Workflow definition +# Run Python 2 and Python 3 testing in parallel +# and deploy if the Python 2 build succeeds +workflows: + version: 2 + build_test_deploy: + jobs: + - python2 + - python3 +# disabling until we find a new docs server +# - deploy: +# requires: +# - python2 +# filters: +# branches: +# only: +# - master diff --git a/.editorconfig b/.editorconfig index 08c10f020..2470cbc7e 100644 --- a/.editorconfig +++ b/.editorconfig @@ -20,6 +20,8 @@ trim_trailing_whitespace = true [*.py] indent_style = space indent_size = 4 +trim_trailing_whitespace = true +end_of_line = LF [*.mk] indent_style = tab diff --git a/.gitignore b/.gitignore index eb6b83785..dec06d5da 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ .project .pydevproject +.python-version dronekit.egg-info dist build @@ -8,3 +9,4 @@ build .idea *.pyc env +vs/.vs/dronekit-python/v14/*.suo diff --git a/.style.yapf b/.style.yapf new file mode 100644 index 000000000..728e3f0e0 --- /dev/null +++ b/.style.yapf @@ -0,0 +1,3 @@ +[style] +based_on_style = pep8 +column_limit = 100 diff --git a/.travis.yml b/.travis.yml index a061c5378..16dfd6d8b 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,36 +1,47 @@ +language: python + env: global: - SITL_SPEEDUP=10 - SITL_RATE=200 python: - - "2.7" + - 2.7 + - 3.6 + +before_install: + - pip install --upgrade pip install: - - 'sudo pip install . -UI' - - 'sudo pip install -r requirements.txt -UI' + - pip install -r requirements.txt + - pip install flake8 + +before_script: + # stop the build if there are Python syntax errors or undefined names + - flake8 . --count --select=E901,E999,F821,F822,F823 --show-source --statistics + # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide + - flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics script: - - '[ "${TRAVIS_PULL_REQUEST}" != "false" ] || nosetests tests/web' - - 'nosetests tests/unit' - - 'nosetests tests/sitl' + - nosetests --debug=nose,nose.importer --debug-log=nose_debug -svx dronekit.test.unit + - nosetests --debug=nose,nose.importer --debug-log=nose_debug -svx dronekit.test.sitl git: depth: 10 -language: objective-c - notifications: email: false - slack: - secure: IYgZ83X065I/LljGrPEACZms+KDwrojiQbboFHhvNxL2Zkc5jHqwBK6PD1BsJh2JVun9fCZ1v2R8KuDsf+Qz2dCximdOZcHI81L9JUOZYuSJ2TVlbKiC00WdXpcQ6g7pDTLm/mGBoPxC+MuC5l8zZdbpPBpEa0F/YCe/n7tbT+g= + webhooks: + urls: + - https://webhooks.gitter.im/e/f3f81b74f930dac9583a + on_success: change + on_failure: always + on_start: never -matrix: - fast_finish: true +#matrix: + #allow_failures: + #- python: 3.6 branches: only: - master - - patch - - major - - minor diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 000000000..ca542f26a --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,201 @@ +# Changelog + + +## Version 2.9.2 (2019-03-18) + +### Improvements +* CI integration improvements +* Python3 compatability +* use logging module +* log statustexts +* documentation improvements +* convenience functions added: wait_for, wait_for_armable, arm, disarm, wait_for_mode, wait_for_alt, wait_simple_takeoff +* play_tune method added +* reboot method added +* send_calibrate_gyro, send_calibrate_magnetometer, send_calibrate_magnetometer, send_calibrate_vehicle_level, send_calibrate_barometer all added +* update gimbal orientation from MOUNT_ORIENTATION +* add a still-waiting callback for connect() to provide debug on where the connection is up to +* several new tests added (including, play_tune, reboot and set_attitude_target) + +### Cleanup +* flake8 compliance improvements +* test includes pruned +* examples cleaned up + +### Bug Fixes +* ignore GCS heartbeats for the purposes of link up +* many! + +## Version 2.9.1 (2017-04-21) + +### Improvements +* home locatin notifications +* notify ci status to gitter +* basic python 3 support +* isolated logger function so implementers can override +* rename windows installer + +### Cleanup +* removed legacy cloud integrations + +### Bug Fixes +* fix missing ** operator for pymavlink compatibility + +## Version 2.9.0 (2016-08-29) + +### Bug Fixes +* MAVConnection stops threads on exit and close +* PX4 Pro flight modes are now properly supported +* go to test now uses correct `global_relative_frame` alt + +### Improvements +* Updated pymavlink dependency to v2 from v1 hoping we don't fall behind + again. + +## Version 2.8.0 (2016-07-15) + +### Bug Fixes +* Makes sure we are listening to `HOME_LOCATION` message, befor we + would only set home location if received by waypoints. + +## Version 2.7.0 (2016-06-21) + +### Improvements +* Adds udpin-multi support + +## Version 2.6.0 (2016-06-17) + +### Bug Fixes +* Fixes patched mavutil sendfn + +## Version 2.5.0 (2016-05-04) + +### Improvements +* Catch and display message and attribute errors, then continue +* Improved takeoff example docs +* Deploy docs on successful merge into master (from CircleCI) +* Drone delivery example, explain port to connect +* MicroCGS example now uses SITL +* Make running examples possible on Vagrant + +### Bug Fixes +* Mav type for rover was incorrect +* `_is_mode_available` can now handle unrecognized mode codes +* Fix broken links on companion computer page +* Fix infinite loop on channel test + + + +## Version 2.4.0 (2016-02-29) + +### Bug Fixes + +* Use monotonic clock for all of the internal timeouts and time + measurements +* Docs fixes + + +## Version 2.3.0 (2016-02-26) + +### New Features + +* PX4 compatibility improvements + +### Updated Features + +* Documentation fixes +* PIP repository improvements +* Mode-setting API improvements +* ardupilot-solo compatibility fixes + + + +## Version 2.2.0 (2016-02-19) + +### Bug Fixes + +* Splits outbound messages into its own thread. +* Remove of capabilities request on HEARTBEAT listener +* Check if mode_mapping has items before iteration + + + +## Version 2.1.0 (2016-02-16) + + +### New Features + + +* Gimbal control attribute +* Autopilot version attribute +* Autopilot capabilities attribute +* Best Practice guide documentation. +* Performance test example (restructured and docs added) + +### Updated Features: + +Many documentation fixes: + +* Restructured documentation with Develop (Concepts) and Guide (HowTo) sections +* Docs separated out "Connection Strings" section. +* Improved test and contribution sections. +* Updated examples and documentation to use DroneKit-Sitl for simulation ("zero setup examples") +* Debugging docs updated with additional libraries. +* Flight Replay example fetches data from TLOG rather than droneshare +* Drone Delivery example now uses strart location for home address. +* Disabled web tests (not currently supported/used) +* Updated copyright range to include changes in 2016 + +### Bug Fixes + +* Numerous minor docs fixes. +* Harmonise nosetest options across each of the integration platforms +* Fix incorrect property marker for airspeed attribute + + + +## Version 2.0.2 (2015-11-30) + +### Bug Fixes: + +* Updates `requests` dependency to work >=2.5.0 + + +## Version 2.0.0 (2015-11-23) + +### New Features: + +* Renamed library and package from DroneAPI to DroneKit on pip +* DroneKit Python is now a standalone library and no longer requires use of MAVProxy +* Connect multiple vehicles in one script by creating separate vehicle instances +* Removed NumPy, ProtoBuf as dependencies +* Add MAVLink message listeners using `add_message_listener` methods +* Added `on_attribute` and `on_message` function decorator shorthands +* Added `mount_status`, `system_status`, `ekf_ok`, `is_armable`, `heading` +* Made settable `groundspeed`, `airspeed` +* Moved `dronekit.lib` entries to root package `dronekit` +* Added `parameters.set` and `parameters.get` for fine-tuned parameter access +* `parameters` now observable and iterable (#442) +* Added `last_heartbeat` attribute, updated every event loop with time since last heartbeat (#451) +* Await attributes through `wait_ready` method and `connect` method parameter +* Adds subclassable Vehicle class, used by `vehicle_class` parameter in `connect` + +### Updated Features: + +* local_connect renamed to connect(), accepting a connection path, link configuration, and timeout settings +* Removed `.set_mavrx_callback`. Use `vehicle.on_message('*', obj)` methods +* Renamed `add_attribute_observer` methods to `add_attribute_listener`, etc. (#420) +* Renamed `wait_init` and `wait_valid` to `wait_ready` +* Split `home_location` is a separate attribute from `commands` waypoint array +* Moved RC channels into `.channels` object (#427) +* Split location information into `local_frame`, `global_frame`, and `global_relative_frame` (and removed `is_relative`) (#445) +* Renamed `flush` to `commands.upload`, as it only impacts waypoints (#276) +* `commands.goto` and `commands.takeoff` renamed to `simple_goto` and `simple_takeoff` + +### Bug Fixes: + +* `armed` and `mode` attributes updated constantly (#60, #446) +* Parameter setting times out (#12) +* `battery` access can throw exception (#298) +* Vehicle.location reports incorrect is_relative value for Copter (#130) +* Excess arming message when already armed diff --git a/README.md b/README.md index 2bb29e500..f024a0b6f 100644 --- a/README.md +++ b/README.md @@ -1,35 +1,74 @@ # DroneKit Python +![dronekit_python_logo](https://cloud.githubusercontent.com/assets/5368500/10805537/90dd4b14-7e22-11e5-9592-5925348a7df9.png) + +[![PyPi published version](https://img.shields.io/pypi/v/dronekit.svg)](https://pypi.org/project/dronekit/) [![Windows Build status](https://img.shields.io/appveyor/ci/3drobotics/dronekit-python/master.svg?label=windows)](https://ci.appveyor.com/project/3drobotics/dronekit-python/branch/master) [![OS X Build Status](https://img.shields.io/travis/dronekit/dronekit-python/master.svg?label=os%20x)](https://travis-ci.org/dronekit/dronekit-python) -[![Linux Build Status](https://img.shields.io/circleci/project/dronekit/dronekit-python/master.svg?label=linux)](https://circleci.com/gh/dronekit/dronekit-python) +[![Linux Build Status](https://img.shields.io/circleci/project/dronekit/dronekit-python/master.svg?label=linux)](https://circleci.com/gh/dronekit/dronekit-python) + +DroneKit-Python helps you create powerful apps for UAVs. + +# ⚠️ ATTENTION: MAINTAINERS NEEDED ⚠️ + +Hey it's true this project is not very active, but it could be with your help. We are looking for maintainers interested in keeping the project alive by keep up with CI and PRs. If you are interested in helping please apply by [creating an issue]([url](https://github.com/dronekit/dronekit-python/issues/new)) and listing the reasons why you would like to help, in return we will be granting committer access to folks who are truly interested in helping. + + +## Overview + +DroneKit-Python (formerly DroneAPI-Python) contains the python language implementation of DroneKit. + +The API allows developers to create Python apps that communicate with vehicles over MAVLink. It provides programmatic access to a connected vehicle's telemetry, state and parameter information, and enables both mission management and direct control over vehicle movement and operations. + +The API is primarily intended for use in onboard companion computers (to support advanced use cases including computer vision, path planning, 3D modelling etc). It can also be used for ground station apps, communicating with vehicles over a higher latency RF-link. + +## Getting Started + +The [Quick Start](https://dronekit-python.readthedocs.io/en/latest/guide/quick_start.html) guide explains how to set up DroneKit on each of the supported platforms (Linux, Mac OSX, Windows) and how to write a script to connect to a vehicle (real or simulated). + +A basic script looks like this: + +```python +from dronekit import connect + +# Connect to UDP endpoint. +vehicle = connect('127.0.0.1:14550', wait_ready=True) +# Use returned Vehicle object to query device state - e.g. to get the mode: +print("Mode: %s" % vehicle.mode.name) +``` + +Once you've got DroneKit set up, the [guide](https://dronekit-python.readthedocs.io/en/latest/guide/index.html) explains how to perform operations like taking off and flying the vehicle. You can also try out most of the tasks by running the [examples](https://dronekit-python.readthedocs.io/en/latest/examples/index.html). + +## Resources + +The project documentation is available at [https://readthedocs.org/projects/dronekit-python/](https://readthedocs.org/projects/dronekit-python/). This includes [guide](https://dronekit-python.readthedocs.io/en/latest/guide/index.html), [example](https://dronekit-python.readthedocs.io/en/latest/examples/index.html) and [API Reference](https://dronekit-python.readthedocs.io/en/latest/automodule.html) material. + +The example source code is hosted here on Github as sub-folders of [/dronekit-python/examples](https://github.com/dronekit/dronekit-python/tree/master/examples). + +The [DroneKit Forums](http://discuss.dronekit.io) are the best place to ask for technical support on how to use the library. You can also check out our [Gitter channel](https://gitter.im/dronekit/dronekit-python?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) though we prefer posts on the forums where possible. + +* **Documentation:** [https://dronekit-python.readthedocs.io/en/latest/about/index.html](https://dronekit-python.readthedocs.io/en/latest/about/index.html) +* **Guides:** [https://dronekit-python.readthedocs.io/en/latest/guide/index.html) +* **API Reference:** [https://dronekit-python.readthedocs.io/en/latest/automodule.html) +* **Examples:** [/dronekit-python/examples](https://github.com/dronekit/dronekit-python/tree/master/examples) +* **Forums:** [http://discuss.dronekit.io/](http://discuss.dronekit.io) +* **Gitter:** [https://gitter.im/dronekit/dronekit-python](https://gitter.im/dronekit/dronekit-python?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) though we prefer posts on the forums where possible. + + +## Users and contributors wanted! -[![Gitter](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/dronekit/dronekit-python?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) +We'd love your [feedback and suggestions](https://github.com/dronekit/dronekit-python/issues) about this API and are eager to evolve it to meet your needs, please feel free to create an issue to report bugs or feature requests. -*(Formerly DroneAPI-python.)* +If you've created some awesome software that uses this project, [let us know on the forums here](https://discuss.dronekit.io/t/notable-projects-using-dronekit/230)! -This package contains the python language implementation of DroneKit. +If you want to contribute, see our [Contributing](https://dronekit-python.readthedocs.io/en/latest/contributing/index.html) guidelines, we welcome all types of contributions but mostly contributions that would help us shrink our [issues list](https://github.com/dronekit/dronekit-python/issues). -## Users wanted! -We'd love your [feedback and suggestions](https://github.com/dronekit/dronekit-python/issues) about this API and are eager to evolve it to meet your needs. +## Licence -For documentation on how to use this API please see: +DroneKit-Python is made available under the permissive open source [Apache 2.0 License](http://python.dronekit.io/about/license.html). -* The new DroneKit-python [website](http://python.dronekit.io/) -* Our [tutorial](http://dev.ardupilot.com/wiki/droneapi-tutorial/) on the ardupilot wiki -* The [overview document](https://docs.google.com/document/d/1ihKneLwA4hXmKS1W2pbG9lty_EAwbmy0giusUwQ8dto) -* The [python autodocs](http://python.dronekit.io/automodule.html) -* The [python code](droneapi/lib/__init__.py) itself -* Answers to [technical support queries](http://stackoverflow.com/questions/tagged/dronekit-python) on Stack Overflow. -* Example code can be found here: ['examples/'](examples/) - * Beginner ['vehicle_state'](examples/vehicle_state/vehicle_state.py) - * Beginner ['simple_goto'](examples/simple_goto/simple_goto.py) - * Beginner ['flight_replay'](examples/flight_replay/flight_replay.py) - * Beginner ['gcs'](examples/gcs/microgcs.py) - * Advanced ['drone_delivery'](examples/drone_delivery/) - * Advanced ['follow-me'](examples/follow_me/) -The [DroneKit Forums](http://discuss.dronekit.io) is the best place to ask for technical support on how to use the library. +*** -Copyright 2015 3D Robotics, Inc. Licensed under the Apache 2.0 License. +Copyright 2015 3D Robotics, Inc. diff --git a/Vagrantfile b/Vagrantfile index 77610a6fa..d916140aa 100644 --- a/Vagrantfile +++ b/Vagrantfile @@ -3,6 +3,7 @@ Vagrant.configure(2) do |config| config.vm.box = "ubuntu/trusty64" + config.vm.network "private_network", type: "dhcp" config.vm.provision "shell", inline: <<-SHELL apt-get -y update @@ -13,6 +14,12 @@ Vagrant.configure(2) do |config| echo "[DroneKit]: Installing pip ..." apt-get -y install python-pip easy_install -U pip + echo "[DroneKit]: Installing python-cherrypy3 ..." + apt-get -y install python-cherrypy3 + echo "[DroneKit]: Installing python-matplotlib ..." + apt-get -y install python-matplotlib + echo "[DroneKit]: Installing python-gps ..." + apt-get -y install python-gps echo "[DroneKit]: Installing Sphinx ... " pip install sphinx cd /vagrant diff --git a/appveyor.yml b/appveyor.yml index 3aadf0baa..b1996b573 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -1,11 +1,20 @@ environment: - global: - SITL_SPEEDUP: 10 - SITL_RATE: 200 + SITL_SPEEDUP: 10 + SITL_RATE: 200 + PYTHONUNBUFFERED: 1 + + matrix: + - PYTHON: "C:\\Python27" + PYTHON_VERSION: "2.7" + - PYTHON: "C:\\Python37" + PYTHON_VERSION: "3.7" cache: - "c:\\Users\\appveyor\\.pip-wheelhouse" +init: + - cmd: "SET PATH=%PYTHON%;%PYTHON%\\Scripts;%PATH%" + install: - ps: | New-Item -ItemType Directory -Force -Path c:\Users\appveyor\pip\ @@ -16,25 +25,14 @@ install: [wheel] wheel-dir = c:/Users/appveyor/.pip-wheelhouse '@ | out-file -Encoding ascii -FilePath c:\Users\appveyor\pip\pip.ini - - - cmd: 'SET PATH=%PYTHON%;c:\\Python27\\Scripts;%PATH%' - - cmd: 'SET PYTHONUNBUFFERED=1' - - cmd: 'python -m pip install pip wheel -U' - cmd: 'pip install . -UI' - cmd: 'pip install -r requirements.txt -UI' -build_script: - - cmd: 'nosetests tests\\web' - - cmd: 'nosetests -s -v tests\\sitl' - - cmd: 'nosetests tests\\unit' +build: off + +test_script: + - cmd: 'nosetests -svx dronekit.test.unit' + - cmd: 'nosetests -svx dronekit.test.sitl' clone_depth: 10 -test: 'off' -branches: - only: - - master - - major - - minor - - patch - - /^v\d+\.\d+\.\d+$/ diff --git a/circle.yml b/circle.yml deleted file mode 100644 index 62b0bb8c7..000000000 --- a/circle.yml +++ /dev/null @@ -1,18 +0,0 @@ -machine: - environment: - SITL_SPEEDUP: 10 - SITL_RATE: 200 - -dependencies: - override: - - 'pip install . -UI' - - 'pip install -r requirements.txt -UI' - -test: - override: - - 'nosetests tests/web' - - 'nosetests tests/unit' - - 'nosetests tests/sitl' - -general: - build_dir: . diff --git a/docs/about/github_latest_release.txt b/docs/about/github_latest_release.txt index 719294867..9a67880ee 100644 --- a/docs/about/github_latest_release.txt +++ b/docs/about/github_latest_release.txt @@ -1,30 +1,227 @@ .. This document was auto-generated by the get_release_notes.py script using latest-release information from github -Release 1.5.0 (August 12, 2015) -=============================== - - -**Features:** - -* Added methods to unset `mavlink_callback`. (#115, #240) -* Publishing scripts now live in `scripts/` (#259) - -**Documentation:** - -* Documented clearing of `mavlink_callback`. (#245) - -Source Code ------------ - - -Source code is available at . - -* View `**commits** included in this release `_ -* View `**bugs** closed by this release `_ -* View `**pull requests** merged into this release `_ - -Notes ------ - - -Thanks to @hamishwillee, @mrpollo, @tcr3dr \ No newline at end of file +Changelog +========= + +Version 2.9.1 (2017-04-21) +-------------------------- + +Improvements +~~~~~~~~~~~~ + +- home locatin notifications +- notify ci status to gitter +- basic python 3 support +- isolated logger function so implementers can override +- rename windows installer + +Cleanup +~~~~~~~ + +- removed legacy cloud integrations + +Bug Fixes +~~~~~~~~~ + +- fix missing \*\* operator for pymavlink compatibility + +Version 2.9.0 (2016-08-29) +-------------------------- + +Bug Fixes +~~~~~~~~~ + +- MAVConnection stops threads on exit and close +- PX4 Pro flight modes are now properly supported +- go to test now uses correct ``global_relative_frame`` alt + +Improvements +~~~~~~~~~~~~ + +- Updated pymavlink dependency to v2 from v1 hoping we don't fall + behind again. + +Version 2.8.0 (2016-07-15) +-------------------------- + +Bug Fixes +~~~~~~~~~ + +- Makes sure we are listening to ``HOME_LOCATION`` message, befor we + would only set home location if received by waypoints. + +Version 2.7.0 (2016-06-21) +-------------------------- + +Improvements +~~~~~~~~~~~~ + +- Adds udpin-multi support + +Version 2.6.0 (2016-06-17) +-------------------------- + +Bug Fixes +~~~~~~~~~ + +- Fixes patched mavutil sendfn + +Version 2.5.0 (2016-05-04) +-------------------------- + +Improvements +~~~~~~~~~~~~ + +- Catch and display message and attribute errors, then continue +- Improved takeoff example docs +- Deploy docs on successful merge into master (from CircleCI) +- Drone delivery example, explain port to connect +- MicroCGS example now uses SITL +- Make running examples possible on Vagrant + +Bug Fixes +~~~~~~~~~ + +- Mav type for rover was incorrect +- ``_is_mode_available`` can now handle unrecognized mode codes +- Fix broken links on companion computer page +- Fix infinite loop on channel test + +Version 2.4.0 (2016-02-29) +-------------------------- + +Bug Fixes +~~~~~~~~~ + +- Use monotonic clock for all of the internal timeouts and time + measurements +- Docs fixes + +Version 2.3.0 (2016-02-26) +-------------------------- + +New Features +~~~~~~~~~~~~ + +- PX4 compatibility improvements + +Updated Features +~~~~~~~~~~~~~~~~ + +- Documentation fixes +- PIP repository improvements +- Mode-setting API improvements +- ardupilot-solo compatibility fixes + +Version 2.2.0 (2016-02-19) +-------------------------- + +Bug Fixes +~~~~~~~~~ + +- Splits outbound messages into its own thread. +- Remove of capabilities request on HEARTBEAT listener +- Check if mode\_mapping has items before iteration + +Version 2.1.0 (2016-02-16) +-------------------------- + +New Features +~~~~~~~~~~~~ + +- Gimbal control attribute +- Autopilot version attribute +- Autopilot capabilities attribute +- Best Practice guide documentation. +- Performance test example (restructured and docs added) + +Updated Features: +~~~~~~~~~~~~~~~~~ + +Many documentation fixes: + +- Restructured documentation with Develop (Concepts) and Guide (HowTo) + sections +- Docs separated out "Connection Strings" section. +- Improved test and contribution sections. +- Updated examples and documentation to use DroneKit-Sitl for + simulation ("zero setup examples") +- Debugging docs updated with additional libraries. +- Flight Replay example fetches data from TLOG rather than droneshare +- Drone Delivery example now uses strart location for home address. +- Disabled web tests (not currently supported/used) +- Updated copyright range to include changes in 2016 + +Bug Fixes +~~~~~~~~~ + +- Numerous minor docs fixes. +- Harmonise nosetest options across each of the integration platforms +- Fix incorrect property marker for airspeed attribute + +Version 2.0.2 (2015-11-30) +-------------------------- + +Bug Fixes: +~~~~~~~~~~ + +- Updates ``requests`` dependency to work >=2.5.0 + +Version 2.0.0 (2015-11-23) +-------------------------- + +New Features: +~~~~~~~~~~~~~ + +- Renamed library and package from DroneAPI to DroneKit on pip +- DroneKit Python is now a standalone library and no longer requires + use of MAVProxy +- Connect multiple vehicles in one script by creating separate vehicle + instances +- Removed NumPy, ProtoBuf as dependencies +- Add MAVLink message listeners using ``add_message_listener`` methods +- Added ``on_attribute`` and ``on_message`` function decorator + shorthands +- Added ``mount_status``, ``system_status``, ``ekf_ok``, + ``is_armable``, ``heading`` +- Made settable ``groundspeed``, ``airspeed`` +- Moved ``dronekit.lib`` entries to root package ``dronekit`` +- Added ``parameters.set`` and ``parameters.get`` for fine-tuned + parameter access +- ``parameters`` now observable and iterable (`#442 `_) +- Added ``last_heartbeat`` attribute, updated every event loop with + time since last heartbeat (`#451 `_) +- Await attributes through ``wait_ready`` method and ``connect`` method + parameter +- Adds subclassable Vehicle class, used by ``vehicle_class`` parameter + in ``connect`` + +Updated Features: +~~~~~~~~~~~~~~~~~ + +- local\_connect renamed to connect(), accepting a connection path, + link configuration, and timeout settings +- Removed ``.set_mavrx_callback``. Use ``vehicle.on_message('*', obj)`` + methods +- Renamed ``add_attribute_observer`` methods to + ``add_attribute_listener``, etc. (`#420 `_) +- Renamed ``wait_init`` and ``wait_valid`` to ``wait_ready`` +- Split ``home_location`` is a separate attribute from ``commands`` + waypoint array +- Moved RC channels into ``.channels`` object (`#427 `_) +- Split location information into ``local_frame``, ``global_frame``, + and ``global_relative_frame`` (and removed ``is_relative``) (`#445 `_) +- Renamed ``flush`` to ``commands.upload``, as it only impacts + waypoints (`#276 `_) +- ``commands.goto`` and ``commands.takeoff`` renamed to ``simple_goto`` + and ``simple_takeoff`` + +Bug Fixes: +~~~~~~~~~~ + +- ``armed`` and ``mode`` attributes updated constantly (`#60 `_, `#446 `_) +- Parameter setting times out (`#12 `_) +- ``battery`` access can throw exception (`#298 `_) +- Vehicle.location reports incorrect is\_relative value for Copter + (`#130 `_) +- Excess arming message when already armed diff --git a/docs/about/index.rst b/docs/about/index.rst index cee9c6eb6..1d5866a3e 100644 --- a/docs/about/index.rst +++ b/docs/about/index.rst @@ -12,6 +12,7 @@ After reading, you will understand what the kit offers and the opportunities it overview release_notes + migrating license diff --git a/docs/about/license.rst b/docs/about/license.rst index a7b95cc12..1fe406e2d 100644 --- a/docs/about/license.rst +++ b/docs/about/license.rst @@ -2,7 +2,7 @@ Open Source Licence ======================= -DroneKit-Python is licensed under the *Apache License Version 2.0, January 2004 (http://www.apache.org/licenses/). This is present in the `LICENSE `_ file in the source tree, and reproduced below: +DroneKit-Python is licensed under the *Apache License Version 2.0, January 2004* (http://www.apache.org/licenses/). This is present in the `LICENSE `_ file in the source tree, and reproduced below: .. include:: ../../LICENSE :literal: \ No newline at end of file diff --git a/docs/about/migrating.rst b/docs/about/migrating.rst new file mode 100644 index 000000000..9654f9ce1 --- /dev/null +++ b/docs/about/migrating.rst @@ -0,0 +1,359 @@ +.. _migrating_dkpy2_0: + +===================== +Migrating to DKPY 2.0 +===================== + +DroneKit-Python 2.0 has undergone a significant *architectural* evolution when compared to version 1.x (the library changed from a MAVProxy extension +to a standalone Python module). The API itself remains similar, with the most important difference being that you +now need to specify the vehicle target address inside the script. + +The sections below outline the main migration areas. + +.. note:: + + *DroneKit-Python version 1.5* has now been superseded (see these links for legacy `documentation `_ + and `examples `_). + + +Installation +============ + +DKPY 2.0 is now installed from `pip` on all platforms - see :ref:`installing_dronekit` for more information. + +Installation is generally simpler than on DK 1.x because there are far fewer dependencies (both MAVProxy and numpy +are no longer needed). + +.. note:: + + * The DroneKit-Python Windows installer cannot be used for DKPY2.x (and is no longer needed). + * One implication of the reduced dependencies is that it should now be easier to use other Python distributions + (like ActivePython - although this has not been verified!) + + +Launching scripts +================= + +DroneKit-Python 2.0 apps are run from an ordinary Python command prompt. For example: + +.. code:: bash + + some_python_script.py # or `python some_python_script.py` + +.. note:: + + This contrasts with DKPY 1.x scripts, which were run from within MAVProxy using the command: + + .. code:: bash + + api start some_python_script.py + + +Code changes +============ + +This section outlines the changes you will need to make to your DroneKit-Python scripts. + +Connecting to a vehicle +----------------------- + +You must specify the target vehicle address in your script (in DKPY 1.x this was done when you launched MAVProxy). + +The code fragment below shows how you import the :py:func:`connect() ` method and use it to return a +connected :py:class:`Vehicle ` object. The address string passed to ``connect()`` takes the same +values as were passed to *MAVProxy* when setting up a connection in DKPY 1.x (in this case, a SITL instance running on the same computer). + +.. code:: python + + from dronekit import connect + + # Connect to the Vehicle (in this case a UDP endpoint) + vehicle = connect('127.0.0.1:14550', wait_ready=True) + +.. note:: + + The ``wait_ready=True`` parameter ensures that ``connect()`` won't return until + :py:attr:`Vehicle.parameters ` and most other default attributes have been + populated with values from the vehicle. Check out :py:func:`Vehicle.wait_ready() ` for more + information (this method is used by the ``connect()`` implementation). + + :py:func:`connect() ` also has arguments for setting the baud rate, + returning your own :ref:`custom vehicle classes ` and setting the length of the connection timeout. + + +After connecting, the returned ``vehicle`` can be used in exactly the same way as in DKPY 1.x. + +.. note:: + + The above code replaces DKPY 1.x code to get the Vehicle (similar to the example below): + + .. code:: python + + # Get an instance of the API endpoint + api = local_connect() + # Get the connected vehicle (currently only one vehicle can be returned). + vehicle = api.get_vehicles()[0] + + + +.. todo:: Above link to the connect method in API ref - make sure connect() is documented. + + +Connection status checks +------------------------ + +DroneKit no longer runs in *MAVProxy* so scripts don't need to monitor and act on external thread shutdown commands. + +Remove code that checks the ``api.exit`` status (note that the ``api.exit`` call below is commented out). + +.. code:: python + + while not vehicle.armed # and not api.exit: + print " Waiting for arming..." + time.sleep(1) + +.. note:: + + In fact you should delete all references to ``APIConnection`` class and its methods (``get_vehicles()``, ``exit()`` and ``stop()``). + + + + +Script completion checks +------------------------ + +Examples that might possibly have outstanding messages should call :py:func:`Vehicle.close() ` +before exiting to ensure that all messages have flushed before the script completes: + +.. code:: python + + # About to exit script + vehicle.close() + + +Command line arguments +---------------------- + +Remove any code that uses the ``local_arguments`` array to get script-local command line arguments (via MAVProxy). + +From DKPY 2.0 command line arguments are passed to ``sys.argv`` as with any other script. The examples use the +`argparse `_ module for argument parsing, but you can +use whatever method you like. + +.. note:: + + In DKPY 1.x the script's ``sys.argv`` values were the values passed to MAVProxy when it was + started. To access arguments passed to the script from *MAVProxy* you used the ``local_arguments`` array. + For example if you started a script as shown below: + + .. code:: bash + + api start my_script.py 101 + + Then you would read the integer in your code using + + .. code:: python + + my_argument = int(local_arguments[0]) + + +.. todo:: This addition closes https://github.com/dronekit/dronekit-python/issues/13 + + +Current script directory +------------------------ + +DroneKit-Python v1.x passed a global property ``load_path`` to any executed file containing the +directory in which the script was running. This is no longer needed in version 2 and has been removed. + +Instead, use normal Python methods for getting file system information: + +.. code:: python + + import os.path + full_directory_path_of_current_script = os.path.dirname(os.path.abspath(__file__)) + + +Vehicle.location +---------------- + +DroneKit-Python v1.x had a ``Vehicle.location`` attribute which provided latitude and longitude information in the +global frame, and altitude either relative to sea-level or the home location (depending on the value of its ``is_relative`` member). + +DKPY2.0 uses and attribute with the same name to provide location in +global, global-relative and local (NED) frames: + +.. code-block:: python + + print "Global Location: %s" % vehicle.location.global_frame + print "Global Location (relative altitude): %s" % vehicle.location.global_relative_frame + print "Local Location: %s" % vehicle.location.local_frame + +For more information see: :py:attr:`Vehicle.location `, +:py:attr:`Vehicle.location.global_frame `, +:py:attr:`Vehicle.location.global_relative_frame `, +:py:attr:`Vehicle.location.local_frame `, and :ref:`vehicle-information`. + + +Takeoff and movement commands +----------------------------- + +DroneKit-Python v1.x provided guided mode takeoff and movement methods ``Vehicle.commands.takeoff()`` +and ``Vehicle.commands.goto()``. + +DKPY2.0 instead provides :py:func:`Vehicle.simple_takeoff ` and +:py:func:`Vehicle.simple_goto `. These are the same as the old methods +except that ``simple_goto`` allows you to optionally set the default target groundspeed and airspeed. + + +:py:attr:`Vehicle.airspeed ` and +:py:attr:`Vehicle.groundspeed ` are now settable values. Call these to +set the default target speed used when moving with :py:func:`Vehicle.simple_goto ` +(or other position-based movement commands). + + +.. _migrating_dkpy2_0_home_location: + +Home location +------------- + +DroneKit-Python 1.x code retrieved the home location from the first element in :py:attr:`Vehicle.commands `. +This code must be replaced with the DroneKit-Python 2.x :py:attr:`Vehicle.home_location ` attribute. + +.. tip:: + + Even though the home location is no longer returned as the first waypoint in :py:attr:`Vehicle.commands `, + you will still need to download the commands in order to populate the value of + :py:attr:`Vehicle.home_location `. + + +Missions and Waypoints +---------------------- + +The API for working with missions has been improved and made significantly more robust. + +One of the major changes is that the :py:attr:`Vehicle.commands ` list no +longer includes the :ref:`home location ` waypoint in the 0th +index. Another change is that we now wait for command download to complete using +:py:attr:`Vehicle.commands.wait_ready() `. + +All the known bugs have been fixed. It is now much easier to download, clear, and add items to the mission +because there is no need to work around race conditions and other issues with the API. + +For more information see :ref:`auto_mode_vehicle_control`. + + +Observing attribute changes +--------------------------- + +The DroneKit-Python 1.x observer function ``vehicle.add_attribute_observer`` has been replaced by +:py:func:`Vehicle.add_attribute_listener() ` or +:py:func:`Vehicle.on_attribute() ` in DKYP2.x, and ``Vehicle.remove_attribute_observer`` +has been repaced by :py:func:`remove_attribute_listener() `. + +The main difference is that the callback function now takes three arguments (the vehicle object, attribute name, attribute value) +rather than just the attribute name. This allows you to more easily write callbacks that support attribute-specific and +vehicle-specific handling and means that you can get the new value from the callback attribute rather than by re-querying +the vehicle. + +.. note:: + + The difference between :py:func:`Vehicle.add_attribute_listener() ` and + :py:func:`Vehicle.on_attribute() ` is that attribute listeners added using + :py:func:`Vehicle.on_attribute() ` cannot be removed (while ``on_attribute()`` + has a more elegant syntax). + +A few attributes have been modified so that they only notify when the value changes: +:py:func:`Vehicle.system_status `, +:py:attr:`Vehicle.armed `, and +:py:attr:`Vehicle.mode `. Users no longer need to add caching code +for these attributes in their listeners. +Attributes that provide "streams" of information (i.e. sensor output) remain unchanged. + +.. note:: + + If you're :ref:`creating your own attributes ` this caching is trivially + provided using the ``cache=True`` argument to + :py:func:`Vehicle.notify_attribute_listeners() `. + +See :ref:`vehicle_state_observe_attributes` for more information. + + +Parameter changes +----------------- + +In DKPY2 you can now :ref:`observe ` parameters in order to +be notified of changes, and also :ref:`iterate ` +:py:attr:`Vehicle.parameters ` to get a list of off the supported +values in the connected vehicle. + +In addition, the code to download parameters and keep information in sync with the vehicle +is now a lot more robust. + + + + + +Intercepting MAVLink Messages +----------------------------- + +DroneKit-Python 1.x used ``Vehicle.set_mavlink_callback()`` and ``Vehicle.unset_mavlink_callback`` +to set/unset a callback function that was invoked for every single mavlink message. + +In DKPY2 this has been replaced by the :py:func:`Vehicle.on_message() ` +decorator, which allows you to specify a callback function that will be invoked for a single message +(or all messages, by specifying the message name as the wildcard string '``*``'). + +.. tip:: + + :py:func:`Vehicle.on_message() ` is used in core DroneKit code for + message capture and to create ``Vehicle`` attributes. + + The API also adds :py:func:`Vehicle.add_message_listener() ` + and :py:func:`Vehicle.remove_message_listener() `. + These can be used instead of :py:func:`Vehicle.on_message() ` when you need to be + able to *remove* an added listener. Typically you won't need to! + +See :ref:`mavlink_messages` for more information. + + +New attributes +-------------- + +In addition to the :ref:`home_location `, +a few more attributes have been added, including: +:py:func:`Vehicle.system_status `, +:py:func:`Vehicle.heading `, +:py:func:`Vehicle.mount_status `, +:py:func:`Vehicle.ekf_ok `, +:py:func:`Vehicle.is_armable `, +:py:func:`Vehicle.last_heartbeat `. + + +Channel Overrides +----------------- + +.. warning:: + + Channel overrides (a.k.a “RC overrides”) are highly discommended (they are primarily implemented for + simulating user input and when implementing certain types of joystick control). + +DKPY v2 replaces the ``vehicle.channel_readback`` attribute with +:py:attr:`Vehicle.channels ` (and the :py:class:`Channels ` +class) and the ``vehicle.channel_override`` attribute with +:py:attr:`Vehicle.channels.overrides ` +(and the :py:class:`ChannelsOverrides ` class). + +Documentation and example code for how to use the new API are provided in :ref:`example_channel_overrides`. + + + + +Debugging +========= + +DroneKit-Python 1.x scripts were run in the context of a MAVProxy. This made them difficult to debug because you had to +instrument your code in order to launch the debugger, and debug messages were interleaved with MAVProxy output. + +Debugging on DroneKit-Python 2.x is much easier. Apps are now just standalone scripts, and can be debugged +using standard Python methods (including the debugger/IDE of your choice). diff --git a/docs/about/overview.rst b/docs/about/overview.rst index f0221b13f..05b814ffb 100644 --- a/docs/about/overview.rst +++ b/docs/about/overview.rst @@ -2,9 +2,10 @@ About DroneKit ============== -DroneKit-Python is DroneKit's main API for *Air Computing* — allowing developers to create apps that run on an onboard :ref:`companion computer ` and communicate with the `ArduPilot `_ flight controller using a low-latency link. *Air apps* can significantly enhance the autopilot, adding greater intelligence to vehicle behaviour, and performing tasks that are computationally intensive or time-sensitive (for example, computer vision, path planning, or 3D modelling). +DroneKit-Python allows developers to create apps that run on an onboard :ref:`companion computer ` and communicate with the `ArduPilot `_ flight controller using a low-latency link. Onboard apps can significantly enhance the autopilot, adding greater intelligence to vehicle behaviour, and performing tasks that are computationally intensive or time-sensitive (for example, computer vision, path planning, or 3D modelling). DroneKit-Python can also be used for ground station apps, communicating with vehicles over a higher latency RF-link. + +The API communicates with vehicles over MAVLink. It provides programmatic access to a connected vehicle's telemetry, state and parameter information, and enables both mission management and direct control over vehicle movement and operations. -The API communicates with "locally connected" vehicles over MAVLink. It provides programmatic access to a connected vehicle's telemetry, state and parameter information, and enables both mission management and direct control over vehicle movement and operations. Open source community @@ -13,14 +14,19 @@ Open source community DroneKit-Python is an open source and community-driven project. You can find all the source code on `Github here `_ and check out our permissive :doc:`Apache v2 Licence `. -If you want to join the community, then see our :doc:`contributing section ` for lots of ideas on how you can help. +If you want to join the community, then see our :doc:`contributing section <../contributing/index>` for lots of ideas on how you can help. Compatibility ============= -DroneKit-Python is compatible with all vehicles using the `MAVLink protocol `_ (including most vehicles made by 3DR and other members of the `DroneCode foundation `_). It runs on Linux, Mac OS X, or Windows. +DroneKit-Python is compatible with vehicles that communicate using the `MAVLink protocol `_ (including most vehicles made by `3DR `_ and other members of the `DroneCode foundation `_). It runs on Linux, Mac OS X, or Windows. + +.. note:: + + DroneKit-Python is validated against, and hence *most compatible* with, the `ArduPilot UAV Platform `_. + Vehicles running other autopilots may be be less compatible due to differences in adhererence/interpretation of the MAVLink specification. + Please report any autopilot-specific issues `on Github here `_. -In addition to "Air apps", it can be used to create apps that run on a desktop ground station and communicate with ArduPilot over a higher latency RF-link. API features @@ -29,12 +35,12 @@ API features The API provides classes and methods to: -- Get a list of connected vehicles. +- Connect to a vehicle (or multiple vehicles) from a script - Get and set vehicle state/telemetry and parameter information. - Receive asynchronous notification of state changes. -- Create and manage waypoint missions (AUTO mode). - Guide a UAV to specified position (GUIDED mode). - Send arbitrary custom messages to control UAV movement and other hardware (GUIDED mode). +- Create and manage waypoint missions (AUTO mode). - Override RC channel settings. A complete API reference is available :ref:`here `. @@ -45,7 +51,7 @@ Technical support This documentation is a great place to get started with developing DroneKit Python APIs. -If you run into problems, the `best place to ask questions is Stack Overflow `_. +If you run into problems, the best place to ask questions is the `DroneKit-Python Forum `_. If your problem turns out to be a bug, then it should be `posted on Github `_. diff --git a/docs/about/release_notes.rst b/docs/about/release_notes.rst index fb4c44d60..c85bc804c 100644 --- a/docs/about/release_notes.rst +++ b/docs/about/release_notes.rst @@ -6,7 +6,7 @@ This page contains the release notes for DroneKit-Python ``minor`` and ``major`` .. note:: - DroneKit-Python marks releases using the ``major.minor.patch`` release numbering convention, where ``patch`` is used to denote only bug fixes, ``minor`` is used for releases with new features, and ``major`` indicates the release contains significant API changes. Patch releases are not separately documented here. + DroneKit-Python marks releases using the ``major.minor.patch`` release numbering convention, where ``patch`` is used to denote only bug fixes, ``minor`` is used for releases with new features, and ``major`` indicates the release contains significant API changes. @@ -23,12 +23,14 @@ For information about all past releases, please see `this link on Github `_ for a list of all releases available on PyPI. diff --git a/docs/automodule.rst b/docs/automodule.rst index 28b53c593..ef3dd5414 100644 --- a/docs/automodule.rst +++ b/docs/automodule.rst @@ -5,10 +5,9 @@ DroneKit-Python API Reference ============================= -.. automodule:: dronekit.lib +.. automodule:: dronekit :members: :inherited-members: - :exclude-members: Mission, get_mission, ConnectionInfo, web_connect, AuthInfo, delete @@ -19,5 +18,4 @@ DroneKit-Python API Reference - .. todolist:: \ No newline at end of file diff --git a/docs/conf.py b/docs/conf.py index f569024a0..2509fc2de 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -41,17 +41,17 @@ master_doc = 'index' # General information about the project. -project = u'DroneKit Air: Python' -copyright = u'2015, 3D Robotics' +project = u'DroneKit Python' +copyright = u'2015-2016, 3D Robotics' # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the # built documents. # # The short X.Y version. -version = '0.5' +version = '2.4' # The full version, including alpha/beta/rc tags. -release = '0.5' +release = '2.4.0' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. diff --git a/docs/contributing/contributions_api.rst b/docs/contributing/contributions_api.rst index 7e137abc5..9a3c97b7f 100644 --- a/docs/contributing/contributions_api.rst +++ b/docs/contributing/contributions_api.rst @@ -6,16 +6,16 @@ Contributing to the API This article provides a high level overview of how to contribute changes to the DroneKit-Python source code. -.. tip:: +.. tip:: - We highly recommend that changes and ideas are `discussed with the project team - `_ before starting work! + We highly recommend that changes and ideas are `discussed with the project team + `_ before starting work! Submitting changes ================== -Contributors should fork the main `dronekit/dronekit-python/ `_ +Contributors should fork the main `dronekit/dronekit-python/ `_ repository and contribute changes back to the project master branch using pull requests * Changes should be :ref:`tested locally ` before submission. @@ -30,21 +30,36 @@ repository and contribute changes back to the project master branch using pull r Test code ========= -Test code should be used to verify new and changed functionality. There are three test suites in DroneKit-Python: +There are three test suites in DroneKit-Python: -* **Unit tests** (:file:`tests/unit`) — verify all code paths of the API. +* **Unit tests** (:file:`tests/unit`) — verify all code paths of the API. * **Integration tests** (:file:`tests/sitl`) — verify real-world code, examples, and documentation as they would perform in a real environment. -* **Web client tests** (:file:`tests/web`) — specifically verify the Python library's capability to talk to `DroneKit Cloud `_. + +Test code should be used to verify new and changed functionality. New tests should: + +#. Verify all code paths that code can take. +#. Be concise and straightforward. +#. Be documented. + Setting up local testing ------------------------ -The links below provide information on how to set up a development environment on your development computer. Changes to DroneKit can then be tested locally. +Follow the links below to set up a development environment on your Linux or Windows computer. * :ref:`dronekit_development_linux` * :ref:`dronekit_development_windows` -Several of the test suites use `nose `_, a Python library for writing test scripts and a command line tool for running these. When setting up your dev environment, all test dependencies will have been installed (via :file:`requirements.txt`). +The tests require additional pip modules, including `nose `_, a +Python library and tool for writing and running test scripts. These can be installed separately using either of the commands below: + +.. code:: bash + + # Install just the additional requirements for tests + pip install requests nose mock + + # (or) Install all requirements for dronekit, tests, and building documentation + pip install -r requirements.txt For several tests, you may be required to set an **environment variable**. In your command line, you can set the name of a variable to equal a value using the following invocation, depending on your OS: @@ -57,41 +72,53 @@ For several tests, you may be required to set an **environment variable**. In yo Unit tests ---------- -Unit tests use *nosetests*. On any OS, enter the following command on a terminal/prompt to run the unit tests (and display a summary of the results): +All new features should be created with accompanying unit tests. + +DroneKit-Python unit tests are based on the `nose `_ test framework, +and use `mock `_ to simulate objects and APIs and +ensure correct results. + +To run the tests and display a summary of the results (on any OS), +navigate to the **dronekit-python** folder and enter the following +command on a terminal/prompt: .. code:: bash - cd dronekit-python - nosetests tests/unit + nosetests dronekit.test.unit + -For unit tests, `mock `_ is used to simulate objects and APIs and ensure correct results. Writing a new unit test ^^^^^^^^^^^^^^^^^^^^^^^ -Good unit tests should: +Create any file named :file:`test_XXX.py` in the :file:`tests/unit` folder to add it as a test. +Feel free to copy from existing tests to get started. When *nosetests* is run, it will add your new test to its summary. -#. Accompany all new features that are written. -#. Verify all code paths that code can take. -#. Be concise and straightforward. +Tests names should be named based on their associated Github issue (for example, +``test_12.py`` for `issue #12 `_) +or describe the functionality covered (for example, ``test_waypoints.py`` +for a unit test for the waypoints API). -Create any file named :file:`test_XXX.py` in the :file:`tests/unit` folder to add it as a test. Feel free to copy from existing tests to get started. When *nosetests* is run, it will add your new test to its summary. +Use assertions to test your code is consistent. You can use the built-in Python ``assert`` macro as well as ``assert_equals`` and ``assert_not_equals`` +from the ``notestools`` module: -Tests names should refer directly to a Github issue (for example, ``test_12.py`` would refer to `issue #12 `_ or describe fully what functionality they encompass (for example, ``test_waypoints.py`` would describe a unit test for the waypoints API). +.. note:: -Avoiding printing any data from your test. Instead, use assertions to test your code is consistent. You can use the built-in Python ``assert`` macro as well as ``assert_equals`` from the ``nose.tools`` module: + Avoiding printing any data from your test! .. code:: python - from nose.tools import assert_equals + from nose.tools import assert_equals, assert_not_equals def test_this(the_number_two): assert the_number_two > 0, '2 should be greater than zero!' - assert_equals(the_number_two, 2, '2 should equal two!' + assert_equals(the_number_two, 2, '2 should equal two!') + assert_not_equals(the_number_two, 1, '2 should equal one!') Please add documentation to each test function describing what behavior it verifies. + Integration tests ----------------- @@ -100,7 +127,18 @@ Integrated tests use a custom test runner that is similar to *nosetests*. On any .. code:: bash cd dronekit-python - python -um tests.sitl + nosetests dronekit.test.sitl + +You can choose to run a specific tests. The example below shows how to run +**\dronekit-python\dronekit\test\sitl\test_12.py**. + +.. code:: bash + + nosetests dronekit.test.sitl.test_12 + + +Configuring the test environment +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Integrated tests use the SITL environment to run DroneKit tests against a simulated Copter. Because these tests emulate Copter in real-time, you can set several environment variables to tweak the environment that code is run in: @@ -108,11 +146,7 @@ Integrated tests use the SITL environment to run DroneKit tests against a simula #. ``TEST_RATE`` - Sets framerate. Default is ``TEST_RATE=200`` for copter, 50 for rover, 50 for plane. #. ``TEST_RETRY`` - Retry failed tests. Default is ``TEST_RETRY=1``. This is useful if your testing environment generates inconsistent success rates because of timing. -You can choose to test specific files by passing them as arguments: -.. code:: bash - - python -um tests.sitl test_1.py test2_.py ... Writing a new integration test ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ @@ -123,23 +157,40 @@ Integration tests should be written or improved whenever: #. Example code or documentation has been added. #. A feature could not be tested by unit tests alone (e.g. timing issues, mode changing, etc.) -You can write a new integrated test by adding a file with the naming scheme :file:`test_XXX.py` to the :file:`tests/sitl` directory. In this file, functions with the prefix ``test_`` will be called with the ``local_connect`` parameter. For example: +You can write a new integrated test by adding (or copying) a file with the naming scheme :file:`test_XXX.py` to the :file:`tests/sitl` directory. + +Tests names should be named based on their associated Github issue (for example, +``test_12.py`` for `issue #12 `_) +or describe the functionality covered (for example, ``test_waypoints.py`` +for an integration test for the waypoints API). + +Tests should minimally use the imports shown below and decorate test functions with ``@with_sitl`` +(this sets up the test and passes in a connection string for SITL). .. code:: python - from testlib import assert_equals + from dronekit import connect + from dronekit.test import with_sitl + from nose.tools import assert_equals, assert_not_equals + + @with_sitl + def test_something(connpath): + vehicle = connect(connpath) + + # Test using assert, assert_equals and assert_not_equals + ... + + vehicle.close() - def test_parameters(local_connect): - v = local_connect().get_vehicles()[0] - # Simple parameter checks - assert_equals(type(v.parameters['THR_MIN']), float) +Use assertions to test your code is consistent. You can use the built-in Python ``assert`` macro as well as ``assert_equals`` and ``assert_not_equals`` +from the ``testlib`` module: -This checks to see that the parameter object is of type `float`. +.. note:: + + Avoiding printing any data from your test! -Tests names should refer directly to a Github issue (for example, ``test_12.py`` would refer to `issue #12 `_ or describe fully what functionality they encompass (for example, ``test_waypoints.py`` would describe a unit test for the waypoints API). -Avoiding printing any data from your test. Instead, use assertions to test your code is consistent. You can use the built-in Python ``assert`` macro as well as ``assert_equals`` from the ``testlib`` module: .. code:: python @@ -150,21 +201,3 @@ Avoiding printing any data from your test. Instead, use assertions to test your assert_equals(the_number_two, 2, '2 should equal two!' Please add documentation to each test function describing what behavior it verifies. - -Web client tests ----------------- - -.. warning:: - - The web client library is being rewritten. Please `discuss with the project team - `_ if you intend to develop with or for the present version of the web client. - -Web client tests use *nosetests*. To run these, you will need to sign up for API keys from `cloud.dronekit.io `_. -With these, export a variable named ``DRONEAPI_KEY`` with a value in the format ``.`` to your environment. - -On any OS, enter the following command on a terminal/prompt to run the web-client tests (and display summary results): - -.. code:: bash - - cd dronekit-python - nosetests tests/web diff --git a/docs/contributing/developer_setup_linux.rst b/docs/contributing/developer_setup_linux.rst index 00ffeffce..6bf8a965c 100644 --- a/docs/contributing/developer_setup_linux.rst +++ b/docs/contributing/developer_setup_linux.rst @@ -5,7 +5,8 @@ Building DroneKit-Python on Linux =================================== The setup for *developing* DroneKit-Python on Linux is almost the same as for *using* -DroneKit-Python. We therefore recommend that you start by following the instructions in :ref:`Getting Started `. +DroneKit-Python. We therefore recommend that you start by following the instructions in +:ref:`installing_dronekit`. When you've got DroneKit and a vehicle (simulated or real) communicating, you can then build and install your own fork of DroneKit, as discussed below. diff --git a/docs/contributing/developer_setup_windows.rst b/docs/contributing/developer_setup_windows.rst index 81d7e90b1..b9d65ef25 100644 --- a/docs/contributing/developer_setup_windows.rst +++ b/docs/contributing/developer_setup_windows.rst @@ -6,60 +6,12 @@ Building DroneKit-Python on Windows This article shows how to set up an environment for *developing* DroneKit-Python on Windows. -.. tip:: - - If you just want to *use* DroneKit-Python on Windows then easiest way to get started is to use the - :ref:`Windows Installer <_get_started_install_dk_windows>`. The installer is rebuilt with every patch - release, so you can always be up to date with the latest features and bug fixes. - Install DroneKit using WinPython command line ============================================= -First set up a command line DroneKit-Python installation using *WinPython*. This Python distribution already includes most of the needed dependencies (though you will need remove *python-dateutil* as the installation comes bundled with a version that does not work with *DroneKit*). - -The steps to install this package and the most important add-on modules are: - -#. Download and run the correct `WinPython installer `_ (**v2.7**) for your platform (win32 vs win64). - - * Run the installer as an administrator (**Right-click** on file, select **Run as Administrator**). - * When prompted for the destination location, specify **C:\Program Files (x86)** - (the default location is under the **Downloads** folder). - -#. Register the Python that came from *WinPython* as the preferred interpreter for your machine: - Open the folder where you installed WinPython, run *WinPython Control Panel* and choose **Advanced/Register Distribution**. - - .. image:: http://dev.ardupilot.com/wp-content/uploads/sites/6/2014/03/Screenshot-from-2014-09-03-083816.png - -#. Install DroneKit-Python and its remaining dependencies (including `MAVProxy `_) from the public PyPi repository: - - Open the *WinPython Command Prompt* and run the following two commands: - - .. code:: bash - - pip uninstall python-dateutil - pip install dronekit - -The dependencies above are all that are required to build DroneKit-Python and the *MAVProxy command line* (i.e. the minimum needed for testing). -If you also want the *MAVProxy console* and map install: - -#. OpenCV - - * `Download and install OpenCV version 2.4 for Windows `_ (this can be extracted anywhere) - * Copy/paste the file :file:`cv2.pyd` from :file:`OpenCV\\build\\python\\2.7\\x64\\` to :file:`site_packages` - on your Python installation (e.g. :file:`\\python-2.7.6.amd64\\Lib\\site-packages`). -#. WxPython - - * `Download and install WxPython `_. Make sure the target - path is your WinPython installation. -#. Console - - * Open the WinPython command prompt and enter: - - .. code:: bash - - pip install console +First set up a command line DroneKit-Python installation. We recommend *WinPython* or *ActivePython*, as discussed in :ref:`installing_dronekit`. @@ -78,7 +30,7 @@ Fetch and build DroneKit source python setup.py install - + Updating DroneKit ================= diff --git a/docs/guide/MissionPlanner_ConnectPort.png b/docs/develop/MissionPlanner_ConnectPort.png similarity index 100% rename from docs/guide/MissionPlanner_ConnectPort.png rename to docs/develop/MissionPlanner_ConnectPort.png diff --git a/docs/develop/best_practice.rst b/docs/develop/best_practice.rst new file mode 100644 index 000000000..dec8c0640 --- /dev/null +++ b/docs/develop/best_practice.rst @@ -0,0 +1,246 @@ +.. _best_practices: + +============== +Best Practices +============== + +This guide provides a broad overview of how to use the API, its main programming idioms +and best practices. More detail information is linked from each section. + + +General considerations +====================== + +DroneKit-Python communicates with vehicle autopilots using the MAVLink protocol, +which defines how commands, telemetry and vehicle settings/parameters +are sent between vehicles, companion computers, ground stations and other systems on +a MAVLink network. + +Some general considerations from using this protocol are: + +* Messages and message acknowledgments are not guaranteed to arrive (the protocol is not "lossless"). +* Commands may be silently ignored by the Autopilot if it is not in a state where it can + safely act on them. +* Command acknowledgment and completion messages are not sent in most cases + (and if sent, may not arrive). +* Commands may be interrupted before completion. +* Autopilots may choose to interpret the protocol in slightly different ways. +* Commands can arrive at the autopilot from multiple sources. + +Developers should code defensively. Where possible: + +* Check that a vehicle is in a state to obey a command (for example, + poll on :py:func:`Vehicle.is_armable ` + before trying to arm the vehicle). +* Don't assume that a command has succeeded until the changed behaviour is observed. + In particular we recommend a launch sequence where you check that the mode and arming + have succeeded before attempting to take off. +* Monitor for state changes and react accordingly. + For example, if the user changes the mode from ``GUIDED`` your script should + stop sending commands. +* Verify that your script can run inside the normal latency limits for message passing + from the vehicle and tune any monitoring appropriately. + + +Connecting +========== + +In most cases you'll use the normal way to :ref:`connect to a vehicle `, +setting ``wait_ready=True`` to ensure that the vehicle is already populated with attributes +when the :py:func:`connect() ` returns: + +.. code:: python + + from dronekit import connect + + # Connect to the Vehicle (in this case a UDP endpoint) + vehicle = connect('REPLACE_connection_string_for_your_vehicle', wait_ready=True) + +The ``connect()`` call will sometimes fail with an exception. +Additional information about an exception can be obtained by +running the connect within a ``try-catch`` block as shown: + +.. code-block:: python + + import dronekit + import socket + import exceptions + + + try: + dronekit.connect('REPLACE_connection_string_for_your_vehicle', heartbeat_timeout=15) + + # Bad TCP connection + except socket.error: + print 'No server exists!' + + # Bad TTY connection + except exceptions.OSError as e: + print 'No serial exists!' + + # API Error + except dronekit.APIException: + print 'Timeout!' + + # Other error + except: + print 'Some other error!' + +.. tip:: + + The default ``heartbeat_timeout`` on connection is 30 sections. Usually a connection will + succeed quite quickly, so you may wish to reduce this in the ``connect()`` method as shown in the + code snippet above. + +If a connection succeeds from a ground station, but not from DroneKit-Python it may be that your baud +rate is incorrect for your hardware. This rate can also be set in the ``connect()`` method. + + +Launch sequence +=============== + +Generally you should use the standard launch sequence described in :doc:`../guide/taking_off`: + +* Poll on :py:func:`Vehicle.is_armable ` + until the vehicle is ready to arm. +* Set the :py:attr:`Vehicle.mode ` to ``GUIDED`` +* Set :py:attr:`Vehicle.armed ` to ``True`` and + poll on the same attribute until the vehicle is armed. +* Call :py:func:`Vehicle.simple_takeoff ` + with a target altitude. +* Poll on the altitude and allow the code to continue only when it is reached. + +The approach ensures that commands are only sent to the vehicle when it is able +to act on them (e.g. we know :py:func:`Vehicle.is_armable ` +is ``True`` before trying to arm, we know +:py:attr:`Vehicle.armed ` is ``True`` before we take off). +It also makes debugging takeoff problems a lot easier. + + +Movement commands +================= + +DroneKit-Python provides :py:func:`Vehicle.simple_goto ` for moving to a specific position (at a defined speed). It is also possible to control movement by sending commands to specify the vehicle's :ref:`velocity components `. + +.. note:: + + As with :py:func:`Vehicle.simple_takeoff `, movement + commands are asynchronous, and will be interrupted if another command arrives + before the vehicle reaches its target. Calling code should block and wait (or + check that the operation is complete) before preceding to the next command. + +For more information see: :ref:`guided_mode_copter`. + + +Vehicle information +=================== + +Vehicle state information is exposed through vehicle *attributes* which can be read and observed (and in some cases written) +and vehicle settings which can be read, written, iterated and observed using *parameters* (a special attribute). All the attributes are documented in :doc:`../guide/vehicle_state_and_parameters`. + +Attributes are populated by MAVLink messages from the vehicle. +Information read from an attribute may not precisely reflect the actual value on the vehicle. Commands sent +to the vehicle may not arrive, or may be ignored by the autopilot. + +If low-latency is critical, we recommend you verify that the update rate is achievable and +perhaps modify script behaviour if :py:attr:`Vehicle.last_heartbeat ` falls outside +a useful range. + +When setting attributes, poll their values to confirm that they have changed. This applies, in particular, +to :py:attr:`Vehicle.armed ` and :py:attr:`Vehicle.mode `. + + + +Missions and waypoints +====================== + +DroneKit-Python can also :ref:`create and modify autonomous missions `. + +While it is possible to construct DroneKit-Python apps by dynamically constructing missions "on the fly", we recommend you use guided mode for Copter apps. This generally results in a better experience. + +.. tip:: + + If a mission command is not available in guided mode, + it can be useful to switch to a mission and call it, then change + back to normal guided mode operation. + + +Monitor and react to state changes +================================== + +Almost all attributes can be observed - see :ref:`vehicle_state_observe_attributes` for more information. + +Exactly what state information you observe, and how you react to it, depends on your particular script: + +* Most standalone apps should monitor the :py:func:`Vehicle.mode ` and + stop sending commands if the mode changes unexpectedly (this usually indicates + that the user has taken control of the vehicle). +* Apps might monitor :py:func:`Vehicle.last_heartbeat ` + and could attempt to reconnect if the value gets too high. +* Apps could monitor :py:func:`Vehicle.system_status ` + for ``CRITICAL`` or ``EMERGENCY`` in order to implement specific emergency handling. + + +Sleep the script when not needed +================================ + +Sleeping your script can reduce the CPU overhead. + +For example, at low speeds you might only need to check whether you've reached a target every few seconds. +Using ``time.sleep(2)`` between checks will be more efficient than checking more often. + + +Exiting a script +================ + +Scripts should call :py:func:`Vehicle.close() ` +before exiting to ensure that all messages have flushed before the script completes: + +.. code:: python + + # About to exit script + vehicle.close() + + +Subclass Vehicle +===================================== + +If you need to use functionality that is specific to particular hardware, we +recommend you subclass :py:class:`Vehicle ` and pass this new class into +:py:func:`connect() `. + +:doc:`../examples/create_attribute` shows how you can do this. + + + + +Debugging +========= + +DroneKit-Python apps are ordinary standalone Python scripts, and can be :doc:`debugged using standard Python methods <../guide/debugging>` (including the debugger/IDE of your choice). + + +Launching scripts +================= + +Scripts are run from an ordinary Python command prompt. For example: + +.. code:: bash + + python some_python_script.py [arguments] + +Command line arguments are passed into the script as ``sys.argv`` variables (the normal) +and you can use these directly or via an argument parser (e.g. +`argparse `_). + + +Current script directory +======================== + +You can use normal Python methods for getting file system information: + +.. code-block:: python + + import os.path + full_directory_path_of_current_script = os.path.dirname(os.path.abspath(__file__)) + diff --git a/docs/develop/coding_standards.rst b/docs/develop/coding_standards.rst new file mode 100644 index 000000000..36828a5c5 --- /dev/null +++ b/docs/develop/coding_standards.rst @@ -0,0 +1,14 @@ +.. _coding_standards: + +========================== +Coding Standards +========================== + +DroneKit-Python does not impose (or recommend) a particular set of coding standards +for third party code. + +Internally we run the `YAPF formatter `_ +on major releases and we expect contributors to copy the patterns used in similar +code within the existing code base. + + \ No newline at end of file diff --git a/docs/guide/companion-computers.rst b/docs/develop/companion-computers.rst similarity index 54% rename from docs/guide/companion-computers.rst rename to docs/develop/companion-computers.rst index 2bc19c4da..f18dcb2b7 100644 --- a/docs/guide/companion-computers.rst +++ b/docs/develop/companion-computers.rst @@ -15,21 +15,21 @@ The following computing platforms are known to work with DroneKit, and are suppo RaspberryPi ----------- -* `Communicating with Raspberry Pi via MAVLink `_ -* `Making a Mavlink WiFi bridge using the Raspberry Pi `_ +* `Communicating with Raspberry Pi via MAVLink `_ +* `Making a Mavlink WiFi bridge using the Raspberry Pi `_ Intel Edison ------------ -* `Edison for drones `_ +* `Edison for drones `_ BeagleBoneBlack --------------- -* `BeaglePilot `_ +* `BeaglePilot `_ Odroid ------ -* `Communicating with ODroid via MAVLink `_ -* `ODroid Wifi Access Point for sharing files via Samba `_ +* `Communicating with ODroid via MAVLink `_ +* `ODroid Wifi Access Point for sharing files via Samba `_ diff --git a/docs/develop/index.rst b/docs/develop/index.rst new file mode 100644 index 000000000..250992942 --- /dev/null +++ b/docs/develop/index.rst @@ -0,0 +1,24 @@ +======================== +Developing with DroneKit +======================== + +DroneKit-Python is primarily intended for use on Linux-based :doc:`companion-computers` that travel +on a vehicle and communicate with the autopilot via a serial port. It can also be used +on ground-based computers running Linux, Windows or Mac OSX (communicating using WiFi or a telemetry radio). + +During development you'll generally run it on a development computer, communicating with a +:doc:`simulated vehicle` running on the same machine (via a UDP connection). + +This section contains topics explaining how to develop with DroneKit-Python, +covering subjects like installation, setting up the target vehicle or simulator, best practices +and coding standards. + + +.. toctree:: + :maxdepth: 1 + + installation + companion-computers + Simulated Vehicle + best_practice + coding_standards \ No newline at end of file diff --git a/docs/develop/installation.rst b/docs/develop/installation.rst new file mode 100644 index 000000000..bcfe09f64 --- /dev/null +++ b/docs/develop/installation.rst @@ -0,0 +1,49 @@ +.. _installing_dronekit: + +=================== +Installing DroneKit +=================== + +DroneKit-Python can be installed on a Linux, Mac OSX, or Windows computer that +has Python 2.7 or Python 3 installed and can install Python packages from the Internet. + +It is installed from **pip** on all platforms: + +.. code-block:: bash + + pip install dronekit + + +**Installation notes:** + +* Install `dronekit` with `pip` inside a virtualenv: + + .. code-block:: bash + + python3 -m venv .venv + . .venv/bin/activate + pip install dronekit + +* On Linux you may need to first install **pip** and **python-dev**: + + .. code-block:: bash + + sudo apt-get install python-pip python-dev + + Alternatively, you can use the `ensurepip` module to install or upgrade Pip on your system: + + + .. code-block:: bash + + python -m ensurepip --upgrade + +* :doc:`companion-computers` are likely to run on stripped down versions of Linux. Ensure + you use a variant that supports Python 2.7 and can install Python packages from the Internet. +* Windows does not come with Python by default, but there are + `many distributions available `_. + We have tested against: + + * `WinPython 2.7 64bit `_ (see + `these instructions for installation and registration `_). This is the most tested version. + * `ActiveState ActivePython 2.7 `_. +* Python 3 is fully supported. diff --git a/docs/develop/sitl_setup.rst b/docs/develop/sitl_setup.rst new file mode 100644 index 000000000..8da0c3871 --- /dev/null +++ b/docs/develop/sitl_setup.rst @@ -0,0 +1,200 @@ +.. _sitl_setup: + +===================================== +Setting up a Simulated Vehicle (SITL) +===================================== + +The `SITL (Software In The Loop) `_ +simulator allows you to create and test DroneKit-Python apps without a real vehicle (and from the comfort of +your own developer desktop!). + +SITL can run natively on Linux (x86 architecture only), Mac and Windows, or within a virtual machine. It can be +installed on the same computer as DroneKit, or on another computer on the same network. + +The sections below explain how to install and run SITL, and how to connect to DroneKit-Python and Ground +Stations at the same time. + + +.. _dronekit_sitl: + +DroneKit-SITL +============= + +DroneKit-SITL is the simplest, fastest and easiest way to run SITL on Windows, Linux (x86 architecture only), or Mac OS X. +It is installed from Python's *pip* tool on all platforms, and works by downloading and running pre-built +vehicle binaries that are appropriate for the host operating system. + +This section provides an overview of how to install and use DroneKit-SITL. For more information, see +the `project on Github `_. + +.. note:: + + DroneKit-SITL is still relatively experimental and there are only a few pre-built vehicles + (some of which are quite old and/or unstable). + + The binaries are built and tested on Windows 10, Ubuntu Linux, and Mac OS X + "El Capitan". Binaries are only available for x86 architectures. ARM builds + (e.g. for RPi) are not supported. + + Please report any issues on `Github here `_. + +Installation +------------ + +The tool is installed (or updated) on all platforms using the command: + +.. code-block:: bash + + pip install dronekit-sitl -UI + +Running SITL +------------ + +To run the latest version of Copter for which we have binaries (downloading the binaries if needed), you can simply call: + +.. code-block:: bash + + dronekit-sitl copter + +SITL will then start and wait for TCP connections on ``127.0.0.1:5760``. + +You can specify a particular vehicle and version, and also parameters like the home location, +the vehicle model type (e.g. "quad"), etc. For example: + +.. code-block:: bash + + dronekit-sitl plane-3.3.0 --home=-35.363261,149.165230,584,353 + +There are a number of other useful arguments: + +.. code-block:: bash + + dronekit-sitl -h #List all parameters to dronekit-sitl. + dronekit-sitl copter -h #List additional parameters for the specified vehicle (in this case "copter"). + dronekit-sitl --list #List all available vehicles. + dronekit-sitl --reset #Delete all downloaded vehicle binaries. + dronekit-sitl ./path [args...] #Start SITL instance at target file location. + + +.. note:: + + You can also use *dronekit-sitl* to start a SITL executable that you have built locally from source. + To do this, put the file path of the target executable in the `SITL_BINARY` environment variable, + or as the first argument when calling the tool. + + +.. _connecting_dronekit_sitl: + +Connecting to DroneKit-SITL +--------------------------- + +DroneKit-SITL waits for TCP connections on ``127.0.0.1:5760``. DroneKit-Python scripts running on the same +computer can connect to the simulation using the connection string as shown: + +.. code-block:: python + + vehicle = connect('tcp:127.0.0.1:5760', wait_ready=True) + +After something connects to port ``5760``, SITL will then wait for additional connections on port ``5763`` +(and subsequently ``5766``, ``5769`` etc.) + +.. note:: + + While you can connect to these additional ports, some users have reported problems when + viewing the running examples with *Mission Planner*. If you need to connect a ground station + and DroneKit at the same time we recommend you use *MAVProxy* (see :ref:`viewing_uav_on_map`). + + + +.. _dronekit_sitl_api: + +DroneKit-SITL Python API +------------------------ + +DroneKit-SITL `exposes a Python API `_, which you can use to start and control simulation from within your scripts. This is particularly useful for test code and :ref:`examples `. + + + + +Building SITL from source +========================= + +You can natively build SITL from source on Linux, Windows and Mac OS X, +or from within a Vagrant Linux virtual environment. + +Building from source is useful if you want to need to test the latest changes (or any use +a version for which DroneKit-SITL does not have pre-built binaries). +It can also be useful if you have problems getting DroneKit-SITL to work. + +SITL built from source has a few differences from DroneKit-SITL: + +* MAVProxy is included and started by default. You can use MAVProxy terminal to control the autopilot. +* You connect to SITL via UDP on ``127.0.0.1:14550``. You can use MAVProxy's ``output add`` command to add additional ports if needed. +* You may need to disable arming checks and load autotest parameters to run examples. +* It is easier to `add a virtual rangefinder `_ and `add a virtual gimbal `_ for testing. + +The following topics from the ArduPilot wiki explain how to set up Native SITL builds: + +* `Setting up SITL on Linux `_ +* `Setting up SITL on Windows `_ +* `Setting up SITL using Vagrant `_ + + +.. _viewing_uav_on_map: + +Connecting an additional Ground Station +======================================= + +You can connect a ground station to an unused port to which messages are being forwarded. + +The most reliable way to add new ports is to use *MAVProxy*: + +* If you're using SITL built from source you will already have *MAVProxy* running. + You can add new ports in the MAVProxy console using ``output add``: + + .. code:: bash + + output add 127.0.0.1:14552 + +* If you're using Dronekit-SITL you can: + + * `Install MAVProxy `_ + for your system. + * In a second terminal spawn an instance of *MAVProxy* to forward messages from + TCP ``127.0.0.1:5760`` to other UDP ports like ``127.0.0.1:14550`` and ``127.0.0.1:14551``: + + .. code-block:: bash + + mavproxy.py --master tcp:127.0.0.1:5760 --sitl 127.0.0.1:5501 --out 127.0.0.1:14550 --out 127.0.0.1:14551 + +Once you have available ports you can connect to a ground station using one UDP address, and DroneKit-Python using the other. + +For example, first connect the script: + +.. code-block:: python + + vehicle = connect('127.0.0.1:14550', wait_ready=True) + + +Then connect Mission Planner to the second UDP port: + +* `Download and install Mission Planner `_ +* Ensure the selection list at the top right of the Mission Planner screen says *UDP* and then select the **Connect** button next to it. + When prompted, enter the port number (in this case 14552). + + .. figure:: MissionPlanner_ConnectPort.png + :width: 50 % + + Mission Planner: Listen Port Dialog + +After connecting, vehicle parameters will be loaded into *Mission Planner* and the vehicle is displayed on the map. + +.. tip:: + + If you're using the :ref:`dronekit_sitl_api` then you will instead have to + connect to SITLs TCP port (as there is no way to set up MAVProxy in this case). + So if DroneKit is connecting to TCP port 5760, you would connect your GCS to 5763. + + Note that a few examples may not behave perfectly using this approach. If you need to + observe them in a GCS you should run SITL externally and use MAVProxy to connect to it. + diff --git a/docs/examples/channel_overrides.rst b/docs/examples/channel_overrides.rst new file mode 100644 index 000000000..8c95143f9 --- /dev/null +++ b/docs/examples/channel_overrides.rst @@ -0,0 +1,176 @@ +.. _example_channel_overrides: +.. _vehicle_state_channel_override: + +======================================= +Example: Channels and Channel Overrides +======================================= + +This example shows how to get channel information and to get/set channel-override information. + +.. warning:: + + Channel overrides (a.k.a. "RC overrides") are highly dis-commended (they are primarily intended + for simulating user input and when implementing certain types of joystick control). + + Instead use the appropriate MAVLink commands like DO_SET_SERVO/DO_SET_RELAY, or more generally set + the desired position or direction/speed. + + If you have no choice but to use a channel-override please explain why in a + `Github issue `_ and we will attempt to find a + better alternative. + + +Running the example +=================== + +The example can be run as described in :doc:`running_examples` (which in turn assumes that the vehicle +and DroneKit have been set up as described in :ref:`installing_dronekit`). + +In summary, after cloning the repository: + +#. Navigate to the example folder as shown: + + .. code-block:: bash + + cd dronekit-python/examples/channel_overrides/ + + +#. You can run the example against a simulator (DroneKit-SITL) by specifying the Python script without any arguments. + The example will download SITL binaries (if needed), start the simulator, and then connect to it: + + .. code-block:: bash + + python channel_overrides.py + + On the command prompt you should see (something like): + + .. code:: bash + + Starting copter simulator (SITL) + SITL already Downloaded. + Connecting to vehicle on: tcp:127.0.0.1:5760 + >>> APM:Copter V3.3 (d6053245) + >>> Frame: QUAD + >>> Calibrating barometer + >>> Initialising APM... + >>> barometer calibration complete + >>> GROUND START + Channel values from RC Tx: {'1': 1500, '3': 1000, '2': 1500, '5': 1800, '4': 1500, '7': 1000, '6': 1000, '8': 1800} + Read channels individually: + Ch1: 1500 + Ch2: 1500 + Ch3: 1000 + Ch4: 1500 + Ch5: 1800 + Ch6: 1000 + Ch7: 1000 + Ch8: 1800 + Number of channels: 8 + Channel overrides: {} + Set Ch2 override to 200 (indexing syntax) + Channel overrides: {'2': 200} + Ch2 override: 200 + Set Ch3 override to 300 (dictionary syntax) + Channel overrides: {'3': 300} + Set Ch1-Ch8 overrides to 110-810 respectively + Channel overrides: {'1': 110, '3': 310, '2': 210, '5': 510, '4': 4100, '7': 710, '6': 610, '8': 810} + Cancel Ch2 override (indexing syntax) + Channel overrides: {'1': 110, '3': 310, '5': 510, '4': 4100, '7': 710, '6': 610, '8': 810} + Clear Ch3 override (del syntax) + Channel overrides: {'1': 110, '5': 510, '4': 4100, '7': 710, '6': 610, '8': 810} + Clear Ch5, Ch6 override and set channel 3 to 500 (dictionary syntax) + Channel overrides: {'3': 500} + Clear all overrides + Channel overrides: {} + Close vehicle object + Completed + +#. You can run the example against a specific connection (simulated or otherwise) by passing the :ref:`connection string ` for your vehicle in the ``--connect`` parameter. + + For example, to connect to SITL running on UDP port 14550 on your local computer: + + .. code-block:: bash + + python channel_overrides.py --connect 127.0.0.1:14550 + + +How does it work? +================= + +The RC transmitter channels are connected to the autopilot and control the vehicle. + +The values of the first four channels map to the main flight controls: 1=Roll, 2=Pitch, 3=Throttle, 4=Yaw (the mapping is defined in ``RCMAP_`` parameters in +`Plane `_, +`Copter `_ , +`Rover `_). + +The remaining channel values are configurable, and their purpose can be determined using the +`RCn_FUNCTION parameters `_. +In general a value of 0 set for a specific ``RCn_FUNCTION`` indicates that the channel can be +`mission controlled `_ (i.e. it will not directly be +controlled by normal autopilot code). + +You can read the values of the channels using the :py:attr:`Vehicle.channels ` attribute. The values are regularly updated, +from the UAV, based on the RC inputs from the transmitter. These can be read either as a set or individually: + +.. code:: python + + # Get all channel values from RC transmitter + print "Channel values from RC Tx:", vehicle.channels + + # Access channels individually + print "Read channels individually:" + print " Ch1: %s" % vehicle.channels['1'] + print " Ch2: %s" % vehicle.channels['2'] + +You can override the values sent to the vehicle by the autopilot using :py:attr:`Vehicle.channels.overrides `. +The overrides can be written individually using an indexing syntax or as a set using a dictionary syntax. + +.. code:: python + + # Set Ch2 override to 200 using indexing syntax + vehicle.channels.overrides['2'] = 200 + # Set Ch3, Ch4 override to 300,400 using dictionary syntax" + vehicle.channels.overrides = {'3':300, '4':400} + +To clear all overrides, set the attribute to an empty dictionary. +To clear an individual override you can set its value to ``None`` (or call ``del`` on it): + +.. code:: python + + # Clear override by setting channels to None + # Clear using index syntax + vehicle.channels.overrides['2'] = None + + # Clear using 'del' syntax + del vehicle.channels.overrides['3'] + + # Clear using dictionary syntax (and set override at same time!) + vehicle.channels.overrides = {'5':None, '6':None,'3':500} + + # Clear all overrides by setting an empty dictionary + vehicle.channels.overrides = {} + +Read the channel overrides either as a dictionary or by index. + +.. code:: python + + # Get all channel overrides + print " Channel overrides: %s" % vehicle.channels.overrides + # Print just one channel override + print " Ch2 override: %s" % vehicle.channels.overrides['2'] + +.. note:: + + You'll get a ``KeyError`` exception if you read a channel override that has + not been set. + + +Source code +=========== + +The full source code at documentation build-time is listed below (`current version on github `_): + +.. literalinclude:: ../../examples/channel_overrides/channel_overrides.py + :language: python + diff --git a/docs/examples/create_attribute.rst b/docs/examples/create_attribute.rst new file mode 100644 index 000000000..0f83b22f2 --- /dev/null +++ b/docs/examples/create_attribute.rst @@ -0,0 +1,283 @@ +.. _example_create_attribute: + +================================ +Example: Create Attribute in App +================================ + +This example shows how you can subclass :py:class:`Vehicle ` in order to support +new attributes for MAVLink messages within your DroneKit-Python script. The new class is defined in a +separate file (making re-use easy) and is very similar to the code used to implement the in-built attributes. +The new attributes are used *in the same way* as the built-in +:py:class:`Vehicle ` attributes. + +The new class uses the :py:func:`Vehicle.on_message() ` decorator +to set a function that is called to process a specific message, copy its values into an attribute, and notify +observers. An observer is then set on the new attribute using +:py:func:`Vehicle.add_attribute_listener() `. + +Additional information is provided in the guide topic :ref:`mavlink_messages`. + +.. tip:: + + This approach is useful when you urgently need to access messages that are not yet supported as + :py:class:`Vehicle ` attributes. + + Please :ref:`contribute your code to the API ` so that it is available to + (and can be tested by) the whole DroneKit-Python community. + + + +Running the example +=================== + +The example can be run as described in :doc:`running_examples` (which in turn assumes that the vehicle +and DroneKit have been set up as described in :ref:`installing_dronekit`). + +In summary, after cloning the repository: + +#. Navigate to the example folder as shown: + + .. code-block:: bash + + cd dronekit-python\examples\create_attribute\ + +#. You can run the example against a simulator (DroneKit-SITL) by specifying the Python script without any arguments. + The example will download SITL binaries (if needed), start the simulator, and then connect to it: + + .. code-block:: bash + + python create_attribute.py + + On the command prompt you should see (something like): + + .. code:: bash + + Starting copter simulator (SITL) + SITL already Downloaded. + Connecting to vehicle on: tcp:127.0.0.1:5760 + >>> APM:Copter V3.3 (d6053245) + >>> Frame: QUAD + >>> Calibrating barometer + >>> Initialising APM... + >>> barometer calibration complete + >>> GROUND START + Display RAW_IMU messages for 5 seconds and then exit. + RAW_IMU: time_boot_us=15340000,xacc=0,yacc=0,zacc=-1000,xgyro=0,ygyro=1,zgyro=0,xmag=161,ymag=19,zmag=-365 + RAW_IMU: time_boot_us=15580000,xacc=0,yacc=0,zacc=-1000,xgyro=0,ygyro=0,zgyro=0,xmag=161,ymag=19,zmag=-365 + RAW_IMU: time_boot_us=15820000,xacc=0,yacc=0,zacc=-999,xgyro=0,ygyro=1,zgyro=0,xmag=161,ymag=19,zmag=-365 + RAW_IMU: time_boot_us=16060000,xacc=0,yacc=1,zacc=-999,xgyro=0,ygyro=0,zgyro=0,xmag=161,ymag=19,zmag=-365 + RAW_IMU: time_boot_us=16300000,xacc=0,yacc=0,zacc=-999,xgyro=0,ygyro=0,zgyro=0,xmag=161,ymag=19,zmag=-365 + RAW_IMU: time_boot_us=16540000,xacc=0,yacc=0,zacc=-1000,xgyro=0,ygyro=1,zgyro=0,xmag=161,ymag=19,zmag=-365 + RAW_IMU: time_boot_us=16780000,xacc=0,yacc=0,zacc=-999,xgyro=0,ygyro=0,zgyro=0,xmag=161,ymag=19,zmag=-365 + RAW_IMU: time_boot_us=17020000,xacc=1,yacc=0,zacc=-999,xgyro=0,ygyro=0,zgyro=0,xmag=161,ymag=19,zmag=-365 + RAW_IMU: time_boot_us=17260000,xacc=0,yacc=0,zacc=-999,xgyro=0,ygyro=0,zgyro=0,xmag=161,ymag=19,zmag=-365 + RAW_IMU: time_boot_us=17500000,xacc=0,yacc=0,zacc=-1000,xgyro=1,ygyro=0,zgyro=0,xmag=161,ymag=19,zmag=-365 + RAW_IMU: time_boot_us=17740000,xacc=0,yacc=0,zacc=-999,xgyro=0,ygyro=0,zgyro=0,xmag=161,ymag=19,zmag=-365 + RAW_IMU: time_boot_us=17980000,xacc=0,yacc=0,zacc=-999,xgyro=0,ygyro=0,zgyro=0,xmag=161,ymag=19,zmag=-365 + RAW_IMU: time_boot_us=18220000,xacc=0,yacc=0,zacc=-1000,xgyro=0,ygyro=0,zgyro=1,xmag=161,ymag=19,zmag=-365 + RAW_IMU: time_boot_us=18460000,xacc=0,yacc=0,zacc=-1000,xgyro=0,ygyro=0,zgyro=0,xmag=161,ymag=19,zmag=-365 + RAW_IMU: time_boot_us=18700000,xacc=0,yacc=0,zacc=-999,xgyro=0,ygyro=0,zgyro=0,xmag=161,ymag=19,zmag=-365 + RAW_IMU: time_boot_us=18940000,xacc=1,yacc=0,zacc=-1000,xgyro=0,ygyro=1,zgyro=0,xmag=161,ymag=19,zmag=-365 + RAW_IMU: time_boot_us=19180000,xacc=1,yacc=0,zacc=-1000,xgyro=0,ygyro=0,zgyro=0,xmag=161,ymag=19,zmag=-365 + RAW_IMU: time_boot_us=19420000,xacc=0,yacc=0,zacc=-999,xgyro=0,ygyro=0,zgyro=0,xmag=161,ymag=19,zmag=-365 + RAW_IMU: time_boot_us=19660000,xacc=0,yacc=0,zacc=-1000,xgyro=0,ygyro=0,zgyro=0,xmag=154,ymag=52,zmag=-365 + RAW_IMU: time_boot_us=19900000,xacc=0,yacc=0,zacc=-999,xgyro=0,ygyro=0,zgyro=0,xmag=154,ymag=52,zmag=-365 + RAW_IMU: time_boot_us=20140000,xacc=0,yacc=0,zacc=-1000,xgyro=0,ygyro=0,zgyro=0,xmag=154,ymag=52,zmag=-365 + Close vehicle object + + +#. You can run the example against a specific connection (simulated or otherwise) by passing the :ref:`connection string ` for your vehicle in the ``--connect`` parameter. + + For example, to connect to SITL running on UDP port 14550 on your local computer: + + .. code-block:: bash + + python create_attribute.py --connect 127.0.0.1:14550 + + + + +How does it work? +================= + +Subclassing Vehicle +------------------- + +The example file **my_vehicle.py** defines a class for the new attribute (``RawIMU``) and a new vehicle subclass (``MyVehicle``). + +.. note:: + + The example uses the same documentation markup used by the native code, which can be generated into a document set using + Sphinx/autodoc. + + +``RawIMU`` has members for each of the values in the message +(in this case `RAW_IMU `_). It provides an initialiser that sets all the values to +``None`` and a string representation for printing the object. + +.. code:: python + + class RawIMU(object): + """ + The RAW IMU readings for the usual 9DOF sensor setup. + This contains the true raw values without any scaling to allow data capture and system debugging. + + The message definition is here: http://mavlink.org/messages/common#RAW_IMU + + :param time_boot_us: Timestamp (microseconds since system boot). #Note, not milliseconds as per spec + :param xacc: X acceleration (mg) + :param yacc: Y acceleration (mg) + :param zacc: Z acceleration (mg) + :param xgyro: Angular speed around X axis (millirad /sec) + :param ygyro: Angular speed around Y axis (millirad /sec) + :param zgyro: Angular speed around Z axis (millirad /sec) + :param xmag: X Magnetic field (milli tesla) + :param ymag: Y Magnetic field (milli tesla) + :param zmag: Z Magnetic field (milli tesla) + """ + + def __init__(self, time_boot_us=None, xacc=None, yacc=None, zacc=None, xygro=None, ygyro=None, zgyro=None, xmag=None, ymag=None, zmag=None): + """ + RawIMU object constructor. + """ + self.time_boot_us = time_boot_us + self.xacc = xacc + self.yacc = yacc + self.zacc = zacc + self.xgyro = zgyro + self.ygyro = ygyro + self.zgyro = zgyro + self.xmag = xmag + self.ymag = ymag + self.zmag = zmag + + def __str__(self): + """ + String representation of the RawIMU object + """ + return "RAW_IMU: time_boot_us={},xacc={},yacc={},zacc={},xgyro={},ygyro={},zgyro={},xmag={},ymag={},zmag={}".format(self.time_boot_us, self.xacc, self.yacc,self.zacc,self.xgyro,self.ygyro,self.zgyro,self.xmag,self.ymag,self.zmag) + + +``MyVehicle`` is a superclass of ``Vehicle`` (and hence inherits all its attributes). +This first creates a private instance of ``RawIMU``. + +We create a listener using the :py:func:`Vehicle.on_message() ` +decorator. The listener is called for messages that contain the string "RAW_IMU", +with arguments for the vehicle, message name, and the message. It copies the message information into +the attribute and then notifies all observers. + +.. code-block:: python + :emphasize-lines: 6, 9-10, 32 + + class MyVehicle(Vehicle): + def __init__(self, *args): + super(MyVehicle, self).__init__(*args) + + # Create an Vehicle.raw_imu object with initial values set to None. + self._raw_imu = RawIMU() + + # Create a message listener using the decorator. + @self.on_message('RAW_IMU') + def listener(self, name, message): + """ + The listener is called for messages that contain the string specified in the decorator, + passing the vehicle, message name, and the message. + + The listener writes the message to the (newly attached) ``vehicle.raw_imu`` object + and notifies observers. + """ + self._raw_imu.time_boot_us=message.time_usec + self._raw_imu.xacc=message.xacc + self._raw_imu.yacc=message.yacc + self._raw_imu.zacc=message.zacc + self._raw_imu.xgyro=message.xgyro + self._raw_imu.ygyro=message.ygyro + self._raw_imu.zgyro=message.zgyro + self._raw_imu.xmag=message.xmag + self._raw_imu.ymag=message.ymag + self._raw_imu.zmag=message.zmag + + # Notify all observers of new message (with new value) + # Note that argument `cache=False` by default so listeners + # are updaed with every new message + self.notify_attribute_listeners('raw_imu', self._raw_imu) + + @property + def raw_imu(self): + return self._raw_imu + + +.. note:: + + The notifier function (:py:func:`Vehicle.notify_attribute_listeners() `) + should be called every time there is an update from the vehicle. + + You can set a third parameter (``cache=True``) so that it only invokes the listeners when the value *changes*. + This is normally used for attributes like the vehicle mode, where the information is updated + regularly from the vehicle, but client code is only interested when the attribute changes. + + You should not set ``cache=True`` for attributes that represent sensor information or other "live" information, including + the RAW_IMU attribute demonstrated here. Clients can then implement their own caching strategy if needed. + + +At the end of the class we create the public properly ``raw_imu`` which client code may read and observe. + +.. note:: + + The decorator pattern means that you can have multiple listeners for a particular message or for different + messages and they can all have the same function name/prototype (in this case ``listener(self, name, message``). + + +Using the Vehicle subclass +-------------------------- + +The **create_attribute.py** file first imports the ``MyVehicle`` class. + + +.. code-block:: python + :emphasize-lines: 2 + + from dronekit import connect, Vehicle + from my_vehicle import MyVehicle #Our custom vehicle class + import time + + +We then call ``connect()``, specifying this new class in the ``vehicle_class`` argument. + +.. code-block:: python + + # Connect to our custom vehicle_class `MyVehicle` at address `args.connect` + vehicle = connect(args.connect, wait_ready=True, vehicle_class=MyVehicle) + +``connect()`` returns a ``MyVehicle`` class which can be used in *exactly the same way* as ``Vehicle`` but with an +additional attribute ``raw_imu``. You can query the attribute to get any of its members, and even add an observer as shown: + +.. code:: python + + # Add observer for the custom attribute + + def raw_imu_callback(self, attr_name, value): + # attr_name == 'raw_imu' + # value == vehicle.raw_imu + print value + + vehicle.add_attribute_listener('raw_imu', raw_imu_callback) + + + +Known issues +============ + +This code has no known issues. + + +Source code +=========== + +The full source code at documentation build-time is listed below (`current version on github `_): + +.. literalinclude:: ../../examples/create_attribute/create_attribute.py + :language: python + +.. literalinclude:: ../../examples/create_attribute/my_vehicle.py + :language: python \ No newline at end of file diff --git a/docs/examples/drone_delivery.rst b/docs/examples/drone_delivery.rst index e7f1ae520..371708e9b 100644 --- a/docs/examples/drone_delivery.rst +++ b/docs/examples/drone_delivery.rst @@ -2,7 +2,9 @@ Example: Drone Delivery =========================== -This demonstration is a bit more extensive. It is a `CherryPy `_ based web application that displays a mapbox map to let you view the current vehicle position and send the vehicle commands to fly to a particular latitude and longitude. +This example shows how to create a `CherryPy `_ based web application that +displays a mapbox map to let you view the current vehicle position and send the vehicle commands +to fly to a particular latitude and longitude. New functionality demonstrated by this example includes: @@ -10,34 +12,92 @@ New functionality demonstrated by this example includes: * Starting *CherryPy* from a DroneKit application. -Starting the demo -================= +Running the example +=================== + +The example can be run much as described in :doc:`running_examples` (which in turn assumes that the vehicle +and DroneKit have been set up as described in :ref:`installing_dronekit`). The main exception is that you need to +install the CherryPy dependencies and view the behaviour in a web browser. + +In summary, after cloning the repository: + +#. Navigate to the example folder as shown: + + .. code-block:: bash + + cd dronekit-python\examples\drone_delivery\ + + +#. Install *CherryPy* and any other dependencies from **requirements.pip** in that directory: + + .. code-block:: bash + + pip install -r requirements.pip + +#. You can run the example against the simulator by specifying the Python script without any arguments. + The example will download and start DroneKit-SITL, and then connect to it: + + .. code-block:: bash + + python drone_delivery.py + + On the command prompt you should see (something like): + + .. code:: bash -The demonstration is started similar to the previous tutorials. You should see output that looks like the following: + >python drone_delivery.py + + D:\Github\dronekit-python\examples\drone_delivery>drone_delivery.py + Starting copter simulator (SITL) + SITL already Downloaded. + local path: D:\Github\dronekit-python\examples\drone_delivery + Connecting to vehicle on: tcp:127.0.0.1:5760 + >>> APM:Copter V3.3 (d6053245) + >>> Frame: QUAD + >>> Calibrating barometer + >>> Initialising APM... + >>> barometer calibration complete + >>> GROUND START + Launching Drone... + [DEBUG]: Connected to vehicle. + [DEBUG]: DroneDelivery Start + [DEBUG]: Waiting for location... + [DEBUG]: Waiting for ability to arm... + [DEBUG]: Running initial boot sequence + [DEBUG]: Changing to mode: GUIDED + [DEBUG]: ... polled mode: GUIDED + [DEBUG]: Waiting for arming... + >>> ARMING MOTORS + >>> GROUND START + >>> Initialising APM... + [DEBUG]: Taking off + http://localhost:8080/ + Waiting for cherrypy engine... -:: +#. You can run the example against a specific connection (simulated or otherwise) by passing the :ref:`connection string ` for your vehicle in the ``--connect`` parameter. + For example, to connect to Solo: + + .. code-block:: bash + + python drone_delivery.py --connect udpin:0.0.0.0:14550 + + +#. After a short while you should be able to reach your new webserver at http://localhost:8080. + Navigate to the **Command** screen, select a target on the map, then select **Go**. + The command prompt will show something like the message below. + + .. code-block:: bash + + [DEBUG]: Goto: [u'-35.4', u'149.2'], 29.98 + + The web server will switch you to the **Track** screen. You can view the vehicle progress by pressing the + **Update** button. - GUIDED> api start drone_delivery.py - GUIDED> [DEBUG]: DroneDelivery Start - [DEBUG]: Waiting for GPS Lock - [DEBUG]: DroneDelivery Armed Callback - [DEBUG]: GPS: GPSInfo:fix=3,num_sat=10 - [DEBUG]: Running initial boot sequence - [DEBUG]: Arming - [DEBUG]: Taking off - [DEBUG]: Mode: GUIDED - INFO:cherrypy.error:[03/Mar/2015:14:29:01] ENGINE Bus STARTING - INFO:cherrypy.error:[03/Mar/2015:14:29:01] ENGINE Started monitor thread '_TimeoutMonitor'. - INFO:cherrypy.error:[03/Mar/2015:14:29:01] ENGINE Started monitor thread 'Autoreloader'. - INFO:cherrypy.error:[03/Mar/2015:14:29:01] ENGINE Serving on http://0.0.0.0:8080 - INFO:cherrypy.error:[03/Mar/2015:14:29:01] ENGINE Bus STARTED - ARMED - GPS lock at 0 meters Screenshots =========== -You should be able to reach your new webserver at http://localhost:8080. It will look like the following: +The webserver (http://localhost:8080) will look like the following: .. image:: drone-delivery-splash.png @@ -46,31 +106,37 @@ You should be able to reach your new webserver at http://localhost:8080. It will .. image:: drone-delivery-command.png -Looking at the code -=================== +How it works +============ Using attribute observers ------------------------- -All attributes in DroneKit can have observers - this is the primary mechanism you should use to be notified of changes in vehicle state. For instance, `drone_delivery.py `_ calls: - -:: +All attributes in DroneKit can have observers - this is the primary mechanism you should use to be notified of changes in vehicle state. +For instance, `drone_delivery.py `_ calls: - self.vehicle.add_attribute_observer('location', self.location_callback) +.. code-block:: python - ... + self.vehicle.add_attribute_listener('location', self.location_callback) - def location_callback(self, location): - location = self.vehicle.location + ... - if location.alt is not None: - self.altitude = location.alt + def location_callback(self, vehicle, name, location): + if location.global_relative_frame.alt is not None: + self.altitude = location.global_relative_frame.alt - self.current_location = location + self.current_location = location.global_relative_frame This results in DroneKit calling our ``location_callback`` method any time the location attribute gets changed. +.. tip:: + + It is also possible (and often more elegant) to add listeners using a decorator + - see :py:func:`Vehicle.on_attribute `. + + + Starting CherryPy from a DroneKit application --------------------------------------------- @@ -78,9 +144,16 @@ We start running a web server by calling ``cherrypy.engine.start()``. *CherryPy* is a very small and simple webserver. It is probably best to refer to their eight line `tutorial `_ for more information. -Next we'll look at the basics of using the webservice and the local vehicle API to 'replay' a flight which has been uploaded to `Droneshare `_. +Known issues +============ + +This example has the following issues: + +* `#537: Dronekit delivery tracking needs to zoom and also ideally auto update `_ +* `#538: Dronekit delivery example does not exit `_ + Source code =========== diff --git a/docs/examples/flight_replay.rst b/docs/examples/flight_replay.rst index 4f8fac2be..63b3d175b 100644 --- a/docs/examples/flight_replay.rst +++ b/docs/examples/flight_replay.rst @@ -1,76 +1,192 @@ -========================= +====================== Example: Flight Replay -========================= - -This is an interesting demo that uses our web API to query raw flight data from a particular flight. - - -Starting the demo -================= - -In this case, we pick some public flight from `Droneshare `_: - -.. image:: flight_replay_example.png - -You'll notice that the mission number for this flight is 101. - -Now we'll launch **flight_replay.py** (/examples/flight_replay/flight_replay.py) and ask it to try and 'replay' mission 101. It will ask the web server for representative points from the flight, parse the JSON response and use that data to generate 100 waypoints we would like our vehicle to hit. For safety rather than using the altitude from the original flight we instead ask our vehicle to fly at a height of 30 meters. - -One possible use of some variant of this tool to replay your old flights at your regular test field. - -:: - - STABILIZE> api start flight_replay.py 101 - STABILIZE> JSON downloaded... - Genrating 95 waypoints from replay... - APIThread-1 exiting... - Got MAVLink msg: MISSION_ACK {target_system : 255, target_component : 0, type : 0} - Sent waypoint 0 : MISSION_ITEM {target_system : 1, target_component : 1, seq : 0, frame : 3, command : 16, current : 0, autocontinue : 0, param1 : 0, param2 : 0, param3 : 0, param4 : 0, x : 45.7379052, y : 126.6273574, z : 30.0} - Sent waypoint 1 : MISSION_ITEM {target_system : 1, target_component : 1, seq : 1, frame : 3, command : 16, current : 0, autocontinue : 0, param1 : 0, param2 : 0, param3 : 0, param4 : 0, x : 45.7378905, y : 126.6273609, z : 30.0} - Sent waypoint 2 : MISSION_ITEM {target_system : 1, target_component : 1, seq : 2, frame : 3, command : 16, current : 0, autocontinue : 0, param1 : 0, param2 : 0, param3 : 0, - ... - Sent waypoint 92 : MISSION_ITEM {target_system : 1, target_component : 1, seq : 92, frame : 3, command : 16, current : 0, autocontinue : 0, param1 : 0, param2 : 0, param3 : 0, param4 : 0, x : 45.737971, y : 126.6274908, z : 30.0} - Sent waypoint 93 : MISSION_ITEM {target_system : 1, target_component : 1, seq : 93, frame : 3, command : 16, current : 0, autocontinue : 0, param1 : 0, param2 : 0, param3 : 0, param4 : 0, x : 45.738018, y : 126.6275664, z : 30.0} - Sent waypoint 94 : MISSION_ITEM {target_system : 1, target_component : 1, seq : 94, frame : 3, command : 16, current : 0, autocontinue : 0, param1 : 0, param2 : 0, param3 : 0, param4 : 0, x : 45.7380429, y : 126.6275067, z : 30.0} - Sent all 95 waypoints - Got MAVLink msg: MISSION_ACK {target_system : 255, target_component : 0, type : 0} - APM: flight plan received - - +====================== + +This example creates and runs a waypoint mission using position information from a TLOG file. + +The log used in this example contains around 2700 points. This is too many points to upload +to the autopilot (and to usefully display). Instead we only add points that are more than +3 metres away from the previously kept point, and only store 99 points in total. +After 60 seconds the mission is ended by setting the mode to RTL (return to launch). + +.. figure:: flight_replay_example.png + :width: 50% + + 99 point mission generated from log + + +.. note:: + + The method used to reduce the number of points is fairly effective, but we + could do better by grouping some of the waypoints, and mapping others using + spline waypoints. This might be a + `fun research project `_! + + + +Running the example +=================== + +The example can be run as described in :doc:`running_examples` (which in turn assumes that the vehicle +and DroneKit have been set up as described in :ref:`installing_dronekit`). + +In summary, after cloning the repository: + +#. Navigate to the example folder as shown: + + .. code-block:: bash + + cd dronekit-python/examples/flight_replay/ + +#. You can run the example against a simulator (DroneKit-SITL) by specifying the Python script without any arguments. + The example will download SITL binaries if needed, start the simulator, and then connect to it: + + .. code-block:: bash + + python flight_replay.py + + On the command prompt you should see (something like): + + .. code:: bash + + Generating waypoints from tlog... + Generated 100 waypoints from tlog + Starting copter simulator (SITL) + SITL already Downloaded. + Connecting to vehicle on: tcp:127.0.0.1:5760 + >>> APM:Copter V3.3 (d6053245) + >>> Frame: QUAD + >>> Calibrating barometer + >>> Initialising APM... + >>> barometer calibration complete + >>> GROUND START + Uploading 100 waypoints to vehicle... + Arm and Takeoff + Waiting for vehicle to initialise... + >>> flight plan received + Waiting for arming... + Waiting for arming... + Waiting for arming... + Waiting for arming... + >>> ARMING MOTORS + >>> GROUND START + Waiting for arming... + >>> Initialising APM... + Waiting for arming... + >>> ARMING MOTORS + Taking off! + Altitude: 0.000000 < 28.500000 + Altitude: 0.010000 < 28.500000 + ... + Altitude: 26.350000 < 28.500000 + Altitude: 28.320000 < 28.500000 + Reached target altitude of ~30.000000 + Starting mission + Distance to waypoint (1): 3.02389745499 + >>> Reached Command #1 + Distance to waypoint (2): 5.57718471895 + Distance to waypoint (2): 4.1504263025 + >>> Reached Command #2 + Distance to waypoint (3): 0.872847106279 + Distance to waypoint (3): 1.88967925144 + Distance to waypoint (3): 2.16157704522 + >>> Reached Command #3 + Distance to waypoint (4): 4.91867197924 + ... + ... + Distance to waypoint (35): 4.37309981133 + >>> Reached Command #35 + Distance to waypoint (36): 5.61829417257 + >>> Reached Command #36 + Return to launch + Close vehicle object + Completed... + + + .. tip:: + It is more interesting to watch the example run on a map than the console. The topic :ref:`viewing_uav_on_map` + explains how to set up *Mission Planner* to view a vehicle running on the simulator (SITL). + +#. You can run the example against a specific connection (simulated or otherwise) by passing the :ref:`connection string ` for your vehicle in the ``--connect`` parameter. + + For example, to connect to SITL running on UDP port 14550 on your local computer: + + .. code-block:: bash + + python simple_goto.py --connect 127.0.0.1:14550 + + + How it works ============ Getting the points ------------------ -The following simple function asks for the droneshare flight data: - -:: +The example parses the **flight.tlog** file for position information. First we read all the points. +We then keep the first 99 points that are at least 3 metres separated from the preceding kept point. + +For safety reasons, the altitude for the waypoints is set to 30 meters (irrespective of the recorded height). + +.. code:: python + + def position_messages_from_tlog(filename): + """ + Given telemetry log, get a series of wpts approximating the previous flight + """ + # Pull out just the global position msgs + messages = [] + mlog = mavutil.mavlink_connection(filename) + while True: + try: + m = mlog.recv_match(type=['GLOBAL_POSITION_INT']) + if m is None: + break + except Exception: + break + # ignore we get where there is no fix: + if m.lat == 0: + continue + messages.append(m) + + # Shrink the number of points for readability and to stay within autopilot memory limits. + # For coding simplicity we: + # - only keep points that are with 3 metres of the previous kept point. + # - only keep the first 100 points that meet the above criteria. + num_points = len(messages) + keep_point_distance=3 #metres + kept_messages = [] + kept_messages.append(messages[0]) #Keep the first message + pt1num=0 + pt2num=1 + while True: + #Keep the last point. Only record 99 points. + if pt2num==num_points-1 or len(kept_messages)==99: + kept_messages.append(messages[pt2num]) + break + pt1 = LocationGlobalRelative(messages[pt1num].lat/1.0e7,messages[pt1num].lon/1.0e7,0) + pt2 = LocationGlobalRelative(messages[pt2num].lat/1.0e7,messages[pt2num].lon/1.0e7,0) + distance_between_points = get_distance_metres(pt1,pt2) + if distance_between_points > keep_point_distance: + kept_messages.append(messages[pt2num]) + pt1num=pt2num + pt2num=pt2num+1 + + return kept_messages - def download_messages(mission_id, max_freq = 1.0): - """Download a public mission from droneshare (as JSON)""" - f = urllib.urlopen("%s/api/v1/mission/%s/messages.json?max_freq=%s&api_key=%s" % (api_server, mission_id, max_freq, api_key)) - j = json.load(f, object_hook=_decode_dict) - f.close() - return j - -Some comments: - -* ``max_freq`` is used to throttle the messages found in the raw flight data to a lower message rate -* ``_decode_dict`` is a utility function found on stack overflow which extracts usable strings from unicode encoded JSON (see `flight_replay.py `_ for its implementation). Setting the new waypoints -========================= +------------------------- -We generate up to 100 waypoints for the vehicle with the following code: +The following code shows how the vehicle writes the received messages as commands (this part of the code is very similar to that +shown in :ref:`example_mission_basic`): -:: +.. code:: python print "Generating %s waypoints from replay..." % len(messages) - cmds = v.commands + cmds = vehicle.commands cmds.clear() - v.flush() for i in xrange(0, len(messages)): pt = messages[i] lat = pt['lat'] @@ -86,11 +202,16 @@ We generate up to 100 waypoints for the vehicle with the following code: 0, 0, 0, 0, 0, 0, lat, lon, altitude) cmds.add(cmd) - v.flush() + #Upload clear message and command messages to vehicle. + cmds.upload() -Next we'll work with existing Linux services (gpsd) to add a new drone based feature called :ref:`Follow Me `. +Known issues +============ + +There are no known issues with this example. + Source code =========== @@ -100,4 +221,3 @@ The full source code at documentation build-time is listed below (`current versi .. literalinclude:: ../../examples/flight_replay/flight_replay.py :language: python - \ No newline at end of file diff --git a/docs/examples/flight_replay_example.png b/docs/examples/flight_replay_example.png index 5464c7ec8..67f29570f 100644 Binary files a/docs/examples/flight_replay_example.png and b/docs/examples/flight_replay_example.png differ diff --git a/docs/examples/follow_me.rst b/docs/examples/follow_me.rst index e9691a279..532c362b9 100644 --- a/docs/examples/follow_me.rst +++ b/docs/examples/follow_me.rst @@ -4,39 +4,157 @@ Example: Follow Me ================== -This is a significantly more complex example – showing closed-loop control of the vehicle. It will use a USB GPS attached to your laptop to have the vehicle follow you as you walk around a field. +The *Follow Me* example moves a vehicle to track your position, using location information from a USB GPS attached to your (Linux) laptop. -Run this example with caution - be ready to exit follow-me mode by switching the flight mode switch on your RC radio. +The source code is a good *starting point* for your own applications. It can be extended to use other +python language features and libraries (OpenCV, classes, lots of packages etc...) -In practice, you don't really want to use this follow-me implementation, rather you can use this example as a starting point to build your own custom application. -Before running this demo you'll need to make sure your computer has the gpsd service installed. +.. note:: This example can only run on a Linux computer, because it depends on the Linux-only *gpsd* service. + +.. warning:: Run this example with caution - be ready to exit follow-me mode by switching the flight mode switch on your RC radio. -*Ubuntu install* -.. code-block:: bash +Running the example +=================== - apt-get install gpsd gpsd-clients +DroneKit (for Linux) and the vehicle should be set up as described in :ref:`installing_dronekit`. -You can then plug in a USB GPS and run the "xgps" client to confirm that it is working. If you do not have a USB GPS you can use simulated data by running *dronekit-python/examples/run-fake-gps.sh*. +Once you've done that: -Once your GPS is plugged in you can start follow-me by running the following command inside of MAVProxy: +#. Install the *gpsd* service (as shown for Ubuntu Linux below): -.. code-block:: bash + .. code-block:: bash - RTL> api start follow_me.py - RTL> Going to: Location:lat=50.616468333,lon=7.131903333,alt=30,is_relative=True - Got MAVLink msg: MISSION_ACK {target_system : 255, target_component : 0, type : 0} - GUIDED> Mode GUIDED - Going to: Location:lat=50.616468333,lon=7.131903333,alt=30,is_relative=True - Got MAVLink msg: MISSION_ACK {target_system : 255, target_component : 0, type : 0} - ... + sudo apt-get install gpsd gpsd-clients -These debugging messages will appear every two seconds - when a new target position is sent to the vehicle, to stop follow-me either change the vehicle mode switch on your RC transmitter or type "api stop". + You can then plug in a USB GPS and run the "xgps" client to confirm that it is working. + + .. note:: + + If you do not have a USB GPS you can use simulated data by running *dronekit-python/examples/follow_me/run-fake-gps.sh* + (in a separate terminal from where you're running DroneKit-Python). This approach simulates a single location, and so + is really only useful for verifying that the script is working correctly. + -The source code for this example is a good starting point for your own application, from here you can use all python language features and libraries (OpenCV, classes, lots of packages etc...) +#. Get the DroneKit-Python example source code onto your local machine. The easiest way to do this + is to clone the **dronekit-python** repository from Github. On the command prompt enter: + + .. code-block:: bash + + git clone http://github.com/dronekit/dronekit-python.git + +#. Navigate to the example folder as shown: + + .. code-block:: bash + + cd dronekit-python/examples/follow_me/ + + +#. You can run the example against a simulator (DroneKit-SITL) by specifying the Python script without any arguments. + The example will download SITL binaries (if needed), start the simulator, and then connect to it: + + .. code-block:: bash + + python follow_me.py + + On the command prompt you should see (something like): + + .. code:: bash + + Starting copter simulator (SITL) + SITL already Downloaded. + Connecting to vehicle on: tcp:127.0.0.1:5760 + >>> APM:Copter V3.4-dev (e0810c2e) + >>> Frame: QUAD + Link timeout, no heartbeat in last 5 seconds + Basic pre-arm checks + Waiting for GPS...: None + ... + Waiting for GPS...: None + Taking off! + Altitude: 0.019999999553 + ... + Altitude: 4.76000022888 + Reached target altitude + Going to: Location:lat=50.616468333,lon=7.131903333,alt=30,is_relative=True + ... + Going to: Location:lat=50.616468333,lon=7.131903333,alt=30,is_relative=True + Going to: Location:lat=50.616468333,lon=7.131903333,alt=30,is_relative=True + User has changed flight modes - aborting follow-me + Close vehicle object + Completed + + .. note:: + + The terminal output above was created using simulated GPS data + (which is why the same target location is returned every time). + + To stop follow-me you can change the vehicle mode or do Ctrl+C + (on a real flight you can just change the mode switch on your + RC transmitter). + + +#. You can run the example against a specific connection (simulated or otherwise) by passing the :ref:`connection string ` for your vehicle in the ``--connect`` parameter. + + For example, to connect to SITL running on UDP port 14550 on your local computer: + + .. code-block:: bash + + python follow_me.py --connect 127.0.0.1:14550 + + + +How does it work? +================= + +Most of the example should be fairly familiar as it uses the same code as other examples for connecting to the vehicle, +:ref:`taking off `, and closing the vehicle object. + +The example-specific code is shown below. All this does is attempt to get a gps socket and read the location in a two second loop. If it is successful it +reports the value and uses :py:func:`Vehicle.simple_goto ` to move to the new position. The loop exits when +the mode is changed. + +.. code-block:: python + + import gps + import socket + + ... + + try: + # Use the python gps package to access the laptop GPS + gpsd = gps.gps(mode=gps.WATCH_ENABLE) + + #Arm and take off to an altitude of 5 meters + arm_and_takeoff(5) + + while True: + + if vehicle.mode.name != "GUIDED": + print "User has changed flight modes - aborting follow-me" + break + + # Read the GPS state from the laptop + gpsd.next() + + # Once we have a valid location (see gpsd documentation) we can start moving our vehicle around + if (gpsd.valid & gps.LATLON_SET) != 0: + altitude = 30 # in meters + dest = LocationGlobalRelative(gpsd.fix.latitude, gpsd.fix.longitude, altitude) + print "Going to: %s" % dest + + # A better implementation would only send new waypoints if the position had changed significantly + vehicle.simple_goto(dest) + + # Send a new target every two seconds + # For a complete implementation of follow me you'd want adjust this delay + time.sleep(2) + + except socket.error: + print "Error: gpsd service does not seem to be running, plug in USB GPS or run run-fake-gps.sh" + sys.exit(1) -Next, take a look at the full :ref:`api_reference` for more information. Source code diff --git a/docs/examples/guided-set-speed-yaw-demo.rst b/docs/examples/guided-set-speed-yaw-demo.rst index 7c272927d..22554157f 100644 --- a/docs/examples/guided-set-speed-yaw-demo.rst +++ b/docs/examples/guided-set-speed-yaw-demo.rst @@ -4,9 +4,14 @@ Example: Guided Mode Movement and Commands (Copter) =================================================== -This example shows how to control Copter movement and send immediate commands in :ref:`GUIDED mode `. It demonstrates three methods for explicitly specifying a target position and two commands for controlling movement by setting the vehicle's velocity vectors. It also shows how to send commands to control the yaw (direction that the front of the vehicle is pointing), region of interest, speed and home position, along with some useful functions for converting between frames of reference. +This example shows how to control Copter movement and send immediate commands in :ref:`GUIDED mode `. +It demonstrates three methods for explicitly specifying a target position and two commands for controlling movement by +setting the vehicle's velocity vectors. It also shows how to send commands to control the yaw (direction that the front +of the vehicle is pointing), region of interest, speed and home location, along with some useful functions for +converting between frames of reference. -The example is :ref:`documented in the source code `. More detailed information about using GUIDED mode can be found in the guide: :ref:`guided_mode_copter`. +The example is :ref:`documented in the source code `. +More detailed information about using GUIDED mode can be found in the guide: :ref:`guided_mode_copter`. .. figure:: GuidedModeExample_FlyByPosition.png @@ -27,123 +32,149 @@ The example is :ref:`documented in the source code ` Running the example =================== -The vehicle and DroneKit should be set up as described in :ref:`get-started`. -If you're using a simulated vehicle, remember to :ref:`disable arming checks ` so -that the example can run. +The example can be run as described in :doc:`running_examples` (which in turn assumes that the vehicle +and DroneKit have been set up as described in :ref:`installing_dronekit`). -Once *MAVProxy* is running and the API is loaded, you can start the example by typing: ``api start guided_set_speed_yaw.py``. +In summary, after cloning the repository: + +#. Navigate to the example folder as shown: + + .. code-block:: bash + + cd dronekit-python/examples/guided_set_speed_yaw/ -.. note:: - The command above assumes you started the *MAVProxy* prompt in a directory containing the example script. If not, - you will have to specify the full path to the script (e.g. on Linux): - ``api start /home/user/git/dronekit-python/examples/guided_set_speed_yaw/guided_set_speed_yaw.py``. - -On the *MAVProxy* console you should see (something like): - -:: - - MAV> api start /home/user/git/dronekit-python/examples/guided_set_speed_yaw/guided_set_speed_yaw.py - STABILIZE> Basic pre-arm checks - Arming motors - Waiting for arming... - Waiting for arming... - GUIDED> Waiting for arming... - Taking off! - Altitude: 0.00999999977648 - Altitude: 0.159999996424 - Altitude: 0.920000016689 - Altitude: 2.38000011444 - Altitude: 3.93000006676 - Altitude: 4.65000009537 - Altitude: 4.82999992371 - Reached target altitude - - TRIANGLE path using standard Vehicle.commands.goto() - Position North 80 West 50 - Distance to target: 100.792762965 - Distance to target: 100.25918006 - ... - Distance to target: 2.34237912414 - Distance to target: 0.308823685384 - Reached target - Position North 0 East 100 - Distance to target: 122.62321461 - ... - Distance to target: 5.39403923852 - Distance to target: 1.00445126117 - Reached target - Position North -80 West 50 - goto_target_globalint_position - Distance to target: 100.792430952 - Distance to target: 100.221083739 - ... - Distance to target: 1.69678155659 - Distance to target: 0.0798488767383 - Reached target - - TRIANGLE path using standard SET_POSITION_TARGET_GLOBAL_INT message and with varying speed. - Position South 100 West 130 - Set speed to 5m/s. - Distance to target: 181.439594672 - Distance to target: 132.170351744 - ... - Distance to target: 2.67615248028 - Distance to target: 0.382959594982 - Reached target - Set speed to 15m/s (max). - Position South 0 East 200 - Distance to target: 318.826739407 - Distance to target: 317.613357051 - ... - Distance to target: 3.5935761745 - Distance to target: 0.114090613451 - Reached target - Set speed to 10m/s (max). - Position North 100 West 130 - goto_target_globalint_position - Distance to target: 188.182423388 - Distance to target: 187.540272979 - ... - Distance to target: 4.82317050152 - Distance to target: 0.377390539948 - Reached target - - SQUARE path using SET_POSITION_TARGET_LOCAL_NED and position parameters - North 50m, East 0m, 10m altitude for 20 seconds - Point ROI at current location (home position) - North 50m, East 50m, 10m altitude - Point ROI at current location - North 0m, East 50m, 10m altitude - North 0m, East 0m, 10m altitude - - SQUARE path using SET_POSITION_TARGET_LOCAL_NED and velocity parameters - Velocity South & up - Yaw 180 absolute (South) - Velocity West & down - Yaw 270 absolute (West) - Velocity North - Yaw 0 absolute (North) - Velocity East - - DIAMOND path using SET_POSITION_TARGET_GLOBAL_INT and velocity parameters - Velocity North, East and up - Yaw 225 absolute - Velocity South, East and down - Yaw 90 relative (to previous yaw heading) - Set new Home location to current location - Get new home location - Home WP: MISSION_ITEM {target_system : 255, target_component : 0, seq : 0, frame : 0, command : 16, current : 0, autocontinue : 1, param1 : 0.0, param2 : 0.0, param3 : 0.0, param4 : 0.0, x : - -35.3632583618, y : 149.164352417, z : 593.91998291} - Velocity South and West - Yaw 90 relative (to previous yaw heading) - Velocity North and West - Yaw 90 relative (to previous yaw heading) - - Setting LAND mode... - Completed - APIThread-0 exiting... - LAND> +#. You can run the example against a simulator (DroneKit-SITL) by specifying the Python script without any arguments. + The example will download SITL binaries if needed, start the simulator, and then connect to it: + + .. code-block:: bash + + python guided_set_speed_yaw.py + + On the command prompt you should see (something like): + + .. code:: bash + + Starting copter simulator (SITL) + SITL already Downloaded. + Connecting to vehicle on: tcp:127.0.0.1:5760 + >>> APM:Copter V3.3 (d6053245) + >>> Frame: QUAD + >>> Calibrating barometer + >>> Initialising APM... + >>> barometer calibration complete + >>> GROUND START + Basic pre-arm checks + Waiting for vehicle to initialise... + ... + Waiting for vehicle to initialise... + Arming motors + Waiting for arming... + ... + Waiting for arming... + >>> ARMING MOTORS + >>> GROUND START + Waiting for arming... + >>> Link timeout, no heartbeat in last 5 seconds + >>> ...link restored. + >>> Initialising APM... + Taking off! + Altitude: 0.0 + Altitude: 0.28 + ... + Altitude: 4.76 + Reached target altitude + TRIANGLE path using standard Vehicle.simple_goto() + Set groundspeed to 5m/s. + Position North 80 West 50 + Distance to target: 100.792763565 + Distance to target: 99.912599325 + ... + Distance to target: 1.21731863826 + Distance to target: 0.846001925791 + Reached target + Position North 0 East 100 + Distance to target: 122.623210813 + ... + Distance to target: 4.75876224557 + Distance to target: 0.244650555031 + Reached target + Position North -80 West 50 + Distance to target: 100.792430814 + Distance to target: 100.592652053 + ... + Distance to target: 2.48849019535 + Distance to target: 0.73822537077 + Reached target + TRIANGLE path using standard SET_POSITION_TARGET_GLOBAL_INT message and with varying speed. + Position South 100 West 130 + Set groundspeed to 5m/s. + Distance to target: 188.180927131 + Distance to target: 186.578341133 + ... + Distance to target: 9.87090024758 + Distance to target: 1.4668164732 + Reached target + Set groundspeed to 15m/s (max). + Position South 0 East 200 + Distance to target: 318.826732298 + Distance to target: 320.787965033 + ... + Distance to target: 11.5626483964 + Distance to target: 0.335164775811 + Reached target + Set airspeed to 10m/s (max). + Position North 100 West 130 + Distance to target: 188.182420209 + Distance to target: 189.860730713 + ... + Distance to target: 10.4263414971 + Distance to target: 1.29857175712 + Reached target + SQUARE path using SET_POSITION_TARGET_LOCAL_NED and position parameters + North 50m, East 0m, 10m altitude for 20 seconds + Point ROI at current location (home position) + North 50m, East 50m, 10m altitude + Point ROI at current location + North 0m, East 50m, 10m altitude + North 0m, East 0m, 10m altitude + SQUARE path using SET_POSITION_TARGET_LOCAL_NED and velocity parameters + Yaw 180 absolute (South) + Velocity South & up + Yaw 270 absolute (West) + Velocity West & down + Yaw 0 absolute (North) + Velocity North + Yaw 90 absolute (East) + Velocity East + DIAMOND path using SET_POSITION_TARGET_GLOBAL_INT and velocity parameters + Yaw 225 absolute + Velocity South, West and Up + Yaw 90 relative (to previous yaw heading) + Velocity North, West and Down + Set new home location to current location + Get new home location + Home Location: LocationGlobal:lat=-35.363243103,lon=149.164337158,alt=593.890014648 + Yaw 90 relative (to previous yaw heading) + Velocity North and East + Yaw 90 relative (to previous yaw heading) + Velocity South and East + Setting LAND mode... + Close vehicle object + Completed + + .. tip:: + + It is more interesting to watch the example run on a map than the console. The topic :ref:`viewing_uav_on_map` + explains how to set up *Mission Planner* to view a vehicle running on the simulator (SITL). + +#. You can run the example against a specific connection (simulated or otherwise) by passing the :ref:`connection string ` for your vehicle in the ``--connect`` parameter. + + For example, to connect to SITL running on UDP port 14550 on your local computer: + + .. code-block:: bash + + python guided_set_speed_yaw.py --connect 127.0.0.1:14550 @@ -154,43 +185,56 @@ The example is :ref:`documented in source code `. Ad The functions for controlling vehicle movement are: -* :ref:`Vehicle.commands.goto() ` is the standard DroneKit position controller method. It is called from :ref:`goto ` to fly a triangular path. -* :ref:`goto_position_target_global_int() ` is a position controller that uses the `SET_POSITION_TARGET_GLOBAL_INT `_ command. -* :ref:`goto_position_target_local_ned() ` is a position controller that uses `SET_POSITION_TARGET_LOCAL_NED `_ command (taking values in NED frame, relative to the home position). This is used to fly a square path. The script is put to sleep for a certain time in order to allow the vehicle to reach the specified position. -* :ref:`send_ned_velocity() ` is a velocity controller. It uses `SET_POSITION_TARGET_LOCAL_NED `_ to fly a square path using velocity vectors to define the speed in each direction. -* :ref:`send_global_velocity() ` is a velocity controller. It uses `SET_POSITION_TARGET_GLOBAL_INT `_ to fly a diamond-shaped path. The behaviour is essentially the same as for ``send_ned_velocity()`` because the velocity components in both commands are in the NED frame. -* :ref:`goto ` is a convenience function for specifying a target location in metres from the current location and reporting the result. +* :ref:`Vehicle.simple_goto() ` is the standard + DroneKit position controller method. It is called from :ref:`goto ` to fly a triangular path. +* :ref:`goto_position_target_global_int() ` + is a position controller that uses the + `SET_POSITION_TARGET_GLOBAL_INT `_ command. +* :ref:`goto_position_target_local_ned() ` + is a position controller that uses `SET_POSITION_TARGET_LOCAL_NED `_ + command (taking values in NED frame, relative to the home position). This is used to fly a square path. + The script is put to sleep for a certain time in order to allow the vehicle to reach the specified position. +* :ref:`send_ned_velocity() ` is a velocity controller. + It uses `SET_POSITION_TARGET_LOCAL_NED `_ + to fly a square path using velocity vectors to define the speed in each direction. +* :ref:`send_global_velocity() ` is a velocity controller. + It uses `SET_POSITION_TARGET_GLOBAL_INT `_ + to fly a diamond-shaped path. The behaviour is essentially the same as for ``send_ned_velocity()`` + because the velocity components in both commands are in the NED frame. +* :ref:`goto ` is a convenience function for specifying a target location + in metres from the current location and reporting the result. The functions sending immediate commands are: * :ref:`condition_yaw() ` * :ref:`set_roi(location) ` -* :ref:`set_speed(speed) ` -* :ref:`set_home() ` -The example uses a number functions to convert global locations co-ordinates (decimal degrees) into local coordinates relative to the vehicle (in metres). These are :ref:`described in the guide `. +The example uses a number functions to convert global locations co-ordinates (decimal degrees) into local +coordinates relative to the vehicle (in metres). These are :ref:`described in the guide `. + .. _example_guided_mode_goto_convenience: goto() - convenience function ----------------------------- -This is a convenience function for setting position targets in metres North and East of the current location. It reports the distance to the target every two seconds and completes when the target is reached. +This is a convenience function for setting position targets in metres North and East of the current location. +It reports the distance to the target every two seconds and completes when the target is reached. -This takes a function argument of either :ref:`Vehicle.commands.goto() ` or :ref:`goto_position_target_global_int() ` +This takes a function argument of either :ref:`Vehicle.simple_goto() ` or +:ref:`goto_position_target_global_int() ` .. code-block:: python - def goto(dNorth, dEast, gotoFunction=vehicle.commands.goto): - currentLocation=vehicle.location + def goto(dNorth, dEast, gotoFunction=vehicle.simple_goto): + currentLocation=vehicle.location.global_relative_frame targetLocation=get_location_metres(currentLocation, dNorth, dEast) targetDistance=get_distance_metres(currentLocation, targetLocation) gotoFunction(targetLocation) - vehicle.flush() - - while not api.exit and vehicle.mode.name=="GUIDED": #Stop action if we are no longer in guided mode. - remainingDistance=get_distance_metres(vehicle.location, targetLocation) + + while vehicle.mode.name=="GUIDED": #Stop action if we are no longer in guided mode. + remainingDistance=get_distance_metres(vehicle.location.global_frame, targetLocation) print "Distance to target: ", remainingDistance if remainingDistance<=targetDistance*0.01: #Just below target, in case of undershoot. print "Reached target" @@ -205,8 +249,9 @@ send_ned_velocity() ------------------- The function ``send_ned_velocity()`` generates a ``SET_POSITION_TARGET_LOCAL_NED`` MAVLink message -which is used to directly specify the speed components of the vehicle. The distance travelled is controlled -by a delay before the next command is sent. +which is used to directly specify the speed components of the vehicle. + +The message is resent at 1Hz for a set duration. This is documented in :ref:`the guide here `. @@ -216,65 +261,82 @@ This is documented in :ref:`the guide here send_global_velocity() ---------------------- -The function ``send_global_velocity()`` generates a `SET_POSITION_TARGET_GLOBAL_INT `_ MAVLink message -which is used to directly specify the speed components of the vehicle. The function behaviour is otherwise exactly the same as when using :ref:`SET_POSITION_TARGET_LOCAL_NED ` +The function ``send_global_velocity()`` generates a +`SET_POSITION_TARGET_GLOBAL_INT `_ +MAVLink message which is used to directly specify the speed components of the vehicle in the NED +frame. + +The function behaviour is otherwise exactly the same as when using +:ref:`SET_POSITION_TARGET_LOCAL_NED `. .. code-block:: python - def send_global_velocity(velocity_x, velocity_y, velocity_z): + def send_global_velocity(velocity_x, velocity_y, velocity_z, duration): """ Move vehicle in direction based on specified velocity vectors. """ msg = vehicle.message_factory.set_position_target_global_int_encode( 0, # time_boot_ms (not used) 0, 0, # target system, target component - mavutil.mavlink.MAV_FRAME_GLOBAL_RELATIVE_ALT_INT, # frame + mavutil.mavlink.MAV_FRAME_GLOBAL_RELATIVE_ALT_INT, # frame 0b0000111111000111, # type_mask (only speeds enabled) 0, # lat_int - X Position in WGS84 frame in 1e7 * meters 0, # lon_int - Y Position in WGS84 frame in 1e7 * meters 0, # alt - Altitude in meters in AMSL altitude(not WGS84 if absolute or relative) - # altitude above terrain if GLOBAL_TERRAIN_ALT_INT + # altitude above terrain if GLOBAL_TERRAIN_ALT_INT velocity_x, # X velocity in NED frame in m/s - velocity_y, # Y velocity in NED frame in m/s - velocity_z, # Z velocity in NED frame in m/s + velocity_y, # Y velocity in NED frame in m/s + velocity_z, # Z velocity in NED frame in m/s 0, 0, 0, # afx, afy, afz acceleration (not supported yet, ignored in GCS_Mavlink) 0, 0) # yaw, yaw_rate (not supported yet, ignored in GCS_Mavlink) - # send command to vehicle - vehicle.send_mavlink(msg) - vehicle.flush() + + # send command to vehicle on 1 Hz cycle + for x in range(0,duration): + vehicle.send_mavlink(msg) + time.sleep(1) + +.. note:: + + The message is re-sent every second for the specified duration. From Copter 3.3 the vehicle will stop + moving if a new message is not received in approximately 3 seconds. Prior to Copter 3.3 the message only + needs to be sent once, and the velocity remains active until the next movement message is received. + The above code works for both cases! + - .. _example_guided_mode_goto_position_target_global_int: goto_position_target_global_int() --------------------------------- -The function ``goto_position_target_global_int()`` generates a `SET_POSITION_TARGET_GLOBAL_INT `_ MAVLink message -which is used to directly specify the target location of the vehicle. When used with ``MAV_FRAME_GLOBAL_RELATIVE_ALT_INT`` as shown below, this method is effectively the same as :ref:`Vehicle.commands.goto `. - +The function ``goto_position_target_global_int()`` generates a +`SET_POSITION_TARGET_GLOBAL_INT `_ +MAVLink message which is used to directly specify the target location of the vehicle. +When used with ``MAV_FRAME_GLOBAL_RELATIVE_ALT_INT`` as shown below, +this method is effectively the same as :ref:`Vehicle.simple_goto `. + .. code-block:: python def goto_position_target_global_int(aLocation): """ Send SET_POSITION_TARGET_GLOBAL_INT command to request the vehicle fly to a specified location. - """ + """ msg = vehicle.message_factory.set_position_target_global_int_encode( 0, # time_boot_ms (not used) 0, 0, # target system, target component - mavutil.mavlink.MAV_FRAME_GLOBAL_RELATIVE_ALT_INT, # frame + mavutil.mavlink.MAV_FRAME_GLOBAL_RELATIVE_ALT_INT, # frame 0b0000111111111000, # type_mask (only speeds enabled) aLocation.lat*1e7, # lat_int - X Position in WGS84 frame in 1e7 * meters aLocation.lon*1e7, # lon_int - Y Position in WGS84 frame in 1e7 * meters aLocation.alt, # alt - Altitude in meters in AMSL altitude, not WGS84 if absolute or relative, above terrain if GLOBAL_TERRAIN_ALT_INT 0, # X velocity in NED frame in m/s - 0, # Y velocity in NED frame in m/s - 0, # Z velocity in NED frame in m/s + 0, # Y velocity in NED frame in m/s + 0, # Z velocity in NED frame in m/s 0, 0, 0, # afx, afy, afz acceleration (not supported yet, ignored in GCS_Mavlink) 0, 0) # yaw, yaw_rate (not supported yet, ignored in GCS_Mavlink) + # send command to vehicle vehicle.send_mavlink(msg) - vehicle.flush() In the example code this function is called from the :ref:`goto() ` convenience function. @@ -285,8 +347,10 @@ In the example code this function is called from the :ref:`goto() `_ MAVLink message -which is used to directly specify the target location in the North, East, Down frame. The ``type_mask`` enables the position parameters (the last three bits of of the mask are zero). +The function ``goto_position_target_local_ned()`` generates a +`SET_POSITION_TARGET_LOCAL_NED `_ +MAVLink message which is used to directly specify the target location in the North, East, Down frame. +The ``type_mask`` enables the position parameters (the last three bits of of the mask are zero). .. warning:: @@ -295,47 +359,42 @@ which is used to directly specify the target location in the North, East, Down f .. note:: - The `documentation `_ lists a number of possible frames of reference. At time of writing experimentation indicates that the actual frame use is always relative to the home location (not the vehicle, as indicated by MAV_FRAME_BODY_NED). - + The `MAVLink protocol documentation `_ + lists a number of possible frames of reference. Up until Copter 3.2.1 the actual frame used is always + relative to the home location (as indicated by MAV_FRAME_LOCAL_NED). Starting from Copter 3.3 + you can specify `other frames `_, + for example to move the vehicle relative to its current position. -.. code-block:: python +.. code-block:: python def goto_position_target_local_ned(north, east, down): - """ + """ Send SET_POSITION_TARGET_LOCAL_NED command to request the vehicle fly to a specified location in the North, East, Down frame. - """ + """ msg = vehicle.message_factory.set_position_target_local_ned_encode( 0, # time_boot_ms (not used) 0, 0, # target system, target component - mavutil.mavlink.MAV_FRAME_BODY_NED, # frame + mavutil.mavlink.MAV_FRAME_LOCAL_NED, # frame 0b0000111111111000, # type_mask (only positions enabled) - north, east, down, # x, y, z positions (or North, East, Down in the MAV_FRAME_BODY_NED frame + north, east, down, 0, 0, 0, # x, y, z velocity in m/s (not used) 0, 0, 0, # x, y, z acceleration (not supported yet, ignored in GCS_Mavlink) 0, 0) # yaw, yaw_rate (not supported yet, ignored in GCS_Mavlink) # send command to vehicle vehicle.send_mavlink(msg) - vehicle.flush() - -At time of writing, acceleration and yaw bits are ignored. -Testbed settings -================ +At time of writing, acceleration and yaw bits are ignored. -This demo has been tested on Windows against SITL running both natively and in a virtual machine (as described in :ref:`get-started`). -DroneKit environment (from PIP): - -* droneapi: 1.2.0 -* pymavlink: 1.1.57 -* MAVProxy: 1.4.23 -* protobuf: 2.6.1 +Testbed settings +================ -ArduPilot version: +This example has been tested on Windows against SITL running both natively and in a virtual machine (as described in :ref:`installing_dronekit`). -* `3.3.0beta2 `_. +* DroneKit version: 2.0.2 +* ArduPilot version: 3.3 @@ -344,7 +403,8 @@ ArduPilot version: Source code =========== -The full source code at documentation build-time is listed below (`current version on github `_): +The full source code at documentation build-time is listed below +(`current version on Github `_): .. literalinclude:: ../../examples/guided_set_speed_yaw/guided_set_speed_yaw.py :language: python diff --git a/docs/examples/index.rst b/docs/examples/index.rst index 65b983659..d2d815158 100644 --- a/docs/examples/index.rst +++ b/docs/examples/index.rst @@ -18,9 +18,13 @@ during missions and outside missions using custom commands. Guided Movement and Commands Basic Mission Mission Import/Export + Create Attribute in App Follow Me (Linux only) Drone Delivery Flight Replay + Channel Overrides + Performance Test + diff --git a/docs/examples/mission_basic.rst b/docs/examples/mission_basic.rst index 9e41a0faf..a37afc571 100644 --- a/docs/examples/mission_basic.rst +++ b/docs/examples/mission_basic.rst @@ -12,92 +12,107 @@ command. The guide topic :ref:`auto_mode_vehicle_control` provides more detailed explanation of how the API should be used. +.. figure:: mission_basic_example_copter_path.png + :width: 50 % + :alt: Basic Mission Path + + Basic Mission Example: Flight path + Running the example =================== -The vehicle and DroneKit should be set up as described in :ref:`get-started`. - -If you're using a simulated vehicle remember to :ref:`disable arming checks ` so -that the example can run. - -Once MAVProxy is running and the API is loaded, you can start the example by typing: ``api start mission_basic.py``. - -.. note:: - - The command above assumes you started the *MAVProxy* prompt in a directory containing the example script. If not, - you will have to specify the full path to the script (something like): - ``api start /home/user/git/dronekit-python/examples/mission_basic/mission_basic.py``. - - -On the *MAVProxy* console you should see (something like): - -.. code:: bash - - MAV> api start mission_basic.py - STABILIZE> Clear the current mission - Got MAVLink msg: MISSION_ACK {target_system : 255, target_component : 0, type : 0} - Requesting 0 waypoints t=Wed Jul 29 21:27:58 2015 now=Wed Jul 29 21:27:58 2015 - Create a new mission - Got MAVLink msg: MISSION_ACK {target_system : 255, target_component : 0, type : 0} - Sent waypoint 0 : MISSION_ITEM {target_system : 1, target_component : 0, seq : 0, frame : 0, command : 16, current : 0, autocontinue : 1, param1 : 0.0, param2 : 0.0, param3 : 0.0, param4 : 0.0, x : -35.3632621765, y : 149.165237427, z : 584.0} - Sent waypoint 1 : MISSION_ITEM {target_system : 1, target_component : 0, seq : 1, frame : 3, command : 22, current : 0, autocontinue : 0, param1 : 0, param2 : 0, param3 : 0, param4 : 0, x : 0, y : 0, z : 10} - Sent waypoint 2 : MISSION_ITEM {target_system : 1, target_component : 0, seq : 2, frame : 3, command : 16, current : 0, autocontinue : 0, param1 : 0, param2 : 0, param3 : 0, param4 : 0, x : -35.3628118424, y : 149.164679124, z : 11} - Sent waypoint 3 : MISSION_ITEM {target_system : 1, target_component : 0, seq : 3, frame : 3, command : 16, current : 0, autocontinue : 0, param1 : 0, param2 : 0, param3 : 0, param4 : 0, x : -35.3628118424, y : 149.165780676, z : 12} - Sent waypoint 4 : MISSION_ITEM {target_system : 1, target_component : 0, seq : 4, frame : 3, command : 16, current : 0, autocontinue : 0, param1 : 0, param2 : 0, param3 : 0, param4 : 0, x : -35.3637101576, y : 149.165780676, z : 13} - Sent waypoint 5 : MISSION_ITEM {target_system : 1, target_component : 0, seq : 5, frame : 3, command : 16, current : 0, autocontinue : 0, param1 : 0, param2 : 0, param3 : 0, param4 : 0, x : -35.3637101576, y : 149.164679124, z : 14} - Sent all 6 waypoints - Got MAVLink msg: MISSION_ACK {target_system : 255, target_component : 0, type : 0} - APM: flight plan received - Basic pre-arm checks - Arming motors - Waiting for arming... - Got MAVLink msg: COMMAND_ACK {command : 11, result : 0} - Waiting for arming... - APM: ARMING MOTORS - APM: GROUND START - Waiting for arming... - GUIDED> Mode GUIDED - APM: Initialising APM... - Got MAVLink msg: COMMAND_ACK {command : 400, result : 0} - Waiting for arming... - ARMED - Taking off! - Altitude: 0.0 - Got MAVLink msg: COMMAND_ACK {command : 22, result : 0} - GPS lock at 0 meters - Altitude: 0.10000000149 - ... - Altitude: 8.84000015259 - Altitude: 9.60999965668 - Reached target altitude - Starting mission - Got MAVLink msg: COMMAND_ACK {command : 11, result : 0} - waypoint 1 - waypoint 2 - AUTO> Mode AUTO - Distance to waypoint (2): 79.3138466142 - Distance to waypoint (2): 79.1869592549 - Distance to waypoint (2): 77.8436803794 - ... - Distance to waypoint (2): 20.7677087176 - Distance to waypoint (2): 15.4592692026 - APM: Reached Command #2 - waypoint 3 - Distance to waypoint (3): 115.328425048 - Skipping to Waypoint 4 when reach waypoint 3 - waypoint 4 - Distance to waypoint (4): 152.376018911 - Distance to waypoint (4): 154.882233097 - ... - Distance to waypoint (4): 20.4052797291 - Distance to waypoint (4): 15.0592597507 - APM: Reached Command #4 - waypoint 5 - Distance to waypoint (5): 114.450267446 - Exit 'standard' mission when start heading to final waypoint (5) - Return to launch - APIThread-0 exiting... +The example can be run as described in :doc:`running_examples` (which in turn assumes that the vehicle +and DroneKit have been set up as described in :ref:`installing_dronekit`). + +In summary, after cloning the repository: + +#. Navigate to the example folder as shown: + + .. code-block:: bash + + cd dronekit-python/examples/mission_basic/ + +#. You can run the example against a simulator (DroneKit-SITL) by specifying the Python script without any arguments. + The example will download SITL binaries (if needed), start the simulator, and then connect to it: + + .. code-block:: bash + + python mission_basic.py + + On the command prompt you should see (something like): + + .. code:: bash + + Starting copter simulator (SITL) + SITL already Downloaded. + Connecting to vehicle on: tcp:127.0.0.1:5760 + >>> APM:Copter V3.3 (d6053245) + >>> Frame: QUAD + >>> Calibrating barometer + >>> Initialising APM... + >>> barometer calibration complete + >>> GROUND START + >>> Mission Planner 1.3.35 + Create a new mission (for current location) + Clear any existing commands + Define/add new commands. + Upload new commands to vehicle + Basic pre-arm checks + Waiting for vehicle to initialise... + >>> flight plan received + Waiting for vehicle to initialise... + ... + Waiting for vehicle to initialise... + Arming motors + Waiting for arming... + ... + Waiting for arming... + >>> ARMING MOTORS + >>> GROUND START + Waiting for arming... + >>> Initialising APM... + Taking off! + Altitude: 0.0 + Altitude: 0.11 + ... + Altitude: 8.9 + Altitude: 9.52 + Reached target altitude + Starting mission + Distance to waypoint (0): None + Distance to waypoint (1): 78.8000191616 + Distance to waypoint (1): 78.3723704927 + ... + Distance to waypoint (1): 20.7131390269 + Distance to waypoint (1): 15.4196151863 + >>> Reached Command #1 + Distance to waypoint (2): 115.043560356 + Distance to waypoint (2): 117.463458185 + ... + Distance to waypoint (2): 25.7122243168 + Distance to waypoint (2): 16.8624794106 + >>> Reached Command #2 + Distance to waypoint (3): 100.45231832 + Skipping to Waypoint 5 when reach waypoint 3 + Distance to waypoint (5): 154.645144788 + Exit 'standard' mission when start heading to final waypoint (5) + Return to launch + Close vehicle object + + + .. tip:: + + It is more interesting to watch the example run on a map than the console. The topic :ref:`viewing_uav_on_map` + explains how to set up *Mission Planner* to view a vehicle running on the simulator (SITL). + +#. You can run the example against a specific connection (simulated or otherwise) by passing the :ref:`connection string ` for your vehicle in the ``--connect`` parameter. + + For example, to connect to SITL running on UDP port 14550 on your local computer: + + .. code-block:: bash + + python mission_basic.py --connect 127.0.0.1:14550 @@ -107,9 +122,11 @@ How does it work? The :ref:`source code ` is relatively self-documenting, and most of its main operations are explained in the guide topic :ref:`auto_mode_vehicle_control` . -In overview, the example first calls ``clear_mission()`` to clear the current mission and then creates and -uploads a new mission using ``adds_square_mission(vehicle.location,50)``. This function defines a mission with a takeoff -command and four waypoints arranged in a square around the central position. +In overview, the example calls ``adds_square_mission(vehicle.location.global_frame,50)`` to first +clear the current mission and then define a new mission with a takeoff command and four waypoints arranged +in a square around the central position (two waypoints are added in the last position - +we use :py:func:`next ` to determine when we've reached the final point). +The clear command and new mission items are then uploaded to the vehicle. After taking off (in guided mode using the ``takeoff()`` function) the example starts the mission by setting the mode to AUTO: @@ -118,32 +135,32 @@ After taking off (in guided mode using the ``takeoff()`` function) the example s print "Starting mission" # Set mode to AUTO to start mission vehicle.mode = VehicleMode("AUTO") - vehicle.flush() The progress of the mission is monitored in a loop. The convenience function :ref:`distance_to_current_waypoint() ` gets the distance to the next waypoint and -:py:func:`Vehicle.commands.next ` gets the value of +:py:func:`Vehicle.commands.next ` gets the value of the next command. -We also show how to move to a specified command using -:py:func:`Vehicle.commands.next ` (note how we skip the third command below): +We also show how to jump to a specified command using +:py:func:`Vehicle.commands.next ` (note how we skip the third command below): .. code:: python while True: - nextwaypoint =vehicle.commands.next - if nextwaypoint > 1: - print 'Distance to waypoint (%s): %s' % (nextwaypoint, distance_to_current_waypoint()) - if nextwaypoint ==3: #Skip to next waypoint - print 'Skipping to Waypoint 4 when reach waypoint 3' - vehicle.commands.next=4 - if nextwaypoint ==5: #Skip to next waypoint + nextwaypoint=vehicle.commands.next + print 'Distance to waypoint (%s): %s' % (nextwaypoint, distance_to_current_waypoint()) + + if nextwaypoint==3: #Skip to next waypoint + print 'Skipping to Waypoint 5 when reach waypoint 3' + vehicle.commands.next=5 + vehicle.commands.upload() + if nextwaypoint==5: #Dummy waypoint - as soon as we reach waypoint 4 this is true and we exit. print "Exit 'standard' mission when start heading to final waypoint (5)" break; time.sleep(1) -When the vehicle starts the 5th command the loop breaks and the mode is set to RTL (return to launch). +When the vehicle starts the 5th command (a dummy waypoint) the loop breaks and the mode is set to RTL (return to launch). .. _example_mission_basic_known_issues: @@ -151,9 +168,7 @@ When the vehicle starts the 5th command the loop breaks and the mode is set to R Known issues ============ -This example works around the :ref:`known issues in the API `. -Provided that the vehicle is connected and able to arm, it should run through to completion. - +This example has no known issues. .. _example_mission_basic_source_code: @@ -161,8 +176,9 @@ Provided that the vehicle is connected and able to arm, it should run through to Source code =========== -The full source code at documentation build-time is listed below (`current version on github `_): - +The full source code at documentation build-time is listed below +(`current version on Github `_): + .. literalinclude:: ../../examples/mission_basic/mission_basic.py :language: python - + diff --git a/docs/examples/mission_basic_example_copter_path.png b/docs/examples/mission_basic_example_copter_path.png new file mode 100644 index 000000000..d858dd028 Binary files /dev/null and b/docs/examples/mission_basic_example_copter_path.png differ diff --git a/docs/examples/mission_import_export.rst b/docs/examples/mission_import_export.rst index ce7371ec9..c8b0954db 100644 --- a/docs/examples/mission_import_export.rst +++ b/docs/examples/mission_import_export.rst @@ -8,8 +8,9 @@ This example shows how to import and export files in the `Waypoint file format `_. The commands are first imported from a file into a list and then uploaded to the vehicle. -Then the current mission is downloaded from the vehicle and put into a list, and finally -saved into (another file). +Then the current mission is downloaded from the vehicle and put into a list, which is then +saved into (another file). Finally, we print out both the original and new files +for comparison The example does not show how missions can be modified, but once the mission is in a list, changing the order and content of commands is straightforward. @@ -21,66 +22,89 @@ missions and AUTO mode. Running the example =================== -The vehicle and DroneKit should be set up as described in :ref:`get-started`. - -Once MAVProxy is running and the API is loaded, you can start the example by typing: ``api start mission_import_export.py``. - -.. note:: - - The command above assumes you started the *MAVProxy* prompt in a directory containing the example script. If not, - you will have to specify the absolute path to the script. For example: - ``api start /home/user/git/dronekit-python/examples/mission_import_export/mission_import_export.py``. - - -On the *MAVProxy* console you should see (something like): - -.. code:: bash - - STABILIZE> api start mission_import_export.py - STABILIZE> - Upload mission from a file: mpmission.txt - Reading mission from file: mpmission.txt - - STABILIZE> Import line: 0 1 0 16 0 0 0 0 -35.360500 149.172363 747.000000 1 - - Import line: 1 0 3 22 0.000000 0.000000 0.000000 0.000000 -35.359831 149.166334 100.000000 1 - - - Import line: 2 0 3 16 0.000000 0.000000 0.000000 0.000000 -35.363489 149.167213 100.000000 1 - - Import line: 3 0 3 16 0.000000 0.000000 0.000000 0.000000 -35.355491 149.169595 100.000000 1 - - Import line: 4 0 3 16 0.000000 0.000000 0.000000 0.000000 -35.355071 149.175839 100.000000 1 - - Import line: 5 0 3 113 0.000000 0.000000 0.000000 0.000000 -35.362666 149.178715 22222.000000 1 - Import line: 6 0 3 115 2.000000 22.000000 1.000000 3.000000 0.000000 0.000000 0.000000 1 - - Import line: 7 0 3 16 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 1 - - Clear mission - Requesting 9 waypoints t=Wed Jul 29 20:12:17 2015 now=Wed Jul 29 20:12:17 2015 - ClearCount: 0 - Got MAVLink msg: MISSION_ACK {target_system : 255, target_component : 0, type : 0} - Requesting 0 waypoints t=Wed Jul 29 20:12:17 2015 now=Wed Jul 29 20:12:17 2015 - Got MAVLink msg: MISSION_ACK {target_system : 255, target_component : 0, type : 0} - Sent waypoint 0 : MISSION_ITEM {target_system : 1, target_component : 0, seq : 0, frame : 0, command : 16, current : 0, autocontinue : 1, param1 : 0.0, param2 : 0.0, param3 : 0.0, param4 : 0.0, x : -35.3632621765, y : 149.165237427, z : 584.0} - Sent waypoint 1 : MISSION_ITEM {target_system : 1, target_component : 0, seq : 1, frame : 0, command : 16, current : 1, autocontinue : 1, param1 : 0.0, param2 : 0.0, param3 : 0.0, param4 : 0.0, x : -35.3605, y : 149.172363, z : 747.0} - Sent waypoint 2 : MISSION_ITEM {target_system : 1, target_component : 0, seq : 2, frame : 3, command : 22, current : 0, autocontinue : 1, param1 : 0.0, param2 : 0.0, param3 : 0.0, param4 : 0.0, x : -35.359831, y : 149.166334, z : 100.0} - Sent waypoint 3 : MISSION_ITEM {target_system : 1, target_component : 0, seq : 3, frame : 3, command : 16, current : 0, autocontinue : 1, param1 : 0.0, param2 : 0.0, param3 : 0.0, param4 : 0.0, x : -35.363489, y : 149.167213, z : 100.0} - Sent waypoint 4 : MISSION_ITEM {target_system : 1, target_component : 0, seq : 4, frame : 3, command : 16, current : 0, autocontinue : 1, param1 : 0.0, param2 : 0.0, param3 : 0.0, param4 : 0.0, x : -35.355491, y : 149.169595, z : 100.0} - Sent waypoint 5 : MISSION_ITEM {target_system : 1, target_component : 0, seq : 5, frame : 3, command : 16, current : 0, autocontinue : 1, param1 : 0.0, param2 : 0.0, param3 : 0.0, param4 : 0.0, x : -35.355071, y : 149.175839, z : 100.0} - Sent waypoint 6 : MISSION_ITEM {target_system : 1, target_component : 0, seq : 6, frame : 3, command : 113, current : 0, autocontinue : 1, param1 : 0.0, param2 : 0.0, param3 : 0.0, param4 : 0.0, x : -35.362666, y : 149.178715, z : 22222.0} - Sent waypoint 7 : MISSION_ITEM {target_system : 1, target_component : 0, seq : 7, frame : 3, command : 115, current : 0, autocontinue : 1, param1 : 2.0, param2 : 22.0, param3 : 1.0, param4 : 3.0, x : 0.0, y : 0.0, z : 0.0} - Sent waypoint 8 : MISSION_ITEM {target_system : 1, target_component : 0, seq : 8, frame : 3, command : 16, current : 0, autocontinue : 1, param1 : 0.0, param2 : 0.0, param3 : 0.0, param4 : 0.0, x : 0.0,y : 0.0, z : 0.0} - Sent all 9 waypoints - Got MAVLink msg: MISSION_ACK {target_system : 255, target_component : 0, type : 0} - APM: flight plan received - - Save mission from Vehicle to file: exportedmission.txt - Requesting 9 waypoints t=Wed Jul 29 20:12:18 2015 now=Wed Jul 29 20:12:18 2015 - APIThread-1 exiting... - - +The example can be run as described in :doc:`running_examples` (which in turn assumes that the vehicle +and DroneKit have been set up as described in :ref:`installing_dronekit`). + +In summary, after cloning the repository: + +#. Navigate to the example folder as shown: + + .. code-block:: bash + + cd dronekit-python/examples/mission_import_export/ + +#. You can run the example against a simulator (DroneKit-SITL) by specifying the Python script without any arguments. + The example will download SITL binaries (if needed), start the simulator, and then connect to it: + + .. code-block:: bash + + python mission_import_export.py + + On the command prompt you should see (something like): + + .. code:: bash + + Starting copter simulator (SITL) + SITL already Downloaded. + Connecting to vehicle on: tcp:127.0.0.1:5760 + >>> APM:Copter V3.3 (d6053245) + >>> Frame: QUAD + >>> Calibrating barometer + >>> Initialising APM... + >>> barometer calibration complete + >>> GROUND START + Waiting for vehicle to initialise... + Waiting for vehicle to initialise... + Waiting for vehicle to initialise... + Waiting for vehicle to initialise... + Waiting for vehicle to initialise... + Reading mission from file: mpmission.txt + Upload mission from a file: mpmission.txt + Clear mission + Upload mission + Save mission from Vehicle to file: exportedmission.txt + Download mission from vehicle + >>> flight plan received + Write mission to file + Close vehicle object + Show original and uploaded/downloaded files: + + Mission file: mpmission.txt + QGC WPL 110 + 0 1 0 16 0 0 0 0 -35.363262 149.165237 584.000000 1 + 1 0 0 22 0.000000 0.000000 0.000000 0.000000 -35.361988 149.163753 00.000000 1 + 2 0 0 16 0.000000 0.000000 0.000000 0.000000 -35.361992 149.163593 00.000000 1 + 3 0 0 16 0.000000 0.000000 0.000000 0.000000 -35.363812 149.163609 00.000000 1 + 4 0 0 16 0.000000 0.000000 0.000000 0.000000 -35.363768 149.166055 00.000000 1 + 5 0 0 16 0.000000 0.000000 0.000000 0.000000 -35.361835 149.166012 00.000000 1 + 6 0 0 16 0.000000 0.000000 0.000000 0.000000 -35.362150 149.165046 00.000000 1 + + Mission file: exportedmission.txt + QGC WPL 110 + 0 1 0 16 0 0 0 0 -35.3632621765 149.165237427 583.989990234 1 + 1 0 0 22 0.0 0.0 0.0 0.0 -35.3619880676 149.163757324 100.0 1 + 2 0 0 16 0.0 0.0 0.0 0.0 -35.3619918823 149.163589478 100.0 1 + 3 0 0 16 0.0 0.0 0.0 0.0 -35.3638114929 149.163604736 100.0 1 + 4 0 0 16 0.0 0.0 0.0 0.0 -35.3637695312 149.166061401 100.0 1 + 5 0 0 16 0.0 0.0 0.0 0.0 -35.3618354797 149.166015625 100.0 1 + 6 0 0 16 0.0 0.0 0.0 0.0 -35.3621482849 149.165039062 100.0 1 + + + .. note:: + + The position values uploaded and then downloaded above do not match exactly. This rounding error can be ignored + because the difference is much smaller than the precision provided by GPS. + + The error occurs because all the params are encoded as 32-bit floats rather than 64-bit doubles (Python's native datatype). + +#. You can run the example against a specific connection (simulated or otherwise) by passing the :ref:`connection string ` for your vehicle in the ``--connect`` parameter. + + For example, to connect to SITL running on UDP port 14550 on your local computer: + + .. code-block:: bash + + python mission_import_export.py --connect 127.0.0.1:14550 + How does it work? ================= @@ -95,13 +119,8 @@ More information about the functions can be found in the guide at Known issues ============ -This example works around known issues in the API: - -* A ``time.sleep(1)`` has been placed between uploading the mission to the vehicle (from the file) and downloading the mission. - This is to avoid the race condition where the mission being downloaded has not yet successfully uploaded to the vehicle. - This race condition (probably) shouldn't exist because the mission is flushed to the Vehicle - - see `Race condition when updating and fetching commands `_ - +There are no known issues with this example. + .. _example_mission_import_export_source_code: @@ -110,7 +129,7 @@ Source code =========== The full source code at documentation build-time is listed below (`current version on github `_): - + .. literalinclude:: ../../examples/mission_import_export/mission_import_export.py :language: python - + diff --git a/docs/examples/running_examples.rst b/docs/examples/running_examples.rst index 7bfd2e898..3c20422c6 100644 --- a/docs/examples/running_examples.rst +++ b/docs/examples/running_examples.rst @@ -4,59 +4,53 @@ Running the Examples ==================== -General instructions for running the example source code are given below (more explicit instructions may be -provided in the documentation for each example): +General instructions for running the `example source code `_ are given below. More explicit instructions are provided within the documentation for each example (and within the examples themselves by passing the ``-h`` (help) command line argument). + +.. tip:: + + The examples all launch the :ref:`dronekit-sitl ` simulator and connect to it by default. The ``--connect`` argument is used to instead specify the :ref:`connection string ` for a target vehicle or an externally managed SITL instance. + +To run the examples: + +#. :ref:`Install DroneKit-Python ` if you have not already done so! Install :ref:`dronekit-sitl ` if you want to test against simulated vehicles. #. Get the DroneKit-Python example source code onto your local machine. The easiest way to do this - is to clone the **dronekit-python** repository from Github. On the command prompt enter: + is to clone the **dronekit-python** repository from Github. + + On the command prompt enter: .. code-block:: bash git clone http://github.com/dronekit/dronekit-python.git - .. tip:: + + +#. Navigate to the example you wish to run (or specify the full path in the next step). The examples are all stored in + subdirectories of **dronekit-python/examples/**. + + For example, to run the :ref:`vehicle_state ` example, you would navigate as shown: - The :ref:`Windows Installation ` copies the example code here: - :file:`C:\\Program Files (x86)\\MAVProxy\\examples\\`. + .. code-block:: bash -#. Start MAVProxy and :ref:`connect to the vehicle `. For example: + cd dronekit-python/examples/vehicle_state/ - * To connect to a simulated vehicle when starting *MAVProxy* (from the command line): - .. code-block:: bash +#. Start the example as shown: - mavproxy.py --master=127.0.0.1:14550 + * To connect to a simulator started/managed by the script: - * To connect to a simulated vehicle after starting MAVProxy (for example, on Windows): - .. code-block:: bash - link add 127.0.0.1:14550 + python vehicle_state.py -#. You should already have set up *MAVProxy* to :ref:`load DroneKit automatically `. - If not, manually load the library using: - - .. code-block:: bash - - module load dronekit.module.api - -#. Once the *MAVProxy* console is running, start the example by entering: - - .. code-block:: bash - - api start absolute_path_to_example/example_name.py - - .. tip:: - - If you start *MAVProxy* from the same directory as the target script you can omit - the full file path: + * To connect to a specific vehicle, pass its :ref:`connection string ` via the ``connect`` argument. + For example, to run the example on Solo you would use the following command: + + .. code-block:: bash - .. code-block:: bash + python vehicle_state.py --connect udpin:0.0.0.0:14550 - api start example_name.py - .. warning:: Propellers should be removed before testing examples indoors (on real vehicles). - diff --git a/docs/examples/simple_goto.rst b/docs/examples/simple_goto.rst index 0f1387460..3a3fc45d2 100644 --- a/docs/examples/simple_goto.rst +++ b/docs/examples/simple_goto.rst @@ -4,12 +4,12 @@ Example: Simple Go To (Copter) ============================== -This example demonstrates how to arm and launch a Copter in GUIDED mode, travel to a number of waypoints, and then return -to the home location. It uses :py:func:`Vehicle.commands.takeoff() `, -:py:func:`Vehicle.commands.goto() ` and :py:attr:`Vehicle.mode `. +This example demonstrates how to arm and launch a Copter in GUIDED mode, travel towards a number of target points, and then return +to the home location. It uses :py:func:`Vehicle.simple_takeoff() `, +:py:func:`Vehicle.simple_goto() ` and :py:attr:`Vehicle.mode `. -The locations used are centred around the home location when the :ref:`Simulated Vehicle ` is booted; you can edit the latitude and longitude -to use more appropriate positions for your own vehicle. +The target locations are centered around the home location when the :ref:`Simulated Vehicle ` is booted; +you can edit the latitude and longitude to use more appropriate positions for your own vehicle. .. note:: @@ -19,121 +19,157 @@ to use more appropriate positions for your own vehicle. * *Rover* will ignore the ``takeoff`` command and will then stick at the altitude check. +.. figure:: simple_goto_example_copter_path.png + :width: 75 % + :alt: Setting destination using position and changing speed and ROI + + Simple Goto Example: Flight path + + Running the example =================== -The vehicle and DroneKit should be set up as described in :ref:`get-started`. +The example can be run as described in :doc:`running_examples` (which in turn assumes that the vehicle +and DroneKit have been set up as described in :ref:`installing_dronekit`). -If you're using a simulated vehicle, remember to :ref:`disable arming checks ` so -that the example can run. +In summary, after cloning the repository: -Once MAVProxy is running and the API is loaded, you can start the example by typing: ``api start simple_goto.py``. - -.. note:: +#. Navigate to the example folder as shown: - The command above assumes you started the *MAVProxy* prompt in a directory containing the example script. If not, - you will have to specify the full path to the script (something like): - ``api start /home/user/git/dronekit-python/examples/simple_goto/simple_goto.py``. + .. code-block:: bash -.. tip:: + cd dronekit-python/examples/simple_goto/ + +#. You can run the example against a simulator (DroneKit-SITL) by specifying the Python script without any arguments. + The example will download SITL binaries if needed, start the simulator, and then connect to it: - It is more interesting to watch the example above on a map than the console. The topic :ref:`viewing_uav_on_map` - explains how to set up *Mission Planner* to view a vehicle running on the simulator (SITL). + .. code-block:: bash -On the *MAVProxy* console you should see (something like): + python simple_goto.py -.. code-block:: python + On the command prompt you should see (something like): + + .. code:: bash + + Starting copter simulator (SITL) + SITL already Downloaded. + Connecting to vehicle on: tcp:127.0.0.1:5760 + >>> APM:Copter V3.3 (d6053245) + >>> Frame: QUAD + >>> Calibrating barometer + >>> Initialising APM... + >>> barometer calibration complete + >>> GROUND START + Basic pre-arm checks + Waiting for vehicle to initialise... + ... + Waiting for vehicle to initialise... + Arming motors + Waiting for arming... + ... + Waiting for arming... + >>> ARMING MOTORS + >>> GROUND START + Waiting for arming... + >>> Initialising APM... + Taking off! + Altitude: 0.0 + ... + Altitude: 7.4 + Altitude: 9.0 + Altitude: 9.65 + Reached target altitude + Set default/target airspeed to 3 + Going towards first point for 30 seconds ... + Going towards second point for 30 seconds (groundspeed set to 10 m/s) ... + Returning to Launch + Close vehicle object + + .. tip:: + + It is more interesting to watch the example run on a map than the console. The topic :ref:`viewing_uav_on_map` + explains how to set up *Mission Planner* to view a vehicle running on the simulator (SITL). + +#. You can run the example against a specific connection (simulated or otherwise) by passing the :ref:`connection string ` for your vehicle in the ``--connect`` parameter. + + For example, to connect to SITL running on UDP port 14550 on your local computer: + + .. code-block:: bash + + python simple_goto.py --connect 127.0.0.1:14550 - MAV> api start simple_goto.py - STABILIZE> Basic pre-arm checks - Arming motors - Waiting for arming... - Waiting for arming... - APM: ARMING MOTORS - APM: GROUND START - Waiting for arming... - Waiting for arming... - GUIDED> Mode GUIDED - APM: Initialising APM... - Got MAVLink msg: COMMAND_ACK {command : 400, result : 0} - Waiting for arming... - ARMED - Taking off! - Altitude: 0.0 - Got MAVLink msg: COMMAND_ACK {command : 22, result : 0} - Altitude: 0.10000000149 - Altitude: 0.620000004768 - ... - Altitude: 19.25 - Reached target altitude - Going to first point... - Got MAVLink msg: MISSION_ACK {target_system : 255, target_component : 0, type : 0} - Going to second point... - Got MAVLink msg: MISSION_ACK {target_system : 255, target_component : 0, type : 0} - Returning to Launch - APIThread-0 exiting... - -.. tip:: - - If you get stuck in ``Waiting for arming...`` it is very likely that the vehicle did not pass all pre-arm checks. - On a real device you can view the controller LEDs to determine likely issues. On the Simulator console you - can disable the checks if needed: - - .. code-block:: bash - - STABILIZE>param load ../Tools/autotest/copter_params.parm - STABILIZE>param set ARMING_CHECK 0 How does it work? ================= -The code has three distinct sections: arming and takeoff, flight to a specified location, and return-to-home. +The code has three distinct sections: arming and takeoff, flight to two locations, and return-to-home. Takeoff ------- -To launch *Copter* you need to set the mode to ``GUIDED``, arm the vehicle, and then call -:py:func:`Vehicle.commands.takeoff() `. The takeoff code in this example +To launch *Copter* you need to first check that the vehicle :py:func:`Vehicle.is_armable `. +Then set the mode to ``GUIDED``, arm the vehicle, and call +:py:func:`Vehicle.simple_takeoff() `. The takeoff code in this example is explained in the guide topic :ref:`taking-off`. - -Flying to a point - Goto ------------------------- + +Flying to a point - simple_goto +------------------------------- The vehicle is already in ``GUIDED`` mode, so to send it to a certain point we just need to -call :py:func:`Vehicle.commands.goto() ` with the target location, -and then :py:func:`flush() ` the command: +call :py:func:`Vehicle.simple_goto() ` with the target +:py:class:`dronekit.LocationGlobalRelative`: + +.. code-block:: python -.. code-block:: python + # set the default travel speed + vehicle.airspeed=3 - point1 = Location(-35.361354, 149.165218, 20, is_relative=True) - vehicle.commands.goto(point1) - vehicle.flush() + point1 = LocationGlobalRelative(-35.361354, 149.165218, 20) + vehicle.simple_goto(point1) # sleep so we can see the change in map time.sleep(30) -Without some sort of "wait" the next command would be executed immediately. In this example we just -sleep for 30 seconds - a good opportunity to observe the vehicle's movement on a map. +.. tip:: + + Without some sort of "wait" the next command would be executed immediately. In this example we just + sleep for 30 seconds before executing the next command. + +When moving towards the first point we set the airspeed using the :py:attr:`Vehicle.airspeed ` +attribute. For the second point the example specifies the target groundspeed when calling +:py:func:`Vehicle.simple_goto() ` + +.. code-block:: python + + vehicle.simple_goto(point2, groundspeed=10) + +.. tip:: + + The script doesn't report anything during the sleep periods, + but you can observe the vehicle's movement on a ground station map. + + RTL - Return to launch ------------------------- +---------------------- -To return to the home position and land, we set the mode to ``RTL``: +To return to the home position and land, we set the mode to ``RTL``. +The vehicle travels at the previously set default speed: -.. code-block:: python +.. code-block:: python vehicle.mode = VehicleMode("RTL") - vehicle.flush() Source code =========== -The full source code at documentation build-time is listed below (`current version on github `_): +The full source code at documentation build-time is listed below +(`current version on Github `_): .. literalinclude:: ../../examples/simple_goto/simple_goto.py :language: python \ No newline at end of file diff --git a/docs/examples/simple_goto_example_copter_path.png b/docs/examples/simple_goto_example_copter_path.png new file mode 100644 index 000000000..6c6f1f93d Binary files /dev/null and b/docs/examples/simple_goto_example_copter_path.png differ diff --git a/docs/examples/vehicle_state.rst b/docs/examples/vehicle_state.rst index f07feff9c..64a367f39 100644 --- a/docs/examples/vehicle_state.rst +++ b/docs/examples/vehicle_state.rst @@ -4,99 +4,196 @@ Example: Vehicle State ====================== -This example shows how to get/set vehicle attribute, parameter and channel-override information, +This example shows how to get/set vehicle attribute and parameter information, how to observe vehicle attribute changes, and how to get the home position. -The guide topic :ref:`vehicle-information` provides a more detailed explanation of how the API -should be used. +The guide topic :ref:`vehicle-information` provides a more detailed explanation +of how the API should be used. Running the example =================== -The vehicle and DroneKit should be set up as described in :ref:`get-started`. - -If you're using a simulated vehicle remember to :ref:`disable arming checks ` so -that the example can run. You can also `add a virtual rangefinder `_ -(otherwise the :py:attr:`Vehicle.rangefinder ` attribute may return values of ``None`` for the distance -and voltage). - -Once MAVProxy is running and the API is loaded, you can start the example by typing: ``api start vehicle_state.py``. - -.. note:: - - The command above assumes you started the *MAVProxy* prompt in a directory containing the example script. If not, - you will have to specify the full path to the script (something like): - ``api start /home/user/git/dronekit-python/examples/vehicle_state/vehicle_state.py``. - - -On the *MAVProxy* console you should see (something like): - -.. code:: bash - - MAV> api start vehicle_state.py - STABILIZE> - - Get all vehicle attribute values: - Location: Attitude: Attitude:pitch=-0.00405988190323,yaw=-0.0973932668567,roll=-0.00393210304901 - Velocity: [0.06, -0.07, 0.0] - GPS: GPSInfo:fix=3,num_sat=10 - Groundspeed: 0.0 - Airspeed: 0.0 - Mount status: [None, None, None] - Battery: Battery voltage: 12590, current: 0, level: 99 - Rangefinder: Rangefinder: distance=0.189999997616, voltage=0.0190000012517 - Rangefinder distance: 0.189999997616 - Rangefinder voltage: 0.0190000012517 - Mode: STABILIZE - Armed: False - Set Vehicle.mode=GUIDED (currently: STABILIZE) - Waiting for mode change ... - Got MAVLink msg: COMMAND_ACK {command : 11, result : 0} - GUIDED> Mode GUIDED - Set Vehicle.armed=True (currently: False) - Waiting for arming... - APM: ARMING MOTORS - APM: Initialising APM... - Got MAVLink msg: COMMAND_ACK {command : 400, result : 0} - ARMED - - Add mode attribute observer for Vehicle.mode - Set mode=STABILIZE (currently: GUIDED) - Wait 2s so callback invoked before observer removed - Got MAVLink msg: COMMAND_ACK {command : 11, result : 0} - STABILIZE> Mode STABILIZE - CALLBACK: Mode changed to: STABILIZE - - Get home location - Requesting 0 waypoints t=Fri May 15 11:35:58 2015 now=Fri May 15 11:35:58 2015 - Home WP: MISSION_ITEM {target_system : 255, target_component : 0, seq : 0, frame : 0, command : 16, current : 0, autocontinue : 1, param1 : 0.0, param2 : 0.0, param3 : 0.0, param4 : 0.0, x : -35.3632621765, y : 149.165237427, z : 583.729980469} - - Read vehicle param 'THR_MIN': 130.0 - Write vehicle param 'THR_MIN' : 10 - timeout setting THR_MIN to 10.000000 - Read new value of param 'THR_MIN': 10.0 - - Set MAVLink callback handler (start receiving all MAVLink messages) - Wait 1s so mavrx_debug_handler has a chance to be called before it is removed - Raw MAVLink message: RAW_IMU {time_usec : 894620000, xacc : -3, yacc : 10, zacc : -999, xgyro : 1, ygyro : 0, zgyro : 1, xmag : 153, ymag : 52, zmag : -364} - ... - Raw MAVLink message: SCALED_PRESSURE {time_boot_ms : 895340, press_abs : 945.038024902, press_diff : 0.0, temperature : 2600} - Remove the MAVLink callback handler (stop getting messages) - - - Overriding RC channels for roll and yaw - Current overrides are: {'1': 900, '4': 1000} - Channel default values: {'1': 1500, '3': 1000, '2': 1500, '5': 1800, '4': 1500, '7': 1000, '6': 1000, '8': 1800} - Cancelling override - - Reset vehicle attributes/parameters and exit - Got MAVLink msg: COMMAND_ACK {command : 11, result : 0} - APM: DISARMING MOTORS - Got MAVLink msg: COMMAND_ACK {command : 400, result : 0} - DISARMED - timeout setting THR_MIN to 130.000000 - APIThread-0 exiting... +The example can be run as described in :doc:`running_examples` (which in turn assumes that +the vehicle and DroneKit have been set up as described in :ref:`installing_dronekit`). + +In summary, after cloning the repository: + +#. Navigate to the example folder as shown: + + .. code-block:: bash + + cd dronekit-python/examples/vehicle_state/ + + +#. You can run the example against a simulator (DroneKit-SITL) by specifying the Python script without any arguments. + The example will download SITL binaries (if needed), start the simulator, and then connect to it: + + .. code-block:: bash + + python vehicle_state.py + + On the command prompt you should see (something like): + + .. code:: bash + + Connecting to vehicle on: tcp:127.0.0.1:5760 + >>> APM:Copter V3.3 (d6053245) + >>> Frame: QUAD + >>> Calibrating barometer + >>> Initialising APM... + >>> barometer calibration complete + >>> GROUND START + + Get all vehicle attribute values: + Autopilot Firmware version: APM:Copter-3.3.0-alpha64 + Major version number: 3 + Minor version number: 3 + Patch version number: 0 + Release type: rc + Release version: 0 + Stable release?: True + Autopilot capabilities + Supports MISSION_FLOAT message type: True + Supports PARAM_FLOAT message type: True + Supports MISSION_INT message type: False + Supports COMMAND_INT message type: False + Supports PARAM_UNION message type: False + Supports ftp for file transfers: False + Supports commanding attitude offboard: False + Supports commanding position and velocity targets in local NED frame: True + Supports set position + velocity targets in global scaled integers: True + Supports terrain protocol / data handling: True + Supports direct actuator control: False + Supports the flight termination command: True + Supports mission_float message type: True + Supports onboard compass calibration: False + Global Location: LocationGlobal:lat=-35.363261,lon=149.1652299,alt=None + Global Location (relative altitude): LocationGlobalRelative:lat=-35.363261,lon=149.1652299,alt=0.0 + Local Location: LocationLocal:north=None,east=None,down=None + Attitude: Attitude:pitch=0.00294387154281,yaw=-0.11805768311,roll=0.00139428151306 + Velocity: [-0.03, 0.02, 0.0] + GPS: GPSInfo:fix=3,num_sat=10 + Gimbal status: Gimbal: pitch=None, roll=None, yaw=None + Battery: Battery:voltage=12.587,current=0.0,level=100 + EKF OK?: False + Last Heartbeat: 0.769999980927 + Rangefinder: Rangefinder: distance=None, voltage=None + Rangefinder distance: None + Rangefinder voltage: None + Heading: 353 + Is Armable?: False + System status: STANDBY + Groundspeed: 0.0 + Airspeed: 0.0 + Mode: STABILIZE + Armed: False + Waiting for home location ... + ... + Waiting for home location ... + Waiting for home location ... + + Home location: LocationGlobal:lat=-35.3632621765,lon=149.165237427,alt=583.989990234 + + Set new home location + New Home Location (from attribute - altitude should be 222): LocationGlobal:lat=-35.363261,lon=149.1652299,alt=222 + New Home Location (from vehicle - altitude should be 222): LocationGlobal:lat=-35.3632621765,lon=149.165237427,alt=222.0 + + Set Vehicle.mode=GUIDED (currently: STABILIZE) + Waiting for mode change ... + + Set Vehicle.armed=True (currently: False) + Waiting for arming... + Waiting for arming... + Waiting for arming... + >>> ARMING MOTORS + >>> GROUND START + Waiting for arming... + Waiting for arming... + >>> Initialising APM... + Vehicle is armed: True + + Add `attitude` attribute callback/observer on `vehicle` + Wait 2s so callback invoked before observer removed + CALLBACK: Attitude changed to Attitude:pitch=-0.000483880605316,yaw=-0.0960851684213,roll=-0.00799709651619 + CALLBACK: Attitude changed to Attitude:pitch=0.000153727291035,yaw=-0.0962921902537,roll=-0.00707155792043 + ... + CALLBACK: Attitude changed to Attitude:pitch=0.00485319690779,yaw=-0.100129388273,roll=0.00181497994345 + Remove Vehicle.attitude observer + + Add `mode` attribute callback/observer using decorator + Set mode=STABILIZE (currently: GUIDED) and wait for callback + Wait 2s so callback invoked before moving to next example + CALLBACK: Mode changed to VehicleMode:STABILIZE + + Attempt to remove observer added with `on_attribute` decorator (should fail) + Exception: Cannot remove observer added using decorator + + Add attribute callback detecting ANY attribute change + Wait 1s so callback invoked before observer removed + CALLBACK: (attitude): Attitude:pitch=0.00716688157991,yaw=-0.0950401723385,roll=0.00759896961972 + CALLBACK: (heading): 354 + CALLBACK: (location): + CALLBACK: (airspeed): 0.0 + CALLBACK: (groundspeed): 0.0 + CALLBACK: (ekf_ok): True + CALLBACK: (battery): Battery:voltage=12.538,current=3.48,level=99 + CALLBACK: (gps_0): GPSInfo:fix=3,num_sat=10 + CALLBACK: (location): + CALLBACK: (velocity): [-0.14, 0.1, 0.0] + CALLBACK: (local_position): LocationLocal:north=-0.136136248708,east=-0.0430941730738,down=-0.00938374921679 + CALLBACK: (channels): {'1': 1500, '3': 1000, '2': 1500, '5': 1800, '4': 1500, '7': 1000, '6': 1000, '8': 1800} + ... + CALLBACK: (ekf_ok): True + Remove Vehicle attribute observer + + Read and write parameters + Read vehicle param 'THR_MIN': 130.0 + Write vehicle param 'THR_MIN' : 10 + Read new value of param 'THR_MIN': 10.0 + + Print all parameters (iterate `vehicle.parameters`): + Key:RC7_REV Value:1.0 + Key:GPS_INJECT_TO Value:127.0 + Key:FLTMODE1 Value:7.0 + ... + Key:SR2_POSITION Value:0.0 + Key:SIM_FLOW_DELAY Value:0.0 + Key:BATT_CURR_PIN Value:12.0 + + Create parameter observer using decorator + Write vehicle param 'THR_MIN' : 20 (and wait for callback) + PARAMETER CALLBACK: THR_MIN changed to: 20.0 + + Create (removable) observer for any parameter using wildcard string + Change THR_MID and THR_MIN parameters (and wait for callback) + ANY PARAMETER CALLBACK: THR_MID changed to: 400.0 + PARAMETER CALLBACK: THR_MIN changed to: 30.0 + ANY PARAMETER CALLBACK: THR_MIN changed to: 30.0 + + Reset vehicle attributes/parameters and exit + >>> DISARMING MOTORS + PARAMETER CALLBACK: THR_MIN changed to: 130.0 + ANY PARAMETER CALLBACK: THR_MIN changed to: 130.0 + ANY PARAMETER CALLBACK: THR_MID changed to: 500.0 + + Close vehicle object + Completed + +#. You can run the example against a specific connection (simulated or otherwise) by passing the :ref:`connection string ` for your vehicle in the ``--connect`` parameter. + + For example, to connect to SITL running on UDP port 14550 on your local computer: + + .. code-block:: bash + + python vehicle_state.py --connect 127.0.0.1:14550 + + +.. note:: + + DroneKit-SITL does not automatically add a virtual gimbal and rangefinder, + so these attributes will always report ``None``. + @@ -114,26 +211,16 @@ The guide topic :ref:`vehicle-information` provides an explanation of how this c Known issues ============ -This example works around the :ref:`known issues in the API `. -Provided that the vehicle is connected and able to arm, it should run through to completion. - -Two cases where you may observe issues are: - -* You will see an error ``Timeout setting THR_MIN to 10.000000``. This can be ignored because the value is actually set. - See `#12 Timeout error when setting a parameter `_ for information. -* When the observer sets the mode callback, it waits two seconds after changing the mode before removing the observer - (to ensure that the callback function is run before the observer is removed). In this time you may see the callback being - called twice even though the mode is only changed once. - See `#60 Attribute observer callbacks are called with heartbeat until disabled - after first called `_ - for more information. +This example has no known issues. Source code =========== -The full source code at documentation build-time is listed below (`current version on github `_): - +The full source code at documentation build-time is listed below +(`current version on github `_): + .. literalinclude:: ../../examples/vehicle_state/vehicle_state.py :language: python - + diff --git a/docs/guide/auto_mode.rst b/docs/guide/auto_mode.rst index 65decadd9..4fe9e168b 100644 --- a/docs/guide/auto_mode.rst +++ b/docs/guide/auto_mode.rst @@ -61,33 +61,28 @@ wiki topic provides a more detailed overview of commands. Download current mission ======================== -The mission commands for a vehicle are accessed using the :py:attr:`Vehicle.commands ` -attribute. The attribute is of type :py:class:`CommandSequence `, a class that provides ‘array style’ indexed access to the +The mission commands for a vehicle are accessed using the :py:attr:`Vehicle.commands ` +attribute. The attribute is of type :py:class:`CommandSequence `, a class that provides ‘array style’ indexed access to the waypoints which make up the mission. -Waypoints are not downloaded from vehicle until :py:func:`download() ` is called. The download is asynchronous; -use :py:func:`wait_valid() ` to block your thread until the download is complete: +Waypoints are not downloaded from vehicle until :py:func:`download() ` is called. The download is asynchronous; +use :py:func:`wait_ready() ` to block your thread until the download is complete: .. code:: python - # Connect to API provider and get vehicle - api = local_connect() - vehicle = api.get_vehicles()[0] + # Connect to the Vehicle (in this case a simulated vehicle at 127.0.0.1:14550) + vehicle = connect('127.0.0.1:14550', wait_ready=True) # Download the vehicle waypoints (commands). Wait until download is complete. cmds = vehicle.commands cmds.download() - cmds.wait_valid() + cmds.wait_ready() .. note:: - The commands downloaded from the vehicle will include a waypoint for the :ref:`home location ` in the first position (0 index). - This waypoint is not editable - it cannot be removed or modified. + In DroneKit-Python 2 :py:attr:`Vehicle.commands ` contains just the editable waypoints (in version 1.x, the + commands included the non-editable home location as the first item). -.. todo:: - - The information about home location will change with - `#207 WIP:Adds separate .home_location from .commands array `_. .. _auto_mode_clear_mission: @@ -95,33 +90,26 @@ use :py:func:`wait_valid() ` to block y Clearing current mission ======================== -To clear a mission you call :py:func:`clear() ` and then -:py:func:`flush() ` (to upload the changes to the vehicle): +To clear a mission you call :py:func:`clear() ` and then +:py:func:`Vehicle.commands.upload() ` (to upload the changes to the vehicle): .. code:: python - # Connect to API provider and get vehicle - api = local_connect() - vehicle = api.get_vehicles()[0] + # Connect to the Vehicle (in this case a simulated vehicle at 127.0.0.1:14550) + vehicle = connect('127.0.0.1:14550', wait_ready=True) + + # Get commands object from Vehicle. cmds = vehicle.commands - # Clear Vehicle.commands and flush. + # Call clear() on Vehicle.commands and upload the command to the vehicle. cmds.clear() - vehicle.flush() - - # Reset the Vehicle.commands from the vehicle. - cmds.download() - cmds.wait_valid() - -.. warning:: + cmds.upload() - You must re-download the mission from the vehicle after clearing (as shown above) or the first command you add - will be lost when you upload the new mission. +.. note:: - This happens because :py:attr:`Vehicle.commands ` removes the :ref:`home location ` - (see `#132 `_). Downloading adds it back again. + If a mission that is underway is cleared, the mission will continue to the next waypoint. If you don't add a new command + before the waypoint is reached then the vehicle mode will change to RTL (return to launch) mode. -If the current command completes before you add a new mission, then the vehicle mode will change to RTL (return to launch). .. _auto_mode_adding_command: @@ -130,34 +118,29 @@ Creating/adding mission commands ================================ After :ref:`downloading ` or :ref:`clearing ` a mission new commands -can be added and uploaded to the vehicle. Commands are added to the mission using :py:func:`add() ` -and are sent to the vehicle (either individually or in batches) using :py:func:`flush() `. +can be added and uploaded to the vehicle. Commands are added to the mission using :py:func:`add() ` +and are sent to the vehicle (either individually or in batches) using :py:func:`upload() `. -Each command is packaged in a :py:class:`Command ` object (see that class for the order/meaning of the parameters). +Each command is packaged in a :py:class:`Command ` object (see that class for the order/meaning of the parameters). The supported commands for each vehicle are :ref:`linked above `. .. code:: python - from dronekit.lib import Command - from pymavlink import mavutil - - # Connect to API provider and get vehicle - api = local_connect() - vehicle = api.get_vehicles()[0] + # Connect to the Vehicle (in this case a simulated vehicle at 127.0.0.1:14550) + vehicle = connect('127.0.0.1:14550', wait_ready=True) # Get the set of commands from the vehicle cmds = vehicle.commands cmds.download() - cmds.wait_valid() + cmds.wait_ready() # Create and add commands cmd1=Command( 0, 0, 0, mavutil.mavlink.MAV_FRAME_GLOBAL_RELATIVE_ALT, mavutil.mavlink.MAV_CMD_NAV_TAKEOFF, 0, 0, 0, 0, 0, 0, 0, 0, 10) cmd2=Command( 0, 0, 0, mavutil.mavlink.MAV_FRAME_GLOBAL_RELATIVE_ALT, mavutil.mavlink.MAV_CMD_NAV_WAYPOINT, 0, 0, 0, 0, 0, 0, 10, 10, 10) cmds.add(cmd1) cmds.add(cmd2) - vehicle.flush() # Send commands - + cmds.upload() # Send commands @@ -168,44 +151,43 @@ Modifying missions While you can :ref:`add new commands ` after :ref:`downloading a mission ` it is not possible to directly modify and upload existing commands in ``Vehicle.commands`` (you can modify the commands but -changes do not propagate to the vehicle). +changes do not propagate to the vehicle). + +.. todo:: test above statement. Might not be true in DKPY2. Also check if we can flush items in a cycle. Instead you copy all the commands into another container (e.g. a list), modify them as needed, then clear ``Vehicle.commands`` and upload the list as a new mission: .. code:: python - api = local_connect() - vehicle = api.get_vehicles()[0] - - # Download the current vehicle commands + # Connect to the Vehicle (in this case a simulated vehicle at 127.0.0.1:14550) + vehicle = connect('127.0.0.1:14550', wait_ready=True) + + # Get the set of commands from the vehicle cmds = vehicle.commands cmds.download() - cmds.wait_valid() + cmds.wait_ready() # Save the vehicle commands to a list missionlist=[] - for cmd in cmds[1:]: #skip first item as it is home waypoint. + for cmd in cmds: missionlist.append(cmd) # Modify the mission as needed. For example, here we change the # first waypoint into a MAV_CMD_NAV_TAKEOFF command. missionlist[0].command=mavutil.mavlink.MAV_CMD_NAV_TAKEOFF - # Clear the current mission + # Clear the current mission (command is sent when we call upload()) cmds.clear() - vehicle.flush() - cmds.download() - cmds.wait_valid() #Write the modified mission and flush to the vehicle for cmd in missionlist: cmds.add(cmd) - vehicle.flush() + cmds.upload() The changes are not guaranteed to be complete until -:py:func:`flush() ` is called on the parent ``Vehicle`` object. +:py:func:`upload() ` is called on the parent ``Vehicle.commands`` object. .. _auto_mode_monitoring_controlling: @@ -213,17 +195,15 @@ The changes are not guaranteed to be complete until Running and monitoring missions =============================== -To start a mission change the mode to AUTO: +To start a mission, change the mode to AUTO: .. code:: python - # Get an instance of the API endpoint and a vehicle - api = local_connect() - vehicle = api.get_vehicles()[0] + # Connect to the Vehicle (in this case a simulated vehicle at 127.0.0.1:14550) + vehicle = connect('127.0.0.1:14550', wait_ready=True) # Set the vehicle into auto mode vehicle.mode = VehicleMode("AUTO") - vehicle.flush() .. note:: @@ -239,7 +219,7 @@ AUTO mode the mission will either restart at the beginning or resume at the curr `MIS_RESTART `_ parameter (available on all vehicle types). -You can monitor the progress of the mission by polling the :py:func:`Vehicle.commands.next ` attribute +You can monitor the progress of the mission by polling the :py:func:`Vehicle.commands.next ` attribute to get the current command number. You can also change the current command by setting the attribute to the desired command number. .. code:: python @@ -249,8 +229,8 @@ to get the current command number. You can also change the current command by se vehicle.commands.next=4 print "Current Waypoint: %s" % vehicle.commands.next -There is no need to ``flush()`` changes to ``next`` to the vehicle (and as with other attributes, if you fetch a value, it is updated -from the vehicle). +There is no need to ``upload()`` changes to send an update to the ``next`` attribute to the vehicle +(and as with other attributes, if you fetch a value, it is updated from the vehicle). .. _auto_mode_handle_mission_end: @@ -265,9 +245,9 @@ AUTO mode to start it running again. Currently there is no notification in DroneKit when a mission completes. If you need to detect mission end (in order to perform some other operation) then you can either: -* Add a dummy mission command and poll :py:func:`Vehicle.commands.next ` for the +* Add a dummy mission command and poll :py:func:`Vehicle.commands.next ` for the transition to the final command, or -* Compare the current position to the position in the last command. +* Compare the current position to the target position in the final waypoint. @@ -290,34 +270,32 @@ The implementation calls ``readmission()`` (below) to import the mission from a clears the existing mission and uploads the new version. Adding mission commands is discussed :ref:`here in the guide `. - + .. code:: python def upload_mission(aFileName): - """ - Upload a mission from a file. - """ - missionlist = readmission(aFileName) - #clear existing mission - print 'Clear mission' - cmds = vehicle.commands - cmds.download() - cmds.wait_valid() - cmds.clear() - vehicle.flush() - print 'ClearCount: %s' % cmds.count - #add new mission - cmds.download() - cmds.wait_valid() - for command in missionlist: - cmds.add(command) - vehicle.flush() - - -``readmission()`` reads a mission from the specified file and returns a list of :py:class:`Command ` objects. + """ + Upload a mission from a file. + """ + #Read mission from file + missionlist = readmission(aFileName) + + print "\nUpload mission from a file: %s" % import_mission_filename + #Clear existing mission from vehicle + print ' Clear mission' + cmds = vehicle.commands + cmds.clear() + #Add new mission to vehicle + for command in missionlist: + cmds.add(command) + print ' Upload mission' + vehicle.commands.upload() + + +``readmission()`` reads a mission from the specified file and returns a list of :py:class:`Command ` objects. Each line is split up. The first line is used to test whether the file has the correct (stated) format. -For subsequent lines the values are stored in a :py:class:`Command ` object +For subsequent lines the values are stored in a :py:class:`Command ` object (the values are first cast to the correct ``float`` and ``int`` types for their associated parameters). The commands are added to a list which is returned by the function. @@ -338,7 +316,6 @@ The commands are added to a list which is returned by the function. if not line.startswith('QGC WPL 110'): raise Exception('File is not supported WP version') else: - print ' Import line: %s' % line linearray=line.split('\t') ln_index=int(linearray[0]) ln_currentwp=int(linearray[1]) @@ -350,8 +327,8 @@ The commands are added to a list which is returned by the function. ln_param4=float(linearray[7]) ln_param5=float(linearray[8]) ln_param6=float(linearray[9]) - ln_param7=float(linearray[10]) - ln_autocontinue=int(linearray[11].strip()) + ln_param7=float(linearray[10]) + ln_autocontinue=int(linearray[11].strip()) cmd = Command( 0, 0, 0, ln_frame, ln_command, ln_currentwp, ln_autocontinue, ln_param1, ln_param2, ln_param3, ln_param4, ln_param5, ln_param6, ln_param7) missionlist.append(cmd) return missionlist @@ -367,7 +344,7 @@ Save a mission to a file It uses ``download_mission()`` (below) to get them mission, and then writes the list line-by-line to the file. .. code:: python - + def save_mission(aFileName): """ Save a mission in the Waypoint file format (http://qgroundcontrol.org/mavlink/waypoint_protocol#waypoint_file_format). @@ -380,7 +357,7 @@ It uses ``download_mission()`` (below) to get them mission, and then writes the with open(aFileName, 'w') as file_: file_.write(output) -``download_mission()`` downloads the :py:attr:`Vehicle.commands ` from the vehicle and +``download_mission()`` downloads the :py:attr:`Vehicle.commands ` from the vehicle and adds them to a list. Downloading mission is discussed :ref:`in the guide `. .. code:: python @@ -393,8 +370,8 @@ adds them to a list. Downloading mission is discussed :ref:`in the guide ` +The function determines the current target waypoint number with :py:func:`Vehicle.commands.next ` and uses it to index the commands to get the latitude, longitude and altitude of the target waypoint. The ``get_distance_metres()`` function (see :ref:`guided_mode_copter_useful_conversion_functions`) is then used to calculate and return the (horizontal) distance from the current vehicle location. -The implementation ignores the first waypoint (which will be the "home location"). - .. tip:: This implementation is very basic. It assumes that the next command number is for a valid NAV command (it might not be) @@ -448,17 +423,3 @@ Useful Links * `MAVLink mission command messages `_ (all vehicle types - wiki). - -.. _auto_mode_mission_known_issues: - -Known Issues -============ - -AUTO Mode/mission control has the following known issues (at time of writing): - -* `#230 vehicle.commands must be reset after clearing `_ -* `#132 Vehicle.commands is throwing away the first command sent `_ -* `#252 Expose home location as separate from .commands array `_ -* `#207 WIP:Adds separate .home_location from .commands array `_ -* `#105 Implement Vehicle.waypoint_home `_ -* `#227 Race condition when updating and fetching commands `_ \ No newline at end of file diff --git a/docs/guide/connecting_vehicle.rst b/docs/guide/connecting_vehicle.rst new file mode 100644 index 000000000..af2d68fc2 --- /dev/null +++ b/docs/guide/connecting_vehicle.rst @@ -0,0 +1,101 @@ +.. _connecting_vehicle: +.. _get_started_connecting: + +======================= +Connecting to a Vehicle +======================= + +The connection to the vehicle (or multiple vehicles) is set up within the +DroneKit script. Scripts import and call the :py:func:`connect() ` +method. After connecting this returns a :py:class:`Vehicle ` +object from which you can get/set parameters and attributes, and control vehicle movement. + +The most common way to call :py:func:`connect() ` is shown below: + +.. code-block:: python + + from dronekit import connect + + # Connect to the Vehicle (in this case a UDP endpoint) + vehicle = connect('127.0.0.1:14550', wait_ready=True) + +The first parameter specifies the target address (in this case the loopback +address for UDP port 14550). See :ref:`get_started_connect_string` for the strings to use for +other common vehicles. + +The second parameter (``wait_ready``) is used to determine whether ``connect()`` returns immediately +on connection or if it waits until *some* vehicle parameters and attributes are populated. In most cases you +should use ``wait_ready=True`` to wait on the default set of parameters. + +Connecting over a serial device will look something like this: + +.. code-block:: python + + from dronekit import connect + + # Connect to the Vehicle (in this case a serial endpoint) + vehicle = connect('/dev/ttyAMA0', wait_ready=True, baud=57600) + +.. tip:: + + If the baud rate is not set correctly, ``connect`` may fail with a + timeout error. It is best to set the baud rate explicitly. + +:py:func:`connect() ` also has arguments for setting the baud rate, +the length of the connection timeout, and/or to use +a :ref:`custom vehicle class `. + +There is more documentation on all of the parameters in the :py:func:`API Reference `. + + +.. _connection_string_options: +.. _get_started_connect_string: + +Connection string options +========================= + +The table below shows *connection strings* you can use for some of the more common connection types: + +.. list-table:: + :widths: 10 10 + :header-rows: 1 + + * - Connection type + - Connection string + * - Linux computer connected to the vehicle via USB + - ``/dev/ttyUSB0`` + * - Linux computer connected to the vehicle via Serial port (RaspberryPi example) + - ``/dev/ttyAMA0`` (also set ``baud=57600``) + * - SITL connected to the vehicle via UDP + - ``127.0.0.1:14550`` + * - SITL connected to the vehicle via TCP + - ``tcp:127.0.0.1:5760`` + * - OSX computer connected to the vehicle via USB + - ``dev/cu.usbmodem1`` + * - Windows computer connected to the vehicle via USB (in this case on COM14) + - ``com14`` + * - Windows computer connected to the vehicle using a 3DR Telemetry Radio on COM14 + - ``com14`` (also set ``baud=57600``) + +.. tip:: + + The strings above are the same as are used when connecting the MAVProxy GCS. For other options see the + `MAVProxy documentation `_. + +.. note:: + + The default baud rate may not be appropriate for all connection types (this may be the cause + if you can connect via a GCS but not DroneKit). + + +Connecting to multiple vehicles +=============================== + +You can control multiple vehicles from within a single script by calling +:py:func:`connect() ` for each vehicle +with the appropriate :ref:`connection strings `. + +The returned :py:class:`Vehicle ` objects are independent of +each other and can be separately used to control their respective +vehicle. + diff --git a/docs/guide/copter/guided_mode.rst b/docs/guide/copter/guided_mode.rst index 4c8d5a480..e7ec0def5 100644 --- a/docs/guide/copter/guided_mode.rst +++ b/docs/guide/copter/guided_mode.rst @@ -37,7 +37,9 @@ Position control Controlling the vehicle by explicitly setting the target position is useful when the final position is known/fixed. -The recommended method for position control is :py:func:`Vehicle.commands.goto() `. This takes a :py:class:`Location ` argument for the target position in the global `WGS84 coordinate system `_, but with altitude relative to the home location (home altitude = 0). +The recommended method for position control is :py:func:`Vehicle.simple_goto() `. +This takes a :py:class:`LocationGlobal ` or +:py:class:`LocationGlobalRelative ` argument. The method is used as shown below: @@ -46,18 +48,48 @@ The method is used as shown below: # Set mode to guided - this is optional as the goto method will change the mode if needed. vehicle.mode = VehicleMode("GUIDED") - # Set the target location and then call flush() - a_location = Location(-34.364114, 149.166022, 30, is_relative=True) - vehicle.commands.goto(a_location) - vehicle.flush() + # Set the target location in global-relative frame + a_location = LocationGlobalRelative(-34.364114, 149.166022, 30) + vehicle.simple_goto(a_location) -``Vehicle.commands.goto()`` can be interrupted by a later command, and does not provide any functionality to indicate when the vehicle has reached its destination. Developers can use either a time delay or :ref:`measure proximity to the target ` to give the vehicle an opportunity to reach its destination. The :ref:`example-guided-mode-setting-speed-yaw` shows both approaches. -When moving the vehicle you can send a separate command to :ref:`control the speed ` (and other vehicle behaviour). +``Vehicle.simple_goto()`` can be interrupted by a later command, and does not provide any functionality +to indicate when the vehicle has reached its destination. Developers can use either a time delay or +:ref:`measure proximity to the target ` to give the vehicle an +opportunity to reach its destination. The :ref:`example-guided-mode-setting-speed-yaw` shows both approaches. + +You can optionally set the target movement speed using the function's ``airspeed`` or ``groundspeed`` parameters +(this is equivalent to setting :py:attr:`Vehicle.airspeed ` +or :py:attr:`Vehicle.groundspeed `). The speed setting will then be used +for all positional movement commands until it is set to another value. + +.. code-block:: python + + # Set airspeed using attribute + vehicle.airspeed = 5 #m/s + + # Set groundspeed using attribute + vehicle.groundspeed = 7.5 #m/s + + # Set groundspeed using `simple_goto()` parameter + vehicle.simple_goto(a_location, groundspeed=10) + +.. note:: + + ``Vehicle.simple_goto()`` will use the last speed value set. If both speed values are set at the + same time the resulting behaviour will be vehicle dependent. + .. tip:: - You can also set the position by sending the MAVLink commands `SET_POSITION_TARGET_GLOBAL_INT `_ or `SET_POSITION_TARGET_LOCAL_NED `_, specifying a ``type_mask`` bitmask that enables the position parameters. The main difference between these commands is that the former allows you to specify the location relative to the "global" frames (like ``Vehicle.commands.goto()``), while the later lets you specify the location in NED co-ordinates relative to the home location. For more information on these options see the example code: :ref:`example_guided_mode_goto_position_target_global_int` and :ref:`example_guided_mode_goto_position_target_local_ned`. + You can also set the position by sending the MAVLink commands + `SET_POSITION_TARGET_GLOBAL_INT `_ or + `SET_POSITION_TARGET_LOCAL_NED `_, specifying + a ``type_mask`` bitmask that enables the position parameters. The main difference between these commands is + that the former allows you to specify the location relative to the "global" frames (like + ``Vehicle.simple_goto()``), while the later lets you specify the location in NED co-ordinates relative + to the home location or the vehicle itself. For more information on these options see the example code: + :ref:`example_guided_mode_goto_position_target_global_int` and :ref:`example_guided_mode_goto_position_target_local_ned`. @@ -66,39 +98,61 @@ When moving the vehicle you can send a separate command to :ref:`control the spe Velocity control ---------------- -Controlling vehicle movement using velocity is much smoother than using position when there are likely to be many updates (for example when tracking moving objects). +Controlling vehicle movement using velocity is much smoother than using position when there are likely +to be many updates (for example when tracking moving objects). The function ``send_ned_velocity()`` below generates a ``SET_POSITION_TARGET_LOCAL_NED`` MAVLink message -which is used to directly specify the speed components of the vehicle. +which is used to directly specify the speed components of the vehicle in the ``MAV_FRAME_LOCAL_NED`` +frame (relative to home location). The message is re-sent every second for the specified duration. + +.. note:: + + From Copter 3.3 the vehicle will stop moving if a new message is not received in approximately 3 seconds. + Prior to Copter 3.3 the message only needs to be sent once, and the velocity remains active until the next + movement command is received. The example code works for both cases! + .. code-block:: python - def send_ned_velocity(velocity_x, velocity_y, velocity_z): + def send_ned_velocity(velocity_x, velocity_y, velocity_z, duration): """ Move vehicle in direction based on specified velocity vectors. """ msg = vehicle.message_factory.set_position_target_local_ned_encode( 0, # time_boot_ms (not used) 0, 0, # target system, target component - mavutil.mavlink.MAV_FRAME_BODY_NED, # frame + mavutil.mavlink.MAV_FRAME_LOCAL_NED, # frame 0b0000111111000111, # type_mask (only speeds enabled) 0, 0, 0, # x, y, z positions (not used) velocity_x, velocity_y, velocity_z, # x, y, z velocity in m/s 0, 0, 0, # x, y, z acceleration (not supported yet, ignored in GCS_Mavlink) - 0, 0) # yaw, yaw_rate (not supported yet, ignored in GCS_Mavlink) - # send command to vehicle - vehicle.send_mavlink(msg) - vehicle.flush() - + 0, 0) # yaw, yaw_rate (not supported yet, ignored in GCS_Mavlink) + + + # send command to vehicle on 1 Hz cycle + for x in range(0,duration): + vehicle.send_mavlink(msg) + time.sleep(1) + + The ``type_mask`` parameter is a bitmask that indicates which of the other parameters in the message are used/ignored by the vehicle (0 means that the dimension is enabled, 1 means ignored). In the example the value 0b0000111111000111 is used to enable the velocity components. - -The speed components ``velocity_x`` and ``velocity_y`` are parallel to the North and East directions (not to the front and side of the vehicle). + +In the ``MAV_FRAME_LOCAL_NED`` the speed components ``velocity_x`` and ``velocity_y`` are parallel to the North and East +directions (not to the front and side of the vehicle). The ``velocity_z`` component is perpendicular to the plane of ``velocity_x`` and ``velocity_y``, with a positive value **towards the ground**, following -the right-hand convention. For more information about the ``mavutil.mavlink.MAV_FRAME_BODY_NED`` frame of reference, see this wikipedia article +the right-hand convention. For more information about the ``MAV_FRAME_LOCAL_NED`` frame of reference, see this wikipedia article on `NED `_. +.. tip:: + + From Copter 3.3 you can `specify other frames `_, + for example ``MAV_FRAME_BODY_OFFSET_NED`` makes the velocity components relative to the current vehicle heading. + In Copter 3.2.1 (and earlier) the frame setting is ignored (``MAV_FRAME_LOCAL_NED`` is always used). + + + The code fragment below shows how to call this method: .. code-block:: python @@ -114,16 +168,18 @@ The code fragment below shows how to call this method: UP=-0.5 #NOTE: up is negative! #Fly south and up. - send_ned_velocity(SOUTH,0,UP) + send_ned_velocity(SOUTH,0,UP,DURATION) -The command can be interrupted by a later movement command. When moving the vehicle you can send separate commands to control the yaw (and other behaviour). +When moving the vehicle you can send separate commands to control the yaw (and other behaviour). .. tip:: - You can also control the velocity using the `SET_POSITION_TARGET_GLOBAL_INT `_ MAVLink command in almost exactly the same way (there is no real benefit in sending one command over the other). For more information on this option see :ref:`example_guided_mode_send_global_velocity` in the example code. + You can also control the velocity using the + `SET_POSITION_TARGET_GLOBAL_INT `_ + MAVLink command, as described in :ref:`example_guided_mode_send_global_velocity`. + - .. _guided_mode_copter_accel_force_control: Acceleration and force control @@ -133,14 +189,14 @@ ArduPilot does not currently support controlling the vehicle by specifying accel .. note:: - The `SET_POSITION_TARGET_GLOBAL_INT `_ and - `SET_POSITION_TARGET_LOCAL_NED `_ + The `SET_POSITION_TARGET_GLOBAL_INT `_ and + `SET_POSITION_TARGET_LOCAL_NED `_ MAVLink commands allow you to specify the acceleration, force and yaw. However, commands setting these parameters are ignored by the vehicle. -.. _guided_mode_copter_commands: +.. _guided_mode_copter_commands: Guided mode commands ===================== @@ -153,15 +209,27 @@ This section explains how to send MAVLink commands, what commands can be sent, a Sending messages/commands ------------------------- -MAVLink commands are sent by first using :py:func:`message_factory ` to encode the message and then calling :py:func:`send_mavlink ` and ``flush()`` to send them. +MAVLink commands are sent by first using :py:func:`message_factory() ` +to encode the message and then calling :py:func:`send_mavlink() ` to send them. -``message_factory()`` uses a factory method for the encoding. The name of this method will always be the lower case version of the message/command name with ``_encode`` appended. For example, to encode a `SET_POSITION_TARGET_LOCAL_NED `_ message we call ``message_factory.set_position_target_local_ned_encode()`` with values for all the message fields as arguments: +.. note:: + + Vehicles support a subset of the messages defined in the MAVLink standard. For more information + about the supported sets see wiki topics: + `Copter Commands in Guided Mode `_ + and `Plane Commands in Guided Mode `_. + +``message_factory()`` uses a factory method for the encoding. The name of this method will always be the +lower case version of the message/command name with ``_encode`` appended. For example, to encode a +`SET_POSITION_TARGET_LOCAL_NED `_ +message we call ``message_factory.set_position_target_local_ned_encode()`` with values for all the +message fields as arguments: .. code-block:: python msg = vehicle.message_factory.set_position_target_local_ned_encode( 0, # time_boot_ms (not used) - 0, 0, # target system, target component + 0, 0, # target_system, target_component mavutil.mavlink.MAV_FRAME_BODY_NED, # frame 0b0000111111000111, # type_mask (only speeds enabled) 0, 0, 0, # x, y, z positions @@ -170,18 +238,25 @@ MAVLink commands are sent by first using :py:func:`message_factory `_ can be used send/package *a number* of different `supported MAV_CMD commands `_. The factory function is again the lower case message name with suffix ``_encode`` (``message_factory.command_long_encode``). The message parameters include the actual command to be sent (in the code fragment below ``MAV_CMD_CONDITION_YAW``) and its fields. +In Copter, the `COMMAND_LONG message `_ can be used send/package +*a number* of different `supported MAV_CMD commands `_. +The factory function is again the lower case message name with suffix ``_encode`` (``message_factory.command_long_encode``). +The message parameters include the actual command to be sent (in the code fragment below ``MAV_CMD_CONDITION_YAW``) and its fields. .. code-block:: python msg = vehicle.message_factory.command_long_encode( - 0, 0, # target system, target component + 0, 0, # target_system, target_component mavutil.mavlink.MAV_CMD_CONDITION_YAW, #command 0, #confirmation heading, # param 1, yaw in degrees @@ -191,10 +266,8 @@ In Copter, the `COMMAND_LONG message `_ lists all the commands that *can* be sent to Copter in GUIDED mode (in fact most of the commands can be sent in any mode!) -DroneKit-Python provides a friendly Python API that abstracts many of the commands. Where possible you should use the API rather than send messages directly. For example it is better to use :py:func:`Vehicle.commands.takeoff() ` than to explicitly send the ``MAV_CMD_NAV_TAKEOFF`` command. +DroneKit-Python provides a friendly Python API that abstracts many of the commands. +Where possible you should use the API rather than send messages directly> For example, use: + +* :py:func:`Vehicle.simple_takeoff() ` instead of the ``MAV_CMD_NAV_TAKEOFF`` command. +* :py:func:`Vehicle.simple_goto() `, :py:attr:`Vehicle.airspeed `, + or :py:attr:`Vehicle.groundspeed ` rather than ``MAV_CMD_DO_CHANGE_SPEED``. -Some of the MAV_CMD commands that you might want to send include: :ref:`MAV_CMD_CONDITION_YAW `, :ref:`MAV_CMD_DO_CHANGE_SPEED `, :ref:`MAV_CMD_DO_SET_HOME `, :ref:`MAV_CMD_DO_SET_ROI `, ``MAV_CMD_DO_SET_SERVO``, ``MAV_CMD_DO_REPEAT_SERVO``, ``MAV_CMD_DO_SET_RELAY``, ``MAV_CMD_DO_REPEAT_RELAY``, ``MAV_CMD_DO_FENCE_ENABLE``, ``MAV_CMD_DO_PARACHUTE``, ``MAV_CMD_DO_GRIPPER``, ``MAV_CMD_MISSION_START``. These would be sent in a ``COMMAND_LONG`` message :ref:`as discussed above `. +Some of the MAV_CMD commands that you might want to send include: +:ref:`MAV_CMD_CONDITION_YAW `, +:ref:`MAV_CMD_DO_SET_ROI `, +``MAV_CMD_DO_SET_SERVO``, +``MAV_CMD_DO_REPEAT_SERVO``, +``MAV_CMD_DO_SET_RELAY``, +``MAV_CMD_DO_REPEAT_RELAY``, +``MAV_CMD_DO_FENCE_ENABLE``, +``MAV_CMD_DO_PARACHUTE``, +``MAV_CMD_DO_GRIPPER``, +``MAV_CMD_MISSION_START``. +These would be sent in a ``COMMAND_LONG`` message :ref:`as discussed above `. @@ -236,59 +325,27 @@ You can set the yaw direction using the `MAV_CMD_CONDITION_YAW `_ there is no *safe way* to return to the default yaw "face direction of travel" behaviour. - * After taking off, yaw commands are ignored until the first "movement" command has been received. If you need to + * After taking off, yaw commands are ignored until the first "movement" command has been received. If you need to yaw immediately following takeoff then send a command to "move" to your current position. * :ref:`guided_mode_copter_set_roi` may work to get yaw to track a particular point (depending on the gimbal setup). -.. _guided_mode_copter_set_speed: - -Setting the speed ------------------ - -Send `MAV_CMD_DO_CHANGE_SPEED `_ to change the current speed (metres/second) when travelling to a point. - -.. code-block:: python - - def set_speed(speed): - msg = vehicle.message_factory.command_long_encode( - 0, 0, # target system, target component - mavutil.mavlink.MAV_CMD_DO_CHANGE_SPEED, #command - 0, #confirmation - 0, #param 1 - speed, # speed in metres/second - 0, 0, 0, 0, 0 #param 3 - 7 - ) - - # send command to vehicle - vehicle.send_mavlink(msg) - vehicle.flush() - - -The command is useful when setting the vehicle position directly. It is not needed when controlling movement using velocity vectors. - -.. note:: - - In AC3.2.1 Copter will accelerate to the target speed across the journey and then decelerate as it reaches the target. In AC3.3 the speed changes immediately. - - .. _guided_mode_copter_set_roi: Setting the ROI --------------- -Send the `MAV_CMD_DO_SET_ROI `_ command to point camera gimbal at a specified region of interest (:py:class:`Location `). The vehicle may also turn to face the ROI. - +Send the `MAV_CMD_DO_SET_ROI `_ command to point camera gimbal at a specified region of interest (:py:class:`LocationGlobal `). The vehicle may also turn to face the ROI. + .. code-block:: python def set_roi(location): @@ -304,61 +361,28 @@ Send the `MAV_CMD_DO_SET_ROI `_ - command with zero in all parameters. The front of the vehicle will then follow the direction of travel. - -The ROI (and yaw) is also reset when the mode, or the command used to control movement, is changed. - - -.. _guided_mode_copter_set_home: - -Setting the home location -------------------------- - -Send the `MAV_CMD_DO_SET_HOME `_ command to set the *home location* to either the current location or a specified location. + command with zero in all parameters. The front of the vehicle will then follow the direction of travel. -.. code-block:: python - - def set_home(aLocation, aCurrent=1): - msg = vehicle.message_factory.command_long_encode( - 0, 0, # target system, target component - mavutil.mavlink.MAV_CMD_DO_SET_HOME, #command - 0, #confirmation - aCurrent, #param 1: 1 to use current position, 2 to use the entered values. - 0, 0, 0, #params 2-4 - aLocation.lat, - aLocation.lon, - aLocation.alt - ) - # send command to vehicle - vehicle.send_mavlink(msg) - vehicle.flush() - - -The *home location* is updated immediately in ArduPilot, but the change may not appear in the GCS/*Mission Planner*. You can force an update by reading the mission commands (this works, because the home location is currently implemented as the 0th waypoint command): +The ROI (and yaw) is also reset when the mode, or the command used to control movement, is changed. -.. code-block:: python - # Set new Home location to current location - set_home(vehicle.location) - # Reloads the home location in GCSs - cmds = vehicle.commands - cmds.download() - cmds.wait_valid() - print " Home WP: %s" % cmds[0] -.. _guided_mode_copter_responses: +.. _guided_mode_copter_responses: Command acknowledgements and response values -------------------------------------------- -ArduPilot typically sends a command acknowledgement indicating whether a command was received, and whether it was accepted or rejected. At time of writing there is no way to intercept this acknowledgement in the API (`#168 `_). +ArduPilot typically sends a command acknowledgement indicating whether a command was received, and whether +it was accepted or rejected. At time of writing there is no way to intercept this acknowledgement +in the API (`#168 `_). -Some MAVLink messages request information from the autopilot, and expect the result to be returned in another message. At time of writing you can send the request (provided the message is handled by the AutoPilot in GUIDED mode) but there is no way to intercept the response in DroneKit-Python (`#169 `_). +Some MAVLink messages request information from the autopilot, and expect the result to be returned +in another message. Provided the message is handled by the AutoPilot in GUIDED mode you can send the request +and process the response by creating a :ref:`message listener `. .. _guided_mode_copter_useful_conversion_functions: @@ -377,8 +401,8 @@ to the Earth's poles. def get_location_metres(original_location, dNorth, dEast): """ - Returns a Location object containing the latitude/longitude `dNorth` and `dEast` metres from the - specified `original_location`. The returned Location has the same `alt and `is_relative` values + Returns a LocationGlobal object containing the latitude/longitude `dNorth` and `dEast` metres from the + specified `original_location`. The returned LocationGlobal has the same `alt` value as `original_location`. The function is useful when you want to move the vehicle around specifying locations relative to @@ -397,34 +421,41 @@ to the Earth's poles. #New position in decimal degrees newlat = original_location.lat + (dLat * 180/math.pi) newlon = original_location.lon + (dLon * 180/math.pi) - return Location(newlat, newlon,original_location.alt,original_location.is_relative) + if type(original_location) is LocationGlobal: + targetlocation=LocationGlobal(newlat, newlon,original_location.alt) + elif type(original_location) is LocationGlobalRelative: + targetlocation=LocationGlobalRelative(newlat, newlon,original_location.alt) + else: + raise Exception("Invalid Location object passed") + + return targetlocation; .. code-block:: python def get_distance_metres(aLocation1, aLocation2): """ - Returns the ground distance in metres between two Location objects. - + Returns the ground distance in metres between two `LocationGlobal` or `LocationGlobalRelative` objects. + This method is an approximation, and will not be accurate over large distances and close to the earth's poles. It comes from the ArduPilot test code: https://github.com/diydrones/ardupilot/blob/master/Tools/autotest/common.py """ - dlat = aLocation2.lat - aLocation1.lat - dlong = aLocation2.lon - aLocation1.lon + dlat = aLocation2.lat - aLocation1.lat + dlong = aLocation2.lon - aLocation1.lon return math.sqrt((dlat*dlat) + (dlong*dlong)) * 1.113195e5 - + .. code-block:: python def get_bearing(aLocation1, aLocation2): """ - Returns the bearing between the two Location objects passed as parameters. - + Returns the bearing between the two LocationGlobal objects passed as parameters. + This method is an approximation, and may not be accurate over large distances and close to the earth's poles. It comes from the ArduPilot test code: https://github.com/diydrones/ardupilot/blob/master/Tools/autotest/common.py - """ + """ off_x = aLocation2.lon - aLocation1.lon off_y = aLocation2.lat - aLocation1.lat bearing = 90.00 + math.atan2(-off_y, off_x) * 57.2957795 @@ -434,7 +465,8 @@ to the Earth's poles. .. tip:: - The `common.py `_ file in the ArduPilot test code may have other functions that you will find useful. + The `common.py `_ file + in the ArduPilot test code may have other functions that you will find useful. @@ -442,7 +474,7 @@ Other information ================= * `NED Frame `_ -* `MISSION_ITEM `_ +* `MISSION_ITEM `_ * `GUIDED Mode for Copter `_ (wiki). * `GUIDED mode for Plane `_ (wiki). * `Copter Commands in Guided Mode `_ (wiki). diff --git a/docs/guide/debugging.rst b/docs/guide/debugging.rst index 7037a2bd8..2ce7ef0ed 100644 --- a/docs/guide/debugging.rst +++ b/docs/guide/debugging.rst @@ -4,93 +4,86 @@ Debugging ========= -DroneKit-Python apps are run in the context of a MAVProxy console. This can make debugging -more challenging: +DroneKit-Python apps can be debugged in the same way as any other standalone Python scripts. +The sections below outline a few methods. -* MAVLink updates are continually displayed in the console and may be interspersed with (or even - mixed into) output from the script or a console debug session. This can make it hard to run a debug session - or read debug output. -* It is not possible to directly launch a script in a debug session. Instead debugging must be - either started from within the script or the debugger must be attached to the running script. -That said, it is possible to effectively debug DroneKit apps. The main methods are discussed below. -.. note:: +pdb - The Python Debugger +========================= - We are actively working to improve debugging on DroneKit-Python! You can track progress and suggestions - on `Github Issue #118 `_. +The `Python Debugger - pdb `_ can be used to debug *DroneKit-Python* apps. +The command below can be used to run a script in debug mode: -Print/log statements -==================== +.. code-block:: bash -The simplest and most common method of debugging is to manually add debug/print statements to the source. + python -m pdb my_dronekit_script.py + +You can also instrument your code to invoke the debugger at a certain point. To do this +add ``set-trace()`` at the point where you want to break execution: .. code-block:: python - :emphasize-lines: 6 + :emphasize-lines: 4 - # Get the vehicle - api = local_connect() - vehicle = api.get_vehicles()[0] + # Connect to the Vehicle on udp at 127.0.0.1:14550 + vehicle = connect('127.0.0.1:14550', wait_ready=True) - # print out debug information - print "Location: %s" % vehicle.location + import pdb; pdb.set_trace() + print "Global Location: %s" % vehicle.location.global_frame -In addition to printing DroneKit variables, Python provides numerous inbuilt and add-on modules/methods -for inspecting code (e.g. `dir() `_, `traceback `_, etc.) +The available `debugger commands are listed here `_. -pdb - The Python Debugger -========================= +pudb - A full-screen, console-based Python debugger +=================================================== -The `Python Debugger - pdb `_ can be used to debug *DroneKit-Python* apps. -To start debugging, add ``set-trace()`` at the point where you want to break execution (as shown below): +If you prefer a IDE like debug you can use `pudb - A full-screen, console-based Python debugger `_. +.. code-block:: python + :emphasize-lines: 4 + + pip install pudb + + +To start debugging, simply insert: .. code-block:: python - :emphasize-lines: 5 - - # Get the vehicle - api = local_connect() - vehicle = api.get_vehicles()[0] - - import pdb; pdb.set_trace() - print "Location: %s" % v.location + :emphasize-lines: 4 -When you run the app, the code will stop at the marked line: + from pudb import set_trace; set_trace() -.. code:: bash +Insert either of these snippets into the piece of code you want to debug, or run the entire script with: + +.. code-block:: python + :emphasize-lines: 4 + + pudb my-script.py - MAV> api start small_demo.py - AUTO> > c:\users\hamis_000\documents\vagranttesting\tmpdeleteme\small_demo.py(20)() - -> print "Location: %s" % v.location - -Press **Enter** to bring up the **(Pdb)** prompt. This is where you can enter the commands to step through the code, show stack traces, -etc. For example the console output below shows the **w** command being used to output the current stack trace. -.. code:: bash +Print/log statements +==================== + +The simplest and most common method of debugging is to manually add debug/print statements to the source. + +.. code-block:: python + :emphasize-lines: 4 - AUTO> - (Pdb) w - c:\users\hamis_000\downloads\winpython-64bit-2.7.6.4\python-2.7.6.amd64\lib\threading.py(783)__bootstrap() - cAUTO> :\users\hamis_000\downloads\winpython-64bit-2.7.6.4\python-2.7.6.amd64\lib\site-packages\dronekit\module\api.py(321)run() - -> self.fn()amis_000\downloads\winpython-64bit-2.7.6.4\python-2.7.6.amd64\lib\threading.py(810)__bootstrap_inner() - c:\users\hamis_000\downloads\winpython-64bit-2.7.6.4\python-2.7.6.amd64\lib\site-packages\dronekit\module\api.py(592)() - -> APIThread(self, lambda: execfile(args[1], g), args[1]) - -The available `debugger commands are listed here `_. For more information -about *pdb* see the `Python Debugger site `_. - -.. note:: + # Connect to the Vehicle on udp at 127.0.0.1:14550 + vehicle = connect('127.0.0.1:14550', wait_ready=True) + + # print out debug information + print "Global Location: %s" % vehicle.location.global_frame + +In addition to printing DroneKit variables, Python provides numerous inbuilt and add-on modules/methods +for inspecting code (e.g. `dir() `_, `traceback `_, etc.) + + +Other IDEs/debuggers +==================== - *Pdb* commands must be entered in the **(Pdb)** prompt. If you press "Enter" in an empty prompt the previous command will be called - again. This is helpful if you want to **s** (step) through every line of code. +There is no reason you should not be able to straightforwardly use other popular Python IDEs including IDLE and Eclipse. -Other debuggers -=============== -At time of writing we don't have information about how to debug DroneKit apps with other debuggers (although anecdotally *gdb* has -successfully been used). -If you have information about how to set up and use other debuggers with DroneKit, please :ref:`contribute them `. \ No newline at end of file diff --git a/docs/guide/getting_started.rst b/docs/guide/getting_started.rst deleted file mode 100644 index 349c748ec..000000000 --- a/docs/guide/getting_started.rst +++ /dev/null @@ -1,293 +0,0 @@ -.. _get-started: - -=============== -Getting Started -=============== - -DroneKit-Python apps are typically run on Linux-based *companion computers* that travel -on the vehicle and communicate with the autopilot via a serial port. However, during development it is usually easier to -prototype apps on a standard Mac, Windows, or Linux computer using a *simulated* autopilot. - - -This topic explains how to set up and run DroneKit-Python (within MAVProxy) on the different host operating systems -and then run a basic DroneKit app. - - - - -Setting up the vehicle/autopilot -================================ - -For information on how to set up a vehicle (real and simulated) see: - -* :ref:`supported-companion-computers` for links to tested hardware/software configurations for a number of onboard Linux computers. -* :ref:`sitle_setup` for links explaining how to set up a simulated vehicle for Copter, Plane, or Rover. - - - -Installing DroneKit -=================== - -*DroneKit* can be installed on Linux, Windows and Mac OSX. - - -.. _getting_started_installing_dronekit_linux: - -Installing DroneKit on Linux ----------------------------- - -If you are using Ubuntu or Debian Linux you can get most of the *DroneKit* dependencies by running: - -.. code:: bash - - sudo apt-get install python-pip python-dev python-opencv python-serial python-pyparsing python-wxgtk2.8 - - -The remaining dependencies (including `MAVProxy `_), are -installed when you get DroneKit-Python from the public PyPi repository: - -.. code:: bash - - sudo pip install dronekit - - - -.. tip:: - - If you are planning to run *DroneKit* on a :ref:`companion computer `, make sure that the - computer runs a variant of Linux that support Python and can install Python packages from the internet. - - -Installing DroneKit on Mac OSX ------------------------------- - -If you're on Mac OSX, you can use `Homebrew `_ to install *WXMac*. - -.. code:: bash - - brew tap homebrew/science - brew install wxmac wxpython opencv - - -Uninstall *python-dateutil* (OSX and Windows come bundled with a version that is not supported for some dependencies): - -.. code:: bash - - sudo pip uninstall python-dateutil - -Install DroneKit-Python and its remaining dependencies (including `MAVProxy `_) from the public PyPi repository: - -.. code:: bash - - sudo pip install pyparsing - sudo pip install dronekit - - -.. _get_started_install_dk_windows: - -Installing DroneKit on Windows ------------------------------- - -The easiest way to set up DroneKit-Python on Windows is to use the Windows Installer. -This is applied over the top of the *MAVProxy* Windows installation and includes all needed -dependencies and the DroneKit-Python examples. - -.. tip:: - - A new version of the Windows Installer is created with every patch revision (`get old versions - here `_). - Don't forget to update regularly for bug fixes and new features! - -To install DroneKit-Python using the installer: - -#. Download and run the `latest MAVProxy installer `_ - — accept all prompts. -#. Download and run the `latest DroneKit installer `_ - — accept all prompts (install in the same location as MAVProxy). - -The installer packages DroneKit-Python as an application, which is launched by double-clicking an icon -in the system GUI. After the *MAVProxy prompt* and *console* have started you can -:ref:`connect to the vehicle ` (instead of setting the -connection when starting *MAVProxy*). You will still need to :ref:`load DroneKit ` (not done by the installer -- see `#267 `_). The examples are copied to :file:`C:\\Program Files (x86)\\MAVProxy\\examples\\`. - -It is also possible to set up DroneKit-Python on the command line (see :ref:`dronekit_development_windows`). - - -.. _starting-mavproxy: - -Starting MAVProxy -================= - -Before executing DroneKit scripts you must first start *MAVProxy* and connect to your autopilot (simulated or real). -The connection to the vehicle can be set up on the command line when starting *MAVProxy* or after MAVProxy is running. - -.. tip:: - - If you're using DroneKit-Python from the Windows installer there is no way to pass command line options to MAVProxy; - you will have to start MAVProxy by double-clicking its icon and then :ref:`connect to the target vehicle after MAVProxy - has started `. - -Connecting at startup ---------------------- - -The table below shows the command lines used to start *MAVProxy* for the respective connection types: - -.. list-table:: MAVProxy connection options - :widths: 10 10 - :header-rows: 1 - - * - Connection type - - MAVProxy command - * - Linux computer connected to the vehicle via USB - - ``mavproxy.py --master=/dev/ttyUSB0`` - * - Linux computer connected to the vehicle via Serial port (RaspberryPi example) - - ``mavproxy.py --master=/dev/ttyAMA0 --baudrate 57600`` - * - SITL connected to the vehicle via UDP - - ``mavproxy.py --master=127.0.0.1:14550`` - * - OSX computer connected to the vehicle via USB - - ``mavproxy.py --master=/dev/cu.usbmodem1`` - * - Windows computer connected to the vehicle via USB - - ``mavproxy.py --master=/dev/cu.usbmodem1`` - -For other connection options see the `MAVProxy documentation `_. - -.. _starting-mavproxy_set_link_when_mavproxy_running: - -Connecting after startup ------------------------- - -To connect to the autopilot once *MAVProxy* has already started use ``link add `` in the *MAVProxy command prompt*, where ```` -takes the same values as ``master`` in the table above. For example, to set up a connection to SITL running on the local computer at port 14550 do: - -.. code:: bash - - link add 127.0.0.1:14550 - -If you're connecting using a serial port you may need to first set up the baud rate first (the default is 57600). You can change the default baudrate used for -new connections as shown: - -.. code:: bash - - set baudrate 57600 #Set the default baud rate for new connections (do before calling "link add") - -See `Link Management `_ (MAVProxy documentation) for more information. - - - - -.. _loading-dronekit: - -Loading DroneKit -================ - -*DroneKit* is implemented as a *MAVProxy* module (MAVProxy is installed automatically with DroneKit). -The best way to load the *DroneKit* module into *MAVProxy* is to -`add it to the startup script `_ (**mavinit.scr**). - -Linux/MAC OSX: - -.. code:: bash - - echo "module load dronekit.module.api" >> ~/.mavinit.scr - -Windows: - -.. code:: bash - - echo module load dronekit.module.api >> %HOMEPATH%\AppData\Local\MAVProxy\mavinit.scr - - -Alternatively you can choose to manually (re)load *DroneKit* into *MAVProxy* every time you need it: - -.. code-block:: bash - :emphasize-lines: 1 - - MANUAL> module load dronekit.module.api - DroneAPI loaded - MANUAL> - - - -.. _getting-started-running_examples: - -Running an app/example -====================== - -This section shows how to run the :ref:`Vehicle State ` example, -which reads and writes :ref:`vehicle state and parameter ` information. - -.. warning:: - - This example doesn't take off, but it does arm the motors. Don't run any example indoors on a real vehicle - unless you have first removed its propellers. - -The steps are: - -#. Get the DroneKit-Python example source code onto your local machine. - - The easiest way to do this is to clone the **dronekit-python** repository from Github. - On the command prompt enter: - - .. code-block:: bash - - git clone http://github.com/dronekit/dronekit-python.git - - .. tip:: - - The :ref:`Windows Installation ` copies the example code here: - :file:`C:\\Program Files (x86)\\MAVProxy\\examples\\`. - -#. Start MAVProxy and :ref:`connect to the vehicle `. For example: - - * To connect to a simulated vehicle when starting *MAVProxy* (from the command line): - - .. code-block:: bash - - mavproxy.py --master=127.0.0.1:14550 - - * To connect to a simulated vehicle after starting *MAVProxy* (for example, on Windows): - - .. code-block:: bash - - link add 127.0.0.1:14550 - -#. You should already have set up *MAVProxy* to :ref:`load DroneKit automatically `. - If not, manually load the library using: - - .. code-block:: bash - - module load dronekit.module.api - -#. Once the *MAVProxy* console is running, start ``vehicle_state.py`` by entering ``api start`` followed by the - full file path of the script. For example: - - .. code-block:: bash - - api start "C:\Program Files (x86)\MAVProxy\examples\vehicle_state\vehicle_state.py" - - - The output should look something like that shown below - - .. code-block:: bash - :emphasize-lines: 1 - - MANUAL> api start "C:\Program Files (x86)\MAVProxy\examples\vehicle_state\vehicle_state.py" - STABILIZE> - - Get all vehicle attribute values: - Location: Attitude: Attitude:pitch=-0.00405988190323,yaw=-0.0973932668567,roll=-0.00393210304901 - Velocity: [0.06, -0.07, 0.0] - GPS: GPSInfo:fix=3,num_sat=10 - groundspeed: 0.0 - airspeed: 0.0 - mount_status: [None, None, None] - Mode: STABILIZE - Armed: False - Set Vehicle.mode=GUIDED (currently: STABILIZE) - Waiting for mode change ... - Got MAVLink msg: COMMAND_ACK {command : 11, result : 0} - ... - - -For more information on running the examples (and other apps) see :ref:`running_examples_top`. diff --git a/docs/guide/index.rst b/docs/guide/index.rst index 612d296a7..c6d10c7b7 100644 --- a/docs/guide/index.rst +++ b/docs/guide/index.rst @@ -2,18 +2,16 @@ Guide ===== -The guide contains how-to documentation for using the DroneKit-Python API. These show how to :ref:`set up DroneKit` and use the API. Additional guide material can be found in the :ref:`example-toc`. - +The guide contains how-to documentation for using the DroneKit-Python API. +Additional guide material can be found in the :ref:`example-toc`. .. toctree:: :maxdepth: 1 - migrating - getting_started - companion-computers - Simulated Vehicle + connecting_vehicle vehicle_state_and_parameters taking_off copter/guided_mode Missions debugging + mavlink_messages \ No newline at end of file diff --git a/docs/guide/mavlink_messages.rst b/docs/guide/mavlink_messages.rst new file mode 100644 index 000000000..16cac5747 --- /dev/null +++ b/docs/guide/mavlink_messages.rst @@ -0,0 +1,87 @@ +.. _mavlink_messages: + +================ +MAVLink Messages +================ + +Some useful MAVLink messages sent by the autopilot are not (yet) directly available to DroneKit-Python scripts +through the :ref:`observable attributes ` in :py:class:`Vehicle `. + +This topic shows how you can intercept specific MAVLink messages by defining a listener callback function +using the :py:func:`Vehicle.on_message() ` decorator. + +.. tip:: + + :ref:`example_create_attribute` shows how you can extend this approach to create a new :py:class:`Vehicle ` + attribute in your client code. + + +.. _mavlink_messages_message_listener: +.. _mavlink_messages_set_mavlink_callback: + +Creating a message listener +=========================== + +The :py:func:`Vehicle.on_message() ` decorator can be used to +set a particular function as the callback handler for a particular message type. You can create listeners +for as many messages as you like, or even multiple listeners for the same message. + +For example, the code snippet below shows how to set a listener for the ``RANGEFINDER`` message: + +.. code:: python + + #Create a message listener using the decorator. + @vehicle.on_message('RANGEFINDER') + def listener(self, name, message): + print message + +.. tip:: + + Every single listener can have the same name/prototpye as above ("``listener``") because + Python does not notice the decorated functions are in the same scope. + + Unfortunately this also means that you can't unregister a callback created using this method. + +The messages are `classes `_ from the +`pymavlink `_ library. +The output of the code above looks something like: + +.. code:: bash + + RANGEFINDER {distance : 0.0899999961257, voltage : 0.00900000054389} + ... + +You can access the message fields directly. For example, to access the ``RANGEFINDER`` message your listener +function might look like this: + +.. code:: python + + #Create a message listener using the decorator. + @vehicle.on_message('RANGEFINDER') + def listener(self, name, message): + print 'distance: %s' % message.distance + print 'voltage: %s' % message.voltage + + +Watching all messages +===================== + +You can register a callback for *all messages* by setting the message name as the wildcard string ('``*``'): + +.. code:: python + + #Create a message listener for all messages. + @vehicle.on_message('*') + def listener(self, name, message): + print 'message: %s' % message + + +Removing an observer +==================== + +Callbacks registered using the :py:func:`Vehicle.on_message() ` decorator *cannot be removed*. +This is generally not a problem, because in most cases you're interested in messages for the lifetime of a session. + +If you do need to be able to remove messages you can instead add the callback using +:py:func:`Vehicle.add_message_listener `, and then remove it by calling +:py:func:`Vehicle.remove_message_listener `. diff --git a/docs/guide/migrating.rst b/docs/guide/migrating.rst deleted file mode 100644 index fca08b729..000000000 --- a/docs/guide/migrating.rst +++ /dev/null @@ -1,135 +0,0 @@ -.. _migrating_dkpy2_0: - -===================== -Migrating to DKPY 2.0 -===================== - -DroneKit-Python 2.0 has undergone a significant *architectural* evolution when compared to version 1.x (the library changed from a MAVProxy extension -to a standalone Python module). The API itself remains largely compatible, with the most important difference being that you -now need to specify the vehicle target address inside the script. - -The sections below outline the main migration areas. - - -Installation -============ - -DKPY 2.0 is installed from `pip` on all platforms - see :ref:`get-started` for more information. - -.. note:: - - The DroneKit-Python Windows installer is no longer needed. Installation is generally simpler - than on DK 1.x because MAVProxy is not a dependency. - - -Launching scripts -================= - -DroneKit-Python 2.0 apps are run from an ordinary Python command prompt. For example: - -.. code:: bash - - some_python_script.py # or `python some_python_script.py` - -.. note:: - - This contrasts with DKPY 1.x scripts, which were run from within MAVProxy using the command: - - .. code:: bash - - api start some_python_script.py - - -Code changes -============ - -This section outlines the changes you will need to make to your DroneKit-Python scripts. - -Connecting to a vehicle ------------------------ - -You must specify the target vehicle address in your script (in DKPY 1.x this was done when you launched MAVProxy). - -The code fragment below shows how you import the :py:func:`connect() ` method and use it to return a -connected :py:class:`Vehicle ` object. The address string passed to ``connect()`` takes the same -values as were passed to MAVProxy when setting up a connection in DKPY 1.x (in this case, a SITL instance running on the same computer). - -.. code:: python - - from dronekit import connect - - # Connect to the Vehicle - vehicle = connect('127.0.0.1:14550') - - # Wait for attributes to accumulate. - time.sleep(5) - -The thread is normally suspended for a few seconds after connecting. This allows *MAVLink* messages to arrive from the connected vehicle -and populate the ``Vehicle`` attributes (before they are read). The vehicle can then be used in exactly the same way as in DKPY 1.x. - -.. note:: - - The above code replaces DKPY 1.x code to get the Vehicle (similar to the example below): - - .. code:: python - - # Get an instance of the API endpoint - api = local_connect() - # Get the connected vehicle (currently only one vehicle can be returned). - vehicle = api.get_vehicles()[0] - - - -.. todo:: Above link to the connect method in API ref - make sure connect() is documented. - - -Exit status checks ------------------- - -Remove code that checks the ``api.exit`` status (note that the ``api.exit`` call below is commented out). DroneKit no -longer runs in *MAVProxy* so scripts don't need to monitor and act on external thread shutdown commands. - -.. code:: python - - while not vehicle.armed # and not api.exit: - print " Waiting for arming..." - time.sleep(1) - -.. note:: - - In fact you should delete all references to ``APIConnection`` class and its methods (``get_vehicles()``, ``exit()`` and ``stop()``). - - -.. todo:: Find out how to check the connection status is still valid. That would go in separate section. - - - -Command line arguments ----------------------- - -Remove any code that uses the ``local_arguments`` array to get script-local command line arguments (via MAVProxy). - -From DKPY 2.0 command line arguments are passed to ``sys.argv`` as with any other script. The examples use the -`argparse `_ module for argument parsing, but you can -use whatever method you like. - -.. note:: - - In DKPY 1.x the script's ``sys.argv`` values were the values passed to MAVProxy when it was - started. To access arguments passed to the script from *MAVProxy you used the ``local_arguments`` array. - For example if you started a script as shown below: - - .. code:: bash - - api start my_script.py 101 - - Then you would read the integer in your code using - - .. code:: python - - my_argument = int(local_arguments[0]) - - -.. todo:: This addition closes https://github.com/dronekit/dronekit-python/issues/13 - - diff --git a/docs/guide/quick_start.rst b/docs/guide/quick_start.rst new file mode 100644 index 000000000..7dc795e04 --- /dev/null +++ b/docs/guide/quick_start.rst @@ -0,0 +1,118 @@ +.. _quick_start_top: + +=========== +Quick Start +=========== + +This topic shows how to quickly install a DroneKit-Python +*development environment* and run a simple example to get +vehicle attributes from a *simulated* Copter. + + +Installation +============ + +DroneKit-Python and the *dronekit-sitl simulator* are installed +from **pip** on all platforms. + +On Linux you will first need to install **pip** and **python-dev**: + +.. code-block:: bash + + sudo apt-get install python-pip python-dev + + +**pip** is then used to install *dronekit* and *dronekit-sitl*. +Mac and Linux may require you to prefix these commands with ``sudo``: + +.. code-block:: bash + + pip install dronekit + pip install dronekit-sitl + +See :doc:`../develop/installation` and `dronekit-sitl `_ +for more detailed installation instructions. + + +Basic "Hello Drone" +=================== + +The script below first launches the simulator. It then +imports and calls the :py:func:`connect() ` method, +specifying the simulator's connection string (``tcp:127.0.0.1:5760``). +The method returns a :py:class:`Vehicle ` object that +we then use to query the attributes. + +.. code:: python + + print "Start simulator (SITL)" + import dronekit_sitl + sitl = dronekit_sitl.start_default() + connection_string = sitl.connection_string() + + # Import DroneKit-Python + from dronekit import connect, VehicleMode + + # Connect to the Vehicle. + print("Connecting to vehicle on: %s" % (connection_string,)) + vehicle = connect(connection_string, wait_ready=True) + + # Get some vehicle attributes (state) + print "Get some vehicle attribute values:" + print " GPS: %s" % vehicle.gps_0 + print " Battery: %s" % vehicle.battery + print " Last Heartbeat: %s" % vehicle.last_heartbeat + print " Is Armable?: %s" % vehicle.is_armable + print " System status: %s" % vehicle.system_status.state + print " Mode: %s" % vehicle.mode.name # settable + + # Close vehicle object before exiting script + vehicle.close() + + # Shut down simulator + sitl.stop() + print("Completed") + + +Copy the text above into a new text file (**hello.py**) and run it in the same way +as you would any other standalone Python script. + +.. code-block:: bash + + python hello.py + +You should see the following output from the simulated vehicle: + +.. code-block:: bash + + Start simulator (SITL) + Downloading SITL from http://dronekit-assets.s3.amazonaws.com/sitl/copter/sitl-win-copter-3.3.tar.gz + Extracted. + Connecting to vehicle on: 'tcp:127.0.0.1:5760' + >>> APM:Copter V3.3 (d6053245) + >>> Frame: QUAD + >>> Calibrating barometer + >>> Initialising APM... + >>> barometer calibration complete + >>> GROUND START + Get some vehicle attribute values: + GPS: GPSInfo:fix=3,num_sat=10 + Battery: Battery:voltage=12.587,current=0.0,level=100 + Last Heartbeat: 0.713999986649 + Is Armable?: False + System status: STANDBY + Mode: STABILIZE + Completed + +That's it- you've run your first DroneKit-Python script. + +Next Steps +========== + +* Learn more about :doc:`../develop/index`. + This covers development best practices and coding standards, + and has more information about installation, working with a simulator + and setting up a companion computer. +* Read through our step by step :doc:`index` to learn how to connect to your + vehicle, takeoff, fly, and much more. +* Check out our :doc:`../examples/index`. diff --git a/docs/guide/sitl_setup.rst b/docs/guide/sitl_setup.rst deleted file mode 100644 index 76cd5d839..000000000 --- a/docs/guide/sitl_setup.rst +++ /dev/null @@ -1,134 +0,0 @@ -.. _sitle_setup: - -===================================== -Setting up a Simulated Vehicle (SITL) -===================================== - -The `SITL (software in the loop) `_ -simulator allows you to create and test DroneKit-Python apps without a real vehicle (and from the comfort of -your own developer desktop!) - -SITL runs natively on Linux and Windows, or within a virtual machine on Mac OSX, Linux or Windows. It can be -installed on the same computer as DroneKit, or on another computer on the same network. - -The sections below explain how set up SITL for the different operating systems, -and how you can set up SITL to connect to Mission Planner and DroneKit at the same time. - -Setting up SITL on Linux -======================== - -Build and install SITL on Linux using the instructions in: -`Setting up SITL on Linux `_ (ArduPilot wiki) - - -Setting up SITL on Windows -========================== - -Build and install SITL on Windows using the instructions in: -`Setting up SITL on Windows `_ -(ArduPilot wiki) - - -.. _vagrant-sitl-from-full-image: - -Set up SITL using Vagrant (MacOS) -================================= - -This section shows how to bring up a pre-built SITL instance hosted in a `Vagrant `_ -virtual machine. This approach should be used if you need to run SITL on MacOS. It can also be used for Windows -and Linux (though we recommend the native installations linked above). - -.. warning:: - - The Vagrant virtual machine is "headless" (has no UI) and so SITL cannot display the MAVProxy map and console. - You can still see and send messages in the SITL Command Prompt. - - -#. Get the software for hosting the Simulator onto your computer (Windows, OS-X and Linux are supported): - - * `Download and install VirtualBox `_. - * `Download and install Vagrant `_. - -#. Install SSH (Windows only - SSH is present by default on Linux/Mac OSX) - - * Download and install `Git for Windows `_ (or another client that comes with SSH). - After installing you can locate the file using the command ``C:\where ssh`` (normally it is installed to **C:\Program Files (x86)\Git\bin\ssh.exe** - * Add the ssh.exe location to the *Path* (**System Properties | Advanced tab | Environment Variables | Path**) - -#. Create a new directory where you will run *Vagrant*, and open a command prompt/terminal in it: - -#. Enter the following commands to fetch a *Vagrantfile* for the pre-built SITL image: - - .. code:: bash - - vagrant init 3drobotics/ardupilot-sitl - -#. Launch the new image. This takes a long time the *first time* it is run while it downloads the image from the Internet. - - .. code:: bash - - vagrant up - -#. SSH into the vagrant instance, and start a vehicle: - - .. code:: bash - - vagrant ssh - ./sitl.sh - - When prompted, enter your desired vehicle (e.g. "copter") to build/start SITL. - Once complete, you will see a MAVProxy prompt displaying periodic vehicle-status updates: - - .. code:: bash - - Ready to FLY ublox Received 454 parameters - fence breach - APM: PreArm: RC not calibrated - ... - -.. _disable-arming-checks: - -#. Load a default set of parameters and disable the arming check: - - .. code:: bash - - STABILIZE>param load ../Tools/autotest/copter_params.parm - STABILIZE>param set ARMING_CHECK 0 - - .. note:: - - SITL simulates (by default) a vehicle that may not pass the arming check. This change makes the simulated - vehicle more forgiving, which allows the examples to arm and run. - - You should never disable the arming check in a script or on a real vehicle. - - -.. _viewing_uav_on_map: - -Connecting an additional Ground Station -======================================= - -This section explains how you can connect multiple ground stations to a running SITL instance in addition to your DroneKit MAVProxy link. - -To do this you first need to get SITL to output to an additional UDP port of your computer: - -* Find the network IP address of your computer (On Windows you can get this by running *ipconfig* in the *Windows Command Prompt*). -* In the *SITL Command Prompt*, add the IP address of the GCS computer (e.g. 192.168.2.10) and an unused port (e.g. 145502) as an output: - - .. code:: bash - - output add 192.168.2.10:14552 - -Then connect Mission Planner to this UDP port: - -* `Download and install Mission Planner `_ -* Ensure the selection list at the top right of the Mission Planner screen says *UDP* and then select the **Connect** button next to it. - When prompted, enter the port number (in this case 14552). - - .. figure:: MissionPlanner_ConnectPort.png - :width: 50 % - - Mission Planner: Listen Port Dialog - -After connecting, vehicle parameters will be loaded into *Mission Planner* and the vehicle is displayed on the map. - diff --git a/docs/guide/taking_off.rst b/docs/guide/taking_off.rst index 0ab398ab6..725ce11e5 100644 --- a/docs/guide/taking_off.rst +++ b/docs/guide/taking_off.rst @@ -4,8 +4,10 @@ Taking Off ========== -This article explains how to get your *Copter* to take off. At high level, the steps are: set the mode to ``GUIDED``, -arm the vehicle, and then call :py:func:`Vehicle.commands.takeoff() `. +This article explains how to get your *Copter* to take off. + +At high level, the steps are: check that the vehicle is *able* to arm, set the mode to ``GUIDED``, +command the vehicle to arm, takeoff and block until we reach the desired altitude. .. todo:: @@ -15,16 +17,25 @@ arm the vehicle, and then call :py:func:`Vehicle.commands.takeoff() `_ waypoint - in your mission (you can run a mission by switching to ``AUTO`` mode after you're in the air). + Copter is usually started in ``GUIDED`` mode. + + * For Copter 3.2.1 and earlier you cannot take off in ``AUTO`` mode (if you need to run a mission you take off + in ``GUIDED`` mode and then switch to ``AUTO`` mode once you're in the air). + * Starting from Copter 3.3 you can takeoff in ``AUTO`` mode (provided the mission has a + `MAV_CMD_NAV_TAKEOFF `_ command) + but the mission will not start until you explicitly send the + `MAV_CMD_MISSION_START `_ + message. + + By contrast, Plane apps take off using the ``MAV_CMD_NAV_TAKEOFF`` command in a mission. + Plane should first arm and then change to ``AUTO`` mode to start the mission. The code below shows a function to arm a Copter, take off, and fly to a specified altitude. This is taken from :ref:`example_simple_goto`. .. code-block:: python - api = local_connect() - vehicle = api.get_vehicles()[0] + # Connect to the Vehicle (in this case a simulator running the same computer) + vehicle = connect('tcp:127.0.0.1:5760', wait_ready=True) def arm_and_takeoff(aTargetAltitude): """ @@ -32,39 +43,36 @@ The code below shows a function to arm a Copter, take off, and fly to a specifie """ print "Basic pre-arm checks" - # Don't let the user try to fly autopilot is booting - if vehicle.mode.name == "INITIALISING": - print "Waiting for vehicle to initialise" + # Don't try to arm until autopilot is ready + while not vehicle.is_armable: + print " Waiting for vehicle to initialise..." time.sleep(1) - while vehicle.gps_0.fix_type < 2: - print "Waiting for GPS...:", vehicle.gps_0.fix_type - time.sleep(1) - + print "Arming motors" # Copter should arm in GUIDED mode vehicle.mode = VehicleMode("GUIDED") vehicle.armed = True - vehicle.flush() - while not vehicle.armed and not api.exit: + # Confirm vehicle armed before attempting to take off + while not vehicle.armed: print " Waiting for arming..." time.sleep(1) print "Taking off!" - vehicle.commands.takeoff(aTargetAltitude) # Take off to target altitude - vehicle.flush() + vehicle.simple_takeoff(aTargetAltitude) # Take off to target altitude # Wait until the vehicle reaches a safe height before processing the goto (otherwise the command - # after Vehicle.commands.takeoff will execute immediately). - while not api.exit: - print " Altitude: ", vehicle.location.alt - if vehicle.location.alt>=aTargetAltitude*0.95: #Just below target, in case of undershoot. + # after Vehicle.simple_takeoff will execute immediately). + while True: + print " Altitude: ", vehicle.location.global_relative_frame.alt + #Break and return from function just below target altitude. + if vehicle.location.global_relative_frame.alt>=aTargetAltitude*0.95: print "Reached target altitude" - break; + break time.sleep(1) - arm_and_takeoff(3) - + arm_and_takeoff(20) + The function first performs some pre-arm checks. @@ -73,37 +81,49 @@ The function first performs some pre-arm checks. Arming turns on the vehicle's motors in preparation for flight. The flight controller will not arm until the vehicle has passed a series of pre-arm checks to ensure that it is safe to fly. -DroneKit-Python can't check every possible symptom that might prevent arming, but we can confirm that the -vehicle has booted and has a GPS lock: +These checks are encapsulated by the :py:func:`Vehicle.is_armable ` +attribute, which is ``true`` when the vehicle has booted, EKF is ready, and the vehicle has GPS lock. .. code-block:: python - if v.mode.name == "INITIALISING": - print "Waiting for vehicle to initialise" - time.sleep(1) - while vehicle.gps_0.fix_type < 2: - print "Waiting for GPS...:", vehicle.gps_0.fix_type - time.sleep(1) + print "Basic pre-arm checks" + # Don't let the user try to arm until autopilot is ready + while not vehicle.is_armable: + print " Waiting for vehicle to initialise..." + time.sleep(1) + +.. note:: + + If you need more status information you can perform the following sorts of checks: + + .. code-block:: python + + if v.mode.name == "INITIALISING": + print "Waiting for vehicle to initialise" + time.sleep(1) + while vehicle.gps_0.fix_type < 2: + print "Waiting for GPS...:", vehicle.gps_0.fix_type + time.sleep(1) + + You should always do a final check on :py:func:`Vehicle.is_armable `! + -Once the vehicle is ready we set the mode to ``GUIDED`` and arm it. We then call :py:func:`flush() ` -to guarantee that the commands have been sent, and then wait until arming is confirmed before sending the -:py:func:`takeoff ` command. +Once the vehicle is ready we set the mode to ``GUIDED`` and arm it. We then wait until arming is confirmed +before sending the :py:func:`takeoff ` command. .. code-block:: python - + print "Arming motors" # Copter should arm in GUIDED mode vehicle.mode = VehicleMode("GUIDED") vehicle.armed = True - vehicle.flush() - while not vehicle.armed and not api.exit: + while not vehicle.armed: print " Waiting for arming..." time.sleep(1) print "Taking off!" - vehicle.commands.takeoff(aTargetAltitude) # Take off to target altitude - vehicle.flush() + vehicle.simple_takeoff(aTargetAltitude) # Take off to target altitude The ``takeoff`` command is asynchronous and can be interrupted if another command arrives before it reaches the target altitude. This could have potentially serious consequences if the vehicle is commanded to move @@ -111,15 +131,16 @@ horizontally before it reaches a safe height. In addition, there is no message s to inform the client code that the target altitude has been reached. To address these issues, the function waits until the vehicle reaches a specified height before returning. If you're not -so concerned about reaching a particular height, a simpler implementation might just "wait" for a few seconds. - -.. code-block:: python - - while not api.exit: - print " Altitude: ", vehicle.location.alt - if vehicle.location.alt>=aTargetAltitude*0.95: #Just below target, in case of undershoot. - print "Reached target altitude" - break; - time.sleep(1) +concerned about reaching a particular height, a simpler implementation might just "wait" for a few seconds. + +.. code-block:: python + + while True: + print " Altitude: ", vehicle.location.global_relative_frame.alt + #Break and return from function just below target altitude. + if vehicle.location.global_relative_frame.alt>=aTargetAltitude*0.95: + print "Reached target altitude" + break + time.sleep(1) -When the function returns the app can continue in ``GUIDED`` mode or switch to ``AUTO`` mode to start a mission. \ No newline at end of file +When the function returns the app can continue in ``GUIDED`` mode or switch to ``AUTO`` mode to start a mission. diff --git a/docs/guide/vehicle_state_and_parameters.rst b/docs/guide/vehicle_state_and_parameters.rst index b0b220d9d..1bb23ddde 100644 --- a/docs/guide/vehicle_state_and_parameters.rst +++ b/docs/guide/vehicle_state_and_parameters.rst @@ -4,13 +4,12 @@ Vehicle State and Settings =========================== -The :py:class:`Vehicle ` class exposes *most* state information (position, speed, etc.) through python +The :py:class:`Vehicle ` class exposes *most* state information (position, speed, etc.) through python :ref:`attributes `, while vehicle :ref:`parameters ` (settings) -are accessed though named elements of :py:attr:`Vehicle.parameters `. +are accessed though named elements of :py:attr:`Vehicle.parameters `. This topic explains how to get, set and observe vehicle state and parameter information (including getting the -:ref:`Home location `). It also describes a few APIs that -:ref:`should be used with caution `. +:ref:`Home location `). .. tip:: You can test most of the code in this topic by running the :ref:`Vehicle State ` example. @@ -21,107 +20,150 @@ Attributes ========== Vehicle state information is exposed through vehicle *attributes*. DroneKit-Python currently supports the following -"standard" attributes: -:py:attr:`Vehicle.location `, -:py:attr:`Vehicle.attitude `, -:py:attr:`Vehicle.velocity `, -:py:attr:`Vehicle.airspeed `, -:py:attr:`Vehicle.groundspeed `, -:py:attr:`Vehicle.gps_0 `, -:py:attr:`Vehicle.mount_status `, -:py:attr:`Vehicle.battery `, -:py:attr:`Vehicle.rangefinder `, -:py:attr:`Vehicle.armed `, -:py:attr:`Vehicle.mode `. - -All of the attributes can be :ref:`read ` and :ref:`observed `, -but only the :py:attr:`Vehicle.mode ` and :py:attr:`Vehicle.armed ` -status can be :ref:`written `. - - +"standard" attributes: +:py:attr:`Vehicle.version `, +:py:attr:`Vehicle.location.capabilities `, +:py:attr:`Vehicle.location.global_frame `, +:py:attr:`Vehicle.location.global_relative_frame `, +:py:attr:`Vehicle.location.local_frame `, +:py:attr:`Vehicle.attitude `, +:py:attr:`Vehicle.velocity `, +:py:attr:`Vehicle.gps_0 `, +:py:attr:`Vehicle.gimbal `, +:py:attr:`Vehicle.battery `, +:py:attr:`Vehicle.rangefinder `, +:py:attr:`Vehicle.ekf_ok `, +:py:attr:`Vehicle.last_heartbeat `, +:py:attr:`Vehicle.home_location `, +:py:func:`Vehicle.system_status `, +:py:func:`Vehicle.heading `, +:py:func:`Vehicle.is_armable `, +:py:attr:`Vehicle.airspeed `, +:py:attr:`Vehicle.groundspeed `, +:py:attr:`Vehicle.armed `, +:py:attr:`Vehicle.mode `. + +Attributes are initially created with ``None`` values for their members. In most cases the members are populated +(and repopulated) as new MAVLink messages of the associated types are received from the vehicle. + +All of the attributes can be :ref:`read `, +but only the :py:attr:`Vehicle.home_location `, +:py:attr:`Vehicle.gimbal ` +:py:attr:`Vehicle.airspeed `, +:py:attr:`Vehicle.groundspeed `, +:py:attr:`Vehicle.mode ` and +:py:attr:`Vehicle.armed ` +status can be :ref:`set `. + +Almost all of the attributes can be :ref:`observed `. + +The behaviour of :py:attr:`Vehicle.home_location ` is different +from the other attributes, and is :ref:`discussed in its own section below `. .. _vehicle_state_read_attributes: Getting attributes ------------------ -The code fragment below shows how to read and print all the attributes. The values are retrieved from the remote device -(not cached). +The code fragment below shows how to read and print almost all the attributes (values are +regularly updated from MAVLink messages sent by the vehicle). .. code:: python # vehicle is an instance of the Vehicle class - print "Location: %s" % vehicle.location + print "Autopilot Firmware version: %s" % vehicle.version + print "Autopilot capabilities (supports ftp): %s" % vehicle.capabilities.ftp + print "Global Location: %s" % vehicle.location.global_frame + print "Global Location (relative altitude): %s" % vehicle.location.global_relative_frame + print "Local Location: %s" % vehicle.location.local_frame #NED print "Attitude: %s" % vehicle.attitude print "Velocity: %s" % vehicle.velocity print "GPS: %s" % vehicle.gps_0 print "Groundspeed: %s" % vehicle.groundspeed print "Airspeed: %s" % vehicle.airspeed - print "Mount status: %s" % vehicle.mount_status + print "Gimbal status: %s" % vehicle.gimbal print "Battery: %s" % vehicle.battery + print "EKF OK?: %s" % vehicle.ekf_ok + print "Last Heartbeat: %s" % vehicle.last_heartbeat print "Rangefinder: %s" % vehicle.rangefinder print "Rangefinder distance: %s" % vehicle.rangefinder.distance print "Rangefinder voltage: %s" % vehicle.rangefinder.voltage + print "Heading: %s" % vehicle.heading + print "Is Armable?: %s" % vehicle.is_armable + print "System status: %s" % vehicle.system_status.state print "Mode: %s" % vehicle.mode.name # settable print "Armed: %s" % vehicle.armed # settable -If an attribute cannot be retrieved then the returned object will contain -``None`` values for its members (for example, if there was no GPS lock then -:py:attr:`Vehicle.gps_0 ` would return a :py:class:`GPSInfo ` -with ``None`` values for ``eph``, ``satellites_visible`` etc.) -Attributes will also return ``None`` if the associated hardware is not present on the connected device. +.. note:: + + A value of ``None`` for an attribute member indicates that the value has not yet been populated from the vehicle. + For example, before GPS lock :py:attr:`Vehicle.gps_0 ` will return a + :py:class:`GPSInfo ` with ``None`` values for ``eph``, ``satellites_visible`` etc. + Attributes will also return ``None`` if the associated hardware is not present on the connected device. + .. tip:: - If you're using a :ref:`simulated vehicle ` you can add support for optional hardware including + If you're using a :ref:`simulated vehicle ` you can add support for optional hardware including `rangefinders `_ and `optical flow sensors `_. - - + .. todo:: we need to be able to verify mount_status works/setup. .. _vehicle_state_set_attributes: - + Setting attributes ------------------ -Only the :py:attr:`Vehicle.mode ` and :py:attr:`Vehicle.armed ` -attributes can be written. +The :py:attr:`Vehicle.mode `, :py:attr:`Vehicle.armed ` +, :py:attr:`Vehicle.airspeed ` and :py:attr:`Vehicle.groundspeed `, +attributes can all be "directly" written (:py:attr:`Vehicle.home_location ` can also be directly written, +but has special considerations that are :ref:`discussed below `). -The attributes are set by assigning a value. Calling :py:func:`Vehicle.flush() ` -then forces DroneKit to send outstanding messages. +These attributes are set by assigning a value: .. code:: python #disarm the vehicle vehicle.armed = False - vehicle.flush() # Flush to ensure changes are sent to autopilot - - -.. warning:: - - After ``flush()`` returns the message is guaranteed to have been sent to the autopilot, but it is **not guaranteed to succeed**. - For example, vehicle arming can fail if the vehicle doesn't pass pre-arming checks. - - While the autopilot does send information about the success (or failure) of the request, this is `not currently handled by DroneKit `_. + + #set the default groundspeed to be used in movement commands + vehicle.groundspeed = 3.2 -Code should not assume that an attempt to set an attribute will succeed. The example code snippet below polls the attribute values -to confirm they have changed before proceeding. +Commands to change a value are **not guaranteed to succeed** (or even to be received) and code should be written with this in mind. +For example, the code snippet below polls the attribute values to confirm they have changed before proceeding. .. code:: python vehicle.mode = VehicleMode("GUIDED") vehicle.armed = True - vehicle.flush() # Flush to ensure changes are sent to autopilot while not vehicle.mode.name=='GUIDED' and not vehicle.armed and not api.exit: print " Getting ready to take off ..." time.sleep(1) + +.. note:: + + While the autopilot does send information about the success (or failure) of the request, + this is `not currently handled by DroneKit `_. + +:py:attr:`Vehicle.gimbal ` can't be written directly, but the gimbal can be controlled using the +:py:func:`Vehicle.gimbal.rotate() ` and :py:func:`Vehicle.gimbal.target_location() ` +methods. The first method lets you set the precise orientation of the gimbal while the second makes the gimbal track a specific "region of interest". + +.. code:: python + #Point the gimbal straight down + vehicle.gimbal.rotate(-90, 0, 0) + time.sleep(10) + + #Set the camera to track the current home position. + vehicle.gimbal.target_location(vehicle.home_location) + time.sleep(10) .. _vehicle_state_observe_attributes: @@ -129,240 +171,292 @@ to confirm they have changed before proceeding. Observing attribute changes --------------------------- -You can observe any of the attributes and will receive notification every time a value is received from the connected vehicle. -This allows you to monitor changes to velocity and other vehicle state without the need for polling. - -Observers are added using :py:func:`Vehicle.add_attribute_observer() `, -specifying the name of the attribute to observe and a callback function. The same string is passed to the callback -when it is notified. Observers are removed using :py:func:`remove_attribute_observer() `. - -The code snippet below shows how to add (and remove) a callback function to observe :py:attr:`location ` attribute changes. The two second ``sleep()`` is required because otherwise the observer might be removed before the the callback is first run. - -.. code:: python +You can observe any of the vehicle attributes and monitor for changes without the need for polling. + +Listeners ("observer callback functions") are invoked differently based on the *type of observed attribute*. Attributes that represent sensor values or other +"streams of information" are updated whenever a message is received from the vehicle. Attributes which reflect vehicle +"state" are only updated when their values change (for example +:py:func:`Vehicle.system_status `, +:py:attr:`Vehicle.armed `, and +:py:attr:`Vehicle.mode `). + +Callbacks are added using :py:func:`Vehicle.add_attribute_listener() ` or the +:py:func:`Vehicle.on_attribute() ` decorator method. The main difference between these methods +is that only attribute callbacks added with :py:func:`Vehicle.add_attribute_listener() ` +can be removed (see :py:func:`remove_attribute_listener() `). + +The ``observer`` callback function is invoked with the following arguments: + +* ``self`` - the associated :py:class:`Vehicle`. This may be compared to a global vehicle handle + to implement vehicle-specific callback handling (if needed). +* ``attr_name`` - the attribute name. This can be used to infer which attribute has triggered + if the same callback is used for watching several attributes. +* ``value`` - the attribute value (so you don't need to re-query the vehicle object). + +The code snippet below shows how to add (and remove) a callback function to observe changes +in :py:attr:`Vehicle.location.global_frame ` using +:py:func:`Vehicle.add_attribute_listener() `. +The two second ``sleep()`` is required because otherwise the observer might be removed before the the +callback is first run. + + +.. code-block:: python + :emphasize-lines: 7 - # Callback function. The parameter is the name of the observed attribute (a string) - def location_callback(attribute): - print " CALLBACK: Location changed to: ", vehicle.location + #Callback to print the location in global frames. 'value' is the updated value + def location_callback(self, attr_name, value): + print "Location (Global): ", value - # Add a callback. The first parameter the name of the observed attribute (a string). - vehicle.add_attribute_observer('location', location_callback) + + # Add a callback `location_callback` for the `global_frame` attribute. + vehicle.add_attribute_listener('location.global_frame', location_callback) # Wait 2s so callback can be notified before the observer is removed time.sleep(2) # Remove observer - specifying the attribute and previously registered callback function - vehicle.remove_attribute_observer('location', location_callback) + vehicle.remove_message_listener('location.global_frame', location_callback) + +.. note:: + + The example above adds a listener on ``Vehicle`` to for attribute name ``'location.global_frame'`` + You can alternatively add (and remove) a listener ``Vehicle.location`` for the attribute name ``'global_frame'``. + Both alternatives are shown below: + + .. code-block:: python + vehicle.add_attribute_listener('location.global_frame', location_callback) + vehicle.location.add_attribute_listener('global_frame', location_callback) -The callback is triggered `every time a message is received from the vehicle `_ -(whether or not the observed attribute changes). Callback code may therefore choose to cache the result and only report changes. -For example, the following code can be used in the callback to only print output when the value of :py:attr:`Vehicle.rangefinder ` changes. + +The example below shows how you can declare an attribute callback using the +:py:func:`Vehicle.on_attribute() ` decorator function. -.. code:: python + +.. code-block:: python + :emphasize-lines: 3,4 last_rangefinder_distance=0 - - def rangefinder_callback(rangefinder): + + @vehicle.on_attribute('rangefinder') + def rangefinder_callback(self,attr_name): + #attr_name not used here. global last_rangefinder_distance - if last_rangefinder_distance == round(vehicle.rangefinder.distance, 1): + if last_rangefinder_distance == round(self.rangefinder.distance, 1): return - last_rangefinder_distance = round(vehicle.rangefinder.distance, 1) + last_rangefinder_distance = round(self.rangefinder.distance, 1) print " Rangefinder (metres): %s" % last_rangefinder_distance - - vehicle.add_attribute_observer('rangefinder', rangefinder_callback) +.. note:: + The fragment above stores the result of the previous callback and only prints the output when there is a + signficant change in :py:attr:`Vehicle.rangefinder `. You might want to + perform caching like this to ignore updates that are not significant to your code. + +The examples above show how you can monitor a single attribute. You can pass the special name ('``*``') to specify a +callback that will be called for any/all attribute changes: +.. code-block:: python -.. _vehicle_state_parameters: + # Demonstrate getting callback on any attribute change + def wildcard_callback(self, attr_name, value): + print " CALLBACK: (%s): %s" % (attr_name,value) -Parameters -========== + print "\nAdd attribute callback detecting any attribute change" + vehicle.add_attribute_listener('*', wildcard_callback) -Vehicle parameters provide the information used to configure the autopilot for the vehicle-specific hardware/capabilities. -These can be read and set using the :py:attr:`Vehicle.parameters ` -attribute (a :py:class:`Parameters ` object). -.. tip:: + print " Wait 1s so callback invoked before observer removed" + time.sleep(1) - `Copter Parameters `_, - `Plane Parameters `_, - and `Rover Parameters `_ list all the supported parameters for each platform. - The lists are automatically generated from the latest ArduPilot source code, and may contain parameters - that are not yet in the stable released versions of the code. + print " Remove Vehicle attribute observer" + # Remove observer added with `add_attribute_listener()` + vehicle.remove_attribute_listener('*', wildcard_callback) -Getting parameters ------------------- +.. _vehicle_state_home_location: -The parameters are read using the parameter name as a key. Reads will generally succeed unless you attempt to read an unsupported parameter -(which results in a Key error exception). +Home location +------------- -The code example below shows how to set Minimum Throttle (THR_MIN) setting. On Copter and Rover (not Plane), this is the minimum PWM setting for the -throttle at which the motors will keep spinning. +The *Home location* is set when a vehicle first gets a good location fix from the GPS. The location is used +as the target when the vehicle does a "return to launch". In Copter missions (and often Plane) missions, the altitude of +waypoints is set relative to this position. -.. code:: python +:py:attr:`Vehicle.home_location ` has the following behaviour: - # Print the value of the THR_MIN parameter. - print "Param: %s" % vehicle.parameters['THR_MIN'] +* In order to *get* the current value (in a :py:class:`LocationGlobal ` object) you must first download + :py:attr:`Vehicle.commands `, as shown: + .. code:: python + cmds = vehicle.commands + cmds.download() + cmds.wait_ready() + print " Home Location: %s" % vehicle.home_location + + The returned value is ``None`` before you download the commands or if the ``home_location`` has not yet been set by the autopilot. + For this reason our example code checks that the value exists (in a loop) before writing it. + + .. code:: python + + # Get Vehicle Home location - will be `None` until first set by autopilot + while not vehicle.home_location: + cmds = vehicle.commands + cmds.download() + cmds.wait_ready() + if not vehicle.home_location: + print " Waiting for home location ..." + + # We have a home location. + print "\n Home location: %s" % vehicle.home_location + +* The attribute can be *set* to a :py:class:`LocationGlobal ` object + (the code fragment below sets it to the current location): + + .. code:: python + + vehicle.home_location=vehicle.location.global_frame + + There are some caveats: + + * You must be able to read a non-``None`` value before you can write it + (the autopilot has to set the value initially before it can be written or read). + * The new location must be within 50 km of the EKF origin or setting the value will silently fail. + * The value is cached in the ``home_location``. If the variable can potentially change on the vehicle + you will need to re-download the ``Vehicle.commands`` in order to confirm the value. + +* The attribute is not observable. - -Setting parameters ------------------- - -Vehicle parameters are set as shown in the code fragment below, using the parameter name as a "key". As with attributes, the values are not guaranteed to have been sent to the vehicle until after -:py:func:`flush() ` returns. - -.. code:: python - - # Change the parameter value (Copter, Rover) - vehicle.parameters['THR_MIN']=100 - vehicle.flush() - - + +.. note:: -Observing parameter changes ---------------------------- + :py:attr:`Vehicle.home_location ` behaves this way because + ArduPilot implements/stores the home location as a waypoint rather than sending them as messages. + While DroneKit-Python hides this fact from you when working with commands, to access the value + you still need to download the commands. + + We hope to improve this attribute in later versions of ArduPilot, where there may be specific + commands to get the home location from the vehicle. -At time of writing :py:class:`Parameters ` does `not support `_ observing parameter changes. - -.. todo:: - Check to see if observers have been implemented and if so, update the information here, in about, and in Vehicle class: - https://github.com/dronekit/dronekit-python/issues/107 +.. _vehicle_state_parameters: +Parameters +========== +Vehicle parameters provide the information used to configure the autopilot for the vehicle-specific hardware/capabilities. +The available parameters for each platform are documented in the ArduPilot wiki here: +`Copter Parameters `_, +`Plane Parameters `_, +`Rover Parameters `_ +(the lists are automatically generated from the latest ArduPilot source code, and may contain or omit parameters +in your vehicle). +DroneKit downloads all parameters when you first connect to the UAV (forcing parameter reads to wait +until the download completes), and subsequently keeps the values updated by monitoring vehicle messages +for changes to individual parameters. This process ensures that it is always safe to read supported parameters, +and that their values will match the information on the vehicle. -.. _vehicle_state_home_location: +Parameters can be read, set, observed and iterated using the :py:attr:`Vehicle.parameters ` +attribute (a :py:class:`Parameters ` object). -Home location -============= -The *Home location* is set when a vehicle is armed and first gets a good location fix from the GPS. The location is used -as the target when the vehicle does a "return to launch". In Copter missions (and most Plane) missions, the altitude of -waypoints is set relative to this position. +Getting parameters +------------------ -Unlike other vehicle state information, the home location is accessed as the 0 index value of -:py:attr:`Vehicle.commands `: +The parameters are read using the parameter name as a key (case-insensitive). Reads will always succeed unless you +attempt to access an unsupported parameter (which will result in a ``KeyError`` exception). + +The code snippet below shows how to get the Minimum Throttle (THR_MIN) setting. On Copter and Rover (not Plane), this is the minimum PWM setting for the +throttle at which the motors will keep spinning. .. code:: python - - cmds = vehicle.commands - cmds.download() - cmds.wait_valid() - print " Home WP: %s" % cmds[0] -The returned value is a :py:class:`Command ` object. - - - -.. _vehicle_state_disrecommended: - -Discommended APIs -================= - -This section describes methods that we recommend you do not use! In general they are provided to handle the (hopefully rare) -cases where the "proper" API is missing some needed functionality. + # Print the value of the THR_MIN parameter. + print "Param: %s" % vehicle.parameters['THR_MIN'] -If you have to use these methods please `provide feedback explaining why `_. -.. _vehicle_state_set_mavlink_callback: +Setting parameters +------------------ -MAVLink Message Observer ------------------------- +Vehicle parameters are set as shown in the code fragment below, using the parameter name as a "key": -The :py:func:`Vehicle.set_mavlink_callback() ` method provides asynchronous -notification when any *MAVLink* packet is received by this vehicle. The notification can be stopped by -calling :py:func:`unset_mavlink_callback() ` to remove the callback. +.. code:: python + # Change the parameter value (Copter, Rover) + vehicle.parameters['THR_MIN']=100 -.. tip:: + +.. _vehicle_state_iterating_parameters: + +Listing all parameters +---------------------- - Use :ref:`attribute observers ` instead of this method where possible. +:py:attr:`Vehicle.parameters ` can be iterated to list all parameters and their values: +.. code:: python -The code snippet below shows how to set and clear a "demo" callback function as the callback handler: + + print "\nPrint all parameters (iterate `vehicle.parameters`):" + for key, value in vehicle.parameters.iteritems(): + print " Key:%s Value:%s" % (key,value) -.. code:: python - # Demo callback handler for raw MAVLink messages - def mavrx_debug_handler(message): - print "Received", message - # Set MAVLink callback handler (after getting Vehicle instance) - vehicle.set_mavlink_callback(mavrx_debug_handler) +.. _vehicle_state_observing_parameters: + +Observing parameter changes +--------------------------- - # Wait to allow the callback to be invoked before it is removed. - time.sleep(1) +You can observe any of the vehicle parameters and monitor for changes without the need for polling. +The parameters are cached, so that callback functions are only invoked when parameter values change. - # Remove the MAVLink callback handler. Callback will not be - # called after this point. - vehicle.unset_mavlink_callback() +.. tip:: + Observing parameters is virtually identical to :ref:`observing attributes `. -.. _vehicle_state_channel_override: -Channel Overrides ------------------ +The code snippet below shows how to add a callback function to observe changes in the "THR_MIN" +parameter using a decorator. Note that the parameter name is case-insensitive, and that callbacks +added using a decorator cannot be removed. -.. warning:: +.. code-block:: python + + @vehicle.parameters.on_attribute('THR_MIN') + def decorated_thr_min_callback(self, attr_name, value): + print " PARAMETER CALLBACK: %s changed to: %s" % (attr_name, value) - Channel Overrides may be useful for simulating user input and when implementing certain types of joystick control. - They should not be used for direct control of the vehicle unless there is no other choice! +The ``observer`` callback function is invoked with the following arguments: + +* ``self`` - the associated :py:class:`Parameters`. +* ``attr_name`` - the parameter name + (useful if the same callback is used for watching several parameters). +* ``msg`` - the parameter value (so you don't need to re-query the ``Vehicle.parameters`` object). - Instead use the appropriate MAVLink commands like DO_SET_SERVO/DO_SET_RELAY, or more generally set the desired position or direction/speed. +The code snippet below demonstrates how you can add and remove a listener (in this case +for "any parameter") using the +:py:func:`Parameters.add_attribute_listener() ` and +:py:func:`Parameters.remove_attribute_listener() `. -The :py:attr:`channel_override ` attribute takes a dictionary argument defining the RC *output* channels to be overridden (specified by channel number), and their new values. Channels that are not specified in the dictionary are not overridden. All multi-channel updates are atomic. To cancel an override call ``channel_override`` again, setting zero for the overridden channels. +.. code-block:: python -The values of the first four channels map to the main flight controls: 1=Roll, 2=Pitch, 3=Throttle, 4=Yaw (the mapping is defined in ``RCMAP_`` parameters in -`Plane `_, -`Copter `_ , -`Rover `_). - -The remaining channel values are configurable, and their purpose can be determined using the -`RCn_FUNCTION parameters `_. -In general a value of 0 set for a specific ``RCn_FUNCTION`` indicates that the channel can be -`mission controlled `_ (i.e. it will not directly be -controlled by normal autopilot code). + #Callback function for "any" parameter + def any_parameter_callback(self, attr_name, value): + print " ANY PARAMETER CALLBACK: %s changed to: %s" % (attr_name, value) -An example of setting and clearing overrides is given below: + #Add observer for the vehicle's any/all parameters parameter (note wildcard string ``'*'``) + vehicle.parameters.add_attribute_listener('*', any_parameter_callback) + -.. code:: python - - # Override the channel for roll and yaw - vehicle.channel_override = { "1" : 900, "4" : 1000 } - vehicle.flush() - - #print current override values - print "Current overrides are:", vehicle.channel_override - - # Print channel values (values if overrides removed) - print "Channel default values:", vehicle.channel_readback - - # Cancel override by setting channels to 0 - vehicle.channel_override = { "1" : 0, "4" : 0 } - vehicle.flush() - .. _api-information-known-issues: Known issues ============ -Below are a number of bugs and known issues related to vehicle state and settings: - -* `#12 Timeout error when setting a parameter `_ -* `#60 Attribute observer callbacks are called with heartbeat until disabled - after first called `_ -* `#107 Add implementation for observer methods in Parameter class `_ -* `#114 DroneKit has no method for detecting command failure `_ -* `#115 No way to disable the callback set_mavlink_callback `_ - - -Other API issues and improvement suggestions can viewed on `github here `_. \ No newline at end of file +Known issues and improvement suggestions can viewed on `Github here `_. \ No newline at end of file diff --git a/docs/index.rst b/docs/index.rst index 9e6be77d0..fe60ddf22 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -1,23 +1,28 @@ -.. DroneApi documentation master file, created by - sphinx-quickstart on Wed Mar 19 15:28:50 2014. - You can adapt this file completely to your liking, but it should at least - contain the root `toctree` directive. - =========================================== Welcome to DroneKit-Python's documentation! =========================================== -DroneKit's Python API helps you create powerful apps for air-based control of UAVs. These apps run on a UAV's :ref:`Companion Computer `, and augment the autopilot by performing tasks that are both computationally intensive and require a low-latency link (e.g. computer vision). +DroneKit-Python 2.x helps you create powerful apps for UAVs. These apps run on a UAV's :ref:`Companion Computer `, and augment the autopilot by performing tasks that are both computationally intensive and require a low-latency link (e.g. computer vision). + +This documentation provides everything you need to get started with DroneKit-Python, including an :doc:`overview ` of the API, quick start, guide material, a number of demos and examples, +and :doc:`API Reference `. -This documentation provides everything you need to :ref:`get started ` with DroneKit-Python, including an :doc:`overview ` of the API, guide material, a number of demos and examples, and :doc:`API Reference `. +.. tip:: + *DroneKit-Python version 1.5* has now been superseded (see these links for legacy `documentation `_ + and `examples `_). + + If you're migrating from *DroneKit-Python version 1.x*, check out our comprehensive :ref:`Migration Guide `. + Contents: .. toctree:: :maxdepth: 2 Introduction + guide/quick_start + Developing guide/index examples/index contributing/index diff --git a/dronekit/__init__.py b/dronekit/__init__.py index b55ea395c..bc05a7098 100644 --- a/dronekit/__init__.py +++ b/dronekit/__init__.py @@ -1,430 +1,3221 @@ -from __future__ import print_function -import time -import socket -import sys -import os -import platform -import re -from pymavlink import mavutil -from Queue import Empty +# DroneAPI module +""" +This is the API Reference for the DroneKit-Python API. + +The main API is the :py:class:`Vehicle` class. +The code snippet below shows how to use :py:func:`connect` to obtain an instance of a connected vehicle: + +.. code:: python + + from dronekit import connect + + # Connect to the Vehicle using "connection string" (in this case an address on network) + vehicle = connect('127.0.0.1:14550', wait_ready=True) + +:py:class:`Vehicle` provides access to vehicle *state* through python attributes +(e.g. :py:attr:`Vehicle.mode`) +and to settings/parameters though the :py:attr:`Vehicle.parameters` attribute. +Asynchronous notification on vehicle attribute changes is available by registering listeners/observers. + +Vehicle movement is primarily controlled using the :py:attr:`Vehicle.armed` attribute and +:py:func:`Vehicle.simple_takeoff` and :py:attr:`Vehicle.simple_goto` in GUIDED mode. -from pkgutil import extend_path -__path__ = extend_path(__path__, __name__) +Velocity-based movement and control over other vehicle features can be achieved using custom MAVLink messages +(:py:func:`Vehicle.send_mavlink`, :py:func:`Vehicle.message_factory`). -if platform.system() == 'Windows': - from errno import WSAECONNRESET as ECONNABORTED +It is also possible to work with vehicle "missions" using the :py:attr:`Vehicle.commands` attribute, and run them in AUTO mode. + +All the logging is handled through the builtin Python `logging` module. + +A number of other useful classes and methods are listed below. + +---- +""" + +import sys +import collections + +# Python3.10 removed MutableMapping from collections: +if sys.version_info.major == 3 and sys.version_info.minor >= 10: + from collections.abc import MutableMapping else: - from errno import ECONNABORTED + from collections import MutableMapping + +import copy +import logging +import math +import struct +import time -# Clean impl of mp dependencies for dronekit +import monotonic +from past.builtins import basestring -import dronekit.module.api as api +from pymavlink import mavutil, mavwp +from pymavlink.dialects.v10 import ardupilotmega -def errprinter(*args): - print(*args, file=sys.stderr) +from dronekit.util import ErrprinterHandler -class FakeAPI: - def __init__(self, module): - self.__vehicle = api.MPVehicle(module) - self.exit = False - def get_vehicles(self, query=None): - return [ self.__vehicle ] +class APIException(Exception): + """ + Base class for DroneKit related exceptions. -# def mav_thread(conn, state): + :param String message: Message string describing the exception + """ -# return (in_queue, out_queue) -class MavWriter(): - def __init__(self, queue): - self.queue = queue +class TimeoutError(APIException): + '''Raised by operations that have timeouts.''' - def write(self, pkt): - self.queue.put(pkt) - def read(self): - errprinter('writer should not have had a read request') - os._exit(43) +class Attitude(object): + """ + Attitude information. -def send_heartbeat(master): - master.mav.heartbeat_send(mavutil.mavlink.MAV_TYPE_GCS, mavutil.mavlink.MAV_AUTOPILOT_INVALID, 0, 0, 0) + An object of this type is returned by :py:attr:`Vehicle.attitude`. -def request_data_stream_send(master, rate=1): - master.mav.request_data_stream_send(master.target_system, master.target_component, - mavutil.mavlink.MAV_DATA_STREAM_ALL, rate, 1) + .. _figure_attitude: -from Queue import Queue -from threading import Thread + .. figure:: http://upload.wikimedia.org/wikipedia/commons/thumb/c/c1/Yaw_Axis_Corrected.svg/500px-Yaw_Axis_Corrected.svg.png + :width: 400px + :alt: Diagram showing Pitch, Roll, Yaw + :target: http://commons.wikimedia.org/wiki/File:Yaw_Axis_Corrected.svg -class MPFakeState: - def __init__(self, master, status_printer=None): - self.master = master - out_queue = Queue() - # self.mav_thread = mav_thread(master, self) - # self.mav = master.mav + Diagram showing Pitch, Roll, Yaw (`Creative Commons `_) - self.api = None + :param pitch: Pitch in radians + :param yaw: Yaw in radians + :param roll: Roll in radians + """ - # TODO get rid of "master" object as exposed, - # keep it private, expose something smaller for dronekit - self.out_queue = out_queue - self.master.mav = mavutil.mavlink.MAVLink(MavWriter(self.out_queue), srcSystem=self.master.source_system, use_native=False) + def __init__(self, pitch, yaw, roll): + self.pitch = pitch + self.yaw = yaw + self.roll = roll - self.command_map = {} - self.completions = {} + def __str__(self): + fmt = '{}:pitch={pitch},yaw={yaw},roll={roll}' + return fmt.format(self.__class__.__name__, **vars(self)) - self.target_system = 0 - self.target_component = 0 - self.lat = None - self.lon = None - self.alt = None +class LocationGlobal(object): + """ + A global location object. - self.vx = None - self.vy = None - self.vz = None + The latitude and longitude are relative to the `WGS84 coordinate system `_. + The altitude is relative to mean sea-level (MSL). - self.airspeed = None - self.groundspeed = None + For example, a global location object with altitude 30 metres above sea level might be defined as: - self.pitch = None - self.yaw = None - self.roll = None - self.pitchspeed = None - self.yawspeed = None - self.rollspeed = None + .. code:: python - self.mount_pitch = None - self.mount_yaw = None - self.mount_roll = None + LocationGlobal(-34.364114, 149.166022, 30) - self.voltage = -1 - self.current = -1 - self.level = -1 + .. todo:: FIXME: Location class - possibly add a vector3 representation. - self.rc_readback = {} + An object of this type is owned by :py:attr:`Vehicle.location`. See that class for information on + reading and observing location in the global frame. - self.last_waypoint = 0 + :param lat: Latitude. + :param lon: Longitude. + :param alt: Altitude in meters relative to mean sea-level (MSL). + """ - self.eph = None - self.epv = None - self.satellites_visible = None - self.fix_type = None # FIXME support multiple GPSs per vehicle - possibly by using componentId + def __init__(self, lat, lon, alt=None): + self.lat = lat + self.lon = lon + self.alt = alt - self.rngfnd_distance = None - self.rngfnd_voltage = None + # This is for backward compatibility. + self.local_frame = None + self.global_frame = None - self.status = type('MPStatus',(object,),{ - 'flightmode': 'AUTO', - 'armed': False, - })() + def __str__(self): + return "LocationGlobal:lat=%s,lon=%s,alt=%s" % (self.lat, self.lon, self.alt) - self.mav_param = {} - # Weird - self.mpstate = self - self.functions = self - self.mpstate.settings = self +class LocationGlobalRelative(object): + """ + A global location object, with attitude relative to home location altitude. - self.status_printer = status_printer + The latitude and longitude are relative to the `WGS84 coordinate system `_. + The altitude is relative to the *home position*. - def fix_targets(self, message): - pass - # """Set correct target IDs for our vehicle""" - # settings = self.mpstate.settings - # if hasattr(message, 'target_system'): - # message.target_system = settings.target_system - # if hasattr(message, 'target_component'): - # message.target_component = settings.target_component + For example, a ``LocationGlobalRelative`` object with an altitude of 30 metres above the home location might be defined as: - def module(self, which): - # psyche - return self + .. code:: python - def param_set(self, name, value, retries=3): - # TODO dumbly reimplement this using timeout loops - # because we should actually be awaiting an ACK of PARAM_VALUE - # changed, but we don't have a proper ack structure, we'll - # instead just wait until the value itself was changed + LocationGlobalRelative(-34.364114, 149.166022, 30) - name = name.upper() - value = float(value) - success = False - while retries > 0: - retries -= 1 - self.master.param_set_send(name.upper(), value) - tstart = time.time() - while time.time() - tstart < 1: - if self.mav_param[name] == value: - return True - time.sleep(0.1) - - errprinter("timeout setting parameter %s to %f" % (name, value)) - return False + .. todo:: FIXME: Location class - possibly add a vector3 representation. - def __on_change(self, *args): - for a in args: - for v in self.api.get_vehicles(): - v.notify_observers(a) - - def mavlink_packet(self, m): - typ = m.get_type() - if typ == 'STATUSTEXT': - print(re.sub(r'(^|\n)', '>>> \1', m.text.rstrip())) - elif typ == 'GLOBAL_POSITION_INT': - (self.lat, self.lon) = (m.lat / 1.0e7, m.lon / 1.0e7) - (self.vx, self.vy, self.vz) = (m.vx / 100.0, m.vy / 100.0, m.vz / 100.0) - self.__on_change('location', 'velocity') - elif typ == 'GPS_RAW': - pass # better to just use global position int - # (self.lat, self.lon) = (m.lat, m.lon) - # self.__on_change('location') - elif typ == 'GPS_RAW_INT': - # (self.lat, self.lon) = (m.lat / 1.0e7, m.lon / 1.0e7) - self.eph = m.eph - self.epv = m.epv - self.satellites_visible = m.satellites_visible - self.fix_type = m.fix_type - self.__on_change('gps_0') - elif typ == "VFR_HUD": - self.heading = m.heading - self.alt = m.alt - self.airspeed = m.airspeed - self.groundspeed = m.groundspeed - self.__on_change('location', 'airspeed', 'groundspeed') - elif typ == "ATTITUDE": - self.pitch = m.pitch - self.yaw = m.yaw - self.roll = m.roll - self.pitchspeed = m.pitchspeed - self.yawspeed = m.yawspeed - self.rollspeed = m.rollspeed - self.__on_change('attitude') - elif typ == "SYS_STATUS": - self.voltage = m.voltage_battery - self.current = m.current_battery - self.level = m.battery_remaining - self.__on_change('battery') - elif typ == "HEARTBEAT": - self.__on_change('mode', 'armed') - elif typ in ["WAYPOINT_CURRENT", "MISSION_CURRENT"]: - self.last_waypoint = m.seq - elif typ == "RC_CHANNELS_RAW": - def set(chnum, v): - '''Private utility for handling rc channel messages''' - # use port to allow ch nums greater than 8 - self.rc_readback[str(m.port * 8 + chnum)] = v - - set(1, m.chan1_raw) - set(2, m.chan2_raw) - set(3, m.chan3_raw) - set(4, m.chan4_raw) - set(5, m.chan5_raw) - set(6, m.chan6_raw) - set(7, m.chan7_raw) - set(8, m.chan8_raw) - elif typ == "MOUNT_STATUS": - self.mount_pitch = m.pointing_a / 100 - self.mount_roll = m.pointing_b / 100 - self.mount_yaw = m.pointing_c / 100 - self.__on_change('mount') - elif typ == "RANGEFINDER": - self.rngfnd_distance = m.distance - self.rngfnd_voltage = m.voltage - self.__on_change('rangefinder') - - if self.api: - for v in self.api.get_vehicles(): - if v.mavrx_callback: - v.mavrx_callback(m) - - def prepare(self, await_params=False): - # errprinter('Await heartbeat.') - # TODO this should be more rigious. How to avoid - # invalid MAVLink prefix '73' - # invalid MAVLink prefix '13' - - params = type('PState',(object,),{ - "mav_param_count": -1, - "mav_param_set": [], - "loaded": False, - "start": False, - })() - self.mav_param = {} - self.pstate = params - self.api = FakeAPI(self) - - import atexit - self.exiting = False - def onexit(): - self.exiting = True - atexit.register(onexit) - - heartbeat_started = False - - def mavlink_thread(): - # Huge try catch in case we see http://bugs.python.org/issue1856 + An object of this type is owned by :py:attr:`Vehicle.location`. See that class for information on + reading and observing location in the global-relative frame. + + :param lat: Latitude. + :param lon: Longitude. + :param alt: Altitude in meters (relative to the home location). + """ + + def __init__(self, lat, lon, alt=None): + self.lat = lat + self.lon = lon + self.alt = alt + + # This is for backward compatibility. + self.local_frame = None + self.global_frame = None + + def __str__(self): + return "LocationGlobalRelative:lat=%s,lon=%s,alt=%s" % (self.lat, self.lon, self.alt) + + +class LocationLocal(object): + """ + A local location object. + + The north, east and down are relative to the EKF origin. This is most likely the location where the vehicle was turned on. + + An object of this type is owned by :py:attr:`Vehicle.location`. See that class for information on + reading and observing location in the local frame. + + :param north: Position north of the EKF origin in meters. + :param east: Position east of the EKF origin in meters. + :param down: Position down from the EKF origin in meters. (i.e. negative altitude in meters) + """ + + def __init__(self, north, east, down): + self.north = north + self.east = east + self.down = down + + def __str__(self): + return "LocationLocal:north=%s,east=%s,down=%s" % (self.north, self.east, self.down) + + def distance_home(self): + """ + Distance away from home, in meters. Returns 3D distance if `down` is known, otherwise 2D distance. + """ + + if self.north is not None and self.east is not None: + if self.down is not None: + return math.sqrt(self.north**2 + self.east**2 + self.down**2) + else: + return math.sqrt(self.north**2 + self.east**2) + + +class GPSInfo(object): + """ + Standard information about GPS. + + If there is no GPS lock the parameters are set to ``None``. + + :param Int eph: GPS horizontal dilution of position (HDOP). + :param Int epv: GPS vertical dilution of position (VDOP). + :param Int fix_type: 0-1: no fix, 2: 2D fix, 3: 3D fix + :param Int satellites_visible: Number of satellites visible. + + .. todo:: FIXME: GPSInfo class - possibly normalize eph/epv? report fix type as string? + """ + + def __init__(self, eph, epv, fix_type, satellites_visible): + self.eph = eph + self.epv = epv + self.fix_type = fix_type + self.satellites_visible = satellites_visible + + def __str__(self): + return "GPSInfo:fix=%s,num_sat=%s" % (self.fix_type, self.satellites_visible) + + +class Wind(object): + """ + Wind information + + An object of this type is returned by :py:attr: `Vehicle.wind`. + + :param wind_direction: Wind direction in degrees + :param wind_speed: Wind speed in m/s + :param wind_speed_z: vertical wind speed in m/s + """ + def __init__(self, wind_direction, wind_speed, wind_speed_z): + self.wind_direction = wind_direction + self.wind_speed = wind_speed + self.wind_speed_z = wind_speed_z + + def __str__(self): + return "Wind: wind direction: {}, wind speed: {}, wind speed z: {}".format(self.wind_direction, self.wind_speed, self.wind_speed_z) + + +class Battery(object): + """ + System battery information. + + An object of this type is returned by :py:attr:`Vehicle.battery`. + + :param voltage: Battery voltage in millivolts. + :param current: Battery current, in 10 * milliamperes. ``None`` if the autopilot does not support current measurement. + :param level: Remaining battery energy. ``None`` if the autopilot cannot estimate the remaining battery. + """ + + def __init__(self, voltage, current, level): + self.voltage = voltage / 1000.0 + if current == -1: + self.current = None + else: + self.current = current / 100.0 + if level == -1: + self.level = None + else: + self.level = level + + def __str__(self): + return "Battery:voltage={},current={},level={}".format(self.voltage, self.current, + self.level) + + +class Rangefinder(object): + """ + Rangefinder readings. + + An object of this type is returned by :py:attr:`Vehicle.rangefinder`. + + :param distance: Distance (metres). ``None`` if the vehicle doesn't have a rangefinder. + :param voltage: Voltage (volts). ``None`` if the vehicle doesn't have a rangefinder. + """ + + def __init__(self, distance, voltage): + self.distance = distance + self.voltage = voltage + + def __str__(self): + return "Rangefinder: distance={}, voltage={}".format(self.distance, self.voltage) + + +class Version(object): + """ + Autopilot version and type. + + An object of this type is returned by :py:attr:`Vehicle.version`. + + The version number can be read in a few different formats. To get it in a human-readable + format, just print `vehicle.version`. This might print something like "APM:Copter-3.3.2-rc4". + + .. versionadded:: 2.0.3 + + .. py:attribute:: major + + Major version number (integer). + + .. py:attribute::minor + + Minor version number (integer). + + .. py:attribute:: patch + + Patch version number (integer). + + .. py:attribute:: release + + Release type (integer). See the enum `FIRMWARE_VERSION_TYPE `_. + + This is a composite of the product release cycle stage (rc, beta etc) and the version in that cycle - e.g. 23. + + """ + def __init__(self, raw_version, autopilot_type, vehicle_type): + self.autopilot_type = autopilot_type + self.vehicle_type = vehicle_type + self.raw_version = raw_version + if raw_version is None: + self.major = None + self.minor = None + self.patch = None + self.release = None + else: + self.major = raw_version >> 24 & 0xFF + self.minor = raw_version >> 16 & 0xFF + self.patch = raw_version >> 8 & 0xFF + self.release = raw_version & 0xFF + + def is_stable(self): + """ + Returns True if the autopilot reports that the current firmware is a stable + release (not a pre-release or development version). + """ + return self.release == 255 + + def release_version(self): + """ + Returns the version within the release type (an integer). + This method returns "23" for Copter-3.3rc23. + """ + if self.release is None: + return None + if self.release == 255: + return 0 + return self.release % 64 + + def release_type(self): + """ + Returns text describing the release type e.g. "alpha", "stable" etc. + """ + if self.release is None: + return None + types = ["dev", "alpha", "beta", "rc"] + return types[self.release >> 6] + + def __str__(self): + prefix = "" + + if self.autopilot_type == mavutil.mavlink.MAV_AUTOPILOT_ARDUPILOTMEGA: + prefix += "APM:" + elif self.autopilot_type == mavutil.mavlink.MAV_AUTOPILOT_PX4: + prefix += "PX4" + else: + prefix += "UnknownAutoPilot" + + if self.vehicle_type == mavutil.mavlink.MAV_TYPE_QUADROTOR: + prefix += "Copter-" + elif self.vehicle_type == mavutil.mavlink.MAV_TYPE_FIXED_WING: + prefix += "Plane-" + elif self.vehicle_type == mavutil.mavlink.MAV_TYPE_GROUND_ROVER: + prefix += "Rover-" + else: + prefix += "UnknownVehicleType%d-" % self.vehicle_type + + if self.release_type() is None: + release_type = "UnknownReleaseType" + elif self.is_stable(): + release_type = "" + else: + # e.g. "-rc23" + release_type = "-" + str(self.release_type()) + str(self.release_version()) + + return prefix + "%s.%s.%s" % (self.major, self.minor, self.patch) + release_type + + +class Capabilities: + """ + Autopilot capabilities (supported message types and functionality). + + An object of this type is returned by :py:attr:`Vehicle.capabilities`. + + See the enum + `MAV_PROTOCOL_CAPABILITY `_. + + .. versionadded:: 2.0.3 + + + .. py:attribute:: mission_float + + Autopilot supports MISSION float message type (Boolean). + + .. py:attribute:: param_float + + Autopilot supports the PARAM float message type (Boolean). + + .. py:attribute:: mission_int + + Autopilot supports MISSION_INT scaled integer message type (Boolean). + + .. py:attribute:: command_int + + Autopilot supports COMMAND_INT scaled integer message type (Boolean). + + .. py:attribute:: param_union + + Autopilot supports the PARAM_UNION message type (Boolean). + + .. py:attribute:: ftp + + Autopilot supports ftp for file transfers (Boolean). + + .. py:attribute:: set_attitude_target + + Autopilot supports commanding attitude offboard (Boolean). + + .. py:attribute:: set_attitude_target_local_ned + + Autopilot supports commanding position and velocity targets in local NED frame (Boolean). + + .. py:attribute:: set_altitude_target_global_int + + Autopilot supports commanding position and velocity targets in global scaled integers (Boolean). + + .. py:attribute:: terrain + + Autopilot supports terrain protocol / data handling (Boolean). + + .. py:attribute:: set_actuator_target + + Autopilot supports direct actuator control (Boolean). + + .. py:attribute:: flight_termination + + Autopilot supports the flight termination command (Boolean). + + .. py:attribute:: compass_calibration + + Autopilot supports onboard compass calibration (Boolean). + """ + def __init__(self, capabilities): + self.mission_float = (((capabilities >> 0) & 1) == 1) + self.param_float = (((capabilities >> 1) & 1) == 1) + self.mission_int = (((capabilities >> 2) & 1) == 1) + self.command_int = (((capabilities >> 3) & 1) == 1) + self.param_union = (((capabilities >> 4) & 1) == 1) + self.ftp = (((capabilities >> 5) & 1) == 1) + self.set_attitude_target = (((capabilities >> 6) & 1) == 1) + self.set_attitude_target_local_ned = (((capabilities >> 7) & 1) == 1) + self.set_altitude_target_global_int = (((capabilities >> 8) & 1) == 1) + self.terrain = (((capabilities >> 9) & 1) == 1) + self.set_actuator_target = (((capabilities >> 10) & 1) == 1) + self.flight_termination = (((capabilities >> 11) & 1) == 1) + self.compass_calibration = (((capabilities >> 12) & 1) == 1) + + +class VehicleMode(object): + """ + This object is used to get and set the current "flight mode". + + The flight mode determines the behaviour of the vehicle and what commands it can obey. + The recommended flight modes for *DroneKit-Python* apps depend on the vehicle type: + + * Copter apps should use ``AUTO`` mode for "normal" waypoint missions and ``GUIDED`` mode otherwise. + * Plane and Rover apps should use the ``AUTO`` mode in all cases, re-writing the mission commands if "dynamic" + behaviour is required (they support only a limited subset of commands in ``GUIDED`` mode). + * Some modes like ``RETURN_TO_LAUNCH`` can be used on all platforms. Care should be taken + when using manual modes as these may require remote control input from the user. + + The available set of supported flight modes is vehicle-specific (see + `Copter Modes `_, + `Plane Modes `_, + `Rover Modes `_). If an unsupported mode is set the script + will raise a ``KeyError`` exception. + + The :py:attr:`Vehicle.mode` attribute can be queried for the current mode. + The code snippet below shows how to observe changes to the mode and then read the value: + + .. code:: python + + #Callback definition for mode observer + def mode_callback(self, attr_name): + print "Vehicle Mode", self.mode + + #Add observer callback for attribute `mode` + vehicle.add_attribute_listener('mode', mode_callback) + + The code snippet below shows how to change the vehicle mode to AUTO: + + .. code:: python + + # Set the vehicle into auto mode + vehicle.mode = VehicleMode("AUTO") + + For more information on getting/setting/observing the :py:attr:`Vehicle.mode` + (and other attributes) see the :ref:`attributes guide `. + + .. py:attribute:: name + + The mode name, as a ``string``. + """ + + def __init__(self, name): + self.name = name + + def __str__(self): + return "VehicleMode:%s" % self.name + + def __eq__(self, other): + return self.name == other + + def __ne__(self, other): + return self.name != other + + +class SystemStatus(object): + """ + This object is used to get and set the current "system status". + + An object of this type is returned by :py:attr:`Vehicle.system_status`. + + .. py:attribute:: state + + The system state, as a ``string``. + """ + + def __init__(self, state): + self.state = state + + def __str__(self): + return "SystemStatus:%s" % self.state + + def __eq__(self, other): + return self.state == other + + def __ne__(self, other): + return self.state != other + + +class HasObservers(object): + def __init__(self): + logging.basicConfig() + self._logger = logging.getLogger(__name__) + + # A mapping from attr_name to a list of observers + self._attribute_listeners = {} + self._attribute_cache = {} + + def add_attribute_listener(self, attr_name, observer): + """ + Add an attribute listener callback. + + The callback function (``observer``) is invoked differently depending on the *type of attribute*. + Attributes that represent sensor values or which are used to monitor connection status are updated + whenever a message is received from the vehicle. Attributes which reflect vehicle "state" are + only updated when their values change (for example :py:attr:`Vehicle.system_status`, + :py:attr:`Vehicle.armed`, and :py:attr:`Vehicle.mode`). + + The callback can be removed using :py:func:`remove_attribute_listener`. + + .. note:: + + The :py:func:`on_attribute` decorator performs the same operation as this method, but with + a more elegant syntax. Use ``add_attribute_listener`` by preference if you will need to remove + the observer. + + The argument list for the callback is ``observer(object, attr_name, attribute_value)``: + + * ``self`` - the associated :py:class:`Vehicle`. This may be compared to a global vehicle handle + to implement vehicle-specific callback handling (if needed). + * ``attr_name`` - the attribute name. This can be used to infer which attribute has triggered + if the same callback is used for watching several attributes. + * ``value`` - the attribute value (so you don't need to re-query the vehicle object). + + The example below shows how to get callbacks for (global) location changes: + + .. code:: python + + #Callback to print the location in global frame + def location_callback(self, attr_name, msg): + print "Location (Global): ", msg + + #Add observer for the vehicle's current location + vehicle.add_attribute_listener('global_frame', location_callback) + + See :ref:`vehicle_state_observe_attributes` for more information. + + :param String attr_name: The name of the attribute to watch (or '*' to watch all attributes). + :param observer: The callback to invoke when a change in the attribute is detected. + + """ + listeners_for_attr = self._attribute_listeners.get(attr_name) + if listeners_for_attr is None: + listeners_for_attr = [] + self._attribute_listeners[attr_name] = listeners_for_attr + if observer not in listeners_for_attr: + listeners_for_attr.append(observer) + + def remove_attribute_listener(self, attr_name, observer): + """ + Remove an attribute listener (observer) that was previously added using :py:func:`add_attribute_listener`. + + For example, the following line would remove a previously added vehicle 'global_frame' + observer called ``location_callback``: + + .. code:: python + + vehicle.remove_attribute_listener('global_frame', location_callback) + + See :ref:`vehicle_state_observe_attributes` for more information. + + :param String attr_name: The attribute name that is to have an observer removed (or '*' to remove an 'all attribute' observer). + :param observer: The callback function to remove. + + """ + listeners_for_attr = self._attribute_listeners.get(attr_name) + if listeners_for_attr is not None: + listeners_for_attr.remove(observer) + if len(listeners_for_attr) == 0: + del self._attribute_listeners[attr_name] + + def notify_attribute_listeners(self, attr_name, value, cache=False): + """ + This method is used to update attribute observers when the named attribute is updated. + + You should call it in your message listeners after updating an attribute with + information from a vehicle message. + + By default the value of ``cache`` is ``False`` and every update from the vehicle is sent to listeners + (whether or not the attribute has changed). This is appropriate for attributes which represent sensor + or heartbeat-type monitoring. + + Set ``cache=True`` to update listeners only when the value actually changes (cache the previous + attribute value). This should be used where clients will only ever need to know the value when it has + changed. For example, this setting has been used for notifying :py:attr:`mode` changes. + + See :ref:`example_create_attribute` for more information. + + :param String attr_name: The name of the attribute that has been updated. + :param value: The current value of the attribute that has been updated. + :param Boolean cache: Set ``True`` to only notify observers when the attribute value changes. + """ + # Cached values are not re-sent if they are unchanged. + if cache: + if self._attribute_cache.get(attr_name) == value: + return + self._attribute_cache[attr_name] = value + + # Notify observers. + for fn in self._attribute_listeners.get(attr_name, []): try: - # Record the time we received the last "new" param. - last_new_param = time.time() - last_heartbeat_sent = 0 - last_heartbeat_received = 0 - - start_duration = 0.2 - repeat_duration = 1 - duration = start_duration - - while True: - # Downtime - time.sleep(0.05) - - # Check the time duration for last "new" params exceeds watchdog. - if params.start: - if None not in params.mav_param_set: - params.loaded = True - - if not params.loaded and time.time() - last_new_param > duration: - c = 0 - for i, v in enumerate(params.mav_param_set): - if v == None: - self.master.mav.param_request_read_send(self.master.target_system, self.master.target_component, "", i) - c += 1 - if c > 50: - break - duration = repeat_duration - last_new_param = time.time() - - # Send 1 heartbeat per second - if time.time() - last_heartbeat_sent > 1: - send_heartbeat(self.master) - last_heartbeat_sent = time.time() - # Timeout after 5 - if heartbeat_started: - if last_heartbeat_received == 0: - last_heartbeat_received = time.time() - elif time.time() - last_heartbeat_received > 5: - # raise Exception('Link timeout, no heartbeat in last 5 seconds') - errprinter('Link timeout, no heartbeat in last 5 seconds') - last_heartbeat_received = time.time() - - while True: - try: - msg = self.out_queue.get_nowait() - self.master.write(msg) - except socket.error as error: - if error.errno == ECONNABORTED: - errprinter('reestablishing connection after read timeout') - try: - self.master.close() - except: - pass - self.master = mavutil.mavlink_connection(self.master.address) - continue - - # If connection reset (closed), stop polling. - return - except Empty: - break - except Exception as e: - errprinter('mav send error:', e) - break + fn(self, attr_name, value) + except Exception: + self._logger.exception('Exception in attribute handler for %s' % attr_name, exc_info=True) - while True: - try: - msg = self.master.recv_msg() - except socket.error as error: - if error.errno == ECONNABORTED: - errprinter('reestablishing connection after send timeout') - try: - self.master.close() - except: - pass - self.master = mavutil.mavlink_connection(self.master.address) - continue - - # If connection reset (closed), stop polling. - return - except Exception as e: - # TODO debug these. - # errprinter('mav recv error:', e) - msg = None - if not msg: - break + for fn in self._attribute_listeners.get('*', []): + try: + fn(self, attr_name, value) + except Exception: + self._logger.exception('Exception in attribute handler for %s' % attr_name, exc_info=True) + + def on_attribute(self, name): + """ + Decorator for attribute listeners. + + The decorated function (``observer``) is invoked differently depending on the *type of attribute*. + Attributes that represent sensor values or which are used to monitor connection status are updated + whenever a message is received from the vehicle. Attributes which reflect vehicle "state" are + only updated when their values change (for example :py:func:`Vehicle.system_status`, + :py:attr:`Vehicle.armed`, and :py:attr:`Vehicle.mode`). + + The argument list for the callback is ``observer(object, attr_name, attribute_value)`` + + * ``self`` - the associated :py:class:`Vehicle`. This may be compared to a global vehicle handle + to implement vehicle-specific callback handling (if needed). + * ``attr_name`` - the attribute name. This can be used to infer which attribute has triggered + if the same callback is used for watching several attributes. + * ``msg`` - the attribute value (so you don't need to re-query the vehicle object). + + .. note:: + + There is no way to remove an attribute listener added with this decorator. Use + :py:func:`add_attribute_listener` if you need to be able to remove + the :py:func:`attribute listener `. + + The code fragment below shows how you can create a listener for the attitude attribute. - if msg.get_type() == 'PARAM_VALUE': - # If we discover a new param count, assume we - # are receiving a new param set. - if params.mav_param_count != msg.param_count: - params.loaded = False - params.start = True - params.mav_param_count = msg.param_count - params.mav_param_set = [None]*msg.param_count - - # Attempt to set the params. We throw an error - # if the index is out of range of the count or - # we lack a param_id. - try: - if msg.param_index < msg.param_count and msg: - if params.mav_param_set[msg.param_index] == None: - last_new_param = time.time() - duration = start_duration - params.mav_param_set[msg.param_index] = msg - self.mav_param[msg.param_id] = msg.param_value - except: - import traceback - traceback.print_exc() - - elif msg.get_type() == 'HEARTBEAT': - self.status.armed = (msg.base_mode & mavutil.mavlink.MAV_MODE_FLAG_SAFETY_ARMED) != 0 - self.status.flightmode = {v: k for k, v in self.master.mode_mapping().items()}[msg.custom_mode] - last_heartbeat_received = time.time() - - if self.api: - self.mavlink_packet(msg) - - except Exception as e: - # http://bugs.python.org/issue1856 - if self.exiting: + .. code:: python + + @vehicle.on_attribute('attitude') + def attitude_listener(self, name, msg): + print '%s attribute is: %s' % (name, msg) + + See :ref:`vehicle_state_observe_attributes` for more information. + + :param String name: The name of the attribute to watch (or '*' to watch all attributes). + :param observer: The callback to invoke when a change in the attribute is detected. + """ + + def decorator(fn): + if isinstance(name, list): + for n in name: + self.add_attribute_listener(n, fn) + else: + self.add_attribute_listener(name, fn) + + return decorator + + +class ChannelsOverride(dict): + """ + A dictionary class for managing Vehicle channel overrides. + + Channels can be read, written, or cleared by index or using a dictionary syntax. + To clear a value, set it to ``None`` or use ``del`` on the item. + + An object of this type is returned by :py:attr:`Vehicle.channels.overrides `. + + For more information and examples see :ref:`example_channel_overrides`. + """ + + def __init__(self, vehicle): + self._vehicle = vehicle + self._count = 8 # Fixed by MAVLink + self._active = True + + def __getitem__(self, key): + return dict.__getitem__(self, str(key)) + + def __setitem__(self, key, value): + if not (0 < int(key) <= self._count): + raise KeyError('Invalid channel index %s' % key) + if not value: + try: + dict.__delitem__(self, str(key)) + except: + pass + else: + dict.__setitem__(self, str(key), value) + self._send() + + def __delitem__(self, key): + dict.__delitem__(self, str(key)) + self._send() + + def __len__(self): + return self._count + + def _send(self): + if self._active: + overrides = [0] * 8 + for k, v in self.items(): + overrides[int(k) - 1] = v + self._vehicle._master.mav.rc_channels_override_send(0, 0, *overrides) + + +class Channels(dict): + """ + A dictionary class for managing RC channel information associated with a :py:class:`Vehicle`. + + An object of this type is accessed through :py:attr:`Vehicle.channels`. This object also stores + the current vehicle channel overrides through its :py:attr:`overrides` attribute. + + For more information and examples see :ref:`example_channel_overrides`. + """ + + def __init__(self, vehicle, count): + self._vehicle = vehicle + self._count = count + self._overrides = ChannelsOverride(vehicle) + + # populate readback + self._readonly = False + for k in range(0, count): + self[k + 1] = None + self._readonly = True + + @property + def count(self): + """ + The number of channels defined in the dictionary (currently 8). + """ + return self._count + + def __getitem__(self, key): + return dict.__getitem__(self, str(key)) + + def __setitem__(self, key, value): + if self._readonly: + raise TypeError('__setitem__ is not supported on Channels object') + return dict.__setitem__(self, str(key), value) + + def __len__(self): + return self._count + + def _update_channel(self, channel, value): + # If we have channels on different ports, we expand the Channels + # object to support them. + channel = int(channel) + self._readonly = False + self[channel] = value + self._readonly = True + self._count = max(self._count, channel) + + @property + def overrides(self): + """ + Attribute to read, set and clear channel overrides (also known as "rc overrides") + associated with a :py:class:`Vehicle` (via :py:class:`Vehicle.channels`). This is an + object of type :py:class:`ChannelsOverride`. + + For more information and examples see :ref:`example_channel_overrides`. + + To set channel overrides: + + .. code:: python + + # Set and clear overrids using dictionary syntax (clear by setting override to none) + vehicle.channels.overrides = {'5':None, '6':None,'3':500} + + # You can also set and clear overrides using indexing syntax + vehicle.channels.overrides['2'] = 200 + vehicle.channels.overrides['2'] = None + + # Clear using 'del' + del vehicle.channels.overrides['3'] + + # Clear all overrides by setting an empty dictionary + vehicle.channels.overrides = {} + + Read the channel overrides either as a dictionary or by index. Note that you'll get + a ``KeyError`` exception if you read a channel override that has not been set. + + .. code:: python + + # Get all channel overrides + print " Channel overrides: %s" % vehicle.channels.overrides + # Print just one channel override + print " Ch2 override: %s" % vehicle.channels.overrides['2'] + """ + return self._overrides + + @overrides.setter + def overrides(self, newch): + self._overrides._active = False + self._overrides.clear() + for k, v in newch.items(): + if v: + self._overrides[str(k)] = v + else: + try: + del self._overrides[str(k)] + except: + pass + self._overrides._active = True + self._overrides._send() + + +class Locations(HasObservers): + """ + An object for holding location information in global, global relative and local frames. + + :py:class:`Vehicle` owns an object of this type. See :py:attr:`Vehicle.location` for information on + reading and observing location in the different frames. + + The different frames are accessed through the members, which are created with this object. + They can be read, and are observable. + """ + + def __init__(self, vehicle): + super(Locations, self).__init__() + + self._lat = None + self._lon = None + self._alt = None + self._relative_alt = None + + @vehicle.on_message('GLOBAL_POSITION_INT') + def listener(vehicle, name, m): + (self._lat, self._lon) = (m.lat / 1.0e7, m.lon / 1.0e7) + self._relative_alt = m.relative_alt / 1000.0 + self.notify_attribute_listeners('global_relative_frame', self.global_relative_frame) + vehicle.notify_attribute_listeners('location.global_relative_frame', + vehicle.location.global_relative_frame) + + if self._alt is not None or m.alt != 0: + # Require first alt value to be non-0 + # TODO is this the proper check to do? + self._alt = m.alt / 1000.0 + self.notify_attribute_listeners('global_frame', self.global_frame) + vehicle.notify_attribute_listeners('location.global_frame', + vehicle.location.global_frame) + + vehicle.notify_attribute_listeners('location', vehicle.location) + + self._north = None + self._east = None + self._down = None + + @vehicle.on_message('LOCAL_POSITION_NED') + def listener(vehicle, name, m): + self._north = m.x + self._east = m.y + self._down = m.z + self.notify_attribute_listeners('local_frame', self.local_frame) + vehicle.notify_attribute_listeners('location.local_frame', vehicle.location.local_frame) + vehicle.notify_attribute_listeners('location', vehicle.location) + + @property + def local_frame(self): + """ + Location in local NED frame (a :py:class:`LocationGlobalRelative`). + + This is accessed through the :py:attr:`Vehicle.location` attribute: + + .. code-block:: python + + print "Local Location: %s" % vehicle.location.local_frame + + This location will not start to update until the vehicle is armed. + """ + return LocationLocal(self._north, self._east, self._down) + + @property + def global_frame(self): + """ + Location in global frame (a :py:class:`LocationGlobal`). + + The latitude and longitude are relative to the + `WGS84 coordinate system `_. + The altitude is relative to mean sea-level (MSL). + + This is accessed through the :py:attr:`Vehicle.location` attribute: + + .. code-block:: python + + print "Global Location: %s" % vehicle.location.global_frame + print "Sea level altitude is: %s" % vehicle.location.global_frame.alt + + Its ``lat`` and ``lon`` attributes are populated shortly after GPS becomes available. + The ``alt`` can take several seconds longer to populate (from the barometer). + Listeners are not notified of changes to this attribute until it has fully populated. + + To watch for changes you can use :py:func:`Vehicle.on_attribute` decorator or + :py:func:`add_attribute_listener` (decorator approach shown below): + + .. code-block:: python + + @vehicle.on_attribute('location.global_frame') + def listener(self, attr_name, value): + print " Global: %s" % value + + #Alternatively, use decorator: ``@vehicle.location.on_attribute('global_frame')``. + """ + return LocationGlobal(self._lat, self._lon, self._alt) + + @property + def global_relative_frame(self): + """ + Location in global frame, with altitude relative to the home location + (a :py:class:`LocationGlobalRelative`). + + The latitude and longitude are relative to the + `WGS84 coordinate system `_. + The altitude is relative to :py:attr:`home location `. + + This is accessed through the :py:attr:`Vehicle.location` attribute: + + .. code-block:: python + + print "Global Location (relative altitude): %s" % vehicle.location.global_relative_frame + print "Altitude relative to home_location: %s" % vehicle.location.global_relative_frame.alt + """ + return LocationGlobalRelative(self._lat, self._lon, self._relative_alt) + + +class Vehicle(HasObservers): + """ + The main vehicle API. + + Vehicle state is exposed through 'attributes' (e.g. :py:attr:`heading`). All attributes can be + read, and some are also settable + (:py:attr:`mode`, :py:attr:`armed` and :py:attr:`home_location`). + + Attributes can also be asynchronously monitored for changes by registering listener callback + functions. + + Vehicle "settings" (parameters) are read/set using the :py:attr:`parameters` attribute. + Parameters can be iterated and are also individually observable. + + Vehicle movement is primarily controlled using the :py:attr:`armed` attribute and + :py:func:`simple_takeoff` and :py:func:`simple_goto` in GUIDED mode. + + It is also possible to work with vehicle "missions" using the :py:attr:`commands` attribute, + and run them in AUTO mode. + + STATUSTEXT log messages from the autopilot are handled through a separate logger. + It is possible to configure the log level, the formatting, etc. by accessing the logger, e.g.: + + .. code-block:: python + + import logging + autopilot_logger = logging.getLogger('autopilot') + autopilot_logger.setLevel(logging.DEBUG) + + The guide contains more detailed information on the different ways you can use + the ``Vehicle`` class: + + - :doc:`guide/vehicle_state_and_parameters` + - :doc:`guide/copter/guided_mode` + - :doc:`guide/auto_mode` + + + .. note:: + + This class currently exposes just the attributes that are most commonly used by all + vehicle types. if you need to add additional attributes then subclass ``Vehicle`` + as demonstrated in :doc:`examples/create_attribute`. + + Please then :doc:`contribute ` your additions back + to the project! + """ + + def __init__(self, handler): + super(Vehicle, self).__init__() + + self._logger = logging.getLogger(__name__) # Logger for DroneKit + self._autopilot_logger = logging.getLogger('autopilot') # Logger for the autopilot messages + # MAVLink-to-logging-module log severity mappings + self._mavlink_statustext_severity = { + 0: logging.CRITICAL, + 1: logging.CRITICAL, + 2: logging.CRITICAL, + 3: logging.ERROR, + 4: logging.WARNING, + 5: logging.INFO, + 6: logging.INFO, + 7: logging.DEBUG + } + + self._handler = handler + self._master = handler.master + + # Cache all updated attributes for wait_ready. + # By default, we presume all "commands" are loaded. + self._ready_attrs = {'commands'} + + # Default parameters when calling wait_ready() or wait_ready(True). + self._default_ready_attrs = ['parameters', 'gps_0', 'armed', 'mode', 'attitude'] + + @self.on_attribute('*') + def listener(_, name, value): + self._ready_attrs.add(name) + + # Attaches message listeners. + self._message_listeners = dict() + + @handler.forward_message + def listener(_, msg): + self.notify_message_listeners(msg.get_type(), msg) + + self._location = Locations(self) + self._vx = None + self._vy = None + self._vz = None + + + self._wind_direction = None + self._wind_speed = None + self._wind_speed_z = None + + @self.on_message('WIND') + def listener(self,name, m): + """ WIND {direction : -180.0, speed : 0.0, speed_z : 0.0} """ + self._wind_direction = m.direction + self._wind_speed = m.speed + self._wind_speed_z = m.speed_z + + + @self.on_message('STATUSTEXT') + def statustext_listener(self, name, m): + # Log the STATUSTEXT on the autopilot logger, with the correct severity + self._autopilot_logger.log( + msg=m.text.strip(), + level=self._mavlink_statustext_severity[m.severity] + ) + + @self.on_message('GLOBAL_POSITION_INT') + def listener(self, name, m): + (self._vx, self._vy, self._vz) = (m.vx / 100.0, m.vy / 100.0, m.vz / 100.0) + self.notify_attribute_listeners('velocity', self.velocity) + + self._pitch = None + self._yaw = None + self._roll = None + self._pitchspeed = None + self._yawspeed = None + self._rollspeed = None + + @self.on_message('ATTITUDE') + def listener(self, name, m): + self._pitch = m.pitch + self._yaw = m.yaw + self._roll = m.roll + self._pitchspeed = m.pitchspeed + self._yawspeed = m.yawspeed + self._rollspeed = m.rollspeed + self.notify_attribute_listeners('attitude', self.attitude) + + self._heading = None + self._airspeed = None + self._groundspeed = None + + @self.on_message('VFR_HUD') + def listener(self, name, m): + self._heading = m.heading + self.notify_attribute_listeners('heading', self.heading) + self._airspeed = m.airspeed + self.notify_attribute_listeners('airspeed', self.airspeed) + self._groundspeed = m.groundspeed + self.notify_attribute_listeners('groundspeed', self.groundspeed) + + self._rngfnd_distance = None + self._rngfnd_voltage = None + + @self.on_message('RANGEFINDER') + def listener(self, name, m): + self._rngfnd_distance = m.distance + self._rngfnd_voltage = m.voltage + self.notify_attribute_listeners('rangefinder', self.rangefinder) + + self._mount_pitch = None + self._mount_yaw = None + self._mount_roll = None + + @self.on_message('MOUNT_STATUS') + def listener(self, name, m): + self._mount_pitch = m.pointing_a / 100.0 + self._mount_roll = m.pointing_b / 100.0 + self._mount_yaw = m.pointing_c / 100.0 + self.notify_attribute_listeners('mount', self.mount_status) + + self._capabilities = None + self._raw_version = None + self._autopilot_version_msg_count = 0 + + @self.on_message('AUTOPILOT_VERSION') + def listener(vehicle, name, m): + self._capabilities = m.capabilities + self._raw_version = m.flight_sw_version + self._autopilot_version_msg_count += 1 + if self._capabilities != 0 or self._autopilot_version_msg_count > 5: + # ArduPilot <3.4 fails to send capabilities correctly + # straight after boot, and even older versions send + # this back as always-0. + vehicle.remove_message_listener('HEARTBEAT', self.send_capabilities_request) + self.notify_attribute_listeners('autopilot_version', self._raw_version) + + # gimbal + self._gimbal = Gimbal(self) + + # All keys are strings. + self._channels = Channels(self, 8) + + @self.on_message(['RC_CHANNELS_RAW', 'RC_CHANNELS']) + def listener(self, name, m): + def set_rc(chnum, v): + '''Private utility for handling rc channel messages''' + # use port to allow ch nums greater than 8 + port = 0 if name == "RC_CHANNELS" else m.port + self._channels._update_channel(str(port * 8 + chnum), v) + + for i in range(1, (18 if name == "RC_CHANNELS" else 8)+1): + set_rc(i, getattr(m, "chan{}_raw".format(i))) + + self.notify_attribute_listeners('channels', self.channels) + + self._voltage = None + self._current = None + self._level = None + + @self.on_message('SYS_STATUS') + def listener(self, name, m): + self._voltage = m.voltage_battery + self._current = m.current_battery + self._level = m.battery_remaining + self.notify_attribute_listeners('battery', self.battery) + + self._eph = None + self._epv = None + self._satellites_visible = None + self._fix_type = None # FIXME support multiple GPSs per vehicle - possibly by using componentId + + @self.on_message('GPS_RAW_INT') + def listener(self, name, m): + self._eph = m.eph + self._epv = m.epv + self._satellites_visible = m.satellites_visible + self._fix_type = m.fix_type + self.notify_attribute_listeners('gps_0', self.gps_0) + + self._current_waypoint = 0 + + @self.on_message(['WAYPOINT_CURRENT', 'MISSION_CURRENT']) + def listener(self, name, m): + self._current_waypoint = m.seq + + self._ekf_poshorizabs = False + self._ekf_constposmode = False + self._ekf_predposhorizabs = False + + @self.on_message('EKF_STATUS_REPORT') + def listener(self, name, m): + # boolean: EKF's horizontal position (absolute) estimate is good + self._ekf_poshorizabs = (m.flags & ardupilotmega.EKF_POS_HORIZ_ABS) > 0 + # boolean: EKF is in constant position mode and does not know it's absolute or relative position + self._ekf_constposmode = (m.flags & ardupilotmega.EKF_CONST_POS_MODE) > 0 + # boolean: EKF's predicted horizontal position (absolute) estimate is good + self._ekf_predposhorizabs = (m.flags & ardupilotmega.EKF_PRED_POS_HORIZ_ABS) > 0 + + self.notify_attribute_listeners('ekf_ok', self.ekf_ok, cache=True) + + self._flightmode = 'AUTO' + self._armed = False + self._system_status = None + self._autopilot_type = None # PX4, ArduPilot, etc. + self._vehicle_type = None # quadcopter, plane, etc. + + @self.on_message('HEARTBEAT') + def listener(self, name, m): + # ignore groundstations + if m.type == mavutil.mavlink.MAV_TYPE_GCS or (not self._handler.master.probably_vehicle_heartbeat(m)): + return + self._armed = (m.base_mode & mavutil.mavlink.MAV_MODE_FLAG_SAFETY_ARMED) != 0 + self.notify_attribute_listeners('armed', self.armed, cache=True) + self._autopilot_type = m.autopilot + self._vehicle_type = m.type + if self._is_mode_available(m.custom_mode, m.base_mode) is False: + raise APIException("mode (%s, %s) not available on mavlink definition" % (m.custom_mode, m.base_mode)) + if self._autopilot_type == mavutil.mavlink.MAV_AUTOPILOT_PX4: + self._flightmode = mavutil.interpret_px4_mode(m.base_mode, m.custom_mode) + else: + self._flightmode = self._mode_mapping_bynumber[m.custom_mode] + self.notify_attribute_listeners('mode', self.mode, cache=True) + self._system_status = m.system_status + self.notify_attribute_listeners('system_status', self.system_status, cache=True) + + # Waypoints. + + self._home_location = None + self._wploader = mavwp.MAVWPLoader() + self._wp_loaded = True + self._wp_uploaded = None + self._wpts_dirty = False + self._commands = CommandSequence(self) + + @self.on_message(['WAYPOINT_COUNT', 'MISSION_COUNT']) + def listener(self, name, msg): + if not self._wp_loaded: + self._wploader.clear() + self._wploader.expected_count = msg.count + self._master.waypoint_request_send(0) + + @self.on_message(['HOME_POSITION']) + def listener(self, name, msg): + self._home_location = LocationGlobal(msg.latitude / 1.0e7, msg.longitude / 1.0e7, msg.altitude / 1000.0) + self.notify_attribute_listeners('home_location', self.home_location, cache=True) + + @self.on_message(['WAYPOINT', 'MISSION_ITEM']) + def listener(self, name, msg): + if not self._wp_loaded: + if msg.seq == 0: + if not (msg.x == 0 and msg.y == 0 and msg.z == 0): + self._home_location = LocationGlobal(msg.x, msg.y, msg.z) + + if msg.seq > self._wploader.count(): + # Unexpected waypoint + pass + elif msg.seq < self._wploader.count(): + # Waypoint duplicate pass else: - raise e + self._wploader.add(msg) + + if msg.seq + 1 < self._wploader.expected_count: + self._master.waypoint_request_send(msg.seq + 1) + else: + self._wp_loaded = True + self.notify_attribute_listeners('commands', self.commands) + + # Waypoint send to master + @self.on_message(['WAYPOINT_REQUEST', 'MISSION_REQUEST']) + def listener(self, name, msg): + if self._wp_uploaded is not None: + wp = self._wploader.wp(msg.seq) + handler.fix_targets(wp) + self._master.mav.send(wp) + self._wp_uploaded[msg.seq] = True + + # TODO: Waypoint loop listeners + + # Parameters. + + start_duration = 0.2 + repeat_duration = 1 + + self._params_count = -1 + self._params_set = [] + self._params_loaded = False + self._params_start = False + self._params_map = {} + self._params_last = monotonic.monotonic() # Last new param. + self._params_duration = start_duration + self._parameters = Parameters(self) + + @handler.forward_loop + def listener(_): + # Check the time duration for last "new" params exceeds watchdog. + if not self._params_start: + return + + if not self._params_loaded and all(x is not None for x in self._params_set): + self._params_loaded = True + self.notify_attribute_listeners('parameters', self.parameters) + + if not self._params_loaded and monotonic.monotonic() - self._params_last > self._params_duration: + c = 0 + for i, v in enumerate(self._params_set): + if v is None: + self._master.mav.param_request_read_send(0, 0, b'', i) + c += 1 + if c > 50: + break + self._params_duration = repeat_duration + self._params_last = monotonic.monotonic() + + @self.on_message(['PARAM_VALUE']) + def listener(self, name, msg): + # If we discover a new param count, assume we + # are receiving a new param set. + if self._params_count != msg.param_count: + self._params_loaded = False + self._params_start = True + self._params_count = msg.param_count + self._params_set = [None] * msg.param_count + + # Attempt to set the params. We throw an error + # if the index is out of range of the count or + # we lack a param_id. + try: + if msg.param_index < msg.param_count and msg: + if self._params_set[msg.param_index] is None: + self._params_last = monotonic.monotonic() + self._params_duration = start_duration + self._params_set[msg.param_index] = msg + + self._params_map[msg.param_id] = msg.param_value + self._parameters.notify_attribute_listeners(msg.param_id, msg.param_value, + cache=True) + except: + import traceback + traceback.print_exc() + + # Heartbeats. + + self._heartbeat_started = False + self._heartbeat_lastsent = 0 + self._heartbeat_lastreceived = 0 + self._heartbeat_timeout = False + + self._heartbeat_warning = 5 + self._heartbeat_error = 30 + self._heartbeat_system = None + + @handler.forward_loop + def listener(_): + # Send 1 heartbeat per second + if monotonic.monotonic() - self._heartbeat_lastsent > 1: + self._master.mav.heartbeat_send(mavutil.mavlink.MAV_TYPE_GCS, + mavutil.mavlink.MAV_AUTOPILOT_INVALID, 0, 0, 0) + self._heartbeat_lastsent = monotonic.monotonic() + + # Timeouts. + if self._heartbeat_started: + if self._heartbeat_error and monotonic.monotonic() - self._heartbeat_lastreceived > self._heartbeat_error > 0: + raise APIException('No heartbeat in %s seconds, aborting.' % + self._heartbeat_error) + elif monotonic.monotonic() - self._heartbeat_lastreceived > self._heartbeat_warning: + if self._heartbeat_timeout is False: + self._logger.warning('Link timeout, no heartbeat in last %s seconds' % self._heartbeat_warning) + self._heartbeat_timeout = True + + @self.on_message(['HEARTBEAT']) + def listener(self, name, msg): + # ignore groundstations + if msg.type == mavutil.mavlink.MAV_TYPE_GCS or (not self._handler.master.probably_vehicle_heartbeat(msg)): + return + self._heartbeat_system = msg.get_srcSystem() + self._heartbeat_lastreceived = monotonic.monotonic() + if self._heartbeat_timeout: + self._logger.info('...link restored.') + self._heartbeat_timeout = False + + self._last_heartbeat = None + @handler.forward_loop + def listener(_): + if self._heartbeat_lastreceived: + self._last_heartbeat = monotonic.monotonic() - self._heartbeat_lastreceived + self.notify_attribute_listeners('last_heartbeat', self.last_heartbeat) - t = Thread(target=mavlink_thread) - t.daemon = True - t.start() + @property + def last_heartbeat(self): + """ + Time since last MAVLink heartbeat was received (in seconds). - # Wait for first heartbeat. - while True: + The attribute can be used to monitor link activity and implement script-specific timeout handling. + + For example, to pause the script if no heartbeat is received for more than 1 second you might implement + the following observer, and use ``pause_script`` in a program loop to wait until the link is recovered: + + .. code-block:: python + + pause_script=False + + @vehicle.on_attribute('last_heartbeat') + def listener(self, attr_name, value): + global pause_script + if value > 1 and not pause_script: + print "Pausing script due to bad link" + pause_script=True; + if value < 1 and pause_script: + pause_script=False; + print "Un-pausing script" + + The observer will be called at the period of the messaging loop (about every 0.01 seconds). Testing + on SITL indicates that ``last_heartbeat`` averages about .5 seconds, but will rarely exceed 1.5 seconds + when connected. Whether heartbeat monitoring can be useful will very much depend on the application. + + + .. note:: + + If you just want to change the heartbeat timeout you can modify the ``heartbeat_timeout`` + parameter passed to the :py:func:`connect() ` function. + + """ + return self._last_heartbeat + + def on_message(self, name): + """ + Decorator for message listener callback functions. + + .. tip:: + + This is the most elegant way to define message listener callback functions. + Use :py:func:`add_message_listener` only if you need to be able to + :py:func:`remove the listener ` later. + + A decorated message listener function is called with three arguments every time the + specified message is received: + + * ``self`` - the current vehicle. + * ``name`` - the name of the message that was intercepted. + * ``message`` - the actual message (a `pymavlink `_ + `class `_). + + For example, in the fragment below ``my_method`` will be called for every heartbeat message: + + .. code:: python + + @vehicle.on_message('HEARTBEAT') + def my_method(self, name, msg): + pass + + See :ref:`mavlink_messages` for more information. + + :param String name: The name of the message to be intercepted by the decorated listener function (or '*' to get all messages). + """ + + def decorator(fn): + if isinstance(name, list): + for n in name: + self.add_message_listener(n, fn) + else: + self.add_message_listener(name, fn) + + return decorator + + def add_message_listener(self, name, fn): + """ + Adds a message listener function that will be called every time the specified message is received. + + .. tip:: + + We recommend you use :py:func:`on_message` instead of this method as it has a more elegant syntax. + This method is only preferred if you need to be able to + :py:func:`remove the listener `. + + The callback function must have three arguments: + + * ``self`` - the current vehicle. + * ``name`` - the name of the message that was intercepted. + * ``message`` - the actual message (a `pymavlink `_ + `class `_). + + For example, in the fragment below ``my_method`` will be called for every heartbeat message: + + .. code:: python + + #Callback method for new messages + def my_method(self, name, msg): + pass + + vehicle.add_message_listener('HEARTBEAT',my_method) + + See :ref:`mavlink_messages` for more information. + + :param String name: The name of the message to be intercepted by the listener function (or '*' to get all messages). + :param fn: The listener function that will be called if a message is received. + """ + name = str(name) + if name not in self._message_listeners: + self._message_listeners[name] = [] + if fn not in self._message_listeners[name]: + self._message_listeners[name].append(fn) + + def remove_message_listener(self, name, fn): + """ + Removes a message listener (that was previously added using :py:func:`add_message_listener`). + + See :ref:`mavlink_messages` for more information. + + :param String name: The name of the message for which the listener is to be removed (or '*' to remove an 'all messages' observer). + :param fn: The listener callback function to remove. + + """ + name = str(name) + if name in self._message_listeners: + if fn in self._message_listeners[name]: + self._message_listeners[name].remove(fn) + if len(self._message_listeners[name]) == 0: + del self._message_listeners[name] + + def notify_message_listeners(self, name, msg): + for fn in self._message_listeners.get(name, []): + try: + fn(self, name, msg) + except Exception: + self._logger.exception('Exception in message handler for %s' % msg.get_type(), exc_info=True) + + for fn in self._message_listeners.get('*', []): try: - self.master.wait_heartbeat() + fn(self, name, msg) + except Exception: + self._logger.exception('Exception in message handler for %s' % msg.get_type(), exc_info=True) + + def close(self): + return self._handler.close() + + def flush(self): + """ + Call ``flush()`` after :py:func:`adding ` or :py:func:`clearing ` mission commands. + + After the return from ``flush()`` any writes are guaranteed to have completed (or thrown an + exception) and future reads will see their effects. + + .. warning:: + + This method is deprecated. It has been replaced by + :py:func:`Vehicle.commands.upload() `. + """ + return self.commands.upload() + + # + # Private sugar methods + # + + @property + def _mode_mapping(self): + return self._master.mode_mapping() + + @property + def _mode_mapping_bynumber(self): + return mavutil.mode_mapping_bynumber(self._vehicle_type) + + def _is_mode_available(self, custommode_code, basemode_code=0): + try: + if self._autopilot_type == mavutil.mavlink.MAV_AUTOPILOT_PX4: + mode = mavutil.interpret_px4_mode(basemode_code, custommode_code) + return mode in self._mode_mapping + return custommode_code in self._mode_mapping_bynumber + except: + return False + + # + # Operations to support the standard API. + # + + @property + def mode(self): + """ + This attribute is used to get and set the current flight mode. You + can specify the value as a :py:class:`VehicleMode`, like this: + + .. code-block:: python + + vehicle.mode = VehicleMode('LOITER') + + Or as a simple string: + + .. code-block:: python + + vehicle.mode = 'LOITER' + + If you are targeting ArduPilot you can also specify the flight mode + using a numeric value (this will not work with PX4 autopilots): + + .. code-block:: python + + # set mode to LOITER + vehicle.mode = 5 + """ + if not self._flightmode: + return None + return VehicleMode(self._flightmode) + + @mode.setter + def mode(self, v): + if isinstance(v, basestring): + v = VehicleMode(v) + + if self._autopilot_type == mavutil.mavlink.MAV_AUTOPILOT_PX4: + self._master.set_mode(v.name) + elif isinstance(v, int): + self._master.set_mode(v) + else: + self._master.set_mode(self._mode_mapping[v.name]) + + @property + def location(self): + """ + The vehicle location in global, global relative and local frames (:py:class:`Locations`). + + The different frames are accessed through its members: + + * :py:attr:`global_frame ` (:py:class:`LocationGlobal`) + * :py:attr:`global_relative_frame ` (:py:class:`LocationGlobalRelative`) + * :py:attr:`local_frame ` (:py:class:`LocationLocal`) + + For example, to print the location in each frame for a ``vehicle``: + + .. code-block:: python + + # Print location information for `vehicle` in all frames (default printer) + print "Global Location: %s" % vehicle.location.global_frame + print "Global Location (relative altitude): %s" % vehicle.location.global_relative_frame + print "Local Location: %s" % vehicle.location.local_frame #NED + + # Print altitudes in the different frames (see class definitions for other available information) + print "Altitude (global frame): %s" % vehicle.location.global_frame.alt + print "Altitude (global relative frame): %s" % vehicle.location.global_relative_frame.alt + print "Altitude (NED frame): %s" % vehicle.location.local_frame.down + + .. note:: + + All the location "values" (e.g. ``global_frame.lat``) are initially + created with value ``None``. The ``global_frame``, ``global_relative_frame`` + latitude and longitude values are populated shortly after initialisation but + ``global_frame.alt`` may take a few seconds longer to be updated. + The ``local_frame`` does not populate until the vehicle is armed. + + The attribute and its members are observable. To watch for changes in all frames using a listener + created using a decorator (you can also define a listener and explicitly add it). + + .. code-block:: python + + @vehicle.on_attribute('location') + def listener(self, attr_name, value): + # `self`: :py:class:`Vehicle` object that has been updated. + # `attr_name`: name of the observed attribute - 'location' + # `value` is the updated attribute value (a :py:class:`Locations`). This can be queried for the frame information + print " Global: %s" % value.global_frame + print " GlobalRelative: %s" % value.global_relative_frame + print " Local: %s" % value.local_frame + + To watch for changes in just one attribute (in this case ``global_frame``): + + .. code-block:: python + + @vehicle.on_attribute('location.global_frame') + def listener(self, attr_name, value): + # `self`: :py:class:`Locations` object that has been updated. + # `attr_name`: name of the observed attribute - 'global_frame' + # `value` is the updated attribute value. + print " Global: %s" % value + + #Or watch using decorator: ``@vehicle.location.on_attribute('global_frame')``. + """ + return self._location + + @property + def wind(self): + """ + Current wind status (:pu:class: `Wind`) + """ + if self._wind_direction is None or self._wind_speed is None or self._wind_speed_z is None: + return None + return Wind(self._wind_direction, self._wind_speed, self._wind_speed_z) + + @property + def battery(self): + """ + Current system batter status (:py:class:`Battery`). + """ + if self._voltage is None or self._current is None or self._level is None: + return None + return Battery(self._voltage, self._current, self._level) + + @property + def rangefinder(self): + """ + Rangefinder distance and voltage values (:py:class:`Rangefinder`). + """ + return Rangefinder(self._rngfnd_distance, self._rngfnd_voltage) + + @property + def velocity(self): + """ + Current velocity as a three element list ``[ vx, vy, vz ]`` (in meter/sec). + """ + return [self._vx, self._vy, self._vz] + + @property + def version(self): + """ + The autopilot version and type in a :py:class:`Version` object. + + .. versionadded:: 2.0.3 + """ + return Version(self._raw_version, self._autopilot_type, self._vehicle_type) + + @property + def capabilities(self): + """ + The autopilot capabilities in a :py:class:`Capabilities` object. + + .. versionadded:: 2.0.3 + """ + return Capabilities(self._capabilities) + + @property + def attitude(self): + """ + Current vehicle attitude - pitch, yaw, roll (:py:class:`Attitude`). + """ + return Attitude(self._pitch, self._yaw, self._roll) + + @property + def gps_0(self): + """ + GPS position information (:py:class:`GPSInfo`). + """ + return GPSInfo(self._eph, self._epv, self._fix_type, self._satellites_visible) + + @property + def armed(self): + """ + This attribute can be used to get and set the ``armed`` state of the vehicle (``boolean``). + + The code below shows how to read the state, and to arm/disarm the vehicle: + + .. code:: python + + # Print the armed state for the vehicle + print "Armed: %s" % vehicle.armed + + # Disarm the vehicle + vehicle.armed = False + + # Arm the vehicle + vehicle.armed = True + """ + return self._armed + + @armed.setter + def armed(self, value): + if bool(value) != self._armed: + if value: + self._master.arducopter_arm() + else: + self._master.arducopter_disarm() + + @property + def is_armable(self): + """ + Returns ``True`` if the vehicle is ready to arm, false otherwise (``Boolean``). + + This attribute wraps a number of pre-arm checks, ensuring that the vehicle has booted, + has a good GPS fix, and that the EKF pre-arm is complete. + """ + # check that mode is not INITIALSING + # check that we have a GPS fix + # check that EKF pre-arm is complete + return self.mode != 'INITIALISING' and (self.gps_0.fix_type is not None and self.gps_0.fix_type > 1) and self._ekf_predposhorizabs + + @property + def system_status(self): + """ + System status (:py:class:`SystemStatus`). + + The status has a ``state`` property with one of the following values: + + * ``UNINIT``: Uninitialized system, state is unknown. + * ``BOOT``: System is booting up. + * ``CALIBRATING``: System is calibrating and not flight-ready. + * ``STANDBY``: System is grounded and on standby. It can be launched any time. + * ``ACTIVE``: System is active and might be already airborne. Motors are engaged. + * ``CRITICAL``: System is in a non-normal flight mode. It can however still navigate. + * ``EMERGENCY``: System is in a non-normal flight mode. It lost control over parts + or over the whole airframe. It is in mayday and going down. + * ``POWEROFF``: System just initialized its power-down sequence, will shut down now. + """ + return { + 0: SystemStatus('UNINIT'), + 1: SystemStatus('BOOT'), + 2: SystemStatus('CALIBRATING'), + 3: SystemStatus('STANDBY'), + 4: SystemStatus('ACTIVE'), + 5: SystemStatus('CRITICAL'), + 6: SystemStatus('EMERGENCY'), + 7: SystemStatus('POWEROFF'), + }.get(self._system_status, None) + + @property + def heading(self): + """ + Current heading in degrees - 0..360, where North = 0 (``int``). + """ + return self._heading + + @property + def groundspeed(self): + """ + Current groundspeed in metres/second (``double``). + + This attribute is settable. The set value is the default target groundspeed + when moving the vehicle using :py:func:`simple_goto` (or other position-based + movement commands). + """ + return self._groundspeed + + @groundspeed.setter + def groundspeed(self, speed): + speed_type = 1 # ground speed + msg = self.message_factory.command_long_encode( + 0, 0, # target system, target component + mavutil.mavlink.MAV_CMD_DO_CHANGE_SPEED, # command + 0, # confirmation + speed_type, # param 1 + speed, # speed in metres/second + -1, 0, 0, 0, 0 # param 3 - 7 + ) + + # send command to vehicle + self.send_mavlink(msg) + + @property + def airspeed(self): + """ + Current airspeed in metres/second (``double``). + + This attribute is settable. The set value is the default target airspeed + when moving the vehicle using :py:func:`simple_goto` (or other position-based + movement commands). + """ + return self._airspeed + + @airspeed.setter + def airspeed(self, speed): + speed_type = 0 # air speed + msg = self.message_factory.command_long_encode( + 0, 0, # target system, target component + mavutil.mavlink.MAV_CMD_DO_CHANGE_SPEED, # command + 0, # confirmation + speed_type, # param 1 + speed, # speed in metres/second + -1, 0, 0, 0, 0 # param 3 - 7 + ) + + # send command to vehicle + self.send_mavlink(msg) + + @property + def gimbal(self): + """ + Gimbal object for controlling, viewing and observing gimbal status (:py:class:`Gimbal`). + + .. versionadded:: 2.0.1 + """ + return self._gimbal + + @property + def mount_status(self): + """ + .. warning:: This method is deprecated. It has been replaced by :py:attr:`gimbal`. + + Current status of the camera mount (gimbal) as a three element list: ``[ pitch, yaw, roll ]``. + + The values in the list are set to ``None`` if no mount is configured. + """ + return [self._mount_pitch, self._mount_yaw, self._mount_roll] + + @property + def ekf_ok(self): + """ + ``True`` if the EKF status is considered acceptable, ``False`` otherwise (``boolean``). + """ + # legacy check for dronekit-python for solo + # use same check that ArduCopter::system.pde::position_ok() is using + if self.armed: + return self._ekf_poshorizabs and not self._ekf_constposmode + else: + return self._ekf_poshorizabs or self._ekf_predposhorizabs + + @property + def channels(self): + """ + The RC channel values from the RC Transmitter (:py:class:`Channels`). + + The attribute can also be used to set and read RC Override (channel override) values + via :py:attr:`Vehicle.channels.override `. + + For more information and examples see :ref:`example_channel_overrides`. + + To read the channels from the RC transmitter: + + .. code:: python + + # Get all channel values from RC transmitter + print "Channel values from RC Tx:", vehicle.channels + + # Access channels individually + print "Read channels individually:" + print " Ch1: %s" % vehicle.channels['1'] + print " Ch2: %s" % vehicle.channels['2'] + + """ + return self._channels + + @property + def home_location(self): + """ + The current home location (:py:class:`LocationGlobal`). + + To get the attribute you must first download the :py:func:`Vehicle.commands`. + The attribute has a value of ``None`` until :py:func:`Vehicle.commands` has been downloaded + **and** the autopilot has set an initial home location (typically where the vehicle first gets GPS lock). + + .. code-block:: python + + #Connect to a vehicle object (for example, on com14) + vehicle = connect('com14', wait_ready=True) + + # Download the vehicle waypoints (commands). Wait until download is complete. + cmds = vehicle.commands + cmds.download() + cmds.wait_ready() + + # Get the home location + home = vehicle.home_location + + The ``home_location`` is not observable. + + The attribute can be written (in the same way as any other attribute) after it has successfully + been populated from the vehicle. The value sent to the vehicle is cached in the attribute + (and can potentially get out of date if you don't re-download ``Vehicle.commands``): + + .. warning:: + + Setting the value will fail silently if the specified location is more than 50km from the EKF origin. + + + """ + return copy.copy(self._home_location) + + @home_location.setter + def home_location(self, pos): + """ + Sets the home location (``LocationGlobal``). + + The value cannot be set until it has successfully been read from the vehicle. After being + set the value is cached in the home_location attribute and does not have to be re-read. + + .. note:: + + Setting the value will fail silently if the specified location is more than 50km from the EKF origin. + """ + + if not isinstance(pos, LocationGlobal): + raise ValueError('Expecting home_location to be set to a LocationGlobal.') + + # Set cached home location. + self._home_location = copy.copy(pos) + + # Send MAVLink update. + self.send_mavlink(self.message_factory.command_long_encode( + 0, 0, # target system, target component + mavutil.mavlink.MAV_CMD_DO_SET_HOME, # command + 0, # confirmation + 0, # param 1: 1 to use current position, 0 to use the entered values. + 0, 0, 0, # params 2-4 + pos.lat, pos.lon, pos.alt)) + + @property + def commands(self): + """ + Gets the editable waypoints/current mission for this vehicle (:py:class:`CommandSequence`). + + This can be used to get, create, and modify a mission. + + :returns: A :py:class:`CommandSequence` containing the waypoints for this vehicle. + """ + return self._commands + + @property + def parameters(self): + """ + The (editable) parameters for this vehicle (:py:class:`Parameters`). + """ + return self._parameters + + def wait_for(self, condition, timeout=None, interval=0.1, errmsg=None): + '''Wait for a condition to be True. + + Wait for condition, a callable, to return True. If timeout is + nonzero, raise a TimeoutError(errmsg) if the condition is not + True after timeout seconds. Check the condition everal + interval seconds. + ''' + + t0 = time.time() + while not condition(): + t1 = time.time() + if timeout and (t1 - t0) >= timeout: + raise TimeoutError(errmsg) + + time.sleep(interval) + + def wait_for_armable(self, timeout=None): + '''Wait for the vehicle to become armable. + + If timeout is nonzero, raise a TimeoutError if the vehicle + is not armable after timeout seconds. + ''' + + def check_armable(): + return self.is_armable + + self.wait_for(check_armable, timeout=timeout) + + def arm(self, wait=True, timeout=None): + '''Arm the vehicle. + + If wait is True, wait for arm operation to complete before + returning. If timeout is nonzero, raise a TimeouTerror if the + vehicle has not armed after timeout seconds. + ''' + + self.armed = True + + if wait: + self.wait_for(lambda: self.armed, timeout=timeout, + errmsg='failed to arm vehicle') + + def disarm(self, wait=True, timeout=None): + '''Disarm the vehicle. + + If wait is True, wait for disarm operation to complete before + returning. If timeout is nonzero, raise a TimeouTerror if the + vehicle has not disarmed after timeout seconds. + ''' + self.armed = False + + if wait: + self.wait_for(lambda: not self.armed, timeout=timeout, + errmsg='failed to disarm vehicle') + + def wait_for_mode(self, mode, timeout=None): + '''Set the flight mode. + + If wait is True, wait for the mode to change before returning. + If timeout is nonzero, raise a TimeoutError if the flight mode + hasn't changed after timeout seconds. + ''' + + if not isinstance(mode, VehicleMode): + mode = VehicleMode(mode) + + self.mode = mode + + self.wait_for(lambda: self.mode.name == mode.name, + timeout=timeout, + errmsg='failed to set flight mode') + + def wait_for_alt(self, alt, epsilon=0.1, rel=True, timeout=None): + '''Wait for the vehicle to reach the specified altitude. + + Wait for the vehicle to get within epsilon meters of the + given altitude. If rel is True (the default), use the + global_relative_frame. If rel is False, use the global_frame. + If timeout is nonzero, raise a TimeoutError if the specified + altitude has not been reached after timeout seconds. + ''' + + def get_alt(): + if rel: + alt = self.location.global_relative_frame.alt + else: + alt = self.location.global_frame.alt + + return alt + + def check_alt(): + cur = get_alt() + delta = abs(alt - cur) + + return ( + (delta < epsilon) or + (cur > alt > start) or + (cur < alt < start) + ) + + start = get_alt() + + self.wait_for( + check_alt, + timeout=timeout, + errmsg='failed to reach specified altitude') + + def wait_simple_takeoff(self, alt=None, epsilon=0.1, timeout=None): + self.simple_takeoff(alt) + + if alt is not None: + self.wait_for_alt(alt, epsilon=epsilon, timeout=timeout) + + def simple_takeoff(self, alt=None): + """ + Take off and fly the vehicle to the specified altitude (in metres) and then wait for another command. + + .. note:: + + This function should only be used on Copter vehicles. + + + The vehicle must be in GUIDED mode and armed before this is called. + + There is no mechanism for notification when the correct altitude is reached, + and if another command arrives before that point (e.g. :py:func:`simple_goto`) it will be run instead. + + .. warning:: + + Apps should code to ensure that the vehicle will reach a safe altitude before + other commands are executed. A good example is provided in the guide topic :doc:`guide/taking_off`. + + :param alt: Target height, in metres. + """ + if alt is not None: + altitude = float(alt) + if math.isnan(altitude) or math.isinf(altitude): + raise ValueError("Altitude was NaN or Infinity. Please provide a real number") + self._master.mav.command_long_send(0, 0, mavutil.mavlink.MAV_CMD_NAV_TAKEOFF, + 0, 0, 0, 0, 0, 0, 0, altitude) + + def simple_goto(self, location, airspeed=None, groundspeed=None): + ''' + Go to a specified global location (:py:class:`LocationGlobal` or :py:class:`LocationGlobalRelative`). + + There is no mechanism for notification when the target location is reached, and if another command arrives + before that point that will be executed immediately. + + You can optionally set the desired airspeed or groundspeed (this is identical to setting + :py:attr:`airspeed` or :py:attr:`groundspeed`). The vehicle will determine what speed to + use if the values are not set or if they are both set. + + The method will change the :py:class:`VehicleMode` to ``GUIDED`` if necessary. + + .. code:: python + + # Set mode to guided - this is optional as the simple_goto method will change the mode if needed. + vehicle.mode = VehicleMode("GUIDED") + + # Set the LocationGlobal to head towards + a_location = LocationGlobal(-34.364114, 149.166022, 30) + vehicle.simple_goto(a_location) + + :param location: The target location (:py:class:`LocationGlobal` or :py:class:`LocationGlobalRelative`). + :param airspeed: Target airspeed in m/s (optional). + :param groundspeed: Target groundspeed in m/s (optional). + ''' + if isinstance(location, LocationGlobalRelative): + frame = mavutil.mavlink.MAV_FRAME_GLOBAL_RELATIVE_ALT + alt = location.alt + elif isinstance(location, LocationGlobal): + # This should be the proper code: + # frame = mavutil.mavlink.MAV_FRAME_GLOBAL + # However, APM discards information about the relative frame + # and treats any alt value as relative. So we compensate here. + frame = mavutil.mavlink.MAV_FRAME_GLOBAL_RELATIVE_ALT + if not self.home_location: + self.commands.download() + self.commands.wait_ready() + alt = location.alt - self.home_location.alt + else: + raise ValueError('Expecting location to be LocationGlobal or LocationGlobalRelative.') + + self._master.mav.mission_item_send(0, 0, 0, frame, + mavutil.mavlink.MAV_CMD_NAV_WAYPOINT, 2, 0, 0, + 0, 0, 0, location.lat, location.lon, + alt) + + if airspeed is not None: + self.airspeed = airspeed + if groundspeed is not None: + self.groundspeed = groundspeed + + def send_mavlink(self, message): + """ + This method is used to send raw MAVLink "custom messages" to the vehicle. + + The function can send arbitrary messages/commands to the connected vehicle at any time and in any vehicle mode. + It is particularly useful for controlling vehicles outside of missions (for example, in GUIDED mode). + + The :py:func:`message_factory ` is used to create messages in the appropriate format. + + For more information see the guide topic: :ref:`guided_mode_how_to_send_commands`. + + :param message: A ``MAVLink_message`` instance, created using :py:func:`message_factory `. + There is need to specify the system id, component id or sequence number of messages as the API will set these appropriately. + """ + self._master.mav.send(message) + + @property + def message_factory(self): + """ + Returns an object that can be used to create 'raw' MAVLink messages that are appropriate for this vehicle. + The message can then be sent using :py:func:`send_mavlink(message) `. + + .. note:: + + Vehicles support a subset of the messages defined in the MAVLink standard. For more information + about the supported sets see wiki topics: + `Copter Commands in Guided Mode `_ + and `Plane Commands in Guided Mode `_. + + All message types are defined in the central MAVLink github repository. For example, a Pixhawk understands + the following messages (from `pixhawk.xml `_): + + .. code:: xml + + + 0 to disable, 1 to enable + + + The name of the factory method will always be the lower case version of the message name with *_encode* appended. + Each field in the XML message definition must be listed as arguments to this factory method. So for this example + message, the call would be: + + .. code:: python + + msg = vehicle.message_factory.image_trigger_control_encode(True) + vehicle.send_mavlink(msg) + + Some message types include "addressing information". If present, there is no need to specify the ``target_system`` + id (just set to zero) as DroneKit will automatically update messages with the correct ID for the connected + vehicle before sending. + The ``target_component`` should be set to 0 (broadcast) unless the message is to specific component. + CRC fields and sequence numbers (if defined in the message type) are automatically set by DroneKit and can also + be ignored/set to zero. + + For more information see the guide topic: :ref:`guided_mode_how_to_send_commands`. + """ + return self._master.mav + + def initialize(self, rate=4, heartbeat_timeout=30): + self._handler.start() + + # Start heartbeat polling. + start = monotonic.monotonic() + self._heartbeat_error = heartbeat_timeout or 0 + self._heartbeat_started = True + self._heartbeat_lastreceived = start + + # Poll for first heartbeat. + # If heartbeat times out, this will interrupt. + while self._handler._alive: + time.sleep(.1) + if self._heartbeat_lastreceived != start: break - except mavutil.mavlink.MAVError: - continue - heartbeat_started = True + if not self._handler._alive: + raise APIException('Timeout in initializing connection.') + + # Register target_system now. + self._handler.target_system = self._heartbeat_system - # Request a list of all parameters. - request_data_stream_send(self.master) + # Wait until board has booted. + while True: + if self._flightmode not in [None, 'INITIALISING', 'MAV']: + break + time.sleep(0.1) + + # Initialize data stream. + if rate is not None: + self._master.mav.request_data_stream_send(0, 0, mavutil.mavlink.MAV_DATA_STREAM_ALL, + rate, 1) + + self.add_message_listener('HEARTBEAT', self.send_capabilities_request) + + # Ensure initial parameter download has started. while True: # This fn actually rate limits itself to every 2s. # Just retry with persistence to get our first param stream. - self.master.param_fetch_all() + self._master.param_fetch_all() time.sleep(0.1) - if params.mav_param_count > -1: + if self._params_count > -1: break - # We now should get parameters streaming in. - # We may not get the full set; we leave the logic to mavlink_thread - # to determine what params we yet need. Wait if await_params is True. - if await_params: - while not params.loaded: + def send_capabilties_request(self, vehicle, name, m): + '''An alias for send_capabilities_request. + + The word "capabilities" was misspelled in previous versions of this code. This is simply + an alias to send_capabilities_request using the legacy name. + ''' + return self.send_capabilities_request(vehicle, name, m) + + def send_capabilities_request(self, vehicle, name, m): + '''Request an AUTOPILOT_VERSION packet''' + capability_msg = vehicle.message_factory.command_long_encode(0, 0, mavutil.mavlink.MAV_CMD_REQUEST_AUTOPILOT_CAPABILITIES, 0, 1, 0, 0, 0, 0, 0, 0) + vehicle.send_mavlink(capability_msg) + + def play_tune(self, tune): + '''Play a tune on the vehicle''' + msg = self.message_factory.play_tune_encode(0, 0, tune) + self.send_mavlink(msg) + + def wait_ready(self, *types, **kwargs): + """ + Waits for specified attributes to be populated from the vehicle (values are initially ``None``). + + This is typically called "behind the scenes" to ensure that :py:func:`connect` does not return until + attributes have populated (via the ``wait_ready`` parameter). You can also use it after connecting to + wait on a specific value(s). + + There are two ways to call the method: + + .. code-block:: python + + #Wait on default attributes to populate + vehicle.wait_ready(True) + + #Wait on specified attributes (or array of attributes) to populate + vehicle.wait_ready('mode','airspeed') + + Using the ``wait_ready(True)`` waits on :py:attr:`parameters`, :py:attr:`gps_0`, + :py:attr:`armed`, :py:attr:`mode`, and :py:attr:`attitude`. In practice this usually + means that all supported attributes will be populated. + + By default, the method will timeout after 30 seconds and raise an exception if the + attributes were not populated. + + :param types: ``True`` to wait on the default set of attributes, or a + comma-separated list of the specific attributes to wait on. + :param int timeout: Timeout in seconds after which the method will raise an exception + (the default) or return ``False``. The default timeout is 30 seconds. + :param Boolean raise_exception: If ``True`` the method will raise an exception on timeout, + otherwise the method will return ``False``. The default is ``True`` (raise exception). + """ + timeout = kwargs.get('timeout', 30) + raise_exception = kwargs.get('raise_exception', True) + + # Vehicle defaults for wait_ready(True) or wait_ready() + if list(types) == [True] or list(types) == []: + types = self._default_ready_attrs + + if not all(isinstance(item, basestring) for item in types): + raise ValueError('wait_ready expects one or more string arguments.') + + # Wait for these attributes to have been set. + await_attributes = set(types) + start = monotonic.monotonic() + still_waiting_last_message_sent = start + still_waiting_callback = kwargs.get('still_waiting_callback') + still_waiting_message_interval = kwargs.get('still_waiting_interval', 1) + + while not await_attributes.issubset(self._ready_attrs): + time.sleep(0.1) + now = monotonic.monotonic() + if now - start > timeout: + if raise_exception: + raise TimeoutError('wait_ready experienced a timeout after %s seconds.' % + timeout) + else: + return False + if (still_waiting_callback and + now - still_waiting_last_message_sent > still_waiting_message_interval): + still_waiting_last_message_sent = now + if still_waiting_callback: + still_waiting_callback(await_attributes - self._ready_attrs) + + return True + + def reboot(self): + """Requests an autopilot reboot by sending a ``MAV_CMD_PREFLIGHT_REBOOT_SHUTDOWN`` command.""" + + reboot_msg = self.message_factory.command_long_encode( + 0, 0, # target_system, target_component + mavutil.mavlink.MAV_CMD_PREFLIGHT_REBOOT_SHUTDOWN, # command + 0, # confirmation + 1, # param 1, autopilot (reboot) + 0, # param 2, onboard computer (do nothing) + 0, # param 3, camera (do nothing) + 0, # param 4, mount (do nothing) + 0, 0, 0) # param 5 ~ 7 not used + + self.send_mavlink(reboot_msg) + + def send_calibrate_gyro(self): + """Request gyroscope calibration.""" + + calibration_command = self.message_factory.command_long_encode( + self._handler.target_system, 0, # target_system, target_component + mavutil.mavlink.MAV_CMD_PREFLIGHT_CALIBRATION, # command + 0, # confirmation + 1, # param 1, 1: gyro calibration, 3: gyro temperature calibration + 0, # param 2, 1: magnetometer calibration + 0, # param 3, 1: ground pressure calibration + 0, # param 4, 1: radio RC calibration, 2: RC trim calibration + 0, # param 5, 1: accelerometer calibration, 2: board level calibration, 3: accelerometer temperature calibration, 4: simple accelerometer calibration + 0, # param 6, 2: airspeed calibration + 0, # param 7, 1: ESC calibration, 3: barometer temperature calibration + ) + self.send_mavlink(calibration_command) + + def send_calibrate_magnetometer(self): + """Request magnetometer calibration.""" + + # ArduPilot requires the MAV_CMD_DO_START_MAG_CAL command, only present in the ardupilotmega.xml definition + if self._autopilot_type == mavutil.mavlink.MAV_AUTOPILOT_ARDUPILOTMEGA: + calibration_command = self.message_factory.command_long_encode( + self._handler.target_system, 0, # target_system, target_component + mavutil.mavlink.MAV_CMD_DO_START_MAG_CAL, # command + 0, # confirmation + 0, # param 1, uint8_t bitmask of magnetometers (0 means all). + 1, # param 2, Automatically retry on failure (0=no retry, 1=retry). + 1, # param 3, Save without user input (0=require input, 1=autosave). + 0, # param 4, Delay (seconds). + 0, # param 5, Autoreboot (0=user reboot, 1=autoreboot). + 0, # param 6, Empty. + 0, # param 7, Empty. + ) + else: + calibration_command = self.message_factory.command_long_encode( + self._handler.target_system, 0, # target_system, target_component + mavutil.mavlink.MAV_CMD_PREFLIGHT_CALIBRATION, # command + 0, # confirmation + 0, # param 1, 1: gyro calibration, 3: gyro temperature calibration + 1, # param 2, 1: magnetometer calibration + 0, # param 3, 1: ground pressure calibration + 0, # param 4, 1: radio RC calibration, 2: RC trim calibration + 0, # param 5, 1: accelerometer calibration, 2: board level calibration, 3: accelerometer temperature calibration, 4: simple accelerometer calibration + 0, # param 6, 2: airspeed calibration + 0, # param 7, 1: ESC calibration, 3: barometer temperature calibration + ) + + self.send_mavlink(calibration_command) + + def send_calibrate_accelerometer(self, simple=False): + """Request accelerometer calibration. + + :param simple: if True, perform simple accelerometer calibration + """ + + calibration_command = self.message_factory.command_long_encode( + self._handler.target_system, 0, # target_system, target_component + mavutil.mavlink.MAV_CMD_PREFLIGHT_CALIBRATION, # command + 0, # confirmation + 0, # param 1, 1: gyro calibration, 3: gyro temperature calibration + 0, # param 2, 1: magnetometer calibration + 0, # param 3, 1: ground pressure calibration + 0, # param 4, 1: radio RC calibration, 2: RC trim calibration + 4 if simple else 1, # param 5, 1: accelerometer calibration, 2: board level calibration, 3: accelerometer temperature calibration, 4: simple accelerometer calibration + 0, # param 6, 2: airspeed calibration + 0, # param 7, 1: ESC calibration, 3: barometer temperature calibration + ) + self.send_mavlink(calibration_command) + + def send_calibrate_vehicle_level(self): + """Request vehicle level (accelerometer trim) calibration.""" + + calibration_command = self.message_factory.command_long_encode( + self._handler.target_system, 0, # target_system, target_component + mavutil.mavlink.MAV_CMD_PREFLIGHT_CALIBRATION, # command + 0, # confirmation + 0, # param 1, 1: gyro calibration, 3: gyro temperature calibration + 0, # param 2, 1: magnetometer calibration + 0, # param 3, 1: ground pressure calibration + 0, # param 4, 1: radio RC calibration, 2: RC trim calibration + 2, # param 5, 1: accelerometer calibration, 2: board level calibration, 3: accelerometer temperature calibration, 4: simple accelerometer calibration + 0, # param 6, 2: airspeed calibration + 0, # param 7, 1: ESC calibration, 3: barometer temperature calibration + ) + self.send_mavlink(calibration_command) + + def send_calibrate_barometer(self): + """Request barometer calibration.""" + + calibration_command = self.message_factory.command_long_encode( + self._handler.target_system, 0, # target_system, target_component + mavutil.mavlink.MAV_CMD_PREFLIGHT_CALIBRATION, # command + 0, # confirmation + 0, # param 1, 1: gyro calibration, 3: gyro temperature calibration + 0, # param 2, 1: magnetometer calibration + 1, # param 3, 1: ground pressure calibration + 0, # param 4, 1: radio RC calibration, 2: RC trim calibration + 0, # param 5, 1: accelerometer calibration, 2: board level calibration, 3: accelerometer temperature calibration, 4: simple accelerometer calibration + 0, # param 6, 2: airspeed calibration + 0, # param 7, 1: ESC calibration, 3: barometer temperature calibration + ) + self.send_mavlink(calibration_command) + + +class Gimbal(object): + """ + Gimbal status and control. + + An object of this type is returned by :py:attr:`Vehicle.gimbal`. The + gimbal orientation can be obtained from its :py:attr:`roll`, :py:attr:`pitch` and + :py:attr:`yaw` attributes. + + The gimbal orientation can be set explicitly using :py:func:`rotate` + or you can set the gimbal (and vehicle) to track a specific "region of interest" using + :py:func:`target_location`. + + .. note:: + + * The orientation attributes are created with values of ``None``. If a gimbal is present, + the attributes are populated shortly after initialisation by messages from the autopilot. + * The attribute values reflect the last gimbal setting-values rather than actual measured values. + This means that the values won't change if you manually move the gimbal, and that the value + will change when you set it, even if the specified orientation is not supported. + * A gimbal may not support all axes of rotation. For example, the Solo gimbal will set pitch + values from 0 to -90 (straight ahead to straight down), it will rotate the vehicle to follow specified + yaw values, and will ignore roll commands (not supported). + """ + + def __init__(self, vehicle): + super(Gimbal, self).__init__() + + self._pitch = None + self._roll = None + self._yaw = None + self._vehicle = vehicle + + @vehicle.on_message('MOUNT_STATUS') + def listener(vehicle, name, m): + self._pitch = m.pointing_a / 100.0 + self._roll = m.pointing_b / 100.0 + self._yaw = m.pointing_c / 100.0 + vehicle.notify_attribute_listeners('gimbal', vehicle.gimbal) + + @vehicle.on_message('MOUNT_ORIENTATION') + def listener(vehicle, name, m): + self._pitch = m.pitch + self._roll = m.roll + self._yaw = m.yaw + vehicle.notify_attribute_listeners('gimbal', vehicle.gimbal) + + @property + def pitch(self): + """ + Gimbal pitch in degrees relative to the vehicle (see diagram for :ref:`attitude `). + A value of 0 represents a camera pointed straight ahead relative to the front of the vehicle, + while -90 points the camera straight down. + + .. note:: + + This is the last pitch value sent to the gimbal (not the actual/measured pitch). + """ + return self._pitch + + @property + def roll(self): + """ + Gimbal roll in degrees relative to the vehicle (see diagram for :ref:`attitude `). + + .. note:: + + This is the last roll value sent to the gimbal (not the actual/measured roll). + """ + return self._roll + + @property + def yaw(self): + """ + Gimbal yaw in degrees relative to *global frame* (0 is North, 90 is West, 180 is South etc). + + .. note:: + + This is the last yaw value sent to the gimbal (not the actual/measured yaw). + """ + return self._yaw + + def rotate(self, pitch, roll, yaw): + """ + Rotate the gimbal to a specific vector. + + .. code-block:: python + + #Point the gimbal straight down + vehicle.gimbal.rotate(-90, 0, 0) + + :param pitch: Gimbal pitch in degrees relative to the vehicle (see diagram for :ref:`attitude `). + A value of 0 represents a camera pointed straight ahead relative to the front of the vehicle, + while -90 points the camera straight down. + :param roll: Gimbal roll in degrees relative to the vehicle (see diagram for :ref:`attitude `). + :param yaw: Gimbal yaw in degrees relative to *global frame* (0 is North, 90 is West, 180 is South etc.) + """ + msg = self._vehicle.message_factory.mount_configure_encode( + 0, 1, # target system, target component + mavutil.mavlink.MAV_MOUNT_MODE_MAVLINK_TARGETING, #mount_mode + 1, # stabilize roll + 1, # stabilize pitch + 1, # stabilize yaw + ) + self._vehicle.send_mavlink(msg) + msg = self._vehicle.message_factory.mount_control_encode( + 0, 1, # target system, target component + pitch * 100, # pitch is in centidegrees + roll * 100, # roll + yaw * 100, # yaw is in centidegrees + 0 # save position + ) + self._vehicle.send_mavlink(msg) + + def target_location(self, roi): + """ + Point the gimbal at a specific region of interest (ROI). + + .. code-block:: python + + #Set the camera to track the current home location. + vehicle.gimbal.target_location(vehicle.home_location) + + The target position must be defined in a :py:class:`LocationGlobalRelative` or :py:class:`LocationGlobal`. + + This function can be called in AUTO or GUIDED mode. + + In order to clear an ROI you can send a location with all zeros (e.g. ``LocationGlobalRelative(0,0,0)``). + + :param roi: Target location in global relative frame. + """ + # set gimbal to targeting mode + msg = self._vehicle.message_factory.mount_configure_encode( + 0, 1, # target system, target component + mavutil.mavlink.MAV_MOUNT_MODE_GPS_POINT, # mount_mode + 1, # stabilize roll + 1, # stabilize pitch + 1, # stabilize yaw + ) + self._vehicle.send_mavlink(msg) + + # Get altitude relative to home irrespective of Location object passed in. + if isinstance(roi, LocationGlobalRelative): + alt = roi.alt + elif isinstance(roi, LocationGlobal): + if not self.home_location: + self.commands.download() + self.commands.wait_ready() + alt = roi.alt - self.home_location.alt + else: + raise ValueError('Expecting location to be LocationGlobal or LocationGlobalRelative.') + + # set the ROI + msg = self._vehicle.message_factory.command_long_encode( + 0, 1, # target system, target component + mavutil.mavlink.MAV_CMD_DO_SET_ROI, # command + 0, # confirmation + 0, 0, 0, 0, # params 1-4 + roi.lat, + roi.lon, + alt + ) + self._vehicle.send_mavlink(msg) + + def release(self): + """ + Release control of the gimbal to the user (RC Control). + + This should be called once you've finished controlling the mount with either :py:func:`rotate` + or :py:func:`target_location`. Control will automatically be released if you change vehicle mode. + """ + msg = self._vehicle.message_factory.mount_configure_encode( + 0, 1, # target system, target component + mavutil.mavlink.MAV_MOUNT_MODE_RC_TARGETING, # mount_mode + 1, # stabilize roll + 1, # stabilize pitch + 1, # stabilize yaw + ) + self._vehicle.send_mavlink(msg) + + def __str__(self): + return "Gimbal: pitch={0}, roll={1}, yaw={2}".format(self.pitch, self.roll, self.yaw) + + +class Parameters(MutableMapping, HasObservers): + """ + This object is used to get and set the values of named parameters for a vehicle. See the following links for information about + the supported parameters for each platform: `Copter Parameters `_, + `Plane Parameters `_, `Rover Parameters `_. + + The code fragment below shows how to get and set the value of a parameter. + + .. code:: python + + # Print the value of the THR_MIN parameter. + print "Param: %s" % vehicle.parameters['THR_MIN'] + + # Change the parameter value to something different. + vehicle.parameters['THR_MIN']=100 + + It is also possible to observe parameters and to iterate the :py:attr:`Vehicle.parameters`. + + For more information see :ref:`the guide `. + """ + + def __init__(self, vehicle): + super(Parameters, self).__init__() + self._logger = logging.getLogger(__name__) + self._vehicle = vehicle + + def __getitem__(self, name): + name = name.upper() + self.wait_ready() + return self._vehicle._params_map[name] + + def __setitem__(self, name, value): + name = name.upper() + self.wait_ready() + self.set(name, value) + + def __delitem__(self, name): + raise APIException('Cannot delete value from parameters list.') + + def __len__(self): + return len(self._vehicle._params_map) + + def __iter__(self): + return self._vehicle._params_map.__iter__() + + def get(self, name, wait_ready=True): + name = name.upper() + if wait_ready: + self.wait_ready() + return self._vehicle._params_map.get(name, None) + + def set(self, name, value, retries=3, wait_ready=False): + if wait_ready: + self.wait_ready() + + # TODO dumbly reimplement this using timeout loops + # because we should actually be awaiting an ACK of PARAM_VALUE + # changed, but we don't have a proper ack structure, we'll + # instead just wait until the value itself was changed + + name = name.upper() + # convert to single precision floating point number (the type used by low level mavlink messages) + value = float(struct.unpack('f', struct.pack('f', value))[0]) + remaining = retries + while True: + self._vehicle._master.param_set_send(name, value) + tstart = monotonic.monotonic() + if remaining == 0: + break + remaining -= 1 + while monotonic.monotonic() - tstart < 1: + if name in self._vehicle._params_map and self._vehicle._params_map[name] == value: + return True time.sleep(0.1) - return self.api + if retries > 0: + self._logger.error("timeout setting parameter %s to %f" % (name, value)) + return False + + def wait_ready(self, **kwargs): + """ + Block the calling thread until parameters have been downloaded + """ + self._vehicle.wait_ready('parameters', **kwargs) + + def add_attribute_listener(self, attr_name, *args, **kwargs): + """ + Add a listener callback on a particular parameter. + + The callback can be removed using :py:func:`remove_attribute_listener`. + + .. note:: + + The :py:func:`on_attribute` decorator performs the same operation as this method, but with + a more elegant syntax. Use ``add_attribute_listener`` only if you will need to remove + the observer. + + The callback function is invoked only when the parameter changes. + + The callback arguments are: + + * ``self`` - the associated :py:class:`Parameters`. + * ``attr_name`` - the parameter name. This can be used to infer which parameter has triggered + if the same callback is used for watching multiple parameters. + * ``msg`` - the new parameter value (so you don't need to re-query the vehicle object). + + The example below shows how to get callbacks for the ``THR_MIN`` parameter: + + .. code:: python + + #Callback function for the THR_MIN parameter + def thr_min_callback(self, attr_name, value): + print " PARAMETER CALLBACK: %s changed to: %s" % (attr_name, value) + + #Add observer for the vehicle's THR_MIN parameter + vehicle.parameters.add_attribute_listener('THR_MIN', thr_min_callback) + + See :ref:`vehicle_state_observing_parameters` for more information. + + :param String attr_name: The name of the parameter to watch (or '*' to watch all parameters). + :param args: The callback to invoke when a change in the parameter is detected. + + """ + attr_name = attr_name.upper() + return super(Parameters, self).add_attribute_listener(attr_name, *args, **kwargs) + + def remove_attribute_listener(self, attr_name, *args, **kwargs): + """ + Remove a paremeter listener that was previously added using :py:func:`add_attribute_listener`. + + For example to remove the ``thr_min_callback()`` callback function: + + .. code:: python + + vehicle.parameters.remove_attribute_listener('thr_min', thr_min_callback) + + See :ref:`vehicle_state_observing_parameters` for more information. + + :param String attr_name: The parameter name that is to have an observer removed (or '*' to remove an 'all attribute' observer). + :param args: The callback function to remove. + + """ + attr_name = attr_name.upper() + return super(Parameters, self).remove_attribute_listener(attr_name, *args, **kwargs) + + def notify_attribute_listeners(self, attr_name, *args, **kwargs): + attr_name = attr_name.upper() + return super(Parameters, self).notify_attribute_listeners(attr_name, *args, **kwargs) + + def on_attribute(self, attr_name, *args, **kwargs): + """ + Decorator for parameter listeners. + + .. note:: + + There is no way to remove a listener added with this decorator. Use + :py:func:`add_attribute_listener` if you need to be able to remove + the :py:func:`listener `. + + The callback function is invoked only when the parameter changes. + + The callback arguments are: + + * ``self`` - the associated :py:class:`Parameters`. + * ``attr_name`` - the parameter name. This can be used to infer which parameter has triggered + if the same callback is used for watching multiple parameters. + * ``msg`` - the new parameter value (so you don't need to re-query the vehicle object). + + The code fragment below shows how to get callbacks for the ``THR_MIN`` parameter: + + .. code:: python + + @vehicle.parameters.on_attribute('THR_MIN') + def decorated_thr_min_callback(self, attr_name, value): + print " PARAMETER CALLBACK: %s changed to: %s" % (attr_name, value) + + See :ref:`vehicle_state_observing_parameters` for more information. + + :param String attr_name: The name of the parameter to watch (or '*' to watch all parameters). + :param args: The callback to invoke when a change in the parameter is detected. + + """ + attr_name = attr_name.upper() + return super(Parameters, self).on_attribute(attr_name, *args, **kwargs) + + +class Command(mavutil.mavlink.MAVLink_mission_item_message): + """ + A waypoint object. + + This object encodes a single mission item command. The set of commands that are supported + by ArduPilot in Copter, Plane and Rover (along with their parameters) are listed in the wiki article + `MAVLink Mission Command Messages (MAV_CMD) `_. + + For example, to create a `NAV_WAYPOINT `_ command: + + .. code:: python + + cmd = Command(0,0,0, mavutil.mavlink.MAV_FRAME_GLOBAL_RELATIVE_ALT, + mavutil.mavlink.MAV_CMD_NAV_WAYPOINT, 0, 0, 0, 0, 0, 0,-34.364114, 149.166022, 30) + + :param target_system: This can be set to any value + (DroneKit changes the value to the MAVLink ID of the connected vehicle before the command is sent). + :param target_component: The component id if the message is intended for a particular component within the target system + (for example, the camera). Set to zero (broadcast) in most cases. + :param seq: The sequence number within the mission (the autopilot will reject messages sent out of sequence). + This should be set to zero as the API will automatically set the correct value when uploading a mission. + :param frame: The frame of reference used for the location parameters (x, y, z). In most cases this will be + ``mavutil.mavlink.MAV_FRAME_GLOBAL_RELATIVE_ALT``, which uses the WGS84 global coordinate system for latitude and longitude, but sets altitude + as relative to the home position in metres (home altitude = 0). For more information `see the wiki here + `_. + :param command: The specific mission command (e.g. ``mavutil.mavlink.MAV_CMD_NAV_WAYPOINT``). The supported commands (and command parameters + are listed `on the wiki `_. + :param current: Set to zero (not supported). + :param autocontinue: Set to zero (not supported). + :param param1: Command specific parameter (depends on specific `Mission Command (MAV_CMD) `_). + :param param2: Command specific parameter. + :param param3: Command specific parameter. + :param param4: Command specific parameter. + :param x: (param5) Command specific parameter used for latitude (if relevant to command). + :param y: (param6) Command specific parameter used for longitude (if relevant to command). + :param z: (param7) Command specific parameter used for altitude (if relevant). The reference frame for altitude depends on the ``frame``. + + """ + pass + + +class CommandSequence(object): + """ + A sequence of vehicle waypoints (a "mission"). + + Operations include 'array style' indexed access to the various contained waypoints. + + The current commands/mission for a vehicle are accessed using the :py:attr:`Vehicle.commands` attribute. + Waypoints are not downloaded from vehicle until :py:func:`download()` is called. The download is asynchronous; + use :py:func:`wait_ready()` to block your thread until the download is complete. + The code to download the commands from a vehicle is shown below: + + .. code-block:: python + :emphasize-lines: 5-10 + + #Connect to a vehicle object (for example, on com14) + vehicle = connect('com14', wait_ready=True) + + # Download the vehicle waypoints (commands). Wait until download is complete. + cmds = vehicle.commands + cmds.download() + cmds.wait_ready() + + The set of commands can be changed and uploaded to the client. The changes are not guaranteed to be complete until + :py:func:`upload() ` is called. + + .. code:: python + + cmds = vehicle.commands + cmds.clear() + lat = -34.364114, + lon = 149.166022 + altitude = 30.0 + cmd = Command(0,0,0, mavutil.mavlink.MAV_FRAME_GLOBAL_RELATIVE_ALT, mavutil.mavlink.MAV_CMD_NAV_WAYPOINT, + 0, 0, 0, 0, 0, 0, + lat, lon, altitude) + cmds.add(cmd) + cmds.upload() + + """ + + def __init__(self, vehicle): + self._vehicle = vehicle + + def download(self): + ''' + Download all waypoints from the vehicle. + The download is asynchronous. Use :py:func:`wait_ready()` to block your thread until the download is complete. + ''' + self.wait_ready() + self._vehicle._ready_attrs.remove('commands') + self._vehicle._wp_loaded = False + self._vehicle._master.waypoint_request_list_send() + # BIG FIXME - wait for full wpt download before allowing any of the accessors to work + + def wait_ready(self, **kwargs): + """ + Block the calling thread until waypoints have been downloaded. + + This can be called after :py:func:`download()` to block the thread until the asynchronous download is complete. + """ + return self._vehicle.wait_ready('commands', **kwargs) + + def clear(self): + ''' + Clear the command list. + + This command will be sent to the vehicle only after you call :py:func:`upload() `. + ''' + + # Add home point again. + self.wait_ready() + home = None + try: + home = self._vehicle._wploader.wp(0) + except: + pass + self._vehicle._wploader.clear() + if home: + self._vehicle._wploader.add(home, comment='Added by DroneKit') + self._vehicle._wpts_dirty = True + + def add(self, cmd): + ''' + Add a new command (waypoint) at the end of the command list. + + .. note:: + + Commands are sent to the vehicle only after you call ::py:func:`upload() `. + + :param Command cmd: The command to be added. + ''' + self.wait_ready() + self._vehicle._handler.fix_targets(cmd) + self._vehicle._wploader.add(cmd, comment='Added by DroneKit') + self._vehicle._wpts_dirty = True + + def upload(self, timeout=None): + """ + Call ``upload()`` after :py:func:`adding ` or :py:func:`clearing ` mission commands. + + After the return from ``upload()`` any writes are guaranteed to have completed (or thrown an + exception) and future reads will see their effects. + + :param int timeout: The timeout for uploading the mission. No timeout if not provided or set to None. + """ + if self._vehicle._wpts_dirty: + self._vehicle._master.waypoint_clear_all_send() + start_time = time.time() + if self._vehicle._wploader.count() > 0: + self._vehicle._wp_uploaded = [False] * self._vehicle._wploader.count() + self._vehicle._master.waypoint_count_send(self._vehicle._wploader.count()) + while False in self._vehicle._wp_uploaded: + if timeout and time.time() - start_time > timeout: + raise TimeoutError + time.sleep(0.1) + self._vehicle._wp_uploaded = None + self._vehicle._wpts_dirty = False + + @property + def count(self): + ''' + Return number of waypoints. + + :return: The number of waypoints in the sequence. + ''' + return max(self._vehicle._wploader.count() - 1, 0) + + @property + def next(self): + """ + Get the currently active waypoint number. + """ + return self._vehicle._current_waypoint + + @next.setter + def next(self, index): + """ + Set a new ``next`` waypoint for the vehicle. + """ + self._vehicle._master.waypoint_set_current_send(index) + + def __len__(self): + ''' + Return number of waypoints. + + :return: The number of waypoints in the sequence. + ''' + return max(self._vehicle._wploader.count() - 1, 0) + + def __getitem__(self, index): + if isinstance(index, slice): + return [self[ii] for ii in range(*index.indices(len(self)))] + elif isinstance(index, int): + item = self._vehicle._wploader.wp(index + 1) + if not item: + raise IndexError('Index %s out of range.' % index) + return item + else: + raise TypeError('Invalid argument type.') + + def __setitem__(self, index, value): + self._vehicle._wploader.set(value, index + 1) + self._vehicle._wpts_dirty = True + + +def default_still_waiting_callback(atts): + logging.getLogger(__name__).debug("Still waiting for data from vehicle: %s" % ','.join(atts)) + + +def connect(ip, + _initialize=True, + wait_ready=None, + timeout=30, + still_waiting_callback=default_still_waiting_callback, + still_waiting_interval=1, + status_printer=None, + vehicle_class=None, + rate=4, + baud=115200, + heartbeat_timeout=30, + source_system=255, + source_component=0, + use_native=False): + """ + Returns a :py:class:`Vehicle` object connected to the address specified by string parameter ``ip``. + Connection string parameters (``ip``) for different targets are listed in the :ref:`getting started guide `. + + The method is usually called with ``wait_ready=True`` to ensure that vehicle parameters and (most) attributes are + available when ``connect()`` returns. + + .. code:: python + + from dronekit import connect + + # Connect to the Vehicle using "connection string" (in this case an address on network) + vehicle = connect('127.0.0.1:14550', wait_ready=True) + + :param String ip: :ref:`Connection string ` for target address - e.g. 127.0.0.1:14550. + + :param Bool/Array wait_ready: If ``True`` wait until all default attributes have downloaded before + the method returns (default is ``None``). + The default attributes to wait on are: :py:attr:`parameters`, :py:attr:`gps_0`, + :py:attr:`armed`, :py:attr:`mode`, and :py:attr:`attitude`. + + You can also specify a named set of parameters to wait on (e.g. ``wait_ready=['system_status','mode']``). + + For more information see :py:func:`Vehicle.wait_ready `. + + :param status_printer: (deprecated) method of signature ``def status_printer(txt)`` that prints + STATUS_TEXT messages from the Vehicle and other diagnostic information. + By default the status information is handled by the ``autopilot`` logger. + :param Vehicle vehicle_class: The class that will be instantiated by the ``connect()`` method. + This can be any sub-class of ``Vehicle`` (and defaults to ``Vehicle``). + :param int rate: Data stream refresh rate. The default is 4Hz (4 updates per second). + :param int baud: The baud rate for the connection. The default is 115200. + :param int heartbeat_timeout: Connection timeout value in seconds (default is 30s). + If a heartbeat is not detected within this time an exception will be raised. + :param int source_system: The MAVLink ID of the :py:class:`Vehicle` object returned by this method (by default 255). + :param int source_component: The MAVLink Component ID fo the :py:class:`Vehicle` object returned by this method (by default 0). + :param bool use_native: Use precompiled MAVLink parser. + + .. note:: + + The returned :py:class:`Vehicle` object acts as a ground control station from the + perspective of the connected "real" vehicle. It will process/receive messages from the real vehicle + if they are addressed to this ``source_system`` id. Messages sent to the real vehicle are + automatically updated to use the vehicle's ``target_system`` id. + + It is *good practice* to assign a unique id for every system on the MAVLink network. + It is possible to configure the autopilot to only respond to guided-mode commands from a specified GCS ID. + + The ``status_printer`` argument is deprecated. To redirect the logging from the library and from the + autopilot, configure the ``dronekit`` and ``autopilot`` loggers using the Python ``logging`` module. + + + :returns: A connected vehicle of the type defined in ``vehicle_class`` (a superclass of :py:class:`Vehicle`). + """ + + from dronekit.mavlink import MAVConnection + + if not vehicle_class: + vehicle_class = Vehicle + + handler = MAVConnection(ip, baud=baud, source_system=source_system, source_component=source_component, use_native=use_native) + vehicle = vehicle_class(handler) + + if status_printer: + vehicle._autopilot_logger.addHandler(ErrprinterHandler(status_printer)) + + if _initialize: + vehicle.initialize(rate=rate, heartbeat_timeout=heartbeat_timeout) + + if wait_ready: + if wait_ready is True: + vehicle.wait_ready(still_waiting_interval=still_waiting_interval, + still_waiting_callback=still_waiting_callback, + timeout=timeout) + else: + vehicle.wait_ready(*wait_ready) -def connect(ip, await_params=False, status_printer=errprinter): - import dronekit.module.api as api - state = MPFakeState(mavutil.mavlink_connection(ip)) - state.status_printer = status_printer - # api.init(state) - return state.prepare(await_params=await_params).get_vehicles()[0] + return vehicle diff --git a/dronekit/__main__.py b/dronekit/__main__.py deleted file mode 100644 index 73154941f..000000000 --- a/dronekit/__main__.py +++ /dev/null @@ -1,106 +0,0 @@ -from dronekit import local_connect - -def demo(local_connect): - # This example shows how to use DroneKit-Python to get and set vehicle state, parameter and channel-override information. - # It also demonstrates how to observe vehicle attribute (state) changes. - # - # Usage: - # * mavproxy.py - # * module load api - # * api start vehicle-state.py - # - from dronekit.lib import VehicleMode - from pymavlink import mavutil - import time - - # First get an instance of the API endpoint - api = local_connect() - # Get the connected vehicle (currently only one vehicle can be returned). - v = api.get_vehicles()[0] - - # Get all vehicle attributes (state) - print "\nGet all vehicle attribute values:" - print " Location: %s" % v.location - print " Attitude: %s" % v.attitude - print " Velocity: %s" % v.velocity - print " GPS: %s" % v.gps_0 - print " Groundspeed: %s" % v.groundspeed - print " Airspeed: %s" % v.airspeed - print " Mount status: %s" % v.mount_status - print " Battery: %s" % v.battery - print " Mode: %s" % v.mode.name # settable - print " Armed: %s" % v.armed # settable - - # Set vehicle mode and armed attributes (the only settable attributes) - print "Set Vehicle.mode=GUIDED (currently: %s)" % v.mode.name - v.mode = VehicleMode("GUIDED") - v.flush() # Flush to guarantee that previous writes to the vehicle have taken place - while not v.mode.name=='GUIDED' and not api.exit: #Wait until mode has changed - print " Waiting for mode change ..." - time.sleep(1) - - print "Set Vehicle.armed=True (currently: %s)" % v.armed - v.armed = True - v.flush() - while not v.armed and not api.exit: - print " Waiting for arming..." - time.sleep(1) - - - # Show how to add and remove and attribute observer callbacks (using mode as example) - def mode_callback(attribute): - print " CALLBACK: Mode changed to: ", v.mode.name - - print "\nAdd mode attribute observer for Vehicle.mode" - v.add_attribute_observer('mode', mode_callback) - - print " Set mode=STABILIZE (currently: %s)" % v.mode.name - v.mode = VehicleMode("STABILIZE") - v.flush() - - print " Wait 2s so callback invoked before observer removed" - time.sleep(2) - - # Remove observer - specifying the attribute and previously registered callback function - v.remove_attribute_observer('mode', mode_callback) - - - # # Get Vehicle Home location ((0 index in Vehicle.commands) - # print "\nGet home location" - # cmds = v.commands - # cmds.download() - # cmds.wait_valid() - # print " Home WP: %s" % cmds[0] - - - # Get/Set Vehicle Parameters - print "\nRead vehicle param 'THR_MIN': %s" % v.parameters['THR_MIN'] - print "Write vehicle param 'THR_MIN' : 10" - v.parameters['THR_MIN']=10 - v.flush() - print "Read new value of param 'THR_MIN': %s" % v.parameters['THR_MIN'] - - - # # Overriding an RC channel - # # NOTE: CHANNEL OVERRIDES may be useful for simulating user input and when implementing certain types of joystick control. - # #DO NOT use unless there is no other choice (there almost always is!) - # print "\nOverriding RC channels for roll and yaw" - # v.channel_override = { "1" : 900, "4" : 1000 } - # v.flush() - # print " Current overrides are:", v.channel_override - # print " Channel default values:", v.channel_readback # All channel values before override - - # # Cancel override by setting channels to 0 - # print " Cancelling override" - # v.channel_override = { "1" : 0, "4" : 0 } - # v.flush() - - - ## Reset variables to sensible values. - print "\nReset vehicle atributes/parameters and exit" - v.mode = VehicleMode("STABILIZE") - v.armed = False - v.parameters['THR_MIN']=130 - v.flush() - -demo(local_connect) diff --git a/dronekit/lib.py b/dronekit/lib.py new file mode 100644 index 000000000..d2419fcdb --- /dev/null +++ b/dronekit/lib.py @@ -0,0 +1,2 @@ +# Backwards compatibility +from dronekit import * diff --git a/dronekit/lib/CloudClient.py b/dronekit/lib/CloudClient.py deleted file mode 100644 index d04a79af8..000000000 --- a/dronekit/lib/CloudClient.py +++ /dev/null @@ -1,49 +0,0 @@ -import requests - -class CloudError(Exception): - def __init__(self, type, message, response): - self.type = type - self.message = message - self.response = response - - def __str__(self): - return "%s [%s] (%s)" % (self.type, self.response.url, self.message) - - def __repr__(self): - return "%s(type=%s)" % (self.__class__.__name__, self.type) - -class CloudClient(object): - BASE_URL = 'http://api.droneshare.com/api/v1/' - REST_CALLS = { - 'staticMap': 'staticMap', - 'analysis': 'analysis.json', - 'geo': 'messages.geo.json', - 'messages': 'messages.json', - 'parameters': 'parameters.json' - } - - def __init__(self, api_key): - self.api_key = api_key - self.headers = {'Authorization': "DroneApi apikey=\"%s\"" % self.apikey} - - def __getattr__(self, name): - def method(*args): - find_action = name.split('_') - find_args = args - action_url = find_action[0] - if (len(find_action) > 1): - if (find_action[1] in self.REST_CALLS): - if (len(args) > 0): - action_url += "/%s/%s" % (str(args[0]), self.REST_CALLS[find_action[1]]) - else: - action_url += "/%s" % find_action[1] - else: - action_url += "/%s/%s" % (str(args[0]), find_action[1]) - return self._request(action_url, args[1:]) - return method - - def _request(self, url, data): - self.response = requests.get("%s%s" % (self.BASE_URL, url), headers=self.headers) - if self.response.status_code == 404: - raise CloudError(self.response.status_code, 'Unkown Endpoint', self.response) - return self.response diff --git a/dronekit/lib/__init__.py b/dronekit/lib/__init__.py deleted file mode 100644 index 61fbf7f81..000000000 --- a/dronekit/lib/__init__.py +++ /dev/null @@ -1,1005 +0,0 @@ -# DroneAPI module - -""" -This is the API Reference for the DroneKit-Python API. - -The main API is the :py:class:`Vehicle ` class. -The code snippet below shows how to obtain an instance of the (first) connected vehicle: - -.. code:: python - - # Get a local APIConnection to the autopilot (from companion computer or GCS). - api_connection = local_connect() - # Get the first connected vehicle from the APIConnection - vehicle = api.get_vehicles()[0] - -:py:class:`Vehicle ` provides access to vehicle *state* through python attributes -(e.g. :py:attr:`Vehicle.location `) -and to settings/parameters though the :py:attr:`Vehicle.parameters ` attribute. -Asynchronous notification on vehicle attribute changes is available by registering observers. - -:py:class:`Vehicle ` provides two main ways to control vehicle movement and other operations: - -* Missions are downloaded and uploaded through the :py:attr:`Vehicle.commands ` attribute - (see :py:class:`CommandSequence ` for more information). -* Direct control of movement outside of missions is also supported. To set a target position you can use - :py:func:`CommandSequence.goto `. - Control over speed, direction, altitude, camera trigger and any other aspect of the vehicle is supported using custom MAVLink messages - (:py:func:`Vehicle.send_mavlink `, :py:func:`Vehicle.message_factory `). - -A number of other useful classes and methods are listed below. - ----- - -.. todo:: Update this when have confirmed how to register for parameter notifications. -""" - -import threading -from pymavlink import mavutil - -local_path = '' - - - -def web_connect(authinfo): - """ - .. warning:: This API is not fully implemented and should not be used. - - Connect to the central dronehub server. - - :param AuthInfo authinfo: A container for authentication information (username, password, challenge info, etc.) - """ - return APIConnection() - -def local_connect(): - """ - Connect to the API provider for the local vehicle or ground control station. - - :return: The API provider. - :rtype: APIConnection - """ - return APIConnection() - -class APIException(Exception): - """ - Base class for DroneKit related exceptions. - - :param String msg: Message string describing the exception - """ - - def __init__(self, msg): - self.msg = msg - -class AuthInfo(object): - """ - Not implemented. This is part of a (currently) internal API. - - .. INTERNAL NOTE: Base class for various authentication flavors. - - Currently only simple username & password authentication are supported - - :param object: username/password values. - """ - def __init__(self, username, password): - self.username = username - self.password = password - -class ConnectionInfo(object): - """ - Internal API. Do not use. - - Connection information object used by MAVProxy. - """ - - def __init__(self, mavproxy_options): - self.maxproxy_options = mavproxy_options - -class Attitude(object): - """ - Attitude information. - - .. figure:: http://upload.wikimedia.org/wikipedia/commons/thumb/c/c1/Yaw_Axis_Corrected.svg/500px-Yaw_Axis_Corrected.svg.png - :width: 400px - :alt: Diagram showing Pitch, Roll, Yaw - :target: http://commons.wikimedia.org/wiki/File:Yaw_Axis_Corrected.svg - - Diagram showing Pitch, Roll, Yaw (`Creative Commons `_) - - :param pitch: Pitch in radians - :param yaw: Yaw in radians - :param roll: Roll in radians - """ - def __init__(self, pitch, yaw, roll): - self.pitch = pitch - self.yaw = yaw - self.roll = roll - - def __str__(self): - return "Attitude:pitch=%s,yaw=%s,roll=%s" % (self.pitch, self.yaw, self.roll) - -class Location(object): - """ - A location object. - - The latitude and longitude are relative to the `WGS84 coordinate system `_. - The altitude is relative to either the *home position* or "mean sea-level", depending on the value of the ``is_relative``. - - For example, a location object might be defined as: - - .. code:: python - - Location(-34.364114, 149.166022, 30, is_relative=True) - - .. todo:: FIXME: Location class - possibly add a vector3 representation. - - :param lat: Latitude. - :param lon: Longitude. - :param alt: Altitude in meters (either relative or absolute). - :param is_relative: ``True`` if the specified altitude is relative to a 'home' location (this is usually desirable). ``False`` to set altitude relative to "mean sea-level". - """ - def __init__(self, lat, lon, alt=None, is_relative=True): - self.lat = lat - self.lon = lon - self.alt = alt - self.is_relative = is_relative - - def __str__(self): - return "Location:lat=%s,lon=%s,alt=%s,is_relative=%s" % (self.lat, self.lon, self.alt, self.is_relative) - -class GPSInfo(object): - """ - Standard information available about GPS. - - If there is no GPS lock the parameters are set to ``None``. - - :param IntType eph: GPS horizontal dilution of position (HDOP) in cm (m*100). - :param IntType epv: GPS horizontal dilution of position (VDOP) in cm (m*100). - :param IntType fix_type: 0-1: no fix, 2: 2D fix, 3: 3D fix - :param IntType satellites_visible: Number of satellites visible. - - .. todo:: FIXME: GPSInfo class - possibly normalize eph/epv? report fix type as string? - """ - def __init__(self, eph, epv, fix_type, satellites_visible): - self.eph = eph - self.epv = epv - self.fix_type = fix_type - self.satellites_visible = satellites_visible - - def __str__(self): - return "GPSInfo:fix=%s,num_sat=%s" % (self.fix_type, self.satellites_visible) - - -class Battery(object): - """ - System battery information. - - :param voltage: Battery voltage in millivolts. - :param current: Battery current, in 10 * milliamperes. ``None`` if the autopilot does not support current measurement. - :param level: Remaining battery energy. ``None`` if the autopilot cannot estimate the remaining battery. - """ - def __init__(self, voltage, current, level): - self.voltage = voltage / 1000.0 - if current == -1: - self.current = None - else: - self.current = current / 100.0 - if level == -1: - self.level = None - else: - self.level = level - - def __str__(self): - return "Battery:voltage={},current={},level={}".format(self.voltage, self.current, self.level) - - -class Rangefinder(object): - """ - Rangefinder readings. - - :param distance: Distance (metres). ``None`` if the vehicle doesn't have a rangefinder. - :param voltage: Voltage (volts). ``None`` if the vehicle doesn't have a rangefinder. - """ - def __init__(self, distance, voltage): - self.distance = distance - self.voltage = voltage - - def __str__(self): - return "Rangefinder: distance={}, voltage={}".format(self.distance, self.voltage) - - -class VehicleMode(object): - """ - This object is used to get and set the current "flight mode". - - The flight mode determines the behaviour of the vehicle and what commands it can obey. - The recommended flight modes for *DroneKit-Python* apps depend on the vehicle type: - - * Copter apps should use ``AUTO`` mode for "normal" waypoint missions and ``GUIDED`` mode otherwise. - * Plane and Rover apps should use the ``AUTO`` mode in all cases, re-writing the mission commands if "dynamic" - behaviour is required (they support only a limited subset of commands in ``GUIDED`` mode). - * Some modes like ``RETURN_TO_LAUNCH`` can be used on all platforms. Care should be taken - when using manual modes as these may require remote control input from the user. - - The available set of supported flight modes is vehicle-specific (see - `Copter `_, - `Plane `_, - `Rover `_). If an unsupported mode is set the script - will raise a ``KeyError`` exception. - - The :py:attr:`Vehicle.mode ` attribute can be queried for the current mode. The code snippet - below shows how to read (print) and observe changes to the mode: - - .. code:: python - - def mode_callback(self, mode): - print "Vehicle Mode", vehicle.mode - - vehicle.add_attribute_observer('mode', mode_callback) - - - The code snippet below shows how to change the vehicle mode to AUTO: - - .. code:: python - - # Get an instance of the API endpoint and a vehicle - api = local_connect() - vehicle = api.get_vehicles()[0] - - # Set the vehicle into auto mode - vehicle.mode = VehicleMode("AUTO") - - For more information on getting/setting/observing the :py:attr:`Vehicle.mode ` (and other attributes) see the :ref:`attributes guide `. - - .. py:attribute:: name - - The mode name, as a ``string``. - """ - def __init__(self, name): - self.name = name - - def __str__(self): - return "VehicleMode:%s" % self.name - -class APIConnection(object): - """ - An API provider. - - This is the top level API connection returned from :py:func:`local_connect()`. You should not manually create instances of - this class. - - .. INTERNAL_COMMENT: This is also returned by :py:func:`web_connect()` (not supported/fully implemented). - """ - - def get_vehicles(self, query=None): - """ - Get the set of vehicles that are controllable from this connection. - - For example, to get the first vehicle in the set with ``get_vehicles()``: - - .. code:: python - - api = local_connect() # Get an APIConnection - first_vehicle = api.get_vehicles()[0] - - .. note:: - - The set of vehicles connected by the API is configured through MAVProxy. When running on a companion computer there will only ever - be one ``Vehicle`` in the returned set. A ground control station might potentially control (and hence return) more than one vehicle. - - - :param query: This parameter is ignored. Use the default. - :returns: Set of :py:class:`Vehicle` objects controllable from this connection. - """ - # return [ Vehicle(), Vehicle() ] - raise Exception("Subclasses must override") - - @property - def exit(self): - """ - True if the current thread has been asked to exit. - - The connection to the UAV is owned by MAVProxy, which uses this property to signal that the thread should be closed (for whatever reason). - Scripts are expected to check the property and close the thread if this if ``True``. For example: - - .. code:: python - - while not api.exit: - # send commands to vehicle etc. - - .. todo:: FIXME: APIConnection.exit - should this be private, or even part of the drone api at all? - """ - return threading.current_thread().exit - -class HasObservers(object): - def __init__(self): - # A mapping from attr_name to a list of observers - self.__observers = {} - - """ - Provides callback based notification on attribute changes. - - The argument list for observer is ``observer(attr_name)``. - """ - def add_attribute_observer(self, attr_name, observer): - """ - Add an attribute observer. - - The observer is called with the ``attr_name`` argument. This can be used to access - the vehicle parameter, as shown below: - - .. code:: python - - #Add observer for the vehicle's current location - vehicle.add_attribute_observer('location', location_callback) - - #Callback to print the location - def location_callback(location): - print "Location: ", vehicle.location - - - .. note:: - Attribute changes will only be published for changes due to some other entity. - They will not be published for changes made by the local API client - (in order to prevent redundant notification for local changes). - - :param attr_name: The attribute to watch. - :param observer: The callback to invoke when a change in the attribute is detected. - - .. todo:: Check that the defect for endless repetition after thread closes is fixed: https://github.com/dronekit/dronekit-python/issues/74 - """ - l = self.__observers.get(attr_name) - if l is None: - l = [] - self.__observers[attr_name] = l - if not observer in l: - l.append(observer) - - def remove_attribute_observer(self, attr_name, observer): - """ - Remove an observer. - - For example, the following line would remove a previously added vehicle 'location' observer called location_callback: - - .. code:: python - - vehicle.remove_attribute_observer('location', location_callback) - - - :param attr_name: The attribute name that is to have an observer removed. - :param observer: The callback function to remove. - - - """ - l = self.__observers.get(attr_name) - if l is not None: - l.remove(observer) - if len(l) == 0: - del self.__observers[attr_name] - - def notify_observers(self, attr_name): - """ - Internal function. Do not use. - - This method calls observers when the named attribute has changed. - - .. INTERNAL NOTE: (For subclass use only) - """ - # print "Notify: " + attr_name - l = self.__observers.get(attr_name) - if l is not None: - for o in l: - try: - o(attr_name) - except TypeError as e: - # This is commonly called by a bad argument list - print("TypeError calling observer: ", e) - except Exception as e: - print("Error calling observer: ", e) - - def remove_all_observers(self): - """ - Internal function. Do not use. - - This method removes all attached observers. - - .. INTERNAL NOTE: (For subclass use only) - """ - self.__observers = {} - -class Vehicle(HasObservers): - """ - The main vehicle API - - Asynchronous notification on change of vehicle state is available by registering observers (callbacks) for attribute changes. - - Most vehicle state is exposed through python attributes (e.g. ``vehicle.location``). Most of these attributes are - auto-populated based on the capabilities of the connected autopilot/vehicle. - - Particular autopilots/vehicles may define different attributes from this standard list (extra batteries, GPIOs, etc.) - However if a standard attribute is defined it must follow the rules specified below. - - **Autopilot specific attributes & types:** - - To prevent name clashes the following naming convention should be used: - - * ``ap_`` - For autopilot specific parameters (apm 2.5, pixhawk etc.). For example "ap_pin5_mode" and "ap_pin5_value". - * ``user_`` - For user specific parameters - - **Standard attributes & types:** - - .. py:attribute:: location - - Current :py:class:`Location`. - - - .. py:attribute:: attitude - - Current vehicle :py:class:`Attitude` (pitch, yaw, roll). - - - .. py:attribute:: velocity - - Current velocity as a three element list ``[ vx, vy, vz ]`` (in meter/sec). - - - .. py:attribute:: mode - - This attribute is used to get and set the current flight mode (:py:class:`VehicleMode`). - - - .. py:attribute:: airspeed - - Current airspeed in metres/second (``double``). - - .. todo:: FIXME: Should airspeed value move somewhere else from "Standard attributes & types" table? - - .. py:attribute:: groundspeed - - Groundspeed in metres/second (``double``). - - .. py:attribute:: gps_0 - - GPS position information (:py:class:`GPSInfo`). - - - .. py:attribute:: armed - - This attribute can be used to get and set the ``armed`` state of the vehicle (``boolean``). - - The code below shows how to read the state, and to arm/disam the vehicle: - - .. code:: python - - # Print the armed state for the vehicle - print "Armed: %s" % vehicle.armed - - # Disarm the vehicle - vehicle.armed = False - - # Arm the vehicle - vehicle.armed = True - - - .. py:attribute:: mount_status - - Current status of the camera mount (gimbal) as a three element list: ``[ pitch, yaw, roll ]``. - - The values in the list are set to ``None`` if no mount is configured. - - - .. py:attribute:: battery - - Current system :py:class:`Battery` status. - - - .. py:attribute:: rangefinder - - :py:class:`Rangefinder` distance and voltage values. - - - .. py:attribute:: channel_override - - .. warning:: - - RC override may be useful for simulating user input and when implementing certain types of joystick control. - It should not be used for direct control of vehicle channels unless there is no other choice! - - Instead use the appropriate MAVLink commands like DO_SET_SERVO/DO_SET_RELAY, or more generally - set the desired position or direction/speed. - - This attribute takes a dictionary argument defining the RC *output* channels to be overridden (specified by channel number), and their new values. - Channels that are not specified in the dictionary are not overridden. All multi-channel updates are atomic. - - To cancel an override call ``channel_override`` again, setting zero for the overridden channels. - - The values of the first four channels map to the main flight controls: 1=Roll, 2=Pitch, 3=Throttle, 4=Yaw (the mapping is defined in ``RCMAP_`` parameters: - `Plane `_, - `Copter `_ , - `Rover `_). - - The remaining channel values are configurable, and their purpose can be determined using the - `RCn_FUNCTION parameters `_. - In general a value of 0 set for a specific ``RCn_FUNCTION`` indicates that the channel can be - `mission controlled `_ (i.e. it will not directly be - controlled by normal autopilot code). - - An example of setting and clearing the override is given below: - - .. code:: python - - # Override channels 1 and 4 (only). - vehicle.channel_override = { "1" : 900, "4" : 1000 } - vehicle.flush() - - # Cancel override on channel 1 and 4 by sending 0 - vehicle.channel_override = { "1" : 0, "4" : 0 } - vehicle.flush() - - - .. versionchanged:: 1.0 - - This update replaces ``rc_override`` with ``channel_override``/``channel_readback`` documentation. - - - .. todo:: Add note to the examples/guide like warning above not to use this mechanism except as intended: - - https://github.com/dronekit/dronekit-python/issues/72 - - .. todo:: - - channel_override/channel_readback documentation - - In a future update strings will be defined per vehicle type ('pitch', 'yaw', 'roll' etc...) - and rather than setting channel 3 to 1400 (for instance), you will pass in a dict with - 'throttle':1200. - - This change will be useful in two ways: - - * we can hide (eventually we can deprecate) any notion of rc channel numbers at all. - * vehicles can eventually define new 'channels' for overridden values. - - FIXME: Remaining channel_override/channel_readback FIXMEs: - - * how to address the units issue? Merely with documentation or some other way? - * is there any benefit of using lists rather than tuples for these attributes - - - .. py:attribute:: channel_readback - - This read-only attribute returns a dictionary containing the *original* vehicle RC channel values (ignoring any overrides set using - :py:attr:`channel_override `). Dictionary entries have the format ``channelName -> value``. - - For example, the returned dictionary might look like this: - - .. code:: python - - RC readback: {'1': 1500, '3': 1000, '2': 1500, '5': 1800, '4': 1500, '7': 1000, '6': 1000, '8': 1800} ? Dictionary () (read only) - - - - .. todo:: In V2, there may be ardupilot specific attributes & types (as in the introduction). If so, text below might be useful. - - **Autopilot specific attributes & types:** - - .. py:attribute:: ap_pin5_mode - - string (adc, dout, din) - - .. py:attribute:: ap_pin5_value - - ? double (0, 1, 2.3 etc...) - - - .. todo:: Add waypoint_home attribute IF this is added: https://github.com/dronekit/dronekit-python/issues/105 - - """ - - def __init__(self): - super(Vehicle, self).__init__() - self.mavrx_callback = None - - @property - def commands(self): - """ - Gets the editable waypoints for this vehicle (the current "mission"). - - This can be used to get, create, and modify a mission. It can also be used for direct control of vehicle position - (outside missions) using the :py:func:`goto ` method. - - :returns: A :py:class:`CommandSequence` containing the waypoints for this vehicle. - """ - None - - @property - def parameters(self): - """ - The (editable) parameters for this vehicle (:py:class:`Parameters `). - """ - return self._parameters - - def delete(self): - """ - Delete this vehicle object. - - This requests deletion of the Vehicle object on the server. This operation may throw an exception on failure (i.e. for - local connections or insufficient user permissions). - - It is not supported for local connections. - """ - pass - - def get_mission(self, query_params): - """ - Not implemented. - - Returns an object providing access to historical missions. - - .. warning:: - - Mission objects are only accessible from the REST API in release 1 (most use-cases requiring missions prefer a - - :param query_params: Some set of arguments that can be used to find a past mission - :return: Mission - the mission object. - - .. todo:: FIXME: get_mission needs to be updated when class Mission is implemented. The warning needs to be removed and the values of the query_params specified. - """ - - return Mission() - - @property - def message_factory(self): - """ - Returns an object that can be used to create 'raw' MAVLink messages that are appropriate for this vehicle. - The message can then be sent using :py:func:`send_mavlink(message) `. - - These message types are defined in the central MAVLink github repository. For example, a Pixhawk understands - the following messages (from `pixhawk.xml `_): - - .. code:: xml - - - 0 to disable, 1 to enable - - - The name of the factory method will always be the lower case version of the message name with *_encode* appended. - Each field in the XML message definition must be listed as arguments to this factory method. So for this example - message, the call would be: - - .. code:: python - - msg = vehicle.message_factory.image_trigger_control_encode(True) - vehicle.send_mavlink(msg) - - There is no need to specify the system id, component id or sequence number of messages (if defined in the message type) as the - API will set these appropriately when the message is sent. - - .. todo:: When I have a custom message guide topic. Link from here to it. - - .. todo:: Check if the standard MAV_CMD messages can be sent this way too, and if so add link. - """ - None - - def send_mavlink(self, message): - """ - This method is used to send raw MAVLink "custom messages" to the vehicle. - - The function can send arbitrary messages/commands to a vehicle at any time and in any vehicle mode. It is particularly useful for - controlling vehicles outside of missions (for example, in GUIDED mode). - - The :py:func:`message_factory ` is used to create messages in the appropriate format. - Callers do not need to populate sysId/componentId/crc in the packet as the method will take care of that before sending. - - :param message: A ``MAVLink_message`` instance, created using :py:func:`message_factory `. - There is need to specify the system id, component id or sequence number of messages as the API will set these appropriately. - """ - pass - - - def set_mavlink_callback(self, callback): - """ - Provides asynchronous notification when any MAVLink packet is received by this vehicle. - - Only a single callback can be set. :py:func:`unset_mavlink_callback ` removes the callback. - - .. tip:: - - This method is implemented - but we hope you don't need it. - - Because of the asynchronous attribute/waypoint/parameter notifications there should be no need for - API clients to see raw MAVLink. Please provide feedback if we missed a use-case. - - The code snippet below shows how to set a "demo" callback function as the callback handler: - - .. code:: python - - # Demo callback handler for raw MAVLink messages - def mavrx_debug_handler(message): - print "Received", message - - # Set MAVLink callback handler (after getting Vehicle instance) - vehicle.set_mavlink_callback(mavrx_debug_handler) - - :param callback: The callback function to be invoked when a raw MAVLink message is received. - - """ - self.mavrx_callback = callback - - def unset_mavlink_callback(self): - """ - Clears the asynchronous notification added by :py:func:`set_mavlink_callback `. - - The code snippet below shows how to set, then clear, a MAVLink callback function. - - .. code:: python - - # Set MAVLink callback handler (after getting Vehicle instance) - vehicle.set_mavlink_callback(mavrx_debug_handler) - - # Remove the MAVLink callback handler. Callback will not be - # called after this point. - vehicle.unset_mavlink_callback() - """ - self.mavrx_callback = None - - def flush(self): - """ - It is important to understand that setting attributes/changing vehicle state may occur over a slow link. - - It is **not** guaranteed that the effects of previous commands will be visible from reading vehicle attributes unless - ``flush()`` is called first. After the return from flush any writes are guaranteed to have completed (or thrown an - exception) and future reads will see their effects. - """ - pass - -#=============================================================================== -# def __getattr__(self, name): -# """ -# Attributes are automatically populated based on vehicle type. -# -# This override provides that behavior. -# """ -# -# try: -# return self.__dict[name] -# except KeyError: -# msg = "'{0}' object has no attribute '{1}'" -# raise AttributeError(msg.format(type(self).__name__, name)) -# -# def __setattr__(self, name, value): -# """ -# An override to support setting for vehicle attributes. -# -# Note: Exceptions due to loss of communications, missing attributes or insufficient permissions are not guaranteed -# to be thrown from inside this method. Most failures will not be seen until flush() is called. If you require immediate -# notification of failure set autoflush. -# """ -# pass -#=============================================================================== - -class Mission(object): - """ - Access to historical missions. - - .. warning:: This function is a *placeholder*. It has no implementation in DroneKit-Python release 1. - - Mission objects are only accessible from the REST API in release 1 (most use-cases requiring missions prefer a REST interface). - - .. todo:: FIXME: Mission class needs to be updated when it is implemented (after DroneKit Python release 1). - """ - pass - -class Parameters(HasObservers): - """ - This object is used to get and set the values of named parameters for a vehicle. See the following links for information about - the supported parameters for each platform: `Copter `_, - `Plane `_, `Rover `_. - - Attribute names are generated automatically based on parameter names. The example below shows how to get and set the value of a parameter. - Note that 'set' operations are not guaranteed to be complete until :py:func:`flush() ` is called on the parent :py:class:`Vehicle` object. - - .. code:: python - - # Print the value of the THR_MIN parameter. - print "Param: %s" % vehicle.parameters['THR_MIN'] - - # Change the parameter value to something different. - vehicle.parameters['THR_MIN']=100 - vehicle.flush() - - .. note:: - - At time of writing ``Parameters`` does not implement the observer methods, and change notification for parameters - is not supported. - - .. todo:: - - Check to see if observers have been implemented and if so, update the information here, in about, and in Vehicle class: - https://github.com/dronekit/dronekit-python/issues/107 - """ - -class Command(mavutil.mavlink.MAVLink_mission_item_message): - """ - A waypoint object. - - This object encodes a single mission item command. The set of commands that are supported by ArduPilot in Copter, Plane and Rover (along with their parameters) - are listed in the wiki article `MAVLink Mission Command Messages (MAV_CMD) `_. - - For example, to create a `NAV_WAYPOINT `_ command: - - .. code:: python - - cmd = Command(0,0,0, mavutil.mavlink.MAV_FRAME_GLOBAL_RELATIVE_ALT, - mavutil.mavlink.MAV_CMD_NAV_WAYPOINT, 0, 0, 0, 0, 0, 0,-34.364114, 149.166022, 30) - - - :param target_system: The id number of the message's target system (drone, GSC) within the MAVLink network. - Set this to zero (broadcast) when communicating with a companion computer. - :param target_component: The id of a component the message should be routed to within the target system - (for example, the camera). Set to zero (broadcast) in most cases. - :param seq: The sequence number within the mission (the autopilot will reject messages sent out of sequence). - This should be set to zero as the API will automatically set the correct value when uploading a mission. - :param frame: The frame of reference used for the location parameters (x, y, z). In most cases this will be - ``mavutil.mavlink.MAV_FRAME_GLOBAL_RELATIVE_ALT``, which uses the WGS84 global coordinate system for latitude and longitude, but sets altitude - as relative to the home position in metres (home altitude = 0). For more information `see the wiki here - `_. - :param command: The specific mission command (e.g. ``mavutil.mavlink.MAV_CMD_NAV_WAYPOINT``). The supported commands (and command parameters - are listed `on the wiki `_. - :param current: Set to zero (not supported). - :param autocontinue: Set to zero (not supported). - :param param1: Command specific parameter (depends on specific `Mission Command (MAV_CMD) `_). - :param param2: Command specific parameter. - :param param3: Command specific parameter. - :param param4: Command specific parameter. - :param x: (param5) Command specific parameter used for latitude (if relevant to command). - :param y: (param6) Command specific parameter used for longitude (if relevant to command). - :param z: (param7) Command specific parameter used for altitude (if relevant). The reference frame for altitude depends on the ``frame``. - - .. todo:: Confirm if target_sytem, target_component, seq, frame are all handled for you or not. If not, check that these are correct. - .. todo:: FIXME: Command class - for now we just inherit the standard MAVLink mission item contents. - """ - pass - -class CommandSequence(object): - """ - A sequence of vehicle waypoints (a "mission"). - - Operations include 'array style' indexed access to the various contained waypoints. - - The current commands/mission for a vehicle are accessed using the :py:attr:`Vehicle.commands ` attribute. - Waypoints are not downloaded from vehicle until :py:func:`download()` is called. The download is asynchronous; - use :py:func:`wait_valid()` to block your thread until the download is complete. - - The code to download the commands from a vehicle is shown below: - - .. code-block:: python - :emphasize-lines: 5-10 - - # Connect to API provider and get vehicle - api = local_connect() - vehicle = api.get_vehicles()[0] - - # Download the vehicle waypoints (commands). Wait until download is complete. - cmds = vehicle.commands - cmds.download() - cmds.wait_valid() - - The set of commands can be changed and uploaded to the client. The changes are not guaranteed to be complete until - :py:func:`flush() ` is called on the parent :py:class:`Vehicle` object. - - .. code:: python - - cmds = vehicle.commands - cmds.clear() - vehicle.flush() - lat = -34.364114, - lon = 149.166022 - altitude = 30.0 - cmd = Command(0,0,0, mavutil.mavlink.MAV_FRAME_GLOBAL_RELATIVE_ALT, mavutil.mavlink.MAV_CMD_NAV_WAYPOINT, - 0, 0, 0, 0, 0, 0, - lat, lon, altitude) - cmds.add(cmd) - vehicle.flush() - - .. py:function:: takeoff(altitude) - - .. note:: This function should only be used on Copter vehicles. - - Take off and fly the vehicle to the specified altitude (in metres) and then wait for another command. - - The vehicle must be in ``GUIDED`` mode and armed before this is called. - - There is no mechanism for notification when the correct altitude is reached, and if another command arrives - before that point (e.g. :py:func:`goto`) it will be run instead. - - .. warning:: - - Apps should code to ensure that the vehicle will reach a safe altitude before other commands are executed. - A good example is provided in the guide topic :ref:`taking-off`. - - :param altitude: Target height, in metres. - - .. todo:: This is a hack. The actual function should be defined here. See https://github.com/dronekit/dronekit-python/issues/64 - """ - - def download(self): - ''' - Download all waypoints from the vehicle. - - The download is asynchronous. Use :py:func:`wait_valid()` to block your thread until the download is complete. - ''' - pass - - def wait_valid(self): - ''' - Block the calling thread until waypoints have been downloaded. - - This can be called after :py:func:`download()` to block the thread until the asynchronous download is complete. - ''' - pass - - - def goto(self, location): - ''' - Go to a specified location (changing :py:class:`VehicleMode` to ``GUIDED`` if necessary). - - .. code:: python - - # Set mode to guided - this is optional as the goto method will change the mode if needed. - vehicle.mode = VehicleMode("GUIDED") - - # Set the location to goto() and flush() - a_location = Location(-34.364114, 149.166022, 30, is_relative=True) - vehicle.commands.goto(a_location) - vehicle.flush() - - :param Location location: The target location. - ''' - pass - - def clear(self): - ''' - Clear the command list. - - .. warning:: - - Call ``flush()`` immediately after clearing the commands/before adding new commands (see - `#132 for more information `_). - - .. todo:: The above note should be removed when https://github.com/dronekit/dronekit-python/issues/132 fixed - ''' - pass - - def add(self, cmd): - ''' - Add a new command (waypoint) at the end of the command list. - - :param Command cmd: The command to be added. - ''' - pass - - @property - def count(self): - ''' - Return number of waypoints. - - :return: The number of waypoints in the sequence. - ''' - return 0 - - @property - def next(self): - """ - Get the currently active waypoint number. - - .. INTERNAL NOTE: (implementation provided by subclass) - """ - return None - - @next.setter - def next(self, value): - """ - Set a new ``next`` waypoint for the vehicle. - - .. INTERNAL NOTE: (implementation provided by subclass) - """ - pass diff --git a/dronekit/mavlink.py b/dronekit/mavlink.py new file mode 100644 index 000000000..d87f93ae5 --- /dev/null +++ b/dronekit/mavlink.py @@ -0,0 +1,342 @@ +from __future__ import print_function + +import logging +import time +import socket +import errno +import sys +import os +import platform +import copy +from dronekit import APIException +from pymavlink import mavutil +from queue import Queue, Empty +from threading import Thread + +if platform.system() == 'Windows': + from errno import WSAECONNRESET as ECONNABORTED +else: + from errno import ECONNABORTED + + +class MAVWriter(object): + """ + Indirection layer to take messages written to MAVlink and send them all + on the same thread. + """ + + def __init__(self, queue): + self._logger = logging.getLogger(__name__) + self.queue = queue + + def write(self, pkt): + self.queue.put(pkt) + + def read(self): + self._logger.critical('writer should not have had a read request') + os._exit(43) + + +class mavudpin_multi(mavutil.mavfile): + '''a UDP mavlink socket''' + def __init__(self, device, baud=None, input=True, broadcast=False, source_system=255, source_component=0, use_native=mavutil.default_native): + self._logger = logging.getLogger(__name__) + a = device.split(':') + if len(a) != 2: + self._logger.critical("UDP ports must be specified as host:port") + sys.exit(1) + self.port = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + self.udp_server = input + self.broadcast = False + self.addresses = set() + if input: + self.port.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + self.port.bind((a[0], int(a[1]))) + else: + self.destination_addr = (a[0], int(a[1])) + if broadcast: + self.port.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) + self.broadcast = True + mavutil.set_close_on_exec(self.port.fileno()) + self.port.setblocking(False) + mavutil.mavfile.__init__(self, self.port.fileno(), device, source_system=source_system, source_component=source_component, input=input, use_native=use_native) + + def close(self): + self.port.close() + + def recv(self, n=None): + try: + try: + data, new_addr = self.port.recvfrom(65535) + except socket.error as e: + if e.errno in [errno.EAGAIN, errno.EWOULDBLOCK, errno.ECONNREFUSED]: + return "" + if self.udp_server: + self.addresses.add(new_addr) + elif self.broadcast: + self.addresses = {new_addr} + return data + except Exception: + self._logger.exception("Exception while reading data", exc_info=True) + + def write(self, buf): + try: + try: + if self.udp_server: + for addr in self.addresses: + self.port.sendto(buf, addr) + else: + if len(self.addresses) and self.broadcast: + self.destination_addr = self.addresses[0] + self.broadcast = False + self.port.connect(self.destination_addr) + self.port.sendto(buf, self.destination_addr) + except socket.error: + pass + except Exception: + self._logger.exception("Exception while writing data", exc_info=True) + + def recv_msg(self): + '''message receive routine for UDP link''' + self.pre_message() + s = self.recv() + if len(s) > 0: + if self.first_byte: + self.auto_mavlink_version(s) + + m = self.mav.parse_char(s) + if m is not None: + self.post_message(m) + + return m + + +class MAVConnection(object): + + def stop_threads(self): + if self.mavlink_thread_in is not None: + self.mavlink_thread_in.join() + self.mavlink_thread_in = None + if self.mavlink_thread_out is not None: + self.mavlink_thread_out.join() + self.mavlink_thread_out = None + + def __init__(self, ip, baud=115200, target_system=0, source_system=255, source_component=0, use_native=False): + self._logger = logging.getLogger(__name__) + + if ip.startswith("udpin:"): + self.master = mavudpin_multi(ip[6:], input=True, baud=baud, source_system=source_system, source_component=source_component) + else: + self.master = mavutil.mavlink_connection(ip, baud=baud, source_system=source_system, source_component=source_component) + + # TODO get rid of "master" object as exposed, + # keep it private, expose something smaller for dronekit + self.out_queue = Queue() + self.master.mav = mavutil.mavlink.MAVLink( + MAVWriter(self.out_queue), + srcSystem=self.master.source_system, + srcComponent=self.master.source_component, + use_native=use_native) + + # Monkey-patch MAVLink object for fix_targets. + sendfn = self.master.mav.send + + def newsendfn(mavmsg, *args, **kwargs): + self.fix_targets(mavmsg) + return sendfn(mavmsg, *args, **kwargs) + + self.master.mav.send = newsendfn + + # Targets + self.target_system = target_system + + # Listeners. + self.loop_listeners = [] + self.message_listeners = [] + + # Debug flag. + self._accept_input = True + self._alive = True + self._death_error = None + + import atexit + + def onexit(): + self._alive = False + self.stop_threads() + + atexit.register(onexit) + + def mavlink_thread_out(): + # Huge try catch in case we see http://bugs.python.org/issue1856 + try: + while self._alive: + try: + msg = self.out_queue.get(True, timeout=0.01) + self.master.write(msg) + except Empty: + continue + except socket.error as error: + # If connection reset (closed), stop polling. + if error.errno == ECONNABORTED: + raise APIException('Connection aborting during read') + raise + except Exception as e: + self._logger.exception('mav send error: %s' % str(e)) + break + except APIException as e: + self._logger.exception("Exception in MAVLink write loop", exc_info=True) + self._alive = False + self.master.close() + self._death_error = e + + except Exception as e: + # http://bugs.python.org/issue1856 + if not self._alive: + pass + else: + self._alive = False + self.master.close() + self._death_error = e + + # Explicitly clear out buffer so .close closes. + self.out_queue = Queue() + + def mavlink_thread_in(): + # Huge try catch in case we see http://bugs.python.org/issue1856 + try: + while self._alive: + # Loop listeners. + for fn in self.loop_listeners: + fn(self) + + # Sleep + self.master.select(0.05) + + while self._accept_input: + try: + msg = self.master.recv_msg() + except socket.error as error: + # If connection reset (closed), stop polling. + if error.errno == ECONNABORTED: + raise APIException('Connection aborting during send') + raise + except mavutil.mavlink.MAVError as e: + # Avoid + # invalid MAVLink prefix '73' + # invalid MAVLink prefix '13' + self._logger.debug('mav recv error: %s' % str(e)) + msg = None + except Exception: + # Log any other unexpected exception + self._logger.exception('Exception while receiving message: ', exc_info=True) + msg = None + if not msg: + break + + # Message listeners. + for fn in self.message_listeners: + try: + fn(self, msg) + except Exception: + self._logger.exception( + 'Exception in message handler for %s' % msg.get_type(), + exc_info=True + ) + + except APIException as e: + self._logger.exception('Exception in MAVLink input loop') + self._alive = False + self.master.close() + self._death_error = e + return + + except Exception as e: + # http://bugs.python.org/issue1856 + if not self._alive: + pass + else: + self._alive = False + self.master.close() + self._death_error = e + + t = Thread(target=mavlink_thread_in) + t.daemon = True + self.mavlink_thread_in = t + + t = Thread(target=mavlink_thread_out) + t.daemon = True + self.mavlink_thread_out = t + + def reset(self): + self.out_queue = Queue() + if hasattr(self.master, 'reset'): + self.master.reset() + else: + try: + self.master.close() + except: + pass + self.master = mavutil.mavlink_connection(self.master.address) + + def fix_targets(self, message): + """Set correct target IDs for our vehicle""" + if hasattr(message, 'target_system'): + message.target_system = self.target_system + + def forward_loop(self, fn): + """ + Decorator for event loop. + """ + self.loop_listeners.append(fn) + + def forward_message(self, fn): + """ + Decorator for message inputs. + """ + self.message_listeners.append(fn) + + def start(self): + if not self.mavlink_thread_in.is_alive(): + self.mavlink_thread_in.start() + if not self.mavlink_thread_out.is_alive(): + self.mavlink_thread_out.start() + + def close(self): + # TODO this can block forever if parameters continue to be added + self._alive = False + while not self.out_queue.empty(): + time.sleep(0.1) + self.stop_threads() + self.master.close() + + def pipe(self, target): + target.target_system = self.target_system + + # vehicle -> self -> target + @self.forward_message + def callback(_, msg): + try: + target.out_queue.put(msg.pack(target.master.mav)) + except: + try: + assert len(msg.get_msgbuf()) > 0 + target.out_queue.put(msg.get_msgbuf()) + except: + self._logger.exception('Could not pack this object on receive: %s' % type(msg), exc_info=True) + + # target -> self -> vehicle + @target.forward_message + def callback(_, msg): + msg = copy.copy(msg) + target.fix_targets(msg) + try: + self.out_queue.put(msg.pack(self.master.mav)) + except: + try: + assert len(msg.get_msgbuf()) > 0 + self.out_queue.put(msg.get_msgbuf()) + except: + self._logger.exception('Could not pack this object on forward: %s' % type(msg), exc_info=True) + + return target diff --git a/dronekit/module/api.py b/dronekit/module/api.py deleted file mode 100644 index 17962aca5..000000000 --- a/dronekit/module/api.py +++ /dev/null @@ -1,350 +0,0 @@ -import os -import time -import threading -import traceback -import logging -import math -from pymavlink import mavutil -from dronekit.lib import APIConnection, Vehicle, VehicleMode, Location, \ - Attitude, GPSInfo, Parameters, CommandSequence, APIException, Battery, \ - Rangefinder - -# Enable logging here (until this code can be moved into mavproxy) -logging.basicConfig(level=logging.DEBUG) - -logger = logging.getLogger(__name__) - -class MPParameters(Parameters): - """ - See Parameters baseclass for documentation. - - FIXME - properly publish change notification - """ - - def __init__(self, module): - self.__module = module - - def __getitem__(self, name): - self.wait_valid() - return self.__module.mav_param[name] - - def __setitem__(self, name, value): - self.wait_valid() - self.__module.mpstate.functions.param_set(name, value) - - def wait_valid(self): - '''Block the calling thread until parameters have been downloaded''' - # FIXME this is a super crufty spin-wait, also we should give the user the option of specifying a timeout - pstate = self.__param.pstate - while (pstate.mav_param_count == 0 or len(pstate.mav_param_set) != pstate.mav_param_count) and not self.__module.api.exit: - time.sleep(0.200) - - @property - def __param(self): - return self.__module.module('param') - -class MPCommandSequence(CommandSequence): - """ - See CommandSequence baseclass for documentation. - """ - - def __init__(self, module): - self.__module = module - - def download(self): - '''Download all waypoints from the vehicle''' - self.wait_valid() - self.__wp.fetch() - # BIG FIXME - wait for full wpt download before allowing any of the accessors to work - - def wait_valid(self): - '''Block the calling thread until waypoints have been downloaded''' - # FIXME this is a super crufty spin-wait, also we should give the user the option of specifying a timeout - while (self.__wp.wp_op is not None) and not self.__module.api.exit: - time.sleep(0.200) - - def takeoff(self, alt=None): - if alt is not None: - altitude = float(alt) - if math.isnan(alt) or math.isinf(alt): - raise ValueError("Altitude was NaN or Infinity. Please provide a real number") - self.__module.master.mav.command_long_send(self.__module.target_system, - self.__module.target_component, - mavutil.mavlink.MAV_CMD_NAV_TAKEOFF, - 0, 0, 0, 0, 0, 0, 0, - altitude) - - def goto(self, l): - if l.is_relative: - frame = mavutil.mavlink.MAV_FRAME_GLOBAL_RELATIVE_ALT - else: - frame = mavutil.mavlink.MAV_FRAME_GLOBAL - self.__module.master.mav.mission_item_send(self.__module.target_system, - self.__module.target_component, - 0, - frame, - mavutil.mavlink.MAV_CMD_NAV_WAYPOINT, - 2, 0, 0, 0, 0, 0, - l.lat, l.lon, l.alt) - - def clear(self): - '''Clears the command list''' - self.wait_valid() - self.__wp.wploader.clear() - self.__module.vehicle.wpts_dirty = True - - def add(self, cmd): - '''Add a new command at the end of the command list''' - self.wait_valid() - self.__module.fix_targets(cmd) - self.__wp.wploader.add(cmd, comment = 'Added by DroneAPI') - self.__module.vehicle.wpts_dirty = True - - @property - def __wp(self): - return self.__module.module('wp') - - @property - def count(self): - return self.__wp.wploader.count() - - @property - def next(self): - """ - Currently active waypoint number - - (implementation provided by subclass) - """ - return self.__module.last_waypoint - - @next.setter - def next(self, index): - self.__module.master.waypoint_set_current_send(index) - - def __getitem__(self, index): - return self.__wp.wploader.wp(index) - - def __setitem__(self, index, value): - self.__wp.wploader.set(value, index) - self.__module.vehicle.wpts_dirty = True - -class MPVehicle(Vehicle): - def __init__(self, module): - super(MPVehicle, self).__init__() - self.__module = module - self._parameters = MPParameters(module) - self._waypoints = None - self.wpts_dirty = False - - def flush(self): - if self.wpts_dirty: - self.__module.module('wp').send_all_waypoints() - self.wpts_dirty = False - - # - # Private sugar methods - # - - @property - def __master(self): - return self.__module.master - - @property - def __mode_mapping(self): - return self.__master.mode_mapping() - - # - # Operations to support the standard API (FIXME - possibly/probably this - # will move into a private dict of getter/setter tuples (invisible to the API consumer). - # - - @property - def mode(self): - self.wait_init() # We must know vehicle type before this operation can work - return self.__get_mode() - - def __get_mode(self): - """Private method to read current vehicle mode without polling""" - return VehicleMode(self.__module.status.flightmode) - - @mode.setter - def mode(self, v): - self.wait_init() # We must know vehicle type before this operation can work - self.__master.set_mode(self.__mode_mapping[v.name]) - - @property - def location(self): - return Location(self.__module.lat, self.__module.lon, self.__module.alt, is_relative=False) - - @property - def battery(self): - return Battery(self.__module.voltage, self.__module.current, self.__module.level) - - @property - def rangefinder(self): - return Rangefinder(self.__module.rngfnd_distance, self.__module.rngfnd_voltage) - - @property - def velocity(self): - return [ self.__module.vx, self.__module.vy, self.__module.vz ] - - @property - def attitude(self): - return Attitude(self.__module.pitch, self.__module.yaw, self.__module.roll) - - @property - def gps_0(self): - return GPSInfo(self.__module.eph, self.__module.epv, self.__module.fix_type, self.__module.satellites_visible) - - @property - def armed(self): - return self.__module.mpstate.status.armed - - @armed.setter - def armed(self, value): - if value: - self.__master.arducopter_arm() - else: - self.__master.arducopter_disarm() - - @property - def groundspeed(self): - return self.__module.groundspeed - - @property - def airspeed(self): - return self.__module.airspeed - - @property - def mount_status(self): - return [ self.__module.mount_pitch, self.__module.mount_yaw, self.__module.mount_roll ] - - @property - def channel_override(self): - overrides = self.__rc.override - # Only return entries that have a non zero override - return dict((str(num + 1), overrides[num]) for num in range(8) if overrides[num] != 0) - - @channel_override.setter - def channel_override(self, newch): - overrides = self.__rc.override - for k, v in newch.iteritems(): - overrides[int(k) - 1] = v - self.__rc.set_override(overrides) - - @property - def channel_readback(self): - return self.__module.rc_readback - - @property - def __rc(self): - return self.__module.module('rc') - - @property - def commands(self): - """ - The (editable) waypoints for this vehicle. - """ - if(self._waypoints is None): # We create the wpts lazily (because this will start a fetch) - self._waypoints = MPCommandSequence(self.__module) - return self._waypoints - - def send_mavlink(self, message): - self.__module.fix_targets(message) - self.__module.master.mav.send(message) - - @property - def message_factory(self): - """ - Returns an object that can be used to create 'raw' mavlink messages that are appropriate for this vehicle. - These message types are defined in the central Mavlink github repository. For example, a Pixhawk understands - the following messages: (from https://github.com/mavlink/mavlink/blob/master/message_definitions/v1.0/pixhawk.xml). - - - 0 to disable, 1 to enable - - - The name of the factory method will always be the lower case version of the message name with _encode appended. - Each field in the xml message definition must be listed as arguments to this factory method. So for this example - message, the call would be: - - msg = vehicle.message_factory.image_trigger_control_encode(True) - vehicle.send_mavlink(msg) - """ - return self.__module.master.mav - - def wait_init(self): - """Wait for the vehicle to exit the initializing step""" - timeout = 30 - pollinterval = 0.2 - for i in range(0, int(timeout / pollinterval)): - # Don't let the user try to fly while the board is still booting - mode = self.__get_mode().name - # print "mode is", mode - if mode != "INITIALISING" and mode != "MAV": - return - - time.sleep(pollinterval) - raise APIException("Vehicle did not complete initialization") - - -class MPAPIConnection(APIConnection): - """ - A small private version of the APIConnection class - - In Mavproxy you probably just want to call get_vehicles - """ - def __init__(self, module): - self.__vehicle = MPVehicle(module) - - def get_vehicles(self, query=None): - return [ self.__vehicle ] - -class APIThread(threading.Thread): - def __init__(self, module, fn, description): - super(APIThread, self).__init__() - self.module = module - self.description = description - self.exit = False # Python has no standard way to kill threads, this allows - self.fn = fn - self.thread_num = module.next_thread_num - module.next_thread_num = module.next_thread_num + 1 - self.daemon = True # For now I think it is okay to let mavproxy exit if api clients are still running - self.start() - self.name = "APIThread-%s" % self.thread_num - self.module.thread_add(self) - - # DroneAPI might generate many commands, which in turn generate ots of acks and status text, in the interest of speed we ignore processing those messages - try: - self.module.mpstate.rx_blacklist.add('COMMAND_ACK') - self.module.mpstate.rx_blacklist.add('STATUSTEXT') - except: - pass # Silently work with old mavproxies - - def kill(self): - """Ask the thread to exit. The thread must check threading.current_thread().exit periodically""" - print("Asking %s to exit..." % self.name) - self.exit = True - - def run(self): - try: - self.fn() - print("%s exiting..." % self.name) - except Exception as e: - print("Exception in %s: %s" % (self.name, str(e))) - traceback.print_exc() - - try: - self.module.mpstate.rx_blacklist.remove('COMMAND_ACK') - self.module.mpstate.rx_blacklist.remove('STATUSTEXT') - except: - pass # Silently work with old mavproxies - - # Remove all observers. - self.module.vehicle.remove_all_observers() - self.module.vehicle.unset_mavlink_callback() - - self.module.thread_remove(self) - - def __str__(self): - return "%s: %s" % (self.thread_num, self.description) diff --git a/tests/README.md b/dronekit/test/README.md similarity index 100% rename from tests/README.md rename to dronekit/test/README.md diff --git a/dronekit/test/__init__.py b/dronekit/test/__init__.py new file mode 100644 index 000000000..04ad5353a --- /dev/null +++ b/dronekit/test/__init__.py @@ -0,0 +1,39 @@ +from __future__ import print_function +import os +from dronekit_sitl import SITL +from nose.tools import with_setup +import time + +sitl = None +sitl_args = ['-I0', '--model', 'quad', '--home=-35.363261,149.165230,584,353'] + +if 'SITL_SPEEDUP' in os.environ: + sitl_args += ['--speedup', str(os.environ['SITL_SPEEDUP'])] +if 'SITL_RATE' in os.environ: + sitl_args += ['-r', str(os.environ['SITL_RATE'])] + + +def setup_sitl(): + global sitl + sitl = SITL() + sitl.download('copter', '3.3') + sitl.launch(sitl_args, await_ready=True, restart=True) + + +def teardown_sitl(): + sitl.stop() + + +def with_sitl(fn): + @with_setup(setup_sitl, teardown_sitl) + def test(*args, **kargs): + return fn('tcp:127.0.0.1:5760', *args, **kargs) + return test + + +def wait_for(condition, time_max): + time_start = time.time() + while not condition(): + if time.time() - time_start > time_max: + break + time.sleep(0.1) diff --git a/dronekit/test/sitl/__init__.py b/dronekit/test/sitl/__init__.py new file mode 100644 index 000000000..51b3be828 --- /dev/null +++ b/dronekit/test/sitl/__init__.py @@ -0,0 +1,47 @@ +import time +from contextlib import contextmanager +from nose.tools import assert_equal +from pymavlink import mavutil + + +@contextmanager +def assert_command_ack( + vehicle, + command_type, + ack_result=mavutil.mavlink.MAV_RESULT_ACCEPTED, + timeout=10 +): + """Context manager to assert that: + + 1) exactly one COMMAND_ACK is received from a Vehicle; + 2) for a specific command type; + 3) with the given result; + 4) within a timeout (in seconds). + + For example: + + .. code-block:: python + + with assert_command_ack(vehicle, mavutil.mavlink.MAV_CMD_PREFLIGHT_CALIBRATION, timeout=30): + vehicle.calibrate_gyro() + + """ + + acks = [] + + def on_ack(self, name, message): + if message.command == command_type: + acks.append(message) + + vehicle.add_message_listener('COMMAND_ACK', on_ack) + + yield + + start_time = time.time() + while not acks and time.time() - start_time < timeout: + time.sleep(0.1) + vehicle.remove_message_listener('COMMAND_ACK', on_ack) + + assert_equal(1, len(acks)) # one and only one ACK + assert_equal(command_type, acks[0].command) # for the correct command + assert_equal(ack_result, acks[0].result) # the result must be successful diff --git a/dronekit/test/sitl/test_110.py b/dronekit/test/sitl/test_110.py new file mode 100644 index 000000000..43ed92135 --- /dev/null +++ b/dronekit/test/sitl/test_110.py @@ -0,0 +1,69 @@ +import time +from dronekit import connect, VehicleMode +from dronekit.test import with_sitl, wait_for +from nose.tools import assert_equals + + +@with_sitl +def test_110(connpath): + vehicle = connect(connpath, wait_ready=True) + + # NOTE these are *very inappropriate settings* + # to make on a real vehicle. They are leveraged + # exclusively for simulation. Take heed!!! + vehicle.parameters['FS_GCS_ENABLE'] = 0 + vehicle.parameters['FS_EKF_THRESH'] = 100 + + # Await armability. + wait_for(lambda: vehicle.is_armable, 60) + + # Change the vehicle into STABILIZE mode + vehicle.mode = VehicleMode("GUIDED") + + # NOTE wait crudely for ACK on mode update + time.sleep(3) + + # Define example callback for mode + def armed_callback(vehicle, attribute, value): + armed_callback.called += 1 + + armed_callback.called = 0 + + # When the same (event, callback) pair is passed to add_attribute_listener, + # only one instance of the observer callback should be added. + vehicle.add_attribute_listener('armed', armed_callback) + vehicle.add_attribute_listener('armed', armed_callback) + vehicle.add_attribute_listener('armed', armed_callback) + vehicle.add_attribute_listener('armed', armed_callback) + vehicle.add_attribute_listener('armed', armed_callback) + + # arm and see update. + vehicle.armed = True + + # Wait for ACK. + time_max = 10 + wait_for(lambda: armed_callback.called, time_max) + + # Ensure the callback was called. + assert armed_callback.called > 0, "Callback should have been called within %d seconds" % (time_max,) + + # Rmove all listeners. The first call should remove all listeners + # we've added; the second call should be ignored and not throw. + # NOTE: We test if armed_callback were treating adding each additional callback + # and remove_attribute_listener were removing them one at a time; in this + # case, there would be three callbacks still attached. + vehicle.remove_attribute_listener('armed', armed_callback) + vehicle.remove_attribute_listener('armed', armed_callback) + callcount = armed_callback.called + + # Disarm and see update. + vehicle.armed = False + + # Wait for ack + time.sleep(3) + + # Ensure the callback was called zero times. + assert_equals(armed_callback.called, callcount, + "Callback should not have been called once removed.") + + vehicle.close() diff --git a/tests/sitl/test_115.py b/dronekit/test/sitl/test_115.py similarity index 74% rename from tests/sitl/test_115.py rename to dronekit/test/sitl/test_115.py index 93d4c0d86..ae17d4a11 100644 --- a/tests/sitl/test_115.py +++ b/dronekit/test/sitl/test_115.py @@ -1,23 +1,21 @@ -from dronekit import connect -from dronekit.lib import VehicleMode -from dronekit.tools import with_sitl -from pymavlink import mavutil import time -import sys -import os +from dronekit import connect, VehicleMode +from dronekit.test import with_sitl from nose.tools import assert_equals + @with_sitl def test_115(connpath): - v = connect(connpath, await_params=True) + v = connect(connpath, wait_ready=True) # Dummy callback def mavlink_callback(*args): mavlink_callback.count += 1 + mavlink_callback.count = 0 # Set the callback. - v.set_mavlink_callback(mavlink_callback) + v.add_message_listener('*', mavlink_callback) # Change the vehicle into STABILIZE mode v.mode = VehicleMode("STABILIZE") @@ -28,12 +26,11 @@ def mavlink_callback(*args): assert mavlink_callback.count > 0 # Unset the callback. - v.unset_mavlink_callback() + v.remove_message_listener('*', mavlink_callback) savecount = mavlink_callback.count # Disarm. A callback of None should not throw errors v.armed = False - v.flush() # NOTE wait crudely for ACK on mode update time.sleep(3) @@ -42,6 +39,7 @@ def mavlink_callback(*args): # Re-arm should not throw errors. v.armed = True - v.flush() # NOTE wait crudely for ACK on mode update time.sleep(3) + + v.close() diff --git a/tests/sitl/test_12.py b/dronekit/test/sitl/test_12.py similarity index 62% rename from tests/sitl/test_12.py rename to dronekit/test/sitl/test_12.py index 245e67928..c30e30b0b 100644 --- a/tests/sitl/test_12.py +++ b/dronekit/test/sitl/test_12.py @@ -1,18 +1,16 @@ -from dronekit import connect -from dronekit.lib import VehicleMode -from dronekit.tools import with_sitl -from pymavlink import mavutil import time -import sys -import os +from dronekit import connect +from dronekit.test import with_sitl from nose.tools import assert_equals + def current_milli_time(): return int(round(time.time() * 1000)) + @with_sitl def test_timeout(connpath): - v = connect(connpath, await_params=True) + v = connect(connpath, wait_ready=True) value = v.parameters['THR_MIN'] assert_equals(type(value), float) @@ -25,5 +23,8 @@ def test_timeout(connpath): assert_equals(type(newvalue), float) assert_equals(newvalue, value + 10) - # TODO once this issue is fixed - # assert end - start < 1000, 'time to set parameter was %s, over 1s' % (end - start,) + # Checks that time to set parameter was <1s + # see https://github.com/dronekit/dronekit-python/issues/12 + assert end - start < 1000, 'time to set parameter was %s, over 1s' % (end - start, ) + + v.close() diff --git a/dronekit/test/sitl/test_capability_and_version.py b/dronekit/test/sitl/test_capability_and_version.py new file mode 100644 index 000000000..cae0a0c45 --- /dev/null +++ b/dronekit/test/sitl/test_capability_and_version.py @@ -0,0 +1,36 @@ +import time + +from dronekit import connect +from dronekit.test import with_sitl +from nose.tools import assert_false, assert_true + + +@with_sitl +def test_115(connpath): + v = connect(connpath, wait_ready=True) + time.sleep(5) + assert_false(v.capabilities.ftp) + + # versions of ArduCopter prior to v3.3 will send out capabilities + # flags before they are initialised. Vehicle attempts to refetch + # until capabilities are non-zero, but we may need to wait: + start_time = time.time() + slept = False + while v.capabilities.mission_float == 0: + if time.time() > start_time + 30: + break + time.sleep(0.1) + slept = True + if v.capabilities.mission_float: + if slept: + assert_true(v.version.major <= 3) + assert_true(v.version_minor <= 3) + else: + # fail it + assert_true(v.capabilities.mission_float) + + assert_true(v.version.major is not None) + assert_true(len(v.version.release_type()) >= 2) + assert_true(v.version.release_version() is not None) + + v.close() diff --git a/dronekit/test/sitl/test_channels.py b/dronekit/test/sitl/test_channels.py new file mode 100644 index 000000000..a23d536a2 --- /dev/null +++ b/dronekit/test/sitl/test_channels.py @@ -0,0 +1,156 @@ +import time +from dronekit import connect +from dronekit.test import with_sitl +from nose.tools import assert_equals + + +def assert_readback(vehicle, values): + i = 10 + while i > 0: + time.sleep(.1) + i -= .1 + for k, v in values.items(): + if vehicle.channels[k] != v: + continue + break + if i <= 0: + raise Exception('Did not match in channels readback %s' % values) + + +@with_sitl +def test_timeout(connpath): + vehicle = connect(connpath, wait_ready=True) + + assert_equals(len(vehicle.channels), 8) + assert_equals(len(vehicle.channels.overrides), 8) + + assert_equals(sorted(vehicle.channels.keys()), [str(x) for x in range(1, 9)]) + assert_equals(sorted(vehicle.channels.overrides.keys()), []) + + assert_equals(type(vehicle.channels['1']), int) + assert_equals(type(vehicle.channels['2']), int) + assert_equals(type(vehicle.channels['3']), int) + assert_equals(type(vehicle.channels['4']), int) + assert_equals(type(vehicle.channels['5']), int) + assert_equals(type(vehicle.channels['6']), int) + assert_equals(type(vehicle.channels['7']), int) + assert_equals(type(vehicle.channels['8']), int) + assert_equals(type(vehicle.channels[1]), int) + assert_equals(type(vehicle.channels[2]), int) + assert_equals(type(vehicle.channels[3]), int) + assert_equals(type(vehicle.channels[4]), int) + assert_equals(type(vehicle.channels[5]), int) + assert_equals(type(vehicle.channels[6]), int) + assert_equals(type(vehicle.channels[7]), int) + assert_equals(type(vehicle.channels[8]), int) + + vehicle.channels.overrides = {'1': 1010} + assert_readback(vehicle, {'1': 1010}) + + vehicle.channels.overrides = {'2': 1020} + assert_readback(vehicle, {'1': 1500, '2': 1010}) + + vehicle.channels.overrides['1'] = 1010 + assert_readback(vehicle, {'1': 1010, '2': 1020}) + + del vehicle.channels.overrides['1'] + assert_readback(vehicle, {'1': 1500, '2': 1020}) + + vehicle.channels.overrides = {'1': 1010, '2': None} + assert_readback(vehicle, {'1': 1010, '2': 1500}) + + vehicle.channels.overrides['1'] = None + assert_readback(vehicle, {'1': 1500, '2': 1500}) + + # test + try: + vehicle.channels['9'] + assert False, "Can read over end of channels" + except: + pass + + try: + vehicle.channels['0'] + assert False, "Can read over start of channels" + except: + pass + + try: + vehicle.channels['1'] = 200 + assert False, "can write a channel value" + except: + pass + + # Set Ch1 to 100 using braces syntax + vehicle.channels.overrides = {'1': 1000} + assert_readback(vehicle, {'1': 1000}) + + # Set Ch2 to 200 using bracket + vehicle.channels.overrides['2'] = 200 + assert_readback(vehicle, {'1': 200, '2': 200}) + + # Set Ch2 to 1010 + vehicle.channels.overrides = {'2': 1010} + assert_readback(vehicle, {'1': 1500, '2': 1010}) + + # Set Ch3,4,5,6,7 to 300,400-700 respectively + vehicle.channels.overrides = {'3': 300, '4': 400, '5': 500, '6': 600, '7': 700} + assert_readback(vehicle, {'3': 300, '4': 400, '5': 500, '6': 600, '7': 700}) + + # Set Ch8 to 800 using braces + vehicle.channels.overrides = {'8': 800} + assert_readback(vehicle, {'8': 800}) + + # Set Ch8 to 800 using brackets + vehicle.channels.overrides['8'] = 810 + assert_readback(vehicle, {'8': 810}) + + try: + # Try to write channel 9 override to a value with brackets + vehicle.channels.overrides['9'] = 900 + assert False, "can write channels.overrides 9" + except: + pass + + try: + # Try to write channel 9 override to a value with braces + vehicle.channels.overrides = {'9': 900} + assert False, "can write channels.overrides 9 with braces" + except: + pass + + # Clear channel 3 using brackets + vehicle.channels.overrides['3'] = None + assert '3' not in vehicle.channels.overrides, 'overrides hould not contain None' + + # Clear channel 2 using braces + vehicle.channels.overrides = {'2': None} + assert '2' not in vehicle.channels.overrides, 'overrides hould not contain None' + + # Clear all channels + vehicle.channels.overrides = {} + assert_equals(len(vehicle.channels.overrides.keys()), 0) + + # Set Ch2 to 33, clear channel 6 + vehicle.channels.overrides = {'2': 33, '6': None} + assert_readback(vehicle, {'2': 33, '6': 1500}) + assert_equals(list(vehicle.channels.overrides.keys()), ['2']) + + # Callbacks + result = {'success': False} + vehicle.channels.overrides = {} + + def channels_callback(vehicle, name, channels): + if channels['3'] == 55: + result['success'] = True + + vehicle.add_attribute_listener('channels', channels_callback) + vehicle.channels.overrides = {'3': 55} + + i = 5 + while not result['success'] and i > 0: + time.sleep(.1) + i -= 1 + assert result['success'], 'channels callback should be invoked.' + + vehicle.close() diff --git a/dronekit/test/sitl/test_earlyattrs.py b/dronekit/test/sitl/test_earlyattrs.py new file mode 100644 index 000000000..3d48a1140 --- /dev/null +++ b/dronekit/test/sitl/test_earlyattrs.py @@ -0,0 +1,19 @@ +from dronekit import connect +from dronekit.test import with_sitl +from nose.tools import assert_equals, assert_not_equals + + +@with_sitl +def test_battery_none(connpath): + vehicle = connect(connpath, _initialize=False) + + # Ensure we can get (possibly unpopulated) battery object without throwing error. + assert_equals(vehicle.battery, None) + + vehicle.initialize() + + # Ensure we can get battery object without throwing error. + vehicle.wait_ready('battery') + assert_not_equals(vehicle.battery, None) + + vehicle.close() diff --git a/dronekit/test/sitl/test_errprinter.py b/dronekit/test/sitl/test_errprinter.py new file mode 100644 index 000000000..ce14c0af2 --- /dev/null +++ b/dronekit/test/sitl/test_errprinter.py @@ -0,0 +1,34 @@ +import logging +import time + +from nose.tools import assert_true + +from dronekit import connect +from dronekit.test import with_sitl + + +@with_sitl +def test_115(connpath): + """Provide a custom status_printer function to the Vehicle and check that + the autopilot messages are correctly logged. + """ + + logging_check = {'ok': False} + + def errprinter_fn(msg): + if isinstance(msg, str) and "APM:Copter" in msg: + logging_check['ok'] = True + + vehicle = connect(connpath, wait_ready=False, status_printer=errprinter_fn) + + i = 5 + while not logging_check['ok'] and i > 0: + time.sleep(1) + i -= 1 + + assert_true(logging_check['ok']) + vehicle.close() + + # Cleanup the logger + autopilotLogger = logging.getLogger('autopilot') + autopilotLogger.removeHandler(autopilotLogger.handlers[0]) diff --git a/tests/sitl/test_goto.py b/dronekit/test/sitl/test_goto.py similarity index 56% rename from tests/sitl/test_goto.py rename to dronekit/test/sitl/test_goto.py index 1d6273299..ebe06d24d 100644 --- a/tests/sitl/test_goto.py +++ b/dronekit/test/sitl/test_goto.py @@ -1,80 +1,68 @@ """ simple_goto.py: GUIDED mode "simple goto" example (Copter Only) -The example demonstrates how to arm and takeoff in Copter and how to navigate to -points using Vehicle.commands.goto. +The example demonstrates how to arm and takeoff in Copter and how to navigate to +points using Vehicle.simple_goto. Full documentation is provided at http://python.dronekit.io/examples/simple_goto.html """ import time -from dronekit import connect -from dronekit.lib import VehicleMode, Location -from dronekit.tools import with_sitl -from pymavlink import mavutil +from dronekit import connect, VehicleMode, LocationGlobalRelative +from dronekit.test import with_sitl from nose.tools import assert_equals + @with_sitl def test_goto(connpath): - vehicle = connect(connpath, await_params=True) + vehicle = connect(connpath, wait_ready=True) # NOTE these are *very inappropriate settings* # to make on a real vehicle. They are leveraged # exclusively for simulation. Take heed!!! - vehicle.parameters['ARMING_CHECK'] = 0 - vehicle.parameters['FS_THR_ENABLE'] = 0 vehicle.parameters['FS_GCS_ENABLE'] = 0 - vehicle.parameters['EKF_CHECK_THRESH'] = 0 + vehicle.parameters['FS_EKF_THRESH'] = 100 def arm_and_takeoff(aTargetAltitude): """ Arms vehicle and fly to aTargetAltitude. """ - # print "Basic pre-arm checks" # Don't let the user try to fly autopilot is booting - if vehicle.mode.name == "INITIALISING": - # print "Waiting for vehicle to initialise" - time.sleep(1) - while vehicle.gps_0.fix_type < 2: - # print "Waiting for GPS...:", vehicle.gps_0.fix_type + i = 60 + while not vehicle.is_armable and i > 0: time.sleep(1) - - # print "Arming motors" - # Copter should arm in GUIDED mode - vehicle.mode = VehicleMode("GUIDED") - vehicle.flush() + i = i - 1 + assert_equals(vehicle.is_armable, True) + # Copter should arm in GUIDED mode + vehicle.mode = VehicleMode("GUIDED") i = 60 while vehicle.mode.name != 'GUIDED' and i > 0: # print " Waiting for guided %s seconds..." % (i,) time.sleep(1) i = i - 1 + assert_equals(vehicle.mode.name, 'GUIDED') + # Arm copter. vehicle.armed = True - vehicle.flush() - i = 60 while not vehicle.armed and vehicle.mode.name == 'GUIDED' and i > 0: # print " Waiting for arming %s seconds..." % (i,) time.sleep(1) i = i - 1 + assert_equals(vehicle.armed, True) - # Failure will result in arming but immediately landing - assert vehicle.armed - assert_equals(vehicle.mode.name, 'GUIDED') - - # print "Taking off!" - vehicle.commands.takeoff(aTargetAltitude) # Take off to target altitude - vehicle.flush() + # Take off to target altitude + vehicle.simple_takeoff(aTargetAltitude) # Wait until the vehicle reaches a safe height before # processing the goto (otherwise the command after - # Vehicle.commands.takeoff will execute immediately). + # Vehicle.simple_takeoff will execute immediately). while True: - # print " Altitude: ", vehicle.location.alt + # print " Altitude: ", vehicle.location.global_relative_frame.alt # Test for altitude just below target, in case of undershoot. - if vehicle.location.alt >= aTargetAltitude * 0.95: + if vehicle.location.global_relative_frame.alt >= aTargetAltitude * 0.95: # print "Reached target altitude" break @@ -84,21 +72,20 @@ def arm_and_takeoff(aTargetAltitude): arm_and_takeoff(10) # print "Going to first point..." - point1 = Location(-35.361354, 149.165218, 20, is_relative=True) - vehicle.commands.goto(point1) - vehicle.flush() + point1 = LocationGlobalRelative(-35.361354, 149.165218, 20) + vehicle.simple_goto(point1) # sleep so we can see the change in map time.sleep(3) # print "Going to second point..." - point2 = Location(-35.363244, 149.168801, 20, is_relative=True) - vehicle.commands.goto(point2) - vehicle.flush() + point2 = LocationGlobalRelative(-35.363244, 149.168801, 20) + vehicle.simple_goto(point2) # sleep so we can see the change in map time.sleep(3) # print "Returning to Launch" vehicle.mode = VehicleMode("RTL") - vehicle.flush() + + vehicle.close() diff --git a/dronekit/test/sitl/test_locations.py b/dronekit/test/sitl/test_locations.py new file mode 100644 index 000000000..be48cc7a6 --- /dev/null +++ b/dronekit/test/sitl/test_locations.py @@ -0,0 +1,87 @@ +import time +from dronekit import connect, VehicleMode +from dronekit.test import with_sitl, wait_for +from nose.tools import assert_equals, assert_not_equals + + +@with_sitl +def test_timeout(connpath): + vehicle = connect(connpath, wait_ready=True) + + # NOTE these are *very inappropriate settings* + # to make on a real vehicle. They are leveraged + # exclusively for simulation. Take heed!!! + vehicle.parameters['FS_GCS_ENABLE'] = 0 + vehicle.parameters['FS_EKF_THRESH'] = 100 + + def arm_and_takeoff(aTargetAltitude): + """ + Arms vehicle and fly to aTargetAltitude. + """ + + # Don't let the user try to fly autopilot is booting + wait_for(lambda: vehicle.is_armable, 60) + assert_equals(vehicle.is_armable, True) + + # Copter should arm in GUIDED mode + vehicle.mode = VehicleMode("GUIDED") + wait_for(lambda: vehicle.mode.name == 'GUIDED', 60) + assert_equals(vehicle.mode.name, 'GUIDED') + + # Arm copter. + vehicle.armed = True + wait_for(lambda: vehicle.armed, 60) + assert_equals(vehicle.armed, True) + + # Take off to target altitude + vehicle.simple_takeoff(aTargetAltitude) + + # Wait until the vehicle reaches a safe height before + # processing the goto (otherwise the command after + # Vehicle.simple_takeoff will execute immediately). + while True: + # print " Altitude: ", vehicle.location.alt + # Test for altitude just below target, in case of undershoot. + if vehicle.location.global_frame.alt >= aTargetAltitude * 0.95: + # print "Reached target altitude" + break + + assert_equals(vehicle.mode.name, 'GUIDED') + time.sleep(1) + + arm_and_takeoff(10) + vehicle.wait_ready('location.local_frame', timeout=60) + + # .north, .east, and .down are initialized to None. + # Any other value suggests that a LOCAL_POSITION_NED was received and parsed. + assert_not_equals(vehicle.location.local_frame.north, None) + assert_not_equals(vehicle.location.local_frame.east, None) + assert_not_equals(vehicle.location.local_frame.down, None) + + # global_frame + assert_not_equals(vehicle.location.global_frame.lat, None) + assert_not_equals(vehicle.location.global_frame.lon, None) + assert_not_equals(vehicle.location.global_frame.alt, None) + assert_equals(type(vehicle.location.global_frame.lat), float) + assert_equals(type(vehicle.location.global_frame.lon), float) + assert_equals(type(vehicle.location.global_frame.alt), float) + + vehicle.close() + + +@with_sitl +def test_location_notify(connpath): + vehicle = connect(connpath) + + ret = {'success': False} + + @vehicle.location.on_attribute('global_frame') + def callback(*args): + assert_not_equals(args[2].alt, 0) + ret['success'] = True + + wait_for(lambda: ret['success'], 30) + + assert ret['success'], 'Expected location object to emit notifications.' + + vehicle.close() diff --git a/dronekit/test/sitl/test_mavlink.py b/dronekit/test/sitl/test_mavlink.py new file mode 100644 index 000000000..006240981 --- /dev/null +++ b/dronekit/test/sitl/test_mavlink.py @@ -0,0 +1,30 @@ +import time +from dronekit import connect +from dronekit.mavlink import MAVConnection +from dronekit.test import with_sitl + + +@with_sitl +def test_mavlink(connpath): + vehicle = connect(connpath, wait_ready=True) + out = MAVConnection('udpin:localhost:15668') + vehicle._handler.pipe(out) + out.start() + + vehicle2 = connect('udpout:localhost:15668', wait_ready=True) + + result = {'success': False} + + @vehicle2.on_attribute('location') + def callback(*args): + result['success'] = True + + i = 20 + while not result['success'] and i > 0: + time.sleep(1) + i -= 1 + + assert result['success'] + + vehicle2.close() + vehicle.close() diff --git a/dronekit/test/sitl/test_mode_settings.py b/dronekit/test/sitl/test_mode_settings.py new file mode 100644 index 000000000..7c777f7ac --- /dev/null +++ b/dronekit/test/sitl/test_mode_settings.py @@ -0,0 +1,15 @@ +from dronekit import connect +from dronekit.test import with_sitl +from nose.tools import assert_equals + + +@with_sitl +def test_modes_set(connpath): + vehicle = connect(connpath) + + def listener(self, name, m): + assert_equals('STABILIZE', self._flightmode) + + vehicle.add_message_listener('HEARTBEAT', listener) + + vehicle.close() diff --git a/dronekit/test/sitl/test_modeavailable.py b/dronekit/test/sitl/test_modeavailable.py new file mode 100644 index 000000000..f95298f69 --- /dev/null +++ b/dronekit/test/sitl/test_modeavailable.py @@ -0,0 +1,20 @@ +""" +Simple test to trigger a bug in Vehicle class: issue #610 fixed in PR #611 +""" + +from dronekit import connect +from dronekit.test import with_sitl + + +@with_sitl +def test_timeout(connpath): + v = connect(connpath) + + # Set the vehicle and autopilot type to 'unsupported' types that MissionPlanner uses as of 17.Apr.2016 + v._vehicle_type = 6 + v._autopilot_type = 8 + + # The above types trigger 'TypeError: argument of type 'NoneType' is not iterable' which is addressed in issue #610 + v._is_mode_available(0) + + v.close() diff --git a/dronekit/test/sitl/test_parameters.py b/dronekit/test/sitl/test_parameters.py new file mode 100644 index 000000000..bdfc5c3b8 --- /dev/null +++ b/dronekit/test/sitl/test_parameters.py @@ -0,0 +1,63 @@ +import time +from dronekit import connect +from dronekit.test import with_sitl +from nose.tools import assert_equals, assert_not_equals + + +@with_sitl +def test_parameters(connpath): + vehicle = connect(connpath) + + # When called on startup, parameter (may!) be none. + # assert_equals(vehicle.parameters.get('THR_MIN', wait_ready=False), None) + + # With wait_ready, it should not be none. + assert_not_equals(vehicle.parameters.get('THR_MIN', wait_ready=True), None) + + try: + assert_not_equals(vehicle.parameters['THR_MIN'], None) + except: + assert False + + # Garbage value after all parameters are downloaded should be None. + assert_equals(vehicle.parameters.get('xXx_extreme_garbage_value_xXx', wait_ready=True), None) + + vehicle.close() + + +@with_sitl +def test_iterating(connpath): + vehicle = connect(connpath, wait_ready=True) + + # Iterate over parameters. + for k, v in vehicle.parameters.items(): + break + for key in vehicle.parameters: + break + + vehicle.close() + + +@with_sitl +def test_setting(connpath): + vehicle = connect(connpath, wait_ready=True) + + assert_not_equals(vehicle.parameters['THR_MIN'], None) + + result = {'success': False} + + @vehicle.parameters.on_attribute('THR_MIN') + def listener(self, name, value): + result['success'] = (name == 'THR_MIN' and value == 3.000) + + vehicle.parameters['THR_MIN'] = 3.000 + + # Wait a bit. + i = 5 + while not result['success'] and i > 0: + time.sleep(1) + i = i - 1 + + assert_equals(result['success'], True) + + vehicle.close() diff --git a/dronekit/test/sitl/test_reboot.py b/dronekit/test/sitl/test_reboot.py new file mode 100644 index 000000000..bf9236c8e --- /dev/null +++ b/dronekit/test/sitl/test_reboot.py @@ -0,0 +1,29 @@ +from nose.tools import assert_equal + +from dronekit import connect +from dronekit.test import with_sitl +import time + + +@with_sitl +def test_reboot(connpath): + """Tries to reboot the vehicle, and checks that the autopilot ACKs the command.""" + + vehicle = connect(connpath, wait_ready=True) + + reboot_acks = [] + + def on_ack(self, name, message): + if message.command == 246: # reboot/shutdown + reboot_acks.append(message) + + vehicle.add_message_listener('COMMAND_ACK', on_ack) + vehicle.reboot() + time.sleep(0.5) + vehicle.remove_message_listener('COMMAND_ACK', on_ack) + + assert_equal(1, len(reboot_acks)) # one and only one ACK + assert_equal(246, reboot_acks[0].command) # for the correct command + assert_equal(0, reboot_acks[0].result) # the result must be successful + + vehicle.close() diff --git a/dronekit/test/sitl/test_sensor_calibration.py b/dronekit/test/sitl/test_sensor_calibration.py new file mode 100644 index 000000000..d80aa2343 --- /dev/null +++ b/dronekit/test/sitl/test_sensor_calibration.py @@ -0,0 +1,95 @@ +from pymavlink import mavutil + +from dronekit import connect +from dronekit.test import with_sitl + +from dronekit.test.sitl import assert_command_ack + + +@with_sitl +def test_gyro_calibration(connpath): + """Request gyroscope calibration, and check for the COMMAND_ACK.""" + + vehicle = connect(connpath, wait_ready=True) + + with assert_command_ack(vehicle, mavutil.mavlink.MAV_CMD_PREFLIGHT_CALIBRATION, timeout=30): + vehicle.send_calibrate_gyro() + + vehicle.close() + + +@with_sitl +def test_magnetometer_calibration(connpath): + """Request magnetometer calibration, and check for the COMMAND_ACK.""" + + vehicle = connect(connpath, wait_ready=True) + + with assert_command_ack( + vehicle, + mavutil.mavlink.MAV_CMD_DO_START_MAG_CAL, + timeout=30, + ack_result=mavutil.mavlink.MAV_RESULT_UNSUPPORTED, # TODO: change when APM is upgraded + ): + vehicle.send_calibrate_magnetometer() + + vehicle.close() + + +@with_sitl +def test_simple_accelerometer_calibration(connpath): + """Request simple accelerometer calibration, and check for the COMMAND_ACK.""" + + vehicle = connect(connpath, wait_ready=True) + + with assert_command_ack( + vehicle, + mavutil.mavlink.MAV_CMD_PREFLIGHT_CALIBRATION, + timeout=30, + ack_result=mavutil.mavlink.MAV_RESULT_FAILED, + ): + vehicle.send_calibrate_accelerometer(simple=True) + + vehicle.close() + + +@with_sitl +def test_accelerometer_calibration(connpath): + """Request accelerometer calibration, and check for the COMMAND_ACK.""" + + vehicle = connect(connpath, wait_ready=True) + + # The calibration is expected to fail because in the SITL we don't tilt the Vehicle. + # We just check that the command isn't denied or unsupported. + with assert_command_ack( + vehicle, + mavutil.mavlink.MAV_CMD_PREFLIGHT_CALIBRATION, + timeout=30, + ack_result=mavutil.mavlink.MAV_RESULT_FAILED, + ): + vehicle.send_calibrate_accelerometer(simple=False) + + vehicle.close() + + +@with_sitl +def test_board_level_calibration(connpath): + """Request board level calibration, and check for the COMMAND_ACK.""" + + vehicle = connect(connpath, wait_ready=True) + + with assert_command_ack(vehicle, mavutil.mavlink.MAV_CMD_PREFLIGHT_CALIBRATION, timeout=30): + vehicle.send_calibrate_vehicle_level() + + vehicle.close() + + +@with_sitl +def test_barometer_calibration(connpath): + """Request barometer calibration, and check for the COMMAND_ACK.""" + + vehicle = connect(connpath, wait_ready=True) + + with assert_command_ack(vehicle, mavutil.mavlink.MAV_CMD_PREFLIGHT_CALIBRATION, timeout=30): + vehicle.send_calibrate_barometer() + + vehicle.close() diff --git a/tests/sitl/test_simpledemo.py b/dronekit/test/sitl/test_simpledemo.py similarity index 65% rename from tests/sitl/test_simpledemo.py rename to dronekit/test/sitl/test_simpledemo.py index 760f66866..d79dda11b 100644 --- a/tests/sitl/test_simpledemo.py +++ b/dronekit/test/sitl/test_simpledemo.py @@ -3,27 +3,28 @@ Feel free to copy and modify at your leisure. """ -from dronekit import connect -from dronekit.lib import VehicleMode -from dronekit.tools import with_sitl -from pymavlink import mavutil -import time -import sys -import os +from dronekit import connect, VehicleMode +from dronekit.test import with_sitl from nose.tools import assert_equals + # This test runs first! @with_sitl def test_parameter(connpath): - v = connect(connpath, await_params=True) + v = connect(connpath, wait_ready=True) # Perform a simple parameter check assert_equals(type(v.parameters['THR_MIN']), float) + v.close() + + # This test runs second. Add as many tests as you like @with_sitl def test_mode(connpath): - v = connect(connpath, await_params=True) + v = connect(connpath, wait_ready=True) # Ensure Mode is an instance of VehicleMode assert isinstance(v.mode, VehicleMode) + + v.close() diff --git a/dronekit/test/sitl/test_state.py b/dronekit/test/sitl/test_state.py new file mode 100644 index 000000000..b53889548 --- /dev/null +++ b/dronekit/test/sitl/test_state.py @@ -0,0 +1,13 @@ +from dronekit import connect, SystemStatus +from dronekit.test import with_sitl +from nose.tools import assert_equals + + +@with_sitl +def test_state(connpath): + vehicle = connect(connpath, wait_ready=['system_status']) + + assert_equals(type(vehicle.system_status), SystemStatus) + assert_equals(type(vehicle.system_status.state), str) + + vehicle.close() diff --git a/dronekit/test/sitl/test_timeout.py b/dronekit/test/sitl/test_timeout.py new file mode 100644 index 000000000..11b99baf1 --- /dev/null +++ b/dronekit/test/sitl/test_timeout.py @@ -0,0 +1,41 @@ +import time +import socket +from dronekit import connect +from dronekit.test import with_sitl +from nose.tools import assert_equals + + +@with_sitl +def test_timeout(connpath): + # Connect with timeout of 10s. + vehicle = connect(connpath, wait_ready=True, heartbeat_timeout=20) + + # Stall input. + vehicle._handler._accept_input = False + + start = time.time() + while vehicle._handler._alive and time.time() - start < 30: + time.sleep(.1) + + assert_equals(vehicle._handler._alive, False) + + vehicle.close() + + +def test_timeout_empty(): + # Create a dummy server. + s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + s.bind(('127.0.0.1', 5760)) + s.listen(1) + + try: + # Connect with timeout of 10s. + vehicle = connect('tcp:127.0.0.1:5760', wait_ready=True, heartbeat_timeout=20) + + vehicle.close() + + # Should not pass + assert False + except: + pass diff --git a/dronekit/test/sitl/test_vehicleclass.py b/dronekit/test/sitl/test_vehicleclass.py new file mode 100644 index 000000000..ddc1054be --- /dev/null +++ b/dronekit/test/sitl/test_vehicleclass.py @@ -0,0 +1,25 @@ +import time +from dronekit import connect, Vehicle +from dronekit.test import with_sitl + + +class DummyVehicle(Vehicle): + def __init__(self, *args): + super(DummyVehicle, self).__init__(*args) + + self.success = False + + def success_fn(self, name, m): + self.success = True + + self.add_message_listener('HEARTBEAT', success_fn) + + +@with_sitl +def test_timeout(connpath): + v = connect(connpath, vehicle_class=DummyVehicle) + + while not v.success: + time.sleep(0.1) + + v.close() diff --git a/dronekit/test/sitl/test_waypoints.py b/dronekit/test/sitl/test_waypoints.py new file mode 100644 index 000000000..af594ca1b --- /dev/null +++ b/dronekit/test/sitl/test_waypoints.py @@ -0,0 +1,144 @@ +import time +from dronekit import connect, LocationGlobal, Command +from pymavlink import mavutil +from dronekit.test import with_sitl +from nose.tools import assert_not_equals, assert_equals + + +@with_sitl +def test_empty_clear(connpath): + vehicle = connect(connpath) + + # Calling clear() on an empty object should not crash. + vehicle.commands.clear() + vehicle.commands.upload() + + assert_equals(len(vehicle.commands), 0) + + vehicle.close() + + +@with_sitl +def test_set_home(connpath): + vehicle = connect(connpath, wait_ready=True) + + # Wait for home position to be real and not 0, 0, 0 + # once we request it via cmds.download() + time.sleep(10) + vehicle.commands.download() + vehicle.commands.wait_ready() + assert_not_equals(vehicle.home_location, None) + + # Note: If the GPS values differ heavily from EKF values, this command + # will basically fail silently. This GPS coordinate is tailored for that + # the with_sitl initializer uses to not fail. + vehicle.home_location = LocationGlobal(-35, 149, 600) + vehicle.commands.download() + vehicle.commands.wait_ready() + + assert_equals(vehicle.home_location.lat, -35) + assert_equals(vehicle.home_location.lon, 149) + assert_equals(vehicle.home_location.alt, 600) + + vehicle.close() + + +@with_sitl +def test_parameter(connpath): + vehicle = connect(connpath, wait_ready=True) + + # Home should be None at first. + assert_equals(vehicle.home_location, None) + + # Wait for home position to be real and not 0, 0, 0 + # once we request it via cmds.download() + time.sleep(10) + + # Initial + vehicle.commands.download() + vehicle.commands.wait_ready() + assert_equals(len(vehicle.commands), 0) + assert_not_equals(vehicle.home_location, None) + + # Save home for comparison. + home = vehicle.home_location + + # After clearing + vehicle.commands.clear() + vehicle.commands.upload() + vehicle.commands.download() + vehicle.commands.wait_ready() + assert_equals(len(vehicle.commands), 0) + + # Upload + for command in [ + Command(0, 0, 0, 0, 16, 1, 1, 0.0, 0.0, 0.0, 0.0, -35.3605, 149.172363, 747.0), + Command(0, 0, 0, 3, 22, 0, 1, 0.0, 0.0, 0.0, 0.0, -35.359831, 149.166334, 100.0), + Command(0, 0, 0, 3, 16, 0, 1, 0.0, 0.0, 0.0, 0.0, -35.363489, 149.167213, 100.0), + Command(0, 0, 0, 3, 16, 0, 1, 0.0, 0.0, 0.0, 0.0, -35.355491, 149.169595, 100.0), + Command(0, 0, 0, 3, 16, 0, 1, 0.0, 0.0, 0.0, 0.0, -35.355071, 149.175839, 100.0), + Command(0, 0, 0, 3, 113, 0, 1, 0.0, 0.0, 0.0, 0.0, -35.362666, 149.178715, 22222.0), + Command(0, 0, 0, 3, 115, 0, 1, 2.0, 22.0, 1.0, 3.0, 0.0, 0.0, 0.0), + Command(0, 0, 0, 3, 16, 0, 1, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0), + ]: + vehicle.commands.add(command) + vehicle.commands.upload() + + # After upload + vehicle.commands.download() + vehicle.commands.wait_ready() + assert_equals(len(vehicle.commands), 8) + + # Test iteration. + count = 0 + for cmd in vehicle.commands: + assert_not_equals(cmd, None) + count += 1 + assert_equals(count, 8) + + # Test slicing + count = 3 + for cmd in vehicle.commands[2:5]: + assert_not_equals(cmd, None) + assert_equals(cmd.seq, count) + count += 1 + assert_equals(count, 6) + + # Test next property + assert_equals(vehicle.commands.next, 0) + vehicle.commands.next = 3 + while vehicle.commands.next != 3: + time.sleep(0.1) + assert_equals(vehicle.commands.next, 3) + + # Home should be preserved + assert_equals(home.lat, vehicle.home_location.lat) + assert_equals(home.lon, vehicle.home_location.lon) + assert_equals(home.alt, vehicle.home_location.alt) + + vehicle.close() + + +@with_sitl +def test_227(connpath): + """ + Tests race condition when downloading items + """ + + vehicle = connect(connpath, wait_ready=True) + + def assert_commands(count): + vehicle.commands.download() + vehicle.commands.wait_ready() + assert_equals(len(vehicle.commands), count) + + assert_commands(0) + + vehicle.commands.add(Command(0, 0, 0, mavutil.mavlink.MAV_FRAME_GLOBAL_RELATIVE_ALT, + mavutil.mavlink.MAV_CMD_NAV_WAYPOINT, 0, 0, 0, 0, 0, 0, 10, 10, + 10)) + vehicle.flush() # Send commands + + assert_commands(1) + + vehicle.close() diff --git a/dronekit/module/__init__.py b/dronekit/test/unit/__init__.py similarity index 100% rename from dronekit/module/__init__.py rename to dronekit/test/unit/__init__.py diff --git a/dronekit/test/unit/test_api.py b/dronekit/test/unit/test_api.py new file mode 100644 index 000000000..1ce8190dd --- /dev/null +++ b/dronekit/test/unit/test_api.py @@ -0,0 +1,10 @@ +from dronekit import VehicleMode +from nose.tools import assert_equals, assert_not_equals + + +def test_vehicle_mode_eq(): + assert_equals(VehicleMode('GUIDED'), VehicleMode('GUIDED')) + + +def test_vehicle_mode_neq(): + assert_not_equals(VehicleMode('AUTO'), VehicleMode('GUIDED')) diff --git a/dronekit/tools/__init__.py b/dronekit/tools/__init__.py deleted file mode 100644 index 14f3f48ab..000000000 --- a/dronekit/tools/__init__.py +++ /dev/null @@ -1,23 +0,0 @@ -import os -from nose.tools import assert_equals, with_setup -from dronekit.sitl import SITL - -sitl = SITL('copter', '3.3-rc5') -sitl_args = ['-I0', '-S', '--model', 'quad', '--home=-35.363261,149.165230,584,353'] - -if 'SITL_SPEEDUP' in os.environ: - sitl_args += ['--speedup', str(os.environ['SITL_SPEEDUP'])] -if 'SITL_RATE' in os.environ: - sitl_args += ['-r', str(os.environ['SITL_RATE'])] - -def setup_sitl(): - sitl.launch(sitl_args, await_ready=True, restart=True) - -def teardown_sitl(): - sitl.stop() - -def with_sitl(fn): - @with_setup(setup_sitl, teardown_sitl) - def test(*args, **kargs): - return fn('tcp:127.0.0.1:5760', *args, **kargs) - return test diff --git a/dronekit/util.py b/dronekit/util.py new file mode 100644 index 000000000..4b7bd43a6 --- /dev/null +++ b/dronekit/util.py @@ -0,0 +1,25 @@ +from __future__ import print_function + +import logging +import sys + + +def errprinter(*args): + logger(*args) + + +def logger(*args): + print(*args, file=sys.stderr) + sys.stderr.flush() + + +class ErrprinterHandler(logging.Handler): + """Logging handler to support the deprecated `errprinter` argument to connect()""" + + def __init__(self, errprinter): + logging.Handler.__init__(self) + self.errprinter = errprinter + + def emit(self, record): + msg = self.format(record) + self.errprinter(msg) diff --git a/examples/channel_overrides/channel_overrides.py b/examples/channel_overrides/channel_overrides.py new file mode 100644 index 000000000..358a7ea51 --- /dev/null +++ b/examples/channel_overrides/channel_overrides.py @@ -0,0 +1,104 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +© Copyright 2015-2016, 3D Robotics. + +channel_overrides.py: + +Demonstrates how set and clear channel-override information. + +# NOTE: +Channel overrides (a.k.a "RC overrides") are highly discommended (they are primarily implemented +for simulating user input and when implementing certain types of joystick control). + +They are provided for development purposes. Please raise an issue explaining why you need them +and we will try to find a better alternative: https://github.com/dronekit/dronekit-python/issues + +Full documentation is provided at http://python.dronekit.io/examples/channel_overrides.html +""" +from __future__ import print_function +from dronekit import connect + + +#Set up option parsing to get connection string +import argparse +parser = argparse.ArgumentParser(description='Example showing how to set and clear vehicle channel-override information.') +parser.add_argument('--connect', + help="vehicle connection target string. If not specified, SITL automatically started and used.") +args = parser.parse_args() + +connection_string = args.connect +sitl = None + + +#Start SITL if no connection string specified +if not connection_string: + import dronekit_sitl + sitl = dronekit_sitl.start_default() + connection_string = sitl.connection_string() + + +# Connect to the Vehicle +print('Connecting to vehicle on: %s' % connection_string) +vehicle = connect(connection_string, wait_ready=True) + +# Get all original channel values (before override) +print("Channel values from RC Tx:", vehicle.channels) + +# Access channels individually +print("Read channels individually:") +print(" Ch1: %s" % vehicle.channels['1']) +print(" Ch2: %s" % vehicle.channels['2']) +print(" Ch3: %s" % vehicle.channels['3']) +print(" Ch4: %s" % vehicle.channels['4']) +print(" Ch5: %s" % vehicle.channels['5']) +print(" Ch6: %s" % vehicle.channels['6']) +print(" Ch7: %s" % vehicle.channels['7']) +print(" Ch8: %s" % vehicle.channels['8']) +print("Number of channels: %s" % len(vehicle.channels)) + + +# Override channels +print("\nChannel overrides: %s" % vehicle.channels.overrides) + +print("Set Ch2 override to 200 (indexing syntax)") +vehicle.channels.overrides['2'] = 200 +print(" Channel overrides: %s" % vehicle.channels.overrides) +print(" Ch2 override: %s" % vehicle.channels.overrides['2']) + +print("Set Ch3 override to 300 (dictionary syntax)") +vehicle.channels.overrides = {'3':300} +print(" Channel overrides: %s" % vehicle.channels.overrides) + +print("Set Ch1-Ch8 overrides to 110-810 respectively") +vehicle.channels.overrides = {'1': 110, '2': 210,'3': 310,'4':4100, '5':510,'6':610,'7':710,'8':810} +print(" Channel overrides: %s" % vehicle.channels.overrides) + + +# Clear override by setting channels to None +print("\nCancel Ch2 override (indexing syntax)") +vehicle.channels.overrides['2'] = None +print(" Channel overrides: %s" % vehicle.channels.overrides) + +print("Clear Ch3 override (del syntax)") +del vehicle.channels.overrides['3'] +print(" Channel overrides: %s" % vehicle.channels.overrides) + +print("Clear Ch5, Ch6 override and set channel 3 to 500 (dictionary syntax)") +vehicle.channels.overrides = {'5':None, '6':None,'3':500} +print(" Channel overrides: %s" % vehicle.channels.overrides) + +print("Clear all overrides") +vehicle.channels.overrides = {} +print(" Channel overrides: %s" % vehicle.channels.overrides) + +#Close vehicle object before exiting script +print("\nClose vehicle object") +vehicle.close() + +# Shut down simulator if it was started. +if sitl is not None: + sitl.stop() + +print("Completed") diff --git a/examples/create_attribute/create_attribute.py b/examples/create_attribute/create_attribute.py new file mode 100644 index 000000000..5d8a20e15 --- /dev/null +++ b/examples/create_attribute/create_attribute.py @@ -0,0 +1,67 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +© Copyright 2015-2016, 3D Robotics. + +create_attribute.py: + +Demonstrates how to create attributes from MAVLink messages within your DroneKit-Python script +and use them in the same way as the built-in Vehicle attributes. + +The code adds a new attribute to the Vehicle class, populating it with information from RAW_IMU messages +intercepted using the message_listener decorator. + +Full documentation is provided at http://python.dronekit.io/examples/create_attribute.html +""" +from __future__ import print_function + +from dronekit import connect, Vehicle +from my_vehicle import MyVehicle #Our custom vehicle class +import time + + + +#Set up option parsing to get connection string +import argparse +parser = argparse.ArgumentParser(description='Demonstrates how to create attributes from MAVLink messages. ') +parser.add_argument('--connect', + help="Vehicle connection target string. If not specified, SITL automatically started and used.") +args = parser.parse_args() + +connection_string = args.connect +sitl = None + + +#Start SITL if no connection string specified +if not connection_string: + import dronekit_sitl + sitl = dronekit_sitl.start_default() + connection_string = sitl.connection_string() + + +# Connect to the Vehicle +print('Connecting to vehicle on: %s' % connection_string) +vehicle = connect(connection_string, wait_ready=True, vehicle_class=MyVehicle) + +# Add observer for the custom attribute + +def raw_imu_callback(self, attr_name, value): + # attr_name == 'raw_imu' + # value == vehicle.raw_imu + print(value) + +vehicle.add_attribute_listener('raw_imu', raw_imu_callback) + +print('Display RAW_IMU messages for 5 seconds and then exit.') +time.sleep(5) + +#The message listener can be unset using ``vehicle.remove_message_listener`` + +#Close vehicle object before exiting script +print("Close vehicle object") +vehicle.close() + +# Shut down simulator if it was started. +if sitl is not None: + sitl.stop() diff --git a/examples/create_attribute/my_vehicle.py b/examples/create_attribute/my_vehicle.py new file mode 100644 index 000000000..9b129f937 --- /dev/null +++ b/examples/create_attribute/my_vehicle.py @@ -0,0 +1,90 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +© Copyright 2015-2016, 3D Robotics. + +my_vehicle.py: + +Custom Vehicle subclass to add IMU data. +""" + +from dronekit import Vehicle + + +class RawIMU(object): + """ + The RAW IMU readings for the usual 9DOF sensor setup. + This contains the true raw values without any scaling to allow data capture and system debugging. + + The message definition is here: https://mavlink.io/en/messages/common.html#RAW_IMU + + :param time_boot_us: Timestamp (microseconds since system boot). #Note, not milliseconds as per spec + :param xacc: X acceleration (mg) + :param yacc: Y acceleration (mg) + :param zacc: Z acceleration (mg) + :param xgyro: Angular speed around X axis (millirad /sec) + :param ygyro: Angular speed around Y axis (millirad /sec) + :param zgyro: Angular speed around Z axis (millirad /sec) + :param xmag: X Magnetic field (milli tesla) + :param ymag: Y Magnetic field (milli tesla) + :param zmag: Z Magnetic field (milli tesla) + """ + def __init__(self, time_boot_us=None, xacc=None, yacc=None, zacc=None, xygro=None, ygyro=None, zgyro=None, xmag=None, ymag=None, zmag=None): + """ + RawIMU object constructor. + """ + self.time_boot_us = time_boot_us + self.xacc = xacc + self.yacc = yacc + self.zacc = zacc + self.xgyro = zgyro + self.ygyro = ygyro + self.zgyro = zgyro + self.xmag = xmag + self.ymag = ymag + self.zmag = zmag + + def __str__(self): + """ + String representation used to print the RawIMU object. + """ + return "RAW_IMU: time_boot_us={},xacc={},yacc={},zacc={},xgyro={},ygyro={},zgyro={},xmag={},ymag={},zmag={}".format(self.time_boot_us, self.xacc, self.yacc,self.zacc,self.xgyro,self.ygyro,self.zgyro,self.xmag,self.ymag,self.zmag) + + +class MyVehicle(Vehicle): + def __init__(self, *args): + super(MyVehicle, self).__init__(*args) + + # Create an Vehicle.raw_imu object with initial values set to None. + self._raw_imu = RawIMU() + + # Create a message listener using the decorator. + @self.on_message('RAW_IMU') + def listener(self, name, message): + """ + The listener is called for messages that contain the string specified in the decorator, + passing the vehicle, message name, and the message. + + The listener writes the message to the (newly attached) ``vehicle.raw_imu`` object + and notifies observers. + """ + self._raw_imu.time_boot_us=message.time_usec + self._raw_imu.xacc=message.xacc + self._raw_imu.yacc=message.yacc + self._raw_imu.zacc=message.zacc + self._raw_imu.xgyro=message.xgyro + self._raw_imu.ygyro=message.ygyro + self._raw_imu.zgyro=message.zgyro + self._raw_imu.xmag=message.xmag + self._raw_imu.ymag=message.ymag + self._raw_imu.zmag=message.zmag + + # Notify all observers of new message (with new value) + # Note that argument `cache=False` by default so listeners + # are updated with every new message + self.notify_attribute_listeners('raw_imu', self._raw_imu) + + @property + def raw_imu(self): + return self._raw_imu diff --git a/examples/drone_delivery/drone_delivery.py b/examples/drone_delivery/drone_delivery.py index da141aa57..10848877c 100644 --- a/examples/drone_delivery/drone_delivery.py +++ b/examples/drone_delivery/drone_delivery.py @@ -1,14 +1,43 @@ -import os, os.path -import simplejson +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +© Copyright 2015-2016, 3D Robotics. +drone_delivery.py: + +A CherryPy based web application that displays a mapbox map to let you view the current vehicle position and send the vehicle commands to fly to a particular latitude and longitude. -from pymavlink import mavutil -import dronekit.lib -from dronekit.lib import VehicleMode, Location +Full documentation is provided at http://python.dronekit.io/examples/drone_delivery.html +""" + +from __future__ import print_function +import os +import simplejson +import time +from dronekit import connect, VehicleMode, LocationGlobal, LocationGlobalRelative import cherrypy -from cherrypy.process import wspbus, plugins from jinja2 import Environment, FileSystemLoader +# Set up option parsing to get connection string +import argparse +parser = argparse.ArgumentParser(description='Creates a CherryPy based web application that displays a mapbox map to let you view the current vehicle position and send the vehicle commands to fly to a particular latitude and longitude. Will start and connect to SITL if no connection string specified.') +parser.add_argument('--connect', + help="vehicle connection target string. If not specified, SITL is automatically started and used.") +args = parser.parse_args() + +connection_string = args.connect + +# Start SITL if no connection string specified +if not connection_string: + import dronekit_sitl + sitl = dronekit_sitl.start_default() + connection_string = sitl.connection_string() + +local_path = os.path.dirname(os.path.abspath(__file__)) +print("local path: %s" % local_path) + + cherrypy_conf = { '/': { 'tools.sessions.on': True, @@ -20,126 +49,127 @@ } } + class Drone(object): - def __init__(self, home_coords, server_enabled=True): - self.api = local_connect() + def __init__(self, server_enabled=True): self.gps_lock = False self.altitude = 30.0 - self.vehicle = self.api.get_vehicles()[0] + + # Connect to the Vehicle + self._log('Connected to vehicle.') + self.vehicle = vehicle self.commands = self.vehicle.commands self.current_coords = [] - self.home_coords = home_coords self.webserver_enabled = server_enabled self._log("DroneDelivery Start") # Register observers - self.vehicle.add_attribute_observer('armed', self.armed_callback) - self.vehicle.add_attribute_observer('location', self.location_callback) - #self.vehicle.add_attribute_observer('mode', self.mode_callback) - self.vehicle.add_attribute_observer('gps_0', self.gps_callback) + self.vehicle.add_attribute_listener('location', self.location_callback) - self._log("Waiting for GPS Lock") + def launch(self): + self._log("Waiting for location...") + while self.vehicle.location.global_frame.lat == 0: + time.sleep(0.1) + self.home_coords = [self.vehicle.location.global_frame.lat, + self.vehicle.location.global_frame.lon] - def takeoff(self): - self._log("Taking off") - self.commands.takeoff(30.0) - self.vehicle.flush() - - def arm(self, toggle=True): - if toggle: - self._log("Arming") - else: - self._log("Disarming") - self.vehicle.armed = True - self.vehicle.flush() + self._log("Waiting for ability to arm...") + while not self.vehicle.is_armable: + time.sleep(.1) - def run(self): self._log('Running initial boot sequence') + self.change_mode('GUIDED') self.arm() self.takeoff() - self.change_mode('GUIDED') if self.webserver_enabled is True: self._run_server() + def takeoff(self): + self._log("Taking off") + self.vehicle.simple_takeoff(30.0) + + def arm(self, value=True): + if value: + self._log('Waiting for arming...') + self.vehicle.armed = True + while not self.vehicle.armed: + time.sleep(.1) + else: + self._log("Disarming!") + self.vehicle.armed = False + def _run_server(self): # Start web server if enabled cherrypy.tree.mount(DroneDelivery(self), '/', config=cherrypy_conf) - cherrypy.config.update({ - 'server.socket_port': 8080, - 'server.socket_host': '0.0.0.0', - 'log.screen': None - }) + cherrypy.config.update({'server.socket_port': 8080, + 'server.socket_host': '0.0.0.0', + 'log.screen': None}) + print('''Server is bound on all addresses, port 8080 +You may connect to it using your web broser using a URL looking like this: +http://localhost:8080/ +''') cherrypy.engine.start() def change_mode(self, mode): - self._log("Mode: {0}".format(mode)) + self._log("Changing to mode: {0}".format(mode)) self.vehicle.mode = VehicleMode(mode) - self.vehicle.flush() + while self.vehicle.mode.name != mode: + self._log(' ... polled mode: {0}'.format(mode)) + time.sleep(1) def goto(self, location, relative=None): self._log("Goto: {0}, {1}".format(location, self.altitude)) - self.commands.goto( - Location( - float(location[0]), float(location[1]), - float(self.altitude), - is_relative=relative + if relative: + self.vehicle.simple_goto( + LocationGlobalRelative( + float(location[0]), float(location[1]), + float(self.altitude) + ) + ) + else: + self.vehicle.simple_goto( + LocationGlobal( + float(location[0]), float(location[1]), + float(self.altitude) + ) ) - ) self.vehicle.flush() def get_location(self): return [self.current_location.lat, self.current_location.lon] - def location_callback(self, location): - location = self.vehicle.location - - if location.alt is not None: - self.altitude = location.alt + def location_callback(self, vehicle, name, location): + if location.global_relative_frame.alt is not None: + self.altitude = location.global_relative_frame.alt - self.current_location = location - - def armed_callback(self, armed): - self._log("DroneDelivery Armed Callback") - self.vehicle.remove_attribute_observer('armed', self.armed_callback) - - def mode_callback(self, mode): - self._log("Mode: {0}".format(self.vehicle.mode)) - - def gps_callback(self, gps): - self._log("GPS: {0}".format(self.vehicle.gps_0)) - if self.gps_lock is False: - self.gps_lock = True - self.vehicle.remove_attribute_observer('gps_0', self.gps_callback) - self.run() + self.current_location = location.global_relative_frame def _log(self, message): - print "[DEBUG]: {0}".format(message) + print("[DEBUG]: {0}".format(message)) + class Templates: def __init__(self, home_coords): self.home_coords = home_coords self.options = self.get_options() - self.environment = Environment(loader=FileSystemLoader( local_path + '/html')) + self.environment = Environment(loader=FileSystemLoader(local_path + '/html')) def get_options(self): - return { - 'width': 670, + return {'width': 670, 'height': 470, 'zoom': 13, 'format': 'png', - 'access_token': 'pk.eyJ1IjoibXJwb2xsbyIsImEiOiJtUG0tRk9BIn0.AqAiefUV9fFYRo-w0jFR1Q', - 'mapid': 'mrpollo.kfbnjbl0', + 'access_token': 'pk.eyJ1Ijoia2V2aW4zZHIiLCJhIjoiY2lrOGoxN2s2MDJzYnR6a3drbTYwdGxmMiJ9.bv5u7QgmcJd6dZfLDGoykw', + 'mapid': 'kevin3dr.n56ffjoo', 'home_coords': self.home_coords, - 'menu': [ - {'name': 'Home', 'location': '/'}, - {'name': 'Track', 'location': '/track'}, - {'name': 'Command', 'location': '/command'} - ], + 'menu': [{'name': 'Home', 'location': '/'}, + {'name': 'Track', 'location': '/track'}, + {'name': 'Command', 'location': '/command'}], 'current_url': '/', 'json': '' } @@ -163,9 +193,10 @@ def command(self, current_coords): return self.get_template('command') def get_template(self, file_name): - template = self.environment.get_template( file_name + '.html') + template = self.environment.get_template(file_name + '.html') return template.render(options=self.options) + class DroneDelivery(object): def __init__(self, drone): self.drone = drone @@ -193,5 +224,17 @@ def track(self, lat=None, lon=None): return self.templates.track(self.drone.get_location()) -Drone([32.5738, -117.0068]) + +# Connect to the Vehicle +print('Connecting to vehicle on: %s' % connection_string) +vehicle = connect(connection_string, wait_ready=True) + +print('Launching Drone...') +Drone().launch() + +print('Waiting for cherrypy engine...') cherrypy.engine.block() + +if not args.connect: + # Shut down simulator if it was started. + sitl.stop() diff --git a/examples/flight_replay/flight.tlog b/examples/flight_replay/flight.tlog new file mode 100644 index 000000000..d2c7563d7 Binary files /dev/null and b/examples/flight_replay/flight.tlog differ diff --git a/examples/flight_replay/flight_replay.py b/examples/flight_replay/flight_replay.py index f496ddb71..a7564abd1 100644 --- a/examples/flight_replay/flight_replay.py +++ b/examples/flight_replay/flight_replay.py @@ -1,110 +1,230 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + """ +© Copyright 2015-2016, 3D Robotics. flight_replay.py: -An example of talking to Droneshare to receive a past flight, and then 'replaying' -that flight by sending waypoints to a vehicle. - -Start this example as shown below, specifying the -missionid (a numeric mission number from a public droneshare flight): - - api start flight_replay.py +This example requests a past flight from Droneshare, and then 'replays' +the flight by sending waypoints to a vehicle. Full documentation is provided at http://python.dronekit.io/examples/flight_replay.html """ +from __future__ import print_function -from dronekit.lib import Command +from dronekit import connect, Command, VehicleMode, LocationGlobalRelative from pymavlink import mavutil import json, urllib, math - -api_server = "https://api.3drobotics.com" -api_key = "a8948c11.9e44351f6c0aa7e3e2ff6d00b14a71c5" - -# First get an instance of the API endpoint -api = local_connect() -# Get our vehicle - when running with MAVProxy it only knows about one vehicle (for now) -v = api.get_vehicles()[0] - - -def _decode_list(data): - """A utility function for decoding JSON strings from unicode""" - rv = [] - for item in data: - if isinstance(item, unicode): - item = item.encode('utf-8') - elif isinstance(item, list): - item = _decode_list(item) - elif isinstance(item, dict): - item = _decode_dict(item) - rv.append(item) - return rv - -def _decode_dict(data): - """A utility function for decoding JSON strings from Unicode""" - rv = {} - for key, value in data.iteritems(): - if isinstance(key, unicode): - key = key.encode('utf-8') - if isinstance(value, unicode): - value = value.encode('utf-8') - elif isinstance(value, list): - value = _decode_list(value) - elif isinstance(value, dict): - value = _decode_dict(value) - rv[key] = value - return rv - -def download_messages(mission_id, max_freq = 1.0): - """Download a public mission from droneshare (as JSON)""" - f = urllib.urlopen("%s/api/v1/mission/%s/messages.json?max_freq=%s&api_key=%s" % (api_server, mission_id, max_freq, api_key)) - j = json.load(f, object_hook=_decode_dict) - f.close() - return j - -def replay_mission(payload): - """Given mission JSON, set a series of wpts approximating the previous flight""" +import time + +#Set up option parsing to get connection string +import argparse +parser = argparse.ArgumentParser(description='Load a telemetry log and use position data to create mission waypoints for a vehicle. Connects to SITL on local PC by default.') +parser.add_argument('--connect', help="vehicle connection target.") +parser.add_argument('--tlog', default='flight.tlog', + help="Telemetry log containing path to replay") +args = parser.parse_args() + + +def get_distance_metres(aLocation1, aLocation2): + """ + Returns the ground distance in metres between two LocationGlobal objects. + + This method is an approximation, and will not be accurate over large distances and close to the + earth's poles. It comes from the ArduPilot test code: + https://github.com/diydrones/ardupilot/blob/master/Tools/autotest/common.py + """ + dlat = aLocation2.lat - aLocation1.lat + dlong = aLocation2.lon - aLocation1.lon + return math.sqrt((dlat*dlat) + (dlong*dlong)) * 1.113195e5 + + + +def distance_to_current_waypoint(): + """ + Gets distance in metres to the current waypoint. + It returns None for the first waypoint (Home location). + """ + nextwaypoint = vehicle.commands.next + if nextwaypoint==0: + return None + missionitem=vehicle.commands[nextwaypoint-1] #commands are zero indexed + lat = missionitem.x + lon = missionitem.y + alt = missionitem.z + targetWaypointLocation = LocationGlobalRelative(lat,lon,alt) + distancetopoint = get_distance_metres(vehicle.location.global_frame, targetWaypointLocation) + return distancetopoint + +def position_messages_from_tlog(filename): + """ + Given telemetry log, get a series of wpts approximating the previous flight + """ # Pull out just the global position msgs - messages = payload['messages'] - messages = filter(lambda obj: obj['typ'] == 'MAVLINK_MSG_ID_GLOBAL_POSITION_INT', messages) - messages = map(lambda obj: obj['fld'], messages) - - # Shrink the # of pts to be lower than the max # of wpts allowed by vehicle + messages = [] + mlog = mavutil.mavlink_connection(filename) + while True: + try: + m = mlog.recv_match(type=['GLOBAL_POSITION_INT']) + if m is None: + break + except Exception: + break + # ignore we get where there is no fix: + if m.lat == 0: + continue + messages.append(m) + + # Shrink the number of points for readability and to stay within autopilot memory limits. + # For coding simplicity we: + # - only keep points that are with 3 metres of the previous kept point. + # - only keep the first 100 points that meet the above criteria. num_points = len(messages) - max_points = 99 - if num_points > max_points: - step = int(math.ceil((float(num_points) / max_points))) - shorter = [messages[i] for i in xrange(0, num_points, step)] - messages = shorter - - print "Generating %s waypoints from replay..." % len(messages) - cmds = v.commands - cmds.clear() - v.flush() - for i in xrange(0, len(messages)): - pt = messages[i] - lat = pt['lat'] - lon = pt['lon'] - # To prevent accidents we don't trust the altitude in the original flight, instead - # we just put in a conservative cruising altitude. - altitude = 30.0 # pt['alt'] - cmd = Command( 0, - 0, - 0, - mavutil.mavlink.MAV_FRAME_GLOBAL_RELATIVE_ALT, - mavutil.mavlink.MAV_CMD_NAV_WAYPOINT, - 0, 0, 0, 0, 0, 0, - lat, lon, altitude) - cmds.add(cmd) - v.flush() - -if len(local_arguments) != 1: - print 'Error: usage "api start flight_replay.py "' + keep_point_distance=3 #metres + kept_messages = [] + kept_messages.append(messages[0]) #Keep the first message + pt1num=0 + pt2num=1 + while True: + #Keep the last point. Only record 99 points. + if pt2num==num_points-1 or len(kept_messages)==99: + kept_messages.append(messages[pt2num]) + break + pt1 = LocationGlobalRelative(messages[pt1num].lat/1.0e7,messages[pt1num].lon/1.0e7,0) + pt2 = LocationGlobalRelative(messages[pt2num].lat/1.0e7,messages[pt2num].lon/1.0e7,0) + distance_between_points = get_distance_metres(pt1,pt2) + if distance_between_points > keep_point_distance: + kept_messages.append(messages[pt2num]) + pt1num=pt2num + pt2num=pt2num+1 + + return kept_messages + + +def arm_and_takeoff(aTargetAltitude): + """ + Arms vehicle and fly to aTargetAltitude. + """ + + # Don't try to arm until autopilot is ready + while not vehicle.is_armable: + print(" Waiting for vehicle to initialise...") + time.sleep(1) + + # Set mode to GUIDED for arming and takeoff: + while (vehicle.mode.name != "GUIDED"): + vehicle.mode = VehicleMode("GUIDED") + time.sleep(0.1) + + # Confirm vehicle armed before attempting to take off + while not vehicle.armed: + vehicle.armed = True + print(" Waiting for arming...") + time.sleep(1) + + print(" Taking off!") + vehicle.simple_takeoff(aTargetAltitude) # Take off to target altitude + + # Wait until the vehicle reaches a safe height + # before allowing next command to process. + while True: + requiredAlt = aTargetAltitude*0.95 + #Break and return from function just below target altitude. + if vehicle.location.global_relative_frame.alt>=requiredAlt: + print(" Reached target altitude of ~%f" % (aTargetAltitude)) + break + print(" Altitude: %f < %f" % (vehicle.location.global_relative_frame.alt, + requiredAlt)) + time.sleep(1) + + +print("Generating waypoints from tlog...") +messages = position_messages_from_tlog(args.tlog) +print(" Generated %d waypoints from tlog" % len(messages)) +if len(messages) == 0: + print("No position messages found in log") + exit(0) + +#Start SITL if no connection string specified +if args.connect: + connection_string = args.connect + sitl = None else: - # Now download the vehicle waypoints - cmds = v.commands - cmds.wait_valid() - - mission_id = int(local_arguments[0]) - max_freq = 0.1 - json = download_messages(mission_id, max_freq) - print "JSON downloaded..." - replay_mission(json) + start_lat = messages[0].lat/1.0e7 + start_lon = messages[0].lon/1.0e7 + + import dronekit_sitl + sitl = dronekit_sitl.start_default(lat=start_lat,lon=start_lon) + connection_string = sitl.connection_string() + +# Connect to the Vehicle +print('Connecting to vehicle on: %s' % connection_string) +vehicle = connect(connection_string, wait_ready=True) + + +# Now download the vehicle waypoints +cmds = vehicle.commands +cmds.wait_ready() + + +cmds = vehicle.commands +cmds.clear() +for pt in messages: + #print "Point: %d %d" % (pt.lat, pt.lon,) + lat = pt.lat + lon = pt.lon + # To prevent accidents we don't trust the altitude in the original flight, instead + # we just put in a conservative cruising altitude. + altitude = 30.0 + cmd = Command( 0, + 0, + 0, + mavutil.mavlink.MAV_FRAME_GLOBAL_RELATIVE_ALT, + mavutil.mavlink.MAV_CMD_NAV_WAYPOINT, + 0, 0, 0, 0, 0, 0, + lat/1.0e7, lon/1.0e7, altitude) + cmds.add(cmd) + +#Upload clear message and command messages to vehicle. +print("Uploading %d waypoints to vehicle..." % len(messages)) +cmds.upload() + +print("Arm and Takeoff") +arm_and_takeoff(30) + + +print("Starting mission") + +# Reset mission set to first (0) waypoint +vehicle.commands.next=0 + +# Set mode to AUTO to start mission: +while (vehicle.mode.name != "AUTO"): + vehicle.mode = VehicleMode("AUTO") + time.sleep(0.1) + +# Monitor mission for 60 seconds then RTL and quit: +time_start = time.time() +while time.time() - time_start < 60: + nextwaypoint=vehicle.commands.next + print('Distance to waypoint (%s): %s' % (nextwaypoint, distance_to_current_waypoint())) + + if nextwaypoint==len(messages): + print("Exit 'standard' mission when start heading to final waypoint") + break; + time.sleep(1) + +print('Return to launch') +while (vehicle.mode.name != "RTL"): + vehicle.mode = VehicleMode("RTL") + time.sleep(0.1) + +#Close vehicle object before exiting script +print("Close vehicle object") +vehicle.close() + +# Shut down simulator if it was started. +if sitl is not None: + sitl.stop() + +print("Completed...") diff --git a/examples/follow_me/follow_me.py b/examples/follow_me/follow_me.py index 8dd2b51be..ef1227bce 100644 --- a/examples/follow_me/follow_me.py +++ b/examples/follow_me/follow_me.py @@ -1,65 +1,123 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +© Copyright 2015-2016, 3D Robotics. +followme - Tracks GPS position of your computer (Linux only). + +This example uses the python gps package to read positions from a GPS attached to your +laptop and sends a new vehicle.simple_goto command every two seconds to move the +vehicle to the current point. + +When you want to stop follow-me, either change vehicle modes or type Ctrl+C to exit the script. + +Example documentation: http://python.dronekit.io/examples/follow_me.html +""" +from __future__ import print_function + +from dronekit import connect, VehicleMode, LocationGlobalRelative import gps import socket import time -from dronekit.lib import VehicleMode, Location +import sys -def followme(): - """ - followme - A DroneAPI example +#Set up option parsing to get connection string +import argparse +parser = argparse.ArgumentParser(description='Tracks GPS position of your computer (Linux only).') +parser.add_argument('--connect', + help="vehicle connection target string. If not specified, SITL automatically started and used.") +args = parser.parse_args() + +connection_string = args.connect +sitl = None - This is a somewhat more 'meaty' example on how to use the DroneAPI. It uses the - python gps package to read positions from the GPS attached to your laptop an - every two seconds it sends a new goto command to the vehicle. - To use this example: - * Run mavproxy.py with the correct options to connect to your vehicle - * module load api - * api start +#Start SITL if no connection string specified +if not connection_string: + import dronekit_sitl + sitl = dronekit_sitl.start_default() + connection_string = sitl.connection_string() - When you want to stop follow-me, either change vehicle modes from your RC - transmitter or type "api stop". +# Connect to the Vehicle +print('Connecting to vehicle on: %s' % connection_string) +vehicle = connect(connection_string, wait_ready=True, timeout=300) + + + +def arm_and_takeoff(aTargetAltitude): + """ + Arms vehicle and fly to aTargetAltitude. """ - try: - # First get an instance of the API endpoint (the connect via web case will be similar) - api = local_connect() - - # Now get our vehicle (we assume the user is trying to control the first vehicle attached to the GCS) - v = api.get_vehicles()[0] - - # Don't let the user try to fly while the board is still booting - if v.mode.name == "INITIALISING": - print "Vehicle still booting, try again later" - return - - cmds = v.commands - is_guided = False # Have we sent at least one destination point? - - # Use the python gps package to access the laptop GPS - gpsd = gps.gps(mode=gps.WATCH_ENABLE) - - while not api.exit: - # This is necessary to read the GPS state from the laptop - gpsd.next() - - if is_guided and v.mode.name != "GUIDED": - print "User has changed flight modes - aborting follow-me" - break - - # Once we have a valid location (see gpsd documentation) we can start moving our vehicle around - if (gpsd.valid & gps.LATLON_SET) != 0: - altitude = 30 # in meters - dest = Location(gpsd.fix.latitude, gpsd.fix.longitude, altitude, is_relative=True) - print "Going to: %s" % dest - - # A better implementation would only send new waypoints if the position had changed significantly - cmds.goto(dest) - is_guided = True - v.flush() - - # Send a new target every two seconds - # For a complete implementation of follow me you'd want adjust this delay - time.sleep(2) - except socket.error: - print "Error: gpsd service does not seem to be running, plug in USB GPS or run run-fake-gps.sh" - -followme() + + print("Basic pre-arm checks") + # Don't let the user try to arm until autopilot is ready + while not vehicle.is_armable: + print(" Waiting for vehicle to initialise...") + time.sleep(1) + + + print("Arming motors") + # Copter should arm in GUIDED mode + vehicle.mode = VehicleMode("GUIDED") + vehicle.armed = True + + while not vehicle.armed: + print(" Waiting for arming...") + time.sleep(1) + + print("Taking off!") + vehicle.simple_takeoff(aTargetAltitude) # Take off to target altitude + + # Wait until the vehicle reaches a safe height before processing the goto (otherwise the command + # after Vehicle.simple_takeoff will execute immediately). + while True: + print(" Altitude: ", vehicle.location.global_relative_frame.alt) + if vehicle.location.global_relative_frame.alt>=aTargetAltitude*0.95: #Trigger just below target alt. + print("Reached target altitude") + break + time.sleep(1) + + + +try: + # Use the python gps package to access the laptop GPS + gpsd = gps.gps(mode=gps.WATCH_ENABLE) + + #Arm and take off to altitude of 5 meters + arm_and_takeoff(5) + + while True: + + if vehicle.mode.name != "GUIDED": + print("User has changed flight modes - aborting follow-me") + break + + # Read the GPS state from the laptop + next(gpsd) + + # Once we have a valid location (see gpsd documentation) we can start moving our vehicle around + if (gpsd.valid & gps.LATLON_SET) != 0: + altitude = 30 # in meters + dest = LocationGlobalRelative(gpsd.fix.latitude, gpsd.fix.longitude, altitude) + print("Going to: %s" % dest) + + # A better implementation would only send new waypoints if the position had changed significantly + vehicle.simple_goto(dest) + + # Send a new target every two seconds + # For a complete implementation of follow me you'd want adjust this delay + time.sleep(2) + +except socket.error: + print("Error: gpsd service does not seem to be running, plug in USB GPS or run run-fake-gps.sh") + sys.exit(1) + +#Close vehicle object before exiting script +print("Close vehicle object") +vehicle.close() + +# Shut down simulator if it was started. +if sitl is not None: + sitl.stop() + +print("Completed") diff --git a/examples/gcs/microgcs.py b/examples/gcs/microgcs.py index 6b1ed878d..a3d33aaca 100644 --- a/examples/gcs/microgcs.py +++ b/examples/gcs/microgcs.py @@ -1,36 +1,52 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +© Copyright 2015-2016, 3D Robotics. +""" +from __future__ import print_function # # This is a small example of the python drone API - an ultra minimal GCS -# Usage: -# * mavproxy.py --master=/dev/ttyACM0,115200 -# * module load api -# * api start microgcs.py # -from dronekit.lib import VehicleMode + +from dronekit import connect, VehicleMode from pymavlink import mavutil from Tkinter import * # The tkinter root object global root -# First get an instance of the API endpoint -api = local_connect() -# get our vehicle - when running with mavproxy it only knows about one vehicle (for now) -v = api.get_vehicles()[0] +#Set up option parsing to get connection string +import argparse +parser = argparse.ArgumentParser(description='Tracks GPS position of your computer (Linux only). Connects to SITL on local PC by default.') +parser.add_argument('--connect', + help="vehicle connection target.") +args = parser.parse_args() + +connection_string = args.connect +sitl = None + +#Start SITL if no connection string specified +if not connection_string: + import dronekit_sitl + sitl = dronekit_sitl.start_default() + connection_string = sitl.connection_string() + +# Connect to the Vehicle +print('Connecting to vehicle on: %s' % connection_string) +vehicle = connect(connection_string, wait_ready=True) def setMode(mode): # Now change the vehicle into auto mode - v.mode = VehicleMode(mode) + vehicle.mode = VehicleMode(mode) - # Always call flush to guarantee that previous writes to the vehicle have taken place - v.flush() def updateGUI(label, value): label['text'] = value def addObserverAndInit(name, cb): """We go ahead and call our observer once at startup to get an initial value""" - cb(name) - v.add_attribute_observer(name, cb) + vehicle.add_attribute_listener(name, cb) root = Tk() root.wm_title("microGCS - the worlds crummiest GCS") @@ -44,11 +60,15 @@ def addObserverAndInit(name, cb): modeLabel = Label(frame, text = "mode") modeLabel.pack() -addObserverAndInit('attitude', lambda attr: updateGUI(attitudeLabel, v.attitude)) -addObserverAndInit('location', lambda attr: updateGUI(locationLabel, v.location)) -addObserverAndInit('mode', lambda attr: updateGUI(modeLabel, v.mode)) +addObserverAndInit('attitude', lambda vehicle, name, attitude: updateGUI(attitudeLabel, vehicle.attitude)) +addObserverAndInit('location', lambda vehicle, name, location: updateGUI(locationLabel, str(location.global_frame))) +addObserverAndInit('mode', lambda vehicle,name,mode: updateGUI(modeLabel, mode)) Button(frame, text = "Auto", command = lambda : setMode("AUTO")).pack() Button(frame, text = "RTL", command = lambda : setMode("RTL")).pack() -root.mainloop() \ No newline at end of file +root.mainloop() + +# Shut down simulator if it was started. +if sitl is not None: + sitl.stop() diff --git a/examples/guided_set_speed_yaw/guided_set_speed_yaw.py b/examples/guided_set_speed_yaw/guided_set_speed_yaw.py index 2729dfc19..3dee73025 100644 --- a/examples/guided_set_speed_yaw/guided_set_speed_yaw.py +++ b/examples/guided_set_speed_yaw/guided_set_speed_yaw.py @@ -1,52 +1,76 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + """ +© Copyright 2015-2016, 3D Robotics. +guided_set_speed_yaw.py: (Copter Only) + This example shows how to move/direct Copter and send commands in GUIDED mode using DroneKit Python. Example documentation: http://python.dronekit.io/examples/guided-set-speed-yaw-demo.html """ +from __future__ import print_function -from dronekit.lib import VehicleMode, Location -from pymavlink import mavutil +from dronekit import connect, VehicleMode, LocationGlobal, LocationGlobalRelative +from pymavlink import mavutil # Needed for command message definitions import time import math -api = local_connect() -vehicle = api.get_vehicles()[0] + +#Set up option parsing to get connection string +import argparse +parser = argparse.ArgumentParser(description='Control Copter and send commands in GUIDED mode ') +parser.add_argument('--connect', + help="Vehicle connection target string. If not specified, SITL automatically started and used.") +args = parser.parse_args() + +connection_string = args.connect +sitl = None + + +#Start SITL if no connection string specified +if not connection_string: + import dronekit_sitl + sitl = dronekit_sitl.start_default() + connection_string = sitl.connection_string() + + +# Connect to the Vehicle +print('Connecting to vehicle on: %s' % connection_string) +vehicle = connect(connection_string, wait_ready=True) def arm_and_takeoff(aTargetAltitude): """ - Arm vehicle and fly to aTargetAltitude. + Arms vehicle and fly to aTargetAltitude. """ - print "Basic pre-arm checks" - # Don't let the user try to fly while autopilot is booting - if vehicle.mode.name == "INITIALISING": - print "Waiting for vehicle to initialise" - time.sleep(1) - while vehicle.gps_0.fix_type < 2: - print "Waiting for GPS...:", vehicle.gps_0.fix_type + + print("Basic pre-arm checks") + # Don't let the user try to arm until autopilot is ready + while not vehicle.is_armable: + print(" Waiting for vehicle to initialise...") time.sleep(1) - - print "Arming motors" + + + print("Arming motors") # Copter should arm in GUIDED mode - vehicle.mode = VehicleMode("GUIDED") - vehicle.armed = True - vehicle.flush() + vehicle.mode = VehicleMode("GUIDED") + vehicle.armed = True - while not vehicle.armed and not api.exit: - print " Waiting for arming..." + while not vehicle.armed: + print(" Waiting for arming...") time.sleep(1) - print "Taking off!" - vehicle.commands.takeoff(aTargetAltitude) # Take off to target altitude - vehicle.flush() + print("Taking off!") + vehicle.simple_takeoff(aTargetAltitude) # Take off to target altitude # Wait until the vehicle reaches a safe height before processing the goto (otherwise the command - # after Vehicle.commands.takeoff will execute immediately). - while not api.exit: - print " Altitude: ", vehicle.location.alt - if vehicle.location.alt>=aTargetAltitude*0.95: #Just below target, in case of undershoot. - print "Reached target altitude" - break; + # after Vehicle.simple_takeoff will execute immediately). + while True: + print(" Altitude: ", vehicle.location.global_relative_frame.alt) + if vehicle.location.global_relative_frame.alt>=aTargetAltitude*0.95: #Trigger just below target alt. + print("Reached target altitude") + break time.sleep(1) @@ -62,7 +86,6 @@ def arm_and_takeoff(aTargetAltitude): * MAV_CMD_CONDITION_YAW - set direction of the front of the Copter (latitude, longitude) * MAV_CMD_DO_SET_ROI - set direction where the camera gimbal is aimed (latitude, longitude, altitude) * MAV_CMD_DO_CHANGE_SPEED - set target speed in metres/second. -* MAV_CMD_DO_SET_HOME - set the home location. The full set of available commands are listed here: @@ -75,18 +98,18 @@ def condition_yaw(heading, relative=False): This method sets an absolute heading by default, but you can set the `relative` parameter to `True` to set yaw relative to the current yaw heading. - + By default the yaw of the vehicle will follow the direction of travel. After setting the yaw using this function there is no way to return to the default yaw "follow direction of travel" behaviour (https://github.com/diydrones/ardupilot/issues/2427) - + For more information see: http://copter.ardupilot.com/wiki/common-mavlink-mission-command-messages-mav_cmd/#mav_cmd_condition_yaw """ if relative: - is_relative=1 #yaw relative to direction of travel + is_relative = 1 #yaw relative to direction of travel else: - is_relative=0 #yaw is an absolute angle + is_relative = 0 #yaw is an absolute angle # create the CONDITION_YAW command using command_long_encode() msg = vehicle.message_factory.command_long_encode( 0, 0, # target system, target component @@ -99,12 +122,12 @@ def condition_yaw(heading, relative=False): 0, 0, 0) # param 5 ~ 7 not used # send command to vehicle vehicle.send_mavlink(msg) - vehicle.flush() def set_roi(location): """ - Send MAV_CMD_DO_SET_ROI message to point camera gimbal at a specified region of interest (Location). + Send MAV_CMD_DO_SET_ROI message to point camera gimbal at a + specified region of interest (LocationGlobal). The vehicle may also turn to face the ROI. For more information see: @@ -122,62 +145,8 @@ def set_roi(location): ) # send command to vehicle vehicle.send_mavlink(msg) - vehicle.flush() -def set_speed(speed): - """ - Send MAV_CMD_DO_CHANGE_SPEED to change the current speed when travelling to a point. - - In AC3.2.1 Copter will accelerate to this speed near the centre of its journey and then - decelerate as it reaches the target. In AC3.3 the speed changes immediately. - - This method is only useful when controlling the vehicle using position/goto commands. - It is not needed when controlling vehicle movement using velocity components. - - For more information see: - http://copter.ardupilot.com/common-mavlink-mission-command-messages-mav_cmd/#mav_cmd_do_change_speed - """ - # create the MAV_CMD_DO_CHANGE_SPEED command - msg = vehicle.message_factory.command_long_encode( - 0, 0, # target system, target component - mavutil.mavlink.MAV_CMD_DO_CHANGE_SPEED, #command - 0, #confirmation - 0, #param 1 - speed, # speed - 0, 0, 0, 0, 0 #param 3 - 7 - ) - - # send command to vehicle - vehicle.send_mavlink(msg) - vehicle.flush() - - -def set_home(aLocation, aCurrent=1): - """ - Send MAV_CMD_DO_SET_HOME command to set the Home location to either the current location - or a specified location. - - For more information see: - http://copter.ardupilot.com/common-mavlink-mission-command-messages-mav_cmd/#mav_cmd_do_set_home - """ - # create the MAV_CMD_DO_SET_HOME command: - # http://copter.ardupilot.com/common-mavlink-mission-command-messages-mav_cmd/#mav_cmd_do_set_home - msg = vehicle.message_factory.command_long_encode( - 0, 0, # target system, target component - mavutil.mavlink.MAV_CMD_DO_SET_HOME, #command - 0, #confirmation - aCurrent, #param 1: 1 to use current position, 2 to use the entered values. - 0, 0, 0, #params 2-4 - aLocation.lat, - aLocation.lon, - aLocation.alt - ) - # send command to vehicle - vehicle.send_mavlink(msg) - vehicle.flush() - - """ Functions to make it easy to convert between the different frames-of-reference. In particular these @@ -188,15 +157,15 @@ def set_home(aLocation, aCurrent=1): to the Earth's poles. Specifically, it provides: -* get_location_metres - Get Location (decimal degrees) at distance (m) North & East of a given Location. -* get_distance_metres - Get the distance between two Location objects in metres -* get_bearing - Get the bearing in degrees to a Location +* get_location_metres - Get LocationGlobal (decimal degrees) at distance (m) North & East of a given LocationGlobal. +* get_distance_metres - Get the distance between two LocationGlobal objects in metres +* get_bearing - Get the bearing in degrees to a LocationGlobal """ def get_location_metres(original_location, dNorth, dEast): """ - Returns a Location object containing the latitude/longitude `dNorth` and `dEast` metres from the - specified `original_location`. The returned Location has the same `alt and `is_relative` values + Returns a LocationGlobal object containing the latitude/longitude `dNorth` and `dEast` metres from the + specified `original_location`. The returned LocationGlobal has the same `alt` value as `original_location`. The function is useful when you want to move the vehicle around specifying locations relative to @@ -207,7 +176,7 @@ def get_location_metres(original_location, dNorth, dEast): For more information see: http://gis.stackexchange.com/questions/2951/algorithm-for-offsetting-a-latitude-longitude-by-some-amount-of-meters """ - earth_radius=6378137.0 #Radius of "spherical" earth + earth_radius = 6378137.0 #Radius of "spherical" earth #Coordinate offsets in radians dLat = dNorth/earth_radius dLon = dEast/(earth_radius*math.cos(math.pi*original_location.lat/180)) @@ -215,26 +184,33 @@ def get_location_metres(original_location, dNorth, dEast): #New position in decimal degrees newlat = original_location.lat + (dLat * 180/math.pi) newlon = original_location.lon + (dLon * 180/math.pi) - return Location(newlat, newlon,original_location.alt,original_location.is_relative) + if type(original_location) is LocationGlobal: + targetlocation=LocationGlobal(newlat, newlon,original_location.alt) + elif type(original_location) is LocationGlobalRelative: + targetlocation=LocationGlobalRelative(newlat, newlon,original_location.alt) + else: + raise Exception("Invalid Location object passed") + + return targetlocation; def get_distance_metres(aLocation1, aLocation2): """ - Returns the ground distance in metres between two Location objects. - + Returns the ground distance in metres between two LocationGlobal objects. + This method is an approximation, and will not be accurate over large distances and close to the earth's poles. It comes from the ArduPilot test code: https://github.com/diydrones/ardupilot/blob/master/Tools/autotest/common.py """ - dlat = aLocation2.lat - aLocation1.lat - dlong = aLocation2.lon - aLocation1.lon + dlat = aLocation2.lat - aLocation1.lat + dlong = aLocation2.lon - aLocation1.lon return math.sqrt((dlat*dlat) + (dlong*dlong)) * 1.113195e5 def get_bearing(aLocation1, aLocation2): """ - Returns the bearing between the two Location objects passed as parameters. - + Returns the bearing between the two LocationGlobal objects passed as parameters. + This method is an approximation, and may not be accurate over large distances and close to the earth's poles. It comes from the ArduPilot test code: https://github.com/diydrones/ardupilot/blob/master/Tools/autotest/common.py @@ -246,7 +222,7 @@ def get_bearing(aLocation1, aLocation2): bearing += 360.00 return bearing; - + """ Functions to move the vehicle to a specified position (as opposed to controlling movement by setting velocity components). @@ -256,7 +232,7 @@ def get_bearing(aLocation1, aLocation2): MAV_FRAME_GLOBAL_RELATIVE_ALT_INT frame * goto_position_target_local_ned - Sets position using SET_POSITION_TARGET_LOCAL_NED command in MAV_FRAME_BODY_NED frame -* goto - A convenience function that can use Vehicle.commands.goto (default) or +* goto - A convenience function that can use Vehicle.simple_goto (default) or goto_position_target_global_int to travel to a specific position in metres North and East from the current location. This method reports distance to the destination. @@ -264,55 +240,51 @@ def get_bearing(aLocation1, aLocation2): def goto_position_target_global_int(aLocation): """ - Send SET_POSITION_TARGET_GLOBAL_INT command to request the vehicle fly to a specified location. - + Send SET_POSITION_TARGET_GLOBAL_INT command to request the vehicle fly to a specified LocationGlobal. + For more information see: https://pixhawk.ethz.ch/mavlink/#SET_POSITION_TARGET_GLOBAL_INT See the above link for information on the type_mask (0=enable, 1=ignore). At time of writing, acceleration and yaw bits are ignored. - """ - print 'goto_target_globalint_position' + """ msg = vehicle.message_factory.set_position_target_global_int_encode( 0, # time_boot_ms (not used) 0, 0, # target system, target component - mavutil.mavlink.MAV_FRAME_GLOBAL_RELATIVE_ALT_INT, # frame + mavutil.mavlink.MAV_FRAME_GLOBAL_RELATIVE_ALT_INT, # frame 0b0000111111111000, # type_mask (only speeds enabled) aLocation.lat*1e7, # lat_int - X Position in WGS84 frame in 1e7 * meters aLocation.lon*1e7, # lon_int - Y Position in WGS84 frame in 1e7 * meters aLocation.alt, # alt - Altitude in meters in AMSL altitude, not WGS84 if absolute or relative, above terrain if GLOBAL_TERRAIN_ALT_INT 0, # X velocity in NED frame in m/s - 0, # Y velocity in NED frame in m/s - 0, # Z velocity in NED frame in m/s + 0, # Y velocity in NED frame in m/s + 0, # Z velocity in NED frame in m/s 0, 0, 0, # afx, afy, afz acceleration (not supported yet, ignored in GCS_Mavlink) 0, 0) # yaw, yaw_rate (not supported yet, ignored in GCS_Mavlink) # send command to vehicle vehicle.send_mavlink(msg) - vehicle.flush() - + def goto_position_target_local_ned(north, east, down): """ Send SET_POSITION_TARGET_LOCAL_NED command to request the vehicle fly to a specified location in the North, East, Down frame. - + It is important to remember that in this frame, positive altitudes are entered as negative "Down" values. So if down is "10", this will be 10 metres below the home altitude. - - At time of writing the method ignores the frame value and the NED frame is relative to the - HOME Location. If you want to specify the frame in terms of the vehicle (i.e. really use - MAV_FRAME_BODY_NED) then you can translate values relative to the home position (or move - the home position if this is sensible in your context). - - For more information see: https://pixhawk.ethz.ch/mavlink/#SET_POSITION_TARGET_LOCAL_NED + + Starting from AC3.3 the method respects the frame setting. Prior to that the frame was + ignored. For more information see: + http://dev.ardupilot.com/wiki/copter-commands-in-guided-mode/#set_position_target_local_ned See the above link for information on the type_mask (0=enable, 1=ignore). At time of writing, acceleration and yaw bits are ignored. - """ + + """ msg = vehicle.message_factory.set_position_target_local_ned_encode( 0, # time_boot_ms (not used) 0, 0, # target system, target component - mavutil.mavlink.MAV_FRAME_BODY_NED, # frame + mavutil.mavlink.MAV_FRAME_LOCAL_NED, # frame 0b0000111111111000, # type_mask (only positions enabled) north, east, down, # x, y, z positions (or North, East, Down in the MAV_FRAME_BODY_NED frame 0, 0, 0, # x, y, z velocity in m/s (not used) @@ -320,32 +292,34 @@ def goto_position_target_local_ned(north, east, down): 0, 0) # yaw, yaw_rate (not supported yet, ignored in GCS_Mavlink) # send command to vehicle vehicle.send_mavlink(msg) - vehicle.flush() - -def goto(dNorth, dEast, gotoFunction=vehicle.commands.goto): + +def goto(dNorth, dEast, gotoFunction=vehicle.simple_goto): """ Moves the vehicle to a position dNorth metres North and dEast metres East of the current position. - - The method takes a function pointer argument with a single `dronekit.lib.Location` parameter for + + The method takes a function pointer argument with a single `dronekit.lib.LocationGlobal` parameter for the target position. This allows it to be called with different position-setting commands. - By default it uses the standard method: dronekit.lib.Vehicle.commands.goto(). - + By default it uses the standard method: dronekit.lib.Vehicle.simple_goto(). + The method reports the distance to target every two seconds. """ - currentLocation=vehicle.location - targetLocation=get_location_metres(currentLocation, dNorth, dEast) - targetDistance=get_distance_metres(currentLocation, targetLocation) + + currentLocation = vehicle.location.global_relative_frame + targetLocation = get_location_metres(currentLocation, dNorth, dEast) + targetDistance = get_distance_metres(currentLocation, targetLocation) gotoFunction(targetLocation) - vehicle.flush() + + #print "DEBUG: targetLocation: %s" % targetLocation + #print "DEBUG: targetLocation: %s" % targetDistance - - while not api.exit and vehicle.mode.name=="GUIDED": #Stop action if we are no longer in guided mode. - remainingDistance=get_distance_metres(vehicle.location, targetLocation) - print "Distance to target: ", remainingDistance + while vehicle.mode.name=="GUIDED": #Stop action if we are no longer in guided mode. + #print "DEBUG: mode: %s" % vehicle.mode.name + remainingDistance=get_distance_metres(vehicle.location.global_relative_frame, targetLocation) + print("Distance to target: ", remainingDistance) if remainingDistance<=targetDistance*0.01: #Just below target, in case of undershoot. - print "Reached target" + print("Reached target") break; time.sleep(2) @@ -353,19 +327,28 @@ def goto(dNorth, dEast, gotoFunction=vehicle.commands.goto): """ Functions that move the vehicle by specifying the velocity components in each direction. -The two functions use different MAVLink commands, but effectively implement the same behaviour. +The two functions use different MAVLink commands. The main difference is +that depending on the frame used, the NED velocity can be relative to the vehicle +orientation. The methods include: * send_ned_velocity - Sets velocity components using SET_POSITION_TARGET_LOCAL_NED command * send_global_velocity - Sets velocity components using SET_POSITION_TARGET_GLOBAL_INT command """ -def send_ned_velocity(velocity_x, velocity_y, velocity_z): +def send_ned_velocity(velocity_x, velocity_y, velocity_z, duration): """ - Move vehicle in direction based on specified velocity vectors. - + Move vehicle in direction based on specified velocity vectors and + for the specified duration. + This uses the SET_POSITION_TARGET_LOCAL_NED command with a type mask enabling only - velocity components (https://pixhawk.ethz.ch/mavlink/#SET_POSITION_TARGET_LOCAL_NED). + velocity components + (http://dev.ardupilot.com/wiki/copter-commands-in-guided-mode/#set_position_target_local_ned). + + Note that from AC3.3 the message should be re-sent every second (after about 3 seconds + with no message the velocity will drop back to zero). In AC3.2.1 and earlier the specified + velocity persists until it is canceled. The code below should work on either version + (sending the message multiple times does not cause problems). See the above link for information on the type_mask (0=enable, 1=ignore). At time of writing, acceleration and yaw bits are ignored. @@ -373,24 +356,33 @@ def send_ned_velocity(velocity_x, velocity_y, velocity_z): msg = vehicle.message_factory.set_position_target_local_ned_encode( 0, # time_boot_ms (not used) 0, 0, # target system, target component - mavutil.mavlink.MAV_FRAME_BODY_NED, # frame + mavutil.mavlink.MAV_FRAME_LOCAL_NED, # frame 0b0000111111000111, # type_mask (only speeds enabled) 0, 0, 0, # x, y, z positions (not used) velocity_x, velocity_y, velocity_z, # x, y, z velocity in m/s 0, 0, 0, # x, y, z acceleration (not supported yet, ignored in GCS_Mavlink) 0, 0) # yaw, yaw_rate (not supported yet, ignored in GCS_Mavlink) - # send command to vehicle - vehicle.send_mavlink(msg) - vehicle.flush() + # send command to vehicle on 1 Hz cycle + for x in range(0,duration): + vehicle.send_mavlink(msg) + time.sleep(1) + + -def send_global_velocity(velocity_x, velocity_y, velocity_z): +def send_global_velocity(velocity_x, velocity_y, velocity_z, duration): """ Move vehicle in direction based on specified velocity vectors. - + This uses the SET_POSITION_TARGET_GLOBAL_INT command with type mask enabling only - velocity components (https://pixhawk.ethz.ch/mavlink/#SET_POSITION_TARGET_GLOBAL_INT). + velocity components + (http://dev.ardupilot.com/wiki/copter-commands-in-guided-mode/#set_position_target_global_int). + + Note that from AC3.3 the message should be re-sent every second (after about 3 seconds + with no message the velocity will drop back to zero). In AC3.2.1 and earlier the specified + velocity persists until it is canceled. The code below should work on either version + (sending the message multiple times does not cause problems). See the above link for information on the type_mask (0=enable, 1=ignore). At time of writing, acceleration and yaw bits are ignored. @@ -398,31 +390,36 @@ def send_global_velocity(velocity_x, velocity_y, velocity_z): msg = vehicle.message_factory.set_position_target_global_int_encode( 0, # time_boot_ms (not used) 0, 0, # target system, target component - mavutil.mavlink.MAV_FRAME_GLOBAL_RELATIVE_ALT_INT, # frame + mavutil.mavlink.MAV_FRAME_GLOBAL_RELATIVE_ALT_INT, # frame 0b0000111111000111, # type_mask (only speeds enabled) 0, # lat_int - X Position in WGS84 frame in 1e7 * meters 0, # lon_int - Y Position in WGS84 frame in 1e7 * meters 0, # alt - Altitude in meters in AMSL altitude(not WGS84 if absolute or relative) - # altitude above terrain if GLOBAL_TERRAIN_ALT_INT + # altitude above terrain if GLOBAL_TERRAIN_ALT_INT velocity_x, # X velocity in NED frame in m/s - velocity_y, # Y velocity in NED frame in m/s - velocity_z, # Z velocity in NED frame in m/s + velocity_y, # Y velocity in NED frame in m/s + velocity_z, # Z velocity in NED frame in m/s 0, 0, 0, # afx, afy, afz acceleration (not supported yet, ignored in GCS_Mavlink) 0, 0) # yaw, yaw_rate (not supported yet, ignored in GCS_Mavlink) - # send command to vehicle - vehicle.send_mavlink(msg) - vehicle.flush() + + # send command to vehicle on 1 Hz cycle + for x in range(0,duration): + vehicle.send_mavlink(msg) + time.sleep(1) - """ -Fly a triangular path using the standard Vehicle.commands.goto() method. +Fly a triangular path using the standard Vehicle.simple_goto() method. The method is called indirectly via a custom "goto" that allows the target position to be specified as a distance in metres (North/East) from the current position, and which reports the distance-to-target. """ -print("TRIANGLE path using standard Vehicle.commands.goto()") +print("TRIANGLE path using standard Vehicle.simple_goto()") + +print("Set groundspeed to 5m/s.") +vehicle.groundspeed=5 + print("Position North 80 West 50") goto(80, -50) @@ -434,6 +431,7 @@ def send_global_velocity(velocity_x, velocity_y, velocity_z): + """ Fly a triangular path using the SET_POSITION_TARGET_GLOBAL_INT command and specifying a target position (rather than controlling movement using velocity vectors). The command is @@ -450,17 +448,17 @@ def send_global_velocity(velocity_x, velocity_y, velocity_z): print("TRIANGLE path using standard SET_POSITION_TARGET_GLOBAL_INT message and with varying speed.") print("Position South 100 West 130") -print("Set speed to 5m/s.") -set_speed(5) +print("Set groundspeed to 5m/s.") +vehicle.groundspeed = 5 goto(-100, -130, goto_position_target_global_int) -print("Set speed to 15m/s (max).") -set_speed(15) +print("Set groundspeed to 15m/s (max).") +vehicle.groundspeed = 15 print("Position South 0 East 200") goto(0, 260, goto_position_target_global_int) -print("Set speed to 10m/s (max).") -set_speed(10, goto_position_target_global_int) +print("Set airspeed to 10m/s (max).") +vehicle.airspeed = 10 print("Position North 100 West 130") goto(100, -130, goto_position_target_global_int) @@ -484,14 +482,14 @@ def send_global_velocity(velocity_x, velocity_y, velocity_z): """ print("SQUARE path using SET_POSITION_TARGET_LOCAL_NED and position parameters") -DURATION=20 #Set duration for each segment. +DURATION = 20 #Set duration for each segment. print("North 50m, East 0m, 10m altitude for %s seconds" % DURATION) goto_position_target_local_ned(50,0,-10) print("Point ROI at current location (home position)") # NOTE that this has to be called after the goto command as first "move" command of a particular type # "resets" ROI/YAW commands -set_roi(vehicle.location) +set_roi(vehicle.location.global_relative_frame) time.sleep(DURATION) print("North 50m, East 50m, 10m altitude") @@ -499,7 +497,7 @@ def send_global_velocity(velocity_x, velocity_y, velocity_z): time.sleep(DURATION) print("Point ROI at current location") -set_roi(vehicle.location) +set_roi(vehicle.location.global_relative_frame) print("North 0m, East 50m, 10m altitude") goto_position_target_local_ned(0,50,-10) @@ -519,56 +517,61 @@ def send_global_velocity(velocity_x, velocity_y, velocity_z): The code also sets the yaw (MAV_CMD_CONDITION_YAW) using the `set_yaw()` method in each segment so that the front of the vehicle points in the direction of travel -""" +""" + #Set up velocity vector to map to each direction. # vx > 0 => fly North # vx < 0 => fly South -NORTH=2 -SOUTH=-2 +NORTH = 2 +SOUTH = -2 # Note for vy: # vy > 0 => fly East # vy < 0 => fly West -EAST=2 -WEST=-2 +EAST = 2 +WEST = -2 # Note for vz: # vz < 0 => ascend # vz > 0 => descend -UP=-0.5 -DOWN=0.5 +UP = -0.5 +DOWN = 0.5 # Square path using velocity print("SQUARE path using SET_POSITION_TARGET_LOCAL_NED and velocity parameters") -print("Velocity South & up") -send_ned_velocity(SOUTH,0,UP) + print("Yaw 180 absolute (South)") condition_yaw(180) -time.sleep(DURATION) -send_ned_velocity(0,0,0) -print("Velocity West & down") -send_ned_velocity(0,WEST,DOWN) +print("Velocity South & up") +send_ned_velocity(SOUTH,0,UP,DURATION) +send_ned_velocity(0,0,0,1) + + print("Yaw 270 absolute (West)") condition_yaw(270) -time.sleep(DURATION) -send_ned_velocity(0,0,0) -print("Velocity North") -send_ned_velocity(NORTH,0,0) +print("Velocity West & down") +send_ned_velocity(0,WEST,DOWN,DURATION) +send_ned_velocity(0,0,0,1) + + print("Yaw 0 absolute (North)") condition_yaw(0) -time.sleep(DURATION) -send_ned_velocity(0,0,0) -print("Velocity East") +print("Velocity North") +send_ned_velocity(NORTH,0,0,DURATION) +send_ned_velocity(0,0,0,1) + + +print("Yaw 90 absolute (East)") condition_yaw(90) -send_ned_velocity(0,EAST,0) -time.sleep(DURATION) -send_ned_velocity(0,0,0) +print("Velocity East") +send_ned_velocity(0,EAST,0,DURATION) +send_ned_velocity(0,0,0,1) """ @@ -581,53 +584,66 @@ def send_global_velocity(velocity_x, velocity_y, velocity_z): so that the front of the vehicle points in the direction of travel. At the end of the second segment the code sets a new home location to the current point. -""" +""" print("DIAMOND path using SET_POSITION_TARGET_GLOBAL_INT and velocity parameters") # vx, vy are parallel to North and East (independent of the vehicle orientation) -print("Velocity North, East and up") -send_global_velocity(SOUTH,WEST,UP) + print("Yaw 225 absolute") condition_yaw(225) -time.sleep(DURATION) -send_global_velocity(0,0,0) -print("Velocity South, East and down") -send_global_velocity(NORTH,WEST,DOWN) +print("Velocity South, West and Up") +send_global_velocity(SOUTH,WEST,UP,DURATION) +send_global_velocity(0,0,0,1) + + print("Yaw 90 relative (to previous yaw heading)") condition_yaw(90,relative=True) -time.sleep(DURATION) -send_global_velocity(0,0,0) -print("Set new Home location to current location") -set_home(vehicle.location) -print "Get new home location" #This reloads the home location in GCSs +print("Velocity North, West and Down") +send_global_velocity(NORTH,WEST,DOWN,DURATION) +send_global_velocity(0,0,0,1) + +print("Set new home location to current location") +vehicle.home_location=vehicle.location.global_frame +print("Get new home location") +#This reloads the home location in DroneKit and GCSs cmds = vehicle.commands cmds.download() -cmds.wait_valid() -print " Home WP: %s" % cmds[0] +cmds.wait_ready() +print(" Home Location: %s" % vehicle.home_location) + -print("Velocity South and West") -send_global_velocity(NORTH,EAST,0) print("Yaw 90 relative (to previous yaw heading)") condition_yaw(90,relative=True) -time.sleep(DURATION) -send_global_velocity(0,0,0) -print("Velocity North and West") -send_global_velocity(SOUTH,EAST,0) +print("Velocity North and East") +send_global_velocity(NORTH,EAST,0,DURATION) +send_global_velocity(0,0,0,1) + + print("Yaw 90 relative (to previous yaw heading)") condition_yaw(90,relative=True) -time.sleep(DURATION) -send_global_velocity(0,0,0) + +print("Velocity South and East") +send_global_velocity(SOUTH,EAST,0,DURATION) +send_global_velocity(0,0,0,1) """ The example is completing. LAND at current location. -""" +""" print("Setting LAND mode...") vehicle.mode = VehicleMode("LAND") -vehicle.flush() -print("Completed") \ No newline at end of file + +#Close vehicle object before exiting script +print("Close vehicle object") +vehicle.close() + +# Shut down simulator if it was started. +if sitl is not None: + sitl.stop() + +print("Completed") diff --git a/examples/guided_set_speed_yaw/readme.md b/examples/guided_set_speed_yaw/readme.md deleted file mode 100644 index 0908d4fac..000000000 --- a/examples/guided_set_speed_yaw/readme.md +++ /dev/null @@ -1,3 +0,0 @@ -# Guided Demo - -Please check the documentation folder for more info \ No newline at end of file diff --git a/examples/mission_basic/mission_basic.py b/examples/mission_basic/mission_basic.py index e31700c1c..226d327dd 100644 --- a/examples/mission_basic/mission_basic.py +++ b/examples/mission_basic/mission_basic.py @@ -1,198 +1,217 @@ -""" -mission_basic.py: Example demonstrating basic mission operations including creating, clearing and monitoring missions. - -Full documentation is provided at http://python.dronekit.io/examples/mission_basic.html -""" - -import time -import math -from dronekit.lib import VehicleMode, Location, Command -from pymavlink import mavutil - -# Connect to API provider and get vehicle -api = local_connect() -vehicle = api.get_vehicles()[0] - - -def get_location_metres(original_location, dNorth, dEast): - """ - Returns a Location object containing the latitude/longitude `dNorth` and `dEast` metres from the - specified `original_location`. The returned Location has the same `alt and `is_relative` values - as `original_location`. - - The function is useful when you want to move the vehicle around specifying locations relative to - the current vehicle position. - The algorithm is relatively accurate over small distances (10m within 1km) except close to the poles. - For more information see: - http://gis.stackexchange.com/questions/2951/algorithm-for-offsetting-a-latitude-longitude-by-some-amount-of-meters - """ - earth_radius=6378137.0 #Radius of "spherical" earth - #Coordinate offsets in radians - dLat = dNorth/earth_radius - dLon = dEast/(earth_radius*math.cos(math.pi*original_location.lat/180)) - - #New position in decimal degrees - newlat = original_location.lat + (dLat * 180/math.pi) - newlon = original_location.lon + (dLon * 180/math.pi) - return Location(newlat, newlon,original_location.alt,original_location.is_relative) - - -def get_distance_metres(aLocation1, aLocation2): - """ - Returns the ground distance in metres between two Location objects. - - This method is an approximation, and will not be accurate over large distances and close to the - earth's poles. It comes from the ArduPilot test code: - https://github.com/diydrones/ardupilot/blob/master/Tools/autotest/common.py - """ - dlat = aLocation2.lat - aLocation1.lat - dlong = aLocation2.lon - aLocation1.lon - return math.sqrt((dlat*dlat) + (dlong*dlong)) * 1.113195e5 - - - -def distance_to_current_waypoint(): - """ - Gets distance in metres to the current waypoint. - It returns None for the first waypoint (Home location). - """ - nextwaypoint=vehicle.commands.next - if nextwaypoint ==1: - return None - missionitem=vehicle.commands[nextwaypoint] - lat=missionitem.x - lon=missionitem.y - alt=missionitem.z - targetWaypointLocation=Location(lat,lon,alt,is_relative=True) - distancetopoint = get_distance_metres(vehicle.location, targetWaypointLocation) - return distancetopoint - - -def clear_mission(): - """ - Clear the current mission. - """ - cmds = vehicle.commands - vehicle.commands.clear() - vehicle.flush() - - # After clearing the mission you MUST re-download the mission from the vehicle - # before vehicle.commands can be used again - # (see https://github.com/dronekit/dronekit-python/issues/230) - cmds = vehicle.commands - cmds.download() - cmds.wait_valid() - - -def download_mission(): - """ - Download the current mission from the vehicle. - """ - cmds = vehicle.commands - cmds.download() - cmds.wait_valid() # wait until download is complete. - - - -def adds_square_mission(aLocation, aSize): - """ - Adds a takeoff command and four waypoint commands to the current mission. - The waypoints are positioned to form a square of side length 2*aSize around the specified location (aLocation). - - The function assumes vehicle.commands matches the vehicle mission state - (you must have called download at least once in the session and after clearing the mission) - """ - # Add the commands. The meaning/order of the parameters is documented in the Command class. - cmds = vehicle.commands - #Add MAV_CMD_NAV_TAKEOFF command. This is ignored if the vehicle is already in the air. - cmds.add(Command( 0, 0, 0, mavutil.mavlink.MAV_FRAME_GLOBAL_RELATIVE_ALT, mavutil.mavlink.MAV_CMD_NAV_TAKEOFF, 0, 0, 0, 0, 0, 0, 0, 0, 10)) - - #Define the four MAV_CMD_NAV_WAYPOINT locations and add the commands - point1 = get_location_metres(aLocation, aSize, -aSize) - point2 = get_location_metres(aLocation, aSize, aSize) - point3 = get_location_metres(aLocation, -aSize, aSize) - point4 = get_location_metres(aLocation, -aSize, -aSize) - cmds.add(Command( 0, 0, 0, mavutil.mavlink.MAV_FRAME_GLOBAL_RELATIVE_ALT, mavutil.mavlink.MAV_CMD_NAV_WAYPOINT, 0, 0, 0, 0, 0, 0, point1.lat, point1.lon, 11)) - cmds.add(Command( 0, 0, 0, mavutil.mavlink.MAV_FRAME_GLOBAL_RELATIVE_ALT, mavutil.mavlink.MAV_CMD_NAV_WAYPOINT, 0, 0, 0, 0, 0, 0, point2.lat, point2.lon, 12)) - cmds.add(Command( 0, 0, 0, mavutil.mavlink.MAV_FRAME_GLOBAL_RELATIVE_ALT, mavutil.mavlink.MAV_CMD_NAV_WAYPOINT, 0, 0, 0, 0, 0, 0, point3.lat, point3.lon, 13)) - cmds.add(Command( 0, 0, 0, mavutil.mavlink.MAV_FRAME_GLOBAL_RELATIVE_ALT, mavutil.mavlink.MAV_CMD_NAV_WAYPOINT, 0, 0, 0, 0, 0, 0, point4.lat, point4.lon, 14)) - - # Send commands to vehicle. - vehicle.flush() - - -def arm_and_takeoff(aTargetAltitude): - """ - Arms vehicle and fly to aTargetAltitude. - """ - print "Basic pre-arm checks" - # Don't let the user try to fly autopilot is booting - if vehicle.mode.name == "INITIALISING": - print "Waiting for vehicle to initialise" - time.sleep(1) - while vehicle.gps_0.fix_type < 2: - print "Waiting for GPS...:", vehicle.gps_0.fix_type - time.sleep(1) - - print "Arming motors" - # Copter should arm in GUIDED mode - vehicle.mode = VehicleMode("GUIDED") - vehicle.armed = True - vehicle.flush() - - while not vehicle.armed and not api.exit: - print " Waiting for arming..." - time.sleep(1) - - print "Taking off!" - vehicle.commands.takeoff(aTargetAltitude) # Take off to target altitude - vehicle.flush() - - # Wait until the vehicle reaches a safe height before processing the goto (otherwise the command - # after Vehicle.commands.takeoff will execute immediately). - while not api.exit: - print " Altitude: ", vehicle.location.alt - if vehicle.location.alt>=aTargetAltitude*0.95: #Just below target, in case of undershoot. - print "Reached target altitude" - break; - time.sleep(1) - - -print 'Clear the current mission' -clear_mission() - -print 'Create a new mission' -adds_square_mission(vehicle.location,50) -time.sleep(2) # This is here so that mission being sent is displayed on console - - -# From Copter 3.3 you will be able to take off using a mission item. Plane must take off using a mission item (currently). -arm_and_takeoff(10) - -print "Starting mission" -# Set mode to AUTO to start mission -vehicle.mode = VehicleMode("AUTO") -vehicle.flush() - -# Monitor mission. -# Demonstrates getting and setting the command number -# Uses distance_to_current_waypoint(), a convenience function for finding the -# distance to the next waypoint. - -while True: - nextwaypoint=vehicle.commands.next - if nextwaypoint > 1: - print 'Distance to waypoint (%s): %s' % (nextwaypoint, distance_to_current_waypoint()) - if nextwaypoint==3: #Skip to next waypoint - print 'Skipping to Waypoint 4 when reach waypoint 3' - vehicle.commands.next=4 - if nextwaypoint==5: #Skip to next waypoint - print "Exit 'standard' mission when start heading to final waypoint (5)" - break; - time.sleep(1) - -print 'Return to launch' -vehicle.mode = VehicleMode("RTL") -vehicle.flush() # Flush to ensure changes are sent to autopilot - - +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +© Copyright 2015-2016, 3D Robotics. +mission_basic.py: Example demonstrating basic mission operations including creating, clearing and monitoring missions. + +Full documentation is provided at https://dronekit-python.readthedocs.io/en/latest/examples/mission_basic.html +""" +from __future__ import print_function + +from dronekit import connect, VehicleMode, LocationGlobalRelative, LocationGlobal, Command +import time +import math +from pymavlink import mavutil + + +#Set up option parsing to get connection string +import argparse +parser = argparse.ArgumentParser(description='Demonstrates basic mission operations.') +parser.add_argument('--connect', + help="vehicle connection target string. If not specified, SITL automatically started and used.") +args = parser.parse_args() + +connection_string = args.connect +sitl = None + + +#Start SITL if no connection string specified +if not connection_string: + import dronekit_sitl + sitl = dronekit_sitl.start_default() + connection_string = sitl.connection_string() + + +# Connect to the Vehicle +print('Connecting to vehicle on: %s' % connection_string) +vehicle = connect(connection_string, wait_ready=True) + + +def get_location_metres(original_location, dNorth, dEast): + """ + Returns a LocationGlobal object containing the latitude/longitude `dNorth` and `dEast` metres from the + specified `original_location`. The returned Location has the same `alt` value + as `original_location`. + + The function is useful when you want to move the vehicle around specifying locations relative to + the current vehicle position. + The algorithm is relatively accurate over small distances (10m within 1km) except close to the poles. + For more information see: + http://gis.stackexchange.com/questions/2951/algorithm-for-offsetting-a-latitude-longitude-by-some-amount-of-meters + """ + earth_radius=6378137.0 #Radius of "spherical" earth + #Coordinate offsets in radians + dLat = dNorth/earth_radius + dLon = dEast/(earth_radius*math.cos(math.pi*original_location.lat/180)) + + #New position in decimal degrees + newlat = original_location.lat + (dLat * 180/math.pi) + newlon = original_location.lon + (dLon * 180/math.pi) + return LocationGlobal(newlat, newlon,original_location.alt) + + +def get_distance_metres(aLocation1, aLocation2): + """ + Returns the ground distance in metres between two LocationGlobal objects. + + This method is an approximation, and will not be accurate over large distances and close to the + earth's poles. It comes from the ArduPilot test code: + https://github.com/diydrones/ardupilot/blob/master/Tools/autotest/common.py + """ + dlat = aLocation2.lat - aLocation1.lat + dlong = aLocation2.lon - aLocation1.lon + return math.sqrt((dlat*dlat) + (dlong*dlong)) * 1.113195e5 + + + +def distance_to_current_waypoint(): + """ + Gets distance in metres to the current waypoint. + It returns None for the first waypoint (Home location). + """ + nextwaypoint = vehicle.commands.next + if nextwaypoint==0: + return None + missionitem=vehicle.commands[nextwaypoint-1] #commands are zero indexed + lat = missionitem.x + lon = missionitem.y + alt = missionitem.z + targetWaypointLocation = LocationGlobalRelative(lat,lon,alt) + distancetopoint = get_distance_metres(vehicle.location.global_frame, targetWaypointLocation) + return distancetopoint + + +def download_mission(): + """ + Download the current mission from the vehicle. + """ + cmds = vehicle.commands + cmds.download() + cmds.wait_ready() # wait until download is complete. + + + +def adds_square_mission(aLocation, aSize): + """ + Adds a takeoff command and four waypoint commands to the current mission. + The waypoints are positioned to form a square of side length 2*aSize around the specified LocationGlobal (aLocation). + + The function assumes vehicle.commands matches the vehicle mission state + (you must have called download at least once in the session and after clearing the mission) + """ + + cmds = vehicle.commands + + print(" Clear any existing commands") + cmds.clear() + + print(" Define/add new commands.") + # Add new commands. The meaning/order of the parameters is documented in the Command class. + + #Add MAV_CMD_NAV_TAKEOFF command. This is ignored if the vehicle is already in the air. + cmds.add(Command( 0, 0, 0, mavutil.mavlink.MAV_FRAME_GLOBAL_RELATIVE_ALT, mavutil.mavlink.MAV_CMD_NAV_TAKEOFF, 0, 0, 0, 0, 0, 0, 0, 0, 10)) + + #Define the four MAV_CMD_NAV_WAYPOINT locations and add the commands + point1 = get_location_metres(aLocation, aSize, -aSize) + point2 = get_location_metres(aLocation, aSize, aSize) + point3 = get_location_metres(aLocation, -aSize, aSize) + point4 = get_location_metres(aLocation, -aSize, -aSize) + cmds.add(Command( 0, 0, 0, mavutil.mavlink.MAV_FRAME_GLOBAL_RELATIVE_ALT, mavutil.mavlink.MAV_CMD_NAV_WAYPOINT, 0, 0, 0, 0, 0, 0, point1.lat, point1.lon, 11)) + cmds.add(Command( 0, 0, 0, mavutil.mavlink.MAV_FRAME_GLOBAL_RELATIVE_ALT, mavutil.mavlink.MAV_CMD_NAV_WAYPOINT, 0, 0, 0, 0, 0, 0, point2.lat, point2.lon, 12)) + cmds.add(Command( 0, 0, 0, mavutil.mavlink.MAV_FRAME_GLOBAL_RELATIVE_ALT, mavutil.mavlink.MAV_CMD_NAV_WAYPOINT, 0, 0, 0, 0, 0, 0, point3.lat, point3.lon, 13)) + cmds.add(Command( 0, 0, 0, mavutil.mavlink.MAV_FRAME_GLOBAL_RELATIVE_ALT, mavutil.mavlink.MAV_CMD_NAV_WAYPOINT, 0, 0, 0, 0, 0, 0, point4.lat, point4.lon, 14)) + #add dummy waypoint "5" at point 4 (lets us know when have reached destination) + cmds.add(Command( 0, 0, 0, mavutil.mavlink.MAV_FRAME_GLOBAL_RELATIVE_ALT, mavutil.mavlink.MAV_CMD_NAV_WAYPOINT, 0, 0, 0, 0, 0, 0, point4.lat, point4.lon, 14)) + + print(" Upload new commands to vehicle") + cmds.upload() + + +def arm_and_takeoff(aTargetAltitude): + """ + Arms vehicle and fly to aTargetAltitude. + """ + + print("Basic pre-arm checks") + # Don't let the user try to arm until autopilot is ready + while not vehicle.is_armable: + print(" Waiting for vehicle to initialise...") + time.sleep(1) + + + print("Arming motors") + # Copter should arm in GUIDED mode + vehicle.mode = VehicleMode("GUIDED") + vehicle.armed = True + + while not vehicle.armed: + print(" Waiting for arming...") + time.sleep(1) + + print("Taking off!") + vehicle.simple_takeoff(aTargetAltitude) # Take off to target altitude + + # Wait until the vehicle reaches a safe height before processing the goto (otherwise the command + # after Vehicle.simple_takeoff will execute immediately). + while True: + print(" Altitude: ", vehicle.location.global_relative_frame.alt) + if vehicle.location.global_relative_frame.alt>=aTargetAltitude*0.95: #Trigger just below target alt. + print("Reached target altitude") + break + time.sleep(1) + + +print('Create a new mission (for current location)') +adds_square_mission(vehicle.location.global_frame,50) + + +# From Copter 3.3 you will be able to take off using a mission item. Plane must take off using a mission item (currently). +arm_and_takeoff(10) + +print("Starting mission") +# Reset mission set to first (0) waypoint +vehicle.commands.next=0 + +# Set mode to AUTO to start mission +vehicle.mode = VehicleMode("AUTO") + + +# Monitor mission. +# Demonstrates getting and setting the command number +# Uses distance_to_current_waypoint(), a convenience function for finding the +# distance to the next waypoint. + +while True: + nextwaypoint=vehicle.commands.next + print('Distance to waypoint (%s): %s' % (nextwaypoint, distance_to_current_waypoint())) + + if nextwaypoint==3: #Skip to next waypoint + print('Skipping to Waypoint 5 when reach waypoint 3') + vehicle.commands.next = 5 + if nextwaypoint==5: #Dummy waypoint - as soon as we reach waypoint 4 this is true and we exit. + print("Exit 'standard' mission when start heading to final waypoint (5)") + break; + time.sleep(1) + +print('Return to launch') +vehicle.mode = VehicleMode("RTL") + + +#Close vehicle object before exiting script +print("Close vehicle object") +vehicle.close() + +# Shut down simulator if it was started. +if sitl is not None: + sitl.stop() diff --git a/examples/mission_import_export/mission_import_export.py b/examples/mission_import_export/mission_import_export.py index 35d8b8519..fc36e7f91 100644 --- a/examples/mission_import_export/mission_import_export.py +++ b/examples/mission_import_export/mission_import_export.py @@ -1,113 +1,173 @@ -""" -mission_import_export.py: - -This example demonstrates how to import and export files in the Waypoint file format -(http://qgroundcontrol.org/mavlink/waypoint_protocol#waypoint_file_format). The commands are imported -into a list, and can be modified before saving and/or uploading. - -Documentation is provided at http://python.dronekit.io/examples/mission_import_export.html -""" - -import time -import math -from dronekit.lib import Command -from pymavlink import mavutil - -# Connect to API provider and get vehicle -api = local_connect() -vehicle = api.get_vehicles()[0] - - -def readmission(aFileName): - """ - Load a mission from a file into a list. The mission definition is in the Waypoint file - format (http://qgroundcontrol.org/mavlink/waypoint_protocol#waypoint_file_format). - - This function is used by upload_mission(). - """ - print "Reading mission from file: %s\n" % aFileName - cmds = vehicle.commands - missionlist=[] - with open(aFileName) as f: - for i, line in enumerate(f): - if i==0: - if not line.startswith('QGC WPL 110'): - raise Exception('File is not supported WP version') - else: - print ' Import line: %s' % line - linearray=line.split('\t') - ln_index=int(linearray[0]) - ln_currentwp=int(linearray[1]) - ln_frame=int(linearray[2]) - ln_command=int(linearray[3]) - ln_param1=float(linearray[4]) - ln_param2=float(linearray[5]) - ln_param3=float(linearray[6]) - ln_param4=float(linearray[7]) - ln_param5=float(linearray[8]) - ln_param6=float(linearray[9]) - ln_param7=float(linearray[10]) - ln_autocontinue=int(linearray[11].strip()) - cmd = Command( 0, 0, 0, ln_frame, ln_command, ln_currentwp, ln_autocontinue, ln_param1, ln_param2, ln_param3, ln_param4, ln_param5, ln_param6, ln_param7) - missionlist.append(cmd) - return missionlist - - -def upload_mission(aFileName): - """ - Upload a mission from a file. - """ - missionlist = readmission(aFileName) - #clear existing mission - print 'Clear mission' - cmds = vehicle.commands - cmds.download() - cmds.wait_valid() - cmds.clear() - vehicle.flush() - print 'ClearCount: %s' % cmds.count - #add new mission - cmds.download() - cmds.wait_valid() - for command in missionlist: - cmds.add(command) - vehicle.flush() - - -def download_mission(): - """ - Downloads the current mission and returns it in a list. - It is used in save_mission() to get the file information to save. - """ - missionlist=[] - cmds = vehicle.commands - cmds.download() - cmds.wait_valid() - for cmd in cmds[1:]: #skip first item as it is home waypoint. - missionlist.append(cmd) - return missionlist - -def save_mission(aFileName): - """ - Save a mission in the Waypoint file format (http://qgroundcontrol.org/mavlink/waypoint_protocol#waypoint_file_format). - """ - missionlist = download_mission() - output='QGC WPL 110\n' - for cmd in missionlist: - commandline="%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n" % (cmd.seq,cmd.current,cmd.frame,cmd.command,cmd.param1,cmd.param2,cmd.param3,cmd.param4,cmd.x,cmd.y,cmd.z,cmd.autocontinue) - output+=commandline - with open(aFileName, 'w') as file_: - file_.write(output) - - -import_mission_filename = 'mpmission.txt' -export_mission_filename = 'exportedmission.txt' - -print "\nUpload mission from a file: %s" % import_mission_filename -upload_mission(import_mission_filename) - -time.sleep(1) - -print "\nSave mission from Vehicle to file: %s" % export_mission_filename -save_mission(export_mission_filename) - +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +© Copyright 2015-2016, 3D Robotics. +mission_import_export.py: + +This example demonstrates how to import and export files in the Waypoint file format +(http://qgroundcontrol.org/mavlink/waypoint_protocol#waypoint_file_format). The commands are imported +into a list, and can be modified before saving and/or uploading. + +Documentation is provided at http://python.dronekit.io/examples/mission_import_export.html +""" +from __future__ import print_function + + +from dronekit import connect, Command +import time + + +#Set up option parsing to get connection string +import argparse +parser = argparse.ArgumentParser(description='Demonstrates mission import/export from a file.') +parser.add_argument('--connect', + help="Vehicle connection target string. If not specified, SITL automatically started and used.") +args = parser.parse_args() + +connection_string = args.connect +sitl = None + + +#Start SITL if no connection string specified +if not connection_string: + import dronekit_sitl + sitl = dronekit_sitl.start_default() + connection_string = sitl.connection_string() + + +# Connect to the Vehicle +print('Connecting to vehicle on: %s' % connection_string) +vehicle = connect(connection_string, wait_ready=True) + +# Check that vehicle is armable. +# This ensures home_location is set (needed when saving WP file) + +while not vehicle.is_armable: + print(" Waiting for vehicle to initialise...") + time.sleep(1) + + +def readmission(aFileName): + """ + Load a mission from a file into a list. The mission definition is in the Waypoint file + format (http://qgroundcontrol.org/mavlink/waypoint_protocol#waypoint_file_format). + + This function is used by upload_mission(). + """ + print("\nReading mission from file: %s" % aFileName) + cmds = vehicle.commands + missionlist=[] + with open(aFileName) as f: + for i, line in enumerate(f): + if i==0: + if not line.startswith('QGC WPL 110'): + raise Exception('File is not supported WP version') + else: + linearray=line.split('\t') + ln_index=int(linearray[0]) + ln_currentwp=int(linearray[1]) + ln_frame=int(linearray[2]) + ln_command=int(linearray[3]) + ln_param1=float(linearray[4]) + ln_param2=float(linearray[5]) + ln_param3=float(linearray[6]) + ln_param4=float(linearray[7]) + ln_param5=float(linearray[8]) + ln_param6=float(linearray[9]) + ln_param7=float(linearray[10]) + ln_autocontinue=int(linearray[11].strip()) + cmd = Command( 0, 0, 0, ln_frame, ln_command, ln_currentwp, ln_autocontinue, ln_param1, ln_param2, ln_param3, ln_param4, ln_param5, ln_param6, ln_param7) + missionlist.append(cmd) + return missionlist + + +def upload_mission(aFileName): + """ + Upload a mission from a file. + """ + #Read mission from file + missionlist = readmission(aFileName) + + print("\nUpload mission from a file: %s" % aFileName) + #Clear existing mission from vehicle + print(' Clear mission') + cmds = vehicle.commands + cmds.clear() + #Add new mission to vehicle + for command in missionlist: + cmds.add(command) + print(' Upload mission') + vehicle.commands.upload() + + +def download_mission(): + """ + Downloads the current mission and returns it in a list. + It is used in save_mission() to get the file information to save. + """ + print(" Download mission from vehicle") + missionlist=[] + cmds = vehicle.commands + cmds.download() + cmds.wait_ready() + for cmd in cmds: + missionlist.append(cmd) + return missionlist + +def save_mission(aFileName): + """ + Save a mission in the Waypoint file format + (http://qgroundcontrol.org/mavlink/waypoint_protocol#waypoint_file_format). + """ + print("\nSave mission from Vehicle to file: %s" % aFileName) + #Download mission from vehicle + missionlist = download_mission() + #Add file-format information + output='QGC WPL 110\n' + #Add home location as 0th waypoint + home = vehicle.home_location + output+="%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n" % (0,1,0,16,0,0,0,0,home.lat,home.lon,home.alt,1) + #Add commands + for cmd in missionlist: + commandline="%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n" % (cmd.seq,cmd.current,cmd.frame,cmd.command,cmd.param1,cmd.param2,cmd.param3,cmd.param4,cmd.x,cmd.y,cmd.z,cmd.autocontinue) + output+=commandline + with open(aFileName, 'w') as file_: + print(" Write mission to file") + file_.write(output) + + +def printfile(aFileName): + """ + Print a mission file to demonstrate "round trip" + """ + print("\nMission file: %s" % aFileName) + with open(aFileName) as f: + for line in f: + print(' %s' % line.strip()) + + +import_mission_filename = 'mpmission.txt' +export_mission_filename = 'exportedmission.txt' + + +#Upload mission from file +upload_mission(import_mission_filename) + +#Download mission we just uploaded and save to a file +save_mission(export_mission_filename) + +#Close vehicle object before exiting script +print("Close vehicle object") +vehicle.close() + +# Shut down simulator if it was started. +if sitl is not None: + sitl.stop() + + +print("\nShow original and uploaded/downloaded files:") +#Print original file (for demo purposes only) +printfile(import_mission_filename) +#Print exported file (for demo purposes only) +printfile(export_mission_filename) diff --git a/examples/mission_import_export/mpmission.txt b/examples/mission_import_export/mpmission.txt new file mode 100644 index 000000000..2ca281d09 --- /dev/null +++ b/examples/mission_import_export/mpmission.txt @@ -0,0 +1,8 @@ +QGC WPL 110 +0 1 0 16 0 0 0 0 -35.363262 149.165237 584.000000 1 +1 0 0 22 0.000000 0.000000 0.000000 0.000000 -35.361988 149.163753 100.000000 1 +2 0 0 16 0.000000 0.000000 0.000000 0.000000 -35.361992 149.163593 100.000000 1 +3 0 0 16 0.000000 0.000000 0.000000 0.000000 -35.363812 149.163609 100.000000 1 +4 0 0 16 0.000000 0.000000 0.000000 0.000000 -35.363768 149.166055 100.000000 1 +5 0 0 16 0.000000 0.000000 0.000000 0.000000 -35.361835 149.166012 100.000000 1 +6 0 0 16 0.000000 0.000000 0.000000 0.000000 -35.362150 149.165046 100.000000 1 diff --git a/examples/perf/perf_test.py b/examples/perf/perf_test.py deleted file mode 100644 index 03a334e53..000000000 --- a/examples/perf/perf_test.py +++ /dev/null @@ -1,259 +0,0 @@ -# -# This is a small example of the python drone API -# Usage: -# * mavproxy.py -# * module load api -# * api start small-demo.py -# -from dronekit.lib import VehicleMode -from pymavlink import mavutil -import time -from datetime import datetime -import traceback - -from matplotlib import pyplot -from numpy import arange -import bisect - -findings = """ -Reception latency: -* due to a top level select() on the udp port, the latency for calling process_master seems to be sub 1ms (limit of python timer resolution) - -Reception periodicity: -TBD - -Background processing perodicity: -TBD - -Sending perodicity: -TBD - -Computation efficency: -FIXME - -Max closed loop rate: -* 20ms+/-300us when talking to SITL (every time we recieve a cmd_ack we immediately send a pair of ROI related msgs) -* The less than 300us variablity makes me think SITL has some 20ms poll rate - need to try with real vehicle - -SITL copter load -Interval (sec) 0.019865 -MaxInterval (sec) 0.021927 -MinInterval (sec) 0.018421 - -AVR plane load: 20ms+/-7ms -Interval 0.02061 -MaxInterval 0.025496 -MinInterval 0.011533 - -PX4 quad load on Edsion: 20ms +60ms -5ms (VERY HIGH VARIABILITY - mostly due to px4 side - see below) -Interval 0.0281970000001 -MaxInterval 0.0786720000001 -MinInterval 0.0161290000001 - -PX4 quad load on a pixhawk (a9defa35) talking to my desktop - similar variability as with an Edison: -Interval 0.01989 -MaxInterval 0.0688479999999 -MinInterval 0.00722900000005 -Interval 0.019929 -MaxInterval 0.0688479999999 -MinInterval 0.00722900000005 -Interval 0.0189700000001 -MaxInterval 0.0688479999999 -MinInterval 0.00722900000005 - -or here's 20ish of the interval values seen on the px4 (a9defa35) Test -Interval 0.020012 -Interval 0.0199689999999 -Interval 0.0229640000002 -Interval 0.0171049999999 -Interval 0.0198150000001 -Interval 0.0211049999998 -Interval 0.0199740000003 -Interval 0.0199459999999 -Interval 0.0199590000002 -Interval 0.0200379999997 -Interval 0.0200850000001 -Interval 0.0198839999998 -Interval 0.0200420000001 -Interval 0.0199539999999 -Interval 0.0200760000002 -Interval 0.0199029999999 -Interval 0.0200950000003 -Interval 0.0517199999999 - -now testing with a plane load with a px4 (a9defa35) at 56kbps - highly variable 25 to 82ms -Interval 0.0589850000001 -MaxInterval 0.0829760000001 -MinInterval 0.0258819999999 - -but change to 115kbps and things are much better -Interval 0.0201160000001 -MaxInterval 0.044656 -MinInterval 0.0150279999998 - -and changing things to 500kbps everything is just peachie - 18ms -Interval 0.018119 -MaxInterval 0.02527 -MinInterval 0.015737 - -Recommendations: -Run link as fast as you can 1500kbps? -Turn on hw flow control (and use --rtscts on mavproxy) - -mavproxy.py --master=/dev/ttyMFD1,115200 --cmd="api start perf_test.py" -""" - -global v - - -def scatterplot(x,y): - pyplot.plot(x,y,'b.') - pyplot.xlim(min(x)-1,max(x)+1) - pyplot.ylim(min(y)-1,max(y)+1) - pyplot.show() - - -def cur_usec(): - """Return current time in usecs""" - # t = time.time() - dt = datetime.now() - t = dt.minute * 60 + dt.second + dt.microsecond / (1e6) - return t - -class MeasureTime(object): - def __init__(self): - self.prevtime = cur_usec() - self.previnterval = 0 - self.numcount = 0 - self.reset() - - def reset(self): - self.maxinterval = 0 - self.mininterval = 10000 - - def update(self): - now = cur_usec() - self.numcount = self.numcount + 1 - self.previnterval = now - self.prevtime - self.prevtime = now - self.maxinterval = max(self.previnterval, self.maxinterval) - self.mininterval = min(self.mininterval, self.previnterval) - - #print "Interval", self.previnterval - if (self.numcount % 100) == 0: - if self.numcount == 200: - # Ignore delays during startup - self.reset() - print "Interval", self.previnterval - print "MaxInterval", self.maxinterval - print "MinInterval", self.mininterval - - -acktime = MeasureTime() - -def mavrx_debug_handler(message): - """Measure heartbeat periodically""" - mtype = message.get_type() - global sendtime - - #if mtype == 'HEARTBEAT': - if mtype == 'COMMAND_ACK': - #traceback.print_stack() - #print "GOT ACK", message - acktime.update() - send_testpackets() - - -def send_testpackets(): - #print "send ROI cmds" - - # create the SET_POSITION_TARGET_GLOBAL_INT command - msg = v.message_factory.set_position_target_global_int_encode( - 0, # time_boot_ms (not used) - 1, 1, # target system, target component - mavutil.mavlink.MAV_FRAME_GLOBAL, # frame - 0b1000000011000111, # type_mask - enable velocity only - 0, 0, 0, # x, y, z positions (not used) - 0, 0, 0.0, # x, y, z velocity in m/s - 0, 0, 0, # x, y, z acceleration (not used) - 0, 0) # yaw, yaw_rate (not used) - - # send command to vehicle - v.send_mavlink(msg) - - # set ROI - msg = v.message_factory.command_long_encode( - 1, 1, # target system, target component - #mavutil.mavlink.MAV_CMD_DO_SET_RELAY, #command - mavutil.mavlink.MAV_CMD_DO_SET_ROI, #command - 0, #confirmation - 0, 0, 0, 0, #params 1-4 - 0, - 0, - 0 - ) - - v.send_mavlink(msg) - - # Always call flush to guarantee that previous writes to the vehicle have taken place - v.flush() - -# First get an instance of the API endpoint -api = local_connect() -# get our vehicle - when running with mavproxy it only knows about one vehicle (for now) -v = api.get_vehicles()[0] -# Print out some interesting stats about the vehicle -print "Mode: %s" % v.mode -print "Location: %s" % v.location -print "Attitude: %s" % v.attitude -print "Velocity: %s" % v.velocity -print "GPS: %s" % v.gps_0 -print "Armed: %s" % v.armed -print "groundspeed: %s" % v.groundspeed -print "airspeed: %s" % v.airspeed - -import time -time.sleep(30) - -# Use of the following method is not recommended (it is better to add observer callbacks to attributes) but if you need it -# it is available... -v.set_mavlink_callback(mavrx_debug_handler) - -# You can read and write parameters -#print "Param: %s" % v.parameters['THR_MAX'] - -# Now download the vehicle waypoints -cmds = v.commands -cmds.download() -cmds.wait_valid() -print "Home WP: %s" % cmds[0] -print "Current dest: %s" % cmds.next - -# Test custom commands -# Note: For mavlink messages that include a target_system & target_component, those values -# can just be filled with zero. The API will take care of using the correct values -# For instance, from the xml for command_long: -# Send a command with up to seven parameters to the MAV -# -# target_system : System which should execute the command (uint8_t) -# target_component : Component which should execute the command, 0 for all components (uint8_t) -# command : Command ID, as defined by MAV_CMD enum. (uint16_t) -# confirmation : 0: First transmission of this command. 1-255: Confirmation transmissions (e.g. for kill command) (uint8_t) -# param1 : Parameter 1, as defined by MAV_CMD enum. (float) -# param2 : Parameter 2, as defined by MAV_CMD enum. (float) -# param3 : Parameter 3, as defined by MAV_CMD enum. (float) -# param4 : Parameter 4, as defined by MAV_CMD enum. (float) -# param5 : Parameter 5, as defined by MAV_CMD enum. (float) -# param6 : Parameter 6, as defined by MAV_CMD enum. (float) -# param7 : Parameter 7, as defined by MAV_CMD enum. (float) -#msg = v.message_factory.command_long_encode(0, 0, -# mavutil.mavlink.MAV_CMD_CONDITION_YAW, 0, -# 0, 0, 0, 0, 1, 0, 0) -#print "Created msg: %s" % msg -#v.send_mavlink(msg) - -print "Disarming..." -v.armed = False -v.flush() - -# send_testpackets() diff --git a/examples/perf/tx_perf_test.py b/examples/perf/tx_perf_test.py deleted file mode 100644 index 4c0e4129c..000000000 --- a/examples/perf/tx_perf_test.py +++ /dev/null @@ -1,316 +0,0 @@ -# -# This is the entry point for MavProxy running DroneAPI on the vehicle -# Usage: -# * mavproxy.py -# * module load api -# * api start TestPerformance.py -# - -""" -non native test: -mavproxy.py --master=/dev/ttyUSB0,921600 --rtscts --cmd "api start tx_perf_test.py" --max-packets=5000 --daemon - -name ncall tsub ttot tavg -..hon2.7/threading.py:752 Thread.run 1 0.000012 3.122469 3.122469 -..MAVProxy/mavproxy.py:686 main_loop 1 0.060946 3.122457 3.122457 -..mavproxy.py:510 process_master_all 1633 0.013251 2.671116 0.001636 -..oxy/mavproxy.py:470 process_master 3406 0.089617 2.657865 0.000780 -..tmega.py:6495 MAVLink.parse_buffer 2289 0.022196 2.171655 0.000949 -..lotmega.py:6433 MAVLink.parse_char 7399 0.103295 2.164799 0.000293 -..otmega.py:6428 MAVLink.__callbacks 5004 0.020360 1.178805 0.000236 -...py:230 LinkModule.master_callback 5004 0.261917 1.158446 0.000232 -..y:6461 MAVLink.__parse_char_legacy 7399 0.097154 0.835198 0.000113 -..b/python2.7/Queue.py:150 Queue.get 871.. 0.100600 0.734111 0.000084 -..dupilotmega.py:6508 MAVLink.decode 4938 0.267052 0.704184 0.000143 -../api.py:243 MPVehicle.send_mavlink 1938 0.015008 0.594257 0.000307 -..ardupilotmega.py:6402 MAVLink.send 1958 0.027275 0.528169 0.000270 -..erator/mavcrc.py:6 x25crc.__init__ 8854 0.048278 0.501256 0.000057 -.. MAVLink_command_long_message.pack 3876 0.035650 0.484348 0.000125 -..ink.py:407 LinkModule.__to_modules 5004 0.150852 0.436901 0.000087 -..143 MAVLink_heartbeat_message.pack 3916 0.080226 0.435734 0.000111 -..7/threading.py:309 _Condition.wait 189.. 0.043024 0.423701 0.000224 -..tor/mavcrc.py:14 x25crc.accumulate 12770 0.381646 0.381646 0.000030 -..b/python2.7/Queue.py:107 Queue.put 9184 0.094449 0.336993 0.000037 -..y:9626 MAVLink.command_long_encode 1938 0.015715 0.330282 0.000170 -..mavcrc.py:23 x25crc.accumulate_str 7832 0.106192 0.291197 0.000037 -..link/mavutil.py:726 mavserial.recv 3655 0.016554 0.281399 0.000077 -..ial/serialposix.py:453 Serial.read 3655 0.072221 0.264846 0.000072 -..mp_module.py:83 ParamModule.master 10611 0.027430 0.246833 0.000023 -..oxy/mavproxy.py:651 periodic_tasks 1633 0.077704 0.246205 0.000151 -..threading.py:373 _Condition.notify 17897 0.104504 0.244802 0.000014 -..oxy/mavproxy.py:188 MPState.master 10611 0.164999 0.219403 0.000021 -../python2.7/Queue.py:93 Queue.empty 12462 0.067670 0.215484 0.000017 -..77 LinkModule.master_send_callback 1958 0.052664 0.157868 0.000081 -...py:228 ParamModule.mavlink_packet 5004 0.022396 0.140830 0.000028 -..ink/mavutil.py:736 mavserial.write 1958 0.007324 0.127015 0.000065 -..util.py:212 mavserial.post_message 10008 0.100039 0.123820 0.000012 -..al/serialposix.py:488 Serial.write 1958 0.030760 0.119692 0.000061 -..eading.py:300 _Condition._is_owned 19792 0.056270 0.119383 0.000006 -..i.py:247 MPVehicle.message_factory 1938 0.008873 0.074651 0.000039 -..ings.py:104 MPSettings.__getattr__ 51190 0.070214 0.070214 0.000001 -..pi.py:396 APIModule.mavlink_packet 5004 0.035746 0.062572 0.000013 - -mavproxy.py --master=/dev/ttyUSB0,921600 --rtscts --cmd "api start tx_perf_test.py" --max-packets=5000 --daemon --native -name ncall tsub ttot tavg -..hon2.7/threading.py:752 Thread.run 1 0.000029 2.452683 2.452683 -..MAVProxy/mavproxy.py:686 main_loop 1 0.067917 2.452654 2.452654 -..mavproxy.py:510 process_master_all 1913 0.014797 1.963876 0.001027 -..oxy/mavproxy.py:470 process_master 3829 0.093805 1.949079 0.000509 -..tmega.py:6495 MAVLink.parse_buffer 2406 0.021892 1.463879 0.000608 -..lotmega.py:6433 MAVLink.parse_char 7437 0.104221 1.440324 0.000194 -..otmega.py:6428 MAVLink.__callbacks 5020 0.018822 1.160177 0.000231 -...py:230 LinkModule.master_callback 5020 0.263063 1.141356 0.000227 -..b/python2.7/Queue.py:150 Queue.get 892.. 0.103531 0.653408 0.000073 -../api.py:243 MPVehicle.send_mavlink 1958 0.016154 0.604459 0.000309 -..ardupilotmega.py:6402 MAVLink.send 1978 0.027734 0.535485 0.000271 -.. MAVLink_command_long_message.pack 3916 0.036748 0.495637 0.000127 -..143 MAVLink_heartbeat_message.pack 3956 0.083566 0.446711 0.000113 -..ink.py:407 LinkModule.__to_modules 5020 0.149475 0.428373 0.000085 -..7/threading.py:309 _Condition.wait 195.. 0.042275 0.353284 0.000181 -..y:9626 MAVLink.command_long_encode 1958 0.015960 0.335833 0.000172 -..b/python2.7/Queue.py:107 Queue.put 9404 0.094564 0.332117 0.000035 -..mavcrc.py:23 x25crc.accumulate_str 7912 0.107540 0.297585 0.000038 -..oxy/mavproxy.py:651 periodic_tasks 1913 0.089161 0.282385 0.000148 -..link/mavutil.py:726 mavserial.recv 3848 0.017526 0.269924 0.000070 -..erator/mavcrc.py:6 x25crc.__init__ 3956 0.021304 0.263655 0.000067 -..ial/serialposix.py:453 Serial.read 3848 0.073747 0.252398 0.000066 -..mp_module.py:83 ParamModule.master 10964 0.027469 0.252241 0.000023 -..threading.py:373 _Condition.notify 18331 0.105160 0.246178 0.000013 -..oxy/mavproxy.py:188 MPState.master 10964 0.168562 0.224772 0.000021 -../python2.7/Queue.py:93 Queue.empty 12996 0.071587 0.200717 0.000015 -..tor/mavcrc.py:14 x25crc.accumulate 7912 0.177303 0.177303 0.000022 -..77 LinkModule.master_send_callback 1978 0.053488 0.159170 0.000080 -...py:228 ParamModule.mavlink_packet 5020 0.021206 0.137314 0.000027 -..y:6423 MAVLink.__parse_char_native 7437 0.017084 0.129977 0.000017 -..ink/mavutil.py:736 mavserial.write 1978 0.007265 0.124608 0.000063 -..util.py:212 mavserial.post_message 10040 0.100500 0.124568 0.000012 -..eading.py:300 _Condition._is_owned 20285 0.057222 0.120491 0.000006 -..al/serialposix.py:488 Serial.write 1978 0.032346 0.117342 0.000059 -..i.py:247 MPVehicle.message_factory 1958 0.008865 0.075378 0.000038 -..ings.py:104 MPSettings.__getattr__ 54128 0.075145 0.075145 0.000001 -..param.py:232 ParamModule.idle_task 1913 0.015143 0.065751 0.000034 -..ython2.7/Queue.py:200 Queue._qsize 23873 0.044139 0.061933 0.000003 -..pi.py:396 APIModule.mavlink_packet 5020 0.035728 0.061797 0.000012 -..MAVLink_heartbeat_message.__init__ 6998 0.043136 0.060533 0.000009 - -mavproxy.py --master=/dev/ttyUSB0,921600 --rtscts --cmd "api start tx_perf_test.py" --max-packets=5000 --daemon --native --nolog -name ncall tsub ttot tavg -..hon2.7/threading.py:752 Thread.run 1 0.000020 1.909725 1.909725 -..MAVProxy/mavproxy.py:686 main_loop 1 0.078463 1.909705 1.909705 -..mavproxy.py:510 process_master_all 2355 0.015327 1.343029 0.000570 -..oxy/mavproxy.py:470 process_master 4444 0.080486 1.327703 0.000299 -..tmega.py:6495 MAVLink.parse_buffer 2618 0.020202 1.026420 0.000392 -..lotmega.py:6433 MAVLink.parse_char 7708 0.095572 1.013109 0.000131 -..otmega.py:6428 MAVLink.__callbacks 5007 0.017085 0.743219 0.000148 -...py:230 LinkModule.master_callback 5007 0.151202 0.726134 0.000145 -.. MAVLink_command_long_message.pack 3952 0.036492 0.505875 0.000128 * 0.5, 25% of remaining - can shrink -../api.py:243 MPVehicle.send_mavlink 1976 0.015951 0.455050 0.000230 -..143 MAVLink_heartbeat_message.pack 3992 0.084775 0.454919 0.000114 -..ink.py:407 LinkModule.__to_modules 5007 0.127681 0.381962 0.000076 -..ardupilotmega.py:6402 MAVLink.send 1996 0.028108 0.380732 0.000191 -..y:9626 MAVLink.command_long_encode 1976 0.016601 0.346609 0.000175 -..oxy/mavproxy.py:651 periodic_tasks 2355 0.094366 0.305242 0.000130 -..mavcrc.py:23 x25crc.accumulate_str 7984 0.111864 0.301511 0.000038 -..erator/mavcrc.py:6 x25crc.__init__ 3992 0.022327 0.265754 0.000067 -..mp_module.py:83 ParamModule.master 11413 0.027317 0.253586 0.000022 -..oxy/mavproxy.py:188 MPState.master 11413 0.172153 0.226270 0.000020 -..link/mavutil.py:726 mavserial.recv 4604 0.020573 0.197259 0.000043 -..ial/serialposix.py:453 Serial.read 4604 0.083076 0.176686 0.000038 -..tor/mavcrc.py:14 x25crc.accumulate 7984 0.176148 0.176148 0.000022 -..y:6423 MAVLink.__parse_char_native 7708 0.017185 0.128009 0.000017 -...py:228 ParamModule.mavlink_packet 5007 0.019605 0.123487 0.000025 -..ink/mavutil.py:736 mavserial.write 1996 0.007271 0.115052 0.000058 -..util.py:212 mavserial.post_message 10014 0.087308 0.108892 0.000011 -..al/serialposix.py:488 Serial.write 1996 0.032340 0.107780 0.000054 -..i.py:247 MPVehicle.message_factory 1976 0.009835 0.084162 0.000043 -..param.py:232 ParamModule.idle_task 2355 0.017717 0.074372 0.000032 -..ings.py:104 MPSettings.__getattr__ 56145 0.067339 0.067339 0.000001 -..MAVLink_heartbeat_message.__init__ 7003 0.042130 0.060125 0.000009 -..pi.py:396 APIModule.mavlink_packet 5007 0.031268 0.055463 0.000011 -..Link_command_long_message.__init__ 1976 0.024271 0.047687 0.000024 -..til.py:1058 periodic_event.trigger 11776 0.030910 0.038640 0.000003 -..y/mavproxy.py:613 set_stream_rates 2356 0.022065 0.032767 0.000014 -..k.py:291 LinkModule.__update_state 5007 0.020987 0.029680 0.000006 -..proxy_wp.py:105 WPModule.idle_task 2355 0.011025 0.029247 0.000012 -../python2.7/Queue.py:93 Queue.empty 2355 0.015377 0.026646 0.000011 -..mega.py:43 MAVLink_header.__init__ 10995 0.025427 0.025427 0.000002 -..MAVLink_heartbeat_message.get_type 37436 0.023302 0.023302 0.000001 -..b/mp_module.py:51 APIModule.status 30058 0.020056 0.020056 0.000001 -..ilotmega.py:50 MAVLink_header.pack 3992 0.011181 0.019509 0.000005 - -Same options above, but fixed to remove unneeded pack call (about a 10% speedup on send: -name ncall tsub ttot tavg -/usr/lib/python2.7/threading.py:752 Thread.run 1 0.000012 2.105375 2.105375 -..rone/MAVProxy/MAVProxy/mavproxy.py:687 main_loop 1 0.083402 2.105364 2.105364 -..roxy/MAVProxy/mavproxy.py:510 process_master_all 2271 0.016510 1.507761 0.000664 -..MAVProxy/MAVProxy/mavproxy.py:470 process_master 4357 0.088733 1.491251 0.000342 -..s/v10/ardupilotmega.py:6495 MAVLink.parse_buffer 2613 0.022761 1.165566 0.000446 -..cts/v10/ardupilotmega.py:6433 MAVLink.parse_char 7720 0.108637 1.147566 0.000149 -..ts/v10/ardupilotmega.py:6428 MAVLink.__callbacks 5001 0.019474 0.842866 0.000169 -../mavproxy_link.py:230 LinkModule.master_callback 5001 0.173330 0.823391 0.000165 -..roneapi/module/api.py:243 MPVehicle.send_mavlink 1982 0.017148 0.554968 0.000280 -../dialects/v10/ardupilotmega.py:6402 MAVLink.send 2002 0.031468 0.478323 0.000239 -..les/mavproxy_link.py:407 LinkModule.__to_modules 5001 0.145099 0.432095 0.000086 -..MAVProxy/MAVProxy/mavproxy.py:651 periodic_tasks 2271 0.106075 0.333850 0.000147 -..otmega.py:4418 MAVLink_command_long_message.pack 1982 0.022973 0.308794 0.000156 -..upilotmega.py:143 MAVLink_heartbeat_message.pack 2002 0.051051 0.274553 0.000137 -..y/modules/lib/mp_module.py:83 ParamModule.master 11334 0.029509 0.272114 0.000024 -..MAVProxy/MAVProxy/mavproxy.py:188 MPState.master 11334 0.185972 0.242605 0.000021 -..6_64-2.7/pymavlink/mavutil.py:726 mavserial.recv 4566 0.022184 0.209205 0.000046 -..t-packages/serial/serialposix.py:453 Serial.read 4566 0.087580 0.187021 0.000041 -..ink/generator/mavcrc.py:23 x25crc.accumulate_str 4004 0.072369 0.183322 0.000046 -../pymavlink/generator/mavcrc.py:6 x25crc.__init__ 2002 0.014129 0.164718 0.000082 -..rdupilotmega.py:6423 MAVLink.__parse_char_native 7720 0.019109 0.144126 0.000019 -..mavproxy_param.py:228 ParamModule.mavlink_packet 5001 0.022000 0.140084 0.000028 -../pymavlink/mavutil.py:212 mavserial.post_message 10002 0.097754 0.122129 0.000012 -.._64-2.7/pymavlink/mavutil.py:736 mavserial.write 2002 0.008113 0.122124 0.000061 -..-packages/serial/serialposix.py:488 Serial.write 2002 0.033661 0.114011 0.000057 -..mavlink/generator/mavcrc.py:14 x25crc.accumulate 4004 0.102416 0.102416 0.000026 -..eapi/module/api.py:247 MPVehicle.message_factory 1982 0.009796 0.089112 0.000045 -..ules/mavproxy_param.py:232 ParamModule.idle_task 2271 0.019467 0.080866 0.000036 -..es/lib/mp_settings.py:104 MPSettings.__getattr__ 55391 0.070717 0.070717 0.000001 -..lotmega.py:56 MAVLink_heartbeat_message.__init__ 7003 0.047517 0.068058 0.000010 -..rdupilotmega.py:9452 MAVLink.command_long_encode 1982 0.013160 0.066973 0.000034 -..neapi/module/api.py:396 APIModule.mavlink_packet 5001 0.034765 0.061318 0.000012 -..ga.py:4403 MAVLink_command_long_message.__init__ 1982 0.027512 0.053814 0.000027 -..pymavlink/mavutil.py:1058 periodic_event.trigger 11356 0.033456 0.041815 0.000004 -..VProxy/MAVProxy/mavproxy.py:613 set_stream_rates 2272 0.024629 0.035985 0.000016 -..s/mavproxy_link.py:291 LinkModule.__update_state 5001 0.023399 0.032898 0.000007 -..xy/modules/mavproxy_wp.py:105 WPModule.idle_task 2271 0.012621 0.032238 0.000014 - -New baseline with previous optimiz applied but a longer run - -mavproxy.py --master=/dev/ttyUSB0,921600 --rtscts --cmd "api start tx_perf_test.py" --max-packets=20000 --daemon --native --nolog -name ncall tsub ttot tavg -/usr/lib/python2.7/threading.py:752 Thread.run 1 0.000014 9.295308 9.295308 -..rone/MAVProxy/MAVProxy/mavproxy.py:687 main_loop 1 0.357102 9.295294 9.295294 -..roxy/MAVProxy/mavproxy.py:510 process_master_all 9222 0.073024 6.726796 0.000729 -..MAVProxy/MAVProxy/mavproxy.py:470 process_master 17691 0.396538 6.653772 0.000376 -..s/v10/ardupilotmega.py:6495 MAVLink.parse_buffer 10787 0.103953 5.217913 0.000484 -..cts/v10/ardupilotmega.py:6433 MAVLink.parse_char 30840 0.475240 5.102247 0.000165 -..ts/v10/ardupilotmega.py:6428 MAVLink.__callbacks 20001 0.083017 3.773949 0.000189 -../mavproxy_link.py:230 LinkModule.master_callback 20001 0.771989 3.690932 0.000185 -..roneapi/module/api.py:243 MPVehicle.send_mavlink 7284 0.060705 2.045226 0.000281 -..les/mavproxy_link.py:407 LinkModule.__to_modules 20001 0.664688 1.929889 0.000096 -../dialects/v10/ardupilotmega.py:6402 MAVLink.send 7362 0.113751 1.770477 0.000240 -..MAVProxy/MAVProxy/mavproxy.py:651 periodic_tasks 9222 0.478214 1.513542 0.000164 -..otmega.py:4418 MAVLink_command_long_message.pack 7284 0.083047 1.141213 0.000157 -..y/modules/lib/mp_module.py:83 ParamModule.master 44230 0.127558 1.115275 0.000025 -..upilotmega.py:143 MAVLink_heartbeat_message.pack 7362 0.191518 1.018261 0.000138 -..MAVProxy/MAVProxy/mavproxy.py:188 MPState.master 44230 0.751921 0.987717 0.000022 -..6_64-2.7/pymavlink/mavutil.py:726 mavserial.recv 17790 0.096199 0.908664 0.000051 -..t-packages/serial/serialposix.py:453 Serial.read 17790 0.376893 0.812465 0.000046 -..ink/generator/mavcrc.py:23 x25crc.accumulate_str 14724 0.265819 0.676481 0.000046 -..rdupilotmega.py:6423 MAVLink.__parse_char_native 30840 0.084953 0.631528 0.000020 -..mavproxy_param.py:228 ParamModule.mavlink_packet 20001 0.099333 0.612545 0.000031 -../pymavlink/generator/mavcrc.py:6 x25crc.__init__ 7362 0.050913 0.606722 0.000082 -../pymavlink/mavutil.py:212 mavserial.post_message 40002 0.441143 0.552693 0.000014 -.._64-2.7/pymavlink/mavutil.py:736 mavserial.write 7362 0.029389 0.457603 0.000062 -..-packages/serial/serialposix.py:488 Serial.write 7362 0.124079 0.428214 0.000058 -..mavlink/generator/mavcrc.py:14 x25crc.accumulate 14724 0.379388 0.379388 0.000026 -..ules/mavproxy_param.py:232 ParamModule.idle_task 9222 0.088616 0.367637 0.000040 -..eapi/module/api.py:247 MPVehicle.message_factory 7284 0.038850 0.325071 0.000045 -..es/lib/mp_settings.py:104 MPSettings.__getattr__ 220.. 0.310079 0.310079 0.000001 -..lotmega.py:56 MAVLink_heartbeat_message.__init__ 27363 0.194285 0.275280 0.000010 -..neapi/module/api.py:396 APIModule.mavlink_packet 20001 0.153114 0.271093 0.000014 -..rdupilotmega.py:9452 MAVLink.command_long_encode 7284 0.046631 0.234445 0.000032 -..pymavlink/mavutil.py:1058 periodic_event.trigger 46111 0.154911 0.192188 0.000004 -..ga.py:4403 MAVLink_command_long_message.__init__ 7284 0.096461 0.187814 0.000026 -..s/mavproxy_link.py:291 LinkModule.__update_state 20001 0.115403 0.162070 0.000008 -..VProxy/MAVProxy/mavproxy.py:613 set_stream_rates 9223 0.106982 0.158716 0.000017 -..xy/modules/mavproxy_wp.py:105 WPModule.idle_task 9222 0.058149 0.147688 0.000016 -/usr/lib/python2.7/Queue.py:93 Queue.empty 9222 0.074266 0.128052 0.000014 -..lotmega.py:81 MAVLink_heartbeat_message.get_type 149.. 0.116623 0.116623 0.000001 -..oxy/modules/lib/mp_module.py:51 APIModule.status 122.. 0.103564 0.103564 0.000001 -..es/lib/mp_module.py:36 LinkModule.mavlink_packet 160.. 0.097872 0.097872 0.000001 -../v10/ardupilotmega.py:43 MAVLink_header.__init__ 34725 0.094988 0.094988 0.000003 -..xy/modules/lib/mp_module.py:43 LinkModule.module 27666 0.062593 0.093906 0.000003 - -Same args as previous, but optimzations to iterate only over the modules that have packet/idle -handlers. (a 30% speedup of master_callback and 10% speedup overall in tx_perf_test) - -name ncall tsub ttot tavg -/usr/lib/python2.7/threading.py:752 Thread.run 1 0.000014 7.506126 7.506126 -..rone/MAVProxy/MAVProxy/mavproxy.py:695 main_loop 1 0.341018 7.506112 7.506112 -..roxy/MAVProxy/mavproxy.py:520 process_master_all 10080 0.064470 5.301439 0.000526 -..MAVProxy/MAVProxy/mavproxy.py:480 process_master 18691 0.343054 5.236969 0.000280 -..s/v10/ardupilotmega.py:6495 MAVLink.parse_buffer 10922 0.085098 3.990414 0.000365 -..cts/v10/ardupilotmega.py:6433 MAVLink.parse_char 31061 0.402903 3.901436 0.000126 -..ts/v10/ardupilotmega.py:6428 MAVLink.__callbacks 20002 0.071189 2.773164 0.000139 -../mavproxy_link.py:230 LinkModule.master_callback 20002 0.649377 2.701975 0.000135 -..roneapi/module/api.py:243 MPVehicle.send_mavlink 7328 0.053543 1.706073 0.000233 -../dialects/v10/ardupilotmega.py:6402 MAVLink.send 7406 0.098886 1.474602 0.000199 -..les/mavproxy_link.py:408 LinkModule.__to_modules 20002 0.365783 1.235950 0.000062 -..MAVProxy/MAVProxy/mavproxy.py:661 periodic_tasks 10080 0.346112 1.176351 0.000117 -..otmega.py:4418 MAVLink_command_long_message.pack 7328 0.070202 0.952317 0.000130 -..y/modules/lib/mp_module.py:83 ParamModule.master 45160 0.102112 0.937280 0.000021 -..upilotmega.py:143 MAVLink_heartbeat_message.pack 7406 0.159165 0.847291 0.000114 -..MAVProxy/MAVProxy/mavproxy.py:190 MPState.master 45160 0.639222 0.835168 0.000018 -..6_64-2.7/pymavlink/mavutil.py:726 mavserial.recv 18966 0.086703 0.793945 0.000042 -..t-packages/serial/serialposix.py:453 Serial.read 18966 0.339244 0.707242 0.000037 -..ink/generator/mavcrc.py:23 x25crc.accumulate_str 14812 0.222656 0.562755 0.000038 -..rdupilotmega.py:6423 MAVLink.__parse_char_native 31061 0.070296 0.532188 0.000017 -../pymavlink/generator/mavcrc.py:6 x25crc.__init__ 7406 0.043614 0.509105 0.000069 -..mavproxy_param.py:228 ParamModule.mavlink_packet 20002 0.079863 0.493304 0.000025 -../pymavlink/mavutil.py:212 mavserial.post_message 40004 0.362677 0.449900 0.000011 -.._64-2.7/pymavlink/mavutil.py:736 mavserial.write 7406 0.025578 0.374007 0.000051 -..-packages/serial/serialposix.py:488 Serial.write 7406 0.108336 0.348429 0.000047 -..ules/mavproxy_param.py:232 ParamModule.idle_task 10080 0.076702 0.320194 0.000032 -..mavlink/generator/mavcrc.py:14 x25crc.accumulate 14812 0.313900 0.313900 0.000021 -..eapi/module/api.py:247 MPVehicle.message_factory 7328 0.033713 0.288784 0.000039 -..es/lib/mp_settings.py:104 MPSettings.__getattr__ 228.. 0.260477 0.260477 0.000001 -..lotmega.py:56 MAVLink_heartbeat_message.__init__ 27408 0.162975 0.230682 0.000008 -..neapi/module/api.py:396 APIModule.mavlink_packet 20002 0.129320 0.229457 0.000011 -..rdupilotmega.py:9452 MAVLink.command_long_encode 7328 0.040013 0.202411 0.000028 -..pymavlink/mavutil.py:1058 periodic_event.trigger 50401 0.136115 0.169156 0.000003 -..ga.py:4403 MAVLink_command_long_message.__init__ 7328 0.083786 0.162398 0.000022 -..VProxy/MAVProxy/mavproxy.py:623 set_stream_rates 10081 0.098845 0.144738 0.000014 -..s/mavproxy_link.py:292 LinkModule.__update_state 20002 0.099563 0.138054 0.000007 -..xy/modules/mavproxy_wp.py:105 WPModule.idle_task 10080 0.051101 0.130417 0.000013 -/usr/lib/python2.7/Queue.py:93 Queue.empty 10080 0.066707 0.116093 0.000012 -..lotmega.py:81 MAVLink_heartbeat_message.get_type 149.. 0.095856 0.095856 0.000001 -..xy/modules/lib/mp_module.py:43 LinkModule.module 30240 0.054315 0.083003 0.000003 - -""" - -from pymavlink import mavutil -import time - -UPDATE_RATE = 50.0 - -class PerfTest(): - def __init__(self): - # First get an instance of the API endpoint - api = local_connect() - - # get our vehicle - when running with mavproxy it only knows about one vehicle (for now) - self.vehicle = api.get_vehicles()[0] - - self.vehicle.wait_init() - - while True: - msg = self.vehicle.message_factory.command_long_encode( - 0, 1, # target system, target component - mavutil.mavlink.MAV_CMD_DO_CHANGE_SPEED, # frame - 0, # confirmation - 1, 1.0, -1, # params 1-3 - 0.0, 0.0, 0.0, 0.0 ) # params 4-7 (not used) - - - # send command to vehicle - self.vehicle.send_mavlink(msg) - - msg2 = self.vehicle.message_factory.command_long_encode( - 0, 1, # target system, target component - mavutil.mavlink.MAV_CMD_DO_SET_ROI, #command - 0, #confirmation - 0, 0, 0, 0, #params 1-4 - 33.0, - 100.9, - 40.6 - ) - - self.vehicle.send_mavlink(msg2) - time.sleep( 1 / UPDATE_RATE ) - -PerfTest() \ No newline at end of file diff --git a/examples/performance_test/performance_test.py b/examples/performance_test/performance_test.py new file mode 100644 index 000000000..2be53b46c --- /dev/null +++ b/examples/performance_test/performance_test.py @@ -0,0 +1,120 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +© Copyright 2015-2016, 3D Robotics. +performance_test.py: + +This performance test logs the interval between messages being +sent by Dronekit-Python and an acknowledgment being received +from the autopilot. It provides a running report of the maximum, +minimum, and most recent interval for 30 seconds. + +Full documentation is provided at http://python.dronekit.io/examples/performance_test.html +""" +from __future__ import print_function +from dronekit import connect +from pymavlink import mavutil +import time +import sys +from datetime import datetime + + +#Set up option parsing to get connection string +import argparse +parser = argparse.ArgumentParser(description='Generates max, min and current interval between message sent and ack recieved. Will start and connect to SITL if no connection string specified.') +parser.add_argument('--connect', + help="vehicle connection target string. If not specified, SITL automatically started and used.") +args = parser.parse_args() + +connection_string=args.connect + +#Start SITL if no connection string specified +if not connection_string: + import dronekit_sitl + sitl = dronekit_sitl.start_default() + connection_string = sitl.connection_string() + + +# Connect to the Vehicle +print('Connecting to vehicle on: %s' % connection_string) +vehicle = connect(connection_string, wait_ready=True) + +#global vehicle + + +def cur_usec(): + """Return current time in usecs""" + # t = time.time() + dt = datetime.now() + t = dt.minute * 60 + dt.second + dt.microsecond / (1e6) + return t + +class MeasureTime(object): + def __init__(self): + self.prevtime = cur_usec() + self.previnterval = 0 + self.numcount = 0 + self.reset() + + def reset(self): + self.maxinterval = 0 + self.mininterval = 10000 + + def log(self): + #print "Interval", self.previnterval + #print "MaxInterval", self.maxinterval + #print "MinInterval", self.mininterval + sys.stdout.write('MaxInterval: %s\tMinInterval: %s\tInterval: %s\r' % (self.maxinterval,self.mininterval, self.previnterval) ) + sys.stdout.flush() + + + def update(self): + now = cur_usec() + self.numcount = self.numcount + 1 + self.previnterval = now - self.prevtime + self.prevtime = now + if self.numcount>1: #ignore first value where self.prevtime not reliable. + self.maxinterval = max(self.previnterval, self.maxinterval) + self.mininterval = min(self.mininterval, self.previnterval) + self.log() + + +acktime = MeasureTime() + + +#Create COMMAND_ACK message listener. +@vehicle.on_message('COMMAND_ACK') +def listener(self, name, message): + acktime.update() + send_testpackets() + + +def send_testpackets(): + #Send message using `command_long_encode` (returns an ACK) + msg = vehicle.message_factory.command_long_encode( + 1, 1, # target system, target component + #mavutil.mavlink.MAV_CMD_DO_SET_RELAY, #command + mavutil.mavlink.MAV_CMD_DO_SET_ROI, #command + 0, #confirmation + 0, 0, 0, 0, #params 1-4 + 0, + 0, + 0 + ) + + vehicle.send_mavlink(msg) + +#Start logging by sending a test packet +send_testpackets() + +print("Logging for 30 seconds") +for x in range(1,30): + time.sleep(1) + +# Close vehicle object before exiting script +vehicle.close() + +if not args.connect: + # Shut down simulator if it was started. + sitl.stop() diff --git a/examples/play_tune/play_tune.py b/examples/play_tune/play_tune.py new file mode 100644 index 000000000..df0995222 --- /dev/null +++ b/examples/play_tune/play_tune.py @@ -0,0 +1,42 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +© Copyright 2017, Peter Barker +play_tune.py: GUIDED mode "simple goto" example (Copter Only) + +Demonstrates how to play a custom tune on a vehicle using the vehicle's buzzer + +Full documentation is provided at http://python.dronekit.io/examples/play_tune.html +""" + +from __future__ import print_function +import time +from dronekit import connect + + +# Set up option parsing to get connection string +import argparse +parser = argparse.ArgumentParser(description='Play tune on vehicle buzzer.') +parser.add_argument('--connect', + help="Vehicle connection target string. If not specified, SITL automatically started and used.") +parser.add_argument('--tune', type=str, help="tune to play", default="AAAA") +args = parser.parse_args() + +connection_string = args.connect +sitl = None + + +# Start SITL if no connection string specified +if not connection_string: + print("SITL doesn't do tunes?!") + import dronekit_sitl + sitl = dronekit_sitl.start_default() + connection_string = sitl.connection_string() + + +# Connect to the Vehicle +print('Connecting to vehicle on: %s' % connection_string) +vehicle = connect(connection_string, wait_ready=True) + +vehicle.play_tune(args.tune) diff --git a/examples/reboot/reboot.py b/examples/reboot/reboot.py new file mode 100755 index 000000000..698d10142 --- /dev/null +++ b/examples/reboot/reboot.py @@ -0,0 +1,36 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +from __future__ import print_function + +from dronekit import connect +import time + +# Set up option parsing to get connection string +import argparse +parser = argparse.ArgumentParser(description='Reboots vehicle') +parser.add_argument('--connect', + help="Vehicle connection target string. If not specified, SITL automatically started and used.") +args = parser.parse_args() + +connection_string = args.connect +sitl = None + + +# Start SITL if no connection string specified +if not connection_string: + import dronekit_sitl + sitl = dronekit_sitl.start_default() + connection_string = sitl.connection_string() + + +# Connect to the Vehicle +print('Connecting to vehicle on: %s' % connection_string) +vehicle = connect(connection_string, wait_ready=True) + +vehicle.reboot() +time.sleep(1) + +# Shut down simulator if it was started. +if sitl: + sitl.stop() diff --git a/examples/set_attitude_target/set_attitude_target.py b/examples/set_attitude_target/set_attitude_target.py new file mode 100755 index 000000000..d182637b1 --- /dev/null +++ b/examples/set_attitude_target/set_attitude_target.py @@ -0,0 +1,192 @@ +#!/usr/bin/env python + +""" + +set_attitude_target.py: (Copter Only) + +This example shows how to move/direct Copter and send commands + in GUIDED_NOGPS mode using DroneKit Python. + +Caution: A lot of unexpected behaviors may occur in GUIDED_NOGPS mode. + Always watch the drone movement, and make sure that you are in dangerless environment. + Land the drone as soon as possible when it shows any unexpected behavior. + +Tested in Python 2.7.10 + +""" + +from dronekit import connect, VehicleMode, LocationGlobal, LocationGlobalRelative +from pymavlink import mavutil # Needed for command message definitions +import time +import math + +# Set up option parsing to get connection string +import argparse +parser = argparse.ArgumentParser(description='Control Copter and send commands in GUIDED mode ') +parser.add_argument('--connect', + help="Vehicle connection target string. If not specified, SITL automatically started and used.") +args = parser.parse_args() + +connection_string = args.connect +sitl = None + +# Start SITL if no connection string specified +if not connection_string: + import dronekit_sitl + sitl = dronekit_sitl.start_default() + connection_string = sitl.connection_string() + + +# Connect to the Vehicle +print('Connecting to vehicle on: %s' % connection_string) +vehicle = connect(connection_string, wait_ready=True) + +def arm_and_takeoff_nogps(aTargetAltitude): + """ + Arms vehicle and fly to aTargetAltitude without GPS data. + """ + + ##### CONSTANTS ##### + DEFAULT_TAKEOFF_THRUST = 0.7 + SMOOTH_TAKEOFF_THRUST = 0.6 + + print("Basic pre-arm checks") + # Don't let the user try to arm until autopilot is ready + # If you need to disable the arming check, + # just comment it with your own responsibility. + while not vehicle.is_armable: + print(" Waiting for vehicle to initialise...") + time.sleep(1) + + + print("Arming motors") + # Copter should arm in GUIDED_NOGPS mode + vehicle.mode = VehicleMode("GUIDED_NOGPS") + vehicle.armed = True + + while not vehicle.armed: + print(" Waiting for arming...") + vehicle.armed = True + time.sleep(1) + + print("Taking off!") + + thrust = DEFAULT_TAKEOFF_THRUST + while True: + current_altitude = vehicle.location.global_relative_frame.alt + print(" Altitude: %f Desired: %f" % + (current_altitude, aTargetAltitude)) + if current_altitude >= aTargetAltitude*0.95: # Trigger just below target alt. + print("Reached target altitude") + break + elif current_altitude >= aTargetAltitude*0.6: + thrust = SMOOTH_TAKEOFF_THRUST + set_attitude(thrust = thrust) + time.sleep(0.2) + +def send_attitude_target(roll_angle = 0.0, pitch_angle = 0.0, + yaw_angle = None, yaw_rate = 0.0, use_yaw_rate = False, + thrust = 0.5): + """ + use_yaw_rate: the yaw can be controlled using yaw_angle OR yaw_rate. + When one is used, the other is ignored by Ardupilot. + thrust: 0 <= thrust <= 1, as a fraction of maximum vertical thrust. + Note that as of Copter 3.5, thrust = 0.5 triggers a special case in + the code for maintaining current altitude. + """ + if yaw_angle is None: + # this value may be unused by the vehicle, depending on use_yaw_rate + yaw_angle = vehicle.attitude.yaw + # Thrust > 0.5: Ascend + # Thrust == 0.5: Hold the altitude + # Thrust < 0.5: Descend + msg = vehicle.message_factory.set_attitude_target_encode( + 0, # time_boot_ms + 1, # Target system + 1, # Target component + 0b00000000 if use_yaw_rate else 0b00000100, + to_quaternion(roll_angle, pitch_angle, yaw_angle), # Quaternion + 0, # Body roll rate in radian + 0, # Body pitch rate in radian + math.radians(yaw_rate), # Body yaw rate in radian/second + thrust # Thrust + ) + vehicle.send_mavlink(msg) + +def set_attitude(roll_angle = 0.0, pitch_angle = 0.0, + yaw_angle = None, yaw_rate = 0.0, use_yaw_rate = False, + thrust = 0.5, duration = 0): + """ + Note that from AC3.3 the message should be re-sent more often than every + second, as an ATTITUDE_TARGET order has a timeout of 1s. + In AC3.2.1 and earlier the specified attitude persists until it is canceled. + The code below should work on either version. + Sending the message multiple times is the recommended way. + """ + send_attitude_target(roll_angle, pitch_angle, + yaw_angle, yaw_rate, False, + thrust) + start = time.time() + while time.time() - start < duration: + send_attitude_target(roll_angle, pitch_angle, + yaw_angle, yaw_rate, False, + thrust) + time.sleep(0.1) + # Reset attitude, or it will persist for 1s more due to the timeout + send_attitude_target(0, 0, + 0, 0, True, + thrust) + +def to_quaternion(roll = 0.0, pitch = 0.0, yaw = 0.0): + """ + Convert degrees to quaternions + """ + t0 = math.cos(math.radians(yaw * 0.5)) + t1 = math.sin(math.radians(yaw * 0.5)) + t2 = math.cos(math.radians(roll * 0.5)) + t3 = math.sin(math.radians(roll * 0.5)) + t4 = math.cos(math.radians(pitch * 0.5)) + t5 = math.sin(math.radians(pitch * 0.5)) + + w = t0 * t2 * t4 + t1 * t3 * t5 + x = t0 * t3 * t4 - t1 * t2 * t5 + y = t0 * t2 * t5 + t1 * t3 * t4 + z = t1 * t2 * t4 - t0 * t3 * t5 + + return [w, x, y, z] + +# Take off 2.5m in GUIDED_NOGPS mode. +arm_and_takeoff_nogps(2.5) + +# Hold the position for 3 seconds. +print("Hold position for 3 seconds") +set_attitude(duration = 3) + +# Uncomment the lines below for testing roll angle and yaw rate. +# Make sure that there is enough space for testing this. + +# set_attitude(roll_angle = 1, thrust = 0.5, duration = 3) +# set_attitude(yaw_rate = 30, thrust = 0.5, duration = 3) + +# Move the drone forward and backward. +# Note that it will be in front of original position due to inertia. +print("Move forward") +set_attitude(pitch_angle = -5, thrust = 0.5, duration = 3.21) + +print("Move backward") +set_attitude(pitch_angle = 5, thrust = 0.5, duration = 3) + + +print("Setting LAND mode...") +vehicle.mode = VehicleMode("LAND") +time.sleep(1) + +# Close vehicle object before exiting script +print("Close vehicle object") +vehicle.close() + +# Shut down simulator if it was started. +if sitl is not None: + sitl.stop() + +print("Completed") diff --git a/examples/simple_goto/simple_goto.py b/examples/simple_goto/simple_goto.py index fb7caeb49..9fe54ee2f 100644 --- a/examples/simple_goto/simple_goto.py +++ b/examples/simple_goto/simple_goto.py @@ -1,75 +1,105 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + """ +© Copyright 2015-2016, 3D Robotics. simple_goto.py: GUIDED mode "simple goto" example (Copter Only) -The example demonstrates how to arm and takeoff in Copter and how to navigate to -points using Vehicle.commands.goto. +Demonstrates how to arm and takeoff in Copter and how to navigate to points using Vehicle.simple_goto. Full documentation is provided at http://python.dronekit.io/examples/simple_goto.html """ +from __future__ import print_function import time -from dronekit.lib import VehicleMode, Location -from pymavlink import mavutil +from dronekit import connect, VehicleMode, LocationGlobalRelative + + +# Set up option parsing to get connection string +import argparse +parser = argparse.ArgumentParser(description='Commands vehicle using vehicle.simple_goto.') +parser.add_argument('--connect', + help="Vehicle connection target string. If not specified, SITL automatically started and used.") +args = parser.parse_args() + +connection_string = args.connect +sitl = None + + +# Start SITL if no connection string specified +if not connection_string: + import dronekit_sitl + sitl = dronekit_sitl.start_default() + connection_string = sitl.connection_string() + + +# Connect to the Vehicle +print('Connecting to vehicle on: %s' % connection_string) +vehicle = connect(connection_string, wait_ready=True) -api = local_connect() -vehicle = api.get_vehicles()[0] def arm_and_takeoff(aTargetAltitude): """ Arms vehicle and fly to aTargetAltitude. """ - print "Basic pre-arm checks" - # Don't let the user try to fly autopilot is booting - if vehicle.mode.name == "INITIALISING": - print "Waiting for vehicle to initialise" - time.sleep(1) - while vehicle.gps_0.fix_type < 2: - print "Waiting for GPS...:", vehicle.gps_0.fix_type + print("Basic pre-arm checks") + # Don't try to arm until autopilot is ready + while not vehicle.is_armable: + print(" Waiting for vehicle to initialise...") time.sleep(1) - - print "Arming motors" + + print("Arming motors") # Copter should arm in GUIDED mode - vehicle.mode = VehicleMode("GUIDED") - vehicle.armed = True - vehicle.flush() + vehicle.mode = VehicleMode("GUIDED") + vehicle.armed = True - while not vehicle.armed and not api.exit: - print " Waiting for arming..." + # Confirm vehicle armed before attempting to take off + while not vehicle.armed: + print(" Waiting for arming...") time.sleep(1) - print "Taking off!" - vehicle.commands.takeoff(aTargetAltitude) # Take off to target altitude - vehicle.flush() - - # Wait until the vehicle reaches a safe height before processing the goto (otherwise the command - # after Vehicle.commands.takeoff will execute immediately). - while not api.exit: - print " Altitude: ", vehicle.location.alt - if vehicle.location.alt>=aTargetAltitude*0.95: #Just below target, in case of undershoot. - print "Reached target altitude" - break; + print("Taking off!") + vehicle.simple_takeoff(aTargetAltitude) # Take off to target altitude + + # Wait until the vehicle reaches a safe height before processing the goto + # (otherwise the command after Vehicle.simple_takeoff will execute + # immediately). + while True: + print(" Altitude: ", vehicle.location.global_relative_frame.alt) + # Break and return from function just below target altitude. + if vehicle.location.global_relative_frame.alt >= aTargetAltitude * 0.95: + print("Reached target altitude") + break time.sleep(1) -arm_and_takeoff(20) +arm_and_takeoff(10) + +print("Set default/target airspeed to 3") +vehicle.airspeed = 3 -print "Going to first point..." -point1 = Location(-35.361354, 149.165218, 20, is_relative=True) -vehicle.commands.goto(point1) -vehicle.flush() +print("Going towards first point for 30 seconds ...") +point1 = LocationGlobalRelative(-35.361354, 149.165218, 20) +vehicle.simple_goto(point1) # sleep so we can see the change in map time.sleep(30) -print "Going to second point..." -point2 = Location(-35.363244, 149.168801, 20, is_relative=True) -vehicle.commands.goto(point2) -vehicle.flush() +print("Going towards second point for 30 seconds (groundspeed set to 10 m/s) ...") +point2 = LocationGlobalRelative(-35.363244, 149.168801, 20) +vehicle.simple_goto(point2, groundspeed=10) # sleep so we can see the change in map -time.sleep(20) +time.sleep(30) + +print("Returning to Launch") +vehicle.mode = VehicleMode("RTL") + +# Close vehicle object before exiting script +print("Close vehicle object") +vehicle.close() -print "Returning to Launch" -vehicle.mode = VehicleMode("RTL") -vehicle.flush() \ No newline at end of file +# Shut down simulator if it was started. +if sitl: + sitl.stop() diff --git a/examples/vehicle_state/vehicle_state.py b/examples/vehicle_state/vehicle_state.py index f33eb5228..3fd3717d0 100644 --- a/examples/vehicle_state/vehicle_state.py +++ b/examples/vehicle_state/vehicle_state.py @@ -1,131 +1,271 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + """ +© Copyright 2015-2016, 3D Robotics. vehicle_state.py: -Demonstrates how to get and set vehicle state, parameter and channel-override information, +Demonstrates how to get and set vehicle state and parameter information, and how to observe vehicle attribute (state) changes. Full documentation is provided at http://python.dronekit.io/examples/vehicle_state.html """ -from dronekit import connect -from dronekit.lib import VehicleMode -from pymavlink import mavutil +from __future__ import print_function +from dronekit import connect, VehicleMode import time #Set up option parsing to get connection string import argparse parser = argparse.ArgumentParser(description='Print out vehicle state information. Connects to SITL on local PC by default.') -parser.add_argument('--connect', default='127.0.0.1:14550', - help="vehicle connection target. Default '127.0.0.1:14550'") +parser.add_argument('--connect', + help="vehicle connection target string. If not specified, SITL automatically started and used.") args = parser.parse_args() +connection_string = args.connect +sitl = None + + +#Start SITL if no connection string specified +if not connection_string: + import dronekit_sitl + sitl = dronekit_sitl.start_default() + connection_string = sitl.connection_string() -# Connect to the Vehicle -print 'Connecting to vehicle on: %s' % args.connect -vehicle = connect(args.connect, await_params=True) -# Wait for attribtues to accumulate. -time.sleep(5) +# Connect to the Vehicle. +# Set `wait_ready=True` to ensure default attributes are populated before `connect()` returns. +print("\nConnecting to vehicle on: %s" % connection_string) +vehicle = connect(connection_string, wait_ready=True) + +vehicle.wait_ready('autopilot_version') # Get all vehicle attributes (state) -print "\nGet all vehicle attribute values:" -print " Location: %s" % vehicle.location -print " Attitude: %s" % vehicle.attitude -print " Velocity: %s" % vehicle.velocity -print " GPS: %s" % vehicle.gps_0 -print " Groundspeed: %s" % vehicle.groundspeed -print " Airspeed: %s" % vehicle.airspeed -print " Mount status: %s" % vehicle.mount_status -print " Battery: %s" % vehicle.battery -print " Rangefinder: %s" % vehicle.rangefinder -print " Rangefinder distance: %s" % vehicle.rangefinder.distance -print " Rangefinder voltage: %s" % vehicle.rangefinder.voltage -print " Mode: %s" % vehicle.mode.name # settable -print " Armed: %s" % vehicle.armed # settable - - -# Set vehicle mode and armed attributes (the only settable attributes) -print "Set Vehicle.mode=GUIDED (currently: %s)" % vehicle.mode.name +print("\nGet all vehicle attribute values:") +print(" Autopilot Firmware version: %s" % vehicle.version) +print(" Major version number: %s" % vehicle.version.major) +print(" Minor version number: %s" % vehicle.version.minor) +print(" Patch version number: %s" % vehicle.version.patch) +print(" Release type: %s" % vehicle.version.release_type()) +print(" Release version: %s" % vehicle.version.release_version()) +print(" Stable release?: %s" % vehicle.version.is_stable()) +print(" Autopilot capabilities") +print(" Supports MISSION_FLOAT message type: %s" % vehicle.capabilities.mission_float) +print(" Supports PARAM_FLOAT message type: %s" % vehicle.capabilities.param_float) +print(" Supports MISSION_INT message type: %s" % vehicle.capabilities.mission_int) +print(" Supports COMMAND_INT message type: %s" % vehicle.capabilities.command_int) +print(" Supports PARAM_UNION message type: %s" % vehicle.capabilities.param_union) +print(" Supports ftp for file transfers: %s" % vehicle.capabilities.ftp) +print(" Supports commanding attitude offboard: %s" % vehicle.capabilities.set_attitude_target) +print(" Supports commanding position and velocity targets in local NED frame: %s" % vehicle.capabilities.set_attitude_target_local_ned) +print(" Supports set position + velocity targets in global scaled integers: %s" % vehicle.capabilities.set_altitude_target_global_int) +print(" Supports terrain protocol / data handling: %s" % vehicle.capabilities.terrain) +print(" Supports direct actuator control: %s" % vehicle.capabilities.set_actuator_target) +print(" Supports the flight termination command: %s" % vehicle.capabilities.flight_termination) +print(" Supports mission_float message type: %s" % vehicle.capabilities.mission_float) +print(" Supports onboard compass calibration: %s" % vehicle.capabilities.compass_calibration) +print(" Global Location: %s" % vehicle.location.global_frame) +print(" Global Location (relative altitude): %s" % vehicle.location.global_relative_frame) +print(" Local Location: %s" % vehicle.location.local_frame) +print(" Attitude: %s" % vehicle.attitude) +print(" Velocity: %s" % vehicle.velocity) +print(" GPS: %s" % vehicle.gps_0) +print(" Gimbal status: %s" % vehicle.gimbal) +print(" Battery: %s" % vehicle.battery) +print(" EKF OK?: %s" % vehicle.ekf_ok) +print(" Last Heartbeat: %s" % vehicle.last_heartbeat) +print(" Rangefinder: %s" % vehicle.rangefinder) +print(" Rangefinder distance: %s" % vehicle.rangefinder.distance) +print(" Rangefinder voltage: %s" % vehicle.rangefinder.voltage) +print(" Heading: %s" % vehicle.heading) +print(" Is Armable?: %s" % vehicle.is_armable) +print(" System status: %s" % vehicle.system_status.state) +print(" Groundspeed: %s" % vehicle.groundspeed) # settable +print(" Airspeed: %s" % vehicle.airspeed) # settable +print(" Mode: %s" % vehicle.mode.name) # settable +print(" Armed: %s" % vehicle.armed) # settable + + + +# Get Vehicle Home location - will be `None` until first set by autopilot +while not vehicle.home_location: + cmds = vehicle.commands + cmds.download() + cmds.wait_ready() + if not vehicle.home_location: + print(" Waiting for home location ...") +# We have a home location, so print it! +print("\n Home location: %s" % vehicle.home_location) + + +# Set vehicle home_location, mode, and armed attributes (the only settable attributes) + +print("\nSet new home location") +# Home location must be within 50km of EKF home location (or setting will fail silently) +# In this case, just set value to current location with an easily recognisable altitude (222) +my_location_alt = vehicle.location.global_frame +my_location_alt.alt = 222.0 +vehicle.home_location = my_location_alt +print(" New Home Location (from attribute - altitude should be 222): %s" % vehicle.home_location) + +#Confirm current value on vehicle by re-downloading commands +cmds = vehicle.commands +cmds.download() +cmds.wait_ready() +print(" New Home Location (from vehicle - altitude should be 222): %s" % vehicle.home_location) + + +print("\nSet Vehicle.mode = GUIDED (currently: %s)" % vehicle.mode.name) vehicle.mode = VehicleMode("GUIDED") -vehicle.flush() # Flush to guarantee that previous writes to the vehicle have taken place while not vehicle.mode.name=='GUIDED': #Wait until mode has changed - print " Waiting for mode change ..." + print(" Waiting for mode change ...") time.sleep(1) -print "Set Vehicle.armed=True (currently: %s)" % vehicle.armed -vehicle.armed = True -vehicle.flush() -while not vehicle.armed: - print " Waiting for arming..." + +# Check that vehicle is armable +while not vehicle.is_armable: + print(" Waiting for vehicle to initialise...") time.sleep(1) + # If required, you can provide additional information about initialisation + # using `vehicle.gps_0.fix_type` and `vehicle.mode.name`. + +#print "\nSet Vehicle.armed=True (currently: %s)" % vehicle.armed +#vehicle.armed = True +#while not vehicle.armed: +# print " Waiting for arming..." +# time.sleep(1) +#print " Vehicle is armed: %s" % vehicle.armed + + +# Add and remove and attribute callbacks + +#Define callback for `vehicle.attitude` observer +last_attitude_cache = None +def attitude_callback(self, attr_name, value): + # `attr_name` - the observed attribute (used if callback is used for multiple attributes) + # `self` - the associated vehicle object (used if a callback is different for multiple vehicles) + # `value` is the updated attribute value. + global last_attitude_cache + # Only publish when value changes + if value!=last_attitude_cache: + print(" CALLBACK: Attitude changed to", value) + last_attitude_cache=value + +print("\nAdd `attitude` attribute callback/observer on `vehicle`") +vehicle.add_attribute_listener('attitude', attitude_callback) + +print(" Wait 2s so callback invoked before observer removed") +time.sleep(2) +print(" Remove Vehicle.attitude observer") +# Remove observer added with `add_attribute_listener()` specifying the attribute and callback function +vehicle.remove_attribute_listener('attitude', attitude_callback) -# Show how to add and remove and attribute observer callbacks (using mode as example) -def mode_callback(attribute): - print " CALLBACK: Mode changed to: ", vehicle.mode.name -print "\nAdd mode attribute observer for Vehicle.mode" -vehicle.add_attribute_observer('mode', mode_callback) + +# Add mode attribute callback using decorator (callbacks added this way cannot be removed). +print("\nAdd `mode` attribute callback/observer using decorator") +@vehicle.on_attribute('mode') +def decorated_mode_callback(self, attr_name, value): + # `attr_name` is the observed attribute (used if callback is used for multiple attributes) + # `attr_name` - the observed attribute (used if callback is used for multiple attributes) + # `value` is the updated attribute value. + print(" CALLBACK: Mode changed to", value) -print " Set mode=STABILIZE (currently: %s)" % vehicle.mode.name +print(" Set mode=STABILIZE (currently: %s) and wait for callback" % vehicle.mode.name) vehicle.mode = VehicleMode("STABILIZE") -vehicle.flush() -print " Wait 2s so callback invoked before observer removed" +print(" Wait 2s so callback invoked before moving to next example") time.sleep(2) -# Remove observer - specifying the attribute and previously registered callback function -vehicle.remove_attribute_observer('mode', mode_callback) +print("\n Attempt to remove observer added with `on_attribute` decorator (should fail)") +try: + vehicle.remove_attribute_listener('mode', decorated_mode_callback) +except: + print(" Exception: Cannot remove observer added using decorator") -# Get Vehicle Home location ((0 index in Vehicle.commands) -print "\nGet home location" -cmds = vehicle.commands -cmds.download() -cmds.wait_valid() -print " Home WP: %s" % cmds[0] + + +# Demonstrate getting callback on any attribute change +def wildcard_callback(self, attr_name, value): + print(" CALLBACK: (%s): %s" % (attr_name,value)) + +print("\nAdd attribute callback detecting ANY attribute change") +vehicle.add_attribute_listener('*', wildcard_callback) + + +print(" Wait 1s so callback invoked before observer removed") +time.sleep(1) + +print(" Remove Vehicle attribute observer") +# Remove observer added with `add_attribute_listener()` +vehicle.remove_attribute_listener('*', wildcard_callback) + # Get/Set Vehicle Parameters -print "\nRead vehicle param 'THR_MIN': %s" % vehicle.parameters['THR_MIN'] -print "Write vehicle param 'THR_MIN' : 10" +print("\nRead and write parameters") +print(" Read vehicle param 'THR_MIN': %s" % vehicle.parameters['THR_MIN']) + +print(" Write vehicle param 'THR_MIN' : 10") vehicle.parameters['THR_MIN']=10 -vehicle.flush() -print "Read new value of param 'THR_MIN': %s" % vehicle.parameters['THR_MIN'] +print(" Read new value of param 'THR_MIN': %s" % vehicle.parameters['THR_MIN']) -# Demo callback handler for raw MAVLink messages -def mavrx_debug_handler(message): - print "Raw MAVLink message: ", message +print("\nPrint all parameters (iterate `vehicle.parameters`):") +for key, value in vehicle.parameters.iteritems(): + print(" Key:%s Value:%s" % (key,value)) + -print "\nSet MAVLink callback handler (start receiving all MAVLink messages)" -vehicle.set_mavlink_callback(mavrx_debug_handler) +print("\nCreate parameter observer using decorator") +# Parameter string is case-insensitive +# Value is cached (listeners are only updated on change) +# Observer added using decorator can't be removed. + +@vehicle.parameters.on_attribute('THR_MIN') +def decorated_thr_min_callback(self, attr_name, value): + print(" PARAMETER CALLBACK: %s changed to: %s" % (attr_name, value)) -print "Wait 1s so mavrx_debug_handler has a chance to be called before it is removed" -time.sleep(1) - -print "Remove the MAVLink callback handler (stop getting messages)" -vehicle.unset_mavlink_callback() +print("Write vehicle param 'THR_MIN' : 20 (and wait for callback)") +vehicle.parameters['THR_MIN']=20 +for x in range(1,5): + #Callbacks may not be updated for a few seconds + if vehicle.parameters['THR_MIN']==20: + break + time.sleep(1) -# Overriding an RC channel -# NOTE: CHANNEL OVERRIDES may be useful for simulating user input and when implementing certain types of joystick control. -#DO NOT use unless there is no other choice (there almost always is!) -print "\nOverriding RC channels for roll and yaw" -vehicle.channel_override = { "1" : 900, "4" : 1000 } -vehicle.flush() -print " Current overrides are:", vehicle.channel_override -print " Channel default values:", vehicle.channel_readback # All channel values before override +#Callback function for "any" parameter +print("\nCreate (removable) observer for any parameter using wildcard string") +def any_parameter_callback(self, attr_name, value): + print(" ANY PARAMETER CALLBACK: %s changed to: %s" % (attr_name, value)) -# Cancel override by setting channels to 0 -print " Cancelling override" -vehicle.channel_override = { "1" : 0, "4" : 0 } -vehicle.flush() +#Add observer for the vehicle's any/all parameters parameter (defined using wildcard string ``'*'``) +vehicle.parameters.add_attribute_listener('*', any_parameter_callback) +print(" Change THR_MID and THR_MIN parameters (and wait for callback)") +vehicle.parameters['THR_MID']=400 +vehicle.parameters['THR_MIN']=30 ## Reset variables to sensible values. -print "\nReset vehicle attributes/parameters and exit" +print("\nReset vehicle attributes/parameters and exit") vehicle.mode = VehicleMode("STABILIZE") -vehicle.armed = False +#vehicle.armed = False vehicle.parameters['THR_MIN']=130 -vehicle.flush() +vehicle.parameters['THR_MID']=500 + + +#Close vehicle object before exiting script +print("\nClose vehicle object") +vehicle.close() + +# Shut down simulator if it was started. +if sitl is not None: + sitl.stop() + +print("Completed") + + + + diff --git a/requirements.txt b/requirements.txt index a8eaab4f4..9e70c79bb 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,10 +1,6 @@ -poster==0.8.1 -protobuf==2.6.1 -requests==2.5.1 -pymavlink>=1.1.62 -sphinx-3dr-theme>=0.0.6 +pymavlink>=2.2.20 +monotonic>=1.3 nose>=1.3.7 -psutil>=3.0.0 -mock>=1.3.0 -six>=1.9.0 -dronekit-sitl>=2.2.0 +mock>=2.0.0 +dronekit-sitl==3.2.0 +sphinx-3dr-theme>=0.4.3 diff --git a/scripts/compile-protobufs.sh b/scripts/compile-protobufs.sh deleted file mode 100755 index 588c00285..000000000 --- a/scripts/compile-protobufs.sh +++ /dev/null @@ -1,7 +0,0 @@ -#!/bin/bash - -cd $(dirname $0) -cd .. - -DESTDIR=dronekit/lib -protoc --python_out=$DESTDIR --proto_path=../dronekit-protobuf/src ../dronekit-protobuf/src/webapi.proto diff --git a/scripts/get_release_notes.py b/scripts/get_release_notes.py deleted file mode 100755 index acdf524f8..000000000 --- a/scripts/get_release_notes.py +++ /dev/null @@ -1,110 +0,0 @@ -#!/usr/bin/env python - -""" -This script gets the latest release notes from a specfied github repo, converts it from markdown to -reStructured Text format, and writes it to a text file. - -The text file can be auto-imported by Sphinx into the official release notes. - ----- - -The script uses the "Requests" module to make the API requests: http://docs.python-requests.org/en/latest/ - -Github API doc for getting latest release: https://developer.github.com/v3/repos/releases/#get-the-latest-release - GET /repos/:owner/:repo/releases/latest - -Where our release is: https://github.com/dronekit/dronekit-python/releases/tag/1.1.1 -""" - -import os -import requests # for making API requests -import re # regular expressions to convert markup to reStructuredText -from optparse import OptionParser # for command line options - - -def convert_markup_to_rst(text): - """ - Convert mardown to rst format. Currently converts only (up to level 5) headings and links to rst format. - """ - - def fixheadings(matchobj): - """ - Convert mardown headings to rst headings. - """ - #print 'matched' - #print matchobj.group(0) - #print matchobj.group(1) - #print matchobj.group(2) - #print 'endmatched' - heading_level=len(matchobj.group(1)) - heading_text=matchobj.group(2).strip() - heading_length= len(heading_text) - #print heading_level - #print heading_text - #print heading_length - - heading_char='=' - if heading_level == 1 : - heading_char = '=' - if heading_level == 2 : - heading_char = '-' - if heading_level == 3 : - heading_char = '~' - if heading_level == 4 : - heading_char = '+' - if heading_level == 5 : - heading_char = '*' - heading_underline=heading_char*heading_length - return heading_text+'\n'+heading_underline+'\n' - - - def fixlinks(matchobj): - """ - Convert mardown links to rst links. - """ - #print 'matched' - #print matchobj.group(0) - #print matchobj.group(1) - #print matchobj.group(2) - #print 'endmatched' - link_url=matchobj.group(2) - link_text=matchobj.group(1) - return '`%s <%s>`_' % (link_text,link_url) - - text=re.sub(r'^(\#+)\s+(.*)', fixheadings, text, flags=re.MULTILINE) - text=re.sub(r'\[(.*?)\]\((.*?)\)', fixlinks, text, flags=re.MULTILINE) - - return text - - -curpath = os.path.realpath(os.path.dirname(__file__)) - -#parser options -parser = OptionParser(version="%prog 0.0.1", usage="Usage: %prog [options] version") -parser.add_option("-o", "--gitowner", dest="owner", default="diydrones", help="Owner of github repository") -parser.add_option("-r", "--repo", dest="repo", default="dronekit-python", help="Repo name") -parser.add_option("-f" , "--file", dest="file", default=os.path.join(curpath, "../docs/about/github_latest_release.txt"), help="File path put output file") - -(options, args) = parser.parse_args() - -latest_release_url='https://api.github.com/repos/'+options.owner+'/'+options.repo+'/releases/latest' - -print 'Fetching release notes: %s' % latest_release_url - -r = requests.get(latest_release_url) -bodytext_markdown=r.json()[u'body'] -#print bodytext_markdown -print ' Converting markdown to reStructured text' -bodytext_rst = convert_markup_to_rst(bodytext_markdown) - -#Prefix with some documentation -bodytext_rst = ".. This document was auto-generated by the get_release_notes.py script using latest-release information from github \n\n" + bodytext_rst - -#print "RST" -#print bodytext_rst -release_notes_file=open(options.file,'w') -release_notes_file.write(bodytext_rst) -release_notes_file.close() - -print ' Complete' - diff --git a/scripts/update-docs.sh b/scripts/update-docs.sh index a651404d6..50459adad 100755 --- a/scripts/update-docs.sh +++ b/scripts/update-docs.sh @@ -1,22 +1,3 @@ #!/bin/bash -cd $(dirname $0) -cd .. - -rm -r /tmp/autodocs -set -e -python ./setup.py build -echo Switching to doc tree -cd docs -make html -cp -a _build/html /tmp/autodocs -cd .. -git checkout gh-pages -cp -a /tmp/autodocs/* . -set +e -find . | xargs git add -set -e -git commit -m "Update docs" -git push -git checkout -f master - +ssh dronekit.io "./update_python.sh" diff --git a/scripts/update-releasenotes.sh b/scripts/update-releasenotes.sh new file mode 100755 index 000000000..789aa9fc9 --- /dev/null +++ b/scripts/update-releasenotes.sh @@ -0,0 +1,37 @@ +#!/usr/bin/env python + +""" +This script gets the release notes, converts it from markdown to +reStructured Text format, and writes it to a text file. + +The text file can be auto-imported by Sphinx into the official release notes. +""" + +import os +from pypandoc import convert +from optparse import OptionParser +import re + +curpath = os.path.realpath(os.path.dirname(__file__)) + +#parser options +parser = OptionParser(version="%prog 1.0.0", usage="Usage: %prog [options] version") +parser.add_option("-i" , "--input", dest="input", default=os.path.join(curpath, "../CHANGELOG.md"), help="Input file") +parser.add_option("-f" , "--file", dest="file", default=os.path.join(curpath, "../docs/about/github_latest_release.txt"), help="Output file") + +(options, args) = parser.parse_args() + +rst_content = convert(options.input, 'rst') + +# Hyperlink Github issues +rst_content = re.sub(r'#\d+', lambda m: '`' + m.group() + ' `_', rst_content) + +# Prefix with some documentation +rst_content = ".. This document was auto-generated by the get_release_notes.py script using latest-release information from github \n\n" + rst_content + +release_notes_file = open(options.file,'w') +release_notes_file.write(rst_content) +release_notes_file.close() + +print 'complete.' + diff --git a/setup.py b/setup.py index 7e7742895..49d7477e2 100644 --- a/setup.py +++ b/setup.py @@ -1,27 +1,35 @@ -from setuptools import setup, Extension -import platform +import setuptools +import os -version = '2.0.0b2' +version = '2.9.2' -setup (name = 'dronekit', - zip_safe=True, - version = version, - description = 'Python language bindings for the DroneApi', - long_description = 'Python language bindings for the DroneApi', - url = 'https://github.com/dronekit/dronekit-python', - author = '3D Robotics', - install_requires = ['pymavlink >= 1.1.62', - 'protobuf >= 2.5.0', - 'requests == 2.5.1' ], - author_email = 'tim@3drobotics.com, kevinh@geeksville.com', - classifiers=['Development Status :: 4 - Beta', - 'Environment :: Console', - 'Intended Audience :: Science/Research', - 'License :: OSI Approved :: Apache Software License', - 'Operating System :: OS Independent', - 'Programming Language :: Python :: 2.7', - 'Topic :: Scientific/Engineering' - ], - license='apache', - packages = ['dronekit', 'dronekit.module', 'dronekit.lib' ], - ext_modules = []) +with open(os.path.join(os.path.dirname(__file__), 'README.md')) as f: + LongDescription = f.read() + +setuptools.setup( + name='dronekit', + zip_safe=True, + version=version, + description='Developer Tools for Drones.', + long_description_content_type="text/markdown", + long_description=LongDescription, + url='https://github.com/dronekit/dronekit-python', + author='3D Robotics', + install_requires=[ + 'pymavlink>=2.2.20', + 'monotonic>=1.3', + ], + author_email='tim@3drobotics.com, kevinh@geeksville.com', + classifiers=[ + 'Development Status :: 5 - Production/Stable', + 'Environment :: Console', + 'Intended Audience :: Science/Research', + 'License :: OSI Approved :: Apache Software License', + 'Operating System :: OS Independent', + 'Programming Language :: Python :: 2.7', + 'Programming Language :: Python :: 3', + 'Topic :: Scientific/Engineering', + ], + license='apache', + packages=setuptools.find_packages() +) diff --git a/tests/__init__.py b/tests/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/tests/sitl/__init__.py b/tests/sitl/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/tests/sitl/test_110.py b/tests/sitl/test_110.py deleted file mode 100644 index da8203b22..000000000 --- a/tests/sitl/test_110.py +++ /dev/null @@ -1,66 +0,0 @@ -from dronekit import connect -from dronekit.lib import VehicleMode -from dronekit.tools import with_sitl -from pymavlink import mavutil -import time -import sys -import os -from nose.tools import assert_equals - -@with_sitl -def test_110(connpath): - v = connect(connpath, await_params=True) - - # NOTE these are *very inappropriate settings* - # to make on a real vehicle. They are leveraged - # exclusively for simulation. Take heed!!! - v.parameters['ARMING_CHECK'] = 0 - v.parameters['FS_THR_ENABLE'] = 0 - v.parameters['FS_GCS_ENABLE'] = 0 - v.parameters['EKF_CHECK_THRESH'] = 0 - - # Change the vehicle into STABILIZE mode - v.mode = VehicleMode("STABILIZE") - - # NOTE wait crudely for ACK on mode update - time.sleep(3) - - # Define example callback for mode - def armed_callback(attribute): - armed_callback.called += 1 - armed_callback.called = 0 - - # When the same (event, callback) pair is passed to add_attribute_observer, - # only one instance of the observer callback should be added. - v.add_attribute_observer('armed', armed_callback) - v.add_attribute_observer('armed', armed_callback) - v.add_attribute_observer('armed', armed_callback) - v.add_attribute_observer('armed', armed_callback) - v.add_attribute_observer('armed', armed_callback) - - # Disarm and see update. - v.armed = False - v.flush() - # Wait for ACK. - time.sleep(3) - - # Ensure the callback was called. - assert armed_callback.called > 0, "Callback should have been called." - - # Rmove all observers. The first call should remove all listeners - # we've added; the second call should be ignored and not throw. - # NOTE: We test if armed_callback were treating adding each additional callback - # and remove_attribute_observer were removing them one at a time; in this - # case, there would be three callbacks still attached. - v.remove_attribute_observer('armed', armed_callback) - v.remove_attribute_observer('armed', armed_callback) - callcount = armed_callback.called - - # Re-arm and see update. - v.armed = True - v.flush() - # Wait for ack - time.sleep(3) - - # Ensure the callback was called zero times. - assert_equals(armed_callback.called, callcount, "Callback should not have been called once removed.") diff --git a/tests/unit/__init__.py b/tests/unit/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/tests/unit/test_api.py b/tests/unit/test_api.py deleted file mode 100644 index d83ea3f21..000000000 --- a/tests/unit/test_api.py +++ /dev/null @@ -1,9 +0,0 @@ -import mock -from mock import MagicMock -import dronekit -from dronekit import FakeAPI -from nose.tools import assert_equals - -def test_mode(): - api = FakeAPI(MagicMock()) - assert_equals(len(api.get_vehicles()), 1) diff --git a/tests/web/__init__.py b/tests/web/__init__.py deleted file mode 100644 index 8b1378917..000000000 --- a/tests/web/__init__.py +++ /dev/null @@ -1 +0,0 @@ - diff --git a/tests/web/cloud_client_test.py b/tests/web/cloud_client_test.py deleted file mode 100644 index f55891515..000000000 --- a/tests/web/cloud_client_test.py +++ /dev/null @@ -1,65 +0,0 @@ -import os -from nose.tools import assert_raises -from dronekit.lib.CloudClient import * - - -def setup_module(): - global api - api_key = os.environ['DRONEAPI_KEY'] - api = CloudClient(api_key) - - -def teardown_module(): - pass - - -def test_unhandled_endpoint(): - assert_raises(CloudError, api.bogus) - - -def test_mission_endpoint(): - api.mission() - - -def test_mission_static_map(): - api.mission_staticMap() - - -def test_mission_by_id_endpoint(): - api.mission_(1141) - - -def test_mission_by_id_analysis_endpoint(): - api.mission_analysis(1141) - - -def test_mission_by_id_geo_endpoint(): - api.mission_geo(1141) - - -def test_mission_by_id_messages_endpoint(): - api.mission_messages(1141) - - -def test_mission_by_id_parameters_endpoint(): - api.mission_parameters(1141) - - -def test_mission_by_id_dseries_endpoint(): - api.mission_dseries(1141) - - -def test_vehicle_endpoint(): - api.vehicle() - - -def test_vehicle_by_id_endpoint(): - api.vehicle_(218) - - -def test_user_endpoint(): - api.user() - - -def test_user_by_login_endpoint(): - api.user('mrpollo') diff --git a/windows/droneapiWinBuild.bat b/windows/droneapiWinBuild.bat deleted file mode 100644 index dba643871..000000000 --- a/windows/droneapiWinBuild.bat +++ /dev/null @@ -1,37 +0,0 @@ -rem build the standalone Dronekit.exe for Windows. -rem This assumes Python is installed in C:\Python27 -SETLOCAL enableextensions - -rem get the version -for /f "tokens=*" %%a in ( - 'python returnVersion.py' - ) do ( - set VERSION=%%a - ) - -rem -----Get protobuf and unzip----- -C:\python27\scripts\pip.exe install --download .\ protobuf -rem get the protobuf fullname -for /f "tokens=*" %%a in ( - 'dir /B protobuf-*' - ) do ( - set PROTOBUFFULL=%%a - ) - -rem Use 7zip to unzip files. Works for both 32 and 64 bit versions -"C:\Program Files\7-zip\7z.exe" x -y %PROTOBUFFULL% -"C:\Program Files\7-zip\7z.exe" x -y .\dist\protobuf* -"C:\Program Files (x86)\7-zip\7z.exe" x -y %PROTOBUFFULL% -"C:\Program Files (x86)\7-zip\7z.exe" x -y .\dist\protobuf* - -rem get the protobuf fullname -for /f "tokens=*" %%a in ( - 'dir /A:D /B protobuf-*' - ) do ( - set PROTOBUFDIR=%%a - ) -xcopy /E /Y %PROTOBUFDIR%\google .\google\ - - -rem -----Build the Installer----- -"C:\Program Files (x86)\Inno Setup 5\ISCC.exe" /dMyAppVersion=%VERSION% dronekit_installer.iss diff --git a/windows/dronekitWinBuild.bat b/windows/dronekitWinBuild.bat new file mode 100644 index 000000000..95c8a79bb --- /dev/null +++ b/windows/dronekitWinBuild.bat @@ -0,0 +1,14 @@ +rem build the standalone Dronekit.exe for Windows. +rem This assumes Python2 is installed in C:\Python27 +rem This assumes Inno Setup 5 is installed in C:\Program Files (x86)\Inno Setup 5 +SETLOCAL enableextensions + +rem get the version +for /f "tokens=*" %%a in ( + 'python returnVersion.py' + ) do ( + set VERSION=%%a + ) + +rem -----Build the Installer----- +"C:\Program Files (x86)\Inno Setup 5\ISCC.exe" /dMyAppVersion=%VERSION% dronekit_installer.iss diff --git a/windows/droneapi_installer.iss b/windows/dronekit_installer.iss similarity index 93% rename from windows/droneapi_installer.iss rename to windows/dronekit_installer.iss index b9e3f5e76..707baa6ef 100644 --- a/windows/droneapi_installer.iss +++ b/windows/dronekit_installer.iss @@ -32,7 +32,6 @@ Name: "english"; MessagesFile: "compiler:Default.isl" [Files] Source: "..\dronekit\*"; DestDir: "{code:GetMAVProxyPath}\dronekit"; Flags: ignoreversion recursesubdirs createallsubdirs Source: "..\examples\*"; DestDir: "{code:GetMAVProxyPath}\examples"; Flags: ignoreversion recursesubdirs createallsubdirs -Source: "google\*"; DestDir: "{code:GetMAVProxyPath}\google"; Flags: ignoreversion recursesubdirs createallsubdirs ; NOTE: Don't use "Flags: ignoreversion" on any shared system files diff --git a/windows/returnVersion.py b/windows/returnVersion.py index a15976f88..ec9ebbadc 100644 --- a/windows/returnVersion.py +++ b/windows/returnVersion.py @@ -1,12 +1,13 @@ -# This script reads the setup.py and returns the current version number -# Used as part of building the WIndows setup file (DronekitWinBuild.bat) -# It assumes there is a line like this: -# version = "12344" - -# glob supports Unix style pathname extensions -with open("../setup.py") as f: - searchlines = f.readlines() - for i, line in enumerate(searchlines): - if "version = " in line: - print line[11:len(line)-2] +from __future__ import print_function +# This script reads the setup.py and returns the current version number +# Used as part of building the WIndows setup file (DronekitWinBuild.bat) +# It assumes there is a line like this: +# version = "12344" + +# glob supports Unix style pathname extensions +with open("../setup.py") as f: + searchlines = f.readlines() + for i, line in enumerate(searchlines): + if "version = " in line: + print(line[11:len(line)-2]) break \ No newline at end of file