diff --git a/.github/workflows/publish-to-pypi.yml b/.github/workflows/publish-to-pypi.yml index 57add4e..ea79ca7 100644 --- a/.github/workflows/publish-to-pypi.yml +++ b/.github/workflows/publish-to-pypi.yml @@ -5,8 +5,8 @@ on: types: [published] env: - LSL_RELEASE_URL: 'https://github.com/sccn/liblsl/releases/download/v1.16.1/' - LSL_RELEASE: '1.16.1' + LSL_RELEASE_URL: 'https://github.com/sccn/liblsl/releases/download/v1.16.2/' + LSL_RELEASE: '1.16.2' defaults: run: diff --git a/README.md b/README.md index f36e38d..e9ee932 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # pylsl -[![Build status](https://ci.appveyor.com/api/projects/status/ggouc09585l2518i/branch/master?svg=true)](https://ci.appveyor.com/project/cboulay/liblsl-python/branch/master) +![publish workflow](https://github.com/labstreaminglayer/pylsl/actions/workflows/publish-to-pypi.yml/badge.svg) [![PyPI version](https://badge.fury.io/py/pylsl.svg)](https://badge.fury.io/py/pylsl) This is the Python interface to the [Lab Streaming Layer (LSL)](https://github.com/sccn/labstreaminglayer). @@ -15,26 +15,18 @@ the GitHub project). ## Prerequisites -On all non-Windows platforms and for some Windows-Python combinations, you must first obtain a liblsl shared library: +On all non-Windows platforms and for some Windows-Python combinations, you must first obtain a liblsl shared library. See the [liblsl repo documentation](https://github.com/sccn/liblsl) for further details. -* On many platforms it can be installed with `conda install -c conda-forge liblsl` -* Additionally, on Mac it can be installed with `brew install labstreaminglayer/tap/lsl` -* You might be able to find the appropriate liblsl shared object (*.so on Linux, *.dylib on MacOS, or *.dll on Windows) from the [liblsl release page](https://github.com/sccn/liblsl/releases). -* Otherwise you might try to clone liblsl and use its `standalone_compilation_linux.sh` script (works on raspberry pi). +## Get pylsl from PyPI -## Prepared distributions +* `pip install pylsl` -Install from [pypi](https://pypi.org/project/pylsl/) -using [pip](https://pip.pypa.io/en/stable/installing/): `pip install pylsl` +## Get pylsl from source -For several distributions, the pip distribution ships with lsl.dll. For every other case, liblsl must be installed somewhere on the PATH (see Prerequisites above) or downloaded and copied somewhere on the search path. We recommend you copy it to the pylsl installed module path's `lib` subfolder. i.e. `{path/to/env/}site-packages/pylsl/lib`. Use `python -m site` to find the "site-packages" path. -(use `cp -L` on platforms that use symlinks) +This should only be necessary if you need to modify or debug pylsl. -## Self-built - -* Download the pylsl source: `git clone https://github.com/labstreaminglayer/liblsl-Python.git && cd liblsl-Python` -* Copy the shared object (see Prerequisites above) into `liblsl-Python/pylsl/lib`. -* From the `liblsl-Python` working directory, run `pip install .`. +* Download the pylsl source: `git clone https://github.com/labstreaminglayer/pylsl.git && cd pylsl` +* From the `pylsl` working directory, run `pip install .`. * Note: You can use `pip install -e .` to install while keeping the files in-place. This is convenient for developing pylsl. # Usage @@ -43,30 +35,34 @@ See the examples in pylsl/examples. Note that these can be run directly from the You can get a list of the examples with `python -c "import pylsl.examples; help(pylsl.examples)"` -## liblsl dependency +## liblsl loading -See the note above about separately installing the liblsl dependency on non-Windows platforms. `pylsl` will search for liblsl first in the package directory (default location for Windows), then in normal system library folders, then finally at the filepath specified by an environment variable named `PYLSL_LIB`. A user-installed liblsl will typically be findable by Python's `util.find_library` in most cases. +`pylsl` will search for `liblsl` first at the filepath specified by an environment variable named `PYLSL_LIB`, then in the package directory (default location for Windows), then finally in normal system library folders. -If `pylsl` cannot find the liblsl binary (e.g., see [this issue](https://github.com/labstreaminglayer/liblsl-Python/issues/48)), set the `PYLSL_LIB` environment variable to the location of the library or set `LD_LIBRARY_PATH` to the folder containing the library. i.e., +If the shared object is not installed onto a standard search path (or it is but can't be found for some [other bug](https://github.com/labstreaminglayer/pylsl/issues/48)), then we recommend that you copy it to the pylsl installed module path's `lib` subfolder. i.e. `{path/to/env/}site-packages/pylsl/lib`. -`LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/usr/local/lib python -m pylsl.examples.{name-of-example}` +* The `site-packages/pylsl` path will only exist _after_ you install `pylsl` in your Pyton environment. +* You may have to create the `lib` subfolder. +* Use `python -m site` to find the "site-packages" path. +* Use `cp -L` on platforms that use symlinks. -# For maintainers +Alternatively, you can use an environment variable. Set the `PYLSL_LIB` environment variable to the location of the library or set `LD_LIBRARY_PATH` to the folder containing the library. For example, -## Continuous Integration +1. `PYLSL_LIB=/usr/local/lib/liblsl.so python -m pylsl.examples.{name-of-example}`, or +2. `LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/usr/local/lib python -m pylsl.examples.{name-of-example}` -pylsl uses continuous integration and distribution. +# For maintainers -Whenever a new commit is pushed, AppVeyor prepares several files. First it prepares the source wheels -- this is useful on any platform & Python version that does not have a specific binary distribution. Then it prepares the binary wheels; it downloads liblsl from its releases page, copies it to the package, then builds wheels for distribution. This process is repeated for several variants of Windows and Mac. +## Continuous Integration -In addition, whenever a new `git tag` is used on a commit that is pushed to the master branch, the CI systems will deploy the wheels to pypi. +pylsl uses continuous integration and distribution. GitHub Actions will upload a new release to pypi whenever a Release is created in GitHub. +Before creating the GitHub release, be sure to bump the version number in `pylsl/version.py` and consider updating the liblsl dependency +in `.github/workflows/publish-to-pypi.yml`. ### Linux Binaries Deprecated We recently stopped building binary wheels for Linux. In practice, the `manylinux` dependencies were often incompatible with real systems. -When we did make manylinux distributions, these relied on special liblsl builds that are not automatically pushed to the liblsl releases page. Special pipelines needed to be run manually on [Azure](https://dev.azure.com/labstreaminglayer/liblsl), then the artifacts uploaded to the release page. The Azure pipelines config remains in the liblsl repo in case it is needed again (unlikely). - ## Manual Distribution 1. Manual way: @@ -82,8 +78,8 @@ When we did make manylinux distributions, these relied on special liblsl builds # Known Issues with Multithreading on Linux -* At least for some versions of pylsl , is has been reported that running on Linux one cannot call ``pylsl`` functions from a thread that is not the main thread. This has been reported to cause access violations, and can occur during pulling from an inlet, and also from accessing an inlets info structure in a thread. -* Recent tests with mulithreading (especially when safeguarding library calls with locks) using Python 3.7.6. with pylsl 1.14 on Linux Mint 20 suggest that this issue is solved, or at least depends on your machine. See https://github.com/labstreaminglayer/liblsl-Python/issues/29 +* At least for some versions of pylsl, it has been reported that running on Linux one cannot call ``pylsl`` functions from a thread that is not the main thread. This has been reported to cause access violations, and can occur during pulling from an inlet, and also from accessing an inlets info structure in a thread. +* Recent tests with mulithreading (especially when safeguarding library calls with locks) using Python 3.7.6. with pylsl 1.14 on Linux Mint 20 suggest that this issue is solved, or at least depends on your machine. See https://github.com/labstreaminglayer/pylsl/issues/29 # Acknowledgments diff --git a/pylsl/examples/SendDataAdvanced.py b/pylsl/examples/SendDataAdvanced.py index de50cb4..9ec5e43 100644 --- a/pylsl/examples/SendDataAdvanced.py +++ b/pylsl/examples/SendDataAdvanced.py @@ -27,6 +27,11 @@ def main(name='LSLExampleAmp', stream_type='EEG', srate=100): # First create a new stream info. # The first 4 arguments are stream name, stream type, number of channels, and # sampling rate -- all parameterized by the keyword arguments or the channel list above. + # The 5th parameter is the data format. This should match the origin format (unless the + # data will be transformed prior to pushing, then it should match the transformed-to format). + # Possible values are "float32", "double64", "string", "int32", "int16", "int8", or "int64". + # Alternatively, one could use the constants in the pylsl namespace beginning with `cf_`. + # i.e., cf_float32, cf_double64, etc. # For this example, we will always use float32 data so we provide that as the 5th parameter. # The last value would be the serial number of the device or some other more or # less locally unique identifier for the stream as far as available (you diff --git a/pylsl/pylsl.py b/pylsl/pylsl.py index 4827ce6..13a75eb 100644 --- a/pylsl/pylsl.py +++ b/pylsl/pylsl.py @@ -411,6 +411,7 @@ def __init__(self, info, chunk_size=0, max_buffered=360): self.channel_count = info.channel_count() self.do_push_sample = fmt2push_sample[self.channel_format] self.do_push_chunk = fmt2push_chunk[self.channel_format] + self.do_push_chunk_n = fmt2push_chunk_n[self.channel_format] self.value_type = fmt2type[self.channel_format] self.sample_type = self.value_type * self.channel_count @@ -458,43 +459,75 @@ def push_sample(self, x, timestamp=0.0, pushthrough=True): def push_chunk(self, x, timestamp=0.0, pushthrough=True): """Push a list of samples into the outlet. - samples -- A list of samples, either as a list of lists or a list of + samples -- A list of samples, preferably as a 2-D numpy array. + `samples` can also be a list of lists, or a list of multiplexed values. - timestamp -- Optionally the capture time of the most recent sample, in - agreement with local_clock(); if omitted, the current + timestamp -- Optional, float or 1-D list of floats. + If float: the capture time of the most recent sample, in + agreement with local_clock(); if omitted/default (0.0), the current time is used. The time stamps of other samples are automatically derived according to the sampling rate of - the stream. (default 0.0) + the stream. + If list of floats: the time stamps for each sample. + Must be the same length as `samples`. pushthrough Whether to push the chunk through to the receivers instead of buffering it with subsequent samples. Note that the chunk_size, if specified at outlet construction, takes precedence over the pushthrough flag. (default True) + Note: performance is optimized for the following argument types: + - `samples`: 2-D numpy array + - `timestamp`: float """ + # Convert timestamp to corresponding ctype + try: + timestamp_c = c_double(timestamp) + # Select the corresponding push_chunk method + liblsl_push_chunk_func = self.do_push_chunk + except TypeError: + try: + timestamp_c = (c_double * len(timestamp))(*timestamp) + liblsl_push_chunk_func = self.do_push_chunk_n + except TypeError: + raise TypeError( + "timestamp must be a float or an iterable of floats" + ) + try: n_values = self.channel_count * len(x) data_buff = (self.value_type * n_values).from_buffer(x) - handle_error(self.do_push_chunk(self.obj, data_buff, - c_long(n_values), - c_double(timestamp), - c_int(pushthrough))) + handle_error( + liblsl_push_chunk_func( + self.obj, data_buff, + c_long(n_values), + timestamp_c, + c_int(pushthrough) + ) + ) except TypeError: + # don't send empty chunks if len(x): if type(x[0]) is list: x = [v for sample in x for v in sample] if self.channel_format == cf_string: x = [v.encode('utf-8') for v in x] if len(x) % self.channel_count == 0: + # x is a flattened list of multiplexed values constructor = self.value_type * len(x) # noinspection PyCallingNonCallable - handle_error(self.do_push_chunk(self.obj, constructor(*x), - c_long(len(x)), - c_double(timestamp), - c_int(pushthrough))) + handle_error( + liblsl_push_chunk_func( + self.obj, constructor(*x), + c_long(len(x)), + timestamp_c, + c_int(pushthrough) + ) + ) else: raise ValueError("Each sample must have the same number of channels (" + str(self.channel_count) + ").") + def have_consumers(self): """Check whether consumers are currently registered. @@ -1401,11 +1434,15 @@ def find_liblsl_libraries(verbose=False): push_sample_int64 = lib.lsl_push_sample_ltp pull_sample_int64 = lib.lsl_pull_sample_l push_chunk_int64 = lib.lsl_push_chunk_ltp + push_chunk_int64_n = lib.lsl_push_chunk_ltnp pull_chunk_int64 = lib.lsl_pull_chunk_l else: def push_sample_int64(*_): raise NotImplementedError('int64 support isn\'t enabled on your platform') - pull_sample_int64 = push_chunk_int64 = pull_chunk_int64 = push_sample_int64 + pull_sample_int64 = push_sample_int64 + push_chunk_int64 = push_sample_int64 + push_chunk_int64_n = push_sample_int64 + pull_chunk_int64 = push_sample_int64 # set up some type maps string2fmt = {'float32': cf_float32, 'double64': cf_double64, @@ -1425,10 +1462,14 @@ def push_sample_int64(*_): fmt2push_chunk = [[], lib.lsl_push_chunk_ftp, lib.lsl_push_chunk_dtp, lib.lsl_push_chunk_strtp, lib.lsl_push_chunk_itp, lib.lsl_push_chunk_stp, lib.lsl_push_chunk_ctp, push_chunk_int64] + fmt2push_chunk_n = [[], lib.lsl_push_chunk_ftnp, lib.lsl_push_chunk_dtnp, + lib.lsl_push_chunk_strtnp, lib.lsl_push_chunk_itnp, + lib.lsl_push_chunk_stnp, lib.lsl_push_chunk_ctnp, push_chunk_int64_n] fmt2pull_chunk = [[], lib.lsl_pull_chunk_f, lib.lsl_pull_chunk_d, lib.lsl_pull_chunk_str, lib.lsl_pull_chunk_i, lib.lsl_pull_chunk_s, lib.lsl_pull_chunk_c, pull_chunk_int64] except: # if not available - fmt2push_chunk = [None, None, None, None, None, None, None, None] - fmt2pull_chunk = [None, None, None, None, None, None, None, None] + fmt2push_chunk = [None] * len(fmt2string) + fmt2push_chunk_n = [None] * len(fmt2string) + fmt2pull_chunk = [None] * len(fmt2string) diff --git a/pylsl/version.py b/pylsl/version.py index 4b2887f..e4a8b8f 100644 --- a/pylsl/version.py +++ b/pylsl/version.py @@ -1 +1 @@ -__version__ = '1.16.1' +__version__ = '1.16.2' diff --git a/setup.py b/setup.py index 3b743f6..57f00fc 100644 --- a/setup.py +++ b/setup.py @@ -49,7 +49,7 @@ def get_tag(self): long_description_content_type="text/markdown", # The project's main homepage. - url='https://github.com/labstreaminglayer/liblsl-Python', + url='https://github.com/labstreaminglayer/pylsl', # Author details author='Christian Kothe',