DeviceSession
One connected device. Owns a supervised CoroutineScope cancelled on disconnect; the handshake awaits suspend acks from the GattConnection. Fallible ops return Result.
Created by BrainAccessClient; not constructed directly.
Properties
The device's own low-battery flag (0x2BED power-state charge level: below 15 % on current firmware), best-effort; null = unknown. At 3 % without a charger the headset powers itself off.
Whether a charger is plugged in (0x2BED power-state "wired external power", the charger's PGOOD), best-effort; null = unknown. Differs from charging once the cell is full: the charger stays connected but stops charging. Unless ChargingSettings.stayOnWhileCharging was set before plugging in, the headset drops the link as soon as a charger is connected.
Per-active-channel electrode contact from the latest chunk; false = not connected.
Hardware generation, resolved during initialize from the Device Information Service Model Number; until then, and whenever the string is missing or names a board that cannot carry the advertised montage, the V2 board of model. Decides which IMU blocks the stream footer carries and how EegChunk.accel is scaled — a HALO V3 has an accelerometer and gyroscope, a HALO V2 has neither.
Device Information Service strings; populated once during initialize.
Cumulative device-side samples lost (gap-filled) this session.
Cumulative packets whose device sample counter went partly backwards, moved back and stayed there (the numbering resyncs to it), or jumped further than the decoder gap-fills (8 s of signal), this session — losses missedSamples cannot count. A packet wholly behind the numbering (a duplicate or late one) is dropped and not counted here. See com.neurotec.brainaccess.protocol.DecodeState.counterJumps.
Cumulative stream packets whose samples were already delivered, this session: every sample number they carry was already emitted or gap-filled (a duplicate, a late packet, or a packet whose counter restarted at 0). They yield no chunk, so this is the only trace of them. See com.neurotec.brainaccess.protocol.DecodeState.stalePackets.
Whether this device exposes the charging-settings service (see readChargingSettings).
Whether this device can stream its digital input (stream(digitalInput = true)): firmware 3.8.1+ exposes the stream-options characteristic, older firmware does not.
Whether this device exposes the vendor firmware-update service (see updateFirmware).
Whether this device can stream OutputMode.TEST_SIGNAL: firmware 3.7.0+ exposes the output-mode characteristic, older firmware does not (see stream).
Cumulative stream packets the codec could not decode (malformed, truncated), this session. Their samples are not emitted; the next good packet gap-fills them like an on-air loss.
Functions
Apply a new configuration (writes the config blob; it takes effect at the next stream start). Fails with BrainAccessError.ConfigRejected when the device refuses the blob; the previous config then stays current. A rate this board cannot stream (DeviceHardware.supportedSrates) fails with IllegalArgumentException before anything is written — firmware before 3.7.0 does not report a refused rate.
The config currently applied to the device.
Stop streaming, unsubscribe, disconnect, and cancel this session's scope.
Bring the device to DeviceState.Ready: discover, raise MTU, read+subscribe battery, subscribe stream data, and write the stored (or default) config.
Read the device's full battery gauge (voltage, current, health, state-of-charge) from the battery service's energy / health / level-status characteristics (see BatteryGauge). Returns null when the energy characteristic is absent (firmware without the fuel gauge exposes no voltage or current) or reads back too short to decode. Absent status/health leave those optional fields null; a GATT failure reading a characteristic that IS present propagates as BrainAccessError.
Read the device's ChargingSettings. Null when the service is absent or a value reads back empty; a GATT failure propagates as BrainAccessError.
The host time (System.nanoTime(), the clock of EegChunk.hostTimeNs) at which device sample sample was taken, from the headset's own clock — the timestamp of that sample for LSL or any other consumer that wants the moment of acquisition rather than of arrival. sample is the device's sample counter, as the packet footers count it and as sampleIndexAt returns it (unsigned 32-bit, wrapping at 2^32).
The device sample number being converted at host time hostTimeNs (System.nanoTime(), the clock of EegChunk.hostTimeNs) — the sample an annotation made at that moment belongs to — or null when no stream is running. Meant for annotations and triggers: call it when the event happens, with the default time, and store the index.
The device sample whose sampleHostTimeNs is nearest hostTimeNs (System.nanoTime()) — the sample a marker made at that moment belongs to, when the marker should sit on the sample closest in time rather than on the one being converted (sampleIndexAt, which is early by up to a sample against it). Numbered as the device counts (unsigned 32-bit, wrapping at 2^32), never negative.
Cold stream of decoded chunks. On collection it starts the device stream (control 0x01) and decodes each notification into one EegChunk; on completion it stops the stream (control 0x00, best-effort). The notification listener is attached BEFORE the start write, so the first packets cannot slip past it, and each packet is stamped (EegChunk.hostTimeNs) as it leaves the transport — before the queue to the decoder, so a slow collector delays chunks but not their timestamps. The queue is UNBOUNDED, so a transiently slow collector never causes packets to be discarded (which would show up as device-side loss in missedSamples). A backlog costs memory (~500 B/packet), never data; only a consumer that is sustainedly slower than real time can grow it. Each chunk also carries per-active-channel EegChunk.contact; device-side losses (gap-filled samples) accumulate into missedSamples, undecodable packets into undecodablePackets, counter discontinuities into sampleCounterJumps.
Build the StreamMetadata of this session for the given channels (the LSL path passes lslContractChannels; recorders pass their own file-specific lists). Identity fields come from the Device Information Service (DeviceSession.info; null when not read or blank), the montage from DeviceSession.currentConfig. software adds entries to versions (e.g. board); the SDK adds kotlin_sdk (BrainAccessKit.VERSION) and contract.
How sampleIndexAt is placing host times and how well the device's clock fits the host's; see TimeSyncInfo. For diagnostics; callable at any time. All zero/false on a device without time sync (firmware before 3.8.0) and before its first stream.
Flash firmware onto the device (vendor OTA service; see OtaControl for the wire contract). Cold flow: collection performs the update and reports OtaProgress; the terminal emission is OtaProgress.Done, after which the device applies the image and typically reboots — expect the session to go DeviceState.Disconnected. The device boots into its power-off state and advertises again only after a long press of its power button, so a host waiting for it to reappear must ask the user for that press. On any failure (NAK, timeout, link drop) the device keeps its previous firmware.
Write settings to the device: the idle power-off time first, then the stay-on flag. Two independent writes — if the second fails, the first stays applied. Fails with IllegalStateException when the device has no charging-settings service.