Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .github/workflows/run_unix.yml
Original file line number Diff line number Diff line change
Expand Up @@ -261,6 +261,9 @@ jobs:
# start testing
- name: Run unit tests
run: $GITHUB_WORKSPACE/build/tests/brainflow_tests
- name: NeuroSky serial emulator
if: runner.os == 'Linux'
run: python3 $GITHUB_WORKSPACE/emulator/brainflow_emulator/neurosky_serial_test.py $GITHUB_WORKSPACE/installed/lib/libBoardController.so
- name: Synthetic C# Test MacOS
if: runner.os == 'macOS'
run: |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,8 @@ public enum BoardIds
IRONBCI_32_BOARD = 65,
NEUROPAWN_KNIGHT_BOARD_IMU = 66,
MUSE_S_ATHENA_BOARD = 67,
SHIMMER3_BOARD = 68
SHIMMER3_BOARD = 68,
NEUROSKY_BOARD = 69
};


Expand Down
42 changes: 42 additions & 0 deletions docs/SupportedBoards.rst
Original file line number Diff line number Diff line change
Expand Up @@ -1581,3 +1581,45 @@ Common sensor bits:
- :code:`0x000008`, EXG2 24-bit
- :code:`0x000004`, GSR, exposed as EDA
- :code:`0x002000`, battery

NeuroSky MindWave / TGAM
-----------------------

``BoardIds.NEUROSKY_BOARD`` (69) reads the ThinkGear binary stream from a serial
port (USB or an operating-system-paired Bluetooth Classic RFCOMM port). Set
``serial_port`` to that port. The default baud rate is 57600; ``other_info`` may
be ``"57600"`` or ``"115200"``. ``timeout`` is the first-sample timeout in seconds
(default 5). The device must already emit 16-bit raw EEG at 512 Hz.
This driver does not configure Bluetooth pairing, use ThinkGear Connector, or
support BLE, TGEM's 128 Hz stream, 8-bit raw mode, or CardioChip.

The EEG row is in microvolts using NeuroSky's documented TGAT/TGAM gain.
``other_channels`` contains poor-signal quality, attention, meditation, followed
by the eight ASIC band powers (delta, theta, low/high alpha, low/high beta,
low/mid gamma). These slower values are held until updated and are NaN until
first received. Band powers are device-relative values, not microvolts squared.
The package counter is generated by the host; timestamps are host receive times
(within-packet samples spaced at 512 Hz), not device-clock synchronization.

.. code-block:: python

from brainflow.board_shim import BoardShim, BrainFlowInputParams, BoardIds

params = BrainFlowInputParams()
params.serial_port = "/dev/rfcomm0" # or a paired COM port on Windows
board = BoardShim(BoardIds.NEUROSKY_BOARD, params)
board.prepare_session()
try:
board.start_stream()
# Consume data with get_board_data() while recording.
board.stop_stream()
finally:
board.release_session()

Validation covers published protocol fixtures, malformed-packet recovery and a
pseudo-terminal stream. Physical MindWave/TGAM and Bluetooth serial operation
still require validation by a device owner.

Protocol: https://developer.neurosky.com/docs/doku.php?id=thinkgear_communications_protocol

Voltage conversion: https://support.neurosky.com/kb/science/how-to-convert-raw-values-to-voltage
71 changes: 71 additions & 0 deletions emulator/brainflow_emulator/neurosky_serial_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
"""Exercise the native driver with a POSIX pseudo-terminal, no device required.

Usage: python3 neurosky_serial_test.py /path/to/libBoardController.so
"""
import ctypes
import json
import os
import pty
import sys
import threading
import time


