Data Collection with Physiological Sensors

Now that you have explored both Unity and the various types of physiological sensors, it’s time to begin integrating them together. You will do this using the Delsys Trigno Link, a device that allows multiple physiological sensors to be connected to a single computer via bluetooth. This tutorial will include an in-depth tutorial on how to connect and setup the Trigno Link, and how to collect data from the sensors attached to it. You will start simple, and develop a simple, console-based based Unity application that prints out the data recieved rom the sensors. The main goal of this module is to ensure that you can effectively use and read from the Trigno Link, and access that data in Unity. Once you have these basics down, the next module wll move on to more complex integration techniques using VR.

Connecting to Unity

Warning

There is a known bug preventing the usage of the Delsys API with specific versions of Unity. While it is unknown exactly which versions are affected, 2022.3.7f1 has been tested and is working, so that is what will be used here and what is recommended for you to use as well.

Now that you have your sensors connected and are ready to begin using them, it’s time to create a Unity application. Follow the steps below to get set up for Unity development with the Delsys API.

An Image of the Unity login screen.
  1. Launch Unity Hub and login to your Unity account. If you do not have either of these completed, see Installing Unity.

An Image of the projects tab in Unity Hub.
  1. On the sidebar on the left side of the application, select the Projects tab.

  2. On the top right of the application, press the button titled New project.

An Image of the 2D project template in Unity Hub.
  1. From here you can select any template project type you would like, depending on the context of the application you are creating. For this basic introduction to the integration, we will use the 2D Core template.

  2. Likewise to the projects you created previously in this set of tutorials, you can name the project whatever you like and choose where it is saved.

  3. Click the Create project button in the bottom right corner of the application to create the project and launch the Unity editor.

An Image of navigating to the package manager in the new 3D project.
  1. In the editor, navigate to the Edit option in the top ribbon, and select Project Settings from the dropdown menu.

An Image of the configuration section of the Player Settings menu.
  1. In the project settings menu, select the option on the right toolbar titled Player. In the menu for your selected platform (in this case Windows, Mac, and Linux), scroll down to the Configuration section. In this location, make sure the Scripting Backend is set to IL2CPP and that the Api Compatibility Level is set to .NET Framework. Your project may take a minute to apply these changes.

An Image of the plugins folder in the assets folder.
  1. In the assets folder of your Unity project, add an empty folder called Plugins. This is where you will put the API files.

An Image of the Delsys Example Applications Github Repository.
  1. To get the API files you will need to add to your project, go to the Delsys Example Applications Github page. Select the green button labeled Code, and select the Download ZIP option from the dropdown menu that appears. This should download a ZIP file containing the example projects, as well as the API files you’ll need.

  2. Right click the ZIP file you just downloaded in your file explorer, and Extract it. In Windows 11, this is done through an option called Extract All… in the right click menu.

An Image of Assets subfolder in the Unity Example.
  1. Open the extracted folder, and navigate to the ~\Example-Applications-main\Unity\Assets subdirectory.

  2. Copy the Streaming Assets folder into your project’s assets folder.

  3. Navigate to the ~\Example-Applications-main\Delsys Unity Example\Assets\Plugins folder and copy everything contained in it to the Plugins folder you created for your project.

An Image of the Plugins folder of your project with System.Reactive.Linq.dll and System.Reactive.Linq.dll.meta, and System.Management.dll, and System.Management.dll.meta removed, and System.CodeDom.dll and System.CodeDom.dll.meta selected.
  1. In the plugins folder in your project, delete the file called System.CodeDom.dll and its associated meta file System.CodeDom.dll.meta. Delete System.Reactive.Linq.dll and its associated meta file System.Reactive.Linq.dll.meta. Delete System.Management.dll and its associated meta file System.Management.dll.meta.

An Image of the project's root folder with the moved file in it.
  1. In the plugins folder in your project, Move the file called SiUSBXp.dll and its associated meta file SiUSBXp.dll.meta to the root folder of your project. This should result in something looking like the image above.

An Image of Assets subfolder with the UnityScript.cs in it.
  1. Lastly, copy the UnityExample.cs script from the Delsys Unity Example project into the Assets folder of your project. You will be adding to this script to implement the Trigno Link, since it is easier than starting from scratch.

  2. Allow your Unity project time to reload its domain, and then ensure there are no compilation errors with the project (You may see warnings, indicated with a yellow exclamation mark, these are okay). If there are none, you are ready to move on. If not, retry the steps above on a different version of Unity or try and troubleshoot the compilation error using the Troubleshooting Resources page of this site.

A Simple Unity Application

For this simple application, you are going to be slightly modifying the Unity Example script that you got from the Delsys Unity Example Application. It is entirely possible to make your own script utilizing the Delsys API, and if you wish to do so you may, but using the Example script as a template offers useful and working functions right from the start, saving you time and providing you with something you know works. If you do decide to create your own scripts utilizing the Delsys API in the future, you can use the Example script alongside the Delsys API Quickstart Guide and Delsys API User Guide to learn about the available functionality. You can find the Unity project on our GitHub page.

Modifying the Example Script

Note

Before you begin modifying your script, please remember to put your API license and key into the private string key = ""; and private string license = ""; variables of your modified Example script. You cannot use the API without them.

