Skip to content

Electrode-impedance check (Run mode)

Before a session (and optionally before each block) the operator checks per-electrode impedance on the OpenBCI Cyton, lowers it to their satisfaction, and the readings are saved into the recording. This closes one of the pilot's back-fill gaps: impedances are captured in-app and travel with the data, never pulled from a side spreadsheet afterwards (DESIGN_PRINCIPLES #1).

Operator SOP

  1. Cap/gel the participant as usual. With the board powered on and connected, lock the protocol on Set up Session (Load protocol… / Resolve / 🔒 Lock all live there — #147), then switch to Run Session and launch the check:
  2. GUI: expand the collapsible QA check pane — click its ▸ QA check (impedance) title row (it also expands automatically when you press Prep selected run) — and click Start / Re-check. A collapsed pane is always still there as that one title row: the dividers between panes cannot be dragged over a pane to hide it, so the triangle is the only way a pane closes and the only way it reopens. (Before #126 this lived on its own QA check tab; it is the same widget, just folded into Run Session so the whole session runs from one place.)
  3. or headless one-shot: python -m ssvep.runtime.openbci --serial-port COM5 --impedance. (The OpenBCI GUI must be closed — it locks the serial port.)
  4. The montage graphic shows one circle per electrode at its 2-D position, labelled the way the selected carrier labels its channels — 10-10 site names on the default Occipital 10-10 montage and the Electro-Cap 8-ch (10-20), Cyton wire colours on the free electrode array — coloured by impedance on a green / yellow / red traffic light (the printed number is the actual kΩ; n/a = a channel that could not be measured). The bands are fixed per amplifier and never rescale to the data, so a colour means the same kΩ for the whole check (#564): actiCHamp green below 30, yellow 30–60, red from 60; every Cyton green below 750, yellow 750–2000, red from 2000 (provisional — see Thresholds below). A board with neither (Unicorn, synthetic) gets a fixed green→red gradient over 0–100 kΩ, clipped above. It updates live as you adjust electrodes.
  5. Set Label to when the reading is taken (pre-session, then pre-block-3, …) and click Done. The current values are captured as a snapshot immediately; switching the drive off is asked for at the same moment, but see Stopping is not instant below — the status line stays honest about which of those two has actually happened.
  6. Repeat before later blocks as needed — each Done appends another snapshot.

During the check the channels are not recording EEG (the board is injecting the drive tone), so this is always a distinct step before/between recording, not concurrent with it.

Stopping is not instant — the status line (and Start session) say so

On the Cyton, turning the drive off is 8 sequential synchronous serial round-trips (openbci._drive_all_channels) followed by releasing the board session — real hardware I/O that has been observed to take 5–10 seconds, not the sub-second teardown Done otherwise looks like. Until that finishes:

  • The impedance status line reads "stopping… (releasing the amplifier — this can take several seconds)" — not "captured" or "ready" — and only flips to the real result ("captured '
  • Start session stays disabled the whole time. Clicking it anyway (e.g. from a stale UI state) is also refused directly, with "An impedance check is running — click Done to stop it before recording."

Both of those track the actual worker thread, not a fixed wait: an earlier version (#119) reported "stopped" unconditionally after a bounded synchronous wait, even when that wait timed out and the Cyton was still mid-teardown — so Start session could look clickable for several seconds after Done, then refuse anyway with "impedance still running" when clicked. If you see the "stopping…" status linger, that delay is real board teardown time, not a hang — wait for it; there is nothing to retry.

How it works

The Cyton has no continuous impedance stream. We use the ADS1299 lead-off detection: enable a 6 nA / 31.2 Hz drive on each channel (BrainFlow config_board("z<ch>01Z")), read back the amplitude of that tone, and convert with the OpenBCI formula

Z = √2 · V_rms / I_drive − R_series      # I_drive = 6 nA, R_series = 2200 Ω

Pure core (testable, no hardware): ssvep.runtime.impedance. Live board read (hardware-gated): ssvep.runtime.openbci.stream_impedance / measure_impedance_once. UI: ssvep.ui.run.

⚠ Known limitation — Cyton/ADS1299 only (Unicorn reports bogus ~0 kΩ)

This method is specific to the Cyton's ADS1299 lead-off drive. Boards without that path report invalid numbers:

  • g.tec Unicorn — observed 2026-07-14: impedance shows ≈ 0 kΩ on every channel regardless of real contact (with dry Unicorn electrodes and obviously poor signal quality, i.e. the true impedances are much higher). The Unicorn exposes no lead-off drive via BrainFlow, so the Cyton formula is meaningless for it. Do not trust Unicorn impedance readings — needs a Unicorn-specific method or an explicit "impedance unsupported on this board" state. Not yet fixed.
  • actiCHamp — has its own impedance mode (ssvep.runtime.actichamp.impedances_kohm), but that read layout is not yet verified on hardware.

Where it's saved

Each session sidecar (*_eeg.session.json) gains an impedance list — one entry per snapshot with its label, timestamp (date-shifted, never the real date: DATA_GOVERNANCE.md), drive parameters, and per-channel kΩ. The same entries go to the per-session sub-XXX_ses-YYY_impedance.json log on every Done. The latest snapshot is also mirrored into that recording's manifest montage (acquisition.montage[].impedance_kohm), so analysis can read impedance by lookup with zero heuristics. The versioned design-time run manifest in run-manifests/ is never modified — impedance is acquisition-time data.

Every Done also appends its snapshot to a per-session log, sourcedata/sub-XXX/ses-YYY/eeg/sub-XXX_ses-YYY_impedance.json, so the contact record survives even if no run follows. Each entry carries the session's data-use stamp (data_use: its purpose and tier, #286), as every file the session writes does. Three cases write nothing. The synthetic amp has nothing to measure. An observer-only session (option 0) keeps no data, so the reading is only shown on screen and the status line says "captured '

Thresholds — deliberately not set yet

There is no pass/fail cut-off: nothing refuses to record on impedance. The colour bands are an at-a-glance guide, and only actiCHamp's rest on established gel practice. The Cyton bands come from a small #118 survey of dry-electrode runs against decoding accuracy, and its two pilot recordings disagree on where "bad" starts — read yellow as "ambiguous", not "marginal". Cyton gold-cup + paste has no calibration of its own yet and borrows the same bands (#564). Its first pilots read 140–5700 kΩ on the Cyton, the same range as dry. What impedance actually predicts good vs. poor SSVEP classification for each electrode type is still to be determined against decoding performance.

A first dry-electrode bring-up (bench self-test) read 170–1030 kΩ across the occipital montage and still decoded 9-class SSVEP at 25.9 % @ 4 s (chance 11.1 %, p≈0.025) — weak but above chance, i.e. a lot of headroom between "measurable" and "good" for dry electrodes.

Impedance checking is routine EEG practice, and the 6 nA drive is far below any safety concern — but whether it needs its own sign-off is a question for your ethics board, not this doc. It is the hardware itself that most often needs it, and each deployment tracks its own divergences (AGENTS.md §4).