Skip to content

Commit

Permalink
Merge pull request #53 from urish/staging
Browse files Browse the repository at this point in the history
Staging
  • Loading branch information
CaydenPierce authored Dec 20, 2021
2 parents 650b0ec + c584ce3 commit 4e86457
Show file tree
Hide file tree
Showing 10 changed files with 375 additions and 29 deletions.
24 changes: 23 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

[![Build Status](https://travis-ci.org/urish/muse-js.png?branch=master)](https://travis-ci.org/urish/muse-js)

Muse 2016 EEG Headset JavaScript Library (using Web Bluetooth)
Muse 1, Muse 2, and Muse S EEG Headset JavaScript Library (using Web Bluetooth).

## Running the demo app

Expand Down Expand Up @@ -76,6 +76,28 @@ async function main() {
}
```

## PPG (Photoplethysmography) / Optical Sensor

The Muse 2 and Muse S contain PPG/optical blood sensors, which this library supports. There are three signal streams, ppg1, ppg2, and ppg3. These are ambient, infrared, and red (respectively) on the Muse 2, and (we think, unconfirmed) infrared, green, and unknown (respectively) on the Muse S. To use PPG, ensure you enable it before connecting to a Muse. PPG is not present and thus will not work on Muse 1/1.5, and enabling it may have unexpected consequences.

To enable PPG:

```javascript
async function main() {
let client = new MuseClient();
client.enablePpg = true;
await client.connect();
}
```

To subscribe and receive values from PPG, it's just like subscribing to EEG (see **Usage Example**):

```javascript
client.ppgReadings.subscribe((ppgreading) => {
console.log(ppgreading);
});
```

## Event Markers

For convenience, there is an `eventMarkers` stream included in `MuseClient` that you can use in order to introduce timestamped event markers into your project. Just subscribe to `eventMarkers` and use the `injectMarker` method with the value and optional timestamp of an event to send it through the stream.
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "muse-js",
"version": "3.2.0",
"version": "3.3.0",
"description": "Muse 2016 EEG Headset JavaScript Library",
"main": "dist/muse.js",
"typings": "dist/muse.d.ts",
Expand Down
7 changes: 7 additions & 0 deletions src/lib/muse-interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,13 @@ export interface EEGReading {
samples: number[]; // 12 samples each time
}

export interface PPGReading {
index: number;
ppgChannel: number; // 0 to 2
timestamp: number; // milliseconds since epoch
samples: number[]; // 6 samples each time
}

export interface TelemetryData {
sequenceId: number;
batteryLevel: number;
Expand Down
8 changes: 8 additions & 0 deletions src/lib/muse-parse.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { toArray } from 'rxjs/operators';

import {
decodeUnsigned12BitData,
decodeUnsigned24BitData,
parseAccelerometer,
parseControl,
parseGyroscope,
Expand Down Expand Up @@ -88,6 +89,13 @@ describe('decodeUnsigned12BitData', () => {
});
});

describe('decodeUnsigned24BitData', () => {
it('should correctly decode 24-bit PPG samples received from muse', () => {
const input = new Uint8Array([87, 33, 192, 82, 73, 6, 106, 242, 49, 64, 88, 153, 128, 66, 254, 44, 119, 157]);
expect(decodeUnsigned24BitData(input)).toEqual([5710272, 5392646, 7008817, 4216985, 8405758, 2914205]);
});
});

describe('parseTelemtry', () => {
it('should correctly parse Muse telemetry data', () => {
const input = new DataView(
Expand Down
28 changes: 22 additions & 6 deletions src/lib/muse-parse.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Observable } from 'rxjs';
import { concatMap, filter, map, scan } from 'rxjs/operators';

import { AccelerometerData, EEGReading, GyroscopeData, TelemetryData } from './muse-interfaces';
import { AccelerometerData, EEGReading, GyroscopeData, PPGReading, TelemetryData } from './muse-interfaces';

export function parseControl(controlData: Observable<string>) {
return controlData.pipe(
Expand All @@ -23,26 +23,42 @@ export function decodeUnsigned12BitData(samples: Uint8Array) {
// tslint:disable:no-bitwise
for (let i = 0; i < samples.length; i++) {
if (i % 3 === 0) {
samples12Bit.push(samples[i] << 4 | samples[i + 1] >> 4);
samples12Bit.push((samples[i] << 4) | (samples[i + 1] >> 4));
} else {
samples12Bit.push((samples[i] & 0xf) << 8 | samples[i + 1]);
samples12Bit.push(((samples[i] & 0xf) << 8) | samples[i + 1]);
i++;
}
}
// tslint:enable:no-bitwise
return samples12Bit;
}

export function decodeUnsigned24BitData(samples: Uint8Array) {
const samples24Bit = [];
// tslint:disable:no-bitwise
for (let i = 0; i < samples.length; i = i + 3) {
samples24Bit.push((samples[i] << 16) | (samples[i + 1] << 8) | samples[i + 2]);
}
// tslint:enable:no-bitwise
return samples24Bit;
}

export function decodeEEGSamples(samples: Uint8Array) {
return decodeUnsigned12BitData(samples)
.map((n) => 0.48828125 * (n - 0x800));
return decodeUnsigned12BitData(samples).map((n) => 0.48828125 * (n - 0x800));
}

export function decodePPGSamples(samples: Uint8Array) {
// Decode data packet of one PPG channel.
// Each packet is encoded with a 16bit timestamp followed by 6
// samples with a 24 bit resolution.
return decodeUnsigned24BitData(samples);
}

export function parseTelemetry(data: DataView): TelemetryData {
// tslint:disable:object-literal-sort-keys
return {
sequenceId: data.getUint16(0),
batteryLevel: data.getUint16(2) / 512.,
batteryLevel: data.getUint16(2) / 512,
fuelGaugeVoltage: data.getUint16(4) * 2.2,
// Next 2 bytes are probably ADC millivolt level, not sure
temperature: data.getUint16(8),
Expand Down
80 changes: 80 additions & 0 deletions src/lib/zip-samplesPpg.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import { Observable, of } from 'rxjs';
import { toArray } from 'rxjs/operators';

import { zipSamplesPpg } from './zip-samplesPpg';

// tslint:disable:object-literal-sort-keys

describe('zipSamplesPpg', () => {
it('should zip all ppg channels into one array', async () => {
const input = of(
{
ppgChannel: 0,
index: 100,
timestamp: 1000,
samples: [0.01, 0.02, 0.03, 0.04, 0.05, 0.06],
},
{
ppgChannel: 1,
index: 100,
timestamp: 1000,
samples: [1.01, 1.02, 1.03, 1.04, 1.05, 1.06],
},
{
ppgChannel: 2,
index: 100,
timestamp: 1000,
samples: [2.01, 2.02, 2.03, 2.04, 2.05, 2.06],
},
{
ppgChannel: 0,
index: 101,
timestamp: 1046.875,
samples: [10.01, 10.02, 10.03, 10.04, 10.05, 10.06],
},
{
ppgChannel: 1,
index: 101,
timestamp: 1046.875,
samples: [11.01, 11.02, 11.03, 11.04, 11.05, 11.06],
},
{
ppgChannel: 2,
index: 101,
timestamp: 1046.875,
samples: [12.01, 12.02, 12.03, 12.04, 12.05, 12.06],
},
);
const zipped = zipSamplesPpg(input);
const result = await zipped.pipe(toArray()).toPromise();
expect(result).toEqual([
{ index: 100, timestamp: 1000.0, data: [0.01, 1.01, 2.01] },
{ index: 100, timestamp: 1015.625, data: [0.02, 1.02, 2.02] },
{ index: 100, timestamp: 1031.25, data: [0.03, 1.03, 2.03] },
{ index: 100, timestamp: 1046.875, data: [0.04, 1.04, 2.04] },
{ index: 100, timestamp: 1062.5, data: [0.05, 1.05, 2.05] },
{ index: 100, timestamp: 1078.125, data: [0.06, 1.06, 2.06] },
{ index: 101, timestamp: 1046.875, data: [10.01, 11.01, 12.01] },
{ index: 101, timestamp: 1062.5, data: [10.02, 11.02, 12.02] },
{ index: 101, timestamp: 1078.125, data: [10.03, 11.03, 12.03] },
{ index: 101, timestamp: 1093.75, data: [10.04, 11.04, 12.04] },
{ index: 101, timestamp: 1109.375, data: [10.05, 11.05, 12.05] },
{ index: 101, timestamp: 1125, data: [10.06, 11.06, 12.06] },
]);
});

it('should indicate missing samples with NaN', async () => {
const input = of(
{ index: 50, timestamp: 5000, ppgChannel: 0, samples: [0.01, 0.02, 0.03, 0.04] },
{ index: 50, timestamp: 5000, ppgChannel: 2, samples: [2.01, 2.02, 2.03, 2.04] },
);
const zipped = zipSamplesPpg(input);
const result = await zipped.pipe(toArray()).toPromise();
expect(result).toEqual([
{ index: 50, timestamp: 5000.0, data: [0.01, NaN, 2.01] },
{ index: 50, timestamp: 5015.625, data: [0.02, NaN, 2.02] },
{ index: 50, timestamp: 5031.25, data: [0.03, NaN, 2.03] },
{ index: 50, timestamp: 5046.875, data: [0.04, NaN, 2.04] },
]);
});
});
44 changes: 44 additions & 0 deletions src/lib/zip-samplesPpg.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { from, Observable } from 'rxjs';
import { concat, mergeMap } from 'rxjs/operators';
import { PPG_FREQUENCY } from './../muse';
import { PPGReading } from './muse-interfaces';

export interface PPGSample {
index: number;
timestamp: number; // milliseconds since epoch
data: number[];
}

export function zipSamplesPpg(ppgReadings: Observable<PPGReading>): Observable<PPGSample> {
const buffer: PPGReading[] = [];
let lastTimestamp: number | null = null;
return ppgReadings.pipe(
mergeMap((reading) => {
if (reading.timestamp !== lastTimestamp) {
lastTimestamp = reading.timestamp;
if (buffer.length) {
const result = from([[...buffer]]);
buffer.splice(0, buffer.length, reading);
return result;
}
}
buffer.push(reading);
return from([]);
}),
concat(from([buffer])),
mergeMap((readings: PPGReading[]) => {
const result = readings[0].samples.map((x, index) => {
const data = [NaN, NaN, NaN];
for (const reading of readings) {
data[reading.ppgChannel] = reading.samples[index];
}
return {
data,
index: readings[0].index,
timestamp: readings[0].timestamp + (index * 1000) / PPG_FREQUENCY,
};
});
return from(result);
}),
);
}
Loading

0 comments on commit 4e86457

Please sign in to comment.