Below is the modified code for the Example script that allows it to search for Trigno Link components alongside the Trigno RF ones. Beneath that you will be given descriptions of what each modification does, so you can use it in your future projects.

  1  using DelsysAPI.Channels.Transform;
  2  using DelsysAPI.Configurations;
  3  using DelsysAPI.Configurations.DataSource;
  4  using DelsysAPI.Contracts;
  5  using DelsysAPI.DelsysDevices;
  6  using DelsysAPI.Events;
  7  using DelsysAPI.Pipelines;
  8  using DelsysAPI.Transforms;
  9  using DelsysAPI.Utils;
 10  using System;
 11  using System.Collections.Generic;
 12  using System.IO;
 13  using System.Linq;
 14  using System.Threading.Tasks;
 15  using UnityEngine;
 16  using UnityEngine.UI;
 17  using UnityEngine.Events;
 18  using TMPro;
 19  using DelsysAPI.Components.TrignoLink;
 20  public class UnityExample : MonoBehaviour
 21  {
 22      //Paste key/license strings here
 23      private string key = "";
 24      private string license = "";
 25
 26
 27      /// <summary>
 28      /// Data structure for recording every channel of data.
 29      /// </summary>
 30      List<List<double>> Data = new List<List<double>>();
 31      public Button ScanButton;
 32      public Button StartButton;
 33      public Button StopButton;
 34      public Button SelectButton;
 35      public Button PairButton;
 36      IDelsysDevice DeviceSource = null;
 37      int TotalLostPackets = 0;
 38      int TotalDataPoints = 0;
 39      public TMP_Text APIStatusText, TestText, PipelineState, EMGText, HRText;
 40      Pipeline RFPipeline;
 41      ITransformManager TransformManager;
 42      string text, pipeline_state;
 43      UnityEvent m_scan;
 44      bool select, scan, start, stop, pair;
 45      string[] compoentNames;
 46      List<List<List<double>>> AllCollectionData = new List<List<List<double>>>();
 47      VerticalLayoutGroup verticalLayoutGroup;
 48      public string latestEMGData;
 49      public string latestHRData;
 50
 51      private bool usingTrignoLink;
 52      public string latestEMGData;
 53      public string latestHRData;
 54
 55      // Use this for initialization
 56      void Start()
 57      {
 58
 59          Debug.Log("Entered Start Function.");
 60
 61          usingTrignoLink = true;
 62
 63          //Finding references to all the buttons in the scene
 64          ScanButton = GameObject.Find("ScanButton").GetComponent<Button>();
 65          ScanButton.onClick.AddListener((UnityEngine.Events.UnityAction) this.clk_Scan);
 66
 67          StartButton = GameObject.Find("StartButton").GetComponent<Button>();
 68          StartButton.onClick.AddListener((UnityEngine.Events.UnityAction) this.clk_Start);
 69
 70          StopButton = GameObject.Find("StopButton").GetComponent<Button>();
 71          StopButton.onClick.AddListener((UnityEngine.Events.UnityAction) this.clk_Stop);
 72
 73          SelectButton = GameObject.Find("SelectButton").GetComponent<Button>();
 74          SelectButton.onClick.AddListener((UnityEngine.Events.UnityAction) this.clk_Select);
 75
 76          PairButton = GameObject.Find("PairButton").GetComponent<Button>();
 77          PairButton.onClick.AddListener((UnityEngine.Events.UnityAction) this.clk_Pair);
 78
 79          scan = true; //Enabling only the Scan button for now.
 80          start = false;
 81          stop = false;
 82          select = false;
 83          pair = false;
 84
 85          CopyUSBDriver(); // Copying the SiUSBXp.dll file if not present
 86          InitializeDataSource(); //Initializing the Delsys API Data source
 87      }
 88
 89
 90      // Update is called once per frame
 91      void Update()
 92      {
 93          APIStatusText.text = text;
 94          SelectButton.enabled = select;
 95          ScanButton.enabled = scan;
 96          StartButton.enabled = start;
 97          StopButton.enabled = stop;
 98          PairButton.enabled = pair;
 99          PipelineState.text = PipelineController.Instance.PipelineIds[0].CurrentState.ToString();
100          EMGText.text = "Latest EMG Data: " + latestEMGData;
101          HRText.text = "Latest HR Data: " + latestHRData;
102      }
103
104      public void CopyUSBDriver()
105      {
106          string unityAssetPath = Application.streamingAssetsPath + "/SiUSBXp.dll";
107          string adjacentToExePath = Application.dataPath + "/../SiUSBXp.dll";
108          if (!File.Exists(adjacentToExePath))
109          {
110              File.Copy(unityAssetPath, adjacentToExePath);
111          }
112      }
113
114      /// <summary>
115      /// Dumping all the debug statements from DelsysAPI into the Unity's Log file, see https://docs.unity3d.com/Manual/LogFiles.html for more details.
116      /// </summary>
117      /// <returns> None </returns>
118      public void TraceWriteline(string s, object[] args)
119      {
120          for(int i=0; i< args.Count();i++){
121              s = s + "; " + args[i];
122          }
123          Debug.Log("Delsys API:- " + s);
124
125      }
126
127      #region Initialization
128      public void InitializeDataSource()
129      {
130
131          text = "Creating device source . . . ";
132          if(key.Equals("") || license.Equals("")){
133              text = "Please add your license details from the code.";
134          }
135          var deviceSourceCreator = new DeviceSourcePortable(key, license);
136          deviceSourceCreator.SetDebugOutputStream(TraceWriteline);
137          DeviceSource = deviceSourceCreator.GetDataSource(new SourceType[2] { SourceType.TRIGNO_RF, SourceType.TRIGNO_LINK });
138          text  = "Device source created.";
139          DeviceSource.Key = key;
140          DeviceSource.License = license;
141          text = "Loading data source . . . ";
142
143          try
144          {
145              LoadDataSource(DeviceSource);
146          }
147          catch(Exception exception)
148          {
149              text = "Something went wrong: " + exception.Message;
150              return;
151          }
152          text = "Data source loaded and ready to Scan.";
153      }
154
155      public void LoadDataSource(IDelsysDevice ds)
156      {
157          PipelineController.Instance.AddPipeline(ds);
158
159          RFPipeline = PipelineController.Instance.PipelineIds[0];
160          TransformManager = PipelineController.Instance.PipelineIds[0].TransformManager;
161
162          RFPipeline.TrignoRfManager.ComponentScanComplete += ComponentScanComplete;
163          RFPipeline.CollectionStarted += CollectionStarted;
164          RFPipeline.CollectionDataReady += CollectionDataReady;
165          RFPipeline.CollectionComplete += CollectionComplete;
166          RFPipeline.TrignoRfManager.ComponentAdded += ComponentAdded;
167          RFPipeline.TrignoRfManager.ComponentLost += ComponentLost;
168          RFPipeline.TrignoRfManager.ComponentRemoved += ComponentRemoved;
169      }
170
171      #endregion
172
173      #region Button Click events: clk_Scan, clk_Select, clk_Start, clk_Stop, clk_Pair
174      public virtual async void clk_Scan()
175      {
176          Console.WriteLine("Scan Clicked");
177          foreach(var comp in RFPipeline.TrignoRfManager.Components)
178          {
179              await RFPipeline.TrignoRfManager.DeselectComponentAsync(comp);
180          }
181          //Trying to search for Link components - will set link bool to false if fail
182          try{
183              foreach (var component in RFPipeline.TrignoLinkManager.Components)
184                  await RFPipeline.TrignoLinkManager.DeselectComponentAsync(component);
185                  Debug.Log("Trigno Link has sensors connected.");
186          }
187          catch(Exception e){
188              Debug.Log("Trigno Link has no sensors connected.");
189              usingTrignoLink = false;
190          }
191          text = "Scanning . . .";
192          await RFPipeline.Scan();
193      }
194
195      public virtual void clk_Select()
196      {
197          SelectSensors();
198      }
199
200      public virtual async void clk_Start()
201      {
202
203          // The pipeline must be reconfigured before it can be started again.
204          bool success = ConfigurePipeline();
205          if(success){
206              Debug.Log("Starting data streaming....");
207              text = "Starting data streaming....";
208              await RFPipeline.Start();
209              stop = true;
210          }
211          else{
212              Debug.Log("Configuration failed. Cannot start streaming!!");
213              text = "Fatal error!";
214          }
215
216      }
217
218      public virtual async void clk_Stop()
219      {
220          await RFPipeline.StopInformationStream();
221          await RFPipeline.Stop();
222          RFPipeline.SetActiveDataSources(new List<SourceType>{SourceType.TRIGNO_RF, SourceType.TRIGNO_LINK});
223          await RFPipeline.DisarmPipeline();
224          PipelineController.Instance.RemovePipeline(0);
225
226      }
227
228      public virtual async void clk_Pair()
229      {
230          text = "Awaiting a sensor pair . . .";
231          await RFPipeline.TrignoRfManager.AddTrignoComponent(new System.Threading.CancellationToken());
232      }
233
234      #endregion
235
236      public void SelectSensors()
237      {
238          text = "Selecting all sensors . . .";
239
240          // Select every component we found and didn't filter out.
241          foreach (var component in RFPipeline.TrignoRfManager.Components)
242          {
243              bool success = RFPipeline.TrignoRfManager.SelectComponentAsync(component).Result;
244              if(success){
245                  text = component.FriendlyName + " selected!";
246              }
247              else{
248                  text = "Could not select sensor!!";
249              }
250          }
251          //Now doing search for link components if link is enabled.
252          if(usingTrignoLink == true){
253              foreach(var component in RFPipeline.TrignoLinkManager.Components){
254                  bool success = RFPipeline.TrignoLinkManager.SelectComponentAsync(component).Result;
255                  if (success == true){
256                      text = component.FriendlyName + "selected!";
257                  }else{
258                      text = "Could not select sensor!!";
259                  }
260              }
261          }
262          start = true;
263      }
264
265
266      /// <summary>
267      /// Configures the input and output of the pipeline.
268      /// </summary>
269      /// <returns></returns>
270      private bool ConfigurePipeline()
271      {
272          var inputConfiguration = new TrignoDsConfig();
273
274          if (PortableIoc.Instance.CanResolve<TrignoDsConfig>())
275          {
276              PortableIoc.Instance.Unregister<TrignoDsConfig>();
277          }
278
279          PortableIoc.Instance.Register(ioc => inputConfiguration);
280
281          foreach (var somecomp in RFPipeline.TrignoRfManager.Components.Where(x => x.State == SelectionState.Allocated))
282          {
283              somecomp.SelectSampleMode(somecomp.DefaultMode);
284          }
285
286          try
287          {
288              Debug.Log("Applying Input configurations");
289              bool success_1 = RFPipeline.ApplyInputConfigurations(inputConfiguration);
290              if(success_1){
291                  text =  "Applied input configuration";
292                  Debug.Log("Applied input configuration");
293              }
294              else{
295                  text = "Input configurations failed";
296                  Debug.Log("Input configurations failed");
297              }
298          }
299          catch (Exception exception)
300          {
301              text = exception.Message;
302          }
303          RFPipeline.RunTime = int.MaxValue;
304
305          TransformConnector transformConnector = new TransformConnector(RFPipeline);
306          OutputConfig outputConfig = transformConnector.SetupTransforms();
307
308          bool success_2 = RFPipeline.ApplyOutputConfigurations(outputConfig);
309          if(success_2){
310              text = "Applied Output configurations";
311              Debug.Log("Applied Output configurations");
312              return true;
313          }
314          else{
315              text = "Output configurations failed!";
316              Debug.Log("Output configurations failed!");
317              return false;
318          }
319      }
320
321
322
323      #region Collection Callbacks -- Data Ready, Colleciton Started, and Collection Complete
324      public virtual void CollectionDataReady(object sender, ComponentDataReadyEventArgs e)
325      {
326          //Channel based list of data for this frame interval
327          List<List<double>> data = new List<List<double>>();
328
329          for (int k = 0; k < e.Data.Count(); k++)
330          {
331              // Loops through each connected sensor
332              for (int i = 0; i < e.Data[k].SensorData.Count(); i++)
333              {
334                  // Loops through each channel for a sensor
335                  for (int j = 0; j < e.Data[k].SensorData[i].ChannelData.Count(); j++)
336                  {
337                      data.Add(e.Data[k].SensorData[i].ChannelData[j].Data);
338                      for (int k2 = 0; k2 <e.Data[k].SensorData[i].ChannelData[j].Data.Count(); k2++){
339                          if (i == 1)//Heart Rate Sensor Index
340                          {
341                              latestHRData = e.Data[k].SensorData[i].ChannelData[j].Data[k2].ToString();
342                          }
343                          else if (i == 0)//Avanti Sensor Index
344                          {
345                              latestEMGData = e.Data[k].SensorData[i].ChannelData[j].Data[k2].ToString();
346                          }
347                          Debug.Log(e.Data[k].SensorData[i].ChannelData[j].Data[k2]);
348                      }
349                  }
350              }
351
352          }
353
354          //Add frame data to entire collection data buffer
355          AllCollectionData.Add(data);
356          text = AllCollectionData.Count.ToString();
357      }
358
359      public virtual void CollectionStarted(object sender, DelsysAPI.Events.CollectionStartedEvent e)
360      {
361          AllCollectionData = new List<List<List<double>>>();
362          text = "CollectionStarted event triggered!";
363          var comps = PipelineController.Instance.PipelineIds[0].TrignoRfManager.Components;
364
365          // Refresh the counters for display.
366          TotalDataPoints = 0;
367          TotalLostPackets = 0;
368
369          // Recreate the list of data channels for recording
370          int totalChannels = 0;
371          for (int i = 0; i < comps.Count; i++)
372          {
373              for (int j = 0; j < comps[i].TrignoChannels.Count; j++)
374              {
375                  if (Data.Count <= totalChannels)
376                  {
377                      Data.Add(new List<double>());
378                  }
379                  else
380                  {
381                      Data[totalChannels] = new List<double>();
382                  }
383                  totalChannels++;
384              }
385          }
386      }
387
388      public virtual async void CollectionComplete(object sender, DelsysAPI.Events.CollectionCompleteEvent e)
389      {
390          text = "CollectionComplete event triggered!";
391          await RFPipeline.DisarmPipeline();
392      }
393
394      #endregion
395
396      #region Component Events: Scan complete, Component Added, Lost, Removed
397      public virtual void ComponentScanComplete(object sender, DelsysAPI.Events.ComponentScanCompletedEventArgs e)
398      {
399          text = "Scan Complete";
400
401          select = true;
402          pair = true;
403
404      }
405
406      public async void ComponentAdded(object sender, ComponentAddedEventArgs e)
407      {
408
409      }
410
411      public virtual void ComponentLost(object sender, ComponentLostEventArgs e)
412      {
413          int sensorStickerNumber = RFPipeline.TrignoRfManager.Components.Where(sensor => sensor.Id == e.Component.Id).First().PairNumber;
414          Console.WriteLine("It appears sensor " + sensorStickerNumber + " has lost connection. Please power cycle this sensor.");
415          text = "It appears sensor " + sensorStickerNumber + " has lost connection";
416
417      }
418
419      public virtual void ComponentRemoved(object sender, ComponentRemovedEventArgs e)
420      {
421
422      }
423
424      #endregion
425
426  }