def main(library):
lib = ctypes.CDLL(library)
lib.prepare_session.argtypes = [ctypes.c_int, ctypes.c_char_p]
lib.start_stream.argtypes = [ctypes.c_int, ctypes.c_char_p, ctypes.c_int, ctypes.c_char_p]
lib.stop_stream.argtypes = lib.prepare_session.argtypes
lib.release_session.argtypes = lib.prepare_session.argtypes
lib.get_current_board_data.argtypes = [ctypes.c_int, ctypes.c_int,
ctypes.POINTER(ctypes.c_double), ctypes.POINTER(ctypes.c_int),
ctypes.c_int, ctypes.c_char_p]
master, slave = pty.openpty()
params = json.dumps(dict(serial_port=os.ttyname(slave), mac_address='',
ip_address='', ip_address_aux='', ip_address_anc='', ip_port=0,
ip_port_aux=0, ip_port_anc=0, ip_protocol=0, other_info='', timeout=1,
serial_number='', file='', file_aux='', file_anc='', master_board=-100)).encode()
stop = threading.Event()
active = threading.Event()

def device():
# A raw value of 42; the checksum is independently specified.
packet = bytes([0xaa, 0xaa, 4, 0x80, 2, 0, 42, 0x53])
while not stop.wait(0.002):
if active.is_set():
os.write(master, packet[:3])
os.write(master, packet[3:])

worker = threading.Thread(target=device)
worker.start()
try:
assert lib.prepare_session(69, params) == 0
# No raw samples: start must fail, and its worker must be joined.
assert lib.start_stream(1024, b'', 69, params) == 18
active.set()
for _ in range(2):
assert lib.start_stream(1024, b'', 69, params) == 0
assert lib.start_stream(1024, b'', 69, params) == 8
time.sleep(0.05)
data = (ctypes.c_double * (15 * 1024))()
count = ctypes.c_int()
assert lib.get_current_board_data(1024, 0, data, ctypes.byref(count), 69, params) == 0
assert count.value > 1
assert all(data[count.value + i] == 42 * 0.2197265625 for i in range(count.value))
assert lib.stop_stream(69, params) == 0
assert lib.stop_stream(69, params) == 11
assert lib.release_session(69, params) == 0
# Opening the port again after release must work.
assert lib.prepare_session(69, params) == 0
assert lib.start_stream(1024, b'', 69, params) == 0
finally:
lib.release_session(69, params)
stop.set()
worker.join()
os.close(master)
os.close(slave)
print('NeuroSky serial lifecycle passed')


if __name__ == '__main__':
main(sys.argv[1])
3 changes: 2 additions & 1 deletion java_package/brainflow/src/main/java/brainflow/BoardIds.java
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,8 @@ public enum BoardIds
IRONBCI_32_BOARD(65),
NEUROPAWN_KNIGHT_BOARD_IMU(66),
MUSE_S_ATHENA_BOARD(67),
SHIMMER3_BOARD(68);
SHIMMER3_BOARD(68),
NEUROSKY_BOARD(69);

private final int board_id;
private static final Map<Integer, BoardIds> bi_map = new HashMap<Integer, BoardIds> ();
Expand Down
1 change: 1 addition & 0 deletions julia_package/brainflow/src/board_shim.jl
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ export BrainFlowInputParams
NEUROPAWN_KNIGHT_BOARD_IMU = 66
MUSE_S_ATHENA_BOARD = 67
SHIMMER3_BOARD = 68
NEUROSKY_BOARD = 69

end

Expand Down
1 change: 1 addition & 0 deletions matlab_package/brainflow/BoardIds.m
Original file line number Diff line number Diff line change
Expand Up @@ -68,5 +68,6 @@
NEUROPAWN_KNIGHT_BOARD_IMU(66)
MUSE_S_ATHENA_BOARD(67)
SHIMMER3_BOARD(68)
NEUROSKY_BOARD(69)
end
end
3 changes: 2 additions & 1 deletion nodejs_package/brainflow/brainflow.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,8 @@ export enum BoardIds {
IRONBCI_32_BOARD = 65,
NEUROPAWN_KNIGHT_BOARD_IMU = 66,
MUSE_S_ATHENA_BOARD = 67,
SHIMMER3_BOARD = 68
SHIMMER3_BOARD = 68,
NEUROSKY_BOARD = 69
}

