├── .gitattributes ├── .github └── workflows │ └── python.yml ├── .gitignore ├── LICENSE.txt ├── MANIFEST.in ├── README.md ├── docs ├── .statigen.toml ├── api.md ├── examples.md ├── getting-started.md ├── index.md └── myo-logo.jpg ├── examples ├── 01_hello_myo.py ├── 02_display_data.py ├── 03_live_emg.py ├── 04_emg_rate.py └── 05_api_listener.py ├── myo ├── __init__.py ├── _device_listener.py ├── _ffi.py ├── libmyo.h ├── macaddr.py ├── math.py ├── types │ ├── __init__.py │ ├── macaddr.py │ └── math.py └── utils.py ├── package.yml ├── setup.py └── test └── test_import.py /.gitattributes: -------------------------------------------------------------------------------- 1 | *.py text eol=lf 2 | *.md text eol=lf 3 | -------------------------------------------------------------------------------- /.github/workflows/python.yml: -------------------------------------------------------------------------------- 1 | # This workflow will install Python dependencies, run tests and lint with a variety of Python versions 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions 3 | 4 | name: Python package 5 | 6 | on: 7 | push: 8 | branches: [ master ] 9 | tags: [ '*' ] 10 | pull_request: 11 | branches: [ master ] 12 | 13 | jobs: 14 | test: 15 | runs-on: ubuntu-latest 16 | strategy: 17 | matrix: 18 | python-version: [ '3.5', '3.6', '3.7', '3.8', '3.9' ] 19 | steps: 20 | - uses: actions/checkout@v2 21 | - uses: conda-incubator/setup-miniconda@v2 22 | with: 23 | miniconda-version: latest 24 | python-version: 3.8 25 | activate-environment: testenv 26 | channels: conda-forge 27 | - name: Setup Python ${{ matrix.python-version }} with Conda 28 | run: conda create -q -n env python=${{ matrix.python-version }} 29 | - name: Install Shut 30 | run: python -m pip install shut==0.18.2 -q 31 | - name: Test 32 | run: PYTHON=$CONDA/envs/env/bin/python shut pkg test --isolate 33 | - name: Verify package metadata 34 | run: shut pkg update --verify 35 | 36 | publish: 37 | if: startsWith(github.ref, 'refs/tags/') 38 | runs-on: ubuntu-latest 39 | needs: test 40 | steps: 41 | - uses: actions/checkout@v2 42 | - name: Set up Python 3.8 43 | uses: actions/setup-python@v2 44 | with: 45 | python-version: 3.8 46 | - name: Install dependencies 47 | run: pip install -q shut==0.18.2 -q 48 | - name: Shut publish 49 | env: 50 | PYPI_TOKEN: ${{ secrets.PYPI_TOKEN }} 51 | TEST_PYPI_TOKEN: ${{ secrets.TEST_PYPI_TOKEN }} 52 | run: | 53 | shut pkg update --verify-tag "$GITHUB_REF" 54 | shut pkg publish warehouse:pypi --test 55 | shut pkg publish warehouse:pypi 56 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | build/ 2 | docs/build/ 3 | dist/ 4 | myo-sdk-*/ 5 | .venv 6 | .env/ 7 | Pipfile 8 | 9 | *.py[cod] 10 | *.egg-info 11 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright (c) 2021 Niklas Rosenstein 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | this software and associated documentation files (the "Software"), to deal in 7 | Software without restriction, including without limitation the rights to use, 8 | modify, merge, publish, distribute, sublicense, and/or sell copies of the 9 | and to permit persons to whom the Software is furnished to do so, subject to 10 | following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 17 | PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 18 | BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF 19 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR 20 | USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | # This section is auto-generated by Shut. DO NOT EDIT { 2 | include package.yml 3 | include LICENSE.txt 4 | include README.md 5 | include ./myo/libmyo.h 6 | # } 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | ## Announcements 3 | 4 | > [Oct 15, 2018] Thalmic Labs have announced the discontinuation of the Myo 5 | > armband. Software that used to be accessible on the Thalmic downloads page 6 | > can be found on the GitHub [releases](https://github.com/NiklasRosenstein/myo-python/releases) page. 7 | 8 | > [Jun 28, 2018] Myo-Python 1.0 has been released. It comes with a number of 9 | > API changes. If you have been following an older tutorial, you might have 10 | > the new version installed but use code that worked with the old version. 11 | > Check the [Migrating from v0.2.x](#migrating-from-v02x) section below. 12 | 13 | --- 14 | 15 |

16 | 17 | 18 |

19 |

20 | 21 | 22 | 23 |

24 |

Python bindings for the Myo SDK