The first change made to the code from the original example script was the addition of the lines using TMPro; and using DelsysAPI.Components.TrignoLink. These lines add necessary functionality for Unity’s updated text system and the Trigno Link, respectively.

After adding TMPro, change the line public Text APIStatusText, TestText, PipelineState; to public TMP_Text APIStatusText, TestText, PipelineState;. This will convert the text variables used to the updated text system.

On the same line as the previous modification, add EMGDataText and HRDataText. The result should look as follows: public TMP_Text APIStatusText, TestText, PipelineState, EMGDataText, HRDataText;. These two variables will be used to display the latest EMG and HR data on screen.

The next modification that was made was the creation of the boolean variable usingTrignoLink. This variable lets the script know whether or not a Trigno Link is being used. In the start method, its default value is set to true. Later, you will see that the scan function can change this value.

Also in the start method, the lines:

ScanButton = GameObject.FindGameObjectWithTag ("ScanButton").GetComponent<Button>();
ScanButton.onClick.AddListener((UnityEngine.Events.UnityAction) this.clk_Scan);

StartButton = GameObject.FindGameObjectWithTag ("StartButton").GetComponent<Button>();
StartButton.onClick.AddListener((UnityEngine.Events.UnityAction) this.clk_Start);

StopButton = GameObject.FindGameObjectWithTag ("StopButton").GetComponent<Button>();
StopButton.onClick.AddListener((UnityEngine.Events.UnityAction) this.clk_Stop);

