eyesDxWindowsPythonC

Custom Data Acquisition with the MAPPS Software Suite

2024/02/03

Overview


The MAPPS software suite easily combines data for visualization and analysis. Its built-in tools can record video, eye-tracking, HID, and 3rd-party hardware data with ease. Sometimes you’ll have a data source that isn’t supported or may want to create a custom data stream. For these cases, the MAPPS network adapter gives you a simple yet flexible way to stream data to the eyesDx recording system.

The network adapter listens for incoming data over your local network. To send data to MAPPS you need to configure the network adapter to listen for your data. You also need to write software to read your data send it to the adapter over the network. Data can be sent over UDP, UDP Multicast and TCP.

Adapter Configuration


The Network Adapter can listen to any data you want to send. You configure the network adapter by describing the data you plan to send in an XML file. The files are normally located at C:\eDx_data\configuration\Adapter Launcher v10.0\Network Adapter

Configuration Folder  

Adapter Setup

The first thing you should configure is the <log_path>. The log path is where the network adapter stores the data as it comes in over the network.

 <log_path>c:\eDx_data\adapter_logs\YOUR ADAPTER FOLDER\</log_path>

The next thing to configure is the data <mode>. This tells the network adapter how the data is going to be sent over the network. There are three modes:

  • ascii : Comma delimited ASCII sent over UDP
  • binary : Binary data sent over UDP
  • stream : Direct TCP stream

To set the mode, place one of these values in the <mode> node.

<mode>ascii</mode>

You also need to tell the adapter which port to listen to.

<network>
  <port>23222</port>
</network>

Finally you should describe the adapter in the <global> section.

<global>
  <!-- Type of device (e.g., phyio, eye_tracking, eeg, ...). -->
  <device_type>CSharp_Sample_Device</device_type>

  <!-- Unique name of this device. -->
  <unique_name>C#_Sample_Bus</unique_name>

  <!-- Ideal run rate, in Hz. -->
  <target_rate_hz>50</target_rate_hz>

  <!-- Unique index, used if multiple parallel network translators are running. [0,9] -->
  <multi_device_index>1</multi_device_index>
</global>
  • <device_type> and <unique_name> are used to describe the adapter in the Adapter Launcher GUI. You should provide descriptive names here.
  • <target_rate_hz> is used to determine if the data stream is running slower than expected. You should provide your best estimate of the data rate here.
  • <multi_device_index> is used to differentiate multiple network adapters. If you only plan on having one network adapter you can leave this at one. If you have multiple adapters, each adapter should have a unique index.

Data Definition

The adapter supports the following data types:

  • string(n): Where n is the length of the string field.
  • int: 32-bit Integer
  • float: 32-bit
  • double: 64-bit
  • int64: 64-bit Integer

For the programming examples in this post, I’ll be using the following data definition.

<bus_description>
  <elements>

    <!-- First element. -->
    <element>
      <name>SomeTextField</name>     <!-- Element name. -->
      <type>string64</type>          <!-- Element type. -->
    </element>

    <!-- Second element. -->
    <element>
      <name>AnIntegerField</name>    <!-- Element name. -->
      <type>int</type>               <!-- Element type. -->
    </element>

    <!-- Third element. -->
    <element>
      <name>SomeFloatField</name>    <!-- Element name. -->
      <type>float</type>             <!-- Element type. -->
    </element>

    <!-- Fourth element. -->
    <element>
      <name>SomeDoubleField</name>   <!-- Element name. -->
      <type>double</type>            <!-- Element type. -->
    </element>

    <!-- Fifth element. -->
    <element>
      <name>Some64BitIntField</name> <!-- Element name. -->
      <type>int64</type>             <!-- Element type. -->
    </element>

    <!-- You may continue to add as many elements to the list as needed. -->
    <!-- ... -->

  </elements>
</bus_description>

Timestamps

