BCI Core
- class gpype.backend.sources.bci_core.BCICore[source]
Bases:
AmplifierSourceBCI Core amplifier for wireless EEG acquisition.
Interface to the g.tec BCI Core family of wireless EEG amplifiers over BLE. By default it acquires every EEG channel the amplifier reports at 250 Hz;
channel_count,channelsandsampling_ratenarrow, reorder or redeclare that.The node is named for the family, not for one member of it. It was
BCICore8, which read as the name of the 8-channel product – but a BCI Core-4 speaks the same protocol, reports itself the same way and is driven by exactly this code, so the 8 named a device rather than a node and made the node look inapplicable to half the devices it already supported.BCICore8still resolves, with a DeprecationWarning; seebci_core8.py.- SCANNING_TIMEOUT_S = 6
Bluetooth scanning timeout in seconds
- SAMPLING_RATE = 250
Default sampling rate in Hz, for an amplifier not told otherwise. Not a hardware limit: the BLE amplifier configuration is external to this node (see
sampling_rateon __init__), and this is only what a caller gets by not asking.
- MAX_NUM_CHANNELS = 8
Maximum number of supported EEG channels
- QUEUE_CAPACITY_S = 1.0
Capacity of the hand-off queue between the BLE callback thread and the worker that drives the pipeline, in seconds of signal.
This is not a jitter buffer and it is not held at any fill level: the worker drains it as data arrives, so it normally sits near empty and adds no latency. It only needs to absorb scheduling slack between the two threads, and one second of room makes overflow a symptom of something genuinely wrong rather than a tuning question.
In seconds, not samples. It was 250 samples: one second at 250 Hz, a quarter of one at 1000, where one gCore payload is 40 samples. Converted at start(), after the connect, since a rate left undeclared is only known once the device has answered.
- CONNECT_ATTEMPTS = 3
- CONNECT_RETRY_DELAY_S = 1.0
Pause between connection attempts in seconds
- INSTANT_FAILURE_S = 0.25
An open that fails faster than this never paged the amplifier, so the host’s adapter is the suspect. Timed per attempt, and without the retry’s pause and rescan, either of which would hide it.
- LOSS_LOG_INTERVAL_S = 10.0
Minimum seconds between repeated packet-loss warnings
- LOSS_WARN_S = 0.1
Signal lost in one dropout, in seconds, above which it is reported as a warning rather than a note. 100 ms is well clear of the single-payload dropouts an ordinary link produces, and short enough that anything above it is worth an operator’s attention.
In seconds, because a payload is a span of time, not a fixed count: a legacy payload is four samples, 16 ms at 250 Hz; a gCore event packet (firmware 0.2.1.1 on) covers one 10 ms connection event in every mode, and its sized predecessor 40 ms. It was 25 samples, which is this at 250 Hz and made every single lost payload a warning at 1000.
- LOSS_WARN_BURSTS = 5
Dropouts within LOSS_WARN_WINDOW_S above which the link is reported as degrading, however small each one is on its own. Repetition is the signal there: one short dropout says nothing, five of them in a minute say the radio is struggling.
- LOSS_WARN_WINDOW_S = 60.0
Window over which repeated dropouts are counted, in seconds.
- RECONNECT_COUNTER_MAX = 16
Highest device counter value that can still be a restart. The driver resumes numbering at 1 after a reconnect, so only the first few counts of a fresh connection look like this; a mid-stream counter never does.
- RECONNECT_MIN_GAP_S = 0.5
Shortest arrival gap that can be an outage, in seconds. A reconnect takes far longer than this; a notification overtaking its predecessor takes essentially no time at all.
- STALL_TIMEOUT_S = 2.0
How long a stream may deliver nothing before it is reported, in seconds. Twice the driver’s one-second back-fill horizon, so an ordinary dropout the driver repairs itself never trips this.
- FIRST_SAMPLE_TIMEOUT_S = 5.0
- STALL_ERROR_S = 60.0
- LINK_CONDITIONS = (('interval_off', 'The BLE connection interval is not the 10 ms the stream is paced for, so samples arrive later and in larger bursts.', 'The BLE connection interval is back at 10 ms.'), ('phy_off', 'The BLE link is not on the 2M PHY and carries half the data it could; at the higher rates the amplifier may fall behind.', 'The BLE link is on the 2M PHY again.'), ('backlog', 'The amplifier is 100 ms or more behind with sending its samples; they arrive late, not lost.', 'The amplifier has caught up with sending its samples.'), ('dropped', 'The amplifier reports dropped samples. A gap of up to a second is filled and marked invalid; a longer one is missing from the timeline, which runs that much short.', 'The amplifier reports no further dropped samples.'))
Link conditions a gCore’s event packets report (gtec-ble 2.9.3,
Amplifier.stream_status): the field, the warning when it turns on, the note when it clears. A warning goes out at most once per LOSS_LOG_INTERVAL_S for each, so a condition hovering at its threshold cannot flood the log, and a note only after a warning.
- NUM_TIMELINE_CHANNELS = 3
master index, sample validity, and the host time the sample arrived at, in that order. Appended to the EEG channels in band, and stripped again by the Sync node at the end of the chain.
The arrival time is in band because it has to survive a Link. Under distributed residency the Sync that builds the master-timeline relation runs in the server process, where reading a clock measures when the frame finished crossing the network. The instant that matters is this one, known only in the acquisition callback – and not even at the moment the frame is emitted, because frames wait in _frame_buffer first.
- Type:
Channels carrying the timeline
- TIMELINE_LABELS = ('master_index', 'valid', 'host_time')
Labels of the timeline channels
- DEFAULT_MONTAGE_8 = ('Fz', 'C3', 'Cz', 'C4', 'Pz', 'PO7', 'POz', 'PO8')
The electrode montage the 8-channel device ships with, in acquisition order.
Applied only when eight EEG channels are acquired and the author named none, because it describes that headset. A four-channel device wears a different cap, and guessing its electrodes from this list – by truncation or otherwise – would put a confident wrong name on every column.
This does change what an unlabelled recording looks like: the same session that produced Ch01..Ch08 now produces Fz..PO8. That is the point. A positional name is not neutral, it is merely silent about something the device does know, and the electrode a column came from is the one fact a recording cannot recover afterwards. An author who wants different names still passes a montage, and one who wants none cannot – which is the right way round for a device whose electrode positions are fixed.
- class Configuration[source]
Bases:
ConfigurationConfiguration class for BCI Core specific parameters.
- class Keys[source]
Bases:
KeysConfiguration keys for BCI Core settings.
- EEG_CHANNEL_COUNT = 'eeg_channel_count'
Number of EEG channels, as requested. Recorded separately because the port’s channel_count is the total including the timeline channels: reading the EEG count back out of that total would add the timeline channels a second time on every round trip.
- class OptionalKeys[source]
Bases:
OptionalKeysOptional configuration keys.
A key listed in Keys must be present and non-None, and a montage is genuinely optional, so it belongs here.
- MONTAGE_LABELS = 'montage_labels'
Electrode labels, as a plain list. A Montage object is not JSON-serialisable, so the labels travel and the object is rebuilt from them.
- SERIAL = 'serial'
Serial number of the target device. Recorded because it selects which physical amplifier is opened: a pipeline rebuilt without it would silently connect to whichever device happened to answer first.
- __init__(serial=None, channel_count=None, frame_size=None, montage=None, sampling_rate=None, enable_oscar=False, edge_id=None, **kwargs)[source]
Initialize BCI Core-8 amplifier source.
Samples are numbered on the master timeline as they arrive and the pipeline advances when data is available. There is no regenerated sampling clock, so nothing has to be kept in step with the device and no placeholder samples are ever produced.
- Parameters:
serial (
Optional[str]) – Serial number of target device. Uses first discovered if None.channel_count (
Optional[int]) – Number of EEG channels (1-8). Defaults to 8, or to the size of the montage if one is given.montage (
Optional[Montage]) – Electrode labels for the EEG channels. Names the channels for downstream nodes, files and streams.sampling_rate (
Optional[int]) –Rate the device runs at, in Hz. Leave it unset and the device is asked: the node connects during the master-timeline election and adopts whatever rate the amplifier reports.
Give a value to choose one. On a gCore device the rate is the channel mode – the device derives one from the other – so the three rates below are the whole configuration space, and the device is switched into the matching mode at connect:
sampling_rate
EEG channels
250 Hz
16
500 Hz
8
1000 Hz
4
There is no 2-channel mode and no 2000 Hz rate, however natural the halving pattern makes them look. The device’s own 1-channel mode is not offered either: it runs below the rate it reports; see
GCORE_MODES.channel_countneed not be given with a rate: it follows from the mode. Give it only to take fewer channels than the mode provides. The node acquires at most eight, so at 250 Hz it takes the first eight of the sixteen.On a legacy BCI Core-8 the rate is fixed in hardware, so a declared rate is checked rather than applied and a disagreement is an error.
enable_oscar (
bool) – Run OSCAR artifact removal on this stream.edge_id (
Optional[str]) – Which edge runs this node, matched against the edge process’s –edge-id. None, the default, is every edge; ignored when the pipeline is not distributed.**kwargs – Additional arguments for parent AmplifierSource.
- Raises:
ValueError – If montage size and channel_count disagree.
- start()[source]
Start BCI Core-8 amplifier and begin data acquisition.
Initializes buffers, starts background thread, establishes BLE connection, and begins real-time data streaming.
- Raises:
ConnectionError – If amplifier connection fails.
RuntimeError – If background thread creation fails.
- Return type:
- open_for_attestation()[source]
Connect before the entitlement gate runs. See the base class.
Safe to call twice: _connect is already guarded by
if self._device is Nonein start(), and the same guard is applied here, so the handle opened for the challenge is the one start() goes on to use. Opening a second BLE handle would be refused anyway – the device is exclusive.Never raises. A Core-8 that is out of range or off is not an entitlement question: the run stays unattested, and start() raises the failure remembered here without a second discovery (_connect_early).
Loads the driver first. The gate runs before start(), which is where the driver used to be loaded, so in a fresh process _connect scanned with
blestill None, failed on the attribute, and a licensed run was marked non-commercial while start() then connected without complaint.- Return type:
- master_candidacy()[source]
Claim the master timeline, at the rate the device actually runs.
The pipeline elects its master before it opens any handle, and the election reads whatever rate the source declares. For a device whose rate is fixed in hardware that is fine. A gCore amplifier is runtime-configurable, so the only honest way to declare its rate is to have asked it – which is why this connects first when no rate was given.
Connecting here moves the connect earlier inside the same
start()call; it does not add one. A handle it opens is the onestart()uses, and a failure is remembered for the rest of that start (_connect_early), so neither the entitlement gate norstart()repeats the discovery. The failure is swallowed here and raised bystart()a moment later; declining candidacy would only let a synthetic source take the timeline first.Asked by the election, and under
save_asby the raw tap when it writes its metadata, by which time the handle is open. The tap used to ask at pipeline construction, which connected from insideconnect()– with the device absent, a discovery no start could remember (_RawTap.attach_candidacy).The driver is loaded here for the reason open_for_attestation gives: the election runs before start(), and without it the connect failed in a fresh process, so the timeline was elected at the default rate while the device ran at its own.
- Returns:
(rate, DEVICE), or None if no usable rate is known.
- attach_timeline(timeline)[source]
Bind this source to the pipeline’s master timeline.
The pipeline calls this as each
start()begins, before the election, so it also lets go of a connect failure remembered by the previous start (_connect_early). A start refused before this node’s ownstart()ran would otherwise hand its failure on, and a device switched on in between would not be looked for.- Parameters:
timeline – Timeline manager owned by the pipeline.
- Return type:
- channel_units()[source]
One ‘uV’ per EEG channel (gtec-ble’s
to_microvolts, andAmplifier(microvolts=True)is this node’s default), ‘count’ for the device sample index, and None for the validity flag and the arrival stamp – the orderTIMELINE_LABELSfixes.
- stop()[source]
Stop BCI Core-8 amplifier and clean up resources.
Stops data acquisition, terminates background thread, and disconnects from amplifier hardware.