SelectButton = GameObject.FindGameObjectWithTag ("SelectButton").GetComponent<Button>();
SelectButton.onClick.AddListener((UnityEngine.Events.UnityAction) this.clk_Select);

PairButton = GameObject.FindGameObjectWithTag ("PairButton").GetComponent<Button>();
PairButton.onClick.AddListener((UnityEngine.Events.UnityAction) this.clk_Pair);

are changed to:

ScanButton = GameObject.Find("ScanButton").GetComponent<Button>();
ScanButton.onClick.AddListener((UnityEngine.Events.UnityAction) this.clk_Scan);

StartButton = GameObject.Find("StartButton").GetComponent<Button>();
StartButton.onClick.AddListener((UnityEngine.Events.UnityAction) this.clk_Start);

StopButton = GameObject.Find("StopButton").GetComponent<Button>();
StopButton.onClick.AddListener((UnityEngine.Events.UnityAction) this.clk_Stop);

SelectButton = GameObject.Find("SelectButton").GetComponent<Button>();
SelectButton.onClick.AddListener((UnityEngine.Events.UnityAction) this.clk_Select);

PairButton = GameObject.Find("PairButton").GetComponent<Button>();
PairButton.onClick.AddListener((UnityEngine.Events.UnityAction) this.clk_Pair);