export enum IpProtocolTypes {
Expand Down
1 change: 1 addition & 0 deletions python_package/brainflow/board_shim.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ class BoardIds(enum.IntEnum):
NEUROPAWN_KNIGHT_BOARD_IMU = 66 #:
MUSE_S_ATHENA_BOARD = 67 #:
SHIMMER3_BOARD = 68 #:
NEUROSKY_BOARD = 69 #:


class IpProtocolTypes(enum.IntEnum):
Expand Down
1 change: 1 addition & 0 deletions rust_package/brainflow/src/ffi/constants.rs
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,7 @@ pub enum BoardIds {
NeuropawnKnightBoardImu = 66,
MuseSAthenaBoard = 67,
Shimmer3Board = 68,
NeuroskyBoard = 69,
}
#[repr(i32)]
#[derive(FromPrimitive, ToPrimitive, Debug, Copy, Clone, Hash, PartialEq, Eq)]
Expand Down
4 changes: 4 additions & 0 deletions src/board_controller/board_controller.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@
#include "muse.h"
#include "muse_athena.h"
#include "muse_bled.h"
#include "neurosky.h"
#include "notion_osc.h"
#include "ntl_wifi.h"
#include "pieeg_board.h"
Expand Down Expand Up @@ -315,6 +316,9 @@ int prepare_session (int board_id, const char *json_brainflow_input_params)
board = std::shared_ptr<Board> (
new KnightIMU ((int)BoardIds::NEUROPAWN_KNIGHT_BOARD_IMU, params));
break;
case BoardIds::NEUROSKY_BOARD:
board = std::shared_ptr<Board> (new NeuroSky (params));
break;
case BoardIds::SHIMMER3_BOARD:
board = std::shared_ptr<Board> (new Shimmer3 (params));
break;
Expand Down
13 changes: 13 additions & 0 deletions src/board_controller/brainflow_boards.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1217,6 +1217,19 @@ BrainFlowBoards::BrainFlowBoards()
{"battery_channel", 16},
{"other_channels", {17}}
};
brainflow_boards_json["boards"]["69"]["default"] =
{
{"name", "NeuroSky"},
{"sampling_rate", 512},
{"package_num_channel", 0},
{"eeg_channels", {1}},
{"eeg_names", "Fp1"},
{"other_channels", {2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12}},
{"timestamp_channel", 13},
{"marker_channel", 14},
{"num_rows", 15}
};

}

BrainFlowBoards boards_struct;
2 changes: 2 additions & 0 deletions src/board_controller/build.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ SET (BOARD_CONTROLLER_SRC
${CMAKE_CURRENT_SOURCE_DIR}/src/board_controller/neuropawn/knight_base.cpp
${CMAKE_CURRENT_SOURCE_DIR}/src/board_controller/neuropawn/knight_imu.cpp
${CMAKE_CURRENT_SOURCE_DIR}/src/board_controller/biolistener/biolistener.cpp
${CMAKE_CURRENT_SOURCE_DIR}/src/board_controller/neurosky/neurosky.cpp
${CMAKE_CURRENT_SOURCE_DIR}/src/board_controller/shimmer/shimmer3.cpp
)

Expand Down Expand Up @@ -157,6 +158,7 @@ target_include_directories (
${CMAKE_CURRENT_SOURCE_DIR}/src/board_controller/synchroni/inc
${CMAKE_CURRENT_SOURCE_DIR}/src/board_controller/neuropawn/inc
${CMAKE_CURRENT_SOURCE_DIR}/src/board_controller/biolistener/inc
${CMAKE_CURRENT_SOURCE_DIR}/src/board_controller/neurosky/inc
${CMAKE_CURRENT_SOURCE_DIR}/src/board_controller/shimmer/inc
)

Expand Down
30 changes: 30 additions & 0 deletions src/board_controller/neurosky/inc/neurosky.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
#pragma once

