sccn/lsl_archived

g.tec nautilus -- EEG channels incorrect

fd301 opened this issue · 27 comments

fd301 commented

I built labstreaming for a g.tec nautilus device (16 EEG channels) based on the code in the apps.
However the information of the EEG channels are not correct, though the auxiliary channels are ok.
Bellow is a copy of the first three lines of a csv file I create with the input LSL streamed data.
The value of the first channel is always 0. I have also used the device with openvibe and it seems that openvibe acquires more reasonable values. It seems that LSL is not able to tell correctly the type of each channels, ie. float32 vs float64 etc? Or is sth else?
Is there any way around this?

time-fs 250.0 Ch 1 EEG uV Ch 2 EEG uV Ch 3 EEG uV Ch 4 EEG uV Ch 5 EEG uV Ch 6 EEG uV Ch 7 EEG uV Ch 8 EEG uV Ch 9 EEG uV Ch 10 EEG uV Ch 11 EEG uV Ch 12 EEG uV Ch 13 EEG uV Ch 14 EEG uV Ch 15 EEG uV Ch 16 EEG uV Ch ACC X Accelerometer g Ch ACC Y Accelerometer g Ch ACC Z Accelerometer g Ch Counter Counter samples Ch Link Quality LinkQuality unknown Ch Battery Level BatteryLevel unknown Ch Validation Indicator ValidationIndicator unknown
0 0 -33982.2 -160494 -41696.3 -160532 -73008.3 -160735 -160520 -139109 -21472.5 -90066.8 -48933.4 -15293.1 -88741.3 -67169.8 -160485 0.07822 0.657046 -1.01686 356505 57.83133 100 1
4 0 -34001.5 -160411 -41659.1 -160449 -73010.3 -160651 -159900 -137022 -21442 -90036.9 -48897.5 -15247.5 -88717.8 -67154.4 -160401 0.07822 0.657046 -1.0325 356506 57.83133 100 1

I built labstreaming for a g.tec nautilus device (16 EEG channels) based on the code in the apps.

Can you please tell me exactly what you mean?

fd301 commented

I compiled labstreaminglayer with enabled the gtec app. This created a g.needaccess app with LSL.

Ok thanks. When you said "based on" that made me think that you wrote your own app that was merely based on example code.

Sorry I no longer have a g.nautilus to test. However, all 3 devices use the same callback function here, and I've been testing g.USBamps recently and they work fine.

The only differences between the devices are in how the API is used to configure them.
Does everything look correct in the config GUI for the g.nautilus?

Can you point me to the OpenVibe code that seems to work? Make sure that the data aren't being processed at all (i.e., no filters, no standardization, etc) before evaluating whether or not OpenVibe works as expected.

You can try joining Slack and having a real-time conversation with me there, though I'm leaving for home now and I honestly don't know how I could debug this without a device.

fd301 commented

In addition to the tests above with data acquired via labstreaminglayer in python, I tried the following scenarios:

  1. I used the openvibe acquisition server to acquire data directly from g.needAccess and then the openvibe designer to display the raw and filtered data
  2. I used openvibe server to acquire data via LSL streaming while I used the LSL-g.needAccess app.

In the second case, the first channel was always flat and I didn't manage to get any blinking to show up on the display. In the first case channels were noisy (dry cap) but I was able to get signal from all channels and blinks were obvious.

fd301 commented

According to openvibe:
https://gitlab.inria.fr/openvibe/extras/blob/master/contrib/plugins/server-drivers/gtec-gnautilus/src/ovasCDrivergNautilusInterface.cpp

I see that number of scans is set before the acquisition loop:

m_ui32AvailableScans = m_oNautilusDeviceCfg.NumberOfScans;
m_ui32BufferSize = l_ui32AcquiredChannelCount*m_oNautilusDeviceCfg.NumberOfScans;
m_oGdsResult = GDS_GetData(m_pDevice,&m_ui32AvailableScans,m_pBuffer,m_ui32BufferSize);

whereas in lsl-g.needaccess:

size_t bufferSize = thisWin->m_devInfo.scans_per_block * thisWin->m_devInfo.nsamples_per_scan;
size_t scans_available = 0;  // Read as many scans as possible, up to as many will fit in m_dataBuffer
GDS_RESULT res = GDS_GetData(thisWin->m_connectionHandle, &scans_available, &thisWin ->m_dataBuffer[0], bufferSize);

These two pieces of code are not equivalent, are they?

No, they aren't. I'll take a look at the API docs tomorrow to see if what I did was strictly wrong. My impression from my // comment is that passing 0 will fetch as many scans as possible, at least as many as is possible without overflowing the buffer whose size is determined by the last argument.

In OpenVibe, m_ui32BufferSize is the product of the number of channels (including extra channels) and the number of scans.

In the LSL app, bufferSize is equal to the product of scans_per_block, which I fully expect to be the same as NumberOfScans, and nsamples_per_scan which I am less confident is equal to l_ui32AcquiredChannelCount; maybe it's missing auxiliary channels. I'll check tomorrow.

