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.

Types

Link copied to clipboard
object Companion

Properties

Link copied to clipboard
val battery: StateFlow<Float>
Link copied to clipboard
val batteryLow: StateFlow<Boolean?>

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.

Link copied to clipboard
val chargerConnected: StateFlow<Boolean?>

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.

Link copied to clipboard
val charging: StateFlow<Boolean?>

Charger state from the SIG Battery Level Status characteristic (0x2BED), best-effort: true = charging, false = discharging, null = unknown or not exposed by this firmware.

Link copied to clipboard
val contact: StateFlow<BooleanArray>

Per-active-channel electrode contact from the latest chunk; false = not connected.

Link copied to clipboard

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.

Link copied to clipboard
Link copied to clipboard
val info: StateFlow<DeviceInfo?>

Device Information Service strings; populated once during initialize.

Link copied to clipboard
val link: StateFlow<LinkInfo>

The current negotiated BLE link parameters (MTU, PHY, connection interval/latency/timeout), as last reported by the transport. See LinkInfo.

Link copied to clipboard
val missedSamples: StateFlow<Long>

Cumulative device-side samples lost (gap-filled) this session.

Link copied to clipboard
Link copied to clipboard

The advertised BLE name of the headset (BA MAXI 1234) — its identity on LSL and in recordings.

Link copied to clipboard
val sampleCounterJumps: StateFlow<Long>

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.

Link copied to clipboard
val stalePackets: StateFlow<Long>

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.

Link copied to clipboard
val state: StateFlow<DeviceState>
Link copied to clipboard

Whether this device exposes the charging-settings service (see readChargingSettings).

Link copied to clipboard

Whether this device can stream its digital input (stream(digitalInput = true)): firmware 3.8.1+ exposes the stream-options characteristic, older firmware does not.

Link copied to clipboard

Whether this device exposes the vendor firmware-update service (see updateFirmware).

Link copied to clipboard

Whether this device can stream OutputMode.TEST_SIGNAL: firmware 3.7.0+ exposes the output-mode characteristic, older firmware does not (see stream).

Link copied to clipboard
val undecodablePackets: StateFlow<Long>

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

Link copied to clipboard
suspend fun configure(newConfig: DeviceConfig): Result<Unit>

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.

Link copied to clipboard

The config currently applied to the device.

Link copied to clipboard
suspend fun disconnect()

Stop streaming, unsubscribe, disconnect, and cancel this session's scope.

Link copied to clipboard
suspend fun initialize(): Result<Unit>

Bring the device to DeviceState.Ready: discover, raise MTU, read+subscribe battery, subscribe stream data, and write the stored (or default) config.

Link copied to clipboard

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.

Link copied to clipboard

Read the device's ChargingSettings. Null when the service is absent or a value reads back empty; a GATT failure propagates as BrainAccessError.

Link copied to clipboard
fun sampleHostTimeNs(sample: Long): Long?

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).

Link copied to clipboard
fun sampleIndexAt(hostTimeNs: Long = hostClockNs()): Long?

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.

Link copied to clipboard
fun sampleNearest(hostTimeNs: Long = hostClockNs()): Long?

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.

Link copied to clipboard
fun stream(outputMode: OutputMode = OutputMode.NORMAL, digitalInput: Boolean = false): Flow<EegChunk>

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.

Link copied to clipboard
fun DeviceSession.streamMetadata(channels: List<ChannelMetadata>, outputMode: OutputMode, software: Map<String, String> = emptyMap()): StreamMetadata

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.

Link copied to clipboard

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.

Link copied to clipboard
fun updateFirmware(firmware: ByteArray): Flow<OtaProgress>

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.

Link copied to clipboard

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.