Tutorial 2: TMSi + WaveX experiment#

Corresponding files for this tutorial: TMSiPlugins/WaveX/example_stream_lsl_wavex.py and TMSiPlugins/WaveX/wavex/wavex_device.py

This tutorial provides a step-by-step guide, showing how to use the combined SAGA and WaveX example and learn how you can extend this for your use case. Please ensure that both devices are powered and connected to the PC with a USB cable.

Adding the correct directory of files#

To use the TMSi Python interface folder, which is necessary for running the examples, your Python interpreter needs to know the corresponding path. Therefore, the first lines of code add this information. If you wish to store the recordings in a different folder than the default one, you can change the directory specified in the highlighted code line 37 below.

33import sys
34from os.path import join, dirname, realpath
35Plugin_dir = dirname(realpath(__file__)) # directory of this file
36modules_dir = join(Plugin_dir, '..', '..') # directory with all modules
37measurements_dir = join(Plugin_dir, '../../measurements') # directory with all measurements
38configs_dir = join(Plugin_dir, '../../TMSiSDK\\tmsi_resources') # directory with configurations
39sys.path.append(modules_dir)
40repo_dir = join(Plugin_dir, '..', '..', '..') # directory with repo
41sys.path.append(repo_dir)

Importing the required classes and functions#

The next lines import all the libraries which are required for the example script to work. They are used for purposes such as handling errors, writing recording files on PC and setting device configurations for both SAGA and WaveX.

45from wavex.wavex_device import WaveXDevice
46from wavex.wavex_structures import wavex_API_enums
47
48from TMSiSDK.tmsi_sdk import TMSiSDK, DeviceType, DeviceInterfaceType, DeviceState
49from TMSiSDK.tmsi_errors.error import TMSiError
50from TMSiFileFormats.file_writer import FileWriter, FileFormat
51from TMSiGui.gui import Gui
52from TMSiPlotterHelpers.filtered_signal_plotter_helper import FilteredSignalPlotterHelper
53from TMSiPlugins.WaveX.signal_plotter_helper_wave_x import SignalPlotterHelperWaveX 
54from TMSiSDK.device import ChannelType
55from TMSiSDK.device.devices.saga.saga_API_enums import SagaBaseSampleRate

Opening a connection to SAGA#

As it’s possible to have more than one SAGA connected, it’s necessary to specify the type of connected device as well as the interface via which the device is connected to the PC (line 64). The Data Recorder interface type for SAGA can be set to either docked, optical or wifi (line 64). Next, a list of connected devices is stored inside the variable discoveryList.

The next line of code (line 67) is a check to ensure that at least one device has been discovered. The first discovered SAGA is selected and used for the remainder of the experiment, by storing it in the SAGA variable. Afterwards, the connection to SAGA is opened (line 73).

63    # Execute a device discovery. This returns a list of device-objects for every discovered device.
64    TMSiSDK().discover(dev_type = DeviceType.saga, dr_interface = DeviceInterfaceType.docked, ds_interface = DeviceInterfaceType.usb)
65    discoveryList = TMSiSDK().get_device_list(DeviceType.saga)
66
67    if (len(discoveryList) > 0):
68        # Get the handle to the first discovered device and open the connection.
69        for i,_ in enumerate(discoveryList):
70            SAGA = discoveryList[i]
71            if SAGA.get_dr_interface() == DeviceInterfaceType.docked:
72                # Open the connection to SAGA
73                SAGA.open()
74                break

Setting the SAGA configuration#

The next step is to configure SAGA for a 2 kHz measurement, so that it uses the same sampling rate as WaveX’ EMG channels.

76    # Configure the SAGA sampling configuration
77    SAGA.import_configuration(join(configs_dir, "saga32_config_textile_grid_" + grid_type + ".xml"))
78    SAGA.set_device_sampling_config(base_sample_rate = SagaBaseSampleRate.Decimal, 
79                                    channel_type = ChannelType.all_types, 
80                                    channel_divider = 2)

Opening a connection to WaveX#

The connection to a WaveX device can be opened in two steps. First, the WaveX class is intialized by the code in line 82. The created variable is used throughout the remainder of the example to interact with WaveX. Secondly, the connection can be opened in similar fashion as SAGA’s connection, by using the code on line 85.

82    # Get the handle to the first discovered device.
83    WaveX = WaveXDevice()    
84    # Open a connection to the device
85    WaveX.open()

Setting the WaveX configuration#

Configuration of the WaveX system consists of two steps. First, the specific sensors used for the acquisition need to be configured. Secondly, the acquisition types need to be defined. The example does not show all the possibilities of the SDK, but does show the most important ones for setting up an acquisition using either EMG channels, IMU channels or a combination of both. The example uses the MiniX sensor type. For a full list of functionalities, please refer to the SDK documentation provided by Cometa. For the implemented functions in the Python wrapper, please take a look at the wavex_device.py code in the wavex folder.