This is because the Unity tag system used in the FindGameObjectWithTag method is outdated and no longer functions. Simply changing each instance of this function to the newer Find method will fix this.

Note

You must name the GameObject s the same name as given to the Find method. For example, the button you want to use as a “Stop” button must be named “StopButton” for it to be found by Find. You can change the argument of Find to whatever you like, but just know that the corresponding GameObject must share that name, and it is case-sensitive.

Next, in the Update method, add the lines EMGText.text = "Latest EMG Data: " + latestEMGData; and HRData.text = "Latest HR Data: " + latestHRData;. Doing this makes sure that Unity updates the on-screen text displaying the latest data every frame.

Now getting into more substantial changes, in the function InitializeDataSource, the line DeviceSource = deviceSourceCreator.GetDataSource(SourceType.TRIGNO_RF); must be changed to DeviceSource = deviceSourceCreator.GetDataSource(new SourceType[2] { SourceType.TRIGNO_RF, SourceType.TRIGNO_LINK }); . This is what tells the API to search for Trigno Link components alongside the regular RF ones.

Another important change to make is to the clk_Scan method. Here, add a new try-catch block right after the foreach loop but before the assigning variable text to “scanning” and awaiting the scan. The try-catch should be composed of the following:

try{
  foreach (var component in RFPipeline.TrignoLinkManager.Components)
      await RFPipeline.TrignoLinkManager.DeselectComponentAsync(component);
      Debug.Log("Trigno Link has sensors connected.");
}
  catch(Exception e){
  Debug.Log("Trigno Link has no sensors connected.");
  usingTrignoLink = false;
}

This block of code tries to check for Trigno Link components connected to the system. If it finds them, it adds them to the list of components. If none are found, the system sets the boolean usingTrignoLink to false, and the Trigno Link is not considered connected.

Next, in the clk_stop method, add the following four lines of code:

await RFPipeline.StopInformationStream();
RFPipeline.SetActiveDataSources(new List<SourceType>{SourceType.TRIGNO_RF, SourceType.TRIGNO_LINK});
await RFPipeline.DisarmPipeline();
PipelineController.Instance.RemovePipeline(0);

The purpose of these lines is to ensure a smooth disconnection of the devices connected to the API, now including the Trigno Link.

