ATB Open Biosignal Profile (ATB-OBP) 1.0
Open, vendor-neutral, royalty-free Bluetooth LE GATT profile for multi-channel EEG and biosignal devices.
Status: Release 1.0 · Date: 4 November 2026 · Owner: HMS Health Mobile Software / Auto Train Brain Inc. · Contact: info@healthmobilesoftware.com
ATB-OBP is an open, vendor-neutral Bluetooth Low Energy (BLE) GATT profile for multi-channel EEG and related biosignal devices. A device that implements this profile connects to ATB Desktop (Windows and macOS) — and later to the ATB mobile apps — without any vendor driver or SDK. The application discovers the device, reads a self-describing JSON record, starts the stream and immediately uses the device for live EEG monitoring, recordings, QEEG measurements and EEG-based brain exercise sessions in clinical EEG software.
The key words MUST, MUST NOT, SHOULD, SHOULD NOT and MAY are used as in RFC 2119.
1. Scope and design goals
- One profile, any hardware: 1–32 channels, 128–4096 Hz, dry / dry-active / gel / saline electrodes.
- Self-describing: channel labels, sample rates, scaling and features are read from the device; the app needs no per-model code.
- Simple firmware: fixed little-endian binary packets; one notification per data packet; no compression.
- Robust: every packet carries a sequence number and a sample index, so the app detects and repairs loss exactly.
- Extensible: unknown JSON keys are ignored; vendor features use the
x-prefix; optional characteristics may be absent.
Out of scope for 1.0: on-device signal processing results, stimulation output, firmware update (use the vendor's own DFU service), more than 32 channels (planned for 2.0).
2. Conformance levels
| Level | Requirement |
|---|---|
| Core (REQUIRED) | Service, Device Info (§5), Control Point with START_STREAM / STOP_STREAM (§6), EEG Data (§7), timing (§9), at least one sample rate ≥ 128 Hz. |
| Recommended | Status (§8.2), impedance or contact quality (§8.1), SYNC_TIME, IDENTIFY, standard Battery Service 0x180F, Device Information Service 0x180A, LE Secure Connections. |
| Optional | Aux (IMU / PPG, §8.4), Events / markers (§8.3), SET_RATE, SET_CHANNEL_MASK, MARKER. |
A device is "ATB-OBP 1.0 Core compatible" when it passes every Core check of the in-app conformance test (§11).
3. GATT layout
3.1 UUIDs
All ATB-OBP UUIDs share one 128-bit base. Replace xxxx with the 16-bit short code:
a7b1xxxx-0b1e-4e0e-8a7b-41544200b1e0 ("415442" = ASCII "ATB")
| Short | UUID | Name | Properties | Level |
|---|---|---|---|---|
0000 |
a7b10000-0b1e-4e0e-8a7b-41544200b1e0 |
ATB-OBP Service (primary) | — | Core |
0001 |
a7b10001-0b1e-4e0e-8a7b-41544200b1e0 |
Device Info | Read | Core |
0002 |
a7b10002-0b1e-4e0e-8a7b-41544200b1e0 |
Control Point | Write (with response), Indicate | Core |
0003 |
a7b10003-0b1e-4e0e-8a7b-41544200b1e0 |
EEG Data | Notify | Core |
0004 |
a7b10004-0b1e-4e0e-8a7b-41544200b1e0 |
Impedance / Contact | Notify | Recommended |
0005 |
a7b10005-0b1e-4e0e-8a7b-41544200b1e0 |
Status | Read, Notify | Recommended |
0006 |
a7b10006-0b1e-4e0e-8a7b-41544200b1e0 |
Aux (IMU / PPG) | Notify | Optional |
0007 |
a7b10007-0b1e-4e0e-8a7b-41544200b1e0 |
Events / Markers | Notify | Optional |
Standard SIG services that MAY be added alongside: Battery Service 0x180F (Battery Level 0x2A19), Device Information Service 0x180A.
3.2 Advertising
- The device MUST include the ATB-OBP Service UUID (128-bit) in its advertising data or scan response, so the app can filter devices by service.
- The device SHOULD advertise a human-readable Complete Local Name (e.g.
AcmeEEG-8 1234; the last 4 characters of the serial help identify one of several units). - Advertising interval while waiting for a connection: 100–250 ms recommended.
3.3 Byte order
All multi-byte integer and float fields are little-endian. Signed integers are two's complement.
4. Connection sequence (what the app does)
- Scan with filter
services = [ATB-OBP Service]; the user picks the device in the app. - Connect; exchange MTU (the app's BLE stack requests the maximum; see §9.1).
- Read Device Info (§5), parse and validate it. Validation errors abort the connection and are shown in the conformance test.
- Enable indications on Control Point; enable notifications on Status, Impedance and Events if present. Read Status once.
- Enable notifications on EEG Data.
- If
sync_timeis listed in features: sendSYNC_TIME. - Send
START_STREAM(defaultRate, mask = all eeg+eog channels, format); wait for the response indication (timeout 3 s). - Stream. On user disconnect: send
STOP_STREAM, wait ≤ 1.5 s for the response, then disconnect.
Firmware MUST tolerate steps 4–5 in any order and MUST NOT start streaming before START_STREAM.
5. Device Info characteristic (0001)
A UTF-8 encoded JSON object (no BOM; trailing NUL bytes allowed). The value MAY be longer than the ATT MTU — the client uses ATT Read Blob (long read) automatically. Maximum length 2048 bytes. The value MUST NOT change while connected.
5.1 Fields
| Key | Type | Req. | Meaning |
|---|---|---|---|
obp |
string "major.minor" |
yes | Profile version implemented. For this spec "1.0". |
vendor |
string ≤ 64 | yes | Manufacturer name. |
model |
string ≤ 64 | yes | Model name. |
serial |
string ≤ 64 | recommended | Unit serial number (no personal data). |
firmware |
string ≤ 32 | recommended | Firmware version. |
hardware |
string ≤ 32 | no | Hardware revision. |
electrodes |
"dry" | "dry-active" | "gel" | "saline" |
recommended | Selects the app's signal-quality and artifact profile. |
rates |
number[] | yes | Supported sample rates in Hz (64–4096). At least one MUST be ≥ 128. 250, 256, 500 or 512 recommended. |
defaultRate |
number | recommended | Rate used by the app; MUST be in rates. |
formats |
string[] | yes | Sample formats supported: "int16", "int24", "float32". |
format |
string | recommended | Preferred format (MUST be in formats). |
lsb_uV |
number | yes for int formats | Microvolts per LSB at the device output (after gain). µV = raw × lsb_uV. |
range_uV |
number | no | ± input range in µV. |
reference |
string | no | Reference electrode / montage, e.g. "Cz", "linked-ears", "A1". |
ground |
string | no | Ground / bias electrode, e.g. "Fpz". |
channels |
object[] (1–32) | yes | One entry per hardware channel, in channel-index order. See §5.2. |
maxFramesPerPacket |
number 1–255 | recommended | Largest nFrames the device sends (§7). |
features |
string[] | recommended | Capability list, §5.3. |
filters |
object | no | { "hp_Hz": 0.5, "lp_Hz": 100, "notch_Hz": null } — analog/digital filtering applied by the device. |
Unknown keys MUST be ignored by the app; vendors MAY add keys prefixed x- (e.g. "x-acme-gain": 24).
5.2 Channel entries
{ "label": "Fp1", "type": "eeg" }
label: electrode name in the international 10-20 / 10-10 system (e.g.Fp1,F3,Cz,O2,TP9). Legacy namesT3/T4/T5/T6are accepted and mapped toT7/T8/P7/P8. Labels MUST be unique.type:eeg(default) ·ref·eog·emg·ecg·aux. The app streamseegandeogchannels by default; at least oneeegchannel is required.- The channel's index (0-based position in the array) is its bit in the channel mask and its column in data packets.
5.3 Features
| Feature | Meaning |
|---|---|
impedance |
Impedance in kΩ via IMPEDANCE_START (§8.1, format 1). |
contact_quality |
Contact quality 0–100 % instead of kΩ (§8.1, format 2) — typical for dry electrodes. |
impedance_during_stream |
EEG keeps streaming while impedance is measured. |
status |
Status characteristic present. |
events |
Events characteristic present. |
aux.accel / aux.gyro / aux.ppg |
Aux characteristic streams these types. |
sync_time / marker / identify / set_rate / channel_mask |
Control Point opcodes supported. |
battery_service / dis |
SIG services 0x180F / 0x180A present. |
x-… |
Vendor specific. |
5.4 Example
{
"obp": "1.0",
"vendor": "Acme Neuro",
"model": "AN-8",
"serial": "AN8-000123",
"firmware": "1.2.0",
"hardware": "rev C",
"electrodes": "dry",
"rates": [250, 500],
"defaultRate": 250,
"formats": ["int24"],
"format": "int24",
"lsb_uV": 0.02235,
"range_uV": 187500,
"reference": "linked-ears",
"ground": "Fpz",
"channels": [
{ "label": "Fp1", "type": "eeg" }, { "label": "Fp2", "type": "eeg" },
{ "label": "C3", "type": "eeg" }, { "label": "C4", "type": "eeg" },
{ "label": "P3", "type": "eeg" }, { "label": "P4", "type": "eeg" },
{ "label": "O1", "type": "eeg" }, { "label": "O2", "type": "eeg" }
],
"maxFramesPerPacket": 9,
"features": ["contact_quality", "status", "sync_time", "identify", "battery_service"],
"filters": { "hp_Hz": 0.5, "lp_Hz": 100, "notch_Hz": null }
}
6. Control Point characteristic (0002)
The app writes a command (Write with response). The device answers every command with one indication on the same characteristic.
6.1 Command format
| Byte | Field |
|---|---|
| 0 | Opcode (u8) |
| 1… | Parameters (little-endian) |
| Opcode | Name | Parameters | Level |
|---|---|---|---|
0x01 |
START_STREAM |
rate_Hz u16 · channelMask u32 · format u8 (0 = device default, 1 = int16, 2 = int24, 3 = float32) |
Core |
0x02 |
STOP_STREAM |
— | Core |
0x03 |
IMPEDANCE_START |
— | Recommended |
0x04 |
IMPEDANCE_STOP |
— | Recommended |
0x05 |
SET_RATE |
rate_Hz u16 (only while not streaming) |
Optional |
0x06 |
SET_CHANNEL_MASK |
channelMask u32 (only while not streaming) |
Optional |
0x07 |
SYNC_TIME |
unixTime_ms u64 |
Recommended |
0x08 |
MARKER |
code u16 |
Optional |
0x09 |
IDENTIFY |
seconds u8 (blink LED / vibrate) |
Recommended |
0x0A |
GET_STATUS |
— | Recommended |
channelMask: bit i selects channels[i]. Bits ≥ channel count MUST be ignored. A mask of 0 is INVALID_PARAM.
6.2 Response indication
| Byte | Field |
|---|---|
| 0 | 0x80 (response code) |
| 1 | Request opcode |
| 2 | Result (u8) |
| 3… | Optional response data |
| Result | Name | When |
|---|---|---|
0x00 |
SUCCESS | Command executed. |
0x01 |
NOT_SUPPORTED | Unknown or unsupported opcode. |
0x02 |
INVALID_PARAM | Rate not in rates, mask 0, unsupported format… |
0x03 |
INVALID_STATE | e.g. SET_RATE while streaming, START_STREAM while already streaming with different parameters. |
0x04 |
FAILED | Hardware error (also set Status error code). |
Response data:
SYNC_TIME→sampleIndex u32of the device at reception (lets the app align device sample clock to wall time).GET_STATUS→ the 8-byte Status payload (§8.2).START_STREAM→ MAY returnrate_Hz u16 · channelMask u32 · format u8actually applied.
Timing: the response SHOULD be indicated within 200 ms (MUST within 3 s). START_STREAM while already streaming with identical parameters SHOULD return SUCCESS (idempotent). After START_STREAM the first data packet MUST start with firstSampleIndex = 0.
7. EEG Data characteristic (0003)
One notification = one packet = a 10-byte header + nFrames frames. A frame is one sample of every channel in the active mask, in ascending channel-index order.
7.1 Packet layout
| Offset | Size | Field | Notes |
|---|---|---|---|
| 0 | u8 | format |
Low nibble: 0x01 int16 · 0x02 int24 · 0x03 float32 (value already in µV). High nibble reserved = 0. |
| 1 | u16 | seq |
Packet counter, +1 per packet, wraps 65535 → 0. Resets to 0 on START_STREAM. |
| 3 | u32 | firstSampleIndex |
Index of the first frame in this packet since START_STREAM (sample clock), wraps at 2³². |
| 7 | u8 | nFrames |
Frames in this packet (1–255, ≤ maxFramesPerPacket). |
| 8 | u8 | nChannels |
Channels per frame = popcount(active mask). |
| 9 | u8 | flags |
bit0 LEAD_OFF (any channel off) · bit1 CLIPPING · bit2 MARKER (an Events record refers to this packet) · bit3 LOW_BATTERY · bits4–7 reserved = 0. |
| 10 | n | samples | nFrames × nChannels × bytesPerSample, frame-major (F0C0, F0C1 … F0Cn, F1C0 …). |
bytesPerSample: int16 = 2, int24 = 3 (little-endian, sign-extended from bit 23), float32 = 4 (IEEE-754).
Scaling: µV = raw × lsb_uV for int formats; float32 values are µV directly.
7.2 Sizing packets
The ATT notification payload is MTU − 3 bytes. The firmware MUST choose nFrames so that 10 + nFrames × nChannels × bytesPerSample ≤ MTU − 3.
| MTU | Channels | Format | Max frames / packet | Packets/s at 250 Hz |
|---|---|---|---|---|
| 247 | 8 | int24 | 9 | ~28 |
| 247 | 8 | int16 | 14 | ~18 |
| 247 | 4 | int24 | 19 | ~13 |
| 247 | 16 | int24 | 4 | ~63 |
| 185 | 8 | int24 | 7 | ~36 |
| 23 (default) | 2 | int16 | 2 | 125 |
Firmware SHOULD send a packet at least every 40 ms even if it is not full (latency, §9.2).
7.3 Golden packets (for firmware unit tests)
int16, seq = 0x0102, firstSampleIndex = 1000, 2 frames × 2 channels, samples [100, −100], [32767, −32768]:
01 02 01 E8 03 00 00 02 02 00 64 00 9C FF FF 7F 00 80
int24, seq = 0, index 0, LEAD_OFF set, 1 frame × 3 channels, samples [1, −1, −8388608]:
02 00 00 00 00 00 00 01 03 01 01 00 00 FF FF FF 00 00 80
7.4 Loss handling (app side, informative)
seqgap → packets lost;firstSampleIndexgap → exact number of frames lost.- Gaps ≤ 1 s are bridged by linear interpolation so filters and spectra stay continuous; the loss is counted and reported (loss %).
- Gaps > 1 s break the stream (discontinuity); recordings mark the break.
- Packets with an older
firstSampleIndex(duplicates / reordered) are dropped. - The device MUST NOT renumber samples after a buffer overrun: if frames are dropped on the device, skip their indices (so the app sees the gap) and increment the Status
droppedPacketscounter.
8. Optional characteristics
8.1 Impedance / Contact (0004, Notify)
Sent while impedance mode is active (IMPEDANCE_START … IMPEDANCE_STOP), at least once per second.
| Offset | Size | Field |
|---|---|---|
| 0 | u8 | format: 0x01 kΩ (u16 per channel) · 0x02 contact quality 0–100 % (u8 per channel) |
| 1 | u8 | firstChannel (index into channels) |
| 2 | u8 | count |
| 3 | … | values: format 1 → u16 kΩ, 0xFFFE = open / lead-off, 0xFFFF = unknown; format 2 → u8 0–100, 0xFF = unknown |
Large channel counts MAY be split across several notifications using firstChannel. Unless impedance_during_stream is listed, the device MAY pause EEG data during impedance mode and MUST resume it on IMPEDANCE_STOP (continuing seq/firstSampleIndex numbering, which the app treats as a gap).
App colour coding: kΩ ≤ 20 green, ≤ 50 amber, > 50 red; quality ≥ 70 green, ≥ 40 amber.
8.2 Status (0005, Read + Notify)
8 bytes. Notify on change and at least every 10 s while connected.
| Offset | Size | Field |
|---|---|---|
| 0 | u8 | Battery % (0–100, 0xFF unknown) |
| 1 | u8 | Flags: bit0 CHARGING · bit1 STREAMING · bit2 IMPEDANCE · bit3 LOW_BATTERY · bit4 ERROR |
| 2 | u8 | State: 0 idle · 1 streaming · 2 impedance · 3 error |
| 3 | u8 | Error code (0 = none; vendor-defined otherwise) |
| 4 | u32 | Dropped packets (device-side overruns since START_STREAM) |
Safety recommendation: devices SHOULD NOT stream EEG while charging from mains (mains noise and electrical safety); report CHARGING so the app can warn.
8.3 Events / Markers (0007, Notify)
7 bytes per event.
| Offset | Size | Field |
|---|---|---|
| 0 | u8 | Type: 1 marker echo (from MARKER) · 2 button press · 3 lead-off change · 4 hardware trigger input |
| 1 | u32 | sampleIndex (on the EEG sample clock) |
| 5 | u16 | Code (marker code, button id, channel index, trigger value) |
8.4 Aux — IMU / PPG (0006, Notify)
| Offset | Size | Field |
|---|---|---|
| 0 | u8 | Type: 0x10 accelerometer (mg) · 0x11 gyroscope (0.01 °/s) · 0x12 PPG (raw) |
| 1 | u16 | seq |
| 3 | u32 | sampleIndex on the EEG sample clock (for alignment) |
| 7 | u8 | nFrames |
| 8 | u8 | nAxes (accel/gyro 3, PPG 1–3) |
| 9 | … | int16 samples, frame-major |
ATB Desktop 1.11 ignores Aux data; it is reserved for motion-artifact rejection and HRV in a later release.
9. Timing and performance
9.1 MTU and connection parameters
- Support ATT MTU exchange up to 247 (BLE 4.2 Data Length Extension). The device MUST still work at the default MTU 23 by sending smaller packets.
- Connection interval 7.5–30 ms recommended (15 ms typical); slave latency 0 while streaming; supervision timeout 2–4 s.
- 2M PHY SHOULD be supported for ≥ 16 channels or ≥ 500 Hz.
9.2 Sampling and latency
| Item | Requirement |
|---|---|
| Sample rate | ≥ 128 Hz (Core); 250 / 256 / 500 Hz recommended |
| Rate accuracy | Effective rate within ±2 % of nominal (crystal-derived sample clock; ±50 ppm recommended) |
| Packet loss | ≤ 0.5 % frames over 10 min at 1 m line of sight |
| Latency | Sample-to-notification ≤ 100 ms (Core: max gap between packets ≤ 100 ms); ≤ 50 ms recommended |
| Jitter | Inter-packet arrival standard deviation ≤ 20 ms recommended |
| Channels | All channels sampled simultaneously (or skew ≤ 1 sample) |
| Data rate check | 8 ch × 250 Hz × int24 = 6 kB/s payload — comfortably within BLE 1M PHY |
9.3 Signal quality (recommendations)
- Input-referred noise ≤ 1 µV RMS (0.5–40 Hz); CMRR ≥ 100 dB; resolution ≤ 0.1 µV/LSB.
- Do not apply a mains notch filter on the device unless reported in
filters; the app applies 50/60 Hz notch by region. - High-pass ≤ 0.5 Hz; anti-alias low-pass < 0.45 × sample rate.
10. Security and privacy
- LE Secure Connections pairing (Just Works or Passkey) is RECOMMENDED; the profile also works unpaired for evaluation units.
- The device MUST NOT store or transmit personal or health information about the wearer.
serialidentifies the hardware only. - The Control Point MUST reject malformed commands with INVALID_PARAM and never crash or reboot.
- Firmware updates are out of scope; use the vendor's own DFU service (it MUST NOT be reachable through ATB-OBP characteristics).
11. Conformance checklist (in-app test)
ATB Desktop → Clinic → device card "ATB-OBP Bluetooth device" → 🧪 Conformance test runs these checks live and exports a JSON report (Save report). Send the report to info@healthmobilesoftware.com to be listed as compatible.
| ID | Level | Check | Pass criterion |
|---|---|---|---|
| C1 | Core | Device Info JSON | Valid JSON, obp major = 1, required fields present, rates/format/lsb consistent, unique labels, ≥ 1 EEG channel. Warnings (e.g. non 10-10 label) do not fail. |
| C2 | Core | START_STREAM | Response SUCCESS within 3 s. |
| C3 | Core | Sample rate | Effective rate within ±2 % of defaultRate (warn ±5 %), ≥ 10 s streaming. |
| C4 | Core | Frame loss | ≤ 0.5 % (warn ≤ 2 %). |
| C5 | Core | Packet gap | Longest gap ≤ 100 ms (warn ≤ 250 ms). |
| C6 | Core | STOP_STREAM | Response SUCCESS on disconnect. |
| C7 | Core | 10-10 labels | All EEG labels map to 10-10 names (warning only). |
| O1 | Optional | Status | Status read/notify decodes. |
| O2 | Optional | Impedance / contact | At least one impedance/quality notification received. |
Firmware-side self test: reproduce the golden packets of §7.3 byte-for-byte.
12. Versioning
obp="major.minor". Minor versions only add optional fields, opcodes, characteristics or flag bits; a 1.x app accepts any 1.y device (unknown parts ignored, a newer minor is reported as a warning).- A major version change (2.0) may change packet layouts; it will use a new service UUID short code so 1.x and 2.x can coexist on one device.
- Reserved bits and bytes MUST be sent as 0 and ignored on receipt.
13. Reference material
| File | Purpose |
|---|---|
atb_obp.h |
C header: UUIDs, opcodes, packed structs, packet builder for embedded firmware. |
reference/atb_obp_sim.py |
Reference BLE peripheral (Python, bless) that simulates an 8-channel device — test the app without hardware. |
ATB Desktop src/shared/obp/protocol.ts |
The app's decoder and validator (normative behaviour of §5–§7 on the client side). |
Appendix A — Minimal firmware state machine
┌────────────── IMPEDANCE_STOP ───────────────┐
▼ │
IDLE ──START_STREAM──► STREAMING ──IMPEDANCE_START──► IMPEDANCE
▲ ▲ │ │
│ └──── STOP_STREAM ────┘ │
└────────────── STOP_STREAM / disconnect ──────────────┘
- On disconnect: stop streaming, keep advertising.
- On
START_STREAM: resetseqandfirstSampleIndexto 0, apply rate/mask/format, respond SUCCESS, then start notifications.
Appendix B — Example timeline
App → Read 0001 ← {"obp":"1.0",...}
App → CCCD 0002 = indicate, 0003/0004/0005 = notify
App → 0002: 07 <unix ms u64> ← 80 07 00 <sampleIndex u32> (SYNC_TIME)
App → 0002: 01 FA 00 FF 00 00 00 02 ← 80 01 00 (START 250 Hz, 8 ch, int24)
← 0003: 02 00 00 00 00 00 00 09 08 00 <216 bytes> seq 0, idx 0
← 0003: 02 01 00 09 00 00 00 09 08 00 <216 bytes> seq 1, idx 9
…
App → 0002: 02 ← 80 02 00 (STOP)