├── .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 | 
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 |
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 |
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 |
--------------------------------------------------------------------------------