Time is critical when synchronizing data from different sources. Your computer clocks must be synchronized with a good NTP client (such as Meinberg NTP

Data synchronized by MAPPS relies on timestamps sent with the data. Ideally, each packet of data sent to MAPPS will have a synchronized timestamp with it. MAPPS uses 64-bit Windows Filetime as its timestamp format.

With the network adapter you have two options for setting timestamps for you data. You can add your own timestamps to the data sent to the network adapter or have the network adapter use the time it received the data as the timestamp. It’s strongly recommended that you get a synchronized timestamp at the time your data is produced and send that to the network adapter. If you choose to have network adapter use the received time you risk network delays in your data. This will give you poor data synchronization.

Sending Comma Delimited ASCII data with Python


The simplest way to send data to the Network Adapter is to use a comma delimited ASCII string. Setting <mode> to ‘ascii’ in the configuration file will put your network adapter into ‘ascii’ mode

<mode>ascii</mode>

And set its global values to

<global>
  <!-- Type of device (e.g., phyio, Eye Tracker, eeg, ...). -->
  <device_type>Sample_Device</device_type>

  <!-- Unique name of this device. -->
  <unique_name>Python_Sample_Bus</unique_name>

  <!-- Ideal run rate, in Hz. -->
  <target_rate_hz>50</target_rate_hz>

  <!-- Unique index, used if multiple parallel network translators are running. -->
  <multi_device_index>0</multi_device_index>
</global>

You can look at a full configuration file here

Sending UDP data

The data we’re sending is ASCII so we can just create a comma delimited string containing data we want to send

        msg = "0," + "Some text value," + str(counter) + "," + str(math.cos(time)) + "," + str(math.sin(time)) + "," + str(counter*counter)

Note the ‘0’ in the first field. This tells the Adapter to use the time it receives this data as the timestamp. This is is not recommended. You should instead create a windows filetime timestamp and add it to the data.

Sending UDP data in Python is trivial. You create a datagram socket with a call to socket.socket()

sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)

