ECG Light Connector

ECG Light Connector предоставляет предельно простой для исследователей и экспериментаторов интерфейс к USB кардиографу-приставке ECG Light. И не один, а сразу целых три! Если вы хотите получать живые данные с кардиографической приставки для своих собственных нужд, и при этом не усложнять себе жизнь реализацией протокола обмена с кардиографом на уровне последовательного порта - это именно то, что вам нужно. 

 

В общем

Как и всё остальное в проекте домашнего кардиографа, эта программа совершенно бесплатна и может использоваться (или не использоваться) как вам угодно. Естественно, мы не несем ответственности за любые моральные или материальные издержки, связанные с её использованием (или, особенно, не использованием, поскольку это, наверняка, должно нагонять на вас невыносимую грусть-тоску 🙂).

Ладно, переходим к вопросу по существу.
ECG Light Connector автоматически соединяется/переподсоединяется к кардиографу, как только он физически будет подключен к компьютеру и установятся драйвера. Никаких пользовательских настроек осуществлять не требуется.

После подключения к устройству оно сразу же переводится в режим оцифровки. Получаемые данные кардиосигналов второго и третьего отведений помещаются в кольцевой буфер (длиной около 10 секунд) для необработанных данных, а также пропускаются через цифровой фильтр режекции помех (он вносит некоторую задержку) и также помещаются во второй аналогичный кольцевой буфер для отфильтрованных данных. Доступ к данным в этих буферах производится асинхронно через интерфейсы линейных интерполяторов (по времени) без удаления данных при чтении, что и обеспечивает одновременную обработку запросов всех подключенных клиентов по всем каналам обмена данными с учетом требуемой частоты дискретизации. Физическая частота дискретизации составляет 3 кГц, допустимый диапазон значений частот дискретизации, устанавливаемый пользователем - от 10 Гц до 3 кГц.

Данные можно забирать по следующим интерфейсам:
1. Именованные каналы (Named Pipes) - актуально только для Windows приложений;
2. COM (OLE) - тоже только для программ под Windows;
3. TCP - для организации доступа к кардиосигналам в локальных сетях или через Интернет, актуально в сочетании с любыми приложениями под любыми осями.

По каждому из этих интерфейсов ECG Light Connector выполняет роль сервера.

Количество клиентов Named Pipes не может превышать 10, COM и TCP ограничены только естественными факторами (производительность вашего ПК). Допустимы подключения клиентов сразу по всем интерфейсам.

Где взять

Инсталлятор ECG Light Connector можно скачать по этой ссылке. Также может потребоваться драйвер VCP FT232, который входит в состав инсталляшки ECG Control.

Именованные каналы (Named Pipes)

ECG Light Connector регистрирует в операционной системе два именованных канала: один - для "сырых" данных, другой - для данных, предварительно пропущенных через фильтр подавления помехи 50Гц. К каждому из этих каналов допускается одновременное подключение до 5 клиентов.

Алгоритм работы таков:
1. Создаете дескриптор файлового доступа к нужному именованному каналу - строго только с доступом на чтение. Также следует указать флаг разрешения записи атрибутов файла.

1
2
3
4
5
6
7
8
9
10
11
12
13
//pipe for 50 Hz filtered off data "\\\\.\\pipe\\EcgLightFiltData"
//pipe for raw ADC data "\\\\.\\pipe\\EcgLightRawData"
LPTSTR lpszPipename = TEXT("\\\\.\\pipe\\EcgLightRawData");
 
// Try to open a named pipe
hPipe = CreateFile(
    lpszPipename, // pipe name
    GENERIC_READ | FILE_WRITE_ATTRIBUTES, // read only access
    0, // no sharing
    NULL, // default security attributes
    OPEN_EXISTING, // opens existing pipe
    0, // default attributes
    NULL); // no template file
//pipe for 50 Hz filtered off data "\\\\.\\pipe\\EcgLightFiltData"
//pipe for raw ADC data "\\\\.\\pipe\\EcgLightRawData"
LPTSTR lpszPipename = TEXT("\\\\.\\pipe\\EcgLightRawData");

