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 858a774..4920911 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,12 @@ __pycache__ *.pyc *.swp +*.log dist *.egg-info RELEASE-VERSION +ev3dev2/version.py +build +_build/ +docs/_build/ +.idea diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..5af5621 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "tests/fake-sys"] + path = tests/fake-sys + 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 71ffed1..87ed261 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,16 +1,33 @@ language: python +env: + - USE_MICROPYTHON=false + - USE_MICROPYTHON=true python: -- 2.7 +- 3.8 +cache: + pip: true + directories: + - ~/micropython + - ~/.micropython sudo: false +git: + depth: 100 install: -- pip install PIL --allow-external PIL --allow-unverified PIL +- 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 new file mode 100644 index 0000000..bad507f --- /dev/null +++ b/CONTRIBUTING.rst @@ -0,0 +1,140 @@ +Contributing to the Python language bindings for ev3dev +======================================================= + +This repository holds the Python bindings for ev3dev_, ev3dev-lang-python. + +Opening issues +-------------- + +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! + +Submitting Pull Requests +------------------------ + +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. + +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. + +The ``ev3dev-stretch`` branch +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +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 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 +issues! + +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: + + +.. 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 new file mode 100644 index 0000000..5b4ca0e --- /dev/null +++ b/ISSUE_TEMPLATE.md @@ -0,0 +1,10 @@ + + +- **ev3dev version:** PASTE THE OUTPUT OF `uname -r` HERE +- **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 8cae63a..a1c414a 100644 --- a/README.rst +++ b/README.rst @@ -1,13 +1,241 @@ 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=latest - :target: http://python-ev3dev.readthedocs.org/en/latest/?badge=latest - :alt: Documentation Statu +.. 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 -This is a python library implementing unified interface for ev3dev_ devices. +A Python3 library implementing an interface for ev3dev_ devices, +letting you control motors, sensors, hardware buttons, LCD +displays and more from Python code. -.. _ev3dev: http://ev3dev.org +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 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`_. + +Usage +----- + +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. + +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. + +The template for a Python script +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Every Python program should have a few basic parts. Use this template +to get started: + +.. code-block:: python + + #!/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 + + # TODO: Add code here + +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. + +You should use the ``.py`` extension for your file, e.g. ``my-file.py``. + +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`_. + +Important: Make your script executable (non-Visual Studio Code only) +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +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. + +**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 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 = TouchSensor() + leds = Leds() + + print("Press the touch sensor to change the LED color!") + + 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) + +If you'd like to use a sensor on a specific port, specify the port like this: + +.. 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 + +Running a single motor +~~~~~~~~~~~~~~~~~~~~~~ + +This will run a LEGO Large Motor at 75% of maximum speed for 5 rotations. +.. code-block:: python + + m = LargeMotor(OUTPUT_A) + m.on_for_rotations(SpeedPercent(75), 5) + +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: + +- 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 + +Driving with two motors +~~~~~~~~~~~~~~~~~~~~~~~ + +The simplest drive control style is with the `MoveTank` class: + +.. code-block:: python + + tank_drive = MoveTank(OUTPUT_A, OUTPUT_B) + + # 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) + + # drive in a different turn for 3 seconds + tank_drive.on_for_seconds(SpeedPercent(60), SpeedPercent(30), 3) + +There are also `MoveSteering` and `MoveJoystick` classes which provide +different styles of control. See the following pages for more information: + +- http://python-ev3dev.readthedocs.io/en/ev3dev-stretch/motors.html#multiple-motor-groups +- http://python-ev3dev.readthedocs.io/en/ev3dev-stretch/motors.html#units + +Using text-to-speech +~~~~~~~~~~~~~~~~~~~~ + +If you want to make your robot speak, you can use the ``Sound.speak`` method: + +.. code-block:: python + + from ev3dev2.sound import Sound + + sound = Sound() + sound.speak('Welcome to the E V 3 dev project!') + +More Demo Code +~~~~~~~~~~~~~~ + +There are several demo programs that you can run to get acquainted with +this language binding. The programs are available +`at this GitHub site `_. + +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. + +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 +----------------- + +Normal Python too slow? Review `Micropython`_ to see if it supports the +features your project needs. + +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. + + +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. + + +.. _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/debian/changelog b/debian/changelog index c5f75ff..a88a243 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,5 +1,118 @@ -python-ev3dev (0.4.2) stable; urgency=low +python-ev3dev2 (2.1.0) stretch; urgency=medium - * source package automatically created by stdeb 0.8.2 + [Daniel Walton] + * 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 - -- Ralph Hempel Sun, 01 Nov 2015 16:51:45 -0500 + [Matěj Volf] + * LED animation fix duration None + * Add rest notes to sound.Sound.play_song() + + [Kaelin Laundry] + * 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 + + [Nithin Shenoy] + * Add Gyro-based driving support to MoveTank + + -- Kaelin Laundry Sun, 22 Mar 2020 19:14:35 -0700 + +python-ev3dev2 (2.0.0) stretch; urgency=medium + + [David Lechner] + * Fix gyro sensor reset + + -- Kaelin Laundry Sun, 24 Nov 2019 20:41:18 -0800 + +python-ev3dev2 (2.0.0~beta5) stretch; urgency=medium + + [Brady Merkel] + * Add console and stopwatch to Debian rules file so they are + included in MicroPython package + + -- Kaelin Laundry Sun, 18 Aug 2019 11:37:17 -0700 + +python-ev3dev2 (2.0.0~beta4) stretch; urgency=medium + + [Daniel Walton] + * 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 + + [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] + * Added new binary package for Micropython + + [Viktor Garske] + * Fixed error when using Motor.is_stalled + + -- Kaelin Laundry Sat, 2 Feb 2019 1:58:00 -0800 + +python-ev3dev2 (2.0.0~beta2) stable; urgency=medium + + * 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 + + -- Kaelin Laundry Sun, 23 Sep 2018 16:03:00 -0700 + +python-ev3dev2 (2.0.0~beta1) stable; urgency=medium + + Initial beta release for ev3dev-stretch. + + -- Kaelin Laundry Tue, 31 Jul 2018 20:37:00 -0700 diff --git a/debian/control b/debian/control index c15cda1..c189a01 100644 --- a/debian/control +++ b/debian/control @@ -1,17 +1,26 @@ -Source: python-ev3dev -Maintainer: Ralph Hempel +Source: python-ev3dev2 +Maintainer: ev3dev Python team Section: python Priority: optional -Standards-Version: 3.9.5 -Build-Depends: python-setuptools (>= 0.6b3), python-all (>= 2.6.6-3), 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: python-ev3dev +Package: python3-ev3dev2 Architecture: all -Depends: ${misc:Depends}, ${python:Depends} +Depends: ${misc:Depends}, ${python3:Depends} Description: Python language bindings for ev3dev 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. + +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/copyright b/debian/copyright index 772f36b..d787b3f 100644 --- a/debian/copyright +++ b/debian/copyright @@ -1,13 +1,17 @@ Files: debian/* License: MIT -Copyright: 2015 Ralph Hempel +Copyright: 2016 Ralph Hempel Files: * License: MIT -Copyright: 2015 Ralph Hempel -Copyright: 2015 Denis Demidov +Copyright: 2016 Ralph Hempel +Copyright: 2015-2016 Denis Demidov Copyright: 2015 Eric Pascual Copyright: 2015 Anton Vanhoucke +Copyright: 2016 Kaelin Laundry +Copyright: 2016 Daniel Walton +Copyright: 2016 Donald Webster +Copyright: 2016 Frank Busse License: MIT Permission is hereby granted, free of charge, to any person obtaining a 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 65d6d91..edc3d3b 100755 --- a/debian/rules +++ b/debian/rules @@ -1,17 +1,68 @@ #!/usr/bin/make -f +#export DH_VERBOSE=1 -export PYBUILD_NAME=python-ev3dev +export PYBUILD_NAME=ev3dev2 -VERSION=$(shell dpkg-parsechangelog | sed -rne 's,^Version: (.*),\1,p') +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 python2 --buildsystem=pybuild + 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 + echo $(VERSION) > RELEASE-VERSION dh_auto_configure override_dh_auto_clean: - echo VERSION > RELEASE-VERSION + echo $(VERSION) > RELEASE-VERSION dh_auto_clean rm -f RELEASE-VERSION diff --git a/docs/Makefile b/docs/Makefile index b968e19..5f14e56 100644 --- a/docs/Makefile +++ b/docs/Makefile @@ -3,7 +3,7 @@ # You can set these variables from the command line. SPHINXOPTS = -SPHINXBUILD = sphinx-build +SPHINXBUILD = ./sphinx3-build PAPER = BUILDDIR = _build diff --git a/docs/_static/custom.css b/docs/_static/custom.css new file mode 100644 index 0000000..fa50035 --- /dev/null +++ b/docs/_static/custom.css @@ -0,0 +1,14 @@ +pre { + box-shadow: 0px 1px 6px 0px lightgray; +} + +dl.class { + padding: 5px; + box-shadow: 0px 1px 6px 0px lightgray; +} + +pre { + overflow: auto; + word-wrap: normal; + white-space: pre; +} diff --git a/docs/_static/fonts.png b/docs/_static/fonts.png new file mode 100644 index 0000000..31fe846 Binary files /dev/null and b/docs/_static/fonts.png differ diff --git a/docs/_templates/page.html b/docs/_templates/page.html new file mode 100644 index 0000000..c65ef34 --- /dev/null +++ b/docs/_templates/page.html @@ -0,0 +1,5 @@ +{# Import the theme's layout. #} +{% extends "!page.html" %} + +{# Custom CSS overrides #} +{% set bootswatch_css_custom = ['_static/custom.css'] %} 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 af92edc..b9e9a8b 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -15,20 +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 +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 @@ -41,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' @@ -75,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. @@ -85,61 +92,65 @@ # 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 # a list of builtin themes. -#html_theme = 'sphinx_rtd_theme' +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/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, @@ -149,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' @@ -212,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 ------------------------------------------- @@ -273,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 new file mode 100644 index 0000000..2033729 --- /dev/null +++ b/docs/faq.rst @@ -0,0 +1,81 @@ +Frequently-Asked Questions +========================== + +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: + + .. code:: shell + + 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. + +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 7d9a2ec..1d13ed6 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -1,133 +1,12 @@ -.. 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 -Module interface ----------------- - -.. automodule:: ev3dev - -.. autosummary:: - :nosignatures: - - Device - Motor - DcMotor - ServoMotor - MediumMotor - LargeMotor - Sensor - I2cSensor - TouchSensor - ColorSensor - UltrasonicSensor - GyroSensor - SoundSensor - LightSensor - InfraredSensor - RemoteControl - Led - PowerSupply - Button - Sound - Screen - -Generic device -^^^^^^^^^^^^^^ - -.. autoclass:: Device - :members: - -Motors -^^^^^^ - -.. autoclass:: Motor - :members: - -.. autoclass:: MediumMotor - :members: - :show-inheritance: - -.. autoclass:: LargeMotor - :members: - :show-inheritance: - -.. autoclass:: DcMotor - :members: - -.. autoclass:: ServoMotor - :members: - -Sensors -^^^^^^^ - -.. autoclass:: Sensor - :members: - -.. autoclass:: I2cSensor - :members: - :show-inheritance: - -.. autoclass:: TouchSensor - :members: - :show-inheritance: - -.. autoclass:: ColorSensor - :members: - :show-inheritance: - -.. autoclass:: UltrasonicSensor - :members: - :show-inheritance: - -.. autoclass:: GyroSensor - :members: - :show-inheritance: - -.. autoclass:: SoundSensor - :members: - :show-inheritance: - -.. autoclass:: LightSensor - :members: - :show-inheritance: - -.. autoclass:: InfraredSensor - :members: - :show-inheritance: - -.. autoclass:: RemoteControl - :members: - :inherited-members: - -Other -^^^^^ - -.. autoclass:: Led - :members: - -.. autoclass:: PowerSupply - :members: - -.. autoclass:: Button - :members: - :inherited-members: - -.. autoclass:: Sound - :members: - -.. autoclass:: Screen - :members: - :show-inheritance: - - -Indices and tables -================== +.. rubric:: Contents -* :ref:`genindex` -* :ref:`modindex` -* :ref:`search` +.. toctree:: + :maxdepth: 3 + micropython + upgrading-to-stretch + spec + rpyc + faq 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 new file mode 100644 index 0000000..beb399f --- /dev/null +++ b/docs/motors.rst @@ -0,0 +1,129 @@ +Motor classes +============= + +.. currentmodule:: ev3dev2.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 new file mode 100644 index 0000000..e7dc2bf --- /dev/null +++ b/docs/other.rst @@ -0,0 +1,34 @@ +:orphan: + +Other classes +============= + +Button +------ + +See :py:class:`ev3dev2.button.Button`. + +Leds +---- + +See :ref:`led-classes`. + +Power Supply +------------ + +See :py:class:`ev3dev2.power.PowerSupply`. + +Sound +----- + +See :py:class:`ev3dev2.sound.Sound`. + +Display +------- + +See :py:class:`ev3dev2.display.Display`. + +Lego Port +--------- + +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 new file mode 100644 index 0000000..46a41d6 --- /dev/null +++ b/docs/rpyc.rst @@ -0,0 +1,115 @@ +************** +RPyC on 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. + +For both of these scenarios you can use RPyC to control multiple remote ev3dev devices. + + +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 + +The `ev3dev networking documentation `_ should get +you up and running in terms of networking connectivity. + + +Install +======= + +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 + + echo "[Unit] + Description=RPyC Classic Service + After=multi-user.target + + [Service] + Type=simple + ExecStart=/usr/bin/rpyc_classic.py + + [Install] + WantedBy=multi-user.target" > rpyc-classic.service + + sudo cp rpyc-classic.service /lib/systemd/system/ + sudo systemctl daemon-reload + sudo systemctl enable rpyc-classic.service + sudo systemctl start rpyc-classic.service + + +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: + + .. code-block:: shell + + sudo apt-get install python3-rpyc + + 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. + +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 + + # 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 new file mode 100644 index 0000000..0c86881 --- /dev/null +++ b/docs/sensors.rst @@ -0,0 +1,100 @@ +Sensor classes +============== + +.. contents:: :local: + +*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 `_. + + +Dedicated 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. + +.. + +.. currentmodule:: ev3dev2.sensor.lego + +Touch Sensor +############ + +.. autoclass:: TouchSensor + :members: + :show-inheritance: + + + +Color Sensor +############ + +.. autoclass:: ColorSensor + :members: + :show-inheritance: + + + +Ultrasonic Sensor +################# + +.. autoclass:: UltrasonicSensor + :members: + :show-inheritance: + + + +Gyro Sensor +########### + +.. autoclass:: GyroSensor + :members: + :show-inheritance: + + + +Infrared Sensor +############### + +.. autoclass:: InfraredSensor + :members: + :show-inheritance: + + + +Sound Sensor +############ + +.. autoclass:: SoundSensor + :members: + :show-inheritance: + + + +Light Sensor +############ + +.. autoclass:: LightSensor + :members: + :show-inheritance: + + + +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: + + +.. + 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 new file mode 100644 index 0000000..9738ac8 --- /dev/null +++ b/docs/spec.rst @@ -0,0 +1,38 @@ +API reference +============= + +Device interfaces +----------------- + +.. rubric:: Contents: + +.. toctree:: + :maxdepth: 2 + + motors + sensors + 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 new file mode 100755 index 0000000..89b4cee --- /dev/null +++ b/docs/sphinx3-build @@ -0,0 +1,17 @@ +#!/usr/bin/python3 +# -*- coding: utf-8 -*- +""" +Same as /usr/bin/sphinx-build but with different +interpreter + +Source: http://stackoverflow.com/a/24675806 +""" + +import sys + +if __name__ == '__main__': + from sphinx import main, make_main + if sys.argv[1:2] == ['-M']: + 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/__init__.py b/ev3dev/__init__.py deleted file mode 100644 index 4123341..0000000 --- a/ev3dev/__init__.py +++ /dev/null @@ -1,18 +0,0 @@ -import platform - -# ----------------------------------------------------------------------------- -# Guess platform we are running on -def current_platform(): - machine = platform.machine() - if machine == 'armv5tejl': - return 'ev3' - elif machine == 'armv6l': - return 'brickpi' - else: - return 'unsupported' - -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/core.py b/ev3dev/core.py deleted file mode 100644 index ca71b92..0000000 --- a/ev3dev/core.py +++ /dev/null @@ -1,2381 +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 v0.9.3-pre, rev 2 - -# ~autogen - -import os -import fnmatch -import numbers -import fcntl -import array -import mmap -import ctypes -import re -from os.path import abspath -from PIL import Image, ImageDraw -from struct import pack, unpack -from subprocess import Popen - -INPUT_AUTO = '' -OUTPUT_AUTO = '' - -# ----------------------------------------------------------------------------- -# Attribute reader/writer with cached file access -class FileCache(object): - def __init__(self): - self._cache = {} - - def __del__(self): - for f in self._cache.values(): - f.close() - - def file_handle(self, path, mode, reopen=False): - """Manages the file handle cache and opening the files in the correct mode""" - - if path not in self._cache: - f = open(path, mode, 0) - self._cache[path] = f - elif reopen: - self._cache[path].close() - f = open(path, mode, 0) - self._cache[path] = f - else: - f = self._cache[path] - - return f - - def read(self, path): - f = self.file_handle(path, 'r') - - try: - f.seek(0) - value = f.read() - except IOError: - f = self.file_handle(path, 'w+', reopen=True) - value = f.read() - - return value.strip() - - def write(self, path, value): - f = self.file_handle(path, 'w') - - try: - f.seek(0) - f.write(value) - except IOError: - f = self.file_handle(path, 'w+', reopen=True) - f.write(value) - - -# ----------------------------------------------------------------------------- -# Define the base class from which all other ev3dev classes are defined. - -class Device(object): - """The ev3dev device base class""" - - DEVICE_ROOT_PATH = '/sys/class' - - _DEVICE_INDEX = re.compile(r'^.*(?P\d+)$') - - def __init__(self, class_name, name='*', **kwargs): - """Spin through the Linux sysfs class for the device type and find - a device that matches the provided name and attributes (if any). - - Parameters: - class_name: class name of the device, a subdirectory of /sys/class. - For example, 'tacho-motor'. - name: pattern that device name should match. - For example, 'sensor*' or 'motor*'. Default value: '*'. - keyword arguments: used for matching the corresponding device - attributes. For example, port_name='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', port_name='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._attribute_cache = FileCache() - - for file in os.listdir(classpath): - if fnmatch.fnmatch(file, name): - self._path = abspath(classpath + '/' + file) - - # See if requested attributes match: - if all([self._matches(k, kwargs[k]) for k in kwargs]): - self.connected = True - - match = Device._DEVICE_INDEX.match(file) - if match: - self._device_index = int(match.group('idx')) - else: - self._device_index = None - - return - - self._path = '' - self.connected = False - - def _matches(self, attribute, pattern): - """Test if attribute value matches pattern (that is, if pattern is a - substring of attribute value). If pattern is a list, then a match with - any one entry is enough. - """ - value = self._get_attribute(attribute) - if isinstance(pattern, list): - return any([value.find(pat) >= 0 for pat in pattern]) - else: - return value.find(pattern) >= 0 - - def _get_attribute(self, attribute): - """Device attribute getter""" - return self._attribute_cache.read(abspath(self._path + '/' + attribute)) - - def _set_attribute(self, attribute, value): - """Device attribute setter""" - self._attribute_cache.write(abspath(self._path + '/' + attribute), value) - - def get_attr_int(self, attribute): - return int(self._get_attribute(attribute)) - - def set_attr_int(self, attribute, value): - self._set_attribute(attribute, '{0:d}'.format(int(value))) - - def get_attr_string(self, attribute): - return self._get_attribute(attribute) - - def set_attr_string(self, attribute, value): - self._set_attribute(attribute, "{0}".format(value)) - - def get_attr_line(self, attribute): - return self._get_attribute(attribute) - - def get_attr_set(self, attribute): - return [v.strip('[]') for v in self.get_attr_line(attribute).split()] - - def get_attr_from_set(self, attribute): - for a in self.get_attr_line(attribute).split(): - v = a.strip('[]') - if v != a: - return v - return "" - - @property - def device_index(self): - return self._device_index - - -# ~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`. - """ - - SYSTEM_CLASS_NAME = 'tacho-motor' - SYSTEM_DEVICE_NAME_CONVENTION = 'motor*' - - def __init__(self, port=None, name=SYSTEM_DEVICE_NAME_CONVENTION, **kwargs): - if port is not None: - kwargs['port_name'] = port - Device.__init__(self, self.SYSTEM_CLASS_NAME, name, **kwargs) - - -# ~autogen -# ~autogen generic-get-set classes.motor>currentClass - - @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.set_attr_string('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 command specified in `stop_command`. - - `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 command specified by `stop_command`. - - `run-timed` will run the motor for the amount of time specified in `time_sp` - and then stop the motor using the command specified by `stop_command`. - - `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 - command specified by `stop_command`. - - `reset` will reset all of the motor parameter attributes to their default value. - This will also have the effect of stopping the motor. - """ - return self.get_attr_set('commands') - - @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. In the case of linear - actuators, the units here will be counts per centimeter. - """ - return self.get_attr_int('count_per_rot') - - @property - def driver_name(self): - """ - Returns the name of the driver that provides this tacho motor device. - """ - return self.get_attr_string('driver_name') - - @property - def duty_cycle(self): - """ - Returns the current duty cycle of the motor. Units are percent. Values - are -100 to 100. - """ - return self.get_attr_int('duty_cycle') - - @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. This value is only used when `speed_regulation` - is off. - """ - return self.get_attr_int('duty_cycle_sp') - - @duty_cycle_sp.setter - def duty_cycle_sp(self, value): - self.set_attr_int('duty_cycle_sp', value) - - @property - def encoder_polarity(self): - """ - Sets the polarity of the rotary encoder. This is an advanced feature to all - use of motors that send inversed encoder signals to the EV3. This should - be set correctly by the driver of a device. It You only need to change this - value if you are using a unsupported device. Valid values are `normal` and - `inversed`. - """ - return self.get_attr_string('encoder_polarity') - - @encoder_polarity.setter - def encoder_polarity(self, value): - self.set_attr_string('encoder_polarity', 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`. - """ - return self.get_attr_string('polarity') - - @polarity.setter - def polarity(self, value): - self.set_attr_string('polarity', value) - - @property - def port_name(self): - """ - Returns the name of the port that the motor is connected to. - """ - return self.get_attr_string('port_name') - - @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. - """ - return self.get_attr_int('position') - - @position.setter - def position(self, value): - self.set_attr_int('position', value) - - @property - def position_p(self): - """ - The proportional constant for the position PID. - """ - return self.get_attr_int('hold_pid/Kp') - - @position_p.setter - def position_p(self, value): - self.set_attr_int('hold_pid/Kp', value) - - @property - def position_i(self): - """ - The integral constant for the position PID. - """ - return self.get_attr_int('hold_pid/Ki') - - @position_i.setter - def position_i(self, value): - self.set_attr_int('hold_pid/Ki', value) - - @property - def position_d(self): - """ - The derivative constant for the position PID. - """ - return self.get_attr_int('hold_pid/Kd') - - @position_d.setter - def position_d(self, value): - self.set_attr_int('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. - """ - return self.get_attr_int('position_sp') - - @position_sp.setter - def position_sp(self, value): - self.set_attr_int('position_sp', value) - - @property - def speed(self): - """ - Returns the current motor speed in tacho counts per second. Not, 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. - """ - return self.get_attr_int('speed') - - @property - def speed_sp(self): - """ - Writing sets the target speed in tacho counts per second used when `speed_regulation` - is on. Reading returns the current value. Use the `count_per_rot` attribute - to convert RPM or deg/sec to tacho counts per second. - """ - return self.get_attr_int('speed_sp') - - @speed_sp.setter - def speed_sp(self, value): - self.set_attr_int('speed_sp', value) - - @property - def ramp_up_sp(self): - """ - Writing sets the ramp up setpoint. Reading returns the current value. Units - are in milliseconds. When set to a value > 0, the motor will ramp the power - sent to the motor from 0 to 100% duty cycle over the span of this setpoint - when starting the motor. If the maximum duty cycle is limited by `duty_cycle_sp` - or speed regulation, the actual ramp time duration will be less than the setpoint. - """ - return self.get_attr_int('ramp_up_sp') - - @ramp_up_sp.setter - def ramp_up_sp(self, value): - self.set_attr_int('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. When set to a value > 0, the motor will ramp the power - sent to the motor from 100% duty cycle down to 0 over the span of this setpoint - when stopping the motor. If the starting duty cycle is less than 100%, the - ramp time duration will be less than the full span of the setpoint. - """ - return self.get_attr_int('ramp_down_sp') - - @ramp_down_sp.setter - def ramp_down_sp(self, value): - self.set_attr_int('ramp_down_sp', value) - - @property - def speed_regulation_enabled(self): - """ - Turns speed regulation on or off. If speed regulation is on, the motor - controller will vary the power supplied to the motor to try to maintain the - speed specified in `speed_sp`. If speed regulation is off, the controller - will use the power specified in `duty_cycle_sp`. Valid values are `on` and - `off`. - """ - return self.get_attr_string('speed_regulation') - - @speed_regulation_enabled.setter - def speed_regulation_enabled(self, value): - self.set_attr_string('speed_regulation', value) - - @property - def speed_regulation_p(self): - """ - The proportional constant for the speed regulation PID. - """ - return self.get_attr_int('speed_pid/Kp') - - @speed_regulation_p.setter - def speed_regulation_p(self, value): - self.set_attr_int('speed_pid/Kp', value) - - @property - def speed_regulation_i(self): - """ - The integral constant for the speed regulation PID. - """ - return self.get_attr_int('speed_pid/Ki') - - @speed_regulation_i.setter - def speed_regulation_i(self, value): - self.set_attr_int('speed_pid/Ki', value) - - @property - def speed_regulation_d(self): - """ - The derivative constant for the speed regulation PID. - """ - return self.get_attr_int('speed_pid/Kd') - - @speed_regulation_d.setter - def speed_regulation_d(self, value): - self.set_attr_int('speed_pid/Kd', value) - - @property - def state(self): - """ - Reading returns a list of state flags. Possible flags are - `running`, `ramping` `holding` and `stalled`. - """ - return self.get_attr_set('state') - - @property - def stop_command(self): - """ - Reading returns the current stop command. Writing sets the stop command. - 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_commands` for a list of possible values. - """ - return self.get_attr_string('stop_command') - - @stop_command.setter - def stop_command(self, value): - self.set_attr_string('stop_command', value) - - @property - def stop_commands(self): - """ - Returns a list of stop modes 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 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. - """ - return self.get_attr_set('stop_commands') - - @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. - """ - return self.get_attr_int('time_sp') - - @time_sp.setter - def time_sp(self, value): - self.set_attr_int('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 command specified in `stop_command`. - 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 command specified by `stop_command`. - 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 command specified by `stop_command`. - 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 - # command specified by `stop_command`. - 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' - - # The motor controller will vary the power supplied to the motor - # to try to maintain the speed specified in `speed_sp`. - SPEED_REGULATION_ON = 'on' - - # The motor controller will use the power specified in `duty_cycle_sp`. - SPEED_REGULATION_OFF = 'off' - - # Power will be removed from the motor and it will freely coast to a stop. - STOP_COMMAND_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_COMMAND_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_COMMAND_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 = 'run-forever' - - def run_to_abs_pos(self, **kwargs): - """Run to an absolute position specified by `position_sp` and then - stop using the command specified in `stop_command`. - """ - for key in kwargs: - setattr(self, key, kwargs[key]) - 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 command specified by `stop_command`. - """ - for key in kwargs: - setattr(self, key, kwargs[key]) - 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 command specified by `stop_command`. - """ - for key in kwargs: - setattr(self, key, kwargs[key]) - 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 = 'run-direct' - - def stop(self, **kwargs): - """Stop any of the run commands before they are complete using the - command specified by `stop_command`. - """ - for key in kwargs: - setattr(self, key, kwargs[key]) - 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 = 'reset' - - -# ~autogen -# ~autogen generic-class classes.largeMotor>currentClass - -class LargeMotor(Motor): - - """ - EV3 large servo motor - """ - - SYSTEM_CLASS_NAME = Motor.SYSTEM_CLASS_NAME - SYSTEM_DEVICE_NAME_CONVENTION = Motor.SYSTEM_DEVICE_NAME_CONVENTION - - def __init__(self, port=None, name=SYSTEM_DEVICE_NAME_CONVENTION, **kwargs): - if port is not None: - kwargs['port_name'] = port - Device.__init__(self, self.SYSTEM_CLASS_NAME, name, driver_name=['lego-ev3-l-motor'], **kwargs) - - -# ~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 = Motor.SYSTEM_DEVICE_NAME_CONVENTION - - def __init__(self, port=None, name=SYSTEM_DEVICE_NAME_CONVENTION, **kwargs): - if port is not None: - kwargs['port_name'] = port - Device.__init__(self, self.SYSTEM_CLASS_NAME, name, driver_name=['lego-ev3-m-motor'], **kwargs) - - -# ~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, port=None, name=SYSTEM_DEVICE_NAME_CONVENTION, **kwargs): - if port is not None: - kwargs['port_name'] = port - Device.__init__(self, self.SYSTEM_CLASS_NAME, name, **kwargs) - - -# ~autogen -# ~autogen generic-get-set classes.dcMotor>currentClass - - @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.set_attr_string('command', value) - - @property - def commands(self): - """ - Returns a list of commands supported by the motor - controller. - """ - return self.get_attr_set('commands') - - @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. - """ - return self.get_attr_string('driver_name') - - @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%). - """ - return self.get_attr_int('duty_cycle') - - @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. - """ - return self.get_attr_int('duty_cycle_sp') - - @duty_cycle_sp.setter - def duty_cycle_sp(self, value): - self.set_attr_int('duty_cycle_sp', value) - - @property - def polarity(self): - """ - Sets the polarity of the motor. Valid values are `normal` and `inversed`. - """ - return self.get_attr_string('polarity') - - @polarity.setter - def polarity(self, value): - self.set_attr_string('polarity', value) - - @property - def port_name(self): - """ - Returns the name of the port that the motor is connected to. - """ - return self.get_attr_string('port_name') - - @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. - """ - return self.get_attr_int('ramp_down_sp') - - @ramp_down_sp.setter - def ramp_down_sp(self, value): - self.set_attr_int('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. - """ - return self.get_attr_int('ramp_up_sp') - - @ramp_up_sp.setter - def ramp_up_sp(self, value): - self.set_attr_int('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`. - """ - return self.get_attr_set('state') - - @property - def stop_command(self): - """ - Sets the stop command that will be used when the motor stops. Read - `stop_commands` to get the list of valid values. - """ - raise Exception("stop_command is a write-only property!") - - @stop_command.setter - def stop_command(self, value): - self.set_attr_string('stop_command', value) - - @property - def stop_commands(self): - """ - Gets a list of stop commands. Valid values are `coast` - and `brake`. - """ - return self.get_attr_set('stop_commands') - - @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. - """ - return self.get_attr_int('time_sp') - - @time_sp.setter - def time_sp(self, value): - self.set_attr_int('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 command specified by `stop_command`. - 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 - # command specified by `stop_command`. - 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_COMMAND_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_COMMAND_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 = '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 command specified by `stop_command`. - """ - for key in kwargs: - setattr(self, key, kwargs[key]) - 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 = 'run-direct' - - def stop(self, **kwargs): - """Stop any of the run commands before they are complete using the - command specified by `stop_command`. - """ - for key in kwargs: - setattr(self, key, kwargs[key]) - 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, port=None, name=SYSTEM_DEVICE_NAME_CONVENTION, **kwargs): - if port is not None: - kwargs['port_name'] = port - Device.__init__(self, self.SYSTEM_CLASS_NAME, name, **kwargs) - - -# ~autogen -# ~autogen generic-get-set classes.servoMotor>currentClass - - @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.set_attr_string('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. - """ - return self.get_attr_string('driver_name') - - @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. - """ - return self.get_attr_int('max_pulse_sp') - - @max_pulse_sp.setter - def max_pulse_sp(self, value): - self.set_attr_int('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. - """ - return self.get_attr_int('mid_pulse_sp') - - @mid_pulse_sp.setter - def mid_pulse_sp(self, value): - self.set_attr_int('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. - """ - return self.get_attr_int('min_pulse_sp') - - @min_pulse_sp.setter - def min_pulse_sp(self, value): - self.set_attr_int('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`. - """ - return self.get_attr_string('polarity') - - @polarity.setter - def polarity(self, value): - self.set_attr_string('polarity', value) - - @property - def port_name(self): - """ - Returns the name of the port that the motor is connected to. - """ - return self.get_attr_string('port_name') - - @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`. - """ - return self.get_attr_int('position_sp') - - @position_sp.setter - def position_sp(self, value): - self.set_attr_int('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. - """ - return self.get_attr_int('rate_sp') - - @rate_sp.setter - def rate_sp(self, value): - self.set_attr_int('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. - """ - return self.get_attr_set('state') - - -# ~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 = 'run' - - def float(self, **kwargs): - """Remove power from the motor. - """ - for key in kwargs: - setattr(self, key, kwargs[key]) - 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 `port_name` 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, port=None, name=SYSTEM_DEVICE_NAME_CONVENTION, **kwargs): - if port is not None: - kwargs['port_name'] = port - Device.__init__(self, self.SYSTEM_CLASS_NAME, name, **kwargs) - - -# ~autogen -# ~autogen generic-get-set classes.sensor>currentClass - - @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.set_attr_string('command', value) - - @property - def commands(self): - """ - Returns a list of the valid commands for the sensor. - Returns -EOPNOTSUPP if no commands are supported. - """ - return self.get_attr_set('commands') - - @property - def decimals(self): - """ - Returns the number of decimal places for the values in the `value` - attributes of the current mode. - """ - return self.get_attr_int('decimals') - - @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. - """ - return self.get_attr_string('driver_name') - - @property - def mode(self): - """ - Returns the current mode. Writing one of the values returned by `modes` - sets the sensor to that mode. - """ - return self.get_attr_string('mode') - - @mode.setter - def mode(self, value): - self.set_attr_string('mode', value) - - @property - def modes(self): - """ - Returns a list of the valid modes for the sensor. - """ - return self.get_attr_set('modes') - - @property - def num_values(self): - """ - Returns the number of `value` attributes that will return a valid value - for the current mode. - """ - return self.get_attr_int('num_values') - - @property - def port_name(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`. - """ - return self.get_attr_string('port_name') - - @property - def units(self): - """ - Returns the units of the measured value for the current mode. May return - empty string - """ - return self.get_attr_string('units') - - -# ~autogen - - def value(self, n=0): - if isinstance(n, numbers.Integral): - n = '{0:d}'.format(n) - elif isinstance(n, numbers.Real): - n = '{0:.0f}'.format(n) - - if isinstance(n, str): - return self.get_attr_int('value'+n) - else: - return 0 - - @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) - """ - return self.get_attr_string('bin_data_format') - - 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.SYSTEM_DEVICE_NAME_CONVENTION - - def __init__(self, port=None, name=SYSTEM_DEVICE_NAME_CONVENTION, **kwargs): - if port is not None: - kwargs['port_name'] = port - Device.__init__(self, self.SYSTEM_CLASS_NAME, name, driver_name=['nxt-i2c-sensor'], **kwargs) - - -# ~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. - """ - return self.get_attr_string('fw_version') - - @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. - """ - return self.get_attr_int('poll_ms') - - @poll_ms.setter - def poll_ms(self, value): - self.set_attr_int('poll_ms', value) - - -# ~autogen -# ~autogen generic-class classes.colorSensor>currentClass - -class ColorSensor(Sensor): - - """ - LEGO EV3 color sensor. - """ - - SYSTEM_CLASS_NAME = Sensor.SYSTEM_CLASS_NAME - SYSTEM_DEVICE_NAME_CONVENTION = Sensor.SYSTEM_DEVICE_NAME_CONVENTION - - def __init__(self, port=None, name=SYSTEM_DEVICE_NAME_CONVENTION, **kwargs): - if port is not None: - kwargs['port_name'] = port - Device.__init__(self, self.SYSTEM_CLASS_NAME, name, driver_name=['lego-ev3-color'], **kwargs) - - -# ~autogen -# ~autogen generic-property-value classes.colorSensor>currentClass - - # 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' - - -# ~autogen -# ~autogen generic-class classes.ultrasonicSensor>currentClass - -class UltrasonicSensor(Sensor): - - """ - LEGO EV3 ultrasonic sensor. - """ - - SYSTEM_CLASS_NAME = Sensor.SYSTEM_CLASS_NAME - SYSTEM_DEVICE_NAME_CONVENTION = Sensor.SYSTEM_DEVICE_NAME_CONVENTION - - def __init__(self, port=None, name=SYSTEM_DEVICE_NAME_CONVENTION, **kwargs): - if port is not None: - kwargs['port_name'] = port - Device.__init__(self, self.SYSTEM_CLASS_NAME, name, driver_name=['lego-ev3-us', 'lego-nxt-us'], **kwargs) - - -# ~autogen -# ~autogen generic-property-value classes.ultrasonicSensor>currentClass - - # Continuous measurement in centimeters. - # LEDs: On, steady - MODE_US_DIST_CM = 'US-DIST-CM' - - # Continuous measurement in inches. - # LEDs: On, steady - MODE_US_DIST_IN = 'US-DIST-IN' - - # Listen. LEDs: On, blinking - MODE_US_LISTEN = 'US-LISTEN' - - # Single measurement in centimeters. - # LEDs: On momentarily when mode is set, then off - MODE_US_SI_CM = 'US-SI-CM' - - # Single measurement in inches. - # LEDs: On momentarily when mode is set, then off - MODE_US_SI_IN = 'US-SI-IN' - - -# ~autogen -# ~autogen generic-class classes.gyroSensor>currentClass - -class GyroSensor(Sensor): - - """ - LEGO EV3 gyro sensor. - """ - - SYSTEM_CLASS_NAME = Sensor.SYSTEM_CLASS_NAME - SYSTEM_DEVICE_NAME_CONVENTION = Sensor.SYSTEM_DEVICE_NAME_CONVENTION - - def __init__(self, port=None, name=SYSTEM_DEVICE_NAME_CONVENTION, **kwargs): - if port is not None: - kwargs['port_name'] = port - Device.__init__(self, self.SYSTEM_CLASS_NAME, name, driver_name=['lego-ev3-gyro'], **kwargs) - - -# ~autogen -# ~autogen generic-property-value classes.gyroSensor>currentClass - - # 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' - - -# ~autogen -# ~autogen generic-class classes.infraredSensor>currentClass - -class InfraredSensor(Sensor): - - """ - LEGO EV3 infrared sensor. - """ - - SYSTEM_CLASS_NAME = Sensor.SYSTEM_CLASS_NAME - SYSTEM_DEVICE_NAME_CONVENTION = Sensor.SYSTEM_DEVICE_NAME_CONVENTION - - def __init__(self, port=None, name=SYSTEM_DEVICE_NAME_CONVENTION, **kwargs): - if port is not None: - kwargs['port_name'] = port - Device.__init__(self, self.SYSTEM_CLASS_NAME, name, driver_name=['lego-ev3-ir'], **kwargs) - - -# ~autogen -# ~autogen generic-property-value classes.infraredSensor>currentClass - - # 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' - - -# ~autogen -# ~autogen generic-class classes.soundSensor>currentClass - -class SoundSensor(Sensor): - - """ - LEGO NXT Sound Sensor - """ - - SYSTEM_CLASS_NAME = Sensor.SYSTEM_CLASS_NAME - SYSTEM_DEVICE_NAME_CONVENTION = Sensor.SYSTEM_DEVICE_NAME_CONVENTION - - def __init__(self, port=None, name=SYSTEM_DEVICE_NAME_CONVENTION, **kwargs): - if port is not None: - kwargs['port_name'] = port - Device.__init__(self, self.SYSTEM_CLASS_NAME, name, driver_name=['lego-nxt-sound'], **kwargs) - - -# ~autogen -# ~autogen generic-property-value classes.soundSensor>currentClass - - # Sound pressure level. Flat weighting - MODE_DB = 'DB' - - # Sound pressure level. A weighting - MODE_DBA = 'DBA' - - -# ~autogen -# ~autogen generic-class classes.lightSensor>currentClass - -class LightSensor(Sensor): - - """ - LEGO NXT Light Sensor - """ - - SYSTEM_CLASS_NAME = Sensor.SYSTEM_CLASS_NAME - SYSTEM_DEVICE_NAME_CONVENTION = Sensor.SYSTEM_DEVICE_NAME_CONVENTION - - def __init__(self, port=None, name=SYSTEM_DEVICE_NAME_CONVENTION, **kwargs): - if port is not None: - kwargs['port_name'] = port - Device.__init__(self, self.SYSTEM_CLASS_NAME, name, driver_name=['lego-nxt-light'], **kwargs) - - -# ~autogen -# ~autogen generic-property-value classes.lightSensor>currentClass - - # Reflected light. LED on - MODE_REFLECT = 'REFLECT' - - # Ambient light. LED off - MODE_AMBIENT = 'AMBIENT' - - -# ~autogen -# ~autogen generic-class classes.touchSensor>currentClass - -class TouchSensor(Sensor): - - """ - Touch Sensor - """ - - SYSTEM_CLASS_NAME = Sensor.SYSTEM_CLASS_NAME - SYSTEM_DEVICE_NAME_CONVENTION = Sensor.SYSTEM_DEVICE_NAME_CONVENTION - - def __init__(self, port=None, name=SYSTEM_DEVICE_NAME_CONVENTION, **kwargs): - if port is not None: - kwargs['port_name'] = port - Device.__init__(self, self.SYSTEM_CLASS_NAME, name, driver_name=['lego-ev3-touch', 'lego-nxt-touch'], **kwargs) - - -# ~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, port=None, name=SYSTEM_DEVICE_NAME_CONVENTION, **kwargs): - if port is not None: - kwargs['port_name'] = port - Device.__init__(self, self.SYSTEM_CLASS_NAME, name, **kwargs) - - -# ~autogen -# ~autogen generic-get-set classes.led>currentClass - - @property - def max_brightness(self): - """ - Returns the maximum allowable brightness value. - """ - return self.get_attr_int('max_brightness') - - @property - def brightness(self): - """ - Sets the brightness level. Possible values are from 0 to `max_brightness`. - """ - return self.get_attr_int('brightness') - - @brightness.setter - def brightness(self, value): - self.set_attr_int('brightness', value) - - @property - def triggers(self): - """ - Returns a list of available triggers. - """ - return self.get_attr_set('trigger') - - @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. - """ - return self.get_attr_from_set('trigger') - - @trigger.setter - def trigger(self, value): - self.set_attr_string('trigger', value) - - @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. - """ - return self.get_attr_int('delay_on') - - @delay_on.setter - def delay_on(self, value): - self.set_attr_int('delay_on', value) - - @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. - """ - return self.get_attr_int('delay_off') - - @delay_off.setter - def delay_off(self, value): - self.set_attr_int('delay_off', value) - - -# ~autogen - - @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. - """ - - @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([]) - - @property - 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]) - - -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 = FileCache() - self._buffer_cache = {} - for b in self._buttons: - self._button_file(self._buttons[b]['name']) - self._button_buffer(self._buttons[b]['name']) - - def _button_file(self, name): - return self._file_cache.file_handle(name, 'r') - - def _button_buffer(self, name): - if name not in self._buffer_cache: - self._buffer_cache[name] = array.array('B', [0] * self.KEY_BUF_LEN) - 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 not bool(buf[int(bit / 8)] & 1 << bit % 8): - pressed += [k] - return pressed - - -# ~autogen remote-control classes.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'] - } - - on_red_up = None - on_red_down = None - on_blue_up = None - on_blue_down = None - 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 buttons_pressed(self): - """ - Returns list of currently pressed buttons. - """ - return RemoteControl._BUTTON_VALUES.get(self._sensor.value(self._channel), []) - - -# ~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, port=None, name=SYSTEM_DEVICE_NAME_CONVENTION, **kwargs): - if port is not None: - kwargs['port_name'] = port - Device.__init__(self, self.SYSTEM_CLASS_NAME, name, **kwargs) - - -# ~autogen -# ~autogen generic-get-set classes.powerSupply>currentClass - - @property - def measured_current(self): - """ - The measured current that the battery is supplying (in microamps) - """ - return self.get_attr_int('current_now') - - @property - def measured_voltage(self): - """ - The measured voltage that the battery is supplying (in microvolts) - """ - return self.get_attr_int('voltage_now') - - @property - def max_voltage(self): - """ - """ - return self.get_attr_int('voltage_max_design') - - @property - def min_voltage(self): - """ - """ - return self.get_attr_int('voltage_min_design') - - @property - def technology(self): - """ - """ - return self.get_attr_string('technology') - - @property - def type(self): - """ - """ - return self.get_attr_string('type') - - -# ~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 `port_name` attribute to find - a specific port. - """ - - SYSTEM_CLASS_NAME = 'lego_port' - SYSTEM_DEVICE_NAME_CONVENTION = '*' - - def __init__(self, port=None, name=SYSTEM_DEVICE_NAME_CONVENTION, **kwargs): - if port is not None: - kwargs['port_name'] = port - Device.__init__(self, self.SYSTEM_CLASS_NAME, name, **kwargs) - - -# ~autogen -# ~autogen generic-get-set classes.legoPort>currentClass - - @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]. - """ - return self.get_attr_string('driver_name') - - @property - def modes(self): - """ - Returns a list of the available modes of the port. - """ - return self.get_attr_set('modes') - - @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. - """ - return self.get_attr_string('mode') - - @mode.setter - def mode(self, value): - self.set_attr_string('mode', value) - - @property - def port_name(self): - """ - Returns the name of the port. See individual driver documentation for - the name that will be returned. - """ - return self.get_attr_string('port_name') - - @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_attr_string('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. - """ - return self.get_attr_string('status') - - -# ~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): - 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 - - 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") - - -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() - """ - - @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`: http://manpages.debian.org/cgi-bin/man.cgi?query=beep - """ - with open(os.devnull, 'w') as n: - return Popen('/usr/bin/beep %s' % args, stdout=n, shell=True) - - @staticmethod - def tone(*args): - """ - 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() - - 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 = '-n ' - 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(' '.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('/usr/bin/aplay -q "%s"' % wav_file, stdout=n, shell=True) - - @staticmethod - def speak(text): - """ - Speak the given text aloud. - """ - with open(os.devnull, 'w') as n: - return Popen('/usr/bin/espeak -a 200 --stdout "%s" | /usr/bin/aplay -q' % text, stdout=n, shell=True) diff --git a/ev3dev/ev3.py b/ev3dev/ev3.py deleted file mode 100644 index ee702f2..0000000 --- a/ev3dev/ev3.py +++ /dev/null @@ -1,226 +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='ev3-left0:red:ev3dev') - red_right = Led(name='ev3-right0:red:ev3dev') - green_left = Led(name='ev3-left1:green:ev3dev') - green_right = Led(name='ev3-right1:green:ev3dev') - - @staticmethod - def mix_colors(red, green): - Leds.red_left.brightness_pct = red - Leds.red_right.brightness_pct = red - Leds.green_left.brightness_pct = green - Leds.green_right.brightness_pct = green - - @staticmethod - def set_red(pct): - Leds.mix_colors(red=1 * pct, green=0 * pct) - - @staticmethod - def red_on(): - Leds.set_red(1) - - @staticmethod - def set_green(pct): - Leds.mix_colors(red=0 * pct, green=1 * pct) - - @staticmethod - def green_on(): - Leds.set_green(1) - - @staticmethod - def set_amber(pct): - Leds.mix_colors(red=1 * pct, green=1 * pct) - - @staticmethod - def amber_on(): - Leds.set_amber(1) - - @staticmethod - def set_orange(pct): - Leds.mix_colors(red=1 * pct, green=0.5 * pct) - - @staticmethod - def orange_on(): - Leds.set_orange(1) - - @staticmethod - def set_yellow(pct): - Leds.mix_colors(red=0.5 * pct, green=1 * pct) - - @staticmethod - def yellow_on(): - Leds.set_yellow(1) - - @staticmethod - def all_off(): - Leds.red_left.brightness = 0 - Leds.red_right.brightness = 0 - Leds.green_left.brightness = 0 - Leds.green_right.brightness = 0 - - -# ~autogen - -class Button(object): - """ - 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/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/tests/fake_sys_class/lego-sensor/sensor0/command b/ev3dev2/_platform/__init__.py similarity index 100% rename from tests/fake_sys_class/lego-sensor/sensor0/command rename to ev3dev2/_platform/__init__.py diff --git a/ev3dev/brickpi.py b/ev3dev2/_platform/brickpi.py similarity index 62% rename from ev3dev/brickpi.py rename to ev3dev2/_platform/brickpi.py index c548666..75e6991 100644 --- a/ev3dev/brickpi.py +++ b/ev3dev2/_platform/brickpi.py @@ -21,52 +21,35 @@ # 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:outA' -OUTPUT_B = 'ttyAMA0:outB' -OUTPUT_C = 'ttyAMA0:outC' -OUTPUT_D = 'ttyAMA0:outD' - -INPUT_1 = 'ttyAMA0:in1' -INPUT_2 = 'ttyAMA0:in2' -INPUT_3 = 'ttyAMA0:in3' -INPUT_4 = 'ttyAMA0:in4' - - -class Leds(object): - """ - The BrickPi LEDs. - """ - -# ~autogen led-colors platforms.brickpi.led>currentClass +from collections import OrderedDict - blue_one = Led(name='brickpi1:blue:ev3dev') - blue_two = Led(name='brickpi2:blue:ev3dev') +OUTPUT_A = 'serial0-0:MA' +OUTPUT_B = 'serial0-0:MB' +OUTPUT_C = 'serial0-0:MC' +OUTPUT_D = 'serial0-0:MD' - @staticmethod - def mix_colors(blue): - Leds.blue_one.brightness_pct = blue - Leds.blue_two.brightness_pct = blue +INPUT_1 = 'serial0-0:S1' +INPUT_2 = 'serial0-0:S2' +INPUT_3 = 'serial0-0:S3' +INPUT_4 = 'serial0-0:S4' - @staticmethod - def set_blue(pct): - Leds.mix_colors(blue=1 * pct) +BUTTONS_FILENAME = None +EVDEV_DEVICE_NAME = None - @staticmethod - def blue_on(): - Leds.set_blue(1) +LEDS = OrderedDict() +LEDS['blue_led1'] = 'led1:blue:brick-status' +LEDS['blue_led2'] = 'led2:blue:brick-status' - @staticmethod - def all_off(): - Leds.blue_one.brightness = 0 - Leds.blue_two.brightness = 0 +LED_GROUPS = OrderedDict() +LED_GROUPS['LED1'] = ('blue_led1', ) +LED_GROUPS['LED2'] = ('blue_led2', ) +LED_COLORS = OrderedDict() +LED_COLORS['BLACK'] = (0, ) +LED_COLORS['BLUE'] = (1, ) -# ~autogen +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/tests/fake_sys_class/lego-sensor/sensor0/commands b/ev3dev2/control/__init__.py similarity index 100% rename from tests/fake_sys_class/lego-sensor/sensor0/commands 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/ev3dev2/fonts/__init__.py b/ev3dev2/fonts/__init__.py new file mode 100644 index 0000000..95b4804 --- /dev/null +++ b/ev3dev2/fonts/__init__.py @@ -0,0 +1,27 @@ +import os.path +from glob import glob +from PIL import ImageFont + + +def available(): + """ + Returns list of available font names. + """ + 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 + `PIL.ImageFont `_ + class. + """ + try: + 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') diff --git a/ev3dev2/fonts/charB08.pbm b/ev3dev2/fonts/charB08.pbm new file mode 100644 index 0000000..8bd2ffb Binary files /dev/null and b/ev3dev2/fonts/charB08.pbm differ diff --git a/ev3dev2/fonts/charB08.pil b/ev3dev2/fonts/charB08.pil new file mode 100644 index 0000000..f7ee8a4 Binary files /dev/null and b/ev3dev2/fonts/charB08.pil differ diff --git a/ev3dev2/fonts/charB10.pbm b/ev3dev2/fonts/charB10.pbm new file mode 100644 index 0000000..62192fd Binary files /dev/null and b/ev3dev2/fonts/charB10.pbm differ diff --git a/ev3dev2/fonts/charB10.pil b/ev3dev2/fonts/charB10.pil new file mode 100644 index 0000000..185a19e Binary files /dev/null and b/ev3dev2/fonts/charB10.pil differ diff --git a/ev3dev2/fonts/charB12.pbm b/ev3dev2/fonts/charB12.pbm new file mode 100644 index 0000000..9e663ee Binary files /dev/null and b/ev3dev2/fonts/charB12.pbm differ diff --git a/ev3dev2/fonts/charB12.pil b/ev3dev2/fonts/charB12.pil new file mode 100644 index 0000000..cc338bf Binary files /dev/null and b/ev3dev2/fonts/charB12.pil differ diff --git a/ev3dev2/fonts/charB14.pbm b/ev3dev2/fonts/charB14.pbm new file mode 100644 index 0000000..22d8463 Binary files /dev/null and b/ev3dev2/fonts/charB14.pbm differ diff --git a/ev3dev2/fonts/charB14.pil b/ev3dev2/fonts/charB14.pil new file mode 100644 index 0000000..53ca9c6 Binary files /dev/null and b/ev3dev2/fonts/charB14.pil differ diff --git a/ev3dev2/fonts/charB18.pbm b/ev3dev2/fonts/charB18.pbm new file mode 100644 index 0000000..a65f4a0 Binary files /dev/null and b/ev3dev2/fonts/charB18.pbm differ diff --git a/ev3dev2/fonts/charB18.pil b/ev3dev2/fonts/charB18.pil new file mode 100644 index 0000000..11c42fa Binary files /dev/null and b/ev3dev2/fonts/charB18.pil differ diff --git a/ev3dev2/fonts/charB24.pbm b/ev3dev2/fonts/charB24.pbm new file mode 100644 index 0000000..7126632 Binary files /dev/null and b/ev3dev2/fonts/charB24.pbm differ diff --git a/ev3dev2/fonts/charB24.pil b/ev3dev2/fonts/charB24.pil new file mode 100644 index 0000000..c588c23 Binary files /dev/null and b/ev3dev2/fonts/charB24.pil differ diff --git a/ev3dev2/fonts/charBI08.pbm b/ev3dev2/fonts/charBI08.pbm new file mode 100644 index 0000000..99ef950 Binary files /dev/null and b/ev3dev2/fonts/charBI08.pbm differ diff --git a/ev3dev2/fonts/charBI08.pil b/ev3dev2/fonts/charBI08.pil new file mode 100644 index 0000000..b770e1c Binary files /dev/null and b/ev3dev2/fonts/charBI08.pil differ diff --git a/ev3dev2/fonts/charBI10.pbm b/ev3dev2/fonts/charBI10.pbm new file mode 100644 index 0000000..bb5a406 Binary files /dev/null and b/ev3dev2/fonts/charBI10.pbm differ diff --git a/ev3dev2/fonts/charBI10.pil b/ev3dev2/fonts/charBI10.pil new file mode 100644 index 0000000..e07ea82 Binary files /dev/null and b/ev3dev2/fonts/charBI10.pil differ diff --git a/ev3dev2/fonts/charBI12.pbm b/ev3dev2/fonts/charBI12.pbm new file mode 100644 index 0000000..7366d93 Binary files /dev/null and b/ev3dev2/fonts/charBI12.pbm differ diff --git a/ev3dev2/fonts/charBI12.pil b/ev3dev2/fonts/charBI12.pil new file mode 100644 index 0000000..0b20796 Binary files /dev/null and b/ev3dev2/fonts/charBI12.pil differ diff --git a/ev3dev2/fonts/charBI14.pbm b/ev3dev2/fonts/charBI14.pbm new file mode 100644 index 0000000..59f041e Binary files /dev/null and b/ev3dev2/fonts/charBI14.pbm differ diff --git a/ev3dev2/fonts/charBI14.pil b/ev3dev2/fonts/charBI14.pil new file mode 100644 index 0000000..a05a9e3 Binary files /dev/null and b/ev3dev2/fonts/charBI14.pil differ diff --git a/ev3dev2/fonts/charBI18.pbm b/ev3dev2/fonts/charBI18.pbm new file mode 100644 index 0000000..43a873d Binary files /dev/null and b/ev3dev2/fonts/charBI18.pbm differ diff --git a/ev3dev2/fonts/charBI18.pil b/ev3dev2/fonts/charBI18.pil new file mode 100644 index 0000000..6b43040 Binary files /dev/null and b/ev3dev2/fonts/charBI18.pil differ diff --git a/ev3dev2/fonts/charBI24.pbm b/ev3dev2/fonts/charBI24.pbm new file mode 100644 index 0000000..9ed12d1 Binary files /dev/null and b/ev3dev2/fonts/charBI24.pbm differ diff --git a/ev3dev2/fonts/charBI24.pil b/ev3dev2/fonts/charBI24.pil new file mode 100644 index 0000000..e27c593 Binary files /dev/null and b/ev3dev2/fonts/charBI24.pil differ diff --git a/ev3dev2/fonts/charI08.pbm b/ev3dev2/fonts/charI08.pbm new file mode 100644 index 0000000..4857840 Binary files /dev/null and b/ev3dev2/fonts/charI08.pbm differ diff --git a/ev3dev2/fonts/charI08.pil b/ev3dev2/fonts/charI08.pil new file mode 100644 index 0000000..38c890e Binary files /dev/null and b/ev3dev2/fonts/charI08.pil differ diff --git a/ev3dev2/fonts/charI10.pbm b/ev3dev2/fonts/charI10.pbm new file mode 100644 index 0000000..7b936d2 Binary files /dev/null and b/ev3dev2/fonts/charI10.pbm differ diff --git a/ev3dev2/fonts/charI10.pil b/ev3dev2/fonts/charI10.pil new file mode 100644 index 0000000..870af9d Binary files /dev/null and b/ev3dev2/fonts/charI10.pil differ diff --git a/ev3dev2/fonts/charI12.pbm b/ev3dev2/fonts/charI12.pbm new file mode 100644 index 0000000..41d527a Binary files /dev/null and b/ev3dev2/fonts/charI12.pbm differ diff --git a/ev3dev2/fonts/charI12.pil b/ev3dev2/fonts/charI12.pil new file mode 100644 index 0000000..d8aaf17 Binary files /dev/null and b/ev3dev2/fonts/charI12.pil differ diff --git a/ev3dev2/fonts/charI14.pbm b/ev3dev2/fonts/charI14.pbm new file mode 100644 index 0000000..c69bf7b Binary files /dev/null and b/ev3dev2/fonts/charI14.pbm differ diff --git a/ev3dev2/fonts/charI14.pil b/ev3dev2/fonts/charI14.pil new file mode 100644 index 0000000..0ec51c5 Binary files /dev/null and b/ev3dev2/fonts/charI14.pil differ diff --git a/ev3dev2/fonts/charI18.pbm b/ev3dev2/fonts/charI18.pbm new file mode 100644 index 0000000..c538ee0 Binary files /dev/null and b/ev3dev2/fonts/charI18.pbm differ diff --git a/ev3dev2/fonts/charI18.pil b/ev3dev2/fonts/charI18.pil new file mode 100644 index 0000000..eff2a21 Binary files /dev/null and b/ev3dev2/fonts/charI18.pil differ diff --git a/ev3dev2/fonts/charI24.pbm b/ev3dev2/fonts/charI24.pbm new file mode 100644 index 0000000..f816a92 Binary files /dev/null and b/ev3dev2/fonts/charI24.pbm differ diff --git a/ev3dev2/fonts/charI24.pil b/ev3dev2/fonts/charI24.pil new file mode 100644 index 0000000..f1f355b Binary files /dev/null and b/ev3dev2/fonts/charI24.pil differ diff --git a/ev3dev2/fonts/charR08.pbm b/ev3dev2/fonts/charR08.pbm new file mode 100644 index 0000000..8e9b959 Binary files /dev/null and b/ev3dev2/fonts/charR08.pbm differ diff --git a/ev3dev2/fonts/charR08.pil b/ev3dev2/fonts/charR08.pil new file mode 100644 index 0000000..c48a421 Binary files /dev/null and b/ev3dev2/fonts/charR08.pil differ diff --git a/ev3dev2/fonts/charR10.pbm b/ev3dev2/fonts/charR10.pbm new file mode 100644 index 0000000..33fed85 Binary files /dev/null and b/ev3dev2/fonts/charR10.pbm differ diff --git a/ev3dev2/fonts/charR10.pil b/ev3dev2/fonts/charR10.pil new file mode 100644 index 0000000..4ab04a7 Binary files /dev/null and b/ev3dev2/fonts/charR10.pil differ diff --git a/ev3dev2/fonts/charR12.pbm b/ev3dev2/fonts/charR12.pbm new file mode 100644 index 0000000..4796254 Binary files /dev/null and b/ev3dev2/fonts/charR12.pbm differ diff --git a/ev3dev2/fonts/charR12.pil b/ev3dev2/fonts/charR12.pil new file mode 100644 index 0000000..44fd462 Binary files /dev/null and b/ev3dev2/fonts/charR12.pil differ diff --git a/ev3dev2/fonts/charR14.pbm b/ev3dev2/fonts/charR14.pbm new file mode 100644 index 0000000..a348fee Binary files /dev/null and b/ev3dev2/fonts/charR14.pbm differ diff --git a/ev3dev2/fonts/charR14.pil b/ev3dev2/fonts/charR14.pil new file mode 100644 index 0000000..e644336 Binary files /dev/null and b/ev3dev2/fonts/charR14.pil differ diff --git a/ev3dev2/fonts/charR18.pbm b/ev3dev2/fonts/charR18.pbm new file mode 100644 index 0000000..b4dd7f6 Binary files /dev/null and b/ev3dev2/fonts/charR18.pbm differ diff --git a/ev3dev2/fonts/charR18.pil b/ev3dev2/fonts/charR18.pil new file mode 100644 index 0000000..6f19a15 Binary files /dev/null and b/ev3dev2/fonts/charR18.pil differ diff --git a/ev3dev2/fonts/charR24.pbm b/ev3dev2/fonts/charR24.pbm new file mode 100644 index 0000000..090fbae Binary files /dev/null and b/ev3dev2/fonts/charR24.pbm differ diff --git a/ev3dev2/fonts/charR24.pil b/ev3dev2/fonts/charR24.pil new file mode 100644 index 0000000..be68594 Binary files /dev/null and b/ev3dev2/fonts/charR24.pil differ diff --git a/ev3dev2/fonts/courB08.pbm b/ev3dev2/fonts/courB08.pbm new file mode 100644 index 0000000..18ae3d4 Binary files /dev/null and b/ev3dev2/fonts/courB08.pbm differ diff --git a/ev3dev2/fonts/courB08.pil b/ev3dev2/fonts/courB08.pil new file mode 100644 index 0000000..d41e06c Binary files /dev/null and b/ev3dev2/fonts/courB08.pil differ diff --git a/ev3dev2/fonts/courB10.pbm b/ev3dev2/fonts/courB10.pbm new file mode 100644 index 0000000..a42f5e0 Binary files /dev/null and b/ev3dev2/fonts/courB10.pbm differ diff --git a/ev3dev2/fonts/courB10.pil b/ev3dev2/fonts/courB10.pil new file mode 100644 index 0000000..17092f8 Binary files /dev/null and b/ev3dev2/fonts/courB10.pil differ diff --git a/ev3dev2/fonts/courB12.pbm b/ev3dev2/fonts/courB12.pbm new file mode 100644 index 0000000..9cb02ed Binary files /dev/null and b/ev3dev2/fonts/courB12.pbm differ diff --git a/ev3dev2/fonts/courB12.pil b/ev3dev2/fonts/courB12.pil new file mode 100644 index 0000000..11c035b Binary files /dev/null and b/ev3dev2/fonts/courB12.pil differ diff --git a/ev3dev2/fonts/courB14.pbm b/ev3dev2/fonts/courB14.pbm new file mode 100644 index 0000000..04f0221 Binary files /dev/null and b/ev3dev2/fonts/courB14.pbm differ diff --git a/ev3dev2/fonts/courB14.pil b/ev3dev2/fonts/courB14.pil new file mode 100644 index 0000000..ad505df Binary files /dev/null and b/ev3dev2/fonts/courB14.pil differ diff --git a/ev3dev2/fonts/courB18.pbm b/ev3dev2/fonts/courB18.pbm new file mode 100644 index 0000000..9e14ff3 Binary files /dev/null and b/ev3dev2/fonts/courB18.pbm differ diff --git a/ev3dev2/fonts/courB18.pil b/ev3dev2/fonts/courB18.pil new file mode 100644 index 0000000..73706ff Binary files /dev/null and b/ev3dev2/fonts/courB18.pil differ diff --git a/ev3dev2/fonts/courB24.pbm b/ev3dev2/fonts/courB24.pbm new file mode 100644 index 0000000..eb3d6e7 Binary files /dev/null and b/ev3dev2/fonts/courB24.pbm differ diff --git a/ev3dev2/fonts/courB24.pil b/ev3dev2/fonts/courB24.pil new file mode 100644 index 0000000..d28fcca Binary files /dev/null and b/ev3dev2/fonts/courB24.pil differ diff --git a/ev3dev2/fonts/courBO08.pbm b/ev3dev2/fonts/courBO08.pbm new file mode 100644 index 0000000..59f123a Binary files /dev/null and b/ev3dev2/fonts/courBO08.pbm differ diff --git a/ev3dev2/fonts/courBO08.pil b/ev3dev2/fonts/courBO08.pil new file mode 100644 index 0000000..1e0ccc7 Binary files /dev/null and b/ev3dev2/fonts/courBO08.pil differ diff --git a/ev3dev2/fonts/courBO10.pbm b/ev3dev2/fonts/courBO10.pbm new file mode 100644 index 0000000..e990b82 Binary files /dev/null and b/ev3dev2/fonts/courBO10.pbm differ diff --git a/ev3dev2/fonts/courBO10.pil b/ev3dev2/fonts/courBO10.pil new file mode 100644 index 0000000..b57469c Binary files /dev/null and b/ev3dev2/fonts/courBO10.pil differ diff --git a/ev3dev2/fonts/courBO12.pbm b/ev3dev2/fonts/courBO12.pbm new file mode 100644 index 0000000..fb79b14 Binary files /dev/null and b/ev3dev2/fonts/courBO12.pbm differ diff --git a/ev3dev2/fonts/courBO12.pil b/ev3dev2/fonts/courBO12.pil new file mode 100644 index 0000000..d2decd2 Binary files /dev/null and b/ev3dev2/fonts/courBO12.pil differ diff --git a/ev3dev2/fonts/courBO14.pbm b/ev3dev2/fonts/courBO14.pbm new file mode 100644 index 0000000..c606bbd Binary files /dev/null and b/ev3dev2/fonts/courBO14.pbm differ diff --git a/ev3dev2/fonts/courBO14.pil b/ev3dev2/fonts/courBO14.pil new file mode 100644 index 0000000..f2a5c42 Binary files /dev/null and b/ev3dev2/fonts/courBO14.pil differ diff --git a/ev3dev2/fonts/courBO18.pbm b/ev3dev2/fonts/courBO18.pbm new file mode 100644 index 0000000..7801b81 Binary files /dev/null and b/ev3dev2/fonts/courBO18.pbm differ diff --git a/ev3dev2/fonts/courBO18.pil b/ev3dev2/fonts/courBO18.pil new file mode 100644 index 0000000..0a0a96a Binary files /dev/null and b/ev3dev2/fonts/courBO18.pil differ diff --git a/ev3dev2/fonts/courBO24.pbm b/ev3dev2/fonts/courBO24.pbm new file mode 100644 index 0000000..8d5bad0 Binary files /dev/null and b/ev3dev2/fonts/courBO24.pbm differ diff --git a/ev3dev2/fonts/courBO24.pil b/ev3dev2/fonts/courBO24.pil new file mode 100644 index 0000000..7b194c9 Binary files /dev/null and b/ev3dev2/fonts/courBO24.pil differ diff --git a/ev3dev2/fonts/courO08.pbm b/ev3dev2/fonts/courO08.pbm new file mode 100644 index 0000000..4e66edd Binary files /dev/null and b/ev3dev2/fonts/courO08.pbm differ diff --git a/ev3dev2/fonts/courO08.pil b/ev3dev2/fonts/courO08.pil new file mode 100644 index 0000000..3691d89 Binary files /dev/null and b/ev3dev2/fonts/courO08.pil differ diff --git a/ev3dev2/fonts/courO10.pbm b/ev3dev2/fonts/courO10.pbm new file mode 100644 index 0000000..4886600 Binary files /dev/null and b/ev3dev2/fonts/courO10.pbm differ diff --git a/ev3dev2/fonts/courO10.pil b/ev3dev2/fonts/courO10.pil new file mode 100644 index 0000000..2316413 Binary files /dev/null and b/ev3dev2/fonts/courO10.pil differ diff --git a/ev3dev2/fonts/courO12.pbm b/ev3dev2/fonts/courO12.pbm new file mode 100644 index 0000000..11ee8f3 Binary files /dev/null and b/ev3dev2/fonts/courO12.pbm differ diff --git a/ev3dev2/fonts/courO12.pil b/ev3dev2/fonts/courO12.pil new file mode 100644 index 0000000..0905475 Binary files /dev/null and b/ev3dev2/fonts/courO12.pil differ diff --git a/ev3dev2/fonts/courO14.pbm b/ev3dev2/fonts/courO14.pbm new file mode 100644 index 0000000..ec563c7 Binary files /dev/null and b/ev3dev2/fonts/courO14.pbm differ diff --git a/ev3dev2/fonts/courO14.pil b/ev3dev2/fonts/courO14.pil new file mode 100644 index 0000000..5cdb4a1 Binary files /dev/null and b/ev3dev2/fonts/courO14.pil differ diff --git a/ev3dev2/fonts/courO18.pbm b/ev3dev2/fonts/courO18.pbm new file mode 100644 index 0000000..a6e7ee6 Binary files /dev/null and b/ev3dev2/fonts/courO18.pbm differ diff --git a/ev3dev2/fonts/courO18.pil b/ev3dev2/fonts/courO18.pil new file mode 100644 index 0000000..b21b04d Binary files /dev/null and b/ev3dev2/fonts/courO18.pil differ diff --git a/ev3dev2/fonts/courO24.pbm b/ev3dev2/fonts/courO24.pbm new file mode 100644 index 0000000..11e285c Binary files /dev/null and b/ev3dev2/fonts/courO24.pbm differ diff --git a/ev3dev2/fonts/courO24.pil b/ev3dev2/fonts/courO24.pil new file mode 100644 index 0000000..812c0b9 Binary files /dev/null and b/ev3dev2/fonts/courO24.pil differ diff --git a/ev3dev2/fonts/courR08.pbm b/ev3dev2/fonts/courR08.pbm new file mode 100644 index 0000000..67d4dd6 Binary files /dev/null and b/ev3dev2/fonts/courR08.pbm differ diff --git a/ev3dev2/fonts/courR08.pil b/ev3dev2/fonts/courR08.pil new file mode 100644 index 0000000..5ca466b Binary files /dev/null and b/ev3dev2/fonts/courR08.pil differ diff --git a/ev3dev2/fonts/courR10.pbm b/ev3dev2/fonts/courR10.pbm new file mode 100644 index 0000000..7fe8dc0 Binary files /dev/null and b/ev3dev2/fonts/courR10.pbm differ diff --git a/ev3dev2/fonts/courR10.pil b/ev3dev2/fonts/courR10.pil new file mode 100644 index 0000000..704b5af Binary files /dev/null and b/ev3dev2/fonts/courR10.pil differ diff --git a/ev3dev2/fonts/courR12.pbm b/ev3dev2/fonts/courR12.pbm new file mode 100644 index 0000000..9e12eea Binary files /dev/null and b/ev3dev2/fonts/courR12.pbm differ diff --git a/ev3dev2/fonts/courR12.pil b/ev3dev2/fonts/courR12.pil new file mode 100644 index 0000000..11f25aa Binary files /dev/null and b/ev3dev2/fonts/courR12.pil differ diff --git a/ev3dev2/fonts/courR14.pbm b/ev3dev2/fonts/courR14.pbm new file mode 100644 index 0000000..159c460 Binary files /dev/null and b/ev3dev2/fonts/courR14.pbm differ diff --git a/ev3dev2/fonts/courR14.pil b/ev3dev2/fonts/courR14.pil new file mode 100644 index 0000000..4c6a97a Binary files /dev/null and b/ev3dev2/fonts/courR14.pil differ diff --git a/ev3dev2/fonts/courR18.pbm b/ev3dev2/fonts/courR18.pbm new file mode 100644 index 0000000..13176d3 Binary files /dev/null and b/ev3dev2/fonts/courR18.pbm differ diff --git a/ev3dev2/fonts/courR18.pil b/ev3dev2/fonts/courR18.pil new file mode 100644 index 0000000..4eecc84 Binary files /dev/null and b/ev3dev2/fonts/courR18.pil differ diff --git a/ev3dev2/fonts/courR24.pbm b/ev3dev2/fonts/courR24.pbm new file mode 100644 index 0000000..70f9b4f Binary files /dev/null and b/ev3dev2/fonts/courR24.pbm differ diff --git a/ev3dev2/fonts/courR24.pil b/ev3dev2/fonts/courR24.pil new file mode 100644 index 0000000..75cda37 Binary files /dev/null and b/ev3dev2/fonts/courR24.pil differ diff --git a/ev3dev2/fonts/helvB08.pbm b/ev3dev2/fonts/helvB08.pbm new file mode 100644 index 0000000..3886ded Binary files /dev/null and b/ev3dev2/fonts/helvB08.pbm differ diff --git a/ev3dev2/fonts/helvB08.pil b/ev3dev2/fonts/helvB08.pil new file mode 100644 index 0000000..c70fa69 Binary files /dev/null and b/ev3dev2/fonts/helvB08.pil differ diff --git a/ev3dev2/fonts/helvB10.pbm b/ev3dev2/fonts/helvB10.pbm new file mode 100644 index 0000000..582c88d Binary files /dev/null and b/ev3dev2/fonts/helvB10.pbm differ diff --git a/ev3dev2/fonts/helvB10.pil b/ev3dev2/fonts/helvB10.pil new file mode 100644 index 0000000..f9ee75a Binary files /dev/null and b/ev3dev2/fonts/helvB10.pil differ diff --git a/ev3dev2/fonts/helvB12.pbm b/ev3dev2/fonts/helvB12.pbm new file mode 100644 index 0000000..e268d56 Binary files /dev/null and b/ev3dev2/fonts/helvB12.pbm differ diff --git a/ev3dev2/fonts/helvB12.pil b/ev3dev2/fonts/helvB12.pil new file mode 100644 index 0000000..5a6cf5e Binary files /dev/null and b/ev3dev2/fonts/helvB12.pil differ diff --git a/ev3dev2/fonts/helvB14.pbm b/ev3dev2/fonts/helvB14.pbm new file mode 100644 index 0000000..90e0e24 Binary files /dev/null and b/ev3dev2/fonts/helvB14.pbm differ diff --git a/ev3dev2/fonts/helvB14.pil b/ev3dev2/fonts/helvB14.pil new file mode 100644 index 0000000..e62c168 Binary files /dev/null and b/ev3dev2/fonts/helvB14.pil differ diff --git a/ev3dev2/fonts/helvB18.pbm b/ev3dev2/fonts/helvB18.pbm new file mode 100644 index 0000000..a794b16 Binary files /dev/null and b/ev3dev2/fonts/helvB18.pbm differ diff --git a/ev3dev2/fonts/helvB18.pil b/ev3dev2/fonts/helvB18.pil new file mode 100644 index 0000000..5700e0f Binary files /dev/null and b/ev3dev2/fonts/helvB18.pil differ diff --git a/ev3dev2/fonts/helvB24.pbm b/ev3dev2/fonts/helvB24.pbm new file mode 100644 index 0000000..b87707e Binary files /dev/null and b/ev3dev2/fonts/helvB24.pbm differ diff --git a/ev3dev2/fonts/helvB24.pil b/ev3dev2/fonts/helvB24.pil new file mode 100644 index 0000000..bdf616f Binary files /dev/null and b/ev3dev2/fonts/helvB24.pil differ diff --git a/ev3dev2/fonts/helvBO08.pbm b/ev3dev2/fonts/helvBO08.pbm new file mode 100644 index 0000000..60f802b Binary files /dev/null and b/ev3dev2/fonts/helvBO08.pbm differ diff --git a/ev3dev2/fonts/helvBO08.pil b/ev3dev2/fonts/helvBO08.pil new file mode 100644 index 0000000..6dfe880 Binary files /dev/null and b/ev3dev2/fonts/helvBO08.pil differ diff --git a/ev3dev2/fonts/helvBO10.pbm b/ev3dev2/fonts/helvBO10.pbm new file mode 100644 index 0000000..63fcebc Binary files /dev/null and b/ev3dev2/fonts/helvBO10.pbm differ diff --git a/ev3dev2/fonts/helvBO10.pil b/ev3dev2/fonts/helvBO10.pil new file mode 100644 index 0000000..39797be Binary files /dev/null and b/ev3dev2/fonts/helvBO10.pil differ diff --git a/ev3dev2/fonts/helvBO12.pbm b/ev3dev2/fonts/helvBO12.pbm new file mode 100644 index 0000000..e11a6db Binary files /dev/null and b/ev3dev2/fonts/helvBO12.pbm differ diff --git a/ev3dev2/fonts/helvBO12.pil b/ev3dev2/fonts/helvBO12.pil new file mode 100644 index 0000000..a4d674a Binary files /dev/null and b/ev3dev2/fonts/helvBO12.pil differ diff --git a/ev3dev2/fonts/helvBO14.pbm b/ev3dev2/fonts/helvBO14.pbm new file mode 100644 index 0000000..1c6b907 Binary files /dev/null and b/ev3dev2/fonts/helvBO14.pbm differ diff --git a/ev3dev2/fonts/helvBO14.pil b/ev3dev2/fonts/helvBO14.pil new file mode 100644 index 0000000..1e0e0d6 Binary files /dev/null and b/ev3dev2/fonts/helvBO14.pil differ diff --git a/ev3dev2/fonts/helvBO18.pbm b/ev3dev2/fonts/helvBO18.pbm new file mode 100644 index 0000000..c946ef3 Binary files /dev/null and b/ev3dev2/fonts/helvBO18.pbm differ diff --git a/ev3dev2/fonts/helvBO18.pil b/ev3dev2/fonts/helvBO18.pil new file mode 100644 index 0000000..3687b43 Binary files /dev/null and b/ev3dev2/fonts/helvBO18.pil differ diff --git a/ev3dev2/fonts/helvBO24.pbm b/ev3dev2/fonts/helvBO24.pbm new file mode 100644 index 0000000..6e14c04 Binary files /dev/null and b/ev3dev2/fonts/helvBO24.pbm differ diff --git a/ev3dev2/fonts/helvBO24.pil b/ev3dev2/fonts/helvBO24.pil new file mode 100644 index 0000000..e579d63 Binary files /dev/null and b/ev3dev2/fonts/helvBO24.pil differ diff --git a/ev3dev2/fonts/helvO08.pbm b/ev3dev2/fonts/helvO08.pbm new file mode 100644 index 0000000..b98dc62 Binary files /dev/null and b/ev3dev2/fonts/helvO08.pbm differ diff --git a/ev3dev2/fonts/helvO08.pil b/ev3dev2/fonts/helvO08.pil new file mode 100644 index 0000000..8f3be0f Binary files /dev/null and b/ev3dev2/fonts/helvO08.pil differ diff --git a/ev3dev2/fonts/helvO10.pbm b/ev3dev2/fonts/helvO10.pbm new file mode 100644 index 0000000..193fe86 Binary files /dev/null and b/ev3dev2/fonts/helvO10.pbm differ diff --git a/ev3dev2/fonts/helvO10.pil b/ev3dev2/fonts/helvO10.pil new file mode 100644 index 0000000..ddeecbb Binary files /dev/null and b/ev3dev2/fonts/helvO10.pil differ diff --git a/ev3dev2/fonts/helvO12.pbm b/ev3dev2/fonts/helvO12.pbm new file mode 100644 index 0000000..0f1b686 Binary files /dev/null and b/ev3dev2/fonts/helvO12.pbm differ diff --git a/ev3dev2/fonts/helvO12.pil b/ev3dev2/fonts/helvO12.pil new file mode 100644 index 0000000..10c72ee Binary files /dev/null and b/ev3dev2/fonts/helvO12.pil differ diff --git a/ev3dev2/fonts/helvO14.pbm b/ev3dev2/fonts/helvO14.pbm new file mode 100644 index 0000000..7f88842 Binary files /dev/null and b/ev3dev2/fonts/helvO14.pbm differ diff --git a/ev3dev2/fonts/helvO14.pil b/ev3dev2/fonts/helvO14.pil new file mode 100644 index 0000000..f3326b1 Binary files /dev/null and b/ev3dev2/fonts/helvO14.pil differ diff --git a/ev3dev2/fonts/helvO18.pbm b/ev3dev2/fonts/helvO18.pbm new file mode 100644 index 0000000..6c2b9b9 Binary files /dev/null and b/ev3dev2/fonts/helvO18.pbm differ diff --git a/ev3dev2/fonts/helvO18.pil b/ev3dev2/fonts/helvO18.pil new file mode 100644 index 0000000..2bdf0d7 Binary files /dev/null and b/ev3dev2/fonts/helvO18.pil differ diff --git a/ev3dev2/fonts/helvO24.pbm b/ev3dev2/fonts/helvO24.pbm new file mode 100644 index 0000000..402dfc5 Binary files /dev/null and b/ev3dev2/fonts/helvO24.pbm differ diff --git a/ev3dev2/fonts/helvO24.pil b/ev3dev2/fonts/helvO24.pil new file mode 100644 index 0000000..a133db9 Binary files /dev/null and b/ev3dev2/fonts/helvO24.pil differ diff --git a/ev3dev2/fonts/helvR08.pbm b/ev3dev2/fonts/helvR08.pbm new file mode 100644 index 0000000..55e005a Binary files /dev/null and b/ev3dev2/fonts/helvR08.pbm differ diff --git a/ev3dev2/fonts/helvR08.pil b/ev3dev2/fonts/helvR08.pil new file mode 100644 index 0000000..e06046b Binary files /dev/null and b/ev3dev2/fonts/helvR08.pil differ diff --git a/ev3dev2/fonts/helvR10.pbm b/ev3dev2/fonts/helvR10.pbm new file mode 100644 index 0000000..7ae508c Binary files /dev/null and b/ev3dev2/fonts/helvR10.pbm differ diff --git a/ev3dev2/fonts/helvR10.pil b/ev3dev2/fonts/helvR10.pil new file mode 100644 index 0000000..5069207 Binary files /dev/null and b/ev3dev2/fonts/helvR10.pil differ diff --git a/ev3dev2/fonts/helvR12.pbm b/ev3dev2/fonts/helvR12.pbm new file mode 100644 index 0000000..50d3080 Binary files /dev/null and b/ev3dev2/fonts/helvR12.pbm differ diff --git a/ev3dev2/fonts/helvR12.pil b/ev3dev2/fonts/helvR12.pil new file mode 100644 index 0000000..c7cdd8e Binary files /dev/null and b/ev3dev2/fonts/helvR12.pil differ diff --git a/ev3dev2/fonts/helvR14.pbm b/ev3dev2/fonts/helvR14.pbm new file mode 100644 index 0000000..6127cbe Binary files /dev/null and b/ev3dev2/fonts/helvR14.pbm differ diff --git a/ev3dev2/fonts/helvR14.pil b/ev3dev2/fonts/helvR14.pil new file mode 100644 index 0000000..10fbb34 Binary files /dev/null and b/ev3dev2/fonts/helvR14.pil differ diff --git a/ev3dev2/fonts/helvR18.pbm b/ev3dev2/fonts/helvR18.pbm new file mode 100644 index 0000000..594792c Binary files /dev/null and b/ev3dev2/fonts/helvR18.pbm differ diff --git a/ev3dev2/fonts/helvR18.pil b/ev3dev2/fonts/helvR18.pil new file mode 100644 index 0000000..adcfee4 Binary files /dev/null and b/ev3dev2/fonts/helvR18.pil differ diff --git a/ev3dev2/fonts/helvR24.pbm b/ev3dev2/fonts/helvR24.pbm new file mode 100644 index 0000000..7493238 Binary files /dev/null and b/ev3dev2/fonts/helvR24.pbm differ diff --git a/ev3dev2/fonts/helvR24.pil b/ev3dev2/fonts/helvR24.pil new file mode 100644 index 0000000..4ffde2e Binary files /dev/null and b/ev3dev2/fonts/helvR24.pil differ diff --git a/ev3dev2/fonts/luBIS08.pbm b/ev3dev2/fonts/luBIS08.pbm new file mode 100644 index 0000000..47f1b67 Binary files /dev/null and b/ev3dev2/fonts/luBIS08.pbm differ diff --git a/ev3dev2/fonts/luBIS08.pil b/ev3dev2/fonts/luBIS08.pil new file mode 100644 index 0000000..f6b996d Binary files /dev/null and b/ev3dev2/fonts/luBIS08.pil differ diff --git a/ev3dev2/fonts/luBIS10.pbm b/ev3dev2/fonts/luBIS10.pbm new file mode 100644 index 0000000..3bf524f Binary files /dev/null and b/ev3dev2/fonts/luBIS10.pbm differ diff --git a/ev3dev2/fonts/luBIS10.pil b/ev3dev2/fonts/luBIS10.pil new file mode 100644 index 0000000..0056710 Binary files /dev/null and b/ev3dev2/fonts/luBIS10.pil differ diff --git a/ev3dev2/fonts/luBIS12.pbm b/ev3dev2/fonts/luBIS12.pbm new file mode 100644 index 0000000..ac55684 Binary files /dev/null and b/ev3dev2/fonts/luBIS12.pbm differ diff --git a/ev3dev2/fonts/luBIS12.pil b/ev3dev2/fonts/luBIS12.pil new file mode 100644 index 0000000..33a7527 Binary files /dev/null and b/ev3dev2/fonts/luBIS12.pil differ diff --git a/ev3dev2/fonts/luBIS14.pbm b/ev3dev2/fonts/luBIS14.pbm new file mode 100644 index 0000000..f0784be Binary files /dev/null and b/ev3dev2/fonts/luBIS14.pbm differ diff --git a/ev3dev2/fonts/luBIS14.pil b/ev3dev2/fonts/luBIS14.pil new file mode 100644 index 0000000..742e026 Binary files /dev/null and b/ev3dev2/fonts/luBIS14.pil differ diff --git a/ev3dev2/fonts/luBIS18.pbm b/ev3dev2/fonts/luBIS18.pbm new file mode 100644 index 0000000..1f49e4d Binary files /dev/null and b/ev3dev2/fonts/luBIS18.pbm differ diff --git a/ev3dev2/fonts/luBIS18.pil b/ev3dev2/fonts/luBIS18.pil new file mode 100644 index 0000000..622677c Binary files /dev/null and b/ev3dev2/fonts/luBIS18.pil differ diff --git a/ev3dev2/fonts/luBIS19.pbm b/ev3dev2/fonts/luBIS19.pbm new file mode 100644 index 0000000..1541fe2 Binary files /dev/null and b/ev3dev2/fonts/luBIS19.pbm differ diff --git a/ev3dev2/fonts/luBIS19.pil b/ev3dev2/fonts/luBIS19.pil new file mode 100644 index 0000000..5c4338c Binary files /dev/null and b/ev3dev2/fonts/luBIS19.pil differ diff --git a/ev3dev2/fonts/luBIS24.pbm b/ev3dev2/fonts/luBIS24.pbm new file mode 100644 index 0000000..5a79510 Binary files /dev/null and b/ev3dev2/fonts/luBIS24.pbm differ diff --git a/ev3dev2/fonts/luBIS24.pil b/ev3dev2/fonts/luBIS24.pil new file mode 100644 index 0000000..35f239d Binary files /dev/null and b/ev3dev2/fonts/luBIS24.pil differ diff --git a/ev3dev2/fonts/luBS08.pbm b/ev3dev2/fonts/luBS08.pbm new file mode 100644 index 0000000..56e2501 Binary files /dev/null and b/ev3dev2/fonts/luBS08.pbm differ diff --git a/ev3dev2/fonts/luBS08.pil b/ev3dev2/fonts/luBS08.pil new file mode 100644 index 0000000..8a056fb Binary files /dev/null and b/ev3dev2/fonts/luBS08.pil differ diff --git a/ev3dev2/fonts/luBS10.pbm b/ev3dev2/fonts/luBS10.pbm new file mode 100644 index 0000000..9cbedfb Binary files /dev/null and b/ev3dev2/fonts/luBS10.pbm differ diff --git a/ev3dev2/fonts/luBS10.pil b/ev3dev2/fonts/luBS10.pil new file mode 100644 index 0000000..aa99e48 Binary files /dev/null and b/ev3dev2/fonts/luBS10.pil differ diff --git a/ev3dev2/fonts/luBS12.pbm b/ev3dev2/fonts/luBS12.pbm new file mode 100644 index 0000000..cf33703 Binary files /dev/null and b/ev3dev2/fonts/luBS12.pbm differ diff --git a/ev3dev2/fonts/luBS12.pil b/ev3dev2/fonts/luBS12.pil new file mode 100644 index 0000000..a358fa5 Binary files /dev/null and b/ev3dev2/fonts/luBS12.pil differ diff --git a/ev3dev2/fonts/luBS14.pbm b/ev3dev2/fonts/luBS14.pbm new file mode 100644 index 0000000..abfc997 Binary files /dev/null and b/ev3dev2/fonts/luBS14.pbm differ diff --git a/ev3dev2/fonts/luBS14.pil b/ev3dev2/fonts/luBS14.pil new file mode 100644 index 0000000..6fe82be Binary files /dev/null and b/ev3dev2/fonts/luBS14.pil differ diff --git a/ev3dev2/fonts/luBS18.pbm b/ev3dev2/fonts/luBS18.pbm new file mode 100644 index 0000000..e7c4382 Binary files /dev/null and b/ev3dev2/fonts/luBS18.pbm differ diff --git a/ev3dev2/fonts/luBS18.pil b/ev3dev2/fonts/luBS18.pil new file mode 100644 index 0000000..1d6371a Binary files /dev/null and b/ev3dev2/fonts/luBS18.pil differ diff --git a/ev3dev2/fonts/luBS19.pbm b/ev3dev2/fonts/luBS19.pbm new file mode 100644 index 0000000..6832e56 Binary files /dev/null and b/ev3dev2/fonts/luBS19.pbm differ diff --git a/ev3dev2/fonts/luBS19.pil b/ev3dev2/fonts/luBS19.pil new file mode 100644 index 0000000..b398376 Binary files /dev/null and b/ev3dev2/fonts/luBS19.pil differ diff --git a/ev3dev2/fonts/luBS24.pbm b/ev3dev2/fonts/luBS24.pbm new file mode 100644 index 0000000..036ec87 Binary files /dev/null and b/ev3dev2/fonts/luBS24.pbm differ diff --git a/ev3dev2/fonts/luBS24.pil b/ev3dev2/fonts/luBS24.pil new file mode 100644 index 0000000..ff79f63 Binary files /dev/null and b/ev3dev2/fonts/luBS24.pil differ diff --git a/ev3dev2/fonts/luIS08.pbm b/ev3dev2/fonts/luIS08.pbm new file mode 100644 index 0000000..cb624bc Binary files /dev/null and b/ev3dev2/fonts/luIS08.pbm differ diff --git a/ev3dev2/fonts/luIS08.pil b/ev3dev2/fonts/luIS08.pil new file mode 100644 index 0000000..720e0e1 Binary files /dev/null and b/ev3dev2/fonts/luIS08.pil differ diff --git a/ev3dev2/fonts/luIS10.pbm b/ev3dev2/fonts/luIS10.pbm new file mode 100644 index 0000000..0c9ceeb Binary files /dev/null and b/ev3dev2/fonts/luIS10.pbm differ diff --git a/ev3dev2/fonts/luIS10.pil b/ev3dev2/fonts/luIS10.pil new file mode 100644 index 0000000..1da2790 Binary files /dev/null and b/ev3dev2/fonts/luIS10.pil differ diff --git a/ev3dev2/fonts/luIS12.pbm b/ev3dev2/fonts/luIS12.pbm new file mode 100644 index 0000000..8f66ccb Binary files /dev/null and b/ev3dev2/fonts/luIS12.pbm differ diff --git a/ev3dev2/fonts/luIS12.pil b/ev3dev2/fonts/luIS12.pil new file mode 100644 index 0000000..ee35e78 Binary files /dev/null and b/ev3dev2/fonts/luIS12.pil differ diff --git a/ev3dev2/fonts/luIS14.pbm b/ev3dev2/fonts/luIS14.pbm new file mode 100644 index 0000000..4604b97 Binary files /dev/null and b/ev3dev2/fonts/luIS14.pbm differ diff --git a/ev3dev2/fonts/luIS14.pil b/ev3dev2/fonts/luIS14.pil new file mode 100644 index 0000000..a638dc0 Binary files /dev/null and b/ev3dev2/fonts/luIS14.pil differ diff --git a/ev3dev2/fonts/luIS18.pbm b/ev3dev2/fonts/luIS18.pbm new file mode 100644 index 0000000..01bfd72 Binary files /dev/null and b/ev3dev2/fonts/luIS18.pbm differ diff --git a/ev3dev2/fonts/luIS18.pil b/ev3dev2/fonts/luIS18.pil new file mode 100644 index 0000000..d7f1dce Binary files /dev/null and b/ev3dev2/fonts/luIS18.pil differ diff --git a/ev3dev2/fonts/luIS19.pbm b/ev3dev2/fonts/luIS19.pbm new file mode 100644 index 0000000..c590c57 Binary files /dev/null and b/ev3dev2/fonts/luIS19.pbm differ diff --git a/ev3dev2/fonts/luIS19.pil b/ev3dev2/fonts/luIS19.pil new file mode 100644 index 0000000..5c7912c Binary files /dev/null and b/ev3dev2/fonts/luIS19.pil differ diff --git a/ev3dev2/fonts/luIS24.pbm b/ev3dev2/fonts/luIS24.pbm new file mode 100644 index 0000000..0a0e63c Binary files /dev/null and b/ev3dev2/fonts/luIS24.pbm differ diff --git a/ev3dev2/fonts/luIS24.pil b/ev3dev2/fonts/luIS24.pil new file mode 100644 index 0000000..9e0fe64 Binary files /dev/null and b/ev3dev2/fonts/luIS24.pil differ diff --git a/ev3dev2/fonts/luRS08.pbm b/ev3dev2/fonts/luRS08.pbm new file mode 100644 index 0000000..677896b Binary files /dev/null and b/ev3dev2/fonts/luRS08.pbm differ diff --git a/ev3dev2/fonts/luRS08.pil b/ev3dev2/fonts/luRS08.pil new file mode 100644 index 0000000..ee48ee0 Binary files /dev/null and b/ev3dev2/fonts/luRS08.pil differ diff --git a/ev3dev2/fonts/luRS10.pbm b/ev3dev2/fonts/luRS10.pbm new file mode 100644 index 0000000..384e977 Binary files /dev/null and b/ev3dev2/fonts/luRS10.pbm differ diff --git a/ev3dev2/fonts/luRS10.pil b/ev3dev2/fonts/luRS10.pil new file mode 100644 index 0000000..22b57d0 Binary files /dev/null and b/ev3dev2/fonts/luRS10.pil differ diff --git a/ev3dev2/fonts/luRS12.pbm b/ev3dev2/fonts/luRS12.pbm new file mode 100644 index 0000000..f87c717 Binary files /dev/null and b/ev3dev2/fonts/luRS12.pbm differ diff --git a/ev3dev2/fonts/luRS12.pil b/ev3dev2/fonts/luRS12.pil new file mode 100644 index 0000000..86297b0 Binary files /dev/null and b/ev3dev2/fonts/luRS12.pil differ diff --git a/ev3dev2/fonts/luRS14.pbm b/ev3dev2/fonts/luRS14.pbm new file mode 100644 index 0000000..35e2d99 Binary files /dev/null and b/ev3dev2/fonts/luRS14.pbm differ diff --git a/ev3dev2/fonts/luRS14.pil b/ev3dev2/fonts/luRS14.pil new file mode 100644 index 0000000..dcd5d70 Binary files /dev/null and b/ev3dev2/fonts/luRS14.pil differ diff --git a/ev3dev2/fonts/luRS18.pbm b/ev3dev2/fonts/luRS18.pbm new file mode 100644 index 0000000..243d012 Binary files /dev/null and b/ev3dev2/fonts/luRS18.pbm differ diff --git a/ev3dev2/fonts/luRS18.pil b/ev3dev2/fonts/luRS18.pil new file mode 100644 index 0000000..784bc90 Binary files /dev/null and b/ev3dev2/fonts/luRS18.pil differ diff --git a/ev3dev2/fonts/luRS19.pbm b/ev3dev2/fonts/luRS19.pbm new file mode 100644 index 0000000..049e49b Binary files /dev/null and b/ev3dev2/fonts/luRS19.pbm differ diff --git a/ev3dev2/fonts/luRS19.pil b/ev3dev2/fonts/luRS19.pil new file mode 100644 index 0000000..642e2b7 Binary files /dev/null and b/ev3dev2/fonts/luRS19.pil differ diff --git a/ev3dev2/fonts/luRS24.pbm b/ev3dev2/fonts/luRS24.pbm new file mode 100644 index 0000000..f583802 Binary files /dev/null and b/ev3dev2/fonts/luRS24.pbm differ diff --git a/ev3dev2/fonts/luRS24.pil b/ev3dev2/fonts/luRS24.pil new file mode 100644 index 0000000..15ef9c6 Binary files /dev/null and b/ev3dev2/fonts/luRS24.pil differ diff --git a/ev3dev2/fonts/lubB08.pbm b/ev3dev2/fonts/lubB08.pbm new file mode 100644 index 0000000..e807c4f Binary files /dev/null and b/ev3dev2/fonts/lubB08.pbm differ diff --git a/ev3dev2/fonts/lubB08.pil b/ev3dev2/fonts/lubB08.pil new file mode 100644 index 0000000..9b8d003 Binary files /dev/null and b/ev3dev2/fonts/lubB08.pil differ diff --git a/ev3dev2/fonts/lubB10.pbm b/ev3dev2/fonts/lubB10.pbm new file mode 100644 index 0000000..bed6803 Binary files /dev/null and b/ev3dev2/fonts/lubB10.pbm differ diff --git a/ev3dev2/fonts/lubB10.pil b/ev3dev2/fonts/lubB10.pil new file mode 100644 index 0000000..4236bdc Binary files /dev/null and b/ev3dev2/fonts/lubB10.pil differ diff --git a/ev3dev2/fonts/lubB12.pbm b/ev3dev2/fonts/lubB12.pbm new file mode 100644 index 0000000..268c068 Binary files /dev/null and b/ev3dev2/fonts/lubB12.pbm differ diff --git a/ev3dev2/fonts/lubB12.pil b/ev3dev2/fonts/lubB12.pil new file mode 100644 index 0000000..9e79f06 Binary files /dev/null and b/ev3dev2/fonts/lubB12.pil differ diff --git a/ev3dev2/fonts/lubB14.pbm b/ev3dev2/fonts/lubB14.pbm new file mode 100644 index 0000000..b93eb58 Binary files /dev/null and b/ev3dev2/fonts/lubB14.pbm differ diff --git a/ev3dev2/fonts/lubB14.pil b/ev3dev2/fonts/lubB14.pil new file mode 100644 index 0000000..f0b880d Binary files /dev/null and b/ev3dev2/fonts/lubB14.pil differ diff --git a/ev3dev2/fonts/lubB18.pbm b/ev3dev2/fonts/lubB18.pbm new file mode 100644 index 0000000..c3b3045 Binary files /dev/null and b/ev3dev2/fonts/lubB18.pbm differ diff --git a/ev3dev2/fonts/lubB18.pil b/ev3dev2/fonts/lubB18.pil new file mode 100644 index 0000000..7927f90 Binary files /dev/null and b/ev3dev2/fonts/lubB18.pil differ diff --git a/ev3dev2/fonts/lubB19.pbm b/ev3dev2/fonts/lubB19.pbm new file mode 100644 index 0000000..4aa70be Binary files /dev/null and b/ev3dev2/fonts/lubB19.pbm differ diff --git a/ev3dev2/fonts/lubB19.pil b/ev3dev2/fonts/lubB19.pil new file mode 100644 index 0000000..7f02085 Binary files /dev/null and b/ev3dev2/fonts/lubB19.pil differ diff --git a/ev3dev2/fonts/lubB24.pbm b/ev3dev2/fonts/lubB24.pbm new file mode 100644 index 0000000..b2febf4 Binary files /dev/null and b/ev3dev2/fonts/lubB24.pbm differ diff --git a/ev3dev2/fonts/lubB24.pil b/ev3dev2/fonts/lubB24.pil new file mode 100644 index 0000000..81de42d Binary files /dev/null and b/ev3dev2/fonts/lubB24.pil differ diff --git a/ev3dev2/fonts/lubBI08.pbm b/ev3dev2/fonts/lubBI08.pbm new file mode 100644 index 0000000..abc1372 Binary files /dev/null and b/ev3dev2/fonts/lubBI08.pbm differ diff --git a/ev3dev2/fonts/lubBI08.pil b/ev3dev2/fonts/lubBI08.pil new file mode 100644 index 0000000..b467e70 Binary files /dev/null and b/ev3dev2/fonts/lubBI08.pil differ diff --git a/ev3dev2/fonts/lubBI10.pbm b/ev3dev2/fonts/lubBI10.pbm new file mode 100644 index 0000000..337f9c7 Binary files /dev/null and b/ev3dev2/fonts/lubBI10.pbm differ diff --git a/ev3dev2/fonts/lubBI10.pil b/ev3dev2/fonts/lubBI10.pil new file mode 100644 index 0000000..f9cb983 Binary files /dev/null and b/ev3dev2/fonts/lubBI10.pil differ diff --git a/ev3dev2/fonts/lubBI12.pbm b/ev3dev2/fonts/lubBI12.pbm new file mode 100644 index 0000000..d5a0eb3 Binary files /dev/null and b/ev3dev2/fonts/lubBI12.pbm differ diff --git a/ev3dev2/fonts/lubBI12.pil b/ev3dev2/fonts/lubBI12.pil new file mode 100644 index 0000000..6e30002 Binary files /dev/null and b/ev3dev2/fonts/lubBI12.pil differ diff --git a/ev3dev2/fonts/lubBI14.pbm b/ev3dev2/fonts/lubBI14.pbm new file mode 100644 index 0000000..94fad4a Binary files /dev/null and b/ev3dev2/fonts/lubBI14.pbm differ diff --git a/ev3dev2/fonts/lubBI14.pil b/ev3dev2/fonts/lubBI14.pil new file mode 100644 index 0000000..67c40b9 Binary files /dev/null and b/ev3dev2/fonts/lubBI14.pil differ diff --git a/ev3dev2/fonts/lubBI18.pbm b/ev3dev2/fonts/lubBI18.pbm new file mode 100644 index 0000000..84c7199 Binary files /dev/null and b/ev3dev2/fonts/lubBI18.pbm differ diff --git a/ev3dev2/fonts/lubBI18.pil b/ev3dev2/fonts/lubBI18.pil new file mode 100644 index 0000000..f97d0d2 Binary files /dev/null and b/ev3dev2/fonts/lubBI18.pil differ diff --git a/ev3dev2/fonts/lubBI19.pbm b/ev3dev2/fonts/lubBI19.pbm new file mode 100644 index 0000000..5cea7ec Binary files /dev/null and b/ev3dev2/fonts/lubBI19.pbm differ diff --git a/ev3dev2/fonts/lubBI19.pil b/ev3dev2/fonts/lubBI19.pil new file mode 100644 index 0000000..98580bb Binary files /dev/null and b/ev3dev2/fonts/lubBI19.pil differ diff --git a/ev3dev2/fonts/lubBI24.pbm b/ev3dev2/fonts/lubBI24.pbm new file mode 100644 index 0000000..5e7766a Binary files /dev/null and b/ev3dev2/fonts/lubBI24.pbm differ diff --git a/ev3dev2/fonts/lubBI24.pil b/ev3dev2/fonts/lubBI24.pil new file mode 100644 index 0000000..e50c5c3 Binary files /dev/null and b/ev3dev2/fonts/lubBI24.pil differ diff --git a/ev3dev2/fonts/lubI08.pbm b/ev3dev2/fonts/lubI08.pbm new file mode 100644 index 0000000..d24ddff Binary files /dev/null and b/ev3dev2/fonts/lubI08.pbm differ diff --git a/ev3dev2/fonts/lubI08.pil b/ev3dev2/fonts/lubI08.pil new file mode 100644 index 0000000..8df4787 Binary files /dev/null and b/ev3dev2/fonts/lubI08.pil differ diff --git a/ev3dev2/fonts/lubI10.pbm b/ev3dev2/fonts/lubI10.pbm new file mode 100644 index 0000000..de51a1f Binary files /dev/null and b/ev3dev2/fonts/lubI10.pbm differ diff --git a/ev3dev2/fonts/lubI10.pil b/ev3dev2/fonts/lubI10.pil new file mode 100644 index 0000000..f2147bb Binary files /dev/null and b/ev3dev2/fonts/lubI10.pil differ diff --git a/ev3dev2/fonts/lubI12.pbm b/ev3dev2/fonts/lubI12.pbm new file mode 100644 index 0000000..cad002a Binary files /dev/null and b/ev3dev2/fonts/lubI12.pbm differ diff --git a/ev3dev2/fonts/lubI12.pil b/ev3dev2/fonts/lubI12.pil new file mode 100644 index 0000000..1d40a67 Binary files /dev/null and b/ev3dev2/fonts/lubI12.pil differ diff --git a/ev3dev2/fonts/lubI14.pbm b/ev3dev2/fonts/lubI14.pbm new file mode 100644 index 0000000..5f02acc Binary files /dev/null and b/ev3dev2/fonts/lubI14.pbm differ diff --git a/ev3dev2/fonts/lubI14.pil b/ev3dev2/fonts/lubI14.pil new file mode 100644 index 0000000..3e92439 Binary files /dev/null and b/ev3dev2/fonts/lubI14.pil differ diff --git a/ev3dev2/fonts/lubI18.pbm b/ev3dev2/fonts/lubI18.pbm new file mode 100644 index 0000000..e5be51b Binary files /dev/null and b/ev3dev2/fonts/lubI18.pbm differ diff --git a/ev3dev2/fonts/lubI18.pil b/ev3dev2/fonts/lubI18.pil new file mode 100644 index 0000000..a868bf8 Binary files /dev/null and b/ev3dev2/fonts/lubI18.pil differ diff --git a/ev3dev2/fonts/lubI19.pbm b/ev3dev2/fonts/lubI19.pbm new file mode 100644 index 0000000..e5e6d8c Binary files /dev/null and b/ev3dev2/fonts/lubI19.pbm differ diff --git a/ev3dev2/fonts/lubI19.pil b/ev3dev2/fonts/lubI19.pil new file mode 100644 index 0000000..a48d282 Binary files /dev/null and b/ev3dev2/fonts/lubI19.pil differ diff --git a/ev3dev2/fonts/lubI24.pbm b/ev3dev2/fonts/lubI24.pbm new file mode 100644 index 0000000..53ce06e Binary files /dev/null and b/ev3dev2/fonts/lubI24.pbm differ diff --git a/ev3dev2/fonts/lubI24.pil b/ev3dev2/fonts/lubI24.pil new file mode 100644 index 0000000..ef2294e Binary files /dev/null and b/ev3dev2/fonts/lubI24.pil differ diff --git a/ev3dev2/fonts/lubR08.pbm b/ev3dev2/fonts/lubR08.pbm new file mode 100644 index 0000000..c477e74 Binary files /dev/null and b/ev3dev2/fonts/lubR08.pbm differ diff --git a/ev3dev2/fonts/lubR08.pil b/ev3dev2/fonts/lubR08.pil new file mode 100644 index 0000000..6674ef1 Binary files /dev/null and b/ev3dev2/fonts/lubR08.pil differ diff --git a/ev3dev2/fonts/lubR10.pbm b/ev3dev2/fonts/lubR10.pbm new file mode 100644 index 0000000..3b62f2e Binary files /dev/null and b/ev3dev2/fonts/lubR10.pbm differ diff --git a/ev3dev2/fonts/lubR10.pil b/ev3dev2/fonts/lubR10.pil new file mode 100644 index 0000000..8fb6c75 Binary files /dev/null and b/ev3dev2/fonts/lubR10.pil differ diff --git a/ev3dev2/fonts/lubR12.pbm b/ev3dev2/fonts/lubR12.pbm new file mode 100644 index 0000000..511cfb8 Binary files /dev/null and b/ev3dev2/fonts/lubR12.pbm differ diff --git a/ev3dev2/fonts/lubR12.pil b/ev3dev2/fonts/lubR12.pil new file mode 100644 index 0000000..d86e0d3 Binary files /dev/null and b/ev3dev2/fonts/lubR12.pil differ diff --git a/ev3dev2/fonts/lubR14.pbm b/ev3dev2/fonts/lubR14.pbm new file mode 100644 index 0000000..01fdc56 Binary files /dev/null and b/ev3dev2/fonts/lubR14.pbm differ diff --git a/ev3dev2/fonts/lubR14.pil b/ev3dev2/fonts/lubR14.pil new file mode 100644 index 0000000..144e893 Binary files /dev/null and b/ev3dev2/fonts/lubR14.pil differ diff --git a/ev3dev2/fonts/lubR18.pbm b/ev3dev2/fonts/lubR18.pbm new file mode 100644 index 0000000..da8ae38 Binary files /dev/null and b/ev3dev2/fonts/lubR18.pbm differ diff --git a/ev3dev2/fonts/lubR18.pil b/ev3dev2/fonts/lubR18.pil new file mode 100644 index 0000000..42000e0 Binary files /dev/null and b/ev3dev2/fonts/lubR18.pil differ diff --git a/ev3dev2/fonts/lubR19.pbm b/ev3dev2/fonts/lubR19.pbm new file mode 100644 index 0000000..d6d9b78 Binary files /dev/null and b/ev3dev2/fonts/lubR19.pbm differ diff --git a/ev3dev2/fonts/lubR19.pil b/ev3dev2/fonts/lubR19.pil new file mode 100644 index 0000000..4a873a1 Binary files /dev/null and b/ev3dev2/fonts/lubR19.pil differ diff --git a/ev3dev2/fonts/lubR24.pbm b/ev3dev2/fonts/lubR24.pbm new file mode 100644 index 0000000..eaf003b Binary files /dev/null and b/ev3dev2/fonts/lubR24.pbm differ diff --git a/ev3dev2/fonts/lubR24.pil b/ev3dev2/fonts/lubR24.pil new file mode 100644 index 0000000..de986c5 Binary files /dev/null and b/ev3dev2/fonts/lubR24.pil differ diff --git a/ev3dev2/fonts/lutBS08.pbm b/ev3dev2/fonts/lutBS08.pbm new file mode 100644 index 0000000..1fe0fc5 Binary files /dev/null and b/ev3dev2/fonts/lutBS08.pbm differ diff --git a/ev3dev2/fonts/lutBS08.pil b/ev3dev2/fonts/lutBS08.pil new file mode 100644 index 0000000..8a21d0e Binary files /dev/null and b/ev3dev2/fonts/lutBS08.pil differ diff --git a/ev3dev2/fonts/lutBS10.pbm b/ev3dev2/fonts/lutBS10.pbm new file mode 100644 index 0000000..e488981 Binary files /dev/null and b/ev3dev2/fonts/lutBS10.pbm differ diff --git a/ev3dev2/fonts/lutBS10.pil b/ev3dev2/fonts/lutBS10.pil new file mode 100644 index 0000000..bdc7bd9 Binary files /dev/null and b/ev3dev2/fonts/lutBS10.pil differ diff --git a/ev3dev2/fonts/lutBS12.pbm b/ev3dev2/fonts/lutBS12.pbm new file mode 100644 index 0000000..8cbca91 Binary files /dev/null and b/ev3dev2/fonts/lutBS12.pbm differ diff --git a/ev3dev2/fonts/lutBS12.pil b/ev3dev2/fonts/lutBS12.pil new file mode 100644 index 0000000..358dfe8 Binary files /dev/null and b/ev3dev2/fonts/lutBS12.pil differ diff --git a/ev3dev2/fonts/lutBS14.pbm b/ev3dev2/fonts/lutBS14.pbm new file mode 100644 index 0000000..6754d32 Binary files /dev/null and b/ev3dev2/fonts/lutBS14.pbm differ diff --git a/ev3dev2/fonts/lutBS14.pil b/ev3dev2/fonts/lutBS14.pil new file mode 100644 index 0000000..97acf1a Binary files /dev/null and b/ev3dev2/fonts/lutBS14.pil differ diff --git a/ev3dev2/fonts/lutBS18.pbm b/ev3dev2/fonts/lutBS18.pbm new file mode 100644 index 0000000..8a98341 Binary files /dev/null and b/ev3dev2/fonts/lutBS18.pbm differ diff --git a/ev3dev2/fonts/lutBS18.pil b/ev3dev2/fonts/lutBS18.pil new file mode 100644 index 0000000..70c2ae9 Binary files /dev/null and b/ev3dev2/fonts/lutBS18.pil differ diff --git a/ev3dev2/fonts/lutBS19.pbm b/ev3dev2/fonts/lutBS19.pbm new file mode 100644 index 0000000..7eedbee Binary files /dev/null and b/ev3dev2/fonts/lutBS19.pbm differ diff --git a/ev3dev2/fonts/lutBS19.pil b/ev3dev2/fonts/lutBS19.pil new file mode 100644 index 0000000..83869fc Binary files /dev/null and b/ev3dev2/fonts/lutBS19.pil differ diff --git a/ev3dev2/fonts/lutBS24.pbm b/ev3dev2/fonts/lutBS24.pbm new file mode 100644 index 0000000..e272e1b Binary files /dev/null and b/ev3dev2/fonts/lutBS24.pbm differ diff --git a/ev3dev2/fonts/lutBS24.pil b/ev3dev2/fonts/lutBS24.pil new file mode 100644 index 0000000..8928e21 Binary files /dev/null and b/ev3dev2/fonts/lutBS24.pil differ diff --git a/ev3dev2/fonts/lutRS08.pbm b/ev3dev2/fonts/lutRS08.pbm new file mode 100644 index 0000000..ea9048d Binary files /dev/null and b/ev3dev2/fonts/lutRS08.pbm differ diff --git a/ev3dev2/fonts/lutRS08.pil b/ev3dev2/fonts/lutRS08.pil new file mode 100644 index 0000000..cf84292 Binary files /dev/null and b/ev3dev2/fonts/lutRS08.pil differ diff --git a/ev3dev2/fonts/lutRS10.pbm b/ev3dev2/fonts/lutRS10.pbm new file mode 100644 index 0000000..fe1de24 Binary files /dev/null and b/ev3dev2/fonts/lutRS10.pbm differ diff --git a/ev3dev2/fonts/lutRS10.pil b/ev3dev2/fonts/lutRS10.pil new file mode 100644 index 0000000..c4de0ec Binary files /dev/null and b/ev3dev2/fonts/lutRS10.pil differ diff --git a/ev3dev2/fonts/lutRS12.pbm b/ev3dev2/fonts/lutRS12.pbm new file mode 100644 index 0000000..17ad422 Binary files /dev/null and b/ev3dev2/fonts/lutRS12.pbm differ diff --git a/ev3dev2/fonts/lutRS12.pil b/ev3dev2/fonts/lutRS12.pil new file mode 100644 index 0000000..e82f429 Binary files /dev/null and b/ev3dev2/fonts/lutRS12.pil differ diff --git a/ev3dev2/fonts/lutRS14.pbm b/ev3dev2/fonts/lutRS14.pbm new file mode 100644 index 0000000..1c21406 Binary files /dev/null and b/ev3dev2/fonts/lutRS14.pbm differ diff --git a/ev3dev2/fonts/lutRS14.pil b/ev3dev2/fonts/lutRS14.pil new file mode 100644 index 0000000..d6b96bf Binary files /dev/null and b/ev3dev2/fonts/lutRS14.pil differ diff --git a/ev3dev2/fonts/lutRS18.pbm b/ev3dev2/fonts/lutRS18.pbm new file mode 100644 index 0000000..c44a91d Binary files /dev/null and b/ev3dev2/fonts/lutRS18.pbm differ diff --git a/ev3dev2/fonts/lutRS18.pil b/ev3dev2/fonts/lutRS18.pil new file mode 100644 index 0000000..0b7257c Binary files /dev/null and b/ev3dev2/fonts/lutRS18.pil differ diff --git a/ev3dev2/fonts/lutRS19.pbm b/ev3dev2/fonts/lutRS19.pbm new file mode 100644 index 0000000..815b9fe Binary files /dev/null and b/ev3dev2/fonts/lutRS19.pbm differ diff --git a/ev3dev2/fonts/lutRS19.pil b/ev3dev2/fonts/lutRS19.pil new file mode 100644 index 0000000..37b5356 Binary files /dev/null and b/ev3dev2/fonts/lutRS19.pil differ diff --git a/ev3dev2/fonts/lutRS24.pbm b/ev3dev2/fonts/lutRS24.pbm new file mode 100644 index 0000000..76c6fbe Binary files /dev/null and b/ev3dev2/fonts/lutRS24.pbm differ diff --git a/ev3dev2/fonts/lutRS24.pil b/ev3dev2/fonts/lutRS24.pil new file mode 100644 index 0000000..97e89c5 Binary files /dev/null and b/ev3dev2/fonts/lutRS24.pil differ diff --git a/ev3dev2/fonts/ncenB08.pbm b/ev3dev2/fonts/ncenB08.pbm new file mode 100644 index 0000000..ef7919a Binary files /dev/null and b/ev3dev2/fonts/ncenB08.pbm differ diff --git a/ev3dev2/fonts/ncenB08.pil b/ev3dev2/fonts/ncenB08.pil new file mode 100644 index 0000000..8f2d757 Binary files /dev/null and b/ev3dev2/fonts/ncenB08.pil differ diff --git a/ev3dev2/fonts/ncenB10.pbm b/ev3dev2/fonts/ncenB10.pbm new file mode 100644 index 0000000..bc04ed2 Binary files /dev/null and b/ev3dev2/fonts/ncenB10.pbm differ diff --git a/ev3dev2/fonts/ncenB10.pil b/ev3dev2/fonts/ncenB10.pil new file mode 100644 index 0000000..cc835c1 Binary files /dev/null and b/ev3dev2/fonts/ncenB10.pil differ diff --git a/ev3dev2/fonts/ncenB12.pbm b/ev3dev2/fonts/ncenB12.pbm new file mode 100644 index 0000000..b9bb280 Binary files /dev/null and b/ev3dev2/fonts/ncenB12.pbm differ diff --git a/ev3dev2/fonts/ncenB12.pil b/ev3dev2/fonts/ncenB12.pil new file mode 100644 index 0000000..20a3885 Binary files /dev/null and b/ev3dev2/fonts/ncenB12.pil differ diff --git a/ev3dev2/fonts/ncenB14.pbm b/ev3dev2/fonts/ncenB14.pbm new file mode 100644 index 0000000..883c190 Binary files /dev/null and b/ev3dev2/fonts/ncenB14.pbm differ diff --git a/ev3dev2/fonts/ncenB14.pil b/ev3dev2/fonts/ncenB14.pil new file mode 100644 index 0000000..0324ddd Binary files /dev/null and b/ev3dev2/fonts/ncenB14.pil differ diff --git a/ev3dev2/fonts/ncenB18.pbm b/ev3dev2/fonts/ncenB18.pbm new file mode 100644 index 0000000..9522f0f Binary files /dev/null and b/ev3dev2/fonts/ncenB18.pbm differ diff --git a/ev3dev2/fonts/ncenB18.pil b/ev3dev2/fonts/ncenB18.pil new file mode 100644 index 0000000..dc73c54 Binary files /dev/null and b/ev3dev2/fonts/ncenB18.pil differ diff --git a/ev3dev2/fonts/ncenB24.pbm b/ev3dev2/fonts/ncenB24.pbm new file mode 100644 index 0000000..ef00bee Binary files /dev/null and b/ev3dev2/fonts/ncenB24.pbm differ diff --git a/ev3dev2/fonts/ncenB24.pil b/ev3dev2/fonts/ncenB24.pil new file mode 100644 index 0000000..44454cf Binary files /dev/null and b/ev3dev2/fonts/ncenB24.pil differ diff --git a/ev3dev2/fonts/ncenBI08.pbm b/ev3dev2/fonts/ncenBI08.pbm new file mode 100644 index 0000000..6f77a7b Binary files /dev/null and b/ev3dev2/fonts/ncenBI08.pbm differ diff --git a/ev3dev2/fonts/ncenBI08.pil b/ev3dev2/fonts/ncenBI08.pil new file mode 100644 index 0000000..f3513bf Binary files /dev/null and b/ev3dev2/fonts/ncenBI08.pil differ diff --git a/ev3dev2/fonts/ncenBI10.pbm b/ev3dev2/fonts/ncenBI10.pbm new file mode 100644 index 0000000..c22dff6 Binary files /dev/null and b/ev3dev2/fonts/ncenBI10.pbm differ diff --git a/ev3dev2/fonts/ncenBI10.pil b/ev3dev2/fonts/ncenBI10.pil new file mode 100644 index 0000000..bb554d3 Binary files /dev/null and b/ev3dev2/fonts/ncenBI10.pil differ diff --git a/ev3dev2/fonts/ncenBI12.pbm b/ev3dev2/fonts/ncenBI12.pbm new file mode 100644 index 0000000..c1fd3af Binary files /dev/null and b/ev3dev2/fonts/ncenBI12.pbm differ diff --git a/ev3dev2/fonts/ncenBI12.pil b/ev3dev2/fonts/ncenBI12.pil new file mode 100644 index 0000000..58c42d9 Binary files /dev/null and b/ev3dev2/fonts/ncenBI12.pil differ diff --git a/ev3dev2/fonts/ncenBI14.pbm b/ev3dev2/fonts/ncenBI14.pbm new file mode 100644 index 0000000..1785b78 Binary files /dev/null and b/ev3dev2/fonts/ncenBI14.pbm differ diff --git a/ev3dev2/fonts/ncenBI14.pil b/ev3dev2/fonts/ncenBI14.pil new file mode 100644 index 0000000..9f9502e Binary files /dev/null and b/ev3dev2/fonts/ncenBI14.pil differ diff --git a/ev3dev2/fonts/ncenBI18.pbm b/ev3dev2/fonts/ncenBI18.pbm new file mode 100644 index 0000000..326f5f0 Binary files /dev/null and b/ev3dev2/fonts/ncenBI18.pbm differ diff --git a/ev3dev2/fonts/ncenBI18.pil b/ev3dev2/fonts/ncenBI18.pil new file mode 100644 index 0000000..ddb53ef Binary files /dev/null and b/ev3dev2/fonts/ncenBI18.pil differ diff --git a/ev3dev2/fonts/ncenBI24.pbm b/ev3dev2/fonts/ncenBI24.pbm new file mode 100644 index 0000000..d9b94f8 Binary files /dev/null and b/ev3dev2/fonts/ncenBI24.pbm differ diff --git a/ev3dev2/fonts/ncenBI24.pil b/ev3dev2/fonts/ncenBI24.pil new file mode 100644 index 0000000..2362b77 Binary files /dev/null and b/ev3dev2/fonts/ncenBI24.pil differ diff --git a/ev3dev2/fonts/ncenI08.pbm b/ev3dev2/fonts/ncenI08.pbm new file mode 100644 index 0000000..9af2099 Binary files /dev/null and b/ev3dev2/fonts/ncenI08.pbm differ diff --git a/ev3dev2/fonts/ncenI08.pil b/ev3dev2/fonts/ncenI08.pil new file mode 100644 index 0000000..695276a Binary files /dev/null and b/ev3dev2/fonts/ncenI08.pil differ diff --git a/ev3dev2/fonts/ncenI10.pbm b/ev3dev2/fonts/ncenI10.pbm new file mode 100644 index 0000000..446fdc0 Binary files /dev/null and b/ev3dev2/fonts/ncenI10.pbm differ diff --git a/ev3dev2/fonts/ncenI10.pil b/ev3dev2/fonts/ncenI10.pil new file mode 100644 index 0000000..a47c763 Binary files /dev/null and b/ev3dev2/fonts/ncenI10.pil differ diff --git a/ev3dev2/fonts/ncenI12.pbm b/ev3dev2/fonts/ncenI12.pbm new file mode 100644 index 0000000..b927d6c Binary files /dev/null and b/ev3dev2/fonts/ncenI12.pbm differ diff --git a/ev3dev2/fonts/ncenI12.pil b/ev3dev2/fonts/ncenI12.pil new file mode 100644 index 0000000..cac91f1 Binary files /dev/null and b/ev3dev2/fonts/ncenI12.pil differ diff --git a/ev3dev2/fonts/ncenI14.pbm b/ev3dev2/fonts/ncenI14.pbm new file mode 100644 index 0000000..f11b493 Binary files /dev/null and b/ev3dev2/fonts/ncenI14.pbm differ diff --git a/ev3dev2/fonts/ncenI14.pil b/ev3dev2/fonts/ncenI14.pil new file mode 100644 index 0000000..bf2dadd Binary files /dev/null and b/ev3dev2/fonts/ncenI14.pil differ diff --git a/ev3dev2/fonts/ncenI18.pbm b/ev3dev2/fonts/ncenI18.pbm new file mode 100644 index 0000000..bab39da Binary files /dev/null and b/ev3dev2/fonts/ncenI18.pbm differ diff --git a/ev3dev2/fonts/ncenI18.pil b/ev3dev2/fonts/ncenI18.pil new file mode 100644 index 0000000..c51bca0 Binary files /dev/null and b/ev3dev2/fonts/ncenI18.pil differ diff --git a/ev3dev2/fonts/ncenI24.pbm b/ev3dev2/fonts/ncenI24.pbm new file mode 100644 index 0000000..1e70e03 Binary files /dev/null and b/ev3dev2/fonts/ncenI24.pbm differ diff --git a/ev3dev2/fonts/ncenI24.pil b/ev3dev2/fonts/ncenI24.pil new file mode 100644 index 0000000..d737b38 Binary files /dev/null and b/ev3dev2/fonts/ncenI24.pil differ diff --git a/ev3dev2/fonts/ncenR08.pbm b/ev3dev2/fonts/ncenR08.pbm new file mode 100644 index 0000000..f97c8f6 Binary files /dev/null and b/ev3dev2/fonts/ncenR08.pbm differ diff --git a/ev3dev2/fonts/ncenR08.pil b/ev3dev2/fonts/ncenR08.pil new file mode 100644 index 0000000..c5a9a92 Binary files /dev/null and b/ev3dev2/fonts/ncenR08.pil differ diff --git a/ev3dev2/fonts/ncenR10.pbm b/ev3dev2/fonts/ncenR10.pbm new file mode 100644 index 0000000..83febe8 Binary files /dev/null and b/ev3dev2/fonts/ncenR10.pbm differ diff --git a/ev3dev2/fonts/ncenR10.pil b/ev3dev2/fonts/ncenR10.pil new file mode 100644 index 0000000..db54af9 Binary files /dev/null and b/ev3dev2/fonts/ncenR10.pil differ diff --git a/ev3dev2/fonts/ncenR12.pbm b/ev3dev2/fonts/ncenR12.pbm new file mode 100644 index 0000000..bbd33cf Binary files /dev/null and b/ev3dev2/fonts/ncenR12.pbm differ diff --git a/ev3dev2/fonts/ncenR12.pil b/ev3dev2/fonts/ncenR12.pil new file mode 100644 index 0000000..b5ef356 Binary files /dev/null and b/ev3dev2/fonts/ncenR12.pil differ diff --git a/ev3dev2/fonts/ncenR14.pbm b/ev3dev2/fonts/ncenR14.pbm new file mode 100644 index 0000000..561feb5 Binary files /dev/null and b/ev3dev2/fonts/ncenR14.pbm differ diff --git a/ev3dev2/fonts/ncenR14.pil b/ev3dev2/fonts/ncenR14.pil new file mode 100644 index 0000000..8f7513c Binary files /dev/null and b/ev3dev2/fonts/ncenR14.pil differ diff --git a/ev3dev2/fonts/ncenR18.pbm b/ev3dev2/fonts/ncenR18.pbm new file mode 100644 index 0000000..2dd5a9b Binary files /dev/null and b/ev3dev2/fonts/ncenR18.pbm differ diff --git a/ev3dev2/fonts/ncenR18.pil b/ev3dev2/fonts/ncenR18.pil new file mode 100644 index 0000000..4794b8e Binary files /dev/null and b/ev3dev2/fonts/ncenR18.pil differ diff --git a/ev3dev2/fonts/ncenR24.pbm b/ev3dev2/fonts/ncenR24.pbm new file mode 100644 index 0000000..a3bae75 Binary files /dev/null and b/ev3dev2/fonts/ncenR24.pbm differ diff --git a/ev3dev2/fonts/ncenR24.pil b/ev3dev2/fonts/ncenR24.pil new file mode 100644 index 0000000..1d8fcac Binary files /dev/null and b/ev3dev2/fonts/ncenR24.pil differ diff --git a/ev3dev2/fonts/symb08.pbm b/ev3dev2/fonts/symb08.pbm new file mode 100644 index 0000000..0951afa Binary files /dev/null and b/ev3dev2/fonts/symb08.pbm differ diff --git a/ev3dev2/fonts/symb08.pil b/ev3dev2/fonts/symb08.pil new file mode 100644 index 0000000..4f52d0b Binary files /dev/null and b/ev3dev2/fonts/symb08.pil differ diff --git a/ev3dev2/fonts/symb10.pbm b/ev3dev2/fonts/symb10.pbm new file mode 100644 index 0000000..319114e Binary files /dev/null and b/ev3dev2/fonts/symb10.pbm differ diff --git a/ev3dev2/fonts/symb10.pil b/ev3dev2/fonts/symb10.pil new file mode 100644 index 0000000..5402109 Binary files /dev/null and b/ev3dev2/fonts/symb10.pil differ diff --git a/ev3dev2/fonts/symb12.pbm b/ev3dev2/fonts/symb12.pbm new file mode 100644 index 0000000..5d927da Binary files /dev/null and b/ev3dev2/fonts/symb12.pbm differ diff --git a/ev3dev2/fonts/symb12.pil b/ev3dev2/fonts/symb12.pil new file mode 100644 index 0000000..7b262c6 Binary files /dev/null and b/ev3dev2/fonts/symb12.pil differ diff --git a/ev3dev2/fonts/symb14.pbm b/ev3dev2/fonts/symb14.pbm new file mode 100644 index 0000000..092ace9 Binary files /dev/null and b/ev3dev2/fonts/symb14.pbm differ diff --git a/ev3dev2/fonts/symb14.pil b/ev3dev2/fonts/symb14.pil new file mode 100644 index 0000000..6595788 Binary files /dev/null and b/ev3dev2/fonts/symb14.pil differ diff --git a/ev3dev2/fonts/symb18.pbm b/ev3dev2/fonts/symb18.pbm new file mode 100644 index 0000000..3f8e6fe Binary files /dev/null and b/ev3dev2/fonts/symb18.pbm differ diff --git a/ev3dev2/fonts/symb18.pil b/ev3dev2/fonts/symb18.pil new file mode 100644 index 0000000..516f51e Binary files /dev/null and b/ev3dev2/fonts/symb18.pil differ diff --git a/ev3dev2/fonts/symb24.pbm b/ev3dev2/fonts/symb24.pbm new file mode 100644 index 0000000..a2c9b64 Binary files /dev/null and b/ev3dev2/fonts/symb24.pbm differ diff --git a/ev3dev2/fonts/symb24.pil b/ev3dev2/fonts/symb24.pil new file mode 100644 index 0000000..3827826 Binary files /dev/null and b/ev3dev2/fonts/symb24.pil differ diff --git a/ev3dev2/fonts/tech14.pbm b/ev3dev2/fonts/tech14.pbm new file mode 100644 index 0000000..6a5a35b Binary files /dev/null and b/ev3dev2/fonts/tech14.pbm differ diff --git a/ev3dev2/fonts/tech14.pil b/ev3dev2/fonts/tech14.pil new file mode 100644 index 0000000..f206b7d Binary files /dev/null and b/ev3dev2/fonts/tech14.pil differ diff --git a/ev3dev2/fonts/techB14.pbm b/ev3dev2/fonts/techB14.pbm new file mode 100644 index 0000000..5ec1e07 Binary files /dev/null and b/ev3dev2/fonts/techB14.pbm differ diff --git a/ev3dev2/fonts/techB14.pil b/ev3dev2/fonts/techB14.pil new file mode 100644 index 0000000..f206b7d Binary files /dev/null and b/ev3dev2/fonts/techB14.pil differ diff --git a/ev3dev2/fonts/term14.pbm b/ev3dev2/fonts/term14.pbm new file mode 100644 index 0000000..4a79819 Binary files /dev/null and b/ev3dev2/fonts/term14.pbm differ diff --git a/ev3dev2/fonts/term14.pil b/ev3dev2/fonts/term14.pil new file mode 100644 index 0000000..f5921e9 Binary files /dev/null and b/ev3dev2/fonts/term14.pil differ diff --git a/ev3dev2/fonts/termB14.pbm b/ev3dev2/fonts/termB14.pbm new file mode 100644 index 0000000..0c33d0f Binary files /dev/null and b/ev3dev2/fonts/termB14.pbm differ diff --git a/ev3dev2/fonts/termB14.pil b/ev3dev2/fonts/termB14.pil new file mode 100644 index 0000000..f5921e9 Binary files /dev/null and b/ev3dev2/fonts/termB14.pil differ diff --git a/ev3dev2/fonts/timB08.pbm b/ev3dev2/fonts/timB08.pbm new file mode 100644 index 0000000..d3a838a Binary files /dev/null and b/ev3dev2/fonts/timB08.pbm differ diff --git a/ev3dev2/fonts/timB08.pil b/ev3dev2/fonts/timB08.pil new file mode 100644 index 0000000..df39646 Binary files /dev/null and b/ev3dev2/fonts/timB08.pil differ diff --git a/ev3dev2/fonts/timB10.pbm b/ev3dev2/fonts/timB10.pbm new file mode 100644 index 0000000..272d107 Binary files /dev/null and b/ev3dev2/fonts/timB10.pbm differ diff --git a/ev3dev2/fonts/timB10.pil b/ev3dev2/fonts/timB10.pil new file mode 100644 index 0000000..f471f62 Binary files /dev/null and b/ev3dev2/fonts/timB10.pil differ diff --git a/ev3dev2/fonts/timB12.pbm b/ev3dev2/fonts/timB12.pbm new file mode 100644 index 0000000..febba57 Binary files /dev/null and b/ev3dev2/fonts/timB12.pbm differ diff --git a/ev3dev2/fonts/timB12.pil b/ev3dev2/fonts/timB12.pil new file mode 100644 index 0000000..18f12af Binary files /dev/null and b/ev3dev2/fonts/timB12.pil differ diff --git a/ev3dev2/fonts/timB14.pbm b/ev3dev2/fonts/timB14.pbm new file mode 100644 index 0000000..c5e0b34 Binary files /dev/null and b/ev3dev2/fonts/timB14.pbm differ diff --git a/ev3dev2/fonts/timB14.pil b/ev3dev2/fonts/timB14.pil new file mode 100644 index 0000000..7e4ac89 Binary files /dev/null and b/ev3dev2/fonts/timB14.pil differ diff --git a/ev3dev2/fonts/timB18.pbm b/ev3dev2/fonts/timB18.pbm new file mode 100644 index 0000000..546548e Binary files /dev/null and b/ev3dev2/fonts/timB18.pbm differ diff --git a/ev3dev2/fonts/timB18.pil b/ev3dev2/fonts/timB18.pil new file mode 100644 index 0000000..8c1c24a Binary files /dev/null and b/ev3dev2/fonts/timB18.pil differ diff --git a/ev3dev2/fonts/timB24.pbm b/ev3dev2/fonts/timB24.pbm new file mode 100644 index 0000000..9f90e56 Binary files /dev/null and b/ev3dev2/fonts/timB24.pbm differ diff --git a/ev3dev2/fonts/timB24.pil b/ev3dev2/fonts/timB24.pil new file mode 100644 index 0000000..e7393c1 Binary files /dev/null and b/ev3dev2/fonts/timB24.pil differ diff --git a/ev3dev2/fonts/timBI08.pbm b/ev3dev2/fonts/timBI08.pbm new file mode 100644 index 0000000..45c0196 Binary files /dev/null and b/ev3dev2/fonts/timBI08.pbm differ diff --git a/ev3dev2/fonts/timBI08.pil b/ev3dev2/fonts/timBI08.pil new file mode 100644 index 0000000..f9c97e3 Binary files /dev/null and b/ev3dev2/fonts/timBI08.pil differ diff --git a/ev3dev2/fonts/timBI10.pbm b/ev3dev2/fonts/timBI10.pbm new file mode 100644 index 0000000..f449d5c Binary files /dev/null and b/ev3dev2/fonts/timBI10.pbm differ diff --git a/ev3dev2/fonts/timBI10.pil b/ev3dev2/fonts/timBI10.pil new file mode 100644 index 0000000..dedbfea Binary files /dev/null and b/ev3dev2/fonts/timBI10.pil differ diff --git a/ev3dev2/fonts/timBI12.pbm b/ev3dev2/fonts/timBI12.pbm new file mode 100644 index 0000000..cf2245e Binary files /dev/null and b/ev3dev2/fonts/timBI12.pbm differ diff --git a/ev3dev2/fonts/timBI12.pil b/ev3dev2/fonts/timBI12.pil new file mode 100644 index 0000000..f5c1f43 Binary files /dev/null and b/ev3dev2/fonts/timBI12.pil differ diff --git a/ev3dev2/fonts/timBI14.pbm b/ev3dev2/fonts/timBI14.pbm new file mode 100644 index 0000000..261e965 Binary files /dev/null and b/ev3dev2/fonts/timBI14.pbm differ diff --git a/ev3dev2/fonts/timBI14.pil b/ev3dev2/fonts/timBI14.pil new file mode 100644 index 0000000..a2d0543 Binary files /dev/null and b/ev3dev2/fonts/timBI14.pil differ diff --git a/ev3dev2/fonts/timBI18.pbm b/ev3dev2/fonts/timBI18.pbm new file mode 100644 index 0000000..e1fd02b Binary files /dev/null and b/ev3dev2/fonts/timBI18.pbm differ diff --git a/ev3dev2/fonts/timBI18.pil b/ev3dev2/fonts/timBI18.pil new file mode 100644 index 0000000..717d994 Binary files /dev/null and b/ev3dev2/fonts/timBI18.pil differ diff --git a/ev3dev2/fonts/timBI24.pbm b/ev3dev2/fonts/timBI24.pbm new file mode 100644 index 0000000..c96808e Binary files /dev/null and b/ev3dev2/fonts/timBI24.pbm differ diff --git a/ev3dev2/fonts/timBI24.pil b/ev3dev2/fonts/timBI24.pil new file mode 100644 index 0000000..6e9bd78 Binary files /dev/null and b/ev3dev2/fonts/timBI24.pil differ diff --git a/ev3dev2/fonts/timI08.pbm b/ev3dev2/fonts/timI08.pbm new file mode 100644 index 0000000..512b576 Binary files /dev/null and b/ev3dev2/fonts/timI08.pbm differ diff --git a/ev3dev2/fonts/timI08.pil b/ev3dev2/fonts/timI08.pil new file mode 100644 index 0000000..f1d9994 Binary files /dev/null and b/ev3dev2/fonts/timI08.pil differ diff --git a/ev3dev2/fonts/timI10.pbm b/ev3dev2/fonts/timI10.pbm new file mode 100644 index 0000000..445f131 Binary files /dev/null and b/ev3dev2/fonts/timI10.pbm differ diff --git a/ev3dev2/fonts/timI10.pil b/ev3dev2/fonts/timI10.pil new file mode 100644 index 0000000..e4133e5 Binary files /dev/null and b/ev3dev2/fonts/timI10.pil differ diff --git a/ev3dev2/fonts/timI12.pbm b/ev3dev2/fonts/timI12.pbm new file mode 100644 index 0000000..a156989 Binary files /dev/null and b/ev3dev2/fonts/timI12.pbm differ diff --git a/ev3dev2/fonts/timI12.pil b/ev3dev2/fonts/timI12.pil new file mode 100644 index 0000000..8fd3c91 Binary files /dev/null and b/ev3dev2/fonts/timI12.pil differ diff --git a/ev3dev2/fonts/timI14.pbm b/ev3dev2/fonts/timI14.pbm new file mode 100644 index 0000000..c9671d3 Binary files /dev/null and b/ev3dev2/fonts/timI14.pbm differ diff --git a/ev3dev2/fonts/timI14.pil b/ev3dev2/fonts/timI14.pil new file mode 100644 index 0000000..d75e226 Binary files /dev/null and b/ev3dev2/fonts/timI14.pil differ diff --git a/ev3dev2/fonts/timI18.pbm b/ev3dev2/fonts/timI18.pbm new file mode 100644 index 0000000..9d3ddae Binary files /dev/null and b/ev3dev2/fonts/timI18.pbm differ diff --git a/ev3dev2/fonts/timI18.pil b/ev3dev2/fonts/timI18.pil new file mode 100644 index 0000000..569650f Binary files /dev/null and b/ev3dev2/fonts/timI18.pil differ diff --git a/ev3dev2/fonts/timI24.pbm b/ev3dev2/fonts/timI24.pbm new file mode 100644 index 0000000..0cf4305 Binary files /dev/null and b/ev3dev2/fonts/timI24.pbm differ diff --git a/ev3dev2/fonts/timI24.pil b/ev3dev2/fonts/timI24.pil new file mode 100644 index 0000000..319b6cb Binary files /dev/null and b/ev3dev2/fonts/timI24.pil differ diff --git a/ev3dev2/fonts/timR08.pbm b/ev3dev2/fonts/timR08.pbm new file mode 100644 index 0000000..d8d099a Binary files /dev/null and b/ev3dev2/fonts/timR08.pbm differ diff --git a/ev3dev2/fonts/timR08.pil b/ev3dev2/fonts/timR08.pil new file mode 100644 index 0000000..920ec80 Binary files /dev/null and b/ev3dev2/fonts/timR08.pil differ diff --git a/ev3dev2/fonts/timR10.pbm b/ev3dev2/fonts/timR10.pbm new file mode 100644 index 0000000..907f636 Binary files /dev/null and b/ev3dev2/fonts/timR10.pbm differ diff --git a/ev3dev2/fonts/timR10.pil b/ev3dev2/fonts/timR10.pil new file mode 100644 index 0000000..6e56229 Binary files /dev/null and b/ev3dev2/fonts/timR10.pil differ diff --git a/ev3dev2/fonts/timR12.pbm b/ev3dev2/fonts/timR12.pbm new file mode 100644 index 0000000..bab6818 Binary files /dev/null and b/ev3dev2/fonts/timR12.pbm differ diff --git a/ev3dev2/fonts/timR12.pil b/ev3dev2/fonts/timR12.pil new file mode 100644 index 0000000..f116b8c Binary files /dev/null and b/ev3dev2/fonts/timR12.pil differ diff --git a/ev3dev2/fonts/timR14.pbm b/ev3dev2/fonts/timR14.pbm new file mode 100644 index 0000000..924d0d3 Binary files /dev/null and b/ev3dev2/fonts/timR14.pbm differ diff --git a/ev3dev2/fonts/timR14.pil b/ev3dev2/fonts/timR14.pil new file mode 100644 index 0000000..f919940 Binary files /dev/null and b/ev3dev2/fonts/timR14.pil differ diff --git a/ev3dev2/fonts/timR18.pbm b/ev3dev2/fonts/timR18.pbm new file mode 100644 index 0000000..7faa6cc Binary files /dev/null and b/ev3dev2/fonts/timR18.pbm differ diff --git a/ev3dev2/fonts/timR18.pil b/ev3dev2/fonts/timR18.pil new file mode 100644 index 0000000..beba4d7 Binary files /dev/null and b/ev3dev2/fonts/timR18.pil differ diff --git a/ev3dev2/fonts/timR24.pbm b/ev3dev2/fonts/timR24.pbm new file mode 100644 index 0000000..dee4bb1 Binary files /dev/null and b/ev3dev2/fonts/timR24.pbm differ diff --git a/ev3dev2/fonts/timR24.pil b/ev3dev2/fonts/timR24.pil new file mode 100644 index 0000000..984044f Binary files /dev/null and b/ev3dev2/fonts/timR24.pil differ 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 22234d4..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,45 +13,37 @@ # # 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 def read_release_version(): try: - f = open("RELEASE-VERSION", "r") - - try: + with open('{}/RELEASE-VERSION'.format(os.path.dirname(__file__)), 'r') as f: version = f.readlines()[0] return version.strip() - - finally: - f.close() - - except: + except Exception: return None def write_release_version(version): - f = open("RELEASE-VERSION", "w") - f.write("%s\n" % version) - f.close() + with open('{}/RELEASE-VERSION'.format(os.path.dirname(__file__)), 'w') as f: + f.write("%s\n" % version) 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 @@ -66,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. @@ -78,6 +72,9 @@ def git_version(abbrev=4): if version != release_version: write_release_version(version) + # 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 d3697c6..6decb29 100644 --- a/setup.py +++ b/setup.py @@ -1,17 +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'], - 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 6825b92..0000000 --- a/spec_version.py +++ /dev/null @@ -1,4 +0,0 @@ -# ~autogen spec_version -spec_version = "spec: 0.9.3-pre-r2, kernel: v3.16.7-ckt16-7-ev3dev-ev3" - -# ~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/generic-class.liquid b/templates/generic-class.liquid deleted file mode 100644 index 2187d36..0000000 --- a/templates/generic-class.liquid +++ /dev/null @@ -1,41 +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 - 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, port=None, name=SYSTEM_DEVICE_NAME_CONVENTION, **kwargs): - if port is not None: - kwargs['port_name'] = port - Device.__init__(self, self.SYSTEM_CLASS_NAME, name,{{ driver_name }} **kwargs) - diff --git a/templates/generic-get-set.liquid b/templates/generic-get-set.liquid deleted file mode 100644 index 05f98d7..0000000 --- a/templates/generic-get-set.liquid +++ /dev/null @@ -1,35 +0,0 @@ -{% assign class_name = currentClass.friendlyName | downcase | underscore_spaces %}{% -for prop in currentClass.systemProperties %}{% - assign prop_name = prop.name | downcase | underscore_spaces %}{% - 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 %} - return self.get_attr_{{ getter }}('{{ prop.systemName }}'){% - else %} - raise Exception("{{prop_name}} is a write-only property!"){% - endif %}{% - if prop.writeAccess %} - - @{{prop_name}}.setter - def {{ prop_name }}(self, value): - self.set_attr_{{ setter }}('{{ prop.systemName }}', value){% -endif%}{% unless forloop.last %} -{% endunless %}{% -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 87078eb..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 90695c7..0000000 --- a/templates/led-colors.liquid +++ /dev/null @@ -1,37 +0,0 @@ -{% for instance in currentClass.instances %}{% - assign instanceName = instance.name | downcase | underscore_spaces %} - {{instanceName}} = Led(name='{{instance.systemName}}'){% -endfor %} - - @staticmethod - def mix_colors({% -for group in currentClass.groups%}{{ group.name | downcase | underscore_spaces }}{% - unless forloop.last %}, {% endunless %}{% -endfor %}):{% -for group in currentClass.groups %}{% - assign groupName = group.name | downcase | underscore_spaces %}{% - for instance in group.entries %}{% - assign instanceName = instance | downcase | underscore_spaces %} - Leds.{{instanceName}}.brightness_pct = {{groupName}}{% - endfor %}{% -endfor %} -{% for color in currentClass.colors %}{% - assign colorName = color.name | downcase | underscore_spaces %} - @staticmethod - def set_{{ colorName }}(pct): - Leds.mix_colors({% - for group in color.groups %}{{ group.name | downcase | underscore_spaces }}={{ group.value }} * pct{% - unless forloop.last %}, {% endunless %}{% - endfor %}) - - @staticmethod - def {{ colorName }}_on(): - Leds.set_{{ colorName }}(1) -{% endfor %} - @staticmethod - def all_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 f63878d..0000000 --- a/templates/motor_commands.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/remote-control.liquid b/templates/remote-control.liquid deleted file mode 100644 index 87a0f48..0000000 --- a/templates/remote-control.liquid +++ /dev/null @@ -1,28 +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 %} - 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 ff395a7..0000000 --- a/templates/spec_version.liquid +++ /dev/null @@ -1 +0,0 @@ -spec_version = "spec: {{ meta.version }}{% if meta.specRevision %}-r{{ meta.specRevision }}{% endif %}, kernel: {{ meta.supportedKernel }}" diff --git a/tests/.gitignore b/tests/.gitignore new file mode 100644 index 0000000..1634661 --- /dev/null +++ b/tests/.gitignore @@ -0,0 +1 @@ +lego-sensor 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 a607111..7ff4533 100755 --- a/tests/api_tests.py +++ b/tests/api_tests.py @@ -1,36 +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__), '..')) -import ev3dev +from populate_arena import populate_arena # noqa: E402 +from clean_arena import clean_arena # noqa: E402 + +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 -ev3dev.Device.DEVICE_ROOT_PATH = os.path.join(os.path.dirname(__file__), 'fake_sys_class') 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): - d = ev3dev.Device('tacho-motor', 'motor*') - self.assertTrue(d.connected) + clean_arena() + populate_arena([('medium_motor', 0, 'outA'), ('infrared_sensor', 0, 'in1')]) + + ev3dev2.Device('tacho-motor', 'motor*') - d = ev3dev.Device('tacho-motor', 'motor0') - self.assertTrue(d.connected) + ev3dev2.Device('tacho-motor', 'motor0') - d = ev3dev.Device('tacho-motor', 'motor*', driver_name='lego-ev3-m-motor') - self.assertTrue(d.connected) + ev3dev2.Device('tacho-motor', 'motor*', driver_name='lego-ev3-m-motor') - d = ev3dev.Device('tacho-motor', 'motor*', port_name='outA') - self.assertTrue(d.connected) + ev3dev2.Device('tacho-motor', 'motor*', address='outA') - d = ev3dev.Device('tacho-motor', 'motor*', port_name='outA', driver_name='not-valid') - self.assertTrue(not d.connected) + with self.assertRaises(ev3dev2.DeviceNotFound): + ev3dev2.Device('tacho-motor', 'motor*', address='outA', driver_name='not-valid') - d = ev3dev.Device('lego-sensor', 'sensor*') - self.assertTrue(d.connected) + with self.assertRaises(ev3dev2.DeviceNotFound): + ev3dev2.Device('tacho-motor', 'motor*', address='this-does-not-exist') + + ev3dev2.Device('lego-sensor', 'sensor*') + + with self.assertRaises(ev3dev2.DeviceNotFound): + ev3dev2.Device('this-does-not-exist') def test_medium_motor(self): - m = ev3dev.MediumMotor() + def dummy(self): + pass + + clean_arena() + populate_arena([('medium_motor', 0, 'outA')]) - self.assertTrue(m.connected); + # Do not write motor.command on exit (so that fake tree stays intact) + MediumMotor.__del__ = dummy + + m = MediumMotor() self.assertEqual(m.device_index, 0) @@ -38,38 +129,380 @@ def test_medium_motor(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.encoder_polarity, 'normal') - self.assertEqual(m.polarity, 'normal') - self.assertEqual(m.port_name, '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_regulation_enabled, 'off') - self.assertEqual(m.speed_sp, 0) - self.assertEqual(m.state, []) - self.assertEqual(m.stop_command, '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): - s = ev3dev.InfraredSensor() + clean_arena() + 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 + time.sleep(toc - time.time()) + + def join(self, timeout=None): + self.done.set() + super(LogThread, self).join(timeout) + + +test = json.loads(open(args.infile).read()) + + +def execute_actions(actions): + for p, c in actions['ports'].items(): + for b in c: + 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) + +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) + + 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 + +for a in test['actions']: + if a['time'] >= 0: + then = start + a['time'] * 1e-3 + while time.time() < then: + pass + execute_actions(a) + +while time.time() < end: + pass + +test['data'] = {} + +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)) diff --git a/tests/motor/motor_info.py b/tests/motor/motor_info.py new file mode 100644 index 0000000..6c9bacc --- /dev/null +++ b/tests/motor/motor_info.py @@ -0,0 +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, + } +} diff --git a/tests/motor/motor_motion_unittest.py b/tests/motor/motor_motion_unittest.py new file mode 100644 index 0000000..9acece4 --- /dev/null +++ b/tests/motor/motor_motion_unittest.py @@ -0,0 +1,145 @@ +#!/usr/bin/env python3 + +# 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 + + +class TestMotorMotion(ptc.ParameterizedTestCase): + @classmethod + def setUpClass(cls): + pass + + @classmethod + def tearDownClass(cls): + pass + + def initialize_motor(self): + self._param['motor'].command = 'reset' + + 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 + + target = self._param['motor'].position + + for i in positions: + self._param['motor'].position_sp = i + if 'run-to-rel-pos' == command: + target += i + else: + target = i + 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)) + 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) + + 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) + + 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) + + 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) + + 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) + + 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) + + 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) + + 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) + + 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) + + 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) + + 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) + + 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) + + +# Add all the tests to the suite - some tests apply only to certain drivers! + + +def AddTachoMotorMotionTestsToSuite(suite, params): + suite.addTest(ptc.ParameterizedTestCase.parameterize(TestMotorMotion, param=params)) + + +if __name__ == '__main__': + 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, ev3_params) + + unittest.TextTestRunner(verbosity=1, buffer=True).run(suite) diff --git a/tests/motor/motor_param_unittest.py b/tests/motor/motor_param_unittest.py new file mode 100644 index 0000000..1c721b9 --- /dev/null +++ b/tests/motor/motor_param_unittest.py @@ -0,0 +1,612 @@ +#!/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 ev3dev.ev3 as ev3 +import parameterizedtestcase as ptc + +from motor_info import motor_info + + +class TestTachoMotorAddressValue(ptc.ParameterizedTestCase): + def test_address_value(self): + self.assertEqual(self._param['motor'].address, self._param['port']) + + def test_address_value_is_read_only(self): + 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): + with self.assertRaises(AttributeError): + self._param['motor'].commands = "ThisShouldNotWork" + + +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']) + + 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): + 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']) + + 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): + 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']) + + 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): + def test_driver_name_value(self): + self.assertEqual(self._param['motor'].driver_name, self._param['driver_name']) + + def test_driver_name_value_is_read_only(self): + with self.assertRaises(AttributeError): + self._param['motor'].driver_name = "ThisShouldNotWork" + + +class TestTachoMotorDutyCycleValue(ptc.ParameterizedTestCase): + def test_duty_cycle_value_is_read_only(self): + 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): + 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): + 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 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 + + 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 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 + + 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 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') + + 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): + 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 = 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): + 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 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 + + 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 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 + + 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 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' + + def test_state_value_after_reset(self): + self._param['motor'].command = 'reset' + self.assertEqual(self._param['motor'].state, []) + + +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): + 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'].action = 'reset' + self.assertEqual(self._param['motor'].stop_action, self._param['stop_actions'][0]) + + +class TestTachoMotorStopActionsValue(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): + 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 = 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 new file mode 100644 index 0000000..3b2482d --- /dev/null +++ b/tests/motor/motor_ramps.json @@ -0,0 +1,25 @@ +{ + "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_action: coast", + "interval": 10, + "name": "run-direct-test", + "max_time": 8000, + "ports": { "outA": { "log_attributes": [ "speed", + "position", + "duty_cycle" ], + "device_class": "Motor" } } + }, + "actions": [ + { "time": -1, "ports": { "outA": [ { "command": "reset" }, + { "position": 0 }, + { "ramp_up_sp": 1000 }, + { "ramp_down_sp": 2000 }, + { "time_sp": 2000 }, + { "speed_sp": 900 } ] } }, + { "time": 0, "ports": { "outA": [ { "command": "run-timed" } ] } }, + { "time": 4000, "ports": { "outA": [ { "speed_sp": -900 }, + { "command": "run-timed" } ] } } + ] +} diff --git a/tests/motor/motor_ramps.log.json b/tests/motor/motor_ramps.log.json new file mode 100644 index 0000000..1684864 --- /dev/null +++ b/tests/motor/motor_ramps.log.json @@ -0,0 +1,3146 @@ +{ + "meta": { + "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_action: coast", + "interval": 10, + "max_time": 8000, + "ports": { + "outA": { + "log_attributes": [ + "speed", + "position", + "duty_cycle" + ], + "device_class": "Motor" + } + } + }, + "data": { + "outA": [ + [ + 0.00013518333435058594, + [ + 0, + 0, + 8 + ] + ], + [ + 0.04027819633483887, + [ + 0, + 1, + 19 + ] + ], + [ + 0.06146502494812012, + [ + 0, + 2, + 25 + ] + ], + [ + 0.08081412315368652, + [ + 0, + 4, + 25 + ] + ], + [ + 0.10143303871154785, + [ + 118, + 8, + 24 + ] + ], + [ + 0.12082600593566895, + [ + 144, + 11, + 25 + ] + ], + [ + 0.14131617546081543, + [ + 152, + 15, + 29 + ] + ], + [ + 0.16076302528381348, + [ + 195, + 19, + 30 + ] + ], + [ + 0.18114113807678223, + [ + 217, + 23, + 32 + ] + ], + [ + 0.20133519172668457, + [ + 194, + 28, + 33 + ] + ], + [ + 0.22158503532409668, + [ + 215, + 33, + 38 + ] + ], + [ + 0.2416090965270996, + [ + 273, + 39, + 39 + ] + ], + [ + 0.26160311698913574, + [ + 298, + 45, + 39 + ] + ], + [ + 0.2807960510253906, + [ + 263, + 51, + 43 + ] + ], + [ + 0.30077314376831055, + [ + 334, + 57, + 45 + ] + ], + [ + 0.33133602142333984, + [ + 303, + 68, + 46 + ] + ], + [ + 0.35071301460266113, + [ + 392, + 76, + 51 + ] + ], + [ + 0.3829371929168701, + [ + 422, + 89, + 51 + ] + ], + [ + 0.4008300304412842, + [ + 404, + 97, + 56 + ] + ], + [ + 0.4338810443878174, + [ + 463, + 113, + 55 + ] + ], + [ + 0.4515411853790283, + [ + 482, + 121, + 61 + ] + ], + [ + 0.47162413597106934, + [ + 503, + 132, + 61 + ] + ], + [ + 0.4914360046386719, + [ + 524, + 142, + 67 + ] + ], + [ + 0.5108420848846436, + [ + 539, + 153, + 67 + ] + ], + [ + 0.5311532020568848, + [ + 562, + 165, + 66 + ] + ], + [ + 0.5512230396270752, + [ + 583, + 177, + 74 + ] + ], + [ + 0.5715811252593994, + [ + 590, + 189, + 74 + ] + ], + [ + 0.591480016708374, + [ + 632, + 202, + 79 + ] + ], + [ + 0.6119110584259033, + [ + 641, + 215, + 80 + ] + ], + [ + 0.6313910484313965, + [ + 669, + 229, + 79 + ] + ], + [ + 0.6512160301208496, + [ + 692, + 243, + 84 + ] + ], + [ + 0.6713430881500244, + [ + 713, + 257, + 84 + ] + ], + [ + 0.6913950443267822, + [ + 733, + 273, + 91 + ] + ], + [ + 0.7123169898986816, + [ + 754, + 289, + 93 + ] + ], + [ + 0.7313201427459717, + [ + 779, + 304, + 92 + ] + ], + [ + 0.7515971660614014, + [ + 778, + 320, + 98 + ] + ], + [ + 0.7716891765594482, + [ + 804, + 337, + 96 + ] + ], + [ + 0.7908751964569092, + [ + 827, + 355, + 100 + ] + ], + [ + 0.8114039897918701, + [ + 873, + 373, + 100 + ] + ], + [ + 0.8313910961151123, + [ + 875, + 390, + 100 + ] + ], + [ + 0.8511860370635986, + [ + 894, + 409, + 99 + ] + ], + [ + 0.8714900016784668, + [ + 908, + 427, + 97 + ] + ], + [ + 0.8907861709594727, + [ + 915, + 445, + 96 + ] + ], + [ + 0.9104740619659424, + [ + 915, + 464, + 95 + ] + ], + [ + 0.930840015411377, + [ + 913, + 482, + 94 + ] + ], + [ + 0.9511921405792236, + [ + 908, + 499, + 94 + ] + ], + [ + 0.9713881015777588, + [ + 902, + 518, + 95 + ] + ], + [ + 0.9908061027526855, + [ + 898, + 535, + 95 + ] + ], + [ + 1.0116791725158691, + [ + 893, + 554, + 96 + ] + ], + [ + 1.0314559936523438, + [ + 892, + 571, + 96 + ] + ], + [ + 1.0505871772766113, + [ + 894, + 589, + 97 + ] + ], + [ + 1.0713601112365723, + [ + 896, + 607, + 97 + ] + ], + [ + 1.0915980339050293, + [ + 897, + 625, + 97 + ] + ], + [ + 1.1108200550079346, + [ + 901, + 643, + 96 + ] + ], + [ + 1.1318080425262451, + [ + 902, + 662, + 96 + ] + ], + [ + 1.1515791416168213, + [ + 905, + 679, + 96 + ] + ], + [ + 1.1716489791870117, + [ + 903, + 697, + 96 + ] + ], + [ + 1.1912081241607666, + [ + 902, + 715, + 96 + ] + ], + [ + 1.2116401195526123, + [ + 900, + 733, + 96 + ] + ], + [ + 1.2413690090179443, + [ + 897, + 760, + 96 + ] + ], + [ + 1.2615861892700195, + [ + 898, + 778, + 96 + ] + ], + [ + 1.2931079864501953, + [ + 896, + 806, + 97 + ] + ], + [ + 1.3116600513458252, + [ + 897, + 823, + 97 + ] + ], + [ + 1.3427700996398926, + [ + 899, + 851, + 97 + ] + ], + [ + 1.3614530563354492, + [ + 901, + 868, + 97 + ] + ], + [ + 1.3815879821777344, + [ + 902, + 886, + 97 + ] + ], + [ + 1.4012200832366943, + [ + 902, + 904, + 97 + ] + ], + [ + 1.4217309951782227, + [ + 902, + 922, + 96 + ] + ], + [ + 1.4415371417999268, + [ + 900, + 940, + 96 + ] + ], + [ + 1.4611961841583252, + [ + 899, + 958, + 97 + ] + ], + [ + 1.4813330173492432, + [ + 898, + 976, + 97 + ] + ], + [ + 1.5015990734100342, + [ + 899, + 994, + 97 + ] + ], + [ + 1.5219101905822754, + [ + 899, + 1013, + 97 + ] + ], + [ + 1.5408201217651367, + [ + 901, + 1030, + 96 + ] + ], + [ + 1.5608291625976562, + [ + 903, + 1048, + 96 + ] + ], + [ + 1.5814220905303955, + [ + 904, + 1066, + 96 + ] + ], + [ + 1.6007511615753174, + [ + 902, + 1084, + 96 + ] + ], + [ + 1.621718168258667, + [ + 900, + 1103, + 96 + ] + ], + [ + 1.6416480541229248, + [ + 898, + 1120, + 96 + ] + ], + [ + 1.661363124847412, + [ + 896, + 1138, + 97 + ] + ], + [ + 1.6816210746765137, + [ + 897, + 1156, + 97 + ] + ], + [ + 1.7012240886688232, + [ + 898, + 1174, + 97 + ] + ], + [ + 1.7215180397033691, + [ + 898, + 1193, + 97 + ] + ], + [ + 1.741178035736084, + [ + 901, + 1210, + 97 + ] + ], + [ + 1.7607932090759277, + [ + 902, + 1228, + 96 + ] + ], + [ + 1.780851125717163, + [ + 903, + 1246, + 96 + ] + ], + [ + 1.8015460968017578, + [ + 902, + 1264, + 96 + ] + ], + [ + 1.8208889961242676, + [ + 901, + 1282, + 96 + ] + ], + [ + 1.8407981395721436, + [ + 899, + 1300, + 96 + ] + ], + [ + 1.8611969947814941, + [ + 898, + 1318, + 97 + ] + ], + [ + 1.8814051151275635, + [ + 899, + 1336, + 97 + ] + ], + [ + 1.9015750885009766, + [ + 899, + 1356, + 97 + ] + ], + [ + 1.9216539859771729, + [ + 900, + 1373, + 96 + ] + ], + [ + 1.9601070880889893, + [ + 904, + 1408, + 96 + ] + ], + [ + 1.9815161228179932, + [ + 902, + 1427, + 96 + ] + ], + [ + 2.0013790130615234, + [ + 901, + 1451, + 91 + ] + ], + [ + 2.0413761138916016, + [ + 890, + 1480, + 88 + ] + ], + [ + 2.0605549812316895, + [ + 883, + 1503, + 86 + ] + ], + [ + 2.0917160511016846, + [ + 849, + 1523, + 84 + ] + ], + [ + 2.11147403717041, + [ + 849, + 1539, + 85 + ] + ], + [ + 2.1322569847106934, + [ + 834, + 1556, + 86 + ] + ], + [ + 2.151508092880249, + [ + 824, + 1571, + 83 + ] + ], + [ + 2.171658992767334, + [ + 813, + 1588, + 83 + ] + ], + [ + 2.1913931369781494, + [ + 793, + 1603, + 81 + ] + ], + [ + 2.2108120918273926, + [ + 785, + 1619, + 81 + ] + ], + [ + 2.2316691875457764, + [ + 772, + 1635, + 80 + ] + ], + [ + 2.251427173614502, + [ + 774, + 1649, + 78 + ] + ], + [ + 2.271404981613159, + [ + 756, + 1664, + 78 + ] + ], + [ + 2.2916781902313232, + [ + 745, + 1679, + 75 + ] + ], + [ + 2.3114349842071533, + [ + 736, + 1693, + 75 + ] + ], + [ + 2.3313751220703125, + [ + 725, + 1709, + 75 + ] + ], + [ + 2.3515751361846924, + [ + 716, + 1722, + 72 + ] + ], + [ + 2.3716001510620117, + [ + 707, + 1736, + 71 + ] + ], + [ + 2.3914411067962646, + [ + 697, + 1750, + 69 + ] + ], + [ + 2.4116740226745605, + [ + 686, + 1764, + 68 + ] + ], + [ + 2.4316179752349854, + [ + 674, + 1777, + 69 + ] + ], + [ + 2.4515221118927, + [ + 662, + 1790, + 66 + ] + ], + [ + 2.4715380668640137, + [ + 649, + 1803, + 66 + ] + ], + [ + 2.490838050842285, + [ + 630, + 1815, + 64 + ] + ], + [ + 2.5116231441497803, + [ + 627, + 1828, + 65 + ] + ], + [ + 2.531548023223877, + [ + 615, + 1840, + 64 + ] + ], + [ + 2.561472177505493, + [ + 604, + 1858, + 61 + ] + ], + [ + 2.581374168395996, + [ + 590, + 1870, + 62 + ] + ], + [ + 2.6060361862182617, + [ + 581, + 1883, + 59 + ] + ], + [ + 2.6316521167755127, + [ + 564, + 1898, + 60 + ] + ], + [ + 2.656917095184326, + [ + 557, + 1912, + 56 + ] + ], + [ + 2.6808249950408936, + [ + 547, + 1926, + 56 + ] + ], + [ + 2.700824022293091, + [ + 535, + 1936, + 53 + ] + ], + [ + 2.721391201019287, + [ + 521, + 1946, + 53 + ] + ], + [ + 2.7416110038757324, + [ + 509, + 1957, + 51 + ] + ], + [ + 2.7616310119628906, + [ + 498, + 1966, + 51 + ] + ], + [ + 2.7813642024993896, + [ + 487, + 1976, + 51 + ] + ], + [ + 2.8015360832214355, + [ + 476, + 1985, + 49 + ] + ], + [ + 2.821135997772217, + [ + 441, + 1995, + 48 + ] + ], + [ + 2.8421711921691895, + [ + 461, + 2004, + 48 + ] + ], + [ + 2.861478090286255, + [ + 451, + 2013, + 48 + ] + ], + [ + 2.8808372020721436, + [ + 417, + 2021, + 45 + ] + ], + [ + 2.9016151428222656, + [ + 430, + 2030, + 43 + ] + ], + [ + 2.9215970039367676, + [ + 419, + 2038, + 43 + ] + ], + [ + 2.941148042678833, + [ + 411, + 2047, + 42 + ] + ], + [ + 2.9614100456237793, + [ + 391, + 2054, + 41 + ] + ], + [ + 2.981394052505493, + [ + 386, + 2062, + 42 + ] + ], + [ + 3.001209020614624, + [ + 383, + 2070, + 46 + ] + ], + [ + 3.0211341381073, + [ + 304, + 2077, + 39 + ] + ], + [ + 3.041372060775757, + [ + 364, + 2085, + 42 + ] + ], + [ + 3.061551094055176, + [ + 359, + 2092, + 36 + ] + ], + [ + 3.0811820030212402, + [ + 345, + 2099, + 43 + ] + ], + [ + 3.100750207901001, + [ + 288, + 2106, + 38 + ] + ], + [ + 3.1215920448303223, + [ + 329, + 2113, + 33 + ] + ], + [ + 3.1408510208129883, + [ + 262, + 2119, + 31 + ] + ], + [ + 3.1617441177368164, + [ + 265, + 2125, + 36 + ] + ], + [ + 3.181612014770508, + [ + 294, + 2131, + 36 + ] + ], + [ + 3.2015581130981445, + [ + 288, + 2137, + 28 + ] + ], + [ + 3.2214581966400146, + [ + 224, + 2142, + 28 + ] + ], + [ + 3.2415060997009277, + [ + 268, + 2147, + 25 + ] + ], + [ + 3.271630048751831, + [ + 209, + 2155, + 30 + ] + ], + [ + 3.291457176208496, + [ + 206, + 2160, + 26 + ] + ], + [ + 3.32293701171875, + [ + 183, + 2167, + 23 + ] + ], + [ + 3.340757131576538, + [ + 215, + 2171, + 23 + ] + ], + [ + 3.361647129058838, + [ + 169, + 2175, + 23 + ] + ], + [ + 3.3815550804138184, + [ + 164, + 2179, + 22 + ] + ], + [ + 3.4013421535491943, + [ + 167, + 2182, + 16 + ] + ], + [ + 3.4207370281219482, + [ + 152, + 2186, + 18 + ] + ], + [ + 3.4407761096954346, + [ + 137, + 2189, + 16 + ] + ], + [ + 3.4615700244903564, + [ + 121, + 2192, + 15 + ] + ], + [ + 3.4814071655273438, + [ + 121, + 2194, + 17 + ] + ], + [ + 3.501552104949951, + [ + 103, + 2197, + 12 + ] + ], + [ + 3.521136999130249, + [ + 113, + 2199, + 12 + ] + ], + [ + 3.5415561199188232, + [ + 86, + 2201, + 9 + ] + ], + [ + 3.5607290267944336, + [ + 85, + 2202, + 10 + ] + ], + [ + 3.5816121101379395, + [ + 66, + 2204, + 9 + ] + ], + [ + 3.6022980213165283, + [ + 67, + 2205, + 5 + ] + ], + [ + 3.6213650703430176, + [ + 50, + 2206, + 5 + ] + ], + [ + 3.641601085662842, + [ + 45, + 2206, + 2 + ] + ], + [ + 3.660835027694702, + [ + 37, + 2206, + 1 + ] + ], + [ + 3.68133807182312, + [ + 31, + 2206, + 0 + ] + ], + [ + 3.7016379833221436, + [ + 27, + 2206, + 0 + ] + ], + [ + 3.721386194229126, + [ + 23, + 2206, + 0 + ] + ], + [ + 3.7415170669555664, + [ + 0, + 2206, + 0 + ] + ], + [ + 3.7614572048187256, + [ + 0, + 2206, + 0 + ] + ], + [ + 3.7814810276031494, + [ + 0, + 2206, + 0 + ] + ], + [ + 3.801488161087036, + [ + 0, + 2206, + 0 + ] + ], + [ + 3.821394205093384, + [ + 0, + 2206, + 0 + ] + ], + [ + 3.8414101600646973, + [ + 0, + 2206, + 0 + ] + ], + [ + 3.86076021194458, + [ + 0, + 2206, + 0 + ] + ], + [ + 3.8813350200653076, + [ + 0, + 2206, + 0 + ] + ], + [ + 3.9013559818267822, + [ + 0, + 2206, + 0 + ] + ], + [ + 3.9215750694274902, + [ + 0, + 2206, + 0 + ] + ], + [ + 3.9405581951141357, + [ + 0, + 2206, + 0 + ] + ], + [ + 3.961583137512207, + [ + 0, + 2206, + 0 + ] + ], + [ + 3.9811320304870605, + [ + 0, + 2206, + 0 + ] + ], + [ + 4.000523090362549, + [ + 0, + 2206, + 0 + ] + ], + [ + 4.02121901512146, + [ + 0, + 2206, + 0 + ] + ], + [ + 4.041488170623779, + [ + 0, + 2206, + 0 + ] + ], + [ + 4.061634063720703, + [ + 0, + 2206, + -7 + ] + ], + [ + 4.0813751220703125, + [ + 0, + 2206, + -10 + ] + ], + [ + 4.101608037948608, + [ + -7, + 2205, + -18 + ] + ], + [ + 4.133968114852905, + [ + -7, + 2202, + -28 + ] + ], + [ + 4.151486158370972, + [ + -110, + 2200, + -22 + ] + ], + [ + 4.1712141036987305, + [ + -135, + 2196, + -22 + ] + ], + [ + 4.1913840770721436, + [ + -174, + 2193, + -24 + ] + ], + [ + 4.211337089538574, + [ + -147, + 2189, + -30 + ] + ], + [ + 4.2314300537109375, + [ + -170, + 2185, + -28 + ] + ], + [ + 4.251475095748901, + [ + -176, + 2181, + -37 + ] + ], + [ + 4.271409034729004, + [ + -208, + 2176, + -35 + ] + ], + [ + 4.291422128677368, + [ + -225, + 2171, + -38 + ] + ], + [ + 4.3118181228637695, + [ + -284, + 2165, + -40 + ] + ], + [ + 4.331489086151123, + [ + -305, + 2159, + -44 + ] + ], + [ + 4.350754976272583, + [ + -320, + 2153, + -45 + ] + ], + [ + 4.371709108352661, + [ + -337, + 2146, + -45 + ] + ], + [ + 4.391467094421387, + [ + -304, + 2139, + -45 + ] + ], + [ + 4.411620140075684, + [ + -382, + 2131, + -60 + ] + ], + [ + 4.431546211242676, + [ + -408, + 2123, + -51 + ] + ], + [ + 4.451472997665405, + [ + -422, + 2114, + -57 + ] + ], + [ + 4.471180200576782, + [ + -443, + 2105, + -57 + ] + ], + [ + 4.490777015686035, + [ + -461, + 2095, + -56 + ] + ], + [ + 4.511502027511597, + [ + -486, + 2085, + -61 + ] + ], + [ + 4.531397104263306, + [ + -503, + 2075, + -64 + ] + ], + [ + 4.551336050033569, + [ + -522, + 2065, + -66 + ] + ], + [ + 4.571191072463989, + [ + -539, + 2054, + -69 + ] + ], + [ + 4.591391086578369, + [ + -558, + 2042, + -67 + ] + ], + [ + 4.61155104637146, + [ + -566, + 2030, + -75 + ] + ], + [ + 4.631395101547241, + [ + -608, + 2018, + -75 + ] + ], + [ + 4.651485204696655, + [ + -618, + 2005, + -80 + ] + ], + [ + 4.671399116516113, + [ + -645, + 1992, + -80 + ] + ], + [ + 4.6915810108184814, + [ + -671, + 1978, + -79 + ] + ], + [ + 4.711333990097046, + [ + -692, + 1964, + -85 + ] + ], + [ + 4.743326187133789, + [ + -722, + 1941, + -85 + ] + ], + [ + 4.761660099029541, + [ + -742, + 1927, + -90 + ] + ], + [ + 4.7929511070251465, + [ + -781, + 1902, + -90 + ] + ], + [ + 4.811446189880371, + [ + -778, + 1887, + -98 + ] + ], + [ + 4.830804109573364, + [ + -799, + 1870, + -99 + ] + ], + [ + 4.851659059524536, + [ + -824, + 1852, + -100 + ] + ], + [ + 4.871709108352661, + [ + -845, + 1835, + -100 + ] + ], + [ + 4.891830205917358, + [ + -867, + 1817, + -100 + ] + ], + [ + 4.911573171615601, + [ + -884, + 1799, + -99 + ] + ], + [ + 4.931425094604492, + [ + -899, + 1781, + -98 + ] + ], + [ + 4.9515180587768555, + [ + -907, + 1763, + -97 + ] + ], + [ + 4.9717631340026855, + [ + -910, + 1744, + -97 + ] + ], + [ + 4.991427183151245, + [ + -909, + 1726, + -96 + ] + ], + [ + 5.0115931034088135, + [ + -907, + 1708, + -96 + ] + ], + [ + 5.031227111816406, + [ + -905, + 1690, + -96 + ] + ], + [ + 5.051515102386475, + [ + -900, + 1672, + -96 + ] + ], + [ + 5.072525978088379, + [ + -895, + 1654, + -97 + ] + ], + [ + 5.091385126113892, + [ + -893, + 1637, + -97 + ] + ], + [ + 5.111462116241455, + [ + -893, + 1619, + -98 + ] + ], + [ + 5.130731105804443, + [ + -894, + 1601, + -98 + ] + ], + [ + 5.151494979858398, + [ + -896, + 1583, + -98 + ] + ], + [ + 5.171610116958618, + [ + -895, + 1564, + -98 + ] + ], + [ + 5.19083309173584, + [ + -899, + 1547, + -98 + ] + ], + [ + 5.21076512336731, + [ + -900, + 1529, + -98 + ] + ], + [ + 5.230787038803101, + [ + -900, + 1511, + -98 + ] + ], + [ + 5.251658201217651, + [ + -899, + 1493, + -98 + ] + ], + [ + 5.270776987075806, + [ + -899, + 1467, + -98 + ] + ], + [ + 5.3013811111450195, + [ + -900, + 1448, + -98 + ] + ], + [ + 5.321514129638672, + [ + -901, + 1430, + -98 + ] + ], + [ + 5.340814113616943, + [ + -902, + 1412, + -98 + ] + ], + [ + 5.361563205718994, + [ + -900, + 1394, + -98 + ] + ], + [ + 5.381470203399658, + [ + -902, + 1376, + -98 + ] + ], + [ + 5.401555061340332, + [ + -903, + 1357, + -97 + ] + ], + [ + 5.420812129974365, + [ + -903, + 1340, + -97 + ] + ], + [ + 5.451624155044556, + [ + -901, + 1312, + -97 + ] + ], + [ + 5.470808029174805, + [ + -899, + 1295, + -98 + ] + ], + [ + 5.501665115356445, + [ + -898, + 1267, + -98 + ] + ], + [ + 5.5214221477508545, + [ + -898, + 1250, + -98 + ] + ], + [ + 5.553164005279541, + [ + -897, + 1221, + -98 + ] + ], + [ + 5.571439981460571, + [ + -899, + 1205, + -98 + ] + ], + [ + 5.590807199478149, + [ + -901, + 1187, + -98 + ] + ], + [ + 5.611687183380127, + [ + -901, + 1168, + -97 + ] + ], + [ + 5.6316821575164795, + [ + -902, + 1150, + -98 + ] + ], + [ + 5.651430130004883, + [ + -899, + 1133, + -98 + ] + ], + [ + 5.671199083328247, + [ + -897, + 1115, + -98 + ] + ], + [ + 5.691615104675293, + [ + -896, + 1097, + -98 + ] + ], + [ + 5.7105631828308105, + [ + -897, + 1073, + -98 + ] + ], + [ + 5.731765031814575, + [ + -900, + 1060, + -98 + ] + ], + [ + 5.750833988189697, + [ + -901, + 1043, + -98 + ] + ], + [ + 5.771650075912476, + [ + -901, + 1024, + -98 + ] + ], + [ + 5.790861129760742, + [ + -903, + 1006, + -98 + ] + ], + [ + 5.811756134033203, + [ + -904, + 988, + -97 + ] + ], + [ + 5.832771062850952, + [ + -904, + 969, + -97 + ] + ], + [ + 5.851535081863403, + [ + -904, + 952, + -97 + ] + ], + [ + 5.871742010116577, + [ + -902, + 934, + -97 + ] + ], + [ + 5.891646146774292, + [ + -901, + 916, + -97 + ] + ], + [ + 5.911404132843018, + [ + -900, + 898, + -97 + ] + ], + [ + 5.931385040283203, + [ + -901, + 880, + -97 + ] + ], + [ + 5.951478004455566, + [ + -899, + 862, + -97 + ] + ], + [ + 5.971441984176636, + [ + -897, + 844, + -97 + ] + ], + [ + 5.990740060806274, + [ + -897, + 826, + -98 + ] + ], + [ + 6.011535167694092, + [ + -898, + 808, + -94 + ] + ], + [ + 6.031505107879639, + [ + -896, + 790, + -93 + ] + ], + [ + 6.051656007766724, + [ + -891, + 773, + -89 + ] + ], + [ + 6.071416139602661, + [ + -882, + 756, + -88 + ] + ], + [ + 6.091546058654785, + [ + -871, + 739, + -88 + ] + ], + [ + 6.111616134643555, + [ + -860, + 722, + -84 + ] + ], + [ + 6.131525993347168, + [ + -837, + 704, + -84 + ] + ], + [ + 6.150875091552734, + [ + -837, + 689, + -82 + ] + ], + [ + 6.171428203582764, + [ + -822, + 673, + -82 + ] + ], + [ + 6.191707134246826, + [ + -810, + 657, + -83 + ] + ], + [ + 6.211153984069824, + [ + -801, + 641, + -80 + ] + ], + [ + 6.23157000541687, + [ + -783, + 626, + -79 + ] + ], + [ + 6.250800132751465, + [ + -772, + 611, + -77 + ] + ], + [ + 6.271198034286499, + [ + -760, + 596, + -77 + ] + ], + [ + 6.291377067565918, + [ + -747, + 581, + -78 + ] + ], + [ + 6.311171054840088, + [ + -741, + 566, + -74 + ] + ], + [ + 6.331611156463623, + [ + -733, + 551, + -74 + ] + ], + [ + 6.360791206359863, + [ + -719, + 530, + -71 + ] + ], + [ + 6.381644010543823, + [ + -705, + 516, + -71 + ] + ], + [ + 6.4129111766815186, + [ + -685, + 495, + -69 + ] + ], + [ + 6.431450128555298, + [ + -675, + 483, + -69 + ] + ], + [ + 6.4514241218566895, + [ + -668, + 469, + -67 + ] + ], + [ + 6.471430063247681, + [ + -652, + 456, + -66 + ] + ], + [ + 6.4914491176605225, + [ + -649, + 443, + -66 + ] + ], + [ + 6.511454105377197, + [ + -637, + 431, + -65 + ] + ], + [ + 6.5307841300964355, + [ + -617, + 419, + -65 + ] + ], + [ + 6.551218032836914, + [ + -604, + 407, + -63 + ] + ], + [ + 6.571604013442993, + [ + -600, + 395, + -62 + ] + ], + [ + 6.591418027877808, + [ + -599, + 383, + -62 + ] + ], + [ + 6.61078405380249, + [ + -588, + 371, + -58 + ] + ], + [ + 6.631582021713257, + [ + -583, + 360, + -58 + ] + ], + [ + 6.651499032974243, + [ + -563, + 348, + -56 + ] + ], + [ + 6.671603202819824, + [ + -549, + 338, + -56 + ] + ], + [ + 6.690822124481201, + [ + -540, + 327, + -57 + ] + ], + [ + 6.711470127105713, + [ + -530, + 316, + -54 + ] + ], + [ + 6.7314770221710205, + [ + -523, + 306, + -53 + ] + ], + [ + 6.751376152038574, + [ + -514, + 296, + -50 + ] + ], + [ + 6.770539999008179, + [ + -509, + 286, + -50 + ] + ], + [ + 6.7912070751190186, + [ + -493, + 276, + -50 + ] + ], + [ + 6.810803174972534, + [ + -483, + 267, + -47 + ] + ], + [ + 6.831449031829834, + [ + -440, + 257, + -48 + ] + ], + [ + 6.851155042648315, + [ + -453, + 248, + -46 + ] + ], + [ + 6.87148118019104, + [ + -442, + 240, + -46 + ] + ], + [ + 6.8914101123809814, + [ + -435, + 231, + -46 + ] + ], + [ + 6.911706209182739, + [ + -429, + 222, + -43 + ] + ], + [ + 6.931344985961914, + [ + -412, + 214, + -44 + ] + ], + [ + 6.951891183853149, + [ + -416, + 206, + -41 + ] + ], + [ + 6.981703996658325, + [ + -395, + 194, + -40 + ] + ], + [ + 7.001948118209839, + [ + -385, + 186, + -45 + ] + ], + [ + 7.021485090255737, + [ + -302, + 178, + -38 + ] + ], + [ + 7.041447162628174, + [ + -365, + 168, + -40 + ] + ], + [ + 7.072902202606201, + [ + -293, + 160, + -37 + ] + ], + [ + 7.095211982727051, + [ + -292, + 152, + -36 + ] + ], + [ + 7.120098114013672, + [ + -338, + 143, + -33 + ] + ], + [ + 7.15159797668457, + [ + -325, + 133, + -39 + ] + ], + [ + 7.17136812210083, + [ + -257, + 127, + -29 + ] + ], + [ + 7.190798044204712, + [ + -306, + 121, + -30 + ] + ], + [ + 7.21083402633667, + [ + -297, + 115, + -33 + ] + ], + [ + 7.231142044067383, + [ + -273, + 109, + -27 + ] + ], + [ + 7.251557111740112, + [ + -223, + 104, + -28 + ] + ], + [ + 7.271229982376099, + [ + -210, + 99, + -29 + ] + ], + [ + 7.291424036026001, + [ + -200, + 94, + -28 + ] + ], + [ + 7.310808181762695, + [ + -234, + 89, + -25 + ] + ], + [ + 7.331562042236328, + [ + -219, + 85, + -27 + ] + ], + [ + 7.351387023925781, + [ + -205, + 81, + -27 + ] + ], + [ + 7.371474981307983, + [ + -196, + 77, + -24 + ] + ], + [ + 7.391128063201904, + [ + -159, + 73, + -23 + ] + ], + [ + 7.411434173583984, + [ + -163, + 70, + -18 + ] + ], + [ + 7.430778980255127, + [ + -151, + 66, + -19 + ] + ], + [ + 7.4515540599823, + [ + -129, + 63, + -20 + ] + ], + [ + 7.475989103317261, + [ + -131, + 60, + -15 + ] + ], + [ + 7.500794172286987, + [ + -115, + 56, + -13 + ] + ], + [ + 7.521330118179321, + [ + -113, + 54, + -13 + ] + ], + [ + 7.541465997695923, + [ + -99, + 52, + -13 + ] + ], + [ + 7.5613579750061035, + [ + -92, + 50, + -9 + ] + ], + [ + 7.5814220905303955, + [ + -73, + 48, + -8 + ] + ], + [ + 7.60143518447876, + [ + -73, + 47, + -6 + ] + ], + [ + 7.6214001178741455, + [ + -63, + 46, + -5 + ] + ], + [ + 7.641455173492432, + [ + -48, + 45, + -4 + ] + ], + [ + 7.661764144897461, + [ + -43, + 45, + 0 + ] + ], + [ + 7.681565046310425, + [ + -35, + 45, + 0 + ] + ], + [ + 7.7015461921691895, + [ + -30, + 45, + 0 + ] + ], + [ + 7.721444129943848, + [ + -26, + 45, + 0 + ] + ], + [ + 7.7414870262146, + [ + -23, + 45, + 0 + ] + ], + [ + 7.762083053588867, + [ + 0, + 45, + 0 + ] + ], + [ + 7.781381130218506, + [ + 0, + 45, + 0 + ] + ], + [ + 7.800822019577026, + [ + 0, + 45, + 0 + ] + ], + [ + 7.821552991867065, + [ + 0, + 45, + 0 + ] + ], + [ + 7.841476202011108, + [ + 0, + 45, + 0 + ] + ], + [ + 7.861203193664551, + [ + 0, + 45, + 0 + ] + ], + [ + 7.8813982009887695, + [ + 0, + 45, + 0 + ] + ], + [ + 7.901153087615967, + [ + 0, + 45, + 0 + ] + ], + [ + 7.921393156051636, + [ + 0, + 45, + 0 + ] + ], + [ + 7.941126108169556, + [ + 0, + 45, + 0 + ] + ], + [ + 7.960553169250488, + [ + 0, + 45, + 0 + ] + ], + [ + 7.9814441204071045, + [ + 0, + 45, + 0 + ] + ] + ] + }, + "actions": [ + { + "ports": { + "outA": [ + { + "command": "reset" + }, + { + "position": 0 + }, + { + "ramp_up_sp": 1000 + }, + { + "ramp_down_sp": 2000 + }, + { + "time_sp": 2000 + }, + { + "speed_sp": 900 + } + ] + }, + "time": -1 + }, + { + "ports": { + "outA": [ + { + "command": "run-timed" + } + ] + }, + "time": 0 + }, + { + "ports": { + "outA": [ + { + "speed_sp": -900 + }, + { + "command": "run-timed" + } + ] + }, + "time": 4000 + } + ] +} diff --git a/tests/motor/motor_ramps.log.json-outA.png b/tests/motor/motor_ramps.log.json-outA.png new file mode 100644 index 0000000..a14ad0d Binary files /dev/null and b/tests/motor/motor_ramps.log.json-outA.png differ diff --git a/tests/motor/motor_run_direct_unittest.py b/tests/motor/motor_run_direct_unittest.py new file mode 100755 index 0000000..4ae42a9 --- /dev/null +++ b/tests/motor/motor_run_direct_unittest.py @@ -0,0 +1,54 @@ +#!/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 + + +class TestMotorRunDirect(ptc.ParameterizedTestCase): + @classmethod + def setUpClass(cls): + pass + + @classmethod + def tearDownClass(cls): + pass + + def initialize_motor(self): + self._param['motor'].command = 'reset' + + 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 + 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]) + + +# Add all the tests to the suite - some tests apply only to certain drivers! + + +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'} + + suite = unittest.TestSuite() + + AddTachoMotorRunDirectTestsToSuite(suite, 'lego-ev3-l-motor', params) + + 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 new file mode 100644 index 0000000..bdca528 --- /dev/null +++ b/tests/motor/parameterizedtestcase.py @@ -0,0 +1,22 @@ +import unittest + + +class ParameterizedTestCase(unittest.TestCase): + """ TestCase classes that want to be parametrized should + inherit from this class. + """ + def __init__(self, methodName='runTest', param=None): + super(ParameterizedTestCase, self).__init__(methodName) + self._param = param + + @staticmethod + def parameterize(testcase_class, param=None): + """ Create a suite containing all tests taken from the given + subclass, passing them the parameter 'param'. + """ + testloader = unittest.TestLoader() + testnames = testloader.getTestCaseNames(testcase_class) + suite = unittest.TestSuite() + for name in testnames: + suite.addTest(testcase_class(name, param=param)) + return suite diff --git a/tests/motor/plot_matplotlib.py b/tests/motor/plot_matplotlib.py new file mode 100644 index 0000000..c7a3725 --- /dev/null +++ b/tests/motor/plot_matplotlib.py @@ -0,0 +1,90 @@ +import matplotlib.pyplot as plt +import json +import argparse + +parser = argparse.ArgumentParser(description='Plot ev3dev datalogs.') +parser.add_argument('infile', help='the input file to be logged') + +args = parser.parse_args() + +# Note, this code is a modified version from these pages: +# +# http://www.randalolson.com/2014/06/28/how-to-make-beautiful-data-visualizations-in-python-with-matplotlib/ +# 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)] + +# Scale the RGB values to the [0, 1] range, which is the format matplotlib accepts. +for i in range(len(tableau20)): + r, g, b = tableau20[i] + tableau20[i] = (r / 255., g / 255., b / 255.) + +plt.style.use(['dark_background']) + +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(): + 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]}) + + 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') + + # Clean up the chartjunk + for i, ax in enumerate(axarr): + print(i, ax) + # Remove the plot frame lines. They are unnecessary chartjunk. + ax.spines["top"].set_visible(False) + + # Ensure that the axis ticks only show up on the bottom and left of the plot. + # Ticks on the right and top of the plot are generally unnecessary chartjunk. + 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']), + fontsize=14, + color=tableau20[i], + horizontalalignment='right', + verticalalignment='center', + transform=axarr[i].transAxes) + + 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 new file mode 100755 index 0000000..bfafd45 --- /dev/null +++ b/utils/move_motor.py @@ -0,0 +1,36 @@ +#!/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 ev3dev2.motor import OUTPUT_A, OUTPUT_B, OUTPUT_C, OUTPUT_D, Motor +import argparse +import logging + +# command line args +parser = argparse.ArgumentParser(description="Used to adjust the position of a motor in an already assembled robot") +parser.add_argument("motor", type=str, help="A, B, C or D") +parser.add_argument("degrees", type=int) +parser.add_argument("-s", "--speed", type=int, default=50) +args = parser.parse_args() + +# logging +logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)5s: %(message)s") +log = logging.getLogger(__name__) + +if args.motor == "A": + motor = Motor(OUTPUT_A) +elif args.motor == "B": + motor = Motor(OUTPUT_B) +elif args.motor == "C": + motor = Motor(OUTPUT_C) +elif args.motor == "D": + motor = Motor(OUTPUT_D) +else: + raise Exception("%s is invalid, options are A, B, C, D") + +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') diff --git a/utils/stop_all_motors.py b/utils/stop_all_motors.py new file mode 100755 index 0000000..e040146 --- /dev/null +++ b/utils/stop_all_motors.py @@ -0,0 +1,8 @@ +#!/usr/bin/env micropython +""" +Stop all motors +""" +from ev3dev2.motor import list_motors + +for motor in list_motors(): + motor.stop(stop_action='brake')