EEG_SSVEP_Drone β€” SSVEP control on a 32-channel dry-electrode cap

Mirror of github.com/twu3202/EEG_SSVEP_Drone. Data also at Twu31/cap32-ssvep.

Steering a DJI Tello with SSVEP (steady-state visually evoked potentials) recorded on a low-cost 32-channel dry-electrode cap (TI ADS1299, 250 Hz, WiFi/UDP). The aim was a continuous control loop β€” a command every 300–400 ms β€” rather than one command every five seconds.

Companion project: EEG_MI (motor imagery on the same cap; this repo reuses its acquisition/protocol layer rather than forking it).

What actually happened

Decoding is barely above chance, and this repo is mostly the record of finding out why.

Session Targets Best decoder Accuracy Chance 95 % CI
ssvep_20260725_150031 12 FBCCA, 2.4 s 0.150 0.083 [0.10, 0.22]
ssvep_20260725_171821 4 TRCA-CCA, 1.8 s 0.450 0.250 [0.33, 0.58]

Above chance, statistically β€” and nowhere near usable for flying anything. The interesting part is the diagnosis.

The misclassifications all fell to low frequencies, and it was not alpha

Errors piled up at 8 Hz regardless of the true target. The obvious suspect is the alpha rhythm, and this subject's IAF is exactly 10.00 Hz β€” but that turned out to be the second effect, not the cause.

CCA/FBCCA's score ρ is a variance ratio, so argmax over candidate frequencies is really comparing absolute power at each. Under a S(f) ∝ f^-Ξ± aperiodic background the noise floor itself goes as ρ_noise(f) ∝ f^(-Ξ±/2) β€” monotonically decreasing, so the lowest candidate frequency wins by default. Measured slope: βˆ’0.95 to βˆ’1.11, against an independently fitted Ξ± = 1.79–2.34 whose prediction matches to within 0.06. The 8 Hz noise floor sits 1.7–1.9Γ— higher than 15.7 Hz.

Alpha is real and does sit on top of it β€” in the one confound-free session the excess over the power law peaks at exactly 10.0 Hz, matching the independently measured IAF β€” but that excess is only +19.7 % against the +71 % from the 1/f floor.

Full derivation and the measurements: docs/lowfreq_bias_report.pdf (δΈ­ζ–‡).

Two results that did not survive scrutiny

  • Channel selection. "Dropping unstable electrodes measurably helps" held until stimulus duration was put on an axis and paired tests were run β€” then it went away.
  • The onset-skip hypothesis (that discarding the first N ms of each trial would help) was tested directly and failed.

Holm correction is applied throughout, which is why several early effects disappeared.

Data

Four calibration sessions, published with the code in calib/ and mirrored to huggingface.co/datasets/Twu31/cap32-ssvep.

import numpy as np
d = np.load("calib/ssvep_20260725_171821.npz", allow_pickle=True)
d["X"]         # (60, 10, 500) float32 Β΅V β€” trials Γ— channels Γ— samples
d["y"]         # (60,) int32 β€” target index
d["freqs"]     # (4,) Hz;  d["phases"] (4,) rad;  d["names"] (4,) target names
d["ch_names"]  # ('P7','P3','PZ','P4','P8','PO3','PO4','O1','OZ','O2')

⚠ These files contain 10 posterior channels, not all 32. Channel selection was applied at record time, so these sessions cannot be re-analysed with a different montage. meta_json.channels_full lists the full 32-channel montage the cap was wearing.

Layout

src/ssvep/          stimulus, decoders (CCA / FBCCA / eTRCA / TDCA / TRCA-CCA),
                    calibration, quality checks, the 1/f bias diagnostic, benchmarks
src/control/        Tello SDK driver, safety layer, latency test, MuJoCo simulator
src/acquisition/    SsvepStream β€” timing-exact capture with gap reconstruction
                    (imports EEG_MI's protocol layer; does not fork it)
glasses/            Rokid AR-glasses on-device SSVEP probe (Android/Kotlin)
calib/              four calibration sessions: .npz signals + .json paradigm sidecars
docs/               the 1/f bias report; Tello control-stack notes
results/            benchmarks, bias diagnostics, duration frontiers, simulator captures

Why timing matters more here than for MI

At 12 Hz one cycle is 83 ms, so a 4 ms epoch-boundary error is 14Β° of phase. eTRCA and TDCA are phase-locking decoders and collapse to chance when that drifts β€” which is exactly what happened before the phase-aware templates fix. The acquisition layer reconstructs dropped UDP frames rather than skipping them, for the same reason: skipping compresses the time axis.

Related: the cap's sample clock was later measured against an external generator at +3235 ppm β€” small for band power, but a 0.4 % clock error rotates a 12 Hz tone by a full cycle in ~2 s. See the crosstalk report in EEG_MI.

Running it

pip install -r requirements.txt

python src/ssvep/benchmark.py --demo          # decoders on synthetic SSVEP, no hardware
python src/ssvep/stimulus.py --targets 4      # frame-accurate flicker stimulus
python src/ssvep/bias_diag.py calib/ssvep_20260725_150031.npz    # the 1/f diagnostic
python src/control/sim_mujoco.py              # Tello simulator, no drone needed

Live capture needs the cap on its ESPBCI access point and the Tello on its own β€” one WiFi adapter cannot hold both, which is the first architectural problem in this project. Options are written up in docs/tello_control_stack.md.

License

MIT for the code. The recordings are CC BY 4.0.

Recordings are from a single consenting adult subject (the author). No clinical or identifying information is included.

Downloads last month

-

Downloads are not tracked for this model. How to track
Inference Providers NEW
This model isn't deployed by any Inference Provider. πŸ™‹ Ask for provider support