diff --git a/.flake8.cfg b/.flake8.cfg new file mode 100644 index 0000000..ead0c3d --- /dev/null +++ b/.flake8.cfg @@ -0,0 +1,10 @@ +[flake8] +max-line-length = 120 +max-complexity = 40 +ignore = + W504 +exclude = + .git + ./ev3dev2/auto.py + ./tests/fake-sys/ + ./venv diff --git a/.gitignore b/.gitignore index 27a178b..4920911 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,8 @@ __pycache__ dist *.egg-info RELEASE-VERSION -ev3dev/version.py +ev3dev2/version.py build +_build/ +docs/_build/ .idea diff --git a/.gitmodules b/.gitmodules index ba05cc2..5af5621 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,6 +1,3 @@ -[submodule "ev3dev-lang"] - path = ev3dev-lang - url = https://github.com/ev3dev/ev3dev-lang.git [submodule "tests/fake-sys"] path = tests/fake-sys - url = https://github.com/rhempel/ev3dev-lang-fake-sys.git + url = https://github.com/ddemidov/ev3dev-lang-fake-sys.git diff --git a/.readthedocs.yml b/.readthedocs.yml new file mode 100644 index 0000000..75c0d10 --- /dev/null +++ b/.readthedocs.yml @@ -0,0 +1,12 @@ +version: 2 + +# Build documentation in the docs/ directory with Sphinx +sphinx: + configuration: docs/conf.py + +formats: all + +python: + version: 3.7 + install: + - requirements: docs/requirements.txt \ No newline at end of file diff --git a/.travis.yml b/.travis.yml index 1be1447..87ed261 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,16 +1,33 @@ language: python +env: + - USE_MICROPYTHON=false + - USE_MICROPYTHON=true python: -- 3.4 +- 3.8 +cache: + pip: true + directories: + - ~/micropython + - ~/.micropython sudo: false +git: + depth: 100 install: -- pip install Pillow +- if [ $USE_MICROPYTHON = false ]; then pip install Pillow Sphinx==2.4.4 sphinx_bootstrap_theme recommonmark evdev==1.0.0 && pip install -r ./docs/requirements.txt; fi +- if [ $USE_MICROPYTHON = true ]; then ./.travis/install-micropython.sh && MICROPYTHON=~/micropython/ports/unix/micropython; fi script: -- ./tests/api_tests.py +- chmod -R g+rw ./tests/fake-sys/devices/**/* +- if [ $USE_MICROPYTHON = false ]; then python3 -W ignore::ResourceWarning tests/api_tests.py; fi +- if [ $USE_MICROPYTHON = true ]; then $MICROPYTHON tests/api_tests.py; fi +- if [ $USE_MICROPYTHON = false ]; then sphinx-build -nW -b html ./docs/ ./docs/_build/html; fi deploy: provider: pypi user: Denis.Demidov password: - secure: W13pXc2pp2A9gBUDz2QW/K3OfDT/cn/iapkW6NMdkwRQ+CL7MWXsY2qayfxs6QPfR5W7838pmMXogLOie70n8Hov5/XIrou+dEz/xh6If2VFG41KUz/tP9cy1yUhDYeADn5I+mr+8Yrh59OkHkaeZH1EFndbsxY4pyXV0DT+FNVg4eLc8sE5ZCfa/itVdsqp8M4xrMe8NO/ldnFEIHFymyEMTZHR5qoJD+Uk+AGMR4cSeSuOrZixnSLpQViZDFHpwej6F1LrreLltbT9ChjSEmdPAr1Jp1ReQfuD+vBzUMkhVfZyEo+fY1x8FZVPrTAEtbJGDhvTAsrV1KTgPqanMyyrCIG+OWfCSYYPyg7MYbAJVfB+P0BRp1cm2D5oFrpAZE129oVtATOykQarFzzRFhH4Tyc04WriyY/greEAe58MqYoJAZXUIe/JLf9+GLdjBKD07+q7QMZvyEEdsDCPYkqKQeuwrWZ/JlpWO5rmy12L23aYzvJqhcKo9LZQOY8LkmFmxuxt7k5eb/3iZ1trhj/lwoLLSu8l29B8cK3dax38URie0x9bMMhQRxaO59JQuhGuu0sNHiuFQHs6wLs/V8ff1IZIlRSlzztqIP3KW514TEdsFIuF0Gpn5wvagAXFbSnyxAUUTy81eQjY/ExTiUHKfU3zjluaAYGt9zjF0Bc= + secure: cTDN4WMJEjxZ0zwFwkLPpOIOSG/1JlHbQsHzd/tV9LfjBMR0nJR5HrmyiIO1TE5Wo6AFSFd7S3JmKdEnFO1ByvN/EFtsKGRUR9L6Yst4KfFi4nHwdZ7vgPTV0nNdvgC1YM/3bx8W8PcjFSm/k8awx7aicwXj09yDA8ENTf5XedaX22Z+9OKhb1mKola1cqEoc0GwaYzd8UX0Ruwh9/6RRbvTt7zn8BCZc9vIVqNd6mZgBWY9zAU40ZZsjYORbiZmDNCygEI+RViZ51M58WYkPPhoivzcG9em8DMRS7SC3CjWGapiOaXUHa3Fhnn+IQ+Q8Xv9YU5+qmj65MWQy3SSnMnvuxmrLf4aLoOlSJUMhDarjni4tdBOTX5PdOkdmhskyQt1DqDrw0+WhPItYfGe5zQfQwqW+YOpGbOipAeU+844arPo5jvZG/IOBX2qVUwdSxo8Y/98ocjqoZOq8b5xkWtJef0Kh1RCkp1bR2XELQVe76qeWqQxWz3OPqq+wK3xeNvj5kMQmytl3dCEB//D6UcES7Qr8YxD+LWoaIf32JIj/4LaCXXuxMVH+PJ68Oc72/ox0qLmXK0qhbea2QvaqXyGDrO+a2X6VbMdH32D2xHzH4Mg75xLnXnSaFvGovhYl1zEVcYUDioxrXZEuDmymGf9nH2mivJ24Fon6u+C3QQ= on: tags: true - repo: rhempel/ev3dev-lang-python + condition: + - $TRAVIS_TAG != ev3dev-stretch/* + - $USE_MICROPYTHON = false + repo: ev3dev/ev3dev-lang-python diff --git a/.travis/install-micropython.sh b/.travis/install-micropython.sh new file mode 100755 index 0000000..add6e2d --- /dev/null +++ b/.travis/install-micropython.sh @@ -0,0 +1,25 @@ +#!/bin/sh +set -ex + +if [ -e ~/micropython/ports/unix/micropython ]; then + # the micropython binary already exists, which means that the cached folder was restored + exit 0 +fi + +cd ~/ + +# Build micropython from source +# TODO: cache micropython build output +git clone --recurse-submodules https://github.com/micropython/micropython.git --depth 1 --branch v1.9.4 --quiet +cd ./micropython/ports/unix +make axtls +make + +# Install upip +~/micropython/tools/bootstrap_upip.sh + +# Install micropython library modules +~/micropython/ports/unix/micropython -m upip install micropython-unittest micropython-os micropython-os.path micropython-shutil micropython-io micropython-fnmatch micropython-numbers micropython-struct micropython-time micropython-logging micropython-select micropython-fcntl +# Make unittest module show error output; will run until failure then print first error +# See https://github.com/micropython/micropython-lib/blob/f20d89c6aad9443a696561ca2a01f7ef0c8fb302/unittest/unittest.py#L203 +sed -i 's/#raise/raise/g' ~/.micropython/lib/unittest.py diff --git a/.yapf.cfg b/.yapf.cfg new file mode 100644 index 0000000..c30b99c --- /dev/null +++ b/.yapf.cfg @@ -0,0 +1,3 @@ +[style] +based_on_style = pep8 +COLUMN_LIMIT = 120 diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst index e365a2a..bad507f 100644 --- a/CONTRIBUTING.rst +++ b/CONTRIBUTING.rst @@ -1,68 +1,140 @@ Contributing to the Python language bindings for ev3dev ======================================================= -This repository holds the pure Python bindings for peripheral -devices that use the drivers available in the ev3dev_ distribution. -for embedded systems. +This repository holds the Python bindings for ev3dev_, ev3dev-lang-python. -Contributions are welcome in the form of pull requests - but please -take a moment to read our suggestions for a happy maintainer (me) and -even happier users. - -The ``master`` branch ---------------------- - -This is where the latest tagged version lives - it changes whenever -we release a tag that gets released. - -The ``develop`` branch ----------------------- +Opening issues +-------------- -This is the branch that is undergoing active development intended -for the next tagged version that gets released to ``master``. +Please make sure you have read the FAQ_. If you are still encountering your +problem, open an issue, and ensure that the questions asked by the issue +template are completely answered with correct info. This will make it much +easier for us to help you! -Please make sure that your pull requests are against the -``develop`` branch. +Submitting Pull Requests +------------------------ -Before you issue a Pull Request -------------------------------- - -This is a hobby for me, I get no compensation for the work I do -on this repo or any other contributions to ev3dev_. That does not -make me special, it's the same situation that everyone involved -in the project is in. +Contributions are welcome in the form of pull requests - but please +take a moment to read our suggestions for happy maintainers and +even happier users. -Therefore, do not count on me to test your PR before I do the -merge - I will certainly review the code and if it looks OK I will -just merge automatically. +Sometimes, it isn't easy for us to pull your suggested change and run +rigorous testing on it. So please help us out by validating your changes +and mentioning what kinds of testing you did when you open your PR. +Please also consider adding relevant tests to ``api_tests.py`` and documentation +changes within the ``docs`` directory. -I would ask that you have at least tested your changes by running -a test script on your target of choice as the generic ``robot`` user -that is used by the ``Brickman`` UI for ev3dev_. +The ``ev3dev-stretch`` branch +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Please do not run as ``root`` or your own user that may have group -memberships or special privileges not enjoyed by ``robot``. +This is where the latest version of our library lives. It targets +``ev3dev-stretch``, which is the current stable version of ev3dev. +We publish releases from this branch. If your change breaks or changes an API ---------------------------------------- +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Breaking changes are discouraged, but sometimes they are necessary. A more common change is to add a new function or property to a class. +If you add a new parameter to an existing function, give it a default value +so as not to break existing code that calls the function. Either way, if it's more than a bug fix, please add enough text to the -comments in the pull request message so that I can paste it into the -draft release notes that I keep running for the next release out of -master. +comments in the pull request message so that we know what was updated +and can easily discuss the breaking change and add it to the release +notes. If your change addresses an Issue ---------------------------------- +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Bug fixes are always welcome, especially if they are against known +Bug fixes are always welcome, especially if they are against known issues! -When you send a pull request that addresses an issue, please add a -note something like ``Fixes #24`` in the PR so that we get linkage -back to the specific issue. +When you send a pull request that addresses an issue, please add a +note of the format ``Fixes #24`` in the PR so that the PR links back +to its relevant issue and will automatically close the issue when the +PR is merged. + +Building and testing changes on the EV3 +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +In an SSH terminal window with an EV3 with Internet access, +run the following commands: +(recall that the default ``sudo`` password is ``maker``) + +```shell +git clone https://github.com/ev3dev/ev3dev-lang-python.git +cd ev3dev-lang-python +sudo make install +``` + +To update the module, use the following commands: + +.. code-block:: bash + + cd ev3dev-lang-python + git pull + sudo make install + + +If you are developing micropython support, you can take a shortcut +and use the following command to build and deploy the micropython +files only: -.. _ev3dev: http://ev3dev.org +.. code-block:: bash + + cd ev3dev-lang-python + sudo make micropython-install + +To re-install the latest release, use the following command: + + +.. code-block:: bash + + sudo apt-get --reinstall install python3-ev3dev2 + +Or, to update your current ev3dev2 to the latest release, use the +following commands: + + +.. code-block:: bash + + sudo apt update + sudo apt install --only-upgrade micropython-ev3dev2 + +Publishing releases +------------------- + +#. Update the changelog, including a correct release date. Use ``date -R`` to get correctly-formatted date. +#. Commit the changelog. By convention, use message like ``Update changelog for 2.1.0 release``. +#. Build/update pbuilder base images: ``OS=debian DIST=stretch ARCH=amd64 pbuilder-ev3dev base`` and ``OS=raspbian DIST=stretch ARCH=armhf pbuilder-ev3dev base``. +#. ``./debian/release.sh`` and enter passwords/key passphrases as needed +#. ``git push`` +#. ``git push --tags`` +#. ``git tag -d stable`` +#. ``git tag stable`` +#. ``git push --tags --force`` +#. ``git tag -a 2.1.0 -m "python-ev3dev2 PyPi release 2.1.0"`` +#. ``git push --tags`` + +Note that push order is important; the CI server will get confused if you push +other tags pointing to the same commit after you push the PyPi release tag. This +doesn't actually cause release issues, but does mark the CI builds as "failed" +because it tried to publish the same release again. + +**Check all of the following after release is complete:** + +- Emails from package server don't include any errors +- All Travis CI builds succeeded +- New release is available on PyPi +- Release tags are up on GitHub +- ReadTheDocs is updated + + - ReadTheDocs "stable" version points to latest release + - There is an explicit version tag for the last-released version (exeption: ``2.1.0``) + - There is an explicit version tag for this version (you will likely need to manually activate it) + - All ReadTheDocs builds succeeded + +.. _ev3dev: http://ev3dev.org +.. _FAQ: https://python-ev3dev.readthedocs.io/en/ev3dev-stretch/faq.html diff --git a/ISSUE_TEMPLATE.md b/ISSUE_TEMPLATE.md index 9c3ebdf..5b4ca0e 100644 --- a/ISSUE_TEMPLATE.md +++ b/ISSUE_TEMPLATE.md @@ -1,10 +1,10 @@ - **ev3dev version:** PASTE THE OUTPUT OF `uname -r` HERE -- **ev3dev-lang-python version:** +- **ev3dev-lang-python version:** INSERT ALL VERSIONS GIVEN BY `dpkg-query -l {python3,micropython}-ev3dev*` HERE - + diff --git a/MANIFEST.in b/MANIFEST.in index 072c60e..8368fdc 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,3 +1,2 @@ include git_version.py -include spec_version.py include RELEASE-VERSION diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..f16352c --- /dev/null +++ b/Makefile @@ -0,0 +1,26 @@ +# Makefile to assist developers while modifying and testing changes before release +# Note: to re-install a release of EV3DEV2, use `sudo apt-get --reinstall install python3-ev3dev2` +OUT := build +MPYCROSS := /usr/bin/mpy-cross +MPYC_FLAGS := -v -v -mcache-lookup-bc +PYS := $(shell find ./ev3dev2 -type f \( -iname "*.py" ! -iname "setup.py" \)) +MPYS := $(PYS:./%.py=${OUT}/%.mpy) +vpath %.py . + +${OUT}/%.mpy: %.py + @mkdir -p $(dir $@) + ${MPYCROSS} ${MPYC_FLAGS} -o $@ $< + +install: + python3 setup.py install + +micropython-install: ${MPYS} + cp -R $(OUT)/* /usr/lib/micropython/ + +clean: + find . -name __pycache__ | xargs rm -rf + find . -name *.pyc | xargs rm -rf + +format: + yapf --style .yapf.cfg --in-place --exclude tests/fake-sys/ --recursive . + python3 -m flake8 --config=.flake8.cfg . diff --git a/README.rst b/README.rst index f5735ad..a1c414a 100644 --- a/README.rst +++ b/README.rst @@ -1,222 +1,214 @@ Python language bindings for ev3dev =================================== -.. image:: https://travis-ci.org/rhempel/ev3dev-lang-python.svg?branch=master - :target: https://travis-ci.org/rhempel/ev3dev-lang-python -.. image:: https://readthedocs.org/projects/python-ev3dev/badge/?version=stable - :target: http://python-ev3dev.readthedocs.org/en/stable/?badge=stable +.. image:: https://travis-ci.org/ev3dev/ev3dev-lang-python.svg?branch=ev3dev-stretch + :target: https://travis-ci.org/ev3dev/ev3dev-lang-python +.. image:: https://readthedocs.org/projects/python-ev3dev/badge/?version=ev3dev-stretch + :target: http://python-ev3dev.readthedocs.org/en/ev3dev-stretch/?badge=ev3dev-stretch :alt: Documentation Status +.. image:: https://badges.gitter.im/ev3dev/chat.svg + :target: https://gitter.im/ev3dev/chat + :alt: Chat at https://gitter.im/ev3dev/chat A Python3 library implementing an interface for ev3dev_ devices, letting you control motors, sensors, hardware buttons, LCD displays and more from Python code. -If you haven't written code in Python before, you'll need to learn the language -before you can use this library. +If you haven't written code in Python before, you can certainly use this +library to help you learn the language! Getting Started --------------- This library runs on ev3dev_. Before continuing, make sure that you have set up -your EV3 or other ev3dev device as explained in the `ev3dev Getting Started guide`_. -Make sure that you have a kernel version that includes ``-10-ev3dev`` or higher (a -larger number). You can check the kernel version by selecting "About" in Brickman -and scrolling down to the "kernel version". If you don't have a compatible version, -`upgrade the kernel before continuing`_. Also note that if the ev3dev image you downloaded -was created before September 2016, you probably don't have the most recent version of this -library installed: see `Upgrading this Library`_ to upgrade it. +your EV3 or other ev3dev device as explained in the +`ev3dev Getting Started guide`_. Make sure you have an ev3dev-stretch version +greater than ``2.2.0``. You can check the kernel version by selecting +"About" in Brickman and scrolling down to the "kernel version". +If you don't have a compatible version, +`upgrade the kernel before continuing`_. -Once you have booted ev3dev and `connected to your EV3 (or Raspberry Pi / BeagleBone) -via SSH`_, you should be ready to start using ev3dev with Python: this library -is included out-of-the-box. If you want to go through some basic usage examples, -check out the `Usage Examples`_ section to try out motors, sensors and LEDs. -Then look at `Writing Python Programs for Ev3dev`_ to see how you can save -your Python code to a file. +Usage +----- -Make sure that you look at the `User Resources`_ section as well for links -to documentation and larger examples. +To start out, you'll need a way to work with Python. We recommend the +`ev3dev Visual Studio Code extension`_. If you're interested in using that, +check out our `Python + VSCode introduction tutorial`_ and then come back +once you have that set up. -Usage Examples --------------- +Otherwise, you can can work with files `via an SSH connection`_ with an editor +such as `nano`_, use the Python interactive REPL (type ``python3``), or roll +your own solution. If you don't know how to do that, you are probably +better off choosing the recommended option above. -To run these minimal examples, run the Python3 interpreter from -the terminal using the ``python3`` command: +The template for a Python script +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -.. code-block:: bash +Every Python program should have a few basic parts. Use this template +to get started: - $ python3 - Python 3.4.2 (default, Oct 8 2014, 14:47:30) - [GCC 4.9.1] on linux - Type "help", "copyright", "credits" or "license" for more information. - >>> +.. code-block:: python -The ``>>>`` characters are the default prompt for Python. In the examples -below, we have removed these characters so it's easier to cut and -paste the code into your session. + #!/usr/bin/env python3 + + from time import sleep + + from ev3dev2.motor import LargeMotor, OUTPUT_A, OUTPUT_B, SpeedPercent, MoveTank + from ev3dev2.sensor import INPUT_1 + from ev3dev2.sensor.lego import TouchSensor + from ev3dev2.led import Leds -Required: Import the library -~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + # TODO: Add code here -If you are using an EV3 brick (which is the case for most users), add the -following to the top of your file: +The first line should be included in every Python program you write +for ev3dev. It allows you to run this program from Brickman, the graphical +menu that you see on the device screen. The other lines are import statements +which give you access to the library functionality. You will need to add +additional classes to the import list if you want to use other types of devices +or additional utilities. -.. code-block:: python +You should use the ``.py`` extension for your file, e.g. ``my-file.py``. - import ev3dev.ev3 as ev3 +If you encounter an error such as +``/usr/bin/env: 'python3\r': No such file or directory``, +you must switch your editor's "line endings" setting for the file from +"CRLF" to just "LF". This is usually in the status bar at the bottom. +For help, see `our FAQ page`_. -If you are using a BrickPi, use this line: +Important: Make your script executable (non-Visual Studio Code only) +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -.. code-block:: python +To be able to run your Python file, **your program must be executable**. If +you are using the `ev3dev Visual Studio Code extension`_, you can skip this +step, as it will be automatically performed when you download your code to the +brick. - import ev3dev.brickpi as ev3 +**To mark a program as executable from the command line (often an SSH session), +run** ``chmod +x my-file.py``. + +You can now run ``my-file.py`` via the Brickman File Browser or you can run it +from the command line by preceding the file name with ``./``: ``./my-file.py`` Controlling the LEDs with a touch sensor ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -This code will turn the left LED red whenever the touch sensor is pressed, and -back to green when it's released. Plug a touch sensor into any sensor port and -then paste in this code - you'll need to hit ``Enter`` after pasting to complete -the loop and start the program. Hit ``Ctrl-C`` to exit the loop. +This code will turn the LEDs red whenever the touch sensor is pressed, and +back to green when it's released. Plug a touch sensor into any sensor port +before trying this out. .. code-block:: python - ts = ev3.TouchSensor() - while True: - ev3.Leds.set_color(ev3.Leds.LEFT, (ev3.Leds.GREEN, ev3.Leds.RED)[ts.value()]) - -Running a motor -~~~~~~~~~~~~~~~ + ts = TouchSensor() + leds = Leds() -Now plug a motor into the ``A`` port and paste this code into the Python prompt. -This little program will run the motor at 500 ticks per second, which on the EV3 -"large" motors equates to around 1.4 rotations per second, for three seconds -(3000 milliseconds). + print("Press the touch sensor to change the LED color!") -.. code-block:: python + while True: + if ts.is_pressed: + leds.set_color("LEFT", "GREEN") + leds.set_color("RIGHT", "GREEN") + else: + leds.set_color("LEFT", "RED") + leds.set_color("RIGHT", "RED") + # don't let this loop use 100% CPU + sleep(0.01) - m = ev3.LargeMotor('outA') - m.run_timed(time_sp=3000, speed_sp=500) +If you'd like to use a sensor on a specific port, specify the port like this: -The units for ``speed_sp`` that you see above are in "tacho ticks" per second. -On the large EV3 motor, these equate to one tick per degree, so this is 500 -degress per second. +.. code-block:: python + ts = TouchSensor(INPUT_1) +*Heads-up:* If you are using a BrickPi instead of an EV3, you will need to manually configure the sensor. See the example here: https://github.com/ev3dev/ev3dev-lang-python-demo/blob/stretch/platform/brickpi3-motor-and-sensor.py -Using text-to-speech -~~~~~~~~~~~~~~~~~~~~ +Running a single motor +~~~~~~~~~~~~~~~~~~~~~~ -If you want to make your robot speak, you can use the `Sound.speak` method: +This will run a LEGO Large Motor at 75% of maximum speed for 5 rotations. .. code-block:: python - ev3.Sound.speak('Welcome to the E V 3 dev project!').wait() + m = LargeMotor(OUTPUT_A) + m.on_for_rotations(SpeedPercent(75), 5) -**To quit the Python REPL, just type** ``exit()`` **or press** ``Ctrl-D`` **.** +You can also run a motor for a number of degrees, an amount of time, or simply +start it and let it run until you tell it to stop. Additionally, other units +are also available. See the following pages for more information: -Make sure to check out the `User Resources`_ section for more detailed -information on these features and many others. +- http://python-ev3dev.readthedocs.io/en/ev3dev-stretch/motors.html#ev3dev.motor.Motor.on_for_degrees +- http://python-ev3dev.readthedocs.io/en/ev3dev-stretch/motors.html#units -Writing Python Programs for Ev3dev ----------------------------------- +Driving with two motors +~~~~~~~~~~~~~~~~~~~~~~~ -Every Python program should have a few basic parts. Use this template -to get started: +The simplest drive control style is with the `MoveTank` class: .. code-block:: python - #!/usr/bin/env python3 - from ev3dev.ev3 import * - - # TODO: Add code here - -The first two lines should be included in every Python program you write -for ev3dev. The first allows you to run this program from Brickman, while the -second imports this library. + tank_drive = MoveTank(OUTPUT_A, OUTPUT_B) -When saving Python files, it is best to use the ``.py`` extension, e.g. ``my-file.py``. -To be able to run your Python code, **your program must be executable**. To mark a -program as executable run ``chmod +x my-file.py``. You can then run ``my-file.py`` -via the Brickman File Browser or you can run it from the command line via ``$ ./my-file.py`` + # drive in a turn for 5 rotations of the outer motor + # the first two parameters can be unit classes or percentages. + tank_drive.on_for_rotations(SpeedPercent(50), SpeedPercent(75), 10) -User Resources --------------- + # drive in a different turn for 3 seconds + tank_drive.on_for_seconds(SpeedPercent(60), SpeedPercent(30), 3) -Library Documentation - **Class documentation for this library can be found on** `our Read the Docs page`_ **.** - You can always go there to get information on how you can use this - library's functionality. +There are also `MoveSteering` and `MoveJoystick` classes which provide +different styles of control. See the following pages for more information: -ev3python.com - One of our community members, @ndward, has put together a great website - with detailed guides on using this library which are targeted at beginners. - If you are just getting started with programming, we highly recommend - that you check it out at `ev3python.com`_! +- http://python-ev3dev.readthedocs.io/en/ev3dev-stretch/motors.html#multiple-motor-groups +- http://python-ev3dev.readthedocs.io/en/ev3dev-stretch/motors.html#units -Frequently-Asked Questions - Experiencing an odd error or unsure of how to do something that seems - simple? Check our our `FAQ`_ to see if there's an existing answer. +Using text-to-speech +~~~~~~~~~~~~~~~~~~~~ -ev3dev.org - `ev3dev.org`_ is a great resource for finding guides and tutorials on - using ev3dev, straight from the maintainers. +If you want to make your robot speak, you can use the ``Sound.speak`` method: -Support - If you are having trouble using this library, please open an issue - at `our Issues tracker`_ so that we can help you. When opening an - issue, make sure to include as much information as possible about - what you are trying to do and what you have tried. The issue template - is in place to guide you through this process. +.. code-block:: python -Demo Robot - Laurens Valk of robot-square_ has been kind enough to allow us to - reference his excellent `EXPLOR3R`_ robot. Consider building the - `EXPLOR3R`_ and running the demo programs referenced below to get - familiar with what Python programs using this binding look like. + from ev3dev2.sound import Sound -Demo Code - There are `demo programs`_ that you can run to get acquainted with - this language binding. The programs are designed to work with the - `EXPLOR3R`_ robot. + sound = Sound() + sound.speak('Welcome to the E V 3 dev project!') -Upgrading this Library ----------------------- +More Demo Code +~~~~~~~~~~~~~~ -You can upgrade this library from the command line as follows. Make sure -to type the password (the default is ``maker``) when prompted. +There are several demo programs that you can run to get acquainted with +this language binding. The programs are available +`at this GitHub site `_. -.. code-block:: bash +You can also copy and run the programs in the `utils` directory to +understand some of the code constructs to use the EV3 motors, sensors, +LCD console, buttons, sound, and LEDs. - sudo apt-get update - sudo apt-get install --only-upgrade python3-ev3dev +We also highly recommend `ev3python.com`_ where one of our community +members, @ndward, has put together a great website with detailed guides +on using this library which are targeted at beginners. If you are just +getting started with programming, we highly recommend that you check +it out at `ev3python.com`_! +Using Micropython +----------------- -Developer Resources -------------------- +Normal Python too slow? Review `Micropython`_ to see if it supports the +features your project needs. -Python Package Index - The Python language has a `package repository`_ where you can find - libraries that others have written, including the `latest version of - this package`_. +Library Documentation +--------------------- -The ev3dev Binding Specification - Like all of the language bindings for ev3dev_ supported hardware, the - Python binding follows the minimal API that must be provided per - `this document`_. +Class documentation for this library can be found on +`our Read the Docs page`_. You can always go there to get +information on how you can use this library's functionality. -The ev3dev-lang Project on GitHub - The `source repository for the generic API`_ and the scripts to automatically - generate the binding. Only developers of the ev3dev-lang-python_ binding - would normally need to access this information. -Python 2.x and Python 3.x Compatibility ---------------------------------------- +Frequently-Asked Questions +-------------------------- -Some versions of the ev3dev_ distribution come with both `Python 2.x`_ and `Python 3.x`_ installed -but this library is compatible only with Python 3. +Experiencing an odd error or unsure of how to do something that seems +simple? Check our our `FAQ`_ to see if there's an existing answer. -As of the 2016-10-17 ev3dev image, the version of this library which is included runs on -Python 3 and this is the only version that will be supported from here forward. .. _ev3dev: http://ev3dev.org .. _ev3dev.org: ev3dev_ @@ -225,21 +217,15 @@ Python 3 and this is the only version that will be supported from here forward. .. _ev3dev-getting-started: http://www.ev3dev.org/docs/getting-started/ .. _upgrade the kernel before continuing: http://www.ev3dev.org/docs/tutorials/upgrading-ev3dev/ .. _detailed instructions for USB connections: ev3dev-usb-internet_ -.. _connected to your EV3 (or Raspberry Pi / BeagleBone) via SSH: http://www.ev3dev.org/docs/tutorials/connecting-to-ev3dev-with-ssh/ +.. _via an SSH connection: http://www.ev3dev.org/docs/tutorials/connecting-to-ev3dev-with-ssh/ .. _ev3dev-usb-internet: http://www.ev3dev.org/docs/tutorials/connecting-to-the-internet-via-usb/ -.. _our Read the Docs page: http://python-ev3dev.readthedocs.org/en/stable/ -.. _source repository for the generic API: ev3dev-lang_ +.. _our Read the Docs page: http://python-ev3dev.readthedocs.org/en/ev3dev-stretch/ .. _ev3python.com: http://ev3python.com/ -.. _FAQ: http://python-ev3dev.readthedocs.io/en/stable/faq.html -.. _ev3dev-lang: https://github.com/ev3dev/ev3dev-lang -.. _ev3dev-lang-python: https://github.com/rhempel/ev3dev-lang-python -.. _our Issues tracker: https://github.com/rhempel/ev3dev-lang-python/issues -.. _this document: wrapper-specification_ -.. _wrapper-specification: https://github.com/ev3dev/ev3dev-lang/blob/develop/wrapper-specification.md +.. _FAQ: http://python-ev3dev.readthedocs.io/en/ev3dev-stretch/faq.html +.. _our FAQ page: FAQ_ +.. _our Issues tracker: https://github.com/ev3dev/ev3dev-lang-python/issues .. _EXPLOR3R: demo-robot_ .. _demo-robot: http://robotsquare.com/2015/10/06/explor3r-building-instructions/ -.. _demo programs: demo-code_ -.. _demo-code: https://github.com/rhempel/ev3dev-lang-python/tree/master/demo .. _robot-square: http://robotsquare.com/ .. _Python 2.x: python2_ .. _python2: https://docs.python.org/2/ @@ -248,4 +234,8 @@ Python 3 and this is the only version that will be supported from here forward. .. _package repository: pypi_ .. _pypi: https://pypi.python.org/pypi .. _latest version of this package: pypi-python-ev3dev_ -.. _pypi-python-ev3dev: https://pypi.python.org/pypi/python-ev3dev +.. _pypi-python-ev3dev: https://pypi.python.org/pypi/python-ev3dev2 +.. _ev3dev Visual Studio Code extension: https://github.com/ev3dev/vscode-ev3dev-browser +.. _Python + VSCode introduction tutorial: https://github.com/ev3dev/vscode-hello-python +.. _nano: http://www.ev3dev.org/docs/tutorials/nano-cheat-sheet/ +.. _Micropython: http://python-ev3dev.readthedocs.io/en/ev3dev-stretch/micropython.html diff --git a/autogen-config.json b/autogen-config.json deleted file mode 100644 index a6e14a1..0000000 --- a/autogen-config.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "files": [ - "ev3dev/core.py", - "ev3dev/ev3.py", - "ev3dev/brickpi.py", - "spec_version.py", - "docs/sensors.rst" - ], - "templateDir": "templates/" -} diff --git a/debian/changelog b/debian/changelog index 6f80429..a88a243 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,121 +1,118 @@ -python-ev3dev (1.0.0) stable; urgency=medium - - [Denis Demidov] - * Add constants for color values to color sensor class - * Update documentation on motors - * Add getters to check state of motors - * Implement BeaconSeeker class - * Fix scaling of floating-point values from sensors - - [Eric Pascual] - * Add Sound.play_song method - - [Stefan Sauer] - * Make sound commands cancelable - - [Maximilian Nöthe] - * Add tuples for sensor modes +python-ev3dev2 (2.1.0) stretch; urgency=medium [Daniel Walton] - * Add wait_until_not_moving for motors - - -- Denis Demidov Tue, 05 Sep 2017 16:50:20 +0300 - -python-ev3dev (0.8.1) stable; urgency=medium + * RPyC update docs and make it easier to use + * use double backticks consistently in docstrings + * format code via yapf + * MoveDifferential use gyro for better accuracy. Make MoveTank and + MoveDifferential "turn" APIs more consistent. + * MoveTank turn_degrees convert speed to SpeedValue object + * correct flake8 errors + + [Matěj Volf] + * LED animation fix duration None + * Add rest notes to sound.Sound.play_song() [Kaelin Laundry] - * Documentation updates - - [Thomas Watson] - * Use speed instead of duty cycle in EXPLOR3R/auto-drive.py - - [Denis Demidov] - * Provide Sound.set_volume(pct) - * Implement Sound.get_volume() - * Documentation updates + * Add "value_secs" and "is_elapsed_*" methods to StopWatch + * Rename "value_hms" to "hms_str". New "value_hms" returns original tuple. + * Fix bug in Button.process() on Micropython - [Daniel Walton] - * Documentation updates - * Update utils to python3 - * Clean up MINDCUB3R demo - - [Pepijn de Vos] - * Check for non-existing device attributes + [Nithin Shenoy] + * Add Gyro-based driving support to MoveTank - [Stefan Sauer] - * Fix inverted button logic for the latest kernel + -- Kaelin Laundry Sun, 22 Mar 2020 19:14:35 -0700 - [@craigsammisutherland] - * Added a function to list all the connected sensors +python-ev3dev2 (2.0.0) stretch; urgency=medium - -- Denis Demidov Mon, 06 Feb 2017 09:27:21 +0300 + [David Lechner] + * Fix gyro sensor reset -python-ev3dev (0.8.0) stable; urgency=medium + -- Kaelin Laundry Sun, 24 Nov 2019 20:41:18 -0800 - [Denis Demidov] - * Add option to not set mode before reading sensor value - * Return a tuple for multiple values from a sensor property - * Add ColorSensor.raw() method to get all color channels at once - * Merge NXTMotor functionality into LargeMotor class and remove NXTMotor - * Distribute fonts from xfonts-75dpi in PIL format and expose them in - code - * Replace special sensor methods with properties for the sake of consistency - (breaking change!) - * Provide waiting functions for motors. - * Make implementation of Led timer-based trigger more robust. +python-ev3dev2 (2.0.0~beta5) stretch; urgency=medium - -- Denis Demidov Thu, 03 Nov 2016 19:18:49 +0300 + [Brady Merkel] + * Add console and stopwatch to Debian rules file so they are + included in MicroPython package -python-ev3dev (0.7.0) stable; urgency=medium + -- Kaelin Laundry Sun, 18 Aug 2019 11:37:17 -0700 - [Denis Demidov] - * Support "-13-ev3dev" and newer kernels. - * Rename FirgelliL1250Motor and FirgelliL12100Motor to ActuonixL1250Motor - and ActuonixL12100Motor. - * Allow passing espeak options to Sound.speak - * Make sure that device classes connect to devices of the right - type (driver name) - * Fix fatal error due to calling an undefined function in - some property setters, such as LED triggers +python-ev3dev2 (2.0.0~beta4) stretch; urgency=medium [Daniel Walton] - * Add "__version__" property. - * Add ev3dev/helper.py with classes for Tanks, Motors, Web UI, etc. - * Add ev3dev/GyroBalancer.py for robots that use a gyroscope. + * PID line follower + * micropython Button support + * micropython Sound support + * micropython support for LED animations + * StopWatch class + * Avoid race condition due to poll(None) + * MoveDifferential odometry support, tracks robot's (x,y) position + * Display support via raspberry pi HDMI + * Update tests/README to include sphinx-build instructions + * LED animation support, brickpi3 LED corrected from BLUE to AMBER + * Sound: fallback to regular case of 'note' if uppercase is not found + * wait_until_not_moving should consider "running holding" as "moving" + + [David Lechner] + * Fix and document ev3dev2.sensors.lego.GyroSensor.reset() + + [Brady Merkel] + * Added the Console() class for positional text display and font support + on the EV3 LCD console + * Added a utility program to demonstrate the Console() functionality and + show the various system fonts + * Added documentation for the Console() class + * Updated the micropython documentation to reflect the Console() class + * Updated the CONTRIBUTING documentation with guidance using the makefile + to build and install the module while developing enancements + + -- Kaelin Laundry Sat, 17 Aug 2019 23:05:00 -0700 + +python-ev3dev2 (2.0.0~beta3) stable; urgency=medium - [Donald Webster] - * Fix port names on BrickPi. - - [Frank Busse] - * Fix bad division logic in display classes. + [Daniel Walton] + * brickpi(3) support use of the Motor class + * Add port name constants for stacked brickpi3s + * Moved MoveTank.off() to MotorSet + * cache attributes that never change + * Display: correct xy variable names passed to Display.rectangle() + * GyroSensor wait_until_angle_changed_by added direction_sensitive option + * Added MoveDifferential + * Sound: removed play() in favor of play_file() + * suppress unittest ResourceWarning messages + * Added DistanceValue classes + * wait_until_not_moving should consider "running holding" as "not moving" + * brickpi(3) raise exception if LargeMotor not used for a motor + * MoveJoyStick should use SpeedPercent + * renamed GyroSensor rate_and_angle to angle_and_rate [Kaelin Laundry] - * Fix fatal error when a requested sysfs device class hasn't been - populated yet. - - -- Denis Demidov Fri, 30 Sep 2016 21:29:28 +0300 - -python-ev3dev (0.7.0~rc1) stable; urgency=medium - - * Drop python 2.x support. - * Performance improvements for reading/writing sysfs attributes. - * Updates for breaking ev3dev kernel changes. + * Added new binary package for Micropython - -- David Lechner Mon, 25 Jul 2016 21:13:43 -0500 + [Viktor Garske] + * Fixed error when using Motor.is_stalled -python-ev3dev (0.6.0) stable; urgency=medium + -- Kaelin Laundry Sat, 2 Feb 2019 1:58:00 -0800 - [Ralph Hempel] - * Change port_name to address. +python-ev3dev2 (2.0.0~beta2) stable; urgency=medium - [Denis Demidov] - * Restore python3 compatibility - * Implement device enumeration functions + * Mitigate performance regression when using Display.update() on the EV3 + * Fix erroneous values in InfraredSensor and other sensor classes caused by + rapid mode reset when using new properties + * Improve performance of reading sensor values when using new properties + * Rename SpeedInteger to SpeedValue + * Support floating-point values in SpeedValue + * Rename "speed_pct" parameters to "speed" + * Fix error when calling Sound.play_tone + * Support negative and zero speed/distance combinations in on_for_* methods + for both single motors and motor pairs + * Experimental support for motors and sensors on Micropython - -- David Lechner Tue, 29 Dec 2015 23:05:39 -0600 + -- Kaelin Laundry Sun, 23 Sep 2018 16:03:00 -0700 -python-ev3dev (0.5.0) stable; urgency=low +python-ev3dev2 (2.0.0~beta1) stable; urgency=medium - * Initial Release. + Initial beta release for ev3dev-stretch. - -- David Lechner Tue, 10 Nov 2015 21:30:53 -0600 + -- Kaelin Laundry Tue, 31 Jul 2018 20:37:00 -0700 diff --git a/debian/control b/debian/control index e80bae1..c189a01 100644 --- a/debian/control +++ b/debian/control @@ -1,13 +1,13 @@ -Source: python-ev3dev -Maintainer: Ralph Hempel +Source: python-ev3dev2 +Maintainer: ev3dev Python team Section: python Priority: optional -Standards-Version: 3.9.5 -Build-Depends: python3-setuptools (>= 0.6b3), python3-all (>= 3.4), debhelper (>= 9), dh-python -VCS-Git: git://github.com/rhempel/ev3dev-lang-python.git -VCS-Browser: https://github.com/rhempel/ev3dev-lang-python +Standards-Version: 3.9.8 +Build-Depends: python3-setuptools (>= 0.6b3), python3-all (>= 3.4), debhelper (>= 9), dh-python, python3-pillow, mpy-cross +VCS-Git: https://github.com/ev3dev/ev3dev-lang-python.git +VCS-Browser: https://github.com/ev3dev/ev3dev-lang-python -Package: python3-ev3dev +Package: python3-ev3dev2 Architecture: all Depends: ${misc:Depends}, ${python3:Depends} Description: Python language bindings for ev3dev @@ -15,3 +15,12 @@ Description: Python language bindings for ev3dev devices on hardware that is supported by ev3dev.org - a minimal Debian distribution optimized for the LEGO MINDSTORMS EV3. + +Package: micropython-ev3dev2 +Architecture: all +Depends: ${misc:Depends}, micropython, micropython-lib +Description: Python language bindings for ev3dev for MicroPython + This package is a pure Python binding to the peripheral + devices on hardware that is supported by ev3dev.org - a + minimal Debian distribution optimized for the LEGO + MINDSTORMS EV3. This package is designed to run on MicroPython. diff --git a/debian/gbp.conf b/debian/gbp.conf new file mode 100644 index 0000000..e723f5a --- /dev/null +++ b/debian/gbp.conf @@ -0,0 +1,3 @@ +[DEFAULT] +debian-branch=ev3dev-stretch +debian-tag=ev3dev-stretch/%(version)s diff --git a/debian/release.sh b/debian/release.sh new file mode 100755 index 0000000..f225106 --- /dev/null +++ b/debian/release.sh @@ -0,0 +1,21 @@ +#!/bin/bash +# +# Maintainer script for publishing releases. + +set -e + +source=$(dpkg-parsechangelog -S Source) +version=$(dpkg-parsechangelog -S Version) +distribution=$(dpkg-parsechangelog -S Distribution) +codename=$distribution # as of Aug 13 2019, the distribution is the codename + +OS=debian DIST=${codename} ARCH=amd64 pbuilder-ev3dev build +OS=raspbian DIST=${codename} ARCH=armhf pbuilder-ev3dev build + +debsign ~/pbuilder-ev3dev/debian/${codename}-amd64/${source}_${version}_amd64.changes +debsign ~/pbuilder-ev3dev/raspbian/${codename}-armhf/${source}_${version}_armhf.changes + +dput ev3dev-debian ~/pbuilder-ev3dev/debian/${codename}-amd64/${source}_${version}_amd64.changes +dput ev3dev-raspbian ~/pbuilder-ev3dev/raspbian/${codename}-armhf/${source}_${version}_armhf.changes + +gbp buildpackage --git-tag-only diff --git a/debian/rules b/debian/rules index e97e845..edc3d3b 100755 --- a/debian/rules +++ b/debian/rules @@ -1,12 +1,63 @@ #!/usr/bin/make -f +#export DH_VERBOSE=1 -export PYBUILD_NAME=python3-ev3dev +export PYBUILD_NAME=ev3dev2 VERSION=$(shell dpkg-parsechangelog | sed -rne 's,^Version: (.*),\1,p' | sed 's,~,,') +mpy_files = \ + ev3dev2/__init__.mpy \ + ev3dev2/_platform/__init__.mpy \ + ev3dev2/_platform/brickpi.mpy \ + ev3dev2/_platform/brickpi3.mpy \ + ev3dev2/_platform/ev3.mpy \ + ev3dev2/_platform/evb.mpy \ + ev3dev2/_platform/fake.mpy \ + ev3dev2/_platform/pistorms.mpy \ + ev3dev2/auto.mpy \ + ev3dev2/button.mpy \ + ev3dev2/console.mpy \ + ev3dev2/control/__init__.mpy \ + ev3dev2/control/GyroBalancer.mpy \ + ev3dev2/control/rc_tank.mpy \ + ev3dev2/control/webserver.mpy \ + ev3dev2/display.mpy \ + ev3dev2/fonts/__init__.mpy \ + ev3dev2/led.mpy \ + ev3dev2/motor.mpy \ + ev3dev2/port.mpy \ + ev3dev2/power.mpy \ + ev3dev2/sensor/__init__.mpy \ + ev3dev2/sensor/lego.mpy \ + ev3dev2/sound.mpy \ + ev3dev2/stopwatch.mpy \ + ev3dev2/unit.mpy \ + ev3dev2/version.mpy \ + ev3dev2/wheel.mpy + %: dh $@ --with python3 --buildsystem=pybuild +%.mpy: %.py + mpy-cross -v -v -mcache-lookup-bc $< + +# compile .py > .mpy +override_dh_auto_build: $(mpy_files) + # build python3 package + dh_auto_build + +# fail build if any files aren't installed into a package +override_dh_install: + dh_install --fail-missing + +override_dh_auto_install: + # install python3 package + dh_auto_install + # install .mpy files for micropython + for d in $(mpy_files); do \ + install -D --mode=644 $$d debian/micropython-ev3dev2/usr/lib/micropython/ev3dev2/$${d#*/}; \ + done + override_dh_auto_configure: echo $(VERSION) > RELEASE-VERSION dh_auto_configure diff --git a/demo/BALANC3R b/demo/BALANC3R deleted file mode 100755 index 1cc572f..0000000 --- a/demo/BALANC3R +++ /dev/null @@ -1,29 +0,0 @@ -#!/usr/bin/env python3 - -import logging -from ev3dev.GyroBalancer import GyroBalancer - - -class BALANC3R(GyroBalancer): - """ - Laurens Valk's BALANC3R - http://robotsquare.com/2014/06/23/tutorial-building-balanc3r/ - """ - def __init__(self): - GyroBalancer.__init__(self, - gainGyroAngle=1156, - gainGyroRate=146, - gainMotorAngle=7, - gainMotorAngularSpeed=9, - gainMotorAngleErrorAccumulated=3) - - -if __name__ == '__main__': - logging.basicConfig(level=logging.DEBUG, - format='%(asctime)s %(levelname)5s: %(message)s') - log = logging.getLogger(__name__) - - log.info("Starting BALANC3R") - robot = BALANC3R() - robot.main() - log.info("Exiting BALANC3R") diff --git a/demo/EV3D4/EV3D4RemoteControl.py b/demo/EV3D4/EV3D4RemoteControl.py deleted file mode 100755 index 6d2df5d..0000000 --- a/demo/EV3D4/EV3D4RemoteControl.py +++ /dev/null @@ -1,28 +0,0 @@ -#!/usr/bin/env python3 - -import logging -import sys -from ev3dev.auto import OUTPUT_A, OUTPUT_B, OUTPUT_C -from ev3dev.helper import RemoteControlledTank, MediumMotor - -class EV3D4RemoteControlled(RemoteControlledTank): - - def __init__(self, medium_motor=OUTPUT_A, left_motor=OUTPUT_C, right_motor=OUTPUT_B): - RemoteControlledTank.__init__(self, left_motor, right_motor) - self.medium_motor = MediumMotor(medium_motor) - - if not self.medium_motor.connected: - log.error("%s is not connected" % self.medium_motor) - sys.exit(1) - - self.medium_motor.reset() - - -logging.basicConfig(level=logging.INFO, - format='%(asctime)s %(levelname)5s: %(message)s') -log = logging.getLogger(__name__) - -log.info("Starting EV3D4") -ev3d4 = EV3D4RemoteControlled() -ev3d4.main() -log.info("Exiting EV3D4") diff --git a/demo/EV3D4/EV3D4WebControl.py b/demo/EV3D4/EV3D4WebControl.py deleted file mode 100755 index da70e7e..0000000 --- a/demo/EV3D4/EV3D4WebControl.py +++ /dev/null @@ -1,29 +0,0 @@ -#!/usr/bin/env python3 - -import logging -import sys -from ev3dev.auto import OUTPUT_A, OUTPUT_B, OUTPUT_C -from ev3dev.helper import MediumMotor -from ev3dev.webserver import WebControlledTank - -class EV3D4WebControlled(WebControlledTank): - - def __init__(self, medium_motor=OUTPUT_A, left_motor=OUTPUT_C, right_motor=OUTPUT_B): - WebControlledTank.__init__(self, left_motor, right_motor) - self.medium_motor = MediumMotor(medium_motor) - - if not self.medium_motor.connected: - log.error("%s is not connected" % self.medium_motor) - sys.exit(1) - - self.medium_motor.reset() - - -logging.basicConfig(level=logging.INFO, - format='%(asctime)s %(levelname)5s: %(message)s') -log = logging.getLogger(__name__) - -log.info("Starting EV3D4") -ev3d4 = EV3D4WebControlled() -ev3d4.main() # start the web server -log.info("Exiting EV3D4") diff --git a/demo/EV3D4/README.md b/demo/EV3D4/README.md deleted file mode 100644 index fd44827..0000000 --- a/demo/EV3D4/README.md +++ /dev/null @@ -1,46 +0,0 @@ -# EV3D4 -EV3D4 is designed to look like R2-D2 from Star Wars. There are two options for -controlling EV3D4. The first is to use the IR remote to send commands to the IR -sensor, run EV3D4RemoteControl.py to use this method. The second means of -controlling EV3D4 is via a web browser, run EV3D4WebControl.py to use this method. -You can run both of these from the Brickman interface or if logged in via ssh -you can run them via ./EV3D4RemoteControl.py or ./EV3D4WebControl.py. - -**Building instructions**: https://www.lego.com/en-us/mindstorms/build-a-robot/ev3d4 - -### EV3D4RemoteControl -EV3D4RemoteControl.py creates a child class of ev3dev/helper.py's -RemoteControlledTank. - - -### EV3D4WebControl -EV3D4WebControl creates a child class of ev3dev/webserver.py's WebControlledTank. -The WebControlledTank class runs a web server that serves the web pages, -images, etc but it also services the AJAX calls made via the client. The user -loads the initial web page at which point they choose the "Desktop interface" -or the "Mobile Interface". - -Desktop Interface - The user is presented with four arrows for moving forwards, -backwards, spinning clockwise or spinning counter-clockwise. Two additional -buttons are provided for controlling the medium motor. There are two sliders, -one to control the speed of the tank and the other to control the speed of the -medium motor. - -Mobile Interface - The user is presented with a virtual joystick that is used -to control the movements of the robot. Slide your thumb forward and the robot -moves forward, slide it to the right and the robot spins clockwise, etc. The -further you move the joystick towards the edge of the circle the faster the -robot moves. Buttons and a speed slider for the medium motor are also provided. - -Both interfaces have touch support so you can use either Desktop or Mobile from -your smartphone. When the user clicks/touches a button some jQuery code will -fire off an AJAX call to let the EV3D4 web server know what the user clicked or -where the joystick is if using the Mobile Interface. The web server in -WebControlledTank services this AJAX call and adjust motor speed/power -accordingly. - -You can see a demo of the web interface below. Note that the demo is on a -simple Tank robot, not EV3D4, but that doesn't really matter as EV3D4 is also -just a Tank robot. - -**Demo Video**: https://www.youtube.com/watch?v=x5VauXr7W4A diff --git a/demo/EV3D4/desktop.html b/demo/EV3D4/desktop.html deleted file mode 100644 index f031785..0000000 --- a/demo/EV3D4/desktop.html +++ /dev/null @@ -1,47 +0,0 @@ - - - - - - - - - - - -Lego Tank - - -
- - -
- -
- - - - -
- - -
-
- -
- - -
-
- -
-
- -
- -
- -
- - diff --git a/demo/EV3D4/favicon.ico b/demo/EV3D4/favicon.ico deleted file mode 100644 index f562293..0000000 Binary files a/demo/EV3D4/favicon.ico and /dev/null differ diff --git a/demo/EV3D4/include/ArrowClockwise.png b/demo/EV3D4/include/ArrowClockwise.png deleted file mode 100644 index 6d14ef8..0000000 Binary files a/demo/EV3D4/include/ArrowClockwise.png and /dev/null differ diff --git a/demo/EV3D4/include/ArrowDown.png b/demo/EV3D4/include/ArrowDown.png deleted file mode 100644 index 6051bfe..0000000 Binary files a/demo/EV3D4/include/ArrowDown.png and /dev/null differ diff --git a/demo/EV3D4/include/ArrowLeft.png b/demo/EV3D4/include/ArrowLeft.png deleted file mode 100644 index 54ba5a4..0000000 Binary files a/demo/EV3D4/include/ArrowLeft.png and /dev/null differ diff --git a/demo/EV3D4/include/ArrowRight.png b/demo/EV3D4/include/ArrowRight.png deleted file mode 100644 index 7014f9f..0000000 Binary files a/demo/EV3D4/include/ArrowRight.png and /dev/null differ diff --git a/demo/EV3D4/include/ArrowUp.png b/demo/EV3D4/include/ArrowUp.png deleted file mode 100644 index e9068ae..0000000 Binary files a/demo/EV3D4/include/ArrowUp.png and /dev/null differ diff --git a/demo/EV3D4/include/desktop.png b/demo/EV3D4/include/desktop.png deleted file mode 100644 index 857c500..0000000 Binary files a/demo/EV3D4/include/desktop.png and /dev/null differ diff --git a/demo/EV3D4/include/gear.png b/demo/EV3D4/include/gear.png deleted file mode 100644 index 8898a16..0000000 Binary files a/demo/EV3D4/include/gear.png and /dev/null differ diff --git a/demo/EV3D4/include/jquery.ui.touch-punch.min.js b/demo/EV3D4/include/jquery.ui.touch-punch.min.js deleted file mode 100644 index 518b3a5..0000000 --- a/demo/EV3D4/include/jquery.ui.touch-punch.min.js +++ /dev/null @@ -1,11 +0,0 @@ -/*! - * jQuery UI Touch Punch 0.2.3 - * - * Copyright 2011-2014, Dave Furfero - * Dual licensed under the MIT or GPL Version 2 licenses. - * - * Depends: - * jquery.ui.widget.js - * jquery.ui.mouse.js - */ -!function(a){function f(a,b){if(!(a.originalEvent.touches.length>1)){a.preventDefault();var c=a.originalEvent.changedTouches[0],d=document.createEvent("MouseEvents");d.initMouseEvent(b,!0,!0,window,1,c.screenX,c.screenY,c.clientX,c.clientY,!1,!1,!1,!1,0,null),a.target.dispatchEvent(d)}}if(a.support.touch="ontouchend"in document,a.support.touch){var e,b=a.ui.mouse.prototype,c=b._mouseInit,d=b._mouseDestroy;b._touchStart=function(a){var b=this;!e&&b._mouseCapture(a.originalEvent.changedTouches[0])&&(e=!0,b._touchMoved=!1,f(a,"mouseover"),f(a,"mousemove"),f(a,"mousedown"))},b._touchMove=function(a){e&&(this._touchMoved=!0,f(a,"mousemove"))},b._touchEnd=function(a){e&&(f(a,"mouseup"),f(a,"mouseout"),this._touchMoved||f(a,"click"),e=!1)},b._mouseInit=function(){var b=this;b.element.bind({touchstart:a.proxy(b,"_touchStart"),touchmove:a.proxy(b,"_touchMove"),touchend:a.proxy(b,"_touchEnd")}),c.call(b)},b._mouseDestroy=function(){var b=this;b.element.unbind({touchstart:a.proxy(b,"_touchStart"),touchmove:a.proxy(b,"_touchMove"),touchend:a.proxy(b,"_touchEnd")}),d.call(b)}}}(jQuery); diff --git a/demo/EV3D4/include/mobile.png b/demo/EV3D4/include/mobile.png deleted file mode 100644 index 24f761f..0000000 Binary files a/demo/EV3D4/include/mobile.png and /dev/null differ diff --git a/demo/EV3D4/include/tank-desktop.js b/demo/EV3D4/include/tank-desktop.js deleted file mode 100644 index 595c934..0000000 --- a/demo/EV3D4/include/tank-desktop.js +++ /dev/null @@ -1,165 +0,0 @@ - -var moving = 0; -var ip = 0; -var seq = 0; - -function stop_motors() { - var ajax_url = "/" + seq + "/move-stop/" - seq++; - - $.ajax({ - type: "GET", - cache: false, - dataType: 'text', - url: ajax_url - }); - - moving = 0 -} - -function stop_medium_motor() { - var ajax_url = "/" + seq + "/motor-stop/medium/" - seq++; - - $.ajax({ - type: "GET", - cache: false, - dataType: 'text', - url: ajax_url - }); - return false; -} - -// Prevent the page from scrolling on an iphone -// http://stackoverflow.com/questions/7768269/ipad-safari-disable-scrolling-and-bounce-effect -$(document).bind( - 'touchmove', - function(e) { - e.preventDefault(); - } -); - -$(document).ready(function() { - - $("#medium-motor-speed").slider({ - min: 0, - max: 100, - step: 5, - value: 50 - }); - - $("#tank-speed").slider({ - min: 0, - max: 100, - step: 5, - value: 25 - }); - - // Desktop Interface - $('#ArrowUp').bind('touchstart mousedown', function() { - console.log('ArrowUp down') - var power = $('#tank-speed').slider("value") - var ajax_url = "/" + seq + "/move-start/forward/" + power + "/" - seq++; - moving = 1 - - $.ajax({ - type: "GET", - cache: false, - dataType: 'text', - url: ajax_url - }); - return false; - }); - - $('#ArrowDown').bind('touchstart mousedown', function() { - console.log('ArrowDown down') - var power = $('#tank-speed').slider("value") - var ajax_url = "/" + seq + "/move-start/backward/" + power + "/" - seq++; - moving = 1 - - $.ajax({ - type: "GET", - cache: false, - dataType: 'text', - url: ajax_url - }); - return false; - }); - - $('#ArrowLeft').bind('touchstart mousedown', function() { - console.log('ArrowLeft down') - var power = $('#tank-speed').slider("value") - var ajax_url = "/" + seq + "/move-start/left/" + power + "/" - seq++; - moving = 1 - - $.ajax({ - type: "GET", - cache: false, - dataType: 'text', - url: ajax_url - }); - return false; - }); - - $('#ArrowRight').bind('touchstart mousedown', function() { - console.log('ArrowRight down') - var power = $('#tank-speed').slider("value") - var ajax_url = "/" + seq + "/move-start/right/" + power + "/" - seq++; - moving = 1 - - $.ajax({ - type: "GET", - cache: false, - dataType: 'text', - url: ajax_url - }); - return false; - }); - - $('#desktop-medium-motor-spin .CounterClockwise').bind('touchstart mousedown', function() { - console.log('CounterClockwise down') - var power = $('#medium-motor-speed').slider("value") - var ajax_url = "/" + seq + "/motor-start/medium/counter-clockwise/" + power + "/" - seq++; - - $.ajax({ - type: "GET", - cache: false, - dataType: 'text', - url: ajax_url - }); - return false; - }); - - $('#desktop-medium-motor-spin .Clockwise').bind('touchstart mousedown', function() { - console.log('Clockwise down') - var power = $('#medium-motor-speed').slider("value") - var ajax_url = "/" + seq + "/motor-start/medium/clockwise/" + power + "/" - seq++; - - $.ajax({ - type: "GET", - cache: false, - dataType: 'text', - url: ajax_url - }); - return false; - }); - - $('.medium').bind('touchend mouseup mouseout', function() { - stop_medium_motor() - return false; - }); - - $('.nav').bind('touchend mouseup mouseout', function() { - if (moving) { - console.log('Mouse no longer over button') - stop_motors() - return false; - } - }); -}); diff --git a/demo/EV3D4/include/tank-mobile.js b/demo/EV3D4/include/tank-mobile.js deleted file mode 100644 index 62ac8a1..0000000 --- a/demo/EV3D4/include/tank-mobile.js +++ /dev/null @@ -1,195 +0,0 @@ - -var start_x = 0; -var start_y = 0; -var moving = 0; -var seq = 0; -var prev_x = 0; -var prev_y = 0; - -function stop_motors() { - var ajax_url = "/" + seq + "/move-stop/" - seq++; - - $.ajax({ - type: "GET", - cache: false, - dataType: 'text', - async: true, - url: ajax_url - }); - moving = 0; -} - -function stop_medium_motor() { - var ajax_url = "/" + seq + "/motor-stop/medium/" - seq++; - - $.ajax({ - type: "GET", - cache: false, - dataType: 'text', - async: true, - url: ajax_url - }); - return false; -} - -function ajax_move_xy(x, y) { - // console.log("FIRE ajax call with x,y " + x + "," + y) - var ajax_url = "/" + seq + "/move-xy/" + x + "/" + y + "/" - seq++; - - $.ajax({ - type: "GET", - cache: false, - dataType: 'text', - async: true, - url: ajax_url - }); - moving = 1; -} - -function ajax_log(msg) { - var ajax_url = "/" + seq + "/log/" + msg + "/" - seq++; - - $.ajax({ - type: "GET", - cache: false, - dataType: 'text', - async: true, - url: ajax_url - }); -} - - -// Prevent the page from scrolling on an iphone -// http://stackoverflow.com/questions/7768269/ipad-safari-disable-scrolling-and-bounce-effect -$(document).bind( - 'touchmove', - function(e) { - e.preventDefault(); - } -); - -$(document).ready(function() { - - // Used the 'Restrict the inside circle to the outside circle' code - var r = $('#joystick-wrapper').width()/2; - var small_r = $('#joystick').width()/2; - var origin_x = r - small_r; - var origin_y = r - small_r; - - $("#medium-motor-speed").slider({ - min: 0, - max: 100, - step: 5, - value: 50 - }); - - $('#medium-motor-spin .CounterClockwise').bind('touchstart mousedown', function() { - var power = $('#medium-motor-speed').slider("value") - var ajax_url = "/" + seq + "/motor-start/medium/counter-clockwise/" + power + "/" - seq++; - - $.ajax({ - type: "GET", - cache: false, - dataType: 'text', - url: ajax_url - }); - return false; - }); - - $('#medium-motor-spin .Clockwise').bind('touchstart mousedown', function() { - var power = $('#medium-motor-speed').slider("value") - var ajax_url = "/" + seq + "/motor-start/medium/clockwise/" + power + "/" - seq++; - - $.ajax({ - type: "GET", - cache: false, - dataType: 'text', - url: ajax_url - }); - return false; - }); - - $('.medium').bind('touchend mouseup', function() { - stop_medium_motor() - return false; - }); - - $("div#joystick").draggable({ - revert: true, - containment: "parent", - create: function() { - start_x = parseInt($(this).css("left")); - start_y = parseInt($(this).css("top")); - prev_x = start_x; - prev_y = start_y; - }, - drag: function(event, ui) { - - // Restrict the inside circle to the outside circle - // http://stackoverflow.com/questions/26787996/containing-draggable-circle-to-a-larger-circle - var x = ui.position.left - origin_x, y = ui.position.top - origin_y; - var l = Math.sqrt(x*x + y*y); - var l_in = Math.min(r - small_r, l); - ui.position = {'left': Math.round(x/l*l_in) + origin_x, 'top': Math.round(y/l*l_in) + origin_y}; - - // Get coordinates - var x = ui.position.left - start_x - var y = (ui.position.top - start_y) * -1 - var distance = 0; - - // If this is the initial touch then set the distance high so we'll move - if (prev_x == start_x && prev_y == start_y) { - distance = 99; - } else { - distance = Math.round(Math.sqrt(((x - prev_x) * (x - prev_x)) + ((y - prev_y) * (y - prev_y)))); - } - - // When you drag the joystick it can fire off a LOT of drag - // events (one about every 8 ms), it ends up overwhelming the - // web server on the EV3. It takes the EV3 ~55ms to process - // one of these request so don't send one if the x,y coordinates - // have only changed a tiny bit - if (distance >= 10) { - ajax_move_xy(x, y); - prev_x = x; - prev_y = y; - } - }, - stop: function() { - if (moving) { - stop_motors(); - } - prev_x = start_x; - prev_y = start_y; - } - }); - - // This reacts much faster than the draggable stop event - $('#joystick-wrapper').bind('touchend mouseup', function() { - if (moving) { - stop_motors() - } - prev_x = start_x; - prev_y = start_y; - return true; - }); - - $('#joystick-wrapper').bind('touchstart mousedown', function() { - var ajax_url = "/" + seq + "/joystick-engaged/" - seq++; - - $.ajax({ - type: "GET", - cache: false, - dataType: 'text', - async: true, - url: ajax_url - }); - }); -}); diff --git a/demo/EV3D4/include/tank.css b/demo/EV3D4/include/tank.css deleted file mode 100644 index 83d746f..0000000 --- a/demo/EV3D4/include/tank.css +++ /dev/null @@ -1,129 +0,0 @@ -/* Entire Page - layout */ - -body { - margin: 0px; - padding: 0; - background: #FFF; - font-family: 'Armata', sans-serif; - font-size: 12px; - color: #222; -} - -.alignCenter{ - width: 960px; - margin: 0px auto; -} - -div#header { - padding-bottom: 70px; -} - -div#controls { - float: left; - width: 450px; -} - -img.button { - cursor: pointer; - border: 4px solid white; - - /* rounded corners */ - -moz-border-radius: 20px; - -webkit-border-radius: 20px; - -khtml-border-radius: 20px; - border-radius: 20px; - - -webkit-touch-callout: none; - -webkit-user-select: none; -} - -img.button:hover { - border: 4px solid black; -} - -img.button:active { - border: 4px solid red; -} - -img#ArrowUp, -img#ArrowDown { - margin-left: 137px; -} - -img#ArrowLeft { -} - -img.Clockwise { - width: 75px; - height: 75px; -} - -img.CounterClockwise { - -moz-transform: scaleX(-1); - -o-transform: scaleX(-1); - -webkit-transform: scaleX(-1); - transform: scaleX(-1); - filter: FlipH; - -ms-filter: "FlipH"; - width: 75px; - height: 75px; -} - -div#desktop-interface { - width: 520px; - float: left; - padding-top: 100px; - text-align: center; -} - -div#mobile-interface { - width: 300px; - padding-left: 600px; - padding-top: 100px; - text-align: center; -} - -div#medium-motor-spin { - width: 250px; - margin-left: 300px; - text-align: center; -} - -div#desktop-medium-motor-spin { - width: 250px; - margin-left: 450px; - text-align: center; -} - -div#tank-speed, -div#medium-motor-speed { - margin-top: 20px; - margin-bottom: 10px; -} - -div#joystick-wrapper { - float: left; - position: fixed; - top: 5px; - left: 10px; - width: 250px; - height: 250px; - -webkit-border-radius: 125px; - -moz-border-radius: 125px; - border-radius: 125px; - background: #848484; -} - -div#joystick { - width: 50px; - height: 50px; - -webkit-border-radius: 25px; - -moz-border-radius: 25px; - border-radius: 25px; - background: black; - cursor: pointer; - position: relative; - top: 100px; - left: 100px -} - diff --git a/demo/EV3D4/include/tank.png b/demo/EV3D4/include/tank.png deleted file mode 100644 index b83b552..0000000 Binary files a/demo/EV3D4/include/tank.png and /dev/null differ diff --git a/demo/EV3D4/index.html b/demo/EV3D4/index.html deleted file mode 100644 index 147d9a9..0000000 --- a/demo/EV3D4/index.html +++ /dev/null @@ -1,27 +0,0 @@ - - - - - - -Lego Tank - - -
- - -
-Desktop -

Desktop Interface

-
- -
-Mobile -

Mobile Interface

-
- -
-
- - diff --git a/demo/EV3D4/mobile.html b/demo/EV3D4/mobile.html deleted file mode 100644 index 7b5971f..0000000 --- a/demo/EV3D4/mobile.html +++ /dev/null @@ -1,33 +0,0 @@ - - - - - - - - - - - - -Lego Tank - - - - -
-
-
-
- -
- - -
-
-
-
-
- - diff --git a/demo/EV3RSTORM/README.md b/demo/EV3RSTORM/README.md deleted file mode 100644 index 2608875..0000000 --- a/demo/EV3RSTORM/README.md +++ /dev/null @@ -1,14 +0,0 @@ -EV3RSTORM -========= - -EV3RSTORM is the most advanced of the LEGO(r) MINDSTORMS(r) Robots. -Equipped with a blasting bazooka and a spinning tri-blade, EV3RSTORM is -superior in both intelligence as well as in fighting power. - -Our version, being built with ev3dev, is also vastly more intelligent (one -could say, it has a [brain size of a planet](https://en.wikipedia.org/wiki/Marvin_(character))) -so it may be afflicted with severe depression and boredom at times. - -The build instructions may be found at the official LEGO MINDSTROMS site -[here](http://www.lego.com/en-us/mindstorms/build-a-robot/ev3rstorm). - diff --git a/demo/EV3RSTORM/ev3rstorm.py b/demo/EV3RSTORM/ev3rstorm.py deleted file mode 100644 index b7354bf..0000000 --- a/demo/EV3RSTORM/ev3rstorm.py +++ /dev/null @@ -1,157 +0,0 @@ -#!/usr/bin/env python3 - -import time, random -import ev3dev.ev3 as ev3 - -random.seed( time.time() ) - -def quote(topic): - """ - Recite a random Marvin the Paranoid Android quote on the specified topic. - See https://en.wikipedia.org/wiki/Marvin_(character) - """ - - marvin_quotes = { - 'initiating' : ( - "Life? Don't talk to me about life!", - "Now I've got a headache.", - "This will all end in tears.", - ), - 'depressed' : ( - "I think you ought to know I'm feeling very depressed.", - "Incredible... it's even worse than I thought it would be.", - "I'd make a suggestion, but you wouldn't listen.", - ), - } - - ev3.Sound.speak(random.choice(marvin_quotes[topic])).wait() - -def check(condition, message): - """ - Check that condition is true, - loudly complain and throw an exception otherwise. - """ - if not condition: - ev3.Sound.speak(message).wait() - quote('depressed') - raise Exception(message) - -class ev3rstorm: - def __init__(self): - # Connect the required equipement - self.lm = ev3.LargeMotor('outB') - self.rm = ev3.LargeMotor('outC') - self.mm = ev3.MediumMotor() - - self.ir = ev3.InfraredSensor() - self.ts = ev3.TouchSensor() - self.cs = ev3.ColorSensor() - - self.screen = ev3.Screen() - - # Check if everything is attached - check(self.lm.connected, 'My left leg is missing!') - check(self.rm.connected, 'Right leg is not found!') - check(self.mm.connected, 'My left arm is not connected!') - - check(self.ir.connected, 'My eyes, I can not see!') - check(self.ts.connected, 'Touch sensor is not attached!') - check(self.cs.connected, 'Color sensor is not responding!') - - # Reset the motors - for m in (self.lm, self.rm, self.mm): - m.reset() - m.position = 0 - m.stop_action = 'brake' - - self.draw_face() - - quote('initiating') - - def draw_face(self): - w,h = self.screen.shape - y = h // 2 - - eye_xrad = 20 - eye_yrad = 30 - - pup_xrad = 10 - pup_yrad = 10 - - def draw_eye(x): - self.screen.draw.ellipse((x-eye_xrad, y-eye_yrad, x+eye_xrad, y+eye_yrad)) - self.screen.draw.ellipse((x-pup_xrad, y-pup_yrad, x+pup_xrad, y+pup_yrad), fill='black') - - draw_eye(w//3) - draw_eye(2*w//3) - - self.screen.update() - - def shoot(self, direction='up'): - """ - Shot a ball in the specified direction (valid choices are 'up' and 'down') - """ - self.mm.run_to_rel_pos(speed_sp=900, position_sp=(-1080 if direction == 'up' else 1080)) - while 'running' in self.mm.state: - time.sleep(0.1) - - def rc_loop(self): - """ - Enter the remote control loop. RC buttons on channel 1 control the - robot movement, channel 2 is for shooting things. - The loop ends when the touch sensor is pressed. - """ - - def roll(motor, led_group, speed): - """ - Generate remote control event handler. It rolls given motor into - given direction (1 for forward, -1 for backward). When motor rolls - forward, the given led group flashes green, when backward -- red. - When motor stops, the leds are turned off. - - The on_press function has signature required by RemoteControl - class. It takes boolean state parameter; True when button is - pressed, False otherwise. - """ - def on_press(state): - if state: - # Roll when button is pressed - motor.run_forever(speed_sp=speed) - ev3.Leds.set_color(led_group, - ev3.Leds.GREEN if speed > 0 else ev3.Leds.RED) - else: - # Stop otherwise - motor.stop() - ev3.Leds.set_color(led_group, ev3.Leds.BLACK) - - return on_press - - rc1 = ev3.RemoteControl(self.ir, 1) - rc1.on_red_up = roll(self.lm, ev3.Leds.LEFT, 900) - rc1.on_red_down = roll(self.lm, ev3.Leds.LEFT, -900) - rc1.on_blue_up = roll(self.rm, ev3.Leds.RIGHT, 900) - rc1.on_blue_down = roll(self.rm, ev3.Leds.RIGHT, -900) - - - def shoot(direction): - def on_press(state): - if state: self.shoot(direction) - return on_press - - rc2 = ev3.RemoteControl(self.ir, 2) - rc2.on_red_up = shoot('up') - rc2.on_blue_up = shoot('up') - rc2.on_red_down = shoot('down') - rc2.on_blue_down = shoot('down') - - # Now that the event handlers are assigned, - # lets enter the processing loop: - while not self.ts.is_pressed: - rc1.process() - rc2.process() - time.sleep(0.1) - - -if __name__ == '__main__': - Marvin = ev3rstorm() - Marvin.rc_loop() diff --git a/demo/EXPLOR3R/README.rst b/demo/EXPLOR3R/README.rst deleted file mode 100644 index 146d6a9..0000000 --- a/demo/EXPLOR3R/README.rst +++ /dev/null @@ -1,3 +0,0 @@ -The examples in this folder, unless stated otherwise, are based on Explor3r -robot by Laurens Valk. The assembling instructions for the robot may be found -here: http://robotsquare.com/2015/10/06/explor3r-building-instructions. diff --git a/demo/EXPLOR3R/auto-drive.py b/demo/EXPLOR3R/auto-drive.py deleted file mode 100755 index bae8fe3..0000000 --- a/demo/EXPLOR3R/auto-drive.py +++ /dev/null @@ -1,139 +0,0 @@ -#!/usr/bin/env python3 - -# ----------------------------------------------------------------------------- -# Copyright (c) 2015 Denis Demidov -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. -# ----------------------------------------------------------------------------- - -# In this demo an Explor3r robot with touch sensor attachement drives -# autonomously. It drives forward until an obstacle is bumped (determined by -# the touch sensor), then turns in a random direction and continues. The robot -# slows down when it senses obstacle ahead (with the infrared sensor). -# -# The program may be stopped by pressing any button on the brick. -# -# This demonstrates usage of motors, sound, sensors, buttons, and leds. - -from time import sleep -from random import choice, randint - -from ev3dev.auto import * - -# Connect two large motors on output ports B and C: -motors = [LargeMotor(address) for address in (OUTPUT_B, OUTPUT_C)] - -# Every device in ev3dev has `connected` property. Use it to check that the -# device has actually been connected. -assert all([m.connected for m in motors]), \ - "Two large motors should be connected to ports B and C" - -# Connect infrared and touch sensors. -ir = InfraredSensor(); assert ir.connected -ts = TouchSensor(); assert ts.connected - -print('Robot Starting') - -# We will need to check EV3 buttons state. -btn = Button() - -def start(): - """ - Start both motors. `run-direct` command will allow to vary motor - performance on the fly by adjusting `duty_cycle_sp` attribute. - """ - for m in motors: - m.run_direct() - -def backup(): - """ - Back away from an obstacle. - """ - - # Sound backup alarm. - Sound.tone([(1000, 500, 500)] * 3) - - # Turn backup lights on: - for light in (Leds.LEFT, Leds.RIGHT): - Leds.set_color(light, Leds.RED) - - # Stop both motors and reverse for 1.5 seconds. - # `run-timed` command will return immediately, so we will have to wait - # until both motors are stopped before continuing. - for m in motors: - m.stop(stop_action='brake') - m.run_timed(speed_sp=-500, time_sp=1500) - - # When motor is stopped, its `state` attribute returns empty list. - # Wait until both motors are stopped: - while any(m.state for m in motors): - sleep(0.1) - - # Turn backup lights off: - for light in (Leds.LEFT, Leds.RIGHT): - Leds.set_color(light, Leds.GREEN) - -def turn(): - """ - Turn the robot in random direction. - """ - - # We want to turn the robot wheels in opposite directions from 1/4 to 3/4 - # of a second. Use `random.choice()` to decide which wheel will turn which - # way. - power = choice([(1, -1), (-1, 1)]) - t = randint(250, 750) - - for m, p in zip(motors, power): - m.run_timed(speed_sp = p * 750, time_sp = t) - - # Wait until both motors are stopped: - while any(m.state for m in motors): - sleep(0.1) - -# Run the robot until a button is pressed. -start() -while not btn.any(): - - if ts.is_pressed: - # We bumped an obstacle. - # Back away, turn and go in other direction. - backup() - turn() - start() - - # Infrared sensor in proximity mode will measure distance to the closest - # object in front of it. - distance = ir.proximity - - if distance > 60: - # Path is clear, run at full speed. - dc = 95 - else: - # Obstacle ahead, slow down. - dc = 30 - - for m in motors: - m.duty_cycle_sp = dc - - sleep(0.1) - -# Stop the motors before exiting. -for m in motors: - m.stop() diff --git a/demo/EXPLOR3R/remote-control.py b/demo/EXPLOR3R/remote-control.py deleted file mode 100755 index f591334..0000000 --- a/demo/EXPLOR3R/remote-control.py +++ /dev/null @@ -1,105 +0,0 @@ -#!/usr/bin/env python3 - -# ----------------------------------------------------------------------------- -# Copyright (c) 2015 Denis Demidov -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. -# ----------------------------------------------------------------------------- - -# This demo shows how to remote control an Explor3r robot with touch sensor -# attachment. -# -# Red buttons control left motor, blue buttons control right motor. -# Leds are used to indicate movement direction. -# Whenever an obstacle is bumped, robot backs away and apologises. - -from time import sleep -from ev3dev.auto import * - -# Connect two large motors on output ports B and C -lmotor, rmotor = [LargeMotor(address) for address in (OUTPUT_B, OUTPUT_C)] - -# Check that the motors are actually connected -assert lmotor.connected -assert rmotor.connected - -# Connect touch sensor and remote control -ts = TouchSensor(); assert ts.connected -rc = RemoteControl(); assert rc.connected - -# Initialize button handler -button = Button() - -# Turn leds off -Leds.all_off() - -def roll(motor, led_group, direction): - """ - Generate remote control event handler. It rolls given motor into given - direction (1 for forward, -1 for backward). When motor rolls forward, the - given led group flashes green, when backward -- red. When motor stops, the - leds are turned off. - - The on_press function has signature required by RemoteControl class. - It takes boolean state parameter; True when button is pressed, False - otherwise. - """ - def on_press(state): - if state: - # Roll when button is pressed - motor.run_forever(speed_sp=600*direction) - Leds.set_color(led_group, direction > 0 and Leds.GREEN or Leds.RED) - else: - # Stop otherwise - motor.stop(stop_action='brake') - Leds.set(led_group, brightness_pct=0) - - return on_press - -# Assign event handler to each of the remote buttons -rc.on_red_up = roll(lmotor, Leds.LEFT, 1) -rc.on_red_down = roll(lmotor, Leds.LEFT, -1) -rc.on_blue_up = roll(rmotor, Leds.RIGHT, 1) -rc.on_blue_down = roll(rmotor, Leds.RIGHT, -1) - -# Enter event processing loop -while not button.any(): - rc.process() - - # Backup when bumped an obstacle - if ts.is_pressed: - Sound.speak('Oops, excuse me!') - - for motor in (lmotor, rmotor): - motor.stop(stop_action='brake') - - # Turn red lights on - for led in (Leds.LEFT, Leds.RIGHT): - Leds.set_color(led, Leds.RED) - - # Run both motors backwards for 0.5 seconds - for motor in (lmotor, rmotor): - motor.run_timed(speed_sp=-600, time_sp=500) - - # Wait 0.5 seconds while motors are rolling - sleep(0.5) - - Leds.all_off() - - sleep(0.01) diff --git a/demo/MINDCUB3R/.gitignore b/demo/MINDCUB3R/.gitignore deleted file mode 100644 index 06cf653..0000000 --- a/demo/MINDCUB3R/.gitignore +++ /dev/null @@ -1 +0,0 @@ -cache diff --git a/demo/MINDCUB3R/README.md b/demo/MINDCUB3R/README.md deleted file mode 100644 index 5f8a45e..0000000 --- a/demo/MINDCUB3R/README.md +++ /dev/null @@ -1,61 +0,0 @@ -# MINDCUB3R - -## Installation -### Installing kociemba -The kociemba program produces a sequence of moves used to solve -a 3x3x3 rubiks cube. -``` -$ sudo apt-get install build-essential libffi-dev -$ cd ~/ -$ git clone https://github.com/dwalton76/kociemba.git -$ cd ~/kociemba/kociemba/ckociemba/ -$ make -$ sudo make install -``` - -### Installing rubiks-color-resolver -When the cube is scanned we get the RGB (red, green, blue) value for -all 54 squares of a 3x3x3 cube. rubiks-color-resolver analyzes those RGB -values to determine which of the six possible cube colors is the color for -each square. -``` -$ sudo apt-get install python3-pip -$ sudo pip3 install git+https://github.com/dwalton76/rubiks-color-resolver.git -``` - -### Installing the MINDCUB3R demo -We must git clone the ev3dev-lang-python repository. MINDCUB3R is included -in the demo directory. -``` -$ cd ~/ -$ git clone https://github.com/rhempel/ev3dev-lang-python.git -$ cd ~/ev3dev-lang-python/demo/MINDCUB3R/ -$ kociemba DRLUUBFBRBLURRLRUBLRDDFDLFUFUFFDBRDUBRUFLLFDDBFLUBLRBD -``` - -## Running MINDCUB3R -``` -$ cd ~/ev3dev-lang-python/demo/MINDCUB3R/ -$ ./rubiks.py -``` - -## About kociemba -You may have noticed that the -`kociemba DRLUUBFBRBLURRLRUBLRDDFDLFUFUFFDBRDUBRUFLLFDDBFLUBLRBD` -step of the install looks a little odd. The "DRLUU..." string is a -representation of the colors of each of the 54 squares of a 3x3x3 cube. So -the D at the beginning means that square `#1` is the same color as the middle -square of the Down side (the bottom), the R means that square `#2` is the same -color as the middle square of the Right side, etc. The kociemba program takes -that color data and returns a sequence of moves that can be used to solve the -cube. - -``` -$ kociemba DRLUUBFBRBLURRLRUBLRDDFDLFUFUFFDBRDUBRUFLLFDDBFLUBLRBD -D2 R' D' F2 B D R2 D2 R' F2 D' F2 U' B2 L2 U2 D R2 U -$ -``` - -Running the kociemba program is part of the install process because the first -time you run it, it takes about 30 seconds to build a series of tables that -it caches to the filesystem. After that first run it is nice and fast. diff --git a/demo/MINDCUB3R/rubiks.py b/demo/MINDCUB3R/rubiks.py deleted file mode 100755 index 0e07627..0000000 --- a/demo/MINDCUB3R/rubiks.py +++ /dev/null @@ -1,587 +0,0 @@ -#!/usr/bin/env python3 - -from ev3dev.auto import OUTPUT_A, OUTPUT_B, OUTPUT_C, InfraredSensor -from ev3dev.helper import LargeMotor, MediumMotor, ColorSensor, MotorStall -from pprint import pformat -from rubikscolorresolver import RubiksColorSolverGeneric -from subprocess import check_output -from time import sleep -import json -import logging -import os -import signal -import sys -import time - -log = logging.getLogger(__name__) - - -class ScanError(Exception): - pass - - -class Rubiks(object): - scan_order = [ - 5, 9, 6, 3, 2, 1, 4, 7, 8, - 23, 27, 24, 21, 20, 19, 22, 25, 26, - 50, 54, 51, 48, 47, 46, 49, 52, 53, - 14, 10, 13, 16, 17, 18, 15, 12, 11, - 41, 43, 44, 45, 42, 39, 38, 37, 40, - 32, 34, 35, 36, 33, 30, 29, 28, 31] - - hold_cube_pos = 85 - rotate_speed = 400 - flip_speed = 300 - flip_speed_push = 400 - corner_to_edge_diff = 60 - - def __init__(self): - self.shutdown = False - self.flipper = LargeMotor(OUTPUT_A) - self.turntable = LargeMotor(OUTPUT_B) - self.colorarm = MediumMotor(OUTPUT_C) - self.color_sensor = ColorSensor() - self.color_sensor.mode = self.color_sensor.MODE_RGB_RAW - self.infrared_sensor = InfraredSensor() - self.cube = {} - self.init_motors() - self.state = ['U', 'D', 'F', 'L', 'B', 'R'] - self.rgb_solver = None - signal.signal(signal.SIGTERM, self.signal_term_handler) - signal.signal(signal.SIGINT, self.signal_int_handler) - - def init_motors(self): - - for x in (self.flipper, self.turntable, self.colorarm): - if not x.connected: - log.error("%s is not connected" % x) - sys.exit(1) - x.reset() - - log.info("Initialize flipper %s" % self.flipper) - self.flipper.run_forever(speed_sp=-50, stop_action='hold') - self.flipper.wait_for_stop() - self.flipper.stop() - self.flipper.reset() - self.flipper.stop(stop_action='hold') - - log.info("Initialize colorarm %s" % self.colorarm) - self.colorarm.run_forever(speed_sp=500, stop_action='hold') - self.colorarm.wait_for_stop() - self.colorarm.stop() - self.colorarm.reset() - self.colorarm.stop(stop_action='hold') - - log.info("Initialize turntable %s" % self.turntable) - self.turntable.reset() - self.turntable.stop(stop_action='hold') - - def shutdown_robot(self): - log.info('Shutting down') - self.shutdown = True - - if self.rgb_solver: - self.rgb_solver.shutdown = True - - for x in (self.flipper, self.turntable, self.colorarm): - x.shutdown = True - - for x in (self.flipper, self.turntable, self.colorarm): - x.stop(stop_action='brake') - - def signal_term_handler(self, signal, frame): - log.error('Caught SIGTERM') - self.shutdown_robot() - - def signal_int_handler(self, signal, frame): - log.error('Caught SIGINT') - self.shutdown_robot() - - def apply_transformation(self, transformation): - self.state = [self.state[t] for t in transformation] - - def rotate_cube(self, direction, nb): - current_pos = self.turntable.position - final_pos = 135 * round((self.turntable.position + (270 * direction * nb)) / 135.0) - log.info("rotate_cube() direction %s, nb %s, current_pos %d, final_pos %d" % (direction, nb, current_pos, final_pos)) - - if self.flipper.position > 35: - self.flipper_away() - - self.turntable.run_to_abs_pos(position_sp=final_pos, - speed_sp=Rubiks.rotate_speed, - stop_action='hold', - ramp_up_sp=0) - self.turntable.wait_for_running() - self.turntable.wait_for_position(final_pos) - self.turntable.wait_for_stop() - - if nb >= 1: - for i in range(nb): - if direction > 0: - transformation = [0, 1, 5, 2, 3, 4] - else: - transformation = [0, 1, 3, 4, 5, 2] - self.apply_transformation(transformation) - - def rotate_cube_1(self): - self.rotate_cube(1, 1) - - def rotate_cube_2(self): - self.rotate_cube(1, 2) - - def rotate_cube_3(self): - self.rotate_cube(-1, 1) - - def rotate_cube_blocked(self, direction, nb): - - # Move the arm down to hold the cube in place - self.flipper_hold_cube() - - # OVERROTATE depends on lot on Rubiks.rotate_speed - current_pos = self.turntable.position - OVERROTATE = 18 - final_pos = int(135 * round((current_pos + (270 * direction * nb)) / 135.0)) - temp_pos = int(final_pos + (OVERROTATE * direction)) - - log.info("rotate_cube_blocked() direction %s nb %s, current pos %s, temp pos %s, final pos %s" % - (direction, nb, current_pos, temp_pos, final_pos)) - - self.turntable.run_to_abs_pos(position_sp=temp_pos, - speed_sp=Rubiks.rotate_speed, - stop_action='hold', - ramp_up_sp=0) - self.turntable.wait_for_running() - self.turntable.wait_for_position(temp_pos) - self.turntable.wait_for_stop() - - self.turntable.run_to_abs_pos(position_sp=final_pos, - speed_sp=int(Rubiks.rotate_speed/4), - stop_action='hold', - ramp_up_sp=0) - self.turntable.wait_for_running() - self.turntable.wait_for_position(final_pos, stall_ok=True) - self.turntable.wait_for_stop() - - def rotate_cube_blocked_1(self): - self.rotate_cube_blocked(1, 1) - - def rotate_cube_blocked_2(self): - self.rotate_cube_blocked(1, 2) - - def rotate_cube_blocked_3(self): - self.rotate_cube_blocked(-1, 1) - - def flipper_hold_cube(self, speed=300): - current_position = self.flipper.position - - # Push it forward so the cube is always in the same position - # when we start the flip - if (current_position <= Rubiks.hold_cube_pos - 10 or - current_position >= Rubiks.hold_cube_pos + 10): - self.flipper.run_to_abs_pos(position_sp=Rubiks.hold_cube_pos, - ramp_down_sp=400, - speed_sp=speed) - self.flipper.wait_for_running() - self.flipper.wait_for_position(Rubiks.hold_cube_pos) - self.flipper.wait_for_stop() - sleep(0.05) - - def flipper_away(self, speed=300): - """ - Move the flipper arm out of the way - """ - log.info("flipper_away()") - self.flipper.run_to_abs_pos(position_sp=0, - ramp_down_sp=400, - speed_sp=speed) - self.flipper.wait_for_running() - - try: - self.flipper.wait_for_position(0) - self.flipper.wait_for_stop() - except MotorStall: - self.flipper.stop() - - def flip(self): - """ - Motors will sometimes stall if you call run_to_abs_pos() multiple - times back to back on the same motor. To avoid this we call a 50ms - sleep in flipper_hold_cube() and after each run_to_abs_pos() below. - - We have to sleep after the 2nd run_to_abs_pos() because sometimes - flip() is called back to back. - """ - log.info("flip()") - - if self.shutdown: - return - - # Move the arm down to hold the cube in place - self.flipper_hold_cube() - - # Grab the cube and pull back - self.flipper.run_to_abs_pos(position_sp=190, - ramp_up_sp=200, - ramp_down_sp=0, - speed_sp=self.flip_speed) - self.flipper.wait_for_running() - self.flipper.wait_for_position(190) - self.flipper.wait_for_stop() - sleep(0.05) - - # At this point the cube is at an angle, push it forward to - # drop it back down in the turntable - self.flipper.run_to_abs_pos(position_sp=Rubiks.hold_cube_pos, - ramp_up_sp=200, - ramp_down_sp=400, - speed_sp=self.flip_speed_push) - self.flipper.wait_for_running() - self.flipper.wait_for_position(Rubiks.hold_cube_pos) - self.flipper.wait_for_stop() - sleep(0.05) - - transformation = [2, 4, 1, 3, 0, 5] - self.apply_transformation(transformation) - - def colorarm_middle(self): - log.info("colorarm_middle()") - self.colorarm.run_to_abs_pos(position_sp=-750, - speed_sp=600, - stop_action='hold') - self.colorarm.wait_for_running() - self.colorarm.wait_for_position(-750) - self.colorarm.wait_for_stop() - - def colorarm_corner(self, square_index): - log.info("colorarm_corner(%d)" % square_index) - position_target = -580 - - if square_index == 1: - position_target += 20 - elif square_index == 3: - pass - elif square_index == 5: - position_target -= 20 - elif square_index == 7: - pass - else: - raise ScanError("colorarm_corner was given unsupported square_index %d" % square_index) - - self.colorarm.run_to_abs_pos(position_sp=position_target, - speed_sp=600, - stop_action='hold') - - def colorarm_edge(self, square_index): - log.info("colorarm_edge(%d)" % square_index) - position_target = -640 - - if square_index == 2: - pass - elif square_index == 4: - position_target -= 20 - elif square_index == 6: - position_target -= 20 - elif square_index == 8: - pass - else: - raise ScanError("colorarm_edge was given unsupported square_index %d" % square_index) - - self.colorarm.run_to_abs_pos(position_sp=position_target, - speed_sp=600, - stop_action='hold') - - def colorarm_remove(self): - log.info("colorarm_remove()") - self.colorarm.run_to_abs_pos(position_sp=0, - speed_sp=600) - self.colorarm.wait_for_running() - try: - self.colorarm.wait_for_position(0) - self.colorarm.wait_for_stop() - except MotorStall: - self.colorarm.stop() - - def colorarm_remove_halfway(self): - log.info("colorarm_remove_halfway()") - self.colorarm.run_to_abs_pos(position_sp=-400, - speed_sp=600) - self.colorarm.wait_for_running() - self.colorarm.wait_for_position(-400) - self.colorarm.wait_for_stop() - - def scan_middle(self, face_number): - log.info("scan_middle() %d/6" % face_number) - - if self.flipper.position > 35: - self.flipper_away() - - self.colorarm_middle() - log.info(self.color_sensor.rgb()) - self.colorarm_remove_halfway() - - def scan_middles(self): - """ - Used once to get the RGB values of the middle squares to - populate the crayola_colors in rubiks_rgb_solver.py - """ - log.info("scan_middle()") - self.colors = {} - self.k = 0 - self.scan_middle(1) - raw_input('Paused') - - self.flip() - self.scan_middle(2) - raw_input('Paused') - - self.flip() - self.scan_middle(3) - raw_input('Paused') - - self.rotate_cube(-1, 1) - self.flip() - self.scan_middle(4) - raw_input('Paused') - - self.rotate_cube(1, 1) - self.flip() - self.scan_middle(5) - raw_input('Paused') - - self.flip() - self.scan_middle(6) - raw_input('Paused') - - def scan_face(self, face_number): - log.info("scan_face() %d/6" % face_number) - - if self.shutdown: - return - - if self.flipper.position > 35: - self.flipper_away() - - self.colorarm_middle() - self.colors[int(Rubiks.scan_order[self.k])] = tuple(self.color_sensor.rgb()) - - self.k += 1 - i = 1 - self.colorarm_corner(i) - - # The gear ratio is 3:1 so 1080 is one full rotation - self.turntable.reset() - self.turntable.run_to_abs_pos(position_sp=1080, - speed_sp=Rubiks.rotate_speed, - stop_action='hold') - - while True: - current_position = self.turntable.position - - # 135 is 1/8 of full rotation - if current_position >= (i * 135): - current_color = tuple(self.color_sensor.rgb()) - self.colors[int(Rubiks.scan_order[self.k])] = current_color - # log.info("%s: i %d, current_position %d, current_color %s" % - # (self.turntable, i, current_position, current_color)) - - i += 1 - self.k += 1 - - if i == 9: - # Last face, move the color arm all the way out of the way - if face_number == 6: - self.colorarm_remove() - - # Move the color arm far enough away so that the flipper - # arm doesn't hit it - else: - self.colorarm_remove_halfway() - - break - - elif i % 2: - self.colorarm_corner(i) - else: - self.colorarm_edge(i) - - if self.shutdown: - return - - if i < 9: - raise ScanError('i is %d..should be 9' % i) - - self.turntable.wait_for_position(1080) - self.turntable.stop() - self.turntable.reset() - log.info("\n") - - def scan(self): - log.info("scan()") - self.colors = {} - self.k = 0 - self.scan_face(1) - - self.flip() - self.scan_face(2) - - self.flip() - self.scan_face(3) - - self.rotate_cube(-1, 1) - self.flip() - self.scan_face(4) - - self.rotate_cube(1, 1) - self.flip() - self.scan_face(5) - - self.flip() - self.scan_face(6) - - if self.shutdown: - return - - log.info("RGB json:\n%s\n" % json.dumps(self.colors)) - self.rgb_solver = RubiksColorSolverGeneric(3) - self.rgb_solver.enter_scan_data(self.colors) - self.rgb_solver.crunch_colors() - self.cube_kociemba = self.rgb_solver.cube_for_kociemba_strict() - log.info("Final Colors (kociemba): %s" % ''.join(self.cube_kociemba)) - - # This is only used if you want to rotate the cube so U is on top, F is - # in the front, etc. You would do this if you were troubleshooting color - # detection and you want to pause to compare the color pattern on the - # cube vs. what we think the color pattern is. - ''' - log.info("Position the cube so that U is on top, F is in the front, etc...to make debugging easier") - self.rotate_cube(-1, 1) - self.flip() - self.flipper_away() - self.rotate_cube(1, 1) - raw_input('Paused') - ''' - - def move(self, face_down): - log.info("move() face_down %s" % face_down) - - position = self.state.index(face_down) - actions = { - 0: ["flip", "flip"], - 1: [], - 2: ["rotate_cube_2", "flip"], - 3: ["rotate_cube_1", "flip"], - 4: ["flip"], - 5: ["rotate_cube_3", "flip"] - }.get(position, None) - - for a in actions: - - if self.shutdown: - break - - getattr(self, a)() - - def run_kociemba_actions(self, actions): - log.info('Action (kociemba): %s' % ' '.join(actions)) - total_actions = len(actions) - - for (i, a) in enumerate(actions): - - if self.shutdown: - break - - if a.endswith("'"): - face_down = list(a)[0] - rotation_dir = 1 - elif a.endswith("2"): - face_down = list(a)[0] - rotation_dir = 2 - else: - face_down = a - rotation_dir = 3 - - log.info("Move %d/%d: %s%s (a %s)" % (i, total_actions, face_down, rotation_dir, pformat(a))) - self.move(face_down) - - if rotation_dir == 1: - self.rotate_cube_blocked_1() - elif rotation_dir == 2: - self.rotate_cube_blocked_2() - elif rotation_dir == 3: - self.rotate_cube_blocked_3() - log.info("\n") - - def resolve(self): - - if rub.shutdown: - return - - output = check_output(['kociemba', ''.join(map(str, self.cube_kociemba))]).decode('ascii') - actions = output.strip().split() - self.run_kociemba_actions(actions) - self.cube_done() - - def cube_done(self): - self.flipper_away() - - def wait_for_cube_insert(self): - rubiks_present = 0 - rubiks_present_target = 10 - log.info('wait for cube...to be inserted') - - while True: - - if self.shutdown: - break - - dist = self.infrared_sensor.proximity - - # It is odd but sometimes when the cube is inserted - # the IR sensor returns a value of 100...most of the - # time it is just a value less than 50 - if dist < 50 or dist == 100: - rubiks_present += 1 - log.info("wait for cube...distance %d, present for %d/%d" % - (dist, rubiks_present, rubiks_present_target)) - else: - if rubiks_present: - log.info('wait for cube...cube removed (%d)' % dist) - rubiks_present = 0 - - if rubiks_present >= rubiks_present_target: - log.info('wait for cube...cube found and stable') - break - - time.sleep(0.1) - - -if __name__ == '__main__': - - # logging.basicConfig(filename='rubiks.log', - logging.basicConfig(level=logging.INFO, - format='%(asctime)s %(filename)12s %(levelname)8s: %(message)s') - log = logging.getLogger(__name__) - - # Color the errors and warnings in red - logging.addLevelName(logging.ERROR, "\033[91m %s\033[0m" % logging.getLevelName(logging.ERROR)) - logging.addLevelName(logging.WARNING, "\033[91m %s\033[0m" % logging.getLevelName(logging.WARNING)) - - rub = Rubiks() - - try: - rub.wait_for_cube_insert() - - # Push the cube to the right so that it is in the expected - # position when we begin scanning - rub.flipper_hold_cube(100) - rub.flipper_away(100) - - rub.scan() - rub.resolve() - rub.shutdown_robot() - - except Exception as e: - log.exception(e) - rub.shutdown_robot() - sys.exit(1) diff --git a/demo/R3PTAR/README.md b/demo/R3PTAR/README.md deleted file mode 100644 index 6466009..0000000 --- a/demo/R3PTAR/README.md +++ /dev/null @@ -1,16 +0,0 @@ -R3PTAR -====== - -One of the most loved robots, the standing 35 cm. / 13,8 inch tall R3PTAR robot -slithers across the floor like a real cobra, and strikes at lightning speed -with it’s pointed red fangs. - -Coincidentally, its also a nice example to demonstrate how to use threads in -Python. - -**Building instructions**: http://www.lego.com/en-us/mindstorms/build-a-robot/r3ptar - -**Resources**: - -* `rattle-snake.wav`: https://www.freesound.org/people/7h3_lark/sounds/268580/ -* `snake-hiss.wav`: https://www.freesound.org/people/Reitanna/sounds/343928/ diff --git a/demo/R3PTAR/r3ptar.py b/demo/R3PTAR/r3ptar.py deleted file mode 100755 index 8b9d658..0000000 --- a/demo/R3PTAR/r3ptar.py +++ /dev/null @@ -1,73 +0,0 @@ -#!/usr/bin/env python3 - -import time -import threading -import signal -import ev3dev.ev3 as ev3 - -def tail_waggler(done): - """ - This is the first thread of execution that will be responsible for waggling - r3ptar's tail every couple of seconds. - """ - m = ev3.MediumMotor(); assert m.connected - - while not done.is_set(): - m.run_timed(speed_sp=90, time_sp=1000, stop_action='coast') - time.sleep(1) - ev3.Sound.play('rattle-snake.wav').wait() - m.run_timed(speed_sp=-90, time_sp=1000, stop_action='coast') - time.sleep(2) - -def hand_biter(done): - """ - This is the second thread of execution. It will constantly poll the - infrared sensor for proximity info and bite anything that gets too close. - """ - m = ev3.LargeMotor('outD'); assert m.connected - s = ev3.InfraredSensor(); assert s.connected - - m.run_timed(speed_sp=-200, time_sp=1000, stop_action='brake') - - while not done.is_set(): - # Wait until something (a hand?!) gets too close: - while s.proximity > 30: - if done.is_set(): return - time.sleep(0.1) - - # Bite it! Also, don't forget to hiss: - ev3.Sound.play('snake-hiss.wav') - m.run_timed(speed_sp=600, time_sp=500, stop_action='brake') - time.sleep(0.6) - m.run_timed(speed_sp=-200, time_sp=500, stop_action='brake') - time.sleep(1) - -# The 'done' event will be used to signal the threads to stop: -done = threading.Event() - -# We also need to catch SIGINT (keyboard interrup) and SIGTERM (termination -# signal from brickman) and exit gracefully: -def signal_handler(signal, frame): - done.set() - -signal.signal(signal.SIGINT, signal_handler) -signal.signal(signal.SIGTERM, signal_handler) - -# Now that we have the worker functions defined, lets run those in separate -# threads. -tail = threading.Thread(target=tail_waggler, args=(done,)) -head = threading.Thread(target=hand_biter, args=(done,)) - -tail.start() -head.start() - -# The main thread will wait for the 'back' button to be pressed. When that -# happens, it will signal the worker threads to stop and wait for their -# completion. -btn = ev3.Button() -while not btn.backspace and not done.is_set(): - time.sleep(0.1) - -done.set() -tail.join() -head.join() diff --git a/demo/R3PTAR/rattle-snake.wav b/demo/R3PTAR/rattle-snake.wav deleted file mode 100644 index 10ed3ff..0000000 Binary files a/demo/R3PTAR/rattle-snake.wav and /dev/null differ diff --git a/demo/R3PTAR/snake-hiss.wav b/demo/R3PTAR/snake-hiss.wav deleted file mode 100644 index 2276581..0000000 Binary files a/demo/R3PTAR/snake-hiss.wav and /dev/null differ diff --git a/demo/README.md b/demo/README.md deleted file mode 100644 index 98839d8..0000000 --- a/demo/README.md +++ /dev/null @@ -1,73 +0,0 @@ -# ev3dev demo programs - -This folder contains several demo programs that you can use to help you in -developing your own code. Brief descriptions of each demo are provided below; -you can access the full source code and some more detailed information on each -by opening the respective folders above. - -To install these on your EV3, use git to clone the ev3dev-lang-python repository -from github. Your EV3 will need Internet connectivty in order to clone the -repository from github. -``` -$ sudo apt-get install git -$ git clone https://github.com/rhempel/ev3dev-lang-python.git -``` - -## Running A Program -There are two ways to run a program. You can run a program from the command line -or from the brickman interface. - -Note that for both running from the command line and running from Brickman the -program **must be marked as an executable** and the **first line of the program -must be** `#!/usr/bin/env python3`. To mark a program as executable run -`chmod +x PROGRAM_NAME.py`. All of the demo programs are already marked as -executable and already have `#!/usr/bin/env python3` so you should be fine, we -only mention it so you know to do these things when writing your own programs. - -## Command Line -To run one of the demo programs from the command line, cd to the directory and -run the program via `./PROGRAM_NAME.py`. Example: -``` -$ cd ev3dev-lang-python/demo/R3PTAR/ -$ ./r3ptar.py -``` -## Brickman -To run one of the demo programs from Brickman, select the program in the -File Browser. - -## Demo Programs -### BALANC3R - -Laurens Valk's BALANC3R - This robot uses the gyro sensor to balance on two -wheels. Use the IR remote to control BALANC3R - -* http://robotsquare.com/2014/07/01/tutorial-ev3-self-balancing-robot/ - -### EV3D4 - -* http://www.lego.com/en-us/mindstorms/build-a-robot/ev3d4 -* EV3D4RemoteControl - Use the IR remote to control EV3D4 -* EV3D4WebControl - Use a web interface to control EV3D4. There is a desktop version and a mobile version, both support touchscreen so you can drive via your smartphone. The web server will listen on port 8000 so go to http://x.x.x.x:8000/ - -### EXPLOR3R - -Lauren Valk's EXPLOR3R - -* http://robotsquare.com/2015/10/06/explor3r-building-instructions/ - -### MINDCUB3R - -David Gilday's MINDCUB3R - -* http://mindcuber.com/ - -### TRACK3R - -A basic example of Object Oriented programming where there is a base TRACK3R -class with child classes for the various permutations of TRACK3R - -* http://www.lego.com/en-us/mindstorms/build-a-robot/track3r -* TRACK3R.py -* TRACK3RWithBallShooter -* TRACK3RWithClaw -* TRACK3RWithSpinner diff --git a/demo/TRACK3R/TRACK3R.py b/demo/TRACK3R/TRACK3R.py deleted file mode 100644 index 31c1907..0000000 --- a/demo/TRACK3R/TRACK3R.py +++ /dev/null @@ -1,66 +0,0 @@ -#!/usr/bin/env python3 - -import logging -import sys -from ev3dev.auto import OUTPUT_A, OUTPUT_B, OUTPUT_C, OUTPUT_D -from ev3dev.helper import RemoteControlledTank, MediumMotor - -log = logging.getLogger(__name__) - - -class TRACK3R(RemoteControlledTank): - """ - Base class for all TRACK3R variations. The only difference in the child - classes are in how the medium motor is handled. - - To enable the medium motor toggle the beacon button on the EV3 remote. - """ - - def __init__(self, medium_motor, left_motor, right_motor): - RemoteControlledTank.__init__(self, left_motor, right_motor) - self.medium_motor = MediumMotor(medium_motor) - - if not self.medium_motor.connected: - log.error("%s is not connected" % self.medium_motor) - sys.exit(1) - - self.medium_motor.reset() - - -class TRACK3RWithBallShooter(TRACK3R): - - def __init__(self, medium_motor=OUTPUT_A, left_motor=OUTPUT_B, right_motor=OUTPUT_C): - TRACK3R.__init__(self, medium_motor, left_motor, right_motor) - self.remote.on_beacon = self.fire_ball - - def fire_ball(self, state): - if state: - self.medium_motor.run_to_rel_pos(speed_sp=400, position_sp=3*360) - else: - self.medium_motor.stop() - - -class TRACK3RWithSpinner(TRACK3R): - - def __init__(self, medium_motor=OUTPUT_A, left_motor=OUTPUT_B, right_motor=OUTPUT_C): - TRACK3R.__init__(self, medium_motor, left_motor, right_motor) - self.remote.on_beacon = self.spinner - - def spinner(self, state): - if state: - self.medium_motor.run_forever(speed_sp=50) - else: - self.medium_motor.stop() - - -class TRACK3RWithClaw(TRACK3R): - - def __init__(self, medium_motor=OUTPUT_A, left_motor=OUTPUT_B, right_motor=OUTPUT_C): - TRACK3R.__init__(self, medium_motor, left_motor, right_motor) - self.remote.on_beacon = self.move_claw - - def move_claw(self, state): - if state: - self.medium_motor.run_to_rel_pos(speed_sp=200, position_sp=-75) - else: - self.medium_motor.run_to_rel_pos(speed_sp=200, position_sp=75) diff --git a/demo/TRACK3R/TRACK3RWithBallShooter b/demo/TRACK3R/TRACK3RWithBallShooter deleted file mode 100755 index 7d6896c..0000000 --- a/demo/TRACK3R/TRACK3RWithBallShooter +++ /dev/null @@ -1,13 +0,0 @@ -#!/usr/bin/env python3 - -import logging -from TRACK3R import TRACK3RWithBallShooter - -logging.basicConfig(level=logging.DEBUG, - format='%(asctime)s %(levelname)5s: %(message)s') -log = logging.getLogger(__name__) - -log.info("Starting TRACK3RWithBallShooter") -tracker = TRACK3RWithBallShooter() -tracker.main() -log.info("Exiting TRACK3RWithBallShooter") diff --git a/demo/TRACK3R/TRACK3RWithClaw b/demo/TRACK3R/TRACK3RWithClaw deleted file mode 100755 index 965a39b..0000000 --- a/demo/TRACK3R/TRACK3RWithClaw +++ /dev/null @@ -1,13 +0,0 @@ -#!/usr/bin/env python3 - -import logging -from TRACK3R import TRACK3RWithClaw - -logging.basicConfig(level=logging.DEBUG, - format='%(asctime)s %(levelname)5s: %(message)s') -log = logging.getLogger(__name__) - -log.info("Starting TRACK3RWithClaw") -tracker = TRACK3RWithClaw() -tracker.main() -log.info("Exiting TRACK3RWithClaw") diff --git a/demo/TRACK3R/TRACK3RWithSpinner b/demo/TRACK3R/TRACK3RWithSpinner deleted file mode 100755 index 8e2e521..0000000 --- a/demo/TRACK3R/TRACK3RWithSpinner +++ /dev/null @@ -1,13 +0,0 @@ -#!/usr/bin/env python3 - -import logging -from TRACK3R import TRACK3RWithSpinner - -logging.basicConfig(level=logging.DEBUG, - format='%(asctime)s %(levelname)5s: %(message)s') -log = logging.getLogger(__name__) - -log.info("Starting TRACK3RWithSpinner") -tracker = TRACK3RWithSpinner() -tracker.main() -log.info("Exiting TRACK3RWithSpinner") diff --git a/demo/misc/leds.py b/demo/misc/leds.py deleted file mode 100755 index c623373..0000000 --- a/demo/misc/leds.py +++ /dev/null @@ -1,82 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -# ------------------------------------------------------------------------------ -# Copyright (c) 2015 Eric Pascual -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NON INFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. -# ----------------------------------------------------------------------------- - -""" This demo illustrates how to use the two red-green LEDs of the EV3 brick. -""" - -import time -import math - -from ev3dev.ev3 import Leds - -print(__doc__.lstrip()) - -print('saving current LEDs state') - -# save current state -saved_state = [led.brightness_pct for led in Leds.LEFT + Leds.RIGHT] - -Leds.all_off() -time.sleep(1) - -# cycle LEDs like a traffic light -print('traffic light') -for _ in range(3): - for color in (Leds.GREEN, Leds.YELLOW, Leds.RED): - for group in (Leds.LEFT, Leds.RIGHT): - Leds.set_color(group, color) - time.sleep(0.5) - -Leds.all_off() -time.sleep(0.5) - -# blink LEDs from side to side now -print('side to side') -for _ in range(3): - for led in (Leds.red_left, Leds.red_right, Leds.green_left, Leds.green_right): - led.brightness_pct = 100 - time.sleep(0.5) - led.brightness_pct = 0 - -Leds.all_off() -time.sleep(0.5) - -# continuous mix of colors -print('colors fade') -for i in range(360): - rd = math.radians(10 * i) - Leds.red_left.brightness_pct = .5 * (1 + math.cos(rd)) - Leds.green_left.brightness_pct = .5 * (1 + math.sin(rd)) - Leds.red_right.brightness_pct = .5 * (1 + math.sin(rd)) - Leds.green_right.brightness_pct = .5 * (1 + math.cos(rd)) - time.sleep(0.05) - -Leds.all_off() -time.sleep(0.5) - -print('restoring initial LEDs state') -for led, level in zip(Leds.RIGHT + Leds.LEFT, saved_state) : - led.brightness_pct = level - diff --git a/demo/misc/snd/r2d2.wav b/demo/misc/snd/r2d2.wav deleted file mode 100644 index 8c82a81..0000000 Binary files a/demo/misc/snd/r2d2.wav and /dev/null differ diff --git a/docs/button.rst b/docs/button.rst new file mode 100644 index 0000000..037dd8d --- /dev/null +++ b/docs/button.rst @@ -0,0 +1,19 @@ +Button +====== + +.. autoclass:: ev3dev2.button.Button + :members: + :inherited-members: + + .. rubric:: Event handlers + + These will be called when state of the corresponding button is changed: + + .. py:data:: on_up + .. py:data:: on_down + .. py:data:: on_left + .. py:data:: on_right + .. py:data:: on_enter + .. py:data:: on_backspace + + .. rubric:: Member functions and properties \ No newline at end of file diff --git a/docs/conf.py b/docs/conf.py index 2153039..b9e9a8b 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -15,28 +15,23 @@ import sys import os -import shlex +import subprocess +import sphinx_bootstrap_theme +from recommonmark.parser import CommonMarkParser +from recommonmark.transform import AutoStructify sys.path.append(os.path.join(os.path.dirname(__file__), '..')) -from git_version import git_version - -on_rtd = os.environ.get('READTHEDOCS', None) == 'True' - -if on_rtd: - import pip - pip.main(['install', 'sphinx_bootstrap_theme']) - -import sphinx_bootstrap_theme +from git_version import git_version # noqa: E402 # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. -#sys.path.insert(0, os.path.abspath('.')) +# sys.path.insert(0, os.path.abspath('.')) # -- General configuration ------------------------------------------------ # If your documentation needs a minimal Sphinx version, state it here. -#needs_sphinx = '1.0' +# needs_sphinx = '1.0' # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom @@ -49,13 +44,17 @@ # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] +source_parsers = { + '.md': CommonMarkParser, +} + # The suffix(es) of source filenames. # You can specify multiple suffix as a list of string: # source_suffix = ['.rst', '.md'] -source_suffix = '.rst' +source_suffix = ['.rst', '.md'] # The encoding of source files. -#source_encoding = 'utf-8-sig' +# source_encoding = 'utf-8-sig' # The master toctree document. master_doc = 'index' @@ -83,9 +82,9 @@ # There are two options for replacing |today|: either, you set today to some # non-false value, then it is used: -#today = '' +# today = '' # Else, today_fmt is used as the format for a strftime call. -#today_fmt = '%B %d, %Y' +# today_fmt = '%B %d, %Y' # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. @@ -93,32 +92,31 @@ # The reST default role (used for this markup: `text`) to use for all # documents. -#default_role = None +# default_role = None # If true, '()' will be appended to :func: etc. cross-reference text. -#add_function_parentheses = True +# add_function_parentheses = True # If true, the current module name will be prepended to all description # unit titles (such as .. function::). -#add_module_names = True +# add_module_names = True # If true, sectionauthor and moduleauthor directives will be shown in the # output. They are ignored by default. -#show_authors = False +# show_authors = False # The name of the Pygments (syntax highlighting) style to use. pygments_style = 'sphinx' # A list of ignored prefixes for module index sorting. -#modindex_common_prefix = [] +# modindex_common_prefix = [] # If true, keep warnings as "system message" paragraphs in the built documents. -#keep_warnings = False +# keep_warnings = False # If true, `todo` and `todoList` produce output, else they produce nothing. todo_include_todos = False - # -- Options for HTML output ---------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for @@ -126,35 +124,33 @@ html_theme_path = sphinx_bootstrap_theme.get_html_theme_path() html_theme = 'bootstrap' html_theme_options = { - 'bootswatch_theme': 'yeti', - 'navbar_links' : [ - ("GitHub", "https://github.com/rhempel/ev3dev-lang-python", True) - ] - } + 'bootswatch_theme': 'yeti', + 'navbar_links': [("GitHub", "https://github.com/ev3dev/ev3dev-lang-python", True)] +} # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the # documentation. -#html_theme_options = {} +# html_theme_options = {} # Add any paths that contain custom themes here, relative to this directory. -#html_theme_path = [] +# html_theme_path = [] # The name for this set of Sphinx documents. If None, it defaults to # " v documentation". -#html_title = None +# html_title = None # A shorter title for the navigation bar. Default is the same as html_title. -#html_short_title = None +# html_short_title = None # The name of an image file (relative to this directory) to place at the top # of the sidebar. -#html_logo = None +# html_logo = None # The name of an image file (within the static path) to use as favicon of the # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 # pixels large. -#html_favicon = None +# html_favicon = None # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, @@ -164,62 +160,62 @@ # Add any extra paths that contain custom files (such as robots.txt or # .htaccess) here, relative to this directory. These files are copied # directly to the root of the documentation. -#html_extra_path = [] +# html_extra_path = [] # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, # using the given strftime format. -#html_last_updated_fmt = '%b %d, %Y' +# html_last_updated_fmt = '%b %d, %Y' # If true, SmartyPants will be used to convert quotes and dashes to # typographically correct entities. -#html_use_smartypants = True +# html_use_smartypants = True # Custom sidebar templates, maps document names to template names. -#html_sidebars = {} +# html_sidebars = {} # Additional templates that should be rendered to pages, maps page names to # template names. -#html_additional_pages = {} +# html_additional_pages = {} # If false, no module index is generated. -#html_domain_indices = True +# html_domain_indices = True # If false, no index is generated. -#html_use_index = True +# html_use_index = True # If true, the index is split into individual pages for each letter. -#html_split_index = False +# html_split_index = False # If true, links to the reST sources are added to the pages. -#html_show_sourcelink = True +# html_show_sourcelink = True # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. -#html_show_sphinx = True +# html_show_sphinx = True # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. -#html_show_copyright = True +# html_show_copyright = True # If true, an OpenSearch description file will be output, and all pages will # contain a tag referring to it. The value of this option must be the # base URL from which the finished HTML is served. -#html_use_opensearch = '' +# html_use_opensearch = '' # This is the file name suffix for HTML files (e.g. ".xhtml"). -#html_file_suffix = None +# html_file_suffix = None # Language to be used for generating the HTML full-text search index. # Sphinx supports the following languages: # 'da', 'de', 'en', 'es', 'fi', 'fr', 'h', 'it', 'ja' # 'nl', 'no', 'pt', 'ro', 'r', 'sv', 'tr' -#html_search_language = 'en' +# html_search_language = 'en' # A dictionary with options for the search language support, empty by default. # Now only 'ja' uses this config value -#html_search_options = {'type': 'default'} +# html_search_options = {'type': 'default'} # The name of a javascript file (relative to the configuration directory) that # implements a search results scorer. If empty, the default will be used. -#html_search_scorer = 'scorer.js' +# html_search_scorer = 'scorer.js' # Output file base name for HTML help builder. htmlhelp_basename = 'python-ev3devdoc' @@ -227,60 +223,54 @@ # -- Options for LaTeX output --------------------------------------------- latex_elements = { -# The paper size ('letterpaper' or 'a4paper'). -#'papersize': 'letterpaper', + # The paper size ('letterpaper' or 'a4paper'). + # 'papersize': 'letterpaper', -# The font size ('10pt', '11pt' or '12pt'). -#'pointsize': '10pt', + # The font size ('10pt', '11pt' or '12pt'). + # 'pointsize': '10pt', -# Additional stuff for the LaTeX preamble. -#'preamble': '', + # Additional stuff for the LaTeX preamble. + # 'preamble': '', -# Latex figure (float) alignment -#'figure_align': 'htbp', + # Latex figure (float) alignment + # 'figure_align': 'htbp', } # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, # author, documentclass [howto, manual, or own class]). latex_documents = [ - (master_doc, 'python-ev3dev.tex', 'python-ev3dev Documentation', - 'Ralph Hempel et al', 'manual'), + (master_doc, 'python-ev3dev.tex', 'python-ev3dev Documentation', 'Ralph Hempel et al', 'manual'), ] # The name of an image file (relative to this directory) to place at the top of # the title page. -#latex_logo = None +# latex_logo = None # For "manual" documents, if this is true, then toplevel headings are parts, # not chapters. -#latex_use_parts = False +# latex_use_parts = False # If true, show page references after internal links. -#latex_show_pagerefs = False +# latex_show_pagerefs = False # If true, show URL addresses after external links. -#latex_show_urls = False +# latex_show_urls = False # Documents to append as an appendix to all manuals. -#latex_appendices = [] +# latex_appendices = [] # If false, no module index is generated. -#latex_domain_indices = True - +# latex_domain_indices = True # -- Options for manual page output --------------------------------------- # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). -man_pages = [ - (master_doc, 'python-ev3dev', 'python-ev3dev Documentation', - [author], 1) -] +man_pages = [(master_doc, 'python-ev3dev', 'python-ev3dev Documentation', [author], 1)] # If true, show URL addresses after external links. -#man_show_urls = False - +# man_show_urls = False # -- Options for Texinfo output ------------------------------------------- @@ -288,19 +278,33 @@ # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ - (master_doc, 'python-ev3dev', 'python-ev3dev Documentation', - author, 'python-ev3dev', 'One line description of project.', - 'Miscellaneous'), + (master_doc, 'python-ev3dev', 'python-ev3dev Documentation', author, 'python-ev3dev', + 'One line description of project.', 'Miscellaneous'), ] # Documents to append as an appendix to all manuals. -#texinfo_appendices = [] +# texinfo_appendices = [] # If false, no module index is generated. -#texinfo_domain_indices = True +# texinfo_domain_indices = True # How to display URL addresses: 'footnote', 'no', or 'inline'. -#texinfo_show_urls = 'footnote' +# texinfo_show_urls = 'footnote' # If true, do not generate a @detailmenu in the "Top" node's menu. -#texinfo_no_detailmenu = False +# texinfo_no_detailmenu = False + +autodoc_member_order = 'bysource' + +suppress_warnings = ['image.nonlocal_uri'] + +nitpick_ignore = [('py:class', 'ev3dev2.display.FbMem'), ('py:class', 'ev3dev2.button.ButtonBase'), ('py:class', 'int'), + ('py:class', 'float'), ('py:class', 'string'), ('py:class', 'iterable'), ('py:class', 'tuple'), + ('py:class', 'list'), ('py:exc', 'ValueError')] + + +def setup(app): + app.add_config_value('recommonmark_config', { + 'enable_eval_rst': True, + }, True) + app.add_transform(AutoStructify) diff --git a/docs/console.rst b/docs/console.rst new file mode 100644 index 0000000..643921b --- /dev/null +++ b/docs/console.rst @@ -0,0 +1,153 @@ +Console +======= + +.. autoclass:: ev3dev2.console.Console + :members: + +Examples: + +.. code-block:: py + + #!/usr/bin/env micropython + from ev3dev2.console import Console + + # create a Console instance, which uses the default font + console = Console() + + # reset the console to clear it, home the cursor at 1,1, and then turn off the cursor + console.reset_console() + + # display 'Hello World!' at row 5, column 1 in inverse, but reset the EV3 LCD console first + console.text_at('Hello World!', column=1, row=5, reset_console=True, inverse=True) + +.. code-block:: py + + #!/usr/bin/env micropython + from time import sleep + from ev3dev2.sensor import INPUT_1, INPUT_2, INPUT_3 + from ev3dev2.console import Console + from ev3dev2.sensor.lego import GyroSensor, ColorSensor + + console = Console() + gyro = GyroSensor(INPUT_1) + gyro.mode = GyroSensor.MODE_GYRO_ANG + color_sensor_left = ColorSensor(INPUT_2) + color_sensor_right = ColorSensor(INPUT_3) + + # show the gyro angle and reflected light intensity for both of our color sensors + while True: + angle = gyro.angle + left = color_sensor_left.reflected_light_intensity + right = color_sensor_right.reflected_light_intensity + + # show angle; in inverse color when pointing at 0 + console.text_at("G: %03d" % (angle), column=5, row=1, reset_console=True, inverse=(angle == 0)) + + # show light intensity values; in inverse when 'dark' + console.text_at("L: %02d" % (left), column=0, row=3, reset_console=False, inverse=(left < 10)) + console.text_at("R: %02d" % (right), column=10, row=3, reset_console=False, inverse=(right < 10)) + + sleep(0.5) + +Console fonts +------------- + +The :py:class:`ev3dev2.console.Console` class displays text on the LCD console +using ANSI codes in various system console fonts. The system console fonts are +located in `/usr/share/consolefonts`. + +Font filenames consist of the codeset, font face and font size. The codeset +specifies the characters supported. The font face determines the look of the +font. Each font face is available in multiple sizes. + +For Codeset information, see +``. + +Note: `Terminus` fonts are "thinner"; `TerminusBold` and `VGA` offer more +contrast on the LCD console and are thus more readable; the `TomThumb` font is +too small to read! + +Depending on the font used, the EV3 LCD console will support various maximum +rows and columns, as follows for the `Lat15` fonts. See +`utils/console_fonts.py` to discover fonts and their resulting rows/columns. +These fonts are listed in larger-to-smaller size order: + ++----------+------------+--------------------------------+ +| LCD Rows | LCD Columns| Font | ++==========+============+================================+ +| 4 | 11 | Lat15-Terminus32x16.psf.gz | ++----------+------------+--------------------------------+ +| 4 | 11 | Lat15-TerminusBold32x16.psf.gz | ++----------+------------+--------------------------------+ +| 4 | 11 | Lat15-VGA28x16.psf.gz | ++----------+------------+--------------------------------+ +| 4 | 11 | Lat15-VGA32x16.psf.gz | ++----------+------------+--------------------------------+ +| 4 | 12 | Lat15-Terminus28x14.psf.gz | ++----------+------------+--------------------------------+ +| 4 | 12 | Lat15-TerminusBold28x14.psf.gz | ++----------+------------+--------------------------------+ +| 5 | 14 | Lat15-Terminus24x12.psf.gz | ++----------+------------+--------------------------------+ +| 5 | 14 | Lat15-TerminusBold24x12.psf.gz | ++----------+------------+--------------------------------+ +| 5 | 16 | Lat15-Terminus22x11.psf.gz | ++----------+------------+--------------------------------+ +| 5 | 16 | Lat15-TerminusBold22x11.psf.gz | ++----------+------------+--------------------------------+ +| 6 | 17 | Lat15-Terminus20x10.psf.gz | ++----------+------------+--------------------------------+ +| 6 | 17 | Lat15-TerminusBold20x10.psf.gz | ++----------+------------+--------------------------------+ +| 7 | 22 | Lat15-Fixed18.psf.gz | ++----------+------------+--------------------------------+ +| 8 | 22 | Lat15-Fixed15.psf.gz | ++----------+------------+--------------------------------+ +| 8 | 22 | Lat15-Fixed16.psf.gz | ++----------+------------+--------------------------------+ +| 8 | 22 | Lat15-Terminus16.psf.gz | ++----------+------------+--------------------------------+ +| 8 | 22 | Lat15-TerminusBold16.psf.gz | ++----------+------------+--------------------------------+ +| 8 | 22 | Lat15-TerminusBoldVGA16.psf.gz | ++----------+------------+--------------------------------+ +| 8 | 22 | Lat15-VGA16.psf.gz | ++----------+------------+--------------------------------+ +| 9 | 22 | Lat15-Fixed13.psf.gz | ++----------+------------+--------------------------------+ +| 9 | 22 | Lat15-Fixed14.psf.gz | ++----------+------------+--------------------------------+ +| 9 | 22 | Lat15-Terminus14.psf.gz | ++----------+------------+--------------------------------+ +| 9 | 22 | Lat15-TerminusBold14.psf.gz | ++----------+------------+--------------------------------+ +| 9 | 22 | Lat15-TerminusBoldVGA14.psf.gz | ++----------+------------+--------------------------------+ +| 9 | 22 | Lat15-VGA14.psf.gz | ++----------+------------+--------------------------------+ +| 10 | 29 | Lat15-Terminus12x6.psf.gz | ++----------+------------+--------------------------------+ +| 16 | 22 | Lat15-VGA8.psf.gz | ++----------+------------+--------------------------------+ +| 21 | 44 | Lat15-TomThumb4x6.psf.gz | ++----------+------------+--------------------------------+ + +Example: + +.. code-block:: py + + #!/usr/bin/env micropython + from ev3dev2.console import Console + + # create a Console instance, which uses the default font + console = Console() + + # change the console font and reset the console to clear it and turn off the cursor + console.set_font('Lat15-TerminusBold16.psf.gz', True) + + # compute the middle of the console + mid_col = console.columns // 2 + mid_row = console.rows // 2 + + # display 'Hello World!' in the center of the LCD console + console.text_at('Hello World!', column=mid_col, row=mid_row, alignment="C") diff --git a/docs/display.rst b/docs/display.rst new file mode 100644 index 0000000..943286f --- /dev/null +++ b/docs/display.rst @@ -0,0 +1,30 @@ +Display +======= + +.. autoclass:: ev3dev2.display.Display + :members: + :show-inheritance: + + +Bitmap fonts +------------ + +The :py:class:`ev3dev2.display.Display` class allows to write text on the LCD using python +imaging library (PIL) interface (see description of the ``text()`` method +`here `_). +The ``ev3dev2.fonts`` module contains bitmap fonts in PIL format that should +look good on a tiny EV3 screen: + +.. code-block:: py + + import ev3dev2.fonts as fonts + display.draw.text((10,10), 'Hello World!', font=fonts.load('luBS14')) + +.. autofunction:: ev3dev2.fonts.available + +.. autofunction:: ev3dev2.fonts.load + +The following image lists all available fonts. The grid lines correspond +to EV3 screen size: + +.. image:: _static/fonts.png diff --git a/docs/faq.rst b/docs/faq.rst index 8d2d630..2033729 100644 --- a/docs/faq.rst +++ b/docs/faq.rst @@ -1,20 +1,81 @@ Frequently-Asked Questions ========================== -My script works when launched as ``python3 script.py`` but exits immediately or throws an error when launched from Brickman or as ``./script.py`` -------------------------------------------------------------------------------------------------------------------------------------------------- +Q: Why does my Python program exit quickly or immediately throw an error? + A: This may occur if your file includes Windows-style line endings + (CRLF--carriage-return line-feed), which are often inserted by editors on + Windows. To resolve this issue, open an SSH session and run the following + command, replacing ```` with the name of the Python file you're + using: -This may occur if your file includes Windows-style line endings, which are often -inserted by editors on Windows. To resolve this issue, open an SSH session and -run the following command, replacing ```` with the name of the Python file -you're using: + .. code:: shell -.. code:: shell + sed -i 's/\r//g' - sed -i 's/\r//g' + This will fix it for the copy of the file on the brick, but if you plan to edit + it again from Windows, you should configure your editor to use Unix-style + line endings (LF--line-feed). For PyCharm, you can find a guide on doing this + `here `_. + Most other editors have similar options; there may be an option for it in the + status bar at the bottom of the window or in the menu bar at the top. -This will fix it for the copy of the file on the brick, but if you plan to edit -it again from Windows you should configure your editor to use Unix-style endings. -For PyCharm, you can find a guide on doing this `here `_. -Most other editors have similar options; there may be an option for it in the -status bar at the bottom of the window or in the menu bar at the top. +Q: Where can I learn more about the ev3dev operating system? + A: `ev3dev.org`_ is a great resource for finding guides and tutorials on + using ev3dev, straight from the maintainers. + +Q: How can I request support on the ev3dev2 Python library? + A: If you are having trouble using this library, please open an issue + at `our Issues tracker`_ so that we can help you. When opening an + issue, make sure to include as much information as possible about + what you are trying to do and what you have tried. The issue template + is in place to guide you through this process. + +Q: How can I upgrade the library on my EV3? + A: You can upgrade this library from an Internet-connected EV3 with an + SSH shell as follows. Make sure to type the password + (the default is ``maker``) when prompted. + + .. code-block:: bash + + sudo apt-get update + sudo apt-get install --only-upgrade python3-ev3dev2 micropython-ev3dev2 + +Q: Are there other useful Python modules to use on the EV3? + A: The Python language has a `package repository`_ where you can find + libraries that others have written, including the `latest version of + this package`_. + +Q: What compatibility issues are there with the different versions of Python? + A: Some versions of the ev3dev_ distribution come with + `Python 2.x`_, `Python 3.x`_, and `micropython`_ installed, + but this library is compatible only with Python 3 and micropython. + +.. _ev3dev: http://ev3dev.org +.. _ev3dev.org: ev3dev_ +.. _Getting Started: ev3dev-getting-started_ +.. _ev3dev Getting Started guide: ev3dev-getting-started_ +.. _ev3dev-getting-started: http://www.ev3dev.org/docs/getting-started/ +.. _upgrade the kernel before continuing: http://www.ev3dev.org/docs/tutorials/upgrading-ev3dev/ +.. _detailed instructions for USB connections: ev3dev-usb-internet_ +.. _via an SSH connection: http://www.ev3dev.org/docs/tutorials/connecting-to-ev3dev-with-ssh/ +.. _ev3dev-usb-internet: http://www.ev3dev.org/docs/tutorials/connecting-to-the-internet-via-usb/ +.. _our Read the Docs page: http://python-ev3dev.readthedocs.org/en/ev3dev-stretch/ +.. _ev3python.com: http://ev3python.com/ +.. _FAQ: http://python-ev3dev.readthedocs.io/en/ev3dev-stretch/faq.html +.. _our FAQ page: FAQ_ +.. _our Issues tracker: https://github.com/ev3dev/ev3dev-lang-python/issues +.. _EXPLOR3R: demo-robot_ +.. _demo-robot: http://robotsquare.com/2015/10/06/explor3r-building-instructions/ +.. _robot-square: http://robotsquare.com/ +.. _Python 2.x: python2_ +.. _python2: https://docs.python.org/2/ +.. _Python 3.x: python3_ +.. _python3: https://docs.python.org/3/ +.. _package repository: pypi_ +.. _pypi: https://pypi.python.org/pypi +.. _latest version of this package: pypi-python-ev3dev_ +.. _pypi-python-ev3dev: https://pypi.python.org/pypi/python-ev3dev2 +.. _ev3dev Visual Studio Code extension: https://github.com/ev3dev/vscode-ev3dev-browser +.. _Python + VSCode introduction tutorial: https://github.com/ev3dev/vscode-hello-python +.. _nano: http://www.ev3dev.org/docs/tutorials/nano-cheat-sheet/ +.. _Micropython: http://python-ev3dev.readthedocs.io/en/ev3dev-stretch/micropython.html diff --git a/docs/index.rst b/docs/index.rst index eabecfd..1d13ed6 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -1,8 +1,3 @@ -.. python-ev3dev documentation master file, created by - sphinx-quickstart on Sat Oct 31 20:38:27 2015. - You can adapt this file completely to your liking, but it should at least - contain the root `toctree` directive. - .. include:: ../README.rst .. rubric:: Contents @@ -10,14 +5,8 @@ .. toctree:: :maxdepth: 3 + micropython + upgrading-to-stretch spec rpyc faq - -Indices and tables -================== - -* :ref:`genindex` -* :ref:`modindex` -* :ref:`search` - diff --git a/docs/leds.rst b/docs/leds.rst new file mode 100644 index 0000000..655e732 --- /dev/null +++ b/docs/leds.rst @@ -0,0 +1,73 @@ +.. _led-classes: + +Leds +==== + +.. autoclass:: ev3dev2.led.Led + :members: + +.. autoclass:: ev3dev2.led.Leds + :members: + +LED group and color names +------------------------- + +.. rubric:: EV3 platform + +Led groups: + +- ``LEFT`` +- ``RIGHT`` + +Colors: + +- ``BLACK`` +- ``RED`` +- ``GREEN`` +- ``AMBER`` +- ``ORANGE`` +- ``YELLOW`` + +.. rubric:: BrickPI platform + +Led groups: + +- ``LED1`` +- ``LED2`` + +Colors: + +- ``BLACK`` +- ``BLUE`` + +.. rubric:: BrickPI3 platform + +Led groups: + +- ``LED`` + +Colors: + +- ``BLACK`` +- ``BLUE`` + +.. rubric:: PiStorms platform + +Led groups: + +- ``LEFT`` +- ``RIGHT`` + +Colors: + +- ``BLACK`` +- ``RED`` +- ``GREEN`` +- ``BLUE`` +- ``YELLOW`` +- ``CYAN`` +- ``MAGENTA`` + +.. rubric:: EVB platform + +None. diff --git a/docs/micropython.md b/docs/micropython.md new file mode 100644 index 0000000..a3d8d60 --- /dev/null +++ b/docs/micropython.md @@ -0,0 +1,54 @@ +# Using python-ev3dev with MicroPython + +The core modules of this library are shipped as a module for [MicroPython](https://micropython.org/), +which is faster to load and run on the EV3. If your app only requires functionality supported on +MicroPython, we recommend you run your code with it for improved performance. + +## Module support + +```eval_rst +============================== ================= +Module Support status +============================== ================= +`ev3dev2.button` ️️✔️ +`ev3dev2.console` ✔️️ +`ev3dev2.control` [1]_ ⚠️ +`ev3dev2.display` [2]_ ❌ +`ev3dev2.fonts` [3]_ ❌ +`ev3dev2.led` ✔️ +`ev3dev2.motor` ✔️ +`ev3dev2.port` ✔️ +`ev3dev2.power` ✔️ +`ev3dev2.sensor.*` ✔️ +`ev3dev2.sound` ✔️ +`ev3dev2.unit` ✔️ +`ev3dev2.wheel` ✔️ +============================== ================= + +.. [1] Untested/low-priority, but some of it might work. +.. [2] ``ev3dev2.display`` isn't implemented. Use ``ev3dev2.console`` for text-only, using ANSI codes to the EV3 LCD console. +.. [3] ``ev3dev2.console`` supports the system fonts, but the fonts for ``ev3dev2.display`` do not work. +``` + +## Differences from standard Python (CPython) + +See [the MicroPython differences page](http://docs.micropython.org/en/latest/genrst/index.html) for language information. + +### Shebang + +You should modify the first line of your scripts to replace "python3" with "micropython": + +```python +#!/usr/bin/env micropython +``` + +### Running from the command line + +If you previously would have typed `python3 foo.py`, you should now type `micropython foo.py`. + +If you are running programs via an SSH shell to your EV3, use the following command line to +prevent Brickman from interfering: + +```shell +brickrun -- ./program.py +``` diff --git a/docs/motors.rst b/docs/motors.rst index 7017160..beb399f 100644 --- a/docs/motors.rst +++ b/docs/motors.rst @@ -1,36 +1,129 @@ Motor classes ============= -.. currentmodule:: ev3dev.core +.. currentmodule:: ev3dev2.motor -Tacho motor ------------ +.. contents:: :local: + +.. _motor-unit-classes: + +Units +----- + +Most methods which run motors will accept a ``speed`` argument. While this can +be provided as an integer which will be interpreted as a percentage of max +speed, you can also specify an instance of any of the following classes, each +of which represents a different unit system: + +.. autoclass:: SpeedValue +.. autoclass:: SpeedPercent +.. autoclass:: SpeedNativeUnits +.. autoclass:: SpeedRPS +.. autoclass:: SpeedRPM +.. autoclass:: SpeedDPS +.. autoclass:: SpeedDPM + +Example: + +.. code:: python + + from ev3dev2.motor import SpeedRPM + + # later... + + # rotates the motor at 200 RPM (rotations-per-minute) for five seconds. + my_motor.on_for_seconds(SpeedRPM(200), 5) + + + +Common motors +------------- + +Tacho Motor (``Motor``) +~~~~~~~~~~~~~~~~~~~~~~~ .. autoclass:: Motor :members: + :show-inheritance: Large EV3 Motor ---------------- +~~~~~~~~~~~~~~~ .. autoclass:: LargeMotor :members: :show-inheritance: Medium EV3 Motor ----------------- +~~~~~~~~~~~~~~~~ .. autoclass:: MediumMotor :members: :show-inheritance: +Additional motors +----------------- + DC Motor --------- +~~~~~~~~ .. autoclass:: DcMotor :members: + :show-inheritance: Servo Motor ------------ +~~~~~~~~~~~ .. autoclass:: ServoMotor :members: + :show-inheritance: + +Actuonix L12 50 Linear Servo Motor +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. autoclass:: ActuonixL1250Motor + :members: + :show-inheritance: + +Actuonix L12 100 Linear Servo Motor +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. autoclass:: ActuonixL12100Motor + :members: + :show-inheritance: + +Multiple-motor groups +--------------------- + +Motor Set +~~~~~~~~~ + +.. autoclass:: MotorSet + :members: + +Move Tank +~~~~~~~~~ + +.. autoclass:: MoveTank + :members: + :show-inheritance: + +Move Steering +~~~~~~~~~~~~~ + +.. autoclass:: MoveSteering + :members: + :show-inheritance: + +Move Joystick +~~~~~~~~~~~~~ + +.. autoclass:: MoveJoystick + :members: + :show-inheritance: + +Move Differential +~~~~~~~~~~~~~~~~~ + +.. autoclass:: MoveDifferential + :members: + :show-inheritance: diff --git a/docs/other.rst b/docs/other.rst index 4583ce9..e7dc2bf 100644 --- a/docs/other.rst +++ b/docs/other.rst @@ -1,133 +1,34 @@ +:orphan: + Other classes ============= -.. currentmodule:: ev3dev.core - -Remote Control --------------- - -.. autoclass:: RemoteControl - :members: - :inherited-members: - - .. rubric:: Event handlers - - These will be called when state of the corresponding button is changed: - - .. py:data:: on_red_up - .. py:data:: on_red_down - .. py:data:: on_blue_up - .. py:data:: on_blue_down - .. py:data:: on_beacon - - .. rubric:: Member functions and properties - -Beacon Seeker -------------- - -.. autoclass:: BeaconSeeker - :members: - :inherited-members: - Button ------ -.. autoclass:: ev3dev.ev3.Button - :members: - :inherited-members: - - .. rubric:: Event handlers - - These will be called when state of the corresponding button is changed: - - .. py:data:: on_up - .. py:data:: on_down - .. py:data:: on_left - .. py:data:: on_right - .. py:data:: on_enter - .. py:data:: on_backspace - - .. rubric:: Member functions and properties +See :py:class:`ev3dev2.button.Button`. Leds ---- -.. autoclass:: Led - :members: - -.. autoclass:: ev3dev.ev3.Leds - :members: - - .. rubric:: EV3 platform - - Led groups: - - .. py:data:: LEFT - .. py:data:: RIGHT - - Colors: - - .. py:data:: RED - .. py:data:: GREEN - .. py:data:: AMBER - .. py:data:: ORANGE - .. py:data:: YELLOW - - .. rubric:: BrickPI platform - - Led groups: - - .. py:data:: LED1 - .. py:data:: LED2 - - Colors: - - .. py:data:: BLUE +See :ref:`led-classes`. Power Supply ------------ -.. autoclass:: PowerSupply - :members: +See :py:class:`ev3dev2.power.PowerSupply`. Sound ----- -.. autoclass:: Sound - :members: - -Screen ------- - -.. autoclass:: Screen - :members: - :show-inheritance: - -Bitmap fonts -^^^^^^^^^^^^ - -The :py:class:`Screen` class allows to write text on the LCD using python -imaging library (PIL) interface (see description of the ``text()`` method -`here `_). -The ``ev3dev.fonts`` module contains bitmap fonts in PIL format that should -look good on a tiny EV3 screen: - -.. code-block:: py - - import ev3dev.fonts as fonts - screen.draw.text((10,10), 'Hello World!', font=fonts.load('luBS14')) - -.. autofunction:: ev3dev.fonts.available - -.. autofunction:: ev3dev.fonts.load +See :py:class:`ev3dev2.sound.Sound`. -The following image lists all available fonts. The grid lines correspond -to EV3 screen size: +Display +------- -.. image:: _static/fonts.png +See :py:class:`ev3dev2.display.Display`. Lego Port --------- -.. autoclass:: LegoPort - :members: +See :py:class:`ev3dev2.port.LegoPort`. \ No newline at end of file diff --git a/docs/port-names.rst b/docs/port-names.rst new file mode 100644 index 0000000..039a022 --- /dev/null +++ b/docs/port-names.rst @@ -0,0 +1,38 @@ +.. _port-names: + +Port names +========== + +Classes such as :py:class:`ev3dev2.motor.Motor` and those based on +:py:class:`ev3dev2.sensor.Sensor` accept parameters to specify which port the +target device is connected to. This parameter is typically caled ``address``. + +The following constants are available on all platforms: + +.. rubric:: Output + +- ``ev3dev2.motor.OUTPUT_A`` +- ``ev3dev2.motor.OUTPUT_B`` +- ``ev3dev2.motor.OUTPUT_C`` +- ``ev3dev2.motor.OUTPUT_D`` + +.. rubric:: Input + +- ``ev3dev2.sensor.INPUT_1`` +- ``ev3dev2.sensor.INPUT_2`` +- ``ev3dev2.sensor.INPUT_3`` +- ``ev3dev2.sensor.INPUT_4`` + +Additionally, on BrickPi3, the ports of up to four stacked BrickPi's can be +referenced as `OUTPUT_E` through `OUTPUT_P` and `INPUT_5` through `INPUT_16`. + +.. rubric:: Example + +.. code-block:: python + + from ev3dev2.motor import LargeMotor, OUTPUT_A, OUTPUT_B + from ev3dev2.sensor import INPUT_1 + from ev3dev2.sensor.lego import TouchSensor + + m = LargeMotor(OUTPUT_A) + s = TouchSensor(INPUT_1) \ No newline at end of file diff --git a/docs/ports.rst b/docs/ports.rst new file mode 100644 index 0000000..92c5b6a --- /dev/null +++ b/docs/ports.rst @@ -0,0 +1,9 @@ +Lego Port +========= + +The `LegoPort` class is only needed when manually reconfiguring input/output +ports. This is necessary on the BrickPi but not other platforms, such as the +EV3. Most users can ignore this page. + +.. autoclass:: ev3dev2.port.LegoPort + :members: \ No newline at end of file diff --git a/docs/power-supply.rst b/docs/power-supply.rst new file mode 100644 index 0000000..0a92305 --- /dev/null +++ b/docs/power-supply.rst @@ -0,0 +1,5 @@ +Power Supply +============ + +.. autoclass:: ev3dev2.power.PowerSupply + :members: \ No newline at end of file diff --git a/docs/requirements.txt b/docs/requirements.txt new file mode 100644 index 0000000..1d58f15 --- /dev/null +++ b/docs/requirements.txt @@ -0,0 +1,4 @@ +docutils==0.14 +sphinx_bootstrap_theme +recommonmark +evdev \ No newline at end of file diff --git a/docs/rpyc.rst b/docs/rpyc.rst index 3984f78..46a41d6 100644 --- a/docs/rpyc.rst +++ b/docs/rpyc.rst @@ -1,87 +1,115 @@ -Working with ev3dev remotely using RPyC -======================================= +************** +RPyC on ev3dev +************** -RPyC_ (pronounced as are-pie-see), or Remote Python Call, is a transparent -python library for symmetrical remote procedure calls, clustering and -distributed-computing. RPyC makes use of object-proxying, a technique that -employs python’s dynamic nature, to overcome the physical boundaries between -processes and computers, so that remote objects can be manipulated as if they -were local. Here are simple steps you need to follow in order to install and -use RPyC with ev3dev: +`RPyC_ `_ (pronounced as are-pie-see) can be used to: +* run a python program on an ev3dev device that controls another ev3dev device. +This is more commonly known as daisy chaining. +* run a python program on your laptop that controls an ev3dev device. This can be +useful if your robot requires CPU intensive code that would be slow to run on the +EV3. A good example of this is a Rubik's cube solver, calculating the solution to +solve a Rubik's cube can be slow on an EV3. -1. Install RPyC both on the EV3 and on your desktop PC. For the EV3, enter the - following command at the command prompt (after you `connect with SSH`_): +For both of these scenarios you can use RPyC to control multiple remote ev3dev devices. - .. code-block:: shell - - sudo easy_install3 rpyc - On the desktop PC, it really depends on your operating system. In case it is - some flavor of linux, you should be able to do +Networking +========== +You will need IP connectivity between the device where your python code runs +(laptop, an ev3dev device, etc) and the remote ev3dev devices. Some common scenarios +might be: +* Multiple EV3s on the same WiFi network +* A laptop and an EV3 on the same WiFi network +* A bluetooth connection between two EV3s - .. code-block:: shell +The `ev3dev networking documentation `_ should get +you up and running in terms of networking connectivity. - sudo pip3 install rpyc - In case it is Windows, there is a win32 installer on the project's - `sourceforge page`_. Also, have a look at the `Download and Install`_ page - on their site. +Install +======= -2. Create file ``rpyc_server.sh`` with the following contents on the EV3: +1. RPyC is installed on ev3dev but we need to create a service that launches + ``rpyc_classic.py`` at bootup. `SSH `_ to your remote ev3dev devices and + cut-n-paste the following commands at the bash prompt. .. code-block:: shell - #!/bin/bash - python3 `which rpyc_classic.py` + echo "[Unit] + Description=RPyC Classic Service + After=multi-user.target - and make the file executable: + [Service] + Type=simple + ExecStart=/usr/bin/rpyc_classic.py - .. code-block:: shell + [Install] + WantedBy=multi-user.target" > rpyc-classic.service - chmod +x rpyc_server.sh + sudo cp rpyc-classic.service /lib/systemd/system/ + sudo systemctl daemon-reload + sudo systemctl enable rpyc-classic.service + sudo systemctl start rpyc-classic.service - Launch the created file either from SSH session (with - ``./rpyc_server.sh`` command), or from brickman. It should output something - like - .. code-block:: none +2. If you will be using an ev3dev device to control another ev3dev device you + can skip this step. If you will be using your desktop PC to control an ev3dev + device you must install RPyC on your desktop PC. How you install RPyC depends + on your operating system. For Linux you should be able to do: - INFO:SLAVE/18812:server started on [0.0.0.0]:18812 + .. code-block:: shell + + sudo apt-get install python3-rpyc - and keep running. + For Windows there is a win32 installer on the project's `sourceforge page`_. + Also, have a look at the `Download and Install`_ page on their site. -3. Now you are ready to connect to the RPyC server from your desktop PC. The - following python script should make a large motor connected to output port - ``A`` spin for a second. +Example +======= +We will run code on our laptop to control the remote ev3dev device with IP +address X.X.X.X. The goal is to have the LargeMotor connected to ``OUTPUT_A`` +run when the TouchSensor on ``INPUT_1`` is pressed. .. code-block:: py import rpyc - conn = rpyc.classic.connect('ev3dev') # host name or IP address of the EV3 - ev3 = conn.modules['ev3dev.ev3'] # import ev3dev.ev3 remotely - m = ev3.LargeMotor('outA') - m.run_timed(time_sp=1000, speed_sp=600) - -You can run scripts like this from any interactive python environment, like -ipython shell/notebook, spyder, pycharm, etc. - -Some *advantages* of using RPyC with ev3dev are: - -* It uses much less resources than running ipython notebook on EV3; RPyC server - is lightweight, and only requires an IP connection to the EV3 once set up (no - ssh required). -* The scripts you are working with are actually stored and edited on your - desktop PC, with your favorite editor/IDE. -* Some robots may need much more computational power than what EV3 can give - you. A notable example is the Rubics cube solver: there is an algorithm that - provides almost optimal solution (in terms of number of cube rotations), but - it takes more RAM than is available on EV3. With RPYC, you could run the - heavy-duty computations on your desktop. - -The most obvious *disadvantage* is latency introduced by network connection. -This may be a show stopper for robots where reaction speed is essential. - -.. _RPyC: http://rpyc.readthedocs.io/ -.. _sourceforge page: http://sourceforge.net/projects/rpyc/files/main -.. _Download and Install: http://rpyc.readthedocs.io/en/latest/install.html -.. _connect with SSH: http://www.ev3dev.org/docs/tutorials/connecting-to-ev3dev-with-ssh/ + + # Create a RPyC connection to the remote ev3dev device. + # Use the hostname or IP address of the ev3dev device. + # If this fails, verify your IP connectivty via ``ping X.X.X.X`` + conn = rpyc.classic.connect('X.X.X.X') + + # import ev3dev2 on the remote ev3dev device + ev3dev2_motor = conn.modules['ev3dev2.motor'] + ev3dev2_sensor = conn.modules['ev3dev2.sensor'] + ev3dev2_sensor_lego = conn.modules['ev3dev2.sensor.lego'] + + # Use the LargeMotor and TouchSensor on the remote ev3dev device + motor = ev3dev2_motor.LargeMotor(ev3dev2_motor.OUTPUT_A) + ts = ev3dev2_sensor_lego.TouchSensor(ev3dev2_sensor.INPUT_1) + + # If the TouchSensor is pressed, run the motor + while True: + ts.wait_for_pressed() + motor.run_forever(speed_sp=200) + + ts.wait_for_released() + motor.stop() + +Pros +==== +* RPyC is lightweight and only requires an IP connection (no ssh required). +* Some robots may need much more computational power than an EV3 can give + you. A notable example is the Rubik's cube solver. + +Cons +==== +* Latency will be introduced by the network connection. This may be a show stopper for robots where reaction speed is essential. +* RPyC is only supported by python, it is *NOT* supported by micropython + +References +========== +* `RPyC `_ +* `sourceforge page `_ +* `Download and Install `_ +* `connect with SSH `_ diff --git a/docs/sensors.rst b/docs/sensors.rst index a254b90..0c86881 100644 --- a/docs/sensors.rst +++ b/docs/sensors.rst @@ -1,27 +1,26 @@ Sensor classes ============== -Sensor ------- +.. contents:: :local: -This is the base class all the other sensor classes are derived from. +*Note:* If you are using a BrickPi rather than an EV3, you will need to manually +configure the ports before interacting with your sensors. See the example +`here `_. -.. currentmodule:: ev3dev.core -.. autoclass:: Sensor - :members: +Dedicated sensor classes +------------------------ -Special sensor classes ----------------------- +These classes derive from :py:class:`ev3dev2.sensor.Sensor` and provide helper functions +specific to the corresponding sensor type. Each provides sensible property +accessors for the main functionality of the sensor. -The classes derive from :py:class:`Sensor` and provide helper functions -specific to the corresponding sensor type. Each of the functions makes -sure the sensor is in the required mode and then returns the specified value. +.. -.. ~autogen doc-special-sensor-classes +.. currentmodule:: ev3dev2.sensor.lego Touch Sensor -######################## +############ .. autoclass:: TouchSensor :members: @@ -30,7 +29,7 @@ Touch Sensor Color Sensor -######################## +############ .. autoclass:: ColorSensor :members: @@ -39,7 +38,7 @@ Color Sensor Ultrasonic Sensor -######################## +################# .. autoclass:: UltrasonicSensor :members: @@ -48,7 +47,7 @@ Ultrasonic Sensor Gyro Sensor -######################## +########### .. autoclass:: GyroSensor :members: @@ -57,7 +56,7 @@ Gyro Sensor Infrared Sensor -######################## +############### .. autoclass:: InfraredSensor :members: @@ -66,7 +65,7 @@ Infrared Sensor Sound Sensor -######################## +############ .. autoclass:: SoundSensor :members: @@ -75,7 +74,7 @@ Sound Sensor Light Sensor -######################## +############ .. autoclass:: LightSensor :members: @@ -83,6 +82,19 @@ Light Sensor +Base "Sensor" +------------- + +This is the base class all the other sensor classes are derived from. You +generally want to use one of the other classes instead, but if your sensor +doesn't have a dedicated class, this is will let you interface with it as a +generic device. + +.. currentmodule:: ev3dev2.sensor + +.. autoclass:: Sensor + :members: + -.. ~autogen +.. diff --git a/docs/sound.rst b/docs/sound.rst new file mode 100644 index 0000000..7ea2bcb --- /dev/null +++ b/docs/sound.rst @@ -0,0 +1,5 @@ +Sound +===== + +.. autoclass:: ev3dev2.sound.Sound + :members: \ No newline at end of file diff --git a/docs/spec.rst b/docs/spec.rst index 8fa4880..9738ac8 100644 --- a/docs/spec.rst +++ b/docs/spec.rst @@ -1,17 +1,8 @@ API reference ============= -Each class in ev3dev module inherits from the base :py:class:`Device` class. - -.. autoclass:: ev3dev.core.Device - -.. autofunction:: ev3dev.core.list_device_names - -.. autofunction:: ev3dev.core.list_devices - -.. autofunction:: ev3dev.core.list_motors - -.. autofunction:: ev3dev.core.list_sensors +Device interfaces +----------------- .. rubric:: Contents: @@ -20,4 +11,28 @@ Each class in ev3dev module inherits from the base :py:class:`Device` class. motors sensors - other + button + leds + power-supply + sound + display + console + ports + port-names + wheels + + +Other APIs +---------- + +Each class in ev3dev module inherits from the base :py:class:`ev3dev2.Device` class. + +.. autoclass:: ev3dev2.Device + +.. autofunction:: ev3dev2.list_device_names + +.. autofunction:: ev3dev2.list_devices + +.. autofunction:: ev3dev2.motor.list_motors + +.. autofunction:: ev3dev2.sensor.list_sensors diff --git a/docs/sphinx3-build b/docs/sphinx3-build index 9c3e5eb..89b4cee 100755 --- a/docs/sphinx3-build +++ b/docs/sphinx3-build @@ -15,4 +15,3 @@ if __name__ == '__main__': sys.exit(make_main(sys.argv)) else: sys.exit(main(sys.argv)) - diff --git a/docs/upgrading-to-stretch.md b/docs/upgrading-to-stretch.md new file mode 100644 index 0000000..3e9ba82 --- /dev/null +++ b/docs/upgrading-to-stretch.md @@ -0,0 +1,55 @@ +# Upgrading from ev3dev-jessie (library v1) to ev3dev-stretch (library v2) + +With ev3dev-stretch, we have introduced some breaking changes that you must be aware of to get older scripts running with new features. + +**Scripts which worked on ev3dev-jessie are still supported and will continue to work as-is on Stretch.** However, if you want to use any of the new features we have introduced, you will need to switch to using version 2 of the python-ev3dev library. You can switch to version 2 by updating your import statements. + +## Updating import statements + +Previously, we recommended using one of the following as your `import` declaration: + +```python +import ev3dev.ev3 as ev3 +import ev3dev.brickpi as ev3 +import ev3dev.auto as ev3 +``` + +We have re-arranged the library to provide more control over what gets imported. For all platforms, you will now import from individual modules for things like sensors and motors, like this: + +```python +from ev3dev2.motor import Motor, OUTPUT_A +from ev3dev2.sensor.lego import TouchSensor, UltrasonicSensor +``` + +The platform (EV3, BrickPi, etc.) will now be automatically determined. + +You can omit import statements for modules you don't need, and add any additional ones that you do require. With this style of import, members are globally available by their name, so you would now refer to the Motor class as simply `Motor` rather than `ev3.Motor`. + +## Remove references to `connected` attribute + +In version 1 of the library, instantiating a device such as a motor or sensor would always succeed without an error. To see if the device connected successfully you would have to check the `connected` attribute. With the new version of the module, the constructor of device classes will throw an `ev3dev2.DeviceNotConnected` exception. You will need to remove any uses of the `connected` attribute. + +## `Screen` class has been renamed to `Display` + +To match the name used by LEGO's "EV3-G" graphical programming tools, we have renamed the `Screen` module to `Display`. + +## Reorganization of `RemoteControl`, `BeaconSeeker` and `InfraredSensor` + +The `RemoteControl` and `BeaconSeeker` classes have been removed; you will now use `InfraredSensor` for all purposes. + +Additionally, we have renamed many of the properties on the `InfraredSensor` class to make the meaning more obvious. Check out [the `InfraredSensor` documentation](sensors.html#infrared-sensor) for more info. + +## Re-designed `Sound` class + +The names and interfaces of some of the `Sound` class methods have changed. Check out [the `Sound` class docs](other.html#sound) for details. + +# Once you've adapted to breaking changes, check out the cool new features! + +```eval_rst +- New classes are available for coordinating motors: :py:class:`ev3dev2.motor.MotorSet`, :py:class:`ev3dev2.motor.MoveTank`, :py:class:`ev3dev2.motor.MoveSteering`, and :py:class:`ev3dev2.motor.MoveJoystick`. +- Classes representing a variety of motor speed units are available and accepted by many of the motor interfaces: see :ref:`motor-unit-classes`. +- Friendlier interfaces for operating motors and sensors: check out :py:meth:`ev3dev2.motor.Motor.on_for_rotations` and the other ``on_for_*`` methods on motors. +- Easier interactivity via buttons: each button now has ``wait_for_pressed``, ``wait_for_released`` and ``wait_for_bump`` +- Improved :py:class:`ev3dev2.sound.Sound` and :py:class:`ev3dev2.display.Display` interfaces +- New color conversion methods in :py:class:`ev3dev2.sensor.lego.ColorSensor` +``` diff --git a/docs/wheels.rst b/docs/wheels.rst new file mode 100644 index 0000000..b24a4b0 --- /dev/null +++ b/docs/wheels.rst @@ -0,0 +1,35 @@ +Wheels +====== + +All Wheel class units are in millimeters. The diameter and width for various lego wheels can be found at http://wheels.sariel.pl/ + +.. autoclass:: ev3dev2.wheel.Wheel + :members: + +EV3 Rim +~~~~~~~ + +.. autoclass:: ev3dev2.wheel.EV3Rim + :members: + :show-inheritance: + +EV3 Tire +~~~~~~~~ + +.. autoclass:: ev3dev2.wheel.EV3Tire + :members: + :show-inheritance: + +EV3 Education Set Rim +~~~~~~~~~~~~~~~~~~~~~ + +.. autoclass:: ev3dev2.wheel.EV3EducationSetRim + :members: + :show-inheritance: + +EV3 Education Set Tire +~~~~~~~~~~~~~~~~~~~~~~ + +.. autoclass:: ev3dev2.wheel.EV3EducationSetTire + :members: + :show-inheritance: diff --git a/ev3dev-lang b/ev3dev-lang deleted file mode 160000 index 008804f..0000000 --- a/ev3dev-lang +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 008804f5c28acd225f1b43057be2060d9965b2c1 diff --git a/ev3dev/GyroBalancer.py b/ev3dev/GyroBalancer.py deleted file mode 100644 index 4e97ce0..0000000 --- a/ev3dev/GyroBalancer.py +++ /dev/null @@ -1,400 +0,0 @@ -#!/usr/bin/env python3 -# The MIT License (MIT) -# -# Copyright (c) 2016 Laurens Valk (laurensvalk@gmail.com) -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in all -# copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -# SOFTWARE. - -""" -This is a class-based version of https://github.com/laurensvalk/segway -""" -import logging -import math -import time -from collections import deque -from ev3dev.auto import * -from ev3dev.helper import Tank - - -log = logging.getLogger(__name__) - - -######################################################################## -## -## File I/O functions -## -######################################################################## - -# Function for fast reading from sensor files -def FastRead(infile): - infile.seek(0) - return int(infile.read().decode().strip()) - - -# Function for fast writing to motor files -def FastWrite(outfile, value): - outfile.truncate(0) - outfile.write(str(int(value))) - outfile.flush() - - -# Function to set the duty cycle of the motors -def SetDuty(motorDutyFileHandle, duty): - # Clamp the value between -100 and 100 - duty = min(max(duty, -100), 100) - - # Apply the signal to the motor - FastWrite(motorDutyFileHandle, duty) - - -class GyroBalancer(Tank): - """ - Base class for a robot that stands on two wheels and uses a gyro sensor - to keep its balance. - """ - - def __init__(self, - gainGyroAngle, # For every radian (57 degrees) we lean forward, apply this amount of duty cycle - gainGyroRate, # For every radian/s we fall forward, apply this amount of duty cycle - gainMotorAngle, # For every radian we are ahead of the reference, apply this amount of duty cycle - gainMotorAngularSpeed, # For every radian/s drive faster than the reference value, apply this amount of duty cycle - gainMotorAngleErrorAccumulated, # For every radian x s of accumulated motor angle, apply this amount of duty cycle - left_motor=OUTPUT_D, - right_motor=OUTPUT_A): - Tank.__init__(self, left_motor, right_motor) - - # magic numbers - self.gainGyroAngle = gainGyroAngle - self.gainGyroRate = gainGyroRate - self.gainMotorAngle = gainMotorAngle - self.gainMotorAngularSpeed = gainMotorAngularSpeed - self.gainMotorAngleErrorAccumulated = gainMotorAngleErrorAccumulated - - # Sensor setup - self.gyro = GyroSensor() - self.gyro.mode = self.gyro.MODE_GYRO_RATE - self.touch = TouchSensor() - self.remote = RemoteControl(channel=1) - - if not self.remote.connected: - log.error("%s is not connected" % self.remote) - sys.exit(1) - - # Motor setup - self.left_motor.reset() - self.right_motor.reset() - self.left_motor.run_direct() - self.right_motor.run_direct() - - self.speed = 0 - self.steering = 0 - self.red_up = False - self.red_down = False - self.blue_up = False - self.blue_down = False - self.STEER_SPEED = 20 - self.remote.on_red_up = self.make_move('red_up') - self.remote.on_red_down = self.make_move('red_down') - self.remote.on_blue_up = self.make_move('blue_up') - self.remote.on_blue_down = self.make_move('blue_down') - - def make_move(self, button): - def move(state): - # button pressed - if state: - if button == 'red_up': - self.red_up = True - elif button == 'red_down': - self.red_down = True - elif button == 'blue_up': - self.blue_up = True - elif button == 'blue_down': - self.blue_down = True - - # button released - else: - if button == 'red_up': - self.red_up = False - elif button == 'red_down': - self.red_down = False - elif button == 'blue_up': - self.blue_up = False - elif button == 'blue_down': - self.blue_down = False - - # forward - if self.red_up and self.blue_up: - self.speed = self.STEER_SPEED - self.steering = 0 - - # backward - elif self.red_down and self.blue_down: - self.speed = -1 * self.STEER_SPEED - self.steering = 0 - - # turn sharp right - elif self.red_up and self.blue_down: - self.speed = 0 - self.steering = -1 * self.STEER_SPEED * 2 - - # turn right - elif self.red_up: - self.speed = 0 - self.steering = -1 * self.STEER_SPEED - - # turn sharp left - elif self.red_down and self.blue_up: - self.speed = 0 - self.steering = self.STEER_SPEED * 2 - - # turn left - elif self.blue_up: - self.speed = 0 - self.steering = self.STEER_SPEED - - else: - self.speed = 0 - self.steering = 0 - - # log.info("button %8s, state %5s, speed %d, steering %d" % (button, state, self.speed, self.steering)) - - return move - - def main(self): - - def shutdown(): - touchSensorValueRaw.close() - gyroSensorValueRaw.close() - motorEncoderLeft.close() - motorEncoderRight.close() - motorDutyCycleLeft.close() - motorDutyCycleRight.close() - - for motor in list_motors(): - motor.stop() - - try: - - ######################################################################## - ## - ## Definitions and Initialization variables - ## - ######################################################################## - - # Timing settings for the program - loopTimeMilliSec = 10 # Time of each loop, measured in miliseconds. - loopTimeSec = loopTimeMilliSec/1000.0 # Time of each loop, measured in seconds. - motorAngleHistoryLength = 3 # Number of previous motor angles we keep track of. - - # Math constants - radiansPerDegree = math.pi/180 # The number of radians in a degree. - - # Platform specific constants and conversions - degPerSecondPerRawGyroUnit = 1 # For the LEGO EV3 Gyro in Rate mode, 1 unit = 1 deg/s - radiansPerSecondPerRawGyroUnit = degPerSecondPerRawGyroUnit*radiansPerDegree # Express the above as the rate in rad/s per gyro unit - degPerRawMotorUnit = 1 # For the LEGO EV3 Large Motor 1 unit = 1 deg - radiansPerRawMotorUnit = degPerRawMotorUnit*radiansPerDegree # Express the above as the angle in rad per motor unit - RPMperPerPercentSpeed = 1.7 # On the EV3, "1% speed" corresponds to 1.7 RPM (if speed control were enabled) - degPerSecPerPercentSpeed = RPMperPerPercentSpeed*360/60 # Convert this number to the speed in deg/s per "percent speed" - radPerSecPerPercentSpeed = degPerSecPerPercentSpeed * radiansPerDegree # Convert this number to the speed in rad/s per "percent speed" - - # The rate at which we'll update the gyro offset (precise definition given in docs) - gyroDriftCompensationRate = 0.1 * loopTimeSec * radiansPerSecondPerRawGyroUnit - - # A deque (a fifo array) which we'll use to keep track of previous motor positions, which we can use to calculate the rate of change (speed) - motorAngleHistory = deque([0], motorAngleHistoryLength) - - # State feedback control gains (aka the magic numbers) - gainGyroAngle = self.gainGyroAngle - gainGyroRate = self.gainGyroRate - gainMotorAngle = self.gainMotorAngle - gainMotorAngularSpeed = self.gainMotorAngularSpeed - gainMotorAngleErrorAccumulated = self.gainMotorAngleErrorAccumulated - - # Variables representing physical signals (more info on these in the docs) - # The angle of "the motor", measured in raw units (degrees for the - # EV3). We will take the average of both motor positions as "the motor" - # angle, wich is essentially how far the middle of the robot has traveled. - motorAngleRaw = 0 - - # The angle of the motor, converted to radians (2*pi radians equals 360 degrees). - motorAngle = 0 - - # The reference angle of the motor. The robot will attempt to drive - # forward or backward, such that its measured position equals this - # reference (or close enough). - motorAngleReference = 0 - - # The error: the deviation of the measured motor angle from the reference. - # The robot attempts to make this zero, by driving toward the reference. - motorAngleError = 0 - - # We add up all of the motor angle error in time. If this value gets out of - # hand, we can use it to drive the robot back to the reference position a bit quicker. - motorAngleErrorAccumulated = 0 - - # The motor speed, estimated by how far the motor has turned in a given amount of time - motorAngularSpeed = 0 - - # The reference speed during manouvers: how fast we would like to drive, measured in radians per second. - motorAngularSpeedReference = 0 - - # The error: the deviation of the motor speed from the reference speed. - motorAngularSpeedError = 0 - - # The 'voltage' signal we send to the motor. We calulate a new value each - # time, just right to keep the robot upright. - motorDutyCycle = 0 - - # The raw value from the gyro sensor in rate mode. - gyroRateRaw = 0 - - # The angular rate of the robot (how fast it is falling forward or backward), measured in radians per second. - gyroRate = 0 - - # The gyro doesn't measure the angle of the robot, but we can estimate - # this angle by keeping track of the gyroRate value in time - gyroEstimatedAngle = 0 - - # Over time, the gyro rate value can drift. This causes the sensor to think - # it is moving even when it is perfectly still. We keep track of this offset. - gyroOffset = 0 - - # filehandles for fast reads/writes - # ================================= - touchSensorValueRaw = open(self.touch._path + "/value0", "rb") - gyroSensorValueRaw = open(self.gyro._path + "/value0", "rb") - - # Open motor files for (fast) reading - motorEncoderLeft = open(self.left_motor._path + "/position", "rb") - motorEncoderRight = open(self.right_motor._path + "/position", "rb") - - # Open motor files for (fast) writing - motorDutyCycleLeft = open(self.left_motor._path + "/duty_cycle_sp", "w") - motorDutyCycleRight = open(self.right_motor._path + "/duty_cycle_sp", "w") - - ######################################################################## - ## - ## Calibrate Gyro - ## - ######################################################################## - print("-----------------------------------") - print("Calibrating...") - - #As you hold the robot still, determine the average sensor value of 100 samples - gyroRateCalibrateCount = 100 - for i in range(gyroRateCalibrateCount): - gyroOffset = gyroOffset + FastRead(gyroSensorValueRaw) - time.sleep(0.01) - gyroOffset = gyroOffset/gyroRateCalibrateCount - - # Print the result - print("GyroOffset: %s" % gyroOffset) - print("-----------------------------------") - print("GO!") - print("-----------------------------------") - - ######################################################################## - ## - ## MAIN LOOP (Press Touch Sensor to stop the program) - ## - ######################################################################## - - # Initial touch sensor value - touchSensorPressed = FastRead(touchSensorValueRaw) - - while not touchSensorPressed: - - ############################################################### - ## Loop info - ############################################################### - tLoopStart = time.clock() - - ############################################################### - ## Reading the Remote Control - ############################################################### - self.remote.process() - - ############################################################### - ## Reading the Gyro. - ############################################################### - gyroRateRaw = FastRead(gyroSensorValueRaw) - gyroRate = (gyroRateRaw - gyroOffset)*radiansPerSecondPerRawGyroUnit - - ############################################################### - ## Reading the Motor Position - ############################################################### - motorAngleRaw = (FastRead(motorEncoderLeft) + FastRead(motorEncoderRight))/2 - motorAngle = motorAngleRaw*radiansPerRawMotorUnit - - motorAngularSpeedReference = self.speed * radPerSecPerPercentSpeed - motorAngleReference = motorAngleReference + motorAngularSpeedReference * loopTimeSec - - motorAngleError = motorAngle - motorAngleReference - - ############################################################### - ## Computing Motor Speed - ############################################################### - motorAngularSpeed = (motorAngle - motorAngleHistory[0])/(motorAngleHistoryLength * loopTimeSec) - motorAngularSpeedError = motorAngularSpeed - motorAngularSpeedReference - motorAngleHistory.append(motorAngle) - - ############################################################### - ## Computing the motor duty cycle value - ############################################################### - motorDutyCycle =(gainGyroAngle * gyroEstimatedAngle - + gainGyroRate * gyroRate - + gainMotorAngle * motorAngleError - + gainMotorAngularSpeed * motorAngularSpeedError - + gainMotorAngleErrorAccumulated * motorAngleErrorAccumulated) - - ############################################################### - ## Apply the signal to the motor, and add steering - ############################################################### - SetDuty(motorDutyCycleRight, motorDutyCycle + self.steering) - SetDuty(motorDutyCycleLeft, motorDutyCycle - self.steering) - - ############################################################### - ## Update angle estimate and Gyro Offset Estimate - ############################################################### - gyroEstimatedAngle = gyroEstimatedAngle + gyroRate * loopTimeSec - gyroOffset = (1 - gyroDriftCompensationRate) * gyroOffset + gyroDriftCompensationRate * gyroRateRaw - - ############################################################### - ## Update Accumulated Motor Error - ############################################################### - motorAngleErrorAccumulated = motorAngleErrorAccumulated + motorAngleError * loopTimeSec - - ############################################################### - ## Read the touch sensor (the kill switch) - ############################################################### - touchSensorPressed = FastRead(touchSensorValueRaw) - - ############################################################### - ## Busy wait for the loop to complete - ############################################################### - while ((time.clock() - tLoopStart) < loopTimeSec): - time.sleep(0.0001) - - shutdown() - - # Exit cleanly so that all motors are stopped - except (KeyboardInterrupt, Exception) as e: - log.exception(e) - shutdown() diff --git a/ev3dev/auto.py b/ev3dev/auto.py deleted file mode 100644 index ff16c71..0000000 --- a/ev3dev/auto.py +++ /dev/null @@ -1,41 +0,0 @@ - -""" -Use platform.machine() to determine the platform type, cache the -results in /tmp/current_platform so that we do not have to import -platform and run platform.machine() each time someone imports ev3dev.auto -""" -import os - -filename = '/tmp/current_platform' -current_platform = None - -if os.path.exists(filename): - with open(filename, 'r') as fh: - current_platform = fh.read().strip() - -if not current_platform: - import platform - - def get_current_platform(): - """ - Guess platform we are running on - """ - machine = platform.machine() - - if machine == 'armv5tejl': - return 'ev3' - elif machine == 'armv6l': - return 'brickpi' - else: - return 'unsupported' - - current_platform = get_current_platform() - - with open(filename, 'w') as fh: - fh.write(current_platform + '\n') - -if current_platform == 'brickpi': - from .brickpi import * -else: - # Import ev3 by default, so that it is covered by documentation. - from .ev3 import * diff --git a/ev3dev/brickpi.py b/ev3dev/brickpi.py deleted file mode 100644 index 1df0483..0000000 --- a/ev3dev/brickpi.py +++ /dev/null @@ -1,95 +0,0 @@ -# -*- coding: utf-8 -*- - -# ------------------------------------------------------------------------------ -# Copyright (c) 2015 Eric Pascual -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NON INFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. -# ----------------------------------------------------------------------------- - -""" -An assortment of classes modeling specific features of the BrickPi. -""" - -from .core import * - - -OUTPUT_A = 'ttyAMA0:MA' -OUTPUT_B = 'ttyAMA0:MB' -OUTPUT_C = 'ttyAMA0:MC' -OUTPUT_D = 'ttyAMA0:MD' - -INPUT_1 = 'ttyAMA0:S1' -INPUT_2 = 'ttyAMA0:S2' -INPUT_3 = 'ttyAMA0:S3' -INPUT_4 = 'ttyAMA0:S4' - - -class Leds(object): - """ - The BrickPi LEDs. - """ - -# ~autogen led-colors platforms.brickpi.led>currentClass - - blue_led1 = Led(name_pattern='brickpi:led1:blue:ev3dev') - blue_led2 = Led(name_pattern='brickpi:led2:blue:ev3dev') - - LED1 = ( blue_led1, ) - LED2 = ( blue_led2, ) - - BLACK = ( 0, ) - BLUE = ( 1, ) - - @staticmethod - def set_color(group, color, pct=1): - """ - Sets brigthness of leds in the given group to the values specified in - color tuple. When percentage is specified, brightness of each led is - reduced proportionally. - - Example:: - - Leds.set_color(LEFT, AMBER) - """ - for l, v in zip(group, color): - l.brightness_pct = v * pct - - @staticmethod - def set(group, **kwargs): - """ - Set attributes for each led in group. - - Example:: - - Leds.set(LEFT, brightness_pct=0.5, trigger='timer') - """ - for led in group: - for k in kwargs: - setattr(led, k, kwargs[k]) - - @staticmethod - def all_off(): - """ - Turn all leds off - """ - Leds.blue_led1.brightness = 0 - Leds.blue_led2.brightness = 0 - - -# ~autogen diff --git a/ev3dev/core.py b/ev3dev/core.py deleted file mode 100644 index 8747923..0000000 --- a/ev3dev/core.py +++ /dev/null @@ -1,3751 +0,0 @@ -# ----------------------------------------------------------------------------- -# Copyright (c) 2015 Ralph Hempel -# Copyright (c) 2015 Anton Vanhoucke -# Copyright (c) 2015 Denis Demidov -# Copyright (c) 2015 Eric Pascual -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. -# ----------------------------------------------------------------------------- - -# ~autogen autogen-header -# Sections of the following code were auto-generated based on spec v1.2.0 - -# ~autogen - -# ----------------------------------------------------------------------------- - -import sys - -if sys.version_info < (3,4): - raise SystemError('Must be using Python 3.4 or higher') - -# ----------------------------------------------------------------------------- - -import os -import io -import fnmatch -import numbers -import array -import mmap -import ctypes -import re -import select -import shlex -import stat -import time -from os.path import abspath -from struct import pack, unpack -from subprocess import Popen, check_output, PIPE - -try: - # This is a linux-specific module. - # It is required by the Button() class, but failure to import it may be - # safely ignored if one just needs to run API tests on Windows. - import fcntl -except ImportError: - print("WARNING: Failed to import fcntl. Button class will be unuseable!") - -INPUT_AUTO = '' -OUTPUT_AUTO = '' - -# ----------------------------------------------------------------------------- -def list_device_names(class_path, name_pattern, **kwargs): - """ - This is a generator function that lists names of all devices matching the - provided parameters. - - Parameters: - class_path: class path of the device, a subdirectory of /sys/class. - For example, '/sys/class/tacho-motor'. - name_pattern: pattern that device name should match. - For example, 'sensor*' or 'motor*'. Default value: '*'. - keyword arguments: used for matching the corresponding device - attributes. For example, address='outA', or - driver_name=['lego-ev3-us', 'lego-nxt-us']. When argument value - is a list, then a match against any entry of the list is - enough. - """ - - if not os.path.isdir(class_path): - return - - def matches(attribute, pattern): - try: - with io.FileIO(attribute) as f: - value = f.read().strip().decode() - except: - return False - - if isinstance(pattern, list): - return any([value.find(p) >= 0 for p in pattern]) - else: - return value.find(pattern) >= 0 - - for f in os.listdir(class_path): - if fnmatch.fnmatch(f, name_pattern): - path = class_path + '/' + f - if all([matches(path + '/' + k, kwargs[k]) for k in kwargs]): - yield f - -# ----------------------------------------------------------------------------- -# Define the base class from which all other ev3dev classes are defined. - -class Device(object): - """The ev3dev device base class""" - - __slots__ = ['_path', 'connected', '_device_index', 'kwargs'] - - DEVICE_ROOT_PATH = '/sys/class' - - _DEVICE_INDEX = re.compile(r'^.*(?P\d+)$') - - def __init__(self, class_name, name_pattern='*', name_exact=False, **kwargs): - """Spin through the Linux sysfs class for the device type and find - a device that matches the provided name pattern and attributes (if any). - - Parameters: - class_name: class name of the device, a subdirectory of /sys/class. - For example, 'tacho-motor'. - name_pattern: pattern that device name should match. - For example, 'sensor*' or 'motor*'. Default value: '*'. - name_exact: when True, assume that the name_pattern provided is the - exact device name and use it directly. - keyword arguments: used for matching the corresponding device - attributes. For example, address='outA', or - driver_name=['lego-ev3-us', 'lego-nxt-us']. When argument value - is a list, then a match against any entry of the list is - enough. - - Example:: - - d = ev3dev.Device('tacho-motor', address='outA') - s = ev3dev.Device('lego-sensor', driver_name=['lego-ev3-us', 'lego-nxt-us']) - - When connected succesfully, the `connected` attribute is set to True. - """ - - classpath = abspath(Device.DEVICE_ROOT_PATH + '/' + class_name) - self.kwargs = kwargs - - def get_index(file): - match = Device._DEVICE_INDEX.match(file) - if match: - return int(match.group('idx')) - else: - return None - - if name_exact: - self._path = classpath + '/' + name_pattern - self._device_index = get_index(name_pattern) - self.connected = True - else: - try: - name = next(list_device_names(classpath, name_pattern, **kwargs)) - self._path = classpath + '/' + name - self._device_index = get_index(name) - self.connected = True - except StopIteration: - self._path = None - self._device_index = None - self.connected = False - - def __str__(self): - if 'address' in self.kwargs: - return "%s(%s)" % (self.__class__.__name__, self.kwargs.get('address')) - else: - return self.__class__.__name__ - - def _attribute_file_open( self, name ): - path = self._path + '/' + name - mode = stat.S_IMODE(os.stat(path)[stat.ST_MODE]) - r_ok = mode & stat.S_IRGRP - w_ok = mode & stat.S_IWGRP - - if r_ok and w_ok: - mode = 'r+' - elif w_ok: - mode = 'w' - else: - mode = 'r' - - return io.FileIO(path, mode) - - def _get_attribute(self, attribute, name): - """Device attribute getter""" - if self.connected: - if None == attribute: - attribute = self._attribute_file_open( name ) - else: - attribute.seek(0) - return attribute, attribute.read().strip().decode() - else: - raise Exception('Device is not connected') - - def _set_attribute(self, attribute, name, value): - """Device attribute setter""" - if self.connected: - if None == attribute: - attribute = self._attribute_file_open( name ) - else: - attribute.seek(0) - attribute.write(value.encode()) - attribute.flush() - return attribute - else: - raise Exception('Device is not connected') - - def get_attr_int(self, attribute, name): - attribute, value = self._get_attribute(attribute, name) - return attribute, int(value) - - def set_attr_int(self, attribute, name, value): - return self._set_attribute(attribute, name, str(int(value))) - - def get_attr_string(self, attribute, name): - return self._get_attribute(attribute, name) - - def set_attr_string(self, attribute, name, value): - return self._set_attribute(attribute, name, value) - - def get_attr_line(self, attribute, name): - return self._get_attribute(attribute, name) - - def get_attr_set(self, attribute, name): - attribute, value = self.get_attr_line(attribute, name) - return attribute, [v.strip('[]') for v in value.split()] - - def get_attr_from_set(self, attribute, name): - attribute, value = self.get_attr_line(attribute, name) - for a in value.split(): - v = a.strip('[]') - if v != a: - return v - return "" - - @property - def device_index(self): - return self._device_index - -def list_devices(class_name, name_pattern, **kwargs): - """ - This is a generator function that takes same arguments as `Device` class - and enumerates all devices present in the system that match the provided - arguments. - - Parameters: - class_name: class name of the device, a subdirectory of /sys/class. - For example, 'tacho-motor'. - name_pattern: pattern that device name should match. - For example, 'sensor*' or 'motor*'. Default value: '*'. - keyword arguments: used for matching the corresponding device - attributes. For example, address='outA', or - driver_name=['lego-ev3-us', 'lego-nxt-us']. When argument value - is a list, then a match against any entry of the list is - enough. - """ - classpath = abspath(Device.DEVICE_ROOT_PATH + '/' + class_name) - - return (Device(class_name, name, name_exact=True) - for name in list_device_names(classpath, name_pattern, **kwargs)) - -# ~autogen generic-class classes.motor>currentClass - -class Motor(Device): - - """ - The motor class provides a uniform interface for using motors with - positional and directional feedback such as the EV3 and NXT motors. - This feedback allows for precise control of the motors. This is the - most common type of motor, so we just call it `motor`. - - The way to configure a motor is to set the '_sp' attributes when - calling a command or before. Only in 'run_direct' mode attribute - changes are processed immediately, in the other modes they only - take place when a new command is issued. - """ - - SYSTEM_CLASS_NAME = 'tacho-motor' - SYSTEM_DEVICE_NAME_CONVENTION = '*' - - def __init__(self, address=None, name_pattern=SYSTEM_DEVICE_NAME_CONVENTION, name_exact=False, **kwargs): - - if address is not None: - kwargs['address'] = address - super(Motor, self).__init__(self.SYSTEM_CLASS_NAME, name_pattern, name_exact, **kwargs) - - self._address = None - self._command = None - self._commands = None - self._count_per_rot = None - self._count_per_m = None - self._driver_name = None - self._duty_cycle = None - self._duty_cycle_sp = None - self._full_travel_count = None - self._polarity = None - self._position = None - self._position_p = None - self._position_i = None - self._position_d = None - self._position_sp = None - self._max_speed = None - self._speed = None - self._speed_sp = None - self._ramp_up_sp = None - self._ramp_down_sp = None - self._speed_p = None - self._speed_i = None - self._speed_d = None - self._state = None - self._stop_action = None - self._stop_actions = None - self._time_sp = None - -# ~autogen - - self._poll = None - - __slots__ = [ -# ~autogen generic-class-slots classes.motor>currentClass - - '_address', - '_command', - '_commands', - '_count_per_rot', - '_count_per_m', - '_driver_name', - '_duty_cycle', - '_duty_cycle_sp', - '_full_travel_count', - '_polarity', - '_position', - '_position_p', - '_position_i', - '_position_d', - '_position_sp', - '_max_speed', - '_speed', - '_speed_sp', - '_ramp_up_sp', - '_ramp_down_sp', - '_speed_p', - '_speed_i', - '_speed_d', - '_state', - '_stop_action', - '_stop_actions', - '_time_sp', - -# ~autogen - '_poll', - ] - -# ~autogen generic-get-set classes.motor>currentClass - - @property - def address(self): - """ - Returns the name of the port that this motor is connected to. - """ - self._address, value = self.get_attr_string(self._address, 'address') - return value - - @property - def command(self): - """ - Sends a command to the motor controller. See `commands` for a list of - possible values. - """ - raise Exception("command is a write-only property!") - - @command.setter - def command(self, value): - self._command = self.set_attr_string(self._command, 'command', value) - - @property - def commands(self): - """ - Returns a list of commands that are supported by the motor - controller. Possible values are `run-forever`, `run-to-abs-pos`, `run-to-rel-pos`, - `run-timed`, `run-direct`, `stop` and `reset`. Not all commands may be supported. - - - `run-forever` will cause the motor to run until another command is sent. - - `run-to-abs-pos` will run to an absolute position specified by `position_sp` - and then stop using the action specified in `stop_action`. - - `run-to-rel-pos` will run to a position relative to the current `position` value. - The new position will be current `position` + `position_sp`. When the new - position is reached, the motor will stop using the action specified by `stop_action`. - - `run-timed` will run the motor for the amount of time specified in `time_sp` - and then stop the motor using the action specified by `stop_action`. - - `run-direct` will run the motor at the duty cycle specified by `duty_cycle_sp`. - Unlike other run commands, changing `duty_cycle_sp` while running *will* - take effect immediately. - - `stop` will stop any of the run commands before they are complete using the - action specified by `stop_action`. - - `reset` will reset all of the motor parameter attributes to their default value. - This will also have the effect of stopping the motor. - """ - self._commands, value = self.get_attr_set(self._commands, 'commands') - return value - - @property - def count_per_rot(self): - """ - Returns the number of tacho counts in one rotation of the motor. Tacho counts - are used by the position and speed attributes, so you can use this value - to convert rotations or degrees to tacho counts. (rotation motors only) - """ - self._count_per_rot, value = self.get_attr_int(self._count_per_rot, 'count_per_rot') - return value - - @property - def count_per_m(self): - """ - Returns the number of tacho counts in one meter of travel of the motor. Tacho - counts are used by the position and speed attributes, so you can use this - value to convert from distance to tacho counts. (linear motors only) - """ - self._count_per_m, value = self.get_attr_int(self._count_per_m, 'count_per_m') - return value - - @property - def driver_name(self): - """ - Returns the name of the driver that provides this tacho motor device. - """ - self._driver_name, value = self.get_attr_string(self._driver_name, 'driver_name') - return value - - @property - def duty_cycle(self): - """ - Returns the current duty cycle of the motor. Units are percent. Values - are -100 to 100. - """ - self._duty_cycle, value = self.get_attr_int(self._duty_cycle, 'duty_cycle') - return value - - @property - def duty_cycle_sp(self): - """ - Writing sets the duty cycle setpoint. Reading returns the current value. - Units are in percent. Valid values are -100 to 100. A negative value causes - the motor to rotate in reverse. - """ - self._duty_cycle_sp, value = self.get_attr_int(self._duty_cycle_sp, 'duty_cycle_sp') - return value - - @duty_cycle_sp.setter - def duty_cycle_sp(self, value): - self._duty_cycle_sp = self.set_attr_int(self._duty_cycle_sp, 'duty_cycle_sp', value) - - @property - def full_travel_count(self): - """ - Returns the number of tacho counts in the full travel of the motor. When - combined with the `count_per_m` atribute, you can use this value to - calculate the maximum travel distance of the motor. (linear motors only) - """ - self._full_travel_count, value = self.get_attr_int(self._full_travel_count, 'full_travel_count') - return value - - @property - def polarity(self): - """ - Sets the polarity of the motor. With `normal` polarity, a positive duty - cycle will cause the motor to rotate clockwise. With `inversed` polarity, - a positive duty cycle will cause the motor to rotate counter-clockwise. - Valid values are `normal` and `inversed`. - """ - self._polarity, value = self.get_attr_string(self._polarity, 'polarity') - return value - - @polarity.setter - def polarity(self, value): - self._polarity = self.set_attr_string(self._polarity, 'polarity', value) - - @property - def position(self): - """ - Returns the current position of the motor in pulses of the rotary - encoder. When the motor rotates clockwise, the position will increase. - Likewise, rotating counter-clockwise causes the position to decrease. - Writing will set the position to that value. - """ - self._position, value = self.get_attr_int(self._position, 'position') - return value - - @position.setter - def position(self, value): - self._position = self.set_attr_int(self._position, 'position', value) - - @property - def position_p(self): - """ - The proportional constant for the position PID. - """ - self._position_p, value = self.get_attr_int(self._position_p, 'hold_pid/Kp') - return value - - @position_p.setter - def position_p(self, value): - self._position_p = self.set_attr_int(self._position_p, 'hold_pid/Kp', value) - - @property - def position_i(self): - """ - The integral constant for the position PID. - """ - self._position_i, value = self.get_attr_int(self._position_i, 'hold_pid/Ki') - return value - - @position_i.setter - def position_i(self, value): - self._position_i = self.set_attr_int(self._position_i, 'hold_pid/Ki', value) - - @property - def position_d(self): - """ - The derivative constant for the position PID. - """ - self._position_d, value = self.get_attr_int(self._position_d, 'hold_pid/Kd') - return value - - @position_d.setter - def position_d(self, value): - self._position_d = self.set_attr_int(self._position_d, 'hold_pid/Kd', value) - - @property - def position_sp(self): - """ - Writing specifies the target position for the `run-to-abs-pos` and `run-to-rel-pos` - commands. Reading returns the current value. Units are in tacho counts. You - can use the value returned by `counts_per_rot` to convert tacho counts to/from - rotations or degrees. - """ - self._position_sp, value = self.get_attr_int(self._position_sp, 'position_sp') - return value - - @position_sp.setter - def position_sp(self, value): - self._position_sp = self.set_attr_int(self._position_sp, 'position_sp', value) - - @property - def max_speed(self): - """ - Returns the maximum value that is accepted by the `speed_sp` attribute. This - may be slightly different than the maximum speed that a particular motor can - reach - it's the maximum theoretical speed. - """ - self._max_speed, value = self.get_attr_int(self._max_speed, 'max_speed') - return value - - @property - def speed(self): - """ - Returns the current motor speed in tacho counts per second. Note, this is - not necessarily degrees (although it is for LEGO motors). Use the `count_per_rot` - attribute to convert this value to RPM or deg/sec. - """ - self._speed, value = self.get_attr_int(self._speed, 'speed') - return value - - @property - def speed_sp(self): - """ - Writing sets the target speed in tacho counts per second used for all `run-*` - commands except `run-direct`. Reading returns the current value. A negative - value causes the motor to rotate in reverse with the exception of `run-to-*-pos` - commands where the sign is ignored. Use the `count_per_rot` attribute to convert - RPM or deg/sec to tacho counts per second. Use the `count_per_m` attribute to - convert m/s to tacho counts per second. - """ - self._speed_sp, value = self.get_attr_int(self._speed_sp, 'speed_sp') - return value - - @speed_sp.setter - def speed_sp(self, value): - self._speed_sp = self.set_attr_int(self._speed_sp, 'speed_sp', value) - - @property - def ramp_up_sp(self): - """ - Writing sets the ramp up setpoint. Reading returns the current value. Units - are in milliseconds and must be positive. When set to a non-zero value, the - motor speed will increase from 0 to 100% of `max_speed` over the span of this - setpoint. The actual ramp time is the ratio of the difference between the - `speed_sp` and the current `speed` and max_speed multiplied by `ramp_up_sp`. - """ - self._ramp_up_sp, value = self.get_attr_int(self._ramp_up_sp, 'ramp_up_sp') - return value - - @ramp_up_sp.setter - def ramp_up_sp(self, value): - self._ramp_up_sp = self.set_attr_int(self._ramp_up_sp, 'ramp_up_sp', value) - - @property - def ramp_down_sp(self): - """ - Writing sets the ramp down setpoint. Reading returns the current value. Units - are in milliseconds and must be positive. When set to a non-zero value, the - motor speed will decrease from 0 to 100% of `max_speed` over the span of this - setpoint. The actual ramp time is the ratio of the difference between the - `speed_sp` and the current `speed` and max_speed multiplied by `ramp_down_sp`. - """ - self._ramp_down_sp, value = self.get_attr_int(self._ramp_down_sp, 'ramp_down_sp') - return value - - @ramp_down_sp.setter - def ramp_down_sp(self, value): - self._ramp_down_sp = self.set_attr_int(self._ramp_down_sp, 'ramp_down_sp', value) - - @property - def speed_p(self): - """ - The proportional constant for the speed regulation PID. - """ - self._speed_p, value = self.get_attr_int(self._speed_p, 'speed_pid/Kp') - return value - - @speed_p.setter - def speed_p(self, value): - self._speed_p = self.set_attr_int(self._speed_p, 'speed_pid/Kp', value) - - @property - def speed_i(self): - """ - The integral constant for the speed regulation PID. - """ - self._speed_i, value = self.get_attr_int(self._speed_i, 'speed_pid/Ki') - return value - - @speed_i.setter - def speed_i(self, value): - self._speed_i = self.set_attr_int(self._speed_i, 'speed_pid/Ki', value) - - @property - def speed_d(self): - """ - The derivative constant for the speed regulation PID. - """ - self._speed_d, value = self.get_attr_int(self._speed_d, 'speed_pid/Kd') - return value - - @speed_d.setter - def speed_d(self, value): - self._speed_d = self.set_attr_int(self._speed_d, 'speed_pid/Kd', value) - - @property - def state(self): - """ - Reading returns a list of state flags. Possible flags are - `running`, `ramping`, `holding`, `overloaded` and `stalled`. - """ - self._state, value = self.get_attr_set(self._state, 'state') - return value - - @property - def stop_action(self): - """ - Reading returns the current stop action. Writing sets the stop action. - The value determines the motors behavior when `command` is set to `stop`. - Also, it determines the motors behavior when a run command completes. See - `stop_actions` for a list of possible values. - """ - self._stop_action, value = self.get_attr_string(self._stop_action, 'stop_action') - return value - - @stop_action.setter - def stop_action(self, value): - self._stop_action = self.set_attr_string(self._stop_action, 'stop_action', value) - - @property - def stop_actions(self): - """ - Returns a list of stop actions supported by the motor controller. - Possible values are `coast`, `brake` and `hold`. `coast` means that power will - be removed from the motor and it will freely coast to a stop. `brake` means - that power will be removed from the motor and a passive electrical load will - be placed on the motor. This is usually done by shorting the motor terminals - together. This load will absorb the energy from the rotation of the motors and - cause the motor to stop more quickly than coasting. `hold` does not remove - power from the motor. Instead it actively tries to hold the motor at the current - position. If an external force tries to turn the motor, the motor will 'push - back' to maintain its position. - """ - self._stop_actions, value = self.get_attr_set(self._stop_actions, 'stop_actions') - return value - - @property - def time_sp(self): - """ - Writing specifies the amount of time the motor will run when using the - `run-timed` command. Reading returns the current value. Units are in - milliseconds. - """ - self._time_sp, value = self.get_attr_int(self._time_sp, 'time_sp') - return value - - @time_sp.setter - def time_sp(self, value): - self._time_sp = self.set_attr_int(self._time_sp, 'time_sp', value) - - -# ~autogen -# ~autogen generic-property-value classes.motor>currentClass - - #: Run the motor until another command is sent. - COMMAND_RUN_FOREVER = 'run-forever' - - #: Run to an absolute position specified by `position_sp` and then - #: stop using the action specified in `stop_action`. - COMMAND_RUN_TO_ABS_POS = 'run-to-abs-pos' - - #: Run to a position relative to the current `position` value. - #: The new position will be current `position` + `position_sp`. - #: When the new position is reached, the motor will stop using - #: the action specified by `stop_action`. - COMMAND_RUN_TO_REL_POS = 'run-to-rel-pos' - - #: Run the motor for the amount of time specified in `time_sp` - #: and then stop the motor using the action specified by `stop_action`. - COMMAND_RUN_TIMED = 'run-timed' - - #: Run the motor at the duty cycle specified by `duty_cycle_sp`. - #: Unlike other run commands, changing `duty_cycle_sp` while running *will* - #: take effect immediately. - COMMAND_RUN_DIRECT = 'run-direct' - - #: Stop any of the run commands before they are complete using the - #: action specified by `stop_action`. - COMMAND_STOP = 'stop' - - #: Reset all of the motor parameter attributes to their default value. - #: This will also have the effect of stopping the motor. - COMMAND_RESET = 'reset' - - #: Sets the normal polarity of the rotary encoder. - ENCODER_POLARITY_NORMAL = 'normal' - - #: Sets the inversed polarity of the rotary encoder. - ENCODER_POLARITY_INVERSED = 'inversed' - - #: With `normal` polarity, a positive duty cycle will - #: cause the motor to rotate clockwise. - POLARITY_NORMAL = 'normal' - - #: With `inversed` polarity, a positive duty cycle will - #: cause the motor to rotate counter-clockwise. - POLARITY_INVERSED = 'inversed' - - #: Power is being sent to the motor. - STATE_RUNNING = 'running' - - #: The motor is ramping up or down and has not yet reached a constant output level. - STATE_RAMPING = 'ramping' - - #: The motor is not turning, but rather attempting to hold a fixed position. - STATE_HOLDING = 'holding' - - #: The motor is turning, but cannot reach its `speed_sp`. - STATE_OVERLOADED = 'overloaded' - - #: The motor is not turning when it should be. - STATE_STALLED = 'stalled' - - #: Power will be removed from the motor and it will freely coast to a stop. - STOP_ACTION_COAST = 'coast' - - #: Power will be removed from the motor and a passive electrical load will - #: be placed on the motor. This is usually done by shorting the motor terminals - #: together. This load will absorb the energy from the rotation of the motors and - #: cause the motor to stop more quickly than coasting. - STOP_ACTION_BRAKE = 'brake' - - #: Does not remove power from the motor. Instead it actively try to hold the motor - #: at the current position. If an external force tries to turn the motor, the motor - #: will `push back` to maintain its position. - STOP_ACTION_HOLD = 'hold' - - -# ~autogen -# ~autogen motor_commands classes.motor>currentClass - - def run_forever(self, **kwargs): - """Run the motor until another command is sent. - """ - for key in kwargs: - setattr(self, key, kwargs[key]) - self.command = self.COMMAND_RUN_FOREVER - - def run_to_abs_pos(self, **kwargs): - """Run to an absolute position specified by `position_sp` and then - stop using the action specified in `stop_action`. - """ - for key in kwargs: - setattr(self, key, kwargs[key]) - self.command = self.COMMAND_RUN_TO_ABS_POS - - def run_to_rel_pos(self, **kwargs): - """Run to a position relative to the current `position` value. - The new position will be current `position` + `position_sp`. - When the new position is reached, the motor will stop using - the action specified by `stop_action`. - """ - for key in kwargs: - setattr(self, key, kwargs[key]) - self.command = self.COMMAND_RUN_TO_REL_POS - - def run_timed(self, **kwargs): - """Run the motor for the amount of time specified in `time_sp` - and then stop the motor using the action specified by `stop_action`. - """ - for key in kwargs: - setattr(self, key, kwargs[key]) - self.command = self.COMMAND_RUN_TIMED - - def run_direct(self, **kwargs): - """Run the motor at the duty cycle specified by `duty_cycle_sp`. - Unlike other run commands, changing `duty_cycle_sp` while running *will* - take effect immediately. - """ - for key in kwargs: - setattr(self, key, kwargs[key]) - self.command = self.COMMAND_RUN_DIRECT - - def stop(self, **kwargs): - """Stop any of the run commands before they are complete using the - action specified by `stop_action`. - """ - for key in kwargs: - setattr(self, key, kwargs[key]) - self.command = self.COMMAND_STOP - - def reset(self, **kwargs): - """Reset all of the motor parameter attributes to their default value. - This will also have the effect of stopping the motor. - """ - for key in kwargs: - setattr(self, key, kwargs[key]) - self.command = self.COMMAND_RESET - - -# ~autogen -# ~autogen motor_states classes.motor>currentClass - - @property - def is_running(self): - """Power is being sent to the motor. - """ - return self.STATE_RUNNING in self.state - - @property - def is_ramping(self): - """The motor is ramping up or down and has not yet reached a constant output level. - """ - return self.STATE_RAMPING in self.state - - @property - def is_holding(self): - """The motor is not turning, but rather attempting to hold a fixed position. - """ - return self.STATE_HOLDING in self.state - - @property - def is_overloaded(self): - """The motor is turning, but cannot reach its `speed_sp`. - """ - return self.STATE_OVERLOADED in self.state - - @property - def is_stalled(self): - """The motor is not turning when it should be. - """ - return self.STATE_STALLED in self.state - - -# ~autogen - - def wait(self, cond, timeout=None): - """ - Blocks until ``cond(self.state)`` is ``True``. The condition is - checked when there is an I/O event related to the ``state`` attribute. - Exits early when ``timeout`` (in milliseconds) is reached. - - Returns ``True`` if the condition is met, and ``False`` if the timeout - is reached. - """ - - tic = time.time() - - if self._poll is None: - if self._state is None: - self._state = self._attribute_file_open('state') - self._poll = select.poll() - self._poll.register(self._state, select.POLLPRI) - - while True: - self._poll.poll(None if timeout is None else timeout) - - if timeout is not None and time.time() >= tic + timeout / 1000: - return False - - if cond(self.state): - return True - - def wait_until_not_moving(self, timeout=None): - """ - Blocks until ``running`` is not in ``self.state`` or ``stalled`` is in - ``self.state``. The condition is checked when there is an I/O event - related to the ``state`` attribute. Exits early when ``timeout`` - (in milliseconds) is reached. - - Returns ``True`` if the condition is met, and ``False`` if the timeout - is reached. - - Example:: - - m.wait_until_not_moving() - """ - return self.wait(lambda state: self.STATE_RUNNING not in state or self.STATE_STALLED in state, timeout) - - def wait_until(self, s, timeout=None): - """ - Blocks until ``s`` is in ``self.state``. The condition is checked when - there is an I/O event related to the ``state`` attribute. Exits early - when ``timeout`` (in milliseconds) is reached. - - Returns ``True`` if the condition is met, and ``False`` if the timeout - is reached. - - Example:: - - m.wait_until('stalled') - """ - return self.wait(lambda state: s in state, timeout) - - def wait_while(self, s, timeout=None): - """ - Blocks until ``s`` is not in ``self.state``. The condition is checked - when there is an I/O event related to the ``state`` attribute. Exits - early when ``timeout`` (in milliseconds) is reached. - - Returns ``True`` if the condition is met, and ``False`` if the timeout - is reached. - - Example:: - - m.wait_while('running') - """ - return self.wait(lambda state: s not in state, timeout) - -def list_motors(name_pattern=Motor.SYSTEM_DEVICE_NAME_CONVENTION, **kwargs): - """ - This is a generator function that enumerates all tacho motors that match - the provided arguments. - - Parameters: - name_pattern: pattern that device name should match. - For example, 'motor*'. Default value: '*'. - keyword arguments: used for matching the corresponding device - attributes. For example, driver_name='lego-ev3-l-motor', or - address=['outB', 'outC']. When argument value - is a list, then a match against any entry of the list is - enough. - """ - class_path = abspath(Device.DEVICE_ROOT_PATH + '/' + Motor.SYSTEM_CLASS_NAME) - - return (Motor(name_pattern=name, name_exact=True) - for name in list_device_names(class_path, name_pattern, **kwargs)) - -# ~autogen generic-class classes.largeMotor>currentClass - -class LargeMotor(Motor): - - """ - EV3/NXT large servo motor - """ - - SYSTEM_CLASS_NAME = Motor.SYSTEM_CLASS_NAME - SYSTEM_DEVICE_NAME_CONVENTION = '*' - - def __init__(self, address=None, name_pattern=SYSTEM_DEVICE_NAME_CONVENTION, name_exact=False, **kwargs): - - super(LargeMotor, self).__init__(address, name_pattern, name_exact, driver_name=['lego-ev3-l-motor', 'lego-nxt-motor'], **kwargs) - - -# ~autogen - __slots__ = [ -# ~autogen generic-class-slots classes.largeMotor>currentClass - - -# ~autogen - ] -# ~autogen generic-class classes.mediumMotor>currentClass - -class MediumMotor(Motor): - - """ - EV3 medium servo motor - """ - - SYSTEM_CLASS_NAME = Motor.SYSTEM_CLASS_NAME - SYSTEM_DEVICE_NAME_CONVENTION = '*' - - def __init__(self, address=None, name_pattern=SYSTEM_DEVICE_NAME_CONVENTION, name_exact=False, **kwargs): - - super(MediumMotor, self).__init__(address, name_pattern, name_exact, driver_name=['lego-ev3-m-motor'], **kwargs) - - -# ~autogen - __slots__ = [ -# ~autogen generic-class-slots classes.mediumMotor>currentClass - - -# ~autogen - ] -# ~autogen generic-class classes.actuonix50Motor>currentClass - -class ActuonixL1250Motor(Motor): - - """ - Actuonix L12 50 linear servo motor - """ - - SYSTEM_CLASS_NAME = Motor.SYSTEM_CLASS_NAME - SYSTEM_DEVICE_NAME_CONVENTION = 'linear*' - - def __init__(self, address=None, name_pattern=SYSTEM_DEVICE_NAME_CONVENTION, name_exact=False, **kwargs): - - super(ActuonixL1250Motor, self).__init__(address, name_pattern, name_exact, driver_name=['act-l12-ev3-50'], **kwargs) - - -# ~autogen - __slots__ = [ -# ~autogen generic-class-slots classes.actuonix50Motor>currentClass - - -# ~autogen - ] -# ~autogen generic-class classes.actuonix100Motor>currentClass - -class ActuonixL12100Motor(Motor): - - """ - Actuonix L12 100 linear servo motor - """ - - SYSTEM_CLASS_NAME = Motor.SYSTEM_CLASS_NAME - SYSTEM_DEVICE_NAME_CONVENTION = 'linear*' - - def __init__(self, address=None, name_pattern=SYSTEM_DEVICE_NAME_CONVENTION, name_exact=False, **kwargs): - - super(ActuonixL12100Motor, self).__init__(address, name_pattern, name_exact, driver_name=['act-l12-ev3-100'], **kwargs) - - -# ~autogen - __slots__ = [ -# ~autogen generic-class-slots classes.actuonix100Motor>currentClass - - -# ~autogen - ] -# ~autogen generic-class classes.dcMotor>currentClass - -class DcMotor(Device): - - """ - The DC motor class provides a uniform interface for using regular DC motors - with no fancy controls or feedback. This includes LEGO MINDSTORMS RCX motors - and LEGO Power Functions motors. - """ - - SYSTEM_CLASS_NAME = 'dc-motor' - SYSTEM_DEVICE_NAME_CONVENTION = 'motor*' - - def __init__(self, address=None, name_pattern=SYSTEM_DEVICE_NAME_CONVENTION, name_exact=False, **kwargs): - - if address is not None: - kwargs['address'] = address - super(DcMotor, self).__init__(self.SYSTEM_CLASS_NAME, name_pattern, name_exact, **kwargs) - - self._address = None - self._command = None - self._commands = None - self._driver_name = None - self._duty_cycle = None - self._duty_cycle_sp = None - self._polarity = None - self._ramp_down_sp = None - self._ramp_up_sp = None - self._state = None - self._stop_action = None - self._stop_actions = None - self._time_sp = None - -# ~autogen - - __slots__ = [ -# ~autogen generic-class-slots classes.dcMotor>currentClass - - '_address', - '_command', - '_commands', - '_driver_name', - '_duty_cycle', - '_duty_cycle_sp', - '_polarity', - '_ramp_down_sp', - '_ramp_up_sp', - '_state', - '_stop_action', - '_stop_actions', - '_time_sp', - -# ~autogen - ] - -# ~autogen generic-get-set classes.dcMotor>currentClass - - @property - def address(self): - """ - Returns the name of the port that this motor is connected to. - """ - self._address, value = self.get_attr_string(self._address, 'address') - return value - - @property - def command(self): - """ - Sets the command for the motor. Possible values are `run-forever`, `run-timed` and - `stop`. Not all commands may be supported, so be sure to check the contents - of the `commands` attribute. - """ - raise Exception("command is a write-only property!") - - @command.setter - def command(self, value): - self._command = self.set_attr_string(self._command, 'command', value) - - @property - def commands(self): - """ - Returns a list of commands supported by the motor - controller. - """ - self._commands, value = self.get_attr_set(self._commands, 'commands') - return value - - @property - def driver_name(self): - """ - Returns the name of the motor driver that loaded this device. See the list - of [supported devices] for a list of drivers. - """ - self._driver_name, value = self.get_attr_string(self._driver_name, 'driver_name') - return value - - @property - def duty_cycle(self): - """ - Shows the current duty cycle of the PWM signal sent to the motor. Values - are -100 to 100 (-100% to 100%). - """ - self._duty_cycle, value = self.get_attr_int(self._duty_cycle, 'duty_cycle') - return value - - @property - def duty_cycle_sp(self): - """ - Writing sets the duty cycle setpoint of the PWM signal sent to the motor. - Valid values are -100 to 100 (-100% to 100%). Reading returns the current - setpoint. - """ - self._duty_cycle_sp, value = self.get_attr_int(self._duty_cycle_sp, 'duty_cycle_sp') - return value - - @duty_cycle_sp.setter - def duty_cycle_sp(self, value): - self._duty_cycle_sp = self.set_attr_int(self._duty_cycle_sp, 'duty_cycle_sp', value) - - @property - def polarity(self): - """ - Sets the polarity of the motor. Valid values are `normal` and `inversed`. - """ - self._polarity, value = self.get_attr_string(self._polarity, 'polarity') - return value - - @polarity.setter - def polarity(self, value): - self._polarity = self.set_attr_string(self._polarity, 'polarity', value) - - @property - def ramp_down_sp(self): - """ - Sets the time in milliseconds that it take the motor to ramp down from 100% - to 0%. Valid values are 0 to 10000 (10 seconds). Default is 0. - """ - self._ramp_down_sp, value = self.get_attr_int(self._ramp_down_sp, 'ramp_down_sp') - return value - - @ramp_down_sp.setter - def ramp_down_sp(self, value): - self._ramp_down_sp = self.set_attr_int(self._ramp_down_sp, 'ramp_down_sp', value) - - @property - def ramp_up_sp(self): - """ - Sets the time in milliseconds that it take the motor to up ramp from 0% to - 100%. Valid values are 0 to 10000 (10 seconds). Default is 0. - """ - self._ramp_up_sp, value = self.get_attr_int(self._ramp_up_sp, 'ramp_up_sp') - return value - - @ramp_up_sp.setter - def ramp_up_sp(self, value): - self._ramp_up_sp = self.set_attr_int(self._ramp_up_sp, 'ramp_up_sp', value) - - @property - def state(self): - """ - Gets a list of flags indicating the motor status. Possible - flags are `running` and `ramping`. `running` indicates that the motor is - powered. `ramping` indicates that the motor has not yet reached the - `duty_cycle_sp`. - """ - self._state, value = self.get_attr_set(self._state, 'state') - return value - - @property - def stop_action(self): - """ - Sets the stop action that will be used when the motor stops. Read - `stop_actions` to get the list of valid values. - """ - raise Exception("stop_action is a write-only property!") - - @stop_action.setter - def stop_action(self, value): - self._stop_action = self.set_attr_string(self._stop_action, 'stop_action', value) - - @property - def stop_actions(self): - """ - Gets a list of stop actions. Valid values are `coast` - and `brake`. - """ - self._stop_actions, value = self.get_attr_set(self._stop_actions, 'stop_actions') - return value - - @property - def time_sp(self): - """ - Writing specifies the amount of time the motor will run when using the - `run-timed` command. Reading returns the current value. Units are in - milliseconds. - """ - self._time_sp, value = self.get_attr_int(self._time_sp, 'time_sp') - return value - - @time_sp.setter - def time_sp(self, value): - self._time_sp = self.set_attr_int(self._time_sp, 'time_sp', value) - - -# ~autogen -# ~autogen generic-property-value classes.dcMotor>currentClass - - #: Run the motor until another command is sent. - COMMAND_RUN_FOREVER = 'run-forever' - - #: Run the motor for the amount of time specified in `time_sp` - #: and then stop the motor using the action specified by `stop_action`. - COMMAND_RUN_TIMED = 'run-timed' - - #: Run the motor at the duty cycle specified by `duty_cycle_sp`. - #: Unlike other run commands, changing `duty_cycle_sp` while running *will* - #: take effect immediately. - COMMAND_RUN_DIRECT = 'run-direct' - - #: Stop any of the run commands before they are complete using the - #: action specified by `stop_action`. - COMMAND_STOP = 'stop' - - #: With `normal` polarity, a positive duty cycle will - #: cause the motor to rotate clockwise. - POLARITY_NORMAL = 'normal' - - #: With `inversed` polarity, a positive duty cycle will - #: cause the motor to rotate counter-clockwise. - POLARITY_INVERSED = 'inversed' - - #: Power will be removed from the motor and it will freely coast to a stop. - STOP_ACTION_COAST = 'coast' - - #: Power will be removed from the motor and a passive electrical load will - #: be placed on the motor. This is usually done by shorting the motor terminals - #: together. This load will absorb the energy from the rotation of the motors and - #: cause the motor to stop more quickly than coasting. - STOP_ACTION_BRAKE = 'brake' - - -# ~autogen -# ~autogen motor_commands classes.dcMotor>currentClass - - def run_forever(self, **kwargs): - """Run the motor until another command is sent. - """ - for key in kwargs: - setattr(self, key, kwargs[key]) - self.command = self.COMMAND_RUN_FOREVER - - def run_timed(self, **kwargs): - """Run the motor for the amount of time specified in `time_sp` - and then stop the motor using the action specified by `stop_action`. - """ - for key in kwargs: - setattr(self, key, kwargs[key]) - self.command = self.COMMAND_RUN_TIMED - - def run_direct(self, **kwargs): - """Run the motor at the duty cycle specified by `duty_cycle_sp`. - Unlike other run commands, changing `duty_cycle_sp` while running *will* - take effect immediately. - """ - for key in kwargs: - setattr(self, key, kwargs[key]) - self.command = self.COMMAND_RUN_DIRECT - - def stop(self, **kwargs): - """Stop any of the run commands before they are complete using the - action specified by `stop_action`. - """ - for key in kwargs: - setattr(self, key, kwargs[key]) - self.command = self.COMMAND_STOP - - -# ~autogen -# ~autogen generic-class classes.servoMotor>currentClass - -class ServoMotor(Device): - - """ - The servo motor class provides a uniform interface for using hobby type - servo motors. - """ - - SYSTEM_CLASS_NAME = 'servo-motor' - SYSTEM_DEVICE_NAME_CONVENTION = 'motor*' - - def __init__(self, address=None, name_pattern=SYSTEM_DEVICE_NAME_CONVENTION, name_exact=False, **kwargs): - - if address is not None: - kwargs['address'] = address - super(ServoMotor, self).__init__(self.SYSTEM_CLASS_NAME, name_pattern, name_exact, **kwargs) - - self._address = None - self._command = None - self._driver_name = None - self._max_pulse_sp = None - self._mid_pulse_sp = None - self._min_pulse_sp = None - self._polarity = None - self._position_sp = None - self._rate_sp = None - self._state = None - -# ~autogen - - __slots__ = [ -# ~autogen generic-class-slots classes.servoMotor>currentClass - - '_address', - '_command', - '_driver_name', - '_max_pulse_sp', - '_mid_pulse_sp', - '_min_pulse_sp', - '_polarity', - '_position_sp', - '_rate_sp', - '_state', - -# ~autogen - ] - -# ~autogen generic-get-set classes.servoMotor>currentClass - - @property - def address(self): - """ - Returns the name of the port that this motor is connected to. - """ - self._address, value = self.get_attr_string(self._address, 'address') - return value - - @property - def command(self): - """ - Sets the command for the servo. Valid values are `run` and `float`. Setting - to `run` will cause the servo to be driven to the position_sp set in the - `position_sp` attribute. Setting to `float` will remove power from the motor. - """ - raise Exception("command is a write-only property!") - - @command.setter - def command(self, value): - self._command = self.set_attr_string(self._command, 'command', value) - - @property - def driver_name(self): - """ - Returns the name of the motor driver that loaded this device. See the list - of [supported devices] for a list of drivers. - """ - self._driver_name, value = self.get_attr_string(self._driver_name, 'driver_name') - return value - - @property - def max_pulse_sp(self): - """ - Used to set the pulse size in milliseconds for the signal that tells the - servo to drive to the maximum (clockwise) position_sp. Default value is 2400. - Valid values are 2300 to 2700. You must write to the position_sp attribute for - changes to this attribute to take effect. - """ - self._max_pulse_sp, value = self.get_attr_int(self._max_pulse_sp, 'max_pulse_sp') - return value - - @max_pulse_sp.setter - def max_pulse_sp(self, value): - self._max_pulse_sp = self.set_attr_int(self._max_pulse_sp, 'max_pulse_sp', value) - - @property - def mid_pulse_sp(self): - """ - Used to set the pulse size in milliseconds for the signal that tells the - servo to drive to the mid position_sp. Default value is 1500. Valid - values are 1300 to 1700. For example, on a 180 degree servo, this would be - 90 degrees. On continuous rotation servo, this is the 'neutral' position_sp - where the motor does not turn. You must write to the position_sp attribute for - changes to this attribute to take effect. - """ - self._mid_pulse_sp, value = self.get_attr_int(self._mid_pulse_sp, 'mid_pulse_sp') - return value - - @mid_pulse_sp.setter - def mid_pulse_sp(self, value): - self._mid_pulse_sp = self.set_attr_int(self._mid_pulse_sp, 'mid_pulse_sp', value) - - @property - def min_pulse_sp(self): - """ - Used to set the pulse size in milliseconds for the signal that tells the - servo to drive to the miniumum (counter-clockwise) position_sp. Default value - is 600. Valid values are 300 to 700. You must write to the position_sp - attribute for changes to this attribute to take effect. - """ - self._min_pulse_sp, value = self.get_attr_int(self._min_pulse_sp, 'min_pulse_sp') - return value - - @min_pulse_sp.setter - def min_pulse_sp(self, value): - self._min_pulse_sp = self.set_attr_int(self._min_pulse_sp, 'min_pulse_sp', value) - - @property - def polarity(self): - """ - Sets the polarity of the servo. Valid values are `normal` and `inversed`. - Setting the value to `inversed` will cause the position_sp value to be - inversed. i.e `-100` will correspond to `max_pulse_sp`, and `100` will - correspond to `min_pulse_sp`. - """ - self._polarity, value = self.get_attr_string(self._polarity, 'polarity') - return value - - @polarity.setter - def polarity(self, value): - self._polarity = self.set_attr_string(self._polarity, 'polarity', value) - - @property - def position_sp(self): - """ - Reading returns the current position_sp of the servo. Writing instructs the - servo to move to the specified position_sp. Units are percent. Valid values - are -100 to 100 (-100% to 100%) where `-100` corresponds to `min_pulse_sp`, - `0` corresponds to `mid_pulse_sp` and `100` corresponds to `max_pulse_sp`. - """ - self._position_sp, value = self.get_attr_int(self._position_sp, 'position_sp') - return value - - @position_sp.setter - def position_sp(self, value): - self._position_sp = self.set_attr_int(self._position_sp, 'position_sp', value) - - @property - def rate_sp(self): - """ - Sets the rate_sp at which the servo travels from 0 to 100.0% (half of the full - range of the servo). Units are in milliseconds. Example: Setting the rate_sp - to 1000 means that it will take a 180 degree servo 2 second to move from 0 - to 180 degrees. Note: Some servo controllers may not support this in which - case reading and writing will fail with `-EOPNOTSUPP`. In continuous rotation - servos, this value will affect the rate_sp at which the speed ramps up or down. - """ - self._rate_sp, value = self.get_attr_int(self._rate_sp, 'rate_sp') - return value - - @rate_sp.setter - def rate_sp(self, value): - self._rate_sp = self.set_attr_int(self._rate_sp, 'rate_sp', value) - - @property - def state(self): - """ - Returns a list of flags indicating the state of the servo. - Possible values are: - * `running`: Indicates that the motor is powered. - """ - self._state, value = self.get_attr_set(self._state, 'state') - return value - - -# ~autogen -# ~autogen generic-property-value classes.servoMotor>currentClass - - #: Drive servo to the position set in the `position_sp` attribute. - COMMAND_RUN = 'run' - - #: Remove power from the motor. - COMMAND_FLOAT = 'float' - - #: With `normal` polarity, a positive duty cycle will - #: cause the motor to rotate clockwise. - POLARITY_NORMAL = 'normal' - - #: With `inversed` polarity, a positive duty cycle will - #: cause the motor to rotate counter-clockwise. - POLARITY_INVERSED = 'inversed' - - -# ~autogen -# ~autogen motor_commands classes.servoMotor>currentClass - - def run(self, **kwargs): - """Drive servo to the position set in the `position_sp` attribute. - """ - for key in kwargs: - setattr(self, key, kwargs[key]) - self.command = self.COMMAND_RUN - - def float(self, **kwargs): - """Remove power from the motor. - """ - for key in kwargs: - setattr(self, key, kwargs[key]) - self.command = self.COMMAND_FLOAT - - -# ~autogen -# ~autogen generic-class classes.sensor>currentClass - -class Sensor(Device): - - """ - The sensor class provides a uniform interface for using most of the - sensors available for the EV3. The various underlying device drivers will - create a `lego-sensor` device for interacting with the sensors. - - Sensors are primarily controlled by setting the `mode` and monitored by - reading the `value` attributes. Values can be converted to floating point - if needed by `value` / 10.0 ^ `decimals`. - - Since the name of the `sensor` device node does not correspond to the port - that a sensor is plugged in to, you must look at the `address` attribute if - you need to know which port a sensor is plugged in to. However, if you don't - have more than one sensor of each type, you can just look for a matching - `driver_name`. Then it will not matter which port a sensor is plugged in to - your - program will still work. - """ - - SYSTEM_CLASS_NAME = 'lego-sensor' - SYSTEM_DEVICE_NAME_CONVENTION = 'sensor*' - - def __init__(self, address=None, name_pattern=SYSTEM_DEVICE_NAME_CONVENTION, name_exact=False, **kwargs): - - if address is not None: - kwargs['address'] = address - super(Sensor, self).__init__(self.SYSTEM_CLASS_NAME, name_pattern, name_exact, **kwargs) - - self._address = None - self._command = None - self._commands = None - self._decimals = None - self._driver_name = None - self._mode = None - self._modes = None - self._num_values = None - self._units = None - -# ~autogen - - self._value = [None,None,None,None,None,None,None,None] - - self._bin_data_format = None - self._bin_data_size = None - self._bin_data = None - self._mode_scale = {} - - __slots__ = [ -# ~autogen generic-class-slots classes.sensor>currentClass - - '_address', - '_command', - '_commands', - '_decimals', - '_driver_name', - '_mode', - '_modes', - '_num_values', - '_units', - -# ~autogen - '_value', - '_bin_data_format', - '_bin_data_size', - '_bin_data', - '_mode_scale' - ] - - def _scale(self, mode): - """ - Returns value scaling coefficient for the given mode. - """ - if mode in self._mode_scale: - scale = self._mode_scale[mode] - else: - scale = 10**(-self.decimals) - self._mode_scale[mode] = scale - - return scale - -# ~autogen generic-get-set classes.sensor>currentClass - - @property - def address(self): - """ - Returns the name of the port that the sensor is connected to, e.g. `ev3:in1`. - I2C sensors also include the I2C address (decimal), e.g. `ev3:in1:i2c8`. - """ - self._address, value = self.get_attr_string(self._address, 'address') - return value - - @property - def command(self): - """ - Sends a command to the sensor. - """ - raise Exception("command is a write-only property!") - - @command.setter - def command(self, value): - self._command = self.set_attr_string(self._command, 'command', value) - - @property - def commands(self): - """ - Returns a list of the valid commands for the sensor. - Returns -EOPNOTSUPP if no commands are supported. - """ - self._commands, value = self.get_attr_set(self._commands, 'commands') - return value - - @property - def decimals(self): - """ - Returns the number of decimal places for the values in the `value` - attributes of the current mode. - """ - self._decimals, value = self.get_attr_int(self._decimals, 'decimals') - return value - - @property - def driver_name(self): - """ - Returns the name of the sensor device/driver. See the list of [supported - sensors] for a complete list of drivers. - """ - self._driver_name, value = self.get_attr_string(self._driver_name, 'driver_name') - return value - - @property - def mode(self): - """ - Returns the current mode. Writing one of the values returned by `modes` - sets the sensor to that mode. - """ - self._mode, value = self.get_attr_string(self._mode, 'mode') - return value - - @mode.setter - def mode(self, value): - self._mode = self.set_attr_string(self._mode, 'mode', value) - - @property - def modes(self): - """ - Returns a list of the valid modes for the sensor. - """ - self._modes, value = self.get_attr_set(self._modes, 'modes') - return value - - @property - def num_values(self): - """ - Returns the number of `value` attributes that will return a valid value - for the current mode. - """ - self._num_values, value = self.get_attr_int(self._num_values, 'num_values') - return value - - @property - def units(self): - """ - Returns the units of the measured value for the current mode. May return - empty string - """ - self._units, value = self.get_attr_string(self._units, 'units') - return value - - -# ~autogen - - def value(self, n=0): - """ - Returns the value or values measured by the sensor. Check num_values to - see how many values there are. Values with N >= num_values will return - an error. The values are fixed point numbers, so check decimals to see - if you need to divide to get the actual value. - """ -# if isinstance(n, numbers.Integral): -# n = '{0:d}'.format(n) -# elif isinstance(n, numbers.Real): - if isinstance(n, numbers.Real): -# n = '{0:.0f}'.format(n) - n = int(n) - elif isinstance(n, str): - n = int(n) - - self._value[n], value = self.get_attr_int(self._value[n], 'value'+str(n)) - return value - - @property - def bin_data_format(self): - """ - Returns the format of the values in `bin_data` for the current mode. - Possible values are: - - - `u8`: Unsigned 8-bit integer (byte) - - `s8`: Signed 8-bit integer (sbyte) - - `u16`: Unsigned 16-bit integer (ushort) - - `s16`: Signed 16-bit integer (short) - - `s16_be`: Signed 16-bit integer, big endian - - `s32`: Signed 32-bit integer (int) - - `float`: IEEE 754 32-bit floating point (float) - """ - self._bin_data_format, value = self.get_attr_string(self._bin_data_format, 'bin_data_format') - return value - - def bin_data(self, fmt=None): - """ - Returns the unscaled raw values in the `value` attributes as raw byte - array. Use `bin_data_format`, `num_values` and the individual sensor - documentation to determine how to interpret the data. - - Use `fmt` to unpack the raw bytes into a struct. - - Example:: - - >>> from ev3dev import * - >>> ir = InfraredSensor() - >>> ir.value() - 28 - >>> ir.bin_data('currentClass - -class I2cSensor(Sensor): - - """ - A generic interface to control I2C-type EV3 sensors. - """ - - SYSTEM_CLASS_NAME = Sensor.SYSTEM_CLASS_NAME - SYSTEM_DEVICE_NAME_CONVENTION = 'sensor*' - - def __init__(self, address=None, name_pattern=SYSTEM_DEVICE_NAME_CONVENTION, name_exact=False, **kwargs): - - super(I2cSensor, self).__init__(address, name_pattern, name_exact, driver_name=['nxt-i2c-sensor'], **kwargs) - - self._fw_version = None - self._poll_ms = None - -# ~autogen -# ~autogen generic-get-set classes.i2cSensor>currentClass - - @property - def fw_version(self): - """ - Returns the firmware version of the sensor if available. Currently only - I2C/NXT sensors support this. - """ - self._fw_version, value = self.get_attr_string(self._fw_version, 'fw_version') - return value - - @property - def poll_ms(self): - """ - Returns the polling period of the sensor in milliseconds. Writing sets the - polling period. Setting to 0 disables polling. Minimum value is hard - coded as 50 msec. Returns -EOPNOTSUPP if changing polling is not supported. - Currently only I2C/NXT sensors support changing the polling period. - """ - self._poll_ms, value = self.get_attr_int(self._poll_ms, 'poll_ms') - return value - - @poll_ms.setter - def poll_ms(self, value): - self._poll_ms = self.set_attr_int(self._poll_ms, 'poll_ms', value) - - -# ~autogen -# ~autogen special-sensors - -class TouchSensor(Sensor): - - """ - Touch Sensor - """ - - __slots__ = ['auto_mode'] - - SYSTEM_CLASS_NAME = Sensor.SYSTEM_CLASS_NAME - SYSTEM_DEVICE_NAME_CONVENTION = Sensor.SYSTEM_DEVICE_NAME_CONVENTION - - def __init__(self, address=None, name_pattern=SYSTEM_DEVICE_NAME_CONVENTION, name_exact=False, **kwargs): - super(TouchSensor, self).__init__(address, name_pattern, name_exact, driver_name=['lego-ev3-touch', 'lego-nxt-touch'], **kwargs) - self.auto_mode = True - - - #: Button state - MODE_TOUCH = 'TOUCH' - - - MODES = ( - 'TOUCH', - ) - - - @property - def is_pressed(self): - """ - A boolean indicating whether the current touch sensor is being - pressed. - """ - - if self.auto_mode: - self.mode = self.MODE_TOUCH - - return self.value(0) - -class ColorSensor(Sensor): - - """ - LEGO EV3 color sensor. - """ - - __slots__ = ['auto_mode'] - - SYSTEM_CLASS_NAME = Sensor.SYSTEM_CLASS_NAME - SYSTEM_DEVICE_NAME_CONVENTION = Sensor.SYSTEM_DEVICE_NAME_CONVENTION - - def __init__(self, address=None, name_pattern=SYSTEM_DEVICE_NAME_CONVENTION, name_exact=False, **kwargs): - super(ColorSensor, self).__init__(address, name_pattern, name_exact, driver_name=['lego-ev3-color'], **kwargs) - self.auto_mode = True - - - #: Reflected light. Red LED on. - MODE_COL_REFLECT = 'COL-REFLECT' - - #: Ambient light. Red LEDs off. - MODE_COL_AMBIENT = 'COL-AMBIENT' - - #: Color. All LEDs rapidly cycling, appears white. - MODE_COL_COLOR = 'COL-COLOR' - - #: Raw reflected. Red LED on - MODE_REF_RAW = 'REF-RAW' - - #: Raw Color Components. All LEDs rapidly cycling, appears white. - MODE_RGB_RAW = 'RGB-RAW' - - #: No color. - COLOR_NOCOLOR = 0 - - #: Black color. - COLOR_BLACK = 1 - - #: Blue color. - COLOR_BLUE = 2 - - #: Green color. - COLOR_GREEN = 3 - - #: Yellow color. - COLOR_YELLOW = 4 - - #: Red color. - COLOR_RED = 5 - - #: White color. - COLOR_WHITE = 6 - - #: Brown color. - COLOR_BROWN = 7 - - - MODES = ( - 'COL-REFLECT', - 'COL-AMBIENT', - 'COL-COLOR', - 'REF-RAW', - 'RGB-RAW', - ) - - COLORS = ( - 'NoColor', - 'Black', - 'Blue', - 'Green', - 'Yellow', - 'Red', - 'White', - 'Brown', - ) - - - @property - def reflected_light_intensity(self): - """ - Reflected light intensity as a percentage. Light on sensor is red. - """ - - if self.auto_mode: - self.mode = self.MODE_COL_REFLECT - - return self.value(0) - - @property - def ambient_light_intensity(self): - """ - Ambient light intensity. Light on sensor is dimly lit blue. - """ - - if self.auto_mode: - self.mode = self.MODE_COL_AMBIENT - - return self.value(0) - - @property - def color(self): - """ - Color detected by the sensor, categorized by overall value. - - 0: No color - - 1: Black - - 2: Blue - - 3: Green - - 4: Yellow - - 5: Red - - 6: White - - 7: Brown - """ - - if self.auto_mode: - self.mode = self.MODE_COL_COLOR - - return self.value(0) - - @property - def raw(self): - """ - Red, green, and blue components of the detected color, in the range 0-1020. - """ - - if self.auto_mode: - self.mode = self.MODE_RGB_RAW - - return self.value(0), self.value(1), self.value(2) - - @property - def red(self): - """ - Red component of the detected color, in the range 0-1020. - """ - - if self.auto_mode: - self.mode = self.MODE_RGB_RAW - - return self.value(0) - - @property - def green(self): - """ - Green component of the detected color, in the range 0-1020. - """ - - if self.auto_mode: - self.mode = self.MODE_RGB_RAW - - return self.value(1) - - @property - def blue(self): - """ - Blue component of the detected color, in the range 0-1020. - """ - - if self.auto_mode: - self.mode = self.MODE_RGB_RAW - - return self.value(2) - -class UltrasonicSensor(Sensor): - - """ - LEGO EV3 ultrasonic sensor. - """ - - __slots__ = ['auto_mode'] - - SYSTEM_CLASS_NAME = Sensor.SYSTEM_CLASS_NAME - SYSTEM_DEVICE_NAME_CONVENTION = Sensor.SYSTEM_DEVICE_NAME_CONVENTION - - def __init__(self, address=None, name_pattern=SYSTEM_DEVICE_NAME_CONVENTION, name_exact=False, **kwargs): - super(UltrasonicSensor, self).__init__(address, name_pattern, name_exact, driver_name=['lego-ev3-us', 'lego-nxt-us'], **kwargs) - self.auto_mode = True - - - #: Continuous measurement in centimeters. - MODE_US_DIST_CM = 'US-DIST-CM' - - #: Continuous measurement in inches. - MODE_US_DIST_IN = 'US-DIST-IN' - - #: Listen. - MODE_US_LISTEN = 'US-LISTEN' - - #: Single measurement in centimeters. - MODE_US_SI_CM = 'US-SI-CM' - - #: Single measurement in inches. - MODE_US_SI_IN = 'US-SI-IN' - - - MODES = ( - 'US-DIST-CM', - 'US-DIST-IN', - 'US-LISTEN', - 'US-SI-CM', - 'US-SI-IN', - ) - - - @property - def distance_centimeters(self): - """ - Measurement of the distance detected by the sensor, - in centimeters. - """ - - if self.auto_mode: - self.mode = self.MODE_US_DIST_CM - - return self.value(0) * self._scale('US_DIST_CM') - - @property - def distance_inches(self): - """ - Measurement of the distance detected by the sensor, - in inches. - """ - - if self.auto_mode: - self.mode = self.MODE_US_DIST_IN - - return self.value(0) * self._scale('US_DIST_IN') - - @property - def other_sensor_present(self): - """ - Value indicating whether another ultrasonic sensor could - be heard nearby. - """ - - if self.auto_mode: - self.mode = self.MODE_US_LISTEN - - return self.value(0) - -class GyroSensor(Sensor): - - """ - LEGO EV3 gyro sensor. - """ - - __slots__ = ['auto_mode'] - - SYSTEM_CLASS_NAME = Sensor.SYSTEM_CLASS_NAME - SYSTEM_DEVICE_NAME_CONVENTION = Sensor.SYSTEM_DEVICE_NAME_CONVENTION - - def __init__(self, address=None, name_pattern=SYSTEM_DEVICE_NAME_CONVENTION, name_exact=False, **kwargs): - super(GyroSensor, self).__init__(address, name_pattern, name_exact, driver_name=['lego-ev3-gyro'], **kwargs) - self.auto_mode = True - - - #: Angle - MODE_GYRO_ANG = 'GYRO-ANG' - - #: Rotational speed - MODE_GYRO_RATE = 'GYRO-RATE' - - #: Raw sensor value - MODE_GYRO_FAS = 'GYRO-FAS' - - #: Angle and rotational speed - MODE_GYRO_G_A = 'GYRO-G&A' - - #: Calibration ??? - MODE_GYRO_CAL = 'GYRO-CAL' - - - MODES = ( - 'GYRO-ANG', - 'GYRO-RATE', - 'GYRO-FAS', - 'GYRO-G&A', - 'GYRO-CAL', - ) - - - @property - def angle(self): - """ - The number of degrees that the sensor has been rotated - since it was put into this mode. - """ - - if self.auto_mode: - self.mode = self.MODE_GYRO_ANG - - return self.value(0) - - @property - def rate(self): - """ - The rate at which the sensor is rotating, in degrees/second. - """ - - if self.auto_mode: - self.mode = self.MODE_GYRO_RATE - - return self.value(0) - - @property - def rate_and_angle(self): - """ - Angle (degrees) and Rotational Speed (degrees/second). - """ - - if self.auto_mode: - self.mode = self.MODE_GYRO_G_A - - return self.value(0), self.value(1) - -class InfraredSensor(Sensor): - - """ - LEGO EV3 infrared sensor. - """ - - __slots__ = ['auto_mode'] - - SYSTEM_CLASS_NAME = Sensor.SYSTEM_CLASS_NAME - SYSTEM_DEVICE_NAME_CONVENTION = Sensor.SYSTEM_DEVICE_NAME_CONVENTION - - def __init__(self, address=None, name_pattern=SYSTEM_DEVICE_NAME_CONVENTION, name_exact=False, **kwargs): - super(InfraredSensor, self).__init__(address, name_pattern, name_exact, driver_name=['lego-ev3-ir'], **kwargs) - self.auto_mode = True - - - #: Proximity - MODE_IR_PROX = 'IR-PROX' - - #: IR Seeker - MODE_IR_SEEK = 'IR-SEEK' - - #: IR Remote Control - MODE_IR_REMOTE = 'IR-REMOTE' - - #: IR Remote Control. State of the buttons is coded in binary - MODE_IR_REM_A = 'IR-REM-A' - - #: Calibration ??? - MODE_IR_CAL = 'IR-CAL' - - - MODES = ( - 'IR-PROX', - 'IR-SEEK', - 'IR-REMOTE', - 'IR-REM-A', - 'IR-CAL', - ) - - - @property - def proximity(self): - """ - A measurement of the distance between the sensor and the remote, - as a percentage. 100% is approximately 70cm/27in. - """ - - if self.auto_mode: - self.mode = self.MODE_IR_PROX - - return self.value(0) - -class SoundSensor(Sensor): - - """ - LEGO NXT Sound Sensor - """ - - __slots__ = ['auto_mode'] - - SYSTEM_CLASS_NAME = Sensor.SYSTEM_CLASS_NAME - SYSTEM_DEVICE_NAME_CONVENTION = Sensor.SYSTEM_DEVICE_NAME_CONVENTION - - def __init__(self, address=None, name_pattern=SYSTEM_DEVICE_NAME_CONVENTION, name_exact=False, **kwargs): - super(SoundSensor, self).__init__(address, name_pattern, name_exact, driver_name=['lego-nxt-sound'], **kwargs) - self.auto_mode = True - - - #: Sound pressure level. Flat weighting - MODE_DB = 'DB' - - #: Sound pressure level. A weighting - MODE_DBA = 'DBA' - - - MODES = ( - 'DB', - 'DBA', - ) - - - @property - def sound_pressure(self): - """ - A measurement of the measured sound pressure level, as a - percent. Uses a flat weighting. - """ - - if self.auto_mode: - self.mode = self.MODE_DB - - return self.value(0) * self._scale('DB') - - @property - def sound_pressure_low(self): - """ - A measurement of the measured sound pressure level, as a - percent. Uses A-weighting, which focuses on levels up to 55 dB. - """ - - if self.auto_mode: - self.mode = self.MODE_DBA - - return self.value(0) * self._scale('DBA') - -class LightSensor(Sensor): - - """ - LEGO NXT Light Sensor - """ - - __slots__ = ['auto_mode'] - - SYSTEM_CLASS_NAME = Sensor.SYSTEM_CLASS_NAME - SYSTEM_DEVICE_NAME_CONVENTION = Sensor.SYSTEM_DEVICE_NAME_CONVENTION - - def __init__(self, address=None, name_pattern=SYSTEM_DEVICE_NAME_CONVENTION, name_exact=False, **kwargs): - super(LightSensor, self).__init__(address, name_pattern, name_exact, driver_name=['lego-nxt-light'], **kwargs) - self.auto_mode = True - - - #: Reflected light. LED on - MODE_REFLECT = 'REFLECT' - - #: Ambient light. LED off - MODE_AMBIENT = 'AMBIENT' - - - MODES = ( - 'REFLECT', - 'AMBIENT', - ) - - - @property - def reflected_light_intensity(self): - """ - A measurement of the reflected light intensity, as a percentage. - """ - - if self.auto_mode: - self.mode = self.MODE_REFLECT - - return self.value(0) * self._scale('REFLECT') - - @property - def ambient_light_intensity(self): - """ - A measurement of the ambient light intensity, as a percentage. - """ - - if self.auto_mode: - self.mode = self.MODE_AMBIENT - - return self.value(0) * self._scale('AMBIENT') - - -# ~autogen - -# ~autogen generic-class classes.led>currentClass - -class Led(Device): - - """ - Any device controlled by the generic LED driver. - See https://www.kernel.org/doc/Documentation/leds/leds-class.txt - for more details. - """ - - SYSTEM_CLASS_NAME = 'leds' - SYSTEM_DEVICE_NAME_CONVENTION = '*' - - def __init__(self, address=None, name_pattern=SYSTEM_DEVICE_NAME_CONVENTION, name_exact=False, **kwargs): - - if address is not None: - kwargs['address'] = address - super(Led, self).__init__(self.SYSTEM_CLASS_NAME, name_pattern, name_exact, **kwargs) - - self._max_brightness = None - self._brightness = None - self._triggers = None - self._trigger = None - self._delay_on = None - self._delay_off = None - -# ~autogen - - __slots__ = [ -# ~autogen generic-class-slots classes.led>currentClass - - '_max_brightness', - '_brightness', - '_triggers', - '_trigger', - '_delay_on', - '_delay_off', - -# ~autogen - ] - -# ~autogen generic-get-set classes.led>currentClass - - @property - def max_brightness(self): - """ - Returns the maximum allowable brightness value. - """ - self._max_brightness, value = self.get_attr_int(self._max_brightness, 'max_brightness') - return value - - @property - def brightness(self): - """ - Sets the brightness level. Possible values are from 0 to `max_brightness`. - """ - self._brightness, value = self.get_attr_int(self._brightness, 'brightness') - return value - - @brightness.setter - def brightness(self, value): - self._brightness = self.set_attr_int(self._brightness, 'brightness', value) - - @property - def triggers(self): - """ - Returns a list of available triggers. - """ - self._triggers, value = self.get_attr_set(self._triggers, 'trigger') - return value - - - -# ~autogen - - @property - def trigger(self): - """ - Sets the led trigger. A trigger - is a kernel based source of led events. Triggers can either be simple or - complex. A simple trigger isn't configurable and is designed to slot into - existing subsystems with minimal additional code. Examples are the `ide-disk` and - `nand-disk` triggers. - - Complex triggers whilst available to all LEDs have LED specific - parameters and work on a per LED basis. The `timer` trigger is an example. - The `timer` trigger will periodically change the LED brightness between - 0 and the current brightness setting. The `on` and `off` time can - be specified via `delay_{on,off}` attributes in milliseconds. - You can change the brightness value of a LED independently of the timer - trigger. However, if you set the brightness value to 0 it will - also disable the `timer` trigger. - """ - self._trigger, value = self.get_attr_from_set(self._trigger, 'trigger') - return value - - @trigger.setter - def trigger(self, value): - self._trigger = self.set_attr_string(self._trigger, 'trigger', value) - - # Workaround for ev3dev/ev3dev#225. - # When trigger is set to 'timer', we need to wait for 'delay_on' and - # 'delay_off' attributes to appear with correct permissions. - if value == 'timer': - for attr in ('delay_on', 'delay_off'): - path = self._path + '/' + attr - - # Make sure the file has been created: - for _ in range(5): - if os.path.exists(path): - break - time.sleep(0.2) - else: - raise Exception('"{}" attribute has not been created'.format(attr)) - - # Make sure the file has correct permissions: - for _ in range(5): - mode = stat.S_IMODE(os.stat(path)[stat.ST_MODE]) - if mode & stat.S_IRGRP and mode & stat.S_IWGRP: - break - time.sleep(0.2) - else: - raise Exception('"{}" attribute has wrong permissions'.format(attr)) - - - @property - def delay_on(self): - """ - The `timer` trigger will periodically change the LED brightness between - 0 and the current brightness setting. The `on` time can - be specified via `delay_on` attribute in milliseconds. - """ - - # Workaround for ev3dev/ev3dev#225. - # 'delay_on' and 'delay_off' attributes are created when trigger is set - # to 'timer', and destroyed when it is set to anything else. - # This means the file cache may become outdated, and we may have to - # reopen the file. - for retry in (True, False): - try: - self._delay_on, value = self.get_attr_int(self._delay_on, 'delay_on') - return value - except OSError: - if retry: - self._delay_on = None - else: - raise - - @delay_on.setter - def delay_on(self, value): - # Workaround for ev3dev/ev3dev#225. - # 'delay_on' and 'delay_off' attributes are created when trigger is set - # to 'timer', and destroyed when it is set to anything else. - # This means the file cache may become outdated, and we may have to - # reopen the file. - for retry in (True, False): - try: - self._delay_on = self.set_attr_int(self._delay_on, 'delay_on', value) - return - except OSError: - if retry: - self._delay_on = None - else: - raise - - @property - def delay_off(self): - """ - The `timer` trigger will periodically change the LED brightness between - 0 and the current brightness setting. The `off` time can - be specified via `delay_off` attribute in milliseconds. - """ - - # Workaround for ev3dev/ev3dev#225. - # 'delay_on' and 'delay_off' attributes are created when trigger is set - # to 'timer', and destroyed when it is set to anything else. - # This means the file cache may become outdated, and we may have to - # reopen the file. - for retry in (True, False): - try: - self._delay_off, value = self.get_attr_int(self._delay_off, 'delay_off') - return value - except OSError: - if retry: - self._delay_off = None - else: - raise - - @delay_off.setter - def delay_off(self, value): - # Workaround for ev3dev/ev3dev#225. - # 'delay_on' and 'delay_off' attributes are created when trigger is set - # to 'timer', and destroyed when it is set to anything else. - # This means the file cache may become outdated, and we may have to - # reopen the file. - for retry in (True, False): - try: - self._delay_off = self.set_attr_int(self._delay_off, 'delay_off', value) - return - except OSError: - if retry: - self._delay_off = None - else: - raise - - - @property - def brightness_pct(self): - """ - Returns led brightness as a fraction of max_brightness - """ - return float(self.brightness) / self.max_brightness - - @brightness_pct.setter - def brightness_pct(self, value): - self.brightness = value * self.max_brightness - - -class ButtonBase(object): - """ - Abstract button interface. - """ - - def __str__(self): - return self.__class__.__name__ - - @staticmethod - def on_change(changed_buttons): - """ - This handler is called by `process()` whenever state of any button has - changed since last `process()` call. `changed_buttons` is a list of - tuples of changed button names and their states. - """ - pass - - _state = set([]) - - def any(self): - """ - Checks if any button is pressed. - """ - return bool(self.buttons_pressed) - - def check_buttons(self, buttons=[]): - """ - Check if currently pressed buttons exactly match the given list. - """ - return set(self.buttons_pressed) == set(buttons) - - def process(self): - """ - Check for currenly pressed buttons. If the new state differs from the - old state, call the appropriate button event handlers. - """ - new_state = set(self.buttons_pressed) - old_state = self._state - self._state = new_state - - state_diff = new_state.symmetric_difference(old_state) - for button in state_diff: - handler = getattr(self, 'on_' + button) - if handler is not None: handler(button in new_state) - - if self.on_change is not None and state_diff: - self.on_change([(button, button in new_state) for button in state_diff]) - - @property - def buttons_pressed(self): - raise NotImplementedError() - - -class ButtonEVIO(ButtonBase): - - """ - Provides a generic button reading mechanism that works with event interface - and may be adapted to platform specific implementations. - - This implementation depends on the availability of the EVIOCGKEY ioctl - to be able to read the button state buffer. See Linux kernel source - in /include/uapi/linux/input.h for details. - """ - - KEY_MAX = 0x2FF - KEY_BUF_LEN = int((KEY_MAX + 7) / 8) - EVIOCGKEY = (2 << (14 + 8 + 8) | KEY_BUF_LEN << (8 + 8) | ord('E') << 8 | 0x18) - - _buttons = {} - - def __init__(self): - self._file_cache = {} - self._buffer_cache = {} - for b in self._buttons: - name = self._buttons[b]['name'] - if name not in self._file_cache: - self._file_cache[name] = open(name, 'rb', 0) - self._buffer_cache[name] = array.array('B', [0] * self.KEY_BUF_LEN) - - def _button_file(self, name): - return self._file_cache[name] - - def _button_buffer(self, name): - return self._buffer_cache[name] - - @property - def buttons_pressed(self): - """ - Returns list of names of pressed buttons. - """ - for b in self._buffer_cache: - fcntl.ioctl(self._button_file(b), self.EVIOCGKEY, self._buffer_cache[b]) - - pressed = [] - for k, v in self._buttons.items(): - buf = self._buffer_cache[v['name']] - bit = v['value'] - if bool(buf[int(bit / 8)] & 1 << bit % 8): - pressed += [k] - return pressed - - -# ~autogen remote-control specialSensorTypes.infraredSensor.remoteControl>currentClass -class RemoteControl(ButtonBase): - """ - EV3 Remote Controller - """ - - _BUTTON_VALUES = { - 0: [], - 1: ['red_up'], - 2: ['red_down'], - 3: ['blue_up'], - 4: ['blue_down'], - 5: ['red_up', 'blue_up'], - 6: ['red_up', 'blue_down'], - 7: ['red_down', 'blue_up'], - 8: ['red_down', 'blue_down'], - 9: ['beacon'], - 10: ['red_up', 'red_down'], - 11: ['blue_up', 'blue_down'] - } - - #: Handles ``Red Up`` events. - on_red_up = None - - #: Handles ``Red Down`` events. - on_red_down = None - - #: Handles ``Blue Up`` events. - on_blue_up = None - - #: Handles ``Blue Down`` events. - on_blue_down = None - - #: Handles ``Beacon`` events. - on_beacon = None - - - @property - def red_up(self): - """ - Checks if `red_up` button is pressed. - """ - return 'red_up' in self.buttons_pressed - - @property - def red_down(self): - """ - Checks if `red_down` button is pressed. - """ - return 'red_down' in self.buttons_pressed - - @property - def blue_up(self): - """ - Checks if `blue_up` button is pressed. - """ - return 'blue_up' in self.buttons_pressed - - @property - def blue_down(self): - """ - Checks if `blue_down` button is pressed. - """ - return 'blue_down' in self.buttons_pressed - - @property - def beacon(self): - """ - Checks if `beacon` button is pressed. - """ - return 'beacon' in self.buttons_pressed - - -# ~autogen - - def __init__(self, sensor=None, channel=1): - if sensor is None: - self._sensor = InfraredSensor() - else: - self._sensor = sensor - - self._channel = max(1, min(4, channel)) - 1 - self._state = set([]) - - if self._sensor.connected: - self._sensor.mode = 'IR-REMOTE' - - @property - def connected(self): - return self._sensor.connected - - @property - def buttons_pressed(self): - """ - Returns list of currently pressed buttons. - """ - return RemoteControl._BUTTON_VALUES.get(self._sensor.value(self._channel), []) - - -class BeaconSeeker(object): - """ - Seeks EV3 Remote Controller in beacon mode. - """ - - def __init__(self, sensor=None, channel=1): - self._sensor = InfraredSensor() if sensor is None else sensor - self._channel = max(1, min(4, channel)) - 1 - - if self._sensor.connected: - self._sensor.mode = 'IR-SEEK' - - @property - def heading(self): - """ - Returns heading (-25, 25) to the beacon on the given channel. - """ - return self._sensor.value(self._channel * 2) - - @property - def distance(self): - """ - Returns distance (0, 100) to the beacon on the given channel. - Returns -128 when beacon is not found. - """ - return self._sensor.value(self._channel * 2 + 1) - - @property - def heading_and_distance(self): - """ - Returns heading and distance to the beacon on the given channel as a - tuple. - """ - return self._sensor.value(self._channel * 2), self._sensor.value(self._channel * 2 + 1) - - -# ~autogen generic-class classes.powerSupply>currentClass - -class PowerSupply(Device): - - """ - A generic interface to read data from the system's power_supply class. - Uses the built-in legoev3-battery if none is specified. - """ - - SYSTEM_CLASS_NAME = 'power_supply' - SYSTEM_DEVICE_NAME_CONVENTION = '*' - - def __init__(self, address=None, name_pattern=SYSTEM_DEVICE_NAME_CONVENTION, name_exact=False, **kwargs): - - if address is not None: - kwargs['address'] = address - super(PowerSupply, self).__init__(self.SYSTEM_CLASS_NAME, name_pattern, name_exact, **kwargs) - - self._measured_current = None - self._measured_voltage = None - self._max_voltage = None - self._min_voltage = None - self._technology = None - self._type = None - -# ~autogen - - __slots__ = [ -# ~autogen generic-class-slots classes.powerSupply>currentClass - - '_measured_current', - '_measured_voltage', - '_max_voltage', - '_min_voltage', - '_technology', - '_type', - -# ~autogen - ] - -# ~autogen generic-get-set classes.powerSupply>currentClass - - @property - def measured_current(self): - """ - The measured current that the battery is supplying (in microamps) - """ - self._measured_current, value = self.get_attr_int(self._measured_current, 'current_now') - return value - - @property - def measured_voltage(self): - """ - The measured voltage that the battery is supplying (in microvolts) - """ - self._measured_voltage, value = self.get_attr_int(self._measured_voltage, 'voltage_now') - return value - - @property - def max_voltage(self): - """ - """ - self._max_voltage, value = self.get_attr_int(self._max_voltage, 'voltage_max_design') - return value - - @property - def min_voltage(self): - """ - """ - self._min_voltage, value = self.get_attr_int(self._min_voltage, 'voltage_min_design') - return value - - @property - def technology(self): - """ - """ - self._technology, value = self.get_attr_string(self._technology, 'technology') - return value - - @property - def type(self): - """ - """ - self._type, value = self.get_attr_string(self._type, 'type') - return value - - -# ~autogen - - @property - def measured_amps(self): - """ - The measured current that the battery is supplying (in amps) - """ - return self.measured_current / 1e6 - - @property - def measured_volts(self): - """ - The measured voltage that the battery is supplying (in volts) - """ - return self.measured_voltage / 1e6 - - -# ~autogen generic-class classes.legoPort>currentClass - -class LegoPort(Device): - - """ - The `lego-port` class provides an interface for working with input and - output ports that are compatible with LEGO MINDSTORMS RCX/NXT/EV3, LEGO - WeDo and LEGO Power Functions sensors and motors. Supported devices include - the LEGO MINDSTORMS EV3 Intelligent Brick, the LEGO WeDo USB hub and - various sensor multiplexers from 3rd party manufacturers. - - Some types of ports may have multiple modes of operation. For example, the - input ports on the EV3 brick can communicate with sensors using UART, I2C - or analog validate signals - but not all at the same time. Therefore there - are multiple modes available to connect to the different types of sensors. - - In most cases, ports are able to automatically detect what type of sensor - or motor is connected. In some cases though, this must be manually specified - using the `mode` and `set_device` attributes. The `mode` attribute affects - how the port communicates with the connected device. For example the input - ports on the EV3 brick can communicate using UART, I2C or analog voltages, - but not all at the same time, so the mode must be set to the one that is - appropriate for the connected sensor. The `set_device` attribute is used to - specify the exact type of sensor that is connected. Note: the mode must be - correctly set before setting the sensor type. - - Ports can be found at `/sys/class/lego-port/port` where `` is - incremented each time a new port is registered. Note: The number is not - related to the actual port at all - use the `address` attribute to find - a specific port. - """ - - SYSTEM_CLASS_NAME = 'lego-port' - SYSTEM_DEVICE_NAME_CONVENTION = '*' - - def __init__(self, address=None, name_pattern=SYSTEM_DEVICE_NAME_CONVENTION, name_exact=False, **kwargs): - - if address is not None: - kwargs['address'] = address - super(LegoPort, self).__init__(self.SYSTEM_CLASS_NAME, name_pattern, name_exact, **kwargs) - - self._address = None - self._driver_name = None - self._modes = None - self._mode = None - self._set_device = None - self._status = None - -# ~autogen - - __slots__ = [ -# ~autogen generic-class-slots classes.legoPort>currentClass - - '_address', - '_driver_name', - '_modes', - '_mode', - '_set_device', - '_status', - -# ~autogen - ] - -# ~autogen generic-get-set classes.legoPort>currentClass - - @property - def address(self): - """ - Returns the name of the port. See individual driver documentation for - the name that will be returned. - """ - self._address, value = self.get_attr_string(self._address, 'address') - return value - - @property - def driver_name(self): - """ - Returns the name of the driver that loaded this device. You can find the - complete list of drivers in the [list of port drivers]. - """ - self._driver_name, value = self.get_attr_string(self._driver_name, 'driver_name') - return value - - @property - def modes(self): - """ - Returns a list of the available modes of the port. - """ - self._modes, value = self.get_attr_set(self._modes, 'modes') - return value - - @property - def mode(self): - """ - Reading returns the currently selected mode. Writing sets the mode. - Generally speaking when the mode changes any sensor or motor devices - associated with the port will be removed new ones loaded, however this - this will depend on the individual driver implementing this class. - """ - self._mode, value = self.get_attr_string(self._mode, 'mode') - return value - - @mode.setter - def mode(self, value): - self._mode = self.set_attr_string(self._mode, 'mode', value) - - @property - def set_device(self): - """ - For modes that support it, writing the name of a driver will cause a new - device to be registered for that driver and attached to this port. For - example, since NXT/Analog sensors cannot be auto-detected, you must use - this attribute to load the correct driver. Returns -EOPNOTSUPP if setting a - device is not supported. - """ - raise Exception("set_device is a write-only property!") - - @set_device.setter - def set_device(self, value): - self._set_device = self.set_attr_string(self._set_device, 'set_device', value) - - @property - def status(self): - """ - In most cases, reading status will return the same value as `mode`. In - cases where there is an `auto` mode additional values may be returned, - such as `no-device` or `error`. See individual port driver documentation - for the full list of possible values. - """ - self._status, value = self.get_attr_string(self._status, 'status') - return value - - -# ~autogen - -class FbMem(object): - - """The framebuffer memory object. - - Made of: - - the framebuffer file descriptor - - the fix screen info struct - - the var screen info struct - - the mapped memory - """ - - # ------------------------------------------------------------------ - # The code is adapted from - # https://github.com/LinkCareServices/cairotft/blob/master/cairotft/linuxfb.py - # - # The original code came with the following license: - # ------------------------------------------------------------------ - # Copyright (c) 2012 Kurichan - # - # This program is free software. It comes without any warranty, to - # the extent permitted by applicable law. You can redistribute it - # and/or modify it under the terms of the Do What The Fuck You Want - # To Public License, Version 2, as published by Sam Hocevar. See - # http://sam.zoy.org/wtfpl/COPYING for more details. - # ------------------------------------------------------------------ - - __slots__ = ('fid', 'fix_info', 'var_info', 'mmap') - - FBIOGET_VSCREENINFO = 0x4600 - FBIOGET_FSCREENINFO = 0x4602 - - FB_VISUAL_MONO01 = 0 - FB_VISUAL_MONO10 = 1 - - class FixScreenInfo(ctypes.Structure): - - """The fb_fix_screeninfo from fb.h.""" - - _fields_ = [ - ('id_name', ctypes.c_char * 16), - ('smem_start', ctypes.c_ulong), - ('smem_len', ctypes.c_uint32), - ('type', ctypes.c_uint32), - ('type_aux', ctypes.c_uint32), - ('visual', ctypes.c_uint32), - ('xpanstep', ctypes.c_uint16), - ('ypanstep', ctypes.c_uint16), - ('ywrapstep', ctypes.c_uint16), - ('line_length', ctypes.c_uint32), - ('mmio_start', ctypes.c_ulong), - ('mmio_len', ctypes.c_uint32), - ('accel', ctypes.c_uint32), - ('reserved', ctypes.c_uint16 * 3), - ] - - class VarScreenInfo(ctypes.Structure): - - class FbBitField(ctypes.Structure): - - """The fb_bitfield struct from fb.h.""" - - _fields_ = [ - ('offset', ctypes.c_uint32), - ('length', ctypes.c_uint32), - ('msb_right', ctypes.c_uint32), - ] - - """The fb_var_screeninfo struct from fb.h.""" - - _fields_ = [ - ('xres', ctypes.c_uint32), - ('yres', ctypes.c_uint32), - ('xres_virtual', ctypes.c_uint32), - ('yres_virtual', ctypes.c_uint32), - ('xoffset', ctypes.c_uint32), - ('yoffset', ctypes.c_uint32), - - ('bits_per_pixel', ctypes.c_uint32), - ('grayscale', ctypes.c_uint32), - - ('red', FbBitField), - ('green', FbBitField), - ('blue', FbBitField), - ('transp', FbBitField), - ] - - def __init__(self, fbdev=None): - """Create the FbMem framebuffer memory object.""" - fid = FbMem._open_fbdev(fbdev) - fix_info = FbMem._get_fix_info(fid) - fbmmap = FbMem._map_fb_memory(fid, fix_info) - self.fid = fid - self.fix_info = fix_info - self.var_info = FbMem._get_var_info(fid) - self.mmap = fbmmap - - def __del__(self): - """Close the FbMem framebuffer memory object.""" - self.mmap.close() - FbMem._close_fbdev(self.fid) - - @staticmethod - def _open_fbdev(fbdev=None): - """Return the framebuffer file descriptor. - - Try to use the FRAMEBUFFER - environment variable if fbdev is not given. Use '/dev/fb0' by - default. - """ - dev = fbdev or os.getenv('FRAMEBUFFER', '/dev/fb0') - fbfid = os.open(dev, os.O_RDWR) - return fbfid - - @staticmethod - def _close_fbdev(fbfid): - """Close the framebuffer file descriptor.""" - os.close(fbfid) - - @staticmethod - def _get_fix_info(fbfid): - """Return the fix screen info from the framebuffer file descriptor.""" - fix_info = FbMem.FixScreenInfo() - fcntl.ioctl(fbfid, FbMem.FBIOGET_FSCREENINFO, fix_info) - return fix_info - - @staticmethod - def _get_var_info(fbfid): - """Return the var screen info from the framebuffer file descriptor.""" - var_info = FbMem.VarScreenInfo() - fcntl.ioctl(fbfid, FbMem.FBIOGET_VSCREENINFO, var_info) - return var_info - - @staticmethod - def _map_fb_memory(fbfid, fix_info): - """Map the framebuffer memory.""" - return mmap.mmap( - fbfid, - fix_info.smem_len, - mmap.MAP_SHARED, - mmap.PROT_READ | mmap.PROT_WRITE, - offset=0 - ) - - -class Screen(FbMem): - """ - A convenience wrapper for the FbMem class. - Provides drawing functions from the python imaging library (PIL). - """ - - def __init__(self): - from PIL import Image, ImageDraw - FbMem.__init__(self) - - self._img = Image.new( - self.var_info.bits_per_pixel == 1 and "1" or "RGB", - (self.fix_info.line_length * 8 // self.var_info.bits_per_pixel, self.yres), - "white") - - self._draw = ImageDraw.Draw(self._img) - - @property - def xres(self): - """ - Horizontal screen resolution - """ - return self.var_info.xres - - @property - def yres(self): - """ - Vertical screen resolution - """ - return self.var_info.yres - - @property - def shape(self): - """ - Dimensions of the screen. - """ - return (self.xres, self.yres) - - @property - def draw(self): - """ - Returns a handle to PIL.ImageDraw.Draw class associated with the screen. - - Example:: - - screen.draw.rectangle((10,10,60,20), fill='black') - """ - return self._draw - - @property - def image(self): - """ - Returns a handle to PIL.Image class that is backing the screen. This can - be accessed for blitting images to the screen. - - Example:: - - screen.image.paste(picture, (0, 0)) - """ - return self._img - - def clear(self): - """ - Clears the screen - """ - self._draw.rectangle(((0, 0), self.shape), fill="white") - - def _color565(self, r, g, b): - """Convert red, green, blue components to a 16-bit 565 RGB value. Components - should be values 0 to 255. - """ - return (((r & 0xF8) << 8) | ((g & 0xFC) << 3) | (b >> 3)) - - def _img_to_rgb565_bytes(self): - pixels = [self._color565(r, g, b) for (r, g, b) in self._img.getdata()] - return pack('H' * len(pixels), *pixels) - - def update(self): - """ - Applies pending changes to the screen. - Nothing will be drawn on the screen until this function is called. - """ - if self.var_info.bits_per_pixel == 1: - self.mmap[:] = self._img.tobytes("raw", "1;IR") - elif self.var_info.bits_per_pixel == 16: - self.mmap[:] = self._img_to_rgb565_bytes() - else: - raise Exception("Not supported") - - -def _make_scales(notes): - """ Utility function used by Sound class for building the note frequencies table """ - res = dict() - for note, freq in notes: - freq = round(freq) - for n in note.split('/'): - res[n] = freq - return res - - -class Sound: - """ - Sound-related functions. The class has only static methods and is not - intended for instantiation. It can beep, play wav files, or convert text to - speech. - - Note that all methods of the class spawn system processes and return - subprocess.Popen objects. The methods are asynchronous (they return - immediately after child process was spawned, without waiting for its - completion), but you can call wait() on the returned result. - - Examples:: - - # Play 'bark.wav', return immediately: - Sound.play('bark.wav') - - # Introduce yourself, wait for completion: - Sound.speak('Hello, I am Robot').wait() - - # Play a small song - Sound.play_song(( - ('D4', 'e3'), - ('D4', 'e3'), - ('D4', 'e3'), - ('G4', 'h'), - ('D5', 'h') - )) - """ - - channel = None - - @staticmethod - def beep(args=''): - """ - Call beep command with the provided arguments (if any). - See `beep man page`_ and google `linux beep music`_ for inspiration. - - .. _`beep man page`: https://linux.die.net/man/1/beep - .. _`linux beep music`: https://www.google.com/search?q=linux+beep+music - """ - with open(os.devnull, 'w') as n: - return Popen(shlex.split('/usr/bin/beep %s' % args), stdout=n) - - @staticmethod - def tone(*args): - """ - .. rubric:: tone(tone_sequence) - - Play tone sequence. The tone_sequence parameter is a list of tuples, - where each tuple contains up to three numbers. The first number is - frequency in Hz, the second is duration in milliseconds, and the third - is delay in milliseconds between this and the next tone in the - sequence. - - Here is a cheerful example:: - - Sound.tone([ - (392, 350, 100), (392, 350, 100), (392, 350, 100), (311.1, 250, 100), - (466.2, 25, 100), (392, 350, 100), (311.1, 250, 100), (466.2, 25, 100), - (392, 700, 100), (587.32, 350, 100), (587.32, 350, 100), - (587.32, 350, 100), (622.26, 250, 100), (466.2, 25, 100), - (369.99, 350, 100), (311.1, 250, 100), (466.2, 25, 100), (392, 700, 100), - (784, 350, 100), (392, 250, 100), (392, 25, 100), (784, 350, 100), - (739.98, 250, 100), (698.46, 25, 100), (659.26, 25, 100), - (622.26, 25, 100), (659.26, 50, 400), (415.3, 25, 200), (554.36, 350, 100), - (523.25, 250, 100), (493.88, 25, 100), (466.16, 25, 100), (440, 25, 100), - (466.16, 50, 400), (311.13, 25, 200), (369.99, 350, 100), - (311.13, 250, 100), (392, 25, 100), (466.16, 350, 100), (392, 250, 100), - (466.16, 25, 100), (587.32, 700, 100), (784, 350, 100), (392, 250, 100), - (392, 25, 100), (784, 350, 100), (739.98, 250, 100), (698.46, 25, 100), - (659.26, 25, 100), (622.26, 25, 100), (659.26, 50, 400), (415.3, 25, 200), - (554.36, 350, 100), (523.25, 250, 100), (493.88, 25, 100), - (466.16, 25, 100), (440, 25, 100), (466.16, 50, 400), (311.13, 25, 200), - (392, 350, 100), (311.13, 250, 100), (466.16, 25, 100), - (392.00, 300, 150), (311.13, 250, 100), (466.16, 25, 100), (392, 700) - ]).wait() - - .. rubric:: tone(frequency, duration) - - Play single tone of given frequency (Hz) and duration (milliseconds). - """ - def play_tone_sequence(tone_sequence): - def beep_args(frequency=None, duration=None, delay=None): - args = '' - if frequency is not None: args += '-f %s ' % frequency - if duration is not None: args += '-l %s ' % duration - if delay is not None: args += '-D %s ' % delay - - return args - - return Sound.beep(' -n '.join([beep_args(*t) for t in tone_sequence])) - - if len(args) == 1: - return play_tone_sequence(args[0]) - elif len(args) == 2: - return play_tone_sequence([(args[0], args[1])]) - else: - raise Exception("Unsupported number of parameters in Sound.tone()") - - @staticmethod - def play(wav_file): - """ - Play wav file. - """ - with open(os.devnull, 'w') as n: - return Popen(shlex.split('/usr/bin/aplay -q "%s"' % wav_file), stdout=n) - - @staticmethod - def speak(text, espeak_opts='-a 200 -s 130'): - """ - Speak the given text aloud. - """ - with open(os.devnull, 'w') as n: - cmd_line = '/usr/bin/espeak --stdout {0} "{1}"'.format(espeak_opts, text) - espeak = Popen(shlex.split(cmd_line), stdout=PIPE) - play = Popen(['/usr/bin/aplay', '-q'], stdin=espeak.stdout, stdout=n) - return espeak - - @staticmethod - def _get_channel(): - """ - :return: the detected sound channel - :rtype: str - """ - if Sound.channel is None: - # Get default channel as the first one that pops up in - # 'amixer scontrols' output, which contains strings in the - # following format: - # - # Simple mixer control 'Master',0 - # Simple mixer control 'Capture',0 - out = check_output(['amixer', 'scontrols']).decode() - m = re.search("'(?P[^']+)'", out) - if m: - Sound.channel = m.group('channel') - else: - Sound.channel = 'Playback' - - return Sound.channel - - @staticmethod - def set_volume(pct, channel=None): - """ - Sets the sound volume to the given percentage [0-100] by calling - ``amixer -q set %``. - If the channel is not specified, it tries to determine the default one - by running ``amixer scontrols``. If that fails as well, it uses the - ``Playback`` channel, as that is the only channel on the EV3. - """ - - if channel is None: - channel = Sound._get_channel() - - cmd_line = '/usr/bin/amixer -q set {0} {1:d}%'.format(channel, pct) - Popen(shlex.split(cmd_line)).wait() - - @staticmethod - def get_volume(channel=None): - """ - Gets the current sound volume by parsing the output of - ``amixer get ``. - If the channel is not specified, it tries to determine the default one - by running ``amixer scontrols``. If that fails as well, it uses the - ``Playback`` channel, as that is the only channel on the EV3. - """ - - if channel is None: - channel = Sound._get_channel() - - out = check_output(['amixer', 'get', channel]).decode() - m = re.search('\[(?P\d+)%\]', out) - if m: - return int(m.group('volume')) - else: - raise Exception('Failed to parse output of `amixer get {}`'.format(channel)) - - @classmethod - def play_song(cls, song, tempo=120, delay=50): - """ Plays a song provided as a list of tuples containing the note name and its - value using music conventional notation instead of numerical values for frequency - and duration. - - It supports symbolic notes (e.g. ``A4``, ``D#3``, ``Gb5``) and durations (e.g. ``q``, ``h``). - - For an exhaustive list of accepted note symbols and values, have a look at the :py:attr:`_NOTE_FREQUENCIES` - and :py:attr:`_NOTE_VALUES` private dictionaries in the source code. - - The value can be suffixed by modifiers: - - - a *divider* introduced by a ``/`` to obtain triplets for instance - (e.g. ``q/3`` for a triplet of eight note) - - a *multiplier* introduced by ``*`` (e.g. ``*1.5`` is a dotted note). - - Shortcuts exist for common modifiers: - - - ``3`` produces a triplet member note. For instance `e3` gives a triplet of eight notes, - i.e. 3 eight notes in the duration of a single quarter. You must ensure that 3 triplets - notes are defined in sequence to match the count, otherwise the result will not be the - expected one. - - ``.`` produces a dotted note, i.e. which duration is one and a half the base one. Double dots - are not currently supported. - - Example:: - - >>> # A long time ago in a galaxy far, - >>> # far away... - >>> Sound.play_song(( - >>> ('D4', 'e3'), # intro anacrouse - >>> ('D4', 'e3'), - >>> ('D4', 'e3'), - >>> ('G4', 'h'), # meas 1 - >>> ('D5', 'h'), - >>> ('C5', 'e3'), # meas 2 - >>> ('B4', 'e3'), - >>> ('A4', 'e3'), - >>> ('G5', 'h'), - >>> ('D5', 'q'), - >>> ('C5', 'e3'), # meas 3 - >>> ('B4', 'e3'), - >>> ('A4', 'e3'), - >>> ('G5', 'h'), - >>> ('D5', 'q'), - >>> ('C5', 'e3'), # meas 4 - >>> ('B4', 'e3'), - >>> ('C5', 'e3'), - >>> ('A4', 'h.'), - >>> )) - - .. important:: - - Only 4/4 signature songs are supported with respect to note durations. - - Args: - song (iterable[tuple(str, str)]): the song - tempo (int): the song tempo, given in quarters per minute - delay (int): delay in ms between notes - - Returns: - subprocess.Popen: the spawn subprocess - """ - meas_duration = 60000 / tempo * 4 - - def beep_args(note, value): - """ Builds the arguments string for producing a beep matching - the requested note and value. - - Args: - note (str): the note note and octave - value (str): the note value expression - Returns: - str: the arguments to be passed to the beep command - """ - freq = Sound._NOTE_FREQUENCIES[note.upper()] - if '/' in value: - base, factor = value.split('/') - duration = meas_duration * Sound._NOTE_VALUES[base] / float(factor) - elif '*' in value: - base, factor = value.split('*') - duration = meas_duration * Sound._NOTE_VALUES[base] * float(factor) - elif value.endswith('.'): - base = value[:-1] - duration = meas_duration * Sound._NOTE_VALUES[base] * 1.5 - elif value.endswith('3'): - base = value[:-1] - duration = meas_duration * Sound._NOTE_VALUES[base] * 2 / 3 - else: - duration = meas_duration * Sound._NOTE_VALUES[value] - - return '-f %d -l %d -D %d' % (freq, duration, delay) - - return Sound.beep(' -n '.join( - [beep_args(note, value) for note, value in song] - )) - - #: Note frequencies. - #: - #: This dictionary gives the rounded frequency of a note specified by its - #: standard US abbreviation and its octave number (e.g. ``C3``). - #: Alterations use the ``#`` and ``b`` symbols, respectively for - #: *sharp* and *flat*, between the note code and the octave number (e.g. ``D#4``, ``Gb5``). - _NOTE_FREQUENCIES = _make_scales(( - ('C0', 16.35), - ('C#0/Db0', 17.32), - ('D0', 18.35), - ('D#0/Eb0', 19.45), # expanded in one entry per symbol by _make_scales - ('E0', 20.60), - ('F0', 21.83), - ('F#0/Gb0', 23.12), - ('G0', 24.50), - ('G#0/Ab0', 25.96), - ('A0', 27.50), - ('A#0/Bb0', 29.14), - ('B0', 30.87), - ('C1', 32.70), - ('C#1/Db1', 34.65), - ('D1', 36.71), - ('D#1/Eb1', 38.89), - ('E1', 41.20), - ('F1', 43.65), - ('F#1/Gb1', 46.25), - ('G1', 49.00), - ('G#1/Ab1', 51.91), - ('A1', 55.00), - ('A#1/Bb1', 58.27), - ('B1', 61.74), - ('C2', 65.41), - ('C#2/Db2', 69.30), - ('D2', 73.42), - ('D#2/Eb2', 77.78), - ('E2', 82.41), - ('F2', 87.31), - ('F#2/Gb2', 92.50), - ('G2', 98.00), - ('G#2/Ab2', 103.83), - ('A2', 110.00), - ('A#2/Bb2', 116.54), - ('B2', 123.47), - ('C3', 130.81), - ('C#3/Db3', 138.59), - ('D3', 146.83), - ('D#3/Eb3', 155.56), - ('E3', 164.81), - ('F3', 174.61), - ('F#3/Gb3', 185.00), - ('G3', 196.00), - ('G#3/Ab3', 207.65), - ('A3', 220.00), - ('A#3/Bb3', 233.08), - ('B3', 246.94), - ('C4', 261.63), - ('C#4/Db4', 277.18), - ('D4', 293.66), - ('D#4/Eb4', 311.13), - ('E4', 329.63), - ('F4', 349.23), - ('F#4/Gb4', 369.99), - ('G4', 392.00), - ('G#4/Ab4', 415.30), - ('A4', 440.00), - ('A#4/Bb4', 466.16), - ('B4', 493.88), - ('C5', 523.25), - ('C#5/Db5', 554.37), - ('D5', 587.33), - ('D#5/Eb5', 622.25), - ('E5', 659.25), - ('F5', 698.46), - ('F#5/Gb5', 739.99), - ('G5', 783.99), - ('G#5/Ab5', 830.61), - ('A5', 880.00), - ('A#5/Bb5', 932.33), - ('B5', 987.77), - ('C6', 1046.50), - ('C#6/Db6', 1108.73), - ('D6', 1174.66), - ('D#6/Eb6', 1244.51), - ('E6', 1318.51), - ('F6', 1396.91), - ('F#6/Gb6', 1479.98), - ('G6', 1567.98), - ('G#6/Ab6', 1661.22), - ('A6', 1760.00), - ('A#6/Bb6', 1864.66), - ('B6', 1975.53), - ('C7', 2093.00), - ('C#7/Db7', 2217.46), - ('D7', 2349.32), - ('D#7/Eb7', 2489.02), - ('E7', 2637.02), - ('F7', 2793.83), - ('F#7/Gb7', 2959.96), - ('G7', 3135.96), - ('G#7/Ab7', 3322.44), - ('A7', 3520.00), - ('A#7/Bb7', 3729.31), - ('B7', 3951.07), - ('C8', 4186.01), - ('C#8/Db8', 4434.92), - ('D8', 4698.63), - ('D#8/Eb8', 4978.03), - ('E8', 5274.04), - ('F8', 5587.65), - ('F#8/Gb8', 5919.91), - ('G8', 6271.93), - ('G#8/Ab8', 6644.88), - ('A8', 7040.00), - ('A#8/Bb8', 7458.62), - ('B8', 7902.13) - )) - - #: Common note values. - #: - #: See https://en.wikipedia.org/wiki/Note_value - #: - #: This dictionary provides the multiplier to be applied to de whole note duration - #: to obtain subdivisions, given the corresponding symbolic identifier: - #: - #: = =============================== - #: w whole note (UK: semibreve) - #: h half note (UK: minim) - #: q quarter note (UK: crotchet) - #: e eight note (UK: quaver) - #: s sixteenth note (UK: semiquaver) - #: = =============================== - #: - #: - #: Triplets can be obtained by dividing the corresponding reference by 3. - #: For instance, the note value of a eight triplet will be ``NOTE_VALUE['e'] / 3``. - #: It is simpler however to user the ``3`` modifier of notes, as supported by the - #: :py:meth:`Sound.play_song` method. - _NOTE_VALUES = { - 'w': 1., - 'h': 1./2, - 'q': 1./4, - 'e': 1./8, - 's': 1./16, - } - - diff --git a/ev3dev/ev3.py b/ev3dev/ev3.py deleted file mode 100644 index 725f713..0000000 --- a/ev3dev/ev3.py +++ /dev/null @@ -1,219 +0,0 @@ -# -*- coding: utf-8 -*- - -# ------------------------------------------------------------------------------ -# Copyright (c) 2015 Eric Pascual -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NON INFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. -# ----------------------------------------------------------------------------- - -""" -An assortment of classes modeling specific features of the EV3 brick. -""" - -from .core import * - - -OUTPUT_A = 'outA' -OUTPUT_B = 'outB' -OUTPUT_C = 'outC' -OUTPUT_D = 'outD' - -INPUT_1 = 'in1' -INPUT_2 = 'in2' -INPUT_3 = 'in3' -INPUT_4 = 'in4' - - -class Leds(object): - """ - The EV3 LEDs. - """ - -# ~autogen led-colors platforms.ev3.led>currentClass - - red_left = Led(name_pattern='ev3:left:red:ev3dev') - red_right = Led(name_pattern='ev3:right:red:ev3dev') - green_left = Led(name_pattern='ev3:left:green:ev3dev') - green_right = Led(name_pattern='ev3:right:green:ev3dev') - - LEFT = ( red_left, green_left, ) - RIGHT = ( red_right, green_right, ) - - BLACK = ( 0, 0, ) - RED = ( 1, 0, ) - GREEN = ( 0, 1, ) - AMBER = ( 1, 1, ) - ORANGE = ( 1, 0.5, ) - YELLOW = ( 0.1, 1, ) - - @staticmethod - def set_color(group, color, pct=1): - """ - Sets brigthness of leds in the given group to the values specified in - color tuple. When percentage is specified, brightness of each led is - reduced proportionally. - - Example:: - - Leds.set_color(LEFT, AMBER) - """ - for l, v in zip(group, color): - l.brightness_pct = v * pct - - @staticmethod - def set(group, **kwargs): - """ - Set attributes for each led in group. - - Example:: - - Leds.set(LEFT, brightness_pct=0.5, trigger='timer') - """ - for led in group: - for k in kwargs: - setattr(led, k, kwargs[k]) - - @staticmethod - def all_off(): - """ - Turn all leds off - """ - Leds.red_left.brightness = 0 - Leds.red_right.brightness = 0 - Leds.green_left.brightness = 0 - Leds.green_right.brightness = 0 - - -# ~autogen - -class Button(ButtonEVIO): - """ - EV3 Buttons - """ - -# ~autogen button-property platforms.ev3.button>currentClass - - @staticmethod - def on_up(state): - """ - This handler is called by `process()` whenever state of 'up' button - has changed since last `process()` call. `state` parameter is the new - state of the button. - """ - pass - - @staticmethod - def on_down(state): - """ - This handler is called by `process()` whenever state of 'down' button - has changed since last `process()` call. `state` parameter is the new - state of the button. - """ - pass - - @staticmethod - def on_left(state): - """ - This handler is called by `process()` whenever state of 'left' button - has changed since last `process()` call. `state` parameter is the new - state of the button. - """ - pass - - @staticmethod - def on_right(state): - """ - This handler is called by `process()` whenever state of 'right' button - has changed since last `process()` call. `state` parameter is the new - state of the button. - """ - pass - - @staticmethod - def on_enter(state): - """ - This handler is called by `process()` whenever state of 'enter' button - has changed since last `process()` call. `state` parameter is the new - state of the button. - """ - pass - - @staticmethod - def on_backspace(state): - """ - This handler is called by `process()` whenever state of 'backspace' button - has changed since last `process()` call. `state` parameter is the new - state of the button. - """ - pass - - - _buttons = { - 'up': {'name': '/dev/input/by-path/platform-gpio-keys.0-event', 'value': 103}, - 'down': {'name': '/dev/input/by-path/platform-gpio-keys.0-event', 'value': 108}, - 'left': {'name': '/dev/input/by-path/platform-gpio-keys.0-event', 'value': 105}, - 'right': {'name': '/dev/input/by-path/platform-gpio-keys.0-event', 'value': 106}, - 'enter': {'name': '/dev/input/by-path/platform-gpio-keys.0-event', 'value': 28}, - 'backspace': {'name': '/dev/input/by-path/platform-gpio-keys.0-event', 'value': 14}, - } - - @property - def up(self): - """ - Check if 'up' button is pressed. - """ - return 'up' in self.buttons_pressed - - @property - def down(self): - """ - Check if 'down' button is pressed. - """ - return 'down' in self.buttons_pressed - - @property - def left(self): - """ - Check if 'left' button is pressed. - """ - return 'left' in self.buttons_pressed - - @property - def right(self): - """ - Check if 'right' button is pressed. - """ - return 'right' in self.buttons_pressed - - @property - def enter(self): - """ - Check if 'enter' button is pressed. - """ - return 'enter' in self.buttons_pressed - - @property - def backspace(self): - """ - Check if 'backspace' button is pressed. - """ - return 'backspace' in self.buttons_pressed - - -# ~autogen diff --git a/ev3dev/framebuffer.py b/ev3dev/framebuffer.py deleted file mode 100644 index 47ba870..0000000 --- a/ev3dev/framebuffer.py +++ /dev/null @@ -1,181 +0,0 @@ -#!/usr/bin/env python - -# framebuffer.py -# -# Helper class for handling framebuffers for ev3dev - -# The MIT License (MIT) -# -# Copyright (c) 2016 David Lechner -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in all -# copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -# SOFTWARE. - -from ctypes import Structure, c_char, c_ulong, c_uint16, c_uint32 -from enum import Enum -from fcntl import ioctl -from collections import namedtuple - -class FrameBuffer(object): - - # ioctls - _FBIOGET_VSCREENINFO = 0x4600 - _FBIOGET_FSCREENINFO = 0x4602 - _FBIOGET_CON2FBMAP = 0x460F - - class Type(Enum): - PACKED_PIXELS = 0 # Packed Pixels - PLANES = 1 # Non interleaved planes - INTERLEAVED_PLANES = 2 # Interleaved planes - TEXT = 3 # Text/attributes - VGA_PLANES = 4 # EGA/VGA planes - FOURCC = 5 # Type identified by a V4L2 FOURCC - - class Visual(Enum): - MONO01 = 0 # Monochrome 1=Black 0=White - MONO10 = 1 # Monochrome 1=White 0=Black - TRUECOLOR = 2 # True color - PSEUDOCOLOR = 3 # Pseudo color (like atari) - DIRECTCOLOR = 4 # Direct color - STATIC_PSEUDOCOLOR = 5 # Pseudo color readonly - FOURCC = 6 # Visual identified by a V4L2 FOURCC - - class _FixedScreenInfo(Structure): - _fields_ = [ - ('id', c_char * 16), # identification string eg "TT Builtin" - ('smem_start', c_ulong), # Start of frame buffer mem (physical address) - ('smem_len', c_uint32), # Length of frame buffer mem - ('type', c_uint32), # see FB_TYPE_* - ('type_aux', c_uint32), # Interleave for interleaved Planes - ('visual', c_uint32), # see FB_VISUAL_* - ('xpanstep', c_uint16), # zero if no hardware panning - ('ypanstep', c_uint16), # zero if no hardware panning - ('ywrapstep', c_uint16), # zero if no hardware ywrap - ('line_length', c_uint32), # length of a line in bytes - ('mmio_start', c_ulong), # Start of Memory Mapped I/O (physical address) - ('mmio_len', c_uint32), # Length of Memory Mapped I/O - ('accel', c_uint32), # Indicate to driver which specific chip/card we have - ('capabilities', c_uint16), # see FB_CAP_* - ('reserved', c_uint16 * 2), # Reserved for future compatibility - ] - - class _VariableScreenInfo(Structure): - - class _Bitfield(Structure): - _fields_ = [ - ('offset', c_uint32), # beginning of bitfield - ('length', c_uint32), # length of bitfield - ('msb_right', c_uint32), # != 0 : Most significant bit is right - ] - - _fields_ = [ - ('xres', c_uint32), # visible resolution - ('yres', c_uint32), - ('xres_virtual', c_uint32), # virtual resolution - ('yres_virtual', c_uint32), - ('xoffset', c_uint32), # offset from virtual to visible - ('yoffset', c_uint32), # resolution - ('bits_per_pixel', c_uint32), # guess what - ('grayscale', c_uint32), # 0 = color, 1 = grayscale, >1 = FOURCC - ('red', _Bitfield), # bitfield in fb mem if true color, - ('green', _Bitfield), # else only length is significant - ('blue', _Bitfield), - ('transp', _Bitfield), # transparency - ('nonstd', c_uint32), # != 0 Non standard pixel format - ('activate', c_uint32), # see FB_ACTIVATE_* - ('height', c_uint32), # height of picture in mm - ('width', c_uint32), # width of picture in mm - ('accel_flags', c_uint32), # (OBSOLETE) see fb_info.flags - # Timing: All values, in pixclocks, except pixclock (of course) - ('pixclock', c_uint32), # pixel clock in ps (pico seconds) - ('left_margin', c_uint32), # time from sync to picture - ('right_margin', c_uint32), # time from picture to sync - ('upper_margin', c_uint32), # time from sync to picture - ('lower_margin', c_uint32), - ('hsync_len', c_uint32), # length of horizontal sync - ('vsync_len', c_uint32), # length of vertical sync - ('sync', c_uint32), # see FB_SYNC_* - ('vmode', c_uint32), # see FB_VMODE_* - ('rotate', c_uint32), # angle we rotate counter clockwise - ('colorspace', c_uint32), # colorspace for FOURCC-based modes - ('reserved', c_uint32 * 4), # Reserved for future compatibility - ] - - class _Console2FrameBufferMap(Structure): - _fields_ = [ - ('console', c_uint32), - ('framebuffer', c_uint32), - ] - - def __init__(self, device='/dev/fb0'): - self._fd = open(device, mode='r+b', buffering=0) - self._fixed_info = self._FixedScreenInfo() - ioctl(self._fd, self._FBIOGET_FSCREENINFO, self._fixed_info) - self._variable_info = self._VariableScreenInfo() - ioctl(self._fd, self._FBIOGET_VSCREENINFO, self._variable_info) - - def close(self): - self._fd.close() - - def clear(self): - self._fd.seek(0) - self._fd.write(b'\0' * self._fixed_info.smem_len) - - def write_raw(self, data): - self._fd.seek(0) - self._fd.write(data) - - @staticmethod - def get_fb_for_console(console): - with open('/dev/fb0', mode='r+b') as fd: - m = FrameBuffer._Console2FrameBufferMap() - m.console = console - ioctl(fd, FrameBuffer._FBIOGET_CON2FBMAP, m) - return FrameBuffer('/dev/fb{}'.format(m.framebuffer)) - - @property - def type(self): - return self.Type(self._fixed_info.type) - - @property - def visual(self): - return self.Visual(self._fixed_info.visual) - - @property - def line_length(self): - return self._fixed_info.line_length - - @property - def resolution(self): - """Visible resolution""" - Resolution = namedtuple('Resolution', 'x y') - return Resolution(self._variable_info.xres, self._variable_info.yres) - - @property - def bits_per_pixel(self): - return self._variable_info.bits_per_pixel - - @property - def grayscale(self): - return self._variable_info.grayscale - - @property - def size(self): - """Size of picture in mm""" - Size = namedtuple('Size', 'width height') - return Size(self._variable_info.width, self._variable_info.height) diff --git a/ev3dev/helper.py b/ev3dev/helper.py deleted file mode 100644 index c44f3bc..0000000 --- a/ev3dev/helper.py +++ /dev/null @@ -1,265 +0,0 @@ -#!/usr/bin/env python3 - -import logging -import math -import os -import re -import sys -import time -import ev3dev.auto -from ev3dev.auto import (RemoteControl, list_motors, - INPUT_1, INPUT_2, INPUT_3, INPUT_4, - OUTPUT_A, OUTPUT_B, OUTPUT_C, OUTPUT_D) -from time import sleep - -log = logging.getLogger(__name__) - -INPUTS = (INPUT_1, INPUT_2, INPUT_3, INPUT_4) -OUTPUTS = (OUTPUT_A, OUTPUT_B, OUTPUT_C, OUTPUT_D) - - -# ============= -# Motor classes -# ============= -class MotorStartFail(Exception): - pass - - -class MotorStopFail(Exception): - pass - - -class MotorPositionFail(Exception): - pass - - -class MotorStall(Exception): - pass - - -class MotorMixin(object): - shutdown = False - - def running(self): - prev_pos = self.position - time.sleep(0.01) - pos = self.position - return True if pos != prev_pos else False - - def wait_for_running(self, timeout=5): - """ - timeout is in seconds - """ - tic = time.time() + timeout - prev_pos = None - - while time.time() < tic: - - if self.shutdown: - break - - pos = self.position - - if prev_pos is not None and pos != prev_pos: - break - else: - prev_pos = pos - time.sleep(0.001) - else: - raise MotorStartFail("%s: failed to start within %ds" % (self, timeout)) - - def wait_for_stop(self, timeout=60): - """ - timeout is in seconds - """ - tic = time.time() + timeout - prev_pos = None - stall_count = 0 - - while time.time() < tic: - if self.shutdown: - break - - pos = self.position - log.debug("%s: wait_for_stop() pos %s, prev_pos %s, stall_count %d" % (self, pos, prev_pos, stall_count)) - - if prev_pos is not None and pos == prev_pos: - stall_count += 1 - else: - stall_count = 0 - - prev_pos = pos - - if stall_count >= 5: - break - else: - time.sleep(0.001) - else: - raise MotorStopFail("%s: failed to stop within %ds" % (self, timeout)) - - def wait_for_position(self, target_position, delta=2, timeout=10, stall_ok=False): - """ - delta is in degrees - timeout is in seconds - """ - min_pos = target_position - delta - max_pos = target_position + delta - time_cutoff = time.time() + timeout - prev_pos = None - stall_count = 0 - - while time.time() < time_cutoff: - - if self.shutdown: - break - - pos = self.position - log.debug("%s: wait_for_pos() pos %d/%d, min_pos %d, max_pos %d" % (self, pos, target_position, min_pos, max_pos)) - - if pos >= min_pos and pos <= max_pos: - break - - if prev_pos is not None and pos == prev_pos: - stall_count += 1 - else: - stall_count = 0 - - if stall_count == 50: - if stall_ok: - log.warning("%s: stalled at position %d, target was %d" % (self, pos, target_position)) - break - else: - raise MotorStall("%s: stalled at position %d, target was %d" % (self, pos, target_position)) - - prev_pos = pos - time.sleep(0.001) - else: - raise MotorPositionFail("%s: failed to reach %s within %ss, current position %d" % - (self, target_position, timeout, pos)) - - -class LargeMotor(ev3dev.auto.LargeMotor, MotorMixin): - pass - - -class MediumMotor(ev3dev.auto.MediumMotor, MotorMixin): - pass - - -class ColorSensorMixin(object): - - def rgb(self): - """ - Note that the mode for the ColorSensor must be set to MODE_RGB_RAW - """ - # These values are on a scale of 0-1020, convert them to a normal 0-255 scale - red = int((self.value(0) * 255) / 1020) - green = int((self.value(1) * 255) / 1020) - blue = int((self.value(2) * 255) / 1020) - - return (red, green, blue) - - -class ColorSensor(ev3dev.auto.ColorSensor, ColorSensorMixin): - pass - - -# ============ -# Tank classes -# ============ -class Tank(object): - - def __init__(self, left_motor, right_motor, polarity='normal', name='Tank'): - - for motor in (left_motor, right_motor): - if motor not in OUTPUTS: - log.error("%s in an invalid motor, choices are %s" % (motor, ', '.join(OUTPUTS))) - sys.exit(1) - - self.left_motor = LargeMotor(left_motor) - self.right_motor = LargeMotor(right_motor) - - for x in (self.left_motor, self.right_motor): - if not x.connected: - log.error("%s is not connected" % x) - sys.exit(1) - - self.left_motor.reset() - self.right_motor.reset() - self.speed_sp = 400 - self.left_motor.speed_sp = self.speed_sp - self.right_motor.speed_sp = self.speed_sp - self.set_polarity(polarity) - self.name = name - - def __str__(self): - return self.name - - def set_polarity(self, polarity): - valid_choices = ('normal', 'inversed') - assert polarity in valid_choices,\ - "%s is an invalid polarity choice, must be %s" % (polarity, ', '.join(valid_choices)) - - self.left_motor.polarity = polarity - self.right_motor.polarity = polarity - - -class RemoteControlledTank(Tank): - - def __init__(self, left_motor, right_motor, polarity='inversed'): - Tank.__init__(self, left_motor, right_motor, polarity) - self.remote = RemoteControl(channel=1) - - if not self.remote.connected: - log.error("%s is not connected" % self.remote) - sys.exit(1) - - self.remote.on_red_up = self.make_move(self.left_motor, self.speed_sp) - self.remote.on_red_down = self.make_move(self.left_motor, self.speed_sp * -1) - self.remote.on_blue_up = self.make_move(self.right_motor, self.speed_sp) - self.remote.on_blue_down = self.make_move(self.right_motor, self.speed_sp * -1) - - def make_move(self, motor, dc_sp): - def move(state): - if state: - motor.run_forever(speed_sp=dc_sp) - else: - motor.stop() - return move - - def main(self): - - try: - while True: - self.remote.process() - time.sleep(0.01) - - # Exit cleanly so that all motors are stopped - except (KeyboardInterrupt, Exception) as e: - log.exception(e) - - for motor in list_motors(): - motor.stop() - - -# ===================== -# Wheel and Rim classes -# ===================== -class Wheel(object): - """ - A base class for various types of wheels, tires, etc - All units are in mm - """ - - def __init__(self, diameter, width): - self.diameter = float(diameter) - self.width = float(width) - self.radius = float(diameter/2) - self.circumference = diameter * math.pi - - -# A great reference when adding new wheels is http://wheels.sariel.pl/ -class EV3RubberWheel(Wheel): - - def __init__(self): - Wheel.__init__(self, 43.2, 21) diff --git a/ev3dev/virtualterminal.py b/ev3dev/virtualterminal.py deleted file mode 100644 index 34fe6aa..0000000 --- a/ev3dev/virtualterminal.py +++ /dev/null @@ -1,99 +0,0 @@ -#!/usr/bin/env python - -# virtualterminal.py -# -# Helper class for handling framebuffers for ev3dev - -# The MIT License (MIT) -# -# Copyright (c) 2016 David Lechner -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in all -# copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -# SOFTWARE. - -from ctypes import Structure, c_char, c_short, c_ushort, c_uint -from enum import Enum -from fcntl import ioctl - -class VirtualTerminal(object): - - # ioctls - _VT_OPENQRY = 0x5600 # find next available vt - _VT_GETMODE = 0x5601 # get mode of active vt - _VT_SETMODE = 0x5602 # set mode of active vt - _VT_GETSTATE = 0x5603 # get global vt state info - _VT_SENDSIG = 0x5604 # signal to send to bitmask of vts - _VT_RELDISP = 0x5605 # release display - _VT_ACTIVATE = 0x5606 # make vt active - _VT_WAITACTIVE = 0x5607 # wait for vt active - _VT_DISALLOCATE = 0x5608 # free memory associated to vt - _VT_SETACTIVATE = 0x560F # Activate and set the mode of a console - _KDSETMODE = 0x4B3A # set text/graphics mode - - class _VtMode(Structure): - _fields_ = [ - ('mode', c_char), # vt mode - ('waitv', c_char), # if set, hang on writes if not active - ('relsig', c_short), # signal to raise on release request - ('acqsig', c_short), # signal to raise on acquisition - ('frsig', c_short), # unused (set to 0) - ] - - class VtMode(Enum): - AUTO = 0 - PROCESS = 1 - ACKACQ = 2 - - class _VtState(Structure): - _fields_ = [ - ('v_active', c_ushort), # active vt - ('v_signal', c_ushort), # signal to send - ('v_state', c_ushort), # vt bitmask - ] - - class KdMode(Enum): - TEXT = 0x00 - GRAPHICS = 0x01 - TEXT0 = 0x02 # obsolete - TEXT1 = 0x03 # obsolete - - def __init__(self): - self._fd = open('/dev/tty', 'r') - - def close(self): - self._fd.close() - - def get_next_available(self): - n = c_uint() - ioctl(self._fd, self._VT_OPENQRY, n) - return n.value - - def activate(self, num): - ioctl(self._fd, self._VT_ACTIVATE, num) - ioctl(self._fd, self._VT_WAITACTIVE, num) - - def get_active(self): - state = VirtualTerminal._VtState() - ioctl(self._fd, self._VT_GETSTATE, state) - return state.v_active - - def set_graphics_mode(self): - ioctl(self._fd, self._KDSETMODE, self.KdMode.GRAPHICS.value) - - def set_text_mode(self): - ioctl(self._fd, self._KDSETMODE, self.KdMode.TEXT.value) diff --git a/ev3dev/webserver.py b/ev3dev/webserver.py deleted file mode 100644 index ec0c5a3..0000000 --- a/ev3dev/webserver.py +++ /dev/null @@ -1,590 +0,0 @@ -#!/usr/bin/env python3 - -import logging -import math -import os -import re -import sys -import time -import ev3dev.auto -from ev3dev.helper import Tank, list_motors -from http.server import BaseHTTPRequestHandler, HTTPServer -from time import sleep - -log = logging.getLogger(__name__) - - -# =============================================== -# "Joystick" code for the web interface for tanks -# =============================================== -def angle_to_speed_percentage(angle): - """ - (1, 1) - . . . . . . . - . | . - . | . - (0, 1) . | . (1, 0) - . | . - . | . - . | . - . | . - . | . - . | x-axis . - (-1, 1) .---------------------------------------. (1, -1) - . | . - . | . - . | . - . | y-axis . - . | . - (0, -1) . | . (-1, 0) - . | . - . | . - . . . . . . . - (-1, -1) - - - The joystick is a circle within a circle where the (x, y) coordinates - of the joystick form an angle with the x-axis. Our job is to translate - this angle into the percentage of power that should be sent to each motor. - For instance if the joystick is moved all the way to the top of the circle - we want both motors to move forward with 100% power...that is represented - above by (1, 1). If the joystick is moved all the way to the right side of - the circle we want to rotate clockwise so we move the left motor forward 100% - and the right motor backwards 100%...so (1, -1). If the joystick is at - 45 degrees then we move apply (1, 0) to move the left motor forward 100% and - the right motor stays still. - - The 8 points shown above are pretty easy. For the points in between those 8 - we do some math to figure out what the percentages should be. Take 11.25 degrees - for example. We look at how the motors transition from 0 degrees to 45 degrees: - - the left motor is 1 so that is easy - - the right motor moves from -1 to 0 - - We determine how far we are between 0 and 45 degrees (11.25 is 25% of 45) so we - know that the right motor should be 25% of the way from -1 to 0...so -0.75 is the - percentage for the right motor at 11.25 degrees. - """ - - if angle >= 0 and angle <= 45: - - # left motor stays at 1 - left_speed_percentage = 1 - - # right motor transitions from -1 to 0 - right_speed_percentage = -1 + (angle/45.0) - - elif angle > 45 and angle <= 90: - - # left motor stays at 1 - left_speed_percentage = 1 - - # right motor transitions from 0 to 1 - percentage_from_45_to_90 = (angle - 45) / 45.0 - right_speed_percentage = percentage_from_45_to_90 - - elif angle > 90 and angle <= 135: - - # left motor transitions from 1 to 0 - percentage_from_90_to_135 = (angle - 90) / 45.0 - left_speed_percentage = 1 - percentage_from_90_to_135 - - # right motor stays at 1 - right_speed_percentage = 1 - - elif angle > 135 and angle <= 180: - - # left motor transitions from 0 to -1 - percentage_from_135_to_180 = (angle - 135) / 45.0 - left_speed_percentage = -1 * percentage_from_135_to_180 - - # right motor stays at 1 - right_speed_percentage = 1 - - elif angle > 180 and angle <= 225: - - # left motor transitions from -1 to 0 - percentage_from_180_to_225 = (angle - 180) / 45.0 - left_speed_percentage = -1 + percentage_from_180_to_225 - - # right motor transitions from 1 to -1 - # right motor transitions from 1 to 0 between 180 and 202.5 - if angle < 202.5: - percentage_from_180_to_202 = (angle - 180) / 22.5 - right_speed_percentage = 1 - percentage_from_180_to_202 - - # right motor is 0 at 202.5 - elif angle == 202.5: - right_speed_percentage = 0 - - # right motor transitions from 0 to -1 between 202.5 and 225 - else: - percentage_from_202_to_225 = (angle - 202.5) / 22.5 - right_speed_percentage = -1 * percentage_from_202_to_225 - - elif angle > 225 and angle <= 270: - - # left motor transitions from 0 to -1 - percentage_from_225_to_270 = (angle - 225) / 45.0 - left_speed_percentage = -1 * percentage_from_225_to_270 - - # right motor stays at -1 - right_speed_percentage = -1 - - elif angle > 270 and angle <= 315: - - # left motor stays at -1 - left_speed_percentage = -1 - - # right motor transitions from -1 to 0 - percentage_from_270_to_315 = (angle - 270) / 45.0 - right_speed_percentage = -1 + percentage_from_270_to_315 - - elif angle > 315 and angle <= 360: - - # left motor transitions from -1 to 1 - # left motor transitions from -1 to 0 between 315 and 337.5 - if angle < 337.5: - percentage_from_315_to_337 = (angle - 315) / 22.5 - left_speed_percentage = (1 - percentage_from_315_to_337) * -1 - - # left motor is 0 at 337.5 - elif angle == 337.5: - left_speed_percentage = 0 - - # left motor transitions from 0 to 1 between 337.5 and 360 - elif angle > 337.5: - percentage_from_337_to_360 = (angle - 337.5) / 22.5 - left_speed_percentage = percentage_from_337_to_360 - - # right motor transitions from 0 to -1 - percentage_from_315_to_360 = (angle - 315) / 45.0 - right_speed_percentage = -1 * percentage_from_315_to_360 - - else: - raise Exception('You created a circle with more than 360 degrees (%s)...that is quite the trick' % angle) - - return (left_speed_percentage, right_speed_percentage) - - -def xy_to_speed(x, y, max_speed, radius=100.0): - """ - Convert x,y joystick coordinates to left/right motor speed - """ - - vector_length = math.hypot(x, y) - angle = math.degrees(math.atan2(y, x)) - - if angle < 0: - angle += 360 - - # Should not happen but can happen (just by a hair) due to floating point math - if vector_length > radius: - vector_length = radius - - # print "radius : %s" % radius - # print "angle : %s" % angle - # print "vector length : %s" % vector_length - - (left_speed_percentage, right_speed_percentage) = angle_to_speed_percentage(angle) - # print "init left_speed_percentage: %s" % left_speed_percentage - # print "init right_speed_percentage: %s" % right_speed_percentage - - # scale the speed percentages based on vector_length vs. radius - left_speed_percentage = (left_speed_percentage * vector_length) / radius - right_speed_percentage = (right_speed_percentage * vector_length) / radius - # print "final left_speed_percentage: %s" % left_speed_percentage - # print "final right_speed_percentage: %s" % right_speed_percentage - - # calculate the motor speeds based on speed percentages and max_speed of the motors - left_speed = round(left_speed_percentage * max_speed) - right_speed = round(right_speed_percentage * max_speed) - - # safety net - if left_speed > max_speed: - left_speed = max_speed - - if right_speed > max_speed: - right_speed = max_speed - - return (left_speed, right_speed) - - -def test_xy_to_speed(): - """ - Used to test changes to xy_to_speed() and angle_to_speed_percentage() - """ - - # Move straight forward - assert xy_to_speed(0, 100, 400) == (400, 400), "FAILED" - - # Spin clockwise - assert xy_to_speed(100, 0, 400) == (400, -400), "FAILED" - - # Spin counter clockwise - assert xy_to_speed(-100, 0, 400) == (-400, 400), "FAILED" - - # Move straight back - assert xy_to_speed(0, -100, 400) == (-400, -400), "FAILED" - - # Test vector length to power percentages - # Move straight forward, 1/2 power - assert xy_to_speed(0, 50, 400) == (200, 200), "FAILED" - - # Test motor max_speed - # Move straight forward, 1/2 power with lower max_speed - assert xy_to_speed(0, 50, 200) == (100, 100), "FAILED" - - # http://www.pagetutor.com/trigcalc/trig.html - - # top right quadrant - # ================== - # 0 -> 45 degrees - assert xy_to_speed(98.07852804032305, 19.509032201612825, 400) == (400, -300), "FAILED" # 11.25 degrees - assert xy_to_speed(92.38795325112868, 38.26834323650898, 400) == (400, -200), "FAILED" # 22.5 degrees - assert xy_to_speed(83.14696123025452, 55.557023301960214, 400) == (400, -100), "FAILED" # 33.75 degrees - - # 45 degrees, only left motor should turn - assert xy_to_speed(70.71068, 70.71068, 400) == (400, 0), "FAILED" - - # 45 -> 90 degrees - assert xy_to_speed(55.55702330196023, 83.14696123025452, 400) == (400, 100), "FAILED" # 56.25 degrees - assert xy_to_speed(38.26834323650898, 92.38795325112868, 400) == (400, 200), "FAILED" # 67.5 degrees - assert xy_to_speed(19.509032201612833, 98.07852804032305, 400) == (400, 300), "FAILED" # 78.75 degrees - - - # top left quadrant - # ================= - # 90 -> 135 degrees - assert xy_to_speed(-19.509032201612833, 98.07852804032305, 400) == (300, 400), "FAILED" - assert xy_to_speed(-38.26834323650898, 92.38795325112868, 400) == (200, 400), "FAILED" - assert xy_to_speed(-55.55702330196023, 83.14696123025452, 400) == (100, 400), "FAILED" - - # 135 degrees, only right motor should turn - assert xy_to_speed(-70.71068, 70.71068, 400) == (0, 400), "FAILED" - - # 135 -> 180 degrees - assert xy_to_speed(-83.14696123025452, 55.55702330196023, 400) == (-100, 400), "FAILED" - assert xy_to_speed(-92.38795325112868, 38.26834323650898, 400) == (-200, 400), "FAILED" - assert xy_to_speed(-98.07852804032305, 19.509032201612833, 400) == (-300, 400), "FAILED" - - - # bottom left quadrant - # ==================== - # 180 -> 225 degrees - assert xy_to_speed(-98.07852804032305, -19.509032201612833, 400) == (-300, 200), "FAILED" - assert xy_to_speed(-92.38795325112868, -38.26834323650898, 400) == (-200, 0), "FAILED" - assert xy_to_speed(-83.14696123025452, -55.55702330196023, 400) == (-100, -200), "FAILED" - - # 225 degrees, only right motor should turn (backwards) - assert xy_to_speed(-70.71068, -70.71068, 400) == (0, -400), "FAILED" - - # 225 -> 270 degrees - assert xy_to_speed(-55.55702330196023, -83.14696123025452, 400) == (-100, -400), "FAILED" - assert xy_to_speed(-38.26834323650898, -92.38795325112868, 400) == (-200, -400), "FAILED" - assert xy_to_speed(-19.509032201612833, -98.07852804032305, 400) == (-300, -400), "FAILED" - - - # bottom right quadrant - # ===================== - # 270 -> 315 degrees - assert xy_to_speed(19.509032201612833, -98.07852804032305, 400) == (-400, -300), "FAILED" - assert xy_to_speed(38.26834323650898, -92.38795325112868, 400) == (-400, -200), "FAILED" - assert xy_to_speed(55.55702330196023, -83.14696123025452, 400) == (-400, -100), "FAILED" - - # 315 degrees, only left motor should turn (backwards) - assert xy_to_speed(70.71068, -70.71068, 400) == (-400, 0), "FAILED" - - # 315 -> 360 degrees - assert xy_to_speed(83.14696123025452, -55.557023301960214, 400) == (-200, -100), "FAILED" - assert xy_to_speed(92.38795325112868, -38.26834323650898, 400) == (0, -200), "FAILED" - assert xy_to_speed(98.07852804032305, -19.509032201612825, 400) == (200, -300), "FAILED" - - -# ================== -# Web Server classes -# ================== -class RobotWebHandler(BaseHTTPRequestHandler): - """ - Base WebHandler class for various types of robots. - - RobotWebHandler's do_GET() will serve files, it is up to the child - class to handle REST APIish GETs via their do_GET() - - self.robot is populated in RobotWebServer.__init__() - """ - - # File extension to mimetype - mimetype = { - 'css' : 'text/css', - 'gif' : 'image/gif', - 'html' : 'text/html', - 'ico' : 'image/x-icon', - 'jpg' : 'image/jpg', - 'js' : 'application/javascript', - 'png' : 'image/png' - } - - def do_GET(self): - """ - If the request is for a known file type serve the file (or send a 404) and return True - """ - - if self.path == "/": - self.path = "/index.html" - - # Serve a file (image, css, html, etc) - if '.' in self.path: - extension = self.path.split('.')[-1] - mt = self.mimetype.get(extension) - - if mt: - filename = os.curdir + os.sep + self.path - - # Open the static file requested and send it - if os.path.exists(filename): - self.send_response(200) - self.send_header('Content-type', mt) - self.end_headers() - - if extension in ('gif', 'ico', 'jpg', 'png'): - # Open in binary mode, do not encode - with open(filename, mode='rb') as fh: - self.wfile.write(fh.read()) - else: - # Open as plain text and encode - with open(filename, mode='r') as fh: - self.wfile.write(fh.read().encode()) - else: - log.error("404: %s not found" % self.path) - self.send_error(404, 'File Not Found: %s' % self.path) - return True - - return False - - def log_message(self, format, *args): - """ - log using our own handler instead of BaseHTTPServer's - """ - # log.debug(format % args) - pass - - -max_move_xy_seq = 0 -motor_max_speed = None -medium_motor_max_speed = None -joystick_enaged = False - -class TankWebHandler(RobotWebHandler): - - def __str__(self): - return "%s-TankWebHandler" % self.robot - - def do_GET(self): - """ - Returns True if the requested URL is supported - """ - - if RobotWebHandler.do_GET(self): - return True - - global motor_max_speed - global medium_motor_max_speed - global max_move_xy_seq - global joystick_engaged - - if medium_motor_max_speed is None: - motor_max_speed = self.robot.left_motor.max_speed - - if hasattr(self.robot, 'medium_motor'): - medium_motor_max_speed = self.robot.medium_motor.max_speed - else: - medium_motor_max_speed = 0 - - ''' - Sometimes we get AJAX requests out of order like this: - 2016-09-06 02:29:35,846 DEBUG: seq 65: (x, y): 0, 44 -> speed 462 462 - 2016-09-06 02:29:35,910 DEBUG: seq 66: (x, y): 0, 45 -> speed 473 473 - 2016-09-06 02:29:35,979 DEBUG: seq 67: (x, y): 0, 46 -> speed 483 483 - 2016-09-06 02:29:36,033 DEBUG: seq 69: (x, y): -1, 48 -> speed 491 504 - 2016-09-06 02:29:36,086 DEBUG: seq 68: (x, y): -1, 47 -> speed 480 494 - 2016-09-06 02:29:36,137 DEBUG: seq 70: (x, y): -1, 49 -> speed 501 515 - 2016-09-06 02:29:36,192 DEBUG: seq 73: (x, y): -2, 51 -> speed 509 536 - 2016-09-06 02:29:36,564 DEBUG: seq 74: (x, y): -3, 51 -> speed 496 536 - 2016-09-06 02:29:36,649 INFO: seq 75: CLIENT LOG: touchend - 2016-09-06 02:29:36,701 DEBUG: seq 71: (x, y): -1, 50 -> speed 512 525 - 2016-09-06 02:29:36,760 DEBUG: seq 76: move stop - 2016-09-06 02:29:36,814 DEBUG: seq 72: (x, y): -1, 51 -> speed 522 536 - - This can be bad because the last command sequentially was #76 which was "move stop" - but we RXed seq #72 after that so we started moving again and never stopped - - A quick fix is to have the client send us an AJAX request to let us know - when the joystick has been engaged so that we can ignore any move-xy events - that we get out of order and show up after "move stop" but before the - next "joystick-engaged" - - We can also ignore any move-xy requests that show up late by tracking the - max seq for any move-xy we service. - ''' - # dwalton - fix this - - path = self.path.split('/') - seq = int(path[1]) - action = path[2] - - # desktop interface - if action == 'move-start': - direction = path[3] - speed_percentage = path[4] - log.debug("seq %d: move %s" % (seq, direction)) - - left_speed = int(int(speed_percentage) * motor_max_speed)/100.0 - right_speed = int(int(speed_percentage) * motor_max_speed)/100.0 - - if direction == 'forward': - self.robot.left_motor.run_forever(speed_sp=left_speed) - self.robot.right_motor.run_forever(speed_sp=right_speed) - - elif direction == 'backward': - self.robot.left_motor.run_forever(speed_sp=left_speed * -1) - self.robot.right_motor.run_forever(speed_sp=right_speed * -1) - - elif direction == 'left': - self.robot.left_motor.run_forever(speed_sp=left_speed * -1) - self.robot.right_motor.run_forever(speed_sp=right_speed) - - elif direction == 'right': - self.robot.left_motor.run_forever(speed_sp=left_speed) - self.robot.right_motor.run_forever(speed_sp=right_speed * -1) - - # desktop & mobile interface - elif action == 'move-stop': - log.debug("seq %d: move stop" % seq) - self.robot.left_motor.stop() - self.robot.right_motor.stop() - joystick_engaged = False - - # medium motor - elif action == 'motor-stop': - motor = path[3] - log.debug("seq %d: motor-stop %s" % (seq, motor)) - - if motor == 'medium': - if hasattr(self.robot, 'medium_motor'): - self.robot.medium_motor.stop() - else: - raise Exception("motor %s not supported yet" % motor) - - elif action == 'motor-start': - motor = path[3] - direction = path[4] - speed_percentage = path[5] - log.debug("seq %d: start motor %s, direction %s, speed_percentage %s" % (seq, motor, direction, speed_percentage)) - - if motor == 'medium': - if hasattr(self.robot, 'medium_motor'): - if direction == 'clockwise': - medium_speed = int(int(speed_percentage) * medium_motor_max_speed)/100.0 - self.robot.medium_motor.run_forever(speed_sp=medium_speed) - - elif direction == 'counter-clockwise': - medium_speed = int(int(speed_percentage) * medium_motor_max_speed)/100.0 - self.robot.medium_motor.run_forever(speed_sp=medium_speed * -1) - else: - log.info("we do not have a medium_motor") - else: - raise Exception("motor %s not supported yet" % motor) - - # mobile interface - elif action == 'move-xy': - x = int(path[3]) - y = int(path[4]) - - if joystick_engaged: - if seq > max_move_xy_seq: - (left_speed, right_speed) = xy_to_speed(x, y, motor_max_speed) - log.debug("seq %d: (x, y) %4d, %4d -> speed %d %d" % (seq, x, y, left_speed, right_speed)) - max_move_xy_seq = seq - - if left_speed == 0: - self.robot.left_motor.stop() - else: - self.robot.left_motor.run_forever(speed_sp=left_speed) - - if right_speed == 0: - self.robot.right_motor.stop() - else: - self.robot.right_motor.run_forever(speed_sp=right_speed) - else: - log.debug("seq %d: (x, y) %4d, %4d (ignore, max seq %d)" % - (seq, x, y, max_move_xy_seq)) - else: - log.debug("seq %d: (x, y) %4d, %4d (ignore, joystick idle)" % - (seq, x, y)) - - elif action == 'joystick-engaged': - joystick_engaged = True - - elif action == 'log': - msg = ''.join(path[3:]) - re_msg = re.search('^(.*)\?', msg) - - if re_msg: - msg = re_msg.group(1) - - log.debug("seq %d: CLIENT LOG: %s" % (seq, msg)) - - else: - log.warning("Unsupported URL %s" % self.path) - - # It is good practice to send this but if we are getting move-xy we - # tend to get a lot of them and we need to be as fast as possible so - # be bad and don't send a reply. This takes ~20ms. - if action != 'move-xy': - self.send_response(204) - - return True - - -class RobotWebServer(object): - """ - A Web server so that 'robot' can be controlled via 'handler_class' - """ - - def __init__(self, robot, handler_class, port_number=8000): - self.content_server = None - self.handler_class = handler_class - self.handler_class.robot = robot - self.port_number = port_number - - def run(self): - - try: - log.info("Started HTTP server (content) on port %d" % self.port_number) - self.content_server = HTTPServer(('', self.port_number), self.handler_class) - self.content_server.serve_forever() - - # Exit cleanly, stop both web servers and all motors - except (KeyboardInterrupt, Exception) as e: - log.exception(e) - - if self.content_server: - self.content_server.socket.close() - self.content_server = None - - for motor in list_motors(): - motor.stop() - - -class WebControlledTank(Tank): - """ - A tank that is controlled via a web browser - """ - - def __init__(self, left_motor, right_motor, polarity='normal', port_number=8000): - Tank.__init__(self, left_motor, right_motor, polarity) - self.www = RobotWebServer(self, TankWebHandler, port_number) - - def main(self): - # start the web server - self.www.run() diff --git a/ev3dev2/__init__.py b/ev3dev2/__init__.py new file mode 100644 index 0000000..5e776fb --- /dev/null +++ b/ev3dev2/__init__.py @@ -0,0 +1,387 @@ +# ----------------------------------------------------------------------------- +# Copyright (c) 2015 Ralph Hempel +# Copyright (c) 2015 Anton Vanhoucke +# Copyright (c) 2015 Denis Demidov +# Copyright (c) 2015 Eric Pascual +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +# ----------------------------------------------------------------------------- + +import sys +import os +import io +import fnmatch +import re +import stat +import errno +from os.path import abspath + +try: + # if we are in a released build, there will be an auto-generated "version" + # module + from .version import __version__ +except ImportError: + __version__ = "" + +if sys.version_info < (3, 4): + raise SystemError('Must be using Python 3.4 or higher') + + +def is_micropython(): + return sys.implementation.name == "micropython" + + +def chain_exception(exception, cause): + if is_micropython(): + raise exception + else: + raise exception from cause + + +def get_current_platform(): + """ + Look in /sys/class/board-info/ to determine the platform type. + + This can return 'ev3', 'evb', 'pistorms', 'brickpi', 'brickpi3' or 'fake'. + """ + board_info_dir = '/sys/class/board-info/' + + if not os.path.exists(board_info_dir) or os.environ.get("FAKE_SYS"): + return 'fake' + + for board in os.listdir(board_info_dir): + uevent_filename = os.path.join(board_info_dir, board, 'uevent') + + if os.path.exists(uevent_filename): + with open(uevent_filename, 'r') as fh: + for line in fh.readlines(): + (key, value) = line.strip().split('=') + + if key == 'BOARD_INFO_MODEL': + + if value == 'LEGO MINDSTORMS EV3': + return 'ev3' + + elif value in ('FatcatLab EVB', 'QuestCape'): + return 'evb' + + elif value == 'PiStorms': + return 'pistorms' + + # This is the same for both BrickPi and BrickPi+. + # There is not a way to tell the difference. + elif value == 'Dexter Industries BrickPi': + return 'brickpi' + + elif value == 'Dexter Industries BrickPi3': + return 'brickpi3' + + elif value == 'FAKE-SYS': + return 'fake' + + return None + + +# ----------------------------------------------------------------------------- +def list_device_names(class_path, name_pattern, **kwargs): + """ + This is a generator function that lists names of all devices matching the + provided parameters. + + Parameters: + class_path: class path of the device, a subdirectory of /sys/class. + For example, '/sys/class/tacho-motor'. + name_pattern: pattern that device name should match. + For example, 'sensor*' or 'motor*'. Default value: '*'. + keyword arguments: used for matching the corresponding device + attributes. For example, address='outA', or + driver_name=['lego-ev3-us', 'lego-nxt-us']. When argument value + is a list, then a match against any entry of the list is + enough. + """ + + if not os.path.isdir(class_path): + return + + def matches(attribute, pattern): + try: + with io.FileIO(attribute) as f: + value = f.read().strip().decode() + except Exception: + return False + + if isinstance(pattern, list): + return any([value.find(p) >= 0 for p in pattern]) + else: + return value.find(pattern) >= 0 + + for f in os.listdir(class_path): + if fnmatch.fnmatch(f, name_pattern): + path = class_path + '/' + f + if all([matches(path + '/' + k, kwargs[k]) for k in kwargs]): + yield f + + +def library_load_warning_message(library_name, dependent_class): + return 'Import warning: Failed to import "{}". {} will be unusable!'.format(library_name, dependent_class) + + +class DeviceNotFound(Exception): + pass + + +class DeviceNotDefined(Exception): + pass + + +class ThreadNotRunning(Exception): + pass + + +# ----------------------------------------------------------------------------- +# Define the base class from which all other ev3dev classes are defined. + + +class Device(object): + """The ev3dev device base class""" + + __slots__ = [ + '_path', + '_device_index', + '_attr_cache', + 'kwargs', + ] + + DEVICE_ROOT_PATH = '/sys/class' + + _DEVICE_INDEX = re.compile(r'^.*(\d+)$') + + def __init__(self, class_name, name_pattern='*', name_exact=False, **kwargs): + """Spin through the Linux sysfs class for the device type and find + a device that matches the provided name pattern and attributes (if any). + + Parameters: + class_name: class name of the device, a subdirectory of /sys/class. + For example, 'tacho-motor'. + name_pattern: pattern that device name should match. + For example, 'sensor*' or 'motor*'. Default value: '*'. + name_exact: when True, assume that the name_pattern provided is the + exact device name and use it directly. + keyword arguments: used for matching the corresponding device + attributes. For example, address='outA', or + driver_name=['lego-ev3-us', 'lego-nxt-us']. When argument value + is a list, then a match against any entry of the list is + enough. + + Example:: + + d = ev3dev.Device('tacho-motor', address='outA') + s = ev3dev.Device('lego-sensor', driver_name=['lego-ev3-us', 'lego-nxt-us']) + + If there was no valid connected device, an error is thrown. + """ + + classpath = abspath(Device.DEVICE_ROOT_PATH + '/' + class_name) + self.kwargs = kwargs + self._attr_cache = {} + + def get_index(file): + match = Device._DEVICE_INDEX.match(file) + if match: + return int(match.group(1)) + else: + return None + + if name_exact: + self._path = classpath + '/' + name_pattern + self._device_index = get_index(name_pattern) + else: + try: + name = next(list_device_names(classpath, name_pattern, **kwargs)) + self._path = classpath + '/' + name + self._device_index = get_index(name) + except StopIteration: + self._path = None + self._device_index = None + + chain_exception(DeviceNotFound("%s is not connected." % self), None) + + def __str__(self): + if 'address' in self.kwargs: + return "%s(%s)" % (self.__class__.__name__, self.kwargs.get('address')) + else: + return self.__class__.__name__ + + def __repr__(self): + return self.__str__() + + # This allows us to sort lists of Device objects + def __lt__(self, other): + return str(self) < str(other) + + def _attribute_file_open(self, name): + path = os.path.join(self._path, name) + mode = stat.S_IMODE(os.stat(path)[stat.ST_MODE]) + r_ok = mode & stat.S_IRGRP + w_ok = mode & stat.S_IWGRP + + if r_ok and w_ok: + mode_str = 'r+' + elif w_ok: + mode_str = 'w' + else: + mode_str = 'r' + + return io.FileIO(path, mode_str) + + def _get_attribute(self, attribute, name): + """Device attribute getter""" + try: + if attribute is None: + attribute = self._attribute_file_open(name) + else: + attribute.seek(0) + return attribute, attribute.read().strip().decode() + except Exception as ex: + self._raise_friendly_access_error(ex, name, None) + + def _set_attribute(self, attribute, name, value): + """Device attribute setter""" + try: + if attribute is None: + attribute = self._attribute_file_open(name) + else: + attribute.seek(0) + + if isinstance(value, str): + value = value.encode() + attribute.write(value) + attribute.flush() + except Exception as ex: + self._raise_friendly_access_error(ex, name, value) + return attribute + + def _raise_friendly_access_error(self, driver_error, attribute, value): + if not isinstance(driver_error, OSError): + raise driver_error + + driver_errorno = driver_error.args[0] if is_micropython() else driver_error.errno + + if driver_errorno == errno.EINVAL: + if attribute == "speed_sp": + try: + max_speed = self.max_speed + except (AttributeError, Exception): + chain_exception(ValueError("The given speed value {} was out of range".format(value)), driver_error) + else: + chain_exception( + ValueError("The given speed value {} was out of range. Max speed: +/-{}".format( + value, max_speed)), driver_error) + chain_exception(ValueError("One or more arguments were out of range or invalid, value {}".format(value)), + driver_error) + elif driver_errorno == errno.ENODEV or driver_errorno == errno.ENOENT: + # We will assume that a file-not-found error is the result of a disconnected device + # rather than a library error. If that isn't the case, at a minimum the underlying + # error info will be printed for debugging. + chain_exception(DeviceNotFound("%s is no longer connected" % self), driver_error) + raise driver_error + + def get_attr_int(self, attribute, name): + attribute, value = self._get_attribute(attribute, name) + return attribute, int(value) + + def get_cached_attr_int(self, filehandle, keyword): + value = self._attr_cache.get(keyword) + + if value is None: + (filehandle, value) = self.get_attr_int(filehandle, keyword) + self._attr_cache[keyword] = value + + return (filehandle, value) + + def set_attr_int(self, attribute, name, value): + return self._set_attribute(attribute, name, str(int(value))) + + def set_attr_raw(self, attribute, name, value): + return self._set_attribute(attribute, name, value) + + def get_attr_string(self, attribute, name): + return self._get_attribute(attribute, name) + + def get_cached_attr_string(self, filehandle, keyword): + value = self._attr_cache.get(keyword) + + if value is None: + (filehandle, value) = self.get_attr_string(filehandle, keyword) + self._attr_cache[keyword] = value + + return (filehandle, value) + + def set_attr_string(self, attribute, name, value): + return self._set_attribute(attribute, name, value) + + def get_attr_line(self, attribute, name): + return self._get_attribute(attribute, name) + + def get_attr_set(self, attribute, name): + attribute, value = self.get_attr_line(attribute, name) + return attribute, [v.strip('[]') for v in value.split()] + + def get_cached_attr_set(self, filehandle, keyword): + value = self._attr_cache.get(keyword) + + if value is None: + (filehandle, value) = self.get_attr_set(filehandle, keyword) + self._attr_cache[keyword] = value + + return (filehandle, value) + + def get_attr_from_set(self, attribute, name): + attribute, value = self.get_attr_line(attribute, name) + for a in value.split(): + v = a.strip('[]') + if v != a: + return v + return "" + + @property + def device_index(self): + return self._device_index + + +def list_devices(class_name, name_pattern, **kwargs): + """ + This is a generator function that takes same arguments as `Device` class + and enumerates all devices present in the system that match the provided + arguments. + + Parameters: + class_name: class name of the device, a subdirectory of /sys/class. + For example, 'tacho-motor'. + name_pattern: pattern that device name should match. + For example, 'sensor*' or 'motor*'. Default value: '*'. + keyword arguments: used for matching the corresponding device + attributes. For example, address='outA', or + driver_name=['lego-ev3-us', 'lego-nxt-us']. When argument value + is a list, then a match against any entry of the list is + enough. + """ + classpath = abspath(Device.DEVICE_ROOT_PATH + '/' + class_name) + + return (Device(class_name, name, name_exact=True) for name in list_device_names(classpath, name_pattern, **kwargs)) diff --git a/demo/MINDCUB3R/__init__.py b/ev3dev2/_platform/__init__.py similarity index 100% rename from demo/MINDCUB3R/__init__.py rename to ev3dev2/_platform/__init__.py diff --git a/demo/misc/sound.py b/ev3dev2/_platform/brickpi.py old mode 100755 new mode 100644 similarity index 62% rename from demo/misc/sound.py rename to ev3dev2/_platform/brickpi.py index 33dc017..75e6991 --- a/demo/misc/sound.py +++ b/ev3dev2/_platform/brickpi.py @@ -1,8 +1,7 @@ -#!/usr/bin/env python3 # -*- coding: utf-8 -*- # ------------------------------------------------------------------------------ -# Copyright (c) 2017 Eric Pascual +# Copyright (c) 2015 Eric Pascual # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal @@ -22,45 +21,35 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. # ----------------------------------------------------------------------------- - -""" Sound capabilities demonstration. """ +An assortment of classes modeling specific features of the BrickPi. +""" + +from collections import OrderedDict -from textwrap import dedent -import os +OUTPUT_A = 'serial0-0:MA' +OUTPUT_B = 'serial0-0:MB' +OUTPUT_C = 'serial0-0:MC' +OUTPUT_D = 'serial0-0:MD' -from ev3dev.ev3 import Sound +INPUT_1 = 'serial0-0:S1' +INPUT_2 = 'serial0-0:S2' +INPUT_3 = 'serial0-0:S3' +INPUT_4 = 'serial0-0:S4' -_HERE = os.path.dirname(__file__) +BUTTONS_FILENAME = None +EVDEV_DEVICE_NAME = None -print(dedent(""" - A long time ago - in a galaxy far, - far away... -""")) +LEDS = OrderedDict() +LEDS['blue_led1'] = 'led1:blue:brick-status' +LEDS['blue_led2'] = 'led2:blue:brick-status' -Sound.play_song(( - ('D4', 'e3'), - ('D4', 'e3'), - ('D4', 'e3'), - ('G4', 'h'), - ('D5', 'h'), - ('C5', 'e3'), - ('B4', 'e3'), - ('A4', 'e3'), - ('G5', 'h'), - ('D5', 'q'), - ('C5', 'e3'), - ('B4', 'e3'), - ('A4', 'e3'), - ('G5', 'h'), - ('D5', 'q'), - ('C5', 'e3'), - ('B4', 'e3'), - ('C5', 'e3'), - ('A4', 'h.'), -)).wait() +LED_GROUPS = OrderedDict() +LED_GROUPS['LED1'] = ('blue_led1', ) +LED_GROUPS['LED2'] = ('blue_led2', ) -Sound.play(os.path.join(_HERE, 'snd/r2d2.wav')).wait() +LED_COLORS = OrderedDict() +LED_COLORS['BLACK'] = (0, ) +LED_COLORS['BLUE'] = (1, ) -Sound.speak("Luke, I am your father").wait() +LED_DEFAULT_COLOR = 'BLUE' diff --git a/ev3dev2/_platform/brickpi3.py b/ev3dev2/_platform/brickpi3.py new file mode 100644 index 0000000..b875b9b --- /dev/null +++ b/ev3dev2/_platform/brickpi3.py @@ -0,0 +1,57 @@ +from collections import OrderedDict + +# Up to four brickpi3s can be stacked +OUTPUT_A = 'spi0.1:MA' +OUTPUT_B = 'spi0.1:MB' +OUTPUT_C = 'spi0.1:MC' +OUTPUT_D = 'spi0.1:MD' + +OUTPUT_E = 'spi0.1:ME' +OUTPUT_F = 'spi0.1:MF' +OUTPUT_G = 'spi0.1:MG' +OUTPUT_H = 'spi0.1:MH' + +OUTPUT_I = 'spi0.1:MI' +OUTPUT_J = 'spi0.1:MJ' +OUTPUT_K = 'spi0.1:MK' +OUTPUT_L = 'spi0.1:ML' + +OUTPUT_M = 'spi0.1:MM' +OUTPUT_N = 'spi0.1:MN' +OUTPUT_O = 'spi0.1:MO' +OUTPUT_P = 'spi0.1:MP' + +INPUT_1 = 'spi0.1:S1' +INPUT_2 = 'spi0.1:S2' +INPUT_3 = 'spi0.1:S3' +INPUT_4 = 'spi0.1:S4' + +INPUT_5 = 'spi0.1:S5' +INPUT_6 = 'spi0.1:S6' +INPUT_7 = 'spi0.1:S7' +INPUT_8 = 'spi0.1:S8' + +INPUT_9 = 'spi0.1:S9' +INPUT_10 = 'spi0.1:S10' +INPUT_11 = 'spi0.1:S11' +INPUT_12 = 'spi0.1:S12' + +INPUT_13 = 'spi0.1:S13' +INPUT_14 = 'spi0.1:S14' +INPUT_15 = 'spi0.1:S15' +INPUT_16 = 'spi0.1:S16' + +BUTTONS_FILENAME = None +EVDEV_DEVICE_NAME = None + +LEDS = OrderedDict() +LEDS['amber_led'] = 'led1:amber:brick-status' + +LED_GROUPS = OrderedDict() +LED_GROUPS['LED'] = ('amber_led', ) + +LED_COLORS = OrderedDict() +LED_COLORS['BLACK'] = (0, ) +LED_COLORS['AMBER'] = (1, ) + +LED_DEFAULT_COLOR = 'AMBER' diff --git a/ev3dev2/_platform/ev3.py b/ev3dev2/_platform/ev3.py new file mode 100644 index 0000000..92e0c6c --- /dev/null +++ b/ev3dev2/_platform/ev3.py @@ -0,0 +1,61 @@ +# -*- coding: utf-8 -*- + +# ------------------------------------------------------------------------------ +# Copyright (c) 2015 Eric Pascual +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NON INFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +# ----------------------------------------------------------------------------- +""" +An assortment of classes modeling specific features of the EV3 brick. +""" + +from collections import OrderedDict + +OUTPUT_A = 'ev3-ports:outA' +OUTPUT_B = 'ev3-ports:outB' +OUTPUT_C = 'ev3-ports:outC' +OUTPUT_D = 'ev3-ports:outD' + +INPUT_1 = 'ev3-ports:in1' +INPUT_2 = 'ev3-ports:in2' +INPUT_3 = 'ev3-ports:in3' +INPUT_4 = 'ev3-ports:in4' + +BUTTONS_FILENAME = '/dev/input/by-path/platform-gpio_keys-event' +EVDEV_DEVICE_NAME = 'EV3 Brick Buttons' + +LEDS = OrderedDict() +LEDS['red_left'] = 'led0:red:brick-status' +LEDS['red_right'] = 'led1:red:brick-status' +LEDS['green_left'] = 'led0:green:brick-status' +LEDS['green_right'] = 'led1:green:brick-status' + +LED_GROUPS = OrderedDict() +LED_GROUPS['LEFT'] = ('red_left', 'green_left') +LED_GROUPS['RIGHT'] = ('red_right', 'green_right') + +LED_COLORS = OrderedDict() +LED_COLORS['BLACK'] = (0, 0) +LED_COLORS['RED'] = (1, 0) +LED_COLORS['GREEN'] = (0, 1) +LED_COLORS['AMBER'] = (1, 1) +LED_COLORS['ORANGE'] = (1, 0.5) +LED_COLORS['YELLOW'] = (0.1, 1) + +LED_DEFAULT_COLOR = 'GREEN' diff --git a/ev3dev2/_platform/evb.py b/ev3dev2/_platform/evb.py new file mode 100644 index 0000000..7c6470e --- /dev/null +++ b/ev3dev2/_platform/evb.py @@ -0,0 +1,22 @@ +""" +An assortment of classes modeling specific features of the EVB. +""" + +OUTPUT_A = 'outA' +OUTPUT_B = 'outB' +OUTPUT_C = 'outC' +OUTPUT_D = 'outD' + +INPUT_1 = 'in1' +INPUT_2 = 'in2' +INPUT_3 = 'in3' +INPUT_4 = 'in4' + +BUTTONS_FILENAME = '/dev/input/by-path/platform-evb-buttons-event' +EVDEV_DEVICE_NAME = 'evb-input' + +# EVB does not have LEDs +LEDS = {} +LED_GROUPS = {} +LED_COLORS = {} +LED_DEFAULT_COLOR = '' diff --git a/ev3dev2/_platform/fake.py b/ev3dev2/_platform/fake.py new file mode 100644 index 0000000..26a0498 --- /dev/null +++ b/ev3dev2/_platform/fake.py @@ -0,0 +1,17 @@ +OUTPUT_A = 'outA' +OUTPUT_B = 'outB' +OUTPUT_C = 'outC' +OUTPUT_D = 'outD' + +INPUT_1 = 'in1' +INPUT_2 = 'in2' +INPUT_3 = 'in3' +INPUT_4 = 'in4' + +BUTTONS_FILENAME = None +EVDEV_DEVICE_NAME = None + +LEDS = {} +LED_GROUPS = {} +LED_COLORS = {} +LED_DEFAULT_COLOR = '' diff --git a/ev3dev2/_platform/pistorms.py b/ev3dev2/_platform/pistorms.py new file mode 100644 index 0000000..1dfa0b2 --- /dev/null +++ b/ev3dev2/_platform/pistorms.py @@ -0,0 +1,40 @@ +""" +An assortment of classes modeling specific features of the PiStorms. +""" +from collections import OrderedDict + +OUTPUT_A = 'pistorms:BAM1' +OUTPUT_B = 'pistorms:BAM2' +OUTPUT_C = 'pistorms:BBM1' +OUTPUT_D = 'pistorms:BBM2' + +INPUT_1 = 'pistorms:BAS1' +INPUT_2 = 'pistorms:BAS2' +INPUT_3 = 'pistorms:BBS1' +INPUT_4 = 'pistorms:BBS2' + +BUTTONS_FILENAME = '/dev/input/by-path/platform-3f804000.i2c-event' +EVDEV_DEVICE_NAME = 'PiStorms' + +LEDS = OrderedDict() +LEDS['red_left'] = 'pistorms:BB:red:brick-status' +LEDS['red_right'] = 'pistorms:BA:red:brick-status' +LEDS['green_left'] = 'pistorms:BB:green:brick-status' +LEDS['green_right'] = 'pistorms:BA:green:brick-status' +LEDS['blue_left'] = 'pistorms:BB:blue:brick-status' +LEDS['blue_right'] = 'pistorms:BA:blue:brick-status' + +LED_GROUPS = OrderedDict() +LED_GROUPS['LEFT'] = ('red_left', 'green_left', 'blue_left') +LED_GROUPS['RIGHT'] = ('red_right', 'green_right', 'blue_right') + +LED_COLORS = OrderedDict() +LED_COLORS['BLACK'] = (0, 0, 0) +LED_COLORS['RED'] = (1, 0, 0) +LED_COLORS['GREEN'] = (0, 1, 0) +LED_COLORS['BLUE'] = (0, 0, 1) +LED_COLORS['YELLOW'] = (1, 1, 0) +LED_COLORS['CYAN'] = (0, 1, 1) +LED_COLORS['MAGENTA'] = (1, 0, 1) + +LED_DEFAULT_COLOR = 'GREEN' diff --git a/ev3dev2/auto.py b/ev3dev2/auto.py new file mode 100644 index 0000000..7f7c8bb --- /dev/null +++ b/ev3dev2/auto.py @@ -0,0 +1,48 @@ +from ev3dev2 import * + +platform = get_current_platform() + +if platform == 'ev3': + from ev3dev2._platform.ev3 import INPUT_1, INPUT_2, INPUT_3, INPUT_4 + from ev3dev2._platform.ev3 import OUTPUT_A, OUTPUT_B, OUTPUT_C, OUTPUT_D + from ev3dev2._platform.ev3 import LEDS, LED_GROUPS, LED_COLORS + +elif platform == 'evb': + from ev3dev2._platform.evb import INPUT_1, INPUT_2, INPUT_3, INPUT_4 + from ev3dev2._platform.evb import OUTPUT_A, OUTPUT_B, OUTPUT_C, OUTPUT_D + from ev3dev2._platform.evb import LEDS, LED_GROUPS, LED_COLORS + +elif platform == 'pistorms': + from ev3dev2._platform.pistorms import INPUT_1, INPUT_2, INPUT_3, INPUT_4 + from ev3dev2._platform.pistorms import OUTPUT_A, OUTPUT_B, OUTPUT_C, OUTPUT_D + from ev3dev2._platform.pistorms import LEDS, LED_GROUPS, LED_COLORS + +elif platform == 'brickpi': + from ev3dev2._platform.brickpi import INPUT_1, INPUT_2, INPUT_3, INPUT_4 + from ev3dev2._platform.brickpi import OUTPUT_A, OUTPUT_B, OUTPUT_C, OUTPUT_D + from ev3dev2._platform.brickpi import LEDS, LED_GROUPS, LED_COLORS + +elif platform == 'brickpi3': + from ev3dev2._platform.brickpi3 import INPUT_1, INPUT_2, INPUT_3, INPUT_4 + from ev3dev2._platform.brickpi3 import OUTPUT_A, OUTPUT_B, OUTPUT_C, OUTPUT_D + from ev3dev2._platform.brickpi3 import LEDS, LED_GROUPS, LED_COLORS + +elif platform == 'fake': + from ev3dev2._platform.fake import INPUT_1, INPUT_2, INPUT_3, INPUT_4 + from ev3dev2._platform.fake import OUTPUT_A, OUTPUT_B, OUTPUT_C, OUTPUT_D + from ev3dev2._platform.fake import LEDS, LED_GROUPS, LED_COLORS + +else: + raise Exception("Unsupported platform '%s'" % platform) + +from ev3dev2.button import * +from ev3dev2.console import * +from ev3dev2.display import * +from ev3dev2.fonts import * +from ev3dev2.led import * +from ev3dev2.motor import * +from ev3dev2.port import * +from ev3dev2.power import * +from ev3dev2.sensor import * +from ev3dev2.sensor.lego import * +from ev3dev2.sound import * diff --git a/ev3dev2/button.py b/ev3dev2/button.py new file mode 100644 index 0000000..50dd673 --- /dev/null +++ b/ev3dev2/button.py @@ -0,0 +1,484 @@ +# ----------------------------------------------------------------------------- +# Copyright (c) 2015 Ralph Hempel +# Copyright (c) 2015 Anton Vanhoucke +# Copyright (c) 2015 Denis Demidov +# Copyright (c) 2015 Eric Pascual +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +# ----------------------------------------------------------------------------- + +import sys +from ev3dev2.stopwatch import StopWatch +from ev3dev2 import get_current_platform, is_micropython, library_load_warning_message +from logging import getLogger + +# Import the button filenames, this is platform specific +platform = get_current_platform() + +if platform == 'ev3': + from ._platform.ev3 import BUTTONS_FILENAME, EVDEV_DEVICE_NAME + +elif platform == 'evb': + from ._platform.evb import BUTTONS_FILENAME, EVDEV_DEVICE_NAME + +elif platform == 'pistorms': + from ._platform.pistorms import BUTTONS_FILENAME, EVDEV_DEVICE_NAME + +elif platform == 'brickpi': + from ._platform.brickpi import BUTTONS_FILENAME, EVDEV_DEVICE_NAME + +elif platform == 'brickpi3': + from ._platform.brickpi3 import BUTTONS_FILENAME, EVDEV_DEVICE_NAME + +elif platform == 'fake': + from ._platform.fake import BUTTONS_FILENAME, EVDEV_DEVICE_NAME + +else: + raise Exception("Unsupported platform '%s'" % platform) + +if sys.version_info < (3, 4): + raise SystemError('Must be using Python 3.4 or higher') + +log = getLogger(__name__) + + +class MissingButton(Exception): + pass + + +class ButtonCommon(object): + def __str__(self): + return self.__class__.__name__ + + @staticmethod + def on_change(changed_buttons): + """ + This handler is called by ``process()`` whenever state of any button has + changed since last ``process()`` call. ``changed_buttons`` is a list of + tuples of changed button names and their states. + """ + pass + + @property + def buttons_pressed(self): + raise NotImplementedError() + + def any(self): + """ + Checks if any button is pressed. + """ + return bool(self.buttons_pressed) + + def check_buttons(self, buttons=[]): + """ + Check if currently pressed buttons exactly match the given list ``buttons``. + """ + return set(self.buttons_pressed) == set(buttons) + + def _wait(self, wait_for_button_press, wait_for_button_release, timeout_ms): + raise NotImplementedError() + + def wait_for_pressed(self, buttons, timeout_ms=None): + """ + Wait for ``buttons`` to be pressed down. + """ + return self._wait(buttons, [], timeout_ms) + + def wait_for_released(self, buttons, timeout_ms=None): + """ + Wait for ``buttons`` to be released. + """ + return self._wait([], buttons, timeout_ms) + + def wait_for_bump(self, buttons, timeout_ms=None): + """ + Wait for ``buttons`` to be pressed down and then released. + Both actions must happen within ``timeout_ms``. + """ + stopwatch = StopWatch() + stopwatch.start() + + if self.wait_for_pressed(buttons, timeout_ms): + if timeout_ms is not None: + timeout_ms -= stopwatch.value_ms + return self.wait_for_released(buttons, timeout_ms) + + return False + + def process(self, new_state=None): + """ + Check for currenly pressed buttons. If the ``new_state`` differs from the + old state, call the appropriate button event handlers (on_up, on_down, etc). + """ + if new_state is None: + new_state = set(self.buttons_pressed) + old_state = self._state if hasattr(self, '_state') else set() + self._state = new_state + + state_diff = new_state.symmetric_difference(old_state) + for button in state_diff: + handler = getattr(self, 'on_' + button) + + if handler is not None: + handler(button in new_state) + + if self.on_change is not None and state_diff: + self.on_change([(button, button in new_state) for button in state_diff]) + + +class EV3ButtonCommon(object): + + # These handlers are called by ButtonCommon.process() whenever the + # state of 'up', 'down', etc buttons have changed since last + # ButtonCommon.process() call + on_up = None + on_down = None + on_left = None + on_right = None + on_enter = None + on_backspace = None + + @property + def up(self): + """ + Check if ``up`` button is pressed. + """ + return 'up' in self.buttons_pressed + + @property + def down(self): + """ + Check if ``down`` button is pressed. + """ + return 'down' in self.buttons_pressed + + @property + def left(self): + """ + Check if ``left`` button is pressed. + """ + return 'left' in self.buttons_pressed + + @property + def right(self): + """ + Check if ``right`` button is pressed. + """ + return 'right' in self.buttons_pressed + + @property + def enter(self): + """ + Check if ``enter`` button is pressed. + """ + return 'enter' in self.buttons_pressed + + @property + def backspace(self): + """ + Check if ``backspace`` button is pressed. + """ + return 'backspace' in self.buttons_pressed + + +# micropython implementation +if is_micropython(): # noqa: C901 + + try: + # This is a linux-specific module. + # It is required by the Button class, but failure to import it may be + # safely ignored if one just needs to run API tests on Windows. + import fcntl + except ImportError: + log.warning(library_load_warning_message("fcntl", "Button")) + + if platform not in ("ev3", "fake"): + raise Exception("micropython button support has not been implemented for '%s'" % platform) + + def _test_bit(buf, index): + byte = buf[int(index >> 3)] + bit = byte & (1 << (index % 8)) + return bool(bit) + + class ButtonBase(ButtonCommon): + pass + + class Button(ButtonCommon, EV3ButtonCommon): + """ + EV3 Buttons + """ + + # Button key codes + UP = 103 + DOWN = 108 + LEFT = 105 + RIGHT = 106 + ENTER = 28 + BACK = 14 + + # Note, this order is intentional and comes from the EV3-G software + _BUTTONS = (UP, DOWN, LEFT, RIGHT, ENTER, BACK) + _BUTTON_DEV = '/dev/input/by-path/platform-gpio_keys-event' + + _BUTTON_TO_STRING = { + UP: "up", + DOWN: "down", + LEFT: "left", + RIGHT: "right", + ENTER: "enter", + BACK: "backspace", + } + + # stuff from linux/input.h and linux/input-event-codes.h + _KEY_MAX = 0x2FF + _KEY_BUF_LEN = (_KEY_MAX + 7) // 8 + _EVIOCGKEY = 2 << (14 + 8 + 8) | _KEY_BUF_LEN << (8 + 8) | ord('E') << 8 | 0x18 + + def __init__(self): + super(Button, self).__init__() + self._devnode = open(Button._BUTTON_DEV, 'b') + self._fd = self._devnode.fileno() + self._buffer = bytearray(Button._KEY_BUF_LEN) + + @property + def buttons_pressed(self): + """ + Returns list of pressed buttons + """ + fcntl.ioctl(self._fd, Button._EVIOCGKEY, self._buffer, mut=True) + + pressed = [] + for b in Button._BUTTONS: + if _test_bit(self._buffer, b): + pressed.append(Button._BUTTON_TO_STRING[b]) + return pressed + + def process_forever(self): + while True: + self.process() + + def _wait(self, wait_for_button_press, wait_for_button_release, timeout_ms): + stopwatch = StopWatch() + stopwatch.start() + + # wait_for_button_press/release can be a list of buttons or a string + # with the name of a single button. If it is a string of a single + # button convert that to a list. + if isinstance(wait_for_button_press, str): + wait_for_button_press = [ + wait_for_button_press, + ] + + if isinstance(wait_for_button_release, str): + wait_for_button_release = [ + wait_for_button_release, + ] + + while True: + all_pressed = True + all_released = True + pressed = self.buttons_pressed + + for button in wait_for_button_press: + if button not in pressed: + all_pressed = False + break + + for button in wait_for_button_release: + if button in pressed: + all_released = False + break + + if all_pressed and all_released: + return True + + if timeout_ms is not None and stopwatch.value_ms >= timeout_ms: + return False + + +# python3 implementation +else: + import array + + try: + # This is a linux-specific module. + # It is required by the Button class, but failure to import it may be + # safely ignored if one just needs to run API tests on Windows. + import fcntl + except ImportError: + log.warning(library_load_warning_message("fcntl", "Button")) + + try: + # This is a linux-specific module. + # It is required by the Button class, but failure to import it may be + # safely ignored if one just needs to run API tests on Windows. + import evdev + except ImportError: + log.warning(library_load_warning_message("evdev", "Button")) + + class ButtonBase(ButtonCommon): + """ + Abstract button interface. + """ + _state = set([]) + + @property + def evdev_device(self): + """ + Return our corresponding evdev device object + """ + devices = [evdev.InputDevice(fn) for fn in evdev.list_devices()] + + for device in devices: + if device.name == self.evdev_device_name: + return device + + raise Exception("%s: could not find evdev device '%s'" % (self, self.evdev_device_name)) + + def process_forever(self): + for event in self.evdev_device.read_loop(): + if event.type == evdev.ecodes.EV_KEY: + self.process() + + class ButtonEVIO(ButtonBase): + """ + Provides a generic button reading mechanism that works with event interface + and may be adapted to platform specific implementations. + + This implementation depends on the availability of the EVIOCGKEY ioctl + to be able to read the button state buffer. See Linux kernel source + in /include/uapi/linux/input.h for details. + """ + + KEY_MAX = 0x2FF + KEY_BUF_LEN = int((KEY_MAX + 7) / 8) + EVIOCGKEY = (2 << (14 + 8 + 8) | KEY_BUF_LEN << (8 + 8) | ord('E') << 8 | 0x18) + + _buttons = {} + + def __init__(self): + super(ButtonEVIO, self).__init__() + self._file_cache = {} + self._buffer_cache = {} + + for b in self._buttons: + name = self._buttons[b]['name'] + + if name is None: + raise MissingButton("Button '%s' is not available on this platform" % b) + + if name not in self._file_cache: + self._file_cache[name] = open(name, 'rb', 0) + self._buffer_cache[name] = array.array('B', [0] * self.KEY_BUF_LEN) + + def _button_file(self, name): + return self._file_cache[name] + + def _button_buffer(self, name): + return self._buffer_cache[name] + + @property + def buttons_pressed(self): + """ + Returns list of names of pressed buttons. + """ + for b in self._buffer_cache: + fcntl.ioctl(self._button_file(b), self.EVIOCGKEY, self._buffer_cache[b]) + + pressed = [] + for k, v in self._buttons.items(): + buf = self._buffer_cache[v['name']] + bit = v['value'] + + if bool(buf[int(bit / 8)] & 1 << bit % 8): + pressed.append(k) + + return pressed + + def _wait(self, wait_for_button_press, wait_for_button_release, timeout_ms): + stopwatch = StopWatch() + stopwatch.start() + + # wait_for_button_press/release can be a list of buttons or a string + # with the name of a single button. If it is a string of a single + # button convert that to a list. + if isinstance(wait_for_button_press, str): + wait_for_button_press = [ + wait_for_button_press, + ] + + if isinstance(wait_for_button_release, str): + wait_for_button_release = [ + wait_for_button_release, + ] + + for event in self.evdev_device.read_loop(): + if event.type == evdev.ecodes.EV_KEY: + all_pressed = True + all_released = True + pressed = self.buttons_pressed + + for button in wait_for_button_press: + if button not in pressed: + all_pressed = False + break + + for button in wait_for_button_release: + if button in pressed: + all_released = False + break + + if all_pressed and all_released: + return True + + if timeout_ms is not None and stopwatch.value_ms >= timeout_ms: + return False + + class Button(ButtonEVIO, EV3ButtonCommon): + """ + EV3 Buttons + """ + + _buttons = { + 'up': { + 'name': BUTTONS_FILENAME, + 'value': 103 + }, + 'down': { + 'name': BUTTONS_FILENAME, + 'value': 108 + }, + 'left': { + 'name': BUTTONS_FILENAME, + 'value': 105 + }, + 'right': { + 'name': BUTTONS_FILENAME, + 'value': 106 + }, + 'enter': { + 'name': BUTTONS_FILENAME, + 'value': 28 + }, + 'backspace': { + 'name': BUTTONS_FILENAME, + 'value': 14 + }, + } + evdev_device_name = EVDEV_DEVICE_NAME diff --git a/ev3dev2/console.py b/ev3dev2/console.py new file mode 100644 index 0000000..4dec21e --- /dev/null +++ b/ev3dev2/console.py @@ -0,0 +1,185 @@ +# ----------------------------------------------------------------------------- +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +# ----------------------------------------------------------------------------- + +import os + + +class Console(): + """ + A class that represents the EV3 LCD console, which implements ANSI codes + for cursor positioning, text color, and resetting the screen. Supports changing + the console font using standard system fonts. + """ + def __init__(self, font="Lat15-TerminusBold24x12"): + """ + Construct the Console instance, optionally with a font name specified. + + Parameter: + + - ``font`` (string): Font name, as found in ``/usr/share/consolefonts/`` + + """ + self._font = None + self._columns = 0 + self._rows = 0 + self._echo = False + self._cursor = False + self.set_font(font, reset_console=False) # don't reset the screen during construction + self.cursor = False + self.echo = False + + @property + def columns(self): + """ + Return (int) number of columns on the EV3 LCD console supported by the current font. + """ + return self._columns + + @property + def rows(self): + """ + Return (int) number of rows on the EV3 LCD console supported by the current font. + """ + return self._rows + + @property + def echo(self): + """ + Return (bool) whether the console echo mode is enabled. + """ + return self._echo + + @echo.setter + def echo(self, value): + """ + Enable/disable console echo (so that EV3 button presses do not show the escape characters on + the LCD console). Set to True to show the button codes, or False to hide them. + """ + self._echo = value + os.system("stty {}".format("echo" if value else "-echo")) + + @property + def cursor(self): + """ + Return (bool) whether the console cursor is visible. + """ + return self._cursor + + @cursor.setter + def cursor(self, value): + """ + Enable/disable console cursor (to hide the cursor on the LCD). + Set to True to show the cursor, or False to hide it. + """ + self._cursor = value + print("\x1b[?25{}".format('h' if value else 'l'), end='') + + def text_at(self, text, column=1, row=1, reset_console=False, inverse=False, alignment="L"): + """ + Display ``text`` (string) at grid position (``column``, ``row``). + Note that the grid locations are 1-based (not 0-based). + + Depending on the font, the number of columns and rows supported by the EV3 LCD console + can vary. Large fonts support as few as 11 columns and 4 rows, while small fonts support + 44 columns and 21 rows. The default font for the Console() class results in a grid that + is 14 columns and 5 rows. + + Using the ``inverse=True`` parameter will display the ``text`` with more emphasis and contrast, + as the background of the text will be black, and the foreground is white. Using inverse + can help in certain situations, such as to indicate when a color sensor senses + black, or the gyro sensor is pointing to zero. + + Use the ``alignment`` parameter to enable the function to align the ``text`` differently to the + column/row values passed-in. Use ``L`` for left-alignment (default), where the first character + in the ``text`` will show at the column/row position. Use ``R`` for right-alignment, where the + last character will show at the column/row position. Use ``C`` for center-alignment, where the + text string will centered at the column/row position (as close as possible using integer + division--odd-length text string will center better than even-length). + + Parameters: + + - ``text`` (string): Text to display + - ``column`` (int): LCD column position to start the text (1 = left column); + text will wrap when it reaches the right edge + - ``row`` (int): LCD row position to start the text (1 = top row) + - ``reset_console`` (bool): ``True`` to reset the EV3 LCD console before showing + the text; default is ``False`` + - ``inverse`` (bool): ``True`` for white on black, otherwise black on white; + default is ``False`` + - ``alignment`` (string): Align the ``text`` horizontally. Use ``L`` for left-alignment (default), + ``R`` for right-alignment, or ``C`` for center-alignment + + """ + + if reset_console: + self.reset_console() + + if alignment == "R": + column = column - len(text) + 1 + elif alignment == "C": + column -= len(text) // 2 + + if inverse: + text = "\x1b[7m{}\x1b[m".format(text) + + print("\x1b[{};{}H{}".format(row, column, text), end='') + + def set_font(self, font="Lat15-TerminusBold24x12", reset_console=True): + """ + Set the EV3 LCD console font and optionally reset the EV3 LCD console + to clear it and turn off the cursor. + + Parameters: + + - ``font`` (string): Font name, as found in ``/usr/share/consolefonts/`` + - ``reset_console`` (bool): ``True`` to reset the EV3 LCD console + after the font change; default is ``True`` + + """ + if font is not None and font != self._font: + self._font = font + os.system("setfont {}".format(font)) + rows, columns = os.popen('stty size').read().strip().split(" ") + self._rows = int(rows) + self._columns = int(columns) + + if reset_console: + self.reset_console() + + def clear_to_eol(self, column=None, row=None): + """ + Clear to the end of line from the ``column`` and ``row`` position + on the EV3 LCD console. Default to current cursor position. + + Parameters: + + - ``column`` (int): LCD column position to move to before clearing + - ``row`` (int): LCD row position to move to before clearing + + """ + if column is not None and row is not None: + print("\x1b[{};{}H".format(row, column), end='') + print("\x1b[K", end='') + + def reset_console(self): + """ + Clear the EV3 LCD console using ANSI codes, and move the cursor to 1,1 + """ + print("\x1b[2J\x1b[H", end='') diff --git a/ev3dev2/control/GyroBalancer.py b/ev3dev2/control/GyroBalancer.py new file mode 100644 index 0000000..6df1169 --- /dev/null +++ b/ev3dev2/control/GyroBalancer.py @@ -0,0 +1,498 @@ +"""Module for a robot that stands on two wheels and uses a gyro sensor. + +The robot (eg. BALANC3R) will to keep its balance and move in response to +the remote control. This code was adapted from Laurens Valk's script at +https://github.com/laurensvalk/segway. + +""" +# The MIT License (MIT) +# +# Copyright (c) 2016 Laurens Valk (laurensvalk@gmail.com) +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +import logging +import time +import json +import queue +import threading +import math +import signal +from collections import deque +from ev3dev2.power import PowerSupply +from ev3dev2.motor import LargeMotor, OUTPUT_A, OUTPUT_D +from ev3dev2.sensor.lego import GyroSensor, TouchSensor +from ev3dev2.sound import Sound +from collections import OrderedDict + +log = logging.getLogger(__name__) + +# Constants +RAD_PER_DEG = math.pi / 180 + +# EV3 Platform specific constants + +# For the LEGO EV3 Gyro in Rate mode, 1 unit = 1 deg/s +DEG_PER_SEC_PER_RAW_GYRO_UNIT = 1 + +# Express the above as the rate in rad/s per gyro unit +RAD_PER_SEC_PER_RAW_GYRO_UNIT = DEG_PER_SEC_PER_RAW_GYRO_UNIT * RAD_PER_DEG + +# For the LEGO EV3 Large Motor 1 unit = 1 deg +DEG_PER_RAW_MOTOR_UNIT = 1 + +# Express the above as the angle in rad per motor unit +RAD_PER_RAW_MOTOR_UNIT = DEG_PER_RAW_MOTOR_UNIT * RAD_PER_DEG + +# On the EV3, "1% speed" corresponds to 1.7 RPM (if speed +# control were enabled). +RPM_PER_PERCENT_SPEED = 1.7 + +# Convert this number to the speed in deg/s per "percent speed" +DEG_PER_SEC_PER_PERCENT_SPEED = RPM_PER_PERCENT_SPEED * 360 / 60 + +# Convert this number to the speed in rad/s per "percent speed" +RAD_PER_SEC_PER_PERCENT_SPEED = DEG_PER_SEC_PER_PERCENT_SPEED * RAD_PER_DEG + +# Speed and steering limits +SPEED_MAX = 20 +STEER_MAX = 8 + + +class GyroBalancer(object): + """ + Base class for a robot that stands on two wheels and uses a gyro sensor. + + Robot will keep its balance. + """ + def __init__(self, + gain_gyro_angle=1700, + gain_gyro_rate=120, + gain_motor_angle=7, + gain_motor_angular_speed=9, + gain_motor_angle_error_accumulated=3, + power_voltage_nominal=8.0, + pwr_friction_offset_nom=3, + timing_loop_msec=30, + motor_angle_history_length=5, + gyro_drift_compensation_factor=0.05, + left_motor_port=OUTPUT_D, + right_motor_port=OUTPUT_A, + debug=False): + """Create GyroBalancer.""" + # Gain parameters + self.gain_gyro_angle = gain_gyro_angle + self.gain_gyro_rate = gain_gyro_rate + self.gain_motor_angle = gain_motor_angle + self.gain_motor_angular_speed = gain_motor_angular_speed + self.gain_motor_angle_error_accumulated =\ + gain_motor_angle_error_accumulated + + # Power parameters + self.power_voltage_nominal = power_voltage_nominal + self.pwr_friction_offset_nom = pwr_friction_offset_nom + + # Timing parameters + self.timing_loop_msec = timing_loop_msec + self.motor_angle_history_length = motor_angle_history_length + self.gyro_drift_compensation_factor = gyro_drift_compensation_factor + + # Power supply setup + self.power_supply = PowerSupply() + + # Gyro Sensor setup + self.gyro = GyroSensor() + self.gyro.mode = self.gyro.MODE_GYRO_RATE + + # Touch Sensor setup + self.touch = TouchSensor() + + # IR Buttons setup + # self.remote = InfraredSensor() + # self.remote.mode = self.remote.MODE_IR_REMOTE + + # Configure the motors + self.motor_left = LargeMotor(left_motor_port) + self.motor_right = LargeMotor(right_motor_port) + + # Sound setup + self.sound = Sound() + + # Open sensor and motor files + self.gyro_file = open(self.gyro._path + "/value0", "rb") + self.touch_file = open(self.touch._path + "/value0", "rb") + self.encoder_left_file = open(self.motor_left._path + "/position", "rb") + self.encoder_right_file = open(self.motor_right._path + "/position", "rb") + self.dc_left_file = open(self.motor_left._path + "/duty_cycle_sp", "w") + self.dc_right_file = open(self.motor_right._path + "/duty_cycle_sp", "w") + + # Drive queue + self.drive_queue = queue.Queue() + + # Stop event for balance thread + self.stop_balance = threading.Event() + + # Debugging + self.debug = debug + + # Handlers for SIGINT and SIGTERM + signal.signal(signal.SIGINT, self.signal_int_handler) + signal.signal(signal.SIGTERM, self.signal_term_handler) + + def shutdown(self): + """Close all file handles and stop all motors.""" + self.stop_balance.set() # Stop balance thread + self.motor_left.stop() + self.motor_right.stop() + self.gyro_file.close() + self.touch_file.close() + self.encoder_left_file.close() + self.encoder_right_file.close() + self.dc_left_file.close() + self.dc_right_file.close() + + def _fast_read(self, infile): + """Function for fast reading from sensor files.""" + infile.seek(0) + return (int(infile.read().decode().strip())) + + def _fast_write(self, outfile, value): + """Function for fast writing to motor files.""" + outfile.truncate(0) + outfile.write(str(int(value))) + outfile.flush() + + def _set_duty(self, motor_duty_file, duty, friction_offset, voltage_comp): + """Function to set the duty cycle of the motors.""" + # Compensate for nominal voltage and round the input + duty_int = int(round(duty * voltage_comp)) + + # Add or subtract offset and clamp the value between -100 and 100 + if duty_int > 0: + duty_int = min(100, duty_int + friction_offset) + elif duty_int < 0: + duty_int = max(-100, duty_int - friction_offset) + + # Apply the signal to the motor + self._fast_write(motor_duty_file, duty_int) + + def signal_int_handler(self, signum, frame): + """Signal handler for SIGINT.""" + log.info('"Caught SIGINT') + self.shutdown() + raise GracefulShutdown() + + def signal_term_handler(self, signum, frame): + """Signal handler for SIGTERM.""" + log.info('"Caught SIGTERM') + self.shutdown() + raise GracefulShutdown() + + def balance(self): + """Run the _balance method as a thread.""" + balance_thread = threading.Thread(target=self._balance) + balance_thread.start() + + def _balance(self): + """Make the robot balance.""" + while True and not self.stop_balance.is_set(): + + # Reset the motors + self.motor_left.reset() # Reset the encoder + self.motor_right.reset() + self.motor_left.run_direct() # Set to run direct mode + self.motor_right.run_direct() + + # Initialize variables representing physical signals + # (more info on these in the docs) + + # The angle of "the motor", measured in raw units, + # degrees for the EV3). + # We will take the average of both motor positions as + # "the motor" angle, which is essentially how far the middle + # of the robot has travelled. + motor_angle_raw = 0 + + # The angle of the motor, converted to RAD (2*pi RAD + # equals 360 degrees). + motor_angle = 0 + + # The reference angle of the motor. The robot will attempt to + # drive forward or backward, such that its measured position + motor_angle_ref = 0 + # equals this reference (or close enough). + + # The error: the deviation of the measured motor angle from the + # reference. The robot attempts to make this zero, by driving + # toward the reference. + motor_angle_error = 0 + + # We add up all of the motor angle error in time. If this value + # gets out of hand, we can use it to drive the robot back to + # the reference position a bit quicker. + motor_angle_error_acc = 0 + + # The motor speed, estimated by how far the motor has turned in + # a given amount of time. + motor_angular_speed = 0 + + # The reference speed during manouvers: how fast we would like + # to drive, measured in RAD per second. + motor_angular_speed_ref = 0 + + # The error: the deviation of the motor speed from the + # reference speed. + motor_angular_speed_error = 0 + + # The 'voltage' signal we send to the motor. + # We calculate a new value each time, just right to keep the + # robot upright. + motor_duty_cycle = 0 + + # The raw value from the gyro sensor in rate mode. + gyro_rate_raw = 0 + + # The angular rate of the robot (how fast it is falling forward + # or backward), measured in RAD per second. + gyro_rate = 0 + + # The gyro doesn't measure the angle of the robot, but we can + # estimate this angle by keeping track of the gyro_rate value + # in time. + gyro_est_angle = 0 + + # Over time, the gyro rate value can drift. This causes the + # sensor to think it is moving even when it is perfectly still. + # We keep track of this offset. + gyro_offset = 0 + + # Start + log.info("Hold robot upright. Press touch sensor to start.") + self.sound.speak("Press touch sensor to start.") + + self.touch.wait_for_bump() + + # Read battery voltage + voltage_idle = self.power_supply.measured_volts + voltage_comp = self.power_voltage_nominal / voltage_idle + + # Offset to limit friction deadlock + friction_offset = int(round(self.pwr_friction_offset_nom * voltage_comp)) + + # Timing settings for the program + # Time of each loop, measured in seconds. + loop_time_target = self.timing_loop_msec / 1000 + loop_count = 0 # Loop counter, starting at 0 + + # A deque (a fifo array) which we'll use to keep track of + # previous motor positions, which we can use to calculate the + # rate of change (speed) + motor_angle_hist =\ + deque([0], self.motor_angle_history_length) + + # The rate at which we'll update the gyro offset (precise + # definition given in docs) + gyro_drift_comp_rate =\ + self.gyro_drift_compensation_factor *\ + loop_time_target * RAD_PER_SEC_PER_RAW_GYRO_UNIT + + # Calibrate Gyro + log.info("-----------------------------------") + log.info("Calibrating...") + + # As you hold the robot still, determine the average sensor + # value of 100 samples + gyro_calibrate_count = 100 + for i in range(gyro_calibrate_count): + gyro_offset = gyro_offset + self._fast_read(self.gyro_file) + time.sleep(0.01) + gyro_offset = gyro_offset / gyro_calibrate_count + + # Print the result + log.info("gyro_offset: " + str(gyro_offset)) + log.info("-----------------------------------") + log.info("GO!") + log.info("-----------------------------------") + log.info("Press Touch Sensor to re-start.") + log.info("-----------------------------------") + self.sound.beep() + + # Remember start time + prog_start_time = time.time() + + if self.debug: + # Data logging + data = OrderedDict() + loop_times = OrderedDict() + data['loop_times'] = loop_times + gyro_readings = OrderedDict() + data['gyro_readings'] = gyro_readings + + # Initial fast read touch sensor value + touch_pressed = False + + # Driving and Steering + speed, steering = (0, 0) + + # Record start time of loop + loop_start_time = time.time() + + # Balancing Loop + while not touch_pressed and not self.stop_balance.is_set(): + + loop_count += 1 + + # Check for drive instructions and set speed / steering + try: + speed, steering = self.drive_queue.get_nowait() + self.drive_queue.task_done() + except queue.Empty: + pass + + # Read the touch sensor (the kill switch) + touch_pressed = self._fast_read(self.touch_file) + + # Read the Motor Position + motor_angle_raw = ( + (self._fast_read(self.encoder_left_file) + self._fast_read(self.encoder_right_file)) / 2.0) + motor_angle = motor_angle_raw * RAD_PER_RAW_MOTOR_UNIT + + # Read the Gyro + gyro_rate_raw = self._fast_read(self.gyro_file) + + # Busy wait for the loop to reach target time length + loop_time = 0 + while (loop_time < loop_time_target): + loop_time = time.time() - loop_start_time + time.sleep(0.001) + + # Calculate most recent loop time + loop_time = time.time() - loop_start_time + + # Set start time of next loop + loop_start_time = time.time() + + if self.debug: + # Log gyro data and loop time + time_of_sample = time.time() - prog_start_time + gyro_readings[time_of_sample] = gyro_rate_raw + loop_times[time_of_sample] = loop_time * 1000.0 + + # Calculate gyro rate + gyro_rate = (gyro_rate_raw - gyro_offset) *\ + RAD_PER_SEC_PER_RAW_GYRO_UNIT + + # Calculate Motor Parameters + motor_angular_speed_ref =\ + speed * RAD_PER_SEC_PER_PERCENT_SPEED + motor_angle_ref = motor_angle_ref +\ + motor_angular_speed_ref * loop_time_target + motor_angle_error = motor_angle - motor_angle_ref + + # Compute Motor Speed + motor_angular_speed =\ + ((motor_angle - motor_angle_hist[0]) / + (self.motor_angle_history_length * loop_time_target)) + motor_angular_speed_error = motor_angular_speed + motor_angle_hist.append(motor_angle) + + # Compute the motor duty cycle value + motor_duty_cycle =\ + (self.gain_gyro_angle * gyro_est_angle + + self.gain_gyro_rate * gyro_rate + + self.gain_motor_angle * motor_angle_error + + self.gain_motor_angular_speed * + motor_angular_speed_error + + self.gain_motor_angle_error_accumulated * + motor_angle_error_acc) + + # Apply the signal to the motor, and add steering + self._set_duty(self.dc_right_file, motor_duty_cycle + steering, friction_offset, voltage_comp) + self._set_duty(self.dc_left_file, motor_duty_cycle - steering, friction_offset, voltage_comp) + + # Update angle estimate and gyro offset estimate + gyro_est_angle = gyro_est_angle + gyro_rate *\ + loop_time_target + gyro_offset = (1 - gyro_drift_comp_rate) *\ + gyro_offset + gyro_drift_comp_rate * gyro_rate_raw + + # Update Accumulated Motor Error + motor_angle_error_acc = motor_angle_error_acc +\ + motor_angle_error * loop_time_target + + # Closing down & Cleaning up + + # Loop end time, for stats + prog_end_time = time.time() + + # Turn off the motors + self._fast_write(self.dc_left_file, 0) + self._fast_write(self.dc_right_file, 0) + + # Wait for the Touch Sensor to be released + while self.touch.is_pressed: + time.sleep(0.01) + + # Calculate loop time + avg_loop_time = (prog_end_time - prog_start_time) / loop_count + log.info("Loop time:" + str(avg_loop_time * 1000) + "ms") + + # Print a stop message + log.info("-----------------------------------") + log.info("STOP") + log.info("-----------------------------------") + + if self.debug: + # Dump logged data to file + with open("data.txt", 'w') as data_file: + json.dump(data, data_file) + + def _move(self, speed=0, steering=0, seconds=None): + """Move robot.""" + self.drive_queue.put((speed, steering)) + if seconds is not None: + time.sleep(seconds) + self.drive_queue.put((0, 0)) + self.drive_queue.join() + + def move_forward(self, seconds=None): + """Move robot forward.""" + self._move(speed=SPEED_MAX, steering=0, seconds=seconds) + + def move_backward(self, seconds=None): + """Move robot backward.""" + self._move(speed=-SPEED_MAX, steering=0, seconds=seconds) + + def rotate_left(self, seconds=None): + """Rotate robot left.""" + self._move(speed=0, steering=STEER_MAX, seconds=seconds) + + def rotate_right(self, seconds=None): + """Rotate robot right.""" + self._move(speed=0, steering=-STEER_MAX, seconds=seconds) + + def stop(self): + """Stop robot (balancing will continue).""" + self._move(speed=0, steering=0) + + +class GracefulShutdown(Exception): + """Custom exception for SIGINT and SIGTERM.""" + + pass diff --git a/ev3dev/__init__.py b/ev3dev2/control/__init__.py similarity index 100% rename from ev3dev/__init__.py rename to ev3dev2/control/__init__.py diff --git a/ev3dev2/control/rc_tank.py b/ev3dev2/control/rc_tank.py new file mode 100644 index 0000000..6b93a42 --- /dev/null +++ b/ev3dev2/control/rc_tank.py @@ -0,0 +1,46 @@ +import logging +from ev3dev2.motor import MoveTank +from ev3dev2.sensor.lego import InfraredSensor +from time import sleep + +log = logging.getLogger(__name__) + + +# ============ +# Tank classes +# ============ +class RemoteControlledTank(MoveTank): + def __init__(self, left_motor_port, right_motor_port, polarity='inversed', speed=400, channel=1): + MoveTank.__init__(self, left_motor_port, right_motor_port) + self.set_polarity(polarity) + + left_motor = self.motors[left_motor_port] + right_motor = self.motors[right_motor_port] + self.speed_sp = speed + self.remote = InfraredSensor() + self.remote.on_channel1_top_left = self.make_move(left_motor, self.speed_sp) + self.remote.on_channel1_bottom_left = self.make_move(left_motor, self.speed_sp * -1) + self.remote.on_channel1_top_right = self.make_move(right_motor, self.speed_sp) + self.remote.on_channel1_bottom_right = self.make_move(right_motor, self.speed_sp * -1) + self.channel = channel + + def make_move(self, motor, dc_sp): + def move(state): + if state: + motor.run_forever(speed_sp=dc_sp) + else: + motor.stop() + + return move + + def main(self): + + try: + while True: + self.remote.process() + sleep(0.01) + + # Exit cleanly so that all motors are stopped + except (KeyboardInterrupt, Exception) as e: + log.exception(e) + self.off() diff --git a/ev3dev2/control/webserver.py b/ev3dev2/control/webserver.py new file mode 100644 index 0000000..d148af4 --- /dev/null +++ b/ev3dev2/control/webserver.py @@ -0,0 +1,283 @@ +#!/usr/bin/env python3 + +import logging +import os +import re +from ev3dev2.motor import MoveJoystick, list_motors, LargeMotor +from http.server import BaseHTTPRequestHandler, HTTPServer + +log = logging.getLogger(__name__) + + +# ================== +# Web Server classes +# ================== +class RobotWebHandler(BaseHTTPRequestHandler): + """ + Base WebHandler class for various types of robots. + + RobotWebHandler's do_GET() will serve files, it is up to the child + class to handle REST APIish GETs via their do_GET() + + self.robot is populated in RobotWebServer.__init__() + """ + + # File extension to mimetype + mimetype = { + 'css': 'text/css', + 'gif': 'image/gif', + 'html': 'text/html', + 'ico': 'image/x-icon', + 'jpg': 'image/jpg', + 'js': 'application/javascript', + 'png': 'image/png' + } + + def do_GET(self): + """ + If the request is for a known file type serve the file (or send a 404) and return True + """ + + if self.path == "/": + self.path = "/index.html" + + # Serve a file (image, css, html, etc) + if '.' in self.path: + extension = self.path.split('.')[-1] + mt = self.mimetype.get(extension) + + if mt: + filename = os.curdir + os.sep + self.path + + # Open the static file requested and send it + if os.path.exists(filename): + self.send_response(200) + self.send_header('Content-type', mt) + self.end_headers() + + if extension in ('gif', 'ico', 'jpg', 'png'): + # Open in binary mode, do not encode + with open(filename, mode='rb') as fh: + self.wfile.write(fh.read()) + else: + # Open as plain text and encode + with open(filename, mode='r') as fh: + self.wfile.write(fh.read().encode()) + else: + log.error("404: %s not found" % self.path) + self.send_error(404, 'File Not Found: %s' % self.path) + return True + + return False + + def log_message(self, format, *args): + """ + log using our own handler instead of BaseHTTPServer's + """ + # log.debug(format % args) + pass + + +max_move_xy_seq = 0 +motor_max_speed = None +medium_motor_max_speed = None +joystick_engaged = False + + +class TankWebHandler(RobotWebHandler): + def __str__(self): + return "%s-TankWebHandler" % self.robot + + def do_GET(self): + """ + Returns True if the requested URL is supported + """ + + if RobotWebHandler.do_GET(self): + return True + + global motor_max_speed + global medium_motor_max_speed + global max_move_xy_seq + global joystick_engaged + + if medium_motor_max_speed is None: + motor_max_speed = self.robot.left_motor.max_speed + + if hasattr(self.robot, 'medium_motor'): + medium_motor_max_speed = self.robot.medium_motor.max_speed + else: + medium_motor_max_speed = 0 + ''' + Sometimes we get AJAX requests out of order like this: + 2016-09-06 02:29:35,846 DEBUG: seq 65: (x, y): 0, 44 -> speed 462 462 + 2016-09-06 02:29:35,910 DEBUG: seq 66: (x, y): 0, 45 -> speed 473 473 + 2016-09-06 02:29:35,979 DEBUG: seq 67: (x, y): 0, 46 -> speed 483 483 + 2016-09-06 02:29:36,033 DEBUG: seq 69: (x, y): -1, 48 -> speed 491 504 + 2016-09-06 02:29:36,086 DEBUG: seq 68: (x, y): -1, 47 -> speed 480 494 + 2016-09-06 02:29:36,137 DEBUG: seq 70: (x, y): -1, 49 -> speed 501 515 + 2016-09-06 02:29:36,192 DEBUG: seq 73: (x, y): -2, 51 -> speed 509 536 + 2016-09-06 02:29:36,564 DEBUG: seq 74: (x, y): -3, 51 -> speed 496 536 + 2016-09-06 02:29:36,649 INFO: seq 75: CLIENT LOG: touchend + 2016-09-06 02:29:36,701 DEBUG: seq 71: (x, y): -1, 50 -> speed 512 525 + 2016-09-06 02:29:36,760 DEBUG: seq 76: move stop + 2016-09-06 02:29:36,814 DEBUG: seq 72: (x, y): -1, 51 -> speed 522 536 + + This can be bad because the last command sequentially was #76 which was "move stop" + but we RXed seq #72 after that so we started moving again and never stopped + + A quick fix is to have the client send us an AJAX request to let us know + when the joystick has been engaged so that we can ignore any move-xy events + that we get out of order and show up after "move stop" but before the + next "joystick-engaged" + + We can also ignore any move-xy requests that show up late by tracking the + max seq for any move-xy we service. + ''' + path = self.path.split('/') + seq = int(path[1]) + action = path[2] + + # desktop interface + if action == 'move-start': + direction = path[3] + speed_percentage = path[4] + log.debug("seq %d: move %s" % (seq, direction)) + + left_speed = int(int(speed_percentage) * motor_max_speed) / 100.0 + right_speed = int(int(speed_percentage) * motor_max_speed) / 100.0 + + if direction == 'forward': + self.robot.left_motor.run_forever(speed_sp=left_speed) + self.robot.right_motor.run_forever(speed_sp=right_speed) + + elif direction == 'backward': + self.robot.left_motor.run_forever(speed_sp=left_speed * -1) + self.robot.right_motor.run_forever(speed_sp=right_speed * -1) + + elif direction == 'left': + self.robot.left_motor.run_forever(speed_sp=left_speed * -1) + self.robot.right_motor.run_forever(speed_sp=right_speed) + + elif direction == 'right': + self.robot.left_motor.run_forever(speed_sp=left_speed) + self.robot.right_motor.run_forever(speed_sp=right_speed * -1) + + # desktop & mobile interface + elif action == 'move-stop': + log.debug("seq %d: move stop" % seq) + self.robot.left_motor.stop() + self.robot.right_motor.stop() + joystick_engaged = False + + # medium motor + elif action == 'motor-stop': + motor = path[3] + log.debug("seq %d: motor-stop %s" % (seq, motor)) + + if motor == 'medium': + if hasattr(self.robot, 'medium_motor'): + self.robot.medium_motor.stop() + else: + raise Exception("motor %s not supported yet" % motor) + + elif action == 'motor-start': + motor = path[3] + direction = path[4] + speed_percentage = path[5] + log.debug("seq %d: start motor %s, direction %s, speed_percentage %s" % + (seq, motor, direction, speed_percentage)) + + if motor == 'medium': + if hasattr(self.robot, 'medium_motor'): + if direction == 'clockwise': + medium_speed = int(int(speed_percentage) * medium_motor_max_speed) / 100.0 + self.robot.medium_motor.run_forever(speed_sp=medium_speed) + + elif direction == 'counter-clockwise': + medium_speed = int(int(speed_percentage) * medium_motor_max_speed) / 100.0 + self.robot.medium_motor.run_forever(speed_sp=medium_speed * -1) + else: + log.info("we do not have a medium_motor") + else: + raise Exception("motor %s not supported yet" % motor) + + # mobile interface + elif action == 'move-xy': + x = int(path[3]) + y = int(path[4]) + + if joystick_engaged: + if seq > max_move_xy_seq: + self.robot.on(x, y) + max_move_xy_seq = seq + log.debug("seq %d: (x, y) (%4d, %4d)" % (seq, x, y)) + else: + log.debug("seq %d: (x, y) %4d, %4d (ignore, max seq %d)" % (seq, x, y, max_move_xy_seq)) + else: + log.debug("seq %d: (x, y) %4d, %4d (ignore, joystick idle)" % (seq, x, y)) + + elif action == 'joystick-engaged': + joystick_engaged = True + + elif action == 'log': + msg = ''.join(path[3:]) + re_msg = re.search(r'^(.*)\?', msg) + + if re_msg: + msg = re_msg.group(1) + + log.debug("seq %d: CLIENT LOG: %s" % (seq, msg)) + + else: + log.warning("Unsupported URL %s" % self.path) + + # It is good practice to send this but if we are getting move-xy we + # tend to get a lot of them and we need to be as fast as possible so + # be bad and don't send a reply. This takes ~20ms. + if action != 'move-xy': + self.send_response(204) + + return True + + +class RobotWebServer(object): + """ + A Web server so that 'robot' can be controlled via 'handler_class' + """ + def __init__(self, robot, handler_class, port_number=8000): + self.content_server = None + self.handler_class = handler_class + self.handler_class.robot = robot + self.port_number = port_number + + def run(self): + + try: + log.info("Started HTTP server (content) on port %d" % self.port_number) + self.content_server = HTTPServer(('', self.port_number), self.handler_class) + self.content_server.serve_forever() + + # Exit cleanly, stop both web servers and all motors + except (KeyboardInterrupt, Exception) as e: + log.exception(e) + + if self.content_server: + self.content_server.socket.close() + self.content_server = None + + for motor in list_motors(): + motor.stop() + + +class WebControlledTank(MoveJoystick): + """ + A tank that is controlled via a web browser + """ + def __init__(self, left_motor, right_motor, port_number=8000, desc=None, motor_class=LargeMotor): + MoveJoystick.__init__(self, left_motor, right_motor, desc, motor_class) + self.www = RobotWebServer(self, TankWebHandler, port_number) + + def main(self): + # start the web server + self.www.run() diff --git a/ev3dev2/display.py b/ev3dev2/display.py new file mode 100644 index 0000000..de875f2 --- /dev/null +++ b/ev3dev2/display.py @@ -0,0 +1,424 @@ +# ----------------------------------------------------------------------------- +# Copyright (c) 2015 Ralph Hempel +# Copyright (c) 2015 Anton Vanhoucke +# Copyright (c) 2015 Denis Demidov +# Copyright (c) 2015 Eric Pascual +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +# ----------------------------------------------------------------------------- + +import sys +import os +import mmap +import ctypes +import logging +from PIL import Image, ImageDraw +from . import fonts +from . import get_current_platform, library_load_warning_message +from struct import pack + +if sys.version_info < (3, 4): + raise SystemError('Must be using Python 3.4 or higher') + +log = logging.getLogger(__name__) + +try: + # This is a linux-specific module. + # It is required by the Display class, but failure to import it may be + # safely ignored if one just needs to run API tests on Windows. + import fcntl +except ImportError: + log.warning(library_load_warning_message("fcntl", "Display")) + + +class FbMem(object): + """The framebuffer memory object. + + Made of: + - the framebuffer file descriptor + - the fix screen info struct + - the var screen info struct + - the mapped memory + """ + + # ------------------------------------------------------------------ + # The code is adapted from + # https://github.com/LinkCareServices/cairotft/blob/master/cairotft/linuxfb.py + # + # The original code came with the following license: + # ------------------------------------------------------------------ + # Copyright (c) 2012 Kurichan + # + # This program is free software. It comes without any warranty, to + # the extent permitted by applicable law. You can redistribute it + # and/or modify it under the terms of the Do What The Fuck You Want + # To Public License, Version 2, as published by Sam Hocevar. See + # http://sam.zoy.org/wtfpl/COPYING for more details. + # ------------------------------------------------------------------ + + __slots__ = ('fid', 'fix_info', 'var_info', 'mmap') + + FBIOGET_VSCREENINFO = 0x4600 + FBIOGET_FSCREENINFO = 0x4602 + + FB_VISUAL_MONO01 = 0 + FB_VISUAL_MONO10 = 1 + + class FixScreenInfo(ctypes.Structure): + """The fb_fix_screeninfo from fb.h.""" + + _fields_ = [ + ('id_name', ctypes.c_char * 16), + ('smem_start', ctypes.c_ulong), + ('smem_len', ctypes.c_uint32), + ('type', ctypes.c_uint32), + ('type_aux', ctypes.c_uint32), + ('visual', ctypes.c_uint32), + ('xpanstep', ctypes.c_uint16), + ('ypanstep', ctypes.c_uint16), + ('ywrapstep', ctypes.c_uint16), + ('line_length', ctypes.c_uint32), + ('mmio_start', ctypes.c_ulong), + ('mmio_len', ctypes.c_uint32), + ('accel', ctypes.c_uint32), + ('reserved', ctypes.c_uint16 * 3), + ] + + class VarScreenInfo(ctypes.Structure): + class FbBitField(ctypes.Structure): + """The fb_bitfield struct from fb.h.""" + + _fields_ = [ + ('offset', ctypes.c_uint32), + ('length', ctypes.c_uint32), + ('msb_right', ctypes.c_uint32), + ] + + def __str__(self): + return "%s (offset %s, length %s, msg_right %s)" %\ + (self.__class__.__name__, self.offset, self.length, self.msb_right) + + """The fb_var_screeninfo struct from fb.h.""" + + _fields_ = [ + ('xres', ctypes.c_uint32), + ('yres', ctypes.c_uint32), + ('xres_virtual', ctypes.c_uint32), + ('yres_virtual', ctypes.c_uint32), + ('xoffset', ctypes.c_uint32), + ('yoffset', ctypes.c_uint32), + ('bits_per_pixel', ctypes.c_uint32), + ('grayscale', ctypes.c_uint32), + ('red', FbBitField), + ('green', FbBitField), + ('blue', FbBitField), + ('transp', FbBitField), + ] + + def __str__(self): + return ("%sx%s at (%s,%s), bpp %s, grayscale %s, red %s, green %s, blue %s, transp %s" % + (self.xres, self.yres, self.xoffset, self.yoffset, self.bits_per_pixel, self.grayscale, self.red, + self.green, self.blue, self.transp)) + + def __init__(self, fbdev=None): + """Create the FbMem framebuffer memory object.""" + fid = FbMem._open_fbdev(fbdev) + fix_info = FbMem._get_fix_info(fid) + fbmmap = FbMem._map_fb_memory(fid, fix_info) + self.fid = fid + self.fix_info = fix_info + self.var_info = FbMem._get_var_info(fid) + self.mmap = fbmmap + + @staticmethod + def _open_fbdev(fbdev=None): + """Return the framebuffer file descriptor. + + Try to use the FRAMEBUFFER environment variable if fbdev is + not given. Use '/dev/fb0' by default. + """ + dev = fbdev or os.getenv('FRAMEBUFFER', '/dev/fb0') + fbfid = os.open(dev, os.O_RDWR) + return fbfid + + @staticmethod + def _get_fix_info(fbfid): + """Return the fix screen info from the framebuffer file descriptor.""" + fix_info = FbMem.FixScreenInfo() + fcntl.ioctl(fbfid, FbMem.FBIOGET_FSCREENINFO, fix_info) + return fix_info + + @staticmethod + def _get_var_info(fbfid): + """Return the var screen info from the framebuffer file descriptor.""" + var_info = FbMem.VarScreenInfo() + fcntl.ioctl(fbfid, FbMem.FBIOGET_VSCREENINFO, var_info) + return var_info + + @staticmethod + def _map_fb_memory(fbfid, fix_info): + """Map the framebuffer memory.""" + return mmap.mmap(fbfid, fix_info.smem_len, mmap.MAP_SHARED, mmap.PROT_READ | mmap.PROT_WRITE, offset=0) + + +class Display(FbMem): + """ + A convenience wrapper for the FbMem class. + Provides drawing functions from the python imaging library (PIL). + """ + + GRID_COLUMNS = 22 + GRID_COLUMN_PIXELS = 8 + GRID_ROWS = 12 + GRID_ROW_PIXELS = 10 + + def __init__(self, desc='Display'): + FbMem.__init__(self) + + self.platform = get_current_platform() + + if self.var_info.bits_per_pixel == 1: + im_type = "1" + + elif self.platform == "ev3" and self.var_info.bits_per_pixel == 32: + im_type = "L" + + elif self.var_info.bits_per_pixel == 16 or self.var_info.bits_per_pixel == 32: + im_type = "RGB" + + else: + raise Exception("Not supported - platform %s with bits_per_pixel %s" % + (self.platform, self.var_info.bits_per_pixel)) + + self._img = Image.new(im_type, (self.fix_info.line_length * 8 // self.var_info.bits_per_pixel, self.yres), + "white") + + self._draw = ImageDraw.Draw(self._img) + self.desc = desc + + def __str__(self): + return self.desc + + @property + def xres(self): + """ + Horizontal screen resolution + """ + return self.var_info.xres + + @property + def yres(self): + """ + Vertical screen resolution + """ + return self.var_info.yres + + @property + def shape(self): + """ + Dimensions of the screen. + """ + return (self.xres, self.yres) + + @property + def draw(self): + """ + Returns a handle to PIL.ImageDraw.Draw class associated with the screen. + + Example:: + + screen.draw.rectangle((10,10,60,20), fill='black') + """ + return self._draw + + @property + def image(self): + """ + Returns a handle to PIL.Image class that is backing the screen. This can + be accessed for blitting images to the screen. + + Example:: + + screen.image.paste(picture, (0, 0)) + """ + return self._img + + def clear(self): + """ + Clears the screen + """ + self._draw.rectangle(((0, 0), self.shape), fill="white") + + def _color565(self, r, g, b): + """Convert red, green, blue components to a 16-bit 565 RGB value. Components + should be values 0 to 255. + """ + return (((r & 0xF8) << 8) | ((g & 0xFC) << 3) | (b >> 3)) + + def _img_to_rgb565_bytes(self): + pixels = [self._color565(r, g, b) for (r, g, b) in self._img.getdata()] + return pack('H' * len(pixels), *pixels) + + def update(self): + """ + Applies pending changes to the screen. + Nothing will be drawn on the screen until this function is called. + """ + if self.var_info.bits_per_pixel == 1: + b = self._img.tobytes("raw", "1;R") + self.mmap[:len(b)] = b + + elif self.var_info.bits_per_pixel == 16: + self.mmap[:] = self._img_to_rgb565_bytes() + + elif self.var_info.bits_per_pixel == 32: + self.mmap[:] = self._img.convert("RGB").tobytes("raw", "XRGB") + + else: + raise Exception("Not supported - platform %s with bits_per_pixel %s" % + (self.platform, self.var_info.bits_per_pixel)) + + def image_filename(self, filename, clear_screen=True, x1=0, y1=0, x2=None, y2=None): + + if clear_screen: + self.clear() + + filename_im = Image.open(filename) + + if x2 is not None and y2 is not None: + return self._img.paste(filename_im, (x1, y1, x2, y2)) + else: + return self._img.paste(filename_im, (x1, y1)) + + def line(self, clear_screen=True, x1=10, y1=10, x2=50, y2=50, line_color='black', width=1): + """ + Draw a line from (x1, y1) to (x2, y2) + """ + + if clear_screen: + self.clear() + + return self.draw.line((x1, y1, x2, y2), fill=line_color, width=width) + + def circle(self, clear_screen=True, x=50, y=50, radius=40, fill_color='black', outline_color='black'): + """ + Draw a circle of 'radius' centered at (x, y) + """ + + if clear_screen: + self.clear() + + x1 = x - radius + y1 = y - radius + x2 = x + radius + y2 = y + radius + + return self.draw.ellipse((x1, y1, x2, y2), fill=fill_color, outline=outline_color) + + def rectangle(self, clear_screen=True, x1=10, y1=10, x2=80, y2=40, fill_color='black', outline_color='black'): + """ + Draw a rectangle where the top left corner is at (x1, y1) and the + bottom right corner is at (x2, y2) + """ + + if clear_screen: + self.clear() + + return self.draw.rectangle((x1, y1, x2, y2), fill=fill_color, outline=outline_color) + + def point(self, clear_screen=True, x=10, y=10, point_color='black'): + """ + Draw a single pixel at (x, y) + """ + + if clear_screen: + self.clear() + + return self.draw.point((x, y), fill=point_color) + + def text_pixels(self, text, clear_screen=True, x=0, y=0, text_color='black', font=None): + """ + Display ``text`` starting at pixel (x, y). + + The EV3 display is 178x128 pixels + + - (0, 0) would be the top left corner of the display + - (89, 64) would be right in the middle of the display + + ``text_color`` : PIL says it supports "common HTML color names". There + are 140 HTML color names listed here that are supported by all modern + browsers. This is probably a good list to start with. + https://www.w3schools.com/colors/colors_names.asp + + ``font`` : can be any font displayed here + http://ev3dev-lang.readthedocs.io/projects/python-ev3dev/en/ev3dev-stretch/display.html#bitmap-fonts + + - If font is a string, it is the name of a font to be loaded. + - If font is a Font object, returned from :meth:`ev3dev2.fonts.load`, then it is + used directly. This is desirable for faster display times. + + """ + + if clear_screen: + self.clear() + + if font is not None: + if isinstance(font, str): + assert font in fonts.available(), "%s is an invalid font" % font + font = fonts.load(font) + return self.draw.text((x, y), text, fill=text_color, font=font) + else: + return self.draw.text((x, y), text, fill=text_color) + + def text_grid(self, text, clear_screen=True, x=0, y=0, text_color='black', font=None): + """ + Display ``text`` starting at grid (x, y) + + The EV3 display can be broken down in a grid that is 22 columns wide + and 12 rows tall. Each column is 8 pixels wide and each row is 10 + pixels tall. + + ``text_color`` : PIL says it supports "common HTML color names". There + are 140 HTML color names listed here that are supported by all modern + browsers. This is probably a good list to start with. + https://www.w3schools.com/colors/colors_names.asp + + ``font`` : can be any font displayed here + http://ev3dev-lang.readthedocs.io/projects/python-ev3dev/en/ev3dev-stretch/display.html#bitmap-fonts + + - If font is a string, it is the name of a font to be loaded. + - If font is a Font object, returned from :meth:`ev3dev2.fonts.load`, then it is + used directly. This is desirable for faster display times. + + """ + + assert 0 <= x < Display.GRID_COLUMNS,\ + "grid columns must be between 0 and %d, %d was requested" %\ + ((Display.GRID_COLUMNS - 1, x)) + + assert 0 <= y < Display.GRID_ROWS,\ + "grid rows must be between 0 and %d, %d was requested" %\ + ((Display.GRID_ROWS - 1), y) + + return self.text_pixels(text, clear_screen, x * Display.GRID_COLUMN_PIXELS, y * Display.GRID_ROW_PIXELS, + text_color, font) + + def reset_screen(self): + self.clear() + self.update() diff --git a/ev3dev/fonts/__init__.py b/ev3dev2/fonts/__init__.py similarity index 51% rename from ev3dev/fonts/__init__.py rename to ev3dev2/fonts/__init__.py index abfe504..95b4804 100644 --- a/ev3dev/fonts/__init__.py +++ b/ev3dev2/fonts/__init__.py @@ -1,18 +1,17 @@ -import pkg_resources import os.path +from glob import glob from PIL import ImageFont + def available(): """ Returns list of available font names. """ - names = [] - for f in pkg_resources.resource_listdir('ev3dev.fonts', ''): - name, ext = os.path.splitext(os.path.basename(f)) - if ext == '.pil': - names.append(name) + font_dir = os.path.dirname(__file__) + names = [os.path.basename(os.path.splitext(f)[0]) for f in glob(os.path.join(font_dir, '*.pil'))] return sorted(names) + def load(name): """ Loads the font specified by name and returns it as an instance of @@ -20,9 +19,9 @@ def load(name): class. """ try: - pil_file = pkg_resources.resource_filename('ev3dev.fonts', '{}.pil'.format(name)) - pbm_file = pkg_resources.resource_filename('ev3dev.fonts', '{}.pbm'.format(name)) + font_dir = os.path.dirname(__file__) + pil_file = os.path.join(font_dir, '{}.pil'.format(name)) return ImageFont.load(pil_file) except FileNotFoundError: raise Exception('Failed to load font "{}". '.format(name) + - 'Check ev3dev.fonts.available() for the list of available fonts') + 'Check ev3dev.fonts.available() for the list of available fonts') diff --git a/ev3dev/fonts/charB08.pbm b/ev3dev2/fonts/charB08.pbm similarity index 100% rename from ev3dev/fonts/charB08.pbm rename to ev3dev2/fonts/charB08.pbm diff --git a/ev3dev/fonts/charB08.pil b/ev3dev2/fonts/charB08.pil similarity index 100% rename from ev3dev/fonts/charB08.pil rename to ev3dev2/fonts/charB08.pil diff --git a/ev3dev/fonts/charB10.pbm b/ev3dev2/fonts/charB10.pbm similarity index 100% rename from ev3dev/fonts/charB10.pbm rename to ev3dev2/fonts/charB10.pbm diff --git a/ev3dev/fonts/charB10.pil b/ev3dev2/fonts/charB10.pil similarity index 100% rename from ev3dev/fonts/charB10.pil rename to ev3dev2/fonts/charB10.pil diff --git a/ev3dev/fonts/charB12.pbm b/ev3dev2/fonts/charB12.pbm similarity index 100% rename from ev3dev/fonts/charB12.pbm rename to ev3dev2/fonts/charB12.pbm diff --git a/ev3dev/fonts/charB12.pil b/ev3dev2/fonts/charB12.pil similarity index 100% rename from ev3dev/fonts/charB12.pil rename to ev3dev2/fonts/charB12.pil diff --git a/ev3dev/fonts/charB14.pbm b/ev3dev2/fonts/charB14.pbm similarity index 100% rename from ev3dev/fonts/charB14.pbm rename to ev3dev2/fonts/charB14.pbm diff --git a/ev3dev/fonts/charB14.pil b/ev3dev2/fonts/charB14.pil similarity index 100% rename from ev3dev/fonts/charB14.pil rename to ev3dev2/fonts/charB14.pil diff --git a/ev3dev/fonts/charB18.pbm b/ev3dev2/fonts/charB18.pbm similarity index 100% rename from ev3dev/fonts/charB18.pbm rename to ev3dev2/fonts/charB18.pbm diff --git a/ev3dev/fonts/charB18.pil b/ev3dev2/fonts/charB18.pil similarity index 100% rename from ev3dev/fonts/charB18.pil rename to ev3dev2/fonts/charB18.pil diff --git a/ev3dev/fonts/charB24.pbm b/ev3dev2/fonts/charB24.pbm similarity index 100% rename from ev3dev/fonts/charB24.pbm rename to ev3dev2/fonts/charB24.pbm diff --git a/ev3dev/fonts/charB24.pil b/ev3dev2/fonts/charB24.pil similarity index 100% rename from ev3dev/fonts/charB24.pil rename to ev3dev2/fonts/charB24.pil diff --git a/ev3dev/fonts/charBI08.pbm b/ev3dev2/fonts/charBI08.pbm similarity index 100% rename from ev3dev/fonts/charBI08.pbm rename to ev3dev2/fonts/charBI08.pbm diff --git a/ev3dev/fonts/charBI08.pil b/ev3dev2/fonts/charBI08.pil similarity index 100% rename from ev3dev/fonts/charBI08.pil rename to ev3dev2/fonts/charBI08.pil diff --git a/ev3dev/fonts/charBI10.pbm b/ev3dev2/fonts/charBI10.pbm similarity index 100% rename from ev3dev/fonts/charBI10.pbm rename to ev3dev2/fonts/charBI10.pbm diff --git a/ev3dev/fonts/charBI10.pil b/ev3dev2/fonts/charBI10.pil similarity index 100% rename from ev3dev/fonts/charBI10.pil rename to ev3dev2/fonts/charBI10.pil diff --git a/ev3dev/fonts/charBI12.pbm b/ev3dev2/fonts/charBI12.pbm similarity index 100% rename from ev3dev/fonts/charBI12.pbm rename to ev3dev2/fonts/charBI12.pbm diff --git a/ev3dev/fonts/charBI12.pil b/ev3dev2/fonts/charBI12.pil similarity index 100% rename from ev3dev/fonts/charBI12.pil rename to ev3dev2/fonts/charBI12.pil diff --git a/ev3dev/fonts/charBI14.pbm b/ev3dev2/fonts/charBI14.pbm similarity index 100% rename from ev3dev/fonts/charBI14.pbm rename to ev3dev2/fonts/charBI14.pbm diff --git a/ev3dev/fonts/charBI14.pil b/ev3dev2/fonts/charBI14.pil similarity index 100% rename from ev3dev/fonts/charBI14.pil rename to ev3dev2/fonts/charBI14.pil diff --git a/ev3dev/fonts/charBI18.pbm b/ev3dev2/fonts/charBI18.pbm similarity index 100% rename from ev3dev/fonts/charBI18.pbm rename to ev3dev2/fonts/charBI18.pbm diff --git a/ev3dev/fonts/charBI18.pil b/ev3dev2/fonts/charBI18.pil similarity index 100% rename from ev3dev/fonts/charBI18.pil rename to ev3dev2/fonts/charBI18.pil diff --git a/ev3dev/fonts/charBI24.pbm b/ev3dev2/fonts/charBI24.pbm similarity index 100% rename from ev3dev/fonts/charBI24.pbm rename to ev3dev2/fonts/charBI24.pbm diff --git a/ev3dev/fonts/charBI24.pil b/ev3dev2/fonts/charBI24.pil similarity index 100% rename from ev3dev/fonts/charBI24.pil rename to ev3dev2/fonts/charBI24.pil diff --git a/ev3dev/fonts/charI08.pbm b/ev3dev2/fonts/charI08.pbm similarity index 100% rename from ev3dev/fonts/charI08.pbm rename to ev3dev2/fonts/charI08.pbm diff --git a/ev3dev/fonts/charI08.pil b/ev3dev2/fonts/charI08.pil similarity index 100% rename from ev3dev/fonts/charI08.pil rename to ev3dev2/fonts/charI08.pil diff --git a/ev3dev/fonts/charI10.pbm b/ev3dev2/fonts/charI10.pbm similarity index 100% rename from ev3dev/fonts/charI10.pbm rename to ev3dev2/fonts/charI10.pbm diff --git a/ev3dev/fonts/charI10.pil b/ev3dev2/fonts/charI10.pil similarity index 100% rename from ev3dev/fonts/charI10.pil rename to ev3dev2/fonts/charI10.pil diff --git a/ev3dev/fonts/charI12.pbm b/ev3dev2/fonts/charI12.pbm similarity index 100% rename from ev3dev/fonts/charI12.pbm rename to ev3dev2/fonts/charI12.pbm diff --git a/ev3dev/fonts/charI12.pil b/ev3dev2/fonts/charI12.pil similarity index 100% rename from ev3dev/fonts/charI12.pil rename to ev3dev2/fonts/charI12.pil diff --git a/ev3dev/fonts/charI14.pbm b/ev3dev2/fonts/charI14.pbm similarity index 100% rename from ev3dev/fonts/charI14.pbm rename to ev3dev2/fonts/charI14.pbm diff --git a/ev3dev/fonts/charI14.pil b/ev3dev2/fonts/charI14.pil similarity index 100% rename from ev3dev/fonts/charI14.pil rename to ev3dev2/fonts/charI14.pil diff --git a/ev3dev/fonts/charI18.pbm b/ev3dev2/fonts/charI18.pbm similarity index 100% rename from ev3dev/fonts/charI18.pbm rename to ev3dev2/fonts/charI18.pbm diff --git a/ev3dev/fonts/charI18.pil b/ev3dev2/fonts/charI18.pil similarity index 100% rename from ev3dev/fonts/charI18.pil rename to ev3dev2/fonts/charI18.pil diff --git a/ev3dev/fonts/charI24.pbm b/ev3dev2/fonts/charI24.pbm similarity index 100% rename from ev3dev/fonts/charI24.pbm rename to ev3dev2/fonts/charI24.pbm diff --git a/ev3dev/fonts/charI24.pil b/ev3dev2/fonts/charI24.pil similarity index 100% rename from ev3dev/fonts/charI24.pil rename to ev3dev2/fonts/charI24.pil diff --git a/ev3dev/fonts/charR08.pbm b/ev3dev2/fonts/charR08.pbm similarity index 100% rename from ev3dev/fonts/charR08.pbm rename to ev3dev2/fonts/charR08.pbm diff --git a/ev3dev/fonts/charR08.pil b/ev3dev2/fonts/charR08.pil similarity index 100% rename from ev3dev/fonts/charR08.pil rename to ev3dev2/fonts/charR08.pil diff --git a/ev3dev/fonts/charR10.pbm b/ev3dev2/fonts/charR10.pbm similarity index 100% rename from ev3dev/fonts/charR10.pbm rename to ev3dev2/fonts/charR10.pbm diff --git a/ev3dev/fonts/charR10.pil b/ev3dev2/fonts/charR10.pil similarity index 100% rename from ev3dev/fonts/charR10.pil rename to ev3dev2/fonts/charR10.pil diff --git a/ev3dev/fonts/charR12.pbm b/ev3dev2/fonts/charR12.pbm similarity index 100% rename from ev3dev/fonts/charR12.pbm rename to ev3dev2/fonts/charR12.pbm diff --git a/ev3dev/fonts/charR12.pil b/ev3dev2/fonts/charR12.pil similarity index 100% rename from ev3dev/fonts/charR12.pil rename to ev3dev2/fonts/charR12.pil diff --git a/ev3dev/fonts/charR14.pbm b/ev3dev2/fonts/charR14.pbm similarity index 100% rename from ev3dev/fonts/charR14.pbm rename to ev3dev2/fonts/charR14.pbm diff --git a/ev3dev/fonts/charR14.pil b/ev3dev2/fonts/charR14.pil similarity index 100% rename from ev3dev/fonts/charR14.pil rename to ev3dev2/fonts/charR14.pil diff --git a/ev3dev/fonts/charR18.pbm b/ev3dev2/fonts/charR18.pbm similarity index 100% rename from ev3dev/fonts/charR18.pbm rename to ev3dev2/fonts/charR18.pbm diff --git a/ev3dev/fonts/charR18.pil b/ev3dev2/fonts/charR18.pil similarity index 100% rename from ev3dev/fonts/charR18.pil rename to ev3dev2/fonts/charR18.pil diff --git a/ev3dev/fonts/charR24.pbm b/ev3dev2/fonts/charR24.pbm similarity index 100% rename from ev3dev/fonts/charR24.pbm rename to ev3dev2/fonts/charR24.pbm diff --git a/ev3dev/fonts/charR24.pil b/ev3dev2/fonts/charR24.pil similarity index 100% rename from ev3dev/fonts/charR24.pil rename to ev3dev2/fonts/charR24.pil diff --git a/ev3dev/fonts/courB08.pbm b/ev3dev2/fonts/courB08.pbm similarity index 100% rename from ev3dev/fonts/courB08.pbm rename to ev3dev2/fonts/courB08.pbm diff --git a/ev3dev/fonts/courB08.pil b/ev3dev2/fonts/courB08.pil similarity index 100% rename from ev3dev/fonts/courB08.pil rename to ev3dev2/fonts/courB08.pil diff --git a/ev3dev/fonts/courB10.pbm b/ev3dev2/fonts/courB10.pbm similarity index 100% rename from ev3dev/fonts/courB10.pbm rename to ev3dev2/fonts/courB10.pbm diff --git a/ev3dev/fonts/courB10.pil b/ev3dev2/fonts/courB10.pil similarity index 100% rename from ev3dev/fonts/courB10.pil rename to ev3dev2/fonts/courB10.pil diff --git a/ev3dev/fonts/courB12.pbm b/ev3dev2/fonts/courB12.pbm similarity index 100% rename from ev3dev/fonts/courB12.pbm rename to ev3dev2/fonts/courB12.pbm diff --git a/ev3dev/fonts/courB12.pil b/ev3dev2/fonts/courB12.pil similarity index 100% rename from ev3dev/fonts/courB12.pil rename to ev3dev2/fonts/courB12.pil diff --git a/ev3dev/fonts/courB14.pbm b/ev3dev2/fonts/courB14.pbm similarity index 100% rename from ev3dev/fonts/courB14.pbm rename to ev3dev2/fonts/courB14.pbm diff --git a/ev3dev/fonts/courB14.pil b/ev3dev2/fonts/courB14.pil similarity index 100% rename from ev3dev/fonts/courB14.pil rename to ev3dev2/fonts/courB14.pil diff --git a/ev3dev/fonts/courB18.pbm b/ev3dev2/fonts/courB18.pbm similarity index 100% rename from ev3dev/fonts/courB18.pbm rename to ev3dev2/fonts/courB18.pbm diff --git a/ev3dev/fonts/courB18.pil b/ev3dev2/fonts/courB18.pil similarity index 100% rename from ev3dev/fonts/courB18.pil rename to ev3dev2/fonts/courB18.pil diff --git a/ev3dev/fonts/courB24.pbm b/ev3dev2/fonts/courB24.pbm similarity index 100% rename from ev3dev/fonts/courB24.pbm rename to ev3dev2/fonts/courB24.pbm diff --git a/ev3dev/fonts/courB24.pil b/ev3dev2/fonts/courB24.pil similarity index 100% rename from ev3dev/fonts/courB24.pil rename to ev3dev2/fonts/courB24.pil diff --git a/ev3dev/fonts/courBO08.pbm b/ev3dev2/fonts/courBO08.pbm similarity index 100% rename from ev3dev/fonts/courBO08.pbm rename to ev3dev2/fonts/courBO08.pbm diff --git a/ev3dev/fonts/courBO08.pil b/ev3dev2/fonts/courBO08.pil similarity index 100% rename from ev3dev/fonts/courBO08.pil rename to ev3dev2/fonts/courBO08.pil diff --git a/ev3dev/fonts/courBO10.pbm b/ev3dev2/fonts/courBO10.pbm similarity index 100% rename from ev3dev/fonts/courBO10.pbm rename to ev3dev2/fonts/courBO10.pbm diff --git a/ev3dev/fonts/courBO10.pil b/ev3dev2/fonts/courBO10.pil similarity index 100% rename from ev3dev/fonts/courBO10.pil rename to ev3dev2/fonts/courBO10.pil diff --git a/ev3dev/fonts/courBO12.pbm b/ev3dev2/fonts/courBO12.pbm similarity index 100% rename from ev3dev/fonts/courBO12.pbm rename to ev3dev2/fonts/courBO12.pbm diff --git a/ev3dev/fonts/courBO12.pil b/ev3dev2/fonts/courBO12.pil similarity index 100% rename from ev3dev/fonts/courBO12.pil rename to ev3dev2/fonts/courBO12.pil diff --git a/ev3dev/fonts/courBO14.pbm b/ev3dev2/fonts/courBO14.pbm similarity index 100% rename from ev3dev/fonts/courBO14.pbm rename to ev3dev2/fonts/courBO14.pbm diff --git a/ev3dev/fonts/courBO14.pil b/ev3dev2/fonts/courBO14.pil similarity index 100% rename from ev3dev/fonts/courBO14.pil rename to ev3dev2/fonts/courBO14.pil diff --git a/ev3dev/fonts/courBO18.pbm b/ev3dev2/fonts/courBO18.pbm similarity index 100% rename from ev3dev/fonts/courBO18.pbm rename to ev3dev2/fonts/courBO18.pbm diff --git a/ev3dev/fonts/courBO18.pil b/ev3dev2/fonts/courBO18.pil similarity index 100% rename from ev3dev/fonts/courBO18.pil rename to ev3dev2/fonts/courBO18.pil diff --git a/ev3dev/fonts/courBO24.pbm b/ev3dev2/fonts/courBO24.pbm similarity index 100% rename from ev3dev/fonts/courBO24.pbm rename to ev3dev2/fonts/courBO24.pbm diff --git a/ev3dev/fonts/courBO24.pil b/ev3dev2/fonts/courBO24.pil similarity index 100% rename from ev3dev/fonts/courBO24.pil rename to ev3dev2/fonts/courBO24.pil diff --git a/ev3dev/fonts/courO08.pbm b/ev3dev2/fonts/courO08.pbm similarity index 100% rename from ev3dev/fonts/courO08.pbm rename to ev3dev2/fonts/courO08.pbm diff --git a/ev3dev/fonts/courO08.pil b/ev3dev2/fonts/courO08.pil similarity index 100% rename from ev3dev/fonts/courO08.pil rename to ev3dev2/fonts/courO08.pil diff --git a/ev3dev/fonts/courO10.pbm b/ev3dev2/fonts/courO10.pbm similarity index 100% rename from ev3dev/fonts/courO10.pbm rename to ev3dev2/fonts/courO10.pbm diff --git a/ev3dev/fonts/courO10.pil b/ev3dev2/fonts/courO10.pil similarity index 100% rename from ev3dev/fonts/courO10.pil rename to ev3dev2/fonts/courO10.pil diff --git a/ev3dev/fonts/courO12.pbm b/ev3dev2/fonts/courO12.pbm similarity index 100% rename from ev3dev/fonts/courO12.pbm rename to ev3dev2/fonts/courO12.pbm diff --git a/ev3dev/fonts/courO12.pil b/ev3dev2/fonts/courO12.pil similarity index 100% rename from ev3dev/fonts/courO12.pil rename to ev3dev2/fonts/courO12.pil diff --git a/ev3dev/fonts/courO14.pbm b/ev3dev2/fonts/courO14.pbm similarity index 100% rename from ev3dev/fonts/courO14.pbm rename to ev3dev2/fonts/courO14.pbm diff --git a/ev3dev/fonts/courO14.pil b/ev3dev2/fonts/courO14.pil similarity index 100% rename from ev3dev/fonts/courO14.pil rename to ev3dev2/fonts/courO14.pil diff --git a/ev3dev/fonts/courO18.pbm b/ev3dev2/fonts/courO18.pbm similarity index 100% rename from ev3dev/fonts/courO18.pbm rename to ev3dev2/fonts/courO18.pbm diff --git a/ev3dev/fonts/courO18.pil b/ev3dev2/fonts/courO18.pil similarity index 100% rename from ev3dev/fonts/courO18.pil rename to ev3dev2/fonts/courO18.pil diff --git a/ev3dev/fonts/courO24.pbm b/ev3dev2/fonts/courO24.pbm similarity index 100% rename from ev3dev/fonts/courO24.pbm rename to ev3dev2/fonts/courO24.pbm diff --git a/ev3dev/fonts/courO24.pil b/ev3dev2/fonts/courO24.pil similarity index 100% rename from ev3dev/fonts/courO24.pil rename to ev3dev2/fonts/courO24.pil diff --git a/ev3dev/fonts/courR08.pbm b/ev3dev2/fonts/courR08.pbm similarity index 100% rename from ev3dev/fonts/courR08.pbm rename to ev3dev2/fonts/courR08.pbm diff --git a/ev3dev/fonts/courR08.pil b/ev3dev2/fonts/courR08.pil similarity index 100% rename from ev3dev/fonts/courR08.pil rename to ev3dev2/fonts/courR08.pil diff --git a/ev3dev/fonts/courR10.pbm b/ev3dev2/fonts/courR10.pbm similarity index 100% rename from ev3dev/fonts/courR10.pbm rename to ev3dev2/fonts/courR10.pbm diff --git a/ev3dev/fonts/courR10.pil b/ev3dev2/fonts/courR10.pil similarity index 100% rename from ev3dev/fonts/courR10.pil rename to ev3dev2/fonts/courR10.pil diff --git a/ev3dev/fonts/courR12.pbm b/ev3dev2/fonts/courR12.pbm similarity index 100% rename from ev3dev/fonts/courR12.pbm rename to ev3dev2/fonts/courR12.pbm diff --git a/ev3dev/fonts/courR12.pil b/ev3dev2/fonts/courR12.pil similarity index 100% rename from ev3dev/fonts/courR12.pil rename to ev3dev2/fonts/courR12.pil diff --git a/ev3dev/fonts/courR14.pbm b/ev3dev2/fonts/courR14.pbm similarity index 100% rename from ev3dev/fonts/courR14.pbm rename to ev3dev2/fonts/courR14.pbm diff --git a/ev3dev/fonts/courR14.pil b/ev3dev2/fonts/courR14.pil similarity index 100% rename from ev3dev/fonts/courR14.pil rename to ev3dev2/fonts/courR14.pil diff --git a/ev3dev/fonts/courR18.pbm b/ev3dev2/fonts/courR18.pbm similarity index 100% rename from ev3dev/fonts/courR18.pbm rename to ev3dev2/fonts/courR18.pbm diff --git a/ev3dev/fonts/courR18.pil b/ev3dev2/fonts/courR18.pil similarity index 100% rename from ev3dev/fonts/courR18.pil rename to ev3dev2/fonts/courR18.pil diff --git a/ev3dev/fonts/courR24.pbm b/ev3dev2/fonts/courR24.pbm similarity index 100% rename from ev3dev/fonts/courR24.pbm rename to ev3dev2/fonts/courR24.pbm diff --git a/ev3dev/fonts/courR24.pil b/ev3dev2/fonts/courR24.pil similarity index 100% rename from ev3dev/fonts/courR24.pil rename to ev3dev2/fonts/courR24.pil diff --git a/ev3dev/fonts/helvB08.pbm b/ev3dev2/fonts/helvB08.pbm similarity index 100% rename from ev3dev/fonts/helvB08.pbm rename to ev3dev2/fonts/helvB08.pbm diff --git a/ev3dev/fonts/helvB08.pil b/ev3dev2/fonts/helvB08.pil similarity index 100% rename from ev3dev/fonts/helvB08.pil rename to ev3dev2/fonts/helvB08.pil diff --git a/ev3dev/fonts/helvB10.pbm b/ev3dev2/fonts/helvB10.pbm similarity index 100% rename from ev3dev/fonts/helvB10.pbm rename to ev3dev2/fonts/helvB10.pbm diff --git a/ev3dev/fonts/helvB10.pil b/ev3dev2/fonts/helvB10.pil similarity index 100% rename from ev3dev/fonts/helvB10.pil rename to ev3dev2/fonts/helvB10.pil diff --git a/ev3dev/fonts/helvB12.pbm b/ev3dev2/fonts/helvB12.pbm similarity index 100% rename from ev3dev/fonts/helvB12.pbm rename to ev3dev2/fonts/helvB12.pbm diff --git a/ev3dev/fonts/helvB12.pil b/ev3dev2/fonts/helvB12.pil similarity index 100% rename from ev3dev/fonts/helvB12.pil rename to ev3dev2/fonts/helvB12.pil diff --git a/ev3dev/fonts/helvB14.pbm b/ev3dev2/fonts/helvB14.pbm similarity index 100% rename from ev3dev/fonts/helvB14.pbm rename to ev3dev2/fonts/helvB14.pbm diff --git a/ev3dev/fonts/helvB14.pil b/ev3dev2/fonts/helvB14.pil similarity index 100% rename from ev3dev/fonts/helvB14.pil rename to ev3dev2/fonts/helvB14.pil diff --git a/ev3dev/fonts/helvB18.pbm b/ev3dev2/fonts/helvB18.pbm similarity index 100% rename from ev3dev/fonts/helvB18.pbm rename to ev3dev2/fonts/helvB18.pbm diff --git a/ev3dev/fonts/helvB18.pil b/ev3dev2/fonts/helvB18.pil similarity index 100% rename from ev3dev/fonts/helvB18.pil rename to ev3dev2/fonts/helvB18.pil diff --git a/ev3dev/fonts/helvB24.pbm b/ev3dev2/fonts/helvB24.pbm similarity index 100% rename from ev3dev/fonts/helvB24.pbm rename to ev3dev2/fonts/helvB24.pbm diff --git a/ev3dev/fonts/helvB24.pil b/ev3dev2/fonts/helvB24.pil similarity index 100% rename from ev3dev/fonts/helvB24.pil rename to ev3dev2/fonts/helvB24.pil diff --git a/ev3dev/fonts/helvBO08.pbm b/ev3dev2/fonts/helvBO08.pbm similarity index 100% rename from ev3dev/fonts/helvBO08.pbm rename to ev3dev2/fonts/helvBO08.pbm diff --git a/ev3dev/fonts/helvBO08.pil b/ev3dev2/fonts/helvBO08.pil similarity index 100% rename from ev3dev/fonts/helvBO08.pil rename to ev3dev2/fonts/helvBO08.pil diff --git a/ev3dev/fonts/helvBO10.pbm b/ev3dev2/fonts/helvBO10.pbm similarity index 100% rename from ev3dev/fonts/helvBO10.pbm rename to ev3dev2/fonts/helvBO10.pbm diff --git a/ev3dev/fonts/helvBO10.pil b/ev3dev2/fonts/helvBO10.pil similarity index 100% rename from ev3dev/fonts/helvBO10.pil rename to ev3dev2/fonts/helvBO10.pil diff --git a/ev3dev/fonts/helvBO12.pbm b/ev3dev2/fonts/helvBO12.pbm similarity index 100% rename from ev3dev/fonts/helvBO12.pbm rename to ev3dev2/fonts/helvBO12.pbm diff --git a/ev3dev/fonts/helvBO12.pil b/ev3dev2/fonts/helvBO12.pil similarity index 100% rename from ev3dev/fonts/helvBO12.pil rename to ev3dev2/fonts/helvBO12.pil diff --git a/ev3dev/fonts/helvBO14.pbm b/ev3dev2/fonts/helvBO14.pbm similarity index 100% rename from ev3dev/fonts/helvBO14.pbm rename to ev3dev2/fonts/helvBO14.pbm diff --git a/ev3dev/fonts/helvBO14.pil b/ev3dev2/fonts/helvBO14.pil similarity index 100% rename from ev3dev/fonts/helvBO14.pil rename to ev3dev2/fonts/helvBO14.pil diff --git a/ev3dev/fonts/helvBO18.pbm b/ev3dev2/fonts/helvBO18.pbm similarity index 100% rename from ev3dev/fonts/helvBO18.pbm rename to ev3dev2/fonts/helvBO18.pbm diff --git a/ev3dev/fonts/helvBO18.pil b/ev3dev2/fonts/helvBO18.pil similarity index 100% rename from ev3dev/fonts/helvBO18.pil rename to ev3dev2/fonts/helvBO18.pil diff --git a/ev3dev/fonts/helvBO24.pbm b/ev3dev2/fonts/helvBO24.pbm similarity index 100% rename from ev3dev/fonts/helvBO24.pbm rename to ev3dev2/fonts/helvBO24.pbm diff --git a/ev3dev/fonts/helvBO24.pil b/ev3dev2/fonts/helvBO24.pil similarity index 100% rename from ev3dev/fonts/helvBO24.pil rename to ev3dev2/fonts/helvBO24.pil diff --git a/ev3dev/fonts/helvO08.pbm b/ev3dev2/fonts/helvO08.pbm similarity index 100% rename from ev3dev/fonts/helvO08.pbm rename to ev3dev2/fonts/helvO08.pbm diff --git a/ev3dev/fonts/helvO08.pil b/ev3dev2/fonts/helvO08.pil similarity index 100% rename from ev3dev/fonts/helvO08.pil rename to ev3dev2/fonts/helvO08.pil diff --git a/ev3dev/fonts/helvO10.pbm b/ev3dev2/fonts/helvO10.pbm similarity index 100% rename from ev3dev/fonts/helvO10.pbm rename to ev3dev2/fonts/helvO10.pbm diff --git a/ev3dev/fonts/helvO10.pil b/ev3dev2/fonts/helvO10.pil similarity index 100% rename from ev3dev/fonts/helvO10.pil rename to ev3dev2/fonts/helvO10.pil diff --git a/ev3dev/fonts/helvO12.pbm b/ev3dev2/fonts/helvO12.pbm similarity index 100% rename from ev3dev/fonts/helvO12.pbm rename to ev3dev2/fonts/helvO12.pbm diff --git a/ev3dev/fonts/helvO12.pil b/ev3dev2/fonts/helvO12.pil similarity index 100% rename from ev3dev/fonts/helvO12.pil rename to ev3dev2/fonts/helvO12.pil diff --git a/ev3dev/fonts/helvO14.pbm b/ev3dev2/fonts/helvO14.pbm similarity index 100% rename from ev3dev/fonts/helvO14.pbm rename to ev3dev2/fonts/helvO14.pbm diff --git a/ev3dev/fonts/helvO14.pil b/ev3dev2/fonts/helvO14.pil similarity index 100% rename from ev3dev/fonts/helvO14.pil rename to ev3dev2/fonts/helvO14.pil diff --git a/ev3dev/fonts/helvO18.pbm b/ev3dev2/fonts/helvO18.pbm similarity index 100% rename from ev3dev/fonts/helvO18.pbm rename to ev3dev2/fonts/helvO18.pbm diff --git a/ev3dev/fonts/helvO18.pil b/ev3dev2/fonts/helvO18.pil similarity index 100% rename from ev3dev/fonts/helvO18.pil rename to ev3dev2/fonts/helvO18.pil diff --git a/ev3dev/fonts/helvO24.pbm b/ev3dev2/fonts/helvO24.pbm similarity index 100% rename from ev3dev/fonts/helvO24.pbm rename to ev3dev2/fonts/helvO24.pbm diff --git a/ev3dev/fonts/helvO24.pil b/ev3dev2/fonts/helvO24.pil similarity index 100% rename from ev3dev/fonts/helvO24.pil rename to ev3dev2/fonts/helvO24.pil diff --git a/ev3dev/fonts/helvR08.pbm b/ev3dev2/fonts/helvR08.pbm similarity index 100% rename from ev3dev/fonts/helvR08.pbm rename to ev3dev2/fonts/helvR08.pbm diff --git a/ev3dev/fonts/helvR08.pil b/ev3dev2/fonts/helvR08.pil similarity index 100% rename from ev3dev/fonts/helvR08.pil rename to ev3dev2/fonts/helvR08.pil diff --git a/ev3dev/fonts/helvR10.pbm b/ev3dev2/fonts/helvR10.pbm similarity index 100% rename from ev3dev/fonts/helvR10.pbm rename to ev3dev2/fonts/helvR10.pbm diff --git a/ev3dev/fonts/helvR10.pil b/ev3dev2/fonts/helvR10.pil similarity index 100% rename from ev3dev/fonts/helvR10.pil rename to ev3dev2/fonts/helvR10.pil diff --git a/ev3dev/fonts/helvR12.pbm b/ev3dev2/fonts/helvR12.pbm similarity index 100% rename from ev3dev/fonts/helvR12.pbm rename to ev3dev2/fonts/helvR12.pbm diff --git a/ev3dev/fonts/helvR12.pil b/ev3dev2/fonts/helvR12.pil similarity index 100% rename from ev3dev/fonts/helvR12.pil rename to ev3dev2/fonts/helvR12.pil diff --git a/ev3dev/fonts/helvR14.pbm b/ev3dev2/fonts/helvR14.pbm similarity index 100% rename from ev3dev/fonts/helvR14.pbm rename to ev3dev2/fonts/helvR14.pbm diff --git a/ev3dev/fonts/helvR14.pil b/ev3dev2/fonts/helvR14.pil similarity index 100% rename from ev3dev/fonts/helvR14.pil rename to ev3dev2/fonts/helvR14.pil diff --git a/ev3dev/fonts/helvR18.pbm b/ev3dev2/fonts/helvR18.pbm similarity index 100% rename from ev3dev/fonts/helvR18.pbm rename to ev3dev2/fonts/helvR18.pbm diff --git a/ev3dev/fonts/helvR18.pil b/ev3dev2/fonts/helvR18.pil similarity index 100% rename from ev3dev/fonts/helvR18.pil rename to ev3dev2/fonts/helvR18.pil diff --git a/ev3dev/fonts/helvR24.pbm b/ev3dev2/fonts/helvR24.pbm similarity index 100% rename from ev3dev/fonts/helvR24.pbm rename to ev3dev2/fonts/helvR24.pbm diff --git a/ev3dev/fonts/helvR24.pil b/ev3dev2/fonts/helvR24.pil similarity index 100% rename from ev3dev/fonts/helvR24.pil rename to ev3dev2/fonts/helvR24.pil diff --git a/ev3dev/fonts/luBIS08.pbm b/ev3dev2/fonts/luBIS08.pbm similarity index 100% rename from ev3dev/fonts/luBIS08.pbm rename to ev3dev2/fonts/luBIS08.pbm diff --git a/ev3dev/fonts/luBIS08.pil b/ev3dev2/fonts/luBIS08.pil similarity index 100% rename from ev3dev/fonts/luBIS08.pil rename to ev3dev2/fonts/luBIS08.pil diff --git a/ev3dev/fonts/luBIS10.pbm b/ev3dev2/fonts/luBIS10.pbm similarity index 100% rename from ev3dev/fonts/luBIS10.pbm rename to ev3dev2/fonts/luBIS10.pbm diff --git a/ev3dev/fonts/luBIS10.pil b/ev3dev2/fonts/luBIS10.pil similarity index 100% rename from ev3dev/fonts/luBIS10.pil rename to ev3dev2/fonts/luBIS10.pil diff --git a/ev3dev/fonts/luBIS12.pbm b/ev3dev2/fonts/luBIS12.pbm similarity index 100% rename from ev3dev/fonts/luBIS12.pbm rename to ev3dev2/fonts/luBIS12.pbm diff --git a/ev3dev/fonts/luBIS12.pil b/ev3dev2/fonts/luBIS12.pil similarity index 100% rename from ev3dev/fonts/luBIS12.pil rename to ev3dev2/fonts/luBIS12.pil diff --git a/ev3dev/fonts/luBIS14.pbm b/ev3dev2/fonts/luBIS14.pbm similarity index 100% rename from ev3dev/fonts/luBIS14.pbm rename to ev3dev2/fonts/luBIS14.pbm diff --git a/ev3dev/fonts/luBIS14.pil b/ev3dev2/fonts/luBIS14.pil similarity index 100% rename from ev3dev/fonts/luBIS14.pil rename to ev3dev2/fonts/luBIS14.pil diff --git a/ev3dev/fonts/luBIS18.pbm b/ev3dev2/fonts/luBIS18.pbm similarity index 100% rename from ev3dev/fonts/luBIS18.pbm rename to ev3dev2/fonts/luBIS18.pbm diff --git a/ev3dev/fonts/luBIS18.pil b/ev3dev2/fonts/luBIS18.pil similarity index 100% rename from ev3dev/fonts/luBIS18.pil rename to ev3dev2/fonts/luBIS18.pil diff --git a/ev3dev/fonts/luBIS19.pbm b/ev3dev2/fonts/luBIS19.pbm similarity index 100% rename from ev3dev/fonts/luBIS19.pbm rename to ev3dev2/fonts/luBIS19.pbm diff --git a/ev3dev/fonts/luBIS19.pil b/ev3dev2/fonts/luBIS19.pil similarity index 100% rename from ev3dev/fonts/luBIS19.pil rename to ev3dev2/fonts/luBIS19.pil diff --git a/ev3dev/fonts/luBIS24.pbm b/ev3dev2/fonts/luBIS24.pbm similarity index 100% rename from ev3dev/fonts/luBIS24.pbm rename to ev3dev2/fonts/luBIS24.pbm diff --git a/ev3dev/fonts/luBIS24.pil b/ev3dev2/fonts/luBIS24.pil similarity index 100% rename from ev3dev/fonts/luBIS24.pil rename to ev3dev2/fonts/luBIS24.pil diff --git a/ev3dev/fonts/luBS08.pbm b/ev3dev2/fonts/luBS08.pbm similarity index 100% rename from ev3dev/fonts/luBS08.pbm rename to ev3dev2/fonts/luBS08.pbm diff --git a/ev3dev/fonts/luBS08.pil b/ev3dev2/fonts/luBS08.pil similarity index 100% rename from ev3dev/fonts/luBS08.pil rename to ev3dev2/fonts/luBS08.pil diff --git a/ev3dev/fonts/luBS10.pbm b/ev3dev2/fonts/luBS10.pbm similarity index 100% rename from ev3dev/fonts/luBS10.pbm rename to ev3dev2/fonts/luBS10.pbm diff --git a/ev3dev/fonts/luBS10.pil b/ev3dev2/fonts/luBS10.pil similarity index 100% rename from ev3dev/fonts/luBS10.pil rename to ev3dev2/fonts/luBS10.pil diff --git a/ev3dev/fonts/luBS12.pbm b/ev3dev2/fonts/luBS12.pbm similarity index 100% rename from ev3dev/fonts/luBS12.pbm rename to ev3dev2/fonts/luBS12.pbm diff --git a/ev3dev/fonts/luBS12.pil b/ev3dev2/fonts/luBS12.pil similarity index 100% rename from ev3dev/fonts/luBS12.pil rename to ev3dev2/fonts/luBS12.pil diff --git a/ev3dev/fonts/luBS14.pbm b/ev3dev2/fonts/luBS14.pbm similarity index 100% rename from ev3dev/fonts/luBS14.pbm rename to ev3dev2/fonts/luBS14.pbm diff --git a/ev3dev/fonts/luBS14.pil b/ev3dev2/fonts/luBS14.pil similarity index 100% rename from ev3dev/fonts/luBS14.pil rename to ev3dev2/fonts/luBS14.pil diff --git a/ev3dev/fonts/luBS18.pbm b/ev3dev2/fonts/luBS18.pbm similarity index 100% rename from ev3dev/fonts/luBS18.pbm rename to ev3dev2/fonts/luBS18.pbm diff --git a/ev3dev/fonts/luBS18.pil b/ev3dev2/fonts/luBS18.pil similarity index 100% rename from ev3dev/fonts/luBS18.pil rename to ev3dev2/fonts/luBS18.pil diff --git a/ev3dev/fonts/luBS19.pbm b/ev3dev2/fonts/luBS19.pbm similarity index 100% rename from ev3dev/fonts/luBS19.pbm rename to ev3dev2/fonts/luBS19.pbm diff --git a/ev3dev/fonts/luBS19.pil b/ev3dev2/fonts/luBS19.pil similarity index 100% rename from ev3dev/fonts/luBS19.pil rename to ev3dev2/fonts/luBS19.pil diff --git a/ev3dev/fonts/luBS24.pbm b/ev3dev2/fonts/luBS24.pbm similarity index 100% rename from ev3dev/fonts/luBS24.pbm rename to ev3dev2/fonts/luBS24.pbm diff --git a/ev3dev/fonts/luBS24.pil b/ev3dev2/fonts/luBS24.pil similarity index 100% rename from ev3dev/fonts/luBS24.pil rename to ev3dev2/fonts/luBS24.pil diff --git a/ev3dev/fonts/luIS08.pbm b/ev3dev2/fonts/luIS08.pbm similarity index 100% rename from ev3dev/fonts/luIS08.pbm rename to ev3dev2/fonts/luIS08.pbm diff --git a/ev3dev/fonts/luIS08.pil b/ev3dev2/fonts/luIS08.pil similarity index 100% rename from ev3dev/fonts/luIS08.pil rename to ev3dev2/fonts/luIS08.pil diff --git a/ev3dev/fonts/luIS10.pbm b/ev3dev2/fonts/luIS10.pbm similarity index 100% rename from ev3dev/fonts/luIS10.pbm rename to ev3dev2/fonts/luIS10.pbm diff --git a/ev3dev/fonts/luIS10.pil b/ev3dev2/fonts/luIS10.pil similarity index 100% rename from ev3dev/fonts/luIS10.pil rename to ev3dev2/fonts/luIS10.pil diff --git a/ev3dev/fonts/luIS12.pbm b/ev3dev2/fonts/luIS12.pbm similarity index 100% rename from ev3dev/fonts/luIS12.pbm rename to ev3dev2/fonts/luIS12.pbm diff --git a/ev3dev/fonts/luIS12.pil b/ev3dev2/fonts/luIS12.pil similarity index 100% rename from ev3dev/fonts/luIS12.pil rename to ev3dev2/fonts/luIS12.pil diff --git a/ev3dev/fonts/luIS14.pbm b/ev3dev2/fonts/luIS14.pbm similarity index 100% rename from ev3dev/fonts/luIS14.pbm rename to ev3dev2/fonts/luIS14.pbm diff --git a/ev3dev/fonts/luIS14.pil b/ev3dev2/fonts/luIS14.pil similarity index 100% rename from ev3dev/fonts/luIS14.pil rename to ev3dev2/fonts/luIS14.pil diff --git a/ev3dev/fonts/luIS18.pbm b/ev3dev2/fonts/luIS18.pbm similarity index 100% rename from ev3dev/fonts/luIS18.pbm rename to ev3dev2/fonts/luIS18.pbm diff --git a/ev3dev/fonts/luIS18.pil b/ev3dev2/fonts/luIS18.pil similarity index 100% rename from ev3dev/fonts/luIS18.pil rename to ev3dev2/fonts/luIS18.pil diff --git a/ev3dev/fonts/luIS19.pbm b/ev3dev2/fonts/luIS19.pbm similarity index 100% rename from ev3dev/fonts/luIS19.pbm rename to ev3dev2/fonts/luIS19.pbm diff --git a/ev3dev/fonts/luIS19.pil b/ev3dev2/fonts/luIS19.pil similarity index 100% rename from ev3dev/fonts/luIS19.pil rename to ev3dev2/fonts/luIS19.pil diff --git a/ev3dev/fonts/luIS24.pbm b/ev3dev2/fonts/luIS24.pbm similarity index 100% rename from ev3dev/fonts/luIS24.pbm rename to ev3dev2/fonts/luIS24.pbm diff --git a/ev3dev/fonts/luIS24.pil b/ev3dev2/fonts/luIS24.pil similarity index 100% rename from ev3dev/fonts/luIS24.pil rename to ev3dev2/fonts/luIS24.pil diff --git a/ev3dev/fonts/luRS08.pbm b/ev3dev2/fonts/luRS08.pbm similarity index 100% rename from ev3dev/fonts/luRS08.pbm rename to ev3dev2/fonts/luRS08.pbm diff --git a/ev3dev/fonts/luRS08.pil b/ev3dev2/fonts/luRS08.pil similarity index 100% rename from ev3dev/fonts/luRS08.pil rename to ev3dev2/fonts/luRS08.pil diff --git a/ev3dev/fonts/luRS10.pbm b/ev3dev2/fonts/luRS10.pbm similarity index 100% rename from ev3dev/fonts/luRS10.pbm rename to ev3dev2/fonts/luRS10.pbm diff --git a/ev3dev/fonts/luRS10.pil b/ev3dev2/fonts/luRS10.pil similarity index 100% rename from ev3dev/fonts/luRS10.pil rename to ev3dev2/fonts/luRS10.pil diff --git a/ev3dev/fonts/luRS12.pbm b/ev3dev2/fonts/luRS12.pbm similarity index 100% rename from ev3dev/fonts/luRS12.pbm rename to ev3dev2/fonts/luRS12.pbm diff --git a/ev3dev/fonts/luRS12.pil b/ev3dev2/fonts/luRS12.pil similarity index 100% rename from ev3dev/fonts/luRS12.pil rename to ev3dev2/fonts/luRS12.pil diff --git a/ev3dev/fonts/luRS14.pbm b/ev3dev2/fonts/luRS14.pbm similarity index 100% rename from ev3dev/fonts/luRS14.pbm rename to ev3dev2/fonts/luRS14.pbm diff --git a/ev3dev/fonts/luRS14.pil b/ev3dev2/fonts/luRS14.pil similarity index 100% rename from ev3dev/fonts/luRS14.pil rename to ev3dev2/fonts/luRS14.pil diff --git a/ev3dev/fonts/luRS18.pbm b/ev3dev2/fonts/luRS18.pbm similarity index 100% rename from ev3dev/fonts/luRS18.pbm rename to ev3dev2/fonts/luRS18.pbm diff --git a/ev3dev/fonts/luRS18.pil b/ev3dev2/fonts/luRS18.pil similarity index 100% rename from ev3dev/fonts/luRS18.pil rename to ev3dev2/fonts/luRS18.pil diff --git a/ev3dev/fonts/luRS19.pbm b/ev3dev2/fonts/luRS19.pbm similarity index 100% rename from ev3dev/fonts/luRS19.pbm rename to ev3dev2/fonts/luRS19.pbm diff --git a/ev3dev/fonts/luRS19.pil b/ev3dev2/fonts/luRS19.pil similarity index 100% rename from ev3dev/fonts/luRS19.pil rename to ev3dev2/fonts/luRS19.pil diff --git a/ev3dev/fonts/luRS24.pbm b/ev3dev2/fonts/luRS24.pbm similarity index 100% rename from ev3dev/fonts/luRS24.pbm rename to ev3dev2/fonts/luRS24.pbm diff --git a/ev3dev/fonts/luRS24.pil b/ev3dev2/fonts/luRS24.pil similarity index 100% rename from ev3dev/fonts/luRS24.pil rename to ev3dev2/fonts/luRS24.pil diff --git a/ev3dev/fonts/lubB08.pbm b/ev3dev2/fonts/lubB08.pbm similarity index 100% rename from ev3dev/fonts/lubB08.pbm rename to ev3dev2/fonts/lubB08.pbm diff --git a/ev3dev/fonts/lubB08.pil b/ev3dev2/fonts/lubB08.pil similarity index 100% rename from ev3dev/fonts/lubB08.pil rename to ev3dev2/fonts/lubB08.pil diff --git a/ev3dev/fonts/lubB10.pbm b/ev3dev2/fonts/lubB10.pbm similarity index 100% rename from ev3dev/fonts/lubB10.pbm rename to ev3dev2/fonts/lubB10.pbm diff --git a/ev3dev/fonts/lubB10.pil b/ev3dev2/fonts/lubB10.pil similarity index 100% rename from ev3dev/fonts/lubB10.pil rename to ev3dev2/fonts/lubB10.pil diff --git a/ev3dev/fonts/lubB12.pbm b/ev3dev2/fonts/lubB12.pbm similarity index 100% rename from ev3dev/fonts/lubB12.pbm rename to ev3dev2/fonts/lubB12.pbm diff --git a/ev3dev/fonts/lubB12.pil b/ev3dev2/fonts/lubB12.pil similarity index 100% rename from ev3dev/fonts/lubB12.pil rename to ev3dev2/fonts/lubB12.pil diff --git a/ev3dev/fonts/lubB14.pbm b/ev3dev2/fonts/lubB14.pbm similarity index 100% rename from ev3dev/fonts/lubB14.pbm rename to ev3dev2/fonts/lubB14.pbm diff --git a/ev3dev/fonts/lubB14.pil b/ev3dev2/fonts/lubB14.pil similarity index 100% rename from ev3dev/fonts/lubB14.pil rename to ev3dev2/fonts/lubB14.pil diff --git a/ev3dev/fonts/lubB18.pbm b/ev3dev2/fonts/lubB18.pbm similarity index 100% rename from ev3dev/fonts/lubB18.pbm rename to ev3dev2/fonts/lubB18.pbm diff --git a/ev3dev/fonts/lubB18.pil b/ev3dev2/fonts/lubB18.pil similarity index 100% rename from ev3dev/fonts/lubB18.pil rename to ev3dev2/fonts/lubB18.pil diff --git a/ev3dev/fonts/lubB19.pbm b/ev3dev2/fonts/lubB19.pbm similarity index 100% rename from ev3dev/fonts/lubB19.pbm rename to ev3dev2/fonts/lubB19.pbm diff --git a/ev3dev/fonts/lubB19.pil b/ev3dev2/fonts/lubB19.pil similarity index 100% rename from ev3dev/fonts/lubB19.pil rename to ev3dev2/fonts/lubB19.pil diff --git a/ev3dev/fonts/lubB24.pbm b/ev3dev2/fonts/lubB24.pbm similarity index 100% rename from ev3dev/fonts/lubB24.pbm rename to ev3dev2/fonts/lubB24.pbm diff --git a/ev3dev/fonts/lubB24.pil b/ev3dev2/fonts/lubB24.pil similarity index 100% rename from ev3dev/fonts/lubB24.pil rename to ev3dev2/fonts/lubB24.pil diff --git a/ev3dev/fonts/lubBI08.pbm b/ev3dev2/fonts/lubBI08.pbm similarity index 100% rename from ev3dev/fonts/lubBI08.pbm rename to ev3dev2/fonts/lubBI08.pbm diff --git a/ev3dev/fonts/lubBI08.pil b/ev3dev2/fonts/lubBI08.pil similarity index 100% rename from ev3dev/fonts/lubBI08.pil rename to ev3dev2/fonts/lubBI08.pil diff --git a/ev3dev/fonts/lubBI10.pbm b/ev3dev2/fonts/lubBI10.pbm similarity index 100% rename from ev3dev/fonts/lubBI10.pbm rename to ev3dev2/fonts/lubBI10.pbm diff --git a/ev3dev/fonts/lubBI10.pil b/ev3dev2/fonts/lubBI10.pil similarity index 100% rename from ev3dev/fonts/lubBI10.pil rename to ev3dev2/fonts/lubBI10.pil diff --git a/ev3dev/fonts/lubBI12.pbm b/ev3dev2/fonts/lubBI12.pbm similarity index 100% rename from ev3dev/fonts/lubBI12.pbm rename to ev3dev2/fonts/lubBI12.pbm diff --git a/ev3dev/fonts/lubBI12.pil b/ev3dev2/fonts/lubBI12.pil similarity index 100% rename from ev3dev/fonts/lubBI12.pil rename to ev3dev2/fonts/lubBI12.pil diff --git a/ev3dev/fonts/lubBI14.pbm b/ev3dev2/fonts/lubBI14.pbm similarity index 100% rename from ev3dev/fonts/lubBI14.pbm rename to ev3dev2/fonts/lubBI14.pbm diff --git a/ev3dev/fonts/lubBI14.pil b/ev3dev2/fonts/lubBI14.pil similarity index 100% rename from ev3dev/fonts/lubBI14.pil rename to ev3dev2/fonts/lubBI14.pil diff --git a/ev3dev/fonts/lubBI18.pbm b/ev3dev2/fonts/lubBI18.pbm similarity index 100% rename from ev3dev/fonts/lubBI18.pbm rename to ev3dev2/fonts/lubBI18.pbm diff --git a/ev3dev/fonts/lubBI18.pil b/ev3dev2/fonts/lubBI18.pil similarity index 100% rename from ev3dev/fonts/lubBI18.pil rename to ev3dev2/fonts/lubBI18.pil diff --git a/ev3dev/fonts/lubBI19.pbm b/ev3dev2/fonts/lubBI19.pbm similarity index 100% rename from ev3dev/fonts/lubBI19.pbm rename to ev3dev2/fonts/lubBI19.pbm diff --git a/ev3dev/fonts/lubBI19.pil b/ev3dev2/fonts/lubBI19.pil similarity index 100% rename from ev3dev/fonts/lubBI19.pil rename to ev3dev2/fonts/lubBI19.pil diff --git a/ev3dev/fonts/lubBI24.pbm b/ev3dev2/fonts/lubBI24.pbm similarity index 100% rename from ev3dev/fonts/lubBI24.pbm rename to ev3dev2/fonts/lubBI24.pbm diff --git a/ev3dev/fonts/lubBI24.pil b/ev3dev2/fonts/lubBI24.pil similarity index 100% rename from ev3dev/fonts/lubBI24.pil rename to ev3dev2/fonts/lubBI24.pil diff --git a/ev3dev/fonts/lubI08.pbm b/ev3dev2/fonts/lubI08.pbm similarity index 100% rename from ev3dev/fonts/lubI08.pbm rename to ev3dev2/fonts/lubI08.pbm diff --git a/ev3dev/fonts/lubI08.pil b/ev3dev2/fonts/lubI08.pil similarity index 100% rename from ev3dev/fonts/lubI08.pil rename to ev3dev2/fonts/lubI08.pil diff --git a/ev3dev/fonts/lubI10.pbm b/ev3dev2/fonts/lubI10.pbm similarity index 100% rename from ev3dev/fonts/lubI10.pbm rename to ev3dev2/fonts/lubI10.pbm diff --git a/ev3dev/fonts/lubI10.pil b/ev3dev2/fonts/lubI10.pil similarity index 100% rename from ev3dev/fonts/lubI10.pil rename to ev3dev2/fonts/lubI10.pil diff --git a/ev3dev/fonts/lubI12.pbm b/ev3dev2/fonts/lubI12.pbm similarity index 100% rename from ev3dev/fonts/lubI12.pbm rename to ev3dev2/fonts/lubI12.pbm diff --git a/ev3dev/fonts/lubI12.pil b/ev3dev2/fonts/lubI12.pil similarity index 100% rename from ev3dev/fonts/lubI12.pil rename to ev3dev2/fonts/lubI12.pil diff --git a/ev3dev/fonts/lubI14.pbm b/ev3dev2/fonts/lubI14.pbm similarity index 100% rename from ev3dev/fonts/lubI14.pbm rename to ev3dev2/fonts/lubI14.pbm diff --git a/ev3dev/fonts/lubI14.pil b/ev3dev2/fonts/lubI14.pil similarity index 100% rename from ev3dev/fonts/lubI14.pil rename to ev3dev2/fonts/lubI14.pil diff --git a/ev3dev/fonts/lubI18.pbm b/ev3dev2/fonts/lubI18.pbm similarity index 100% rename from ev3dev/fonts/lubI18.pbm rename to ev3dev2/fonts/lubI18.pbm diff --git a/ev3dev/fonts/lubI18.pil b/ev3dev2/fonts/lubI18.pil similarity index 100% rename from ev3dev/fonts/lubI18.pil rename to ev3dev2/fonts/lubI18.pil diff --git a/ev3dev/fonts/lubI19.pbm b/ev3dev2/fonts/lubI19.pbm similarity index 100% rename from ev3dev/fonts/lubI19.pbm rename to ev3dev2/fonts/lubI19.pbm diff --git a/ev3dev/fonts/lubI19.pil b/ev3dev2/fonts/lubI19.pil similarity index 100% rename from ev3dev/fonts/lubI19.pil rename to ev3dev2/fonts/lubI19.pil diff --git a/ev3dev/fonts/lubI24.pbm b/ev3dev2/fonts/lubI24.pbm similarity index 100% rename from ev3dev/fonts/lubI24.pbm rename to ev3dev2/fonts/lubI24.pbm diff --git a/ev3dev/fonts/lubI24.pil b/ev3dev2/fonts/lubI24.pil similarity index 100% rename from ev3dev/fonts/lubI24.pil rename to ev3dev2/fonts/lubI24.pil diff --git a/ev3dev/fonts/lubR08.pbm b/ev3dev2/fonts/lubR08.pbm similarity index 100% rename from ev3dev/fonts/lubR08.pbm rename to ev3dev2/fonts/lubR08.pbm diff --git a/ev3dev/fonts/lubR08.pil b/ev3dev2/fonts/lubR08.pil similarity index 100% rename from ev3dev/fonts/lubR08.pil rename to ev3dev2/fonts/lubR08.pil diff --git a/ev3dev/fonts/lubR10.pbm b/ev3dev2/fonts/lubR10.pbm similarity index 100% rename from ev3dev/fonts/lubR10.pbm rename to ev3dev2/fonts/lubR10.pbm diff --git a/ev3dev/fonts/lubR10.pil b/ev3dev2/fonts/lubR10.pil similarity index 100% rename from ev3dev/fonts/lubR10.pil rename to ev3dev2/fonts/lubR10.pil diff --git a/ev3dev/fonts/lubR12.pbm b/ev3dev2/fonts/lubR12.pbm similarity index 100% rename from ev3dev/fonts/lubR12.pbm rename to ev3dev2/fonts/lubR12.pbm diff --git a/ev3dev/fonts/lubR12.pil b/ev3dev2/fonts/lubR12.pil similarity index 100% rename from ev3dev/fonts/lubR12.pil rename to ev3dev2/fonts/lubR12.pil diff --git a/ev3dev/fonts/lubR14.pbm b/ev3dev2/fonts/lubR14.pbm similarity index 100% rename from ev3dev/fonts/lubR14.pbm rename to ev3dev2/fonts/lubR14.pbm diff --git a/ev3dev/fonts/lubR14.pil b/ev3dev2/fonts/lubR14.pil similarity index 100% rename from ev3dev/fonts/lubR14.pil rename to ev3dev2/fonts/lubR14.pil diff --git a/ev3dev/fonts/lubR18.pbm b/ev3dev2/fonts/lubR18.pbm similarity index 100% rename from ev3dev/fonts/lubR18.pbm rename to ev3dev2/fonts/lubR18.pbm diff --git a/ev3dev/fonts/lubR18.pil b/ev3dev2/fonts/lubR18.pil similarity index 100% rename from ev3dev/fonts/lubR18.pil rename to ev3dev2/fonts/lubR18.pil diff --git a/ev3dev/fonts/lubR19.pbm b/ev3dev2/fonts/lubR19.pbm similarity index 100% rename from ev3dev/fonts/lubR19.pbm rename to ev3dev2/fonts/lubR19.pbm diff --git a/ev3dev/fonts/lubR19.pil b/ev3dev2/fonts/lubR19.pil similarity index 100% rename from ev3dev/fonts/lubR19.pil rename to ev3dev2/fonts/lubR19.pil diff --git a/ev3dev/fonts/lubR24.pbm b/ev3dev2/fonts/lubR24.pbm similarity index 100% rename from ev3dev/fonts/lubR24.pbm rename to ev3dev2/fonts/lubR24.pbm diff --git a/ev3dev/fonts/lubR24.pil b/ev3dev2/fonts/lubR24.pil similarity index 100% rename from ev3dev/fonts/lubR24.pil rename to ev3dev2/fonts/lubR24.pil diff --git a/ev3dev/fonts/lutBS08.pbm b/ev3dev2/fonts/lutBS08.pbm similarity index 100% rename from ev3dev/fonts/lutBS08.pbm rename to ev3dev2/fonts/lutBS08.pbm diff --git a/ev3dev/fonts/lutBS08.pil b/ev3dev2/fonts/lutBS08.pil similarity index 100% rename from ev3dev/fonts/lutBS08.pil rename to ev3dev2/fonts/lutBS08.pil diff --git a/ev3dev/fonts/lutBS10.pbm b/ev3dev2/fonts/lutBS10.pbm similarity index 100% rename from ev3dev/fonts/lutBS10.pbm rename to ev3dev2/fonts/lutBS10.pbm diff --git a/ev3dev/fonts/lutBS10.pil b/ev3dev2/fonts/lutBS10.pil similarity index 100% rename from ev3dev/fonts/lutBS10.pil rename to ev3dev2/fonts/lutBS10.pil diff --git a/ev3dev/fonts/lutBS12.pbm b/ev3dev2/fonts/lutBS12.pbm similarity index 100% rename from ev3dev/fonts/lutBS12.pbm rename to ev3dev2/fonts/lutBS12.pbm diff --git a/ev3dev/fonts/lutBS12.pil b/ev3dev2/fonts/lutBS12.pil similarity index 100% rename from ev3dev/fonts/lutBS12.pil rename to ev3dev2/fonts/lutBS12.pil diff --git a/ev3dev/fonts/lutBS14.pbm b/ev3dev2/fonts/lutBS14.pbm similarity index 100% rename from ev3dev/fonts/lutBS14.pbm rename to ev3dev2/fonts/lutBS14.pbm diff --git a/ev3dev/fonts/lutBS14.pil b/ev3dev2/fonts/lutBS14.pil similarity index 100% rename from ev3dev/fonts/lutBS14.pil rename to ev3dev2/fonts/lutBS14.pil diff --git a/ev3dev/fonts/lutBS18.pbm b/ev3dev2/fonts/lutBS18.pbm similarity index 100% rename from ev3dev/fonts/lutBS18.pbm rename to ev3dev2/fonts/lutBS18.pbm diff --git a/ev3dev/fonts/lutBS18.pil b/ev3dev2/fonts/lutBS18.pil similarity index 100% rename from ev3dev/fonts/lutBS18.pil rename to ev3dev2/fonts/lutBS18.pil diff --git a/ev3dev/fonts/lutBS19.pbm b/ev3dev2/fonts/lutBS19.pbm similarity index 100% rename from ev3dev/fonts/lutBS19.pbm rename to ev3dev2/fonts/lutBS19.pbm diff --git a/ev3dev/fonts/lutBS19.pil b/ev3dev2/fonts/lutBS19.pil similarity index 100% rename from ev3dev/fonts/lutBS19.pil rename to ev3dev2/fonts/lutBS19.pil diff --git a/ev3dev/fonts/lutBS24.pbm b/ev3dev2/fonts/lutBS24.pbm similarity index 100% rename from ev3dev/fonts/lutBS24.pbm rename to ev3dev2/fonts/lutBS24.pbm diff --git a/ev3dev/fonts/lutBS24.pil b/ev3dev2/fonts/lutBS24.pil similarity index 100% rename from ev3dev/fonts/lutBS24.pil rename to ev3dev2/fonts/lutBS24.pil diff --git a/ev3dev/fonts/lutRS08.pbm b/ev3dev2/fonts/lutRS08.pbm similarity index 100% rename from ev3dev/fonts/lutRS08.pbm rename to ev3dev2/fonts/lutRS08.pbm diff --git a/ev3dev/fonts/lutRS08.pil b/ev3dev2/fonts/lutRS08.pil similarity index 100% rename from ev3dev/fonts/lutRS08.pil rename to ev3dev2/fonts/lutRS08.pil diff --git a/ev3dev/fonts/lutRS10.pbm b/ev3dev2/fonts/lutRS10.pbm similarity index 100% rename from ev3dev/fonts/lutRS10.pbm rename to ev3dev2/fonts/lutRS10.pbm diff --git a/ev3dev/fonts/lutRS10.pil b/ev3dev2/fonts/lutRS10.pil similarity index 100% rename from ev3dev/fonts/lutRS10.pil rename to ev3dev2/fonts/lutRS10.pil diff --git a/ev3dev/fonts/lutRS12.pbm b/ev3dev2/fonts/lutRS12.pbm similarity index 100% rename from ev3dev/fonts/lutRS12.pbm rename to ev3dev2/fonts/lutRS12.pbm diff --git a/ev3dev/fonts/lutRS12.pil b/ev3dev2/fonts/lutRS12.pil similarity index 100% rename from ev3dev/fonts/lutRS12.pil rename to ev3dev2/fonts/lutRS12.pil diff --git a/ev3dev/fonts/lutRS14.pbm b/ev3dev2/fonts/lutRS14.pbm similarity index 100% rename from ev3dev/fonts/lutRS14.pbm rename to ev3dev2/fonts/lutRS14.pbm diff --git a/ev3dev/fonts/lutRS14.pil b/ev3dev2/fonts/lutRS14.pil similarity index 100% rename from ev3dev/fonts/lutRS14.pil rename to ev3dev2/fonts/lutRS14.pil diff --git a/ev3dev/fonts/lutRS18.pbm b/ev3dev2/fonts/lutRS18.pbm similarity index 100% rename from ev3dev/fonts/lutRS18.pbm rename to ev3dev2/fonts/lutRS18.pbm diff --git a/ev3dev/fonts/lutRS18.pil b/ev3dev2/fonts/lutRS18.pil similarity index 100% rename from ev3dev/fonts/lutRS18.pil rename to ev3dev2/fonts/lutRS18.pil diff --git a/ev3dev/fonts/lutRS19.pbm b/ev3dev2/fonts/lutRS19.pbm similarity index 100% rename from ev3dev/fonts/lutRS19.pbm rename to ev3dev2/fonts/lutRS19.pbm diff --git a/ev3dev/fonts/lutRS19.pil b/ev3dev2/fonts/lutRS19.pil similarity index 100% rename from ev3dev/fonts/lutRS19.pil rename to ev3dev2/fonts/lutRS19.pil diff --git a/ev3dev/fonts/lutRS24.pbm b/ev3dev2/fonts/lutRS24.pbm similarity index 100% rename from ev3dev/fonts/lutRS24.pbm rename to ev3dev2/fonts/lutRS24.pbm diff --git a/ev3dev/fonts/lutRS24.pil b/ev3dev2/fonts/lutRS24.pil similarity index 100% rename from ev3dev/fonts/lutRS24.pil rename to ev3dev2/fonts/lutRS24.pil diff --git a/ev3dev/fonts/ncenB08.pbm b/ev3dev2/fonts/ncenB08.pbm similarity index 100% rename from ev3dev/fonts/ncenB08.pbm rename to ev3dev2/fonts/ncenB08.pbm diff --git a/ev3dev/fonts/ncenB08.pil b/ev3dev2/fonts/ncenB08.pil similarity index 100% rename from ev3dev/fonts/ncenB08.pil rename to ev3dev2/fonts/ncenB08.pil diff --git a/ev3dev/fonts/ncenB10.pbm b/ev3dev2/fonts/ncenB10.pbm similarity index 100% rename from ev3dev/fonts/ncenB10.pbm rename to ev3dev2/fonts/ncenB10.pbm diff --git a/ev3dev/fonts/ncenB10.pil b/ev3dev2/fonts/ncenB10.pil similarity index 100% rename from ev3dev/fonts/ncenB10.pil rename to ev3dev2/fonts/ncenB10.pil diff --git a/ev3dev/fonts/ncenB12.pbm b/ev3dev2/fonts/ncenB12.pbm similarity index 100% rename from ev3dev/fonts/ncenB12.pbm rename to ev3dev2/fonts/ncenB12.pbm diff --git a/ev3dev/fonts/ncenB12.pil b/ev3dev2/fonts/ncenB12.pil similarity index 100% rename from ev3dev/fonts/ncenB12.pil rename to ev3dev2/fonts/ncenB12.pil diff --git a/ev3dev/fonts/ncenB14.pbm b/ev3dev2/fonts/ncenB14.pbm similarity index 100% rename from ev3dev/fonts/ncenB14.pbm rename to ev3dev2/fonts/ncenB14.pbm diff --git a/ev3dev/fonts/ncenB14.pil b/ev3dev2/fonts/ncenB14.pil similarity index 100% rename from ev3dev/fonts/ncenB14.pil rename to ev3dev2/fonts/ncenB14.pil diff --git a/ev3dev/fonts/ncenB18.pbm b/ev3dev2/fonts/ncenB18.pbm similarity index 100% rename from ev3dev/fonts/ncenB18.pbm rename to ev3dev2/fonts/ncenB18.pbm diff --git a/ev3dev/fonts/ncenB18.pil b/ev3dev2/fonts/ncenB18.pil similarity index 100% rename from ev3dev/fonts/ncenB18.pil rename to ev3dev2/fonts/ncenB18.pil diff --git a/ev3dev/fonts/ncenB24.pbm b/ev3dev2/fonts/ncenB24.pbm similarity index 100% rename from ev3dev/fonts/ncenB24.pbm rename to ev3dev2/fonts/ncenB24.pbm diff --git a/ev3dev/fonts/ncenB24.pil b/ev3dev2/fonts/ncenB24.pil similarity index 100% rename from ev3dev/fonts/ncenB24.pil rename to ev3dev2/fonts/ncenB24.pil diff --git a/ev3dev/fonts/ncenBI08.pbm b/ev3dev2/fonts/ncenBI08.pbm similarity index 100% rename from ev3dev/fonts/ncenBI08.pbm rename to ev3dev2/fonts/ncenBI08.pbm diff --git a/ev3dev/fonts/ncenBI08.pil b/ev3dev2/fonts/ncenBI08.pil similarity index 100% rename from ev3dev/fonts/ncenBI08.pil rename to ev3dev2/fonts/ncenBI08.pil diff --git a/ev3dev/fonts/ncenBI10.pbm b/ev3dev2/fonts/ncenBI10.pbm similarity index 100% rename from ev3dev/fonts/ncenBI10.pbm rename to ev3dev2/fonts/ncenBI10.pbm diff --git a/ev3dev/fonts/ncenBI10.pil b/ev3dev2/fonts/ncenBI10.pil similarity index 100% rename from ev3dev/fonts/ncenBI10.pil rename to ev3dev2/fonts/ncenBI10.pil diff --git a/ev3dev/fonts/ncenBI12.pbm b/ev3dev2/fonts/ncenBI12.pbm similarity index 100% rename from ev3dev/fonts/ncenBI12.pbm rename to ev3dev2/fonts/ncenBI12.pbm diff --git a/ev3dev/fonts/ncenBI12.pil b/ev3dev2/fonts/ncenBI12.pil similarity index 100% rename from ev3dev/fonts/ncenBI12.pil rename to ev3dev2/fonts/ncenBI12.pil diff --git a/ev3dev/fonts/ncenBI14.pbm b/ev3dev2/fonts/ncenBI14.pbm similarity index 100% rename from ev3dev/fonts/ncenBI14.pbm rename to ev3dev2/fonts/ncenBI14.pbm diff --git a/ev3dev/fonts/ncenBI14.pil b/ev3dev2/fonts/ncenBI14.pil similarity index 100% rename from ev3dev/fonts/ncenBI14.pil rename to ev3dev2/fonts/ncenBI14.pil diff --git a/ev3dev/fonts/ncenBI18.pbm b/ev3dev2/fonts/ncenBI18.pbm similarity index 100% rename from ev3dev/fonts/ncenBI18.pbm rename to ev3dev2/fonts/ncenBI18.pbm diff --git a/ev3dev/fonts/ncenBI18.pil b/ev3dev2/fonts/ncenBI18.pil similarity index 100% rename from ev3dev/fonts/ncenBI18.pil rename to ev3dev2/fonts/ncenBI18.pil diff --git a/ev3dev/fonts/ncenBI24.pbm b/ev3dev2/fonts/ncenBI24.pbm similarity index 100% rename from ev3dev/fonts/ncenBI24.pbm rename to ev3dev2/fonts/ncenBI24.pbm diff --git a/ev3dev/fonts/ncenBI24.pil b/ev3dev2/fonts/ncenBI24.pil similarity index 100% rename from ev3dev/fonts/ncenBI24.pil rename to ev3dev2/fonts/ncenBI24.pil diff --git a/ev3dev/fonts/ncenI08.pbm b/ev3dev2/fonts/ncenI08.pbm similarity index 100% rename from ev3dev/fonts/ncenI08.pbm rename to ev3dev2/fonts/ncenI08.pbm diff --git a/ev3dev/fonts/ncenI08.pil b/ev3dev2/fonts/ncenI08.pil similarity index 100% rename from ev3dev/fonts/ncenI08.pil rename to ev3dev2/fonts/ncenI08.pil diff --git a/ev3dev/fonts/ncenI10.pbm b/ev3dev2/fonts/ncenI10.pbm similarity index 100% rename from ev3dev/fonts/ncenI10.pbm rename to ev3dev2/fonts/ncenI10.pbm diff --git a/ev3dev/fonts/ncenI10.pil b/ev3dev2/fonts/ncenI10.pil similarity index 100% rename from ev3dev/fonts/ncenI10.pil rename to ev3dev2/fonts/ncenI10.pil diff --git a/ev3dev/fonts/ncenI12.pbm b/ev3dev2/fonts/ncenI12.pbm similarity index 100% rename from ev3dev/fonts/ncenI12.pbm rename to ev3dev2/fonts/ncenI12.pbm diff --git a/ev3dev/fonts/ncenI12.pil b/ev3dev2/fonts/ncenI12.pil similarity index 100% rename from ev3dev/fonts/ncenI12.pil rename to ev3dev2/fonts/ncenI12.pil diff --git a/ev3dev/fonts/ncenI14.pbm b/ev3dev2/fonts/ncenI14.pbm similarity index 100% rename from ev3dev/fonts/ncenI14.pbm rename to ev3dev2/fonts/ncenI14.pbm diff --git a/ev3dev/fonts/ncenI14.pil b/ev3dev2/fonts/ncenI14.pil similarity index 100% rename from ev3dev/fonts/ncenI14.pil rename to ev3dev2/fonts/ncenI14.pil diff --git a/ev3dev/fonts/ncenI18.pbm b/ev3dev2/fonts/ncenI18.pbm similarity index 100% rename from ev3dev/fonts/ncenI18.pbm rename to ev3dev2/fonts/ncenI18.pbm diff --git a/ev3dev/fonts/ncenI18.pil b/ev3dev2/fonts/ncenI18.pil similarity index 100% rename from ev3dev/fonts/ncenI18.pil rename to ev3dev2/fonts/ncenI18.pil diff --git a/ev3dev/fonts/ncenI24.pbm b/ev3dev2/fonts/ncenI24.pbm similarity index 100% rename from ev3dev/fonts/ncenI24.pbm rename to ev3dev2/fonts/ncenI24.pbm diff --git a/ev3dev/fonts/ncenI24.pil b/ev3dev2/fonts/ncenI24.pil similarity index 100% rename from ev3dev/fonts/ncenI24.pil rename to ev3dev2/fonts/ncenI24.pil diff --git a/ev3dev/fonts/ncenR08.pbm b/ev3dev2/fonts/ncenR08.pbm similarity index 100% rename from ev3dev/fonts/ncenR08.pbm rename to ev3dev2/fonts/ncenR08.pbm diff --git a/ev3dev/fonts/ncenR08.pil b/ev3dev2/fonts/ncenR08.pil similarity index 100% rename from ev3dev/fonts/ncenR08.pil rename to ev3dev2/fonts/ncenR08.pil diff --git a/ev3dev/fonts/ncenR10.pbm b/ev3dev2/fonts/ncenR10.pbm similarity index 100% rename from ev3dev/fonts/ncenR10.pbm rename to ev3dev2/fonts/ncenR10.pbm diff --git a/ev3dev/fonts/ncenR10.pil b/ev3dev2/fonts/ncenR10.pil similarity index 100% rename from ev3dev/fonts/ncenR10.pil rename to ev3dev2/fonts/ncenR10.pil diff --git a/ev3dev/fonts/ncenR12.pbm b/ev3dev2/fonts/ncenR12.pbm similarity index 100% rename from ev3dev/fonts/ncenR12.pbm rename to ev3dev2/fonts/ncenR12.pbm diff --git a/ev3dev/fonts/ncenR12.pil b/ev3dev2/fonts/ncenR12.pil similarity index 100% rename from ev3dev/fonts/ncenR12.pil rename to ev3dev2/fonts/ncenR12.pil diff --git a/ev3dev/fonts/ncenR14.pbm b/ev3dev2/fonts/ncenR14.pbm similarity index 100% rename from ev3dev/fonts/ncenR14.pbm rename to ev3dev2/fonts/ncenR14.pbm diff --git a/ev3dev/fonts/ncenR14.pil b/ev3dev2/fonts/ncenR14.pil similarity index 100% rename from ev3dev/fonts/ncenR14.pil rename to ev3dev2/fonts/ncenR14.pil diff --git a/ev3dev/fonts/ncenR18.pbm b/ev3dev2/fonts/ncenR18.pbm similarity index 100% rename from ev3dev/fonts/ncenR18.pbm rename to ev3dev2/fonts/ncenR18.pbm diff --git a/ev3dev/fonts/ncenR18.pil b/ev3dev2/fonts/ncenR18.pil similarity index 100% rename from ev3dev/fonts/ncenR18.pil rename to ev3dev2/fonts/ncenR18.pil diff --git a/ev3dev/fonts/ncenR24.pbm b/ev3dev2/fonts/ncenR24.pbm similarity index 100% rename from ev3dev/fonts/ncenR24.pbm rename to ev3dev2/fonts/ncenR24.pbm diff --git a/ev3dev/fonts/ncenR24.pil b/ev3dev2/fonts/ncenR24.pil similarity index 100% rename from ev3dev/fonts/ncenR24.pil rename to ev3dev2/fonts/ncenR24.pil diff --git a/ev3dev/fonts/symb08.pbm b/ev3dev2/fonts/symb08.pbm similarity index 100% rename from ev3dev/fonts/symb08.pbm rename to ev3dev2/fonts/symb08.pbm diff --git a/ev3dev/fonts/symb08.pil b/ev3dev2/fonts/symb08.pil similarity index 100% rename from ev3dev/fonts/symb08.pil rename to ev3dev2/fonts/symb08.pil diff --git a/ev3dev/fonts/symb10.pbm b/ev3dev2/fonts/symb10.pbm similarity index 100% rename from ev3dev/fonts/symb10.pbm rename to ev3dev2/fonts/symb10.pbm diff --git a/ev3dev/fonts/symb10.pil b/ev3dev2/fonts/symb10.pil similarity index 100% rename from ev3dev/fonts/symb10.pil rename to ev3dev2/fonts/symb10.pil diff --git a/ev3dev/fonts/symb12.pbm b/ev3dev2/fonts/symb12.pbm similarity index 100% rename from ev3dev/fonts/symb12.pbm rename to ev3dev2/fonts/symb12.pbm diff --git a/ev3dev/fonts/symb12.pil b/ev3dev2/fonts/symb12.pil similarity index 100% rename from ev3dev/fonts/symb12.pil rename to ev3dev2/fonts/symb12.pil diff --git a/ev3dev/fonts/symb14.pbm b/ev3dev2/fonts/symb14.pbm similarity index 100% rename from ev3dev/fonts/symb14.pbm rename to ev3dev2/fonts/symb14.pbm diff --git a/ev3dev/fonts/symb14.pil b/ev3dev2/fonts/symb14.pil similarity index 100% rename from ev3dev/fonts/symb14.pil rename to ev3dev2/fonts/symb14.pil diff --git a/ev3dev/fonts/symb18.pbm b/ev3dev2/fonts/symb18.pbm similarity index 100% rename from ev3dev/fonts/symb18.pbm rename to ev3dev2/fonts/symb18.pbm diff --git a/ev3dev/fonts/symb18.pil b/ev3dev2/fonts/symb18.pil similarity index 100% rename from ev3dev/fonts/symb18.pil rename to ev3dev2/fonts/symb18.pil diff --git a/ev3dev/fonts/symb24.pbm b/ev3dev2/fonts/symb24.pbm similarity index 100% rename from ev3dev/fonts/symb24.pbm rename to ev3dev2/fonts/symb24.pbm diff --git a/ev3dev/fonts/symb24.pil b/ev3dev2/fonts/symb24.pil similarity index 100% rename from ev3dev/fonts/symb24.pil rename to ev3dev2/fonts/symb24.pil diff --git a/ev3dev/fonts/tech14.pbm b/ev3dev2/fonts/tech14.pbm similarity index 100% rename from ev3dev/fonts/tech14.pbm rename to ev3dev2/fonts/tech14.pbm diff --git a/ev3dev/fonts/tech14.pil b/ev3dev2/fonts/tech14.pil similarity index 100% rename from ev3dev/fonts/tech14.pil rename to ev3dev2/fonts/tech14.pil diff --git a/ev3dev/fonts/techB14.pbm b/ev3dev2/fonts/techB14.pbm similarity index 100% rename from ev3dev/fonts/techB14.pbm rename to ev3dev2/fonts/techB14.pbm diff --git a/ev3dev/fonts/techB14.pil b/ev3dev2/fonts/techB14.pil similarity index 100% rename from ev3dev/fonts/techB14.pil rename to ev3dev2/fonts/techB14.pil diff --git a/ev3dev/fonts/term14.pbm b/ev3dev2/fonts/term14.pbm similarity index 100% rename from ev3dev/fonts/term14.pbm rename to ev3dev2/fonts/term14.pbm diff --git a/ev3dev/fonts/term14.pil b/ev3dev2/fonts/term14.pil similarity index 100% rename from ev3dev/fonts/term14.pil rename to ev3dev2/fonts/term14.pil diff --git a/ev3dev/fonts/termB14.pbm b/ev3dev2/fonts/termB14.pbm similarity index 100% rename from ev3dev/fonts/termB14.pbm rename to ev3dev2/fonts/termB14.pbm diff --git a/ev3dev/fonts/termB14.pil b/ev3dev2/fonts/termB14.pil similarity index 100% rename from ev3dev/fonts/termB14.pil rename to ev3dev2/fonts/termB14.pil diff --git a/ev3dev/fonts/timB08.pbm b/ev3dev2/fonts/timB08.pbm similarity index 100% rename from ev3dev/fonts/timB08.pbm rename to ev3dev2/fonts/timB08.pbm diff --git a/ev3dev/fonts/timB08.pil b/ev3dev2/fonts/timB08.pil similarity index 100% rename from ev3dev/fonts/timB08.pil rename to ev3dev2/fonts/timB08.pil diff --git a/ev3dev/fonts/timB10.pbm b/ev3dev2/fonts/timB10.pbm similarity index 100% rename from ev3dev/fonts/timB10.pbm rename to ev3dev2/fonts/timB10.pbm diff --git a/ev3dev/fonts/timB10.pil b/ev3dev2/fonts/timB10.pil similarity index 100% rename from ev3dev/fonts/timB10.pil rename to ev3dev2/fonts/timB10.pil diff --git a/ev3dev/fonts/timB12.pbm b/ev3dev2/fonts/timB12.pbm similarity index 100% rename from ev3dev/fonts/timB12.pbm rename to ev3dev2/fonts/timB12.pbm diff --git a/ev3dev/fonts/timB12.pil b/ev3dev2/fonts/timB12.pil similarity index 100% rename from ev3dev/fonts/timB12.pil rename to ev3dev2/fonts/timB12.pil diff --git a/ev3dev/fonts/timB14.pbm b/ev3dev2/fonts/timB14.pbm similarity index 100% rename from ev3dev/fonts/timB14.pbm rename to ev3dev2/fonts/timB14.pbm diff --git a/ev3dev/fonts/timB14.pil b/ev3dev2/fonts/timB14.pil similarity index 100% rename from ev3dev/fonts/timB14.pil rename to ev3dev2/fonts/timB14.pil diff --git a/ev3dev/fonts/timB18.pbm b/ev3dev2/fonts/timB18.pbm similarity index 100% rename from ev3dev/fonts/timB18.pbm rename to ev3dev2/fonts/timB18.pbm diff --git a/ev3dev/fonts/timB18.pil b/ev3dev2/fonts/timB18.pil similarity index 100% rename from ev3dev/fonts/timB18.pil rename to ev3dev2/fonts/timB18.pil diff --git a/ev3dev/fonts/timB24.pbm b/ev3dev2/fonts/timB24.pbm similarity index 100% rename from ev3dev/fonts/timB24.pbm rename to ev3dev2/fonts/timB24.pbm diff --git a/ev3dev/fonts/timB24.pil b/ev3dev2/fonts/timB24.pil similarity index 100% rename from ev3dev/fonts/timB24.pil rename to ev3dev2/fonts/timB24.pil diff --git a/ev3dev/fonts/timBI08.pbm b/ev3dev2/fonts/timBI08.pbm similarity index 100% rename from ev3dev/fonts/timBI08.pbm rename to ev3dev2/fonts/timBI08.pbm diff --git a/ev3dev/fonts/timBI08.pil b/ev3dev2/fonts/timBI08.pil similarity index 100% rename from ev3dev/fonts/timBI08.pil rename to ev3dev2/fonts/timBI08.pil diff --git a/ev3dev/fonts/timBI10.pbm b/ev3dev2/fonts/timBI10.pbm similarity index 100% rename from ev3dev/fonts/timBI10.pbm rename to ev3dev2/fonts/timBI10.pbm diff --git a/ev3dev/fonts/timBI10.pil b/ev3dev2/fonts/timBI10.pil similarity index 100% rename from ev3dev/fonts/timBI10.pil rename to ev3dev2/fonts/timBI10.pil diff --git a/ev3dev/fonts/timBI12.pbm b/ev3dev2/fonts/timBI12.pbm similarity index 100% rename from ev3dev/fonts/timBI12.pbm rename to ev3dev2/fonts/timBI12.pbm diff --git a/ev3dev/fonts/timBI12.pil b/ev3dev2/fonts/timBI12.pil similarity index 100% rename from ev3dev/fonts/timBI12.pil rename to ev3dev2/fonts/timBI12.pil diff --git a/ev3dev/fonts/timBI14.pbm b/ev3dev2/fonts/timBI14.pbm similarity index 100% rename from ev3dev/fonts/timBI14.pbm rename to ev3dev2/fonts/timBI14.pbm diff --git a/ev3dev/fonts/timBI14.pil b/ev3dev2/fonts/timBI14.pil similarity index 100% rename from ev3dev/fonts/timBI14.pil rename to ev3dev2/fonts/timBI14.pil diff --git a/ev3dev/fonts/timBI18.pbm b/ev3dev2/fonts/timBI18.pbm similarity index 100% rename from ev3dev/fonts/timBI18.pbm rename to ev3dev2/fonts/timBI18.pbm diff --git a/ev3dev/fonts/timBI18.pil b/ev3dev2/fonts/timBI18.pil similarity index 100% rename from ev3dev/fonts/timBI18.pil rename to ev3dev2/fonts/timBI18.pil diff --git a/ev3dev/fonts/timBI24.pbm b/ev3dev2/fonts/timBI24.pbm similarity index 100% rename from ev3dev/fonts/timBI24.pbm rename to ev3dev2/fonts/timBI24.pbm diff --git a/ev3dev/fonts/timBI24.pil b/ev3dev2/fonts/timBI24.pil similarity index 100% rename from ev3dev/fonts/timBI24.pil rename to ev3dev2/fonts/timBI24.pil diff --git a/ev3dev/fonts/timI08.pbm b/ev3dev2/fonts/timI08.pbm similarity index 100% rename from ev3dev/fonts/timI08.pbm rename to ev3dev2/fonts/timI08.pbm diff --git a/ev3dev/fonts/timI08.pil b/ev3dev2/fonts/timI08.pil similarity index 100% rename from ev3dev/fonts/timI08.pil rename to ev3dev2/fonts/timI08.pil diff --git a/ev3dev/fonts/timI10.pbm b/ev3dev2/fonts/timI10.pbm similarity index 100% rename from ev3dev/fonts/timI10.pbm rename to ev3dev2/fonts/timI10.pbm diff --git a/ev3dev/fonts/timI10.pil b/ev3dev2/fonts/timI10.pil similarity index 100% rename from ev3dev/fonts/timI10.pil rename to ev3dev2/fonts/timI10.pil diff --git a/ev3dev/fonts/timI12.pbm b/ev3dev2/fonts/timI12.pbm similarity index 100% rename from ev3dev/fonts/timI12.pbm rename to ev3dev2/fonts/timI12.pbm diff --git a/ev3dev/fonts/timI12.pil b/ev3dev2/fonts/timI12.pil similarity index 100% rename from ev3dev/fonts/timI12.pil rename to ev3dev2/fonts/timI12.pil diff --git a/ev3dev/fonts/timI14.pbm b/ev3dev2/fonts/timI14.pbm similarity index 100% rename from ev3dev/fonts/timI14.pbm rename to ev3dev2/fonts/timI14.pbm diff --git a/ev3dev/fonts/timI14.pil b/ev3dev2/fonts/timI14.pil similarity index 100% rename from ev3dev/fonts/timI14.pil rename to ev3dev2/fonts/timI14.pil diff --git a/ev3dev/fonts/timI18.pbm b/ev3dev2/fonts/timI18.pbm similarity index 100% rename from ev3dev/fonts/timI18.pbm rename to ev3dev2/fonts/timI18.pbm diff --git a/ev3dev/fonts/timI18.pil b/ev3dev2/fonts/timI18.pil similarity index 100% rename from ev3dev/fonts/timI18.pil rename to ev3dev2/fonts/timI18.pil diff --git a/ev3dev/fonts/timI24.pbm b/ev3dev2/fonts/timI24.pbm similarity index 100% rename from ev3dev/fonts/timI24.pbm rename to ev3dev2/fonts/timI24.pbm diff --git a/ev3dev/fonts/timI24.pil b/ev3dev2/fonts/timI24.pil similarity index 100% rename from ev3dev/fonts/timI24.pil rename to ev3dev2/fonts/timI24.pil diff --git a/ev3dev/fonts/timR08.pbm b/ev3dev2/fonts/timR08.pbm similarity index 100% rename from ev3dev/fonts/timR08.pbm rename to ev3dev2/fonts/timR08.pbm diff --git a/ev3dev/fonts/timR08.pil b/ev3dev2/fonts/timR08.pil similarity index 100% rename from ev3dev/fonts/timR08.pil rename to ev3dev2/fonts/timR08.pil diff --git a/ev3dev/fonts/timR10.pbm b/ev3dev2/fonts/timR10.pbm similarity index 100% rename from ev3dev/fonts/timR10.pbm rename to ev3dev2/fonts/timR10.pbm diff --git a/ev3dev/fonts/timR10.pil b/ev3dev2/fonts/timR10.pil similarity index 100% rename from ev3dev/fonts/timR10.pil rename to ev3dev2/fonts/timR10.pil diff --git a/ev3dev/fonts/timR12.pbm b/ev3dev2/fonts/timR12.pbm similarity index 100% rename from ev3dev/fonts/timR12.pbm rename to ev3dev2/fonts/timR12.pbm diff --git a/ev3dev/fonts/timR12.pil b/ev3dev2/fonts/timR12.pil similarity index 100% rename from ev3dev/fonts/timR12.pil rename to ev3dev2/fonts/timR12.pil diff --git a/ev3dev/fonts/timR14.pbm b/ev3dev2/fonts/timR14.pbm similarity index 100% rename from ev3dev/fonts/timR14.pbm rename to ev3dev2/fonts/timR14.pbm diff --git a/ev3dev/fonts/timR14.pil b/ev3dev2/fonts/timR14.pil similarity index 100% rename from ev3dev/fonts/timR14.pil rename to ev3dev2/fonts/timR14.pil diff --git a/ev3dev/fonts/timR18.pbm b/ev3dev2/fonts/timR18.pbm similarity index 100% rename from ev3dev/fonts/timR18.pbm rename to ev3dev2/fonts/timR18.pbm diff --git a/ev3dev/fonts/timR18.pil b/ev3dev2/fonts/timR18.pil similarity index 100% rename from ev3dev/fonts/timR18.pil rename to ev3dev2/fonts/timR18.pil diff --git a/ev3dev/fonts/timR24.pbm b/ev3dev2/fonts/timR24.pbm similarity index 100% rename from ev3dev/fonts/timR24.pbm rename to ev3dev2/fonts/timR24.pbm diff --git a/ev3dev/fonts/timR24.pil b/ev3dev2/fonts/timR24.pil similarity index 100% rename from ev3dev/fonts/timR24.pil rename to ev3dev2/fonts/timR24.pil diff --git a/ev3dev2/led.py b/ev3dev2/led.py new file mode 100644 index 0000000..48e0409 --- /dev/null +++ b/ev3dev2/led.py @@ -0,0 +1,600 @@ +# ----------------------------------------------------------------------------- +# Copyright (c) 2015 Ralph Hempel +# Copyright (c) 2015 Anton Vanhoucke +# Copyright (c) 2015 Denis Demidov +# Copyright (c) 2015 Eric Pascual +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +# ----------------------------------------------------------------------------- + +import sys +import os +import stat +import time +import _thread +from collections import OrderedDict +from ev3dev2 import get_current_platform, Device +from ev3dev2.stopwatch import StopWatch +from time import sleep + +if sys.version_info < (3, 4): + raise SystemError('Must be using Python 3.4 or higher') + +# Import the LED settings, this is platform specific +platform = get_current_platform() + +if platform == 'ev3': + from ev3dev2._platform.ev3 import LEDS, LED_GROUPS, LED_COLORS, LED_DEFAULT_COLOR + +elif platform == 'evb': + from ev3dev2._platform.evb import LEDS, LED_GROUPS, LED_COLORS, LED_DEFAULT_COLOR + +elif platform == 'pistorms': + from ev3dev2._platform.pistorms import LEDS, LED_GROUPS, LED_COLORS, LED_DEFAULT_COLOR + +elif platform == 'brickpi': + from ev3dev2._platform.brickpi import LEDS, LED_GROUPS, LED_COLORS, LED_DEFAULT_COLOR + +elif platform == 'brickpi3': + from ev3dev2._platform.brickpi3 import LEDS, LED_GROUPS, LED_COLORS, LED_DEFAULT_COLOR + +elif platform == 'fake': + from ev3dev2._platform.fake import LEDS, LED_GROUPS, LED_COLORS, LED_DEFAULT_COLOR + +else: + raise Exception("Unsupported platform '%s'" % platform) + + +class Led(Device): + """ + Any device controlled by the generic LED driver. + See https://www.kernel.org/doc/Documentation/leds/leds-class.txt + for more details. + """ + + SYSTEM_CLASS_NAME = 'leds' + SYSTEM_DEVICE_NAME_CONVENTION = '*' + __slots__ = [ + '_max_brightness', + '_brightness', + '_triggers', + '_trigger', + '_delay_on', + '_delay_off', + 'desc', + ] + + def __init__(self, name_pattern=SYSTEM_DEVICE_NAME_CONVENTION, name_exact=False, desc=None, **kwargs): + self.desc = desc + super(Led, self).__init__(self.SYSTEM_CLASS_NAME, name_pattern, name_exact, **kwargs) + self._max_brightness = None + self._brightness = None + self._triggers = None + self._trigger = None + self._delay_on = None + self._delay_off = None + + def __str__(self): + if self.desc: + return self.desc + else: + return Device.__str__(self) + + @property + def max_brightness(self): + """ + Returns the maximum allowable brightness value. + """ + self._max_brightness, value = self.get_cached_attr_int(self._max_brightness, 'max_brightness') + return value + + @property + def brightness(self): + """ + Sets the brightness level. Possible values are from 0 to ``max_brightness``. + """ + self._brightness, value = self.get_attr_int(self._brightness, 'brightness') + return value + + @brightness.setter + def brightness(self, value): + self._brightness = self.set_attr_int(self._brightness, 'brightness', value) + + @property + def triggers(self): + """ + Returns a list of available triggers. + """ + self._triggers, value = self.get_attr_set(self._triggers, 'trigger') + return value + + @property + def trigger(self): + """ + Sets the LED trigger. A trigger is a kernel based source of LED events. + Triggers can either be simple or complex. A simple trigger isn't + configurable and is designed to slot into existing subsystems with + minimal additional code. Examples are the ``ide-disk`` and ``nand-disk`` + triggers. + + Complex triggers whilst available to all LEDs have LED specific + parameters and work on a per LED basis. The ``timer`` trigger is an example. + The ``timer`` trigger will periodically change the LED brightness between + 0 and the current brightness setting. The ``on`` and ``off`` time can + be specified via ``delay_{on,off}`` attributes in milliseconds. + You can change the brightness value of a LED independently of the timer + trigger. However, if you set the brightness value to 0 it will + also disable the ``timer`` trigger. + """ + self._trigger, value = self.get_attr_from_set(self._trigger, 'trigger') + return value + + @trigger.setter + def trigger(self, value): + self._trigger = self.set_attr_string(self._trigger, 'trigger', value) + + # Workaround for ev3dev/ev3dev#225. + # When trigger is set to 'timer', we need to wait for 'delay_on' and + # 'delay_off' attributes to appear with correct permissions. + if value == 'timer': + for attr in ('delay_on', 'delay_off'): + path = self._path + '/' + attr + + # Make sure the file has been created: + for _ in range(5): + if os.path.exists(path): + break + time.sleep(0.2) + else: + raise Exception('"{}" attribute has not been created'.format(attr)) + + # Make sure the file has correct permissions: + for _ in range(5): + mode = stat.S_IMODE(os.stat(path)[stat.ST_MODE]) + if mode & stat.S_IRGRP and mode & stat.S_IWGRP: + break + time.sleep(0.2) + else: + raise Exception('"{}" attribute has wrong permissions'.format(attr)) + + @property + def delay_on(self): + """ + The ``timer`` trigger will periodically change the LED brightness between + 0 and the current brightness setting. The ``on`` time can + be specified via ``delay_on`` attribute in milliseconds. + """ + + # Workaround for ev3dev/ev3dev#225. + # 'delay_on' and 'delay_off' attributes are created when trigger is set + # to 'timer', and destroyed when it is set to anything else. + # This means the file cache may become outdated, and we may have to + # reopen the file. + for retry in (True, False): + try: + self._delay_on, value = self.get_attr_int(self._delay_on, 'delay_on') + return value + except OSError: + if retry: + self._delay_on = None + else: + raise + + @delay_on.setter + def delay_on(self, value): + # Workaround for ev3dev/ev3dev#225. + # 'delay_on' and 'delay_off' attributes are created when trigger is set + # to 'timer', and destroyed when it is set to anything else. + # This means the file cache may become outdated, and we may have to + # reopen the file. + for retry in (True, False): + try: + self._delay_on = self.set_attr_int(self._delay_on, 'delay_on', value) + return + except OSError: + if retry: + self._delay_on = None + else: + raise + + @property + def delay_off(self): + """ + The ``timer`` trigger will periodically change the LED brightness between + 0 and the current brightness setting. The ``off`` time can + be specified via ``delay_off`` attribute in milliseconds. + """ + + # Workaround for ev3dev/ev3dev#225. + # 'delay_on' and 'delay_off' attributes are created when trigger is set + # to 'timer', and destroyed when it is set to anything else. + # This means the file cache may become outdated, and we may have to + # reopen the file. + for retry in (True, False): + try: + self._delay_off, value = self.get_attr_int(self._delay_off, 'delay_off') + return value + except OSError: + if retry: + self._delay_off = None + else: + raise + + @delay_off.setter + def delay_off(self, value): + """ + Workaround for ev3dev/ev3dev#225. + ``delay_on`` and ``delay_off`` attributes are created when trigger is set + to ``timer``, and destroyed when it is set to anything else. + This means the file cache may become outdated, and we may have to + reopen the file. + """ + for retry in (True, False): + try: + self._delay_off = self.set_attr_int(self._delay_off, 'delay_off', value) + return + except OSError: + if retry: + self._delay_off = None + else: + raise + + @property + def brightness_pct(self): + """ + Returns LED brightness as a fraction of max_brightness + """ + return float(self.brightness) / self.max_brightness + + @brightness_pct.setter + def brightness_pct(self, value): + self.brightness = value * self.max_brightness + + +class Leds(object): + def __init__(self): + self.leds = OrderedDict() + self.led_groups = OrderedDict() + self.led_colors = LED_COLORS + self.animate_thread_id = None + self.animate_thread_stop = False + + for (key, value) in LEDS.items(): + self.leds[key] = Led(name_pattern=value, desc=key) + + for (key, value) in LED_GROUPS.items(): + self.led_groups[key] = [] + + for led_name in value: + self.led_groups[key].append(self.leds[led_name]) + + def __str__(self): + return self.__class__.__name__ + + def set_color(self, group, color, pct=1): + """ + Sets brightness of LEDs in the given group to the values specified in + color tuple. When percentage is specified, brightness of each LED is + reduced proportionally. + + Example:: + + my_leds = Leds() + my_leds.set_color('LEFT', 'AMBER') + + With a custom color:: + + my_leds = Leds() + my_leds.set_color('LEFT', (0.5, 0.3)) + """ + # If this is a platform without LEDs there is nothing to do + if not self.leds: + return + + color_tuple = color + if isinstance(color, str): + assert color in self.led_colors, \ + "%s is an invalid LED color, valid choices are %s" % \ + (color, ', '.join(self.led_colors.keys())) + color_tuple = self.led_colors[color] + + assert group in self.led_groups, \ + "%s is an invalid LED group, valid choices are %s" % \ + (group, ', '.join(self.led_groups.keys())) + + for led, value in zip(self.led_groups[group], color_tuple): + led.brightness_pct = value * pct + + def set(self, group, **kwargs): + """ + Set attributes for each LED in group. + + Example:: + + my_leds = Leds() + my_leds.set_color('LEFT', brightness_pct=0.5, trigger='timer') + """ + + # If this is a platform without LEDs there is nothing to do + if not self.leds: + return + + assert group in self.led_groups, \ + "%s is an invalid LED group, valid choices are %s" % \ + (group, ', '.join(self.led_groups.keys())) + + for led in self.led_groups[group]: + for k in kwargs: + setattr(led, k, kwargs[k]) + + def all_off(self): + """ + Turn all LEDs off + """ + + # If this is a platform without LEDs there is nothing to do + if not self.leds: + return + + self.animate_stop() + + for led in self.leds.values(): + led.brightness = 0 + + def reset(self): + """ + Put all LEDs back to their default color + """ + + if not self.leds: + return + + self.animate_stop() + + for group in self.led_groups: + self.set_color(group, LED_DEFAULT_COLOR) + + def animate_stop(self): + """ + Signal the current animation thread to exit and wait for it to exit + """ + + if self.animate_thread_id: + self.animate_thread_stop = True + + while self.animate_thread_id: + pass + + def animate_police_lights(self, + color1, + color2, + group1='LEFT', + group2='RIGHT', + sleeptime=0.5, + duration=5, + block=True): + """ + Cycle the ``group1`` and ``group2`` LEDs between ``color1`` and ``color2`` + to give the effect of police lights. Alternate the ``group1`` and ``group2`` + LEDs every ``sleeptime`` seconds. + + Animate for ``duration`` seconds. If ``duration`` is None animate for forever. + + Example: + + .. code-block:: python + + from ev3dev2.led import Leds + leds = Leds() + leds.animate_police_lights('RED', 'GREEN', sleeptime=0.75, duration=10) + """ + def _animate_police_lights(): + self.all_off() + even = True + duration_ms = duration * 1000 if duration is not None else None + stopwatch = StopWatch() + stopwatch.start() + + while True: + if even: + self.set_color(group1, color1) + self.set_color(group2, color2) + else: + self.set_color(group1, color2) + self.set_color(group2, color1) + + if self.animate_thread_stop or stopwatch.is_elapsed_ms(duration_ms): + break + + even = not even + sleep(sleeptime) + + self.animate_thread_stop = False + self.animate_thread_id = None + + self.animate_stop() + + if block: + _animate_police_lights() + else: + self.animate_thread_id = _thread.start_new_thread(_animate_police_lights, ()) + + def animate_flash(self, color, groups=('LEFT', 'RIGHT'), sleeptime=0.5, duration=5, block=True): + """ + Turn all LEDs in ``groups`` off/on to ``color`` every ``sleeptime`` seconds + + Animate for ``duration`` seconds. If ``duration`` is None animate for forever. + + Example: + + .. code-block:: python + + from ev3dev2.led import Leds + leds = Leds() + leds.animate_flash('AMBER', sleeptime=0.75, duration=10) + """ + def _animate_flash(): + even = True + duration_ms = duration * 1000 if duration is not None else None + stopwatch = StopWatch() + stopwatch.start() + + while True: + if even: + for group in groups: + self.set_color(group, color) + else: + self.all_off() + + if self.animate_thread_stop or stopwatch.is_elapsed_ms(duration_ms): + break + + even = not even + sleep(sleeptime) + + self.animate_thread_stop = False + self.animate_thread_id = None + + self.animate_stop() + + if block: + _animate_flash() + else: + self.animate_thread_id = _thread.start_new_thread(_animate_flash, ()) + + def animate_cycle(self, colors, groups=('LEFT', 'RIGHT'), sleeptime=0.5, duration=5, block=True): + """ + Cycle ``groups`` LEDs through ``colors``. Do this in a loop where + we display each color for ``sleeptime`` seconds. + + Animate for ``duration`` seconds. If ``duration`` is None animate for forever. + + Example: + + .. code-block:: python + + from ev3dev2.led import Leds + leds = Leds() + leds.animate_cycle(('RED', 'GREEN', 'AMBER')) + """ + def _animate_cycle(): + index = 0 + max_index = len(colors) + duration_ms = duration * 1000 if duration is not None else None + stopwatch = StopWatch() + stopwatch.start() + + while True: + for group in groups: + self.set_color(group, colors[index]) + + index += 1 + + if index == max_index: + index = 0 + + if self.animate_thread_stop or stopwatch.is_elapsed_ms(duration_ms): + break + + sleep(sleeptime) + + self.animate_thread_stop = False + self.animate_thread_id = None + + self.animate_stop() + + if block: + _animate_cycle() + else: + self.animate_thread_id = _thread.start_new_thread(_animate_cycle, ()) + + def animate_rainbow(self, group1='LEFT', group2='RIGHT', increment_by=0.1, sleeptime=0.1, duration=5, block=True): + """ + Gradually fade from one color to the next + + Animate for ``duration`` seconds. If ``duration`` is None animate for forever. + + Example: + + .. code-block:: python + + from ev3dev2.led import Leds + leds = Leds() + leds.animate_rainbow() + """ + def _animate_rainbow(): + # state 0: (LEFT,RIGHT) from (0,0) to (1,0)...RED + # state 1: (LEFT,RIGHT) from (1,0) to (1,1)...AMBER + # state 2: (LEFT,RIGHT) from (1,1) to (0,1)...GREEN + # state 3: (LEFT,RIGHT) from (0,1) to (0,0)...OFF + state = 0 + left_value = 0 + right_value = 0 + MIN_VALUE = 0 + MAX_VALUE = 1 + self.all_off() + duration_ms = duration * 1000 if duration is not None else None + stopwatch = StopWatch() + stopwatch.start() + + while True: + + if state == 0: + left_value += increment_by + elif state == 1: + right_value += increment_by + elif state == 2: + left_value -= increment_by + elif state == 3: + right_value -= increment_by + else: + raise Exception("Invalid state {}".format(state)) + + # Keep left_value and right_value within the MIN/MAX values + left_value = min(left_value, MAX_VALUE) + right_value = min(right_value, MAX_VALUE) + left_value = max(left_value, MIN_VALUE) + right_value = max(right_value, MIN_VALUE) + + self.set_color(group1, (left_value, right_value)) + self.set_color(group2, (left_value, right_value)) + + if state == 0 and left_value == MAX_VALUE: + state = 1 + elif state == 1 and right_value == MAX_VALUE: + state = 2 + elif state == 2 and left_value == MIN_VALUE: + state = 3 + elif state == 3 and right_value == MIN_VALUE: + state = 0 + + if self.animate_thread_stop or stopwatch.is_elapsed_ms(duration_ms): + break + + sleep(sleeptime) + + self.animate_thread_stop = False + self.animate_thread_id = None + + self.animate_stop() + + if block: + _animate_rainbow() + else: + self.animate_thread_id = _thread.start_new_thread(_animate_rainbow, ()) diff --git a/ev3dev2/motor.py b/ev3dev2/motor.py new file mode 100644 index 0000000..6e61ecf --- /dev/null +++ b/ev3dev2/motor.py @@ -0,0 +1,3008 @@ +# ----------------------------------------------------------------------------- +# Copyright (c) 2015 Ralph Hempel +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +# ----------------------------------------------------------------------------- + +import sys +import math +import select +import time +import _thread + +# python3 uses collections +# micropython uses ucollections +try: + from collections import OrderedDict +except ImportError: + from ucollections import OrderedDict + +from logging import getLogger +from os.path import abspath +from ev3dev2 import get_current_platform, Device, list_device_names, DeviceNotDefined, ThreadNotRunning +from ev3dev2.stopwatch import StopWatch + +# OUTPUT ports have platform specific values that we must import +platform = get_current_platform() + +if platform == 'ev3': + from ev3dev2._platform.ev3 import OUTPUT_A, OUTPUT_B, OUTPUT_C, OUTPUT_D # noqa: F401 + +elif platform == 'evb': + from ev3dev2._platform.evb import OUTPUT_A, OUTPUT_B, OUTPUT_C, OUTPUT_D # noqa: F401 + +elif platform == 'pistorms': + from ev3dev2._platform.pistorms import OUTPUT_A, OUTPUT_B, OUTPUT_C, OUTPUT_D # noqa: F401 + +elif platform == 'brickpi': + from ev3dev2._platform.brickpi import OUTPUT_A, OUTPUT_B, OUTPUT_C, OUTPUT_D # noqa: F401 + +elif platform == 'brickpi3': + from ev3dev2._platform.brickpi3 import ( # noqa: F401 + OUTPUT_A, OUTPUT_B, OUTPUT_C, OUTPUT_D, OUTPUT_E, OUTPUT_F, OUTPUT_G, OUTPUT_H, OUTPUT_I, OUTPUT_J, OUTPUT_K, + OUTPUT_L, OUTPUT_M, OUTPUT_N, OUTPUT_O, OUTPUT_P) + +elif platform == 'fake': + from ev3dev2._platform.fake import OUTPUT_A, OUTPUT_B, OUTPUT_C, OUTPUT_D # noqa: F401 + +else: + raise Exception("Unsupported platform '%s'" % platform) + +if sys.version_info < (3, 4): + raise SystemError('Must be using Python 3.4 or higher') + +log = getLogger(__name__) + +# The number of milliseconds we wait for the state of a motor to +# update to 'running' in the "on_for_XYZ" methods of the Motor class +WAIT_RUNNING_TIMEOUT = 100 + + +class SpeedInvalid(ValueError): + pass + + +class SpeedValue(object): + """ + A base class for other unit types. Don't use this directly; instead, see + :class:`SpeedPercent`, :class:`SpeedRPS`, :class:`SpeedRPM`, + :class:`SpeedDPS`, and :class:`SpeedDPM`. + """ + def __eq__(self, other): + return self.to_native_units() == other.to_native_units() + + def __ne__(self, other): + return not self.__eq__(other) + + def __lt__(self, other): + return self.to_native_units() < other.to_native_units() + + def __le__(self, other): + return self.to_native_units() <= other.to_native_units() + + def __gt__(self, other): + return self.to_native_units() > other.to_native_units() + + def __ge__(self, other): + return self.to_native_units() >= other.to_native_units() + + def __rmul__(self, other): + return self.__mul__(other) + + +class SpeedPercent(SpeedValue): + """ + Speed as a percentage of the motor's maximum rated speed. + """ + def __init__(self, percent, desc=None): + if percent < -100 or percent > 100: + raise SpeedInvalid("invalid percentage {}, must be between -100 and 100 (inclusive)".format(percent)) + self.percent = percent + self.desc = desc + + def __str__(self): + return "{} ".format(self.desc) if self.desc else "" + str(self.percent) + "%" + + def __mul__(self, other): + assert isinstance(other, (float, int)), "{} can only be multiplied by an int or float".format(self) + return SpeedPercent(self.percent * other) + + def to_native_units(self, motor): + """ + Return this SpeedPercent in native motor units + """ + return self.percent / 100 * motor.max_speed + + +class SpeedNativeUnits(SpeedValue): + """ + Speed in tacho counts per second. + """ + def __init__(self, native_counts, desc=None): + self.native_counts = native_counts + self.desc = desc + + def __str__(self): + return "{} ".format(self.desc) if self.desc else "" + "{:.2f}".format(self.native_counts) + " counts/sec" + + def __mul__(self, other): + assert isinstance(other, (float, int)), "{} can only be multiplied by an int or float".format(self) + return SpeedNativeUnits(self.native_counts * other) + + def to_native_units(self, motor=None): + """ + Return this SpeedNativeUnits as a number + """ + if self.native_counts > motor.max_speed: + raise SpeedInvalid("invalid native-units: {} max speed {}, {} was requested".format( + motor, motor.max_speed, self.native_counts)) + return self.native_counts + + +class SpeedRPS(SpeedValue): + """ + Speed in rotations-per-second. + """ + def __init__(self, rotations_per_second, desc=None): + self.rotations_per_second = rotations_per_second + self.desc = desc + + def __str__(self): + return "{} ".format(self.desc) if self.desc else "" + str(self.rotations_per_second) + " rot/sec" + + def __mul__(self, other): + assert isinstance(other, (float, int)), "{} can only be multiplied by an int or float".format(self) + return SpeedRPS(self.rotations_per_second * other) + + def to_native_units(self, motor): + """ + Return the native speed measurement required to achieve desired rotations-per-second + """ + if abs(self.rotations_per_second) > motor.max_rps: + raise SpeedInvalid("invalid rotations-per-second: {} max RPS is {}, {} was requested".format( + motor, motor.max_rps, self.rotations_per_second)) + return self.rotations_per_second / motor.max_rps * motor.max_speed + + +class SpeedRPM(SpeedValue): + """ + Speed in rotations-per-minute. + """ + def __init__(self, rotations_per_minute, desc=None): + self.rotations_per_minute = rotations_per_minute + self.desc = desc + + def __str__(self): + return "{} ".format(self.desc) if self.desc else "" + str(self.rotations_per_minute) + " rot/min" + + def __mul__(self, other): + assert isinstance(other, (float, int)), "{} can only be multiplied by an int or float".format(self) + return SpeedRPM(self.rotations_per_minute * other) + + def to_native_units(self, motor): + """ + Return the native speed measurement required to achieve desired rotations-per-minute + """ + if abs(self.rotations_per_minute) > motor.max_rpm: + raise SpeedInvalid("invalid rotations-per-minute: {} max RPM is {}, {} was requested".format( + motor, motor.max_rpm, self.rotations_per_minute)) + return self.rotations_per_minute / motor.max_rpm * motor.max_speed + + +class SpeedDPS(SpeedValue): + """ + Speed in degrees-per-second. + """ + def __init__(self, degrees_per_second, desc=None): + self.degrees_per_second = degrees_per_second + self.desc = desc + + def __str__(self): + return "{} ".format(self.desc) if self.desc else "" + str(self.degrees_per_second) + " deg/sec" + + def __mul__(self, other): + assert isinstance(other, (float, int)), "{} can only be multiplied by an int or float".format(self) + return SpeedDPS(self.degrees_per_second * other) + + def to_native_units(self, motor): + """ + Return the native speed measurement required to achieve desired degrees-per-second + """ + if abs(self.degrees_per_second) > motor.max_dps: + raise SpeedInvalid("invalid degrees-per-second: {} max DPS is {}, {} was requested".format( + motor, motor.max_dps, self.degrees_per_second)) + return self.degrees_per_second / motor.max_dps * motor.max_speed + + +class SpeedDPM(SpeedValue): + """ + Speed in degrees-per-minute. + """ + def __init__(self, degrees_per_minute, desc=None): + self.degrees_per_minute = degrees_per_minute + self.desc = desc + + def __str__(self): + return "{} ".format(self.desc) if self.desc else "" + str(self.degrees_per_minute) + " deg/min" + + def __mul__(self, other): + assert isinstance(other, (float, int)), "{} can only be multiplied by an int or float".format(self) + return SpeedDPM(self.degrees_per_minute * other) + + def to_native_units(self, motor): + """ + Return the native speed measurement required to achieve desired degrees-per-minute + """ + if abs(self.degrees_per_minute) > motor.max_dpm: + raise SpeedInvalid("invalid degrees-per-minute: {} max DPM is {}, {} was requested".format( + motor, motor.max_dpm, self.degrees_per_minute)) + return self.degrees_per_minute / motor.max_dpm * motor.max_speed + + +def speed_to_speedvalue(speed, desc=None): + """ + If ``speed`` is not a ``SpeedValue`` object, treat it as a percentage. + Returns a ``SpeedValue`` object. + """ + if isinstance(speed, SpeedValue): + return speed + else: + return SpeedPercent(speed, desc) + + +class Motor(Device): + """ + The motor class provides a uniform interface for using motors with + positional and directional feedback such as the EV3 and NXT motors. + This feedback allows for precise control of the motors. This is the + most common type of motor, so we just call it ``motor``. + """ + + SYSTEM_CLASS_NAME = 'tacho-motor' + SYSTEM_DEVICE_NAME_CONVENTION = '*' + + __slots__ = [ + '_address', + '_command', + '_commands', + '_count_per_rot', + '_count_per_m', + '_driver_name', + '_duty_cycle', + '_duty_cycle_sp', + '_full_travel_count', + '_polarity', + '_position', + '_position_p', + '_position_i', + '_position_d', + '_position_sp', + '_max_speed', + '_speed', + '_speed_sp', + '_ramp_up_sp', + '_ramp_down_sp', + '_speed_p', + '_speed_i', + '_speed_d', + '_state', + '_stop_action', + '_stop_actions', + '_time_sp', + '_poll', + 'max_rps', + 'max_rpm', + 'max_dps', + 'max_dpm', + ] + + #: Run the motor until another command is sent. + COMMAND_RUN_FOREVER = 'run-forever' + + #: Run to an absolute position specified by ``position_sp`` and then + #: stop using the action specified in ``stop_action``. + COMMAND_RUN_TO_ABS_POS = 'run-to-abs-pos' + + #: Run to a position relative to the current ``position`` value. + #: The new position will be current ``position`` + ``position_sp``. + #: When the new position is reached, the motor will stop using + #: the action specified by ``stop_action``. + COMMAND_RUN_TO_REL_POS = 'run-to-rel-pos' + + #: Run the motor for the amount of time specified in ``time_sp`` + #: and then stop the motor using the action specified by ``stop_action``. + COMMAND_RUN_TIMED = 'run-timed' + + #: Run the motor at the duty cycle specified by ``duty_cycle_sp``. + #: Unlike other run commands, changing ``duty_cycle_sp`` while running *will* + #: take effect immediately. + COMMAND_RUN_DIRECT = 'run-direct' + + #: Stop any of the run commands before they are complete using the + #: action specified by ``stop_action``. + COMMAND_STOP = 'stop' + + #: Reset all of the motor parameter attributes to their default value. + #: This will also have the effect of stopping the motor. + COMMAND_RESET = 'reset' + + #: Sets the normal polarity of the rotary encoder. + ENCODER_POLARITY_NORMAL = 'normal' + + #: Sets the inversed polarity of the rotary encoder. + ENCODER_POLARITY_INVERSED = 'inversed' + + #: With ``normal`` polarity, a positive duty cycle will + #: cause the motor to rotate clockwise. + POLARITY_NORMAL = 'normal' + + #: With ``inversed`` polarity, a positive duty cycle will + #: cause the motor to rotate counter-clockwise. + POLARITY_INVERSED = 'inversed' + + #: Power is being sent to the motor. + STATE_RUNNING = 'running' + + #: The motor is ramping up or down and has not yet reached a constant output level. + STATE_RAMPING = 'ramping' + + #: The motor is not turning, but rather attempting to hold a fixed position. + STATE_HOLDING = 'holding' + + #: The motor is turning, but cannot reach its ``speed_sp``. + STATE_OVERLOADED = 'overloaded' + + #: The motor is not turning when it should be. + STATE_STALLED = 'stalled' + + #: Power will be removed from the motor and it will freely coast to a stop. + STOP_ACTION_COAST = 'coast' + + #: Power will be removed from the motor and a passive electrical load will + #: be placed on the motor. This is usually done by shorting the motor terminals + #: together. This load will absorb the energy from the rotation of the motors and + #: cause the motor to stop more quickly than coasting. + STOP_ACTION_BRAKE = 'brake' + + #: Does not remove power from the motor. Instead it actively try to hold the motor + #: at the current position. If an external force tries to turn the motor, the motor + #: will ``push back`` to maintain its position. + STOP_ACTION_HOLD = 'hold' + + def __init__(self, address=None, name_pattern=SYSTEM_DEVICE_NAME_CONVENTION, name_exact=False, **kwargs): + + if platform in ('brickpi', 'brickpi3') and type(self).__name__ != 'Motor' and not isinstance(self, LargeMotor): + raise Exception("{} is unaware of different motor types, use LargeMotor instead".format(platform)) + + if address is not None: + kwargs['address'] = address + super(Motor, self).__init__(self.SYSTEM_CLASS_NAME, name_pattern, name_exact, **kwargs) + + self._address = None + self._command = None + self._commands = None + self._count_per_rot = None + self._count_per_m = None + self._driver_name = None + self._duty_cycle = None + self._duty_cycle_sp = None + self._full_travel_count = None + self._polarity = None + self._position = None + self._position_p = None + self._position_i = None + self._position_d = None + self._position_sp = None + self._max_speed = None + self._speed = None + self._speed_sp = None + self._ramp_up_sp = None + self._ramp_down_sp = None + self._speed_p = None + self._speed_i = None + self._speed_d = None + self._state = None + self._stop_action = None + self._stop_actions = None + self._time_sp = None + self._poll = None + self.max_rps = float(self.max_speed / self.count_per_rot) + self.max_rpm = self.max_rps * 60 + self.max_dps = self.max_rps * 360 + self.max_dpm = self.max_rpm * 360 + + @property + def address(self): + """ + Returns the name of the port that this motor is connected to. + """ + self._address, value = self.get_attr_string(self._address, 'address') + return value + + @property + def command(self): + """ + Sends a command to the motor controller. See ``commands`` for a list of + possible values. + """ + raise Exception("command is a write-only property!") + + @command.setter + def command(self, value): + self._command = self.set_attr_string(self._command, 'command', value) + + @property + def commands(self): + """ + Returns a list of commands that are supported by the motor + controller. Possible values are ``run-forever``, ``run-to-abs-pos``, ``run-to-rel-pos``, + ``run-timed``, ``run-direct``, ``stop`` and ``reset``. Not all commands may be supported. + + - ``run-forever`` will cause the motor to run until another command is sent. + - ``run-to-abs-pos`` will run to an absolute position specified by ``position_sp`` + and then stop using the action specified in ``stop_action``. + - ``run-to-rel-pos`` will run to a position relative to the current ``position`` value. + The new position will be current ``position`` + ``position_sp``. When the new + position is reached, the motor will stop using the action specified by ``stop_action``. + - ``run-timed`` will run the motor for the amount of time specified in ``time_sp`` + and then stop the motor using the action specified by ``stop_action``. + - ``run-direct`` will run the motor at the duty cycle specified by ``duty_cycle_sp``. + Unlike other run commands, changing ``duty_cycle_sp`` while running *will* + take effect immediately. + - ``stop`` will stop any of the run commands before they are complete using the + action specified by ``stop_action``. + - ``reset`` will reset all of the motor parameter attributes to their default value. + This will also have the effect of stopping the motor. + """ + (self._commands, value) = self.get_cached_attr_set(self._commands, 'commands') + return value + + @property + def count_per_rot(self): + """ + Returns the number of tacho counts in one rotation of the motor. Tacho counts + are used by the position and speed attributes, so you can use this value + to convert rotations or degrees to tacho counts. (rotation motors only) + """ + (self._count_per_rot, value) = self.get_cached_attr_int(self._count_per_rot, 'count_per_rot') + return value + + @property + def count_per_m(self): + """ + Returns the number of tacho counts in one meter of travel of the motor. Tacho + counts are used by the position and speed attributes, so you can use this + value to convert from distance to tacho counts. (linear motors only) + """ + (self._count_per_m, value) = self.get_cached_attr_int(self._count_per_m, 'count_per_m') + return value + + @property + def driver_name(self): + """ + Returns the name of the driver that provides this tacho motor device. + """ + (self._driver_name, value) = self.get_cached_attr_string(self._driver_name, 'driver_name') + return value + + @property + def duty_cycle(self): + """ + Returns the current duty cycle of the motor. Units are percent. Values + are -100 to 100. + """ + self._duty_cycle, value = self.get_attr_int(self._duty_cycle, 'duty_cycle') + return value + + @property + def duty_cycle_sp(self): + """ + Writing sets the duty cycle setpoint. Reading returns the current value. + Units are in percent. Valid values are -100 to 100. A negative value causes + the motor to rotate in reverse. + """ + self._duty_cycle_sp, value = self.get_attr_int(self._duty_cycle_sp, 'duty_cycle_sp') + return value + + @duty_cycle_sp.setter + def duty_cycle_sp(self, value): + self._duty_cycle_sp = self.set_attr_int(self._duty_cycle_sp, 'duty_cycle_sp', value) + + @property + def full_travel_count(self): + """ + Returns the number of tacho counts in the full travel of the motor. When + combined with the ``count_per_m`` atribute, you can use this value to + calculate the maximum travel distance of the motor. (linear motors only) + """ + (self._full_travel_count, value) = self.get_cached_attr_int(self._full_travel_count, 'full_travel_count') + return value + + @property + def polarity(self): + """ + Sets the polarity of the motor. With ``normal`` polarity, a positive duty + cycle will cause the motor to rotate clockwise. With ``inversed`` polarity, + a positive duty cycle will cause the motor to rotate counter-clockwise. + Valid values are ``normal`` and ``inversed``. + """ + self._polarity, value = self.get_attr_string(self._polarity, 'polarity') + return value + + @polarity.setter + def polarity(self, value): + self._polarity = self.set_attr_string(self._polarity, 'polarity', value) + + @property + def position(self): + """ + Returns the current position of the motor in pulses of the rotary + encoder. When the motor rotates clockwise, the position will increase. + Likewise, rotating counter-clockwise causes the position to decrease. + Writing will set the position to that value. + """ + self._position, value = self.get_attr_int(self._position, 'position') + return value + + @position.setter + def position(self, value): + self._position = self.set_attr_int(self._position, 'position', value) + + @property + def position_p(self): + """ + The proportional constant for the position PID. + """ + self._position_p, value = self.get_attr_int(self._position_p, 'hold_pid/Kp') + return value + + @position_p.setter + def position_p(self, value): + self._position_p = self.set_attr_int(self._position_p, 'hold_pid/Kp', value) + + @property + def position_i(self): + """ + The integral constant for the position PID. + """ + self._position_i, value = self.get_attr_int(self._position_i, 'hold_pid/Ki') + return value + + @position_i.setter + def position_i(self, value): + self._position_i = self.set_attr_int(self._position_i, 'hold_pid/Ki', value) + + @property + def position_d(self): + """ + The derivative constant for the position PID. + """ + self._position_d, value = self.get_attr_int(self._position_d, 'hold_pid/Kd') + return value + + @position_d.setter + def position_d(self, value): + self._position_d = self.set_attr_int(self._position_d, 'hold_pid/Kd', value) + + @property + def position_sp(self): + """ + Writing specifies the target position for the ``run-to-abs-pos`` and ``run-to-rel-pos`` + commands. Reading returns the current value. Units are in tacho counts. You + can use the value returned by ``count_per_rot`` to convert tacho counts to/from + rotations or degrees. + """ + self._position_sp, value = self.get_attr_int(self._position_sp, 'position_sp') + return value + + @position_sp.setter + def position_sp(self, value): + self._position_sp = self.set_attr_int(self._position_sp, 'position_sp', value) + + @property + def max_speed(self): + """ + Returns the maximum value that is accepted by the ``speed_sp`` attribute. This + may be slightly different than the maximum speed that a particular motor can + reach - it's the maximum theoretical speed. + """ + (self._max_speed, value) = self.get_cached_attr_int(self._max_speed, 'max_speed') + return value + + @property + def speed(self): + """ + Returns the current motor speed in tacho counts per second. Note, this is + not necessarily degrees (although it is for LEGO motors). Use the ``count_per_rot`` + attribute to convert this value to RPM or deg/sec. + """ + self._speed, value = self.get_attr_int(self._speed, 'speed') + return value + + @property + def speed_sp(self): + """ + Writing sets the target speed in tacho counts per second used for all ``run-*`` + commands except ``run-direct``. Reading returns the current value. A negative + value causes the motor to rotate in reverse with the exception of ``run-to-*-pos`` + commands where the sign is ignored. Use the ``count_per_rot`` attribute to convert + RPM or deg/sec to tacho counts per second. Use the ``count_per_m`` attribute to + convert m/s to tacho counts per second. + """ + self._speed_sp, value = self.get_attr_int(self._speed_sp, 'speed_sp') + return value + + @speed_sp.setter + def speed_sp(self, value): + self._speed_sp = self.set_attr_int(self._speed_sp, 'speed_sp', value) + + @property + def ramp_up_sp(self): + """ + Writing sets the ramp up setpoint. Reading returns the current value. Units + are in milliseconds and must be positive. When set to a non-zero value, the + motor speed will increase from 0 to 100% of ``max_speed`` over the span of this + setpoint. The actual ramp time is the ratio of the difference between the + ``speed_sp`` and the current ``speed`` and max_speed multiplied by ``ramp_up_sp``. + """ + self._ramp_up_sp, value = self.get_attr_int(self._ramp_up_sp, 'ramp_up_sp') + return value + + @ramp_up_sp.setter + def ramp_up_sp(self, value): + self._ramp_up_sp = self.set_attr_int(self._ramp_up_sp, 'ramp_up_sp', value) + + @property + def ramp_down_sp(self): + """ + Writing sets the ramp down setpoint. Reading returns the current value. Units + are in milliseconds and must be positive. When set to a non-zero value, the + motor speed will decrease from 0 to 100% of ``max_speed`` over the span of this + setpoint. The actual ramp time is the ratio of the difference between the + ``speed_sp`` and the current ``speed`` and max_speed multiplied by ``ramp_down_sp``. + """ + self._ramp_down_sp, value = self.get_attr_int(self._ramp_down_sp, 'ramp_down_sp') + return value + + @ramp_down_sp.setter + def ramp_down_sp(self, value): + self._ramp_down_sp = self.set_attr_int(self._ramp_down_sp, 'ramp_down_sp', value) + + @property + def speed_p(self): + """ + The proportional constant for the speed regulation PID. + """ + self._speed_p, value = self.get_attr_int(self._speed_p, 'speed_pid/Kp') + return value + + @speed_p.setter + def speed_p(self, value): + self._speed_p = self.set_attr_int(self._speed_p, 'speed_pid/Kp', value) + + @property + def speed_i(self): + """ + The integral constant for the speed regulation PID. + """ + self._speed_i, value = self.get_attr_int(self._speed_i, 'speed_pid/Ki') + return value + + @speed_i.setter + def speed_i(self, value): + self._speed_i = self.set_attr_int(self._speed_i, 'speed_pid/Ki', value) + + @property + def speed_d(self): + """ + The derivative constant for the speed regulation PID. + """ + self._speed_d, value = self.get_attr_int(self._speed_d, 'speed_pid/Kd') + return value + + @speed_d.setter + def speed_d(self, value): + self._speed_d = self.set_attr_int(self._speed_d, 'speed_pid/Kd', value) + + @property + def state(self): + """ + Reading returns a list of state flags. Possible flags are + ``running``, ``ramping``, ``holding``, ``overloaded`` and ``stalled``. + """ + self._state, value = self.get_attr_set(self._state, 'state') + return value + + @property + def stop_action(self): + """ + Reading returns the current stop action. Writing sets the stop action. + The value determines the motors behavior when ``command`` is set to ``stop``. + Also, it determines the motors behavior when a run command completes. See + ``stop_actions`` for a list of possible values. + """ + self._stop_action, value = self.get_attr_string(self._stop_action, 'stop_action') + return value + + @stop_action.setter + def stop_action(self, value): + self._stop_action = self.set_attr_string(self._stop_action, 'stop_action', value) + + @property + def stop_actions(self): + """ + Returns a list of stop actions supported by the motor controller. + Possible values are ``coast``, ``brake`` and ``hold``. ``coast`` means that power will + be removed from the motor and it will freely coast to a stop. ``brake`` means + that power will be removed from the motor and a passive electrical load will + be placed on the motor. This is usually done by shorting the motor terminals + together. This load will absorb the energy from the rotation of the motors and + cause the motor to stop more quickly than coasting. ``hold`` does not remove + power from the motor. Instead it actively tries to hold the motor at the current + position. If an external force tries to turn the motor, the motor will 'push + back' to maintain its position. + """ + (self._stop_actions, value) = self.get_cached_attr_set(self._stop_actions, 'stop_actions') + return value + + @property + def time_sp(self): + """ + Writing specifies the amount of time the motor will run when using the + ``run-timed`` command. Reading returns the current value. Units are in + milliseconds. + """ + self._time_sp, value = self.get_attr_int(self._time_sp, 'time_sp') + return value + + @time_sp.setter + def time_sp(self, value): + self._time_sp = self.set_attr_int(self._time_sp, 'time_sp', value) + + def run_forever(self, **kwargs): + """ + Run the motor until another command is sent. + """ + for key in kwargs: + setattr(self, key, kwargs[key]) + self.command = self.COMMAND_RUN_FOREVER + + def run_to_abs_pos(self, **kwargs): + """ + Run to an absolute position specified by ``position_sp`` and then + stop using the action specified in ``stop_action``. + """ + for key in kwargs: + setattr(self, key, kwargs[key]) + self.command = self.COMMAND_RUN_TO_ABS_POS + + def run_to_rel_pos(self, **kwargs): + """ + Run to a position relative to the current ``position`` value. + The new position will be current ``position`` + ``position_sp``. + When the new position is reached, the motor will stop using + the action specified by ``stop_action``. + """ + for key in kwargs: + setattr(self, key, kwargs[key]) + self.command = self.COMMAND_RUN_TO_REL_POS + + def run_timed(self, **kwargs): + """ + Run the motor for the amount of time specified in ``time_sp`` + and then stop the motor using the action specified by ``stop_action``. + """ + for key in kwargs: + setattr(self, key, kwargs[key]) + self.command = self.COMMAND_RUN_TIMED + + def run_direct(self, **kwargs): + """ + Run the motor at the duty cycle specified by ``duty_cycle_sp``. + Unlike other run commands, changing ``duty_cycle_sp`` while running *will* + take effect immediately. + """ + for key in kwargs: + setattr(self, key, kwargs[key]) + self.command = self.COMMAND_RUN_DIRECT + + def stop(self, **kwargs): + """ + Stop any of the run commands before they are complete using the + action specified by ``stop_action``. + """ + for key in kwargs: + setattr(self, key, kwargs[key]) + self.command = self.COMMAND_STOP + + def reset(self, **kwargs): + """ + Reset all of the motor parameter attributes to their default value. + This will also have the effect of stopping the motor. + """ + for key in kwargs: + setattr(self, key, kwargs[key]) + self.command = self.COMMAND_RESET + + @property + def is_running(self): + """ + Power is being sent to the motor. + """ + return self.STATE_RUNNING in self.state + + @property + def is_ramping(self): + """ + The motor is ramping up or down and has not yet reached a constant output level. + """ + return self.STATE_RAMPING in self.state + + @property + def is_holding(self): + """ + The motor is not turning, but rather attempting to hold a fixed position. + """ + return self.STATE_HOLDING in self.state + + @property + def is_overloaded(self): + """ + The motor is turning, but cannot reach its ``speed_sp``. + """ + return self.STATE_OVERLOADED in self.state + + @property + def is_stalled(self): + """ + The motor is not turning when it should be. + """ + return self.STATE_STALLED in self.state + + def wait(self, cond, timeout=None): + """ + Blocks until ``cond(self.state)`` is ``True``. The condition is + checked when there is an I/O event related to the ``state`` attribute. + Exits early when ``timeout`` (in milliseconds) is reached. + + Returns ``True`` if the condition is met, and ``False`` if the timeout + is reached. + """ + + tic = time.time() + + if self._poll is None: + if self._state is None: + self._state = self._attribute_file_open('state') + self._poll = select.poll() + self._poll.register(self._state, select.POLLPRI) + + # Set poll timeout to something small. For more details, see + # https://github.com/ev3dev/ev3dev-lang-python/issues/583 + if timeout: + poll_tm = min(timeout, 100) + else: + poll_tm = 100 + + while True: + # This check is now done every poll_tm even if poll has nothing to report: + if cond(self.state): + return True + + self._poll.poll(poll_tm) + + if timeout is not None and time.time() >= tic + timeout / 1000: + # Final check when user timeout is reached + return cond(self.state) + + def wait_until_not_moving(self, timeout=None): + """ + Blocks until ``running`` is not in ``self.state`` or ``stalled`` is in + ``self.state``. The condition is checked when there is an I/O event + related to the ``state`` attribute. Exits early when ``timeout`` + (in milliseconds) is reached. + + Returns ``True`` if the condition is met, and ``False`` if the timeout + is reached. + + Example:: + + m.wait_until_not_moving() + """ + return self.wait(lambda state: self.STATE_RUNNING not in state or self.STATE_STALLED in state, timeout) + + def wait_until(self, s, timeout=None): + """ + Blocks until ``s`` is in ``self.state``. The condition is checked when + there is an I/O event related to the ``state`` attribute. Exits early + when ``timeout`` (in milliseconds) is reached. + + Returns ``True`` if the condition is met, and ``False`` if the timeout + is reached. + + Example:: + + m.wait_until('stalled') + """ + return self.wait(lambda state: s in state, timeout) + + def wait_while(self, s, timeout=None): + """ + Blocks until ``s`` is not in ``self.state``. The condition is checked + when there is an I/O event related to the ``state`` attribute. Exits + early when ``timeout`` (in milliseconds) is reached. + + Returns ``True`` if the condition is met, and ``False`` if the timeout + is reached. + + Example:: + + m.wait_while('running') + """ + return self.wait(lambda state: s not in state, timeout) + + def _speed_native_units(self, speed, label=None): + speed = speed_to_speedvalue(speed, label) + return speed.to_native_units(self) + + def _set_rel_position_degrees_and_speed_sp(self, degrees, speed): + degrees = degrees if speed >= 0 else -degrees + speed = abs(speed) + + position_delta = int(round((degrees * self.count_per_rot) / 360)) + speed_sp = int(round(speed)) + + self.position_sp = position_delta + self.speed_sp = speed_sp + + def _set_brake(self, brake): + if brake: + self.stop_action = self.STOP_ACTION_HOLD + else: + self.stop_action = self.STOP_ACTION_COAST + + def on_for_rotations(self, speed, rotations, brake=True, block=True): + """ + Rotate the motor at ``speed`` for ``rotations`` + + ``speed`` can be a percentage or a :class:`ev3dev2.motor.SpeedValue` + object, enabling use of other units. + """ + speed_sp = self._speed_native_units(speed) + self._set_rel_position_degrees_and_speed_sp(rotations * 360, speed_sp) + self._set_brake(brake) + self.run_to_rel_pos() + + if block: + self.wait_until('running', timeout=WAIT_RUNNING_TIMEOUT) + self.wait_until_not_moving() + + def on_for_degrees(self, speed, degrees, brake=True, block=True): + """ + Rotate the motor at ``speed`` for ``degrees`` + + ``speed`` can be a percentage or a :class:`ev3dev2.motor.SpeedValue` + object, enabling use of other units. + """ + speed_sp = self._speed_native_units(speed) + self._set_rel_position_degrees_and_speed_sp(degrees, speed_sp) + self._set_brake(brake) + self.run_to_rel_pos() + + if block: + self.wait_until('running', timeout=WAIT_RUNNING_TIMEOUT) + self.wait_until_not_moving() + + def on_to_position(self, speed, position, brake=True, block=True): + """ + Rotate the motor at ``speed`` to ``position`` + + ``speed`` can be a percentage or a :class:`ev3dev2.motor.SpeedValue` + object, enabling use of other units. + """ + speed = self._speed_native_units(speed) + self.speed_sp = int(round(speed)) + self.position_sp = position + self._set_brake(brake) + self.run_to_abs_pos() + + if block: + self.wait_until('running', timeout=WAIT_RUNNING_TIMEOUT) + self.wait_until_not_moving() + + def on_for_seconds(self, speed, seconds, brake=True, block=True): + """ + Rotate the motor at ``speed`` for ``seconds`` + + ``speed`` can be a percentage or a :class:`ev3dev2.motor.SpeedValue` + object, enabling use of other units. + """ + + if seconds < 0: + raise ValueError("seconds is negative ({})".format(seconds)) + + speed = self._speed_native_units(speed) + self.speed_sp = int(round(speed)) + self.time_sp = int(seconds * 1000) + self._set_brake(brake) + self.run_timed() + + if block: + self.wait_until('running', timeout=WAIT_RUNNING_TIMEOUT) + self.wait_until_not_moving() + + def on(self, speed, brake=True, block=False): + """ + Rotate the motor at ``speed`` for forever + + ``speed`` can be a percentage or a :class:`ev3dev2.motor.SpeedValue` + object, enabling use of other units. + + Note that ``block`` is False by default, this is different from the + other ``on_for_XYZ`` methods. + """ + speed = self._speed_native_units(speed) + self.speed_sp = int(round(speed)) + self._set_brake(brake) + self.run_forever() + + if block: + self.wait_until('running', timeout=WAIT_RUNNING_TIMEOUT) + self.wait_until_not_moving() + + def off(self, brake=True): + self._set_brake(brake) + self.stop() + + @property + def rotations(self): + return float(self.position / self.count_per_rot) + + @property + def degrees(self): + return self.rotations * 360 + + +def list_motors(name_pattern=Motor.SYSTEM_DEVICE_NAME_CONVENTION, **kwargs): + """ + This is a generator function that enumerates all tacho motors that match + the provided arguments. + + Parameters: + name_pattern: pattern that device name should match. + For example, 'motor*'. Default value: '*'. + keyword arguments: used for matching the corresponding device + attributes. For example, driver_name='lego-ev3-l-motor', or + address=['outB', 'outC']. When argument value + is a list, then a match against any entry of the list is + enough. + """ + class_path = abspath(Device.DEVICE_ROOT_PATH + '/' + Motor.SYSTEM_CLASS_NAME) + + return (Motor(name_pattern=name, name_exact=True) for name in list_device_names(class_path, name_pattern, **kwargs)) + + +class LargeMotor(Motor): + """ + EV3/NXT large servo motor. + + Same as :class:`Motor`, except it will only successfully initialize if it finds a "large" motor. + """ + + SYSTEM_CLASS_NAME = Motor.SYSTEM_CLASS_NAME + SYSTEM_DEVICE_NAME_CONVENTION = '*' + __slots__ = [] + + def __init__(self, address=None, name_pattern=SYSTEM_DEVICE_NAME_CONVENTION, name_exact=False, **kwargs): + + super(LargeMotor, self).__init__(address, + name_pattern, + name_exact, + driver_name=['lego-ev3-l-motor', 'lego-nxt-motor'], + **kwargs) + + +class MediumMotor(Motor): + """ + EV3 medium servo motor. + + Same as :class:`Motor`, except it will only successfully initialize if it finds a "medium" motor. + """ + + SYSTEM_CLASS_NAME = Motor.SYSTEM_CLASS_NAME + SYSTEM_DEVICE_NAME_CONVENTION = '*' + __slots__ = [] + + def __init__(self, address=None, name_pattern=SYSTEM_DEVICE_NAME_CONVENTION, name_exact=False, **kwargs): + + super(MediumMotor, self).__init__(address, name_pattern, name_exact, driver_name=['lego-ev3-m-motor'], **kwargs) + + +class ActuonixL1250Motor(Motor): + """ + Actuonix L12 50 linear servo motor. + + Same as :class:`Motor`, except it will only successfully initialize if it finds an + Actuonix L12 50 linear servo motor + """ + + SYSTEM_CLASS_NAME = Motor.SYSTEM_CLASS_NAME + SYSTEM_DEVICE_NAME_CONVENTION = 'linear*' + __slots__ = [] + + def __init__(self, address=None, name_pattern=SYSTEM_DEVICE_NAME_CONVENTION, name_exact=False, **kwargs): + + super(ActuonixL1250Motor, self).__init__(address, + name_pattern, + name_exact, + driver_name=['act-l12-ev3-50'], + **kwargs) + + +class ActuonixL12100Motor(Motor): + """ + Actuonix L12 100 linear servo motor. + + Same as :class:`Motor`, except it will only successfully initialize if it finds an + Actuonix L12 100linear servo motor + """ + + SYSTEM_CLASS_NAME = Motor.SYSTEM_CLASS_NAME + SYSTEM_DEVICE_NAME_CONVENTION = 'linear*' + __slots__ = [] + + def __init__(self, address=None, name_pattern=SYSTEM_DEVICE_NAME_CONVENTION, name_exact=False, **kwargs): + + super(ActuonixL12100Motor, self).__init__(address, + name_pattern, + name_exact, + driver_name=['act-l12-ev3-100'], + **kwargs) + + +class DcMotor(Device): + """ + The DC motor class provides a uniform interface for using regular DC motors + with no fancy controls or feedback. This includes LEGO MINDSTORMS RCX motors + and LEGO Power Functions motors. + """ + + SYSTEM_CLASS_NAME = 'dc-motor' + SYSTEM_DEVICE_NAME_CONVENTION = 'motor*' + __slots__ = [ + '_address', + '_command', + '_commands', + '_driver_name', + '_duty_cycle', + '_duty_cycle_sp', + '_polarity', + '_ramp_down_sp', + '_ramp_up_sp', + '_state', + '_stop_action', + '_stop_actions', + '_time_sp', + ] + + def __init__(self, address=None, name_pattern=SYSTEM_DEVICE_NAME_CONVENTION, name_exact=False, **kwargs): + + if address is not None: + kwargs['address'] = address + super(DcMotor, self).__init__(self.SYSTEM_CLASS_NAME, name_pattern, name_exact, **kwargs) + + self._address = None + self._command = None + self._commands = None + self._driver_name = None + self._duty_cycle = None + self._duty_cycle_sp = None + self._polarity = None + self._ramp_down_sp = None + self._ramp_up_sp = None + self._state = None + self._stop_action = None + self._stop_actions = None + self._time_sp = None + + @property + def address(self): + """ + Returns the name of the port that this motor is connected to. + """ + self._address, value = self.get_attr_string(self._address, 'address') + return value + + @property + def command(self): + """ + Sets the command for the motor. Possible values are ``run-forever``, ``run-timed`` and + ``stop``. Not all commands may be supported, so be sure to check the contents + of the ``commands`` attribute. + """ + raise Exception("command is a write-only property!") + + @command.setter + def command(self, value): + self._command = self.set_attr_string(self._command, 'command', value) + + @property + def commands(self): + """ + Returns a list of commands supported by the motor + controller. + """ + self._commands, value = self.get_attr_set(self._commands, 'commands') + return value + + @property + def driver_name(self): + """ + Returns the name of the motor driver that loaded this device. See the list + of [supported devices] for a list of drivers. + """ + self._driver_name, value = self.get_attr_string(self._driver_name, 'driver_name') + return value + + @property + def duty_cycle(self): + """ + Shows the current duty cycle of the PWM signal sent to the motor. Values + are -100 to 100 (-100% to 100%). + """ + self._duty_cycle, value = self.get_attr_int(self._duty_cycle, 'duty_cycle') + return value + + @property + def duty_cycle_sp(self): + """ + Writing sets the duty cycle setpoint of the PWM signal sent to the motor. + Valid values are -100 to 100 (-100% to 100%). Reading returns the current + setpoint. + """ + self._duty_cycle_sp, value = self.get_attr_int(self._duty_cycle_sp, 'duty_cycle_sp') + return value + + @duty_cycle_sp.setter + def duty_cycle_sp(self, value): + self._duty_cycle_sp = self.set_attr_int(self._duty_cycle_sp, 'duty_cycle_sp', value) + + @property + def polarity(self): + """ + Sets the polarity of the motor. Valid values are ``normal`` and ``inversed``. + """ + self._polarity, value = self.get_attr_string(self._polarity, 'polarity') + return value + + @polarity.setter + def polarity(self, value): + self._polarity = self.set_attr_string(self._polarity, 'polarity', value) + + @property + def ramp_down_sp(self): + """ + Sets the time in milliseconds that it take the motor to ramp down from 100% + to 0%. Valid values are 0 to 10000 (10 seconds). Default is 0. + """ + self._ramp_down_sp, value = self.get_attr_int(self._ramp_down_sp, 'ramp_down_sp') + return value + + @ramp_down_sp.setter + def ramp_down_sp(self, value): + self._ramp_down_sp = self.set_attr_int(self._ramp_down_sp, 'ramp_down_sp', value) + + @property + def ramp_up_sp(self): + """ + Sets the time in milliseconds that it take the motor to up ramp from 0% to + 100%. Valid values are 0 to 10000 (10 seconds). Default is 0. + """ + self._ramp_up_sp, value = self.get_attr_int(self._ramp_up_sp, 'ramp_up_sp') + return value + + @ramp_up_sp.setter + def ramp_up_sp(self, value): + self._ramp_up_sp = self.set_attr_int(self._ramp_up_sp, 'ramp_up_sp', value) + + @property + def state(self): + """ + Gets a list of flags indicating the motor status. Possible + flags are ``running`` and ``ramping``. ``running`` indicates that the motor is + powered. ``ramping`` indicates that the motor has not yet reached the + ``duty_cycle_sp``. + """ + self._state, value = self.get_attr_set(self._state, 'state') + return value + + @property + def stop_action(self): + """ + Sets the stop action that will be used when the motor stops. Read + ``stop_actions`` to get the list of valid values. + """ + raise Exception("stop_action is a write-only property!") + + @stop_action.setter + def stop_action(self, value): + self._stop_action = self.set_attr_string(self._stop_action, 'stop_action', value) + + @property + def stop_actions(self): + """ + Gets a list of stop actions. Valid values are ``coast`` + and ``brake``. + """ + self._stop_actions, value = self.get_attr_set(self._stop_actions, 'stop_actions') + return value + + @property + def time_sp(self): + """ + Writing specifies the amount of time the motor will run when using the + ``run-timed`` command. Reading returns the current value. Units are in + milliseconds. + """ + self._time_sp, value = self.get_attr_int(self._time_sp, 'time_sp') + return value + + @time_sp.setter + def time_sp(self, value): + self._time_sp = self.set_attr_int(self._time_sp, 'time_sp', value) + + #: Run the motor until another command is sent. + COMMAND_RUN_FOREVER = 'run-forever' + + #: Run the motor for the amount of time specified in ``time_sp`` + #: and then stop the motor using the action specified by ``stop_action``. + COMMAND_RUN_TIMED = 'run-timed' + + #: Run the motor at the duty cycle specified by ``duty_cycle_sp``. + #: Unlike other run commands, changing ``duty_cycle_sp`` while running *will* + #: take effect immediately. + COMMAND_RUN_DIRECT = 'run-direct' + + #: Stop any of the run commands before they are complete using the + #: action specified by ``stop_action``. + COMMAND_STOP = 'stop' + + #: With ``normal`` polarity, a positive duty cycle will + #: cause the motor to rotate clockwise. + POLARITY_NORMAL = 'normal' + + #: With ``inversed`` polarity, a positive duty cycle will + #: cause the motor to rotate counter-clockwise. + POLARITY_INVERSED = 'inversed' + + #: Power will be removed from the motor and it will freely coast to a stop. + STOP_ACTION_COAST = 'coast' + + #: Power will be removed from the motor and a passive electrical load will + #: be placed on the motor. This is usually done by shorting the motor terminals + #: together. This load will absorb the energy from the rotation of the motors and + #: cause the motor to stop more quickly than coasting. + STOP_ACTION_BRAKE = 'brake' + + def run_forever(self, **kwargs): + """ + Run the motor until another command is sent. + """ + for key in kwargs: + setattr(self, key, kwargs[key]) + self.command = self.COMMAND_RUN_FOREVER + + def run_timed(self, **kwargs): + """ + Run the motor for the amount of time specified in ``time_sp`` + and then stop the motor using the action specified by ``stop_action``. + """ + for key in kwargs: + setattr(self, key, kwargs[key]) + self.command = self.COMMAND_RUN_TIMED + + def run_direct(self, **kwargs): + """ + Run the motor at the duty cycle specified by ``duty_cycle_sp``. + Unlike other run commands, changing ``duty_cycle_sp`` while running *will* + take effect immediately. + """ + for key in kwargs: + setattr(self, key, kwargs[key]) + self.command = self.COMMAND_RUN_DIRECT + + def stop(self, **kwargs): + """ + Stop any of the run commands before they are complete using the + action specified by ``stop_action``. + """ + for key in kwargs: + setattr(self, key, kwargs[key]) + self.command = self.COMMAND_STOP + + +class ServoMotor(Device): + """ + The servo motor class provides a uniform interface for using hobby type + servo motors. + """ + + SYSTEM_CLASS_NAME = 'servo-motor' + SYSTEM_DEVICE_NAME_CONVENTION = 'motor*' + __slots__ = [ + '_address', + '_command', + '_driver_name', + '_max_pulse_sp', + '_mid_pulse_sp', + '_min_pulse_sp', + '_polarity', + '_position_sp', + '_rate_sp', + '_state', + ] + + def __init__(self, address=None, name_pattern=SYSTEM_DEVICE_NAME_CONVENTION, name_exact=False, **kwargs): + + if address is not None: + kwargs['address'] = address + super(ServoMotor, self).__init__(self.SYSTEM_CLASS_NAME, name_pattern, name_exact, **kwargs) + + self._address = None + self._command = None + self._driver_name = None + self._max_pulse_sp = None + self._mid_pulse_sp = None + self._min_pulse_sp = None + self._polarity = None + self._position_sp = None + self._rate_sp = None + self._state = None + + @property + def address(self): + """ + Returns the name of the port that this motor is connected to. + """ + self._address, value = self.get_attr_string(self._address, 'address') + return value + + @property + def command(self): + """ + Sets the command for the servo. Valid values are ``run`` and ``float``. Setting + to ``run`` will cause the servo to be driven to the position_sp set in the + ``position_sp`` attribute. Setting to ``float`` will remove power from the motor. + """ + raise Exception("command is a write-only property!") + + @command.setter + def command(self, value): + self._command = self.set_attr_string(self._command, 'command', value) + + @property + def driver_name(self): + """ + Returns the name of the motor driver that loaded this device. See the list + of [supported devices] for a list of drivers. + """ + self._driver_name, value = self.get_attr_string(self._driver_name, 'driver_name') + return value + + @property + def max_pulse_sp(self): + """ + Used to set the pulse size in milliseconds for the signal that tells the + servo to drive to the maximum (clockwise) position_sp. Default value is 2400. + Valid values are 2300 to 2700. You must write to the position_sp attribute for + changes to this attribute to take effect. + """ + self._max_pulse_sp, value = self.get_attr_int(self._max_pulse_sp, 'max_pulse_sp') + return value + + @max_pulse_sp.setter + def max_pulse_sp(self, value): + self._max_pulse_sp = self.set_attr_int(self._max_pulse_sp, 'max_pulse_sp', value) + + @property + def mid_pulse_sp(self): + """ + Used to set the pulse size in milliseconds for the signal that tells the + servo to drive to the mid position_sp. Default value is 1500. Valid + values are 1300 to 1700. For example, on a 180 degree servo, this would be + 90 degrees. On continuous rotation servo, this is the 'neutral' position_sp + where the motor does not turn. You must write to the position_sp attribute for + changes to this attribute to take effect. + """ + self._mid_pulse_sp, value = self.get_attr_int(self._mid_pulse_sp, 'mid_pulse_sp') + return value + + @mid_pulse_sp.setter + def mid_pulse_sp(self, value): + self._mid_pulse_sp = self.set_attr_int(self._mid_pulse_sp, 'mid_pulse_sp', value) + + @property + def min_pulse_sp(self): + """ + Used to set the pulse size in milliseconds for the signal that tells the + servo to drive to the miniumum (counter-clockwise) position_sp. Default value + is 600. Valid values are 300 to 700. You must write to the position_sp + attribute for changes to this attribute to take effect. + """ + self._min_pulse_sp, value = self.get_attr_int(self._min_pulse_sp, 'min_pulse_sp') + return value + + @min_pulse_sp.setter + def min_pulse_sp(self, value): + self._min_pulse_sp = self.set_attr_int(self._min_pulse_sp, 'min_pulse_sp', value) + + @property + def polarity(self): + """ + Sets the polarity of the servo. Valid values are ``normal`` and ``inversed``. + Setting the value to ``inversed`` will cause the position_sp value to be + inversed. i.e ``-100`` will correspond to ``max_pulse_sp``, and ``100`` will + correspond to ``min_pulse_sp``. + """ + self._polarity, value = self.get_attr_string(self._polarity, 'polarity') + return value + + @polarity.setter + def polarity(self, value): + self._polarity = self.set_attr_string(self._polarity, 'polarity', value) + + @property + def position_sp(self): + """ + Reading returns the current position_sp of the servo. Writing instructs the + servo to move to the specified position_sp. Units are percent. Valid values + are -100 to 100 (-100% to 100%) where ``-100`` corresponds to ``min_pulse_sp``, + ``0`` corresponds to ``mid_pulse_sp`` and ``100`` corresponds to ``max_pulse_sp``. + """ + self._position_sp, value = self.get_attr_int(self._position_sp, 'position_sp') + return value + + @position_sp.setter + def position_sp(self, value): + self._position_sp = self.set_attr_int(self._position_sp, 'position_sp', value) + + @property + def rate_sp(self): + """ + Sets the rate_sp at which the servo travels from 0 to 100.0% (half of the full + range of the servo). Units are in milliseconds. Example: Setting the rate_sp + to 1000 means that it will take a 180 degree servo 2 second to move from 0 + to 180 degrees. Note: Some servo controllers may not support this in which + case reading and writing will fail with ``-EOPNOTSUPP``. In continuous rotation + servos, this value will affect the rate_sp at which the speed ramps up or down. + """ + self._rate_sp, value = self.get_attr_int(self._rate_sp, 'rate_sp') + return value + + @rate_sp.setter + def rate_sp(self, value): + self._rate_sp = self.set_attr_int(self._rate_sp, 'rate_sp', value) + + @property + def state(self): + """ + Returns a list of flags indicating the state of the servo. + Possible values are: + * ``running``: Indicates that the motor is powered. + """ + self._state, value = self.get_attr_set(self._state, 'state') + return value + + #: Drive servo to the position set in the ``position_sp`` attribute. + COMMAND_RUN = 'run' + + #: Remove power from the motor. + COMMAND_FLOAT = 'float' + + #: With ``normal`` polarity, a positive duty cycle will + #: cause the motor to rotate clockwise. + POLARITY_NORMAL = 'normal' + + #: With ``inversed`` polarity, a positive duty cycle will + #: cause the motor to rotate counter-clockwise. + POLARITY_INVERSED = 'inversed' + + def run(self, **kwargs): + """ + Drive servo to the position set in the ``position_sp`` attribute. + """ + for key in kwargs: + setattr(self, key, kwargs[key]) + self.command = self.COMMAND_RUN + + def float(self, **kwargs): + """ + Remove power from the motor. + """ + for key in kwargs: + setattr(self, key, kwargs[key]) + self.command = self.COMMAND_FLOAT + + +class MotorSet(object): + def __init__(self, motor_specs, desc=None): + """ + motor_specs is a dictionary such as + { + OUTPUT_A : LargeMotor, + OUTPUT_C : LargeMotor, + } + """ + self.motors = OrderedDict() + for motor_port in sorted(motor_specs.keys()): + motor_class = motor_specs[motor_port] + self.motors[motor_port] = motor_class(motor_port) + self.motors[motor_port].reset() + + self.desc = desc + + def __str__(self): + + if self.desc: + return self.desc + else: + return self.__class__.__name__ + + def set_args(self, **kwargs): + motors = kwargs.get('motors', self.motors.values()) + + for motor in motors: + for key in kwargs: + if key != 'motors': + try: + setattr(motor, key, kwargs[key]) + except AttributeError as e: + # log.error("%s %s cannot set %s to %s" % (self, motor, key, kwargs[key])) + raise e + + def set_polarity(self, polarity, motors=None): + valid_choices = (LargeMotor.POLARITY_NORMAL, LargeMotor.POLARITY_INVERSED) + + assert polarity in valid_choices,\ + "%s is an invalid polarity choice, must be %s" % (polarity, ', '.join(valid_choices)) + motors = motors if motors is not None else self.motors.values() + + for motor in motors: + motor.polarity = polarity + + def _run_command(self, **kwargs): + motors = kwargs.get('motors', self.motors.values()) + + for motor in motors: + for key in kwargs: + if key not in ('motors', 'commands'): + # log.debug("%s: %s set %s to %s" % (self, motor, key, kwargs[key])) + setattr(motor, key, kwargs[key]) + + for motor in motors: + motor.command = kwargs['command'] + # log.debug("%s: %s command %s" % (self, motor, kwargs['command'])) + + def run_forever(self, **kwargs): + kwargs['command'] = LargeMotor.COMMAND_RUN_FOREVER + self._run_command(**kwargs) + + def run_to_abs_pos(self, **kwargs): + kwargs['command'] = LargeMotor.COMMAND_RUN_TO_ABS_POS + self._run_command(**kwargs) + + def run_to_rel_pos(self, **kwargs): + kwargs['command'] = LargeMotor.COMMAND_RUN_TO_REL_POS + self._run_command(**kwargs) + + def run_timed(self, **kwargs): + kwargs['command'] = LargeMotor.COMMAND_RUN_TIMED + self._run_command(**kwargs) + + def run_direct(self, **kwargs): + kwargs['command'] = LargeMotor.COMMAND_RUN_DIRECT + self._run_command(**kwargs) + + def reset(self, motors=None): + motors = motors if motors is not None else self.motors.values() + + for motor in motors: + motor.reset() + + def off(self, motors=None, brake=True): + """ + Stop motors immediately. Configure motors to brake if ``brake`` is set. + """ + motors = motors if motors is not None else self.motors.values() + + for motor in motors: + motor._set_brake(brake) + + for motor in motors: + motor.stop() + + def stop(self, motors=None, brake=True): + """ + ``stop`` is an alias of ``off``. This is deprecated but helps keep + the API for MotorSet somewhat similar to Motor which has both ``stop`` + and ``off``. + """ + self.off(motors, brake) + + def _is_state(self, motors, state): + motors = motors if motors is not None else self.motors.values() + + for motor in motors: + if state not in motor.state: + return False + + return True + + @property + def is_running(self, motors=None): + return self._is_state(motors, LargeMotor.STATE_RUNNING) + + @property + def is_ramping(self, motors=None): + return self._is_state(motors, LargeMotor.STATE_RAMPING) + + @property + def is_holding(self, motors=None): + return self._is_state(motors, LargeMotor.STATE_HOLDING) + + @property + def is_overloaded(self, motors=None): + return self._is_state(motors, LargeMotor.STATE_OVERLOADED) + + @property + def is_stalled(self, motors=None): + return self._is_state(motors, LargeMotor.STATE_STALLED) + + def wait(self, cond, timeout=None, motors=None): + motors = motors if motors is not None else self.motors.values() + + for motor in motors: + motor.wait(cond, timeout) + + def wait_until_not_moving(self, timeout=None, motors=None): + motors = motors if motors is not None else self.motors.values() + + for motor in motors: + motor.wait_until_not_moving(timeout) + + def wait_until(self, s, timeout=None, motors=None): + motors = motors if motors is not None else self.motors.values() + + for motor in motors: + motor.wait_until(s, timeout) + + def wait_while(self, s, timeout=None, motors=None): + motors = motors if motors is not None else self.motors.values() + + for motor in motors: + motor.wait_while(s, timeout) + + def _block(self): + self.wait_until('running', timeout=WAIT_RUNNING_TIMEOUT) + self.wait_until_not_moving() + + +# follow gyro angle classes +class FollowGyroAngleErrorTooFast(Exception): + """ + Raised when a gyro following robot has been asked to follow + an angle at an unrealistic speed + """ + pass + + +# line follower classes +class LineFollowErrorLostLine(Exception): + """ + Raised when a line following robot has lost the line + """ + pass + + +class LineFollowErrorTooFast(Exception): + """ + Raised when a line following robot has been asked to follow + a line at an unrealistic speed + """ + pass + + +# line follower functions +def follow_for_forever(tank): + """ + ``tank``: the MoveTank object that is following a line + """ + return True + + +def follow_for_ms(tank, ms): + """ + ``tank``: the MoveTank object that is following a line + ``ms`` : the number of milliseconds to follow the line + """ + if not hasattr(tank, 'stopwatch') or tank.stopwatch is None: + tank.stopwatch = StopWatch() + tank.stopwatch.start() + + if tank.stopwatch.value_ms >= ms: + tank.stopwatch = None + return False + else: + return True + + +class MoveTank(MotorSet): + """ + Controls a pair of motors simultaneously, via individual speed setpoints for each motor. + + Example: + + .. code:: python + + tank_drive = MoveTank(OUTPUT_A, OUTPUT_B) + # drive in a turn for 10 rotations of the outer motor + tank_drive.on_for_rotations(50, 75, 10) + """ + def __init__(self, left_motor_port, right_motor_port, desc=None, motor_class=LargeMotor): + motor_specs = { + left_motor_port: motor_class, + right_motor_port: motor_class, + } + + MotorSet.__init__(self, motor_specs, desc) + self.left_motor = self.motors[left_motor_port] + self.right_motor = self.motors[right_motor_port] + self.max_speed = self.left_motor.max_speed + self._cs = None + self._gyro = None + + # color sensor used by follow_line() + @property + def cs(self): + return self._cs + + @cs.setter + def cs(self, cs): + self._cs = cs + + # gyro sensor used by follow_gyro_angle() + @property + def gyro(self): + return self._gyro + + @gyro.setter + def gyro(self, gyro): + self._gyro = gyro + + def _unpack_speeds_to_native_units(self, left_speed, right_speed): + left_speed = self.left_motor._speed_native_units(left_speed, "left_speed") + right_speed = self.right_motor._speed_native_units(right_speed, "right_speed") + + return (left_speed, right_speed) + + def on_for_degrees(self, left_speed, right_speed, degrees, brake=True, block=True): + """ + Rotate the motors at 'left_speed & right_speed' for 'degrees'. Speeds + can be percentages or any SpeedValue implementation. + + If the left speed is not equal to the right speed (i.e., the robot will + turn), the motor on the outside of the turn will rotate for the full + ``degrees`` while the motor on the inside will have its requested + distance calculated according to the expected turn. + """ + (left_speed_native_units, + right_speed_native_units) = self._unpack_speeds_to_native_units(left_speed, right_speed) + + # proof of the following distance calculation: consider the circle formed by each wheel's path + # v_l = d_l/t, v_r = d_r/t + # therefore, t = d_l/v_l = d_r/v_r + + if degrees == 0 or (left_speed_native_units == 0 and right_speed_native_units == 0): + left_degrees = degrees + right_degrees = degrees + + # larger speed by magnitude is the "outer" wheel, and rotates the full "degrees" + elif abs(left_speed_native_units) > abs(right_speed_native_units): + left_degrees = degrees + right_degrees = abs(right_speed_native_units / left_speed_native_units) * degrees + + else: + left_degrees = abs(left_speed_native_units / right_speed_native_units) * degrees + right_degrees = degrees + + # Set all parameters + self.left_motor._set_rel_position_degrees_and_speed_sp(left_degrees, left_speed_native_units) + self.left_motor._set_brake(brake) + self.right_motor._set_rel_position_degrees_and_speed_sp(right_degrees, right_speed_native_units) + self.right_motor._set_brake(brake) + + # Start the motors + self.left_motor.run_to_rel_pos() + self.right_motor.run_to_rel_pos() + + if block: + self._block() + + def on_for_rotations(self, left_speed, right_speed, rotations, brake=True, block=True): + """ + Rotate the motors at 'left_speed & right_speed' for 'rotations'. Speeds + can be percentages or any SpeedValue implementation. + + If the left speed is not equal to the right speed (i.e., the robot will + turn), the motor on the outside of the turn will rotate for the full + ``rotations`` while the motor on the inside will have its requested + distance calculated according to the expected turn. + """ + MoveTank.on_for_degrees(self, left_speed, right_speed, rotations * 360, brake, block) + + def on_for_seconds(self, left_speed, right_speed, seconds, brake=True, block=True): + """ + Rotate the motors at 'left_speed & right_speed' for 'seconds'. Speeds + can be percentages or any SpeedValue implementation. + """ + + if seconds < 0: + raise ValueError("seconds is negative ({})".format(seconds)) + + (left_speed_native_units, + right_speed_native_units) = self._unpack_speeds_to_native_units(left_speed, right_speed) + + # Set all parameters + self.left_motor.speed_sp = int(round(left_speed_native_units)) + self.left_motor.time_sp = int(seconds * 1000) + self.left_motor._set_brake(brake) + self.right_motor.speed_sp = int(round(right_speed_native_units)) + self.right_motor.time_sp = int(seconds * 1000) + self.right_motor._set_brake(brake) + + log.debug("%s: on_for_seconds %ss at left-speed %s, right-speed %s" % (self, seconds, left_speed, right_speed)) + + # Start the motors + self.left_motor.run_timed() + self.right_motor.run_timed() + + if block: + self._block() + + def on(self, left_speed, right_speed): + """ + Start rotating the motors according to ``left_speed`` and ``right_speed`` forever. + Speeds can be percentages or any SpeedValue implementation. + """ + (left_speed_native_units, + right_speed_native_units) = self._unpack_speeds_to_native_units(left_speed, right_speed) + + # Set all parameters + self.left_motor.speed_sp = int(round(left_speed_native_units)) + self.right_motor.speed_sp = int(round(right_speed_native_units)) + + # Start the motors + self.left_motor.run_forever() + self.right_motor.run_forever() + + def follow_line(self, + kp, + ki, + kd, + speed, + target_light_intensity=None, + follow_left_edge=True, + white=60, + off_line_count_max=20, + sleep_time=0.01, + follow_for=follow_for_forever, + **kwargs): + """ + PID line follower + + ``kp``, ``ki``, and ``kd`` are the PID constants. + + ``speed`` is the desired speed of the midpoint of the robot + + ``target_light_intensity`` is the reflected light intensity when the color sensor + is on the edge of the line. If this is None we assume that the color sensor + is on the edge of the line and will take a reading to set this variable. + + ``follow_left_edge`` determines if we follow the left or right edge of the line + + ``white`` is the reflected_light_intensity that is used to determine if we have + lost the line + + ``off_line_count_max`` is how many consecutive times through the loop the + reflected_light_intensity must be greater than ``white`` before we + declare the line lost and raise an exception + + ``sleep_time`` is how many seconds we sleep on each pass through + the loop. This is to give the robot a chance to react + to the new motor settings. This should be something small such + as 0.01 (10ms). + + ``follow_for`` is called to determine if we should keep following the + line or stop. This function will be passed ``self`` (the current + ``MoveTank`` object). Current supported options are: + - ``follow_for_forever`` + - ``follow_for_ms`` + + ``**kwargs`` will be passed to the ``follow_for`` function + + Example: + + .. code:: python + + from ev3dev2.motor import OUTPUT_A, OUTPUT_B, MoveTank, SpeedPercent, follow_for_ms + from ev3dev2.sensor.lego import ColorSensor + + tank = MoveTank(OUTPUT_A, OUTPUT_B) + tank.cs = ColorSensor() + + try: + # Follow the line for 4500ms + tank.follow_line( + kp=11.3, ki=0.05, kd=3.2, + speed=SpeedPercent(30), + follow_for=follow_for_ms, + ms=4500 + ) + except LineFollowErrorTooFast: + tank.stop() + raise + """ + if not self._cs: + raise DeviceNotDefined( + "The 'cs' variable must be defined with a ColorSensor. Example: tank.cs = ColorSensor()") + + if target_light_intensity is None: + target_light_intensity = self._cs.reflected_light_intensity + + integral = 0.0 + last_error = 0.0 + derivative = 0.0 + off_line_count = 0 + speed = speed_to_speedvalue(speed) + speed_native_units = speed.to_native_units(self.left_motor) + + while follow_for(self, **kwargs): + reflected_light_intensity = self._cs.reflected_light_intensity + error = target_light_intensity - reflected_light_intensity + integral = integral + error + derivative = error - last_error + last_error = error + turn_native_units = (kp * error) + (ki * integral) + (kd * derivative) + + if not follow_left_edge: + turn_native_units *= -1 + + left_speed = SpeedNativeUnits(speed_native_units - turn_native_units) + right_speed = SpeedNativeUnits(speed_native_units + turn_native_units) + + # Have we lost the line? + if reflected_light_intensity >= white: + off_line_count += 1 + + if off_line_count >= off_line_count_max: + self.stop() + raise LineFollowErrorLostLine("we lost the line") + else: + off_line_count = 0 + + if sleep_time: + time.sleep(sleep_time) + + try: + self.on(left_speed, right_speed) + except SpeedInvalid as e: + log.exception(e) + self.stop() + raise LineFollowErrorTooFast("The robot is moving too fast to follow the line") + + self.stop() + + def follow_gyro_angle(self, + kp, + ki, + kd, + speed, + target_angle=0, + sleep_time=0.01, + follow_for=follow_for_forever, + **kwargs): + """ + PID gyro angle follower + + ``kp``, ``ki``, and ``kd`` are the PID constants. + + ``speed`` is the desired speed of the midpoint of the robot + + ``target_angle`` is the angle we want to maintain + + ``sleep_time`` is how many seconds we sleep on each pass through + the loop. This is to give the robot a chance to react + to the new motor settings. This should be something small such + as 0.01 (10ms). + + ``follow_for`` is called to determine if we should keep following the + desired angle or stop. This function will be passed ``self`` (the current + ``MoveTank`` object). Current supported options are: + - ``follow_for_forever`` + - ``follow_for_ms`` + + ``**kwargs`` will be passed to the ``follow_for`` function + + Example: + + .. code:: python + + from ev3dev2.motor import OUTPUT_A, OUTPUT_B, MoveTank, SpeedPercent, follow_for_ms + from ev3dev2.sensor.lego import GyroSensor + + # Instantiate the MoveTank object + tank = MoveTank(OUTPUT_A, OUTPUT_B) + + # Initialize the tank's gyro sensor + tank.gyro = GyroSensor() + + # Calibrate the gyro to eliminate drift, and to initialize the current angle as 0 + tank.gyro.calibrate() + + try: + + # Follow the target_angle for 4500ms + tank.follow_gyro_angle( + kp=11.3, ki=0.05, kd=3.2, + speed=SpeedPercent(30), + target_angle=0, + follow_for=follow_for_ms, + ms=4500 + ) + except FollowGyroAngleErrorTooFast: + tank.stop() + raise + """ + if not self._gyro: + raise DeviceNotDefined( + "The 'gyro' variable must be defined with a GyroSensor. Example: tank.gyro = GyroSensor()") + + integral = 0.0 + last_error = 0.0 + derivative = 0.0 + speed = speed_to_speedvalue(speed) + speed_native_units = speed.to_native_units(self.left_motor) + + while follow_for(self, **kwargs): + current_angle = self._gyro.angle + error = current_angle - target_angle + integral = integral + error + derivative = error - last_error + last_error = error + turn_native_units = (kp * error) + (ki * integral) + (kd * derivative) + + left_speed = SpeedNativeUnits(speed_native_units - turn_native_units) + right_speed = SpeedNativeUnits(speed_native_units + turn_native_units) + + if sleep_time: + time.sleep(sleep_time) + + try: + self.on(left_speed, right_speed) + except SpeedInvalid as e: + log.exception(e) + self.stop() + raise FollowGyroAngleErrorTooFast("The robot is moving too fast to follow the angle") + + self.stop() + + def turn_degrees(self, speed, target_angle, brake=True, error_margin=2, sleep_time=0.01): + """ + Use a GyroSensor to rotate in place for ``target_angle`` + + ``speed`` is the desired speed of the midpoint of the robot + + ``target_angle`` is the number of degrees we want to rotate + + ``brake`` hit the brakes once we reach ``target_angle`` + + ``error_margin`` is the +/- angle threshold to control how accurate the turn should be + + ``sleep_time`` is how many seconds we sleep on each pass through + the loop. This is to give the robot a chance to react + to the new motor settings. This should be something small such + as 0.01 (10ms). + + Rotate in place for ``target_degrees`` at ``speed`` + + Example: + + .. code:: python + + from ev3dev2.motor import OUTPUT_A, OUTPUT_B, MoveTank, SpeedPercent + from ev3dev2.sensor.lego import GyroSensor + + # Instantiate the MoveTank object + tank = MoveTank(OUTPUT_A, OUTPUT_B) + + # Initialize the tank's gyro sensor + tank.gyro = GyroSensor() + + # Calibrate the gyro to eliminate drift, and to initialize the current angle as 0 + tank.gyro.calibrate() + + # Pivot 30 degrees + tank.turn_degrees( + speed=SpeedPercent(5), + target_angle=30 + ) + """ + + # MoveTank does not have information on wheel size and distance (that is + # MoveDifferential) so we must use a GyroSensor to control how far we rotate. + if not self._gyro: + raise DeviceNotDefined( + "The 'gyro' variable must be defined with a GyroSensor. Example: tank.gyro = GyroSensor()") + + speed = speed_to_speedvalue(speed) + speed_native_units = speed.to_native_units(self.left_motor) + target_angle = self._gyro.angle + target_angle + + while True: + current_angle = self._gyro.angle + delta = abs(target_angle - current_angle) + + if delta <= error_margin: + self.stop(brake=brake) + break + + # we are left of our target, rotate clockwise + if current_angle < target_angle: + left_speed = SpeedNativeUnits(speed_native_units) + right_speed = SpeedNativeUnits(-1 * speed_native_units) + + # we are right of our target, rotate counter-clockwise + else: + left_speed = SpeedNativeUnits(-1 * speed_native_units) + right_speed = SpeedNativeUnits(speed_native_units) + + self.on(left_speed, right_speed) + + if sleep_time: + time.sleep(sleep_time) + + def turn_right(self, speed, degrees, brake=True, error_margin=2, sleep_time=0.01): + """ + Rotate clockwise ``degrees`` in place + """ + self.turn_degrees(speed, abs(degrees), brake, error_margin, sleep_time) + + def turn_left(self, speed, degrees, brake=True, error_margin=2, sleep_time=0.01): + """ + Rotate counter-clockwise ``degrees`` in place + """ + self.turn_degrees(speed, abs(degrees) * -1, brake, error_margin, sleep_time) + + +class MoveSteering(MoveTank): + """ + Controls a pair of motors simultaneously, via a single "steering" value and a speed. + + steering [-100, 100]: + * -100 means turn left on the spot (right motor at 100% forward, left motor at 100% backward), + * 0 means drive in a straight line, and + * 100 means turn right on the spot (left motor at 100% forward, right motor at 100% backward). + + "steering" can be any number between -100 and 100. + + Example: + + .. code:: python + + steering_drive = MoveSteering(OUTPUT_A, OUTPUT_B) + # drive in a turn for 10 rotations of the outer motor + steering_drive.on_for_rotations(-20, SpeedPercent(75), 10) + """ + def on_for_rotations(self, steering, speed, rotations, brake=True, block=True): + """ + Rotate the motors according to the provided ``steering``. + + The distance each motor will travel follows the rules of :meth:`MoveTank.on_for_rotations`. + """ + (left_speed, right_speed) = self.get_speed_steering(steering, speed) + MoveTank.on_for_rotations(self, SpeedNativeUnits(left_speed), SpeedNativeUnits(right_speed), rotations, brake, + block) + + def on_for_degrees(self, steering, speed, degrees, brake=True, block=True): + """ + Rotate the motors according to the provided ``steering``. + + The distance each motor will travel follows the rules of :meth:`MoveTank.on_for_degrees`. + """ + (left_speed, right_speed) = self.get_speed_steering(steering, speed) + MoveTank.on_for_degrees(self, SpeedNativeUnits(left_speed), SpeedNativeUnits(right_speed), degrees, brake, + block) + + def on_for_seconds(self, steering, speed, seconds, brake=True, block=True): + """ + Rotate the motors according to the provided ``steering`` for ``seconds``. + """ + (left_speed, right_speed) = self.get_speed_steering(steering, speed) + MoveTank.on_for_seconds(self, SpeedNativeUnits(left_speed), SpeedNativeUnits(right_speed), seconds, brake, + block) + + def on(self, steering, speed): + """ + Start rotating the motors according to the provided ``steering`` and + ``speed`` forever. + """ + (left_speed, right_speed) = self.get_speed_steering(steering, speed) + MoveTank.on(self, SpeedNativeUnits(left_speed), SpeedNativeUnits(right_speed)) + + def get_speed_steering(self, steering, speed): + """ + Calculate the speed_sp for each motor in a pair to achieve the specified + steering. Note that calling this function alone will not make the + motors move, it only calculates the speed. A run_* function must be called + afterwards to make the motors move. + + steering [-100, 100]: + * -100 means turn left on the spot (right motor at 100% forward, left motor at 100% backward), + * 0 means drive in a straight line, and + * 100 means turn right on the spot (left motor at 100% forward, right motor at 100% backward). + + speed: + The speed that should be applied to the outmost motor (the one + rotating faster). The speed of the other motor will be computed + automatically. + """ + + assert steering >= -100 and steering <= 100,\ + "{} is an invalid steering, must be between -100 and 100 (inclusive)".format(steering) + + # We don't have a good way to make this generic for the pair... so we + # assume that the left motor's speed stats are the same as the right + # motor's. + speed = self.left_motor._speed_native_units(speed) + left_speed = speed + right_speed = speed + speed_factor = (50 - abs(float(steering))) / 50 + + if steering >= 0: + right_speed *= speed_factor + else: + left_speed *= speed_factor + + return (left_speed, right_speed) + + +class MoveDifferential(MoveTank): + """ + MoveDifferential is a child of MoveTank that adds the following capabilities: + + - drive in a straight line for a specified distance + + - rotate in place in a circle (clockwise or counter clockwise) for a + specified number of degrees + + - drive in an arc (clockwise or counter clockwise) of a specified radius + for a specified distance + + Odometry can be use to enable driving to specific coordinates and + rotating to a specific angle. + + New arguments: + + wheel_class - Typically a child class of :class:`ev3dev2.wheel.Wheel`. This is used to + get the circumference of the wheels of the robot. The circumference is + needed for several calculations in this class. + + wheel_distance_mm - The distance between the mid point of the two + wheels of the robot. You may need to do some test drives to find + the correct value for your robot. It is not as simple as measuring + the distance between the midpoints of the two wheels. The weight of + the robot, center of gravity, etc come into play. + + You can use utils/move_differential.py to call on_arc_left() to do + some test drives of circles with a radius of 200mm. Adjust your + wheel_distance_mm until your robot can drive in a perfect circle + and stop exactly where it started. It does not have to be a circle + with a radius of 200mm, you can test with any size circle but you do + not want it to be too small or it will be difficult to test small + adjustments to wheel_distance_mm. + + Example: + + .. code:: python + + from ev3dev2.motor import OUTPUT_A, OUTPUT_B, MoveDifferential, SpeedRPM + from ev3dev2.wheel import EV3Tire + + STUD_MM = 8 + + # test with a robot that: + # - uses the standard wheels known as EV3Tire + # - wheels are 16 studs apart + mdiff = MoveDifferential(OUTPUT_A, OUTPUT_B, EV3Tire, 16 * STUD_MM) + + # Rotate 90 degrees clockwise + mdiff.turn_right(SpeedRPM(40), 90) + + # Drive forward 500 mm + mdiff.on_for_distance(SpeedRPM(40), 500) + + # Drive in arc to the right along an imaginary circle of radius 150 mm. + # Drive for 700 mm around this imaginary circle. + mdiff.on_arc_right(SpeedRPM(80), 150, 700) + + # Enable odometry + mdiff.odometry_start() + + # Use odometry to drive to specific coordinates + mdiff.on_to_coordinates(SpeedRPM(40), 300, 300) + + # Use odometry to go back to where we started + mdiff.on_to_coordinates(SpeedRPM(40), 0, 0) + + # Use odometry to rotate in place to 90 degrees + mdiff.turn_to_angle(SpeedRPM(40), 90) + + # Disable odometry + mdiff.odometry_stop() + """ + def __init__(self, + left_motor_port, + right_motor_port, + wheel_class, + wheel_distance_mm, + desc=None, + motor_class=LargeMotor): + + MoveTank.__init__(self, left_motor_port, right_motor_port, desc, motor_class) + self.wheel = wheel_class() + self.wheel_distance_mm = wheel_distance_mm + + # The circumference of the circle made if this robot were to rotate in place + self.circumference_mm = self.wheel_distance_mm * math.pi + + self.min_circle_radius_mm = self.wheel_distance_mm / 2 + + # odometry variables + self.x_pos_mm = 0.0 # robot X position in mm + self.y_pos_mm = 0.0 # robot Y position in mm + self.odometry_thread_run = False + self.theta = 0.0 + + def on_for_distance(self, speed, distance_mm, brake=True, block=True): + """ + Drive in a straight line for ``distance_mm`` + """ + rotations = distance_mm / self.wheel.circumference_mm + log.debug("%s: on_for_rotations distance_mm %s, rotations %s, speed %s" % (self, distance_mm, rotations, speed)) + + MoveTank.on_for_rotations(self, speed, speed, rotations, brake, block) + + def _on_arc(self, speed, radius_mm, distance_mm, brake, block, arc_right): + """ + Drive in a circle with 'radius' for 'distance' + """ + + if radius_mm < self.min_circle_radius_mm: + raise ValueError("{}: radius_mm {} is less than min_circle_radius_mm {}".format( + self, radius_mm, self.min_circle_radius_mm)) + + # The circle formed at the halfway point between the two wheels is the + # circle that must have a radius of radius_mm + circle_outer_mm = 2 * math.pi * (radius_mm + (self.wheel_distance_mm / 2)) + circle_middle_mm = 2 * math.pi * radius_mm + circle_inner_mm = 2 * math.pi * (radius_mm - (self.wheel_distance_mm / 2)) + + if arc_right: + # The left wheel is making the larger circle and will move at 'speed' + # The right wheel is making a smaller circle so its speed will be a fraction of the left motor's speed + left_speed = speed + right_speed = float(circle_inner_mm / circle_outer_mm) * left_speed + + else: + # The right wheel is making the larger circle and will move at 'speed' + # The left wheel is making a smaller circle so its speed will be a fraction of the right motor's speed + right_speed = speed + left_speed = float(circle_inner_mm / circle_outer_mm) * right_speed + + log.debug("%s: arc %s, radius %s, distance %s, left-speed %s, right-speed %s" % + (self, "right" if arc_right else "left", radius_mm, distance_mm, left_speed, right_speed)) + log.debug("%s: circle_outer_mm %s, circle_middle_mm %s, circle_inner_mm %s" % + (self, circle_outer_mm, circle_middle_mm, circle_inner_mm)) + + # We know we want the middle circle to be of length distance_mm so + # calculate the percentage of circle_middle_mm we must travel for the + # middle of the robot to travel distance_mm. + circle_middle_percentage = float(distance_mm / circle_middle_mm) + + # Now multiple that percentage by circle_outer_mm to calculate how + # many mm the outer wheel should travel. + circle_outer_final_mm = circle_middle_percentage * circle_outer_mm + + outer_wheel_rotations = float(circle_outer_final_mm / self.wheel.circumference_mm) + outer_wheel_degrees = outer_wheel_rotations * 360 + + log.debug("%s: arc %s, circle_middle_percentage %s, circle_outer_final_mm %s, " % + (self, "right" if arc_right else "left", circle_middle_percentage, circle_outer_final_mm)) + log.debug("%s: outer_wheel_rotations %s, outer_wheel_degrees %s" % + (self, outer_wheel_rotations, outer_wheel_degrees)) + + MoveTank.on_for_degrees(self, left_speed, right_speed, outer_wheel_degrees, brake, block) + + def on_arc_right(self, speed, radius_mm, distance_mm, brake=True, block=True): + """ + Drive clockwise in a circle with 'radius_mm' for 'distance_mm' + """ + self._on_arc(speed, radius_mm, distance_mm, brake, block, True) + + def on_arc_left(self, speed, radius_mm, distance_mm, brake=True, block=True): + """ + Drive counter-clockwise in a circle with 'radius_mm' for 'distance_mm' + """ + self._on_arc(speed, radius_mm, distance_mm, brake, block, False) + + def turn_degrees(self, speed, degrees, brake=True, block=True, error_margin=2, use_gyro=False): + """ + Rotate in place ``degrees``. Both wheels must turn at the same speed for us + to rotate in place. If the following conditions are met the GryoSensor will + be used to improve the accuracy of our turn: + - ``use_gyro``, ``brake`` and ``block`` are all True + - A GyroSensor has been defined via ``self.gyro = GyroSensor()`` + """ + def final_angle(init_angle, degrees): + result = init_angle - degrees + + while result <= -360: + result += 360 + + while result >= 360: + result -= 360 + + if result < 0: + result += 360 + + return result + + # use the gyro to check that we turned the correct amount? + use_gyro = bool(use_gyro and block and brake) + if use_gyro and not self._gyro: + raise DeviceNotDefined( + "The 'gyro' variable must be defined with a GyroSensor. Example: tank.gyro = GyroSensor()") + + if use_gyro: + angle_init_degrees = self._gyro.circle_angle() + else: + angle_init_degrees = math.degrees(self.theta) + + angle_target_degrees = final_angle(angle_init_degrees, degrees) + + log.info("%s: turn_degrees() %d degrees from %s to %s" % + (self, degrees, angle_init_degrees, angle_target_degrees)) + + # The distance each wheel needs to travel + distance_mm = (abs(degrees) / 360) * self.circumference_mm + + # The number of rotations to move distance_mm + rotations = distance_mm / self.wheel.circumference_mm + + # If degrees is positive rotate clockwise + if degrees > 0: + MoveTank.on_for_rotations(self, speed, speed * -1, rotations, brake, block) + + # If degrees is negative rotate counter-clockwise + else: + MoveTank.on_for_rotations(self, speed * -1, speed, rotations, brake, block) + + if use_gyro: + angle_current_degrees = self._gyro.circle_angle() + + # This can happen if we are aiming for 2 degrees and overrotate to 358 degrees + # We need to rotate counter-clockwise + if 90 >= angle_target_degrees >= 0 and 270 <= angle_current_degrees <= 360: + degrees_error = (angle_target_degrees + (360 - angle_current_degrees)) * -1 + + # This can happen if we are aiming for 358 degrees and overrotate to 2 degrees + # We need to rotate clockwise + elif 360 >= angle_target_degrees >= 270 and 0 <= angle_current_degrees <= 90: + degrees_error = angle_current_degrees + (360 - angle_target_degrees) + + # We need to rotate clockwise + elif angle_current_degrees > angle_target_degrees: + degrees_error = angle_current_degrees - angle_target_degrees + + # We need to rotate counter-clockwise + else: + degrees_error = (angle_target_degrees - angle_current_degrees) * -1 + + log.info("%s: turn_degrees() ended up at %s, error %s, error_margin %s" % + (self, angle_current_degrees, degrees_error, error_margin)) + + if abs(degrees_error) > error_margin: + self.turn_degrees(speed, degrees_error, brake, block, error_margin, use_gyro) + + def turn_right(self, speed, degrees, brake=True, block=True, error_margin=2, use_gyro=False): + """ + Rotate clockwise ``degrees`` in place + """ + self.turn_degrees(speed, abs(degrees), brake, block, error_margin, use_gyro) + + def turn_left(self, speed, degrees, brake=True, block=True, error_margin=2, use_gyro=False): + """ + Rotate counter-clockwise ``degrees`` in place + """ + self.turn_degrees(speed, abs(degrees) * -1, brake, block, error_margin, use_gyro) + + def turn_to_angle(self, speed, angle_target_degrees, brake=True, block=True, error_margin=2, use_gyro=False): + """ + Rotate in place to ``angle_target_degrees`` at ``speed`` + """ + if not self.odometry_thread_run: + raise ThreadNotRunning("odometry_start() must be called to track robot coordinates") + + # Make both target and current angles positive numbers between 0 and 360 + while angle_target_degrees < 0: + angle_target_degrees += 360 + + angle_current_degrees = math.degrees(self.theta) + + while angle_current_degrees < 0: + angle_current_degrees += 360 + + # Is it shorter to rotate to the right or left + # to reach angle_target_degrees? + if angle_current_degrees > angle_target_degrees: + turn_right = True + angle_delta = angle_current_degrees - angle_target_degrees + else: + turn_right = False + angle_delta = angle_target_degrees - angle_current_degrees + + if angle_delta > 180: + angle_delta = 360 - angle_delta + turn_right = not turn_right + + log.debug("%s: turn_to_angle %s, current angle %s, delta %s, turn_right %s" % + (self, angle_target_degrees, angle_current_degrees, angle_delta, turn_right)) + self.odometry_coordinates_log() + + if turn_right: + self.turn_degrees(speed, abs(angle_delta), brake, block, error_margin, use_gyro) + else: + self.turn_degrees(speed, abs(angle_delta) * -1, brake, block, error_margin, use_gyro) + + self.odometry_coordinates_log() + + def odometry_coordinates_log(self): + log.debug("%s: odometry angle %s at (%d, %d)" % (self, math.degrees(self.theta), self.x_pos_mm, self.y_pos_mm)) + + def odometry_start(self, theta_degrees_start=90.0, x_pos_start=0.0, y_pos_start=0.0, sleep_time=0.005): # 5ms + """ + Ported from: + http://seattlerobotics.org/encoder/200610/Article3/IMU%20Odometry,%20by%20David%20Anderson.htm + + A thread is started that will run until the user calls odometry_stop() + which will set odometry_thread_run to False + """ + def _odometry_monitor(): + left_previous = 0 + right_previous = 0 + self.theta = math.radians(theta_degrees_start) # robot heading + self.x_pos_mm = x_pos_start # robot X position in mm + self.y_pos_mm = y_pos_start # robot Y position in mm + TWO_PI = 2 * math.pi + self.odometry_thread_run = True + + while self.odometry_thread_run: + + # sample the left and right encoder counts as close together + # in time as possible + left_current = self.left_motor.position + right_current = self.right_motor.position + + # determine how many ticks since our last sampling + left_ticks = left_current - left_previous + right_ticks = right_current - right_previous + + # Have we moved? + if not left_ticks and not right_ticks: + if sleep_time: + time.sleep(sleep_time) + continue + + # update _previous for next time + left_previous = left_current + right_previous = right_current + + # rotations = distance_mm/self.wheel.circumference_mm + left_rotations = float(left_ticks / self.left_motor.count_per_rot) + right_rotations = float(right_ticks / self.right_motor.count_per_rot) + + # convert longs to floats and ticks to mm + left_mm = float(left_rotations * self.wheel.circumference_mm) + right_mm = float(right_rotations * self.wheel.circumference_mm) + + # calculate distance we have traveled since last sampling + mm = (left_mm + right_mm) / 2.0 + + # accumulate total rotation around our center + self.theta += (right_mm - left_mm) / self.wheel_distance_mm + + # and clip the rotation to plus or minus 360 degrees + self.theta -= float(int(self.theta / TWO_PI) * TWO_PI) + + # now calculate and accumulate our position in mm + self.x_pos_mm += mm * math.cos(self.theta) + self.y_pos_mm += mm * math.sin(self.theta) + + if sleep_time: + time.sleep(sleep_time) + + _thread.start_new_thread(_odometry_monitor, ()) + + # Block until the thread has started doing work + while not self.odometry_thread_run: + pass + + def odometry_stop(self): + """ + Signal the odometry thread to exit + """ + + if self.odometry_thread_run: + self.odometry_thread_run = False + + def on_to_coordinates(self, speed, x_target_mm, y_target_mm, brake=True, block=True): + """ + Drive to (``x_target_mm``, ``y_target_mm``) coordinates at ``speed`` + """ + if not self.odometry_thread_run: + raise ThreadNotRunning("odometry_start() must be called to track robot coordinates") + + # stop moving + self.off(brake='hold') + + # rotate in place so we are pointed straight at our target + x_delta = x_target_mm - self.x_pos_mm + y_delta = y_target_mm - self.y_pos_mm + angle_target_radians = math.atan2(y_delta, x_delta) + angle_target_degrees = math.degrees(angle_target_radians) + self.turn_to_angle(speed, angle_target_degrees, brake=True, block=True) + + # drive in a straight line to the target coordinates + distance_mm = math.sqrt(pow(self.x_pos_mm - x_target_mm, 2) + pow(self.y_pos_mm - y_target_mm, 2)) + self.on_for_distance(speed, distance_mm, brake, block) + + +class MoveJoystick(MoveTank): + """ + Used to control a pair of motors via a single joystick vector. + """ + def on(self, x, y, radius=100.0): + """ + Convert ``x``,``y`` joystick coordinates to left/right motor speed percentages + and move the motors. + + This will use a classic "arcade drive" algorithm: a full-forward joystick + goes straight forward and likewise for full-backward. Pushing the joystick + all the way to one side will make it turn on the spot in that direction. + Positions in the middle will control how fast the vehicle moves and how + sharply it turns. + + ``x``, ``y``: + The X and Y coordinates of the joystick's position, with + (0,0) representing the center position. X is horizontal and Y is vertical. + + ``radius`` (default 100): + The radius of the joystick, controlling the range of the input (x, y) values. + e.g. if "x" and "y" can be between -1 and 1, radius should be set to "1". + """ + + # If joystick is in the middle stop the tank + if not x and not y: + self.off() + return + + vector_length = math.sqrt((x * x) + (y * y)) + angle = math.degrees(math.atan2(y, x)) + + if angle < 0: + angle += 360 + + # Should not happen but can happen (just by a hair) due to floating point math + if vector_length > radius: + vector_length = radius + + (init_left_speed_percentage, init_right_speed_percentage) = MoveJoystick.angle_to_speed_percentage(angle) + + # scale the speed percentages based on vector_length vs. radius + left_speed_percentage = (init_left_speed_percentage * vector_length) / radius + right_speed_percentage = (init_right_speed_percentage * vector_length) / radius + + # log.debug(""" + # x, y : %s, %s + # radius : %s + # angle : %s + # vector length : %s + # init left_speed_percentage : %s + # init right_speed_percentage : %s + # final left_speed_percentage : %s + # final right_speed_percentage : %s + # """ % (x, y, radius, angle, vector_length, + # init_left_speed_percentage, init_right_speed_percentage, + # left_speed_percentage, right_speed_percentage)) + + MoveTank.on(self, SpeedPercent(left_speed_percentage), SpeedPercent(right_speed_percentage)) + + @staticmethod + def angle_to_speed_percentage(angle): + """ + The following graphic illustrates the **motor power outputs** for the + left and right motors based on where the joystick is pointing, of the + form ``(left power, right power)``:: + + (1, 1) + . . . . . . . + . | . + . | . + (0, 1) . | . (1, 0) + . | . + . | . + . | . + . | . + . | . + . | x-axis . + (-1, 1) .---------------------------------------. (1, -1) + . | . + . | . + . | . + . | y-axis . + . | . + (0, -1) . | . (-1, 0) + . | . + . | . + . . . . . . . + (-1, -1) + + + The joystick is a circle within a circle where the (x, y) coordinates + of the joystick form an angle with the x-axis. Our job is to translate + this angle into the percentage of power that should be sent to each motor. + For instance if the joystick is moved all the way to the top of the circle + we want both motors to move forward with 100% power...that is represented + above by (1, 1). If the joystick is moved all the way to the right side of + the circle we want to rotate clockwise so we move the left motor forward 100% + and the right motor backwards 100%...so (1, -1). If the joystick is at + 45 degrees then we move apply (1, 0) to move the left motor forward 100% and + the right motor stays still. + + The 8 points shown above are pretty easy. For the points in between those 8 + we do some math to figure out what the percentages should be. Take 11.25 degrees + for example. We look at how the motors transition from 0 degrees to 45 degrees: + - the left motor is 1 so that is easy + - the right motor moves from -1 to 0 + + We determine how far we are between 0 and 45 degrees (11.25 is 25% of 45) so we + know that the right motor should be 25% of the way from -1 to 0...so -0.75 is the + percentage for the right motor at 11.25 degrees. + """ + + if 0 <= angle <= 45: + + # left motor stays at 1 + left_speed_percentage = 1 + + # right motor transitions from -1 to 0 + right_speed_percentage = -1 + (angle / 45.0) + + elif 45 < angle <= 90: + + # left motor stays at 1 + left_speed_percentage = 1 + + # right motor transitions from 0 to 1 + percentage_from_45_to_90 = (angle - 45) / 45.0 + right_speed_percentage = percentage_from_45_to_90 + + elif 90 < angle <= 135: + + # left motor transitions from 1 to 0 + percentage_from_90_to_135 = (angle - 90) / 45.0 + left_speed_percentage = 1 - percentage_from_90_to_135 + + # right motor stays at 1 + right_speed_percentage = 1 + + elif 135 < angle <= 180: + + # left motor transitions from 0 to -1 + percentage_from_135_to_180 = (angle - 135) / 45.0 + left_speed_percentage = -1 * percentage_from_135_to_180 + + # right motor stays at 1 + right_speed_percentage = 1 + + elif 180 < angle <= 225: + + # left motor transitions from -1 to 0 + percentage_from_180_to_225 = (angle - 180) / 45.0 + left_speed_percentage = -1 + percentage_from_180_to_225 + + # right motor transitions from 1 to -1 + # right motor transitions from 1 to 0 between 180 and 202.5 + if angle < 202.5: + percentage_from_180_to_202 = (angle - 180) / 22.5 + right_speed_percentage = 1 - percentage_from_180_to_202 + + # right motor is 0 at 202.5 + elif angle == 202.5: + right_speed_percentage = 0 + + # right motor transitions from 0 to -1 between 202.5 and 225 + else: + percentage_from_202_to_225 = (angle - 202.5) / 22.5 + right_speed_percentage = -1 * percentage_from_202_to_225 + + elif 225 < angle <= 270: + + # left motor transitions from 0 to -1 + percentage_from_225_to_270 = (angle - 225) / 45.0 + left_speed_percentage = -1 * percentage_from_225_to_270 + + # right motor stays at -1 + right_speed_percentage = -1 + + elif 270 < angle <= 315: + + # left motor stays at -1 + left_speed_percentage = -1 + + # right motor transitions from -1 to 0 + percentage_from_270_to_315 = (angle - 270) / 45.0 + right_speed_percentage = -1 + percentage_from_270_to_315 + + elif 315 < angle <= 360: + + # left motor transitions from -1 to 1 + # left motor transitions from -1 to 0 between 315 and 337.5 + if angle < 337.5: + percentage_from_315_to_337 = (angle - 315) / 22.5 + left_speed_percentage = (1 - percentage_from_315_to_337) * -1 + + # left motor is 0 at 337.5 + elif angle == 337.5: + left_speed_percentage = 0 + + # left motor transitions from 0 to 1 between 337.5 and 360 + elif angle > 337.5: + percentage_from_337_to_360 = (angle - 337.5) / 22.5 + left_speed_percentage = percentage_from_337_to_360 + + # right motor transitions from 0 to -1 + percentage_from_315_to_360 = (angle - 315) / 45.0 + right_speed_percentage = -1 * percentage_from_315_to_360 + + else: + raise Exception( + 'You created a circle with more than 360 degrees ({})...that is quite the trick'.format(angle)) + + return (left_speed_percentage * 100, right_speed_percentage * 100) diff --git a/ev3dev2/port.py b/ev3dev2/port.py new file mode 100644 index 0000000..264e37b --- /dev/null +++ b/ev3dev2/port.py @@ -0,0 +1,154 @@ +# ----------------------------------------------------------------------------- +# Copyright (c) 2015 Ralph Hempel +# Copyright (c) 2015 Anton Vanhoucke +# Copyright (c) 2015 Denis Demidov +# Copyright (c) 2015 Eric Pascual +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +# ----------------------------------------------------------------------------- + +import sys +from . import Device + +if sys.version_info < (3, 4): + raise SystemError('Must be using Python 3.4 or higher') + + +class LegoPort(Device): + """ + The ``lego-port`` class provides an interface for working with input and + output ports that are compatible with LEGO MINDSTORMS RCX/NXT/EV3, LEGO + WeDo and LEGO Power Functions sensors and motors. Supported devices include + the LEGO MINDSTORMS EV3 Intelligent Brick, the LEGO WeDo USB hub and + various sensor multiplexers from 3rd party manufacturers. + + See the following example for using this class to configure sensors: + https://github.com/ev3dev/ev3dev-lang-python-demo/blob/stretch/platform/brickpi3-motor-and-sensor.py + + Some types of ports may have multiple modes of operation. For example, the + input ports on the EV3 brick can communicate with sensors using UART, I2C + or analog validate signals - but not all at the same time. Therefore there + are multiple modes available to connect to the different types of sensors. + + In most cases, ports are able to automatically detect what type of sensor + or motor is connected. In some cases though, this must be manually specified + using the ``mode`` and ``set_device`` attributes. The ``mode`` attribute affects + how the port communicates with the connected device. For example the input + ports on the EV3 brick can communicate using UART, I2C or analog voltages, + but not all at the same time, so the mode must be set to the one that is + appropriate for the connected sensor. The ``set_device`` attribute is used to + specify the exact type of sensor that is connected. Note: the mode must be + correctly set before setting the sensor type. + + Ports can be found at ``/sys/class/lego-port/port`` where ```` is + incremented each time a new port is registered. Note: The number is not + related to the actual port at all - use the ``address`` attribute to find + a specific port. + """ + + SYSTEM_CLASS_NAME = 'lego-port' + SYSTEM_DEVICE_NAME_CONVENTION = '*' + __slots__ = [ + '_address', + '_driver_name', + '_modes', + '_mode', + '_set_device', + '_status', + ] + + def __init__(self, address=None, name_pattern=SYSTEM_DEVICE_NAME_CONVENTION, name_exact=False, **kwargs): + + if address is not None: + kwargs['address'] = address + super(LegoPort, self).__init__(self.SYSTEM_CLASS_NAME, name_pattern, name_exact, **kwargs) + + self._address = None + self._driver_name = None + self._modes = None + self._mode = None + self._set_device = None + self._status = None + + @property + def address(self): + """ + Returns the name of the port. See individual driver documentation for + the name that will be returned. + """ + self._address, value = self.get_attr_string(self._address, 'address') + return value + + @property + def driver_name(self): + """ + Returns the name of the driver that loaded this device. You can find the + complete list of drivers in the [list of port drivers]. + """ + (self._driver_name, value) = self.get_cached_attr_string(self._driver_name, 'driver_name') + return value + + @property + def modes(self): + """ + Returns a list of the available modes of the port. + """ + (self._modes, value) = self.get_cached_attr_set(self._modes, 'modes') + return value + + @property + def mode(self): + """ + Reading returns the currently selected mode. Writing sets the mode. + Generally speaking when the mode changes any sensor or motor devices + associated with the port will be removed new ones loaded, however this + this will depend on the individual driver implementing this class. + """ + self._mode, value = self.get_attr_string(self._mode, 'mode') + return value + + @mode.setter + def mode(self, value): + self._mode = self.set_attr_string(self._mode, 'mode', value) + + @property + def set_device(self): + """ + For modes that support it, writing the name of a driver will cause a new + device to be registered for that driver and attached to this port. For + example, since NXT/Analog sensors cannot be auto-detected, you must use + this attribute to load the correct driver. Returns -EOPNOTSUPP if setting a + device is not supported. + """ + raise Exception("set_device is a write-only property!") + + @set_device.setter + def set_device(self, value): + self._set_device = self.set_attr_string(self._set_device, 'set_device', value) + + @property + def status(self): + """ + In most cases, reading status will return the same value as ``mode``. In + cases where there is an ``auto`` mode additional values may be returned, + such as ``no-device`` or ``error``. See individual port driver documentation + for the full list of possible values. + """ + self._status, value = self.get_attr_string(self._status, 'status') + return value diff --git a/ev3dev2/power.py b/ev3dev2/power.py new file mode 100644 index 0000000..d33c38f --- /dev/null +++ b/ev3dev2/power.py @@ -0,0 +1,111 @@ +# ----------------------------------------------------------------------------- +# Copyright (c) 2015 Ralph Hempel +# Copyright (c) 2015 Anton Vanhoucke +# Copyright (c) 2015 Denis Demidov +# Copyright (c) 2015 Eric Pascual +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +# ----------------------------------------------------------------------------- + +import sys +from ev3dev2 import Device + +if sys.version_info < (3, 4): + raise SystemError('Must be using Python 3.4 or higher') + + +class PowerSupply(Device): + """ + A generic interface to read data from the system's power_supply class. + Uses the built-in legoev3-battery if none is specified. + """ + + SYSTEM_CLASS_NAME = 'power_supply' + SYSTEM_DEVICE_NAME_CONVENTION = '*' + __slots__ = [ + '_measured_current', + '_measured_voltage', + '_max_voltage', + '_min_voltage', + '_technology', + '_type', + ] + + def __init__(self, address=None, name_pattern=SYSTEM_DEVICE_NAME_CONVENTION, name_exact=False, **kwargs): + + if address is not None: + kwargs['address'] = address + super(PowerSupply, self).__init__(self.SYSTEM_CLASS_NAME, name_pattern, name_exact, **kwargs) + + self._measured_current = None + self._measured_voltage = None + self._max_voltage = None + self._min_voltage = None + self._technology = None + self._type = None + + @property + def measured_current(self): + """ + The measured current that the battery is supplying (in microamps) + """ + self._measured_current, value = self.get_attr_int(self._measured_current, 'current_now') + return value + + @property + def measured_voltage(self): + """ + The measured voltage that the battery is supplying (in microvolts) + """ + self._measured_voltage, value = self.get_attr_int(self._measured_voltage, 'voltage_now') + return value + + @property + def max_voltage(self): + self._max_voltage, value = self.get_attr_int(self._max_voltage, 'voltage_max_design') + return value + + @property + def min_voltage(self): + self._min_voltage, value = self.get_attr_int(self._min_voltage, 'voltage_min_design') + return value + + @property + def technology(self): + self._technology, value = self.get_attr_string(self._technology, 'technology') + return value + + @property + def type(self): + self._type, value = self.get_attr_string(self._type, 'type') + return value + + @property + def measured_amps(self): + """ + The measured current that the battery is supplying (in amps) + """ + return self.measured_current / 1e6 + + @property + def measured_volts(self): + """ + The measured voltage that the battery is supplying (in volts) + """ + return self.measured_voltage / 1e6 diff --git a/ev3dev2/sensor/__init__.py b/ev3dev2/sensor/__init__.py new file mode 100644 index 0000000..fc63dca --- /dev/null +++ b/ev3dev2/sensor/__init__.py @@ -0,0 +1,321 @@ +# ----------------------------------------------------------------------------- +# Copyright (c) 2015 Ralph Hempel +# Copyright (c) 2015 Anton Vanhoucke +# Copyright (c) 2015 Denis Demidov +# Copyright (c) 2015 Eric Pascual +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +# ----------------------------------------------------------------------------- + +import sys +from os.path import abspath +from struct import unpack +from ev3dev2 import get_current_platform, Device, list_device_names + +# INPUT ports have platform specific values that we must import +platform = get_current_platform() + +if platform == 'ev3': + from ev3dev2._platform.ev3 import INPUT_1, INPUT_2, INPUT_3, INPUT_4 # noqa: F401 + +elif platform == 'evb': + from ev3dev2._platform.evb import INPUT_1, INPUT_2, INPUT_3, INPUT_4 # noqa: F401 + +elif platform == 'pistorms': + from ev3dev2._platform.pistorms import INPUT_1, INPUT_2, INPUT_3, INPUT_4 # noqa: F401 + +elif platform == 'brickpi': + from ev3dev2._platform.brickpi import INPUT_1, INPUT_2, INPUT_3, INPUT_4 # noqa: F401 + +elif platform == 'brickpi3': + from ev3dev2._platform.brickpi3 import ( # noqa: F401 + INPUT_1, INPUT_2, INPUT_3, INPUT_4, INPUT_5, INPUT_6, INPUT_7, INPUT_8, INPUT_9, INPUT_10, INPUT_11, INPUT_12, + INPUT_13, INPUT_14, INPUT_15, INPUT_16) + +elif platform == 'fake': + from ev3dev2._platform.fake import INPUT_1, INPUT_2, INPUT_3, INPUT_4 # noqa: F401 + +else: + raise Exception("Unsupported platform '%s'" % platform) + +if sys.version_info < (3, 4): + raise SystemError('Must be using Python 3.4 or higher') + + +class Sensor(Device): + """ + The sensor class provides a uniform interface for using most of the + sensors available for the EV3. + """ + + SYSTEM_CLASS_NAME = 'lego-sensor' + SYSTEM_DEVICE_NAME_CONVENTION = 'sensor*' + __slots__ = [ + '_address', '_command', '_commands', '_decimals', '_driver_name', '_mode', '_modes', '_num_values', '_units', + '_value', '_bin_data_format', '_bin_data_size', '_bin_data', '_mode_scale' + ] + + def __init__(self, address=None, name_pattern=SYSTEM_DEVICE_NAME_CONVENTION, name_exact=False, **kwargs): + + if address is not None: + kwargs['address'] = address + super(Sensor, self).__init__(self.SYSTEM_CLASS_NAME, name_pattern, name_exact, **kwargs) + + self._address = None + self._command = None + self._commands = None + self._decimals = None + self._driver_name = None + self._mode = None + self._modes = None + self._num_values = None + self._units = None + self._value = [None, None, None, None, None, None, None, None] + + self._bin_data_format = None + self._bin_data_size = None + self._bin_data = None + self._mode_scale = {} + + def _scale(self, mode): + """ + Returns value scaling coefficient for the given mode. + """ + if mode in self._mode_scale: + scale = self._mode_scale[mode] + else: + scale = 10**(-self.decimals) + self._mode_scale[mode] = scale + + return scale + + @property + def address(self): + """ + Returns the name of the port that the sensor is connected to, e.g. ``ev3:in1``. + I2C sensors also include the I2C address (decimal), e.g. ``ev3:in1:i2c8``. + """ + self._address, value = self.get_attr_string(self._address, 'address') + return value + + @property + def command(self): + """ + Sends a command to the sensor. + """ + raise Exception("command is a write-only property!") + + @command.setter + def command(self, value): + self._command = self.set_attr_string(self._command, 'command', value) + + @property + def commands(self): + """ + Returns a list of the valid commands for the sensor. + Returns -EOPNOTSUPP if no commands are supported. + """ + (self._commands, value) = self.get_cached_attr_set(self._commands, 'commands') + return value + + @property + def decimals(self): + """ + Returns the number of decimal places for the values in the ``value`` + attributes of the current mode. + """ + self._decimals, value = self.get_attr_int(self._decimals, 'decimals') + return value + + @property + def driver_name(self): + """ + Returns the name of the sensor device/driver. See the list of [supported + sensors] for a complete list of drivers. + """ + (self._driver_name, value) = self.get_cached_attr_string(self._driver_name, 'driver_name') + return value + + @property + def mode(self): + """ + Returns the current mode. Writing one of the values returned by ``modes`` + sets the sensor to that mode. + """ + self._mode, value = self.get_attr_string(self._mode, 'mode') + return value + + @mode.setter + def mode(self, value): + self._mode = self.set_attr_string(self._mode, 'mode', value) + + @property + def modes(self): + """ + Returns a list of the valid modes for the sensor. + """ + (self._modes, value) = self.get_cached_attr_set(self._modes, 'modes') + return value + + @property + def num_values(self): + """ + Returns the number of ``value`` attributes that will return a valid value + for the current mode. + """ + self._num_values, value = self.get_attr_int(self._num_values, 'num_values') + return value + + @property + def units(self): + """ + Returns the units of the measured value for the current mode. May return + empty string + """ + self._units, value = self.get_attr_string(self._units, 'units') + return value + + def value(self, n=0): + """ + Returns the value or values measured by the sensor. Check num_values to + see how many values there are. Values with N >= num_values will return + an error. The values are fixed point numbers, so check decimals to see + if you need to divide to get the actual value. + """ + n = int(n) + + self._value[n], value = self.get_attr_int(self._value[n], 'value' + str(n)) + return value + + @property + def bin_data_format(self): + """ + Returns the format of the values in ``bin_data`` for the current mode. + Possible values are: + + - ``u8``: Unsigned 8-bit integer (byte) + - ``s8``: Signed 8-bit integer (sbyte) + - ``u16``: Unsigned 16-bit integer (ushort) + - ``s16``: Signed 16-bit integer (short) + - ``s16_be``: Signed 16-bit integer, big endian + - ``s32``: Signed 32-bit integer (int) + - ``float``: IEEE 754 32-bit floating point (float) + """ + self._bin_data_format, value = self.get_attr_string(self._bin_data_format, 'bin_data_format') + return value + + def bin_data(self, fmt=None): + """ + Returns the unscaled raw values in the ``value`` attributes as raw byte + array. Use ``bin_data_format``, ``num_values`` and the individual sensor + documentation to determine how to interpret the data. + + Use ``fmt`` to unpack the raw bytes into a struct. + + Example:: + + >>> from ev3dev2.sensor.lego import InfraredSensor + >>> ir = InfraredSensor() + >>> ir.value() + 28 + >>> ir.bin_data(' +# Copyright (c) 2015 Anton Vanhoucke +# Copyright (c) 2015 Denis Demidov +# Copyright (c) 2015 Eric Pascual +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +# ----------------------------------------------------------------------------- + +import sys +import logging +import time +from ev3dev2.button import ButtonBase +from ev3dev2.sensor import Sensor + +if sys.version_info < (3, 4): + raise SystemError('Must be using Python 3.4 or higher') + +log = logging.getLogger(__name__) + + +class TouchSensor(Sensor): + """ + Touch Sensor + """ + + SYSTEM_CLASS_NAME = Sensor.SYSTEM_CLASS_NAME + SYSTEM_DEVICE_NAME_CONVENTION = Sensor.SYSTEM_DEVICE_NAME_CONVENTION + + #: Button state + MODE_TOUCH = 'TOUCH' + MODES = (MODE_TOUCH, ) + + def __init__(self, address=None, name_pattern=SYSTEM_DEVICE_NAME_CONVENTION, name_exact=False, **kwargs): + super(TouchSensor, self).__init__(address, + name_pattern, + name_exact, + driver_name=['lego-ev3-touch', 'lego-nxt-touch'], + **kwargs) + + @property + def is_pressed(self): + """ + A boolean indicating whether the current touch sensor is being + pressed. + """ + self._ensure_mode(self.MODE_TOUCH) + return self.value(0) + + @property + def is_released(self): + return not self.is_pressed + + def _wait(self, wait_for_press, timeout_ms, sleep_ms): + tic = time.time() + + if sleep_ms: + sleep_ms = float(sleep_ms / 1000) + + # The kernel does not supoort POLLPRI or POLLIN for sensors so we have + # to drop into a loop and check often + while True: + + if self.is_pressed == wait_for_press: + return True + + if timeout_ms is not None and time.time() >= tic + timeout_ms / 1000: + return False + + if sleep_ms: + time.sleep(sleep_ms) + + def wait_for_pressed(self, timeout_ms=None, sleep_ms=10): + """ + Wait for the touch sensor to be pressed down. + """ + return self._wait(True, timeout_ms, sleep_ms) + + def wait_for_released(self, timeout_ms=None, sleep_ms=10): + """ + Wait for the touch sensor to be released. + """ + return self._wait(False, timeout_ms, sleep_ms) + + def wait_for_bump(self, timeout_ms=None, sleep_ms=10): + """ + Wait for the touch sensor to be pressed down and then released. + Both actions must happen within timeout_ms. + """ + start_time = time.time() + + if self.wait_for_pressed(timeout_ms, sleep_ms): + if timeout_ms is not None: + timeout_ms -= int((time.time() - start_time) * 1000) + return self.wait_for_released(timeout_ms, sleep_ms) + + return False + + +class ColorSensor(Sensor): + """ + LEGO EV3 color sensor. + """ + + __slots__ = ['red_max', 'green_max', 'blue_max'] + + SYSTEM_CLASS_NAME = Sensor.SYSTEM_CLASS_NAME + SYSTEM_DEVICE_NAME_CONVENTION = Sensor.SYSTEM_DEVICE_NAME_CONVENTION + + #: Reflected light. Red LED on. + MODE_COL_REFLECT = 'COL-REFLECT' + + #: Ambient light. Blue LEDs on. + MODE_COL_AMBIENT = 'COL-AMBIENT' + + #: Color. All LEDs rapidly cycling, appears white. + MODE_COL_COLOR = 'COL-COLOR' + + #: Raw reflected. Red LED on + MODE_REF_RAW = 'REF-RAW' + + #: Raw Color Components. All LEDs rapidly cycling, appears white. + MODE_RGB_RAW = 'RGB-RAW' + + #: No color. + COLOR_NOCOLOR = 0 + + #: Black color. + COLOR_BLACK = 1 + + #: Blue color. + COLOR_BLUE = 2 + + #: Green color. + COLOR_GREEN = 3 + + #: Yellow color. + COLOR_YELLOW = 4 + + #: Red color. + COLOR_RED = 5 + + #: White color. + COLOR_WHITE = 6 + + #: Brown color. + COLOR_BROWN = 7 + + MODES = (MODE_COL_REFLECT, MODE_COL_AMBIENT, MODE_COL_COLOR, MODE_REF_RAW, MODE_RGB_RAW) + + COLORS = ( + 'NoColor', + 'Black', + 'Blue', + 'Green', + 'Yellow', + 'Red', + 'White', + 'Brown', + ) + + def __init__(self, address=None, name_pattern=SYSTEM_DEVICE_NAME_CONVENTION, name_exact=False, **kwargs): + super(ColorSensor, self).__init__(address, name_pattern, name_exact, driver_name='lego-ev3-color', **kwargs) + + # See calibrate_white() for more details + self.red_max = 300 + self.green_max = 300 + self.blue_max = 300 + + @property + def reflected_light_intensity(self): + """ + Reflected light intensity as a percentage (0 to 100). Light on sensor is red. + """ + self._ensure_mode(self.MODE_COL_REFLECT) + return self.value(0) + + @property + def ambient_light_intensity(self): + """ + Ambient light intensity, as a percentage (0 to 100). Light on sensor is dimly lit blue. + """ + self._ensure_mode(self.MODE_COL_AMBIENT) + return self.value(0) + + @property + def color(self): + """ + Color detected by the sensor, categorized by overall value. + - 0: No color + - 1: Black + - 2: Blue + - 3: Green + - 4: Yellow + - 5: Red + - 6: White + - 7: Brown + """ + self._ensure_mode(self.MODE_COL_COLOR) + return self.value(0) + + @property + def color_name(self): + """ + Returns NoColor, Black, Blue, etc + """ + return self.COLORS[self.color] + + @property + def raw(self): + """ + Red, green, and blue components of the detected color, as a tuple. + + Officially in the range 0-1020 but the values returned will never be + that high. We do not yet know why the values returned are low, but + pointing the color sensor at a well lit sheet of white paper will return + values in the 250-400 range. + + If this is an issue, check out the rgb() and calibrate_white() methods. + """ + self._ensure_mode(self.MODE_RGB_RAW) + return self.value(0), self.value(1), self.value(2) + + def calibrate_white(self): + """ + The RGB raw values are on a scale of 0-1020 but you never see a value + anywhere close to 1020. This function is designed to be called when + the sensor is placed over a white object in order to figure out what + are the maximum RGB values the robot can expect to see. We will use + these maximum values to scale future raw values to a 0-255 range in + rgb(). + + If you never call this function red_max, green_max, and blue_max will + use a default value of 300. This default was selected by measuring + the RGB values of a white sheet of paper in a well lit room. + + Note that there are several variables that influence the maximum RGB + values detected by the color sensor + - the distance of the color sensor to the white object + - the amount of light in the room + - shadows that the robot casts on the sensor + """ + (self.red_max, self.green_max, self.blue_max) = self.raw + + @property + def rgb(self): + """ + Same as raw() but RGB values are scaled to 0-255 + """ + (red, green, blue) = self.raw + + return (min(int((red * 255) / self.red_max), 255), min(int((green * 255) / self.green_max), + 255), min(int((blue * 255) / self.blue_max), 255)) + + @property + def lab(self): + """ + Return colors in Lab color space + """ + RGB = [0, 0, 0] + XYZ = [0, 0, 0] + + for (num, value) in enumerate(self.rgb): + if value > 0.04045: + value = pow(((value + 0.055) / 1.055), 2.4) + else: + value = value / 12.92 + + RGB[num] = value * 100.0 + + # http://www.brucelindbloom.com/index.html?Eqn_RGB_XYZ_Matrix.html + # sRGB + # 0.4124564 0.3575761 0.1804375 + # 0.2126729 0.7151522 0.0721750 + # 0.0193339 0.1191920 0.9503041 + X = (RGB[0] * 0.4124564) + (RGB[1] * 0.3575761) + (RGB[2] * 0.1804375) + Y = (RGB[0] * 0.2126729) + (RGB[1] * 0.7151522) + (RGB[2] * 0.0721750) + Z = (RGB[0] * 0.0193339) + (RGB[1] * 0.1191920) + (RGB[2] * 0.9503041) + + XYZ[0] = X / 95.047 # ref_X = 95.047 + XYZ[1] = Y / 100.0 # ref_Y = 100.000 + XYZ[2] = Z / 108.883 # ref_Z = 108.883 + + for (num, value) in enumerate(XYZ): + if value > 0.008856: + value = pow(value, (1.0 / 3.0)) + else: + value = (7.787 * value) + (16 / 116.0) + + XYZ[num] = value + + L = (116.0 * XYZ[1]) - 16 + a = 500.0 * (XYZ[0] - XYZ[1]) + b = 200.0 * (XYZ[1] - XYZ[2]) + + L = round(L, 4) + a = round(a, 4) + b = round(b, 4) + + return (L, a, b) + + @property + def hsv(self): + """ + HSV: Hue, Saturation, Value + H: position in the spectrum + S: color saturation ("purity") + V: color brightness + """ + (r, g, b) = self.rgb + maxc = max(r, g, b) + minc = min(r, g, b) + v = maxc + + if minc == maxc: + return 0.0, 0.0, v + + s = (maxc - minc) / maxc + rc = (maxc - r) / (maxc - minc) + gc = (maxc - g) / (maxc - minc) + bc = (maxc - b) / (maxc - minc) + + if r == maxc: + h = bc - gc + elif g == maxc: + h = 2.0 + rc - bc + else: + h = 4.0 + gc - rc + + h = (h / 6.0) % 1.0 + + return (h, s, v) + + @property + def hls(self): + """ + HLS: Hue, Luminance, Saturation + H: position in the spectrum + L: color lightness + S: color saturation + """ + (red, green, blue) = self.rgb + maxc = max(red, green, blue) + minc = min(red, green, blue) + luminance = (minc + maxc) / 2.0 + + if minc == maxc: + return 0.0, luminance, 0.0 + + if luminance <= 0.5: + saturation = (maxc - minc) / (maxc + minc) + else: + if 2.0 - maxc - minc == 0: + saturation = 0 + else: + saturation = (maxc - minc) / (2.0 - maxc - minc) + + rc = (maxc - red) / (maxc - minc) + gc = (maxc - green) / (maxc - minc) + bc = (maxc - blue) / (maxc - minc) + + if red == maxc: + hue = bc - gc + elif green == maxc: + hue = 2.0 + rc - bc + else: + hue = 4.0 + gc - rc + + hue = (hue / 6.0) % 1.0 + + return (hue, luminance, saturation) + + @property + def red(self): + """ + Red component of the detected color, in the range 0-1020. + """ + self._ensure_mode(self.MODE_RGB_RAW) + return self.value(0) + + @property + def green(self): + """ + Green component of the detected color, in the range 0-1020. + """ + self._ensure_mode(self.MODE_RGB_RAW) + return self.value(1) + + @property + def blue(self): + """ + Blue component of the detected color, in the range 0-1020. + """ + self._ensure_mode(self.MODE_RGB_RAW) + return self.value(2) + + +class UltrasonicSensor(Sensor): + """ + LEGO EV3 ultrasonic sensor. + """ + + SYSTEM_CLASS_NAME = Sensor.SYSTEM_CLASS_NAME + SYSTEM_DEVICE_NAME_CONVENTION = Sensor.SYSTEM_DEVICE_NAME_CONVENTION + + #: Continuous measurement in centimeters. + MODE_US_DIST_CM = 'US-DIST-CM' + + #: Continuous measurement in inches. + MODE_US_DIST_IN = 'US-DIST-IN' + + #: Listen. + MODE_US_LISTEN = 'US-LISTEN' + + #: Single measurement in centimeters. + MODE_US_SI_CM = 'US-SI-CM' + + #: Single measurement in inches. + MODE_US_SI_IN = 'US-SI-IN' + + MODES = ( + MODE_US_DIST_CM, + MODE_US_DIST_IN, + MODE_US_LISTEN, + MODE_US_SI_CM, + MODE_US_SI_IN, + ) + + def __init__(self, address=None, name_pattern=SYSTEM_DEVICE_NAME_CONVENTION, name_exact=False, **kwargs): + super(UltrasonicSensor, self).__init__(address, + name_pattern, + name_exact, + driver_name=['lego-ev3-us', 'lego-nxt-us'], + **kwargs) + + @property + def distance_centimeters_continuous(self): + """ + Measurement of the distance detected by the sensor, + in centimeters. + + The sensor will continue to take measurements so + they are available for future reads. + + Prefer using the equivalent :meth:`UltrasonicSensor.distance_centimeters` property. + """ + self._ensure_mode(self.MODE_US_DIST_CM) + return self.value(0) * self._scale('US_DIST_CM') + + @property + def distance_centimeters_ping(self): + """ + Measurement of the distance detected by the sensor, + in centimeters. + + The sensor will take a single measurement then stop + broadcasting. + + If you use this property too frequently (e.g. every + 100msec), the sensor will sometimes lock up and writing + to the mode attribute will return an error. A delay of + 250msec between each usage seems sufficient to keep the + sensor from locking up. + """ + # This mode is special; setting the mode causes the sensor to send out + # a "ping", but the mode isn't actually changed. + self.mode = self.MODE_US_SI_CM + return self.value(0) * self._scale('US_DIST_CM') + + @property + def distance_centimeters(self): + """ + Measurement of the distance detected by the sensor, + in centimeters. + + Equivalent to :meth:`UltrasonicSensor.distance_centimeters_continuous`. + """ + return self.distance_centimeters_continuous + + @property + def distance_inches_continuous(self): + """ + Measurement of the distance detected by the sensor, + in inches. + + The sensor will continue to take measurements so + they are available for future reads. + + Prefer using the equivalent :meth:`UltrasonicSensor.distance_inches` property. + """ + self._ensure_mode(self.MODE_US_DIST_IN) + return self.value(0) * self._scale('US_DIST_IN') + + @property + def distance_inches_ping(self): + """ + Measurement of the distance detected by the sensor, + in inches. + + The sensor will take a single measurement then stop + broadcasting. + + If you use this property too frequently (e.g. every + 100msec), the sensor will sometimes lock up and writing + to the mode attribute will return an error. A delay of + 250msec between each usage seems sufficient to keep the + sensor from locking up. + """ + # This mode is special; setting the mode causes the sensor to send out + # a "ping", but the mode isn't actually changed. + self.mode = self.MODE_US_SI_IN + return self.value(0) * self._scale('US_DIST_IN') + + @property + def distance_inches(self): + """ + Measurement of the distance detected by the sensor, + in inches. + + Equivalent to :meth:`UltrasonicSensor.distance_inches_continuous`. + """ + return self.distance_inches_continuous + + @property + def other_sensor_present(self): + """ + Boolean indicating whether another ultrasonic sensor could + be heard nearby. + """ + self._ensure_mode(self.MODE_US_LISTEN) + return bool(self.value(0)) + + +class GyroSensor(Sensor): + """ + LEGO EV3 gyro sensor. + """ + + SYSTEM_CLASS_NAME = Sensor.SYSTEM_CLASS_NAME + SYSTEM_DEVICE_NAME_CONVENTION = Sensor.SYSTEM_DEVICE_NAME_CONVENTION + + #: Angle + MODE_GYRO_ANG = 'GYRO-ANG' + + #: Rotational speed + MODE_GYRO_RATE = 'GYRO-RATE' + + #: Raw sensor value + MODE_GYRO_FAS = 'GYRO-FAS' + + #: Angle and rotational speed + MODE_GYRO_G_A = 'GYRO-G&A' + + #: Calibration ??? + MODE_GYRO_CAL = 'GYRO-CAL' + + # Newer versions of the Gyro sensor also have an additional second axis + # accessible via the TILT-ANG and TILT-RATE modes that is not usable + # using the official EV3-G blocks + MODE_TILT_ANG = 'TILT-ANG' + MODE_TILT_RATE = 'TILT-RATE' + + MODES = ( + MODE_GYRO_ANG, + MODE_GYRO_RATE, + MODE_GYRO_FAS, + MODE_GYRO_G_A, + MODE_GYRO_CAL, + MODE_TILT_ANG, + MODE_TILT_RATE, + ) + + def __init__(self, address=None, name_pattern=SYSTEM_DEVICE_NAME_CONVENTION, name_exact=False, **kwargs): + super(GyroSensor, self).__init__(address, name_pattern, name_exact, driver_name='lego-ev3-gyro', **kwargs) + self._direct = None + self._init_angle = self.angle + + @property + def angle(self): + """ + The number of degrees that the sensor has been rotated + since it was put into this mode. + """ + self._ensure_mode(self.MODE_GYRO_ANG) + return self.value(0) + + @property + def rate(self): + """ + The rate at which the sensor is rotating, in degrees/second. + """ + self._ensure_mode(self.MODE_GYRO_RATE) + return self.value(0) + + @property + def angle_and_rate(self): + """ + Angle (degrees) and Rotational Speed (degrees/second). + """ + self._ensure_mode(self.MODE_GYRO_G_A) + return self.value(0), self.value(1) + + @property + def tilt_angle(self): + self._ensure_mode(self.MODE_TILT_ANG) + return self.value(0) + + @property + def tilt_rate(self): + self._ensure_mode(self.MODE_TILT_RATE) + return self.value(0) + + def calibrate(self): + """ + The robot should be perfectly still when you call this + """ + current_mode = self.mode + self._ensure_mode(self.MODE_GYRO_CAL) + time.sleep(2) + self._ensure_mode(current_mode) + + def reset(self): + """Resets the angle to 0. + + Caveats: + - This function only resets the angle to 0, it does not fix drift. + - This function only works on EV3, it does not work on BrickPi, + PiStorms, or with any sensor multiplexors. + """ + # 17 comes from inspecting the .vix file of the Gyro sensor block in EV3-G + self._direct = self.set_attr_raw(self._direct, 'direct', b'\x11') + self._init_angle = self.angle + + def wait_until_angle_changed_by(self, delta, direction_sensitive=False): + """ + Wait until angle has changed by specified amount. + + If ``direction_sensitive`` is True we will wait until angle has changed + by ``delta`` and with the correct sign. + + If ``direction_sensitive`` is False (default) we will wait until angle has changed + by ``delta`` in either direction. + """ + assert self.mode in (self.MODE_GYRO_G_A, self.MODE_GYRO_ANG, + self.MODE_TILT_ANG),\ + 'Gyro mode should be MODE_GYRO_ANG, MODE_GYRO_G_A or MODE_TILT_ANG' + start_angle = self.value(0) + + if direction_sensitive: + if delta > 0: + while (self.value(0) - start_angle) < delta: + time.sleep(0.01) + else: + delta *= -1 + while (start_angle - self.value(0)) < delta: + time.sleep(0.01) + else: + while abs(start_angle - self.value(0)) < delta: + time.sleep(0.01) + + def circle_angle(self): + """ + As the gryo rotates clockwise the angle increases, it will increase + by 360 for each full rotation. As the gyro rotates counter-clockwise + the gyro angle will decrease. + + The angles on a circle have the opposite behavior though, they start + at 0 and increase as you move counter-clockwise around the circle. + + Convert the gyro angle to the angle on a circle. We consider the initial + position of the gyro to be at 90 degrees on the cirlce. + """ + current_angle = self.angle + delta = abs(current_angle - self._init_angle) % 360 + + if delta == 0: + result = 90 + + # the gyro has turned clockwise relative to where we started + elif current_angle > self._init_angle: + + if delta <= 90: + result = 90 - delta + + elif delta <= 180: + result = 360 - (delta - 90) + + elif delta <= 270: + result = 270 - (delta - 180) + + else: + result = 180 - (delta - 270) + + # This can be chatty (but helpful) so save it for a rainy day + # log.info("%s moved clockwise %s degrees to %s" % (self, delta, result)) + + # the gyro has turned counter-clockwise relative to where we started + else: + if delta <= 90: + result = 90 + delta + + elif delta <= 180: + result = 180 + (delta - 90) + + elif delta <= 270: + result = 270 + (delta - 180) + + else: + result = delta - 270 + + # This can be chatty (but helpful) so save it for a rainy day + # log.info("%s moved counter-clockwise %s degrees to %s" % (self, delta, result)) + + return result + + +class InfraredSensor(Sensor, ButtonBase): + """ + LEGO EV3 infrared sensor. + """ + + SYSTEM_CLASS_NAME = Sensor.SYSTEM_CLASS_NAME + SYSTEM_DEVICE_NAME_CONVENTION = Sensor.SYSTEM_DEVICE_NAME_CONVENTION + + #: Proximity + MODE_IR_PROX = 'IR-PROX' + + #: IR Seeker + MODE_IR_SEEK = 'IR-SEEK' + + #: IR Remote Control + MODE_IR_REMOTE = 'IR-REMOTE' + + #: IR Remote Control. State of the buttons is coded in binary + MODE_IR_REM_A = 'IR-REM-A' + + #: Calibration ??? + MODE_IR_CAL = 'IR-CAL' + + MODES = (MODE_IR_PROX, MODE_IR_SEEK, MODE_IR_REMOTE, MODE_IR_REM_A, MODE_IR_CAL) + + # The following are all of the various combinations of button presses for + # the remote control. The key/index is the number that will be written in + # the attribute file to indicate what combination of buttons are currently + # pressed. + _BUTTON_VALUES = { + 0: [], + 1: ['top_left'], + 2: ['bottom_left'], + 3: ['top_right'], + 4: ['bottom_right'], + 5: ['top_left', 'top_right'], + 6: ['top_left', 'bottom_right'], + 7: ['bottom_left', 'top_right'], + 8: ['bottom_left', 'bottom_right'], + 9: ['beacon'], + 10: ['top_left', 'bottom_left'], + 11: ['top_right', 'bottom_right'] + } + + _BUTTONS = ('top_left', 'bottom_left', 'top_right', 'bottom_right', 'beacon') + + # Button codes for doing rapid check of remote status + NO_BUTTON = 0 + TOP_LEFT = 1 + BOTTOM_LEFT = 2 + TOP_RIGHT = 3 + BOTTOM_RIGHT = 4 + TOP_LEFT_TOP_RIGHT = 5 + TOP_LEFT_BOTTOM_RIGHT = 6 + BOTTOM_LEFT_TOP_RIGHT = 7 + BOTTOM_LEFT_BOTTOM_RIGHT = 8 + BEACON = 9 + TOP_LEFT_BOTTOM_LEFT = 10 + TOP_RIGHT_BOTTOM_RIGHT = 11 + + #: Handler for top-left button events on channel 1. See :meth:`InfraredSensor.process`. + on_channel1_top_left = None + #: Handler for bottom-left button events on channel 1. See :meth:`InfraredSensor.process`. + on_channel1_bottom_left = None + #: Handler for top-right button events on channel 1. See :meth:`InfraredSensor.process`. + on_channel1_top_right = None + #: Handler for bottom-right button events on channel 1. See :meth:`InfraredSensor.process`. + on_channel1_bottom_right = None + #: Handler for beacon button events on channel 1. See :meth:`InfraredSensor.process`. + on_channel1_beacon = None + + #: Handler for top-left button events on channel 2. See :meth:`InfraredSensor.process`. + on_channel2_top_left = None + #: Handler for bottom-left button events on channel 2. See :meth:`InfraredSensor.process`. + on_channel2_bottom_left = None + #: Handler for top-right button events on channel 2. See :meth:`InfraredSensor.process`. + on_channel2_top_right = None + #: Handler for bottom-right button events on channel 2. See :meth:`InfraredSensor.process`. + on_channel2_bottom_right = None + #: Handler for beacon button events on channel 2. See :meth:`InfraredSensor.process`. + on_channel2_beacon = None + + #: Handler for top-left button events on channel 3. See :meth:`InfraredSensor.process`. + on_channel3_top_left = None + #: Handler for bottom-left button events on channel 3. See :meth:`InfraredSensor.process`. + on_channel3_bottom_left = None + #: Handler for top-right button events on channel 3. See :meth:`InfraredSensor.process`. + on_channel3_top_right = None + #: Handler for bottom-right button events on channel 3. See :meth:`InfraredSensor.process`. + on_channel3_bottom_right = None + #: Handler for beacon button events on channel 3. See :meth:`InfraredSensor.process`. + on_channel3_beacon = None + + #: Handler for top-left button events on channel 4. See :meth:`InfraredSensor.process`. + on_channel4_top_left = None + #: Handler for bottom-left button events on channel 4. See :meth:`InfraredSensor.process`. + on_channel4_bottom_left = None + #: Handler for top-right button events on channel 4. See :meth:`InfraredSensor.process`. + on_channel4_top_right = None + #: Handler for bottom-right button events on channel 4. See :meth:`InfraredSensor.process`. + on_channel4_bottom_right = None + #: Handler for beacon button events on channel 4. See :meth:`InfraredSensor.process`. + on_channel4_beacon = None + + def __init__(self, address=None, name_pattern=SYSTEM_DEVICE_NAME_CONVENTION, name_exact=False, **kwargs): + super(InfraredSensor, self).__init__(address, name_pattern, name_exact, driver_name='lego-ev3-ir', **kwargs) + + def _normalize_channel(self, channel): + assert channel >= 1 and channel <= 4, "channel is %s, it must be 1, 2, 3, or 4" % channel + channel = max(1, min(4, channel)) - 1 + return channel + + @property + def proximity(self): + """ + An estimate of the distance between the sensor and objects in front of + it, as a percentage. 100% is approximately 70cm/27in. + """ + self._ensure_mode(self.MODE_IR_PROX) + return self.value(0) + + def heading(self, channel=1): + """ + Returns heading (-25, 25) to the beacon on the given channel. + """ + self._ensure_mode(self.MODE_IR_SEEK) + channel = self._normalize_channel(channel) + return self.value(channel * 2) + + def distance(self, channel=1): + """ + Returns distance (0, 100) to the beacon on the given channel. + Returns None when beacon is not found. + """ + self._ensure_mode(self.MODE_IR_SEEK) + channel = self._normalize_channel(channel) + ret_value = self.value((channel * 2) + 1) + + # The value will be -128 if no beacon is found, return None instead + return None if ret_value == -128 else ret_value + + def heading_and_distance(self, channel=1): + """ + Returns heading and distance to the beacon on the given channel as a + tuple. + """ + return (self.heading(channel), self.distance(channel)) + + def top_left(self, channel=1): + """ + Checks if ``top_left`` button is pressed. + """ + return 'top_left' in self.buttons_pressed(channel) + + def bottom_left(self, channel=1): + """ + Checks if ``bottom_left`` button is pressed. + """ + return 'bottom_left' in self.buttons_pressed(channel) + + def top_right(self, channel=1): + """ + Checks if ``top_right`` button is pressed. + """ + return 'top_right' in self.buttons_pressed(channel) + + def bottom_right(self, channel=1): + """ + Checks if ``bottom_right`` button is pressed. + """ + return 'bottom_right' in self.buttons_pressed(channel) + + def beacon(self, channel=1): + """ + Checks if ``beacon`` button is pressed. + """ + return 'beacon' in self.buttons_pressed(channel) + + def buttons_pressed(self, channel=1): + """ + Returns list of currently pressed buttons. + + Note that the sensor can only identify up to two buttons pressed at once. + """ + self._ensure_mode(self.MODE_IR_REMOTE) + channel = self._normalize_channel(channel) + return self._BUTTON_VALUES.get(self.value(channel), []) + + def process(self): + """ + Check for currenly pressed buttons. If the new state differs from the + old state, call the appropriate button event handlers. + + To use the on_channel1_top_left, etc handlers your program would do something like: + + .. code:: python + + def top_left_channel_1_action(state): + print("top left on channel 1: %s" % state) + + def bottom_right_channel_4_action(state): + print("bottom right on channel 4: %s" % state) + + ir = InfraredSensor() + ir.on_channel1_top_left = top_left_channel_1_action + ir.on_channel4_bottom_right = bottom_right_channel_4_action + + while True: + ir.process() + time.sleep(0.01) + + """ + new_state = [] + state_diff = [] + + for channel in range(1, 5): + + for button in self.buttons_pressed(channel): + new_state.append((button, channel)) + + # Key was not pressed before but now is pressed + if (button, channel) not in self._state: + state_diff.append((button, channel)) + + # Key was pressed but is no longer pressed + for button in self._BUTTONS: + if (button, channel) not in new_state and (button, channel) in self._state: + state_diff.append((button, channel)) + + self._state = new_state + + for (button, channel) in state_diff: + handler = getattr(self, 'on_channel' + str(channel) + '_' + button) + + if handler is not None: + handler((button, channel) in new_state) + + if self.on_change is not None and state_diff: + self.on_change([(button, channel, button in new_state) for (button, channel) in state_diff]) + + +class SoundSensor(Sensor): + """ + LEGO NXT Sound Sensor + """ + + SYSTEM_CLASS_NAME = Sensor.SYSTEM_CLASS_NAME + SYSTEM_DEVICE_NAME_CONVENTION = Sensor.SYSTEM_DEVICE_NAME_CONVENTION + + #: Sound pressure level. Flat weighting + MODE_DB = 'DB' + + #: Sound pressure level. A weighting + MODE_DBA = 'DBA' + + MODES = ( + MODE_DB, + MODE_DBA, + ) + + def __init__(self, address=None, name_pattern=SYSTEM_DEVICE_NAME_CONVENTION, name_exact=False, **kwargs): + super(SoundSensor, self).__init__(address, name_pattern, name_exact, driver_name='lego-nxt-sound', **kwargs) + + @property + def sound_pressure(self): + """ + A measurement of the measured sound pressure level, as a + percent. Uses a flat weighting. + """ + self._ensure_mode(self.MODE_DB) + return self.value(0) * self._scale('DB') + + @property + def sound_pressure_low(self): + """ + A measurement of the measured sound pressure level, as a + percent. Uses A-weighting, which focuses on levels up to 55 dB. + """ + self._ensure_mode(self.MODE_DBA) + return self.value(0) * self._scale('DBA') + + +class LightSensor(Sensor): + """ + LEGO NXT Light Sensor + """ + + SYSTEM_CLASS_NAME = Sensor.SYSTEM_CLASS_NAME + SYSTEM_DEVICE_NAME_CONVENTION = Sensor.SYSTEM_DEVICE_NAME_CONVENTION + + #: Reflected light. LED on + MODE_REFLECT = 'REFLECT' + + #: Ambient light. LED off + MODE_AMBIENT = 'AMBIENT' + + MODES = ( + MODE_REFLECT, + MODE_AMBIENT, + ) + + def __init__(self, address=None, name_pattern=SYSTEM_DEVICE_NAME_CONVENTION, name_exact=False, **kwargs): + super(LightSensor, self).__init__(address, name_pattern, name_exact, driver_name='lego-nxt-light', **kwargs) + + @property + def reflected_light_intensity(self): + """ + A measurement of the reflected light intensity, as a percentage. + """ + self._ensure_mode(self.MODE_REFLECT) + return self.value(0) * self._scale('REFLECT') + + @property + def ambient_light_intensity(self): + """ + A measurement of the ambient light intensity, as a percentage. + """ + self._ensure_mode(self.MODE_AMBIENT) + return self.value(0) * self._scale('AMBIENT') diff --git a/ev3dev2/sound.py b/ev3dev2/sound.py new file mode 100644 index 0000000..350d82f --- /dev/null +++ b/ev3dev2/sound.py @@ -0,0 +1,661 @@ +# ----------------------------------------------------------------------------- +# Copyright (c) 2015 Ralph Hempel +# Copyright (c) 2015 Anton Vanhoucke +# Copyright (c) 2015 Denis Demidov +# Copyright (c) 2015 Eric Pascual +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +# ----------------------------------------------------------------------------- + +import sys +import os +import re +from time import sleep +from ev3dev2 import is_micropython + +if sys.version_info < (3, 4): + raise SystemError('Must be using Python 3.4 or higher') + +if not is_micropython(): + import shlex + from subprocess import Popen, PIPE + + +def _make_scales(notes): + """ Utility function used by Sound class for building the note frequencies table """ + res = dict() + for note, freq in notes: + freq = round(freq) + for n in note.split('/'): + res[n.upper()] = freq + return res + + +def get_command_processes(command): + """ + :param string command: a string of command(s) to run that may include pipes + :return: a list of Popen objects + """ + + # We must split command into sub-commands to support pipes + if "|" in command: + command_parts = command.split("|") + else: + command_parts = [command] + + processes = [] + + for command_part in command_parts: + if processes: + processes.append(Popen(shlex.split(command_part), stdin=processes[-1].stdout, stdout=PIPE, stderr=PIPE)) + else: + processes.append(Popen(shlex.split(command_part), stdin=None, stdout=PIPE, stderr=PIPE)) + + return processes + + +class Sound(object): + """ + Support beep, play wav files, or convert text to speech. + + Examples:: + + from ev3dev2.sound import Sound + + spkr = Sound() + + # Play 'bark.wav': + spkr.play_file('bark.wav') + + # Introduce yourself: + spkr.speak('Hello, I am Robot') + + # Play a small song + spkr.play_song(( + ('D4', 'e3'), + ('D4', 'e3'), + ('D4', 'e3'), + ('G4', 'h'), + ('D5', 'h') + )) + + In order to mimic EV3-G API parameters, durations used in methods + exposed as EV3-G blocks for sound related operations are expressed + as a float number of seconds. + """ + + channel = None + + # play_types + PLAY_WAIT_FOR_COMPLETE = 0 #: Play the sound and block until it is complete + PLAY_NO_WAIT_FOR_COMPLETE = 1 #: Start playing the sound but return immediately + PLAY_LOOP = 2 #: Never return; start the sound immediately after it completes, until the program is killed + + PLAY_TYPES = (PLAY_WAIT_FOR_COMPLETE, PLAY_NO_WAIT_FOR_COMPLETE, PLAY_LOOP) + + def _validate_play_type(self, play_type): + assert play_type in self.PLAY_TYPES, \ + "Invalid play_type %s, must be one of %s" % (play_type, ','.join(str(t) for t in self.PLAY_TYPES)) + + def _audio_command(self, command, play_type): + if is_micropython(): + + if play_type == Sound.PLAY_WAIT_FOR_COMPLETE: + os.system(command) + + elif play_type == Sound.PLAY_NO_WAIT_FOR_COMPLETE: + os.system('{} &'.format(command)) + + elif play_type == Sound.PLAY_LOOP: + while True: + os.system(command) + + else: + raise Exception("invalid play_type " % play_type) + + return None + + else: + with open(os.devnull, 'w'): + + if play_type == Sound.PLAY_WAIT_FOR_COMPLETE: + processes = get_command_processes(command) + processes[-1].communicate() + processes[-1].wait() + return None + + elif play_type == Sound.PLAY_NO_WAIT_FOR_COMPLETE: + processes = get_command_processes(command) + return processes[-1] + + elif play_type == Sound.PLAY_LOOP: + while True: + processes = get_command_processes(command) + processes[-1].communicate() + processes[-1].wait() + + else: + raise Exception("invalid play_type " % play_type) + + def beep(self, args='', play_type=PLAY_WAIT_FOR_COMPLETE): + """ + Call beep command with the provided arguments (if any). + See `beep man page`_ and google `linux beep music`_ for inspiration. + + :param string args: Any additional arguments to be passed to ``beep`` (see the `beep man page`_ for details) + + :param play_type: The behavior of ``beep`` once playback has been initiated + :type play_type: ``Sound.PLAY_WAIT_FOR_COMPLETE`` or ``Sound.PLAY_NO_WAIT_FOR_COMPLETE`` + + :return: When python3 is used and ``Sound.PLAY_NO_WAIT_FOR_COMPLETE`` is specified, returns the + spawn subprocess from ``subprocess.Popen``; ``None`` otherwise + + .. _`beep man page`: https://linux.die.net/man/1/beep + .. _`linux beep music`: https://www.google.com/search?q=linux+beep+music + """ + return self._audio_command("/usr/bin/beep %s" % args, play_type) + + def tone(self, *args, play_type=PLAY_WAIT_FOR_COMPLETE): + """ + .. rubric:: tone(tone_sequence) + + Play tone sequence. + + Here is a cheerful example:: + + my_sound = Sound() + my_sound.tone([ + (392, 350, 100), (392, 350, 100), (392, 350, 100), (311.1, 250, 100), + (466.2, 25, 100), (392, 350, 100), (311.1, 250, 100), (466.2, 25, 100), + (392, 700, 100), (587.32, 350, 100), (587.32, 350, 100), + (587.32, 350, 100), (622.26, 250, 100), (466.2, 25, 100), + (369.99, 350, 100), (311.1, 250, 100), (466.2, 25, 100), (392, 700, 100), + (784, 350, 100), (392, 250, 100), (392, 25, 100), (784, 350, 100), + (739.98, 250, 100), (698.46, 25, 100), (659.26, 25, 100), + (622.26, 25, 100), (659.26, 50, 400), (415.3, 25, 200), (554.36, 350, 100), + (523.25, 250, 100), (493.88, 25, 100), (466.16, 25, 100), (440, 25, 100), + (466.16, 50, 400), (311.13, 25, 200), (369.99, 350, 100), + (311.13, 250, 100), (392, 25, 100), (466.16, 350, 100), (392, 250, 100), + (466.16, 25, 100), (587.32, 700, 100), (784, 350, 100), (392, 250, 100), + (392, 25, 100), (784, 350, 100), (739.98, 250, 100), (698.46, 25, 100), + (659.26, 25, 100), (622.26, 25, 100), (659.26, 50, 400), (415.3, 25, 200), + (554.36, 350, 100), (523.25, 250, 100), (493.88, 25, 100), + (466.16, 25, 100), (440, 25, 100), (466.16, 50, 400), (311.13, 25, 200), + (392, 350, 100), (311.13, 250, 100), (466.16, 25, 100), + (392.00, 300, 150), (311.13, 250, 100), (466.16, 25, 100), (392, 700) + ]) + + Have also a look at :py:meth:`play_song` for a more musician-friendly way of doing, which uses + the conventional notation for notes and durations. + + :param list[tuple(float,float,float)] tone_sequence: The sequence of tones to play. The first + number of each tuple is frequency in Hz, the second is duration in milliseconds, and the + third is delay in milliseconds between this and the next tone in the sequence. + + :param play_type: The behavior of ``tone`` once playback has been initiated + :type play_type: ``Sound.PLAY_WAIT_FOR_COMPLETE`` or ``Sound.PLAY_NO_WAIT_FOR_COMPLETE`` + + :return: When python3 is used and ``Sound.PLAY_NO_WAIT_FOR_COMPLETE`` is specified, returns the + spawn subprocess from ``subprocess.Popen``; ``None`` otherwise + + .. rubric:: tone(frequency, duration) + + Play single tone of given frequency and duration. + + :param float frequency: The frequency of the tone in Hz + :param float duration: The duration of the tone in milliseconds + + :param play_type: The behavior of ``tone`` once playback has been initiated + :type play_type: ``Sound.PLAY_WAIT_FOR_COMPLETE`` or ``Sound.PLAY_NO_WAIT_FOR_COMPLETE`` + + :return: When python3 is used and ``Sound.PLAY_NO_WAIT_FOR_COMPLETE`` is specified, returns the + spawn subprocess from ``subprocess.Popen``; ``None`` otherwise + """ + def play_tone_sequence(tone_sequence): + def beep_args(frequency=None, duration=None, delay=None): + args = '' + if frequency is not None: + args += '-f %s ' % frequency + if duration is not None: + args += '-l %s ' % duration + if delay is not None: + args += '-D %s ' % delay + + return args + + return self.beep(' -n '.join([beep_args(*t) for t in tone_sequence]), play_type=play_type) + + if len(args) == 1: + return play_tone_sequence(args[0]) + elif len(args) == 2: + return play_tone_sequence([(args[0], args[1])]) + else: + raise Exception("Unsupported number of parameters in Sound.tone(): expected 1 or 2, got " + str(len(args))) + + def play_tone(self, frequency, duration, delay=0.0, volume=100, play_type=PLAY_WAIT_FOR_COMPLETE): + """ Play a single tone, specified by its frequency, duration, volume and final delay. + + :param int frequency: the tone frequency, in Hertz + :param float duration: Tone duration, in seconds + :param float delay: Delay after tone, in seconds (can be useful when chaining calls to ``play_tone``) + :param int volume: The play volume, in percent of maximum volume + + :param play_type: The behavior of ``play_tone`` once playback has been initiated + :type play_type: ``Sound.PLAY_WAIT_FOR_COMPLETE``, ``Sound.PLAY_NO_WAIT_FOR_COMPLETE`` or ``Sound.PLAY_LOOP`` + + :return: When python3 is used and ``Sound.PLAY_NO_WAIT_FOR_COMPLETE`` is specified, returns + the PID of the underlying beep command; ``None`` otherwise + + :raises ValueError: if invalid parameter + """ + self._validate_play_type(play_type) + + if duration <= 0: + raise ValueError('invalid duration (%s)' % duration) + if delay < 0: + raise ValueError('invalid delay (%s)' % delay) + if not 0 < volume <= 100: + raise ValueError('invalid volume (%s)' % volume) + + self.set_volume(volume) + + duration_ms = int(duration * 1000) + delay_ms = int(delay * 1000) + + self.tone([(frequency, duration_ms, delay_ms)], play_type=play_type) + + def play_note(self, note, duration, volume=100, play_type=PLAY_WAIT_FOR_COMPLETE): + """ Plays a note, given by its name as defined in ``_NOTE_FREQUENCIES``. + + :param string note: The note symbol with its octave number + :param float duration: Tone duration, in seconds + :param int volume: The play volume, in percent of maximum volume + + :param play_type: The behavior of ``play_note`` once playback has been initiated + :type play_type: ``Sound.PLAY_WAIT_FOR_COMPLETE``, ``Sound.PLAY_NO_WAIT_FOR_COMPLETE`` or ``Sound.PLAY_LOOP`` + + :return: When python3 is used and ``Sound.PLAY_NO_WAIT_FOR_COMPLETE`` is specified, returns + the PID of the underlying beep command; ``None`` otherwise + + :raises ValueError: is invalid parameter (note, duration,...) + """ + self._validate_play_type(play_type) + try: + freq = self._NOTE_FREQUENCIES.get(note.upper(), self._NOTE_FREQUENCIES[note]) + except KeyError: + raise ValueError('invalid note (%s)' % note) + + if duration <= 0: + raise ValueError('invalid duration (%s)' % duration) + if not 0 < volume <= 100: + raise ValueError('invalid volume (%s)' % volume) + + return self.play_tone(freq, duration=duration, volume=volume, play_type=play_type) + + def play_file(self, wav_file, volume=100, play_type=PLAY_WAIT_FOR_COMPLETE): + """ Play a sound file (wav format) at a given volume. The EV3 audio subsystem will work best if + the file is encoded as 16-bit, mono, 22050Hz. + + :param string wav_file: The sound file path + :param int volume: The play volume, in percent of maximum volume + + :param play_type: The behavior of ``play_file`` once playback has been initiated + :type play_type: ``Sound.PLAY_WAIT_FOR_COMPLETE``, ``Sound.PLAY_NO_WAIT_FOR_COMPLETE`` or ``Sound.PLAY_LOOP`` + + :return: When python3 is used and ``Sound.PLAY_NO_WAIT_FOR_COMPLETE`` is specified, returns the + spawn subprocess from ``subprocess.Popen``; ``None`` otherwise + """ + if not 0 < volume <= 100: + raise ValueError('invalid volume (%s)' % volume) + + if not wav_file.endswith(".wav"): + raise ValueError('invalid sound file (%s), only .wav files are supported' % wav_file) + + if not os.path.exists(wav_file): + raise ValueError("%s does not exist" % wav_file) + + self._validate_play_type(play_type) + self.set_volume(volume) + return self._audio_command('/usr/bin/aplay -q "%s"' % wav_file, play_type) + + def speak(self, text, espeak_opts='-a 200 -s 130', volume=100, play_type=PLAY_WAIT_FOR_COMPLETE): + """ Speak the given text aloud. + + Uses the ``espeak`` external command. + + :param string text: The text to speak + :param string espeak_opts: ``espeak`` command options (advanced usage) + :param int volume: The play volume, in percent of maximum volume + + :param play_type: The behavior of ``speak`` once playback has been initiated + :type play_type: ``Sound.PLAY_WAIT_FOR_COMPLETE``, ``Sound.PLAY_NO_WAIT_FOR_COMPLETE`` or ``Sound.PLAY_LOOP`` + + :return: When python3 is used and ``Sound.PLAY_NO_WAIT_FOR_COMPLETE`` is specified, returns the + spawn subprocess from ``subprocess.Popen``; ``None`` otherwise + """ + self._validate_play_type(play_type) + self.set_volume(volume) + cmd = "/usr/bin/espeak --stdout %s '%s' | /usr/bin/aplay -q" % (espeak_opts, text) + return self._audio_command(cmd, play_type) + + def _get_channel(self): + """ + :return: The detected sound channel + :rtype: string + """ + if self.channel is None: + # Get default channel as the first one that pops up in + # 'amixer scontrols' output, which contains strings in the + # following format: + # + # Simple mixer control 'Master',0 + # Simple mixer control 'Capture',0 + out = os.popen('/usr/bin/amixer scontrols').read() + m = re.search(r"'([^']+)'", out) + if m: + self.channel = m.group(1) + else: + self.channel = 'Playback' + + return self.channel + + def set_volume(self, pct, channel=None): + """ + Sets the sound volume to the given percentage [0-100] by calling + ``amixer -q set %``. + If the channel is not specified, it tries to determine the default one + by running ``amixer scontrols``. If that fails as well, it uses the + ``Playback`` channel, as that is the only channel on the EV3. + """ + + if channel is None: + channel = self._get_channel() + + os.system('/usr/bin/amixer -q set {0} {1:d}%'.format(channel, pct)) + + def get_volume(self, channel=None): + """ + Gets the current sound volume by parsing the output of + ``amixer get ``. + If the channel is not specified, it tries to determine the default one + by running ``amixer scontrols``. If that fails as well, it uses the + ``Playback`` channel, as that is the only channel on the EV3. + """ + + if channel is None: + channel = self._get_channel() + + out = os.popen(['/usr/bin/amixer', 'get', channel]).read() + m = re.search(r'\[(\d+)%\]', out) + if m: + return int(m.group(1)) + else: + raise Exception('Failed to parse output of ``amixer get {}``'.format(channel)) + + def play_song(self, song, tempo=120, delay=0.05): + """ Plays a song provided as a list of tuples containing the note name and its + value using music conventional notation instead of numerical values for frequency + and duration. + + It supports symbolic notes (e.g. ``A4``, ``D#3``, ``Gb5``) and durations (e.g. ``q``, ``h``). + You can also specify rests by using ``R`` instead of note pitch. + + For an exhaustive list of accepted note symbols and values, have a look at the ``_NOTE_FREQUENCIES`` + and ``_NOTE_VALUES`` private dictionaries in the source code. + + The value can be suffixed by modifiers: + + - a *divider* introduced by a ``/`` to obtain triplets for instance + (e.g. ``q/3`` for a triplet of eight note) + - a *multiplier* introduced by ``*`` (e.g. ``*1.5`` is a dotted note). + + Shortcuts exist for common modifiers: + + - ``3`` produces a triplet member note. For instance ``e3`` gives a triplet of eight notes, + i.e. 3 eight notes in the duration of a single quarter. You must ensure that 3 triplets + notes are defined in sequence to match the count, otherwise the result will not be the + expected one. + - ``.`` produces a dotted note, i.e. which duration is one and a half the base one. Double dots + are not currently supported. + + Example:: + + >>> # A long time ago in a galaxy far, + >>> # far away... + >>> from ev3dev2.sound import Sound + >>> spkr = Sound() + >>> spkr.play_song(( + >>> ('D4', 'e3'), # intro anacrouse + >>> ('D4', 'e3'), + >>> ('D4', 'e3'), + >>> ('G4', 'h'), # meas 1 + >>> ('D5', 'h'), + >>> ('C5', 'e3'), # meas 2 + >>> ('B4', 'e3'), + >>> ('A4', 'e3'), + >>> ('G5', 'h'), + >>> ('D5', 'q'), + >>> ('C5', 'e3'), # meas 3 + >>> ('B4', 'e3'), + >>> ('A4', 'e3'), + >>> ('G5', 'h'), + >>> ('D5', 'q'), + >>> ('C5', 'e3'), # meas 4 + >>> ('B4', 'e3'), + >>> ('C5', 'e3'), + >>> ('A4', 'h.'), + >>> )) + + .. important:: + + Only 4/4 signature songs are supported with respect to note durations. + + :param iterable[tuple(string,string)] song: the song + :param int tempo: the song tempo, given in quarters per minute + :param float delay: delay between notes (in seconds) + + :return: When python3 is used the spawn subprocess from ``subprocess.Popen`` is returned; ``None`` otherwise + + :raises ValueError: if invalid note in song or invalid play parameters + """ + if tempo <= 0: + raise ValueError('invalid tempo (%s)' % tempo) + if delay < 0: + raise ValueError('invalid delay (%s)' % delay) + + delay_ms = int(delay * 1000) + meas_duration_ms = 60000 / tempo * 4 # we only support 4/4 bars, hence "* 4" + + for (note, value) in song: + value = value.lower() + + if '/' in value: + base, factor = value.split('/') + factor = float(factor) + + elif '*' in value: + base, factor = value.split('*') + factor = float(factor) + + elif value.endswith('.'): + base = value[:-1] + factor = 1.5 + + elif value.endswith('3'): + base = value[:-1] + factor = float(2 / 3) + + else: + base = value + factor = 1.0 + + try: + duration_ms = meas_duration_ms * self._NOTE_VALUES[base] * factor + except KeyError: + raise ValueError('invalid note (%s)' % base) + + if note == "R": + sleep(duration_ms / 1000 + delay) + else: + freq = self._NOTE_FREQUENCIES[note.upper()] + self.beep('-f %d -l %d -D %d' % (freq, duration_ms, delay_ms)) + + #: Note frequencies. + #: + #: This dictionary gives the rounded frequency of a note specified by its + #: standard US abbreviation and its octave number (e.g. ``C3``). + #: Alterations use the ``#`` and ``b`` symbols, respectively for + #: *sharp* and *flat*, between the note code and the octave number (e.g. ``D#4``, ``Gb5``). + _NOTE_FREQUENCIES = _make_scales(( + ('C0', 16.35), + ('C#0/Db0', 17.32), + ('D0', 18.35), + ('D#0/Eb0', 19.45), # expanded in one entry per symbol by _make_scales + ('E0', 20.60), + ('F0', 21.83), + ('F#0/Gb0', 23.12), + ('G0', 24.50), + ('G#0/Ab0', 25.96), + ('A0', 27.50), + ('A#0/Bb0', 29.14), + ('B0', 30.87), + ('C1', 32.70), + ('C#1/Db1', 34.65), + ('D1', 36.71), + ('D#1/Eb1', 38.89), + ('E1', 41.20), + ('F1', 43.65), + ('F#1/Gb1', 46.25), + ('G1', 49.00), + ('G#1/Ab1', 51.91), + ('A1', 55.00), + ('A#1/Bb1', 58.27), + ('B1', 61.74), + ('C2', 65.41), + ('C#2/Db2', 69.30), + ('D2', 73.42), + ('D#2/Eb2', 77.78), + ('E2', 82.41), + ('F2', 87.31), + ('F#2/Gb2', 92.50), + ('G2', 98.00), + ('G#2/Ab2', 103.83), + ('A2', 110.00), + ('A#2/Bb2', 116.54), + ('B2', 123.47), + ('C3', 130.81), + ('C#3/Db3', 138.59), + ('D3', 146.83), + ('D#3/Eb3', 155.56), + ('E3', 164.81), + ('F3', 174.61), + ('F#3/Gb3', 185.00), + ('G3', 196.00), + ('G#3/Ab3', 207.65), + ('A3', 220.00), + ('A#3/Bb3', 233.08), + ('B3', 246.94), + ('C4', 261.63), + ('C#4/Db4', 277.18), + ('D4', 293.66), + ('D#4/Eb4', 311.13), + ('E4', 329.63), + ('F4', 349.23), + ('F#4/Gb4', 369.99), + ('G4', 392.00), + ('G#4/Ab4', 415.30), + ('A4', 440.00), + ('A#4/Bb4', 466.16), + ('B4', 493.88), + ('C5', 523.25), + ('C#5/Db5', 554.37), + ('D5', 587.33), + ('D#5/Eb5', 622.25), + ('E5', 659.25), + ('F5', 698.46), + ('F#5/Gb5', 739.99), + ('G5', 783.99), + ('G#5/Ab5', 830.61), + ('A5', 880.00), + ('A#5/Bb5', 932.33), + ('B5', 987.77), + ('C6', 1046.50), + ('C#6/Db6', 1108.73), + ('D6', 1174.66), + ('D#6/Eb6', 1244.51), + ('E6', 1318.51), + ('F6', 1396.91), + ('F#6/Gb6', 1479.98), + ('G6', 1567.98), + ('G#6/Ab6', 1661.22), + ('A6', 1760.00), + ('A#6/Bb6', 1864.66), + ('B6', 1975.53), + ('C7', 2093.00), + ('C#7/Db7', 2217.46), + ('D7', 2349.32), + ('D#7/Eb7', 2489.02), + ('E7', 2637.02), + ('F7', 2793.83), + ('F#7/Gb7', 2959.96), + ('G7', 3135.96), + ('G#7/Ab7', 3322.44), + ('A7', 3520.00), + ('A#7/Bb7', 3729.31), + ('B7', 3951.07), + ('C8', 4186.01), + ('C#8/Db8', 4434.92), + ('D8', 4698.63), + ('D#8/Eb8', 4978.03), + ('E8', 5274.04), + ('F8', 5587.65), + ('F#8/Gb8', 5919.91), + ('G8', 6271.93), + ('G#8/Ab8', 6644.88), + ('A8', 7040.00), + ('A#8/Bb8', 7458.62), + ('B8', 7902.13))) + + #: Common note values. + #: + #: See https://en.wikipedia.org/wiki/Note_value + #: + #: This dictionary provides the multiplier to be applied to de whole note duration + #: to obtain subdivisions, given the corresponding symbolic identifier: + #: + #: = =============================== + #: w whole note (UK: semibreve) + #: h half note (UK: minim) + #: q quarter note (UK: crotchet) + #: e eight note (UK: quaver) + #: s sixteenth note (UK: semiquaver) + #: = =============================== + #: + #: + #: Triplets can be obtained by dividing the corresponding reference by 3. + #: For instance, the note value of a eight triplet will be ``NOTE_VALUE['e'] / 3``. + #: It is simpler however to user the ``3`` modifier of notes, as supported by the + #: :py:meth:`Sound.play_song` method. + _NOTE_VALUES = { + 'w': 1., + 'h': 1. / 2, + 'q': 1. / 4, + 'e': 1. / 8, + 's': 1. / 16, + } diff --git a/ev3dev2/stopwatch.py b/ev3dev2/stopwatch.py new file mode 100644 index 0000000..60fba91 --- /dev/null +++ b/ev3dev2/stopwatch.py @@ -0,0 +1,141 @@ +""" +A StopWatch class for tracking the amount of time between events +""" + +from ev3dev2 import is_micropython + +if is_micropython(): + import utime +else: + import datetime as dt + + +def get_ticks_ms(): + if is_micropython(): + return utime.ticks_ms() + else: + return int(dt.datetime.timestamp(dt.datetime.now()) * 1000) + + +class StopWatchAlreadyStartedException(Exception): + """ + Exception raised when start() is called on a StopWatch which was already start()ed and not yet + stopped. + """ + pass + + +class StopWatch(object): + """ + A timer class which lets you start timing and then check the amount of time + elapsed. + """ + def __init__(self, desc=None): + """ + Initializes the StopWatch but does not start it. + + desc: + A string description to print when stringifying. + """ + self.desc = desc + self._start_time = None + self._stopped_total_time = None + + def __str__(self): + name = self.desc if self.desc is not None else self.__class__.__name__ + return "{}: {}".format(name, self.hms_str) + + def start(self): + """ + Starts the timer. If the timer is already running, resets it. + + Raises a :py:class:`ev3dev2.stopwatch.StopWatchAlreadyStartedException` if already started. + """ + if self.is_started: + raise StopWatchAlreadyStartedException() + + self._stopped_total_time = None + self._start_time = get_ticks_ms() + + def stop(self): + """ + Stops the timer. The time value of this Stopwatch is paused and will not continue increasing. + """ + if self._start_time is None: + return + + self._stopped_total_time = get_ticks_ms() - self._start_time + self._start_time = None + + def reset(self): + """ + Resets the timer and leaves it stopped. + """ + self._start_time = None + self._stopped_total_time = None + + def restart(self): + """ + Resets and then starts the timer. + """ + self.reset() + self.start() + + @property + def is_started(self): + """ + True if the StopWatch has been started but not stoped (i.e., it's currently running), false otherwise. + """ + return self._start_time is not None + + @property + def value_ms(self): + """ + Returns the value of the stopwatch in milliseconds + """ + if self._stopped_total_time is not None: + return self._stopped_total_time + + return get_ticks_ms() - self._start_time if self._start_time is not None else 0 + + @property + def value_secs(self): + """ + Returns the value of the stopwatch in seconds + """ + return self.value_ms / 1000 + + @property + def value_hms(self): + """ + Returns this StopWatch's elapsed time as a tuple + ``(hours, minutes, seconds, milliseconds)``. + """ + (hours, x) = divmod(int(self.value_ms), 3600000) + (mins, x) = divmod(x, 60000) + (secs, x) = divmod(x, 1000) + return hours, mins, secs, x + + @property + def hms_str(self): + """ + Returns the stringified value of the stopwatch in HH:MM:SS.msec format + """ + return '%02d:%02d:%02d.%03d' % self.value_hms + + def is_elapsed_ms(self, duration_ms): + """ + Returns True if this timer has measured at least ``duration_ms`` + milliseconds. + Otherwise, returns False. If ``duration_ms`` is None, returns False. + """ + + return duration_ms is not None and self.value_ms >= duration_ms + + def is_elapsed_secs(self, duration_secs): + """ + Returns True if this timer has measured at least ``duration_secs`` seconds. + Otherwise, returns False. If ``duration_secs`` is None, returns False. + """ + + return duration_secs is not None and self.value_secs >= duration_secs diff --git a/ev3dev2/unit.py b/ev3dev2/unit.py new file mode 100644 index 0000000..d7bbf0f --- /dev/null +++ b/ev3dev2/unit.py @@ -0,0 +1,201 @@ +# ----------------------------------------------------------------------------- +# Copyright (c) 2015 Ralph Hempel +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +# ----------------------------------------------------------------------------- + +import sys + +if sys.version_info < (3, 4): + raise SystemError('Must be using Python 3.4 or higher') + +CENTIMETER_MM = 10 +DECIMETER_MM = 100 +METER_MM = 1000 +INCH_MM = 25.4 +FOOT_MM = 304.8 +YARD_MM = 914.4 +STUD_MM = 8 + + +class DistanceValue(object): + """ + A base class for other unit types. Don't use this directly; instead, see + :class:`DistanceMillimeters`, :class:`DistanceCentimeters`, :class:`DistanceDecimeters``, :class:`DistanceMeters`, + :class:`DistanceInches`, :class:`DistanceFeet`, :class:`DistanceYards` and :class:`DistanceStuds`. + """ + + # This allows us to sort lists of DistanceValue objects + def __lt__(self, other): + return self.mm < other.mm + + def __rmul__(self, other): + return self.__mul__(other) + + +class DistanceMillimeters(DistanceValue): + """ + Distance in millimeters + """ + def __init__(self, millimeters): + self.millimeters = millimeters + + def __str__(self): + return str(self.millimeters) + "mm" + + def __mul__(self, other): + assert isinstance(other, (float, int)), "{} can only be multiplied by an int or float".format(self) + return DistanceMillimeters(self.millimeters * other) + + @property + def mm(self): + return self.millimeters + + +class DistanceCentimeters(DistanceValue): + """ + Distance in centimeters + """ + def __init__(self, centimeters): + self.centimeters = centimeters + + def __str__(self): + return str(self.centimeters) + "cm" + + def __mul__(self, other): + assert isinstance(other, (float, int)), "{} can only be multiplied by an int or float".format(self) + return DistanceCentimeters(self.centimeters * other) + + @property + def mm(self): + return self.centimeters * CENTIMETER_MM + + +class DistanceDecimeters(DistanceValue): + """ + Distance in decimeters + """ + def __init__(self, decimeters): + self.decimeters = decimeters + + def __str__(self): + return str(self.decimeters) + "dm" + + def __mul__(self, other): + assert isinstance(other, (float, int)), "{} can only be multiplied by an int or float".format(self) + return DistanceDecimeters(self.decimeters * other) + + @property + def mm(self): + return self.decimeters * DECIMETER_MM + + +class DistanceMeters(DistanceValue): + """ + Distance in meters + """ + def __init__(self, meters): + self.meters = meters + + def __str__(self): + return str(self.meters) + "m" + + def __mul__(self, other): + assert isinstance(other, (float, int)), "{} can only be multiplied by an int or float".format(self) + return DistanceMeters(self.meters * other) + + @property + def mm(self): + return self.meters * METER_MM + + +class DistanceInches(DistanceValue): + """ + Distance in inches + """ + def __init__(self, inches): + self.inches = inches + + def __str__(self): + return str(self.inches) + "in" + + def __mul__(self, other): + assert isinstance(other, (float, int)), "{} can only be multiplied by an int or float".format(self) + return DistanceInches(self.inches * other) + + @property + def mm(self): + return self.inches * INCH_MM + + +class DistanceFeet(DistanceValue): + """ + Distance in feet + """ + def __init__(self, feet): + self.feet = feet + + def __str__(self): + return str(self.feet) + "ft" + + def __mul__(self, other): + assert isinstance(other, (float, int)), "{} can only be multiplied by an int or float".format(self) + return DistanceFeet(self.feet * other) + + @property + def mm(self): + return self.feet * FOOT_MM + + +class DistanceYards(DistanceValue): + """ + Distance in yards + """ + def __init__(self, yards): + self.yards = yards + + def __str__(self): + return str(self.yards) + "yd" + + def __mul__(self, other): + assert isinstance(other, (float, int)), "{} can only be multiplied by an int or float".format(self) + return DistanceYards(self.yards * other) + + @property + def mm(self): + return self.yards * YARD_MM + + +class DistanceStuds(DistanceValue): + """ + Distance in studs + """ + def __init__(self, studs): + self.studs = studs + + def __str__(self): + return str(self.studs) + "stud" + + def __mul__(self, other): + assert isinstance(other, (float, int)), "{} can only be multiplied by an int or float".format(self) + return DistanceStuds(self.studs * other) + + @property + def mm(self): + return self.studs * STUD_MM diff --git a/ev3dev2/wheel.py b/ev3dev2/wheel.py new file mode 100644 index 0000000..16ccf43 --- /dev/null +++ b/ev3dev2/wheel.py @@ -0,0 +1,73 @@ +#!/usr/bin/env python3 +""" +Wheel and Rim classes + +A great reference when adding new wheels is http://wheels.sariel.pl/ +""" +from math import pi + + +class Wheel(object): + """ + A base class for various types of wheels, tires, etc. All units are in mm. + + One scenario where one of the child classes below would be used is when the + user needs their robot to drive at a specific speed or drive for a specific + distance. Both of those calculations require the circumference of the wheel + of the robot. + + Example: + + .. code:: python + + from ev3dev2.wheel import EV3Tire + + tire = EV3Tire() + + # calculate the number of rotations needed to travel forward 500 mm + rotations_for_500mm = 500 / tire.circumference_mm + """ + def __init__(self, diameter_mm, width_mm): + self.diameter_mm = float(diameter_mm) + self.width_mm = float(width_mm) + self.circumference_mm = diameter_mm * pi + + @property + def radius_mm(self): + return float(self.diameter_mm / 2) + + +class EV3Rim(Wheel): + """ + part number 56145 + comes in set 31313 + """ + def __init__(self): + Wheel.__init__(self, 30, 20) + + +class EV3Tire(Wheel): + """ + part number 44309 + comes in set 31313 + """ + def __init__(self): + Wheel.__init__(self, 43.2, 21) + + +class EV3EducationSetRim(Wheel): + """ + part number 56908 + comes in set 45544 + """ + def __init__(self): + Wheel.__init__(self, 43, 26) + + +class EV3EducationSetTire(Wheel): + """ + part number 41897 + comes in set 45544 + """ + def __init__(self): + Wheel.__init__(self, 56, 28) diff --git a/git_version.py b/git_version.py index 54d9b85..2744309 100644 --- a/git_version.py +++ b/git_version.py @@ -1,9 +1,11 @@ from subprocess import Popen, PIPE -import os, sys +import os +import sys pyver = sys.version_info -#---------------------------------------------------------------------------- + +# ---------------------------------------------------------------------------- # Get version string from git # # Author: Douglas Creager @@ -11,16 +13,15 @@ # # PEP 386 adaptation from # https://gist.github.com/ilogue/2567778/f6661ea2c12c070851b2dfb4da8840a6641914bc -#---------------------------------------------------------------------------- +# ---------------------------------------------------------------------------- def call_git_describe(abbrev=4): try: - p = Popen(['git', 'describe', '--abbrev=%d' % abbrev], - stdout=PIPE, stderr=PIPE) + p = Popen(['git', 'describe', '--exclude', 'ev3dev-*', '--abbrev=%d' % abbrev], stdout=PIPE, stderr=PIPE) p.stderr.close() line = p.stdout.readlines()[0] return line.strip().decode('utf8') - except: + except Exception: return None @@ -29,7 +30,7 @@ def read_release_version(): with open('{}/RELEASE-VERSION'.format(os.path.dirname(__file__)), 'r') as f: version = f.readlines()[0] return version.strip() - except: + except Exception: return None @@ -42,7 +43,7 @@ def pep386adapt(version): # adapt git-describe version to be in line with PEP 386 parts = version.split('-') if len(parts) > 1: - parts[-2] = 'post'+parts[-2] + parts[-2] = 'post' + parts[-2] version = '.'.join(parts[:-1]) return version @@ -59,7 +60,7 @@ def git_version(abbrev=4): if version is None: version = release_version else: - #adapt to PEP 386 compatible versioning scheme + # adapt to PEP 386 compatible versioning scheme version = pep386adapt(version) # If we still don't have anything, that's an error. @@ -71,10 +72,9 @@ def git_version(abbrev=4): if version != release_version: write_release_version(version) - # Update the ev3dev/version.py - with open('{}/ev3dev/version.py'.format(os.path.dirname(__file__)), 'w') as f: + # Update the ev3dev2/version.py + with open('{}/ev3dev2/version.py'.format(os.path.dirname(__file__)), 'w') as f: f.write("__version__ = '{}'".format(version)) # Finally, return the current version. return version - diff --git a/setup.cfg b/setup.cfg index f797a99..be98747 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,2 +1,2 @@ [sdist_dsc] -package: python-ev3dev +package: python-ev3dev2 diff --git a/setup.py b/setup.py index 88c0791..6decb29 100644 --- a/setup.py +++ b/setup.py @@ -1,18 +1,21 @@ from setuptools import setup from git_version import git_version +from os import path +this_directory = path.abspath(path.dirname(__file__)) +with open(path.join(this_directory, 'README.rst'), encoding='utf-8') as f: + long_description = f.read() -setup( - name='python-ev3dev', - version=git_version(), - description='Python language bindings for ev3dev', - author='Ralph Hempel', - author_email='rhempel@hempeldesigngroup.com', - license='MIT', - url='https://github.com/rhempel/ev3dev-lang-python', - include_package_data=True, - packages=['ev3dev', 'ev3dev.fonts'], - package_data={'': ['*.pil', '*.pbm']}, - install_requires=['Pillow'] - ) - +setup(name='python-ev3dev2', + version=git_version(), + description='v2.x Python language bindings for ev3dev', + author='ev3dev Python team', + author_email='python-team@ev3dev.org', + license='MIT', + url='https://github.com/ev3dev/ev3dev-lang-python', + include_package_data=True, + long_description=long_description, + long_description_content_type='text/x-rst', + packages=['ev3dev2', 'ev3dev2.fonts', 'ev3dev2.sensor', 'ev3dev2.control', 'ev3dev2._platform'], + package_data={'': ['*.pil', '*.pbm']}, + install_requires=['Pillow']) diff --git a/spec_version.py b/spec_version.py deleted file mode 100644 index 0daacae..0000000 --- a/spec_version.py +++ /dev/null @@ -1,8 +0,0 @@ -# ~autogen spec_version -spec_version = "1.2.0" -kernel_versions = { - "11-ev3dev" - "11-rc1-ev3dev" - } - -# ~autogen diff --git a/templates/autogen-header.liquid b/templates/autogen-header.liquid deleted file mode 100644 index 0315e85..0000000 --- a/templates/autogen-header.liquid +++ /dev/null @@ -1 +0,0 @@ -# Sections of the following code were auto-generated based on spec v{{ meta.version }}{% if meta.specRevision %}, rev {{meta.specRevision}}{% endif %} diff --git a/templates/button-class.liquid b/templates/button-class.liquid deleted file mode 100644 index b54c9ea..0000000 --- a/templates/button-class.liquid +++ /dev/null @@ -1,9 +0,0 @@ -class Button(ButtonBase): - - """{% for line in currentClass.description %} - {{ line }}{% endfor %} - - This implementation depends on the availability of the EVIOCGKEY ioctl - to be able to read the button state buffer. See Linux kernel source - in /include/uapi/linux/input.h for details. - """ diff --git a/templates/button-property.liquid b/templates/button-property.liquid deleted file mode 100644 index 01b5651..0000000 --- a/templates/button-property.liquid +++ /dev/null @@ -1,22 +0,0 @@ -{% for instance in currentClass.instances %} - @staticmethod - def on_{{ instance.name }}(state): - """ - This handler is called by `process()` whenever state of '{{ instance.name }}' button - has changed since last `process()` call. `state` parameter is the new - state of the button. - """ - pass -{% endfor %} - - _buttons = { -{% for instance in currentClass.instances %} '{{ instance.name }}': {'name': '{{ currentClass.systemPath }}/{{ instance.systemName }}', 'value': {{ instance.systemValue }}}, -{% endfor %} } -{% for instance in currentClass.instances %} - @property - def {{ instance.name }}(self): - """ - Check if '{{ instance.name }}' button is pressed. - """ - return '{{ instance.name }}' in self.buttons_pressed -{% endfor %} diff --git a/templates/doc-special-sensor-classes.liquid b/templates/doc-special-sensor-classes.liquid deleted file mode 100644 index 71fabd2..0000000 --- a/templates/doc-special-sensor-classes.liquid +++ /dev/null @@ -1,11 +0,0 @@ -{% for classPair in specialSensorTypes %}{% -assign class = classPair[1] %} -{{ class.friendlyName }} -######################## - -.. autoclass:: {{ class.friendlyName | camel_case | capitalize }} - :members: - :show-inheritance: - - -{% endfor %} diff --git a/templates/generic-class-slots.liquid b/templates/generic-class-slots.liquid deleted file mode 100644 index 068cc13..0000000 --- a/templates/generic-class-slots.liquid +++ /dev/null @@ -1,4 +0,0 @@ -{% for prop in currentClass.systemProperties %}{% - assign prop_name = prop.name | downcase | underscore_spaces %} - '_{{ prop_name }}',{% -endfor %} diff --git a/templates/generic-class.liquid b/templates/generic-class.liquid deleted file mode 100644 index 67b7272..0000000 --- a/templates/generic-class.liquid +++ /dev/null @@ -1,52 +0,0 @@ -{% -assign class_name = currentClass.friendlyName | camel_case | capitalize %}{% -if currentClass.systemDeviceNameConvention %}{% - assign device_name_convention = currentClass.systemDeviceNameConvention | replace: '\{\d\}', '*' %}{% -else %}{% - assign device_name_convention = '*' %}{% -endif %}{% -if currentClass.inheritance %}{% - assign base_class = currentClass.inheritance | camel_case | capitalize %}{% -else %}{% - assign base_class = 'Device' %}{% -endif%}{% -assign driver_name = "" %}{% -if currentClass.driverName %}{% - for name in currentClass.driverName %}{% - capture driver_name %}{{ driver_name }}, '{{name}}'{% endcapture %}{% - endfor %}{% - capture driver_name %} driver_name=[{{ driver_name | remove_first:', ' }}],{% endcapture %}{% -endif %} -class {{ class_name }}({{ base_class }}): - - """{% -for line in currentClass.description %}{% - if line %} - {{ line }}{% - else %} -{% endif %}{% -endfor %} - """ -{% if currentClass.inheritance %} - SYSTEM_CLASS_NAME = {{ base_class }}.SYSTEM_CLASS_NAME{% - if currentClass.systemDeviceNameConvention %} - SYSTEM_DEVICE_NAME_CONVENTION = '{{ device_name_convention }}'{% - else %} - SYSTEM_DEVICE_NAME_CONVENTION = {{ base_class }}.SYSTEM_DEVICE_NAME_CONVENTION{% - endif %} -{% else %} - SYSTEM_CLASS_NAME = '{{ currentClass.systemClassName }}' - SYSTEM_DEVICE_NAME_CONVENTION = '{{ device_name_convention }}' -{% endif %} - def __init__(self, address=None, name_pattern=SYSTEM_DEVICE_NAME_CONVENTION, name_exact=False, **kwargs): -{% if currentClass.inheritance %} - super({{ class_name }}, self).__init__(address, name_pattern, name_exact,{{ driver_name }} **kwargs) -{% else %} - if address is not None: - kwargs['address'] = address - super({{ class_name }}, self).__init__(self.SYSTEM_CLASS_NAME, name_pattern, name_exact,{{ driver_name }} **kwargs) -{% endif %}{% -for prop in currentClass.systemProperties %}{% - assign prop_name = prop.name | downcase | underscore_spaces %} - self._{{ prop_name }} = None{% -endfor %} diff --git a/templates/generic-get-set.liquid b/templates/generic-get-set.liquid deleted file mode 100644 index 0a1bb92..0000000 --- a/templates/generic-get-set.liquid +++ /dev/null @@ -1,38 +0,0 @@ -{% assign class_name = currentClass.friendlyName | downcase | underscore_spaces %}{% -for prop in currentClass.systemProperties %}{% - assign prop_name = prop.name | downcase | underscore_spaces %}{% - if class_name != 'led' or prop_name != 'trigger' and prop_name != 'delay_on' and prop_name != 'delay_off' %}{% - assign getter = prop.type %}{% - assign setter = prop.type %}{% - if prop.type == 'string array' %}{% - assign getter = 'set' %}{% - elsif prop.type == 'string selector' %}{% - assign getter = 'from_set' %}{% - assign setter = 'string' %}{% - endif %} - @property - def {{ prop_name }}(self): - """{% - for line in prop.description %}{% - if line %} - {{ line }}{% - else %} -{% endif %}{% - endfor %} - """{% - if prop.readAccess %} - self._{{ prop_name }}, value = self.get_attr_{{ getter }}(self._{{ prop_name }}, '{{prop.systemName}}') - return value{% - else %} - raise Exception("{{prop_name}} is a write-only property!"){% - endif %}{% - if prop.writeAccess %} - - @{{prop_name}}.setter - def {{ prop_name }}(self, value): - self._{{ prop_name }} = self.set_attr_{{ setter }}(self._{{ prop_name }}, '{{prop.systemName}}', value){% -endif%}{% unless forloop.last %} -{% endunless %}{% -endif %}{% -endfor %} - diff --git a/templates/generic-helper-function.liquid b/templates/generic-helper-function.liquid deleted file mode 100644 index 84197c4..0000000 --- a/templates/generic-helper-function.liquid +++ /dev/null @@ -1,16 +0,0 @@ -{% assign class_name = currentClass.friendlyName | camel_case | capitalize %}{% -for propval in currentClass.propertyValues %}{% - if propval.propertyName == "Command" %}{% - for value in propval.values %}{% - assign cmd = value.name | replace:'-','_' %} - def {{ cmd }}( self, **kwargs ): - """{% - for line in value.description %}{{line}} - {% endfor %}""" - for key in kwargs: - setattr(self, key, kwargs[key]) - self.command = '{{value.name}}' -{% - endfor %}{% - endif %}{% -endfor %} diff --git a/templates/generic-property-value.liquid b/templates/generic-property-value.liquid deleted file mode 100644 index 0247a28..0000000 --- a/templates/generic-property-value.liquid +++ /dev/null @@ -1,10 +0,0 @@ -{% for prop in currentClass.propertyValues %}{% - assign className = currentClass.friendlyName | downcase | underscore_spaces %}{% - assign propName = prop.propertyName | upcase | underscore_spaces %}{% - for value in prop.values %}{% - for line in value.description %} - #: {{ line }}{% - endfor %} - {{ propName }}_{{ value.name | upcase | underscore_non_wc }} = '{{ value.name }}' -{% endfor %}{% -endfor %} diff --git a/templates/led-colors.liquid b/templates/led-colors.liquid deleted file mode 100644 index fec2e28..0000000 --- a/templates/led-colors.liquid +++ /dev/null @@ -1,60 +0,0 @@ -{% for instance in currentClass.instances %}{% - assign instanceName = instance.name | downcase | underscore_spaces %} - {{instanceName}} = Led(name_pattern='{{instance.systemName}}'){% -endfor %} -{% for group in currentClass.groups %}{% - assign groupName = group.name | upcase | underscore_spaces %}{% - assign ledNames = '' %}{% - for name in group.entries %}{% - capture ledNames %}{{ ledNames }}{{ name | downcase | underscore_spaces }}, {% - endcapture %}{% - endfor %} - {{ groupName }} = ( {{ ledNames }}){% -endfor %} -{% for color in currentClass.colors %}{% - assign colorName = color.name | upcase | underscore_spaces %}{% - assign mixValues = '' %}{% - for value in color.value %}{% - capture mixValues %}{{ mixValues }}{{ value }}, {% - endcapture %}{% - endfor %} - {{ colorName }} = ( {{ mixValues }}){% -endfor %} - - @staticmethod - def set_color(group, color, pct=1): - """ - Sets brigthness of leds in the given group to the values specified in - color tuple. When percentage is specified, brightness of each led is - reduced proportionally. - - Example:: - - Leds.set_color(LEFT, AMBER) - """ - for l, v in zip(group, color): - l.brightness_pct = v * pct - - @staticmethod - def set(group, **kwargs): - """ - Set attributes for each led in group. - - Example:: - - Leds.set(LEFT, brightness_pct=0.5, trigger='timer') - """ - for led in group: - for k in kwargs: - setattr(led, k, kwargs[k]) - - @staticmethod - def all_off(): - """ - Turn all leds off - """{% -for instance in currentClass.instances %}{% - assign instanceName = instance.name | downcase | underscore_spaces %} - Leds.{{instanceName}}.brightness = 0{% -endfor %} - diff --git a/templates/motor_commands.liquid b/templates/motor_commands.liquid deleted file mode 100644 index 5ffd6c2..0000000 --- a/templates/motor_commands.liquid +++ /dev/null @@ -1,17 +0,0 @@ -{% assign class_name = currentClass.friendlyName | camel_case | capitalize %}{% -for prop in currentClass.propertyValues %}{% - assign propName = prop.propertyName | upcase | underscore_spaces %}{% - if prop.propertyName == "Command" %}{% - for value in prop.values %}{% - assign cmd = value.name | replace:'-','_' %} - def {{ cmd }}(self, **kwargs): - """{% - for line in value.description %}{{line}} - {% endfor %}""" - for key in kwargs: - setattr(self, key, kwargs[key]) - self.command = self.{{ propName }}_{{ value.name | upcase | underscore_non_wc }} -{% - endfor %}{% - endif %}{% -endfor %} diff --git a/templates/motor_states.liquid b/templates/motor_states.liquid deleted file mode 100644 index ca9fc18..0000000 --- a/templates/motor_states.liquid +++ /dev/null @@ -1,13 +0,0 @@ -{% assign class_name = currentClass.friendlyName | camel_case | capitalize %}{% -for prop in currentClass.propertyValues %}{% - if prop.propertyName == "State" %}{% - for state in prop.values %} - @property - def is_{{ state.name }}(self): - """{% - for line in state.description %}{{line}} - {% endfor %}""" - return self.STATE_{{ state.name | upcase }} in self.state -{% endfor %}{% -endif %}{% -endfor %} diff --git a/templates/remote-control.liquid b/templates/remote-control.liquid deleted file mode 100644 index bb715c4..0000000 --- a/templates/remote-control.liquid +++ /dev/null @@ -1,29 +0,0 @@ -class RemoteControl(ButtonBase): - """{% for line in currentClass.description %} - {{ line }}{% endfor %} - """ - - _BUTTON_VALUES = { -{% for v in currentClass.values -%} {{ v.value }}: [{% - for s in v.state - %}'{{ s | downcase | underscore_spaces }}'{% - unless forloop.last %}, {% - endunless %}{% - endfor %}]{% unless forloop.last %}, -{% endunless %}{% -endfor %} - } -{% for b in currentClass.buttons %} - #: Handles ``{{ b.name }}`` events. - on_{{ b.name | downcase | underscore_spaces }} = None -{% endfor %} -{% for b in currentClass.buttons %}{% - assign name = b.name | downcase | underscore_spaces %} - @property - def {{ name }}(self): - """ - Checks if `{{ name }}` button is pressed. - """ - return '{{ name }}' in self.buttons_pressed -{% endfor %} diff --git a/templates/spec_version.liquid b/templates/spec_version.liquid deleted file mode 100644 index 4e2d028..0000000 --- a/templates/spec_version.liquid +++ /dev/null @@ -1,6 +0,0 @@ -spec_version = "{{ meta.version }}{% if meta.specRevision %}-r{{ meta.specRevision }}{% endif %}" -kernel_versions = { {% - for kernel in meta.supportedKernel.kernels %} - "{{ kernel }}"{% - endfor %} - } diff --git a/templates/special-sensors.liquid b/templates/special-sensors.liquid deleted file mode 100644 index 8049458..0000000 --- a/templates/special-sensors.liquid +++ /dev/null @@ -1,93 +0,0 @@ -{% for currentClassMetadata in specialSensorTypes %}{% - assign currentClass = currentClassMetadata[1] %}{% -assign class_name = currentClass.friendlyName | camel_case | capitalize %}{% -if currentClass.systemDeviceNameConvention %}{% - assign device_name_convention = currentClass.systemDeviceNameConvention | replace: '\{\d\}', '*' %}{% -else %}{% - assign device_name_convention = '*' %}{% -endif %}{% -if currentClass.inheritance %}{% - assign base_class = currentClass.inheritance | camel_case | capitalize %}{% -else %}{% - assign base_class = 'Device' %}{% -endif%}{% -assign driver_name = "" %}{% -if currentClass.driverName %}{% - for name in currentClass.driverName %}{% - capture driver_name %}{{ driver_name }}, '{{name}}'{% endcapture %}{% - endfor %}{% - capture driver_name %} driver_name=[{{ driver_name | remove_first:', ' }}],{% endcapture %}{% -endif %} -class {{ class_name }}({{ base_class }}): - - """{% -for line in currentClass.description %}{% - if line %} - {{ line }}{% - else %} -{% endif %}{% -endfor %} - """ - - __slots__ = ['auto_mode'] -{% if currentClass.inheritance %} - SYSTEM_CLASS_NAME = {{ base_class }}.SYSTEM_CLASS_NAME - SYSTEM_DEVICE_NAME_CONVENTION = {{ base_class }}.SYSTEM_DEVICE_NAME_CONVENTION -{% else %} - SYSTEM_CLASS_NAME = '{{ currentClass.systemClassName }}' - SYSTEM_DEVICE_NAME_CONVENTION = '{{ device_name_convention }}' -{% endif %} - def __init__(self, address=None, name_pattern=SYSTEM_DEVICE_NAME_CONVENTION, name_exact=False, **kwargs): - super({{ class_name }}, self).__init__(address, name_pattern, name_exact,{{ driver_name }} **kwargs) - self.auto_mode = True - -{% for prop in currentClass.propertyValues %}{% - assign className = currentClass.friendlyName | downcase | underscore_spaces %}{% - assign propName = prop.propertyName | upcase | underscore_spaces %}{% - for value in prop.values %}{% - for line in value.description %} - #: {{ line }}{% - endfor %}{% - if value.value %}{% - if value.type == 'int' %}{% - capture val %}{{ value.value }}{% endcapture %}{% - else %}{% - capture val %}'{{ value.value }}'{% endcapture %}{% - endif %}{% - else %}{% - capture val %}'{{ value.name }}'{% endcapture %}{% - endif %} - {{ propName }}_{{ value.name | upcase | underscore_non_wc }} = {{ val }} -{% endfor %}{% -endfor %} -{% for prop in currentClass.propertyValues %}{% -assign propName = prop.propertyName | upcase | underscore_spaces%} - {{ propName }}S = ({% - for value in prop.values %} - '{{ value.name }}',{% - endfor %} - ) -{%endfor %} -{% for mapping in currentClass.sensorValueMappings %}{% - assign name = mapping.name | downcase | underscore_spaces %}{% - assign mode = mapping.requiredMode | upcase | underscore_non_wc %} - @property - def {{ name }}(self): - """{% - for line in mapping.description %}{% - if line %} - {{ line }}{% - else %} -{% endif %}{% - endfor %} - """ - - if self.auto_mode: - self.mode = self.MODE_{{ mode }} - - return {% - for value_index in mapping.sourceValue - %}self.value({{ value_index }}){% if mapping.type contains 'float' %} * self._scale('{{ mode }}'){% endif %}{% unless forloop.last %}, {% endunless %}{% - endfor %} -{% endfor %}{% -endfor %} diff --git a/tests/README.md b/tests/README.md index b8f0017..6f2f00c 100644 --- a/tests/README.md +++ b/tests/README.md @@ -1,8 +1,45 @@ -Commands used to copy the /sys/class node: +# fake-sys directory +The tests require the fake-sys directory which comes from +https://github.com/ddemidov/ev3dev-lang-fake-sys -```sh -node=lego-sensor/sensor0 -mkdir -p ./${node} -# Copy contents of special files, do not follow symlinks: -cp -P --copy-contents -r /sys/class/${node}/* ./${node}/ +If you have already cloned the ev3dev-lang-python repo but do not have the +`fake-sys` directory use `git submodule init` to get it. If you have not +already cloned the ev3dev-lang-python repo you can use the `--recursive` option +when you git clone. Example: + +``` +$ git clone --recursive https://github.com/ev3dev/ev3dev-lang-python.git +``` + +# Running Tests with CPython (default) +To run the API tests: +``` +$ cd ev3dev-lang-python/ +$ chmod -R g+rw ./tests/fake-sys/devices/**/* +$ python3 -W ignore::ResourceWarning tests/api_tests.py +``` + +To run the docs, docstring, etc tests: +``` +$ sudo apt-get install python3-sphinx python3-sphinx-bootstrap-theme python3-recommonmark +$ cd ev3dev-lang-python/ +$ sudo sphinx-build -nW -b html ./docs/ ./docs/_build/html +``` + +If on Windows, the `chmod` command can be ignored. + +# Running Tests with Micropython + +This library also supports a subset of functionality on [Micropython](http://micropython.org/). + +You can follow the instructions on [the Micropython wiki](https://github.com/micropython/micropython/wiki/Getting-Started) +or check out our [installation script for Travis CI workers](https://github.com/ev3dev/ev3dev-lang-python/blob/ev3dev-stretch/.travis/install-micropython.sh) +to get Micropython installed. If following the official instructions, +make sure you install the relevant micropython-lib modules listed in the linked script as well. + +Once Micropython is installed, you can run the tests with: + +``` +$ cd ev3dev-lang-python/ +$ micropython tests/api_tests.py ``` diff --git a/tests/api_tests.py b/tests/api_tests.py index 6edb48e..7ff4533 100755 --- a/tests/api_tests.py +++ b/tests/api_tests.py @@ -1,57 +1,127 @@ -#!/usr/bin/env python -import unittest, sys, os +#!/usr/bin/env python3 +import unittest +import sys +import os.path +import os FAKE_SYS = os.path.join(os.path.dirname(__file__), 'fake-sys') +os.environ["FAKE_SYS"] = "1" sys.path.append(FAKE_SYS) sys.path.append(os.path.join(os.path.dirname(__file__), '..')) -from populate_arena import populate_arena -from clean_arena import clean_arena +from populate_arena import populate_arena # noqa: E402 +from clean_arena import clean_arena # noqa: E402 -import ev3dev.core as ev3 +import ev3dev2 # noqa: E402 +import ev3dev2.stopwatch # noqa: E402 +from ev3dev2.motor import \ + OUTPUT_A, OUTPUT_B, \ + Motor, MediumMotor, LargeMotor, \ + MoveTank, MoveSteering, MoveJoystick, \ + SpeedPercent, SpeedDPM, SpeedDPS, SpeedRPM, SpeedRPS, SpeedNativeUnits # noqa: E402 +from ev3dev2.sensor.lego import InfraredSensor # noqa: E402 +from ev3dev2.stopwatch import StopWatch, StopWatchAlreadyStartedException # noqa: E402 +from ev3dev2.unit import ( # noqa: E402 + DistanceMillimeters, DistanceCentimeters, DistanceDecimeters, DistanceMeters, DistanceInches, DistanceFeet, + DistanceYards, DistanceStuds) + +ev3dev2.Device.DEVICE_ROOT_PATH = os.path.join(FAKE_SYS, 'arena') + +_internal_set_attribute = ev3dev2.Device._set_attribute + + +def _set_attribute(self, attribute, name, value): + # Follow the text with a newline to separate new content from stuff that + # already existed in the buffer. On the real device we're writing to sysfs + # attributes where there isn't any persistent buffer, but in the test + # environment they're normal files on disk which retain previous data. + attribute = _internal_set_attribute(self, attribute, name, value) + attribute.write(b'\n') + return attribute + + +ev3dev2.Device._set_attribute = _set_attribute + +_internal_get_attribute = ev3dev2.Device._get_attribute + + +def _get_attribute(self, attribute, name): + # Split on newline delimiter; see _set_attribute above + attribute, value = _internal_get_attribute(self, attribute, name) + return attribute, value.split('\n', 1)[0] + + +ev3dev2.Device._get_attribute = _get_attribute + + +def dummy_wait(self, cond, timeout=None): + pass + + +Motor.wait = dummy_wait + +# for StopWatch +mock_ticks_ms = 0 + + +def _mock_get_ticks_ms(): + return mock_ticks_ms + + +ev3dev2.stopwatch.get_ticks_ms = _mock_get_ticks_ms + + +def set_mock_ticks_ms(value): + global mock_ticks_ms + mock_ticks_ms = value -ev3.Device.DEVICE_ROOT_PATH = os.path.join(FAKE_SYS, 'arena') class TestAPI(unittest.TestCase): + def setUp(self): + # micropython does not have _testMethodName + try: + print("\n\n{}\n{}".format(self._testMethodName, "=" * len(self._testMethodName, ))) + except AttributeError: + pass + + # ensure tests don't depend on order based on StopWatch tick state + set_mock_ticks_ms(0) + def test_device(self): clean_arena() - populate_arena({'medium_motor' : [0, 'outA'], 'infrared_sensor' : [0, 'in1']}) + populate_arena([('medium_motor', 0, 'outA'), ('infrared_sensor', 0, 'in1')]) + + ev3dev2.Device('tacho-motor', 'motor*') - d = ev3.Device('tacho-motor', 'motor*') - self.assertTrue(d.connected) + ev3dev2.Device('tacho-motor', 'motor0') - d = ev3.Device('tacho-motor', 'motor0') - self.assertTrue(d.connected) + ev3dev2.Device('tacho-motor', 'motor*', driver_name='lego-ev3-m-motor') - d = ev3.Device('tacho-motor', 'motor*', driver_name='lego-ev3-m-motor') - self.assertTrue(d.connected) + ev3dev2.Device('tacho-motor', 'motor*', address='outA') - d = ev3.Device('tacho-motor', 'motor*', address='outA') - self.assertTrue(d.connected) + with self.assertRaises(ev3dev2.DeviceNotFound): + ev3dev2.Device('tacho-motor', 'motor*', address='outA', driver_name='not-valid') - d = ev3.Device('tacho-motor', 'motor*', address='outA', driver_name='not-valid') - self.assertTrue(not d.connected) + with self.assertRaises(ev3dev2.DeviceNotFound): + ev3dev2.Device('tacho-motor', 'motor*', address='this-does-not-exist') - d = ev3.Device('lego-sensor', 'sensor*') - self.assertTrue(d.connected) + ev3dev2.Device('lego-sensor', 'sensor*') - d = ev3.Device('this-does-not-exist') - self.assertFalse(d.connected) + with self.assertRaises(ev3dev2.DeviceNotFound): + ev3dev2.Device('this-does-not-exist') def test_medium_motor(self): def dummy(self): pass clean_arena() - populate_arena({'medium_motor' : [0, 'outA']}) + populate_arena([('medium_motor', 0, 'outA')]) # Do not write motor.command on exit (so that fake tree stays intact) - ev3.MediumMotor.__del__ = dummy + MediumMotor.__del__ = dummy - m = ev3.MediumMotor() - - self.assertTrue(m.connected); + m = MediumMotor() self.assertEqual(m.device_index, 0) @@ -59,39 +129,380 @@ def dummy(self): self.assertEqual(m.driver_name, 'lego-ev3-m-motor') self.assertEqual(m.driver_name, 'lego-ev3-m-motor') - self.assertEqual(m.count_per_rot, 360) - self.assertEqual(m.commands, ['run-forever', 'run-to-abs-pos', 'run-to-rel-pos', 'run-timed', 'run-direct', 'stop', 'reset']) - self.assertEqual(m.duty_cycle, 0) - self.assertEqual(m.duty_cycle_sp, 42) - self.assertEqual(m.polarity, 'normal') - self.assertEqual(m.address, 'outA') - self.assertEqual(m.position, 42) - self.assertEqual(m.position_sp, 42) - self.assertEqual(m.ramp_down_sp, 0) - self.assertEqual(m.ramp_up_sp, 0) - self.assertEqual(m.speed, 0) - self.assertEqual(m.speed_sp, 0) - self.assertEqual(m.state, ['running']) - self.assertEqual(m.stop_action, 'coast') - self.assertEqual(m.time_sp, 1000) + self.assertEqual(m.count_per_rot, 360) + self.assertEqual( + m.commands, ['run-forever', 'run-to-abs-pos', 'run-to-rel-pos', 'run-timed', 'run-direct', 'stop', 'reset']) + self.assertEqual(m.duty_cycle, 0) + self.assertEqual(m.duty_cycle_sp, 42) + self.assertEqual(m.polarity, 'normal') + self.assertEqual(m.address, 'outA') + self.assertEqual(m.position, 42) + self.assertEqual(m.position_sp, 42) + self.assertEqual(m.ramp_down_sp, 0) + self.assertEqual(m.ramp_up_sp, 0) + self.assertEqual(m.speed, 0) + self.assertEqual(m.speed_sp, 0) + self.assertEqual(m.state, ['running']) + self.assertEqual(m.stop_action, 'coast') + self.assertEqual(m.time_sp, 1000) with self.assertRaises(Exception): - c = m.command + m.command def test_infrared_sensor(self): clean_arena() - populate_arena({'infrared_sensor' : [0, 'in1']}) - - s = ev3.InfraredSensor() + populate_arena([('infrared_sensor', 0, 'in1')]) - self.assertTrue(s.connected) + s = InfraredSensor() - self.assertEqual(s.device_index, 0) + self.assertEqual(s.device_index, 0) self.assertEqual(s.bin_data_format, 's8') - self.assertEqual(s.bin_data(' toc - time.time(): toc += self.interval @@ -42,38 +43,39 @@ def join(self, timeout=None): super(LogThread, self).join(timeout) -test = json.loads( open( args.infile ).read() ) +test = json.loads(open(args.infile).read()) + def execute_actions(actions): - for p,c in actions['ports'].items(): + for p, c in actions['ports'].items(): for b in c: - for k,v in b.items(): - setattr( device[p], k, v ) - + for k, v in b.items(): + setattr(device[p], k, v) + + device = {} logs = {} -for p,v in test['meta']['ports'].items(): - device[p] = getattr( ev3, v['device_class'] )( p ) +for p, v in test['meta']['ports'].items(): + device[p] = getattr(ev3, v['device_class'])(p) if test['actions'][0]['time'] < 0: execute_actions(test['actions'][0]) -for p,v in test['meta']['ports'].items(): - device[p] = getattr( ev3, v['device_class'] )( p ) +for p, v in test['meta']['ports'].items(): + device[p] = getattr(ev3, v['device_class'])(p) - logs[p] = LogThread(test['meta']['interval'] * 1e-3, - device[p], - v['log_attributes'] ) + logs[p] = LogThread(test['meta']['interval'] * 1e-3, device[p], v['log_attributes']) logs[p].start() start = time.time() -end = start + test['meta']['max_time'] * 1e-3 +end = start + test['meta']['max_time'] * 1e-3 for a in test['actions']: if a['time'] >= 0: then = start + a['time'] * 1e-3 - while time.time() < then: pass + while time.time() < then: + pass execute_actions(a) while time.time() < end: @@ -81,9 +83,9 @@ def execute_actions(actions): test['data'] = {} -for p,v in test['meta']['ports'].items(): - logs[p].join() - test['data'][p] = logs[p].results +for p, v in test['meta']['ports'].items(): + logs[p].join() + test['data'][p] = logs[p].results # Add a nice JSON formatter here - maybe? -print json.dumps( test, indent = 4 ) +print(json.dumps(test, indent=4)) diff --git a/tests/motor/motor_info.py b/tests/motor/motor_info.py index 82b69e2..6c9bacc 100644 --- a/tests/motor/motor_info.py +++ b/tests/motor/motor_info.py @@ -1,56 +1,64 @@ motor_info = { - 'lego-ev3-l-motor': {'motion_type': 'rotation', - 'count_per_rot': 360, - 'max_speed': 1050, - 'position_p': 80000, - 'position_i': 0, - 'position_d': 0, - 'polarity': 'normal', - 'speed_p': 1000, - 'speed_i': 60, - 'speed_d': 0 }, - 'lego-ev3-m-motor': {'motion_type': 'rotation', - 'count_per_rot': 360, - 'max_speed': 1560, - 'position_p': 160000, - 'position_i': 0, - 'position_d': 0, - 'polarity': 'normal', - 'speed_p': 1000, - 'speed_i': 60, - 'speed_d': 0 }, - 'lego-nxt-motor': {'motion_type': 'rotation', - 'count_per_rot': 360, - 'max_speed': 1020, - 'position_p': 80000, - 'position_i': 0, - 'position_d': 0, - 'polarity': 'normal', - 'speed_p': 1000, - 'speed_i': 60, - 'speed_d': 0 }, - 'fi-l12-ev3-50': {'motion_type': 'linear', - 'count_per_m': 2000, - 'full_travel_count': 100, - 'max_speed': 24, - 'position_p': 40000, - 'position_i': 0, - 'position_d': 0, - 'polarity': 'normal', - 'speed_p': 1000, - 'speed_i': 60, - 'speed_d': 0, - }, - 'fi-l12-ev3-100': {'motion_type': 'linear', - 'count_per_m': 2000, - 'full_travel_count': 200, - 'max_speed': 24, - 'position_p': 40000, - 'position_i': 0, - 'position_d': 0, - 'polarity': 'normal', - 'speed_p': 1000, - 'speed_i': 60, - 'speed_d': 0, - } + 'lego-ev3-l-motor': { + 'motion_type': 'rotation', + 'count_per_rot': 360, + 'max_speed': 1050, + 'position_p': 80000, + 'position_i': 0, + 'position_d': 0, + 'polarity': 'normal', + 'speed_p': 1000, + 'speed_i': 60, + 'speed_d': 0 + }, + 'lego-ev3-m-motor': { + 'motion_type': 'rotation', + 'count_per_rot': 360, + 'max_speed': 1560, + 'position_p': 160000, + 'position_i': 0, + 'position_d': 0, + 'polarity': 'normal', + 'speed_p': 1000, + 'speed_i': 60, + 'speed_d': 0 + }, + 'lego-nxt-motor': { + 'motion_type': 'rotation', + 'count_per_rot': 360, + 'max_speed': 1020, + 'position_p': 80000, + 'position_i': 0, + 'position_d': 0, + 'polarity': 'normal', + 'speed_p': 1000, + 'speed_i': 60, + 'speed_d': 0 + }, + 'fi-l12-ev3-50': { + 'motion_type': 'linear', + 'count_per_m': 2000, + 'full_travel_count': 100, + 'max_speed': 24, + 'position_p': 40000, + 'position_i': 0, + 'position_d': 0, + 'polarity': 'normal', + 'speed_p': 1000, + 'speed_i': 60, + 'speed_d': 0, + }, + 'fi-l12-ev3-100': { + 'motion_type': 'linear', + 'count_per_m': 2000, + 'full_travel_count': 200, + 'max_speed': 24, + 'position_p': 40000, + 'position_i': 0, + 'position_d': 0, + 'polarity': 'normal', + 'speed_p': 1000, + 'speed_i': 60, + 'speed_d': 0, + } } diff --git a/tests/motor/motor_motion_unittest.py b/tests/motor/motor_motion_unittest.py index 52fb3c4..9acece4 100644 --- a/tests/motor/motor_motion_unittest.py +++ b/tests/motor/motor_motion_unittest.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python +#!/usr/bin/env python3 # Based on the parameterized test case technique described here: # @@ -7,13 +7,10 @@ import unittest import time import ev3dev.ev3 as ev3 - import parameterizedtestcase as ptc -from motor_info import motor_info class TestMotorMotion(ptc.ParameterizedTestCase): - @classmethod def setUpClass(cls): pass @@ -25,7 +22,7 @@ def tearDownClass(cls): def initialize_motor(self): self._param['motor'].command = 'reset' - def run_to_positions(self,stop_action,command,speed_sp,positions,tolerance): + def run_to_positions(self, stop_action, command, speed_sp, positions, tolerance): self._param['motor'].stop_action = stop_action self._param['motor'].speed_sp = speed_sp @@ -37,74 +34,112 @@ def run_to_positions(self,stop_action,command,speed_sp,positions,tolerance): target += i else: target = i - print( "PRE position = {0} i = {1} target = {2}".format(self._param['motor'].position, i, target)) + print("PRE position = {0} i = {1} target = {2}".format(self._param['motor'].position, i, target)) self._param['motor'].command = command while 'running' in self._param['motor'].state: pass - print( "POS position = {0} i = {1} target = {2}".format(self._param['motor'].position, i, target)) + print("POS position = {0} i = {1} target = {2}".format(self._param['motor'].position, i, target)) self.assertGreaterEqual(tolerance, abs(self._param['motor'].position - target)) time.sleep(0.2) self._param['motor'].command = 'stop' def test_stop_brake_no_ramp_med_speed_relative(self): + if not self._param['has_brake']: + self.skipTest('brake not supported by this motor controller') self.initialize_motor() - self.run_to_positions('brake','run-to-rel-pos',400,[0,90,180,360,720,-720,-360,-180,-90,0],20) + self.run_to_positions('brake', 'run-to-rel-pos', 400, [0, 90, 180, 360, 720, -720, -360, -180, -90, 0], 20) def test_stop_hold_no_ramp_med_speed_relative(self): self.initialize_motor() - self.run_to_positions('hold','run-to-rel-pos',400,[0,90,180,360,720,-720,-360,-180,-90,0],5) + self.run_to_positions('hold', 'run-to-rel-pos', 400, [0, 90, 180, 360, 720, -720, -360, -180, -90, 0], 5) def test_stop_brake_no_ramp_low_speed_relative(self): + if not self._param['has_brake']: + self.skipTest('brake not supported by this motor controller') self.initialize_motor() - self.run_to_positions('brake','run-to-rel-pos',100,[0,90,180,360,720,-720,-360,-180,-90,0],20) + self.run_to_positions('brake', 'run-to-rel-pos', 100, [0, 90, 180, 360, 720, -720, -360, -180, -90, 0], 20) def test_stop_hold_no_ramp_low_speed_relative(self): self.initialize_motor() - self.run_to_positions('hold','run-to-rel-pos',100,[0,90,180,360,720,-720,-360,-180,-90,0],5) + self.run_to_positions('hold', 'run-to-rel-pos', 100, [0, 90, 180, 360, 720, -720, -360, -180, -90, 0], 5) def test_stop_brake_no_ramp_high_speed_relative(self): + if not self._param['has_brake']: + self.skipTest('brake not supported by this motor controller') self.initialize_motor() - self.run_to_positions('brake','run-to-rel-pos',900,[0,90,180,360,720,-720,-360,-180,-90,0],50) + self.run_to_positions('brake', 'run-to-rel-pos', 900, [0, 90, 180, 360, 720, -720, -360, -180, -90, 0], 50) def test_stop_hold_no_ramp_high_speed_relative(self): self.initialize_motor() - self.run_to_positions('hold','run-to-rel-pos',100,[0,90,180,360,720,-720,-360,-180,-90,0],5) + self.run_to_positions('hold', 'run-to-rel-pos', 100, [0, 90, 180, 360, 720, -720, -360, -180, -90, 0], 5) def test_stop_brake_no_ramp_med_speed_absolute(self): + if not self._param['has_brake']: + self.skipTest('brake not supported by this motor controller') self.initialize_motor() - self.run_to_positions('brake','run-to-abs-pos',400,[0,90,180,360,180,90,0,-90,-180,-360,-180,-90,0],20) + self.run_to_positions('brake', 'run-to-abs-pos', 400, + [0, 90, 180, 360, 180, 90, 0, -90, -180, -360, -180, -90, 0], 20) def test_stop_hold_no_ramp_med_speed_absolute(self): self.initialize_motor() - self.run_to_positions('hold','run-to-abs-pos',400,[0,90,180,360,180,90,0,-90,-180,-360,-180,-90,0],5) + self.run_to_positions('hold', 'run-to-abs-pos', 400, + [0, 90, 180, 360, 180, 90, 0, -90, -180, -360, -180, -90, 0], 5) def test_stop_brake_no_ramp_low_speed_absolute(self): + if not self._param['has_brake']: + self.skipTest('brake not supported by this motor controller') self.initialize_motor() - self.run_to_positions('brake','run-to-abs-pos',100,[0,90,180,360,180,90,0,-90,-180,-360,-180,-90,0],20) + self.run_to_positions('brake', 'run-to-abs-pos', 100, + [0, 90, 180, 360, 180, 90, 0, -90, -180, -360, -180, -90, 0], 20) def test_stop_hold_no_ramp_low_speed_absolute(self): self.initialize_motor() - self.run_to_positions('hold','run-to-abs-pos',100,[0,90,180,360,180,90,0,-90,-180,-360,-180,-90,0],5) + self.run_to_positions('hold', 'run-to-abs-pos', 100, + [0, 90, 180, 360, 180, 90, 0, -90, -180, -360, -180, -90, 0], 5) def test_stop_brake_no_ramp_high_speed_absolute(self): + if not self._param['has_brake']: + self.skipTest('brake not supported by this motor controller') self.initialize_motor() - self.run_to_positions('brake','run-to-abs-pos',900,[0,90,180,360,180,90,0,-90,-180,-360,-180,-90,0],50) + self.run_to_positions('brake', 'run-to-abs-pos', 900, + [0, 90, 180, 360, 180, 90, 0, -90, -180, -360, -180, -90, 0], 50) def test_stop_hold_no_ramp_high_speed_absolute(self): self.initialize_motor() - self.run_to_positions('hold','run-to-abs-pos',100,[0,90,180,360,180,90,0,-90,-180,-360,-180,-90,0],5) + self.run_to_positions('hold', 'run-to-abs-pos', 100, + [0, 90, 180, 360, 180, 90, 0, -90, -180, -360, -180, -90, 0], 5) + # Add all the tests to the suite - some tests apply only to certain drivers! -def AddTachoMotorMotionTestsToSuite( suite, driver_name, params ): + +def AddTachoMotorMotionTestsToSuite(suite, params): suite.addTest(ptc.ParameterizedTestCase.parameterize(TestMotorMotion, param=params)) + if __name__ == '__main__': - params = { 'motor': ev3.Motor('outA'), 'port': 'outA', 'driver_name': 'lego-ev3-l-motor' } + ev3_params = { + 'motor': ev3.Motor('outA'), + 'port': 'outA', + 'driver_name': 'lego-ev3-l-motor', + 'has_brake': True, + } + brickpi_params = { + 'motor': ev3.Motor('ttyAMA0:MA'), + 'port': 'ttyAMA0:MA', + 'driver_name': 'lego-nxt-motor', + 'has_brake': False, + } + pistorms_params = { + 'motor': ev3.Motor('pistorms:BAM1'), + 'port': 'pistorms:BAM1', + 'driver_name': 'lego-nxt-motor', + 'has_brake': True, + } suite = unittest.TestSuite() - AddTachoMotorMotionTestsToSuite( suite, 'lego-ev3-l-motor', params ) + AddTachoMotorMotionTestsToSuite(suite, ev3_params) - unittest.TextTestRunner(verbosity=1,buffer=True ).run(suite) + unittest.TextTestRunner(verbosity=1, buffer=True).run(suite) diff --git a/tests/motor/motor_param_unittest.py b/tests/motor/motor_param_unittest.py index 6b58a4a..1c721b9 100644 --- a/tests/motor/motor_param_unittest.py +++ b/tests/motor/motor_param_unittest.py @@ -5,16 +5,13 @@ # http://eli.thegreenplace.net/2011/08/02/python-unit-testing-parametrized-test-cases import unittest -import time -import sys import ev3dev.ev3 as ev3 - import parameterizedtestcase as ptc from motor_info import motor_info -class TestTachoMotorAddressValue(ptc.ParameterizedTestCase): +class TestTachoMotorAddressValue(ptc.ParameterizedTestCase): def test_address_value(self): self.assertEqual(self._param['motor'].address, self._param['port']) @@ -22,32 +19,27 @@ def test_address_value_is_read_only(self): with self.assertRaises(AttributeError): self._param['motor'].address = "ThisShouldNotWork" -class TestTachoMotorCommandsValue(ptc.ParameterizedTestCase): +class TestTachoMotorCommandsValue(ptc.ParameterizedTestCase): def test_commands_value(self): - self.assertTrue(set(self._param['motor'].commands) == {'run-forever' - ,'run-to-abs-pos' - ,'run-to-rel-pos' - ,'run-timed' - ,'run-direct' - ,'stop' - ,'reset'}) + self.assertTrue(self._param['motor'].commands == self._param['commands']) def test_commands_value_is_read_only(self): with self.assertRaises(AttributeError): self._param['motor'].commands = "ThisShouldNotWork" -class TestTachoMotorCountPerRotValue(ptc.ParameterizedTestCase): +class TestTachoMotorCountPerRotValue(ptc.ParameterizedTestCase): def test_count_per_rot_value(self): - self.assertEqual(self._param['motor'].count_per_rot, motor_info[self._param['motor'].driver_name]['count_per_rot']) + self.assertEqual(self._param['motor'].count_per_rot, + motor_info[self._param['motor'].driver_name]['count_per_rot']) def test_count_per_rot_value_is_read_only(self): with self.assertRaises(AttributeError): self._param['motor'].count_per_rot = "ThisShouldNotWork" -class TestTachoMotorCountPerMValue(ptc.ParameterizedTestCase): +class TestTachoMotorCountPerMValue(ptc.ParameterizedTestCase): def test_count_per_m_value(self): self.assertEqual(self._param['motor'].count_per_m, motor_info[self._param['motor'].driver_name]['count_per_m']) @@ -55,17 +47,18 @@ def test_count_per_m_value_is_read_only(self): with self.assertRaises(AttributeError): self._param['motor'].count_per_m = "ThisShouldNotWork" -class TestTachoMotorFullTravelCountValue(ptc.ParameterizedTestCase): +class TestTachoMotorFullTravelCountValue(ptc.ParameterizedTestCase): def test_full_travel_count_value(self): - self.assertEqual(self._param['motor'].full_travel_count, motor_info[self._param['motor'].driver_name]['full_travel_count']) + self.assertEqual(self._param['motor'].full_travel_count, + motor_info[self._param['motor'].driver_name]['full_travel_count']) def test_full_travel_count_value_is_read_only(self): with self.assertRaises(AttributeError): self._param['motor'].count_per_m = "ThisShouldNotWork" -class TestTachoMotorDriverNameValue(ptc.ParameterizedTestCase): +class TestTachoMotorDriverNameValue(ptc.ParameterizedTestCase): def test_driver_name_value(self): self.assertEqual(self._param['motor'].driver_name, self._param['driver_name']) @@ -73,8 +66,8 @@ def test_driver_name_value_is_read_only(self): with self.assertRaises(AttributeError): self._param['motor'].driver_name = "ThisShouldNotWork" -class TestTachoMotorDutyCycleValue(ptc.ParameterizedTestCase): +class TestTachoMotorDutyCycleValue(ptc.ParameterizedTestCase): def test_duty_cycle_value_is_read_only(self): with self.assertRaises(AttributeError): self._param['motor'].duty_cycle = "ThisShouldNotWork" @@ -83,8 +76,8 @@ def test_duty_cycle_value_after_reset(self): self._param['motor'].command = 'reset' self.assertEqual(self._param['motor'].duty_cycle, 0) -class TestTachoMotorDutyCycleSpValue(ptc.ParameterizedTestCase): +class TestTachoMotorDutyCycleSpValue(ptc.ParameterizedTestCase): def test_duty_cycle_sp_large_negative(self): with self.assertRaises(IOError): self._param['motor'].duty_cycle_sp = -101 @@ -119,8 +112,8 @@ def test_duty_cycle_sp_after_reset(self): self._param['motor'].command = 'reset' self.assertEqual(self._param['motor'].duty_cycle_sp, 0) -class TestTachoMotorMaxSpeedValue(ptc.ParameterizedTestCase): +class TestTachoMotorMaxSpeedValue(ptc.ParameterizedTestCase): def test_max_speed_value(self): self.assertEqual(self._param['motor'].max_speed, motor_info[self._param['motor'].driver_name]['max_speed']) @@ -128,8 +121,8 @@ def test_max_speed_value_is_read_only(self): with self.assertRaises(AttributeError): self._param['motor'].max_speed = "ThisShouldNotWork" -class TestTachoMotorPositionPValue(ptc.ParameterizedTestCase): +class TestTachoMotorPositionPValue(ptc.ParameterizedTestCase): def test_position_p_negative(self): with self.assertRaises(IOError): self._param['motor'].position_p = -1 @@ -145,10 +138,15 @@ def test_position_p_positive(self): def test_position_p_after_reset(self): self._param['motor'].position_p = 1 self._param['motor'].command = 'reset' - self.assertEqual(self._param['motor'].position_p, motor_info[self._param['motor'].driver_name]['position_p']) -class TestTachoMotorPositionIValue(ptc.ParameterizedTestCase): + if self._param['hold_pid']: + expected = self._param['hold_pid']['kP'] + else: + expected = motor_info[self._param['motor'].driver_name]['position_p'] + self.assertEqual(self._param['motor'].position_p, expected) + +class TestTachoMotorPositionIValue(ptc.ParameterizedTestCase): def test_position_i_negative(self): with self.assertRaises(IOError): self._param['motor'].position_i = -1 @@ -164,10 +162,15 @@ def test_position_i_positive(self): def test_position_i_after_reset(self): self._param['motor'].position_i = 1 self._param['motor'].command = 'reset' - self.assertEqual(self._param['motor'].position_i, motor_info[self._param['motor'].driver_name]['position_i']) -class TestTachoMotorPositionDValue(ptc.ParameterizedTestCase): + if self._param['hold_pid']: + expected = self._param['hold_pid']['kI'] + else: + expected = motor_info[self._param['motor'].driver_name]['position_i'] + self.assertEqual(self._param['motor'].position_i, expected) + +class TestTachoMotorPositionDValue(ptc.ParameterizedTestCase): def test_position_d_negative(self): with self.assertRaises(IOError): self._param['motor'].position_d = -1 @@ -183,10 +186,15 @@ def test_position_d_positive(self): def test_position_d_after_reset(self): self._param['motor'].position_d = 1 self._param['motor'].command = 'reset' - self.assertEqual(self._param['motor'].position_d, motor_info[self._param['motor'].driver_name]['position_d']) -class TestTachoMotorPolarityValue(ptc.ParameterizedTestCase): + if self._param['hold_pid']: + expected = self._param['hold_pid']['kD'] + else: + expected = motor_info[self._param['motor'].driver_name]['position_d'] + self.assertEqual(self._param['motor'].position_d, expected) + +class TestTachoMotorPolarityValue(ptc.ParameterizedTestCase): def test_polarity_normal_value(self): self._param['motor'].polarity = 'normal' self.assertEqual(self._param['motor'].polarity, 'normal') @@ -200,18 +208,19 @@ def test_polarity_illegal_value(self): self._param['motor'].polarity = "ThisShouldNotWork" def test_polarity_after_reset(self): - if ('normal' == motor_info[self._param['motor'].driver_name]['polarity']): + if 'normal' == motor_info[self._param['motor'].driver_name]['polarity']: self._param['motor'].polarity = 'inversed' else: self._param['motor'].polarity = 'normal' self._param['motor'].command = 'reset' - if ('normal' == motor_info[self._param['motor'].driver_name]['polarity']): + + if 'normal' == motor_info[self._param['motor'].driver_name]['polarity']: self.assertEqual(self._param['motor'].polarity, 'normal') else: self.assertEqual(self._param['motor'].polarity, 'inversed') -class TestTachoMotorPositionValue(ptc.ParameterizedTestCase): +class TestTachoMotorPositionValue(ptc.ParameterizedTestCase): def test_position_large_negative(self): self._param['motor'].position = -1000000 self.assertEqual(self._param['motor'].position, -1000000) @@ -238,8 +247,8 @@ def test_position_after_reset(self): self._param['motor'].command = 'reset' self.assertEqual(self._param['motor'].position, 0) -class TestTachoMotorPositionSpValue(ptc.ParameterizedTestCase): +class TestTachoMotorPositionSpValue(ptc.ParameterizedTestCase): def test_position_sp_large_negative(self): self._param['motor'].position_sp = -1000000 self.assertEqual(self._param['motor'].position_sp, -1000000) @@ -266,8 +275,8 @@ def test_position_sp_after_reset(self): self._param['motor'].command = 'reset' self.assertEqual(self._param['motor'].position_sp, 0) -class TestTachoMotorRampDownSpValue(ptc.ParameterizedTestCase): +class TestTachoMotorRampDownSpValue(ptc.ParameterizedTestCase): def test_ramp_down_sp_negative_value(self): with self.assertRaises(IOError): self._param['motor'].ramp_down_sp = -1 @@ -294,8 +303,8 @@ def test_ramp_down_sp_after_reset(self): self._param['motor'].command = 'reset' self.assertEqual(self._param['motor'].ramp_down_sp, 0) -class TestTachoMotorRampUpSpValue(ptc.ParameterizedTestCase): +class TestTachoMotorRampUpSpValue(ptc.ParameterizedTestCase): def test_ramp_up_negative_value(self): with self.assertRaises(IOError): self._param['motor'].ramp_up_sp = -1 @@ -322,8 +331,8 @@ def test_ramp_up_sp_after_reset(self): self._param['motor'].command = 'reset' self.assertEqual(self._param['motor'].ramp_up_sp, 0) -class TestTachoMotorSpeedValue(ptc.ParameterizedTestCase): +class TestTachoMotorSpeedValue(ptc.ParameterizedTestCase): def test_speed_value_is_read_only(self): with self.assertRaises(AttributeError): self._param['motor'].speed = 1 @@ -332,44 +341,44 @@ def test_speed_value_after_reset(self): self._param['motor'].command = 'reset' self.assertEqual(self._param['motor'].speed, 0) -class TestTachoMotorSpeedSpValue(ptc.ParameterizedTestCase): - def test_speed_sp_large_negative(self): +class TestTachoMotorSpeedSpValue(ptc.ParameterizedTestCase): + def test_speed_sp_large_negative(self): with self.assertRaises(IOError): - self._param['motor'].speed_sp = -(motor_info[self._param['motor'].driver_name]['max_speed']+1) + self._param['motor'].speed_sp = -(motor_info[self._param['motor'].driver_name]['max_speed'] + 1) - def test_speed_sp_max_negative(self): + def test_speed_sp_max_negative(self): self._param['motor'].speed_sp = -motor_info[self._param['motor'].driver_name]['max_speed'] self.assertEqual(self._param['motor'].speed_sp, -motor_info[self._param['motor'].driver_name]['max_speed']) - def test_speed_sp_min_negative(self): + def test_speed_sp_min_negative(self): self._param['motor'].speed_sp = -1 self.assertEqual(self._param['motor'].speed_sp, -1) - def test_speed_sp_zero(self): + def test_speed_sp_zero(self): self._param['motor'].speed_sp = 0 self.assertEqual(self._param['motor'].speed_sp, 0) - def test_speed_sp_min_positive(self): + def test_speed_sp_min_positive(self): self._param['motor'].speed_sp = 1 self.assertEqual(self._param['motor'].speed_sp, 1) - def test_speed_sp_max_positive(self): - self._param['motor'].speed_sp = motor_info[self._param['motor'].driver_name]['max_speed'] + def test_speed_sp_max_positive(self): + self._param['motor'].speed_sp = (motor_info[self._param['motor'].driver_name]['max_speed']) self.assertEqual(self._param['motor'].speed_sp, motor_info[self._param['motor'].driver_name]['max_speed']) - def test_speed_sp_large_positive(self): + def test_speed_sp_large_positive(self): with self.assertRaises(IOError): - self._param['motor'].speed_sp = (motor_info[self._param['motor'].driver_name]['max_speed']+1) + self._param['motor'].speed_sp = motor_info[self._param['motor'].driver_name]['max_speed'] + 1 - def test_speed_sp_after_reset(self): - self._param['motor'].speed_sp = motor_info[self._param['motor'].driver_name]['max_speed']/2 - self.assertEqual(self._param['motor'].speed_sp, motor_info[self._param['motor'].driver_name]['max_speed']/2) + def test_speed_sp_after_reset(self): + self._param['motor'].speed_sp = motor_info[self._param['motor'].driver_name]['max_speed'] / 2 + self.assertEqual(self._param['motor'].speed_sp, motor_info[self._param['motor'].driver_name]['max_speed'] / 2) self._param['motor'].command = 'reset' self.assertEqual(self._param['motor'].speed_sp, 0) -class TestTachoMotorSpeedPValue(ptc.ParameterizedTestCase): +class TestTachoMotorSpeedPValue(ptc.ParameterizedTestCase): def test_speed_i_negative(self): with self.assertRaises(IOError): self._param['motor'].speed_p = -1 @@ -385,10 +394,15 @@ def test_speed_p_positive(self): def test_speed_p_after_reset(self): self._param['motor'].speed_p = 1 self._param['motor'].command = 'reset' - self.assertEqual(self._param['motor'].speed_p, motor_info[self._param['motor'].driver_name]['speed_p']) -class TestTachoMotorSpeedIValue(ptc.ParameterizedTestCase): + if self._param['speed_pid']: + expected = self._param['speed_pid']['kP'] + else: + expected = motor_info[self._param['motor'].driver_name]['speed_p'] + self.assertEqual(self._param['motor'].speed_p, expected) + +class TestTachoMotorSpeedIValue(ptc.ParameterizedTestCase): def test_speed_i_negative(self): with self.assertRaises(IOError): self._param['motor'].speed_i = -1 @@ -404,10 +418,15 @@ def test_speed_i_positive(self): def test_speed_i_after_reset(self): self._param['motor'].speed_i = 1 self._param['motor'].command = 'reset' - self.assertEqual(self._param['motor'].speed_i, motor_info[self._param['motor'].driver_name]['speed_i']) -class TestTachoMotorSpeedDValue(ptc.ParameterizedTestCase): + if self._param['speed_pid']: + expected = self._param['speed_pid']['kI'] + else: + expected = motor_info[self._param['motor'].driver_name]['speed_i'] + self.assertEqual(self._param['motor'].speed_i, expected) + +class TestTachoMotorSpeedDValue(ptc.ParameterizedTestCase): def test_speed_d_negative(self): with self.assertRaises(IOError): self._param['motor'].speed_d = -1 @@ -423,10 +442,15 @@ def test_speed_d_positive(self): def test_speed_d_after_reset(self): self._param['motor'].speed_d = 1 self._param['motor'].command = 'reset' - self.assertEqual(self._param['motor'].speed_d, motor_info[self._param['motor'].driver_name]['speed_d']) -class TestTachoMotorStateValue(ptc.ParameterizedTestCase): + if self._param['speed_pid']: + expected = self._param['speed_pid']['kD'] + else: + expected = motor_info[self._param['motor'].driver_name]['speed_d'] + self.assertEqual(self._param['motor'].speed_d, expected) + +class TestTachoMotorStateValue(ptc.ParameterizedTestCase): def test_state_value_is_read_only(self): with self.assertRaises(AttributeError): self._param['motor'].state = 'ThisShouldNotWork' @@ -435,42 +459,56 @@ def test_state_value_after_reset(self): self._param['motor'].command = 'reset' self.assertEqual(self._param['motor'].state, []) -class TestTachoMotorStopActionValue(ptc.ParameterizedTestCase): +class TestTachoMotorStopActionValue(ptc.ParameterizedTestCase): def test_stop_action_illegal(self): with self.assertRaises(IOError): self._param['motor'].stop_action = 'ThisShouldNotWork' def test_stop_action_coast(self): - self._param['motor'].stop_action = 'coast' - self.assertEqual(self._param['motor'].stop_action, 'coast') + if 'coast' in self._param['stop_actions']: + self._param['motor'].stop_action = 'coast' + self.assertEqual(self._param['motor'].stop_action, 'coast') + else: + with self.assertRaises(IOError): + self._param['motor'].stop_action = 'coast' def test_stop_action_brake(self): - self._param['motor'].stop_action = 'brake' - self.assertEqual(self._param['motor'].stop_action, 'brake') + if 'brake' in self._param['stop_actions']: + self._param['motor'].stop_action = 'brake' + self.assertEqual(self._param['motor'].stop_action, 'brake') + else: + with self.assertRaises(IOError): + self._param['motor'].stop_action = 'brake' def test_stop_action_hold(self): - self._param['motor'].stop_action = 'hold' - self.assertEqual(self._param['motor'].stop_action, 'hold') + if 'hold' in self._param['stop_actions']: + self._param['motor'].stop_action = 'hold' + self.assertEqual(self._param['motor'].stop_action, 'hold') + else: + with self.assertRaises(IOError): + self._param['motor'].stop_action = 'hold' def test_stop_action_after_reset(self): - self._param['motor'].stop_action = 'hold' - self._param['motor'].command = 'reset' - self.assertEqual(self._param['motor'].stop_action, 'coast') + action = 1 + # controller may only support one stop action + if len(self._param['stop_actions']) < 2: + action = 0 + self._param['motor'].stop_action = self._param['stop_actions'][action] + self._param['motor'].action = 'reset' + self.assertEqual(self._param['motor'].stop_action, self._param['stop_actions'][0]) -class TestTachoMotorStopActionsValue(ptc.ParameterizedTestCase): +class TestTachoMotorStopActionsValue(ptc.ParameterizedTestCase): def test_stop_actions_value(self): - self.assertTrue(set(self._param['motor'].stop_actions) == {'coast' - ,'brake' - ,'hold'}) + self.assertTrue(self._param['motor'].stop_actions == self._param['stop_actions']) def test_stop_actions_value_is_read_only(self): with self.assertRaises(AttributeError): self._param['motor'].stop_actions = "ThisShouldNotWork" -class TestTachoMotorTimeSpValue(ptc.ParameterizedTestCase): +class TestTachoMotorTimeSpValue(ptc.ParameterizedTestCase): def test_time_sp_negative(self): with self.assertRaises(IOError): self._param['motor'].time_sp = -1 @@ -490,62 +528,85 @@ def test_time_sp_large_positive(self): def test_time_sp_after_reset(self): self._param['motor'].time_sp = 1 self._param['motor'].command = 'reset' - self.assertEqual(self._param['motor'].speed_d, 0) - -class TestTachoMotorDummy(ptc.ParameterizedTestCase): - - def test_dummy_no_message(self): - try: - self.assertEqual(self._param['motor'].speed_d, 100, "Some clever error message {0}".format(self._param['motor'].speed_d)) - except: - # Remove traceback info as we don't need it - unittest_exception = sys.exc_info() - raise unittest_exception[0], unittest_exception[1], unittest_exception[2].tb_next - -# Add all the tests to the suite - some tests apply only to certain drivers! - -def AddTachoMotorParameterTestsToSuite( suite, driver_name, params ): - suite.addTest(ptc.ParameterizedTestCase.parameterize(TestTachoMotorAddressValue, param=params)) - suite.addTest(ptc.ParameterizedTestCase.parameterize(TestTachoMotorCommandsValue, param=params)) - if( motor_info[driver_name]['motion_type'] == 'rotation' ): - suite.addTest(ptc.ParameterizedTestCase.parameterize(TestTachoMotorCountPerRotValue, param=params)) - if( motor_info[driver_name]['motion_type'] == 'linear' ): - suite.addTest(ptc.ParameterizedTestCase.parameterize(TestTachoMotorCountPerMValue, param=params)) - suite.addTest(ptc.ParameterizedTestCase.parameterize(TestTachoMotorFullTravelCountValue, param=params)) - suite.addTest(ptc.ParameterizedTestCase.parameterize(TestTachoMotorDriverNameValue, param=params)) - suite.addTest(ptc.ParameterizedTestCase.parameterize(TestTachoMotorDutyCycleSpValue, param=params)) - suite.addTest(ptc.ParameterizedTestCase.parameterize(TestTachoMotorMaxSpeedValue, param=params)) - suite.addTest(ptc.ParameterizedTestCase.parameterize(TestTachoMotorPositionPValue, param=params)) - suite.addTest(ptc.ParameterizedTestCase.parameterize(TestTachoMotorPositionIValue, param=params)) - suite.addTest(ptc.ParameterizedTestCase.parameterize(TestTachoMotorPositionDValue, param=params)) - suite.addTest(ptc.ParameterizedTestCase.parameterize(TestTachoMotorPolarityValue, param=params)) - suite.addTest(ptc.ParameterizedTestCase.parameterize(TestTachoMotorPositionValue, param=params)) - suite.addTest(ptc.ParameterizedTestCase.parameterize(TestTachoMotorPositionSpValue, param=params)) - suite.addTest(ptc.ParameterizedTestCase.parameterize(TestTachoMotorRampDownSpValue, param=params)) - suite.addTest(ptc.ParameterizedTestCase.parameterize(TestTachoMotorRampUpSpValue, param=params)) - suite.addTest(ptc.ParameterizedTestCase.parameterize(TestTachoMotorSpeedValue, param=params)) - suite.addTest(ptc.ParameterizedTestCase.parameterize(TestTachoMotorSpeedSpValue, param=params)) - suite.addTest(ptc.ParameterizedTestCase.parameterize(TestTachoMotorSpeedPValue, param=params)) - suite.addTest(ptc.ParameterizedTestCase.parameterize(TestTachoMotorSpeedIValue, param=params)) - suite.addTest(ptc.ParameterizedTestCase.parameterize(TestTachoMotorSpeedDValue, param=params)) - suite.addTest(ptc.ParameterizedTestCase.parameterize(TestTachoMotorStateValue, param=params)) - suite.addTest(ptc.ParameterizedTestCase.parameterize(TestTachoMotorStopActionValue, param=params)) - suite.addTest(ptc.ParameterizedTestCase.parameterize(TestTachoMotorStopActionsValue, param=params)) - suite.addTest(ptc.ParameterizedTestCase.parameterize(TestTachoMotorTimeSpValue, param=params)) - suite.addTest(ptc.ParameterizedTestCase.parameterize(TestTachoMotorDummy, param=params)) - -if __name__ == '__main__': - for k in motor_info: - file = open('/sys/class/lego-port/port4/set_device', 'w') - file.write('{0}\n'.format(k)) - file.close() - time.sleep(0.5) - - params = { 'motor': ev3.Motor('outA'), 'port': 'outA', 'driver_name': k } + self.assertEqual(self._param['motor'].time_sp, 0) - suite = unittest.TestSuite() - AddTachoMotorParameterTestsToSuite( suite, k, params ) - print( '-------------------- TESTING {0} --------------'.format(k)) - unittest.TextTestRunner(verbosity=1,buffer=True ).run(suite) +ev3_params = { + 'motor': ev3.Motor('outA'), + 'port': 'outA', + 'driver_name': 'lego-ev3-l-motor', + 'commands': ['run-forever', 'run-to-abs-pos', 'run-to-rel-pos', 'run-timed', 'run-direct', 'stop', 'reset'], + 'stop_actions': ['coast', 'brake', 'hold'], +} +evb_params = { + 'motor': ev3.Motor('evb-ports:outA'), + 'port': 'evb-ports:outA', + 'driver_name': 'lego-ev3-l-motor', + 'commands': ['run-forever', 'run-to-abs-pos', 'run-to-rel-pos', 'run-timed', 'run-direct', 'stop', 'reset'], + 'stop_actions': ['coast', 'brake', 'hold'], +} +brickpi_params = { + 'motor': ev3.Motor('ttyAMA0:MA'), + 'port': 'ttyAMA0:MA', + 'driver_name': 'lego-nxt-motor', + 'commands': ['run-forever', 'run-to-abs-pos', 'run-to-rel-pos', 'run-timed', 'run-direct', 'stop', 'reset'], + 'stop_actions': ['coast', 'hold'], + 'speed_pid': { + 'kP': 1000, + 'kI': 60, + 'kD': 0 + }, + 'hold_pid': { + 'kP': 20000, + 'kI': 0, + 'kD': 0 + }, +} +pistorms_params = { + 'motor': ev3.Motor('pistorms:BAM1'), + 'port': 'pistorms:BAM1', + 'driver_name': 'lego-nxt-motor', + 'commands': ['run-forever', 'run-to-abs-pos', 'run-to-rel-pos', 'run-timed', 'stop', 'reset'], + 'stop_actions': ['coast', 'brake', 'hold'], + 'speed_pid': { + 'kP': 1000, + 'kI': 60, + 'kD': 0 + }, + 'hold_pid': { + 'kP': 20000, + 'kI': 0, + 'kD': 0 + }, +} +paramsA = pistorms_params +paramsA['motor'].command = 'reset' + +suite = unittest.TestSuite() + +suite.addTest(ptc.ParameterizedTestCase.parameterize(TestTachoMotorAddressValue, param=paramsA)) +suite.addTest(ptc.ParameterizedTestCase.parameterize(TestTachoMotorCommandsValue, param=paramsA)) +suite.addTest(ptc.ParameterizedTestCase.parameterize(TestTachoMotorCountPerRotValue, param=paramsA)) +suite.addTest(ptc.ParameterizedTestCase.parameterize(TestTachoMotorDriverNameValue, param=paramsA)) +suite.addTest(ptc.ParameterizedTestCase.parameterize(TestTachoMotorDutyCycleSpValue, param=paramsA)) +suite.addTest(ptc.ParameterizedTestCase.parameterize(TestTachoMotorMaxSpeedValue, param=paramsA)) +suite.addTest(ptc.ParameterizedTestCase.parameterize(TestTachoMotorPositionPValue, param=paramsA)) +suite.addTest(ptc.ParameterizedTestCase.parameterize(TestTachoMotorPositionIValue, param=paramsA)) +suite.addTest(ptc.ParameterizedTestCase.parameterize(TestTachoMotorPositionDValue, param=paramsA)) +suite.addTest(ptc.ParameterizedTestCase.parameterize(TestTachoMotorPolarityValue, param=paramsA)) +suite.addTest(ptc.ParameterizedTestCase.parameterize(TestTachoMotorPositionValue, param=paramsA)) +suite.addTest(ptc.ParameterizedTestCase.parameterize(TestTachoMotorPositionSpValue, param=paramsA)) +suite.addTest(ptc.ParameterizedTestCase.parameterize(TestTachoMotorRampDownSpValue, param=paramsA)) +suite.addTest(ptc.ParameterizedTestCase.parameterize(TestTachoMotorRampUpSpValue, param=paramsA)) +suite.addTest(ptc.ParameterizedTestCase.parameterize(TestTachoMotorSpeedValue, param=paramsA)) +suite.addTest(ptc.ParameterizedTestCase.parameterize(TestTachoMotorSpeedSpValue, param=paramsA)) +suite.addTest(ptc.ParameterizedTestCase.parameterize(TestTachoMotorSpeedPValue, param=paramsA)) +suite.addTest(ptc.ParameterizedTestCase.parameterize(TestTachoMotorSpeedIValue, param=paramsA)) +suite.addTest(ptc.ParameterizedTestCase.parameterize(TestTachoMotorSpeedDValue, param=paramsA)) +suite.addTest(ptc.ParameterizedTestCase.parameterize(TestTachoMotorStateValue, param=paramsA)) +suite.addTest(ptc.ParameterizedTestCase.parameterize(TestTachoMotorStopActionValue, param=paramsA)) +suite.addTest(ptc.ParameterizedTestCase.parameterize(TestTachoMotorStopActionsValue, param=paramsA)) +suite.addTest(ptc.ParameterizedTestCase.parameterize(TestTachoMotorTimeSpValue, param=paramsA)) +if __name__ == '__main__': + unittest.main(verbosity=2, buffer=True).run(suite) diff --git a/tests/motor/motor_ramps.json b/tests/motor/motor_ramps.json index aaac717..3b2482d 100644 --- a/tests/motor/motor_ramps.json +++ b/tests/motor/motor_ramps.json @@ -2,7 +2,7 @@ "meta": { "title": "Test Large EV3 Motor Operation", "subtitle": "Positive and Negative Ramps", - "notes": "ramp_up_sp:1000\nramp_down_sp:2000\ntime_sp: 2000\nspeed_sp: +/- 900\nstop_command: coast", + "notes": "ramp_up_sp:1000\nramp_down_sp:2000\ntime_sp: 2000\nspeed_sp: +/- 900\nstop_action: coast", "interval": 10, "name": "run-direct-test", "max_time": 8000, diff --git a/tests/motor/motor_ramps.log.json b/tests/motor/motor_ramps.log.json index b921fcd..1684864 100644 --- a/tests/motor/motor_ramps.log.json +++ b/tests/motor/motor_ramps.log.json @@ -3,7 +3,7 @@ "subtitle": "Positive and Negative Ramps", "name": "run-direct-test", "title": "Test Large EV3 Motor Operation", - "notes": "ramp_up_sp:1000\nramp_down_sp:2000\ntime_sp: 2000\nspeed_sp: +/- 900\nstop_command: coast", + "notes": "ramp_up_sp:1000\nramp_down_sp:2000\ntime_sp: 2000\nspeed_sp: +/- 900\nstop_action: coast", "interval": 10, "max_time": 8000, "ports": { diff --git a/tests/motor/motor_run_direct_unittest.py b/tests/motor/motor_run_direct_unittest.py index c0d63de..4ae42a9 100755 --- a/tests/motor/motor_run_direct_unittest.py +++ b/tests/motor/motor_run_direct_unittest.py @@ -7,13 +7,10 @@ import unittest import time import ev3dev.ev3 as ev3 - import parameterizedtestcase as ptc -from motor_info import motor_info class TestMotorRunDirect(ptc.ParameterizedTestCase): - @classmethod def setUpClass(cls): pass @@ -25,30 +22,33 @@ def tearDownClass(cls): def initialize_motor(self): self._param['motor'].command = 'reset' - def run_direct_duty_cycles(self,stop_action,duty_cycles): + def run_direct_duty_cycles(self, stop_action, duty_cycles): self._param['motor'].stop_action = stop_action self._param['motor'].command = 'run-direct' for i in duty_cycles: - self._param['motor'].duty_cycle_sp = i + self._param['motor'].duty_cycle_sp = i time.sleep(0.5) self._param['motor'].command = 'stop' def test_stop_coast_duty_cycles(self): self.initialize_motor() - self.run_direct_duty_cycles('coast',[0,20,40,60,80,100,66,33,0,-20,-40,-60,-80,-100,-66,-33,0]) + self.run_direct_duty_cycles('coast', [0, 20, 40, 60, 80, 100, 66, 33, 0, -20, -40, -60, -80, -100, -66, -33, 0]) + # Add all the tests to the suite - some tests apply only to certain drivers! -def AddTachoMotorRunDirectTestsToSuite( suite, driver_name, params ): + +def AddTachoMotorRunDirectTestsToSuite(suite, driver_name, params): suite.addTest(ptc.ParameterizedTestCase.parameterize(TestMotorRunDirect, param=params)) + if __name__ == '__main__': - params = { 'motor': ev3.Motor('outA'), 'port': 'outA', 'driver_name': 'lego-ev3-l-motor' } + params = {'motor': ev3.Motor('outA'), 'port': 'outA', 'driver_name': 'lego-ev3-l-motor'} suite = unittest.TestSuite() - AddTachoMotorRunDirectTestsToSuite( suite, 'lego-ev3-l-motor', params ) + AddTachoMotorRunDirectTestsToSuite(suite, 'lego-ev3-l-motor', params) - unittest.TextTestRunner(verbosity=1,buffer=True ).run(suite) + unittest.TextTestRunner(verbosity=1, buffer=True).run(suite) diff --git a/tests/motor/motor_unittest.py b/tests/motor/motor_unittest.py new file mode 100644 index 0000000..090f537 --- /dev/null +++ b/tests/motor/motor_unittest.py @@ -0,0 +1,665 @@ +#!/usr/bin/env python + +# Based on the parameterized test case technique described here: +# +# http://eli.thegreenplace.net/2011/08/02/python-unit-testing-parametrized-test-cases +import unittest +import time +import ev3dev.ev3 as ev3 + +import parameterizedtestcase as ptc +from motor_info import motor_info + + +class TestTachoMotorAddressValue(ptc.ParameterizedTestCase): + def test_address_value(self): + # Use the class variable + self.assertEqual(self._param['motor'].address, self._param['port']) + + def test_address_value_is_read_only(self): + # Use the class variable + with self.assertRaises(AttributeError): + self._param['motor'].address = "ThisShouldNotWork" + + +class TestTachoMotorCommandsValue(ptc.ParameterizedTestCase): + def test_commands_value(self): + self.assertTrue(self._param['motor'].commands == self._param['commands']) + + def test_commands_value_is_read_only(self): + # Use the class variable + with self.assertRaises(AttributeError): + self._param['motor'].commands = "ThisShouldNotWork" + + +class TestTachoMotorCountPerRotValue(ptc.ParameterizedTestCase): + def test_count_per_rot_value(self): + # This is not available for linear motors - move to driver specific tests? + self.assertEqual(self._param['motor'].count_per_rot, 360) + + def test_count_per_rot_value_is_read_only(self): + # Use the class variable + with self.assertRaises(AttributeError): + self._param['motor'].count_per_rot = "ThisShouldNotWork" + + +class TestTachoMotorDriverNameValue(ptc.ParameterizedTestCase): + def test_driver_name_value(self): + # move to driver specific tests? + self.assertEqual(self._param['motor'].driver_name, self._param['driver_name']) + + def test_driver_name_value_is_read_only(self): + # Use the class variable + with self.assertRaises(AttributeError): + self._param['motor'].driver_name = "ThisShouldNotWork" + + +class TestTachoMotorDutyCycleValue(ptc.ParameterizedTestCase): + def test_duty_cycle_value_is_read_only(self): + # Use the class variable + with self.assertRaises(AttributeError): + self._param['motor'].duty_cycle = "ThisShouldNotWork" + + def test_duty_cycle_value_after_reset(self): + self._param['motor'].command = 'reset' + self.assertEqual(self._param['motor'].duty_cycle, 0) + + +class TestTachoMotorDutyCycleSpValue(ptc.ParameterizedTestCase): + def test_duty_cycle_sp_large_negative(self): + with self.assertRaises(IOError): + self._param['motor'].duty_cycle_sp = -101 + + def test_duty_cycle_sp_max_negative(self): + self._param['motor'].duty_cycle_sp = -100 + self.assertEqual(self._param['motor'].duty_cycle_sp, -100) + + def test_duty_cycle_sp_min_negative(self): + self._param['motor'].duty_cycle_sp = -1 + self.assertEqual(self._param['motor'].duty_cycle_sp, -1) + + def test_duty_cycle_sp_zero(self): + self._param['motor'].duty_cycle_sp = 0 + self.assertEqual(self._param['motor'].duty_cycle_sp, 0) + + def test_duty_cycle_sp_min_positive(self): + self._param['motor'].duty_cycle_sp = 1 + self.assertEqual(self._param['motor'].duty_cycle_sp, 1) + + def test_duty_cycle_sp_max_positive(self): + self._param['motor'].duty_cycle_sp = 100 + self.assertEqual(self._param['motor'].duty_cycle_sp, 100) + + def test_duty_cycle_sp_large_positive(self): + with self.assertRaises(IOError): + self._param['motor'].duty_cycle_sp = 101 + + def test_duty_cycle_sp_after_reset(self): + self._param['motor'].duty_cycle_sp = 100 + self.assertEqual(self._param['motor'].duty_cycle_sp, 100) + self._param['motor'].command = 'reset' + self.assertEqual(self._param['motor'].duty_cycle_sp, 0) + + +class TestTachoMotorMaxSpeedValue(ptc.ParameterizedTestCase): + def test_max_speed_value(self): + # This is not available for linear motors - move to driver specific tests? + self.assertEqual(self._param['motor'].max_speed, motor_info[self._param['motor'].driver_name]['max_speed']) + + def test_max_speed_value_is_read_only(self): + # Use the class variable + with self.assertRaises(AttributeError): + self._param['motor'].max_speed = "ThisShouldNotWork" + + +class TestTachoMotorPositionPValue(ptc.ParameterizedTestCase): + def test_position_p_negative(self): + with self.assertRaises(IOError): + self._param['motor'].position_p = -1 + + def test_position_p_zero(self): + self._param['motor'].position_p = 0 + self.assertEqual(self._param['motor'].position_p, 0) + + def test_position_p_positive(self): + self._param['motor'].position_p = 1 + self.assertEqual(self._param['motor'].position_p, 1) + + def test_position_p_after_reset(self): + self._param['motor'].position_p = 1 + + self._param['motor'].command = 'reset' + + if 'hold_pid' in self._param: + expected = self._param['hold_pid']['kP'] + else: + expected = motor_info[self._param['motor'].driver_name]['position_p'] + self.assertEqual(self._param['motor'].position_p, expected) + + +class TestTachoMotorPositionIValue(ptc.ParameterizedTestCase): + def test_position_i_negative(self): + with self.assertRaises(IOError): + self._param['motor'].position_i = -1 + + def test_position_i_zero(self): + self._param['motor'].position_i = 0 + self.assertEqual(self._param['motor'].position_i, 0) + + def test_position_i_positive(self): + self._param['motor'].position_i = 1 + self.assertEqual(self._param['motor'].position_i, 1) + + def test_position_i_after_reset(self): + self._param['motor'].position_i = 1 + + self._param['motor'].command = 'reset' + + if 'hold_pid' in self._param: + expected = self._param['hold_pid']['kI'] + else: + expected = motor_info[self._param['motor'].driver_name]['position_i'] + self.assertEqual(self._param['motor'].position_i, expected) + + +class TestTachoMotorPositionDValue(ptc.ParameterizedTestCase): + def test_position_d_negative(self): + with self.assertRaises(IOError): + self._param['motor'].position_d = -1 + + def test_position_d_zero(self): + self._param['motor'].position_d = 0 + self.assertEqual(self._param['motor'].position_d, 0) + + def test_position_d_positive(self): + self._param['motor'].position_d = 1 + self.assertEqual(self._param['motor'].position_d, 1) + + def test_position_d_after_reset(self): + self._param['motor'].position_d = 1 + + self._param['motor'].command = 'reset' + + if 'hold_pid' in self._param: + expected = self._param['hold_pid']['kD'] + else: + expected = motor_info[self._param['motor'].driver_name]['position_d'] + self.assertEqual(self._param['motor'].position_d, expected) + + +class TestTachoMotorPolarityValue(ptc.ParameterizedTestCase): + def test_polarity_normal_value(self): + self._param['motor'].polarity = 'normal' + self.assertEqual(self._param['motor'].polarity, 'normal') + + def test_polarity_inversed_value(self): + self._param['motor'].polarity = 'inversed' + self.assertEqual(self._param['motor'].polarity, 'inversed') + + def test_polarity_illegal_value(self): + with self.assertRaises(IOError): + self._param['motor'].polarity = "ThisShouldNotWork" + + def test_polarity_after_reset(self): + if 'normal' == motor_info[self._param['motor'].driver_name]['polarity']: + self._param['motor'].polarity = 'inversed' + else: + self._param['motor'].polarity = 'normal' + + self._param['motor'].command = 'reset' + + if 'normal' == motor_info[self._param['motor'].driver_name]['polarity']: + self.assertEqual(self._param['motor'].polarity, 'normal') + else: + self.assertEqual(self._param['motor'].polarity, 'inversed') + + +class TestTachoMotorPositionValue(ptc.ParameterizedTestCase): + def test_position_large_negative(self): + self._param['motor'].position = -1000000 + self.assertEqual(self._param['motor'].position, -1000000) + + def test_position_min_negative(self): + self._param['motor'].position = -1 + self.assertEqual(self._param['motor'].position, -1) + + def test_position_zero(self): + self._param['motor'].position = 0 + self.assertEqual(self._param['motor'].position, 0) + + def test_position_min_positive(self): + self._param['motor'].position = 1 + self.assertEqual(self._param['motor'].position, 1) + + def test_position_large_positive(self): + self._param['motor'].position = 1000000 + self.assertEqual(self._param['motor'].position, 1000000) + + def test_position_after_reset(self): + self._param['motor'].position = 100 + self.assertEqual(self._param['motor'].position, 100) + self._param['motor'].command = 'reset' + self.assertEqual(self._param['motor'].position, 0) + + +class TestTachoMotorPositionSpValue(ptc.ParameterizedTestCase): + def test_position_sp_large_negative(self): + self._param['motor'].position_sp = -1000000 + self.assertEqual(self._param['motor'].position_sp, -1000000) + + def test_position_sp_min_negative(self): + self._param['motor'].position_sp = -1 + self.assertEqual(self._param['motor'].position_sp, -1) + + def test_position_sp_zero(self): + self._param['motor'].position_sp = 0 + self.assertEqual(self._param['motor'].position_sp, 0) + + def test_position_sp_min_positive(self): + self._param['motor'].position_sp = 1 + self.assertEqual(self._param['motor'].position_sp, 1) + + def test_position_sp_large_positive(self): + self._param['motor'].position_sp = 1000000 + self.assertEqual(self._param['motor'].position_sp, 1000000) + + def test_position_sp_after_reset(self): + self._param['motor'].position_sp = 100 + self.assertEqual(self._param['motor'].position_sp, 100) + self._param['motor'].command = 'reset' + self.assertEqual(self._param['motor'].position_sp, 0) + + +class TestTachoMotorRampDownSpValue(ptc.ParameterizedTestCase): + def test_ramp_down_sp_negative_value(self): + with self.assertRaises(IOError): + self._param['motor'].ramp_down_sp = -1 + + def test_ramp_down_sp_zero(self): + self._param['motor'].ramp_down_sp = 0 + self.assertEqual(self._param['motor'].ramp_down_sp, 0) + + def test_ramp_down_sp_min_positive(self): + self._param['motor'].ramp_down_sp = 1 + self.assertEqual(self._param['motor'].ramp_down_sp, 1) + + def test_ramp_down_sp_max_positive(self): + self._param['motor'].ramp_down_sp = 60000 + self.assertEqual(self._param['motor'].ramp_down_sp, 60000) + + def test_ramp_down_sp_large_positive(self): + with self.assertRaises(IOError): + self._param['motor'].ramp_down_sp = 60001 + + def test_ramp_down_sp_after_reset(self): + self._param['motor'].ramp_down_sp = 100 + self.assertEqual(self._param['motor'].ramp_down_sp, 100) + self._param['motor'].command = 'reset' + self.assertEqual(self._param['motor'].ramp_down_sp, 0) + + +class TestTachoMotorRampUpSpValue(ptc.ParameterizedTestCase): + def test_ramp_up_negative_value(self): + with self.assertRaises(IOError): + self._param['motor'].ramp_up_sp = -1 + + def test_ramp_up_sp_zero(self): + self._param['motor'].ramp_up_sp = 0 + self.assertEqual(self._param['motor'].ramp_up_sp, 0) + + def test_ramp_up_sp_min_positive(self): + self._param['motor'].ramp_up_sp = 1 + self.assertEqual(self._param['motor'].ramp_up_sp, 1) + + def test_ramp_up_sp_max_positive(self): + self._param['motor'].ramp_up_sp = 60000 + self.assertEqual(self._param['motor'].ramp_up_sp, 60000) + + def test_ramp_up_sp_large_positive(self): + with self.assertRaises(IOError): + self._param['motor'].ramp_up_sp = 60001 + + def test_ramp_up_sp_after_reset(self): + self._param['motor'].ramp_up_sp = 100 + self.assertEqual(self._param['motor'].ramp_up_sp, 100) + self._param['motor'].command = 'reset' + self.assertEqual(self._param['motor'].ramp_up_sp, 0) + + +class TestTachoMotorSpeedValue(ptc.ParameterizedTestCase): + def test_speed_value_is_read_only(self): + # Use the class variable + with self.assertRaises(AttributeError): + self._param['motor'].speed = 1 + + def test_speed_value_after_reset(self): + self._param['motor'].command = 'reset' + self.assertEqual(self._param['motor'].speed, 0) + + +class TestTachoMotorSpeedSpValue(ptc.ParameterizedTestCase): + def test_speed_sp_large_negative(self): + with self.assertRaises(IOError): + self._param['motor'].speed_sp = -(motor_info[self._param['motor'].driver_name]['max_speed'] + 1) + + def test_speed_sp_max_negative(self): + self._param['motor'].speed_sp = -motor_info[self._param['motor'].driver_name]['max_speed'] + self.assertEqual(self._param['motor'].speed_sp, -motor_info[self._param['motor'].driver_name]['max_speed']) + + def test_speed_sp_min_negative(self): + self._param['motor'].speed_sp = -1 + self.assertEqual(self._param['motor'].speed_sp, -1) + + def test_speed_sp_zero(self): + self._param['motor'].speed_sp = 0 + self.assertEqual(self._param['motor'].speed_sp, 0) + + def test_speed_sp_min_positive(self): + self._param['motor'].speed_sp = 1 + self.assertEqual(self._param['motor'].speed_sp, 1) + + def test_speed_sp_max_positive(self): + self._param['motor'].speed_sp = (motor_info[self._param['motor'].driver_name]['max_speed']) + self.assertEqual(self._param['motor'].speed_sp, motor_info[self._param['motor'].driver_name]['max_speed']) + + def test_speed_sp_large_positive(self): + with self.assertRaises(IOError): + self._param['motor'].speed_sp = motor_info[self._param['motor'].driver_name]['max_speed'] + 1 + + def test_speed_sp_after_reset(self): + self._param['motor'].speed_sp = 100 + self.assertEqual(self._param['motor'].speed_sp, 100) + self._param['motor'].command = 'reset' + self.assertEqual(self._param['motor'].speed_sp, 0) + + +class TestTachoMotorSpeedPValue(ptc.ParameterizedTestCase): + def test_speed_i_negative(self): + with self.assertRaises(IOError): + self._param['motor'].speed_p = -1 + + def test_speed_p_zero(self): + self._param['motor'].speed_p = 0 + self.assertEqual(self._param['motor'].speed_p, 0) + + def test_speed_p_positive(self): + self._param['motor'].speed_p = 1 + self.assertEqual(self._param['motor'].speed_p, 1) + + def test_speed_p_after_reset(self): + self._param['motor'].speed_p = 1 + + self._param['motor'].command = 'reset' + + if 'speed_pid' in self._param: + expected = self._param['speed_pid']['kP'] + else: + expected = motor_info[self._param['motor'].driver_name]['speed_p'] + self.assertEqual(self._param['motor'].speed_p, expected) + + +class TestTachoMotorSpeedIValue(ptc.ParameterizedTestCase): + def test_speed_i_negative(self): + with self.assertRaises(IOError): + self._param['motor'].speed_i = -1 + + def test_speed_i_zero(self): + self._param['motor'].speed_i = 0 + self.assertEqual(self._param['motor'].speed_i, 0) + + def test_speed_i_positive(self): + self._param['motor'].speed_i = 1 + self.assertEqual(self._param['motor'].speed_i, 1) + + def test_speed_i_after_reset(self): + self._param['motor'].speed_i = 1 + + self._param['motor'].command = 'reset' + + if 'speed_pid' in self._param: + expected = self._param['speed_pid']['kI'] + else: + expected = motor_info[self._param['motor'].driver_name]['speed_i'] + self.assertEqual(self._param['motor'].speed_i, expected) + + +class TestTachoMotorSpeedDValue(ptc.ParameterizedTestCase): + def test_speed_d_negative(self): + with self.assertRaises(IOError): + self._param['motor'].speed_d = -1 + + def test_speed_d_zero(self): + self._param['motor'].speed_d = 0 + self.assertEqual(self._param['motor'].speed_d, 0) + + def test_speed_d_positive(self): + self._param['motor'].speed_d = 1 + self.assertEqual(self._param['motor'].speed_d, 1) + + def test_speed_d_after_reset(self): + self._param['motor'].speed_d = 1 + + self._param['motor'].command = 'reset' + + if 'speed_pid' in self._param: + expected = self._param['speed_pid']['kD'] + else: + expected = motor_info[self._param['motor'].driver_name]['speed_d'] + self.assertEqual(self._param['motor'].speed_d, expected) + + +class TestTachoMotorStateValue(ptc.ParameterizedTestCase): + def test_state_value_is_read_only(self): + # Use the class variable + with self.assertRaises(AttributeError): + self._param['motor'].state = 'ThisShouldNotWork' + + def test_state_value_after_reset(self): + self._param['motor'].command = 'reset' + self.assertEqual(self._param['motor'].state, []) + + +# def test_stop_action_value(self): +# self.assertEqual(self._param['motor'].stop_action, 'coast') +class TestTachoMotorStopCommandValue(ptc.ParameterizedTestCase): + def test_stop_action_illegal(self): + with self.assertRaises(IOError): + self._param['motor'].stop_action = 'ThisShouldNotWork' + + def test_stop_action_coast(self): + if 'coast' in self._param['stop_actions']: + self._param['motor'].stop_action = 'coast' + self.assertEqual(self._param['motor'].stop_action, 'coast') + else: + with self.assertRaises(IOError): + self._param['motor'].stop_action = 'coast' + + def test_stop_action_brake(self): + if 'brake' in self._param['stop_actions']: + self._param['motor'].stop_action = 'brake' + self.assertEqual(self._param['motor'].stop_action, 'brake') + else: + with self.assertRaises(IOError): + self._param['motor'].stop_action = 'brake' + + def test_stop_action_hold(self): + if 'hold' in self._param['stop_actions']: + self._param['motor'].stop_action = 'hold' + self.assertEqual(self._param['motor'].stop_action, 'hold') + else: + with self.assertRaises(IOError): + self._param['motor'].stop_action = 'hold' + + def test_stop_action_after_reset(self): + action = 1 + # controller may only support one stop action + if len(self._param['stop_actions']) < 2: + action = 0 + self._param['motor'].stop_action = self._param['stop_actions'][action] + self._param['motor'].command = 'reset' + self.assertEqual(self._param['motor'].stop_action, self._param['stop_actions'][0]) + + +class TestTachoMotorStopCommandsValue(ptc.ParameterizedTestCase): + def test_stop_actions_value(self): + self.assertTrue(self._param['motor'].stop_actions == self._param['stop_actions']) + + def test_stop_actions_value_is_read_only(self): + # Use the class variable + with self.assertRaises(AttributeError): + self._param['motor'].stop_actions = "ThisShouldNotWork" + + +class TestTachoMotorTimeSpValue(ptc.ParameterizedTestCase): + def test_time_sp_negative(self): + with self.assertRaises(IOError): + self._param['motor'].time_sp = -1 + + def test_time_sp_zero(self): + self._param['motor'].time_sp = 0 + self.assertEqual(self._param['motor'].time_sp, 0) + + def test_time_sp_min_positive(self): + self._param['motor'].time_sp = 1 + self.assertEqual(self._param['motor'].time_sp, 1) + + def test_time_sp_large_positive(self): + self._param['motor'].time_sp = 1000000 + self.assertEqual(self._param['motor'].time_sp, 1000000) + + def test_time_sp_after_reset(self): + self._param['motor'].time_sp = 1 + self._param['motor'].command = 'reset' + self.assertEqual(self._param['motor'].time_sp, 0) + + +ev3_params = { + 'motor': ev3.Motor('outA'), + 'port': 'outA', + 'driver_name': 'lego-ev3-l-motor', + 'commands': ['run-forever', 'run-to-abs-pos', 'run-to-rel-pos', 'run-timed', 'run-direct', 'stop', 'reset'], + 'stop_actions': ['coast', 'brake', 'hold'], +} +evb_params = { + 'motor': ev3.Motor('evb-ports:outA'), + 'port': 'evb-ports:outA', + 'driver_name': 'lego-ev3-l-motor', + 'commands': ['run-forever', 'run-to-abs-pos', 'run-to-rel-pos', 'run-timed', 'run-direct', 'stop', 'reset'], + 'stop_actions': ['coast', 'brake', 'hold'], +} +brickpi_params = { + 'motor': ev3.Motor('ttyAMA0:MA'), + 'port': 'ttyAMA0:MA', + 'driver_name': 'lego-nxt-motor', + 'commands': ['run-forever', 'run-to-abs-pos', 'run-to-rel-pos', 'run-timed', 'run-direct', 'stop', 'reset'], + 'stop_actions': ['coast', 'hold'], + 'speed_pid': { + 'kP': 1000, + 'kI': 60, + 'kD': 0 + }, + 'hold_pid': { + 'kP': 20000, + 'kI': 0, + 'kD': 0 + }, +} +pistorms_params = { + 'motor': ev3.Motor('pistorms:BAM1'), + 'port': 'pistorms:BAM1', + 'driver_name': 'lego-nxt-motor', + 'commands': ['run-forever', 'run-to-abs-pos', 'run-to-rel-pos', 'run-timed', 'stop', 'reset'], + 'stop_actions': ['coast', 'brake', 'hold'], + 'speed_pid': { + 'kP': 1000, + 'kI': 60, + 'kD': 0 + }, + 'hold_pid': { + 'kP': 20000, + 'kI': 0, + 'kD': 0 + }, +} +paramsA = ev3_params +paramsA['motor'].command = 'reset' + +suite = unittest.TestSuite() + +suite.addTest(ptc.ParameterizedTestCase.parameterize(TestTachoMotorAddressValue, param=paramsA)) +suite.addTest(ptc.ParameterizedTestCase.parameterize(TestTachoMotorCommandsValue, param=paramsA)) +suite.addTest(ptc.ParameterizedTestCase.parameterize(TestTachoMotorCountPerRotValue, param=paramsA)) +suite.addTest(ptc.ParameterizedTestCase.parameterize(TestTachoMotorDriverNameValue, param=paramsA)) +suite.addTest(ptc.ParameterizedTestCase.parameterize(TestTachoMotorDutyCycleSpValue, param=paramsA)) +suite.addTest(ptc.ParameterizedTestCase.parameterize(TestTachoMotorMaxSpeedValue, param=paramsA)) +suite.addTest(ptc.ParameterizedTestCase.parameterize(TestTachoMotorPositionPValue, param=paramsA)) +suite.addTest(ptc.ParameterizedTestCase.parameterize(TestTachoMotorPositionIValue, param=paramsA)) +suite.addTest(ptc.ParameterizedTestCase.parameterize(TestTachoMotorPositionDValue, param=paramsA)) +suite.addTest(ptc.ParameterizedTestCase.parameterize(TestTachoMotorPolarityValue, param=paramsA)) +suite.addTest(ptc.ParameterizedTestCase.parameterize(TestTachoMotorPositionValue, param=paramsA)) +suite.addTest(ptc.ParameterizedTestCase.parameterize(TestTachoMotorPositionSpValue, param=paramsA)) +suite.addTest(ptc.ParameterizedTestCase.parameterize(TestTachoMotorRampDownSpValue, param=paramsA)) +suite.addTest(ptc.ParameterizedTestCase.parameterize(TestTachoMotorRampUpSpValue, param=paramsA)) +suite.addTest(ptc.ParameterizedTestCase.parameterize(TestTachoMotorSpeedValue, param=paramsA)) +suite.addTest(ptc.ParameterizedTestCase.parameterize(TestTachoMotorSpeedSpValue, param=paramsA)) +suite.addTest(ptc.ParameterizedTestCase.parameterize(TestTachoMotorSpeedPValue, param=paramsA)) +suite.addTest(ptc.ParameterizedTestCase.parameterize(TestTachoMotorSpeedIValue, param=paramsA)) +suite.addTest(ptc.ParameterizedTestCase.parameterize(TestTachoMotorSpeedDValue, param=paramsA)) +suite.addTest(ptc.ParameterizedTestCase.parameterize(TestTachoMotorStateValue, param=paramsA)) +suite.addTest(ptc.ParameterizedTestCase.parameterize(TestTachoMotorStopCommandValue, param=paramsA)) +suite.addTest(ptc.ParameterizedTestCase.parameterize(TestTachoMotorStopCommandsValue, param=paramsA)) +suite.addTest(ptc.ParameterizedTestCase.parameterize(TestTachoMotorTimeSpValue, param=paramsA)) + +if __name__ == '__main__': + unittest.TextTestRunner(verbosity=1, buffer=True).run(suite) + +exit() + +# Move these up later + + +class TestMotorRelativePosition(unittest.TestCase): + @classmethod + def setUpClass(cls): + cls._motor = ev3.Motor('outA') + cls._motor.speed_sp = 400 + cls._motor.ramp_up_sp = 300 + cls._motor.ramp_down_sp = 300 + cls._motor.position = 0 + cls._motor.position_sp = 180 + pass + + @classmethod + def tearDownClass(cls): + pass + + @unittest.skip("Skipping coast mode - always fails") + def test_stop_coast(self): + self._motor.stop_action = 'coast' + self._motor.command = 'run-to-rel-pos' + time.sleep(1) + self.assertGreaterEqual(1, abs(self._motor.position - self._motor.position_sp)) + + def test_stop_brake(self): + self._motor.stop_action = 'brake' + self._motor.position = 0 + + for i in range(1, 5): + self._motor.command = 'run-to-rel-pos' + time.sleep(1) + print(self._motor.position) + self.assertGreaterEqual(8, abs(self._motor.position - (i * self._motor.position_sp))) + + def test_stop_hold(self): + self._motor.stop_action = 'hold' + self._motor.position = 0 + + for i in range(1, 5): + self._motor.command = 'run-to-rel-pos' + time.sleep(1) + print(self._motor.position) + self.assertGreaterEqual(1, abs(self._motor.position - (i * self._motor.position_sp))) + + +if __name__ == '__main__': + unittest.main(verbosity=2, buffer=True).run(suite) diff --git a/tests/motor/parameterizedtestcase.py b/tests/motor/parameterizedtestcase.py index 3c59cd5..bdca528 100644 --- a/tests/motor/parameterizedtestcase.py +++ b/tests/motor/parameterizedtestcase.py @@ -1,5 +1,6 @@ import unittest + class ParameterizedTestCase(unittest.TestCase): """ TestCase classes that want to be parametrized should inherit from this class. @@ -17,5 +18,5 @@ def parameterize(testcase_class, param=None): testnames = testloader.getTestCaseNames(testcase_class) suite = unittest.TestSuite() for name in testnames: - suite.addTest(testcase_class(name,param=param)) + suite.addTest(testcase_class(name, param=param)) return suite diff --git a/tests/motor/plot_matplotlib.py b/tests/motor/plot_matplotlib.py index b7ebfc2..c7a3725 100644 --- a/tests/motor/plot_matplotlib.py +++ b/tests/motor/plot_matplotlib.py @@ -3,8 +3,7 @@ import argparse parser = argparse.ArgumentParser(description='Plot ev3dev datalogs.') -parser.add_argument('infile', - help='the input file to be logged') +parser.add_argument('infile', help='the input file to be logged') args = parser.parse_args() @@ -14,11 +13,10 @@ # http://matplotlib.org/examples/pylab_examples/subplots_demo.html # These are the "Tableau 20" colors as RGB. -tableau20 = [(31, 119, 180), (174, 199, 232), (255, 127, 14), (255, 187, 120), - (44, 160, 44), (152, 223, 138), (214, 39, 40), (255, 152, 150), - (148, 103, 189), (197, 176, 213), (140, 86, 75), (196, 156, 148), - (227, 119, 194), (247, 182, 210), (127, 127, 127), (199, 199, 199), - (188, 189, 34), (219, 219, 141), (23, 190, 207), (158, 218, 229)] +tableau20 = [(31, 119, 180), (174, 199, 232), (255, 127, 14), (255, 187, 120), (44, 160, 44), (152, 223, 138), + (214, 39, 40), (255, 152, 150), (148, 103, 189), (197, 176, 213), (140, 86, 75), (196, 156, 148), + (227, 119, 194), (247, 182, 210), (127, 127, 127), (199, 199, 199), (188, 189, 34), (219, 219, 141), + (23, 190, 207), (158, 218, 229)] # Scale the RGB values to the [0, 1] range, which is the format matplotlib accepts. for i in range(len(tableau20)): @@ -27,47 +25,50 @@ plt.style.use(['dark_background']) -test = json.loads( open( args.infile ).read() ) +test = json.loads(open(args.infile).read()) values = {} # Extract the data from the log in a format that's useful for plotting -for k,d in test['data'].items(): +for k, d in test['data'].items(): values['k'] = {} values['k']['x'] = [row[0] for row in d] values['k']['y'] = [] - for i,a in enumerate(test['meta']['ports'][k]['log_attributes']): - values['k']['y'].append( {'name': a, 'values': [row[1][i] for row in d]}) + for i, a in enumerate(test['meta']['ports'][k]['log_attributes']): + values['k']['y'].append({'name': a, 'values': [row[1][i] for row in d]}) f, axarr = plt.subplots(3, sharex=True) axarr[2].set_xlabel('Time (seconds)') - f.text(.95,0, args.infile, - fontsize=10, - horizontalalignment='left', - verticalalignment='center' ) - - f.text(.5,1, "{0} - {1}".format( test['meta']['title'], k), - fontsize=14, - horizontalalignment='center', - verticalalignment='center' ) - - f.text(.5,.96, "{0}".format( test['meta']['subtitle']), - fontsize=10, - horizontalalignment='center', - verticalalignment='center' ) - - f.text(.92,.5, "{0}".format( test['meta']['notes']), - fontsize=10, - horizontalalignment='left', - verticalalignment='center' ) + f.text(.95, 0, args.infile, fontsize=10, horizontalalignment='left', verticalalignment='center') + + f.text(.5, + 1, + "{0} - {1}".format(test['meta']['title'], k), + fontsize=14, + horizontalalignment='center', + verticalalignment='center') + + f.text(.5, + .96, + "{0}".format(test['meta']['subtitle']), + fontsize=10, + horizontalalignment='center', + verticalalignment='center') + + f.text(.92, + .5, + "{0}".format(test['meta']['notes']), + fontsize=10, + horizontalalignment='left', + verticalalignment='center') # Clean up the chartjunk - for i,ax in enumerate(axarr): - print i, ax + for i, ax in enumerate(axarr): + print(i, ax) # Remove the plot frame lines. They are unnecessary chartjunk. ax.spines["top"].set_visible(False) @@ -76,12 +77,14 @@ ax.get_xaxis().tick_bottom() ax.get_yaxis().tick_left() - axarr[i].plot(values['k']['x'],values['k']['y'][i]['values'], lw=1.5, color=tableau20[i] ) - axarr[i].text(.95,1, "{0}".format( values['k']['y'][i]['name'] ), + axarr[i].plot(values['k']['x'], values['k']['y'][i]['values'], lw=1.5, color=tableau20[i]) + axarr[i].text(.95, + 1, + "{0}".format(values['k']['y'][i]['name']), fontsize=14, color=tableau20[i], horizontalalignment='right', verticalalignment='center', - transform = axarr[i].transAxes) + transform=axarr[i].transAxes) - plt.savefig("{0}-{1}.png".format(args.infile,k), bbox_inches="tight") \ No newline at end of file + plt.savefig("{0}-{1}.png".format(args.infile, k), bbox_inches="tight") diff --git a/utils/console_fonts.py b/utils/console_fonts.py new file mode 100644 index 0000000..ace86b0 --- /dev/null +++ b/utils/console_fonts.py @@ -0,0 +1,57 @@ +#!/usr/bin/env micropython +from time import sleep +from sys import stderr +from os import listdir +from ev3dev2.console import Console +""" +Used to iterate over the system console fonts (in /usr/share/consolefonts) and show the max row/col. + +Font names consist of three parameters - codeset, font face and font size. The codeset specifies +what characters will be supported by the font. The font face determines the general look of the font. Each +font face is available in certain possible sizes. + +For Codeset clarity, see https://www.systutorials.com/docs/linux/man/5-console-setup/#lbAP + +""" + + +def show_fonts(): + """ + Iterate through all the Latin "1 & 5" fonts, and see how many rows/columns + the EV3 LCD console can accommodate for each font. + Note: ``Terminus`` fonts are "thinner"; ``TerminusBold`` and ``VGA`` offer more contrast on the LCD console + and are thus more readable; the ``TomThumb`` font is waaaaay too small to read! + """ + console = Console() + files = [f for f in listdir("/usr/share/consolefonts/") if f.startswith("Lat15") and f.endswith(".psf.gz")] + files.sort() + fonts = [] + for font in files: + console.set_font(font, True) + console.text_at(font, 1, 1, False, True) + console.clear_to_eol() + console.text_at("{}, {}".format(console.columns, console.rows), + column=2, + row=4, + reset_console=False, + inverse=False) + print("{}, {}, \"{}\"".format(console.columns, console.rows, font), file=stderr) + fonts.append((console.columns, console.rows, font)) + + fonts.sort(key=lambda f: (f[0], f[1], f[2])) + + # Paint the screen full of numbers that represent the column number, reversing the even rows + for cols, rows, font in fonts: + print(cols, rows, font, file=stderr) + console.set_font(font, True) + for row in range(1, rows + 1): + for col in range(1, cols + 1): + console.text_at("{}".format(col % 10), col, row, False, (row % 2 == 0)) + console.text_at(font.split(".")[0], 1, 1, False, True) + console.clear_to_eol() + + +# Show the fonts; you may want to adjust the ``startswith`` filter to show other codesets. +show_fonts() + +sleep(5) diff --git a/utils/line-follower-find-kp-ki-kd.py b/utils/line-follower-find-kp-ki-kd.py new file mode 100755 index 0000000..bf97d7f --- /dev/null +++ b/utils/line-follower-find-kp-ki-kd.py @@ -0,0 +1,132 @@ +""" +This program is used to find the kp, ki, kd PID values for +``MoveTank.follow_line()``. These values vary from robot to robot, the best way +to find them for your robot is to have it follow a line, tweak the values a +little, repeat. + +The default speed for this program is SpeedPercent(30). You can use whatever +speed you want, just search for "speed = SpeedPercent(30)" in this file and +modigy that line. + +You can use the pdf from this site to print a line that makes an oval, just +have your robot follow that oval when running this program. +http://robotsquare.com/2012/11/28/line-following/ +""" + +from ev3dev2.motor import OUTPUT_A, OUTPUT_B, MoveTank, SpeedPercent, LineFollowError, follow_for_ms +from ev3dev2.sensor.lego import ColorSensor +import logging + + +def frange(start, end, increment): + """ + range() does not support floats, this frange() does + """ + result = [] + x = start + + while x < end: + result.append(x) + x += increment + + return result + + +def find_kp_ki_kd(tank, start, end, increment, speed, kx_to_tweak, kp, ki, kd): + """ + Return the optimal ``kx_to_tweak`` value where ``kx_to_tweak`` must be "kp", "ki" or "kd" + This will test values from ``start`` to ``end`` in steps of ``increment``. The value + that results in the robot moving the least total distance is the optimal value + that is returned by this function. + """ + min_delta = None + min_delta_kx = None + + for kx in frange(start, end, increment): + log.info("%s %s: place robot on line, then press " % (kx_to_tweak, kx)) + input("") + init_left_motor_pos = tank.left_motor.position + + try: + if kx_to_tweak == "kp": + tank.follow_line( + kp=kx, + ki=ki, + kd=kd, + speed=speed, + follow_for=follow_for_ms, + ms=10000, + ) + + elif kx_to_tweak == "ki": + tank.follow_line( + kp=kp, + ki=kx, + kd=kd, + speed=speed, + follow_for=follow_for_ms, + ms=10000, + ) + + elif kx_to_tweak == "kd": + tank.follow_line( + kp=kp, + ki=ki, + kd=kx, + speed=speed, + follow_for=follow_for_ms, + ms=10000, + ) + + else: + raise Exception("Invalid kx_to_tweak %s" % kx_to_tweak) + + except LineFollowError: + continue + + except Exception: + tank.stop() + raise + + final_left_motor_pos = tank.left_motor.position + delta_left_motor_pos = abs(final_left_motor_pos - init_left_motor_pos) + + if min_delta is None or delta_left_motor_pos < min_delta: + min_delta = delta_left_motor_pos + min_delta_kx = kx + log.info("%s: %s %s, left motor moved %s (NEW MIN)" % (tank, kx_to_tweak, kx, delta_left_motor_pos)) + else: + log.info("%s: %s %s, left motor moved %s" % (tank, kx_to_tweak, kx, delta_left_motor_pos)) + + tank.stop() + return min_delta_kx + + +if __name__ == "__main__": + + # logging + logging.basicConfig(level=logging.DEBUG, format="%(asctime)s %(levelname)5s: %(message)s") + log = logging.getLogger(__name__) + + tank = MoveTank(OUTPUT_A, OUTPUT_B) + tank.cs = ColorSensor() + + speed = SpeedPercent(30) + + # Find the best integer for kp (in increments of 1) then fine tune by + # finding the best float (in increments of 0.1) + kp = find_kp_ki_kd(tank, 1, 20, 1, speed, 'kp', 0, 0, 0) + kp = find_kp_ki_kd(tank, kp - 1, kp + 1, 0.1, speed, 'kp', kp, 0, 0) + print("\n\n\n%s\nkp %s\n%s\n\n\n" % ("" * 10, kp, "*" * 10)) + + # Find the best float ki (in increments of 0.1) + ki = find_kp_ki_kd(tank, 0, 1, 0.1, speed, 'ki', kp, 0, 0) + print("\n\n\n%s\nki %s\n%s\n\n\n" % ("" * 10, ki, "*" * 10)) + + # Find the best integer for kd (in increments of 1) then fine tune by + # finding the best float (in increments of 0.1) + kd = find_kp_ki_kd(tank, 0, 10, 1, speed, 'kd', kp, ki, 0) + kd = find_kp_ki_kd(tank, kd - 1, kd + 1, 0.1, speed, 'kd', kp, ki, 0) + print("\n\n\n%s\nkd %s\n%s\n\n\n" % ("" * 10, kd, "*" * 10)) + + print("Final results: kp %s, ki %s, kd %s" % (kp, ki, kd)) diff --git a/utils/move_differential.py b/utils/move_differential.py new file mode 100755 index 0000000..34d406d --- /dev/null +++ b/utils/move_differential.py @@ -0,0 +1,81 @@ +#!/usr/bin/env python3 +""" +Used to experiment with the MoveDifferential class +""" + +from ev3dev2.motor import OUTPUT_A, OUTPUT_B, MoveDifferential, SpeedRPM +from ev3dev2.wheel import EV3Tire +from math import pi +import logging + +# logging +logging.basicConfig(level=logging.DEBUG, format="%(asctime)s %(levelname)5s: %(message)s") +log = logging.getLogger(__name__) + +STUD_MM = 8 +INCH_MM = 25.4 + +ONE_FOOT_CICLE_RADIUS_MM = (12 * INCH_MM) / 2 +ONE_FOOT_CICLE_CIRCUMFERENCE_MM = 2 * pi * ONE_FOOT_CICLE_RADIUS_MM + +# Testing with RileyRover +# http://www.damienkee.com/rileyrover-ev3-classroom-robot-design/ +# +# The centers of the wheels are 16 studs apart but this is not the +# "effective" wheel seperation. Test drives of circles with +# a diameter of 1-foot shows that the effective wheel seperation is +# closer to 16.3 studs. ndward has a writeup that goes into effective +# wheel seperation. +# https://sites.google.com/site/ev3basic/ev3-basic-programming/going-further/writerbot-v1/drawing-arcs +mdiff = MoveDifferential(OUTPUT_A, OUTPUT_B, EV3Tire, 16.3 * STUD_MM) + +# This goes crazy on brickpi3, does it do the same on ev3? +# mdiff.on_for_distance(SpeedRPM(-40), 720, brake=False) +# mdiff.on_for_distance(SpeedRPM(40), 720, brake=False) + +# Test arc left/right turns +# mdiff.on_arc_right(SpeedRPM(80), ONE_FOOT_CICLE_RADIUS_MM, ONE_FOOT_CICLE_CIRCUMFERENCE_MM / 4) +mdiff.on_arc_left(SpeedRPM(80), ONE_FOOT_CICLE_RADIUS_MM, ONE_FOOT_CICLE_CIRCUMFERENCE_MM) + +# Test turning in place +# mdiff.turn_right(SpeedRPM(40), 180) +# mdiff.turn_left(SpeedRPM(40), 180) + +# Test odometry +# mdiff.odometry_start() +# mdiff.odometry_coordinates_log() + +# from ev3dev2.unit import DistanceFeet +# mdiff.turn_to_angle(SpeedRPM(40), 0) +# mdiff.on_for_distance(SpeedRPM(40), DistanceFeet(2).mm) +# mdiff.turn_right(SpeedRPM(40), 180) +# mdiff.turn_left(SpeedRPM(30), 90) +# mdiff.on_arc_left(SpeedRPM(80), ONE_FOOT_CICLE_RADIUS_MM, ONE_FOOT_CICLE_CIRCUMFERENCE_MM) + +# Drive in a quarter arc to the right then go back to where you started +# log.info("turn on arc to the right") +# mdiff.on_arc_right(SpeedRPM(40), ONE_FOOT_CICLE_RADIUS_MM, ONE_FOOT_CICLE_CIRCUMFERENCE_MM / 4) +# mdiff.odometry_coordinates_log() +# log.info("\n\n\n\n") +# log.info("go back to (0, 0)") +# mdiff.odometry_coordinates_log() +# mdiff.on_to_coordinates(SpeedRPM(40), 0, 0) +# mdiff.turn_to_angle(SpeedRPM(40), 90) + +# Drive in a rectangle +# mdiff.turn_to_angle(SpeedRPM(40), 120) +# mdiff.on_to_coordinates(SpeedRPM(40), 0, DistanceFeet(1).mm) +# mdiff.on_to_coordinates(SpeedRPM(40), DistanceFeet(2).mm, DistanceFeet(1).mm) +# mdiff.on_to_coordinates(SpeedRPM(40), DistanceFeet(2).mm, 0) +# mdiff.on_to_coordinates(SpeedRPM(40), 0, 0) +# mdiff.turn_to_angle(SpeedRPM(40), 90) + +# Use odometry to drive to specific coordinates +# mdiff.on_to_coordinates(SpeedRPM(40), 600, 300) + +# Now go back to where we started and rotate in place to 90 degrees +# mdiff.on_to_coordinates(SpeedRPM(40), 0, 0) +# mdiff.turn_to_angle(SpeedRPM(40), 90) + +# mdiff.odometry_coordinates_log() +# mdiff.odometry_stop() diff --git a/utils/move_motor.py b/utils/move_motor.py index 9b7360b..bfafd45 100755 --- a/utils/move_motor.py +++ b/utils/move_motor.py @@ -1,14 +1,12 @@ #!/usr/bin/env python3 - """ Used to adjust the position of a motor in an already assembled robot where you can"t move the motor by hand. """ -from ev3dev.auto import OUTPUT_A, OUTPUT_B, OUTPUT_C, OUTPUT_D, Motor +from ev3dev2.motor import OUTPUT_A, OUTPUT_B, OUTPUT_C, OUTPUT_D, Motor import argparse import logging -import sys # command line args parser = argparse.ArgumentParser(description="Used to adjust the position of a motor in an already assembled robot") @@ -18,8 +16,7 @@ args = parser.parse_args() # logging -logging.basicConfig(level=logging.INFO, - format="%(asctime)s %(levelname)5s: %(message)s") +logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)5s: %(message)s") log = logging.getLogger(__name__) if args.motor == "A": @@ -33,13 +30,7 @@ else: raise Exception("%s is invalid, options are A, B, C, D") -if not motor.connected: - log.error("%s is not connected" % motor) - sys.exit(1) - if args.degrees: log.info("Motor %s, current position %d, move to position %d, max speed %d" % (args.motor, motor.position, args.degrees, motor.max_speed)) - motor.run_to_rel_pos(speed_sp=args.speed, - position_sp=args.degrees, - stop_action='hold') + motor.run_to_rel_pos(speed_sp=args.speed, position_sp=args.degrees, stop_action='hold') diff --git a/utils/stop_all_motors.py b/utils/stop_all_motors.py index 55da422..e040146 100755 --- a/utils/stop_all_motors.py +++ b/utils/stop_all_motors.py @@ -1,9 +1,8 @@ -#!/usr/bin/env python3 - +#!/usr/bin/env micropython """ Stop all motors """ -from ev3dev.auto import list_motors +from ev3dev2.motor import list_motors for motor in list_motors(): motor.stop(stop_action='brake')