diff --git a/README.md b/README.md index 4498fde..adb6212 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@ Compatible with Python 2.7 and Python 3.x. ## Usage -*Everything can be run using muse-lsl.py or you may integrate into other packages. +*Everything can be run as a CLI using muse-lsl.py To stream data with LSL: @@ -44,6 +44,20 @@ You can choose between gatt, bgapi, and bluemuse backends. * bgapi - used with BLED112 dongle. * bluemuse - used on Windows 10, native Bluetooth stack, requires [BlueMuse](https://github.com/kowalej/BlueMuse/tree/master/Dist) installation. +### Integration into other packages +If you want to integrate Muse LSL into your own Python project, you can import and use its functions as you would any Python package. Examples are available in the `examples` folder. + +ex: +```Python +from muselsl import muse_stream + +muses = muse_stream.list_muses() +muse_stream.stream(muses[0]['address']) + +# Note: Streaming is synchronous, so code here will not execute until after the stream has been closed +print('Stream has ended') +``` + ## Common issues 1. `pygatt.exceptions.BLEError: Unexpected error when scanning: Set scan parameters failed: Operation not permitted` (Linux) diff --git a/examples/recordStream.py b/examples/recordStream.py new file mode 100644 index 0000000..3c4ac5b --- /dev/null +++ b/examples/recordStream.py @@ -0,0 +1,7 @@ +from muselsl import lsl_record + +# Note: an existing Python process running a Muse LSL stream is required +lsl_record.record(60) + +# Note: Recording is synchronous, so code here will not execute until the stream has been closed +print('Recording has ended') diff --git a/examples/startMuseStream.py b/examples/startMuseStream.py new file mode 100644 index 0000000..776d9df --- /dev/null +++ b/examples/startMuseStream.py @@ -0,0 +1,7 @@ +from muselsl import muse_stream + +muses = muse_stream.list_muses() +muse_stream.stream(muses[0]['address']) + +# Note: Streaming is synchronous, so code here will not execute until the stream has been closed +print('Stream has ended') \ No newline at end of file diff --git a/muselsl/__init__.py b/muselsl/__init__.py index e69de29..62b0de2 100644 --- a/muselsl/__init__.py +++ b/muselsl/__init__.py @@ -0,0 +1,9 @@ +from . import muse +from . import constants +from . import lsl_record +from . import lsl_viewer +from . import lsl_viewer_v2 +from . import muse_record +from . import muse_stream + +__version__ = "0.0.1" \ No newline at end of file diff --git a/muselsl/lsl_viewer.py b/muselsl/lsl_viewer.py index 7ee5604..b149065 100644 --- a/muselsl/lsl_viewer.py +++ b/muselsl/lsl_viewer.py @@ -1,3 +1,4 @@ +from .constants import VIEW_BUFFER, VIEW_SUBSAMPLE import numpy as np import matplotlib.pyplot as plt from scipy.signal import lfilter, lfilter_zi, firwin @@ -5,7 +6,6 @@ from pylsl import StreamInlet, resolve_byprop import seaborn as sns from threading import Thread -from constants import VIEW_SUBSAMPLE, VIEW_BUFFER def view(window, scale, refresh, figure): diff --git a/muselsl/lsl_viewer_v2.py b/muselsl/lsl_viewer_v2.py index 2dc8f0a..33fa422 100644 --- a/muselsl/lsl_viewer_v2.py +++ b/muselsl/lsl_viewer_v2.py @@ -190,7 +190,8 @@ def on_key_press(self, event): scale_x, scale_y = self.program['u_scale'] scale_x_new, scale_y_new = (scale_x * math.exp(1.0 * dx), scale_y * math.exp(0.0 * dx)) - self.program['u_scale'] = (max(1, scale_x_new), max(1, scale_y_new)) + self.program['u_scale'] = ( + max(1, scale_x_new), max(1, scale_y_new)) self.update() def on_mouse_wheel(self, event): @@ -205,7 +206,7 @@ def on_timer(self, event): """Add some data at the end of each signal (real-time signals).""" samples, timestamps = self.inlet.pull_chunk(timeout=0.0, - max_samples=100) + max_samples=100) if timestamps: samples = np.array(samples)[:, ::-1] @@ -221,7 +222,8 @@ def on_timer(self, event): elif not self.filt: plot_data = (self.data - self.data.mean(axis=0)) / self.scale - sd = np.std(plot_data[-int(self.sfreq):], axis=0)[::-1] * self.scale + sd = np.std(plot_data[-int(self.sfreq):], + axis=0)[::-1] * self.scale co = np.int32(np.tanh((sd - 30) / 15)*5 + 5) for ii in range(self.n_chans): self.quality[ii].text = '%.2f' % (sd[ii]) @@ -231,7 +233,8 @@ def on_timer(self, event): self.names[ii].font_size = 12 + co[ii] self.names[ii].color = self.quality_colors[co[ii]] - self.program['a_position'].set_data(plot_data.T.ravel().astype(np.float32)) + self.program['a_position'].set_data( + plot_data.T.ravel().astype(np.float32)) self.update() def on_resize(self, event): @@ -241,11 +244,13 @@ def on_resize(self, event): for ii, t in enumerate(self.names): t.transforms.configure(canvas=self, viewport=vp) - t.pos = (self.size[0] * 0.025, ((ii + 0.5) / self.n_chans) * self.size[1]) + t.pos = (self.size[0] * 0.025, + ((ii + 0.5) / self.n_chans) * self.size[1]) for ii, t in enumerate(self.quality): t.transforms.configure(canvas=self, viewport=vp) - t.pos = (self.size[0] * 0.975, ((ii + 0.5) / self.n_chans) * self.size[1]) + t.pos = (self.size[0] * 0.975, + ((ii + 0.5) / self.n_chans) * self.size[1]) def on_draw(self, event): gloo.clear() diff --git a/muselsl/muse-lsl.py b/muselsl/muse-lsl.py index 5f5e405..887eb4f 100644 --- a/muselsl/muse-lsl.py +++ b/muselsl/muse-lsl.py @@ -52,13 +52,14 @@ def __init__(self): getattr(self, args.command)() def list(self): - parser = argparse.ArgumentParser(description='List available Muse devices.') + parser = argparse.ArgumentParser( + description='List available Muse devices.') parser.add_argument("-b", "--backend", - dest="backend", type=str, default="auto", - help="pygatt backend to use. can be auto, gatt or bgapi.") + dest="backend", type=str, default="auto", + help="pygatt backend to use. can be auto, gatt or bgapi.") parser.add_argument("-i", "--interface", - dest="interface", type=str, default=None, - help="the interface to use, 'hci0' for gatt or a com port for bgapi.") + dest="interface", type=str, default=None, + help="the interface to use, 'hci0' for gatt or a com port for bgapi.") args = parser.parse_args(sys.argv[2:]) import muse_stream muses = muse_stream.list_muses(args.backend, args.interface) @@ -68,7 +69,7 @@ def list(self): (muse['name'], muse['address'])) else: print('No Muses found') - + def stream(self): parser = argparse.ArgumentParser( description='Start an LSL stream from Muse headset.') @@ -93,23 +94,24 @@ def record(self): parser = argparse.ArgumentParser( description='Start LSL stream and record data from Muse headset.') parser.add_argument("-a", "--address", - dest="address", type=str, default=None, - help="device MAC address.") + dest="address", type=str, default=None, + help="device MAC address.") parser.add_argument("-n", "--name", - dest="name", type=str, default=None, - help="name of the device.") + dest="name", type=str, default=None, + help="name of the device.") parser.add_argument("-b", "--backend", - dest="backend", type=str, default="auto", - help="pygatt backend to use. can be auto, gatt or bgapi") + dest="backend", type=str, default="auto", + help="pygatt backend to use. can be auto, gatt or bgapi") parser.add_argument("-i", "--interface", - dest="interface", type=str, default=None, - help="the interface to use, 'hci0' for gatt or a com port for bgapi") + dest="interface", type=str, default=None, + help="the interface to use, 'hci0' for gatt or a com port for bgapi") parser.add_argument("-f", "--filename", - dest="filename", type=str, default=None, - help="name of the recording file.") + dest="filename", type=str, default=None, + help="name of the recording file.") args = parser.parse_args(sys.argv[2:]) import muse_record - muse_record.record(args.address, args.backend, args.interface, args.name, args.filename) + muse_record.record(args.address, args.backend, + args.interface, args.name, args.filename) def viewlsl(self): parser = argparse.ArgumentParser( diff --git a/muselsl/muse.py b/muselsl/muse.py index d612806..2db7d04 100644 --- a/muselsl/muse.py +++ b/muselsl/muse.py @@ -64,7 +64,8 @@ def connect(self, interface=None, backend='auto'): self.interface = self.interface or 'hci0' self.adapter = pygatt.GATTToolBackend(self.interface) else: - self.adapter = pygatt.BGAPIBackend(serial_port=self.interface) + self.adapter = pygatt.BGAPIBackend( + serial_port=self.interface) self.adapter.start() self.device = self.adapter.connect(self.address) @@ -140,9 +141,11 @@ def start(self): if self.backend == 'bluemuse': address = self.address if self.address is not None else self.name if address is None: - subprocess.call('start bluemuse://start?streamfirst=true', shell=True) + subprocess.call( + 'start bluemuse://start?streamfirst=true', shell=True) else: - subprocess.call('start bluemuse://start?addresses={0}'.format(address), shell=True) + subprocess.call( + 'start bluemuse://start?addresses={0}'.format(address), shell=True) return self._init_timestamp_correction() @@ -158,7 +161,9 @@ def stop(self): address = self.address if self.address is not None else self.name if address is None: subprocess.call('start bluemuse://stopall', shell=True) - else: subprocess.call('start bluemuse://stop?addresses={0}'.format(address), shell=True) + else: + subprocess.call( + 'start bluemuse://stop?addresses={0}'.format(address), shell=True) return self._write_cmd([0x02, 0x68, 0x0a]) diff --git a/muselsl/muse_record.py b/muselsl/muse_record.py index 66521b6..98e8fd3 100644 --- a/muselsl/muse_record.py +++ b/muselsl/muse_record.py @@ -1,4 +1,4 @@ -from muse import Muse +from .muse import Muse from time import sleep, strftime, gmtime import numpy as np import pandas as pd diff --git a/muselsl/muse_stream.py b/muselsl/muse_stream.py index 503f88a..050fc97 100644 --- a/muselsl/muse_stream.py +++ b/muselsl/muse_stream.py @@ -1,5 +1,5 @@ -from muse import Muse -from constants import NB_CHANNELS, SAMPLING_RATE, SCAN_TIMEOUT, LSL_CHUNK +from .muse import Muse +from .constants import NB_CHANNELS, SAMPLING_RATE, SCAN_TIMEOUT, LSL_CHUNK from time import sleep from pylsl import StreamInfo, StreamOutlet, local_clock import pygatt @@ -8,7 +8,7 @@ # Returns a list of available Muse devices -def list_muses(backend='auto', interface=''): +def list_muses(backend='auto', interface=None): interface = None if backend in ['auto', 'gatt', 'bgapi', 'bluemuse']: @@ -55,7 +55,7 @@ def find_muse(name=None): return muses[0] -def stream(address, backend, interface, name): +def stream(address, backend='auto', interface=None, name=None): if backend != 'bluemuse': if not address: found_muse = find_muse(name) @@ -65,13 +65,15 @@ def stream(address, backend, interface, name): else: address = found_muse['address'] name = found_muse['name'] - print('Connecting to %s : %s...' % (name if name else 'Muse', address)) + print('Connecting to %s : %s...' % + (name if name else 'Muse', address)) else: if not address and not name: print('Connecting to first device in BlueMuse list, see BlueMuse window...') else: - print('Connecting to: ' + ':'.join(filter(None, [name, address])) + '...') + print('Connecting to: ' + + ':'.join(filter(None, [name, address])) + '...') info = info = StreamInfo('Muse', 'EEG', NB_CHANNELS, SAMPLING_RATE, 'float32', 'Muse%s' % address)