diff --git a/.coveragerc b/.coveragerc index 6ec3f28..159c8db 100644 --- a/.coveragerc +++ b/.coveragerc @@ -4,3 +4,13 @@ source = pymetawear include = */pymetawear/* omit = */setup.py + */MetaWear-CppAPI/* + +[report] +exclude_lines = + except ImportError: + def add_stream_logger + if debug: + raise NotImplementedError + pass + def _log diff --git a/.travis.yml b/.travis.yml index ec32461..8f6ae9d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,14 +2,15 @@ language: python sudo: required dist: trusty python: - - 2.7.11 + - 2.7 - 3.4 - 3.5 + - 3.6 - "nightly" matrix: allow_failures: - python: 3.4 - - python: 3.5 + - python: 3.6 - python: "nightly" branches: only: @@ -48,4 +49,4 @@ deploy: script: bash ./deploy_gh_pages.sh on: branch: master - python: 2.7.11 + python: 2.7 diff --git a/AUTHORS.rst b/AUTHORS.rst new file mode 100644 index 0000000..ebcb1b5 --- /dev/null +++ b/AUTHORS.rst @@ -0,0 +1,22 @@ +======= +Credits +======= + +Development Lead +---------------- + +* Henrik Blidh + +Contributors (sorted alphabetically) +------------------------------------ + +* `Jonas Böer `_ (Kinemic GmbH) + - Magnetometer module + +* `Thibaud Mathieu `_ + - Raw example for temperature module + +* `Eric Tsai `_ (mbientlab) + - 64-bit datasignal address handling and typecasting + + diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md deleted file mode 100644 index 5b86dd4..0000000 --- a/CONTRIBUTORS.md +++ /dev/null @@ -1,13 +0,0 @@ -PyMetaWear contributors (sorted alphabetically) -=============================================== - -* [Jonas Böer](https://github.com/morgil) (Kinemic GmbH) - - Magnetometer module - -* [Thibaud Mathieu](https://github.com/enlight3d) - - Raw example for temperature module - -* [Eric Tsai](https://github.com/scaryghost) (mbientlab) - - 64-bit datasignal address handling and typecasting - - diff --git a/HISTORY.rst b/HISTORY.rst index fb28e9d..92823f1 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -1,21 +1,35 @@ +======= +History +======= + +v0.7.0 (2017-01-13) +------------------- +- Using MetaWear-CppAPI version 0.7.4 +- Removed bluepy backend due to it not being fully functional. +- Refactored connection behaviour. Optional autoconnect via keyword. +- Unit test work started with Mock backend. +- Flake8 adaptations. +- Fix for logging bug (#22) +- New examples: Two client setup and complimentary filter sensor fusion (#23). + v0.6.0 (2016-10-31) -=================== +------------------- - Using MetaWear-CppAPI version 0.6.0 - Replaced print-logging with proper logging module usage. - Removed 64-bit special handling that was no longer needed. v0.5.2 (2016-10-13) -=================== +------------------- - Temperature Module - Using Pygatt 3.0.0 (including PR from PyMetaWear contributors) - Builds on Windows v0.5.1 (2016-09-15) -=================== +------------------- - Corrections to make it distributable via PyPI. v0.5.0 (2016-09-15) -=================== +------------------- - Using MetaWear-CppAPI version 0.5.22 - Changed building procedure to handle ARM processors - Updated requirements to make pygatt default, all others extras @@ -28,19 +42,19 @@ v0.5.0 (2016-09-15) - High speed sampling for accelerometer and gyroscope v0.4.4 (2016-04-28) -=================== +------------------- - Updated MetaWear-CppAPI submodule version. - Removed temporary build workaround. v0.4.3 (2016-04-27) -=================== +------------------- - Critical fix for switch notifications. - Updated MetaWear-CppAPI submodule version. - Now using the new ``setup_metawear`` method. - Restructured the ``IS_64_BIT`` usage which is still needed. v0.4.2 (2016-04-27) -=================== +------------------- - Critical fix for timeout in pybluez/gattlib backend. - Added Gyroscope module. - Added soft reset method to client. @@ -48,32 +62,32 @@ v0.4.2 (2016-04-27) - Updated documentation. v0.4.1 (2016-04-20) -=================== +------------------- - Cleanup of new modules sensor data parsing. - Bug fix related to accelerometer module. - Timeout parameter for client and backends. v0.4.0 (2016-04-17) -=================== +------------------- - Major refactorisation into new module layout. - New examples using the new module handling. - Accelerometer convenience methods shows strange lag still. v0.3.1 (2016-04-10) -=================== +------------------- - Critical fix for data signal subscription method. - ``Setup.py`` handling of building made better, - Documentation improved. v0.3.0 (2016-04-09) -=================== +------------------- - Major refactoring: all BLE comm code practically moved to backends. - Backend ``pybluez`` with ``gattlib`` now works well. - Travis CI problems with Python 2.7 encoding led to that we are now building on 2.7.11 v0.2.3 (2016-04-07) -=================== +------------------- - Changed from using ``gattlib`` on its own to using ``pybluez`` with ``gattlib`` - Travis CI and Coveralls @@ -81,29 +95,29 @@ v0.2.3 (2016-04-07) - Some documentation written. v0.2.2 (2016-04-06) -=================== +------------------- - Convenience method for switch. - Sphinx documentation added. - Docstring updates. v0.2.1 (2016-04-04) -=================== +------------------- - Refactoring in moving functionality back to client from backends. - Enable BlueZ 4.X use with ``pygatt``. - Disconnect methods added. - Example with switch button notification. v0.2.0 (2016-04-02) -=================== +------------------- - Two backends: ``pygatt`` and ``gattlib`` - ``pygatt`` backend can be fully initialize, i.e. handles notifications. - ``gattlib`` backend **cannot** fully initialize, i.e. does **not** handles notifications. v0.1.1 (2016-03-30) -=================== +------------------- - Fix to support Python 3 v0.1.0 (2016-03-30) -=================== +------------------- - Initial release - Working communication, tested with very few API options. diff --git a/MANIFEST.in b/MANIFEST.in index d1228c4..01fb463 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,7 +1,7 @@ include LICENSE include README.rst include HISTORY.rst -include CONTRIBUTORS.md +include AUTHORS.rst include requirements.txt recursive-include pymetawear/Metawear-CppAPI * @@ -10,8 +10,14 @@ prune pymetawear/Metawear-CppAPI/build prune pymetawear/Metawear-CppAPI/dist recursive-include examples *.py -recursive-include tests *.py -recursive-include docs * +recursive-include tests * +recursive-include docs *.rst conf.py Makefile make.bat *.jpg *.png *.gif prune docs/build -global-exclude __pycache__/* +recursive-exclude * __pycache__ +recursive-exclude * *.py[co] + + + + + diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..63cfdea --- /dev/null +++ b/Makefile @@ -0,0 +1,81 @@ +make help.PHONY: clean clean-test clean-pyc clean-build docs help +.DEFAULT_GOAL := help +define BROWSER_PYSCRIPT +import os, webbrowser, sys +try: + from urllib import pathname2url +except: + from urllib.request import pathname2url + +webbrowser.open("file://" + pathname2url(os.path.abspath(sys.argv[1]))) +endef +export BROWSER_PYSCRIPT + +define PRINT_HELP_PYSCRIPT +import re, sys + +for line in sys.stdin: + match = re.match(r'^([a-zA-Z_-]+):.*?## (.*)$$', line) + if match: + target, help = match.groups() + print("%-20s %s" % (target, help)) +endef +export PRINT_HELP_PYSCRIPT +BROWSER := python -c "$$BROWSER_PYSCRIPT" + +help: + @python -c "$$PRINT_HELP_PYSCRIPT" < $(MAKEFILE_LIST) + +clean: clean-build clean-pyc clean-test clean-docs ## remove all build, test, coverage and Python artifacts + + +clean-build: ## remove build artifacts + rm -fr build/ + rm -fr dist/ + rm -fr .eggs/ + find . -name '*.egg-info' -exec rm -fr {} + + find . -name '*.egg' -exec rm -f {} + + +clean-pyc: ## remove Python file artifacts + find . -name '*.pyc' -exec rm -f {} + + find . -name '*.pyo' -exec rm -f {} + + find . -name '*~' -exec rm -f {} + + find . -name '__pycache__' -exec rm -fr {} + + +clean-test: ## remove test and coverage artifacts + rm -fr .tox/ + rm -f .coverage + rm -fr htmlcov/ + +clean-docs: ## Clean docs + make -C docs clean + +lint: ## check style with flake8 + flake8 pymetawear --exclude=Metawear-CppAPI,mbientlab --ignore=E501 + +test: ## run tests quickly with the default Python + py.test tests/ + +coverage: ## check code coverage quickly with the default Python + py.test tests/ --cov pymetawear --cov-report term-missing + coverage html + $(BROWSER) htmlcov/index.html + +docs: clean-docs ## generate Sphinx HTML documentation, including API docs + make -C docs html + $(BROWSER) docs/build/html/index.html + +servedocs: docs ## compile the docs watching for changes + watchmedo shell-command -p '*.rst' -c '$(MAKE) -C docs html' -R -D . + +release: clean ## package and upload a release + python setup.py sdist upload + python setup.py bdist_wheel upload + +dist: clean ## builds source and wheel package + python setup.py sdist + python setup.py bdist_wheel + ls -l dist + +install: clean ## install the package to the active Python's site-packages + python setup.py install diff --git a/README.rst b/README.rst index 7e5ee39..48a3e2c 100644 --- a/README.rst +++ b/README.rst @@ -1,3 +1,4 @@ +========== PyMetaWear ========== @@ -11,18 +12,18 @@ Python package for connecting to and using PyMetawear is meant to be a thin wrapper around the `MetaWear C++ API `_, -providing a more Pythonic interface. It has support for several different +providing a more Pythonic interface. It has support for two different Python packages for Bluetooth Low Energy communication: - `pygatt `_ - `pybluez `_ with `gattlib `_ -- `bluepy `_ (not completely functional yet) PyMetaWear can be run with Python 2 and 3.4 with both backends, -but only with the `pygatt` backend for Python 3.5. +but only with the `pygatt` backend for Python 3.5. -**It is a Linux-only package right now**! It can be built on Windows, given that Visual Studio Community 2015 has been installed first, +**It is a Linux-only package right now**! It can be built on Windows, given that +Visual Studio Community 2015 has been installed first, but there is no working backend for Windows BLE yet. Installation @@ -33,17 +34,12 @@ Installation $ pip install pymetawear Currently, only the `pygatt `_ communication -backend is installed by default. The other backends can be installed as extras: +backend is installed by default. The other backend can be installed as extras: .. code-block:: bash $ pip install pymetawear[pybluez] -or - -.. code-block:: bash - - $ pip install pymetawear[bluepy] Debian requirements for ``pymetawear`` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -60,10 +56,6 @@ Additional requirements for ``pybluez`` * ``libboost-python-dev`` * ``libboost-thread-dev`` -Additional requirements for ``bluepy`` -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -* ``libglib2.0-dev`` - Development ~~~~~~~~~~~ @@ -103,7 +95,7 @@ MetaWear board, is equal for both the two usage profiles: .. code-block:: python from pymetawear.client import MetaWearClient - backend = 'pygatt' # Or 'pybluez' or 'bluepy' + backend = 'pygatt' # Or 'pybluez' c = MetaWearClient('DD:3A:7D:4D:56:F0', backend) An example: blinking with the LED lights can be done like this with the diff --git a/docs/source/authors.rst b/docs/source/authors.rst new file mode 100644 index 0000000..7739272 --- /dev/null +++ b/docs/source/authors.rst @@ -0,0 +1 @@ +.. include:: ../../AUTHORS.rst diff --git a/docs/source/backends/bluepy.rst b/docs/source/backends/bluepy.rst deleted file mode 100644 index 31d8c26..0000000 --- a/docs/source/backends/bluepy.rst +++ /dev/null @@ -1,22 +0,0 @@ -.. _backend_bleupy: - -Backend: :mod:`bluepy` -====================== - -PyMetaWear can use ``bluepy`` as BLE communication backend. It is also uses -BlueZ to perform the actual communication, but it bundles the components it -needs to do it instead of utilizing the system installation. - - Right now the PyMetaWear implementation using ``bluepy`` lacks - a proper handling of notifications! - -The code can be found on `Github `_. - -It can be installed separately as such: - -.. code-block:: bash - - $ pip install bluepy - -.. automodule:: pymetawear.backends.bluepy - :members: diff --git a/docs/source/backends/index.rst b/docs/source/backends/index.rst index 9dadf2e..c00d312 100644 --- a/docs/source/backends/index.rst +++ b/docs/source/backends/index.rst @@ -5,12 +5,11 @@ methods handling MetaWear specific tasks such as setting data rates for accelerometers and subscribing to switch status. The actual Bluetooth Low Energy communication is done in this module. -Currently, PyMetaWear implements three different backends: +Currently, PyMetaWear implements two different backends: .. toctree:: :maxdepth: 1 pygatt pybluez - bluepy diff --git a/docs/source/client.rst b/docs/source/client.rst index 783aa59..3822c9a 100644 --- a/docs/source/client.rst +++ b/docs/source/client.rst @@ -8,7 +8,7 @@ The MetaWear client provided by this package. It can be used as such: .. code-block:: python from pymetawear.client import MetaWearClient - backend = 'pygatt' # Or 'pybluez' or 'bluepy' + backend = 'pygatt' # Or 'pybluez' c = MetaWearClient('DD:3A:7D:4D:56:F0', backend) The client can now be used for e.g. subscribing to data signals or logging data. diff --git a/docs/source/conf.py b/docs/source/conf.py index aa8a370..838b54a 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -63,7 +63,7 @@ # |version| and |release|, also used in various other places throughout the # built documents. -with open('../../pymetawear/__init__.py', 'r') as fd: +with open('../../pymetawear/version.py', 'r') as fd: data = fd.read() version = re.search(r'^__version__\s*=\s*[\'"]([^\'"]*)[\'"]', data, re.MULTILINE).group(1) diff --git a/docs/source/discover.rst b/docs/source/discover.rst index fd7e8b6..6e800de 100644 --- a/docs/source/discover.rst +++ b/docs/source/discover.rst @@ -17,7 +17,11 @@ The :func:`~discover_devices` function uses the bluetooth application. See docstring below for more details about privileges using ``hcitool`` from Python. +There is a convenience method named :func:`~select_device` as well, which +displays a list of devices to choose from. + API --- -.. autofunction:: pymetawear.client.discover_devices +.. automodule:: pymetawear.discover + :members: diff --git a/docs/source/history.rst b/docs/source/history.rst new file mode 100644 index 0000000..5f2e348 --- /dev/null +++ b/docs/source/history.rst @@ -0,0 +1 @@ +.. include:: ../../HISTORY.rst diff --git a/docs/source/index.rst b/docs/source/index.rst index a721880..5147fa7 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -15,13 +15,12 @@ Python package for connecting to and using `MbientLab's MetaWear `_, -providing a more Pythonic interface. It has support for several different +providing a more Pythonic interface. It has support for two different Python packages for Bluetooth Low Energy communication: - `pygatt `_ - `pybluez `_ with `gattlib `_ -- `bluepy `_ (not completely functional yet) PyMetaWear can be run with Python 2 or 3 with both backends, but only with the `pygatt` backend for Python 3.5. It builds and runs on Linux systems, @@ -29,7 +28,6 @@ and can be built on Windows, given that Visual Studio Community 2015 has been in but there is no working backend for Windows BLE yet. - Contents -------- @@ -41,6 +39,8 @@ Contents exceptions backends/index modules/index + history + authors Installation ------------ @@ -56,11 +56,6 @@ backend is installed by default. The other backends can be installed as extras: $ pip install pymetawear[pybluez] -or - -.. code-block:: bash - - $ pip install pymetawear[bluepy] Requirements for ``pymetawear`` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -77,9 +72,6 @@ Additional requirements for ``pybluez`` * ``libboost-python-dev`` * ``libboost-thread-dev`` -Additional requirements for ``bluepy`` -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -* ``libglib2.0-dev`` Development ~~~~~~~~~~~ diff --git a/docs/source/modules/gyroscope.rst b/docs/source/modules/gyroscope.rst index 03e95dd..c5e7bf7 100644 --- a/docs/source/modules/gyroscope.rst +++ b/docs/source/modules/gyroscope.rst @@ -1,4 +1,4 @@ -.. _modules_accelerometer: +.. _modules_gyroscope: Gyroscope module ==================== diff --git a/examples/accelerometer.py b/examples/accelerometer.py index 6947e38..8ef74d7 100644 --- a/examples/accelerometer.py +++ b/examples/accelerometer.py @@ -11,15 +11,14 @@ from __future__ import division from __future__ import print_function -from __future__ import unicode_literals from __future__ import absolute_import import time -from discover import scan_and_select_le_device +from pymetawear.discover import select_device from pymetawear.client import MetaWearClient -address = scan_and_select_le_device() +address = select_device() c = MetaWearClient(str(address), 'pygatt', debug=True) print("New client created: {0}".format(c)) diff --git a/examples/ambientlight.py b/examples/ambientlight.py index 587c7f5..4355c79 100644 --- a/examples/ambientlight.py +++ b/examples/ambientlight.py @@ -11,15 +11,14 @@ from __future__ import division from __future__ import print_function -from __future__ import unicode_literals from __future__ import absolute_import import time -from discover import scan_and_select_le_device +from pymetawear.discover import select_device from pymetawear.client import MetaWearClient -address = scan_and_select_le_device() +address = select_device() c = MetaWearClient(str(address), 'pygatt', debug=True) print("New client created: {0}".format(c)) diff --git a/examples/barometer.py b/examples/barometer.py index d906201..34e595b 100644 --- a/examples/barometer.py +++ b/examples/barometer.py @@ -11,15 +11,14 @@ from __future__ import division from __future__ import print_function -from __future__ import unicode_literals from __future__ import absolute_import import time -from discover import scan_and_select_le_device +from pymetawear.discover import select_device from pymetawear.client import MetaWearClient -address = scan_and_select_le_device() +address = select_device() c = MetaWearClient(str(address), 'pybluez', debug=True) print("New client created: {0}".format(c)) diff --git a/examples/battery.py b/examples/battery.py index 98309aa..eb4ba27 100644 --- a/examples/battery.py +++ b/examples/battery.py @@ -11,15 +11,14 @@ from __future__ import division from __future__ import print_function -from __future__ import unicode_literals from __future__ import absolute_import import time -from discover import scan_and_select_le_device +from pymetawear.discover import select_device from pymetawear.client import MetaWearClient -address = scan_and_select_le_device() +address = select_device() c = MetaWearClient(str(address), 'pygatt', debug=True) print("New client created: {0}".format(c)) diff --git a/examples/complimentary.py b/examples/complimentary.py new file mode 100644 index 0000000..886d40b --- /dev/null +++ b/examples/complimentary.py @@ -0,0 +1,139 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +""" +:mod:`battery` +================== + +Created by hbldh +Created on 2016-04-02 + +""" + +from __future__ import division +from __future__ import print_function +from __future__ import absolute_import + +import time +from math import atan2, pi + +import matplotlib.pyplot as plt + +from pymetawear.discover import select_device +from pymetawear.client import MetaWearClient + + +class ComplimentaryFilter(object): + """A simple, not very efficient Complimentary Filter example. + + A ``tau``-value of 0.075 leads to a pretty good filtering. + + Args: + + tau (float): Response coefficientS. Higher value leads to more + gyro-driven filtering. + f (int): Frequency of sensor sampling. + + References: + + http://www.pieter-jan.com/node/11 + http://robottini.altervista.org/tag/complementary-filter + + """ + + def __init__(self, tau, f): + """Constructor""" + + self.frequency = f + self._dt = 1.0 / self.frequency + self.k = tau / (self._dt + tau) + + self._acc_data_added = False + self._gyro_data_added = False + + self.raw_acc_data = [] + self.raw_gyro_data = [] + self.unfiltered_data = [] + self.filtered_data = [] + self.filtered_data_timestamps = [] + + def get_pitch_roll_from_accelerometer(self, acc): + pitch = atan2(acc[1], acc[2]) * 180 / pi + roll = atan2(acc[0], acc[2]) * 180 / pi + return pitch, roll + + def update_angle(self, acc, gyro): + old_pitch, old_roll = self.filtered_data[-1] + acc_pitch, acc_roll = self.get_pitch_roll_from_accelerometer(acc) + self.unfiltered_data.append((acc_pitch, acc_roll)) + new_pitch = self.k * (old_pitch + gyro[0] * self._dt) + (1 - self.k) * acc_pitch + new_roll = self.k * (old_roll + gyro[1] * self._dt) + (1 - self.k) * acc_roll + return new_pitch, new_roll + + def add_accelerometer_data(self, data): + if not self.raw_acc_data: + self.filtered_data.append(self.get_pitch_roll_from_accelerometer(data[1])) + self.raw_acc_data.append(data) + self._acc_data_added = True + if self._acc_data_added and self._gyro_data_added: + self._acc_data_added = False + self._gyro_data_added = False + self.filtered_data.append(self.update_angle( + self.raw_acc_data[-1][1], self.raw_gyro_data[-1][1])) + self.filtered_data_timestamps.append(self.raw_acc_data[-1][0]) + + def add_gyroscope_data(self, data): + self.raw_gyro_data.append(data) + self._gyro_data_added = True + if self._acc_data_added and self._gyro_data_added: + self._acc_data_added = False + self._gyro_data_added = False + self.filtered_data.append(self.update_angle( + self.raw_acc_data[-1][1], self.raw_gyro_data[-1][1])) + self.filtered_data_timestamps.append(self.raw_gyro_data[-1][0]) + + def plot(self): + timestamps = [ts - self.filtered_data_timestamps[0] for ts in self.filtered_data_timestamps] + + ax = plt.subplot(211) + ax.set_title("Pitch") + ax.plot(timestamps, [x[0] for x in self.filtered_data[1:]]) + ax.plot(timestamps, [x[0] for x in self.unfiltered_data], 'r--') + ax.legend(['Filtered pitch', 'Raw pitch']) + + ax = plt.subplot(212) + ax.set_title("Roll") + ax.plot(timestamps, [x[1] for x in self.filtered_data[1:]]) + ax.plot(timestamps, [x[1] for x in self.unfiltered_data], 'r--') + ax.legend(['Filtered roll', 'Raw roll']) + + plt.show() + + +def run(): + """Example of how to run the Complimentary filter.""" + address = select_device() + c = MetaWearClient(str(address), 'pygatt', timeout=10, debug=False) + print("New client created: {0}".format(c)) + f = ComplimentaryFilter(0.075, 50.0) + + print("Write accelerometer settings...") + c.accelerometer.set_settings(data_rate=50.0, data_range=4.0) + c.gyroscope.set_settings(data_rate=50.0, data_range=250.0) + print("Subscribing to accelerometer signal notifications...") + c.accelerometer.high_frequency_stream = False + c.accelerometer.notifications(f.add_accelerometer_data) + c.gyroscope.notifications(f.add_gyroscope_data) + time.sleep(10.0) + + print("Unsubscribe to notification...") + c.accelerometer.notifications(None) + c.gyroscope.notifications(None) + + c.disconnect() + + return f + + +if __name__ == '__main__': + f = run() + f.plot() diff --git a/examples/discover.py b/examples/discover.py index 4941d6c..f5965c9 100644 --- a/examples/discover.py +++ b/examples/discover.py @@ -10,10 +10,9 @@ from __future__ import division from __future__ import print_function -from __future__ import unicode_literals from __future__ import absolute_import -from pymetawear.client import discover_devices +from pymetawear.discover import discover_devices try: input_fcn = raw_input diff --git a/examples/gyroscope.py b/examples/gyroscope.py index bbe43a3..f9a0616 100644 --- a/examples/gyroscope.py +++ b/examples/gyroscope.py @@ -11,15 +11,14 @@ from __future__ import division from __future__ import print_function -from __future__ import unicode_literals from __future__ import absolute_import import time -from discover import scan_and_select_le_device +from pymetawear.discover import select_device from pymetawear.client import MetaWearClient -address = scan_and_select_le_device() +address = select_device() c = MetaWearClient(str(address), 'pygatt', debug=True) print("New client created: {0}".format(c)) diff --git a/examples/led.py b/examples/led.py index 80878be..33669ce 100644 --- a/examples/led.py +++ b/examples/led.py @@ -11,15 +11,14 @@ from __future__ import division from __future__ import print_function -from __future__ import unicode_literals from __future__ import absolute_import import time -from discover import scan_and_select_le_device +from pymetawear.discover import select_device from pymetawear.client import MetaWearClient -address = scan_and_select_le_device() +address = select_device() c = MetaWearClient(str(address), 'pygatt', debug=True) print("New client created: {0}".format(c)) diff --git a/examples/raw/accelerometer.py b/examples/raw/accelerometer.py index 621d834..87a1597 100644 --- a/examples/raw/accelerometer.py +++ b/examples/raw/accelerometer.py @@ -11,37 +11,19 @@ from __future__ import division from __future__ import print_function -from __future__ import unicode_literals from __future__ import absolute_import import time -import platform -from ctypes import cast, POINTER, c_float, c_long +from ctypes import cast, POINTER, c_float -from pymetawear.client import discover_devices, MetaWearClient, libmetawear +from pymetawear.discover import select_device +from pymetawear.client import libmetawear from pymetawear.exceptions import PyMetaWearException from pymetawear.mbientlab.metawear.core import CartesianFloat, DataTypeId, Fn_DataPtr from pymetawear.client import MetaWearClient -def scan_and_select_le_device(timeout=3): - print("Discovering nearby Bluetooth Low Energy devices...") - ble_devices = discover_devices(timeout=timeout) - if len(ble_devices) > 1: - for i, d in enumerate(ble_devices): - print("[{0}] - {1}: {2}".format(i+1, *d)) - s = input("Which device do you want to connect to? ") - if int(s) <= (i + 1): - address = ble_devices[int(s) - 1][0] - else: - raise ValueError("Incorrect selection. Aborting...") - elif len(ble_devices) == 1: - address = ble_devices[0][0] - else: - raise ValueError("Did not detect any BLE devices.") - return address - -address = scan_and_select_le_device() +address = select_device() c = MetaWearClient(str(address), 'pygatt', debug=True) print("New client created: {0}".format(c)) diff --git a/examples/raw/gyroscope.py b/examples/raw/gyroscope.py index 7103b96..f9cd2e4 100644 --- a/examples/raw/gyroscope.py +++ b/examples/raw/gyroscope.py @@ -11,38 +11,19 @@ from __future__ import division from __future__ import print_function -from __future__ import unicode_literals from __future__ import absolute_import import time -import platform -from ctypes import cast, POINTER, c_float, c_long +from ctypes import cast, POINTER -from pymetawear.client import MetaWearClient, libmetawear, discover_devices +from pymetawear.client import MetaWearClient, libmetawear +from pymetawear.discover import select_device from pymetawear.exceptions import PyMetaWearException from pymetawear.mbientlab.metawear.core import \ CartesianFloat, DataTypeId, Fn_DataPtr -def scan_and_select_le_device(timeout=3): - print("Discovering nearby Bluetooth Low Energy devices...") - ble_devices = discover_devices(timeout=timeout) - if len(ble_devices) > 1: - for i, d in enumerate(ble_devices): - print("[{0}] - {1}: {2}".format(i+1, *d)) - s = input("Which device do you want to connect to? ") - if int(s) <= (i + 1): - address = ble_devices[int(s) - 1][0] - else: - raise ValueError("Incorrect selection. Aborting...") - elif len(ble_devices) == 1: - address = ble_devices[0][0] - else: - raise ValueError("DId not detect any BLE devices.") - return address - - -address = scan_and_select_le_device() +address = select_device() c = MetaWearClient(str(address), 'pygatt', debug=True) print("New client created: {0}".format(c)) diff --git a/examples/raw/led.py b/examples/raw/led.py index 5168557..46fd345 100644 --- a/examples/raw/led.py +++ b/examples/raw/led.py @@ -11,32 +11,14 @@ from __future__ import division from __future__ import print_function -from __future__ import unicode_literals from __future__ import absolute_import import time -from pymetawear.client import discover_devices, MetaWearClient - -def scan_and_select_le_device(timeout=3): - print("Discovering nearby Bluetooth Low Energy devices...") - ble_devices = discover_devices(timeout=timeout) - if len(ble_devices) > 1: - for i, d in enumerate(ble_devices): - print("[{0}] - {1}: {2}".format(i+1, *d)) - s = input("Which device do you want to connect to? ") - if int(s) <= (i + 1): - address = ble_devices[int(s) - 1][0] - else: - raise ValueError("Incorrect selection. Aborting...") - elif len(ble_devices) == 1: - address = ble_devices[0][0] - else: - raise ValueError("DId not detect any BLE devices.") - return address - - -address = scan_and_select_le_device() +from pymetawear.client import MetaWearClient +from pymetawear.discover import select_device + +address = select_device() c = MetaWearClient(str(address), debug=True) print("New client created: {0}".format(c)) diff --git a/examples/raw/switch.py b/examples/raw/switch.py index 2592ea0..babfb2f 100644 --- a/examples/raw/switch.py +++ b/examples/raw/switch.py @@ -11,36 +11,17 @@ from __future__ import division from __future__ import print_function -from __future__ import unicode_literals from __future__ import absolute_import -from ctypes import POINTER, c_uint, cast, c_long +from ctypes import POINTER, c_uint, cast import time -import platform from pymetawear import libmetawear -from pymetawear.client import discover_devices, MetaWearClient +from pymetawear.client import MetaWearClient +from pymetawear.discover import select_device from pymetawear.mbientlab.metawear.core import DataTypeId, Fn_DataPtr -def scan_and_select_le_device(timeout=3): - print("Discovering nearby Bluetooth Low Energy devices...") - ble_devices = discover_devices(timeout=timeout) - if len(ble_devices) > 1: - for i, d in enumerate(ble_devices): - print("[{0}] - {1}: {2}".format(i+1, *d)) - s = input("Which device do you want to connect to? ") - if int(s) <= (i + 1): - address = ble_devices[int(s) - 1][0] - else: - raise ValueError("Incorrect selection. Aborting...") - elif len(ble_devices) == 1: - address = ble_devices[0][0] - else: - raise ValueError("DId not detect any BLE devices.") - return address - - -address = scan_and_select_le_device() +address = select_device() c = MetaWearClient(str(address), debug=True) print("New client created: {0}".format(c)) diff --git a/examples/raw/temperature.py b/examples/raw/temperature.py index 858095b..44c35b4 100644 --- a/examples/raw/temperature.py +++ b/examples/raw/temperature.py @@ -11,36 +11,17 @@ from __future__ import division from __future__ import print_function -from __future__ import unicode_literals from __future__ import absolute_import -from ctypes import POINTER, c_float, cast, c_long, c_uint8 +from ctypes import POINTER, c_float, cast import time -import platform from pymetawear import libmetawear -from pymetawear.client import discover_devices, MetaWearClient +from pymetawear.client import MetaWearClient +from pymetawear.discover import select_device from pymetawear.mbientlab.metawear.core import DataTypeId, Fn_DataPtr -def scan_and_select_le_device(timeout=3): - print("Discovering nearby Bluetooth Low Energy devices...") - ble_devices = discover_devices(timeout=timeout) - if len(ble_devices) > 1: - for i, d in enumerate(ble_devices): - print("[{0}] - {1}: {2}".format(i+1, *d)) - s = input("Which device do you want to connect to? ") - if int(s) <= (i + 1): - address = ble_devices[int(s) - 1][0] - else: - raise ValueError("Incorrect selection. Aborting...") - elif len(ble_devices) == 1: - address = ble_devices[0][0] - else: - raise ValueError("DId not detect any BLE devices.") - return address - - -address = scan_and_select_le_device() +address = select_device() c = MetaWearClient(str(address), debug=True) print("New client created: {0}".format(c)) diff --git a/examples/switch.py b/examples/switch.py index 3511893..6811f47 100644 --- a/examples/switch.py +++ b/examples/switch.py @@ -12,14 +12,13 @@ from __future__ import absolute_import from __future__ import division from __future__ import print_function -from __future__ import unicode_literals import time -from discover import scan_and_select_le_device +from pymetawear.discover import select_device from pymetawear.client import MetaWearClient -address = scan_and_select_le_device() +address = select_device() c = MetaWearClient(str(address), 'pybluez', debug=True) print("New client created: {0}".format(c)) diff --git a/examples/temperature.py b/examples/temperature.py index 612e4cc..379a2a8 100644 --- a/examples/temperature.py +++ b/examples/temperature.py @@ -11,17 +11,17 @@ from __future__ import division from __future__ import print_function -from __future__ import unicode_literals from __future__ import absolute_import import time -from discover import scan_and_select_le_device +from pymetawear.discover import select_device from pymetawear.client import MetaWearClient -address = scan_and_select_le_device() +address = select_device() c = MetaWearClient(str(address), 'pygatt', timeout=10, debug=True) print("New client created: {0}".format(c)) +c.connect() def temperature_callback(data): diff --git a/examples/two_clients.py b/examples/two_clients.py new file mode 100644 index 0000000..5e83c21 --- /dev/null +++ b/examples/two_clients.py @@ -0,0 +1,42 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +""" +:mod:`led` +================== + +Created by hbldh +Created on 2016-04-02 + +""" + +from __future__ import division +from __future__ import print_function +from __future__ import absolute_import + +import time +from pymetawear.client import MetaWearClient + +address_1 = 'DD:3A:7D:4D:56:F0' +address_2 = 'FF:50:35:82:3B:5A' + +print("Connect to {0}...".format(address_1)) +client_1 = MetaWearClient(str(address_1), timeout=10.0, debug=False) +print("New client created: {0}".format(client_1)) +print("Connect to {0}...".format(address_2)) +client_2 = MetaWearClient(str(address_2), timeout=10.0, debug=False) +print("New client created: {0}".format(client_2)) + +print("Blinking 10 times with green LED on client 1...") +pattern = client_1.led.load_preset_pattern('blink', repeat_count=10) +client_1.led.write_pattern(pattern, 'g') +client_1.led.play() + +print("Blinking 10 times with red LED on client 2...") +pattern = client_2.led.load_preset_pattern('blink', repeat_count=10) +client_2.led.write_pattern(pattern, 'r') +client_2.led.play() + +time.sleep(5.0) + +client_1.disconnect() +client_2.disconnect() diff --git a/pymetawear/Metawear-CppAPI b/pymetawear/Metawear-CppAPI index 7659beb..175dae5 160000 --- a/pymetawear/Metawear-CppAPI +++ b/pymetawear/Metawear-CppAPI @@ -1 +1 @@ -Subproject commit 7659beb77531f60af14c9bcd0a8dbaf03920b574 +Subproject commit 175dae5d98ecba867e859ca42409de5ff553a7c5 diff --git a/pymetawear/__init__.py b/pymetawear/__init__.py index db6c5cf..2872ec3 100644 --- a/pymetawear/__init__.py +++ b/pymetawear/__init__.py @@ -11,7 +11,7 @@ import glob from ctypes import cdll -from pymetawear.mbientlab.metawear.core import Fn_DataPtr, Fn_VoidPtr_Int +from pymetawear.version import __version__, version # flake8: noqa from pymetawear.mbientlab.metawear.functions import setup_libmetawear # Logging solution inspired by Hitchhiker's Guide to Python and Requests @@ -26,25 +26,20 @@ def emit(self, record): logging.getLogger(__name__).addHandler(NullHandler()) -# Version information. -__version__ = '0.6.0' -version = __version__ # backwards compatibility name -version_info = (0, 6, 0) # Find and import the built MetaWear-CPP shared library. if os.environ.get('METAWEAR_LIB_SO_NAME') is not None: - libmetawear = cdll.LoadLibrary(os.environ["METAWEAR_LIB_SO_NAME"]) + METAWEAR_LIB_SO_NAME = os.environ["METAWEAR_LIB_SO_NAME"] else: if platform.uname()[0] == 'Windows': dll_files = list(glob.glob(os.path.join(os.path.abspath( os.path.dirname(__file__)), 'MetaWear.*.dll'))) - shared_lib_file_name = dll_files[0] + METAWEAR_LIB_SO_NAME = dll_files[0] else: - shared_lib_file_name = 'libmetawear.so' - libmetawear = cdll.LoadLibrary( - os.path.join(os.path.abspath(os.path.dirname(__file__)), - shared_lib_file_name)) - + METAWEAR_LIB_SO_NAME = 'libmetawear.so' + METAWEAR_LIB_SO_NAME = os.path.join(os.path.abspath( + os.path.dirname(__file__)), METAWEAR_LIB_SO_NAME) +libmetawear = cdll.LoadLibrary(METAWEAR_LIB_SO_NAME) setup_libmetawear(libmetawear) @@ -55,6 +50,9 @@ def add_stream_logger(stream=sys.stdout, level=logging.DEBUG): Returns the handler after adding it. """ logger = logging.getLogger(__name__) + has_stream_handler = any([isinstance(hndl, logging.StreamHandler) for hndl in logger.handlers]) + if has_stream_handler: + return logger.handlers[-1] handler = logging.StreamHandler(stream=stream) handler.setFormatter(logging.Formatter('%(asctime)s %(levelname)s %(message)s')) logger.addHandler(handler) diff --git a/pymetawear/backends/__init__.py b/pymetawear/backends/__init__.py index 97f4699..eaab17b 100644 --- a/pymetawear/backends/__init__.py +++ b/pymetawear/backends/__init__.py @@ -8,7 +8,6 @@ """ from __future__ import division from __future__ import print_function -# from __future__ import unicode_literals from __future__ import absolute_import import os @@ -19,38 +18,35 @@ from pymetawear import libmetawear from pymetawear.exceptions import PyMetaWearException -from pymetawear.mbientlab.metawear.core import BtleConnection, Fn_VoidPtr_GattCharPtr, \ +from pymetawear.mbientlab.metawear.core import BtleConnection, \ + Fn_VoidPtr_GattCharPtr, \ Fn_VoidPtr_GattCharPtr_ByteArray, Fn_VoidPtr_Int from pymetawear.specs import METAWEAR_SERVICE_NOTIFY_CHAR -from pymetawear.compat import string_types +from pymetawear.compat import string_types, range_ log = logging.getLogger(__name__) class BLECommunicationBackend(object): - - def __init__(self, address, interface=None, - async=True, timeout=None, debug=False): + def __init__(self, address, interface=None, timeout=None, debug=False): self._address = str(address) self._interface = str(interface) - self._async = async self._debug = debug self._timeout = timeout if debug: log.setLevel(logging.DEBUG) - self._initialization_status = -1 + self.initialization_status = -1 self._requester = None - self._build_handle_dict() - # Define read and write to characteristics methods to be used by # libmetawear. These methods in their turn use the backend read/write # methods implemented in the specific backends. self._btle_connection = BtleConnection( - write_gatt_char=Fn_VoidPtr_GattCharPtr_ByteArray(self.mbl_mw_write_gatt_char), + write_gatt_char=Fn_VoidPtr_GattCharPtr_ByteArray( + self.mbl_mw_write_gatt_char), read_gatt_char=Fn_VoidPtr_GattCharPtr(self.mbl_mw_read_gatt_char)) # Dictionary of callbacks for subscriptions set up through the @@ -60,22 +56,8 @@ def __init__(self, address, interface=None, Fn_VoidPtr_Int(self._initialized_fcn)), } - # Setup the notification characteristic subscription - # required by MetaWear. - self._notify_char_handle = self.get_handle( - METAWEAR_SERVICE_NOTIFY_CHAR[1]) - self.subscribe(METAWEAR_SERVICE_NOTIFY_CHAR[1], - self.handle_notify_char_output) - - # Now create a libmetawear board object and initialize it. - self.board = libmetawear.mbl_mw_metawearboard_create( - byref(self._btle_connection)) - - _response_time = os.environ.get('PYMETAWEAR_RESPONSE_TIME', 300) - libmetawear.mbl_mw_metawearboard_set_time_for_response(self.board, int(_response_time)) - - libmetawear.mbl_mw_metawearboard_initialize( - self.board, self.callbacks.get('initialization')[1]) + self.board = None + self._notify_char_handle = None def __str__(self): return "{0}, {1}".format(self.__class__.__name__, self._address) @@ -88,7 +70,11 @@ def _build_handle_dict(self): @property def initialized(self): - return self._initialization_status >= 0 + return self.initialization_status >= 0 + + @property + def is_connected(self): + raise NotImplementedError("Use backend-specific classes instead!") @property def requester(self): @@ -101,6 +87,34 @@ def requester(self): """ raise NotImplementedError("Use backend-specific classes instead!") + def connect(self, clean_connect=False): + self._build_handle_dict() + + # Setup the notification characteristic subscription + # required by MetaWear. + self._notify_char_handle = self.get_handle( + METAWEAR_SERVICE_NOTIFY_CHAR[1]) + self.subscribe(METAWEAR_SERVICE_NOTIFY_CHAR[1], + self.handle_notify_char_output) + + # Now create a libmetawear board object and initialize it. + # Free memory for any old board first. + if self.board is not None: + try: + libmetawear.mbl_mw_metawearboard_tear_down(self.board) + except: + pass + libmetawear.mbl_mw_metawearboard_free(self.board) + self.board = libmetawear.mbl_mw_metawearboard_create( + byref(self._btle_connection)) + + _response_time = os.environ.get('PYMETAWEAR_RESPONSE_TIME', 300) + libmetawear.mbl_mw_metawearboard_set_time_for_response(self.board, int( + _response_time)) + + libmetawear.mbl_mw_metawearboard_initialize( + self.board, self.callbacks.get('initialization')[1]) + def disconnect(self): """Handle any required disconnecting in the backend, e.g. sever Bluetooth connection. @@ -109,7 +123,7 @@ def disconnect(self): def subscribe(self, characteristic_uuid, callback): self._subscribe(characteristic_uuid, callback) - self._print_debug_output("Subscribe", characteristic_uuid, []) + self._log("Subscribe", characteristic_uuid, [], 0) def mbl_mw_read_gatt_char(self, board, characteristic): """Read the desired data from the MetaWear board. @@ -118,17 +132,12 @@ def mbl_mw_read_gatt_char(self, board, characteristic): characteristic: :class:`ctypes.POINTER` to a GattCharacteristic. """ - if isinstance(characteristic, uuid.UUID): - service_uuid, characteristic_uuid = None, characteristic - else: - service_uuid, characteristic_uuid = self._mbl_mw_characteristic_2_uuids( - characteristic.contents) - response = self.read_gatt_char_by_uuid(characteristic_uuid) - sb = self.read_response_to_str(response) + response = self.read_gatt_char_by_uuid(characteristic) + sb = self._response_2_string_buffer(response) libmetawear.mbl_mw_metawearboard_char_read( self.board, characteristic, sb.raw, len(sb.raw)) - self._print_debug_output("Read", characteristic_uuid, response) + self._log("Read", characteristic, response, len(response)) def mbl_mw_write_gatt_char(self, board, characteristic, command, length): """Write the desired data to the MetaWear board. @@ -139,46 +148,61 @@ def mbl_mw_write_gatt_char(self, board, characteristic, command, length): :param int length: Length of the array that command points. """ + self._log("Write", characteristic, command, length) + self.write_gatt_char_by_uuid(characteristic, command, length) + + # Helper methods + + @staticmethod + def get_uuid(characteristic): if isinstance(characteristic, uuid.UUID): - service_uuid, characteristic_uuid = None, characteristic + return characteristic else: - service_uuid, characteristic_uuid = self._mbl_mw_characteristic_2_uuids( - characteristic.contents) - data_to_send = self.mbl_mw_command_to_input(command, length) - if self._debug: - self._print_debug_output("Write", characteristic_uuid, data_to_send) - self.write_gatt_char_by_uuid(characteristic_uuid, data_to_send) - - def _subscribe(self, characterisitic_uuid, callback): - raise NotImplementedError("Use backend-specific classes instead!") + return uuid.UUID(int=(characteristic.contents.uuid_high << 64) + + characteristic.contents.uuid_low) - def read_gatt_char_by_uuid(self, characteristic_uuid): - raise NotImplementedError("Use backend-specific classes instead!") + @staticmethod + def get_service_uuid(characteristic): + if isinstance(characteristic, uuid.UUID): + return characteristic + else: + return uuid.UUID(int=(characteristic.contents.service_uuid_high << 64) + + characteristic.contents.service_uuid_low) - def write_gatt_char_by_uuid(self, characteristic_uuid, data_to_send): - raise NotImplementedError("Use backend-specific classes instead!") + def sleep(self, t): + """Make backend sleep.""" + time.sleep(t) - # Callback methods + # Callback methods def _initialized_fcn(self, board, status): log.debug("{0} initialized with status {1}.".format(self, status)) - self._initialization_status = status + self.initialization_status = status def handle_notify_char_output(self, handle, value): - self._print_debug_output("Notify", handle, value) + self._log("Notify", handle, value, 0) if handle == self._notify_char_handle: - sb = self.notify_response_to_str(value) + sb = self._response_2_string_buffer(value) libmetawear.mbl_mw_metawearboard_notify_char_changed( self.board, sb.raw, len(sb.raw)) else: raise PyMetaWearException( "Notification on unexpected handle: {0}".format(handle)) - # Helper methods + # Methods to be implemented by backends. - def get_handle(self, uuid, value_handle=True): + def _subscribe(self, characterisitic_uuid, callback): + raise NotImplementedError("Use backend-specific classes instead!") + + def read_gatt_char_by_uuid(self, characteristic): + raise NotImplementedError("Use backend-specific classes instead!") + + def write_gatt_char_by_uuid(self, characteristic, command, length): + raise NotImplementedError("Use backend-specific classes instead!") + + def get_handle(self, uuid, notify_handle=False): """Get handle for a characteristic UUID. :param uuid.UUID uuid: The UUID to get handle of. @@ -189,43 +213,34 @@ def get_handle(self, uuid, value_handle=True): """ raise NotImplementedError("Use backend-specific classes instead!") - @staticmethod - def mbl_mw_command_to_input(command, length): - raise NotImplementedError("Use backend-specific classes instead!") - - @staticmethod - def read_response_to_str(response): + def _response_2_string_buffer(self, response): raise NotImplementedError("Use backend-specific classes instead!") - @staticmethod - def notify_response_to_str(response): - raise NotImplementedError("Use backend-specific classes instead!") + # Debug method - @staticmethod - def _mbl_mw_characteristic_2_uuids(characteristic): - return (uuid.UUID(int=(characteristic.service_uuid_high << 64) + - characteristic.service_uuid_low), - uuid.UUID(int=(characteristic.uuid_high << 64) + - characteristic.uuid_low)) + def _log(self, action, handle_or_char, value, value_length): - def _print_debug_output(self, action, handle_or_char, data): if not self._debug: return - if data and isinstance(data[0], int): - data_as_hex = " ".join(["{:02x}".format(b) for b in data]) + if action == "Write": + data_as_hex = " ".join(["{:02x}".format(b) for + b in [value[i] for i in range_(value_length)]]) + elif action == "Subscribe": + data_as_hex = "" else: - data_as_hex = " ".join(["{:02x}".format(ord(b)) for b in data]) + data_as_hex = " ".join(["{:02x}".format(ord(b)) for + b in self._response_2_string_buffer(value)]) if isinstance(handle_or_char, (uuid.UUID, string_types)): handle = self.get_handle(handle_or_char) elif isinstance(handle_or_char, int): handle = handle_or_char else: - handle = -1 + # Assume it is a Pointer to a GattCharacteristic... + try: + handle = self.get_handle(self.get_uuid(handle_or_char)) + except: + handle = -1 log.debug("{0:<6s} 0x{1:04x}: {2}".format(action, handle, data_as_hex)) - - def sleep(self, t): - """Make backend sleep.""" - time.sleep(t) diff --git a/pymetawear/backends/bluepy/__init__.py b/pymetawear/backends/bluepy/__init__.py deleted file mode 100644 index f626c79..0000000 --- a/pymetawear/backends/bluepy/__init__.py +++ /dev/null @@ -1,194 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- -""" - -.. moduleauthor:: hbldh -Created on 2016-04-02 - -""" - -from __future__ import division -from __future__ import print_function -#from __future__ import unicode_literals -from __future__ import absolute_import - -import time -import threading -import uuid -import warnings -from ctypes import create_string_buffer -import logging - -from bluepy.btle import Peripheral, ADDR_TYPE_RANDOM, BTLEException, DefaultDelegate, Characteristic - -from pymetawear.exceptions import PyMetaWearException, PyMetaWearConnectionTimeout -from pymetawear.compat import range_, string_types, bytearray_to_str -from pymetawear.backends import BLECommunicationBackend - -__all__ = ["BluepyBackend"] - -log = logging.getLogger(__name__) - - -class BluepyDelegate(DefaultDelegate): - - def __init__(self, notify_fcn, *args, **kwargs): - DefaultDelegate.__init__(self) - self.notify_fcn = notify_fcn - - def handleNotification(self, cHandle, data): - self.notify_fcn(cHandle, data) - - -class BluepyBackend(BLECommunicationBackend): - """ - Backend using `bluepy `_ - for BLE communication. - """ - - def __init__(self, address, interface=None, async=False, timeout=None, debug=False): - self._primary_services = {} - self._characteristics_cache = {} - self._peripheral = None - if debug: - log.setLevel(logging.DEBUG) - - warnings.warn("Bluepy backend does not handle notifications properly yet.", RuntimeWarning) - - super(BluepyBackend, self).__init__( - address, interface, async, 10.0 if timeout is None else timeout, debug) - - def _build_handle_dict(self): - self._primary_services = { - uuid.UUID(str(x.uuid)): (x.hndStart, x.hndEnd) - for x in self.requester.getServices()} - self._characteristics_cache = { - uuid.UUID(str(x.uuid)): (x.valHandle, x.properties, x.valHandle + 1) - for x in self.requester.getCharacteristics()} - - @property - def _is_connected(self): - try: - status = self._peripheral.status().get('state') is not None - except BTLEException: - status = False - except Exception: - status = False - return status - - @property - def initialized(self): - #self.requester.waitForNotifications(0.1) - return self._initialization_status - - @property - def requester(self): - """Property handling `Peripheral` and its connection. - - :return: The connected GattRequester instance. - :rtype: :class:`bluepy.btle.Peripheral` - - """ - - if self._peripheral is None: - log.info("Creating new BluePy Peripheral...") - self._peripheral = Peripheral() - - if not self._is_connected: - log.debug("Connecting BluePy Peripheral...") - self._peripheral.connect( - self._address, addrType=ADDR_TYPE_RANDOM, - iface=str(self._interface).replace('hci', '')) - - if not self._is_connected: - raise PyMetaWearConnectionTimeout( - "Could not establish a connection to {0}.".format(self._address)) - - return self._peripheral - - def disconnect(self): - """Disconnect.""" - if self._peripheral is not None and self._is_connected: - self._peripheral.disconnect() - self._peripheral = None - - def _subscribe(self, characteristic_uuid, callback): - # Subscribe to Notify Characteristic. - handles = self._characteristics_cache.get(characteristic_uuid) - c = Characteristic(self._peripheral, characteristic_uuid, *handles) - bytes_to_send = str(bytearray([0x01, 0x00])) - response = c.write(bytes_to_send, withResponse=True) - self.requester.setDelegate(BluepyDelegate(callback)) - return response - - def _subscription_loop(self): - - while True: - try: - log.debug("Waiting for notification") - self.requester.waitForNotifications(1.0) - except BTLEException as e: - log.error("Error waiting: {0}".format(e)) - pass - - # Read and Write methods - - def read_gatt_char_by_uuid(self, characteristic_uuid): - """Read the desired data from the MetaWear board - using bluepy backend. - - :param uuid.UUID characteristic_uuid: Characteristic UUID to read from. - :return: The read data. - :rtype: str - - """ - handle = self.get_handle(characteristic_uuid) - return self.requester.readCharacteristic(handle) - - def write_gatt_char_by_uuid(self, characteristic_uuid, data_to_send): - """Write the desired data to the MetaWear board - using bluepy backend. - - :param uuid.UUID characteristic_uuid: Characteristic UUID to write to. - :param str data_to_send: Data to send. - - """ - handle = self.get_handle(characteristic_uuid) - if not isinstance(data_to_send, string_types): - data_to_send = data_to_send.decode('latin1') - self.requester.writeCharacteristic(handle, data_to_send) - - def get_handle(self, characteristic_uuid, notify_handle=False): - """Get handle for a characteristic UUID. - - :param uuid.UUID characteristic_uuid: The UUID for the characteristic to look up. - :param bool notify_handle: - :return: The handle for this UUID. - :rtype: int - - """ - if isinstance(characteristic_uuid, string_types): - characteristic_uuid = characteristic_uuid.UUID( - characteristic_uuid) - handle = self._characteristics_cache.get( - characteristic_uuid, [None, None])[int(notify_handle)] - if handle is None: - raise PyMetaWearException("Incorrect characteristic.") - else: - return handle - - @staticmethod - def mbl_mw_command_to_input(command, length): - return bytes(bytearray([command[i] for i in range_(length)])) - - @staticmethod - def read_response_to_str(response): - return create_string_buffer(bytearray_to_str(response), len(response)) - - @staticmethod - def notify_response_to_str(response): - bs = bytearray_to_str(response) - return create_string_buffer(bs, len(bs)) - - def sleep(self, t): - self.requester.waitForNotifications(t) diff --git a/pymetawear/backends/pybluez/__init__.py b/pymetawear/backends/pybluez.py similarity index 67% rename from pymetawear/backends/pybluez/__init__.py rename to pymetawear/backends/pybluez.py index 189be07..924135e 100644 --- a/pymetawear/backends/pybluez/__init__.py +++ b/pymetawear/backends/pybluez.py @@ -1,6 +1,8 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- """ +PyBlueZ backend +--------------- .. moduleauthor:: hbldh Created on 2016-04-02 @@ -9,7 +11,6 @@ from __future__ import division from __future__ import print_function -#from __future__ import unicode_literals from __future__ import absolute_import import time @@ -17,7 +18,12 @@ from ctypes import create_string_buffer import logging -from bluetooth.ble import GATTRequester, GATTResponse +try: + from bluetooth.ble import GATTRequester, GATTResponse + _import_failure = None +except ImportError as e: + GATTRequester = object + _import_failure = e from pymetawear.exceptions import PyMetaWearException, PyMetaWearConnectionTimeout from pymetawear.compat import range_, string_types, bytearray_to_str @@ -44,7 +50,11 @@ class PyBluezBackend(BLECommunicationBackend): `gattlib `_ for BLE communication. """ - def __init__(self, address, interface=None, async=True, timeout=None, debug=False): + def __init__(self, address, interface=None, timeout=None, debug=False): + if _import_failure is not None: + raise PyMetaWearException( + "pybluez[ble] package error: {0}".format(_import_failure)) + self.name = 'pybluez/gattlib' self._primary_services = {} self._characteristics_cache = {} self._response = GATTResponse() @@ -52,7 +62,7 @@ def __init__(self, address, interface=None, async=True, timeout=None, debug=Fals log.setLevel(logging.DEBUG) super(PyBluezBackend, self).__init__( - address, interface, async, 10.0 if timeout is None else timeout, debug) + address, interface, 10.0 if timeout is None else timeout, debug) def _build_handle_dict(self): self._primary_services = {uuid.UUID(x.get('uuid')): (x.get('start'), x.get('end')) @@ -60,6 +70,12 @@ def _build_handle_dict(self): self._characteristics_cache = {uuid.UUID(x.get('uuid')): (x.get('value_handle'), x.get('value_handle') + 1) for x in self.requester.discover_characteristics()} + @property + def is_connected(self): + if self._requester is not None: + return self._requester.is_connected() + return False + @property def requester(self): """Property handling `GattRequester` and its connection. @@ -68,28 +84,39 @@ def requester(self): :rtype: :class:`bluetooth.ble.GATTRequester` """ - if self._requester is None: + self.connect() + + if not self.is_connected: + raise PyMetaWearConnectionTimeout( + "PyBluezBackend: Connection to {0} lost...".format(self._address)) + + return self._requester - log.info("Creating new GATTRequester...") - self._requester = Requester(self.handle_notify_char_output, self._address, + def connect(self, clean_connect=False): + if self.is_connected: + return + + if clean_connect or self._requester is None: + log.info("PyBluezBackend: Creating new GATTRequester...") + self._requester = Requester(self.handle_notify_char_output, + self._address, False, self._interface) + log.info("PyBluezBackend: Connecting GATTRequester...") + self._requester.connect(wait=False, channel_type='random') + # Using manual waiting since gattlib's `wait` keyword does not work. + t = 0.0 + t_step = 0.25 + while not self._requester.is_connected() and t < self._timeout: + t += t_step + time.sleep(t_step) + if not self._requester.is_connected(): - log.info("Connecting GATTRequester...") - self._requester.connect(wait=False, channel_type='random') - # Using manual waiting since gattlib's `wait` keyword does not work. - t = 0.0 - t_step = 0.25 - while not self._requester.is_connected() and t < self._timeout: - t += t_step - time.sleep(t_step) - - if not self._requester.is_connected(): - raise PyMetaWearConnectionTimeout( - "Could not establish a connection to {0}.".format(self._address)) + raise PyMetaWearConnectionTimeout( + "PyBluezBackend: Could not establish a connection to {0}.".format(self._address)) - return self._requester + super(PyBluezBackend, self).connect() def disconnect(self): """Disconnect.""" @@ -106,7 +133,7 @@ def _subscribe(self, characteristic_uuid, callback): # Read and Write methods - def read_gatt_char_by_uuid(self, characteristic_uuid): + def read_gatt_char_by_uuid(self, characteristic): """Read the desired data from the MetaWear board using pybluez/gattlib backend. @@ -115,9 +142,10 @@ def read_gatt_char_by_uuid(self, characteristic_uuid): :rtype: str """ + characteristic_uuid = self.get_uuid(characteristic) return self.requester.read_by_uuid(str(characteristic_uuid))[0] - def write_gatt_char_by_uuid(self, characteristic_uuid, data_to_send): + def write_gatt_char_by_uuid(self, characteristic, command, length): """Write the desired data to the MetaWear board using pybluez/gattlib backend. @@ -125,7 +153,9 @@ def write_gatt_char_by_uuid(self, characteristic_uuid, data_to_send): :param str data_to_send: Data to send. """ + characteristic_uuid = self.get_uuid(characteristic) handle = self.get_handle(characteristic_uuid) + data_to_send = bytes(bytearray([command[i] for i in range_(length)])) if not isinstance(data_to_send, string_types): data_to_send = data_to_send.decode('latin1') self.requester.write_by_handle_async(handle, data_to_send, self._response) @@ -149,15 +179,5 @@ def get_handle(self, characteristic_uuid, notify_handle=False): else: return handle - @staticmethod - def mbl_mw_command_to_input(command, length): - return bytes(bytearray([command[i] for i in range_(length)])) - - @staticmethod - def read_response_to_str(response): + def _response_2_string_buffer(self, response): return create_string_buffer(bytearray_to_str(response), len(response)) - - @staticmethod - def notify_response_to_str(response): - bs = bytearray_to_str(response) - return create_string_buffer(bs, len(bs)) diff --git a/pymetawear/backends/pygatt.py b/pymetawear/backends/pygatt.py new file mode 100644 index 0000000..96cf9cb --- /dev/null +++ b/pymetawear/backends/pygatt.py @@ -0,0 +1,148 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +""" +PyGATT backend +-------------- + +.. moduleauthor:: hbldh +Created on 2016-04-02 + +""" + +from __future__ import division +from __future__ import print_function +from __future__ import absolute_import + +import logging + +from ctypes import create_string_buffer +try: + from pygatt import BLEAddressType + from pygatt.backends.gatttool import gatttool + _import_failure = None +except ImportError as e: + _import_failure = e + +from pymetawear.exceptions import PyMetaWearException, PyMetaWearConnectionTimeout +from pymetawear.compat import range_ +from pymetawear.backends import BLECommunicationBackend + +__all__ = ["PyGattBackend"] + +log = logging.getLogger(__name__) + + +class PyGattBackend(BLECommunicationBackend): + """ + Backend using `pygatt `_ + for BLE communication. + """ + + def __init__(self, address, interface=None, timeout=None, debug=False): + if _import_failure is not None: + raise PyMetaWearException( + "pygatt[GATTTOOL] package error: {0}".format(_import_failure)) + + self.name = 'pygatt' + + log.info("PyGattBackend: Creating new GATTToolBackend and starting GATTtool process...") + self._backend = None + + if debug: + log.setLevel(logging.DEBUG) + + super(PyGattBackend, self).__init__( + address, interface, + gatttool.DEFAULT_CONNECT_TIMEOUT_S if timeout is None else timeout, debug) + + @property + def is_connected(self): + if self._requester is not None: + return self._requester._connected + return False + + @property + def requester(self): + """Property handling the backend's device instance and its connection. + + :return: A connected ``pygatt`` BLE device instance. + :rtype: :class:`pygatt.device.BLEDevice` + + """ + if self._requester is None: + self.connect() + + if not self.is_connected: + raise PyMetaWearConnectionTimeout( + "PyGattBackend: Connection to {0} lost...".format( + self._address)) + + return self._requester + + def connect(self, clean_connect=False): + """Connect the GATTool process to the MetaWear board.""" + if self.is_connected: + return + + self._backend = gatttool.GATTToolBackend(hci_device=self._interface) + self._backend.start(reset_on_start=clean_connect) + + log.info("PyGattBackend: Connecting to {0} using GATTTool...".format( + self._address)) + self._requester = self._backend.connect( + self._address, timeout=self._timeout, + address_type=BLEAddressType.random) + + super(PyGattBackend, self).connect() + + def disconnect(self): + """Disconnect via the GATTTool process and terminate the + interactive prompt. + + We can use the `stop` method since only one client can be + connected to one GATTTool backend. + + """ + if self._backend is not None and self._backend: + self._backend.stop() + self._backend = None + self._requester = None + + def _subscribe(self, characteristic_uuid, callback): + return self.requester.subscribe(str(characteristic_uuid), callback) + + def read_gatt_char_by_uuid(self, characteristic): + """Read the desired data from the MetaWear board using pygatt backend. + + :param GattCharacteristic characteristic: :class:`ctypes.POINTER` to a GattCharacteristic. + :return: The read data. + :rtype: str + + """ + return self.requester.char_read(str(self.get_uuid(characteristic))) + + def write_gatt_char_by_uuid(self, characteristic, command, length): + """Write the desired data to the MetaWear board using pygatt backend. + + :param uuid.UUID characteristic: The UUID to the characteristic + to write to. + :param POINTER(c_ubyte) command: Data to send. + :param int length: Number of characters in the command. + + """ + data_to_send = bytearray([command[i] for i in range_(length)]) + self.requester.char_write(str(self.get_uuid(characteristic)), data_to_send) + + def get_handle(self, characteristic_uuid, notify_handle=False): + """Get handle from characteristic UUID. + + :param uuid.UUID characteristic_uuid: The UUID to find handle to. + :param bool notify_handle: Set to true if subscription. + :return: Integer handle. + :rtype: int + + """ + return self.requester.get_handle(characteristic_uuid) + int(notify_handle) + + def _response_2_string_buffer(self, response): + return create_string_buffer(bytes(response), len(response)) diff --git a/pymetawear/backends/pygatt/__init__.py b/pymetawear/backends/pygatt/__init__.py deleted file mode 100644 index bb69ee3..0000000 --- a/pymetawear/backends/pygatt/__init__.py +++ /dev/null @@ -1,130 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- -""" - -.. moduleauthor:: hbldh -Created on 2016-04-02 - -""" - -from __future__ import division -from __future__ import print_function -#from __future__ import unicode_literals -from __future__ import absolute_import - -import uuid -import logging - -from ctypes import create_string_buffer -from pygatt import BLEAddressType -from pygatt.backends.gatttool import gatttool - -from pymetawear.exceptions import PyMetaWearException, PyMetaWearConnectionTimeout -from pymetawear.compat import range_ -from pymetawear.backends import BLECommunicationBackend - -__all__ = ["PyGattBackend"] - -log = logging.getLogger(__name__) - - -class PyGattBackend(BLECommunicationBackend): - """ - Backend using `pygatt `_ - for BLE communication. - """ - - def __init__(self, address, interface=None, async=True, timeout=None, debug=False): - - self._backend = None - if debug: - log.setLevel(logging.DEBUG) - - super(PyGattBackend, self).__init__( - address, interface, async, - gatttool.DEFAULT_CONNECT_TIMEOUT_S if timeout is None else timeout, - debug) - - @property - def requester(self): - """Property handling the backend's device instance and its connection. - - :return: A connected ``pygatt`` BLE device instance. - :rtype: :class:`pygatt.device.BLEDevice` - - """ - if self._requester is None: - - log.info("Creating new GATTToolBackend and starting GATTtool process...") - self._backend = gatttool.GATTToolBackend(hci_device=self._interface) - self._backend.start(reset_on_start=False) - log.info("Connecting GATTTool...") - self._requester = self._backend.connect( - self._address, timeout=self._timeout, address_type=BLEAddressType.random) - - if not self.requester._connected: - raise PyMetaWearConnectionTimeout( - "Could not establish a connection to {0}.".format( - self._address)) - - return self._requester - - def disconnect(self): - """Disconnect via the GATTTool process and terminate the - interactive prompt. - - We can use the `stop` method since only one client can be - connected to one GATTTool backend. - - """ - if self._backend is not None and self._backend: - self._backend.stop() - self._backend = None - self._requester = None - - def _subscribe(self, characteristic_uuid, callback): - return self.requester.subscribe(str(characteristic_uuid), callback) - - def read_gatt_char_by_uuid(self, characteristic_uuid): - """Read the desired data from the MetaWear board using pygatt backend. - - :param pymetawear.mbientlab.metawear.core.GattCharacteristic - characteristic: :class:`ctypes.POINTER` to a GattCharacteristic. - :return: The read data. - :rtype: str - - """ - return self.requester.char_read(str(characteristic_uuid)) - - def write_gatt_char_by_uuid(self, characteristic_uuid, data_to_send): - """Write the desired data to the MetaWear board using pygatt backend. - - :param uuid.UUID characteristic_uuid: The UUID to the characteristic - to write to. - :param str data_to_send: Data to send. - - """ - self.requester.char_write(str(characteristic_uuid), data_to_send) - - def get_handle(self, uuid, notify_handle=False): - """Get handle from characteristic UUID. - - :param uuid.UUID uuid: The UUID to find handle to. - :param bool notify_handle: - :return: Integer handle. - :rtype: int - - """ - return self.requester.get_handle(uuid) + int(notify_handle) - - @staticmethod - def mbl_mw_command_to_input(command, length): - return bytearray([command[i] for i in range_(length)]) - - @staticmethod - def read_response_to_str(response): - return create_string_buffer(bytes(response), len(response)) - - @staticmethod - def notify_response_to_str(response): - return create_string_buffer(bytes(response), len(response)) diff --git a/pymetawear/client.py b/pymetawear/client.py index 352216b..b8fd411 100644 --- a/pymetawear/client.py +++ b/pymetawear/client.py @@ -8,80 +8,23 @@ """ +from __future__ import absolute_import from __future__ import division from __future__ import print_function -# from __future__ import unicode_literals -from __future__ import absolute_import -import os -import time -import subprocess -import signal import logging +import os from pymetawear import libmetawear, specs, add_stream_logger -from pymetawear.exceptions import * from pymetawear import modules +from pymetawear.backends.pybluez import PyBluezBackend +from pymetawear.backends.pygatt import PyGattBackend +from pymetawear.exceptions import PyMetaWearException, PyMetaWearConnectionTimeout from pymetawear.mbientlab.metawear.core import Status -try: - from pymetawear.backends.pygatt import PyGattBackend -except ImportError as e: - PyGattBackend = e -try: - from pymetawear.backends.pybluez import PyBluezBackend -except ImportError as e: - PyBluezBackend = e -try: - from pymetawear.backends.bluepy import BluepyBackend -except ImportError as e: - BluepyBackend = e log = logging.getLogger(__name__) -def discover_devices(timeout=5): - """Discover Bluetooth Low Energy Devices nearby on Linux - - Using ``hcitool`` from Bluez in subprocess, which requires root privileges. - However, ``hcitool`` can be allowed to do scan without elevated permission. - - .. code-block:: bash - - $ sudo apt-get install libcap2-bin - - installs linux capabilities manipulation tools. - - .. code-block:: bash - - $ sudo setcap 'cap_net_raw,cap_net_admin+eip' `which hcitool` - - sets the missing capabilities on the executable quite like the setuid bit. - - **References:** - - * `StackExchange, hcitool without sudo `_ - * `StackOverflow, hcitool lescan with timeout `_ - - :param int timeout: Duration of scanning. - :return: List of tuples with `(address, name)`. - :rtype: list - - """ - p = subprocess.Popen(["hcitool", "lescan"], stdout=subprocess.PIPE, - stderr=subprocess.PIPE) - time.sleep(timeout) - os.kill(p.pid, signal.SIGINT) - out, err = p.communicate() - if len(out) == 0 and len(err) > 0: - if err == b'Set scan parameters failed: Operation not permitted\n': - raise PyMetaWearException("Missing capabilites for hcitool!") - if err == b'Set scan parameters failed: Input/output error\n': - raise PyMetaWearException("Could not perform scan.") - ble_devices = list(set([tuple(x.split(' ')) for x in - filter(None, out.decode('utf8').split('\n')[1:])])) - return ble_devices - - class MetaWearClient(object): """A MetaWear communication client. @@ -96,13 +39,15 @@ class MetaWearClient(object): BLE communication backend that should be used. :param float timeout: Timeout for connecting to the MetaWear board. If ``None`` timeout defaults to the backend default. + :param bool connect: If client should connect automatically, or wait for + explicit :py:meth:`~MetaWearClient.connect` call. Default is ``True``. :param bool debug: If printout of all sent and received data should be done. """ def __init__(self, address, backend='pygatt', - interface='hci0', timeout=None, debug=False): + interface='hci0', timeout=None, connect=True, debug=False): """Constructor.""" self._address = address self._debug = debug @@ -122,87 +67,36 @@ def __init__(self, address, backend='pygatt', timeout = None if backend == 'pygatt': - if isinstance(PyGattBackend, Exception): - raise PyMetaWearException( - "pygatt[GATTTOOL] package error :{0}".format(PyGattBackend)) self._backend = PyGattBackend( self._address, interface=interface, timeout=timeout, debug=debug) elif backend == 'pybluez': - if isinstance(PyBluezBackend, Exception): - raise PyMetaWearException( - "pybluez[ble] package error: {0}".format(PyBluezBackend)) self._backend = PyBluezBackend( self._address, interface=interface, timeout=timeout, debug=debug) - elif backend == 'bluepy': - if isinstance(BluepyBackend, Exception): - raise PyMetaWearException( - "bluepy package error: {0}".format(BluepyBackend)) - self._backend = BluepyBackend( - self._address, interface=interface, - timeout=timeout, debug=debug) else: raise PyMetaWearException("Unknown backend: {0}".format(backend)) - if self._debug: - log.debug("Waiting for MetaWear board to be fully initialized...") - - while (not self.backend.initialized) and (not - libmetawear.mbl_mw_metawearboard_is_initialized(self.board)): - self.backend.sleep(0.1) - - # Check if initialization has been completed successfully. - if self.backend.initialized != Status.OK: - if self.backend._initialization_status == Status.ERROR_TIMEOUT: - raise PyMetaWearConnectionTimeout("libmetawear initialization status 16: Timeout") - else: - raise PyMetaWearException("libmetawear initialization status {0}".format( - self.backend._initialization_status)) - - # Read out firmware and model version. - self.firmware_version = tuple( - [int(x) for x in self.backend.read_gatt_char_by_uuid( - specs.DEV_INFO_FIRMWARE_CHAR[1]).decode().split('.')]) - self.model_version = int(self.backend.read_gatt_char_by_uuid( - specs.DEV_INFO_MODEL_CHAR[1]).decode()) - - # Initialize module classes. - self.accelerometer = modules.AccelerometerModule( - self.board, - libmetawear.mbl_mw_metawearboard_lookup_module( - self.board, modules.Modules.MBL_MW_MODULE_ACCELEROMETER), - debug=self._debug) - self.gyroscope = modules.GyroscopeModule( - self.board, - libmetawear.mbl_mw_metawearboard_lookup_module( - self.board, modules.Modules.MBL_MW_MODULE_GYRO), - debug=self._debug) - self.magnetometer = modules.MagnetometerModule( - self.board, - libmetawear.mbl_mw_metawearboard_lookup_module( - self.board, modules.Modules.MBL_MW_MODULE_MAGNETOMETER), - debug=self._debug) - self.barometer = modules.BarometerModule( - self.board, - libmetawear.mbl_mw_metawearboard_lookup_module( - self.board, modules.Modules.MBL_MW_MODULE_BAROMETER), - debug=self._debug) - self.ambient_light = modules.AmbientLightModule( - self.board, - libmetawear.mbl_mw_metawearboard_lookup_module( - self.board, modules.Modules.MBL_MW_MODULE_AMBIENT_LIGHT), - debug=self._debug) - self.switch = modules.SwitchModule(self.board, debug=self._debug) - self.battery = modules.BatteryModule(self.board, debug=self._debug) - self.temperature = modules.TemperatureModule( - self.board, debug=self._debug) - self.haptic = modules.HapticModule(self.board, debug=self._debug) - self.led = modules.LEDModule(self.board, debug=self._debug) + self.firmware_version = None + self.model_version = None + self.accelerometer = None + self.gyroscope = None + self.magnetometer = None + self.barometer = None + self.ambient_light = None + self.switch = None + self.battery = None + self.temperature = None + self.haptic = None + self.led = None + + if connect: + self.connect() def __str__(self): - return "MetaWearClient, {0}, Model: {1}, Firmware: {2}.{3}.{4}".format( - self._address, self.model_version, *self.firmware_version) + return "MetaWearClient, {0}, Model: {1}, Firmware: {2}".format( + self.backend, self.model_version, + ".".join([str(i) for i in self.firmware_version]) if self.firmware_version else None) def __repr__(self): return "".format(self._address) @@ -221,8 +115,42 @@ def backend(self): def board(self): return self.backend.board + def connect(self, clean_connect=False): + """Connect this client to the MetaWear device. + + :param bool clean_connect: If old backend components should be replaced. + Default is ``False``. + + """ + if self.backend.is_connected: + return + self.backend.connect(clean_connect=clean_connect) + + if self._debug: + log.debug("Waiting for MetaWear board to be fully initialized...") + + while not self.backend.initialized: + self.backend.sleep(0.1) + + # Check if initialization has been completed successfully. + if self.backend.initialization_status != Status.OK: + if self.backend.initialization_status == Status.ERROR_TIMEOUT: + raise PyMetaWearConnectionTimeout("libmetawear initialization status 16: Timeout") + else: + raise PyMetaWearException("libmetawear initialization status {0}".format( + self.backend.initialization_status)) + + # Read out firmware and model version. + self.firmware_version = tuple( + [int(x) for x in self.backend.read_gatt_char_by_uuid( + specs.DEV_INFO_FIRMWARE_CHAR[1]).decode().split('.')]) + self.model_version = int(self.backend.read_gatt_char_by_uuid( + specs.DEV_INFO_MODEL_CHAR[1]).decode()) + + self._initialize_modules() + def disconnect(self): - """Disconnects this client from the MetaWear board.""" + """Disconnects this client from the MetaWear device.""" libmetawear.mbl_mw_metawearboard_tear_down(self.board) libmetawear.mbl_mw_metawearboard_free(self.board) self.backend.disconnect() @@ -250,3 +178,36 @@ def _download_log(self, n_notifies): def soft_reset(self): """Issues a soft reset to the board.""" libmetawear.mbl_mw_debug_reset(self.board) + + def _initialize_modules(self): + self.accelerometer = modules.AccelerometerModule( + self.board, + libmetawear.mbl_mw_metawearboard_lookup_module( + self.board, modules.Modules.MBL_MW_MODULE_ACCELEROMETER), + debug=self._debug) + self.gyroscope = modules.GyroscopeModule( + self.board, + libmetawear.mbl_mw_metawearboard_lookup_module( + self.board, modules.Modules.MBL_MW_MODULE_GYRO), + debug=self._debug) + self.magnetometer = modules.MagnetometerModule( + self.board, + libmetawear.mbl_mw_metawearboard_lookup_module( + self.board, modules.Modules.MBL_MW_MODULE_MAGNETOMETER), + debug=self._debug) + self.barometer = modules.BarometerModule( + self.board, + libmetawear.mbl_mw_metawearboard_lookup_module( + self.board, modules.Modules.MBL_MW_MODULE_BAROMETER), + debug=self._debug) + self.ambient_light = modules.AmbientLightModule( + self.board, + libmetawear.mbl_mw_metawearboard_lookup_module( + self.board, modules.Modules.MBL_MW_MODULE_AMBIENT_LIGHT), + debug=self._debug) + self.switch = modules.SwitchModule(self.board, debug=self._debug) + self.battery = modules.BatteryModule(self.board, debug=self._debug) + self.temperature = modules.TemperatureModule( + self.board, debug=self._debug) + self.haptic = modules.HapticModule(self.board, debug=self._debug) + self.led = modules.LEDModule(self.board, debug=self._debug) diff --git a/pymetawear/compat.py b/pymetawear/compat.py index 85ad4aa..e604962 100644 --- a/pymetawear/compat.py +++ b/pymetawear/compat.py @@ -10,7 +10,6 @@ from __future__ import division from __future__ import print_function -# from __future__ import unicode_literals from __future__ import absolute_import diff --git a/pymetawear/discover.py b/pymetawear/discover.py new file mode 100644 index 0000000..1645e12 --- /dev/null +++ b/pymetawear/discover.py @@ -0,0 +1,102 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +""" +discover +----------- + +:copyright: 2016-11-29 by hbldh + +""" + +from __future__ import division +from __future__ import print_function +from __future__ import absolute_import + +import os +import signal +import subprocess +import time + +from pymetawear.exceptions import PyMetaWearException + +try: + input_fcn = raw_input +except NameError: + input_fcn = input + + +def discover_devices(timeout=5): + """Discover Bluetooth Low Energy Devices nearby on Linux + + Using ``hcitool`` from Bluez in subprocess, which requires root privileges. + However, ``hcitool`` can be allowed to do scan without elevated permission. + + Install linux capabilities manipulation tools: + + .. code-block:: bash + + $ sudo apt-get install libcap2-bin + + Sets the missing capabilities on the executable quite like the setuid bit: + + .. code-block:: bash + + $ sudo setcap 'cap_net_raw,cap_net_admin+eip' `which hcitool` + + **References:** + + * `StackExchange, hcitool without sudo `_ + * `StackOverflow, hcitool lescan with timeout `_ + + :param int timeout: Duration of scanning. + :return: List of tuples with `(address, name)`. + :rtype: list + + """ + p = subprocess.Popen(["hcitool", "lescan"], stdout=subprocess.PIPE, + stderr=subprocess.PIPE) + time.sleep(timeout) + os.kill(p.pid, signal.SIGINT) + out, err = p.communicate() + if len(out) == 0 and len(err) > 0: + if err == b'Set scan parameters failed: Operation not permitted\n': + raise PyMetaWearException("Missing capabilites for hcitool!") + if err == b'Set scan parameters failed: Input/output error\n': + raise PyMetaWearException("Could not perform scan.") + ble_devices = list(set([tuple(x.split(' ')) for x in + filter(None, out.decode('utf8').split('\n')[1:])])) + filtered_devices = {} + for d in ble_devices: + if d[0] not in filtered_devices: + filtered_devices[d[0]] = d[1] + else: + if filtered_devices.get(d[0]) == '(unknown)': + filtered_devices[d[0]] = d[1] + + return [(k, v) for k, v in filtered_devices.items()] + + +def select_device(timeout=3): + """Run `discover_devices` and display a list to select from. + + :param int timeout: Duration of scanning. + :return: The selected device's address. + :rtype: str + + """ + print("Discovering nearby Bluetooth Low Energy devices...") + ble_devices = discover_devices(timeout=timeout) + if len(ble_devices) > 1: + for i, d in enumerate(ble_devices): + print("[{0}] - {1}: {2}".format(i + 1, *d)) + s = input("Which device do you want to connect to? ") + if int(s) <= (i + 1): + address = ble_devices[int(s) - 1][0] + else: + raise ValueError("Incorrect selection. Aborting...") + elif len(ble_devices) == 1: + address = ble_devices[0][0] + print("Found only one device: {0}: {1}.".format(*ble_devices[0][::-1])) + else: + raise ValueError("Did not detect any BLE devices.") + return address diff --git a/pymetawear/exceptions.py b/pymetawear/exceptions.py index a41d6ec..49bb5a1 100644 --- a/pymetawear/exceptions.py +++ b/pymetawear/exceptions.py @@ -9,7 +9,6 @@ from __future__ import division from __future__ import print_function -#from __future__ import unicode_literals from __future__ import absolute_import diff --git a/pymetawear/modules/__init__.py b/pymetawear/modules/__init__.py index 76ae247..44cb807 100644 --- a/pymetawear/modules/__init__.py +++ b/pymetawear/modules/__init__.py @@ -11,7 +11,6 @@ from __future__ import division from __future__ import print_function -from __future__ import unicode_literals from __future__ import absolute_import from .base import PyMetaWearModule, Modules @@ -25,3 +24,12 @@ from .magnetometer import MagnetometerModule from .switch import SwitchModule from .temperature import TemperatureModule + +__all__ = [ + "PyMetaWearModule", "Modules", + "AccelerometerModule", "AmbientLightModule", + "BarometerModule", "BatteryModule", + "GyroscopeModule", "HapticModule", + "LEDModule", "MagnetometerModule", + "SwitchModule", "TemperatureModule" +] diff --git a/pymetawear/modules/accelerometer.py b/pymetawear/modules/accelerometer.py index b408a21..336ecc6 100644 --- a/pymetawear/modules/accelerometer.py +++ b/pymetawear/modules/accelerometer.py @@ -10,7 +10,6 @@ from __future__ import division from __future__ import print_function -from __future__ import unicode_literals from __future__ import absolute_import import re @@ -56,7 +55,7 @@ def __init__(self, board, module_id, debug=False): # Parse possible output data rates for this accelerometer. self.odr = {".".join(re.search( '^ODR_([0-9]+)\_*([0-9]*)HZ', k).groups()): - getattr(self.acc_class, k, None) for k in filter( + getattr(self.acc_class, k, None) for k in filter( lambda x: x.startswith('ODR'), vars(self.acc_class))} # Parse possible output data ranges for this accelerometer. @@ -66,7 +65,7 @@ def __init__(self, board, module_id, debug=False): else: acc_class = self.acc_class self.fsr = {float(re.search('^FSR_([0-9]+)G', k).groups()[0]): - getattr(acc_class, k, None) for k in + getattr(acc_class, k, None) for k in filter(lambda x: x.startswith('FSR'), vars(acc_class))} if debug: @@ -98,23 +97,23 @@ def data_signal(self): return libmetawear.mbl_mw_acc_get_acceleration_data_signal(self.board) def _get_odr(self, value): - sorted_ord_keys = sorted(self.odr.keys(), key=lambda x:(float(x))) + sorted_ord_keys = sorted(self.odr.keys(), key=lambda x: (float(x))) diffs = [abs(value - float(k)) for k in sorted_ord_keys] min_diffs = min(diffs) if min_diffs > 0.5: - raise ValueError("Requested ODR ({0}) was not part of " - "possible values: {1}".format( - value, [float(x) for x in sorted_ord_keys])) + raise ValueError( + "Requested ODR ({0}) was not part of possible values: {1}".format( + value, [float(x) for x in sorted_ord_keys])) return float(value) def _get_fsr(self, value): - sorted_ord_keys = sorted(self.fsr.keys(), key=lambda x:(float(x))) + sorted_ord_keys = sorted(self.fsr.keys(), key=lambda x: (float(x))) diffs = [abs(value - float(k)) for k in sorted_ord_keys] min_diffs = min(diffs) if min_diffs > 0.1: - raise ValueError("Requested FSR ({0}) was not part of " - "possible values: {1}".format( - value, [x for x in sorted(self.fsr.keys())])) + raise ValueError( + "Requested FSR ({0}) was not part of possible values: {1}".format( + value, [x for x in sorted(self.fsr.keys())])) return float(value) def get_current_settings(self): diff --git a/pymetawear/modules/ambientlight.py b/pymetawear/modules/ambientlight.py index 963b4bd..fda69f4 100644 --- a/pymetawear/modules/ambientlight.py +++ b/pymetawear/modules/ambientlight.py @@ -10,7 +10,6 @@ from __future__ import division from __future__ import print_function -from __future__ import unicode_literals from __future__ import absolute_import import re @@ -113,9 +112,9 @@ def _get_gain(self, value): diffs = [abs(value - float(k)) for k in sorted_ord_keys] min_diffs = min(diffs) if min_diffs > 0.5: - raise ValueError("Requested gain ({0}) was not part of " - "possible values: {1}".format( - value, [float(x) for x in sorted_ord_keys])) + raise ValueError( + "Requested gain ({0}) was not part of possible values: {1}".format( + value, [float(x) for x in sorted_ord_keys])) k = int(sorted_ord_keys[diffs.index(min_diffs)]) return self._gain.get(k) @@ -125,9 +124,9 @@ def _get_integration_time(self, value): diffs = [abs(value - float(k)) for k in sorted_ord_keys] min_diffs = min(diffs) if min_diffs > 0.5: - raise ValueError("Requested integration time ({0}) was not part of " - "possible values: {1}".format( - value, [float(x) for x in sorted_ord_keys])) + raise ValueError( + "Requested integration time ({0}) was not part of possible values: {1}".format( + value, [float(x) for x in sorted_ord_keys])) k = int(sorted_ord_keys[diffs.index(min_diffs)]) return self._integration_time.get(k) @@ -137,9 +136,9 @@ def _get_measurement_rate(self, value): diffs = [abs(value - float(k)) for k in sorted_ord_keys] min_diffs = min(diffs) if min_diffs > 0.5: - raise ValueError("Requested measurement rate ({0}) was not part of " - "possible values: {1}".format( - value, [float(x) for x in sorted_ord_keys])) + raise ValueError( + "Requested measurement rate ({0}) was not part of possible values: {1}".format( + value, [float(x) for x in sorted_ord_keys])) k = int(sorted_ord_keys[diffs.index(min_diffs)]) return self._measurement_rate.get(k) @@ -237,29 +236,29 @@ def stop(self): def ambient_light_data(func): @wraps(func) def wrapper(data): - if (data.contents.type_id == DataTypeId.UINT32): + if data.contents.type_id == DataTypeId.UINT32: data_ptr = cast(data.contents.value, POINTER(c_uint)) func(int(data_ptr.contents.value)) - elif (data.contents.type_id == DataTypeId.FLOAT): - data_ptr = cast(data.contents.value, POINTER(c_float)); + elif data.contents.type_id == DataTypeId.FLOAT: + data_ptr = cast(data.contents.value, POINTER(c_float)) func(float(data_ptr.contents.value)) - elif (data.contents.type_id == DataTypeId.CARTESIAN_FLOAT): + elif data.contents.type_id == DataTypeId.CARTESIAN_FLOAT: data_ptr = cast(data.contents.value, POINTER(CartesianFloat)) func((data_ptr.contents.x, data_ptr.contents.y, data_ptr.contents.z)) - elif (data.contents.type_id == DataTypeId.BATTERY_STATE): + elif data.contents.type_id == DataTypeId.BATTERY_STATE: data_ptr = cast(data.contents.value, POINTER(BatteryState)) func((int(data_ptr.contents.voltage), int(data_ptr.contents.charge))) - elif (data.contents.type_id == DataTypeId.BYTE_ARRAY): + elif data.contents.type_id == DataTypeId.BYTE_ARRAY: data_ptr = cast(data.contents.value, POINTER(c_ubyte * data.contents.length)) data_byte_array = [] for i in range(0, data.contents.length): data_byte_array.append(int(data_ptr.contents[i])) func(data_byte_array) - elif (data.contents.type_id == DataTypeId.TCS34725_ADC): + elif data.contents.type_id == DataTypeId.TCS34725_ADC: data_ptr = cast(data.contents.value, POINTER(Tcs34725ColorAdc)) data_tcs34725_adc = copy.deepcopy(data_ptr.contents) func(data_tcs34725_adc) diff --git a/pymetawear/modules/barometer.py b/pymetawear/modules/barometer.py index 803ca9b..584509d 100644 --- a/pymetawear/modules/barometer.py +++ b/pymetawear/modules/barometer.py @@ -10,7 +10,6 @@ from __future__ import division from __future__ import print_function -from __future__ import unicode_literals from __future__ import absolute_import import re @@ -21,7 +20,7 @@ from pymetawear import libmetawear from pymetawear.exceptions import PyMetaWearException from pymetawear.mbientlab.metawear import sensor -from pymetawear.mbientlab.metawear.core import DataTypeId, CartesianFloat +from pymetawear.mbientlab.metawear.core import DataTypeId from pymetawear.modules.base import PyMetaWearModule log = logging.getLogger(__name__) @@ -51,8 +50,7 @@ def __init__(self, board, module_id, debug=False): if self.barometer_class is not None: self.oversampling = {"".join(re.search( '^OVERSAMPLING_([A-Z\_]*)', k).groups()).lower(): - getattr(sensor.BarometerBosch, k, None) for - k in filter( + getattr(sensor.BarometerBosch, k, None) for k in filter( lambda x: x.startswith('OVERSAMPLING'), vars(sensor.BarometerBosch))} for k in self.oversampling: @@ -61,15 +59,13 @@ def __init__(self, board, module_id, debug=False): self.iir_filter = {"".join(re.search( '^IIR_FILTER_([0-9AVG\_OFF]+)', k).groups()).lower(): - getattr(sensor.BarometerBosch, k, None) for - k in filter( + getattr(sensor.BarometerBosch, k, None) for k in filter( lambda x: x.startswith('IIR_FILTER'), vars(sensor.BarometerBosch))} self.standby_time = {float(".".join(re.search( '^STANDBY_TIME_([0-9]+)\_*([0-9]*)MS', k).groups())): - getattr(self.barometer_class, k, None) for - k in filter( + getattr(self.barometer_class, k, None) for k in filter( lambda x: x.startswith('STANDBY_TIME'), vars(self.barometer_class))} else: @@ -104,34 +100,37 @@ def sensor_name(self): @property def data_signal(self): if self._altitude_data: - return libmetawear.mbl_mw_baro_bosch_get_altitude_data_signal(self.board) + return libmetawear.mbl_mw_baro_bosch_get_altitude_data_signal( + self.board) else: - return libmetawear.mbl_mw_baro_bosch_get_pressure_data_signal(self.board) + return libmetawear.mbl_mw_baro_bosch_get_pressure_data_signal( + self.board) def _get_oversampling(self, value): if value.lower() in self.oversampling: return self.oversampling.get(value.lower()) else: - raise ValueError("Requested oversampling ({0}) was not part of " - "possible values: {1}".format( - value.lower(), self.oversampling.keys())) + raise ValueError( + "Requested oversampling ({0}) was not part of possible values: {1}".format( + value.lower(), self.oversampling.keys())) def _get_iir_filter(self, value): if value.lower() in self.iir_filter: return self.iir_filter.get(value.lower()) else: - raise ValueError("Requested IIR filter ({0}) was not part of " - "possible values: {1}".format( - value.lower(), self.iir_filter.keys())) + raise ValueError( + "Requested IIR filter ({0}) was not part of possible values: {1}".format( + value.lower(), self.iir_filter.keys())) def _get_standby_time(self, value): - sorted_ord_keys = sorted(self.standby_time.keys(), key=lambda x: (float(x))) + sorted_ord_keys = sorted(self.standby_time.keys(), + key=lambda x: (float(x))) diffs = [abs(value - float(k)) for k in sorted_ord_keys] min_diffs = min(diffs) if min_diffs > 0.5: - raise ValueError("Requested standby time ({0}) was not part of " - "possible values: {1}".format( - value, [float(x) for x in sorted_ord_keys])) + raise ValueError( + "Requested standby time ({0}) was not part of possible values: {1}".format( + value, [float(x) for x in sorted_ord_keys])) return float(value) def get_current_settings(self): @@ -140,14 +139,15 @@ def get_current_settings(self): def get_possible_settings(self): return { 'oversampling': [x.lower() for x in sorted( - self.oversampling.keys(), key=lambda x:(x.lower()))], + self.oversampling.keys(), key=lambda x: (x.lower()))], 'iir_filter': [x.lower() for x in sorted( self.iir_filter.keys(), key=lambda x: (x.lower()))], - 'standby_time': [float(x)for x in sorted( + 'standby_time': [float(x) for x in sorted( self.standby_time.keys(), key=lambda x: (float(x)))], } - def set_settings(self, oversampling=None, iir_filter=None, standby_time=None): + def set_settings(self, oversampling=None, iir_filter=None, + standby_time=None): """Set barometer settings. Can be called with 1-3 setting: @@ -179,13 +179,15 @@ def set_settings(self, oversampling=None, iir_filter=None, standby_time=None): if oversampling is not None: oversampling = self._get_oversampling(oversampling) if self._debug: - log.debug("Setting Barometer Oversampling to {0}".format(oversampling)) + log.debug("Setting Barometer Oversampling to {0}".format( + oversampling)) libmetawear.mbl_mw_baro_bosch_set_oversampling( self.board, oversampling) if iir_filter is not None: iir_filter = self._get_iir_filter(iir_filter) if self._debug: - log.debug("Setting Barometer IIR filter to {0}".format(iir_filter)) + log.debug( + "Setting Barometer IIR filter to {0}".format(iir_filter)) libmetawear.mbl_mw_baro_bosch_set_iir_filter( self.board, iir_filter) if standby_time is not None: @@ -249,4 +251,5 @@ def wrapper(data): else: raise PyMetaWearException('Incorrect data type id: {0}'.format( data.contents.type_id)) + return wrapper diff --git a/pymetawear/modules/base.py b/pymetawear/modules/base.py index c89346d..8a333ac 100644 --- a/pymetawear/modules/base.py +++ b/pymetawear/modules/base.py @@ -10,7 +10,6 @@ from __future__ import division from __future__ import print_function -from __future__ import unicode_literals from __future__ import absolute_import import logging diff --git a/pymetawear/modules/battery.py b/pymetawear/modules/battery.py index 08e9e59..ff4196b 100644 --- a/pymetawear/modules/battery.py +++ b/pymetawear/modules/battery.py @@ -10,7 +10,6 @@ from __future__ import division from __future__ import print_function -from __future__ import unicode_literals from __future__ import absolute_import import logging diff --git a/pymetawear/modules/gyroscope.py b/pymetawear/modules/gyroscope.py index bdeac1e..d4a8246 100644 --- a/pymetawear/modules/gyroscope.py +++ b/pymetawear/modules/gyroscope.py @@ -10,13 +10,12 @@ from __future__ import division from __future__ import print_function -from __future__ import unicode_literals from __future__ import absolute_import import re import logging from functools import wraps -from ctypes import c_float, cast, POINTER +from ctypes import cast, POINTER from pymetawear import libmetawear from pymetawear.exceptions import PyMetaWearException @@ -33,6 +32,7 @@ def wrapper(*args, **kwargs): raise PyMetaWearException("There is not Gyroscope " "module of your MetaWear board!") return f(*args, **kwargs) + return wrapper @@ -61,10 +61,10 @@ def __init__(self, board, module_id, debug=False): if self.gyro_class is not None: # Parse possible output data rates for this accelerometer. self.odr = {int(re.search('^ODR_([0-9]+)HZ', k).groups()[0]): - getattr(self.gyro_class, k, None) for k in filter( + getattr(self.gyro_class, k, None) for k in filter( lambda x: x.startswith('ODR'), vars(self.gyro_class))} self.fsr = {int(re.search('^FSR_([0-9]+)DPS', k).groups()[0]): - getattr(self.gyro_class, k, None) for k in + getattr(self.gyro_class, k, None) for k in filter(lambda x: x.startswith('FSR'), vars(self.gyro_class))} @@ -75,7 +75,7 @@ def __str__(self): return "{0} {1}: Data rates (Hz): {2}, Data ranges (dps): {3}".format( self.module_name, self.sensor_name, [float(k) for k in sorted(self.odr.keys(), - key=lambda x:(float(x)))], + key=lambda x: (float(x)))], [k for k in sorted(self.fsr.keys())]) def __repr__(self): @@ -96,29 +96,31 @@ def sensor_name(self): @require_bmi160 def data_signal(self): if self.high_frequency_stream: - return libmetawear.mbl_mw_gyro_bmi160_get_high_freq_rotation_data_signal(self.board) + return libmetawear.mbl_mw_gyro_bmi160_get_high_freq_rotation_data_signal( + self.board) else: - return libmetawear.mbl_mw_gyro_bmi160_get_rotation_data_signal(self.board) + return libmetawear.mbl_mw_gyro_bmi160_get_rotation_data_signal( + self.board) def _get_odr(self, value): - sorted_ord_keys = sorted(self.odr.keys(), key=lambda x:(float(x))) + sorted_ord_keys = sorted(self.odr.keys(), key=lambda x: (float(x))) diffs = [abs(value - float(k)) for k in sorted_ord_keys] min_diffs = min(diffs) if min_diffs > 0.5: - raise ValueError("Requested ODR ({0}) was not part of " - "possible values: {1}".format( - value, [float(x) for x in sorted_ord_keys])) + raise ValueError( + "Requested ODR ({0}) was not part of possible values: {1}".format( + value, [float(x) for x in sorted_ord_keys])) k = int(sorted_ord_keys[diffs.index(min_diffs)]) return self.odr.get(k) def _get_fsr(self, value): - sorted_ord_keys = sorted(self.fsr.keys(), key=lambda x:(float(x))) + sorted_ord_keys = sorted(self.fsr.keys(), key=lambda x: (float(x))) diffs = [abs(value - float(k)) for k in sorted_ord_keys] min_diffs = min(diffs) if min_diffs > 0.1: - raise ValueError("Requested FSR ({0}) was not part of " - "possible values: {1}".format( - value, [float(x) for x in sorted(self.fsr.keys())])) + raise ValueError( + "Requested FSR ({0}) was not part of possible values: {1}".format( + value, [float(x) for x in sorted(self.fsr.keys())])) k = int(sorted_ord_keys[diffs.index(min_diffs)]) return self.fsr.get(k) @@ -130,7 +132,7 @@ def get_current_settings(self): def get_possible_settings(self): return { 'data_rate': [float(x) for x in sorted( - self.odr.keys(), key=lambda x:(float(x)))], + self.odr.keys(), key=lambda x: (float(x)))], 'data_range': [x for x in sorted(self.fsr.keys())] } @@ -242,4 +244,5 @@ def wrapper(data): else: raise PyMetaWearException('Incorrect data type id: {0}'.format( data.contents.type_id)) + return wrapper diff --git a/pymetawear/modules/haptic.py b/pymetawear/modules/haptic.py index 6ceac92..6ae8181 100644 --- a/pymetawear/modules/haptic.py +++ b/pymetawear/modules/haptic.py @@ -10,7 +10,6 @@ from __future__ import division from __future__ import print_function -from __future__ import unicode_literals from __future__ import absolute_import import logging diff --git a/pymetawear/modules/led.py b/pymetawear/modules/led.py index d38737f..8fb3ad1 100644 --- a/pymetawear/modules/led.py +++ b/pymetawear/modules/led.py @@ -10,7 +10,6 @@ from __future__ import division from __future__ import print_function -from __future__ import unicode_literals from __future__ import absolute_import import logging diff --git a/pymetawear/modules/magnetometer.py b/pymetawear/modules/magnetometer.py index e4a25c0..ef18b34 100644 --- a/pymetawear/modules/magnetometer.py +++ b/pymetawear/modules/magnetometer.py @@ -10,7 +10,6 @@ from __future__ import division from __future__ import print_function -from __future__ import unicode_literals from __future__ import absolute_import import re diff --git a/pymetawear/modules/switch.py b/pymetawear/modules/switch.py index 1eef93f..11ffeaf 100644 --- a/pymetawear/modules/switch.py +++ b/pymetawear/modules/switch.py @@ -10,7 +10,6 @@ from __future__ import division from __future__ import print_function -from __future__ import unicode_literals from __future__ import absolute_import import logging @@ -19,7 +18,7 @@ from pymetawear import libmetawear from pymetawear.exceptions import PyMetaWearException -from pymetawear.mbientlab.metawear.core import DataTypeId, CartesianFloat +from pymetawear.mbientlab.metawear.core import DataTypeId from pymetawear.modules.base import PyMetaWearModule log = logging.getLogger(__name__) diff --git a/pymetawear/modules/temperature.py b/pymetawear/modules/temperature.py index b6bb090..3458015 100644 --- a/pymetawear/modules/temperature.py +++ b/pymetawear/modules/temperature.py @@ -10,18 +10,16 @@ from __future__ import division from __future__ import print_function -from __future__ import unicode_literals from __future__ import absolute_import import warnings import logging from functools import wraps -from ctypes import c_uint, cast, POINTER, c_long, c_float +from ctypes import cast, POINTER, c_float from pymetawear import libmetawear from pymetawear.exceptions import PyMetaWearException from pymetawear.mbientlab.metawear.core import DataTypeId -from pymetawear.mbientlab.metawear.sensor import MultiChannelTemperature from pymetawear.modules.base import PyMetaWearModule log = logging.getLogger(__name__) @@ -69,7 +67,7 @@ def __init__(self, board, debug=False): self._channel_source_mapping = _CHANNEL_ID_TO_SOURCE_NAME self._reverse_channel_source_mapping = { - v: k for k,v in self._channel_source_mapping.items()} + v: k for k, v in self._channel_source_mapping.items()} for i in range(self.n_channels): source_enum = libmetawear.mbl_mw_multi_chnl_temp_get_source( diff --git a/pymetawear/specs.py b/pymetawear/specs.py index de569e2..e785098 100644 --- a/pymetawear/specs.py +++ b/pymetawear/specs.py @@ -9,13 +9,13 @@ from __future__ import division from __future__ import print_function -# from __future__ import unicode_literals + from __future__ import absolute_import import uuid -def _high_low_2_uuid(uuid_high, uuid_low): +def high_low_2_uuid(uuid_high, uuid_low): """Combine high and low bits of a split UUID. :param uuid_high: The high 64 bits of the UUID. @@ -30,20 +30,25 @@ def _high_low_2_uuid(uuid_high, uuid_low): # Service specs obtained from connection.h -METAWEAR_SERVICE_NOTIFY_CHAR = _high_low_2_uuid(0x326a900085cb9195, - 0xd9dd464cfbbae75a), \ - _high_low_2_uuid(0x326a900685cb9195, - 0xd9dd464cfbbae75a) +METAWEAR_SERVICE_NOTIFY_CHAR = ( + high_low_2_uuid(0x326a900085cb9195, 0xd9dd464cfbbae75a), + high_low_2_uuid(0x326a900685cb9195, 0xd9dd464cfbbae75a) +) # Service & char specs obtained from metawearboard.cpp -_DEVICE_INFO_SERVICE = _high_low_2_uuid(0x0000180a00001000, 0x800000805f9b34fb) - -METAWEAR_COMMAND_CHAR = METAWEAR_SERVICE_NOTIFY_CHAR, \ - _high_low_2_uuid(0x326a900185cb9195, 0xd9dd464cfbbae75a) -DEV_INFO_FIRMWARE_CHAR = _DEVICE_INFO_SERVICE, \ - _high_low_2_uuid(0x00002a2600001000, - 0x800000805f9b34fb) -DEV_INFO_MODEL_CHAR = _DEVICE_INFO_SERVICE, \ - _high_low_2_uuid(0x00002a2400001000, 0x800000805f9b34fb) +_DEVICE_INFO_SERVICE = high_low_2_uuid(0x0000180a00001000, 0x800000805f9b34fb) + +METAWEAR_COMMAND_CHAR = ( + METAWEAR_SERVICE_NOTIFY_CHAR, + high_low_2_uuid(0x326a900185cb9195, 0xd9dd464cfbbae75a) +) +DEV_INFO_FIRMWARE_CHAR = ( + _DEVICE_INFO_SERVICE, + high_low_2_uuid(0x00002a2600001000, 0x800000805f9b34fb) +) +DEV_INFO_MODEL_CHAR = ( + _DEVICE_INFO_SERVICE, + high_low_2_uuid(0x00002a2400001000, 0x800000805f9b34fb) +) diff --git a/pymetawear/version.py b/pymetawear/version.py new file mode 100644 index 0000000..486c0e4 --- /dev/null +++ b/pymetawear/version.py @@ -0,0 +1,16 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +""" +version.py +----------- + +:copyright: 2016-11-28 by hbldh + +""" + +from __future__ import division +from __future__ import print_function +from __future__ import absolute_import + +__version__ = '0.7.0' +version = __version__ # backwards compatibility name diff --git a/requirements_dev.txt b/requirements_dev.txt new file mode 100644 index 0000000..2fa467b --- /dev/null +++ b/requirements_dev.txt @@ -0,0 +1,10 @@ +pip +setuptools +wheel +bumpversion +flake8 +pytest +pytest-cov +python-coveralls +Sphinx +sphinx_rtd_theme diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..9a86cf7 --- /dev/null +++ b/setup.cfg @@ -0,0 +1,15 @@ +[bumpversion] +current_version = 0.7.0 +commit = False +tag = False + +[bumpversion:file:pymetawear/version.py] +search = __version__ = '{current_version}' +replace = __version__ = '{new_version}' + +[bdist_wheel] +universal = 0 + +[flake8] +exclude = docs + diff --git a/setup.py b/setup.py index 4e04aca..7f74730 100644 --- a/setup.py +++ b/setup.py @@ -175,7 +175,7 @@ def build_solution(): except: pass -with open('pymetawear/__init__.py', 'r') as fd: +with open('pymetawear/version.py', 'r') as fd: version = re.search(r'^__version__\s*=\s*[\'"]([^\'"]*)[\'"]', fd.read(), re.MULTILINE).group(1) @@ -222,7 +222,6 @@ def read(f): 'pexpect>=4.2.0' ], extras_require={ - 'bluepy': 'bluepy>=1.0.5', 'pybluez': 'pybluez[ble]>=0.22' }, ext_modules=[], diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/mock_backend.py b/tests/mock_backend.py new file mode 100644 index 0000000..49903ae --- /dev/null +++ b/tests/mock_backend.py @@ -0,0 +1,396 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +""" +mock_backend +----------- + +:copyright: 2016-11-25 by hbldh + +""" + +from __future__ import division +from __future__ import print_function +from __future__ import unicode_literals +from __future__ import absolute_import + +import uuid +from ctypes import create_string_buffer + +from pymetawear.specs import METAWEAR_SERVICE_NOTIFY_CHAR, \ + DEV_INFO_FIRMWARE_CHAR, DEV_INFO_MODEL_CHAR +from pymetawear.backends import BLECommunicationBackend + + +UUID2HANDLES = { + uuid.UUID("326a9001-85cb-9195-d9dd-464cfbbae75a"): 0x001d, + METAWEAR_SERVICE_NOTIFY_CHAR[1]: 0x001d, + DEV_INFO_FIRMWARE_CHAR[1]: 0x0014, + DEV_INFO_MODEL_CHAR[1]: 0x001a +} + + +class MockBackend(BLECommunicationBackend): + + METAWEAR_R_BOARD = 0 + METAWEAR_RG_BOARD = 1 + METAWEAR_RPRO_BOARD = 2 + METAWEAR_CPRO_BOARD = 3 + METAWEAR_ENV_BOARD = 4 + METAWEAR_DETECT_BOARD = 5 + METAWEAR_MOTION_R_BOARD = 6 + + boardType = 0 + + def __init__(self, address, interface=None, timeout=None, debug=False): + self.responses = [] + self.written_data = {} + self.full_history = [] + self.command_history = [] + + self.eventId = 0 + self.timerId = 0 + self.dataprocId = 0 + self.loggerId = 0 + self.timerSignals = [] + + self.metawear_rg_services = { + 0x01: create_string_buffer(b'\x01\x80\x00\x00', 4), + 0x02: create_string_buffer(b'\x02\x80\x00\x00', 4), + 0x03: create_string_buffer(b'\x03\x80\x01\x01', 4), + 0x04: create_string_buffer(b'\x04\x80\x01\x00\x00\x03\x01\x02', 8), + 0x05: create_string_buffer(b'\x05\x80\x00\x00', 4), + 0x06: create_string_buffer(b'\x06\x80\x00\x00', 4), + 0x07: create_string_buffer(b'\x07\x80\x00\x00', 4), + 0x08: create_string_buffer(b'\x08\x80\x00\x00', 4), + 0x09: create_string_buffer(b'\x09\x80\x00\x00\x1c', 5), + 0x0a: create_string_buffer(b'\x0a\x80\x00\x00\x1c', 5), + 0x0b: create_string_buffer(b'\x0b\x80\x00\x02\x08\x80\x2D\x00\x00', 9), + 0x0c: create_string_buffer(b'\x0c\x80\x00\x00\x08', 5), + 0x0d: create_string_buffer(b'\x0d\x80\x00\x00', 4), + 0x0f: create_string_buffer(b'\x0f\x80\x00\x00', 4), + 0x10: create_string_buffer(b'\x10\x80', 2), + 0x11: create_string_buffer(b'\x11\x80\x00\x00', 4), + 0x12: create_string_buffer(b'\x12\x80', 2), + 0x13: create_string_buffer(b'\x13\x80\x00\x01', 4), + 0x14: create_string_buffer(b'\x14\x80', 2), + 0x15: create_string_buffer(b'\x15\x80', 2), + 0x16: create_string_buffer(b'\x16\x80', 2), + 0x17: create_string_buffer(b'\x17\x80', 2), + 0x18: create_string_buffer(b'\x18\x80', 2), + 0x19: create_string_buffer(b'\x19\x80', 2), + 0xfe: create_string_buffer(b'\xfe\x80\x00\x00', 4) + } + + self.metawear_r_services = { + 0x01: create_string_buffer(b'\x01\x80\x00\x00', 4), + 0x02: create_string_buffer(b'\x02\x80\x00\x00', 4), + 0x03: create_string_buffer(b'\x03\x80\x00\x01', 4), + 0x04: create_string_buffer(b'\x04\x80\x01\x00\x00\x01', 6), + 0x05: create_string_buffer(b'\x05\x80\x00\x00', 4), + 0x06: create_string_buffer(b'\x06\x80\x00\x00', 4), + 0x07: create_string_buffer(b'\x07\x80\x00\x00', 4), + 0x08: create_string_buffer(b'\x08\x80\x00\x00', 4), + 0x09: create_string_buffer(b'\x09\x80\x00\x00\x1C', 5), + 0x0a: create_string_buffer(b'\x0A\x80\x00\x00\x1C', 5), + 0x0b: create_string_buffer(b'\x0B\x80\x00\x02\x08\x80\x31\x00\x00', 9), + 0x0c: create_string_buffer(b'\x0C\x80\x00\x00\x08', 5), + 0x0d: create_string_buffer(b'\x0D\x80\x00\x00', 4), + 0x0f: create_string_buffer(b'\x0F\x80\x00\x00', 4), + 0x10: create_string_buffer(b'\x10\x80', 2), + 0x11: create_string_buffer(b'\x11\x80\x00\x00', 4), + 0x12: create_string_buffer(b'\x12\x80', 2), + 0x13: create_string_buffer(b'\x13\x80', 2), + 0x14: create_string_buffer(b'\x14\x80', 2), + 0x15: create_string_buffer(b'\x15\x80', 2), + 0x16: create_string_buffer(b'\x16\x80', 2), + 0x17: create_string_buffer(b'\x17\x80', 2), + 0x18: create_string_buffer(b'\x18\x80', 2), + 0x19: create_string_buffer(b'\x19\x80', 2), + 0xfe: create_string_buffer(b'\xFE\x80\x00\x00', 4) + } + + self.metawear_rpro_services = { + 0x01: create_string_buffer(b'\x01\x80\x00\x00', 4), + 0x02: create_string_buffer(b'\x02\x80\x00\x00', 4), + 0x03: create_string_buffer(b'\x03\x80\x01\x01', 4), + 0x04: create_string_buffer(b'\x04\x80\x01\x00\x00\x03\x01\x02', 8), + 0x05: create_string_buffer(b'\x05\x80\x00\x00', 4), + 0x06: create_string_buffer(b'\x06\x80\x00\x00', 4), + 0x07: create_string_buffer(b'\x07\x80\x00\x00', 4), + 0x08: create_string_buffer(b'\x08\x80\x00\x00', 4), + 0x09: create_string_buffer(b'\x09\x80\x00\x00\x1C', 5), + 0x0a: create_string_buffer(b'\x0A\x80\x00\x00\x1C', 5), + 0x0b: create_string_buffer(b'\x0B\x80\x00\x02\x08\x80\x2D\x00\x00', 9), + 0x0c: create_string_buffer(b'\x0C\x80\x00\x00\x08', 5), + 0x0d: create_string_buffer(b'\x0D\x80\x00\x00', 4), + 0x0f: create_string_buffer(b'\x0F\x80\x00\x00', 4), + 0x10: create_string_buffer(b'\x10\x80', 2), + 0x11: create_string_buffer(b'\x11\x80\x00\x00', 4), + 0x12: create_string_buffer(b'\x12\x80\x00\x00', 4), + 0x13: create_string_buffer(b'\x13\x80\x00\x01', 4), + 0x14: create_string_buffer(b'\x14\x80\x00\x00', 4), + 0x15: create_string_buffer(b'\x15\x80', 2), + 0x16: create_string_buffer(b'\x16\x80', 2), + 0x17: create_string_buffer(b'\x17\x80', 2), + 0x18: create_string_buffer(b'\x18\x80', 2), + 0x19: create_string_buffer(b'\x19\x80', 2), + 0xfe: create_string_buffer(b'\xFE\x80\x00\x00', 4) + } + + self.metawear_cpro_services = { + 0x01: create_string_buffer(b'\x01\x80\x00\x00', 4), + 0x02: create_string_buffer(b'\x02\x80\x00\x00', 4), + 0x03: create_string_buffer(b'\x03\x80\x01\x01', 4), + 0x04: create_string_buffer(b'\x04\x80\x01\x00\x00\x03\x01\x02', 8), + 0x05: create_string_buffer(b'\x05\x80\x00\x00', 4), + 0x06: create_string_buffer(b'\x06\x80\x00\x00', 4), + 0x07: create_string_buffer(b'\x07\x80\x00\x00', 4), + 0x08: create_string_buffer(b'\x08\x80\x00\x00', 4), + 0x09: create_string_buffer(b'\x09\x80\x00\x00\x1C', 5), + 0x0a: create_string_buffer(b'\x0A\x80\x00\x00\x1C', 5), + 0x0b: create_string_buffer(b'\x0B\x80\x00\x02\x08\x80\x2B\x00\x00', 9), + 0x0c: create_string_buffer(b'\x0C\x80\x00\x00\x08', 5), + 0x0d: create_string_buffer(b'\x0D\x80\x00\x00', 4), + 0x0f: create_string_buffer(b'\x0F\x80\x00\x00', 4), + 0x10: create_string_buffer(b'\x10\x80', 2), + 0x11: create_string_buffer(b'\x11\x80\x00\x00', 4), + 0x12: create_string_buffer(b'\x12\x80\x00\x00', 4), + 0x13: create_string_buffer(b'\x13\x80\x00\x01', 4), + 0x14: create_string_buffer(b'\x14\x80\x00\x00', 4), + 0x15: create_string_buffer(b'\x15\x80\x00\x00', 4), + 0x16: create_string_buffer(b'\x16\x80', 2), + 0x17: create_string_buffer(b'\x17\x80', 2), + 0x18: create_string_buffer(b'\x18\x80', 2), + 0x19: create_string_buffer(b'\x19\x80', 2), + 0xfe: create_string_buffer(b'\xFE\x80\x00\x00', 4) + } + + self.metawear_detector_services = { + 0x01: create_string_buffer(b'\x01\x80\x00\x00', 4), + 0x02: create_string_buffer(b'\x02\x80\x00\x00', 4), + 0x03: create_string_buffer(b'\x03\x80\x03\x01', 4), + 0x04: create_string_buffer(b'\x04\x80\x01\x00\x00\x03\x01\x02', 8), + 0x05: create_string_buffer(b'\x05\x80\x00\x01\x03\x03\x03\x03\x01', 9), + 0x06: create_string_buffer(b'\x06\x80\x00\x00', 4), + 0x07: create_string_buffer(b'\x07\x80\x00\x00', 4), + 0x08: create_string_buffer(b'\x08\x80\x00\x00', 4), + 0x09: create_string_buffer(b'\x09\x80\x00\x00\x1c', 5), + 0x0a: create_string_buffer(b'\x0a\x80\x00\x00\x1c', 5), + 0x0b: create_string_buffer(b'\x0b\x80\x00\x02\x08\x80\x2b\x00\x00', 9), + 0x0c: create_string_buffer(b'\x0c\x80\x00\x00\x08', 5), + 0x0d: create_string_buffer(b'\x0d\x80\x00\x00', 4), + 0x0f: create_string_buffer(b'\x0f\x80\x00\x01\x08', 5), + 0x10: create_string_buffer(b'\x10\x80', 2), + 0x11: create_string_buffer(b'\x11\x80\x00\x03', 4), + 0x12: create_string_buffer(b'\x12\x80', 2), + 0x13: create_string_buffer(b'\x13\x80', 2), + 0x14: create_string_buffer(b'\x14\x80\x00\x00', 4), + 0x15: create_string_buffer(b'\x15\x80', 2), + 0x16: create_string_buffer(b'\x16\x80', 2), + 0x17: create_string_buffer(b'\x17\x80', 2), + 0x18: create_string_buffer(b'\x18\x80\x00\x00', 4), + 0x19: create_string_buffer(b'\x19\x80', 2), + 0xfe: create_string_buffer(b'\xfe\x80\x00\x00', 4), + } + + self.metawear_environment_services = { + 0x01: create_string_buffer(b'\x01\x80\x00\x00', 4), + 0x02: create_string_buffer(b'\x02\x80\x00\x00', 4), + 0x03: create_string_buffer(b'\x03\x80\x03\x01', 4), + 0x04: create_string_buffer(b'\x04\x80\x01\x00\x00\x03\x01\x02', 8), + 0x05: create_string_buffer(b'\x05\x80\x00\x01\x03\x03\x03\x03\x01', 9), + 0x06: create_string_buffer(b'\x06\x80\x00\x00', 4), + 0x07: create_string_buffer(b'\x07\x80\x00\x00', 4), + 0x08: create_string_buffer(b'\x08\x80\x00\x00', 4), + 0x09: create_string_buffer(b'\x09\x80\x00\x00\x1c', 5), + 0x0a: create_string_buffer(b'\x0a\x80\x00\x00\x1c', 5), + 0x0b: create_string_buffer(b'\x0b\x80\x00\x02\x08\x80\x2b\x00\x00', 9), + 0x0c: create_string_buffer(b'\x0c\x80\x00\x00\x08', 5), + 0x0d: create_string_buffer(b'\x0d\x80\x00\x00', 4), + 0x0f: create_string_buffer(b'\x0f\x80\x00\x01\x08', 5), + 0x10: create_string_buffer(b'\x10\x80', 2), + 0x11: create_string_buffer(b'\x11\x80\x00\x03', 4), + 0x12: create_string_buffer(b'\x12\x80\x01\x00', 4), + 0x13: create_string_buffer(b'\x13\x80', 2), + 0x14: create_string_buffer(b'\x14\x80', 2), + 0x15: create_string_buffer(b'\x15\x80', 2), + 0x16: create_string_buffer(b'\x16\x80\x00\x00', 4), + 0x17: create_string_buffer(b'\x17\x80\x00\x00', 4), + 0x18: create_string_buffer(b'\x18\x80', 4), + 0x19: create_string_buffer(b'\x19\x80', 2), + 0xfe: create_string_buffer(b'\xfe\x80\x00\x00', 4), + } + self.metawear_motion_r_services = { + 0x01: create_string_buffer(b'\x01\x80\x00\x00', 4), + 0x02: create_string_buffer(b'\x02\x80\x00\x00', 4), + 0x03: create_string_buffer(b'\x03\x80\x01\x01', 4), + 0x04: create_string_buffer(b'\x04\x80\x01\x00\x00\x03\x01\x02', 8), + 0x05: create_string_buffer(b'\x05\x80\x00\x01\x03\x03\x03\x03\x01', 9), + 0x06: create_string_buffer(b'\x06\x80\x00\x00', 4), + 0x07: create_string_buffer(b'\x07\x80\x00\x00', 4), + 0x08: create_string_buffer(b'\x08\x80\x00\x00', 4), + 0x09: create_string_buffer(b'\x09\x80\x00\x00\x1c', 5), + 0x0a: create_string_buffer(b'\x0a\x80\x00\x00\x1c', 5), + 0x0b: create_string_buffer(b'\x0b\x80\x00\x02\x08\x80\x2b\x00\x00', 9), + 0x0c: create_string_buffer(b'\x0c\x80\x00\x00\x08', 5), + 0x0d: create_string_buffer(b'\x0d\x80\x00\x01', 4), + 0x0f: create_string_buffer(b'\x0f\x80\x00\x01\x08', 5), + 0x10: create_string_buffer(b'\x10\x80', 2), + 0x11: create_string_buffer(b'\x11\x80\x00\x03', 4), + 0x12: create_string_buffer(b'\x12\x80\x00\x00', 4), + 0x13: create_string_buffer(b'\x13\x80\x00\x01', 4), + 0x14: create_string_buffer(b'\x14\x80\x00\x00', 4), + 0x15: create_string_buffer(b'\x15\x80\x00\x01', 4), + 0x16: create_string_buffer(b'\x16\x80', 2), + 0x17: create_string_buffer(b'\x17\x80', 2), + 0x18: create_string_buffer(b'\x18\x80', 2), + 0x19: create_string_buffer(b'\x19\x80\x00\x00\x03\x00\x06\x00\x02\x00\x01\x00', 12), + 0xfe: create_string_buffer(b'\xfe\x80\x00\x00', 4), + } + + self.firmware_revision = create_string_buffer(b'1.1.3', 5) + self._connected = False + + super(MockBackend, self).__init__(address, interface, timeout, debug) + + @property + def is_connected(self): + return self._connected + + def connect(self, clean_connect=False): + self._connected = True + super(MockBackend, self).connect(clean_connect) + + def disconnect(self): + self._connected = False + + @property + def requester(self): + """Not used in MockBackend""" + return None + + def read_gatt_char_by_uuid(self, characteristic): + if isinstance(characteristic, uuid.UUID): + if characteristic == DEV_INFO_FIRMWARE_CHAR[1]: + return self.firmware_revision.raw + elif characteristic == DEV_INFO_MODEL_CHAR[1]: + if (self.boardType == self.METAWEAR_RG_BOARD): + model_number = create_string_buffer(b'1', 1) + elif (self.boardType == self.METAWEAR_R_BOARD): + model_number = create_string_buffer(b'0', 1) + elif (self.boardType == self.METAWEAR_RPRO_BOARD): + model_number = create_string_buffer(b'1', 1) + elif (self.boardType == self.METAWEAR_CPRO_BOARD or + self.boardType == self.METAWEAR_DETECT_BOARD or + self.boardType == self.METAWEAR_ENV_BOARD): + model_number = create_string_buffer(b'2', 1) + elif (self.boardType == self.METAWEAR_MOTION_R_BOARD): + model_number = create_string_buffer(b'5', 1) + return model_number.raw + else: + if (characteristic.contents.uuid_high == 0x00002a2400001000 and characteristic.contents.uuid_low == 0x800000805f9b34fb): + if (self.boardType == self.METAWEAR_RG_BOARD): + model_number= create_string_buffer(b'1', 1) + elif (self.boardType == self.METAWEAR_R_BOARD): + model_number= create_string_buffer(b'0', 1) + elif (self.boardType == self.METAWEAR_RPRO_BOARD): + model_number= create_string_buffer(b'1', 1) + elif (self.boardType == self.METAWEAR_CPRO_BOARD or self.boardType == self.METAWEAR_DETECT_BOARD or self.boardType == self.METAWEAR_ENV_BOARD): + model_number= create_string_buffer(b'2', 1) + elif (self.boardType == self.METAWEAR_MOTION_R_BOARD): + model_number= create_string_buffer(b'5', 1) + + self._log("Read", characteristic, model_number.raw, len(model_number.raw, )) + return model_number.raw + + elif (characteristic.contents.uuid_high == 0x00002a2600001000 and characteristic.contents.uuid_low == 0x800000805f9b34fb): + self._log("Read", characteristic, self.firmware_revision.raw, len(self.firmware_revision.raw)) + return self.firmware_revision.raw + + def write_gatt_char_by_uuid(self, characteristic, command, length): + characteristic_uuid = self.get_uuid(characteristic) + + self.command = [] + for i in range(0, length): + self.command.append(command[i]) + + self.full_history.append(self.command) + if command[1] == 0x80: + if (self.boardType == self.METAWEAR_RG_BOARD and + command[0] in self.metawear_rg_services): + service_response = self.metawear_rg_services[command[0]] + elif (self.boardType == self.METAWEAR_R_BOARD and + command[0] in self.metawear_r_services): + service_response = self.metawear_r_services[command[0]] + elif (self.boardType == self.METAWEAR_RPRO_BOARD and + command[0] in self.metawear_rpro_services): + service_response = self.metawear_rpro_services[command[0]] + elif (self.boardType == self.METAWEAR_CPRO_BOARD and + command[0] in self.metawear_cpro_services): + service_response = self.metawear_cpro_services[command[0]] + elif (self.boardType == self.METAWEAR_DETECT_BOARD and + command[0] in self.metawear_detector_services): + service_response = self.metawear_detector_services[command[0]] + elif (self.boardType == self.METAWEAR_ENV_BOARD and + command[0] in self.metawear_environment_services): + service_response = self.metawear_environment_services[ + command[0]] + elif (self.boardType == self.METAWEAR_MOTION_R_BOARD and + command[0] in self.metawear_motion_r_services): + service_response = self.metawear_motion_r_services[command[0]] + self.handle_notify_char_output(self.get_handle(characteristic_uuid), + service_response.raw) + elif (command[0] == 0xb and command[1] == 0x84): + reference_tick = create_string_buffer( + b'\x0b\x84\x15\x04\x00\x00\x05', 7) + self.handle_notify_char_output(self.get_handle(characteristic_uuid), + reference_tick.raw) + else: + # ignore module discovey commands + self.command_history.append(self.command) + if (command[0] == 0xc and command[1] == 0x2): + response = create_string_buffer(b'\x0c\x02', 3) + response[2] = self.timerId + self.timerId += 1 + self.handle_notify_char_output( + self.get_handle(characteristic_uuid), response.raw) + elif (command[0] == 0xa and command[1] == 0x3): + response = create_string_buffer(b'\x0a\x02', 3) + response[2] = self.eventId + self.eventId += 1 + self.handle_notify_char_output( + self.get_handle(characteristic_uuid), response.raw) + elif (command[0] == 0x9 and command[1] == 0x2): + response = create_string_buffer(b'\x09\x02', 3) + response[2] = self.dataprocId + self.dataprocId += 1 + self.handle_notify_char_output( + self.get_handle(characteristic_uuid), response.raw) + elif (command[0] == 0xb and command[1] == 0x2): + response = create_string_buffer(b'\x0b\x02', 3) + response[2] = self.loggerId + self.loggerId += 1 + self.handle_notify_char_output( + self.get_handle(characteristic_uuid), response.raw) + elif (command[0] == 0xb and command[1] == 0x85): + response = create_string_buffer(b'\x0b\x85\x9e\x01\x00\x00', 6) + self.handle_notify_char_output( + self.get_handle(characteristic_uuid), response.raw) + + def get_handle(self, uuid, notify_handle=False): + """Get handle for a characteristic UUID. + + :param uuid.UUID uuid: The UUID to get handle of. + :param bool notify_handle: + :return: Integer handle corresponding to the input characteristic UUID. + :rtype: int + + """ + # TODO: Is this even needed? + return UUID2HANDLES.get(uuid, 1) + int(notify_handle) + + def _subscribe(self, characterisitic_uuid, callback): + return + + def _response_2_string_buffer(self, response): + return create_string_buffer(bytes(response), len(response)) diff --git a/tests/test_mwclient.py b/tests/test_mwclient.py index 20ea762..de74905 100644 --- a/tests/test_mwclient.py +++ b/tests/test_mwclient.py @@ -11,22 +11,57 @@ from __future__ import division from __future__ import print_function -#from __future__ import unicode_literals from __future__ import absolute_import import pytest -from pymetawear.client import MetaWearClient -from pymetawear.backends import BLECommunicationBackend -from pymetawear.backends.pygatt import PyGattBackend +import pymetawear.client +from .mock_backend import MockBackend +pymetawear.client.PyGattBackend = MockBackend +pymetawear.client.PyBluezBackend = MockBackend -#try: -# from unittest import mock -#except: -# import mock +@pytest.mark.parametrize("backend", ['pygatt', 'pybluez']) +@pytest.mark.parametrize("mw_board", range(7)) +def test_client_init(backend, mw_board): + MockBackend.boardType = mw_board + c = pymetawear.client.MetaWearClient('XX:XX:XX:XX:XX:XX', backend=backend, debug=False) + assert isinstance(c.backend, MockBackend) + assert c.backend.boardType == mw_board + assert c.backend.initialized + assert c.backend.initialization_status == 0 -def test_dummy(): - assert True + expected_cmds = [ + [0x01, 0x80], [0x02, 0x80], [0x03, 0x80], [0x04, 0x80], + [0x05, 0x80], [0x06, 0x80], [0x07, 0x80], [0x08, 0x80], + [0x09, 0x80], [0x0a, 0x80], [0x0b, 0x80], [0x0c, 0x80], + [0x0d, 0x80], [0x0f, 0x80], [0x10, 0x80], [0x11, 0x80], + [0x12, 0x80], [0x13, 0x80], [0x14, 0x80], [0x15, 0x80], + [0x16, 0x80], [0x17, 0x80], [0x18, 0x80], [0x19, 0x80], + [0xfe, 0x80], [0x0b, 0x84] + ] + assert c.backend.full_history == expected_cmds +@pytest.mark.parametrize("backend", ['pygatt', 'pybluez']) +@pytest.mark.parametrize("mw_board", range(7)) +def test_client_init_delayed_connect(backend, mw_board): + MockBackend.boardType = mw_board + c = pymetawear.client.MetaWearClient('XX:XX:XX:XX:XX:XX', backend=backend, connect=False, debug=False) + assert isinstance(c.backend, MockBackend) + assert not c.backend.initialized + c.connect() + assert c.backend.boardType == mw_board + assert c.backend.initialized + assert c.backend.initialization_status == 0 + + expected_cmds = [ + [0x01, 0x80], [0x02, 0x80], [0x03, 0x80], [0x04, 0x80], + [0x05, 0x80], [0x06, 0x80], [0x07, 0x80], [0x08, 0x80], + [0x09, 0x80], [0x0a, 0x80], [0x0b, 0x80], [0x0c, 0x80], + [0x0d, 0x80], [0x0f, 0x80], [0x10, 0x80], [0x11, 0x80], + [0x12, 0x80], [0x13, 0x80], [0x14, 0x80], [0x15, 0x80], + [0x16, 0x80], [0x17, 0x80], [0x18, 0x80], [0x19, 0x80], + [0xfe, 0x80], [0x0b, 0x84] + ] + assert c.backend.full_history == expected_cmds diff --git a/travis_pypi_setup.py b/travis_pypi_setup.py new file mode 100644 index 0000000..d0a8a3e --- /dev/null +++ b/travis_pypi_setup.py @@ -0,0 +1,122 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +"""Update encrypted deploy password in Travis config file +""" + + +from __future__ import print_function +import base64 +import json +import os +from getpass import getpass +import yaml +from cryptography.hazmat.primitives.serialization import load_pem_public_key +from cryptography.hazmat.backends import default_backend +from cryptography.hazmat.primitives.asymmetric.padding import PKCS1v15 + + +try: + from urllib import urlopen +except: + from urllib.request import urlopen + + +GITHUB_REPO = 'hbldh/pymetawear' +TRAVIS_CONFIG_FILE = os.path.join( + os.path.dirname(os.path.abspath(__file__)), '.travis.yml') + + +def load_key(pubkey): + """Load public RSA key, with work-around for keys using + incorrect header/footer format. + + Read more about RSA encryption with cryptography: + https://cryptography.io/latest/hazmat/primitives/asymmetric/rsa/ + """ + try: + return load_pem_public_key(pubkey.encode(), default_backend()) + except ValueError: + # workaround for https://github.com/travis-ci/travis-api/issues/196 + pubkey = pubkey.replace('BEGIN RSA', 'BEGIN').replace('END RSA', 'END') + return load_pem_public_key(pubkey.encode(), default_backend()) + + +def encrypt(pubkey, password): + """Encrypt password using given RSA public key and encode it with base64. + + The encrypted password can only be decrypted by someone with the + private key (in this case, only Travis). + """ + key = load_key(pubkey) + encrypted_password = key.encrypt(password, PKCS1v15()) + return base64.b64encode(encrypted_password) + + +def fetch_public_key(repo): + """Download RSA public key Travis will use for this repo. + + Travis API docs: http://docs.travis-ci.com/api/#repository-keys + """ + keyurl = 'https://api.travis-ci.org/repos/{0}/key'.format(repo) + data = json.loads(urlopen(keyurl).read().decode()) + if 'key' not in data: + errmsg = "Could not find public key for repo: {}.\n".format(repo) + errmsg += "Have you already added your GitHub repo to Travis?" + raise ValueError(errmsg) + return data['key'] + + +def prepend_line(filepath, line): + """Rewrite a file adding a line to its beginning. + """ + with open(filepath) as f: + lines = f.readlines() + + lines.insert(0, line) + + with open(filepath, 'w') as f: + f.writelines(lines) + + +def load_yaml_config(filepath): + with open(filepath) as f: + return yaml.load(f) + + +def save_yaml_config(filepath, config): + with open(filepath, 'w') as f: + yaml.dump(config, f, default_flow_style=False) + + +def update_travis_deploy_password(encrypted_password): + """Update the deploy section of the .travis.yml.backup file + to use the given encrypted password. + """ + config = load_yaml_config(TRAVIS_CONFIG_FILE) + + config['deploy']['password'] = dict(secure=encrypted_password) + + save_yaml_config(TRAVIS_CONFIG_FILE, config) + + line = ('# This file was autogenerated and will overwrite' + ' each time you run travis_pypi_setup.py\n') + prepend_line(TRAVIS_CONFIG_FILE, line) + + +def main(args): + public_key = fetch_public_key(args.repo) + password = args.password or getpass('PyPI password: ') + update_travis_deploy_password(encrypt(public_key, password.encode())) + print("Wrote encrypted password to .travis.yml.backup -- you're ready to deploy") + + +if '__main__' == __name__: + import argparse + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument('--repo', default=GITHUB_REPO, + help='GitHub repo (default: %s)' % GITHUB_REPO) + parser.add_argument('--password', + help='PyPI password (will prompt if not provided)') + + args = parser.parse_args() + main(args)