By the way, it would be tremendously helpful if you could print the contents of the data buffer before this line before it gets pushed to LSL.

fd301 commented
fd301 commented

I realise that I forgot to print a space here:
std::cout << "samples per scan: " << scans_available <<
thisWin->m_devInfo.nsamples_per_scan << std::endl;

So the output 823 corresponds to 8scansx23channels. This also looks ok, I guess.

So, as you saw from the printout, LSL itself isn't doing anything wrong; it's relaying the buffer accurately but the contents of the data buffer are wrong. Off the top of my head, there are at least two things to check.

First, maybe the data buffer isn't getting filled by the g.NEEDaccess server.
Can you try modifying the buffer resize line here and passing a second argument to resize of 4.567, so that all elements are filled with that value? Then when you print the buffer later these values should be changed. If they are not then that tells us something.

Second, maybe there's some configuration that I missed.
Can you please inspect m_devconfigs in this line and make sure that everything seems configured correctly?

fd301 commented
fd301 commented

By the way, the piece of code below that is commented out seems to work if you replace the device_names with &device_names[0] and then copy the names to the std::vector of strings:
This code receives the names of the channels from the nautilus device and later pass them to lsl.

			// First determine how many channel names there are.
			uint32_t mountedModulesCount = 0;
			size_t electrodeNamesCount = 0;
			success &= handleResult("GDS_GNAUTILUS_GetChannelNames",
				GDS_GNAUTILUS_GetChannelNames(m_connectionHandle, &device_names[0], &mountedModulesCount, NULL, &electrodeNamesCount));
			// Allocate memory to store the channel names.
			char(*electrode_names)[GDS_GNAUTILUS_ELECTRODE_NAME_LENGTH_MAX] = new char[electrodeNamesCount][GDS_GNAUTILUS_ELECTRODE_NAME_LENGTH_MAX];
			success &= handleResult("GDS_GNAUTILUS_GetChannelNames",
				GDS_GNAUTILUS_GetChannelNames(m_connectionHandle, &device_names[0], &mountedModulesCount, electrode_names, &electrodeNamesCount));

for (int i = 0; i < electrodeNamesCount; i++)
m_chanLabels[i].assign(electrode_names[i]);

Perhaps, you need also to check if channels are enabled before copying.

I tried to fill the buffer with a specific value as you recommended but this didn't show any clear results. The dll still pulls some data. Some of these values are reasonable but not all of them.

OK that's helpful. So the data returned by the g.NEEDaccess server are incorrect, or the data layout of the buffer is somehow incorrect.

here and here:

std::vector<float> m_dataBuffer = {};
m_dataBuffer.clear();
m_dataBuffer.resize(m_devInfo.scans_per_block * m_devInfo.nsamples_per_scan);

and it is passed as:
GDS_GetData(thisWin->m_connectionHandle, &scans_available, &thisWin->m_dataBuffer[0], bufferSize);

In OpenVibe it is:
m_pBuffer = new float[l_ui32AcquiredChannelCount*m_oNautilusDeviceCfg.NumberOfScans];
and it is passed as:
GDS_GetData(m_pDevice,&m_ui32AvailableScans,m_pBuffer,m_ui32BufferSize);.

Without testing, these seem equivalent to me. I guess I would like to change &thisWin->m_dataBuffer[0] to thisWin->m_dataBuffer.data(), just to eliminate the ambiguity as to what the & is operating on. Please go ahead and try, though it shouldn't change anything.

The other thing you can try is initializing scans_available as size_t scans_available = thisWin->m_devInfo.scans_per_block;. It's possible that using 0 here doesn't work with g.Nautilus, though it seems to work well for g.USBamps. This would be a shame, because by using 0 I was trying to always fetch whatever data is available, without waiting, to try and push the most recent data through as fast as possible.

fd301 commented

I think the problem is with the configuration lines:

success &= handleResult("GDS_SetConfiguration", GDS_SetConfiguration(m_connectionHandle, &m_devConfigs[0], m_devConfigs.size()));

Gtec api client demo uses this code to initialise the configuration:

GDS_GNAUTILUS_CONFIGURATION* cfg_nautilus = new GDS_GNAUTILUS_CONFIGURATION;
setup_config( cfg_nautilus, SAMPLE_RATE );
GDS_CONFIGURATION_BASE* cfg = new GDS_CONFIGURATION_BASE[1];
cfg[0].DeviceInfo.DeviceType = GDS_DEVICE_TYPE_GNAUTILUS;
strcpy( cfg[0].DeviceInfo.Name, device_list[idx_selection].c_str() );
cfg[0].Configuration = cfg_nautilus;