// Try to open a named pipe
hPipe = CreateFile(
	lpszPipename, // pipe name
	GENERIC_READ | FILE_WRITE_ATTRIBUTES, // read only access
	0, // no sharing
	NULL, // default security attributes
	OPEN_EXISTING, // opens existing pipe
	0, // default attributes
	NULL); // no template file

2. Устанавливаете атрибуты чтения в режиме сообщений (для этого и нужен был флаг записи атрибутов файла).

1
2
3
4
5
6
7
8
9
10
// The pipe connected; change to message-read mode.
 
dwMode = PIPE_READMODE_MESSAGE;
fSuccess = SetNamedPipeHandleState(
    hPipe, // pipe handle
    &dwMode, // new pipe mode
    NULL, // don't set maximum bytes
    NULL); // don't set maximum time
</pre>
<p style="text-align: justify;">
// The pipe connected; change to message-read mode.

dwMode = PIPE_READMODE_MESSAGE;
fSuccess = SetNamedPipeHandleState(
	hPipe, // pipe handle
	&dwMode, // new pipe mode
	NULL, // don't set maximum bytes
	NULL); // don't set maximum time
</pre>
<p style="text-align: justify;">

3. Читаете в любом удобном режиме (синхронном или асинхронном). Конечно, делать это следует достаточно быстро, чтобы успевать в реальном времени забирать все поступающие данные.  При появлении новых данных ECG Light Connector формирует из них сообщение и записывает в канал. Соответственно, ваше приложение получает свежайший пакет данных и может использовать его в своих целях.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// Read from the pipe.
fSuccess = ReadFile(
    hPipe, // pipe handle
    &EcgData, // buffer to receive reply
    sizeof(EcgData), // size of buffer
    &cbRead, // number of bytes read
    NULL); // not overlapped
 
if (!fSuccess && GetLastError() != ERROR_MORE_DATA){
    _tprintf(TEXT("\nReadFile from pipe (in loop) failed. GLE=%d\n"), GetLastError());
    break;
}
 
// check received data consistency
if (fSuccess && EcgData.CRC != EcgData.CalcCRC()){
    _tprintf(TEXT("\nEcgData CRC ERROR\n"));
    break;
}
 
// use the data
_tprintf(TEXT("\n\nLast message TS: %llu\n"), EcgData.TimeStamp);
_tprintf(TEXT("Samples II: "));
for (int i = 0; i < SAMP_COUNT; i++){
    _tprintf(TEXT(" %d"), EcgData.II[i]);
}
_tprintf(TEXT("\nSamples III: "));
for (int i = 0; i < SAMP_COUNT; i++){
    _tprintf(TEXT(" %d"), EcgData.III[i]);
}
// Read from the pipe.
fSuccess = ReadFile(
	hPipe, // pipe handle
	&EcgData, // buffer to receive reply
	sizeof(EcgData), // size of buffer
	&cbRead, // number of bytes read
	NULL); // not overlapped

if (!fSuccess && GetLastError() != ERROR_MORE_DATA){
	_tprintf(TEXT("\nReadFile from pipe (in loop) failed. GLE=%d\n"), GetLastError());
	break;
}

// check received data consistency
if (fSuccess && EcgData.CRC != EcgData.CalcCRC()){
	_tprintf(TEXT("\nEcgData CRC ERROR\n"));
	break;
}

// use the data
_tprintf(TEXT("\n\nLast message TS: %llu\n"), EcgData.TimeStamp);
_tprintf(TEXT("Samples II: "));
for (int i = 0; i < SAMP_COUNT; i++){
	_tprintf(TEXT(" %d"), EcgData.II[i]);
}
_tprintf(TEXT("\nSamples III: "));
for (int i = 0; i < SAMP_COUNT; i++){
	_tprintf(TEXT(" %d"), EcgData.III[i]);
}

Данные передаются в виде сообщений фиксированной длины следующей структуры:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#define SAMP_COUNT 16
 
