Kernel Tasks SDK
  • 31 Jul 2024
  • PDF

Kernel Tasks SDK

  • PDF

Article summary

The Kernel SDK allows you to emit your own task events to be recorded alongside Flow2 data in the same dataset.

Specification

To send events to our system, send 2 TCP packets for each event to the computer being used to acquire Flow2 data on port 6767. The first packet should contain the byte size of the JSON payload represented as a network-endian/big-endian unsigned 4 byte integer. The second packet should contain the JSON bytes itself.

The fields in the JSON should be:

NAMETYPENOTES
idintegerAn incrementing integer signifying a unique event identifier
timestampintegerAn epoch timestamp in microseconds
eventstringThe name of the event
valuestringThe value of the event

If tasks are not running on the computer being used to acquire Flow2 data, ensure that:

  • The TCP packets can reach the data acquisition computer over the network
  • The two computers' clocks are synchronized via NTP or a similar protocol

Conventions

Context Setting Events

To take advantage of the export and analysis tools available in the Kernel Researcher Portal, structure your custom tasks according to the following conventions:

  • The first event must be start_experiment.
  • The last event must be end_experiment.
  • A task must be structured using the following hierarchy of events: experiment > task > block > trial.
    • Task, block, and trial are optional, but the hierarchy must be respected. (e.g. a block cannot be within a trial.)
  • The value for these context events MUST be an integer that represents the ordinal index (block 1, block 2, ..., block N)
  • Epochs (events with a non-zero duration) must be delineated by 2 events:
    • start_{}
    • end_{}
  • A duration will be calculated from these and will appear in the task data exported in the SNIRF files.

Instantaneous Events

Events (with a zero duration) which you wish to record a timestamp for MUST start with event_.

Metadata Events

All other events (those which are not prefixed with start_, end_, or event_ are considered metadata and their timestamps are not included in the task data in exported SNIRF files.

Additional Notes

The value field of an event can be a dictionary, in which case its keys appear as separate columns (prefixed with the name of the event) in the task data in exported SNIRF files.

BEST PRACTICES REMINDER
Send events whose timestamps matter for your analyses as close in time as possible to when events actually happen in the physical world. E.g., for visual stimuli displayed on a screen, events should be sent right after the screen flip command in environments such as psychopy (Python) or Psychtoolbox (Matlab).

Data Example

A simple finger tapping paradigm may create this sequence of events:

{"id": 1, "timestamp": 1709500189972160, "event": "start_experiment", "value": "1"}
{"id": 2, "timestamp": 1709500189972160, "event": "experiment_type", "value": "finger_tapping"}
{"id": 3, "timestamp": 1709500189972160, "event": "start_rest", "value": "1"}
{"id": 4, "timestamp": 1709500189972169, "event": "end_rest", "value": "1"}
{"id": 5, "timestamp": 1709500189972169, "event": "start_block", "value": "1"}
{"id": 6, "timestamp": 1709500189972169, "event": "block_type", "value": "right"}
{"id": 7, "timestamp": 1709500189972175, "event": "end_block", "value": "1"}
{"id": 8, "timestamp": 1709500189972175, "event": "start_rest", "value": "2"}
{"id": 9, "timestamp": 1709500189972180, "event": "end_rest", "value": "2"}
{"id": 10, "timestamp": 1709500189972180, "event": "start_block", "value": "2"}
{"id": 11, "timestamp": 1709500189972180, "event": "block_type", "value": "left"}
{"id": 12, "timestamp": 1709500189972185, "event": "end_block", "value": "2"}
...
{"id": 248, "timestamp": 1709500246184622, "event": "end_experiment", "value": "1"}

This series of events would be transformed by the Kernel Analysis tools to the following flattened version, ready for analysis (and export)

timestamp
event
duration
experiment
experiment_type
rest
block
block_type
0.000287start_experiment1129.9792741.0finger_tappingNaNNaNnan
0.016940start_rest23.7234861.0finger_tapping1NaNnan
23.740575start_block5.0518491.0finger_tappingNaN1.0right
28.812785start_rest20.2184861.0finger_tapping2NaNnan
49.031372start_block5.0320701.0finger_tappingNaN2.0left

Note that the timestamps are "zeroed" to the beginning of the first timestamp of the fNIRS data.

Task Events SDK Client

Kernel provides a Task SDK Client to make it easier to send events in the format described. See Kernel SDK for available languages and instructions.

Code Examples

import json
import socket
import struct
from time import time

# one time connection
host = 'acquisition.computer.host'
port = 6767
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.connect((host, port))

# then send events
id=1
timestamp = time()
data_to_send = {
    "id": id,
    "timestamp": int(timestamp * 1e6),
    "event": "start_trial",
    "value": "5",
}
event = json.dumps(data_to_send).encode("utf-8")
msg = struct.pack("!I", len(event)) + event
sock.sendall(msg)


using System;
using System.Net;
using System.Net.Sockets;
using Newtonsoft.Json;
using System.Text;

class KernelEvent {
  public int id { get; set; }
  public long timestamp { get; set; }
  public string @event { get; set; }
  public string value { get; set; }
}

class KernelEvents {
  const int PORT = 6767;
  NetworkStream nwStream;
  int id = 0;
  DateTime epoch = new DateTime(1970, 1, 1);

  public KernelEvents(string ip) {
        TcpClient client = new TcpClient(ip, PORT);
        this.nwStream = client.GetStream();
  }

  public void send(KernelEvent kernelEvent) {
	this.id++;
	kernelEvent.id = this.id;
	  
	TimeSpan timestamp = DateTime.UtcNow - epoch;
	kernelEvent.timestamp = (long)(timestamp.TotalMilliseconds * 1000);
	  
    string json = JsonConvert.SerializeObject(kernelEvent);
	Console.WriteLine(json);
	byte[] bytes = Encoding.UTF8.GetBytes(json);
	byte[] size = BitConverter.GetBytes(bytes.Length);
	if (BitConverter.IsLittleEndian) Array.Reverse(size);
    this.nwStream.Write(size, 0, size.Length);
    this.nwStream.Write(bytes, 0, bytes.Length);
  }
}

public class Program {
    public static void Main(string[] args) {
        KernelEvents kernelEvents = new KernelEvents("acquisition.computer.ip");
		kernelEvents.send(new KernelEvent {
			@event="start_trial",
			value="5",
		});
    }
}

Matlab: https://www.mathworks.com/help/instrument/write-and-read-data-over-the-tcpip-interface.html