and send the data with sendto()

        sock.sendto( msg.encode(), ('127.0.0.1', 23222 );

The complete python script looks like this:

import socket
from time import time, sleep
import math

udpAddr = "127.0.0.1"
udpPort = 23222
time = 0

sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
counter = 0;

while True:
        msg = "0," + "Some text value," + str(counter) + "," + str(math.cos(time)) + "," + str(math.sin(time)) + "," + str(counter*counter)
        print ("Sending a packet [" + msg + "]")
        sock.sendto(msg.encode(), (udpAddr, udpPort))
        sleep(0.01)
        time += 0.1
        counter = counter + 1

This code is also available on github

If everything worked, Adapter Launcher should receive data from your python source

adapter launcher python data and relay it to Record Manager record manager python data  

Sending Binary Data with C


The network adapter an also accept binary data. Change <mode> to ’binary in the configuration file

<mode>binary</mode>

For this example I’m configuring the <network> and <global> values as follows:

  <network>
    <!-- ADVANCED USERS ONLY: Force a local network binding. -->
    <local_address></local_address>

    <!-- Multicast port to use (or empty if not using multicast). Applies only for
         'binary' or 'ascii' modes. -->
    <mc_address></mc_address>

    <!-- Network port to use. -->
    <port>23223</port>
  </network>

  <global>
    <!-- Type of device (e.g., phyio, eye_tracking, eeg, ...). -->
    <device_type>CSharp_Sample_Device</device_type>

    <!-- Unique name of this device. -->
    <unique_name>C#_Binary_Sample_Bus</unique_name>

    <!-- Ideal run rate, in Hz. -->
    <target_rate_hz>50</target_rate_hz>

    <!-- Unique index, used if multiple parallel network translators are running. [0,9] -->
    <multi_device_index>2</multi_device_index>
  </global>

The complete configuration file can be found here

Formatting Binary Data with C

Unlike ASCII mode. Binary mode is not delimited. Instead it expects a array of bytes of fixed sizes. The layout is as follows:

|---- 8 bytes ----||-- Size of your payload --|

The first eight bytes of the packet are the FileTime timestamp. If you want the timestamp to be the time at which the network adapter recieves the data, use 0 (not recommended).

The better way to set the timestamp is to use DateTime.UtcNow.ToFileTimeUtc()

var timestamp = DateTime.UtcNow.ToFileTimeUtc()

You can easily combine multiple variables into a variable array with a memory stream and binary writer

 var outBuffer = new MemoryStream();
 var binaryWriter = new BinaryWriter(outBuffer);

Numeric values can be written to the buffer with binaryWriter.Write().

 // Add 'AnIntegerField' to the network adapter message
 int someInt = counter;
 binaryWriter.Write(someInt);

 // Add 'SomeFloatField' to the network adapter message
 float cosAngle = (float)Math.Cos(time);
 binaryWriter.Write(cosAngle);

Strings needed padded to fit the size of the text field specified in the file. In this example, string64 was used.

<!-- First element. -->
<element>
  <name>SomeTextField</name>     <!-- Element name. -->
  <type>string64</type>          <!-- Element type. -->
</element>

So we want to make sure to write out the entire 64 character string. One possible way to do this is to pad out the string itself.

// Add 'SomeTextField' to the network adapter message
string message = "Some C# Text Field";
int length = 64;
string paddedMessage = message.PadRight( length).Substring(0, length );

Then write the string with binaryWriter.Write().

binaryWriter.Write(paddedMessage.ToCharArray());

Sending Binary data over UDP

Like Python, networking in C# is simple. We define a UDP connection with the IP address of the Adapter and it’s port

// setup UDP client connection.
var client = new UdpClient();
IPEndPoint ep = new IPEndPoint(IPAddress.Parse("127.0.0.1"), 23223);
client.Connect(ep);

Sending the data is a simple call with the byte array and its length

// send the message out to the network adapter
client.Send(bytes, bytes.Length);

If everything worked, Adapter Launcher should receive data from your C# source

adapter launcher cs data and relay it to Record Manager record manager cs data  

The complete code can be found at github

Sending a TCP Data Stream in C


The above examples used UDP to send data. UDP is fast but not guaranteed. Network adapter also accepts data over a TCP connection with ‘stream’ mode.

You turn on ‘stream’ mode by setting <mode> in the XML file.

<mode>stream</mode>

For this example I’m configuring the <network> and <global> values as follows:

  <network>
    <!-- ADVANCED USERS ONLY: Force a local network binding. -->
    <local_address></local_address>

    <!-- Multicast port to use (or empty if not using multicast). Applies only for
         'binary' or 'ascii' modes. -->
    <mc_address></mc_address>

    <!-- Network port to use. -->
    <port>23224</port>
  </network>

  <global>
    <!-- Type of device (e.g., phyio, eye_tracking, eeg, ...). -->
    <device_type>C++_Sample_Device</device_type>

    <!-- Unique name of this device. -->
    <unique_name>C++_Binary_Sample_Bus</unique_name>

    <!-- Ideal run rate, in Hz. -->
    <target_rate_hz>50</target_rate_hz>

    <!-- Unique index, used if multiple parallel network translators are running. [0,9] -->
    <multi_device_index>3</multi_device_index>
  </global>

The complete stream configuration file can be found here

Formatting Stream Data

stream mode does not use delimiters. It expects your data to have a header that describes the data you’re sending.

The stream header looks like this

|----- 4 bytes -----||----- 4 bytes ----||---- 8 bytes ----||-- Size of your payload --|

Where each section is:

|--- int32 Header Key  ---||--- int32 CRC Hash ---||--- int64 Timestamp ---||--- Binary payload as BLOB ---|
  • HeaderKey. HeaderKey is an unsigned int with the value 0x1234567. This constant value provides a check that the incoming data is properly aligned.
  • CRC Hash. The CRC is a 4 byte unsigned integer containing the CRC (ITUT V.42) of the payload. This used by the adapter to check the integrity of the incoming data.
  • Timestamp. The 64-bit FileTime timestamp value for the frame (or 0 if using local timestamps).
  • Payload. Your binary payload. It should match the structure defined in the configuration XML file.

The header key can be placed into a buffer with memcpy().

  // set headerkey
  int headerKey = 0x1234567;
  memcpy(buf, &headerKey, sizeof(headerKey));

The current position in the buffer is stored is stored in a variable called offset. We just wrote an int to the buffer, so we set the new offset to the size of an int

  int offset = sizeof(int);

The payload CRC value will be calculated later, so we skip over it

    // Do CRC later.
    offset += sizeof(int);

The next value to write to the buffer is the FileTime.

  //  get UTC time as Windows Filetime.
  FILETIME filetime;
  SystemTimeToFileTime(&systime, &filetime);
  int64_t timestamp = (uint64_t)filetime.dwHighDateTime << 32 | (uint64_t)filetime.dwLowDateTime;
  memcpy(buf+offset, &timestamp, sizeof(timestamp));
  offset += sizeof(timestamp);

Writing data to the payload section also uses calls to memcpy() followed by updating the offset.

Numeric types:

    int someInt = counter;
    memcpy(buf + offset, &someInt, sizeof(someInt));
    offset += sizeof(someInt);

Strings:

  // Add 'SomeTextField' to the network adapter message
  std::string message = "Some C++ Text Field";
  memcpy(buf+ offset, message.c_str(), message.length());
  // Defined String64 in confiruration XML. So we need move 64 bytes
  offset += 64;

Once the data is set, compute the CRC of the payload and place it in the buffer

    // compute CRC hash
    unsigned int hash = 0;
    crc32(buf + 16, 88, &hash);
    memcpy(buf + 4, &hash, sizeof(hash));

Sending Binary Data over TCP

Networking in Windows with C or C++ takes a bit more work than Python or C#

First we setup a connection:

    struct sockaddr_in address;
    int s = 0;
    socklen_t alen = sizeof(address);
    char buf[BUF_LEN];


    // startup Winsock
    WSADATA wsa;
    if (WSAStartup(MAKEWORD(2, 2), &wsa) != 0)
    {
        std::cerr << "WSAStartup() failed" << std::endl;
        return -1;
    }

    // Create the TCP socket
    if ((s = socket(AF_INET, SOCK_STREAM, 0)) < 0)
    {
        std::cerr << "socket() failed" << std::endl;
        return -2;
    }

    // setup address structure
    memset((char*)&address, 0, sizeof(address));
    address.sin_family = AF_INET;
    address.sin_port = htons(PORT);

    InetPton(AF_INET, SERVER, &address.sin_addr.s_addr);


    if (connect(s, (struct sockaddr*) &address, sizeof(address)) != 0) {
        std::cerr <<  "connection with the server failed..." << std::endl;
        return -3;
    }

Send the data with a call to sendto()

    if (sendto(s, buf, 96 + 8, 0, (struct sockaddr*)&address, sizeof(address)) < 0)
    {
        std::cerr << "sendto() failed: "<< WSAGetLastError() << std::endl;

        return -4;
    }

If everything worked, Adapter Launcher should receive data from your application

adapter launcher data and relay it to Record Manager record manager data  

Complete code can be found at GitHub

Multiple Network Adapters

I’ve configured each of the above examples so they can be run simultaneously. Each has it’s own unique <port> and their own <device_type>, <unique_name>, and <multi_device_index>

...

    <port>23223</port>
  </network>

  <global>
    <!-- Type of device (e.g., phyio, eye_tracking, eeg, ...). -->
    <device_type>CSharp_Sample_Device</device_type>

    <!-- Unique name of this device. -->
    <unique_name>C#_Binary_Sample_Bus</unique_name>

    <!-- Ideal run rate, in Hz. -->
    <target_rate_hz>50</target_rate_hz>

    <!-- Unique index, used if multiple parallel network translators are running. [0,9] -->
    <multi_device_index>2</multi_device_index>
  </global>

record manager all data  

About Me

Greg Gallardo

I'm a software developer and sys-admin living in the US. I code with C++, C#, Java, Swift, Python, JavaScript and TypeScript. I also maintain Windows and Linux systems on-site and Linode, AWS and Azure

Github

Mastodon

YouTube

About you

IP Address: 34.239.154.201

User Agent: CCBot/2.0 (https://commoncrawl.org/faq/)

Language: en-US,en;q=0.5

Latest Posts