├── .gitignore ├── CITATION.cff ├── LICENSE ├── README.md ├── examples ├── Notebooks │ └── Classifiers.ipynb ├── data_collector.py ├── dino_jump.py ├── knn_classifier.py ├── live_classifiers.py ├── myo_imu_examp.py ├── myo_multithreading_examp.py ├── plot_emgs.py ├── plot_emgs_mat.py ├── poweroff.py └── speedtest.py ├── pyproject.toml ├── requirements.txt ├── setup.cfg └── src └── pyomyo ├── Classifier.py ├── __init__.py ├── data ├── vals0.dat ├── vals1.dat ├── vals2.dat ├── vals3.dat ├── vals4.dat ├── vals5.dat ├── vals6.dat ├── vals7.dat ├── vals8.dat └── vals9.dat └── pyomyo.py /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__/* 2 | */__pycache__/* 3 | .ipynb_checkpoints/* 4 | 5 | data/* 6 | */data/* 7 | 8 | dist/* 9 | .venv 10 | .env/ 11 | Pipfile 12 | *.egg-info 13 | 14 | *.csv 15 | *.dat 16 | */.ipynb_checkpoints/* -------------------------------------------------------------------------------- /CITATION.cff: -------------------------------------------------------------------------------- 1 | # This CITATION.cff file was generated with cffinit. 2 | # Visit https://bit.ly/cffinit to generate yours today! 3 | 4 | cff-version: 1.2.0 5 | title: PyoMyo 6 | message: Python open-source Myo library 7 | type: software 8 | authors: 9 | - given-names: Peter 10 | family-names: Walkington 11 | email: perlinwarp@gmail.com 12 | orcid: 'https://orcid.org/0000-0003-3195-8679' 13 | identifiers: 14 | - type: doi 15 | value: 10.5281/zenodo.7632303 16 | repository-code: 'https://github.com/PerlinWarp/pyomyo' 17 | url: 'https://github.com/PerlinWarp/pyomyo/wiki' 18 | abstract: >- 19 | Python open-source Myo library (PyoMyo) is a 20 | cross-platform library made for the Thalmic Labs Myo that 21 | implements a Bluetooth protocol over Serial using the 22 | included dongle. 23 | keywords: 24 | - EMG 25 | - electromyography 26 | license: MIT 27 | commit: 23f5d4c57034c7a82a354d0f4e6d5f69061b642b 28 | version: 0.0.5 29 | date-released: '2021-11-13' 30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 PerlinWarp 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 deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | 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 all 13 | 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 FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PyoMyo 2 | Python module for the Thalmic Labs Myo armband. 3 | 4 | Cross platform and multithreaded and works without the Myo SDK. 5 | 6 | ``` 7 | pip install pyomyo 8 | ``` 9 | Documentation is in the Wiki, see [Getting Started](https://github.com/PerlinWarp/pyomyo/wiki/Getting-started). 10 | 11 | ![Playing breakout with sEMG](https://github.com/PerlinWarp/Neuro-Breakout/blob/main/media/Breakout.gif?raw=true "Breakout") 12 | 13 | ### PyoMyo Documentation 14 | [Home](https://github.com/PerlinWarp/pyomyo/wiki) 15 | [Getting started](https://github.com/PerlinWarp/pyomyo/wiki/Getting-started) 16 | [Common Problems](https://github.com/PerlinWarp/pyomyo/wiki/Common-Problems) 17 | [Myo Placement](https://github.com/PerlinWarp/pyomyo/wiki/Myo-Placement) 18 | 19 | #### The big picture 20 | [Why should you care?](https://github.com/PerlinWarp/pyomyo/wiki/Why-should-you-care%3F) 21 | [Basics of EMG Design](https://github.com/PerlinWarp/pyomyo/wiki/The-basics-of-EMG-design) 22 | 23 | [Links to other resources](https://github.com/PerlinWarp/pyomyo/wiki/Links) 24 | 25 | ## Python Open-source Myo library 26 | 27 | This library was made from a fork of the MIT licensed [dhzu/myo-raw.](https://github.com/dzhu/myo-raw) 28 | Bug fixes from [Alvipe/myo-raw](https://github.com/Alvipe/myo-raw) were also added to stop crashes and also add essential features. 29 | 30 | This code was then updated to Python3, multithreading support was added then more bug fixes and other features were added, including support for all 3 EMG modes the Myo can use. 31 | 32 | **Note that sEMG data, the same kind gathered by the Myo is thought to be uniquely identifiable. Do not share this data without careful consideration of the future implications.** 33 | 34 | Also note, the Myo is outdated hardware, over the last year I have noticed a steady incline in the cost of second hand Myos. Both of my Myo's were bought for under £100, I do not recommend spending more than that to acquire one. Instead of buying one you should [join the discord](https://discord.com/invite/mG58PVyk83) to create an open hardware alternative! 35 | 36 | ## Included Example Code 37 | The examples sub-folder contains some different ways of using the pyomyo library. 38 | ``` 39 | git clone https://github.com/PerlinWarp/pyomyo 40 | ``` 41 | 42 | 43 | ### plot_emgs_mat.py 44 |

45 | Left to Right Wrist movements. 46 |

47 | 48 | Starts the Myo in mode 0x01 which provides data that's already preprocessed (bandpass filter + rectified). 49 | This data is then plotted in Matplotlib and is a good first step to see how the Myo works. 50 | Sliding your finger under each sensor on the Myo will help identify which plot is for sensor. 51 | 52 | ### dino_jump.py 53 |

54 | Chrome Dinosaur Game 55 |

56 | 57 | An example showing how to use the live classifier built into pyomyo, see [Getting Started](https://github.com/PerlinWarp/pyomyo/wiki/Getting-started) for more info. 58 | 59 | ### myo_multithreading_examp.py 60 | Devs start here. 61 | This file shows how to use the library and get Myo data in a seperate thread. 62 | 63 | 64 | ## Myo Modes Explained 65 | To communicate with the Myo, I used [dzhu's myo-raw](https://github.com/dzhu/myo-raw). 66 | Then added some functions from [Alvipe](https://github.com/dzhu/myo-raw/pull/23) to allow changing of the Myo's LED. 67 | 68 | emg_mode.PREPROCESSED (0x01) 69 | By default myo-raw sends 50Hz data that has been rectified and filtered, using a hidden 0x01 mode. 70 | 71 | emg_mode.FILTERED (0x02) 72 | Alvipe added the ability to also get filtered non-rectified sEMG (thanks Alvipe). 73 | 74 | emg_mode.RAW (0x03) 75 | Then I further added the ability to get true raw non-filtered data at 200Hz. 76 | This data is unrectified but scales from -128 and 127. 77 | 78 | Sample data and a comparison between data captured in these modes can be found in [MyoEMGPreprocessing.ipynb](https://github.com/PerlinWarp/Neuro-Breakout/blob/main/Notebooks/MyoModesCompared/MyoEMGPreprocessing.ipynb) 79 | 80 | ## The library 81 | 82 | ### pyomyo.py 83 | Prints sEMG readings at 200Hz straight from the Myo's ADC using the raw EMG mode. 84 | Each EMG readings is between -128 and 127, it is the most "raw" the Myo can provide, however it's unlikely to be useful without extra processing. 85 | This file is also where the Myo driver is implemented, which uses Serial commands which are then sent over Bluetooth to interact with the Myo. 86 | 87 | ### Classifier.py 88 | Implements a live classifier using the k-nearest neighbors algorithm. 89 | Press a number from 0-9 to label incoming data as the class represented by the number. 90 | Press e to delete all the data you have gathered. 91 | Once two classes have been made new data is automatically classified. Labelled data is stored as a numpy array in the ``data\`` directory. 92 | -------------------------------------------------------------------------------- /examples/data_collector.py: -------------------------------------------------------------------------------- 1 | # Simplistic data recording 2 | import time 3 | import multiprocessing 4 | import numpy as np 5 | import pandas as pd 6 | 7 | from pyomyo import Myo, emg_mode 8 | 9 | def data_worker(mode, seconds, filepath): 10 | collect = True 11 | 12 | # ------------ Myo Setup --------------- 13 | m = Myo(mode=mode) 14 | m.connect() 15 | 16 | myo_data = [] 17 | 18 | def add_to_queue(emg, movement): 19 | myo_data.append(emg) 20 | 21 | m.add_emg_handler(add_to_queue) 22 | 23 | def print_battery(bat): 24 | print("Battery level:", bat) 25 | 26 | m.add_battery_handler(print_battery) 27 | 28 | # Its go time 29 | m.set_leds([0, 128, 0], [0, 128, 0]) 30 | # Vibrate to know we connected okay 31 | m.vibrate(1) 32 | 33 | print("Data Worker started to collect") 34 | # Start collecing data. 35 | start_time = time.time() 36 | 37 | while collect: 38 | if (time.time() - start_time < seconds): 39 | m.run() 40 | else: 41 | collect = False 42 | collection_time = time.time() - start_time 43 | print("Finished collecting.") 44 | print(f"Collection time: {collection_time}") 45 | print(len(myo_data), "frames collected") 46 | 47 | # Add columns and save to df 48 | myo_cols = ["Channel_1", "Channel_2", "Channel_3", "Channel_4", "Channel_5", "Channel_6", "Channel_7", "Channel_8"] 49 | myo_df = pd.DataFrame(myo_data, columns=myo_cols) 50 | myo_df.to_csv(filepath, index=False) 51 | print("CSV Saved at: ", filepath) 52 | 53 | # -------- Main Program Loop ----------- 54 | if __name__ == '__main__': 55 | seconds = 10 56 | file_name = str(seconds)+"_test_emg.csv" 57 | mode = emg_mode.PREPROCESSED 58 | p = multiprocessing.Process(target=data_worker, args=(mode, seconds, file_name)) 59 | p.start() 60 | -------------------------------------------------------------------------------- /examples/dino_jump.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Instructions: 3 | 0. Install pynput and XGboost e.g. pip install pynput xgboost 4 | 1. Run python dino_jump.py - This launches the training tool. 5 | 2. Click on the pygame window thats opened to make sure windows sends the keypresses to that process. 6 | 3. Relax the Myo arm, and with your other hand press 0 - This labels the incoming data as class 0 7 | 4. Make a fist with your hand and press 1, to label the fist as 1. 8 | 5. Try making a closed and open fist and watching the bars change. 9 | 6. Once you've gathered enough data, exit the pygame window. This saves the data in data/vals0.dat and vals1.dat 10 | 7. If you make a mistake and wrongly classify data, delete vals0 and vals1 and regather 11 | 8. If your happy it works, change TRAINING_MODE to False. 12 | 9. Goto https://trex-runner.com/ and rerun dino_jump.py with TRAINING_MODE set to false. 13 | 10. Click in the brower to start the game and tell windows to send keypresses there 14 | 11. Try making a fist and seeing if the dino jumps 15 | 16 | If it doesn't work, feel free to let me know in the discord: 17 | https://discord.com/invite/mG58PVyk83 18 | 19 | - PerlinWarp 20 | ''' 21 | 22 | import pygame 23 | from pygame.locals import * 24 | from pynput.keyboard import Key, Controller 25 | from pyomyo import Myo, emg_mode 26 | from pyomyo.Classifier import Live_Classifier, MyoClassifier, EMGHandler 27 | from xgboost import XGBClassifier 28 | 29 | TRAINING_MODE = False 30 | 31 | def dino_handler(pose): 32 | print("Pose detected", pose) 33 | if ((pose == 1) and (TRAINING_MODE == False)): 34 | for i in range(0,10): 35 | # Press and release space 36 | keyboard.press(Key.space) 37 | keyboard.release(Key.space) 38 | 39 | if __name__ == '__main__': 40 | keyboard = Controller() 41 | 42 | pygame.init() 43 | w, h = 800, 320 44 | scr = pygame.display.set_mode((w, h)) 45 | font = pygame.font.Font(None, 30) 46 | 47 | # Make an ML Model to train and test with live 48 | # XGBoost Classifier Example 49 | model = XGBClassifier(eval_metric='logloss') 50 | clr = Live_Classifier(model, name="XG", color=(50,50,255)) 51 | m = MyoClassifier(clr, mode=emg_mode.PREPROCESSED, hist_len=10) 52 | 53 | hnd = EMGHandler(m) 54 | m.add_emg_handler(hnd) 55 | m.connect() 56 | 57 | m.add_raw_pose_handler(dino_handler) 58 | 59 | # Set Myo LED color to model color 60 | m.set_leds(m.cls.color, m.cls.color) 61 | # Set pygame window name 62 | pygame.display.set_caption(m.cls.name) 63 | 64 | try: 65 | while True: 66 | # Run the Myo, get more data 67 | m.run() 68 | # Run the classifier GUI 69 | m.run_gui(hnd, scr, font, w, h) 70 | 71 | except KeyboardInterrupt: 72 | pass 73 | finally: 74 | m.disconnect() 75 | print() 76 | pygame.quit() -------------------------------------------------------------------------------- /examples/knn_classifier.py: -------------------------------------------------------------------------------- 1 | ''' 2 | The MIT License (MIT) 3 | Copyright (c) 2020 PerlinWarp 4 | Copyright (c) 2014 Danny Zhu 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy of 7 | this software and associated documentation files (the "Software"), to deal in 8 | the Software without restriction, including without limitation the rights to 9 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 10 | the Software, and to permit persons to whom the Software is furnished to do so, 11 | subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in 14 | all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 18 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 19 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 20 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 21 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 22 | ''' 23 | 24 | from collections import Counter, deque 25 | import struct 26 | import sys 27 | import time 28 | 29 | import pygame 30 | from pygame.locals import * 31 | import numpy as np 32 | from sklearn import neighbors 33 | 34 | from pyomyo import Myo, emg_mode 35 | from pyomyo.Classifier import Classifier, MyoClassifier, EMGHandler 36 | 37 | SUBSAMPLE = 3 38 | K = 15 39 | 40 | class KNN_Classifier(Classifier): 41 | '''Live implimentation of SkLearns KNN''' 42 | 43 | def __init__(self): 44 | Classifier.__init__(self) 45 | 46 | def train(self, X, Y): 47 | self.X = X 48 | self.Y = Y 49 | self.model = None 50 | if self.X.shape[0] >= K * SUBSAMPLE: 51 | self.model = neighbors.KNeighborsClassifier(n_neighbors=K, algorithm='kd_tree') 52 | self.model.fit(self.X[::SUBSAMPLE], self.Y[::SUBSAMPLE]) 53 | 54 | def classify(self, emg): 55 | x = np.array(emg).reshape(1,-1) 56 | if self.X.shape[0] < K * SUBSAMPLE: 57 | return 0 58 | 59 | pred = self.model.predict(x) 60 | return int(pred[0]) 61 | 62 | def text(scr, font, txt, pos, clr=(255,255,255)): 63 | scr.blit(font.render(txt, True, clr), pos) 64 | 65 | if __name__ == '__main__': 66 | pygame.init() 67 | w, h = 800, 320 68 | scr = pygame.display.set_mode((w, h)) 69 | font = pygame.font.Font(None, 30) 70 | 71 | m = MyoClassifier(KNN_Classifier(), mode=emg_mode.PREPROCESSED) 72 | hnd = EMGHandler(m) 73 | m.add_emg_handler(hnd) 74 | m.connect() 75 | 76 | m.add_raw_pose_handler(print) 77 | 78 | try: 79 | while True: 80 | m.run() 81 | 82 | r = m.history_cnt.most_common(1)[0][0] 83 | 84 | # Handle keypresses 85 | for ev in pygame.event.get(): 86 | if ev.type == QUIT or (ev.type == KEYDOWN and ev.unicode == 'q'): 87 | raise KeyboardInterrupt() 88 | elif ev.type == KEYDOWN: 89 | if K_0 <= ev.key <= K_9: 90 | hnd.recording = ev.key - K_0 91 | elif K_KP0 <= ev.key <= K_KP9: 92 | hnd.recording = ev.key - K_Kp0 93 | elif ev.unicode == 'r': 94 | hnd.cl.read_data() 95 | elif ev.unicode == 'e': 96 | print("Pressed e, erasing local data") 97 | m.cls.delete_data() 98 | elif ev.type == KEYUP: 99 | if K_0 <= ev.key <= K_9 or K_KP0 <= ev.key <= K_KP9: 100 | hnd.recording = -1 101 | 102 | # Plotting 103 | scr.fill((0, 0, 0), (0, 0, w, h)) 104 | 105 | for i in range(10): 106 | x = 0 107 | y = 0 + 30 * i 108 | 109 | clr = (0,200,0) if i == r else (255,255,255) 110 | 111 | txt = font.render('%5d' % (m.cls.Y == i).sum(), True, (255,255,255)) 112 | scr.blit(txt, (x + 20, y)) 113 | 114 | txt = font.render('%d' % i, True, clr) 115 | scr.blit(txt, (x + 110, y)) 116 | 117 | # Plot the history of predicitons 118 | scr.fill((0,0,0), (x+130, y + txt.get_height() / 2 - 10, len(m.history) * 20, 20)) 119 | scr.fill(clr, (x+130, y + txt.get_height() / 2 - 10, m.history_cnt[i] * 20, 20)) 120 | 121 | if m.cls.model is not None: 122 | print("emg", hnd.emg) 123 | x = np.array(hnd.emg).reshape(1,-1) 124 | dists, inds = m.cls.model.kneighbors(x) 125 | for i, (d, ind) in enumerate(zip(dists[0], inds[0])): 126 | y = m.cls.Y[SUBSAMPLE*ind] 127 | print("y", y) 128 | text(scr, font, '%d %6d' % (y, d), (650, 20 * i)) 129 | 130 | 131 | pygame.display.flip() 132 | 133 | except KeyboardInterrupt: 134 | pass 135 | finally: 136 | m.disconnect() 137 | print() 138 | pygame.quit() 139 | -------------------------------------------------------------------------------- /examples/live_classifiers.py: -------------------------------------------------------------------------------- 1 | from collections import Counter, deque 2 | import struct 3 | import sys 4 | import time 5 | 6 | import pygame 7 | from pygame.locals import * 8 | import numpy as np 9 | 10 | from sklearn.preprocessing import StandardScaler 11 | from sklearn.svm import SVC 12 | from sklearn.pipeline import make_pipeline 13 | from sklearn.linear_model import LogisticRegression 14 | from sklearn.tree import DecisionTreeClassifier 15 | from xgboost import XGBClassifier 16 | from sklearn.naive_bayes import GaussianNB 17 | 18 | from pyomyo import Myo, emg_mode 19 | from pyomyo.Classifier import Live_Classifier, MyoClassifier, EMGHandler 20 | 21 | class SVM_Classifier(Live_Classifier): 22 | ''' 23 | Live implimentation of an SVM Classifier 24 | ''' 25 | def __init__(self): 26 | Live_Classifier.__init__(self, None, "SVM", (100,0,100)) 27 | 28 | def train(self, X, Y): 29 | self.X = X 30 | self.Y = Y 31 | try: 32 | if self.X.shape[0] > 0: 33 | clf = make_pipeline(StandardScaler(), SVC(gamma='auto')) 34 | #clf = make_pipeline(StandardScaler(), SVC(kernel="linear", C=0.025)) 35 | 36 | clf.fit(self.X, self.Y) 37 | self.model = clf 38 | except: 39 | # SVM Errors when we only have data for 1 class. 40 | self.model = None 41 | 42 | def classify(self, emg): 43 | if self.X.shape[0] == 0 or self.model == None: 44 | # We have no data or model, return 0 45 | return 0 46 | 47 | x = np.array(emg).reshape(1,-1) 48 | pred = self.model.predict(x) 49 | return int(pred[0]) 50 | 51 | 52 | class DC_Classifier(Live_Classifier): 53 | ''' 54 | Live implimentation of Decision Trees 55 | ''' 56 | def __init__(self): 57 | Live_Classifier.__init__(self, DecisionTreeClassifier(), name="DC_Classifier", color=(212,175,55)) 58 | 59 | class XG_Classifier(Live_Classifier): 60 | ''' 61 | Live implimentation of XGBoost 62 | ''' 63 | def __init__(self): 64 | Live_Classifier.__init__(self, XGBClassifier(), name="xgboost", color=(0,150,150)) 65 | 66 | class LR_Classifier(Live_Classifier): 67 | ''' 68 | Live implimentation of Logistic Regression 69 | ''' 70 | def __init__(self): 71 | Live_Classifier.__init__(self, None, name="LR", color=(100,0,100)) 72 | 73 | def train(self, X, Y): 74 | self.X = X 75 | self.Y = Y 76 | try: 77 | if self.X.shape[0] > 0: 78 | self.model = LogisticRegression() 79 | self.model.fit(self.X, self.Y) 80 | except: 81 | # LR Errors when we only have data for 1 class. 82 | self.model = None 83 | 84 | def classify(self, emg): 85 | if self.X.shape[0] == 0 or self.model == None: 86 | # We have no data or model, return 0 87 | return 0 88 | 89 | x = np.array(emg).reshape(1,-1) 90 | pred = self.model.predict(x) 91 | return int(pred[0]) 92 | 93 | 94 | if __name__ == '__main__': 95 | pygame.init() 96 | w, h = 800, 320 97 | scr = pygame.display.set_mode((w, h)) 98 | font = pygame.font.Font(None, 30) 99 | 100 | # SVM Example 101 | m = MyoClassifier(SVM_Classifier(), mode=emg_mode.PREPROCESSED) 102 | # Logistic Regression Example 103 | #m = MyoClassifier(LR_Classifier(), mode=emg_mode.PREPROCESSED) 104 | # Live classifier example 105 | #model = GaussianNB() 106 | #m = MyoClassifier(Live_Classifier(model, name="NB", color=(255,165,50))) 107 | 108 | hnd = EMGHandler(m) 109 | m.add_emg_handler(hnd) 110 | m.connect() 111 | 112 | m.add_raw_pose_handler(print) 113 | 114 | # Set Myo LED color to model color 115 | m.set_leds(m.cls.color, m.cls.color) 116 | # Set pygame window name 117 | pygame.display.set_caption(m.cls.name) 118 | 119 | try: 120 | while True: 121 | m.run() 122 | 123 | m.run_gui(hnd, scr, font, w, h) 124 | 125 | except KeyboardInterrupt: 126 | pass 127 | finally: 128 | m.disconnect() 129 | print() 130 | pygame.quit() 131 | -------------------------------------------------------------------------------- /examples/myo_imu_examp.py: -------------------------------------------------------------------------------- 1 | import multiprocessing 2 | from pyomyo import Myo, emg_mode 3 | import os 4 | 5 | def cls(): 6 | # Clear the screen in a cross platform way 7 | # https://stackoverflow.com/questions/517970/how-to-clear-the-interpreter-console 8 | os.system('cls' if os.name=='nt' else 'clear') 9 | 10 | # ------------ Myo Setup --------------- 11 | q = multiprocessing.Queue() 12 | 13 | def worker(q): 14 | m = Myo(mode=emg_mode.FILTERED) 15 | m.connect() 16 | 17 | def add_to_queue(quat, acc, gyro): 18 | imu_data = [quat, acc, gyro] 19 | q.put(imu_data) 20 | 21 | m.add_imu_handler(add_to_queue) 22 | 23 | # Orange logo and bar LEDs 24 | m.set_leds([128, 128, 0], [128, 128, 0]) 25 | # Vibrate to know we connected okay 26 | m.vibrate(1) 27 | 28 | """worker function""" 29 | while True: 30 | m.run() 31 | print("Worker Stopped") 32 | 33 | # -------- Main Program Loop ----------- 34 | if __name__ == "__main__": 35 | p = multiprocessing.Process(target=worker, args=(q,)) 36 | p.start() 37 | 38 | try: 39 | while True: 40 | while not(q.empty()): 41 | imu = list(q.get()) 42 | quat, acc, gyro = imu 43 | print("Quaternions:", quat) 44 | print("Acceleration:", acc) 45 | print("Gyroscope:", gyro) 46 | cls() 47 | 48 | except KeyboardInterrupt: 49 | print("Quitting") 50 | quit() -------------------------------------------------------------------------------- /examples/myo_multithreading_examp.py: -------------------------------------------------------------------------------- 1 | import multiprocessing 2 | from pyomyo import Myo, emg_mode 3 | 4 | # ------------ Myo Setup --------------- 5 | q = multiprocessing.Queue() 6 | 7 | def worker(q): 8 | m = Myo(mode=emg_mode.FILTERED) 9 | m.connect() 10 | 11 | def add_to_queue(emg, movement): 12 | q.put(emg) 13 | 14 | m.add_emg_handler(add_to_queue) 15 | 16 | def print_battery(bat): 17 | print("Battery level:", bat) 18 | 19 | m.add_battery_handler(print_battery) 20 | 21 | # Orange logo and bar LEDs 22 | m.set_leds([128, 0, 0], [128, 0, 0]) 23 | # Vibrate to know we connected okay 24 | m.vibrate(1) 25 | 26 | """worker function""" 27 | while True: 28 | m.run() 29 | print("Worker Stopped") 30 | 31 | # -------- Main Program Loop ----------- 32 | if __name__ == "__main__": 33 | p = multiprocessing.Process(target=worker, args=(q,)) 34 | p.start() 35 | 36 | try: 37 | while True: 38 | while not(q.empty()): 39 | emg = list(q.get()) 40 | print(emg) 41 | 42 | except KeyboardInterrupt: 43 | print("Quitting") 44 | quit() -------------------------------------------------------------------------------- /examples/plot_emgs.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Can plot EMG data in 2 different ways 3 | change DRAW_LINES to try each. 4 | Press Ctrl + C in the terminal to exit 5 | ''' 6 | 7 | import pygame 8 | from pygame.locals import * 9 | import multiprocessing 10 | 11 | from pyomyo import Myo, emg_mode 12 | 13 | # ------------ Myo Setup --------------- 14 | q = multiprocessing.Queue() 15 | 16 | def worker(q): 17 | m = Myo(mode=emg_mode.PREPROCESSED) 18 | m.connect() 19 | 20 | def add_to_queue(emg, movement): 21 | q.put(emg) 22 | 23 | m.add_emg_handler(add_to_queue) 24 | 25 | def print_battery(bat): 26 | print("Battery level:", bat) 27 | 28 | m.add_battery_handler(print_battery) 29 | 30 | # Orange logo and bar LEDs 31 | m.set_leds([128, 0, 0], [128, 0, 0]) 32 | # Vibrate to know we connected okay 33 | m.vibrate(1) 34 | 35 | """worker function""" 36 | while True: 37 | m.run() 38 | print("Worker Stopped") 39 | 40 | last_vals = None 41 | def plot(scr, vals): 42 | DRAW_LINES = True 43 | 44 | global last_vals 45 | if last_vals is None: 46 | last_vals = vals 47 | return 48 | 49 | D = 5 50 | scr.scroll(-D) 51 | scr.fill((0, 0, 0), (w - D, 0, w, h)) 52 | for i, (u, v) in enumerate(zip(last_vals, vals)): 53 | if DRAW_LINES: 54 | pygame.draw.line(scr, (0, 255, 0), 55 | (w - D, int(h/9 * (i+1 - u))), 56 | (w, int(h/9 * (i+1 - v)))) 57 | pygame.draw.line(scr, (255, 255, 255), 58 | (w - D, int(h/9 * (i+1))), 59 | (w, int(h/9 * (i+1)))) 60 | else: 61 | c = int(255 * max(0, min(1, v))) 62 | scr.fill((c, c, c), (w - D, i * h / 8, D, (i + 1) * h / 8 - i * h / 8)) 63 | 64 | pygame.display.flip() 65 | last_vals = vals 66 | 67 | # -------- Main Program Loop ----------- 68 | if __name__ == "__main__": 69 | p = multiprocessing.Process(target=worker, args=(q,)) 70 | p.start() 71 | 72 | w, h = 800, 600 73 | scr = pygame.display.set_mode((w, h)) 74 | 75 | try: 76 | while True: 77 | # Handle pygame events to keep the window responding 78 | pygame.event.pump() 79 | # Get the emg data and plot it 80 | while not(q.empty()): 81 | emg = list(q.get()) 82 | plot(scr, [e / 500. for e in emg]) 83 | print(emg) 84 | 85 | except KeyboardInterrupt: 86 | print("Quitting") 87 | pygame.quit() 88 | quit() -------------------------------------------------------------------------------- /examples/plot_emgs_mat.py: -------------------------------------------------------------------------------- 1 | import multiprocessing 2 | import queue 3 | import numpy as np 4 | import mpl_toolkits.mplot3d as plt3d 5 | from mpl_toolkits.mplot3d import Axes3D 6 | import matplotlib.pyplot as plt 7 | from matplotlib import animation 8 | from matplotlib.cm import get_cmap 9 | 10 | from pyomyo import Myo, emg_mode 11 | 12 | print("Press ctrl+pause/break to stop") 13 | 14 | # ------------ Myo Setup --------------- 15 | q = multiprocessing.Queue() 16 | 17 | def worker(q): 18 | m = Myo(mode=emg_mode.PREPROCESSED) 19 | m.connect() 20 | 21 | def add_to_queue(emg, movement): 22 | q.put(emg) 23 | 24 | def print_battery(bat): 25 | print("Battery level:", bat) 26 | 27 | # Orange logo and bar LEDs 28 | m.set_leds([128, 0, 0], [128, 0, 0]) 29 | # Vibrate to know we connected okay 30 | m.vibrate(1) 31 | m.add_battery_handler(print_battery) 32 | m.add_emg_handler(add_to_queue) 33 | 34 | """worker function""" 35 | while True: 36 | try: 37 | m.run() 38 | except: 39 | print("Worker Stopped") 40 | quit() 41 | 42 | # ------------ Plot Setup --------------- 43 | QUEUE_SIZE = 100 44 | SENSORS = 8 45 | subplots = [] 46 | lines = [] 47 | # Set the size of the plot 48 | plt.rcParams["figure.figsize"] = (4,8) 49 | # using the variable axs for multiple Axes 50 | fig, subplots = plt.subplots(SENSORS, 1) 51 | fig.canvas.manager.set_window_title("8 Channel EMG plot") 52 | fig.tight_layout() 53 | # Set each line to a different color 54 | 55 | name = "tab10" # Change this if you have sensors > 10 56 | cmap = get_cmap(name) # type: matplotlib.colors.ListedColormap 57 | colors = cmap.colors # type: list 58 | 59 | for i in range(0,SENSORS): 60 | ch_line, = subplots[i].plot(range(QUEUE_SIZE),[0]*(QUEUE_SIZE), color=colors[i]) 61 | lines.append(ch_line) 62 | 63 | emg_queue = queue.Queue(QUEUE_SIZE) 64 | 65 | def animate(i): 66 | # Myo Plot 67 | while not(q.empty()): 68 | myox = list(q.get()) 69 | if (emg_queue.full()): 70 | emg_queue.get() 71 | emg_queue.put(myox) 72 | 73 | channels = np.array(emg_queue.queue) 74 | 75 | if (emg_queue.full()): 76 | for i in range(0,SENSORS): 77 | channel = channels[:,i] 78 | lines[i].set_ydata(channel) 79 | subplots[i].set_ylim(0,max(1024,max(channel))) 80 | 81 | if __name__ == '__main__': 82 | # Start Myo Process 83 | p = multiprocessing.Process(target=worker, args=(q,)) 84 | p.start() 85 | 86 | while(q.empty()): 87 | # Wait until we actually get data 88 | continue 89 | anim = animation.FuncAnimation(fig, animate, blit=False, interval=2) 90 | def on_close(event): 91 | p.terminate() 92 | raise KeyboardInterrupt 93 | print("On close has ran") 94 | fig.canvas.mpl_connect('close_event', on_close) 95 | 96 | try: 97 | plt.show() 98 | except KeyboardInterrupt: 99 | plt.close() 100 | p.close() 101 | quit() -------------------------------------------------------------------------------- /examples/poweroff.py: -------------------------------------------------------------------------------- 1 | import time 2 | from pyomyo import Myo, emg_mode 3 | 4 | ''' 5 | Utility script to poweroff the Myo 6 | The Myo goes into sleep mode by default. 7 | It can be woken by shaking it. 8 | To actually turn the Myo off you need to explicitly send a power off command. 9 | To turn it back on again, you need to charge it. 10 | ''' 11 | 12 | # Make a Myo object 13 | m = Myo(mode=emg_mode.RAW) 14 | # Connect to it 15 | m.connect() 16 | print("Turning the Myo off.") 17 | print("Charge it to reboot it.") 18 | print("Press Ctrl + Break.") 19 | m.vibrate(1) 20 | # Wait until it vibrates before turning it off 21 | time.sleep(2) 22 | # Turn it off 23 | m.power_off() 24 | quit() -------------------------------------------------------------------------------- /examples/speedtest.py: -------------------------------------------------------------------------------- 1 | # SpeedTest 2 | import time 3 | import multiprocessing 4 | 5 | from pyomyo import Myo, emg_mode 6 | 7 | PLOT = True 8 | MODE = emg_mode.PREPROCESSED 9 | 10 | # ------------ Myo Setup --------------- 11 | q = multiprocessing.Queue() 12 | 13 | def worker(q): 14 | m = Myo(mode=MODE) 15 | m.connect() 16 | #print(f"Connected to Myo using {MODE}.") 17 | 18 | def add_to_queue(emg, movement): 19 | q.put(emg) 20 | 21 | m.add_emg_handler(add_to_queue) 22 | 23 | while True: 24 | try: 25 | m.run() 26 | except KeyboardInterrupt: 27 | print("Quitting") 28 | m.set_leds([50, 255, 128], [50, 255, 128]) 29 | m.disconnect() 30 | 31 | # -------- Main Program Loop ----------- 32 | if __name__ == "__main__": 33 | p = multiprocessing.Process(target=worker, args=(q,)) 34 | p.start() 35 | 36 | data = [] 37 | n_points = 50 38 | freqs = [] 39 | 40 | start_time = time.time() 41 | last_n = start_time 42 | 43 | try: 44 | while True: 45 | while not(q.empty()): 46 | # Get the new data from the Myo queue 47 | emg = list(q.get()) 48 | data.append(emg) 49 | 50 | data_points = len(data) 51 | if (data_points % n_points == 0): 52 | time_s = time.time() 53 | print(f"{data_points} points, {n_points} in {time_s-last_n}") 54 | freq = n_points/(time_s-last_n) 55 | print(f"Giving a frequency of {freq} Hz") 56 | last_n = time.time() 57 | freqs.append(freq) 58 | 59 | 60 | except KeyboardInterrupt: 61 | end_time = time.time() 62 | print(f"{len(data)} measurements in {end_time-start_time} seconds.") 63 | freq = len(data)/(end_time-start_time) 64 | print(f"Giving a frequency of {freq} Hz") 65 | print(f"Myo using mode {MODE}.") 66 | 67 | if (PLOT): 68 | import numpy as np 69 | import matplotlib.pyplot as plt 70 | x_ticks = np.array(range(1, len(freqs)+1)) * n_points 71 | plt.plot(x_ticks,freqs) 72 | plt.title("Myo Frequency Plot") 73 | plt.xlabel(f"Number of values sent, measured every {n_points}") 74 | plt.ylabel('Frequency') 75 | plt.show() -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = [ 3 | "setuptools>=42", 4 | "wheel" 5 | ] 6 | build-backend = "setuptools.build_meta" -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | contourpy==1.3.1 2 | cycler==0.12.1 3 | fonttools==4.55.2 4 | joblib==1.4.2 5 | kiwisolver==1.4.7 6 | matplotlib==3.9.3 7 | numpy==2.1.3 8 | packaging==24.2 9 | pillow==11.0.0 10 | pygame==2.6.1 11 | pynput==1.7.7 12 | pyobjc-core==10.3.2 13 | pyobjc-framework-ApplicationServices==10.3.2 14 | pyobjc-framework-Cocoa==10.3.2 15 | pyobjc-framework-CoreText==10.3.2 16 | pyobjc-framework-Quartz==10.3.2 17 | pyomyo==0.0.5 18 | pyparsing==3.2.0 19 | pyserial==3.5 20 | python-dateutil==2.9.0.post0 21 | scikit-learn==1.5.2 22 | scipy==1.14.1 23 | six==1.17.0 24 | threadpoolctl==3.5.0 25 | xgboost==2.1.3 26 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | name = pyomyo 3 | version = 0.0.5 4 | author = PerlinWarp 5 | author_email = PerlinWarp+Myo@gmail.com 6 | description = Python Opensource Myo library 7 | long_description = file: README.md 8 | long_description_content_type = text/markdown 9 | url = https://github.com/PerlinWarp/pyomyo 10 | project_urls = 11 | Bug Tracker = https://github.com/PerlinWarp/pyomyo/issues 12 | classifiers = 13 | Programming Language :: Python :: 3 14 | License :: OSI Approved :: MIT License 15 | Operating System :: OS Independent 16 | 17 | [options] 18 | package_dir = 19 | = src 20 | packages = find: 21 | python_requires = >=3.6 22 | install_requires = 23 | pyserial 24 | numpy 25 | matplotlib 26 | pygame 27 | scikit-learn 28 | xgboost 29 | [options.packages.find] 30 | where = src -------------------------------------------------------------------------------- /src/pyomyo/Classifier.py: -------------------------------------------------------------------------------- 1 | ''' 2 | The MIT License (MIT) 3 | Copyright (c) 2020 PerlinWarp 4 | Copyright (c) 2014 Danny Zhu 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy of 7 | this software and associated documentation files (the "Software"), to deal in 8 | the Software without restriction, including without limitation the rights to 9 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 10 | the Software, and to permit persons to whom the Software is furnished to do so, 11 | subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in 14 | all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 18 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 19 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 20 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 21 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 22 | ''' 23 | 24 | from collections import Counter, deque 25 | import struct 26 | import sys 27 | import time 28 | 29 | import pygame 30 | from pygame.locals import * 31 | import numpy as np 32 | 33 | from pyomyo import Myo, emg_mode 34 | 35 | SUBSAMPLE = 3 36 | K = 15 37 | 38 | class Classifier(object): 39 | '''A wrapper for nearest-neighbor classifier that stores 40 | training data in vals0, ..., vals9.dat.''' 41 | 42 | def __init__(self, name="Classifier", color=(0,200,0)): 43 | # Add some identifiers to the classifier to identify what model was used in different screenshots 44 | self.name = name 45 | self.color = color 46 | 47 | for i in range(10): 48 | with open('data/vals%d.dat' % i, 'ab') as f: pass 49 | self.read_data() 50 | 51 | def store_data(self, cls, vals): 52 | with open('data/vals%d.dat' % cls, 'ab') as f: 53 | f.write(pack('8H', *vals)) 54 | 55 | self.train(np.vstack([self.X, vals]), np.hstack([self.Y, [cls]])) 56 | 57 | def read_data(self): 58 | X = [] 59 | Y = [] 60 | for i in range(10): 61 | X.append(np.fromfile('data/vals%d.dat' % i, dtype=np.uint16).reshape((-1, 8))) 62 | Y.append(i + np.zeros(X[-1].shape[0])) 63 | 64 | self.train(np.vstack(X), np.hstack(Y)) 65 | 66 | def delete_data(self): 67 | for i in range(10): 68 | with open('data/vals%d.dat' % i, 'wb') as f: pass 69 | self.read_data() 70 | 71 | def train(self, X, Y): 72 | self.X = X 73 | self.Y = Y 74 | self.model = None 75 | 76 | def nearest(self, d): 77 | dists = ((self.X - d)**2).sum(1) 78 | ind = dists.argmin() 79 | return self.Y[ind] 80 | 81 | def classify(self, d): 82 | if self.X.shape[0] < K * SUBSAMPLE: return 0 83 | return self.nearest(d) 84 | 85 | class MyoClassifier(Myo): 86 | '''Adds higher-level pose classification and handling onto Myo.''' 87 | 88 | def __init__(self, cls, tty=None, mode=emg_mode.PREPROCESSED, hist_len=25): 89 | Myo.__init__(self, tty, mode=mode) 90 | # Add a classifier 91 | self.cls = cls 92 | self.hist_len = hist_len 93 | self.history = deque([0] * self.hist_len, self.hist_len) 94 | self.history_cnt = Counter(self.history) 95 | self.add_emg_handler(self.emg_handler) 96 | self.last_pose = None 97 | 98 | self.pose_handlers = [] 99 | 100 | def emg_handler(self, emg, moving): 101 | y = self.cls.classify(emg) 102 | self.history_cnt[self.history[0]] -= 1 103 | self.history_cnt[y] += 1 104 | self.history.append(y) 105 | 106 | r, n = self.history_cnt.most_common(1)[0] 107 | if self.last_pose is None or (n > self.history_cnt[self.last_pose] + 5 and n > self.hist_len / 2): 108 | self.on_raw_pose(r) 109 | self.last_pose = r 110 | 111 | def add_raw_pose_handler(self, h): 112 | self.pose_handlers.append(h) 113 | 114 | def on_raw_pose(self, pose): 115 | for h in self.pose_handlers: 116 | h(pose) 117 | 118 | def run_gui(self, hnd, scr, font, w, h): 119 | # Handle keypresses 120 | for ev in pygame.event.get(): 121 | if ev.type == QUIT or (ev.type == KEYDOWN and ev.unicode == 'q'): 122 | raise KeyboardInterrupt() 123 | elif ev.type == KEYDOWN: 124 | if K_0 <= ev.key <= K_9: 125 | # Labelling using row of numbers 126 | hnd.recording = ev.key - K_0 127 | elif K_KP0 <= ev.key <= K_KP9: 128 | # Labelling using Keypad 129 | hnd.recording = ev.key - K_Kp0 130 | elif ev.unicode == 'r': 131 | hnd.cl.read_data() 132 | elif ev.unicode == 'e': 133 | print("Pressed e, erasing local data") 134 | self.cls.delete_data() 135 | elif ev.type == KEYUP: 136 | if K_0 <= ev.key <= K_9 or K_KP0 <= ev.key <= K_KP9: 137 | # Don't record incoming data 138 | hnd.recording = -1 139 | 140 | # Plotting 141 | scr.fill((0, 0, 0), (0, 0, w, h)) 142 | r = self.history_cnt.most_common(1)[0][0] 143 | 144 | for i in range(10): 145 | x = 0 146 | y = 0 + 30 * i 147 | # Set the barplot color 148 | clr = self.cls.color if i == r else (255,255,255) 149 | 150 | txt = font.render('%5d' % (self.cls.Y == i).sum(), True, (255,255,255)) 151 | scr.blit(txt, (x + 20, y)) 152 | 153 | txt = font.render('%d' % i, True, clr) 154 | scr.blit(txt, (x + 110, y)) 155 | 156 | # Plot the barchart plot 157 | scr.fill((0,0,0), (x+130, y + txt.get_height() / 2 - 10, len(self.history) * 20, 20)) 158 | scr.fill(clr, (x+130, y + txt.get_height() / 2 - 10, self.history_cnt[i] * 20, 20)) 159 | 160 | pygame.display.flip() 161 | 162 | def pack(fmt, *args): 163 | return struct.pack('<' + fmt, *args) 164 | 165 | def unpack(fmt, *args): 166 | return struct.unpack('<' + fmt, *args) 167 | 168 | def text(scr, font, txt, pos, clr=(255,255,255)): 169 | scr.blit(font.render(txt, True, clr), pos) 170 | 171 | 172 | class EMGHandler(object): 173 | def __init__(self, m): 174 | self.recording = -1 175 | self.m = m 176 | self.emg = (0,) * 8 177 | 178 | def __call__(self, emg, moving): 179 | self.emg = emg 180 | if self.recording >= 0: 181 | self.m.cls.store_data(self.recording, emg) 182 | 183 | class Live_Classifier(Classifier): 184 | ''' 185 | General class for all Sklearn classifiers 186 | Expects something you can call .fit and .predict on 187 | ''' 188 | def __init__(self, classifier, name="Live Classifier", color=(0,55,175)): 189 | self.model = classifier 190 | Classifier.__init__(self, name=name, color=color) 191 | 192 | def train(self, X, Y): 193 | self.X = X 194 | self.Y = Y 195 | 196 | if self.X.shape[0] > 0 and self.Y.shape[0] > 0: 197 | self.model.fit(self.X, self.Y) 198 | 199 | def classify(self, emg): 200 | if self.X.shape[0] == 0 or self.model == None: 201 | # We have no data or model, return 0 202 | return 0 203 | 204 | x = np.array(emg).reshape(1,-1) 205 | pred = self.model.predict(x) 206 | return int(pred[0]) 207 | 208 | if __name__ == '__main__': 209 | pygame.init() 210 | w, h = 800, 320 211 | scr = pygame.display.set_mode((w, h)) 212 | font = pygame.font.Font(None, 30) 213 | 214 | m = MyoClassifier(Classifier()) 215 | hnd = EMGHandler(m) 216 | m.add_emg_handler(hnd) 217 | m.connect() 218 | 219 | m.add_raw_pose_handler(print) 220 | 221 | # Set Myo LED color to model color 222 | m.set_leds(m.cls.color, m.cls.color) 223 | # Set pygame window name 224 | pygame.display.set_caption(m.cls.name) 225 | 226 | try: 227 | while True: 228 | m.run() 229 | m.run_gui(hnd, scr, font, w, h) 230 | 231 | except KeyboardInterrupt: 232 | pass 233 | finally: 234 | m.disconnect() 235 | print() 236 | pygame.quit() 237 | -------------------------------------------------------------------------------- /src/pyomyo/__init__.py: -------------------------------------------------------------------------------- 1 | from pyomyo.pyomyo import * 2 | -------------------------------------------------------------------------------- /src/pyomyo/data/vals0.dat: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PerlinWarp/pyomyo/e9265ee2eb02bbd009c8f7097ad4b9accf387f31/src/pyomyo/data/vals0.dat -------------------------------------------------------------------------------- /src/pyomyo/data/vals1.dat: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PerlinWarp/pyomyo/e9265ee2eb02bbd009c8f7097ad4b9accf387f31/src/pyomyo/data/vals1.dat -------------------------------------------------------------------------------- /src/pyomyo/data/vals2.dat: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PerlinWarp/pyomyo/e9265ee2eb02bbd009c8f7097ad4b9accf387f31/src/pyomyo/data/vals2.dat -------------------------------------------------------------------------------- /src/pyomyo/data/vals3.dat: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PerlinWarp/pyomyo/e9265ee2eb02bbd009c8f7097ad4b9accf387f31/src/pyomyo/data/vals3.dat -------------------------------------------------------------------------------- /src/pyomyo/data/vals4.dat: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PerlinWarp/pyomyo/e9265ee2eb02bbd009c8f7097ad4b9accf387f31/src/pyomyo/data/vals4.dat -------------------------------------------------------------------------------- /src/pyomyo/data/vals5.dat: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PerlinWarp/pyomyo/e9265ee2eb02bbd009c8f7097ad4b9accf387f31/src/pyomyo/data/vals5.dat -------------------------------------------------------------------------------- /src/pyomyo/data/vals6.dat: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PerlinWarp/pyomyo/e9265ee2eb02bbd009c8f7097ad4b9accf387f31/src/pyomyo/data/vals6.dat -------------------------------------------------------------------------------- /src/pyomyo/data/vals7.dat: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PerlinWarp/pyomyo/e9265ee2eb02bbd009c8f7097ad4b9accf387f31/src/pyomyo/data/vals7.dat -------------------------------------------------------------------------------- /src/pyomyo/data/vals8.dat: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PerlinWarp/pyomyo/e9265ee2eb02bbd009c8f7097ad4b9accf387f31/src/pyomyo/data/vals8.dat -------------------------------------------------------------------------------- /src/pyomyo/data/vals9.dat: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PerlinWarp/pyomyo/e9265ee2eb02bbd009c8f7097ad4b9accf387f31/src/pyomyo/data/vals9.dat -------------------------------------------------------------------------------- /src/pyomyo/pyomyo.py: -------------------------------------------------------------------------------- 1 | ''' 2 | The MIT License (MIT) 3 | Copyright (c) 2020 PerlinWarp 4 | Copyright (c) 2014 Danny Zhu 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy of 7 | this software and associated documentation files (the "Software"), to deal in 8 | the Software without restriction, including without limitation the rights to 9 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 10 | the Software, and to permit persons to whom the Software is furnished to do so, 11 | subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in 14 | all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 18 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 19 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 20 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 21 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 22 | 23 | Original by dzhu 24 | https://github.com/dzhu/myo-raw 25 | 26 | Edited by Fernando Cosentino 27 | http://www.fernandocosentino.net/pyoconnect 28 | 29 | Edited by Alvaro Villoslada (Alvipe) 30 | https://github.com/Alvipe/myo-raw 31 | 32 | Edited by PerlinWarp 33 | https://github.com/PerlinWarp/pyomyo 34 | 35 | Warning, when using this library in a multithreaded way, 36 | know that any function called on Myo_Raw, may try to use the serial port, 37 | in windows if this is tried from a seperate thread you will get a permission error 38 | ''' 39 | 40 | import enum 41 | import re 42 | import struct 43 | import sys 44 | import threading 45 | import time 46 | 47 | import serial 48 | from serial.tools.list_ports import comports 49 | 50 | def pack(fmt, *args): 51 | return struct.pack('<' + fmt, *args) 52 | 53 | def unpack(fmt, *args): 54 | return struct.unpack('<' + fmt, *args) 55 | 56 | def multichr(ords): 57 | if sys.version_info[0] >= 3: 58 | return bytes(ords) 59 | else: 60 | return ''.join(map(chr, ords)) 61 | 62 | 63 | def multiord(b): 64 | if sys.version_info[0] >= 3: 65 | return list(b) 66 | else: 67 | return map(ord, b) 68 | 69 | class emg_mode(enum.Enum): 70 | NO_DATA = 0 # Do not send EMG data 71 | PREPROCESSED = 1 # Sends 50Hz rectified and band pass filtered data 72 | FILTERED = 2 # Sends 200Hz filtered but not rectified data 73 | RAW = 3 # Sends raw 200Hz data from the ADC ranged between -128 and 127 74 | 75 | class Arm(enum.Enum): 76 | UNKNOWN = 0 77 | RIGHT = 1 78 | LEFT = 2 79 | 80 | 81 | class XDirection(enum.Enum): 82 | UNKNOWN = 0 83 | X_TOWARD_WRIST = 1 84 | X_TOWARD_ELBOW = 2 85 | 86 | 87 | class Pose(enum.Enum): 88 | REST = 0 89 | FIST = 1 90 | WAVE_IN = 2 91 | WAVE_OUT = 3 92 | FINGERS_SPREAD = 4 93 | THUMB_TO_PINKY = 5 94 | UNKNOWN = 255 95 | 96 | 97 | class Packet(object): 98 | def __init__(self, ords): 99 | self.typ = ords[0] 100 | self.cls = ords[2] 101 | self.cmd = ords[3] 102 | self.payload = multichr(ords[4:]) 103 | 104 | def __repr__(self): 105 | return 'Packet(%02X, %02X, %02X, [%s])' % \ 106 | (self.typ, self.cls, self.cmd, 107 | ' '.join('%02X' % b for b in multiord(self.payload))) 108 | 109 | 110 | class BT(object): 111 | '''Implements the non-Myo-specific details of the Bluetooth protocol.''' 112 | def __init__(self, tty): 113 | self.ser = serial.Serial(port=tty, baudrate=9600, dsrdtr=1) 114 | self.buf = [] 115 | self.lock = threading.Lock() 116 | self.handlers = [] 117 | 118 | # internal data-handling methods 119 | def recv_packet(self): 120 | n = self.ser.inWaiting() # Windows fix 121 | 122 | while True: 123 | c = self.ser.read() 124 | if not c: 125 | return None 126 | 127 | ret = self.proc_byte(ord(c)) 128 | if ret: 129 | if ret.typ == 0x80: 130 | self.handle_event(ret) 131 | # Windows fix 132 | if n >= 5096: 133 | print("Clearning",n) 134 | self.ser.flushInput() 135 | # End of Windows fix 136 | return ret 137 | 138 | def proc_byte(self, c): 139 | if not self.buf: 140 | if c in [0x00, 0x80, 0x08, 0x88]: # [BLE response pkt, BLE event pkt, wifi response pkt, wifi event pkt] 141 | self.buf.append(c) 142 | return None 143 | elif len(self.buf) == 1: 144 | self.buf.append(c) 145 | self.packet_len = 4 + (self.buf[0] & 0x07) + self.buf[1] 146 | return None 147 | else: 148 | self.buf.append(c) 149 | 150 | if self.packet_len and len(self.buf) == self.packet_len: 151 | p = Packet(self.buf) 152 | self.buf = [] 153 | return p 154 | return None 155 | 156 | def handle_event(self, p): 157 | for h in self.handlers: 158 | h(p) 159 | 160 | def add_handler(self, h): 161 | self.handlers.append(h) 162 | 163 | def remove_handler(self, h): 164 | try: 165 | self.handlers.remove(h) 166 | except ValueError: 167 | pass 168 | 169 | def wait_event(self, cls, cmd): 170 | res = [None] 171 | 172 | def h(p): 173 | if p.cls == cls and p.cmd == cmd: 174 | res[0] = p 175 | self.add_handler(h) 176 | while res[0] is None: 177 | self.recv_packet() 178 | self.remove_handler(h) 179 | return res[0] 180 | 181 | # specific BLE commands 182 | def connect(self, addr): 183 | return self.send_command(6, 3, pack('6sBHHHH', multichr(addr), 0, 6, 6, 64, 0)) 184 | 185 | def get_connections(self): 186 | return self.send_command(0, 6) 187 | 188 | def discover(self): 189 | return self.send_command(6, 2, b'\x01') 190 | 191 | def end_scan(self): 192 | return self.send_command(6, 4) 193 | 194 | def disconnect(self, h): 195 | return self.send_command(3, 0, pack('B', h)) 196 | 197 | def read_attr(self, con, attr): 198 | self.send_command(4, 4, pack('BH', con, attr)) 199 | return self.wait_event(4, 5) 200 | 201 | def write_attr(self, con, attr, val): 202 | self.send_command(4, 5, pack('BHB', con, attr, len(val)) + val) 203 | return self.wait_event(4, 1) 204 | 205 | def send_command(self, cls, cmd, payload=b'', wait_resp=True): 206 | s = pack('4B', 0, len(payload), cls, cmd) + payload 207 | self.ser.write(s) 208 | 209 | while True: 210 | p = self.recv_packet() 211 | # no timeout, so p won't be None 212 | if p.typ == 0: 213 | return p 214 | # not a response: must be an event 215 | self.handle_event(p) 216 | 217 | 218 | class Myo(object): 219 | '''Implements the Myo-specific communication protocol.''' 220 | 221 | def __init__(self, tty=None, mode=1): 222 | if tty is None: 223 | tty = self.detect_tty() 224 | if tty is None: 225 | raise ValueError('Myo dongle not found!') 226 | 227 | self.bt = BT(tty) 228 | self.conn = None 229 | self.emg_handlers = [] 230 | self.imu_handlers = [] 231 | self.arm_handlers = [] 232 | self.pose_handlers = [] 233 | self.battery_handlers = [] 234 | self.mode = mode 235 | 236 | def detect_tty(self): 237 | for p in comports(): 238 | if re.search(r'PID=2458:0*1', p[2]): 239 | print('using device:', p[0]) 240 | return p[0] 241 | 242 | return None 243 | 244 | def run(self): 245 | self.bt.recv_packet() 246 | 247 | def connect(self, addr=None): 248 | ''' 249 | Connect to a Myo 250 | Addr is the MAC address in format: [93, 41, 55, 245, 82, 194] 251 | ''' 252 | # stop everything from before 253 | self.bt.end_scan() 254 | self.bt.disconnect(0) 255 | self.bt.disconnect(1) 256 | self.bt.disconnect(2) 257 | 258 | # start scanning 259 | if (addr is None): 260 | print('scanning...') 261 | self.bt.discover() 262 | while True: 263 | p = self.bt.recv_packet() 264 | print('scan response:', p) 265 | 266 | if p.payload.endswith(b'\x06\x42\x48\x12\x4A\x7F\x2C\x48\x47\xB9\xDE\x04\xA9\x01\x00\x06\xD5'): 267 | addr = list(multiord(p.payload[2:8])) 268 | break 269 | self.bt.end_scan() 270 | # connect and wait for status event 271 | conn_pkt = self.bt.connect(addr) 272 | self.conn = multiord(conn_pkt.payload)[-1] 273 | self.bt.wait_event(3, 0) 274 | 275 | # get firmware version 276 | fw = self.read_attr(0x17) 277 | _, _, _, _, v0, v1, v2, v3 = unpack('BHBBHHHH', fw.payload) 278 | print('firmware version: %d.%d.%d.%d' % (v0, v1, v2, v3)) 279 | 280 | self.old = (v0 == 0) 281 | 282 | if self.old: 283 | # don't know what these do; Myo Connect sends them, though we get data 284 | # fine without them 285 | self.write_attr(0x19, b'\x01\x02\x00\x00') 286 | # Subscribe for notifications from 4 EMG data channels 287 | self.write_attr(0x2f, b'\x01\x00') 288 | self.write_attr(0x2c, b'\x01\x00') 289 | self.write_attr(0x32, b'\x01\x00') 290 | self.write_attr(0x35, b'\x01\x00') 291 | 292 | # enable EMG data 293 | self.write_attr(0x28, b'\x01\x00') 294 | # enable IMU data 295 | self.write_attr(0x1d, b'\x01\x00') 296 | 297 | # Sampling rate of the underlying EMG sensor, capped to 1000. If it's 298 | # less than 1000, emg_hz is correct. If it is greater, the actual 299 | # framerate starts dropping inversely. Also, if this is much less than 300 | # 1000, EMG data becomes slower to respond to changes. In conclusion, 301 | # 1000 is probably a good value.f 302 | C = 1000 303 | emg_hz = 50 304 | # strength of low-pass filtering of EMG data 305 | emg_smooth = 100 306 | 307 | imu_hz = 50 308 | 309 | # send sensor parameters, or we don't get any data 310 | self.write_attr(0x19, pack('BBBBHBBBBB', 2, 9, 2, 1, C, emg_smooth, C // emg_hz, imu_hz, 0, 0)) 311 | 312 | else: 313 | name = self.read_attr(0x03) 314 | print('device name: %s' % name.payload) 315 | 316 | # enable IMU data 317 | self.write_attr(0x1d, b'\x01\x00') 318 | # enable on/off arm notifications 319 | self.write_attr(0x24, b'\x02\x00') 320 | # enable EMG notifications 321 | if (self.mode == emg_mode.PREPROCESSED): 322 | # Send the undocumented filtered 50Hz. 323 | print("Starting filtered, 0x01") 324 | self.start_filtered() # 0x01 325 | elif (self.mode == emg_mode.FILTERED): 326 | print("Starting raw filtered, 0x02") 327 | self.start_raw() # 0x02 328 | elif (self.mode == emg_mode.RAW): 329 | print("Starting raw, unfiltered, 0x03") 330 | self.start_raw_unfiltered() #0x03 331 | else: 332 | print("No EMG mode selected, not sending EMG data") 333 | # Stop the Myo Disconnecting 334 | self.sleep_mode(1) 335 | 336 | # enable battery notifications 337 | self.write_attr(0x12, b'\x01\x10') 338 | 339 | # add data handlers 340 | def handle_data(p): 341 | if (p.cls, p.cmd) != (4, 5): 342 | return 343 | 344 | c, attr, typ = unpack('BHB', p.payload[:4]) 345 | pay = p.payload[5:] 346 | 347 | if attr == 0x27: 348 | # Unpack a 17 byte array, first 16 are 8 unsigned shorts, last one an unsigned char 349 | vals = unpack('8HB', pay) 350 | # not entirely sure what the last byte is, but it's a bitmask that 351 | # seems to indicate which sensors think they're being moved around or 352 | # something 353 | emg = vals[:8] 354 | moving = vals[8] 355 | self.on_emg(emg, moving) 356 | # Read notification handles corresponding to the for EMG characteristics 357 | elif attr == 0x2b or attr == 0x2e or attr == 0x31 or attr == 0x34: 358 | '''According to http://developerblog.myo.com/myocraft-emg-in-the-bluetooth-protocol/ 359 | each characteristic sends two secuential readings in each update, 360 | so the received payload is split in two samples. According to the 361 | Myo BLE specification, the data type of the EMG samples is int8_t. 362 | ''' 363 | emg1 = struct.unpack('<8b', pay[:8]) 364 | emg2 = struct.unpack('<8b', pay[8:]) 365 | self.on_emg(emg1, 0) 366 | self.on_emg(emg2, 0) 367 | # Read IMU characteristic handle 368 | elif attr == 0x1c: 369 | vals = unpack('10h', pay) 370 | quat = vals[:4] 371 | acc = vals[4:7] 372 | gyro = vals[7:10] 373 | self.on_imu(quat, acc, gyro) 374 | # Read classifier characteristic handle 375 | elif attr == 0x23: 376 | typ, val, xdir, _, _, _ = unpack('6B', pay) 377 | 378 | if typ == 1: # on arm 379 | self.on_arm(Arm(val), XDirection(xdir)) 380 | elif typ == 2: # removed from arm 381 | self.on_arm(Arm.UNKNOWN, XDirection.UNKNOWN) 382 | elif typ == 3: # pose 383 | self.on_pose(Pose(val)) 384 | # Read battery characteristic handle 385 | elif attr == 0x11: 386 | battery_level = ord(pay) 387 | self.on_battery(battery_level) 388 | else: 389 | print('data with unknown attr: %02X %s' % (attr, p)) 390 | 391 | self.bt.add_handler(handle_data) 392 | 393 | def write_attr(self, attr, val): 394 | if self.conn is not None: 395 | self.bt.write_attr(self.conn, attr, val) 396 | 397 | def read_attr(self, attr): 398 | if self.conn is not None: 399 | return self.bt.read_attr(self.conn, attr) 400 | return None 401 | 402 | def disconnect(self): 403 | if self.conn is not None: 404 | self.bt.disconnect(self.conn) 405 | 406 | def sleep_mode(self, mode): 407 | self.write_attr(0x19, pack('3B', 9, 1, mode)) 408 | 409 | def power_off(self): 410 | ''' 411 | function to power off the Myo Armband (actually, according to the official BLE specification, 412 | the 0x04 command puts the Myo into deep sleep, there is no way to completely turn the device off). 413 | I think this is a very useful feature since, without this function, you have to wait until the Myo battery is 414 | fully discharged, or use the official Myo app for Windows or Mac and turn off the device from there. 415 | - Alvaro Villoslada (Alvipe) 416 | ''' 417 | self.write_attr(0x19, b'\x04\x00') 418 | 419 | def start_raw(self): 420 | ''' 421 | Sends 200Hz, non rectified signal. 422 | 423 | To get raw EMG signals, we subscribe to the four EMG notification 424 | characteristics by writing a 0x0100 command to the corresponding handles. 425 | ''' 426 | self.write_attr(0x2c, b'\x01\x00') # Suscribe to EmgData0Characteristic 427 | self.write_attr(0x2f, b'\x01\x00') # Suscribe to EmgData1Characteristic 428 | self.write_attr(0x32, b'\x01\x00') # Suscribe to EmgData2Characteristic 429 | self.write_attr(0x35, b'\x01\x00') # Suscribe to EmgData3Characteristic 430 | 431 | '''Bytes sent to handle 0x19 (command characteristic) have the following 432 | format: [command, payload_size, EMG mode, IMU mode, classifier mode] 433 | According to the Myo BLE specification, the commands are: 434 | 0x01 -> set EMG and IMU 435 | 0x03 -> 3 bytes of payload 436 | 0x02 -> send 50Hz filtered signals 437 | 0x01 -> send IMU data streams 438 | 0x01 -> send classifier events or dont (0x00) 439 | ''' 440 | # struct.pack('<5B', 1, 3, emg_mode, imu_mode, classifier_mode) 441 | self.write_attr(0x19, b'\x01\x03\x02\x01\x01') 442 | 443 | '''Sending this sequence for v1.0 firmware seems to enable both raw data and 444 | pose notifications. 445 | ''' 446 | 447 | '''By writting a 0x0100 command to handle 0x28, some kind of "hidden" EMG 448 | notification characteristic is activated. This characteristic is not 449 | listed on the Myo services of the offical BLE specification from Thalmic 450 | Labs. Also, in the second line where we tell the Myo to enable EMG and 451 | IMU data streams and classifier events, the 0x01 command wich corresponds 452 | to the EMG mode is not listed on the myohw_emg_mode_t struct of the Myo 453 | BLE specification. 454 | These two lines, besides enabling the IMU and the classifier, enable the 455 | transmission of a stream of low-pass filtered EMG signals from the eight 456 | sensor pods of the Myo armband (the "hidden" mode I mentioned above). 457 | Instead of getting the raw EMG signals, we get rectified and smoothed 458 | signals, a measure of the amplitude of the EMG (which is useful to have 459 | a measure of muscle strength, but are not as useful as a truly raw signal). 460 | ''' 461 | 462 | # self.write_attr(0x28, b'\x01\x00') # Not needed for raw signals 463 | # self.write_attr(0x19, b'\x01\x03\x01\x01\x01') 464 | 465 | def start_filtered(self): 466 | ''' 467 | Sends 50hz filtered and rectified signal. 468 | 469 | By writting a 0x0100 command to handle 0x28, some kind of "hidden" EMG 470 | notification characteristic is activated. This characteristic is not 471 | listed on the Myo services of the offical BLE specification from Thalmic 472 | Labs. Also, in the second line where we tell the Myo to enable EMG and 473 | IMU data streams and classifier events, the 0x01 command wich corresponds 474 | to the EMG mode is not listed on the myohw_emg_mode_t struct of the Myo 475 | BLE specification. 476 | These two lines, besides enabling the IMU and the classifier, enable the 477 | transmission of a stream of low-pass filtered EMG signals from the eight 478 | sensor pods of the Myo armband (the "hidden" mode I mentioned above). 479 | Instead of getting the raw EMG signals, we get rectified and smoothed 480 | signals, a measure of the amplitude of the EMG (which is useful to have 481 | a measure of muscle strength, but are not as useful as a truly raw signal). 482 | However this seems to use a data rate of 50Hz. 483 | ''' 484 | 485 | self.write_attr(0x28, b'\x01\x00') 486 | self.write_attr(0x19, b'\x01\x03\x01\x01\x00') 487 | 488 | def start_raw_unfiltered(self): 489 | ''' 490 | To get raw EMG signals, we subscribe to the four EMG notification 491 | characteristics by writing a 0x0100 command to the corresponding handles. 492 | ''' 493 | self.write_attr(0x2c, b'\x01\x00') # Suscribe to EmgData0Characteristic 494 | self.write_attr(0x2f, b'\x01\x00') # Suscribe to EmgData1Characteristic 495 | self.write_attr(0x32, b'\x01\x00') # Suscribe to EmgData2Characteristic 496 | self.write_attr(0x35, b'\x01\x00') # Suscribe to EmgData3Characteristic 497 | 498 | # struct.pack('<5B', 1, 3, emg_mode, imu_mode, classifier_mode) 499 | self.write_attr(0x19, b'\x01\x03\x03\x01\x00') 500 | 501 | def mc_start_collection(self): 502 | '''Myo Connect sends this sequence (or a reordering) when starting data 503 | collection for v1.0 firmware; this enables raw data but disables arm and 504 | pose notifications. 505 | ''' 506 | 507 | self.write_attr(0x28, b'\x01\x00') # Suscribe to EMG notifications 508 | self.write_attr(0x1d, b'\x01\x00') # Suscribe to IMU notifications 509 | self.write_attr(0x24, b'\x02\x00') # Suscribe to classifier indications 510 | self.write_attr(0x19, b'\x01\x03\x01\x01\x01') # Set EMG and IMU, payload size = 3, EMG on, IMU on, classifier on 511 | self.write_attr(0x28, b'\x01\x00') # Suscribe to EMG notifications 512 | self.write_attr(0x1d, b'\x01\x00') # Suscribe to IMU notifications 513 | self.write_attr(0x19, b'\x09\x01\x01\x00\x00') # Set sleep mode, payload size = 1, never go to sleep, don't know, don't know 514 | self.write_attr(0x1d, b'\x01\x00') # Suscribe to IMU notifications 515 | self.write_attr(0x19, b'\x01\x03\x00\x01\x00') # Set EMG and IMU, payload size = 3, EMG off, IMU on, classifier off 516 | self.write_attr(0x28, b'\x01\x00') # Suscribe to EMG notifications 517 | self.write_attr(0x1d, b'\x01\x00') # Suscribe to IMU notifications 518 | self.write_attr(0x19, b'\x01\x03\x01\x01\x00') # Set EMG and IMU, payload size = 3, EMG on, IMU on, classifier off 519 | 520 | def mc_end_collection(self): 521 | '''Myo Connect sends this sequence (or a reordering) when ending data collection 522 | for v1.0 firmware; this reenables arm and pose notifications, but 523 | doesn't disable raw data. 524 | ''' 525 | 526 | self.write_attr(0x28, b'\x01\x00') 527 | self.write_attr(0x1d, b'\x01\x00') 528 | self.write_attr(0x24, b'\x02\x00') 529 | self.write_attr(0x19, b'\x01\x03\x01\x01\x01') 530 | self.write_attr(0x19, b'\x09\x01\x00\x00\x00') 531 | self.write_attr(0x1d, b'\x01\x00') 532 | self.write_attr(0x24, b'\x02\x00') 533 | self.write_attr(0x19, b'\x01\x03\x00\x01\x01') 534 | self.write_attr(0x28, b'\x01\x00') 535 | self.write_attr(0x1d, b'\x01\x00') 536 | self.write_attr(0x24, b'\x02\x00') 537 | self.write_attr(0x19, b'\x01\x03\x01\x01\x01') 538 | 539 | def vibrate(self, length): 540 | if length in range(1, 4): 541 | # first byte tells it to vibrate; purpose of second byte is unknown (payload size?) 542 | self.write_attr(0x19, pack('3B', 3, 1, length)) 543 | 544 | def set_leds(self, logo, line): 545 | self.write_attr(0x19, pack('8B', 6, 6, *(logo + line))) 546 | 547 | # def get_battery_level(self): 548 | # battery_level = self.read_attr(0x11) 549 | # return ord(battery_level.payload[5]) 550 | 551 | def add_emg_handler(self, h): 552 | self.emg_handlers.append(h) 553 | 554 | def add_imu_handler(self, h): 555 | self.imu_handlers.append(h) 556 | 557 | def add_pose_handler(self, h): 558 | self.pose_handlers.append(h) 559 | 560 | def add_arm_handler(self, h): 561 | self.arm_handlers.append(h) 562 | 563 | def add_battery_handler(self, h): 564 | self.battery_handlers.append(h) 565 | 566 | def on_emg(self, emg, moving): 567 | for h in self.emg_handlers: 568 | h(emg, moving) 569 | 570 | def on_imu(self, quat, acc, gyro): 571 | for h in self.imu_handlers: 572 | h(quat, acc, gyro) 573 | 574 | def on_pose(self, p): 575 | for h in self.pose_handlers: 576 | h(p) 577 | 578 | def on_arm(self, arm, xdir): 579 | for h in self.arm_handlers: 580 | h(arm, xdir) 581 | 582 | def on_battery(self, battery_level): 583 | for h in self.battery_handlers: 584 | h(battery_level) 585 | 586 | if __name__ == '__main__': 587 | m = Myo(sys.argv[1] if len(sys.argv) >= 2 else None, mode=emg_mode.RAW) 588 | 589 | def proc_emg(emg, moving, times=[]): 590 | print(emg) 591 | 592 | m.add_emg_handler(proc_emg) 593 | m.connect() 594 | 595 | m.add_arm_handler(lambda arm, xdir: print('arm', arm, 'xdir', xdir)) 596 | m.add_pose_handler(lambda p: print('pose', p)) 597 | # m.add_imu_handler(lambda quat, acc, gyro: print('quaternion', quat)) 598 | m.sleep_mode(1) 599 | m.set_leds([128, 128, 255], [128, 128, 255]) # purple logo and bar LEDs 600 | m.vibrate(1) 601 | 602 | try: 603 | while True: 604 | m.run() 605 | 606 | except KeyboardInterrupt: 607 | m.disconnect() 608 | quit() 609 | --------------------------------------------------------------------------------