struct tPipeMessage {
    // UNIX timestamp in microseconds for the first sample in the message
    unsigned long long TimeStamp;
    short II[SAMP_COUNT]; // samples of the lead II, uV
    short III[SAMP_COUNT]; // samples of the lead III, uV
    unsigned short dT; // delta of the timestamps between samples, usec
    unsigned short CRC; // control value of the frame
 
    unsigned short CalcCRC() {
        unsigned short res(0);
        unsigned char *pc = (unsigned char *)&TimeStamp;
        unsigned char *pend = (unsigned char *)&CRC;
        while (pc < pend)
            res += *pc++;
        return res;
    }
};
#define SAMP_COUNT 16

struct tPipeMessage {
	// UNIX timestamp in microseconds for the first sample in the message
	unsigned long long TimeStamp;
	short II[SAMP_COUNT]; // samples of the lead II, uV
	short III[SAMP_COUNT]; // samples of the lead III, uV
	unsigned short dT; // delta of the timestamps between samples, usec
	unsigned short CRC; // control value of the frame

	unsigned short CalcCRC() {
		unsigned short res(0);
		unsigned char *pc = (unsigned char *)&TimeStamp;
		unsigned char *pend = (unsigned char *)&CRC;
		while (pc < pend)
			res += *pc++;
		return res;
	}
};

Комментировать здесь особо нечего - всё написано в комментариях к коду. 🙂

Контрольную сумму после отладки можете не проверять - в именованных каналах сложно представить ситуацию, приводящую к искажению данных. А вот во время написания программы - очень рекомендую.

Подключаться к именованному каналу можно в любой момент после запуска приложения ECG Light Connector, независимо от физического подключения кардиоприставки. Данные получится считывать только при их наличии, то есть после присоединения кардиографа к компьютеру. Следует учитывать, что ваше приложение и ECG Light Connector должны быть запущены от имени одного и того же пользователя (99% именно так и будет, но все же), иначе будут проблемы с доступом к именованным каналам.

Полный код примера чтения из именованного канала (на языке С++) смотрите в примерах кода, которые легко найти по ярлычку в группе ECG Light Connector меню Пуск.

COM (OLE)

Пользоваться COM интерфейсом еще проще, чем именованными каналами. Пример простейшего приложения-клиента на С# входит в состав дистрибутива ECG Light Connector.

Во время инсталляции ECG Light Connector регистрирует себя в качестве COM-сервера, с этого момента его интерфейс становится публично доступным. Прелесть доступа к COM серверу заключается еще в том, что ECG Light Connector будет запускаться автоматически операционной системой, когда ваше приложение затребует создание его ком-объекта.

Как пользоваться (пример на языке для самых распространенных нынче кодеров) :

1. Добавляете в ваш проект ссылку на ком-объект

Самый сложный шаг

2. Объявляете ссылку на интерфейс EcgConnector.IEcgConnector

1
EcgConnector.IEcgConnector pdev;
EcgConnector.IEcgConnector pdev;

3. Инстанциируете объект, что выглядит как вызов конструктора ко-класса

1
pdev = new EcgConnector.CoEcgConnector();
pdev = new EcgConnector.CoEcgConnector();

Когда объект вам больше будет не нужен, его следует освободить

1
Marshal.ReleaseComObject(pdev);
Marshal.ReleaseComObject(pdev);

4. Пользуетесь двумя методами: pdev.IsConnected() и pdev.GetData(), чтобы узнать состояние подключения железа к ПК и получить порцию данных

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
 var bConnected = pdev.IsConnected() != 0;
 // display device connection state            
 TextB.Text = "Connected? " + (bConnected ? "Yes!" : "No");
 
 // process ADC data
 int iSampleRate = 1000;
           
 // sample rate and timestamp may be changed to the appropriate values, so check them after this method call
 Array sa = pdev.GetData(FiltCB.Checked ? 1 : 0, ref iSampleRate, ref dTimeStamp);
 
int len = sa.GetLength(0);
if(len == 0) return;
double  dt = 1.0 / iSampleRate; 
                       
