Commit 535dc7ae authored by Jussi Lindgren's avatar Jussi Lindgren
Browse files

Drivers: Added gUSBAmp (BCI-Lab) driver for Linux

Driver contributed by Tom Stewart / University of Tsukuba
parent 6770e6cf
......@@ -38,6 +38,7 @@ OV_ADD_CONTRIB_DRIVER("${CMAKE_SOURCE_DIR}/contrib/plugins/server-drivers/cognio
OV_ADD_CONTRIB_DRIVER("${CMAKE_SOURCE_DIR}/contrib/plugins/server-drivers/ctfvsm-meg")
OV_ADD_CONTRIB_DRIVER("${CMAKE_SOURCE_DIR}/contrib/plugins/server-drivers/field-trip-protocol")
OV_ADD_CONTRIB_DRIVER("${CMAKE_SOURCE_DIR}/contrib/plugins/server-drivers/gtec-gipsa")
OV_ADD_CONTRIB_DRIVER("${CMAKE_SOURCE_DIR}/contrib/plugins/server-drivers/gtec-bcilab")
OV_ADD_CONTRIB_DRIVER("${CMAKE_SOURCE_DIR}/contrib/plugins/server-drivers/gtec-gmobilabplus")
OV_ADD_CONTRIB_DRIVER("${CMAKE_SOURCE_DIR}/contrib/plugins/server-drivers/gtec-gusbamp")
OV_ADD_CONTRIB_DRIVER("${CMAKE_SOURCE_DIR}/contrib/plugins/server-drivers/mbt-smarting")
......
......@@ -12,6 +12,7 @@
#include "ovasCDriverCtfVsmMeg.h"
#include "ovasCDriverGTecGUSBamp.h"
#include "ovasCDriverGTecGUSBampLegacy.h"
#include "ovasCDriverGTecGUSBampLinux.h"
#include "ovasCDriverGTecGMobiLabPlus.h"
#include "ovasCDriverFieldtrip.h"
#include "ovasCDriverMBTSmarting.h"
......@@ -37,6 +38,9 @@ namespace OpenViBEContributions {
vDriver->push_back(new OpenViBEAcquisitionServer::CDriverGTecGUSBamp(pAcquisitionServer->getDriverContext()));
vDriver->push_back(new OpenViBEAcquisitionServer::CDriverGTecGUSBampLegacy(pAcquisitionServer->getDriverContext()));
#endif
#if defined TARGET_HAS_ThirdPartyGUSBampCAPI_Linux
vDriver->push_back(new OpenViBEAcquisitionServer::CDriverGTecGUSBampLinux(pAcquisitionServer->getDriverContext()));
#endif
#if defined TARGET_HAS_ThirdPartyGMobiLabPlusAPI
vDriver->push_back(new OpenViBEAcquisitionServer::CDriverGTecGMobiLabPlus(pAcquisitionServer->getDriverContext()));
#endif
......
The MIT License (MIT)
Copyright (c) 2015 Thomas Stewart
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
Acknowledgements:
This driver was written by Tom Stewart under the supervision of
Dr. Tomasz M. Rutkowski in the University of Tsukuba's BCI Laboratory
http://bci-lab.info/
Installation:
The driver was developed using gtec's Linux C API 1.14.02. For it build properly,
the install.sh supplied with the gtec API needs to have been run which should
have created the following files:
/usr/lib/libgusbampapiso.so.1.14.02
/usr/include/gAPI.h
/etc/gtec/filter_files/DSPfilter.bin
/etc/gtec/filter_files/DSPNotchfilter.bin
To access the USB without being root, you'll need to add yourself to the
plugdev group and write a udev rule.
To do that, first check if there is a plugdev group:
$ groups
If there isn't already a plugdev group, then add it using:
$ sudo groupadd plugdev
Now execute the following:
$ sudo usermod -a -G plugdev <your-username>
If you're not sure what your username is, check it with:
$ whoami
Next create a file named /etc/udev/rules.d/10-gusbamp-usb.rules
$ sudo touch /etc/udev/rules.d/10-gusbamp-usb.rules
And open it with nano editor
$ sudo nano /etc/udev/rules.d/10-gusbamp-usb.rules
Now paste the following line into the editor:
ATTRS{idProduct}=="0001", ATTRS{idVendor}=="153c", MODE="666", GROUP="plugdev"
The values for idProduct and idVendor should match the ones that show up just
after you've plugged in the amplifier and executed:
$ dmesg
Lastly, be sure to log out and back in.
Useage:
The driver gives access to everything in the API with the exception of the channel
calibration and asynchronous configuration for digital outputs.
To run multiple gUSBAmps in parallel, use multiple instances of the acquisition server
and configure each instance as Master / Slave according to gtec's documentation. When
starting the acquisition, first connect to all the devices then press play on each device
starting with the master.
#ifndef QUEUE_H
#define QUEUE_H
#if defined TARGET_HAS_ThirdPartyGUSBampCAPI_Linux
#include <stdint.h>
#include <string.h>
#ifndef MIN
#define MIN(a, b) (((a) < (b)) ? (a) : (b))
#endif
#ifndef MAX
#define MAX(a, b) (((a) > (b)) ? (a) : (b))
#endif
template <class QueueType> class Queue
{
QueueType *buffer;
int head, tail;
int size;
public:
Queue() { size = head = tail = 0; }
Queue(QueueType *array, int len)
{
head = tail = 0;
size = len;
buffer = array;
}
void SetBuffer(QueueType *array, int len)
{
size = len;
head = tail = 0;
buffer = array;
}
int Get(QueueType *elements, int len)
{
// Trim the length if necessary to only as large as the number of available elements in the buffer
len = MIN(len, Avail());
int nonwrapped = MIN((size - tail), len), wrapped = len - nonwrapped;
// memcpy the data starting at the head all the way up to the last element *(storage - 1)
memcpy(elements, (buffer + tail), nonwrapped * sizeof(QueueType));
// If there's still data to copy memcpy whatever remains, starting at the first element *(begin) until the end of data. The first step will have ensured
// that we don't crash into the tail during this process.
memcpy((elements + nonwrapped), buffer, wrapped * sizeof(QueueType));
// Recalculate head
tail = (tail + nonwrapped + wrapped) % size;
return len;
}
// Returns the number of bytes actually placed in the array
int Put(const QueueType *elements, int len)
{
// Trim the length if necessary to only as large as the nuber of free elements in the buffer
len = MIN(len, Free());
// Figure out how much to append to the end of the buffer and how much will overlap onto the start
int nonwrapped = MIN((size - head), len), wrapped = len - nonwrapped;
// memcpy the data starting at the head all the way up to the last element *(storage - 1)
memcpy((buffer + head), elements, nonwrapped * sizeof(QueueType));
// If there's still data to copy memcpy whatever remains onto the beginning of the array
memcpy(buffer,(elements + nonwrapped), wrapped * sizeof(QueueType));
// Re-recalculate head
head = (head + nonwrapped + wrapped) % size;
return len;
}
// Expand the size of queue without actually modifying any of the contents - useful for copying directly onto the queu buffer
int Pad(int len)
{
// Trim the length if necessary to only as large as the nuber of free elements in the buffer
len = MIN(len, Free());
// Figure out how much to append to the end of the buffer and how much will overlap onto the start
int nonwrapped = MIN((size - head), len), wrapped = len - nonwrapped;
// Re-recalculate head
head = (head + nonwrapped + wrapped) % size;
return len;
}
// Removes the oldest entry from the Queue
void Pop() { if(Avail()) tail = (tail + 1) % size; }
// Returns the oldest element in the array (the one added before any other)
QueueType &Tail() { return buffer[tail]; }
// Returns the newest element in the array (the one added after every other)
QueueType &Head() { return buffer[(head + size - 1) % size]; }
QueueType &operator[] (int n) { return buffer[tail + n % size]; }
void Clear() { head = tail = 0; }
int Avail() { return (size + head - tail) % size; }
int Free() { return (size - 1 - Avail()); }
// Gets the number of free elements that can be stored contiguously
int FreeContiguous() { return head < tail? tail - head - 1 : MIN(size - head, Free()); }
// Gets a pointer to the next free address in the buffer
QueueType *NextFreeAddress() { return buffer + head; }
};
#endif // TARGET_HAS_ThirdPartyGUSBampCAPI_Linux
#endif // QUEUE_H
#ifndef __OpenViBE_AcquisitionServer_CConfigurationGTecGUSBampLinux_H__
#define __OpenViBE_AcquisitionServer_CConfigurationGTecGUSBampLinux_H__
#if defined TARGET_HAS_ThirdPartyGUSBampCAPI_Linux
#include "../ovasCConfigurationBuilder.h"
#include "ovasIDriver.h"
#include <gtk/gtk.h>
#include <gdk/gdk.h>
#include <gAPI.h>
#include <cstdio>
#include <iostream>
#include <string>
#include <cstring>
#include <sstream>
namespace OpenViBEAcquisitionServer
{
/**
* \class CConfigurationGTecGUSBampLinux
* \author Tom Stewart (University of Tsukuba)
* \date Mon Feb 9 18:59:22 2015
* \brief The CConfigurationGTecGUSBampLinux handles the configuration dialog specific to the g.tec g.USBamp for Linux device.
*
* TODO: details
*
* \sa CDriverGTecGUSBampLinux
*/
class CConfigurationGTecGUSBampLinux : public OpenViBEAcquisitionServer::CConfigurationBuilder
{
public:
// Thresholds for reporting on measured impedance, these are the same as the ones that the simulink driver uses
static const int LowImpedance = 5, ModerateImpedance = 7, HighImpedance = 100;
enum ChannelTreeViewColumn { ChannelColumn = 0, BipolarColumn, NotchColumn, NotchIdColumn, BandpassColumn, BandpassIdColumn };
// you may have to add to your constructor some reference parameters
// for example, a connection ID:
CConfigurationGTecGUSBampLinux(OpenViBEAcquisitionServer::IDriverContext& rDriverContext, const char* sGtkBuilderFileName, std::string *pDeviceName, gt_usbamp_config *pConfig);
virtual OpenViBE::boolean preConfigure(void);
virtual OpenViBE::boolean postConfigure(void);
void OnButtonApplyConfigPressed(ChannelTreeViewColumn type);
void OnButtonCheckImpedanceClicked(void);
void OnComboboxSamplingFrequencyChanged(void);
void OnComboboxDeviceChanged(void);
protected:
OpenViBEAcquisitionServer::IDriverContext& m_rDriverContext;
private:
/*
* Insert here all specific attributes, such as a connection ID.
* use references to directly modify the corresponding attribute of the driver
* Example:
*/
std::string *m_pDeviceName;
gt_usbamp_config *m_pConfig;
void UpdateFilters(void);
};
};
#endif // TARGET_HAS_ThirdPartyGUSBampCAPI_Linux
#endif // __OpenViBE_AcquisitionServer_CConfigurationGTecGUSBampLinux_H__
#if defined TARGET_HAS_ThirdPartyGUSBampCAPI_Linux
#include "ovasCDriverGTecGUSBampLinux.h"
#include "ovasCConfigurationGTecGUSBampLinux.h"
#include <toolkit/ovtk_all.h>
using namespace OpenViBEAcquisitionServer;
using namespace OpenViBE;
using namespace OpenViBE::Kernel;
using namespace std;
//___________________________________________________________________//
// //
CDriverGTecGUSBampLinux::CDriverGTecGUSBampLinux(IDriverContext& rDriverContext) :
IDriver(rDriverContext),
m_oSettings("AcquisitionServer_Driver_GTecGUSBampLinux", m_rDriverContext.getConfigurationManager()),
m_pCallback(NULL),
m_ui32SampleCountPerSentBlock(0),
m_pSampleSend(NULL),
m_pSampleReceive(NULL),
m_pSampleBuffer(NULL),
m_ui32CurrentSample(0),
m_ui32CurrentChannel(0)
{
m_oHeader.setSamplingFrequency(512);
m_oHeader.setChannelCount(16);
m_oConfig.ao_config = &m_oAnalogOutConfig;
// Configure some defaults so the settings are reasonable as soon as the driver loads and the user can tweak them from there
// Configure the analog waveform to be created by the internal signal generator
m_oAnalogOutConfig.shape = GT_ANALOGOUT_SINE;
m_oAnalogOutConfig.frequency = 1;
m_oAnalogOutConfig.amplitude = 0;
m_oAnalogOutConfig.offset = 0;
// Set the sampling rate
m_oConfig.sample_rate = 512;
// This pretty much has to be GT_NOS_AUTOSET, don't know why, so says the documentation
m_oConfig.number_of_scans = GT_NOS_AUTOSET;
// Disable the trigger line, digital io scan, slave mode and the shortcut
m_oConfig.enable_trigger_line = m_oConfig.scan_dio = m_oConfig.slave_mode = m_oConfig.enable_sc = GT_FALSE;
// Set the mode to just take readings
m_oConfig.mode = GT_MODE_NORMAL;
// Number of channels to read from
m_oConfig.num_analog_in = m_oHeader.getChannelCount();
// Set all the blocks A-D to use the common ground and reference voltages
for (unsigned int i = 0; i < GT_USBAMP_NUM_GROUND; i++)
{
m_oConfig.common_ground[i] = GT_TRUE;
m_oConfig.common_reference[i] = GT_TRUE;
}
// Configure each input
for (unsigned char i = 0; i < m_oConfig.num_analog_in; i++)
{
// Should be from 1 - 16, specifies which channel to observe as input i
m_oConfig.analog_in_channel[i] = i + 1;
// Don't use any of the filters on channel i
m_oConfig.bandpass[i] = GT_FILTER_NONE;
// Don't use any of the notch filters on channel i
m_oConfig.notch[i] = GT_FILTER_NONE;
// Don't use any of the other channels for bi-polar derivation
m_oConfig.bipolar[i] = GT_BIPOLAR_DERIVATION_NONE;
}
// Now look for any connected devices. If any exist we'll set the name to the first one found
char** l_pDeviceList = 0;
size_t l_ui32ListSize = 0;
// Refresh and get the list of currently connnected devices
GT_UpdateDevices();
l_ui32ListSize = GT_GetDeviceListSize();
l_pDeviceList = GT_GetDeviceList();
// If any devices were found at all, set the combo box to the first one listed
if(l_ui32ListSize)
{
m_oDeviceName = l_pDeviceList[0];
}
GT_FreeDeviceList(l_pDeviceList,l_ui32ListSize);
// Now retrieve all those configs from the settings file if they are there to be found (don't need to worry about sample rate or channel number though since they're already in the header)
m_oSettings.add("DeviceName", (string*)&m_oDeviceName);
m_oSettings.add("Mode", (int*)&m_oConfig.mode);
m_oSettings.add("EnableTrigger", (bool*)&m_oConfig.enable_trigger_line);
m_oSettings.add("ScanDIO", (bool*)&m_oConfig.scan_dio);
m_oSettings.add("SlaveMode", (bool*)&m_oConfig.slave_mode);
m_oSettings.add("EnableShortcut", (bool*)&m_oConfig.enable_sc);
m_oSettings.add("AnalogOutShape", (int*)&m_oAnalogOutConfig.shape);
m_oSettings.add("AnalogOutFrequency", (int*)&m_oAnalogOutConfig.frequency);
m_oSettings.add("AnalogOutAmplitude", (int*)&m_oAnalogOutConfig.amplitude);
m_oSettings.add("AnalogOutOffset", (int*)&m_oAnalogOutConfig.offset);
// Set all the blocks A-D to use the common ground and reference voltages
for (unsigned int i = 0; i < GT_USBAMP_NUM_GROUND; i++)
{
stringstream l_oGndConfigName, l_oRefConfigName;
l_oGndConfigName << "CommonGround" << i;
l_oRefConfigName << "CommonReference" << i;
m_oSettings.add(l_oGndConfigName.str().c_str(), (bool*)&m_oConfig.common_ground[i]);
m_oSettings.add(l_oRefConfigName.str().c_str(), (bool*)&m_oConfig.common_reference[i]);
}
// Configure each input
for (unsigned int i = 0; i < m_oConfig.num_analog_in; i++)
{
stringstream l_oBandpassConfigName, l_oNotchConfigName, l_oBipolarConfigName;
l_oBandpassConfigName << "Bandpass" << i;
l_oNotchConfigName << "Notch" << i;
l_oBipolarConfigName << "Bipolar" << i;
m_oSettings.add(l_oBandpassConfigName.str().c_str(), (int*)&m_oConfig.bandpass[i]);
m_oSettings.add(l_oNotchConfigName.str().c_str(), (int*)&m_oConfig.notch[i]);
m_oSettings.add(l_oBipolarConfigName.str().c_str(), (int*)&m_oConfig.bipolar[i]);
}
m_oSettings.load();
}
CDriverGTecGUSBampLinux::~CDriverGTecGUSBampLinux(void)
{
}
const char* CDriverGTecGUSBampLinux::getName(void)
{
return "g.tec g.USBamp Linux BCI-Lab";
}
//___________________________________________________________________//
// //
boolean CDriverGTecGUSBampLinux::initialize(const uint32 ui32SampleCountPerSentBlock, IDriverCallback& rCallback)
{
if(m_rDriverContext.isConnected()) return false;
if(!m_oHeader.isChannelCountSet()||!m_oHeader.isSamplingFrequencySet()) return false;
// If the scan digital inputs flag is set, the API will return one extra channel outside of the analog data requested, so we need to match that on the header
if(m_oConfig.scan_dio == GT_TRUE)
{
m_oHeader.setChannelCount(m_oHeader.getChannelCount() + 1);
}
// Allocate buffers for...
// Sending to OpenViBE
m_pSampleSend = new float32[m_oHeader.getChannelCount() * ui32SampleCountPerSentBlock];
// Receiving from the hardware,
m_pSampleReceive = new float32[m_oHeader.getChannelCount() * ui32SampleCountPerSentBlock];
// Storing the data so we pass it between the two threads - we're using the recommended buffer size put out by gtec, which is enormous
m_pSampleBuffer = new float32[GT_USBAMP_RECOMMENDED_BUFFER_SIZE / sizeof(float)];
// Set up the queue to help pass the data out of the hardware thread
m_oSampleQueue.SetBuffer(m_pSampleBuffer,m_oHeader.getChannelCount() * m_oHeader.getSamplingFrequency() / 8);
// If any of that allocation fails then give up. Not sure what setting it all to NULL is for, but we'll go with it.
if(!m_pSampleSend || !m_pSampleReceive || !m_pSampleBuffer)
{
delete[] m_pSampleSend;
delete[] m_pSampleReceive;
delete[] m_pSampleBuffer;
m_pSampleSend = m_pSampleReceive = m_pSampleBuffer = NULL;
return false;
}
// Apparently this causes the API to print debug info to the console, I'm yet to see any though
GT_ShowDebugInformation(GT_TRUE);
// Try to open the device with the configured name, let the user know how it goes
if(!GT_OpenDevice(m_oDeviceName.c_str()))
{
m_rDriverContext.getLogManager() << LogLevel_Error << "Could not open device: " << (OpenViBE::CString)m_oDeviceName.c_str() << "\n";
return false;
}
if(!GT_SetConfiguration(m_oDeviceName.c_str(), &m_oConfig))
{
m_rDriverContext.getLogManager() << LogLevel_Error << "Could not apply configuration to device: " << (OpenViBE::CString)m_oDeviceName.c_str() << "\n";
return false;
}
GT_SetDataReadyCallBack(m_oDeviceName.c_str(), &OnDataReady, (void*)(this));
// Saves parameters
m_pCallback = &rCallback;
m_ui32SampleCountPerSentBlock = ui32SampleCountPerSentBlock;
return true;
}
boolean CDriverGTecGUSBampLinux::start(void)
{
if(!m_rDriverContext.isConnected()) return false;
if(m_rDriverContext.isStarted()) return false;
// ...
// request hardware to start
// sending data
// ...
// Need to reset these in case the device is stopped mid-sample and then started again
m_ui32CurrentChannel = m_ui32CurrentSample = 0;
GT_StartAcquisition(m_oDeviceName.c_str());
m_rDriverContext.getLogManager() << LogLevel_Info << "Acquisition Started\n";
return true;
}
// So when the gtec buffer grows larger than a send buffer, copy it all to a send buffer sized array, then copy it into the actual send buffer one by one.
boolean CDriverGTecGUSBampLinux::loop(void)
{
if(!m_rDriverContext.isConnected()) return false;
if(!m_rDriverContext.isStarted()) return true;
OpenViBE::CStimulationSet l_oStimulationSet;
// while there's new data available on the queue
while(m_oSampleQueue.Avail())
{
// take it off and put it in the appropriate element in the outgoing buffer
m_oSampleQueue.Get(m_pSampleSend + m_ui32CurrentChannel * m_ui32SampleCountPerSentBlock + m_ui32CurrentSample,1);
// Increment the current channel
m_ui32CurrentChannel++;
// If the current channel reaches the channel count then move to the next sample
if(m_ui32CurrentChannel == m_oHeader.getChannelCount())
{
m_ui32CurrentChannel = 0;
m_ui32CurrentSample++;
}
// If the sample count reaches the number per sent block, then send it and start again
if(m_ui32CurrentSample == m_ui32SampleCountPerSentBlock)
{
m_pCallback->setSamples(m_pSampleSend); // it looks as if this copies the buffer, so we're free modify it as soon as it executes
// When your sample buffer is fully loaded,
// it is advised to ask the acquisition server
// to correct any drift in the acquisition automatically.
m_rDriverContext.correctDriftSampleCount(m_rDriverContext.getSuggestedDriftCorrectionSampleCount());
// ...
// receive events from hardware
// and put them the correct way in a CStimulationSet object
//...
m_pCallback->setStimulationSet(l_oStimulationSet);
m_ui32CurrentSample = 0;
}
}
return true;
}
boolean CDriverGTecGUSBampLinux::stop(void)
{
if(!m_rDriverContext.isConnected()) return false;
if(!m_rDriverContext.isStarted()) return false;
// ...
// request the hardware to stop
// sending data
// ...
GT_StopAcquisition(m_oDeviceName.c_str());
m_rDriverContext.getLogManager() << LogLevel_Info << "Acquisition Stopped";
return true;
}