25 | 26 | Myo-Python is a [CFFI] wrapper for the [Thalmic Myo SDK]. Minimum required Python version is 3.5. 27 | 28 | __Table of Contents__ 29 | 30 | * [Documentation](#documentation) 31 | * [Example](#example) 32 | * [Migrating from v0.2.x](#migrating-from-v02x) 33 | * [Projects using Myo-Python](#projects-using-myo-python) 34 | 35 | [CFFI]: https://pypi.python.org/pypi/cffi 36 | [Thalmic Myo SDK]: https://developer.thalmic.com/downloads 37 | 38 | ### Documentation 39 | 40 | The documentation can currently be found in the `docs/` directory in the 41 | [GitHub Repository](https://github.com/NiklasRosenstein/myo-python). 42 | 43 | ### Example 44 | 45 | Myo-Python mirrors the usage of the Myo C++ SDK in many ways as it also 46 | requires you to implement a `DeviceListener` that will then be invoked for 47 | any events received from a Myo device. 48 | 49 | ```python 50 | import myo 51 | 52 | class Listener(myo.DeviceListener): 53 | def on_paired(self, event): 54 | print("Hello, {}!".format(event.device_name)) 55 | event.device.vibrate(myo.VibrationType.short) 56 | def on_unpaired(self, event): 57 | return False # Stop the hub 58 | def on_orientation(self, event): 59 | orientation = event.orientation 60 | acceleration = event.acceleration 61 | gyroscope = event.gyroscope 62 | # ... do something with that 63 | 64 | if __name__ == '__main__': 65 | myo.init(sdk_path='./myo-sdk-win-0.9.0/') 66 | hub = myo.Hub() 67 | listener = Listener() 68 | while hub.run(listener.on_event, 500): 69 | pass 70 | ``` 71 | 72 | As an alternative to implementing a custom device listener, you can instead 73 | use the `myo.ApiDeviceListener` class which allows you to read the most recent 74 | state of one or multiple Myo devices. 75 | 76 | ```python 77 | import myo 78 | import time 79 | 80 | def main(): 81 | myo.init(sdk_path='./myo-sdk-win-0.9.0/') 82 | hub = myo.Hub() 83 | listener = myo.ApiDeviceListener() 84 | with hub.run_in_background(listener.on_event): 85 | print("Waiting for a Myo to connect ...") 86 | device = listener.wait_for_single_device(2) 87 | if not device: 88 | print("No Myo connected after 2 seconds.") 89 | return 90 | print("Hello, Myo! Requesting RSSI ...") 91 | device.request_rssi() 92 | while hub.running and device.connected and not device.rssi: 93 | print("Waiting for RRSI...") 94 | time.sleep(0.001) 95 | print("RSSI:", device.rssi) 96 | print("Goodbye, Myo!") 97 | ``` 98 | 99 | ### Migrating from v0.2.x 100 | 101 | The v0.2.x series of the Myo-Python library used `ctypes` and has a little 102 | bit different API. The most important changes are: 103 | 104 | * The `Hub` object no longer needs to be shut down explicitly 105 | * The `DeviceListener` method names changed to match the exact event name 106 | as specified by the Myo SDK (eg. from `on_pair()` to `on_paired()`) 107 | * `Hub.run()`: The order of arguments is reversed (`handler, duration_ms` 108 | instead of `duration_ms, handler`) 109 | * `myo.init()`: Provides a few more parameters to control the way `libmyo` is detected. 110 | * `myo.Feed`: Renamed to `myo.ApiDeviceListener` 111 | 112 | ### Projects using Myo-Python 113 | 114 | - [Myo Matlab](https://github.com/yijuilee/myomatlab) 115 | - [hayanalibhatti/Finger-Movement-Classification-via-Machine-Learning-using-EMG-Armband-for-3D-Printed-Robotic-Hand](https://github.com/shayanalibhatti/Finger-Movement-Classification-via-Machine-Learning-using-EMG-Armband-for-3D-Printed-Robotic-Hand) 116 | 117 | ### Changes 118 | 119 | #### v1.0.5 (2021-09-04) 120 | 121 | - Replace use of `time.clock()` with `time.perf_counter()` (#92) 122 | - Bumped minimum required Python version to 3.5 123 | 124 | #### v1.0.4 (2019-04-29) 125 | 126 | - Remove myo.quaternion, it was a leftover and the Quaternion class was actually in myo.types.math 127 | - move myo.types.math and myo.types.macaddr to myo package instead 128 | - myo.types package is now a stub for backwards compatibility 129 | - Depend on `enum34` package instead of `nr.types.enum` which has been removed in `nr.types>=2.0.0` 130 | - Update the error message of a `ValueError` raised in `myo.init()` 131 | 132 | #### v1.0.3 (2018-06-28) 133 | 134 | - `Event.mac_address` now returns `None` if the event's type is `EventType.emg` (#62) 135 | - `Hub.run()` now accepts `DeviceListener` objects for its *handler* parameter. 136 | This carries over to `Hub.run_forever()` and `Hub.run_in_background()`. 137 | - Replace requirement `nr>=2.0.10,<3` in favor of `nr.types>=1.0.3` 138 | 139 | #### v1.0.2 (2018-06-09) 140 | 141 | - Fix `Event.warmup_result` (PR #58 @fribeiro1) 142 | 143 | #### v1.0.1 (2018-06-09) 144 | 145 | - Fix `Event.rotation_on_arm` (#59) 146 | 147 | #### v1.0.0 (2018-06-03) 148 | 149 | - Rewrite using CFFI 150 | 151 | ---- 152 | 153 |

This project is licensed under the MIT License.
154 | Copyright © 2015-2018 Niklas Rosenstein

155 | -------------------------------------------------------------------------------- /docs/.statigen.toml: -------------------------------------------------------------------------------- 1 | [statigen] 2 | template = "default/docs" 3 | 4 | [site] 5 | title = "Myo Python" 6 | -------------------------------------------------------------------------------- /docs/api.md: -------------------------------------------------------------------------------- 1 | +++ 2 | title = "API Documentation" 3 | ordering = 4 4 | +++ 5 | 6 | ## Data Members 7 | 8 | ### `myo.supported_sdk_version` 9 | 10 | The SDK version that the `myo` Python library supports. This is currently set 11 | to the string `"0.9.0"`. 12 | 13 | ## Functions 14 | 15 | ### `myo.init(lib_name=None, bin_path=None, sdk_path=None)` 16 | 17 | Load the Myo shared library. This must be called before using any other 18 | functionality of the library that interacts with the Myo SDK. 19 | 20 | ## Device Listeners 21 | 22 | ### `myo.DeviceListener` Class 23 | 24 | The base class for implementing a device listener that will receive events 25 | from Myo Connect. Pass the `on_event` method to `Hub.run()`. The default 26 | implementation of `on_event()` method will redirect the event to the 27 | respective handler function based on the event type. 28 | 29 | * `on_paired(event)` 30 | * `on_unpaired(event)` 31 | * `on_connected(event)` 32 | * `on_disconnected(event)` 33 | * `on_arm_synced(event)` 34 | * `on_arm_unsynced(event)` 35 | * `on_unlocked(event)` 36 | * `on_locked(event)` 37 | * `on_pose(event)` 38 | * `on_orientation(event)` 39 | * `on_rssi(event)` 40 | * `on_battery_level(event)` 41 | * `on_emg(event)` 42 | * `on_warmup_complete(event)` 43 | 44 | ### `myo.ApiDeviceListener` 45 | 46 | This device listener implementation records any data it receives per device. 47 | 48 | #### `.wait_for_single_device(timeout=None, interval=0.5)` 49 | 50 | Waits until a Myo is paired and connected and returns its `DeviceProxy`. 51 | If no Myo connects until *timeout* runs out, `None` is returned. 52 | 53 | #### `.devices` 54 | 55 | A list of `DeviceProxy` objects that are paired. 56 | 57 | #### `.connected_devices` 58 | 59 | A list of `DeviceProxy` objects that are connected. This is a subset of 60 | `.devices`. 61 | 62 | ## Classes 63 | 64 | ### `myo.Hub` Class 65 | 66 | The hub is responsible for invoking your `DeviceListener`. The hub can be 67 | run synchronously or in the background. Note that with the background way, 68 | as soon as the end of the context-manager is reached, the hub will stop. 69 | 70 | ```python 71 | hub = myo.Hub() 72 | listener = MyListener() 73 | 74 | # Synchronous -- manual polling. 75 | while hub.run(listener.on_event, duration_ms=500): 76 | # Can do something after every run. 77 | pass 78 | 79 | # Synchronous. 80 | hub.run_forever(listener.on_event) 81 | 82 | # Asynchrnous. 83 | with hub.run_in_background(listener.on_event): 84 | # Do stuff while your listener is invoked in the background. 85 | # Note that reaching the end of the with block ends the hub. 86 | ``` 87 | 88 | #### `.locking_policy` 89 | 90 | #### `.running` 91 | 92 | #### `.run(handler, duration_ms)` 93 | 94 | #### `.run_forever(handler, duration_ms=500)` 95 | 96 | #### `.run_in_background(handler, duration_ms=500)` 97 | 98 | #### `.stop()` 99 | 100 | ### `myo.Device` Class 101 | 102 | Represents a Myo device. 103 | 104 | #### `.vibrate(type=VibrationType.medium)` 105 | 106 | #### `.stream_emg(type)` 107 | 108 | #### `.request_rssi()` 109 | 110 | #### `.request_battery_level()` 111 | 112 | #### `.unlock(type=UnlockType.hold)` 113 | 114 | #### `.lock()` 115 | 116 | #### `.notify_user_action(type=UserActionType.single)` 117 | 118 | ### `myo.Event` Class 119 | 120 | #### `.type` 121 | 122 | #### `.timestamp` 123 | 124 | #### `.device` 125 | 126 | #### `.device_name` 127 | 128 | #### `.mac_address` 129 | 130 | #### `.firmware_version` 131 | 132 | #### `.arm` 133 | 134 | #### `.x_direction` 135 | 136 | #### `.warmup_state` 137 | 138 | #### `.warmup_result` 139 | 140 | #### `.rotation_on_arm` 141 | 142 | #### `.orientation` 143 | 144 | #### `.acceleration` 145 | 146 | #### `.gyroscope` 147 | 148 | #### `.pose` 149 | 150 | #### `.rssi` 151 | 152 | #### `.battery_level` 153 | 154 | #### `.emg` 155 | 156 | ## Enumerations 157 | 158 | ### `myo.Result` 159 | 160 | ### `myo.VibrationType` 161 | 162 | ### `myo.StreamEmg` 163 | 164 | ### `myo.Pose` 165 | 166 | ### `myo.EventType` 167 | 168 | ### `myo.HandlerResult` 169 | 170 | ### `myo.LockingPolicy` 171 | 172 | ### `myo.Arm` 173 | 174 | ### `myo.XDirection` 175 | 176 | ### `myo.UnlockType` 177 | 178 | ### `myo.UserActionType` 179 | 180 | ### `myo.WarmupState` 181 | 182 | ### `myo.WarmupResult` 183 | 184 | ## Exception 185 | 186 | ### `myo.Error` 187 | 188 | ### `myo.ResultError` 189 | 190 | ### `myo.InvalidOperation` 191 | -------------------------------------------------------------------------------- /docs/examples.md: -------------------------------------------------------------------------------- 1 | +++ 2 | title = "Examples" 3 | ordering = 3 4 | +++ 5 | 6 | [0]: https://github.com/NiklasRosenstein/myo-python/tree/master/examples 7 | 8 | You can find the examples on [GitHub][0]. 9 | 10 | ## 03_live_emg 11 | 12 | This example keeps track of the last 512 EMG data events and displays them 13 | in a live graph with `matplotlib`. 14 | 15 | ![](https://i.imgur.com/PRXwcrn.png) 16 | 17 | -------------------------------------------------------------------------------- /docs/getting-started.md: -------------------------------------------------------------------------------- 1 | +++ 2 | title = "Getting Started" 3 | ordering = 1 4 | +++ 5 | 6 | Make sure you follow the installation instructions on the main page, first. 7 | 8 | To receive data from a Myo device, you need to implement a `myo.DeviceListener`. 9 | Alternatively, you can use the `myo.ApiDeviceListener` that allows you to read 10 | the last state of a property without being bound to the event flow. 11 | 12 | ## Custom DeviceListener 13 | 14 | Below is an example that implements a `myo.DeviceListener`: 15 | 16 | ```python 17 | import myo 18 | 19 | class Listener(myo.DeviceListener): 20 | 21 | def on_paired(self, event): 22 | print("Hello, {}!".format(event.device_name)) 23 | event.device.vibrate(myo.VibrationType.short) 24 | 25 | def on_unpaired(self, event): 26 | return False # Stop the hub 27 | 28 | def on_orientation(self, event): 29 | orientation = event.orientation 30 | acceleration = event.acceleration 31 | gyroscope = event.gyroscope 32 | # ... do something with that 33 | 34 | if __name__ == '__main__': 35 | myo.init(sdk_path='./myo-sdk-win-0.9.0/') 36 | hub = myo.Hub() 37 | listener = Listener() 38 | while hub.run(listener.on_event, 500): 39 | pass 40 | ``` 41 | 42 | ## Using ApiDeviceListener 43 | 44 | Using the stateful `myo.ApiDeviceListener` can make development a lot easier 45 | in some cases as you are not restricted to the Myo event flow. 46 | 47 | ```python 48 | import myo 49 | import time 50 | 51 | def main(): 52 | myo.init() 53 | hub = myo.Hub() 54 | listener = myo.ApiDeviceListener() 55 | 56 | with hub.run_in_background(listener.on_event): 57 | print("Waiting for a Myo to connect ...") 58 | device = listener.wait_for_single_device(2) 59 | if not device: 60 | print("No Myo connected after 2 seconds.") 61 | return 62 | 63 | print("Hello, Myo! Requesting RSSI ...") 64 | device.request_rssi() 65 | while hub.running and device.connected and not device.rssi: 66 | print("Waiting for RRSI...") 67 | time.sleep(0.001) 68 | print("RSSI:", device.rssi) 69 | print("Goodbye, Myo!") 70 | 71 | if __name__ == "__main__": 72 | main() 73 | ``` 74 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | +++ 2 | title = "Home" 3 | renderTitle = false 4 | ordering = 0 5 | +++ 6 | 7 | [CFFI]: https://pypi.python.org/pypi/cffi 8 | [Thalmic Myo SDK]: https://developer.thalmic.com/downloads 9 | 10 | # Welcome to the Myo Python Documentation 11 | 12 | `myo-python` is a [CFFI] based wrapper around the [Thalmic Myo SDK]. It is 13 | compatible with Python 2.7+ and 3.4+. You can find the source code over at 14 | [GitHub]. 15 | 16 | ## Installation 17 | 18 | The Thalmic Myo SDK is only available for Windows and macOS, thus you can not 19 | use `myo-python` on Linux (unfortunately). Use Pip to install the library: 20 | 21 | pip install 'myo-python>=1.0.0' 22 | 23 | Next, download the Myo SDK from the [Developer Downloads][Thalmic Myo SDK] 24 | page and extract it so a convenient location. 25 | 26 | Finally, you need to make sure that `myo-python` can find the shared library 27 | contained in the Myo SDK. There a multiple ways to do this. 28 | 29 | * Add the path to the parent directory of the shared library to your 30 | `PATH` (Windows) or `DYLD_LIBRARY_PATH` (macOS) environment variable. 31 | On macOS, to keep the change persistent, add the following line to your 32 | `~/.bashrc` file: `export DYLD_LIBRARY_PATH=$DYLD_LIBRARY_PATH:/Developer/myo_sdk/myo.framework` 33 | 34 | * Pass either `sdk_path`, `bin_path` or `lib_name` to the `myo.init()` function. 35 | The `bin_path` would be the same as the path that you would add to the 36 | environment variable. 37 | 38 | ## Examples 39 | 40 | There are a bunch of examples available on the GitHub repository. Check them 41 | out [here](https://github.com/NiklasRosenstein/myo-python/tree/master/examples)! 42 | -------------------------------------------------------------------------------- /docs/myo-logo.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NiklasRosenstein/myo-python/80967f206ae6f0a0eb9808319840680e6e60f1bf/docs/myo-logo.jpg -------------------------------------------------------------------------------- /examples/01_hello_myo.py: -------------------------------------------------------------------------------- 1 | # The MIT License (MIT) 2 | # 3 | # Copyright (c) 2017 Niklas Rosenstein 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to 7 | # deal in the Software without restriction, including without limitation the 8 | # rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 9 | # sell copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | # 12 | # The above copyright notice and this permission notice shall be included in 13 | # all copies or substantial portions of the Software. 14 | # 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 20 | # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS 21 | # IN THE SOFTWARE. 22 | """ 23 | This simple example demonstrates how to implement a Device Listener and 24 | how to stop the event flow when the double tap pose is recognized. 25 | """ 26 | 27 | from __future__ import print_function 28 | import myo 29 | 30 | 31 | class Listener(myo.DeviceListener): 32 | 33 | def on_connected(self, event): 34 | print("Hello, '{}'! Double tap to exit.".format(event.device_name)) 35 | event.device.vibrate(myo.VibrationType.short) 36 | event.device.request_battery_level() 37 | 38 | def on_battery_level(self, event): 39 | print("Your battery level is:", event.battery_level) 40 | 41 | def on_pose(self, event): 42 | if event.pose == myo.Pose.double_tap: 43 | return False 44 | 45 | 46 | if __name__ == '__main__': 47 | myo.init() 48 | hub = myo.Hub() 49 | listener = Listener() 50 | while hub.run(listener.on_event, 500): 51 | pass 52 | print('Bye, bye!') 53 | -------------------------------------------------------------------------------- /examples/02_display_data.py: -------------------------------------------------------------------------------- 1 | # The MIT License (MIT) 2 | # 3 | # Copyright (c) 2017 Niklas Rosenstein 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to 7 | # deal in the Software without restriction, including without limitation the 8 | # rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 9 | # sell copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | # 12 | # The above copyright notice and this permission notice shall be included in 13 | # all copies or substantial portions of the Software. 14 | # 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 20 | # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS 21 | # IN THE SOFTWARE. 22 | """ 23 | This example displays the orientation, pose and RSSI as well as EMG data 24 | if it is enabled and whether the device is locked or unlocked in the 25 | terminal. 26 | 27 | Enable EMG streaming with double tap and disable it with finger spread. 28 | """ 29 | 30 | from __future__ import print_function 31 | from myo.utils import TimeInterval 32 | import myo 33 | import sys 34 | 35 | 36 | class Listener(myo.DeviceListener): 37 | 38 | def __init__(self): 39 | self.interval = TimeInterval(None, 0.05) 40 | self.orientation = None 41 | self.pose = myo.Pose.rest 42 | self.emg_enabled = False 43 | self.locked = False 44 | self.rssi = None 45 | self.emg = None 46 | 47 | def output(self): 48 | if not self.interval.check_and_reset(): 49 | return 50 | 51 | parts = [] 52 | if self.orientation: 53 | for comp in self.orientation: 54 | parts.append('{}{:.4f}'.format(' ' if comp >= 0 else '', comp)) 55 | parts.append(str(self.pose).ljust(10)) 56 | parts.append('E' if self.emg_enabled else ' ') 57 | parts.append('L' if self.locked else ' ') 58 | parts.append(self.rssi or 'NORSSI') 59 | if self.emg: 60 | for comp in self.emg: 61 | parts.append(str(comp).ljust(5)) 62 | print('\r' + ''.join('[{}]'.format(p) for p in parts), end='') 63 | sys.stdout.flush() 64 | 65 | def on_connected(self, event): 66 | event.device.request_rssi() 67 | 68 | def on_rssi(self, event): 69 | self.rssi = event.rssi 70 | self.output() 71 | 72 | def on_pose(self, event): 73 | self.pose = event.pose 74 | if self.pose == myo.Pose.double_tap: 75 | event.device.stream_emg(True) 76 | self.emg_enabled = True 77 | elif self.pose == myo.Pose.fingers_spread: 78 | event.device.stream_emg(False) 79 | self.emg_enabled = False 80 | self.emg = None 81 | self.output() 82 | 83 | def on_orientation(self, event): 84 | self.orientation = event.orientation 85 | self.output() 86 | 87 | def on_emg(self, event): 88 | self.emg = event.emg 89 | self.output() 90 | 91 | def on_unlocked(self, event): 92 | self.locked = False 93 | self.output() 94 | 95 | def on_locked(self, event): 96 | self.locked = True 97 | self.output() 98 | 99 | 100 | if __name__ == '__main__': 101 | myo.init() 102 | hub = myo.Hub() 103 | listener = Listener() 104 | while hub.run(listener.on_event, 500): 105 | pass 106 | -------------------------------------------------------------------------------- /examples/03_live_emg.py: -------------------------------------------------------------------------------- 1 | # The MIT License (MIT) 2 | # 3 | # Copyright (c) 2017 Niklas Rosenstein 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to 7 | # deal in the Software without restriction, including without limitation the 8 | # rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 9 | # sell copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | # 12 | # The above copyright notice and this permission notice shall be included in 13 | # all copies or substantial portions of the Software. 14 | # 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 20 | # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS 21 | # IN THE SOFTWARE. 22 | 23 | from matplotlib import pyplot as plt 24 | from collections import deque 25 | from threading import Lock, Thread 26 | 27 | import myo 28 | import numpy as np 29 | 30 | 31 | class EmgCollector(myo.DeviceListener): 32 | """ 33 | Collects EMG data in a queue with *n* maximum number of elements. 34 | """ 35 | 36 | def __init__(self, n): 37 | self.n = n 38 | self.lock = Lock() 39 | self.emg_data_queue = deque(maxlen=n) 40 | 41 | def get_emg_data(self): 42 | with self.lock: 43 | return list(self.emg_data_queue) 44 | 45 | # myo.DeviceListener 46 | 47 | def on_connected(self, event): 48 | event.device.stream_emg(True) 49 | 50 | def on_emg(self, event): 51 | with self.lock: 52 | self.emg_data_queue.append((event.timestamp, event.emg)) 53 | 54 | 55 | class Plot(object): 56 | 57 | def __init__(self, listener): 58 | self.n = listener.n 59 | self.listener = listener 60 | self.fig = plt.figure() 61 | self.axes = [self.fig.add_subplot('81' + str(i)) for i in range(1, 9)] 62 | [(ax.set_ylim([-100, 100])) for ax in self.axes] 63 | self.graphs = [ax.plot(np.arange(self.n), np.zeros(self.n))[0] for ax in self.axes] 64 | plt.ion() 65 | 66 | def update_plot(self): 67 | emg_data = self.listener.get_emg_data() 68 | emg_data = np.array([x[1] for x in emg_data]).T 69 | for g, data in zip(self.graphs, emg_data): 70 | if len(data) < self.n: 71 | # Fill the left side with zeroes. 72 | data = np.concatenate([np.zeros(self.n - len(data)), data]) 73 | g.set_ydata(data) 74 | plt.draw() 75 | 76 | def main(self): 77 | while True: 78 | self.update_plot() 79 | plt.pause(1.0 / 30) 80 | 81 | 82 | def main(): 83 | myo.init() 84 | hub = myo.Hub() 85 | listener = EmgCollector(512) 86 | with hub.run_in_background(listener.on_event): 87 | Plot(listener).main() 88 | 89 | 90 | if __name__ == '__main__': 91 | main() 92 | -------------------------------------------------------------------------------- /examples/04_emg_rate.py: -------------------------------------------------------------------------------- 1 | # The MIT License (MIT) 2 | # 3 | # Copyright (c) 2017 Niklas Rosenstein 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to 7 | # deal in the Software without restriction, including without limitation the 8 | # rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 9 | # sell copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | # 12 | # The above copyright notice and this permission notice shall be included in 13 | # all copies or substantial portions of the Software. 14 | # 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 20 | # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS 21 | # IN THE SOFTWARE. 22 | 23 | from __future__ import print_function 24 | import collections 25 | import myo 26 | import time 27 | import sys 28 | 29 | 30 | class EmgRate(myo.DeviceListener): 31 | 32 | def __init__(self, n): 33 | super(EmgRate, self).__init__() 34 | self.times = collections.deque() 35 | self.last_time = None 36 | self.n = int(n) 37 | 38 | @property 39 | def rate(self): 40 | if not self.times: 41 | return 0.0 42 | else: 43 | return 1.0 / (sum(self.times) / float(self.n)) 44 | 45 | def on_arm_synced(self, event): 46 | event.device.stream_emg(True) 47 | 48 | def on_emg(self, event): 49 | t = time.perf_counter() 50 | if self.last_time is not None: 51 | self.times.append(t - self.last_time) 52 | if len(self.times) > self.n: 53 | self.times.popleft() 54 | self.last_time = t 55 | 56 | 57 | def main(): 58 | myo.init() 59 | hub = myo.Hub() 60 | listener = EmgRate(n=50) 61 | while hub.run(listener.on_event, 500): 62 | print("\r\033[KEMG Rate:", listener.rate, end='') 63 | sys.stdout.flush() 64 | 65 | 66 | if __name__ == '__main__': 67 | main() 68 | -------------------------------------------------------------------------------- /examples/05_api_listener.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2015 Niklas Rosenstein 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to deal 5 | # in the Software without restriction, including without limitation the rights 6 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | # copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in 11 | # all copies or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | # THE SOFTWARE. 20 | 21 | from __future__ import print_function 22 | import myo 23 | import time 24 | 25 | 26 | def main(): 27 | myo.init() 28 | hub = myo.Hub() 29 | listener = myo.ApiDeviceListener() 30 | 31 | with hub.run_in_background(listener.on_event): 32 | print("Waiting for a Myo to connect ...") 33 | device = listener.wait_for_single_device(2) 34 | if not device: 35 | print("No Myo connected after 2 seconds.") 36 | return 37 | 38 | print("Hello, Myo! Requesting RSSI ...") 39 | device.request_rssi() 40 | while hub.running and device.connected and not device.rssi: 41 | print("Waiting for RRSI...") 42 | time.sleep(0.001) 43 | print("RSSI:", device.rssi) 44 | print("Goodbye, Myo!") 45 | 46 | 47 | if __name__ == "__main__": 48 | main() 49 | -------------------------------------------------------------------------------- /myo/__init__.py: -------------------------------------------------------------------------------- 1 | # The MIT License (MIT) 2 | # 3 | # Copyright (c) 2015-2018 Niklas Rosenstein 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to 7 | # deal in the Software without restriction, including without limitation the 8 | # rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 9 | # sell copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | # 12 | # The above copyright notice and this permission notice shall be included in 13 | # all copies or substantial portions of the Software. 14 | # 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 20 | # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS 21 | # IN THE SOFTWARE. 22 | 23 | __version__ = '1.0.5' 24 | __author__ = 'Niklas Rosenstein ' 25 | 26 | from ._ffi import * 27 | from ._device_listener import DeviceListener, ApiDeviceListener 28 | 29 | supported_sdk_version = '0.9.0' 30 | -------------------------------------------------------------------------------- /myo/_device_listener.py: -------------------------------------------------------------------------------- 1 | # The MIT License (MIT) 2 | # 3 | # Copyright (c) 2015-2018 Niklas Rosenstein 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to 7 | # deal in the Software without restriction, including without limitation the 8 | # rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 9 | # sell copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | # 12 | # The above copyright notice and this permission notice shall be included in 13 | # all copies or substantial portions of the Software. 14 | # 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 20 | # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS 21 | # IN THE SOFTWARE. 22 | 23 | import threading 24 | import warnings 25 | from ._ffi import EventType, Pose, VibrationType 26 | from .utils import TimeoutManager 27 | from .math import Vector, Quaternion 28 | 29 | 30 | class DeviceListener(object): 31 | """ 32 | Base class for device listeners -- objects that listen to Myo device events. 33 | """ 34 | 35 | def on_event(self, event): 36 | if event.type.name: # An event type that we know of. 37 | attr = 'on_' + event.type.name 38 | try: 39 | method = getattr(self, attr) 40 | except AttributeError: 41 | pass 42 | else: 43 | return method(event) 44 | 45 | warnings.warn('unhandled event: {}'.format(event)) 46 | return True # continue 47 | 48 | def on_paired(self, event): pass 49 | def on_unpaired(self, event): pass 50 | def on_connected(self, event): pass 51 | def on_disconnected(self, event): pass 52 | def on_arm_synced(self, event): pass 53 | def on_arm_unsynced(self, event): pass 54 | def on_unlocked(self, event): pass 55 | def on_locked(self, event): pass 56 | def on_pose(self, event): pass 57 | def on_orientation(self, event): pass 58 | def on_rssi(self, event): pass 59 | def on_battery_level(self, event): pass 60 | def on_emg(self, event): pass 61 | def on_warmup_completed(self, event): pass 62 | 63 | 64 | class DeviceProxy(object): 65 | """ 66 | Stateful container for Myo device data. 67 | """ 68 | 69 | def __init__(self, device, timestamp, firmware_version, mac_address, 70 | condition_class=threading.Condition): 71 | self._device = device 72 | self._mac_address = mac_address 73 | self._cond = condition_class() 74 | self._pair_time = timestamp 75 | self._unpair_time = None 76 | self._connect_time = None 77 | self._disconnect_time = None 78 | self._emg = None 79 | self._orientation_update_index = 0 80 | self._orientation = Quaternion.identity() 81 | self._acceleration = Vector(0, 0, 0) 82 | self._gyroscope = Vector(0, 0, 0) 83 | self._pose = Pose.rest 84 | self._arm = None 85 | self._x_direction = None 86 | self._rssi = None 87 | self._battery_level = None 88 | self._firmware_version = firmware_version 89 | self._name = None 90 | 91 | def __repr__(self): 92 | with self._cond: 93 | con = 'connected' if self._connected else 'disconnected' 94 | return ''.format(con, self.name) 95 | 96 | @property 97 | def _connected(self): 98 | return self._connect_time is not None and self._disconnect_time is None 99 | 100 | @property 101 | def connected(self): 102 | with self._cond: 103 | return self._connected 104 | 105 | @property 106 | def paired(self): 107 | with self._cond: 108 | return self._unpair_time is not None 109 | 110 | @property 111 | def mac_address(self): 112 | return self._mac_address 113 | 114 | @property 115 | def pair_time(self): 116 | return self._pair_time 117 | 118 | @property 119 | def unpair_time(self): 120 | with self._cond: 121 | return self._unpair_time 122 | 123 | @property 124 | def connect_time(self): 125 | return self._connect_time 126 | 127 | @property 128 | def disconnect_time(self): 129 | with self._cond: 130 | return self._disconnect_time 131 | 132 | @property 133 | def firmware_version(self): 134 | return self._firmware_version 135 | 136 | @property 137 | def orientation_update_index(self): 138 | with self._cond: 139 | return self._orientation_update_index 140 | 141 | @property 142 | def orientation(self): 143 | with self._cond: 144 | return self._orientation.copy() 145 | 146 | @property 147 | def acceleration(self): 148 | with self._cond: 149 | return self._acceleration.copy() 150 | 151 | @property 152 | def gyroscope(self): 153 | with self._cond: 154 | return self._gyroscope.copy() 155 | 156 | @property 157 | def pose(self): 158 | with self._cond: 159 | return self._pose 160 | 161 | @property 162 | def arm(self): 163 | with self._cond: 164 | return self._arm 165 | 166 | @property 167 | def x_direction(self): 168 | with self._cond: 169 | return self._x_direction 170 | 171 | @property 172 | def rssi(self): 173 | with self._cond: 174 | return self._rssi 175 | 176 | @property 177 | def emg(self): 178 | with self._cond: 179 | return self._emg 180 | 181 | def set_locking_policy(self, policy): 182 | self._device.set_locking_policy(policy) 183 | 184 | def stream_emg(self, type): 185 | self._device.stream_emg(type) 186 | 187 | def vibrate(self, type=VibrationType.short): 188 | self._device.vibrate(type) 189 | 190 | def request_rssi(self): 191 | with self._cond: 192 | self._rssi = None 193 | self._device.request_rssi() 194 | 195 | def request_battery_level(self): 196 | with self._cond: 197 | self._battery_level = None 198 | self._device.request_battery_level() 199 | 200 | 201 | class ApiDeviceListener(DeviceListener): 202 | 203 | def __init__(self, condition_class=threading.Condition): 204 | self._condition_class = condition_class 205 | self._cond = condition_class() 206 | self._devices = {} 207 | 208 | @property 209 | def devices(self): 210 | with self._cond: 211 | return list(self._devices.values()) 212 | 213 | @property 214 | def connected_devices(self): 215 | with self._cond: 216 | return [x for x in self._devices.values() if x.connected] 217 | 218 | def wait_for_single_device(self, timeout=None, interval=0.5): 219 | """ 220 | Waits until a Myo is was paired **and** connected with the Hub and returns 221 | it. If the *timeout* is exceeded, returns None. This function will not 222 | return a Myo that is only paired but not connected. 223 | 224 | # Parameters 225 | timeout: The maximum time to wait for a device. 226 | interval: The interval at which the function should exit sleeping. We can 227 | not sleep endlessly, otherwise the main thread can not be exit, eg. 228 | through a KeyboardInterrupt. 229 | """ 230 | 231 | timer = TimeoutManager(timeout) 232 | with self._cond: 233 | # As long as there are no Myo's connected, wait until we 234 | # get notified about a change. 235 | while not timer.check(): 236 | # Check if we found a Myo that is connected. 237 | for device in self._devices.values(): 238 | if device.connected: 239 | return device 240 | self._cond.wait(timer.remainder(interval)) 241 | 242 | return None 243 | 244 | def on_event(self, event): 245 | with self._cond: 246 | if event.type == EventType.paired: 247 | device = DeviceProxy(event.device, event.timestamp, 248 | event.firmware_version, self._condition_class) 249 | self._devices[device._device.handle] = device 250 | self._cond.notify_all() 251 | return 252 | else: 253 | try: 254 | if event.type == EventType.unpaired: 255 | device = self._devices.pop(event.device.handle) 256 | else: 257 | device = self._devices[event.device.handle] 258 | except KeyError: 259 | message = 'Myo device not in the device list ({})' 260 | warnings.warn(message.format(event), RuntimeWarning) 261 | return 262 | if event.type == EventType.unpaired: 263 | with device._cond: 264 | device._unpair_time = event.timestamp 265 | self._cond.notify_all() 266 | 267 | with device._cond: 268 | if event.type == EventType.connected: 269 | device._connect_time = event.timestamp 270 | elif event.type == EventType.disconnected: 271 | device._disconnect_time = event.timestamp 272 | elif event.type == EventType.emg: 273 | device._emg = event.emg 274 | elif event.type == EventType.arm_synced: 275 | device._arm = event.arm 276 | device._x_direction = event.x_direction 277 | elif event.type == EventType.rssi: 278 | device._rssi = event.rssi 279 | elif event.type == EventType.battery_level: 280 | device._battery_level = event.battery_level 281 | elif event.type == EventType.pose: 282 | device._pose = event.pose 283 | elif event.type == EventType.orientation: 284 | device._orientation_update_index += 1 285 | device._orientation = event.orientation 286 | device._gyroscope = event.gyroscope 287 | device._acceleration = event.acceleration 288 | -------------------------------------------------------------------------------- /myo/_ffi.py: -------------------------------------------------------------------------------- 1 | # The MIT License (MIT) 2 | # 3 | # Copyright (c) 2015-2018 Niklas Rosenstein 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to 7 | # deal in the Software without restriction, including without limitation the 8 | # rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 9 | # sell copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | # 12 | # The above copyright notice and this permission notice shall be included in 13 | # all copies or substantial portions of the Software. 14 | # 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 20 | # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS 21 | # IN THE SOFTWARE. 22 | 23 | import contextlib 24 | import cffi 25 | import os 26 | import pkgutil 27 | import re 28 | import threading 29 | import six 30 | import sys 31 | 32 | from .macaddr import MacAddress 33 | from .math import Quaternion, Vector 34 | 35 | try: 36 | from enum import IntEnum 37 | except: 38 | from enum34 import IntEnum 39 | 40 | 41 | ## 42 | # Exceptions 43 | ## 44 | 45 | class Error(Exception): 46 | """ 47 | Base class for errors and exceptions in the myo library. 48 | """ 49 | 50 | 51 | class ResultError(Error): 52 | """ 53 | Raised if the result of an operation with the Myo library was anything 54 | but successful. 55 | """ 56 | 57 | def __init__(self, kind, message): 58 | self.kind = kind 59 | self.message = message 60 | 61 | def __str__(self): 62 | return str((self.kind, self.message)) 63 | 64 | 65 | class InvalidOperation(Error): 66 | """ 67 | Raised if an invalid operation is performed, for example if you attempt to 68 | read the firmware version in any event other than *paired* and *connect*. 69 | """ 70 | 71 | 72 | ## 73 | # Enumerations 74 | ## 75 | 76 | class Result(IntEnum): 77 | __fallback__ = True 78 | success = 0 79 | error = 1 80 | error_invalid_argument = 2 81 | error_runtime = 3 82 | 83 | 84 | class VibrationType(IntEnum): 85 | __fallback__ = True 86 | short = 0 87 | medium = 1 88 | long = 2 89 | 90 | 91 | class StreamEmg(IntEnum): 92 | __fallback__ = True 93 | disabled = 0 94 | enabled = 1 95 | 96 | 97 | class Pose(IntEnum): 98 | __fallback__ = True 99 | rest = 0 100 | fist = 1 101 | wave_in = 2 102 | wave_out = 3 103 | fingers_spread = 4 104 | double_tap = 5 105 | 106 | 107 | Pose.num_poses = 6 108 | 109 | 110 | class EventType(IntEnum): 111 | __fallback__ = True 112 | paired = 0 113 | unpaired = 1 114 | connected = 2 115 | disconnected = 3 116 | arm_synced = 4 117 | arm_unsynced = 5 118 | orientation = 6 119 | pose = 7 120 | rssi = 8 121 | unlocked = 9 122 | locked = 10 123 | emg = 11 124 | battery_level = 12 125 | warmup_completed = 13 126 | 127 | 128 | class HandlerResult(IntEnum): 129 | __fallback__ = True 130 | continue_ = 0 131 | stop = 1 132 | 133 | 134 | class LockingPolicy(IntEnum): 135 | __fallback__ = True 136 | none = 0 #: Pose events are always sent. 137 | standard = 1 #: (default) Pose events are not sent while a Myo is locked. 138 | 139 | 140 | class Arm(IntEnum): 141 | __fallback__ = True 142 | right = 0 143 | left = 1 144 | unknown = 2 145 | 146 | 147 | class XDirection(IntEnum): 148 | __fallback__ = True 149 | toward_wrist = 0 150 | toward_elbow = 1 151 | unknown = 2 152 | 153 | 154 | class UnlockType(IntEnum): 155 | __fallback__ = True 156 | timed = 0 157 | hold = 1 158 | 159 | 160 | class UserActionType(IntEnum): 161 | __fallback__ = True 162 | single = 0 163 | 164 | 165 | class WarmupState(IntEnum): 166 | __fallback__ = True 167 | unknown = 0 168 | cold = 1 169 | warm = 2 170 | 171 | 172 | class WarmupResult(IntEnum): 173 | __fallback__ = True 174 | unknown = 0 175 | success = 1 176 | failed_timeout = 2 177 | 178 | 179 | ## 180 | # CFFI 181 | ## 182 | 183 | def _getffi(): 184 | string = pkgutil.get_data(__name__, 'libmyo.h').decode('utf8') 185 | string = string.replace('\r\n', '\n') 186 | # Remove stuff that cffi can not parse. 187 | string = re.sub('^\s*#.*$', '', string, flags=re.M) 188 | string = string.replace('LIBMYO_EXPORT', '') 189 | string = string.replace('extern "C" {', '') 190 | string = string.replace('} // extern "C"', '') 191 | 192 | ffi = cffi.FFI() 193 | ffi.cdef(string) 194 | return ffi 195 | 196 | 197 | ffi = _getffi() 198 | libmyo = None 199 | 200 | 201 | def _getdlname(): 202 | arch = 32 if sys.maxsize <= 2 ** 32 else 64 203 | if sys.platform.startswith('win32'): 204 | return 'myo{}.dll'.format(arch) 205 | elif sys.platform.startswith('darwin'): 206 | return 'myo' 207 | else: 208 | raise RuntimeError('unsupported platform: {!r}'.format(sys.platform)) 209 | 210 | 211 | def init(lib_name=None, bin_path=None, sdk_path=None): 212 | """ 213 | Initialize the Myo SDK by loading the libmyo shared library. With no 214 | arguments, libmyo must be on your `PATH` or `LD_LIBRARY_PATH`. 215 | 216 | You can specify the exact path to libmyo with *lib_name*. Alternatively, 217 | you can specify the binaries directory that contains libmyo with *bin_path*. 218 | Finally, you can also pass the path to the Myo SDK root directory and it 219 | will figure out the path to libmyo by itself. 220 | """ 221 | 222 | if sum(bool(x) for x in [lib_name, bin_path, sdk_path]) > 1: 223 | raise ValueError('expected zero or one argument(s)') 224 | 225 | if sdk_path: 226 | if sys.platform.startswith('win32'): 227 | bin_path = os.path.join(sdk_path, 'bin') 228 | elif sys.platform.startswith('darwin'): 229 | bin_path = os.path.join(sdk_path, 'myo.framework') 230 | else: 231 | raise RuntimeError('unsupported platform: {!r}'.format(sys.platform)) 232 | if bin_path: 233 | lib_name = os.path.join(bin_path, _getdlname()) 234 | if not lib_name: 235 | lib_name = _getdlname() 236 | 237 | global libmyo 238 | libmyo = ffi.dlopen(lib_name) 239 | 240 | 241 | class _BaseWrapper(object): 242 | 243 | def __init__(self, handle): 244 | self._handle = handle 245 | 246 | @property 247 | def handle(self): 248 | return self._handle 249 | 250 | 251 | class ErrorDetails(_BaseWrapper): 252 | """ 253 | Wraps Myo error details information. 254 | """ 255 | 256 | def __init__(self): 257 | super(ErrorDetails, self).__init__(ffi.new('libmyo_hub_t*')) 258 | 259 | def __del__(self): 260 | if self._handle[0]: 261 | libmyo.libmyo_free_error_details(self._handle[0]) 262 | 263 | @property 264 | def kind(self): 265 | if self._handle[0]: 266 | result = Result(libmyo.libmyo_error_kind(self._handle[0])) 267 | else: 268 | result = Result.success 269 | return result 270 | 271 | @property 272 | def message(self): 273 | if self._handle[0]: 274 | return ffi.string(libmyo.libmyo_error_cstring(self._handle[0])) 275 | else: 276 | return '' 277 | 278 | @property 279 | def handle(self): 280 | return self._handle 281 | 282 | def raise_for_kind(self): 283 | kind = self.kind 284 | if kind != Result.success: 285 | raise ResultError(kind, self.message) 286 | 287 | 288 | class Event(_BaseWrapper): 289 | 290 | def __init__(self, handle): 291 | super(Event, self).__init__(handle) 292 | self._type = EventType(libmyo.libmyo_event_get_type(self._handle)) 293 | 294 | def __repr__(self): 295 | return 'Event(type={!r}, timestamp={!r}, mac_address={!r})'.format( 296 | self.type, self.timestamp, self.mac_address) 297 | 298 | @property 299 | def type(self): 300 | return self._type 301 | 302 | @property 303 | def timestamp(self): 304 | return libmyo.libmyo_event_get_timestamp(self._handle) 305 | 306 | @property 307 | def device(self): 308 | return Device(libmyo.libmyo_event_get_myo(self._handle)) 309 | 310 | @property 311 | def device_name(self): 312 | return str(String(libmyo.libmyo_event_get_myo_name(self._handle))) 313 | 314 | @property 315 | def mac_address(self): 316 | if self.type == EventType.emg: 317 | return None 318 | return MacAddress(libmyo.libmyo_event_get_mac_address(self._handle)) 319 | 320 | @property 321 | def firmware_version(self): 322 | return tuple(libmyo.libmyo_event_get_firmware_version(self._handle, x) 323 | for x in [0, 1, 2, 3]) 324 | 325 | @property 326 | def arm(self): 327 | if self.type != EventType.arm_synced: 328 | raise InvalidOperation() 329 | return Arm(libmyo.libmyo_event_get_arm(self._handle)) 330 | 331 | @property 332 | def x_direction(self): 333 | if self.type != EventType.arm_synced: 334 | raise InvalidOperation() 335 | return XDirection(libmyo.libmyo_event_get_x_direction(self._handle)) 336 | 337 | @property 338 | def warmup_state(self): 339 | if self.type != EventType.arm_synced: 340 | raise InvalidOperation() 341 | return WarmupState(libmyo.libmyo_event_get_warmup_state(self._handle)) 342 | 343 | @property 344 | def warmup_result(self): 345 | if self.type != EventType.warmup_completed: 346 | raise InvalidOperation() 347 | return WarmupResult(libmyo.libmyo_event_get_warmup_result(self._handle)) 348 | 349 | @property 350 | def rotation_on_arm(self): 351 | if self.type != EventType.arm_synced: 352 | raise InvalidOperation() 353 | return libmyo.libmyo_event_get_rotation_on_arm(self._handle) 354 | 355 | @property 356 | def orientation(self): 357 | if self.type != EventType.orientation: 358 | raise InvalidOperation() 359 | vals = (libmyo.libmyo_event_get_orientation(self._handle, i) 360 | for i in [0, 1, 2, 3]) 361 | return Quaternion(*vals) 362 | 363 | @property 364 | def acceleration(self): 365 | if self.type != EventType.orientation: 366 | raise InvalidOperation() 367 | vals = (libmyo.libmyo_event_get_accelerometer(self._handle, i) 368 | for i in [0, 1, 2]) 369 | return Vector(*vals) 370 | 371 | @property 372 | def gyroscope(self): 373 | if self.type != EventType.orientation: 374 | raise InvalidOperation() 375 | vals = (libmyo.libmyo_event_get_gyroscope(self._handle, i) 376 | for i in [0, 1, 2]) 377 | return Vector(*vals) 378 | 379 | @property 380 | def pose(self): 381 | if self.type != EventType.pose: 382 | raise InvalidOperation() 383 | return Pose(libmyo.libmyo_event_get_pose(self._handle)) 384 | 385 | @property 386 | def rssi(self): 387 | if self.type != EventType.rssi: 388 | raise InvalidOperation() 389 | return libmyo.libmyo_event_get_rssi(self._handle) 390 | 391 | @property 392 | def battery_level(self): 393 | if self.type != EventType.battery_level: 394 | raise InvalidOperation() 395 | return libmyo.libmyo_event_get_battery_level(self._handle) 396 | 397 | @property 398 | def emg(self): 399 | if self.type != EventType.emg: 400 | raise InvalidOperation() 401 | return [libmyo.libmyo_event_get_emg(self._handle, i) for i in range(8)] 402 | 403 | 404 | class Device(_BaseWrapper): 405 | 406 | # libmyo_get_mac_address() is not in the Myo SDK 0.9.0 DLL. 407 | #@property 408 | #def mac_address(self): 409 | # return MacAddress(libmyo.libmyo_get_mac_address(self._handle)) 410 | 411 | def vibrate(self, type=VibrationType.medium): 412 | if not isinstance(type, VibrationType): 413 | raise TypeError('expected VibrationType') 414 | error = ErrorDetails() 415 | libmyo.libmyo_vibrate(self._handle, int(type), error.handle) 416 | return error.raise_for_kind() 417 | 418 | def stream_emg(self, type): 419 | if type is True: type = StreamEmg.enabled 420 | elif type is False: type = StreamEmg.disabled 421 | elif not isinstance(type, StreamEmg): 422 | raise TypeError('expected bool or StreamEmg') 423 | error = ErrorDetails() 424 | libmyo.libmyo_set_stream_emg(self._handle, int(type), error.handle) 425 | error.raise_for_kind() 426 | 427 | def request_rssi(self): 428 | error = ErrorDetails() 429 | libmyo.libmyo_request_rssi(self._handle, error.handle) 430 | error.raise_for_kind() 431 | 432 | def request_battery_level(self): 433 | error = ErrorDetails() 434 | libmyo.libmyo_request_battery_level(self._handle, error.handle) 435 | error.raise_for_kind() 436 | 437 | def unlock(self, type=UnlockType.hold): 438 | if not isinstance(type, UnlockType): 439 | raise TypeError('expected UnlockType') 440 | error = ErrorDetails() 441 | libmyo.libmyo_myo_unlock(self._handle, int(type), error.handle) 442 | error.raise_for_kind() 443 | 444 | def lock(self): 445 | error = ErrorDetails() 446 | libmyo.libmyo_myo_lock(self._handle, error.handle) 447 | error.raise_for_kind() 448 | 449 | def notify_user_action(self, type=UserActionType.single): 450 | if not isinstance(type, UserActionType): 451 | raise TypeError('expected UserActionType') 452 | error = ErrorDetails() 453 | libmyo.libmyo_myo_notify_user_action(self._handle, int(type), error.handle) 454 | error.raise_for_kind() 455 | 456 | 457 | class String(_BaseWrapper): 458 | 459 | def __str__(self): 460 | return ffi.string(libmyo.libmyo_string_c_str(self._handle)).decode('utf8') 461 | 462 | def __del__(self): 463 | libmyo.libmyo_string_free(self._handle) 464 | 465 | 466 | class Hub(_BaseWrapper): 467 | """ 468 | Low-level wrapper for a Myo Hub object. 469 | """ 470 | 471 | def __init__(self, application_identifier='com.niklasrosenstein.myo-python'): 472 | super(Hub, self).__init__(ffi.new('libmyo_hub_t*')) 473 | error = ErrorDetails() 474 | libmyo.libmyo_init_hub(self._handle, application_identifier.encode('ascii'), error.handle) 475 | error.raise_for_kind() 476 | self.locking_policy = LockingPolicy.none 477 | self._lock = threading.Lock() 478 | self._running = False 479 | self._stop_requested = False 480 | self._stopped = False 481 | 482 | def __del__(self): 483 | if self._handle[0]: 484 | error = ErrorDetails() 485 | libmyo.libmyo_shutdown_hub(self._handle[0], error.handle) 486 | error.raise_for_kind() 487 | 488 | @property 489 | def handle(self): 490 | return self._handle 491 | 492 | @property 493 | def locking_policy(self): 494 | return self._locking_policy 495 | 496 | @locking_policy.setter 497 | def locking_policy(self, policy): 498 | if not isinstance(policy, LockingPolicy): 499 | raise TypeError('expected LockingPolicy') 500 | error = ErrorDetails() 501 | libmyo.libmyo_set_locking_policy(self._handle[0], int(policy), error.handle) 502 | error.raise_for_kind() 503 | 504 | @property 505 | def running(self): 506 | with self._lock: 507 | return self._running 508 | 509 | def run(self, handler, duration_ms): 510 | """ 511 | Runs the *handler* function for *duration_ms* milliseconds. The function 512 | must accept exactly one argument which is an #Event object. The handler 513 | must return either a #HandlerResult value, #False, #True or #None, whereas 514 | #False represents #HandlerResult.stop and #True and #None represent 515 | #HandlerResult.continue_. 516 | 517 | If the run did not complete due to the handler returning #HandlerResult.stop 518 | or #False or the procedure was cancelled via #Hub.stop(), this function 519 | returns #False. If the full *duration_ms* completed, #True is returned. 520 | 521 | This function blocks the caller until either *duration_ms* passed, the 522 | handler returned #HandlerResult.stop or #False or #Hub.stop() was called. 523 | """ 524 | 525 | if not callable(handler): 526 | if hasattr(handler, 'on_event'): 527 | handler = handler.on_event 528 | else: 529 | raise TypeError('expected callable or DeviceListener') 530 | 531 | with self._lock: 532 | if self._running: 533 | raise RuntimeError('a handler is already running in the Hub') 534 | self._running = True 535 | self._stop_requested = False 536 | self._stopped = False 537 | 538 | exc_box = [] 539 | 540 | def callback_on_error(*exc_info): 541 | exc_box.append(exc_info) 542 | with self._lock: 543 | self._stopped = True 544 | return HandlerResult.stop 545 | 546 | def callback(_, event): 547 | with self._lock: 548 | if self._stop_requested: 549 | self._stopped = True 550 | return HandlerResult.stop 551 | 552 | result = handler(Event(event)) 553 | if result is None or result is True: 554 | result = HandlerResult.continue_ 555 | elif result is False: 556 | result = HandlerResult.stop 557 | else: 558 | result = HandlerResult(result) 559 | if result == HandlerResult.stop: 560 | with self._lock: 561 | self._stopped = True 562 | 563 | return result 564 | 565 | cdecl = 'libmyo_handler_result_t(void*, libmyo_event_t)' 566 | callback = ffi.callback(cdecl, callback, onerror=callback_on_error) 567 | 568 | try: 569 | error = ErrorDetails() 570 | libmyo.libmyo_run(self._handle[0], duration_ms, callback, ffi.NULL, error.handle) 571 | error.raise_for_kind() 572 | if exc_box: 573 | six.reraise(*exc_box[0]) 574 | finally: 575 | with self._lock: 576 | self._running = False 577 | result = not self._stopped 578 | 579 | return result 580 | 581 | def run_forever(self, handler, duration_ms=500): 582 | while self.run(handler, duration_ms): 583 | if self._stop_requested: 584 | break 585 | 586 | @contextlib.contextmanager 587 | def run_in_background(self, handler, duration_ms=500): 588 | thread = threading.Thread(target=lambda: self.run_forever(handler, duration_ms)) 589 | thread.start() 590 | try: 591 | yield thread 592 | finally: 593 | self.stop() 594 | 595 | def stop(self): 596 | with self._lock: 597 | self._stop_requested = True 598 | 599 | 600 | __all__ = [ 601 | 'Error', 'ResultError', 'InvalidOperation', 602 | 603 | 'Result', 'VibrationType', 'StreamEmg', 'Pose', 'EventType', 'LockingPolicy', 604 | 'Arm', 'XDirection', 'UnlockType', 'UserActionType', 'WarmupState', 605 | 'WarmupResult', 606 | 607 | 'Event', 'Device', 'Hub', 'init' 608 | ] 609 | -------------------------------------------------------------------------------- /myo/libmyo.h: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2013-2014 Thalmic Labs Inc. 2 | // Distributed under the Myo SDK license agreement. See LICENSE.txt for details. 3 | #ifndef MYO_LIBMYO_H 4 | #define MYO_LIBMYO_H 5 | 6 | #include 7 | 8 | #include "libmyo/detail/visibility.h" 9 | 10 | #ifdef __cplusplus 11 | extern "C" { 12 | #endif 13 | 14 | /// @file libmyo.h 15 | /// libmyo C API declarations. 16 | 17 | typedef void* libmyo_hub_t; 18 | 19 | /// \defgroup errors Error Handling 20 | /// @{ 21 | 22 | /// Function result codes. 23 | /// All libmyo functions that can fail return a value of this type. 24 | typedef enum { 25 | libmyo_success, 26 | libmyo_error, 27 | libmyo_error_invalid_argument, 28 | libmyo_error_runtime 29 | } libmyo_result_t; 30 | 31 | /// Opaque handle to detailed error information. 32 | typedef void* libmyo_error_details_t; 33 | 34 | /// Return a null-terminated string with a detailed error message. 35 | LIBMYO_EXPORT 36 | const char* libmyo_error_cstring(libmyo_error_details_t); 37 | 38 | /// Returns the kind of error that occurred. 39 | LIBMYO_EXPORT 40 | libmyo_result_t libmyo_error_kind(libmyo_error_details_t); 41 | 42 | /// Free the resources allocated by an error object. 43 | LIBMYO_EXPORT 44 | void libmyo_free_error_details(libmyo_error_details_t); 45 | 46 | /// @} 47 | 48 | /// \defgroup libmyo_string Strings 49 | /// @{ 50 | 51 | // Opaque string. 52 | typedef void* libmyo_string_t; 53 | 54 | // Return a null-terminated string from the opaque string. 55 | LIBMYO_EXPORT 56 | const char* libmyo_string_c_str(libmyo_string_t); 57 | 58 | // Free the resources allocated by the string object. 59 | LIBMYO_EXPORT 60 | void libmyo_string_free(libmyo_string_t); 61 | 62 | /// @} 63 | 64 | /// \defgroup libmyo_direct_mac_addresses MAC address utilities 65 | /// @{ 66 | 67 | /// Retrieve the string representation of a MAC address in hex. 68 | /// Returns a string in the format of 00-00-00-00-00-00. 69 | LIBMYO_EXPORT 70 | libmyo_string_t libmyo_mac_address_to_string(uint64_t); 71 | 72 | /// Retrieve the MAC address from a null-terminated string in the format of 00-00-00-00-00-00. 73 | /// Returns 0 if the string does not match the format. 74 | LIBMYO_EXPORT 75 | uint64_t libmyo_string_to_mac_address(const char*); 76 | 77 | /// @} 78 | 79 | /// @defgroup libmyo_hub Hub instance 80 | /// @{ 81 | 82 | /// Initialize a connection to the hub. 83 | /// \a application_identifier must follow a reverse domain name format (ex. com.domainname.appname). Application 84 | /// identifiers can be formed from the set of alphanumeric ASCII characters (a-z, A-Z, 0-9). The hyphen (-) and 85 | /// underscore (_) characters are permitted if they are not adjacent to a period (.) character (i.e. not at the start or 86 | /// end of each segment), but are not permitted in the top-level domain. Application identifiers must have three or more 87 | /// segments. For example, if a company's domain is example.com and the application is named hello-world, one could use 88 | /// "com.example.hello-world" as a valid application identifier. \a application_identifier can be NULL or empty. 89 | /// @returns libmyo_success if the connection is successfully established, otherwise: 90 | /// - libmyo_error_runtime if a connection could not be established 91 | /// - libmyo_error_invalid_argument if \a out_hub is NULL 92 | /// - libmyo_error_invalid_argument if \a application_identifier is longer than 255 characters 93 | /// - libmyo_error_invalid_argument if \a application_identifier is not in the proper reverse domain name format 94 | LIBMYO_EXPORT 95 | libmyo_result_t libmyo_init_hub(libmyo_hub_t* out_hub, const char* application_identifier, 96 | libmyo_error_details_t* out_error); 97 | 98 | /// Free the resources allocated to a hub. 99 | /// @returns libmyo_success if shutdown is successful, otherwise: 100 | /// - libmyo_error_invalid_argument if \a hub is NULL 101 | /// - libmyo_error if \a hub is not a valid hub 102 | LIBMYO_EXPORT 103 | libmyo_result_t libmyo_shutdown_hub(libmyo_hub_t hub, libmyo_error_details_t* out_error); 104 | 105 | // Locking policies. 106 | typedef enum { 107 | libmyo_locking_policy_none, ///< Pose events are always sent. 108 | libmyo_locking_policy_standard ///< Pose events are not sent while a Myo is locked. 109 | } libmyo_locking_policy_t; 110 | 111 | /// Set the locking policy for Myos connected to the hub. 112 | /// @returns libmyo_success if the locking policy is successfully set, otherwise 113 | /// - libmyo_error_invalid_argument if \a hub is NULL 114 | /// - libmyo_error if \a hub is not a valid hub 115 | LIBMYO_EXPORT 116 | libmyo_result_t libmyo_set_locking_policy(libmyo_hub_t hub, libmyo_locking_policy_t locking_policy, 117 | libmyo_error_details_t* out_error); 118 | 119 | /// @} 120 | 121 | /// @defgroup libmyo_myo Myo instances 122 | /// @{ 123 | 124 | /// Opaque type corresponding to a known Myo device. 125 | typedef void* libmyo_myo_t; 126 | 127 | /// Types of vibration 128 | typedef enum { 129 | libmyo_vibration_short, 130 | libmyo_vibration_medium, 131 | libmyo_vibration_long 132 | } libmyo_vibration_type_t; 133 | 134 | /// Retrieve the MAC address of a Myo. 135 | /// The MAC address is unique to the physical Myo, and is a 48-bit number. 136 | LIBMYO_EXPORT 137 | uint64_t libmyo_get_mac_address(libmyo_myo_t myo); 138 | 139 | /// Vibrate the given myo. 140 | /// Can be called when a Myo is paired. 141 | /// @returns libmyo_success if the Myo successfully vibrated, otherwise 142 | /// - libmyo_error_invalid_argument if \a myo is NULL 143 | LIBMYO_EXPORT 144 | libmyo_result_t libmyo_vibrate(libmyo_myo_t myo, libmyo_vibration_type_t type, libmyo_error_details_t* out_error); 145 | 146 | /// Request the RSSI for a given myo. 147 | /// Can be called when a Myo is paired. A libmyo_event_rssi event will likely be generated with the value of the RSSI. 148 | /// @returns libmyo_success if the Myo successfully got the RSSI, otherwise 149 | /// - libmyo_error_invalid_argument if \a myo is NULL 150 | LIBMYO_EXPORT 151 | libmyo_result_t libmyo_request_rssi(libmyo_myo_t myo, libmyo_error_details_t* out_error); 152 | 153 | 154 | /// Request the battery level for a given Myo. 155 | /// A libmyo_event_battery_level event will be generated with the value of the battery level. 156 | /// @returns libmyo_success if the Myo successfully requested the battery level, otherwise 157 | /// - libmyo_error_invalid_argument if \a myo is NULL 158 | LIBMYO_EXPORT 159 | libmyo_result_t libmyo_request_battery_level(libmyo_myo_t myo_opq, libmyo_error_details_t* out_error); 160 | 161 | /// EMG streaming modes. 162 | typedef enum { 163 | libmyo_stream_emg_disabled, ///< Do not send EMG data. 164 | libmyo_stream_emg_enabled ///< Send EMG data. 165 | } libmyo_stream_emg_t; 166 | 167 | /// Set whether or not to stream EMG data for a given myo. 168 | /// Can be called when a Myo is paired. 169 | /// @returns libmyo_success if the EMG mode was set successfully, otherwise 170 | /// - libmyo_error_invalid_argument if \a myo is NULL 171 | LIBMYO_EXPORT 172 | libmyo_result_t libmyo_set_stream_emg(libmyo_myo_t myo, libmyo_stream_emg_t emg, libmyo_error_details_t* out_error); 173 | 174 | /// @} 175 | 176 | /// @defgroup libmyo_poses Pose recognition. 177 | /// @{ 178 | 179 | /// Supported poses. 180 | typedef enum { 181 | libmyo_pose_rest = 0, ///< Rest pose. 182 | libmyo_pose_fist = 1, ///< User is making a fist. 183 | libmyo_pose_wave_in = 2, ///< User has an open palm rotated towards the posterior of their wrist. 184 | libmyo_pose_wave_out = 3, ///< User has an open palm rotated towards the anterior of their wrist. 185 | libmyo_pose_fingers_spread = 4, ///< User has an open palm with their fingers spread away from each other. 186 | libmyo_pose_double_tap = 5, ///< User tapped their thumb and middle finger together twice in succession. 187 | 188 | libmyo_num_poses, ///< Number of poses supported; not a valid pose. 189 | 190 | libmyo_pose_unknown = 0xffff ///< Unknown pose. 191 | } libmyo_pose_t; 192 | 193 | /// @} 194 | 195 | /// @defgroup libmyo_locking Myo locking mechanism 196 | 197 | /// Valid unlock types. 198 | typedef enum { 199 | libmyo_unlock_timed = 0, ///< Unlock for a fixed period of time. 200 | libmyo_unlock_hold = 1, ///< Unlock until explicitly told to re-lock. 201 | } libmyo_unlock_type_t; 202 | 203 | /// Unlock the given Myo. 204 | /// Can be called when a Myo is paired. A libmyo_event_unlocked event will be generated if the Myo was locked. 205 | /// @returns libmyo_success if the Myo was successfully unlocked, otherwise 206 | /// - libmyo_error_invalid_argument if \a myo is NULL 207 | LIBMYO_EXPORT 208 | libmyo_result_t libmyo_myo_unlock(libmyo_myo_t myo, libmyo_unlock_type_t type, libmyo_error_details_t* out_error); 209 | 210 | /// Lock the given Myo immediately. 211 | /// Can be called when a Myo is paired. A libmyo_event_locked event will be generated if the Myo was unlocked. 212 | /// @returns libmyo_success if the Myo was successfully locked, otherwise 213 | /// - libmyo_error_invalid_argument if \a myo is NULL 214 | LIBMYO_EXPORT 215 | libmyo_result_t libmyo_myo_lock(libmyo_myo_t myo, libmyo_error_details_t* out_error); 216 | 217 | /// User action types. 218 | typedef enum { 219 | libmyo_user_action_single = 0, ///< User did a single, discrete action, such as pausing a video. 220 | } libmyo_user_action_type_t; 221 | 222 | /// Notify the given Myo that a user action was recognized. 223 | /// Can be called when a Myo is paired. Will cause Myo to vibrate. 224 | /// @returns libmyo_success if the Myo was successfully notified, otherwise 225 | /// - libmyo_error_invalid_argument if \a myo is NULL 226 | LIBMYO_EXPORT 227 | libmyo_result_t libmyo_myo_notify_user_action(libmyo_myo_t myo, libmyo_user_action_type_t type, 228 | libmyo_error_details_t* out_error); 229 | 230 | /// @} 231 | 232 | /// @defgroup libmyo_events Event Handling 233 | /// @{ 234 | 235 | /// Types of events. 236 | typedef enum { 237 | libmyo_event_paired, ///< Successfully paired with a Myo. 238 | libmyo_event_unpaired, ///< Successfully unpaired from a Myo. 239 | libmyo_event_connected, ///< A Myo has successfully connected. 240 | libmyo_event_disconnected, ///< A Myo has been disconnected. 241 | libmyo_event_arm_synced, ///< A Myo has recognized that the sync gesture has been successfully performed. 242 | libmyo_event_arm_unsynced, ///< A Myo has been moved or removed from the arm. 243 | libmyo_event_orientation, ///< Orientation data has been received. 244 | libmyo_event_pose, ///< A change in pose has been detected. @see libmyo_pose_t. 245 | libmyo_event_rssi, ///< An RSSI value has been received. 246 | libmyo_event_unlocked, ///< A Myo has become unlocked. 247 | libmyo_event_locked, ///< A Myo has become locked. 248 | libmyo_event_emg, ///< EMG data has been received. 249 | libmyo_event_battery_level, ///< A battery level value has been received. 250 | libmyo_event_warmup_completed, ///< The warmup period has completed. 251 | } libmyo_event_type_t; 252 | 253 | /// Information about an event. 254 | typedef const void* libmyo_event_t; 255 | 256 | /// Retrieve the type of an event. 257 | LIBMYO_EXPORT 258 | uint32_t libmyo_event_get_type(libmyo_event_t event); 259 | 260 | /// Retrieve the timestamp of an event. 261 | /// @see libmyo_now() for details on timestamps. 262 | LIBMYO_EXPORT 263 | uint64_t libmyo_event_get_timestamp(libmyo_event_t event); 264 | 265 | /// Retrieve the Myo associated with an event. 266 | LIBMYO_EXPORT 267 | libmyo_myo_t libmyo_event_get_myo(libmyo_event_t event); 268 | 269 | /// Retrieve the MAC address of the myo associated with an event. 270 | LIBMYO_EXPORT 271 | uint64_t libmyo_event_get_mac_address(libmyo_event_t event_opq); 272 | 273 | /// Retrieve the name of the myo associated with an event. 274 | /// Caller must free the returned string. @see libmyo_string functions. 275 | LIBMYO_EXPORT 276 | libmyo_string_t libmyo_event_get_myo_name(libmyo_event_t event); 277 | 278 | /// Components of version. 279 | typedef enum { 280 | libmyo_version_major, ///< Major version. 281 | libmyo_version_minor, ///< Minor version. 282 | libmyo_version_patch, ///< Patch version. 283 | libmyo_version_hardware_rev, ///< Hardware revision. 284 | } libmyo_version_component_t; 285 | 286 | /// Hardware revisions. 287 | typedef enum { 288 | libmyo_hardware_rev_c = 1, ///< Alpha units 289 | libmyo_hardware_rev_d = 2, ///< Consumer units 290 | } libmyo_hardware_rev_t; 291 | 292 | /// Retrieve the Myo armband's firmware version from this event. 293 | /// Valid for libmyo_event_paired and libmyo_event_connected events. 294 | LIBMYO_EXPORT 295 | unsigned int libmyo_event_get_firmware_version(libmyo_event_t event, libmyo_version_component_t); 296 | 297 | /// Enumeration identifying a right arm or left arm. @see libmyo_event_get_arm(). 298 | typedef enum { 299 | libmyo_arm_right, ///< Myo is on the right arm. 300 | libmyo_arm_left, ///< Myo is on the left arm. 301 | libmyo_arm_unknown, ///< Unknown arm. 302 | } libmyo_arm_t; 303 | 304 | /// Retrieve the arm associated with an event. 305 | /// Valid for libmyo_event_arm_synced events only. 306 | LIBMYO_EXPORT 307 | libmyo_arm_t libmyo_event_get_arm(libmyo_event_t event); 308 | 309 | /// Possible directions for Myo's +x axis relative to a user's arm. 310 | typedef enum { 311 | libmyo_x_direction_toward_wrist, ///< Myo's +x axis is pointing toward the user's wrist. 312 | libmyo_x_direction_toward_elbow, ///< Myo's +x axis is pointing toward the user's elbow. 313 | libmyo_x_direction_unknown, ///< Unknown +x axis direction. 314 | } libmyo_x_direction_t; 315 | 316 | /// Retrieve the x-direction associated with an event. 317 | /// The x-direction specifies which way Myo's +x axis is pointing relative to the user's arm. 318 | /// Valid for libmyo_event_arm_synced events only. 319 | LIBMYO_EXPORT 320 | libmyo_x_direction_t libmyo_event_get_x_direction(libmyo_event_t event); 321 | 322 | /// Possible warmup states for Myo. 323 | typedef enum { 324 | libmyo_warmup_state_unknown = 0, ///< Unknown warm up state. 325 | libmyo_warmup_state_cold = 1, ///< Myo needs to warm up. 326 | libmyo_warmup_state_warm = 2, ///< Myo is already in a warmed up state. 327 | } libmyo_warmup_state_t; 328 | 329 | /// Retrieve the warmup state of the Myo associated with an event. 330 | /// Valid for libmyo_event_arm_synced events only. 331 | LIBMYO_EXPORT 332 | libmyo_warmup_state_t libmyo_event_get_warmup_state(libmyo_event_t event); 333 | 334 | /// Possible warmup results for Myo. 335 | typedef enum { 336 | libmyo_warmup_result_unknown = 0, ///< Unknown warm up result. 337 | libmyo_warmup_result_success = 1, ///< The warm up period has completed successfully. 338 | libmyo_warmup_result_failed_timeout = 2, ///< The warm up period timed out. 339 | } libmyo_warmup_result_t; 340 | 341 | /// Retrieve the warmup result of the Myo associated with an event. 342 | /// Valid for libmyo_event_warmup_completed events only. 343 | LIBMYO_EXPORT 344 | libmyo_warmup_result_t libmyo_event_get_warmup_result(libmyo_event_t event); 345 | 346 | /// Retrieve the estimated rotation of Myo on the user's arm after a sync. 347 | /// The values specifies the rotation of the myo on the arm (0 - logo facing down, pi - logo facing up) 348 | /// Only supported by FW 1.3.x and above (older firmware will always report 0 for the rotation) 349 | /// Valid for libmyo_event_arm_synced events only. 350 | LIBMYO_EXPORT 351 | float libmyo_event_get_rotation_on_arm(libmyo_event_t event); 352 | 353 | /// Index into orientation data, which is provided as a quaternion. 354 | /// Orientation data is returned as a unit quaternion of floats, represented as `w + x * i + y * j + z * k`. 355 | typedef enum { 356 | libmyo_orientation_x = 0, ///< First component of the quaternion's vector part 357 | libmyo_orientation_y = 1, ///< Second component of the quaternion's vector part 358 | libmyo_orientation_z = 2, ///< Third component of the quaternion's vector part 359 | libmyo_orientation_w = 3, ///< Scalar component of the quaternion. 360 | } libmyo_orientation_index; 361 | 362 | /// Retrieve orientation data associated with an event. 363 | /// Valid for libmyo_event_orientation events only. 364 | /// @see libmyo_orientation_index 365 | LIBMYO_EXPORT 366 | float libmyo_event_get_orientation(libmyo_event_t event, libmyo_orientation_index index); 367 | 368 | /// Retrieve raw accelerometer data associated with an event in units of g. 369 | /// Valid for libmyo_event_orientation events only. 370 | /// Requires `index < 3`. 371 | LIBMYO_EXPORT 372 | float libmyo_event_get_accelerometer(libmyo_event_t event, unsigned int index); 373 | 374 | /// Retrieve raw gyroscope data associated with an event in units of deg/s. 375 | /// Valid for libmyo_event_orientation events only. 376 | /// Requires `index < 3`. 377 | LIBMYO_EXPORT 378 | float libmyo_event_get_gyroscope(libmyo_event_t event, unsigned int index); 379 | 380 | /// Retrieve the pose associated with an event. 381 | /// Valid for libmyo_event_pose events only. 382 | LIBMYO_EXPORT 383 | libmyo_pose_t libmyo_event_get_pose(libmyo_event_t event); 384 | 385 | /// Retreive the RSSI associated with an event. 386 | /// Valid for libmyo_event_rssi events only. 387 | LIBMYO_EXPORT 388 | int8_t libmyo_event_get_rssi(libmyo_event_t event); 389 | 390 | /// Retrieve the battery level of the Myo armband associated with an event. 391 | /// Only valid for libmyo_event_battery_level event. 392 | LIBMYO_EXPORT 393 | uint8_t libmyo_event_get_battery_level(libmyo_event_t event); 394 | 395 | /// Retrieve an EMG data point associated with an event. 396 | /// Valid for libmyo_event_emg events only. 397 | /// @a sensor must be smaller than 8. 398 | LIBMYO_EXPORT 399 | int8_t libmyo_event_get_emg(libmyo_event_t event, unsigned int sensor); 400 | 401 | /// Return type for event handlers. 402 | typedef enum { 403 | libmyo_handler_continue, ///< Continue processing events 404 | libmyo_handler_stop, ///< Stop processing events 405 | } libmyo_handler_result_t; 406 | 407 | /// Callback function type to handle events as they occur from libmyo_run(). 408 | typedef libmyo_handler_result_t (*libmyo_handler_t)(void* user_data, libmyo_event_t event); 409 | 410 | /// Process events and call the provided callback as they occur. 411 | /// Runs for up to approximately \a duration_ms milliseconds or until a called handler returns libmyo_handler_stop. 412 | /// @returns libmyo_success after a successful run, otherwise 413 | /// - libmyo_error_invalid_argument if \a hub is NULL 414 | /// - libmyo_error_invalid_argument if \a handler is NULL 415 | LIBMYO_EXPORT 416 | libmyo_result_t libmyo_run(libmyo_hub_t hub, unsigned int duration_ms, libmyo_handler_t handler, void* user_data, 417 | libmyo_error_details_t* out_error); 418 | 419 | /// @} 420 | 421 | #ifdef __cplusplus 422 | } // extern "C" 423 | #endif 424 | 425 | #endif // MYO_LIBMYO_H 426 | -------------------------------------------------------------------------------- /myo/macaddr.py: -------------------------------------------------------------------------------- 1 | # The MIT License (MIT) 2 | # 3 | # Copyright (c) 2015-2018 Niklas Rosenstein 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to 7 | # deal in the Software without restriction, including without limitation the 8 | # rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 9 | # sell copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | # 12 | # The above copyright notice and this permission notice shall be included in 13 | # all copies or substantial portions of the Software. 14 | # 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 20 | # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS 21 | # IN THE SOFTWARE. 22 | 23 | import six 24 | 25 | MAX_VALUE = (16 ** 12 - 1) 26 | 27 | 28 | def encode(value): 29 | """ 30 | Encodes the number *value* to a MAC address ASCII string in binary form. 31 | Raises a #ValueError if *value* is a negative number or exceeds the MAC 32 | address range. 33 | """ 34 | 35 | if value > MAX_VALUE: 36 | raise ValueError('value {!r} exceeds MAC address range'.format(value)) 37 | if value < 0: 38 | raise ValueError('value must not be negative') 39 | 40 | # todo: convert to the right byte order. the resulting 41 | # mac address is reversed on my machine compared to the 42 | # mac address displayed by the hello-myo SDK sample. 43 | # See issue #7 44 | 45 | string = ('%x' % value).rjust(12, '0') 46 | assert len(string) == 12 47 | 48 | result = ':'.join(''.join(pair) for pair in zip(*[iter(string)]*2)) 49 | return result.upper() 50 | 51 | 52 | def decode(bstr): 53 | """ 54 | Decodes an ASCII encoded binary MAC address tring into a number. 55 | """ 56 | 57 | bstr = bstr.replace(b':', b'') 58 | if len(bstr) != 12: 59 | raise ValueError('not a valid MAC address: {!r}'.format(bstr)) 60 | 61 | try: 62 | return int(bstr, 16) 63 | except ValueError: 64 | raise ValueError('not a valid MAC address: {!r}'.format(bstr)) 65 | 66 | 67 | class MacAddress(object): 68 | """ 69 | Represents a MAC address. Instances of this class are immutable. 70 | """ 71 | 72 | def __init__(self, value): 73 | if isinstance(value, six.integer_types): 74 | if value < 0 or value > MAX_VALUE: 75 | raise ValueError('value {!r} out of MAC address range'.format(value)) 76 | elif isinstance(value, six.string_to_int): 77 | if isinstance(value, six.text_type): 78 | value = value.encode('ascii') 79 | value = decode(value) 80 | else: 81 | msg = 'expected string, bytes or int for MacAddress, got {}' 82 | return TypeError(msg.format(type(value).__name__)) 83 | 84 | self._value = value 85 | self._string = None 86 | 87 | def __str__(self): 88 | if self._string is None: 89 | self._string = encode(self._value) 90 | return self._string 91 | 92 | def __repr__(self): 93 | return ''.format(self) 94 | 95 | @property 96 | def value(self): 97 | return self._value 98 | -------------------------------------------------------------------------------- /myo/math.py: -------------------------------------------------------------------------------- 1 | # The MIT License (MIT) 2 | # 3 | # Copyright (c) 2015-2018 Niklas Rosenstein 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to 7 | # deal in the Software without restriction, including without limitation the 8 | # rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 9 | # sell copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | # 12 | # The above copyright notice and this permission notice shall be included in 13 | # all copies or substantial portions of the Software. 14 | # 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 20 | # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS 21 | # IN THE SOFTWARE. 22 | 23 | import math 24 | import six 25 | 26 | 27 | class Vector(object): 28 | """ 29 | A three-dimensional vector. 30 | """ 31 | 32 | __slots__ = ('x', 'y', 'z') 33 | 34 | def __init__(self, x, y, z): 35 | super(Vector, self).__init__() 36 | self.x = float(x) 37 | self.y = float(y) 38 | self.z = float(z) 39 | 40 | def __mul__(self, rhs): 41 | """ 42 | Multiplies the vector with *rhs* which can be either a scalar 43 | to retrieve a new Vector or another vector to compute the dot 44 | product. 45 | """ 46 | 47 | if isinstance(rhs, (six.integer_types, float)): 48 | return Vector(self.x * rhs, self.y * rhs, self.z * rhs) 49 | else: 50 | return self.dot(rhs) 51 | 52 | def __add__(self, rhs): 53 | """ 54 | Adds *self* to *rhs* and returns a new vector. 55 | """ 56 | 57 | if isinstance(rhs, (six.integer_types, float)): 58 | return Vector(self.x + rhs, self.y + rhs, self.z + rhs) 59 | else: 60 | return Vector(self.x + rhs.x, self.y + rhs.y, self.z + rhs.z) 61 | 62 | def __sub__(self, rhs): 63 | """ 64 | Substracts *self* from *rhs* and returns a new vector. 65 | """ 66 | 67 | if isinstance(rhs, (six.integer_types, float)): 68 | return Vector(self.x - rhs, self.y - rhs, self.z - rhs) 69 | else: 70 | return Vector(self.x - rhs.x, self.y - rhs.y, self.z - rhs.z) 71 | 72 | def __iter__(self): 73 | return iter((self.x, self.y, self.z)) 74 | 75 | def __repr__(self): 76 | return 'Vector({0}, {1}, {2})'.format(self.x, self.y, self.z) 77 | 78 | def __invert__(self): 79 | """ 80 | Returns the inversion of the vector. 81 | """ 82 | 83 | return Vector(-self.x, -self.y, -self.z) 84 | 85 | def __getitem__(self, index): 86 | return (self.x, self.y, self.z)[index] 87 | 88 | def copy(self): 89 | """ 90 | Returns a shallow copy of the vector. 91 | """ 92 | 93 | return Vector(self.x, self.y, self.z) 94 | 95 | def magnitude(self): 96 | """ 97 | Return the magnitude of this vector. 98 | """ 99 | 100 | return math.sqrt(self.x ** 2 + self.y ** 2 + self.z ** 2) 101 | 102 | def normalized(self): 103 | """ 104 | Returns a normalized copy of this vector. 105 | """ 106 | 107 | norm = self.magnitude() 108 | return Vector(self.x / norm, self.y / norm, self.z / norm) 109 | 110 | def dot(self, rhs): 111 | """ 112 | Return the dot product of this vector and *rhs*. 113 | """ 114 | 115 | return self.x * rhs.x + self.y * rhs.y + self.z * rhs.z 116 | 117 | def cross(self, rhs): 118 | """ 119 | Return the cross product of this vector and *rhs*. 120 | """ 121 | 122 | return Vector( 123 | self.y * rhs.z - self.z * rhs.y, 124 | self.z * rhs.x - self.x * rhs.z, 125 | self.x * rhs.y - self.y * rhs.x) 126 | 127 | def angle_to(self, rhs): 128 | """ 129 | Return the angle between this vector and *rhs* in radians. 130 | """ 131 | 132 | return math.acos(self.dot(rhs) / (self.magnitude() * rhs.magnitude())) 133 | 134 | __abs__ = magnitude 135 | 136 | 137 | class Quaternion(object): 138 | """ 139 | This class represents a quaternion which can be used to represent 140 | gimbal-lock free rotations. 141 | 142 | This implementation can work with any vector type that has members 143 | x, y and z and it has a constructor that accepts values for these 144 | members in order. This is convenient when combining the Myo SDK 145 | with other 3D APIs that provide a vector class. 146 | """ 147 | 148 | __slots__ = ('x', 'y', 'z', 'w') 149 | 150 | def __init__(self, x, y, z, w): 151 | super(Quaternion, self).__init__() 152 | self.x = float(x) 153 | self.y = float(y) 154 | self.z = float(z) 155 | self.w = float(w) 156 | 157 | def __mul__(self, rhs): 158 | """ 159 | Multiplies *self* with the #Quaternion *rhs* and returns a new #Quaternion. 160 | """ 161 | 162 | if not isinstance(rhs, Quaternion): 163 | raise TypeError('can only multiply with Quaternion') 164 | return Quaternion( 165 | self.w * rhs.x + self.x * rhs.w + self.y * rhs.z - self.z * rhs.y, 166 | self.w * rhs.y - self.x * rhs.z + self.y * rhs.w + self.z * rhs.x, 167 | self.w * rhs.z + self.x * rhs.y - self.y * rhs.x + self.z * rhs.w, 168 | self.w * rhs.w - self.x * rhs.x - self.y * rhs.y - self.z * rhs.z) 169 | 170 | def __iter__(self): 171 | return iter((self.x, self.y, self.z, self.w)) 172 | 173 | def __repr__(self): 174 | return '{0}({1}, {2}, {3}, {4})'.format( 175 | type(self).__name__, self.x, self.y, self.z, self.w) 176 | 177 | def __invert__(self): 178 | """ 179 | Returns this Quaternion's conjugate. 180 | """ 181 | 182 | return Quaternion(-self.x, -self.y, -self.z, self.w) 183 | 184 | def __getitem__(self, index): 185 | return (self.x, self.y, self.z, self.w)[index] 186 | 187 | def copy(self): 188 | """ 189 | Returns a shallow copy of the quaternion. 190 | """ 191 | 192 | return Quaternion(self.x, self.y, self.z, self.w) 193 | 194 | def magnitude(self): 195 | """ 196 | Returns the magnitude of the quaternion. 197 | """ 198 | 199 | return math.sqrt(self.x ** 2 + self.y ** 2 + self.z ** 2 + self.w ** 2) 200 | 201 | def normalized(self): 202 | """ 203 | Returns the unit quaternion corresponding to the same rotation 204 | as this one. 205 | """ 206 | 207 | magnitude = self.magnitude() 208 | return Quaternion( 209 | self.x / magnitude, self.y / magnitude, 210 | self.z / magnitude, self.w / magnitude) 211 | 212 | conjugate = __invert__ 213 | 214 | def rotate(self, vec): 215 | """ 216 | Returns *vec* rotated by this #Quaternion. 217 | 218 | :param vec: A vector object. 219 | :return: object of type of *vec* 220 | """ 221 | 222 | qvec = self * Quaternion(vec.x, vec.y, vec.z, 0) * ~self 223 | return type(vec)(qvec.x, qvec.y, qvec.z) 224 | 225 | # Reference: 226 | # http://answers.unity3d.com/questions/416169/finding-pitchrollyaw-from-quaternions.html 227 | 228 | @property 229 | def roll(self): 230 | """ Calculates the Roll of the Quaternion. """ 231 | 232 | x, y, z, w = self.x, self.y, self.z, self.w 233 | return math.atan2(2*y*w - 2*x*z, 1 - 2*y*y - 2*z*z) 234 | 235 | @property 236 | def pitch(self): 237 | """ Calculates the Pitch of the Quaternion. """ 238 | 239 | x, y, z, w = self.x, self.y, self.z, self.w 240 | return math.atan2(2*x*w - 2*y*z, 1 - 2*x*x - 2*z*z) 241 | 242 | @property 243 | def yaw(self): 244 | """ Calculates the Yaw of the Quaternion. """ 245 | 246 | x, y, z, w = self.x, self.y, self.z, self.w 247 | return math.asin(2*x*y + 2*z*w) 248 | 249 | @property 250 | def rpy(self): 251 | """ Calculates the Roll, Pitch and Yaw of the Quaternion. """ 252 | 253 | x, y, z, w = self.x, self.y, self.z, self.w 254 | roll = math.atan2(2*y*w - 2*x*z, 1 - 2*y*y - 2*z*z) 255 | pitch = math.atan2(2*x*w - 2*y*z, 1 - 2*x*x - 2*z*z) 256 | yaw = math.asin(2*x*y + 2*z*w) 257 | return (roll, pitch, yaw) 258 | 259 | @staticmethod 260 | def identity(): 261 | """ 262 | Returns the identity #Quaternion. 263 | """ 264 | 265 | return Quaternion(0, 0, 0, 1) 266 | 267 | @staticmethod 268 | def rotation_of(source, dest): 269 | """ 270 | Returns a #Quaternion that represents a rotation from vector 271 | *source* to *dest*. 272 | """ 273 | 274 | source = Vector(source.x, source.y, source.z) 275 | dest = Vector(dest.x, dest.y, dest.z) 276 | cross = source.cross(dest) 277 | cos_theta = source.dot(dest) 278 | 279 | # Return identity if the vectors are the same direction. 280 | if cos_theta >= 1.0: 281 | return Quaternion.identity() 282 | 283 | # Product of the square of the magnitudes. 284 | k = math.sqrt(source.dot(source), dest.dot(dest)) 285 | 286 | # Return identity in the degenerate case. 287 | if k <= 0.0: 288 | return Quaternion.identity() 289 | 290 | # Special handling for vectors facing opposite directions. 291 | if cos_theta / k <= -1: 292 | x_axis = Vector(1, 0, 0) 293 | y_axis = Vector(0, 1, 1) 294 | if abs(source.dot(x_ais)) < 1.0: 295 | cross = source.cross(x_axis) 296 | else: 297 | cross = source.cross(y_axis) 298 | 299 | return Quaternion(cross.x, cross.y, cross.z, k + cos_theta) 300 | 301 | @staticmethod 302 | def from_axis_angle(axis, angle): 303 | """ 304 | Returns a #Quaternion that represents the right-handed 305 | rotation of *angle* radians about the givne *axis*. 306 | 307 | :param axis: The unit vector representing the axis of rotation. 308 | :param angle: The angle of rotation, in radians. 309 | """ 310 | 311 | sincomp = math.sin(angle / 2.0) 312 | return Quaternion( 313 | axis.x * sincomp, axis.y * sincomp, 314 | axis.z * sincomp, math.cos(angle / 2.0)) 315 | -------------------------------------------------------------------------------- /myo/types/__init__.py: -------------------------------------------------------------------------------- 1 | # backwards compatibility with versions 1.0.0 - 1.0.3 2 | -------------------------------------------------------------------------------- /myo/types/macaddr.py: -------------------------------------------------------------------------------- 1 | from ..macaddr import * -------------------------------------------------------------------------------- /myo/types/math.py: -------------------------------------------------------------------------------- 1 | from ..math import * -------------------------------------------------------------------------------- /myo/utils.py: -------------------------------------------------------------------------------- 1 | # The MIT License (MIT) 2 | # 3 | # Copyright (c) 2015-2018 Niklas Rosenstein 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to 7 | # deal in the Software without restriction, including without limitation the 8 | # rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 9 | # sell copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | # 12 | # The above copyright notice and this permission notice shall be included in 13 | # all copies or substantial portions of the Software. 14 | # 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 20 | # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS 21 | # IN THE SOFTWARE. 22 | 23 | 24 | import time 25 | 26 | 27 | class TimeInterval(object): 28 | """ 29 | A helper class to keep track of a time interval. 30 | """ 31 | 32 | def __init__(self, value, value_on_reset=None, clock=None): 33 | self.value = value 34 | self.value_on_reset = value_on_reset 35 | self.clock = clock or time.perf_counter 36 | self.start = self.clock 37 | 38 | def check(self): 39 | """ 40 | Returns #True if the time interval has passed. 41 | """ 42 | 43 | if self.value is None: 44 | return True 45 | return (self.clock() - self.start) >= self.value 46 | 47 | def reset(self, value=None): 48 | """ 49 | Resets the start time of the interval to now or the specified value. 50 | """ 51 | 52 | if value is None: 53 | value = self.clock() 54 | self.start = value 55 | if self.value_on_reset: 56 | self.value = self.value_on_reset 57 | 58 | def check_and_reset(self, value=None): 59 | """ 60 | Combination of #check() and #reset(). 61 | """ 62 | 63 | if self.check(): 64 | self.reset(value) 65 | return True 66 | return False 67 | 68 | 69 | class TimeoutManager(TimeInterval): 70 | 71 | def check(self): 72 | """ 73 | Returns #True if the timeout is exceeded. 74 | """ 75 | 76 | if self.value is None: 77 | return False 78 | return (self.clock() - self.start) >= self.value 79 | 80 | def remainder(self, max_value=None): 81 | """ 82 | Returns the time remaining for the timeout, or *max_value* if that 83 | remainder is larger. 84 | """ 85 | 86 | if self.value is None: 87 | return max_value 88 | remainder = self.value - (self.clock() - self.start) 89 | if remainder < 0.0: 90 | return 0.0 91 | elif max_value is not None and remainder > max_value: 92 | return max_value 93 | else: 94 | return remainder 95 | -------------------------------------------------------------------------------- /package.yml: -------------------------------------------------------------------------------- 1 | name: myo-python 2 | version: 1.0.5 3 | author: Niklas Rosenstein 4 | modulename: myo 5 | description: Python bindings for the Thalmic Labs Myo SDK. 6 | url: https://github.com/NiklasRosenstein/myo-python 7 | license: MIT 8 | source-directory: . 9 | requirements: 10 | - cffi ^1.11.5 11 | - python ^3.5 12 | - six ^1.11.0 13 | package-data: 14 | - include: libmyo.h 15 | test-drivers: 16 | - type: pytest 17 | publish: 18 | pypi: 19 | credentials: 20 | username: __token__ 21 | password: $PYPI_TOKEN 22 | test_username: __token__ 23 | test_password: $TEST_PYPI_TOKEN 24 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # This file was auto-generated by Shut. DO NOT EDIT 2 | # For more information about Shut, check out https://pypi.org/project/shut/ 3 | 4 | from __future__ import print_function 5 | import io 6 | import os 7 | import setuptools 8 | import sys 9 | 10 | readme_file = 'README.md' 11 | if os.path.isfile(readme_file): 12 | with io.open(readme_file, encoding='utf8') as fp: 13 | long_description = fp.read() 14 | else: 15 | print("warning: file \"{}\" does not exist.".format(readme_file), file=sys.stderr) 16 | long_description = None 17 | 18 | requirements = [ 19 | 'cffi >=1.11.5,<2.0.0', 20 | 'six >=1.11.0,<2.0.0', 21 | ] 22 | 23 | setuptools.setup( 24 | name = 'myo-python', 25 | version = '1.0.5', 26 | author = 'Niklas Rosenstein', 27 | author_email = 'rosensteinniklas@gmail.com', 28 | description = 'Python bindings for the Thalmic Labs Myo SDK.', 29 | long_description = long_description, 30 | long_description_content_type = 'text/markdown', 31 | url = 'https://github.com/NiklasRosenstein/myo-python', 32 | license = 'MIT', 33 | packages = setuptools.find_packages('.', ['test', 'test.*', 'tests', 'tests.*', 'docs', 'docs.*']), 34 | package_dir = {'': '.'}, 35 | include_package_data = True, 36 | install_requires = requirements, 37 | extras_require = {}, 38 | tests_require = [], 39 | python_requires = '>=3.5.0,<4.0.0', 40 | data_files = [], 41 | entry_points = {}, 42 | cmdclass = {}, 43 | keywords = [], 44 | classifiers = [], 45 | zip_safe = True, 46 | ) 47 | -------------------------------------------------------------------------------- /test/test_import.py: -------------------------------------------------------------------------------- 1 | 2 | 3 | def test_import(): 4 | import myo 5 | import myo.types 6 | --------------------------------------------------------------------------------