I recently purchased an EEG headset (NeuroSky MindWave Mobile). It is simply a device worn on one's head to capture brain wave data. The device streams this data in real time via Bluetooth which can then be read/analysed by a software program.
NeuroSky provides an easy-to-use set of APIs which I have used to write a basic class to read the streaming headset data. An abbreviated version of it is as follows:
using System;
using NeuroSky.ThinkGear;
namespace MindWave_Reader
{
class ReadEEG
{
public double AlphaValue { get; set; }
private Connector connector;
public ReadEEG()
{
// Initialize a new Connector and add event handlers
connector = new Connector();
connector.DeviceConnected += new EventHandler(OnDeviceConnected);
// Scan for headset on COM7 port
connector.ConnectScan("COM7");
}
// Called when a device is connected
public void OnDeviceConnected(object sender, EventArgs e) {
Connector.DeviceEventArgs de = (Connector.DeviceEventArgs)e;
Console.WriteLine("Device found on: " + de.Device.PortName);
de.Device.DataReceived += new EventHandler(OnDataReceived);
}
// Called when data is received from a device
public void OnDataReceived(object sender, EventArgs e) {
Device.DataEventArgs de = (Device.DataEventArgs)e;
DataRow[] tempDataRowArray = de.DataRowArray;
TGParser tgParser = new TGParser();
tgParser.Read(de.DataRowArray);
/* Loops through the newly parsed data of the connected headset */
for (int i = 0; i < tgParser.ParsedData.Length; i++) {
if(tgParser.ParsedData[i].ContainsKey("EegPowerAlpha")) {
AlphaValue = tgParser.ParsedData[i]["EegPowerAlpha"];
Console.WriteLine("Alpha: " + AlphaValue);
}
}
}
}
}
The above code first attempts to connect to the EEG headset. Once it is connected, each time data is received from the headset, OnDataReceived()
is called. This method prints relevant streaming headset data values (alpha waves) to the console.
I now want to display these alpha wave values live on a Cartesian chart and I discovered LiveCharts which seems like a neat graphing library. This WinForms example is along the lines of what I am trying to achieve.
The alpha wave values should be plotted on the Y axis against time on the X axis. However, instead of updating the chart every 500ms like in the example, I would like it to update only when data is received from the headset (in other words, when the AlphaValue
variable is updated by OnDataReceived()
in the ReadEEG class).
I would like to know how I can have my WinForm interact with the ReadEEG class to update its Cartesian chart in this manner. I am a bit of a novice so any help will be greatly appreciated.
I really hope I have made myself clear and have tried to keep the explanations as simple as possible. If you have any questions, do not hesitate to ask. Thanks in advance for all your help!
Here's one way you could do it. I added a new event to the ReadEEG
class. This new event gets raised whenever there is a new AlphaValue
. In the main form it subscribes to this event and adds the new values to the ChartValues
collection, which is what is shown on the LiveChart.
using System;
using System.Data;
using NeuroSky.ThinkGear;
namespace MindWave_Reader
{
class ReadEEG
{
public double AlphaValue { get; set; }
private Connector connector;
public ReadEEG()
{
// Initialize a new Connector and add event handlers
connector = new Connector();
connector.DeviceConnected += new EventHandler(OnDeviceConnected);
// Scan for headset on COM7 port
connector.ConnectScan("COM7");
}
// Called when a device is connected
public void OnDeviceConnected(object sender, EventArgs e)
{
Connector.DeviceEventArgs de = (Connector.DeviceEventArgs)e;
Console.WriteLine("Device found on: " + de.Device.PortName);
de.Device.DataReceived += new EventHandler(OnDataReceived);
}
// Called when data is received from a device
public void OnDataReceived(object sender, EventArgs e)
{
Device.DataEventArgs de = (Device.DataEventArgs)e;
DataRow[] tempDataRowArray = de.DataRowArray;
TGParser tgParser = new TGParser();
tgParser.Read(de.DataRowArray);
/* Loops through the newly parsed data of the connected headset */
for (int i = 0; i < tgParser.ParsedData.Length; i++)
{
if (tgParser.ParsedData[i].ContainsKey("EegPowerAlpha"))
{
AlphaValue = tgParser.ParsedData[i]["EegPowerAlpha"];
Console.WriteLine("Alpha: " + AlphaValue);
// Raise the AlphaReceived event with the new reading.
OnAlphaReceived(new AlphaReceivedEventArgs() { Alpha = AlphaValue });
}
}
}
/// <summary>
/// The arguments for the <see cref="AlphaReceived"/> event.
/// </summary>
public class AlphaReceivedEventArgs : EventArgs
{
/// <summary>
/// The alpha value that was just received.
/// </summary>
public double Alpha { get; set; }
}
/// <summary>
/// Raises the <see cref="AlphaReceived"/> event if there is a subscriber.
/// </summary>
/// <param name="e">Contains the new alpha value.</param>
protected virtual void OnAlphaReceived(AlphaReceivedEventArgs e)
{
AlphaReceived?.Invoke(this, e);
}
/// <summary>
/// Event that gets raised whenever a new AlphaValue is received from the
/// device.
/// </summary>
public event EventHandler AlphaReceived;
}
}
On Form1
I added a LiveCharts.WinForms.CartesianChart
with the designer. The code behind is as follows:
using LiveCharts;
using LiveCharts.Configurations;
using LiveCharts.Wpf;
using System;
using System.Windows.Forms;
using static MindWave_Reader.ReadEEG;
namespace MindWave_Reader
{
public partial class Form1 : Form
{
/// <summary>
/// Simple class to hold an alpha value and the time it was received. Used
/// for charting.
/// </summary>
public class EEGPowerAlphaValue
{
public DateTime Time { get; }
public double AlphaValue { get; }
public EEGPowerAlphaValue(DateTime time, double alpha)
{
Time = time;
AlphaValue = alpha;
}
}
private ReadEEG _readEEG;
/// <summary>
/// Contains the alpha values we're showing on the chart.
/// </summary>
public ChartValues<EEGPowerAlphaValue> ChartValues { get; set; }
public Form1()
{
InitializeComponent();
// Create the mapper.
var mapper = Mappers.Xy<EEGPowerAlphaValue>()
.X(model => model.Time.Ticks) // use Time.Ticks as X
.Y(model => model.AlphaValue); // use the AlphaValue property as Y
// Lets save the mapper globally.
Charting.For<EEGPowerAlphaValue>(mapper);
// The ChartValues property will store our values array.
ChartValues = new ChartValues<EEGPowerAlphaValue>();
cartesianChart1.Series = new SeriesCollection
{
new LineSeries
{
Values = ChartValues,
PointGeometrySize = 18,
StrokeThickness = 4
}
};
cartesianChart1.AxisX.Add(new Axis
{
DisableAnimations = true,
LabelFormatter = value => new DateTime((long)value).ToString("mm:ss"),
Separator = new Separator
{
Step = TimeSpan.FromSeconds(1).Ticks
}
});
SetAxisLimits(DateTime.Now);
}
private void StartButton_Click(object sender, EventArgs e)
{
_readEEG = new ReadEEG();
_readEEG.AlphaReceived += _readEEG_AlphaReceived;
}
/// <summary>
/// Called when a new alpha value is received from the device. Updates the
/// chart with the new value.
/// </summary>
/// <param name="sender">The <see cref="ReadEEG"/> object that raised this
/// event.</param>
/// <param name="e">The <see cref="AlphaReceivedEventArgs"/> that contains
/// the new alpha value.</param>
private void _readEEG_AlphaReceived(object sender, EventArgs e)
{
AlphaReceivedEventArgs alphaReceived = (AlphaReceivedEventArgs)e;
// Add the new alpha reading to our ChartValues.
ChartValues.Add(
new EEGPowerAlphaValue(
DateTime.Now,
alphaReceived.Alpha));
// Update the chart limits.
SetAxisLimits(DateTime.Now);
// Lets only use the last 30 values. You may want to adjust this.
if (ChartValues.Count > 30)
{
ChartValues.RemoveAt(0);
}
}
private void SetAxisLimits(DateTime now)
{
if (cartesianChart1.InvokeRequired)
{
cartesianChart1.Invoke(new Action(() => SetAxisLimits(now)));
}
else
{
// Lets force the axis to be 100ms ahead. You may want to adjust this.
cartesianChart1.AxisX[0].MaxValue =
now.Ticks + TimeSpan.FromSeconds(1).Ticks;
// We only care about the last 8 seconds. You may want to adjust this.
cartesianChart1.AxisX[0].MinValue =
now.Ticks - TimeSpan.FromSeconds(8).Ticks;
}
}
}
}