#include <atomic>
#include <condition_variable>
#include <memory>
#include <mutex>
#include <thread>

#include "board.h"
#include "serial.h"

class NeuroSky : public Board
{
std::unique_ptr<Serial> serial;
std::atomic<bool> keep_alive;
std::thread worker;
std::mutex mutex;
std::condition_variable received;
bool ready;
void read_thread ();

public:
explicit NeuroSky (struct BrainFlowInputParams params);
~NeuroSky () override;
int prepare_session () override;
int start_stream (int buffer_size, const char *streamer_params) override;
int stop_stream () override;
int release_session () override;
int config_board (std::string config, std::string &response) override;
};
125 changes: 125 additions & 0 deletions src/board_controller/neurosky/inc/thinkgear_parser.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
#pragma once

#include <stddef.h>

#include <array>
#include <cstdint>
#include <limits>
#include <vector>

// ThinkGear binary protocol, TGAT/TGAM 16-bit EEG mode (512 Hz).
// https://developer.neurosky.com/docs/doku.php?id=thinkgear_communications_protocol
namespace thinkgear
{
struct Sample
{
double eeg;
std::array<double, 11> auxiliary; // quality, attention, meditation, eight ASIC powers
};

class Parser
{
std::vector<uint8_t> buffer;
std::array<double, 11> auxiliary;

bool payload (const uint8_t *data, size_t size, std::vector<Sample> &samples)
{
auto next = auxiliary;
std::vector<Sample> pending;
size_t offset = 0;
while (offset < size)
{
bool extended = false;
while ((offset < size) && (data[offset] == 0x55))
{
extended = true;
++offset;
}
if (offset == size)
return false;
uint8_t code = data[offset++];
size_t length = 1;
if (code >= 0x80)
{
if (offset == size)
return false;
length = data[offset++];
}
if (length > size - offset)
return false;
if (!extended)
{
if ((code == 0x80) && (length != 2))
return false;
if ((code == 0x83) && (length != 24))
return false;
if (code == 0x80)
{
int raw = (int (data[offset]) << 8) | data[offset + 1];
if (raw >= 32768)
raw -= 65536;
// NeuroSky's TGAT conversion: raw * (1.8 / 4096) / 2000 V.
pending.push_back ({raw * (1.8 / 4096.0 / 2000.0 * 1e6), next});
}
else if (code == 0x02)
next[0] = data[offset];
else if (code == 0x04)
next[1] = data[offset];
else if (code == 0x05)
next[2] = data[offset];
else if (code == 0x83)
{
for (size_t i = 0; i < 8; ++i)
next[i + 3] = (uint32_t (data[offset + i * 3]) << 16) |
(uint32_t (data[offset + i * 3 + 1]) << 8) |
data[offset + i * 3 + 2];
}
}
offset += length;
}
auxiliary = next;
samples.insert (samples.end (), pending.begin (), pending.end ());
return true;
}

public:
Parser ()
{
reset ();
}

void reset ()
{
buffer.clear ();
auxiliary.fill (std::numeric_limits<double>::quiet_NaN ());
}

std::vector<Sample> feed (uint8_t byte)
{
std::vector<Sample> samples;
buffer.push_back (byte);
while (buffer.size () >= 3)
{
if ((buffer[0] != 0xaa) || (buffer[1] != 0xaa) || (buffer[2] > 169))
{
buffer.erase (buffer.begin ());
continue;
}
size_t length = buffer[2];
if (buffer.size () < length + 4)
break;
uint8_t sum = 0;
for (size_t i = 3; i < length + 4; ++i)
sum = uint8_t (sum + buffer[i]);
if (sum != 0xff)
{
buffer.erase (buffer.begin ());
continue;
}
payload (buffer.data () + 3, length, samples);
buffer.erase (buffer.begin (), buffer.begin () + length + 4);
}
return samples;
}
};
}
Loading
Loading