An important modification to make is to the SelectSensors method. Here, you are going to want to add an additional foreach loop that only runs if the Trigno Link is being used (as determined by the value of usingTrignoLink). This loop will search through the Trigno Link’s connected sensors and select each of them for data collection. The code should be written as follows, and should be placed right below the outermost closing bracket of the first loop, before start is set to true:

if(usingTrignoLink == true){
    foreach(var component in RFPipeline.TrignoLinkManager.Components){
        bool success = RFPipeline.TrignoLinkManager.SelectComponentAsync(component).Result;
        if (success == true){
            text = component.FriendlyName + "selected!";
        }else{
            text = "Could not select sensor!!";
        }
    }
}

Lastly, In the CollectionDataReady method, add the following code snippet to the innermost for loop:

if (i == 1)//Heart Rate Sensor Index
                    {
                        latestHRData = e.Data[k].SensorData[i].ChannelData[j].Data[k2].ToString();
                    }
                    else if (i == 0)//Avanti Sensor Index
                    {
                        latestEMGData = e.Data[k].SensorData[i].ChannelData[j].Data[k2].ToString();
                    }

The result should be:

public virtual void CollectionDataReady(object sender, ComponentDataReadyEventArgs e)
{
    //Channel based list of data for this frame interval
    List<List<double>> data = new List<List<double>>();

    for (int k = 0; k < e.Data.Count(); k++)
    {
        // Loops through each connected sensor
        for (int i = 0; i < e.Data[k].SensorData.Count(); i++)
        {
            // Loops through each channel for a sensor
            for (int j = 0; j < e.Data[k].SensorData[i].ChannelData.Count(); j++)
            {
                data.Add(e.Data[k].SensorData[i].ChannelData[j].Data);
                for (int k2 = 0; k2 <e.Data[k].SensorData[i].ChannelData[j].Data.Count(); k2++){
                    if (i == 1)//Heart Rate Sensor Index
                    {
                        latestHRData = e.Data[k].SensorData[i].ChannelData[j].Data[k2].ToString();
                    }
                    else if (i == 0)//Avanti Sensor Index
                    {
                        latestEMGData = e.Data[k].SensorData[i].ChannelData[j].Data[k2].ToString();
                    }
                    Debug.Log(e.Data[k].SensorData[i].ChannelData[j].Data[k2]);
                }
            }
        }

    }

    //Add frame data to entire collection data buffer
    AllCollectionData.Add(data);
    text = AllCollectionData.Count.ToString();
}

The purpose in doing this is to update the latest heart rate data and the latest EMG data each time a new data packet is recieved. This ensures that the data displayed is the newest available.

Now that all of the modifications have been made, save your script using ctrl + S on Windows, or cmd + S on Mac, and ensure that Unity does not display any compiler errors. If you encounter errors, you can always copy and paste the entire script from this page and overwrite yours, since the script here is known to work. You are now ready to move on to adding the necessary GameObject s to the project.

Adding GameObjects

An Image of the layout for the project.

If you are building from the example script, you are going to need a few GameObject s in order to create the scene for your application. Please create the following:

  • A Canvas Object: This will be the parent object for your buttons and text, and defines what the screen shows. This is added through the UI section of the object creation dropdown, and creates an Event System with it.

  • An Event System Object: This is used to make your UI elements interactable. Again, this should be created alongside your canvas.

  • A Main Camera: This is included with the sample scene, and it just defines the view of the game. It should be created for you already.

  • A Unity Example Object: This is the first object you must add yourself, and it is the most important object for the integration. It should be an Empty object, and should have the modified Example Script attached as a component.

Note

TMP Buttons are set up slightly differently than the ones you used in earlier modules. For these, the text for the button is contained in a child Text(TMP) object. TO change the text displayed on the button, got to this child object and change the text input component.

  • A Scan Button: This will be used to scan for sensors connected to the base station and the Trigno Link. Ensure that the name for this GameObject matches the one given as argument in the Find function for the GameObject ScanButton in the Example Script. If you have not changed the arguments, the name should be “ScanButton”.

  • A Start Button: This will be used to start collecting data once all of the sensors have been connected. Ensure that the name for this GameObject matches the one given as argument in the Find function for the GameObject StartButton in the Example Script. If you have not changed the arguments, the name should be “StartButton”.

  • A Pair Button: This will be used to pair sensors to the base station that were not previously paired in Trigno Discover. A sensor must be paired to the system before it can be picked up by a scan. Ensure that the name for this GameObject matches the one given as argument in the Find function for the GameObject PairButton in the Example Script. If you have not changed the arguments, the name should be “PairButton”.

  • A Stop Button: This will be used to disarm the Delsys API pipeline and safely stop data collection once clicked. Ensure that the name for this GameObject matches the one given as argument in the Find function for the GameObject StopButton in the Example Script. If you have not changed the arguments, the name should be “StopButton”.

  • A Select Button: This wil be used after the scan button to select every sensor found by the scan, and must be used before clicking the start button. Ensure that the name for this GameObject matches the one given as argument in the Find function for the GameObject SelectButton in the Example Script. If you have not changed the arguments, the name should be “SelectButton”.