for (int i = 0; i < len / 2; i++)
{
   psII.Points.AddXY(dTimeStamp, sa.GetValue(i * 2)); // lead II              
   psIII.Points.AddXY(dTimeStamp, sa.GetValue(i * 2 + 1)); // lead III
   dTimeStamp += dt;
}
 var bConnected = pdev.IsConnected() != 0;
 // display device connection state            
 TextB.Text = "Connected? " + (bConnected ? "Yes!" : "No");

 // process ADC data
 int iSampleRate = 1000;
           
 // sample rate and timestamp may be changed to the appropriate values, so check them after this method call
 Array sa = pdev.GetData(FiltCB.Checked ? 1 : 0, ref iSampleRate, ref dTimeStamp);

int len = sa.GetLength(0);
if(len == 0) return;
double  dt = 1.0 / iSampleRate; 
                       
for (int i = 0; i < len / 2; i++)
{
   psII.Points.AddXY(dTimeStamp, sa.GetValue(i * 2)); // lead II              
   psIII.Points.AddXY(dTimeStamp, sa.GetValue(i * 2 + 1)); // lead III
   dTimeStamp += dt;
}

Для полноты картины - IDL. Писать его никуда не понадобится, вам он нужен только чтобы полностью увидеть сигнатуры методов:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
[
  uuid(C2710EDC-2B55-43CE-8289-8E685D300188),
  version(1.0),
  helpstring("EcgLight Connector COM library (c) 2016 vdd-pro.ru")
 
]
library EcgConnector
{
 
  importlib("stdole2.tlb");
 
  interface IEcgConnector;
  coclass CoEcgConnector;
 
 
  [
    uuid(BC5A54D8-4ED5-453D-993E-C0F6588D2503),
    version(1.0),
    helpstring("Dispatch interface for CoEcgConnector Object"),
    dual,
    oleautomation
  ]
  interface IEcgConnector: IDispatch
  {
    [id(0x000000C9), helpstring("Checks ECG Light device state")]
    HRESULT _stdcall IsConnected([out, retval] long* retIsCon);
    [id(0x000000CA), helpstring("Get live data from the device")]
    HRESULT _stdcall GetData([in] long Filtered_in, [in, out] long* SampleRate_in_out, [in, out] double* StartTimestamp_in_out, [out, retval] SAFEARRAY(long) * ResData_out);
  };
 
  [
    uuid(E0AAFF72-3E21-4495-8819-ADA88CBE91E8),
    version(1.0),
    helpstring("CoEcgConnector Object")
  ]
  coclass CoEcgConnector
  {
    [default] interface IEcgConnector;
  };
 
};
[
  uuid(C2710EDC-2B55-43CE-8289-8E685D300188),
  version(1.0),
  helpstring("EcgLight Connector COM library (c) 2016 vdd-pro.ru")

]
library EcgConnector
{

  importlib("stdole2.tlb");

  interface IEcgConnector;
  coclass CoEcgConnector;


  [
    uuid(BC5A54D8-4ED5-453D-993E-C0F6588D2503),
    version(1.0),
    helpstring("Dispatch interface for CoEcgConnector Object"),
    dual,
    oleautomation
  ]
  interface IEcgConnector: IDispatch
  {
    [id(0x000000C9), helpstring("Checks ECG Light device state")]
    HRESULT _stdcall IsConnected([out, retval] long* retIsCon);
    [id(0x000000CA), helpstring("Get live data from the device")]
    HRESULT _stdcall GetData([in] long Filtered_in, [in, out] long* SampleRate_in_out, [in, out] double* StartTimestamp_in_out, [out, retval] SAFEARRAY(long) * ResData_out);
  };

  [
    uuid(E0AAFF72-3E21-4495-8819-ADA88CBE91E8),
    version(1.0),
    helpstring("CoEcgConnector Object")
  ]
  coclass CoEcgConnector
  {
    [default] interface IEcgConnector;
  };

};

Метод IsConnected() возвращает 0 если кардиограф не подключен, 1 - если подключен и данные поступают.

