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
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
= "0," + "Some text value," + str(counter) + "," + str(math.cos(time)) + "," + str(math.sin(time)) + "," + str(counter*counter) msg
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()
= socket.socket(socket.AF_INET, socket.SOCK_DGRAM) sock
and send the data with sendto()
'127.0.0.1', 23222 ); sock.sendto( msg.encode(), (
The complete python script looks like this:
import socket
from time import time, sleep
import math
= "127.0.0.1"
udpAddr = 23222
udpPort = 0
time
= socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock = 0;
counter
while True:
= "0," + "Some text value," + str(counter) + "," + str(math.cos(time)) + "," + str(math.sin(time)) + "," + str(counter*counter)
msg print ("Sending a packet [" + msg + "]")
sock.sendto(msg.encode(), (udpAddr, udpPort))0.01)
sleep(+= 0.1
time = counter + 1 counter
This code is also available on github
If everything worked, Adapter Launcher should receive data from your python source
and relay it to Record Manager
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;
Write(someInt);
binaryWriter.
// Add 'SomeFloatField' to the network adapter message
float cosAngle = (float)Math.Cos(time);
Write(cosAngle); binaryWriter.
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()
.
Write(paddedMessage.ToCharArray()); binaryWriter.
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();
new IPEndPoint(IPAddress.Parse("127.0.0.1"), 23223);
IPEndPoint ep = Connect(ep); client.
Sending the data is a simple call with the byte array and its length
// send the message out to the network adapter
Send(bytes, bytes.Length); client.
If everything worked, Adapter Launcher should receive data from your C# source
and relay it to Record Manager
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;
sizeof(headerKey)); memcpy(buf, &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.
sizeof(int); offset +=
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;
sizeof(timestamp));
memcpy(buf+offset, ×tamp, sizeof(timestamp); offset +=
Writing data to the payload section also uses calls to memcpy()
followed by updating the offset.
Numeric types:
int someInt = counter;
sizeof(someInt));
memcpy(buf + offset, &someInt, sizeof(someInt); offset +=
Strings:
// Add 'SomeTextField' to the network adapter message
"Some C++ Text Field";
std::string message =
memcpy(buf+ offset, message.c_str(), message.length());// Defined String64 in confiruration XML. So we need move 64 bytes
64; offset +=
Once the data is set, compute the CRC of the payload and place it in the buffer
// compute CRC hash
unsigned int hash = 0;
16, 88, &hash);
crc32(buf + 4, &hash, sizeof(hash)); memcpy(buf +
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;
sizeof(address);
socklen_t alen = char buf[BUF_LEN];
// startup Winsock
WSADATA wsa;if (WSAStartup(MAKEWORD(2, 2), &wsa) != 0)
{"WSAStartup() failed" << std::endl;
std::cerr << return -1;
}
// Create the TCP socket
if ((s = socket(AF_INET, SOCK_STREAM, 0)) < 0)
{"socket() failed" << std::endl;
std::cerr << return -2;
}
// setup address structure
char*)&address, 0, sizeof(address));
memset((
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) {
"connection with the server failed..." << std::endl;
std::cerr << return -3;
}
Send the data with a call to sendto()
if (sendto(s, buf, 96 + 8, 0, (struct sockaddr*)&address, sizeof(address)) < 0)
{"sendto() failed: "<< WSAGetLastError() << std::endl;
std::cerr <<
return -4;
}
If everything worked, Adapter Launcher should receive data from your application
and relay it to Record Manager
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>