These five buttons provide the core functionality for the API, but there are two additional GameObject s you should add that can provide additional information about the API’s status. These should both be Text Mesh Pro Text objects. They are as follows:

  • An API Status Text: This will display the current status of the API in real-time, so you an get a better idea of what state it’s in while your application is running. You will need to select the Unity Example object, and in the script component section of the inspector, drag and drop the API status into the “API Status Text” slot. You will do the same for the following objects as well.

  • A Pipeline State Text: This will display the current state of the pipeline, and whether or not the base station is properly connected. As with the previous object, assign this object to its respective slot in the script component of the Unity Example object.

  • An HR Data Text: This will display the latest HR data received from the sensor.

  • An EMG Data Text: This will display the latest EMG data received from the sensor.

At the end of the object creation, your object hierarchy should look like the following:

An Image of the project's object hierarchy containing the Canvas, Event System, Main Camera, Unity Example, Scan Button, Pair Button, Start Button, Stop Button, Select Button,  API Status Text, and Pipeline State Text ``GameObject`` s. The Scan Button, Pair Button, Start Button, Stop Button, Select Button,  API Status Text, Pipeline State Text, HR Data Text, and EMG Data Text are all children of the Canvas.

And, your Unity Example Script Object’s script component should have the following filled in for it’s public variable slots. You can drag and drop any of the objects that are missing or incorrectly placed in the slots from the object hierarchy into the slots:

An Image of the Unity Example Object's inspector window with the Scan Button assigned to ``ScanButton`` , the Start Button assigned to ``StartButton`` , the Stop Button assigned to ``StopButton`` , the Select Button assigned to ``SelectButton`` , the Pair Button assigned to ``PairButton`` , the API Status Text assigned to ``API Status`` , and the Pipeline State assigned to ``PipelineState`` .

Make sure to move all of the text and button objectws so that they are not overlapping each other. You have now created all the necessary GameObject s for this project, and are ready to run it and begin running your project.

Running the Application

[Click on the above video for our YouTube tutorial on Trigno Link Data Collection.]

Now that you have the application properly set up and know how the data is being read, you are ready to actually run the application you’ve created. Follow the steps below to do so:

  1. Connect your Trigno Base Station to your computer via USB, and ensure it’s power supply is connected to an outlet.

  2. Connect your Trigno Link to your computer via USB.

  3. Press the Play Button in the top-center of the Unity editor and ensure that the API Status is displayed as “Data source loaded and ready to Scan.” and the Pipeline State is displayed as “Off”.

  4. Pull out an Avanti sensor from the base station and press it against a magnet to put it in scanning mode. Remember you need at least one Avanti sensor active to use the Link.

  5. Turn on your Trigno Link compatible sensor and put it in scanning mode.

  6. In Unity, select the Scan Button and wait for the system to pick up the powered on sensors. (You can check the Unity Console to see if they were found.)

  7. After scanning is complete, press the Select Button. Make sure that all the sensors were selected. (again, you can use the Unity Console to check this.)

  8. Click the Start Button. If you check the Console, you should see all of the data being printed out from the sensors in real-time. Note that sensors may have different sampling rates, so data may come at different times. If you have accessed the data anywhere else, you should be seeing its effects now.

  9. When you are done collecting data, click the Stop Button. While the application is still running, you can access all of the data from the AllCollectionData variable.

  10. Return the sensors to the base station or power them off. You have successfully collected data using the Trigno Link and Unity!

Additional Information

Collecting Sensor Data in Non-Real Time

There are two ways to easily access the data collected from using the API. The first method is for non-real-time data collection. You will notice that there is a variable in the modified example script you made called List<List<List<double>>> AllCollectionData . This list is filled with the data collected during real-time collection. Once data is no longer collected, you can read from this list to access the collected data. Notice that this list is a list of lists of lists of doubles. The outermost list contains the components connected to the system (the sensors), the first inner list contains the channels for each component, so if your sensor reads multiple types of data at once you can access all of it. The innermost list contains the actual data for the specific sensors specified channel. So for instance, if I only had one Avanti EMG connected and wanted to read the EMG data from it, I could access the data with AllCollectionData[0][0] . This would return the list of data contained within the first sensor, and in its first channel. If I were looking for a specific piece of data at, for example, time stamp 70, I could access that specific data with AllCollectionData[0][0][69] . Remember that C# starts indexing at zero, so your timestamp will be offset by one. You can access this data in a separate script by modifying the declaration of the variable in the Unity Example script. Unchanged, the declaration is List<List<List<double>>> AllCollectionData = new List<List<List<double>>>(); . By simply adding public in front of the declaration, you can make the variable accessible to other scripts. This should look like public List<List<List<double>>> AllCollectionData = new List<List<List<double>>>(); . To access this in another script, you can use the following code block as a template:

//first create a GameObject the script whose data you're trying to access is attached to
public GameObject ScriptHolder;
//then define the script you are looking for (In our case, type_of_script would be UnityExample, but in other cases it will be the name of the script;s class)
private type_of_script ExampleScript;
//last, define where to store the data. Must be same type as the data. If you don't know what type it is, you can use "var" and C# will auto-assign its type;
var data;