To configure a sensor, two examples are shown in the code below. In this example, sensors with numbers 3 and 4 are configured respectively. Both sensors are MiniX sensors. One of the MiniX sensors is set to EMG-mode, which ensures the sensors only outputs an EMG signal, whereas the other sensor is set to EMG-Inertial mode, which results in the sensor sending both an EMG signal and IMU data. For each sensor, the desired full-scale resolution can be configured. Please check the WaveX SDK documentation, or the wavex_API_enums.py function in the wavex folder, to review all the available options.

 87    # Configure the WaveX sensors
 88    WaveX.set_device_sensor_configuration(
 89        sensor_number = 3, 
 90        enable = True,
 91        mode = wavex_API_enums.SensorMode.EMG,
 92        model = wavex_API_enums.SensorModel.Mini_EmgImu,
 93        acc_full_scale = wavex_API_enums.AccelerometerFullScale.g_8,
 94        gyr_full_scale = wavex_API_enums.GyroscopeFullScale.dps_1000)
 95    WaveX.set_device_sensor_configuration(
 96        sensor_number = 4, 
 97        enable = True,
 98        mode = wavex_API_enums.SensorMode.EMG_INERTIAL,
 99        model = wavex_API_enums.SensorModel.Mini_EmgImu,
100        acc_full_scale = wavex_API_enums.AccelerometerFullScale.g_8,
101        gyr_full_scale = wavex_API_enums.GyroscopeFullScale.dps_1000)

What data is sent out by a specific sensor depends on the sampling configuration, shown in lines 102 to 106. Here, the acquisition types for the EMG, IMU and mixed EMG/IMU acquisitions can be set. In the example, the EMG data from sensor 3 (that was set to EMG SensorMode) will be sampled at 2 kHz. For sensor 4, the 3D raw Accelerometer, 3D Gyroscope and 3D Magnetometer data will be sampled at 100 Hz, together with the EMG signals at 2 kHz. Due to these acquisition type settings, the received data will consist of 1 channel for Sensor 3 and 10 channels for Sensor 4 (1 EMG + 3 Accelerometer + 3 Gyroscope + 3 Magnetometer).

102    WaveX.set_device_sampling_config(
103        EMG_acq_type=wavex_API_enums.EmgAcqXType.Emg_2kHz,
104        EMG_IMU_acq_type=wavex_API_enums.EmgImuAcqXType.Emg_2kHz_RawAccGyroMagData_100Hz,
105        IMU_acq_type=wavex_API_enums.ImuAcqXType.Mixed6xData_200Hz,
106        )

Opening the LSL streams#

After successful configuration of both devices, the LSL streams can be created. Here, the LSL streams can be instantiated and opened by passing the device handle to it. Please note that both devices can be discovered on the network (i.e., LabRecorder) after the open() call has been done. For SAGA, the default delay is used that is set in the LSLStreamWriter, that can be found in the TMSiFileFormats folder. For WaveX, a different delay needs to be configured to correct for timestamping differences. The value has been empirically set to 0.04s. In LabRecorder the streams can be started from this point onwards, although no data will be available as the devices haven’t been set in measurement mode.

108    # Initialise and open the SAGA LSL stream
109    stream_SAGA = FileWriter(FileFormat.lsl, "SAGA")
110    stream_SAGA.open(SAGA)
111
112    # Initialise and open the WaveX lsl-stream
113    stream_WaveX = FileWriter(FileFormat.lsl, "WAVEX", lsl_offset = 0.040)
114    stream_WaveX.open(WaveX)

Data acquisition#

To collect data with LSL, both SAGA and WaveX should start acquiring data. In the example, the measurement is controlled using signal viewers. Once the viewer is opened, the device starts streaming its data to the LSL network. Closing the viewer subsequently stops the data acquisition and the sharing of data via LSL. Hence, the example controls the ability to send data to LSL, whereas the LabRecorder GUI can be used to store the acquired data to a file.

LabRecorder can retrieve the available LSL streams from the network and select them for saving to a desired file. Here, the start and stop buttons can be used to save the streamed data to the PC. In post-processing, the acquired streams can be aligned so that they are synchronized.

116    # Initialise the plotter application
117    app = QApplication(sys.argv)
118    plotter_helper_WaveX = SignalPlotterHelperWaveX(device=WaveX)
119    plotter_helper_SAGA = FilteredSignalPlotterHelper(device = SAGA, hpf = 10, grid_type = grid_type)
120
121    # Define the GUI object and show it 
122    gui_WaveX = Gui(plotter_helper = plotter_helper_WaveX)
123    gui_SAGA = Gui(plotter_helper = plotter_helper_SAGA)
124    
125    # Enter the event loop
126    app.exec_()

The initialization of the measurements, using the GUIs, is shown in the lines of code above. Please note that it is also possible to start (and stop) measurements directly from code. In that case, please use a start_measurement call for each device respectively. Importantly, you should set an appropriate method or time to subsequently stop the measurement in advance. When using this strategy, please use the example code below as a starting point.

# Import the measurement types from the SDK
from wavex.measurements.measurement_type import MeasurementTypeWaveX
from TMSiSDK.device.tmsi_device_enums import MeasurementType

# Start a signal acquisition from SAGA
SAGA.start_measurement(MeasurementType.SAGA_SIGNAL)

# Start a signal acquisition from WaveX
WaveX.start_measurement(MeasurementType.WAVEX_SIGNAL)

# Perform the measurement for 30s
time.sleep(30)

# Stop the measurements on both devices
SAGA.stop_measurement()
WaveX.stop_measurement()

Closing the connections#

After stopping the measurement, both streams can be closed, as well as the communication to both devices. This is shown on the final lines of the example.

128    # Close the LSL stream after GUI termination
129    stream_WaveX.close()
130    stream_SAGA.close()
131
132    # Close the connection to the devices
133    WaveX.close()
134    SAGA.close()

Wrap-up#

To learn how to synchronize your acquired data, please refer to the tutorial for synchronization.