Skip to content

Commit

Permalink
fixed bug with muse plotting
Browse files Browse the repository at this point in the history
  • Loading branch information
jdpigeon committed Sep 19, 2020
1 parent 91aa547 commit b934755
Show file tree
Hide file tree
Showing 6 changed files with 64 additions and 25 deletions.
9 changes: 7 additions & 2 deletions app/constants/interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,9 +113,12 @@ export interface Kernel {
// --------------------------------------------------------------------
// Device

// TODO: type this based on what comes out of muse and emotiv
// For unconnected available devices
export interface Device {
[key: string]: any;
// Human readable
name?: string;
// Unique ID
id: string;
}

export interface EEGData {
Expand All @@ -130,9 +133,11 @@ export interface SignalQualityData {
timestamp?: number;
}

// For connected devices
export interface DeviceInfo {
name: string;
samplingRate: number;
channels: string[];
}

export interface PipesEpoch {
Expand Down
3 changes: 2 additions & 1 deletion app/epics/deviceEpics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -129,14 +129,15 @@ const connectEpic: Epic<DeviceActionType, DeviceActionType, RootState> = (
action$.pipe(
filter(isActionOf(DeviceActions.ConnectToDevice)),
pluck('payload'),
map<Device, Promise<any>>((device) =>
map((device) =>
isNil(device.name) ? connectToEmotiv(device) : connectToMuse(device)
),
mergeMap<Promise<any>, ObservableInput<DeviceInfo>>((promise) =>
promise.then((deviceInfo) => deviceInfo)
),
mergeMap<DeviceInfo, ObservableInput<any>>((deviceInfo) => {
if (!isNil(deviceInfo) && !isNil(deviceInfo.samplingRate)) {
console.log(deviceInfo);
return of(
DeviceActions.SetDeviceType(
deviceInfo.name.includes('Muse') ? DEVICES.MUSE : DEVICES.EMOTIV
Expand Down
2 changes: 2 additions & 0 deletions app/main.dev.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,9 +76,11 @@ const createWindow = async () => {
process.env.ERB_SECURE !== 'true'
? {
nodeIntegration: true,
webviewTag: true,
}
: {
preload: path.join(__dirname, 'dist/renderer.prod.js'),
webviewTag: true,
},
});

Expand Down
2 changes: 1 addition & 1 deletion app/reducers/deviceReducer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ export interface DeviceStateType {
}

const initialState: DeviceStateType = {
availableDevices: [{}],
availableDevices: [],
connectedDevice: { name: 'disconnected', samplingRate: 0 },
connectionStatus: CONNECTION_STATUS.NOT_YET_CONNECTED,
deviceAvailability: DEVICE_AVAILABILITY.NONE,
Expand Down
32 changes: 24 additions & 8 deletions app/utils/eeg/emotiv.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,19 @@ import { parseEmotivSignalQuality } from './pipes';
import { CLIENT_ID, CLIENT_SECRET, LICENSE_ID } from '../../../keys';
import { EMOTIV_CHANNELS, PLOTTING_INTERVAL } from '../../constants/constants';
import Cortex from './cortex';
import { Device, DeviceInfo } from '../../constants/interfaces';

interface EmotivHeadset {
id: string;
status: 'discovered' | 'connecting' | 'connected';
connectedBy: 'dongle' | 'bluetooth' | 'usb cabe' | 'extender';
dongle: string;
firmware: string;
motionSensors: string[];
sensors: string[];
settings: Record<string, string | number>;
customName?: string;
}

// Creates the Cortex object from SDK
const verbose = process.env.LOG_LEVEL || 1;
Expand All @@ -26,11 +39,16 @@ let session;

// Gets a list of available Emotiv devices
export const getEmotiv = async () => {
const devices = await client.queryHeadsets();
return devices;
const devices: EmotivHeadset[] = await client.queryHeadsets();
return devices.map<Device>((headset) => ({
id: headset.id,
name: headset.customName,
}));
};

export const connectToEmotiv = async (device) => {
export const connectToEmotiv = async (
device: Device
): Promise<DeviceInfo | null> => {
await client.ready;

// Authenticate
Expand All @@ -43,14 +61,14 @@ export const connectToEmotiv = async (device) => {
});
} catch (err) {
toast.error(`Authentication failed. ${err.message}`);
return;
return Promise.reject(err);
}
// Connect
try {
await client.controlDevice({ command: 'connect', headset: device.id });
} catch (err) {
toast.error(`Emotiv connection failed. ${err.message}`);
return;
return Promise.reject(err);
}
// Create Session
try {
Expand All @@ -67,11 +85,11 @@ export const connectToEmotiv = async (device) => {
};
} catch (err) {
toast.error(`Session creation failed. ${err.message} `);
return Promise.reject(err);
}
};

export const disconnectFromEmotiv = async () => {
console.log('disconnecting form emotiv');
const sessionStatus = await client.updateSession({
session: session.id,
status: 'close',
Expand All @@ -93,13 +111,11 @@ export const createRawEmotivObservable = async () => {
toast.error(`EEG connection failed. ${err.message}`);
}

// @ts-ignore
return fromEvent(client, 'eeg').pipe(map(createEEGSample));
};

// Creates an observable that will epoch, filter, and add signal quality to EEG stream
export const createEmotivSignalQualityObservable = (rawObservable) => {
// @ts-ignore
const signalQualityObservable = fromEvent(client, 'dev');
const samplingRate = 128;
const channels = EMOTIV_CHANNELS;
Expand Down
41 changes: 28 additions & 13 deletions app/utils/eeg/muse.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,29 @@
import 'hazardous';
import { withLatestFrom, share, startWith, filter } from 'rxjs/operators';
import {
withLatestFrom,
share,
startWith,
filter,
tap,
map,
} from 'rxjs/operators';
import {
addInfo,
epoch,
bandpassFilter,
addSignalQuality,
} from '@neurosity/pipes';
import { release } from 'os';
import { MUSE_SERVICE, MuseClient, zipSamples } from 'muse-js';
import { from } from 'rxjs';
import { MUSE_SERVICE, MuseClient, zipSamples, EEGSample } from 'muse-js';
import { from, Observable } from 'rxjs';
import { isNaN } from 'lodash';
import { parseMuseSignalQuality } from './pipes';
import {
MUSE_SAMPLING_RATE,
MUSE_CHANNELS,
PLOTTING_INTERVAL,
} from '../../constants/constants';
import { Device, EEGData } from '../../constants/interfaces';

const INTER_SAMPLE_INTERVAL = -(1 / 256) * 1000;

Expand All @@ -27,22 +36,23 @@ if (

const client = new MuseClient();
client.enableAux = false;
console.log('this', this, 'ble', navigator.bluetooth);

// Gets an available Muse device
// TODO: test whether this will ever return multiple devices if available
// TODO: is being able to request only one Muse at a time a problem in a classroom scenario?
export const getMuse = async () => {
console.log('getting muse');
const device = await navigator.bluetooth.requestDevice({
const deviceInstance = await navigator.bluetooth.requestDevice({
filters: [{ services: [MUSE_SERVICE] }],
});
console.log('received ', device);
return [device];
return [{ id: deviceInstance.id, name: deviceInstance.name }];
};

// Attempts to connect to a muse device. If successful, returns a device info object
export const connectToMuse = async (device: BluetoothDevice) => {
await client.connect(device.gatt);
export const connectToMuse = async (device: Device) => {
const deviceInstance = await navigator.bluetooth.requestDevice({
filters: [{ services: [MUSE_SERVICE], name: device.name }],
});
const gatt = await deviceInstance.gatt?.connect();
await client.connect(gatt);
return {
name: client.deviceName,
samplingRate: MUSE_SAMPLING_RATE,
Expand All @@ -58,15 +68,20 @@ export const createRawMuseObservable = async () => {
const eegStream = await client.eegReadings;
const markers = await client.eventMarkers.pipe(startWith({ timestamp: 0 }));
return from(zipSamples(eegStream)).pipe(
filter((sample) => !sample.data.includes(NaN)),
// Remove nans if present (muse 2)
map<EEGSample, EEGSample>((sample) => ({
...sample,
data: sample.data.filter((val) => !isNaN(val)),
})),
filter((sample) => sample.data.length >= 4),
withLatestFrom(markers, synchronizeTimestamp),
share()
);
};

// Creates an observable that will epoch, filter, and add signal quality to EEG stream
export const createMuseSignalQualityObservable = (
rawObservable,
rawObservable: Observable<EEGData>,
deviceInfo
) => {
const { samplingRate, channels: channelNames } = deviceInfo;
Expand Down

0 comments on commit b934755

Please sign in to comment.