//then, in start function get the script component from the GameObject
private void Start()
{
    ExampleScript = ScriptHolder.GetComponent<type_of_script>();
}

//to access the data use this line of code. In or case, data_to_access would be AllCollectionData. You can also change the data this way too, so be careful!
data = ExampleScript.data_to_access;

Collecting Sensor Data in Real Time

To access the data in real-time, as it comes in from the sensor, you need to get the data at its source. The easiest way to do this is to modify the CollectionDataReady method in your Unity Example script. by default, the method should look like this:

public virtual void CollectionDataReady(object sender, ComponentDataReadyEventArgs e)
  {
      //Channel based list of data for this frame interval
      List<List<double>> data = new List<List<double>>();

      for (int k = 0; k < e.Data.Count(); k++)
      {
          // Loops through each connected sensor
          for (int i = 0; i < e.Data[k].SensorData.Count(); i++)
          {
              // Loops through each channel for a sensor
              for (int j = 0; j < e.Data[k].SensorData[i].ChannelData.Count(); j++)
              {
                  data.Add(e.Data[k].SensorData[i].ChannelData[j].Data);
                  for (int k2 = 0; k2 <e.Data[k].SensorData[i].ChannelData[j].Data.Count(); k2++){
                      Debug.Log(e.Data[k].SensorData[i].ChannelData[j].Data[k2]);
                  }
              }
          }

      }

      //Add frame data to entire collection data buffer
      AllCollectionData.Add(data);
      text = AllCollectionData.Count.ToString();
  }

Basically, what is happening in this function is the data for the specific frame your application is on is being read, and the three for-loops make sure that the data from each channel of every sensor is read. Notice the line Debug.Log(e.Data[k].SensorData[i].ChannelData[j].Data[k2]); in the innermost for-loop. This line prints out every single piece of data from every sensor for this frame to the Unity log. So, if you wish to access data from a specific sensor, you can add a line into the innermost for-loop to looking for that specific data. For example, you could add the line latestDataFromSensor1Channel1 = e.Data[k].SensorData[0].ChannelData[0].Data[k2]; to the innermost for-loop, and collect new data every frame. And similarly to the above non-real time method, if you want to access this data outside of the Example Script, you can define data as latestDataFromSensor1Channel1 and access it using the template. This will give you access to the data as it comes. If you have multiple sensors connected, make sure you are accessing the data from the correct sensor and channel. Alternatively, if you know the sensor’s index, you can access it’s data in real time using an if statement like this:

//Channel based list of data for this frame interval
List<List<double>> data = new List<List<double>>();

for (int k = 0; k < e.Data.Count(); k++)
{
    // Loops through each connected sensor
    for (int i = 0; i < e.Data[k].SensorData.Count(); i++)
    {
        // Loops through each channel for a sensor
        for (int j = 0; j < e.Data[k].SensorData[i].ChannelData.Count(); j++)
        {
            data.Add(e.Data[k].SensorData[i].ChannelData[j].Data);
            for (int k2 = 0; k2 <e.Data[k].SensorData[i].ChannelData[j].Data.Count(); k2++){
                if (i == 1)//Heart Rate Sensor Index
                {
                    latestHRData = e.Data[k].SensorData[i].ChannelData[j].Data[k2].ToString();
                }
                else if (i == 0)//Avanti Sensor Index
                {
                    latestEMGData = e.Data[k].SensorData[i].ChannelData[j].Data[k2].ToString();
                }
                Debug.Log(e.Data[k].SensorData[i].ChannelData[j].Data[k2]);
            }
        }
    }

}

//Add frame data to entire collection data buffer
AllCollectionData.Add(data);
text = AllCollectionData.Count.ToString();

You can find out more about the sensors and their channels from the Delsys API User Guide.

Section Review

Now that you have collected data from the Trigno Link in a regular 2D Unity project, you are ready to extend that knowledge to VR! Luckily, not much changes in the implementation going from 2D to VR, so it should be a quick process for the next module, especially because you already learned the basics of VR in module two. Thank you so much for sticking around, your hard work is appreciated!

Module Self-Assessment

--- shuffle_answers: false --- ## True or False? You need to have at least one Avanti sensor connected to use the Trigno Link > Re-read the Getting Started section. 1. [x] True 2. [ ] False > The correct answer is True. ## True or False? The Unity Example ``GameObject`` is not necessary for the Trigno Link Integration. > Re-read the section on Adding GameObjects. 1. [ ] True > The correct answer is False. 2. [x] False ## Which of the following is not one of the lists in ``AllCollectionData`` ? > Re-read the section on Collecting Sensor Data in Non-Real Time. 1. [ ] Component > The correct answer is Time Stamp. 2. [x] Time Stamp 3. [ ] Channel > The correct answer is Time Stamp. 4. [ ] Data > The correct answer is Time Stamp. ## Where can you find logged information about the application while it's running? > Re-read the sections on Running the Application. 1. [ ] The Trigno Base Station > The correct answer is The Unity Console. 2. [ ] The Inspector Window > The correct answer is The Unity Console. 3. [ ] The Trigno Link > The correct answer is The Unity Console. 4. [x] The Unity Console