Attached are the visual studio project of the client api provided by gtec. (Edit by Chad: I don't know if you can share that project, and I have it anyway, so I deleted it from here)

In the lsl version of the driver, I cannot find whether the struct GDS_GNAUTILUS_CONFIGURATION is initialised. Also the GDS_CONFIGURATION_BASE is not used? I'm a bit confused on how this is done.

I tried to apply the configuration and indeed changed the output of the eeg channels. However, I'm not convinced that what I get is correct.

Without testing, these seem equivalent to me. I guess I would like to change &thisWin->m_dataBuffer[0] to thisWin->m_dataBuffer.data(), just to eliminate the ambiguity as to what the & is operating on.

For a vector, it's exactly the same, but the second is C++11.

@fd301 For configuration...

The header has std::vector<GDS_CONFIGURATION_BASE> m_devConfigs;.

The configuration code is here:

GDS_CONFIGURATION_BASE *deviceConfigurations = NULL;
	size_t deviceConfigurationsCount = 0;
	success &= handleResult("GDS_GetConfiguration",
		GDS_GetConfiguration(m_connectionHandle, &deviceConfigurations, &deviceConfigurationsCount));
	// Note: API allocates memory during GDS_GetConfiguration. We must free it with GDS_FreeConfigurationList below.

	// Copy device configs to our local map.
	clear_dev_configs();
	for (size_t dev_ix = 0; dev_ix < deviceConfigurationsCount; dev_ix++)
	{
		m_devConfigs.push_back(deviceConfigurations[dev_ix]);
		// We must also copy the data pointed to by the device-specific .Configuration
		size_t copy_bytes = 0;
		switch (deviceConfigurations[dev_ix].DeviceInfo.DeviceType)
		{
		case GDS_DEVICE_TYPE_NOT_SUPPORTED:
			break;
		case GDS_DEVICE_TYPE_GUSBAMP:
			copy_bytes = sizeof(GDS_GUSBAMP_CONFIGURATION);
			m_devConfigs[dev_ix].Configuration = new GDS_GUSBAMP_CONFIGURATION();  // Allocate memory.
			break;
		case GDS_DEVICE_TYPE_GHIAMP:
			copy_bytes = sizeof(GDS_GHIAMP_CONFIGURATION);
			m_devConfigs[dev_ix].Configuration = new GDS_GHIAMP_CONFIGURATION();  // Allocate memory.
			break;
		case GDS_DEVICE_TYPE_GNAUTILUS:
			copy_bytes = sizeof(GDS_GNAUTILUS_CONFIGURATION);
			m_devConfigs[dev_ix].Configuration = new GDS_GNAUTILUS_CONFIGURATION();  // Allocate memory.
			break;
		default:
			break;
		}
		memcpy(m_devConfigs[dev_ix].Configuration, deviceConfigurations[dev_ix].Configuration, copy_bytes);
	}

	// Free the resources used by the config
	success &= handleResult("GDS_FreeConfigurationList",
GDS_FreeConfigurationList(&deviceConfigurations, deviceConfigurationsCount));

I can't look into this in any more detail right now. For that I'll have to reboot into Windows and I can't do that just yet. I'll try to get to this later.

Comparing to the gNautilusDemo, I have a couple ideas as to what should change.

  1. Don't release configuration memory (GDS_FreeConfigurationList) until after GDS_SetConfiguration. You can try commenting out here. This will leak memory but for now that's ok.
  2. I manually set a few cfg arguments that were missing when compared to the gNautilusDemo. You can see the changes here. I haven't merged this into master or anything so it's probably easiest if you just copy-paste for now and let me know if it worked.
fd301 commented

Setting the scans to zero for the configuration did not work. I got a direct GDS error and the program crashed. I'll try to hard code the scans to 4, which most probably is a safe option.

Another problem I have when a crash with gtec drivers happens is that I cannot reconnect. I get an error: 'GDS_BecomeCreator - the data acquistion session associated with the specified connection handle does already have a creator.'
Then I need to restart the computer. Perhaps, we can leave this problem at the side for the moment, since it happens with the company's proprietary code as well.

It is disappointing that gtec does not offer any support. As far as I know all the major EEG companies support LSL directly.

It is disappointing that gtec does not offer any support. As far as I know all the major EEG companies support LSL directly.

I'm sort of their indirect support. They sent me hardware, they answered my questions, (I thought) I finished and sent them the hardware back.

Another problem I have when a crash with gtec drivers happens is that I cannot reconnect. I get an error: 'GDS_BecomeCreator - the data acquistion session associated with the specified connection handle does already have a creator.'

You probably don't have to restart the computer, but you probably do have to restart the GDS service.

fd301 commented

How I can restart the GDS service?

Right click on the taskbar, choose Task Manager, click on the Services tab, in the list find GDS, right click on it then choose Restart.

@fd301
Did you make any progress on this? Just now I sent an e-mail to Fan Cao at g.Tec to ask him to take a look at this thread.

fd301 commented
fd301 commented

@cboulay have you heard back from g.Tec? What do they think about this issue?

I asked g.tec but I asked several questions at once and I guess this question got overlooked. I'll ask again.

OK they are sending me a g.Nautilus so I can fix the problem. I'll let you know how it goes.

fd301 commented

Hi, sorry to bug you again. Did you have any chance to check this out?

This issue was moved to labstreaminglayer/App-g.Tec#7