Метод GetData() возвращает массив переменной длины, элементов типа long, состоящий из чередующихся семплов II и III отведений (в микровольтах). Таким образом, в нем всегда будет четное количество элементов, равное удвоенному количеству сэмплов, считанных из буфера, начиная от указанной метки времени и до самого последнего значения.
Аргументами этого метода являются:
флаг long Filtered_in - если равен 0, то получаем не отфильтрованные (сырые) данные. 1 - данные после фильтра помехи 50 Гц.
long SampleRate_in_out - ссылка на переменную, в которой будет желаемая частота дискретизации, должна быть от 10 до 3000 Гц. Если будет указана частота вне этого диапазона, значение будет исправлено до ближайшей границы.
double StartTimestamp_in_out - ссылка на переменную, содержащую желаемую метку времени начала считываемых данных. Метка времени в секундах, относительно момента подключения кардиографа к ПК. При первом вызове можно установить в 0, поскольку значение этой переменной будет установлено в корректное значение метки времени самого первого сэмпла, возвращенного методом.

TCP

Через GUI ECG Light Connector указываете желаемые порты для подключений по протоколу TCP к компьютеру, на котором запущена эта программа. Не забудьте установить соответствующие разрешения в вашем антивирусе или фаерволе.Настройки TCP сервера

Включаете сервер (изначально он выключен). Можно пользоваться. При последующих запусках ECG Light Connector все ваши настройки будут применены автоматически. Проверить, что сервер доступен, проще всего с помощью любого веб-браузера. Укажите в его адресной строке имя или айпи компьютера, где запущен ECG Light Connector, и порт (например http://localhost:555/). Если всё правильно работает - браузер либо начнет скачивание файла, либо будет выводить в своё окно непрерывный поток кракозябр.

Как пользоваться:

1. Подключаетесь к одному из двух портов, в зависимости от того, нужны вам сырые или фильтрованные данные.

2. Читаете из сокета поток данных, из которого вычленяете пакеты и используете по назначению.

Пакеты имеют следующую структуру:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#define TCP_SAMP_COUNT 64
 
struct tTcpMessage {
    // helps to find next frame start
    unsigned short Magic;
    // UNIX timestamp in microseconds for the first sample
    unsigned long long TimeStamp;
    short II[TCP_SAMP_COUNT]; // samples of the lead II, uV
    short III[TCP_SAMP_COUNT]; // samples of the lead III, uV
    unsigned short dT; // delta of the timestamps between samples, usec
    unsigned short CRC; // control value of the frame
 
    tTcpMessage() : Magic(0xABCD) {
 
    }
 
    unsigned short CalcCRC() {
        unsigned short res(0);
        unsigned char *pc = (unsigned char *)&Magic;
        unsigned char *pend = (unsigned char *)&CRC;
        while (pc < pend)
            res += *pc++;
        return res;
    }
};
#define TCP_SAMP_COUNT 64

struct tTcpMessage {
	// helps to find next frame start
	unsigned short Magic;
	// UNIX timestamp in microseconds for the first sample
	unsigned long long TimeStamp;
	short II[TCP_SAMP_COUNT]; // samples of the lead II, uV
	short III[TCP_SAMP_COUNT]; // samples of the lead III, uV
	unsigned short dT; // delta of the timestamps between samples, usec
	unsigned short CRC; // control value of the frame

	tTcpMessage() : Magic(0xABCD) {

	}

	unsigned short CalcCRC() {
		unsigned short res(0);
		unsigned char *pc = (unsigned char *)&Magic;
		unsigned char *pend = (unsigned char *)&CRC;
		while (pc < pend)
			res += *pc++;
		return res;
	}
};

Свойство Magic всегда имеет значение 0xABCD, и служит одной цели - помочь найти начало пакета в потоке данных, что очень актуально при наличии потерь в сети. Проверять целостность данных следует, сравнивая свойство CRC из принятого пакета и результат, возвращаемый методом CalcCRC(). Битые данные использовать не стоит.

На этом всё, желаю вам успехов! Спасибо, что осилили! 🙂

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *