├── src ├── devreader.sh ├── throttle.pl ├── killreader.sh ├── sample.sh ├── stm.sh ├── time-stamp.pl ├── wired.sh ├── broad-wired.sh ├── simple-capture.sh ├── visualize-signal-with-noise-test.sh ├── chan1-filter.pl ├── chan2-filter.pl ├── filter_coefs.yaml ├── generate_filter_coef.py ├── score_trial.pl ├── plot_frames.pl ├── plot_3chan.pl ├── freq-split-smooth.pl ├── freq-broad-bandpass.pl └── window.pl ├── setup-notes.txt ├── .gitignore ├── README ├── test ├── generate-test-signals.R └── freq-split-smooth.1.out └── COPYING /src/devreader.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | echo "g" > /dev/ttyACM0 3 | cat /dev/ttyACM0 4 | 5 | -------------------------------------------------------------------------------- /src/throttle.pl: -------------------------------------------------------------------------------- 1 | #!/usr/bin/perl 2 | use strict; 3 | use warnings; 4 | use Time::HiRes; 5 | 6 | $| = 1; 7 | 8 | while () { 9 | Time::HiRes::usleep( 4 * 1000 ); 10 | print $_; 11 | } 12 | -------------------------------------------------------------------------------- /src/killreader.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | sleep $1 3 | # 4 | # killall on some systems works only with the first 15 characters 5 | # this killall tries both the full name and the 15 character name 6 | # 7 | killall serial-reader.p 2>/dev/null 8 | killall serial-reader.pl 2>/dev/null 9 | -------------------------------------------------------------------------------- /src/sample.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | EXG="../OpenHardwareExG/src" 4 | SAMPLE=`date +"%Y%m%d.%H%M%S"` 5 | 6 | ./killreader.sh $1 & 7 | $EXG/serial-reader.pl | 8 | $EXG/frame-parser.pl | 9 | ./parsed-frame-filter.pl | 10 | tee sample-${USER}-${SAMPLE}.csv | 11 | ./plot.pl 12 | -------------------------------------------------------------------------------- /src/stm.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | EXG="../OpenHardwareExG/src" 4 | SAMPLE=`date +"%Y%m%d.%H%M%S"` 5 | 6 | # tee >(./plot_frames.pl) | 7 | 8 | ./devreader.sh | 9 | $EXG/frame-parser.pl | 10 | tee -a sample-${USER}-${SAMPLE}.csv | 11 | ./chan2-filter.pl | 12 | ./window.pl | 13 | ./plot_2chan.pl 14 | -------------------------------------------------------------------------------- /setup-notes.txt: -------------------------------------------------------------------------------- 1 | need to install the perl module Device/SerialPort.pm 2 | 3 | sudo apt-get install libdevice-serialport-perl 4 | 5 | need to verify the /dev/ttyACM0 (or adjust serial-reader.pl to match) 6 | 7 | need to have rights to tty device ("dialout" group on ubuntu) 8 | 9 | sudo apt-get install gnuplot 10 | -------------------------------------------------------------------------------- /src/time-stamp.pl: -------------------------------------------------------------------------------- 1 | #!/usr/bin/perl 2 | use strict; 3 | use warnings; 4 | 5 | use Time::HiRes qw(gettimeofday); 6 | 7 | # turn off output buffering 8 | $| = 1; 9 | 10 | while () { 11 | my ($seconds, $microseconds) = gettimeofday(); 12 | chomp $_; 13 | printf("%d.%06d, %s\n", $seconds, $microseconds, $_); 14 | } 15 | -------------------------------------------------------------------------------- /src/wired.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | EXG="../OpenHardwareExG/src" 4 | SAMPLE=`date +"%Y%m%d.%H%M%S"` 5 | 6 | # tee >(./plot_frames.pl) | 7 | 8 | $EXG/serial-reader.pl | 9 | $EXG/frame-parser.pl | 10 | tee -a sample-${USER}-${SAMPLE}.csv | 11 | ./chan1-filter.pl | 12 | ./freq-split-smooth.pl | 13 | ./window.pl | 14 | tee -a trial-${USER}-${SAMPLE}.csv | 15 | ./plot_3chan.pl 16 | -------------------------------------------------------------------------------- /src/broad-wired.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | EXG="../OpenHardwareExG/src" 4 | 5 | SAMPLE=`date +"%Y%m%d.%H%M%S"` 6 | 7 | # tee >(./plot_frames.pl) | 8 | 9 | $EXG/serial-reader.pl | 10 | $EXG/frame-parser.pl | 11 | tee -a sample-${USER}-${SAMPLE}.csv | 12 | ./chan2-filter.pl | 13 | ./freq-broad-bandpass.pl | 14 | ./window.pl | 15 | tee -a trial-${USER}-${SAMPLE}.csv | 16 | ./plot_3chan.pl 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ################### 2 | # backup files 3 | ################### 4 | *.bak 5 | *.bck 6 | *cache.lib 7 | $* 8 | *.swp 9 | *.000 10 | *~ 11 | 12 | ################ 13 | # Output files 14 | ################ 15 | *.o 16 | *.a 17 | *.bin 18 | *.d 19 | *.elf 20 | *.hex 21 | *.list 22 | *.srec 23 | *.map 24 | 25 | #################### 26 | # R temporary files 27 | #################### 28 | .RData 29 | .Rhistory 30 | -------------------------------------------------------------------------------- /README: -------------------------------------------------------------------------------- 1 | The eeg-mouse is a project to attempt to control an on-screen pointer with eeg signals. 2 | 3 | This has potential application for adaptive equipment. 4 | 5 | http://openelectronicslab.github.io/eeg-mouse/ 6 | 7 | Hardware can be found at: 8 | http://openelectronicslab.github.io/OpenHardwareExG/ 9 | and 10 | https://github.com/OpenElectronicsLab/ads1298-breakout 11 | 12 | Notes and data can be found at: 13 | https://github.com/OpenElectronicsLab/eeg-mouse-notes 14 | -------------------------------------------------------------------------------- /src/simple-capture.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | MONTH=`date +"%Y%m%d"` 4 | DATE=`date +"%Y%m%d.%H%M%S"` 5 | DURATION=10 6 | mkdir -p ../data/$MONTH 7 | CAPTURE=../data/$MONTH/capture-${DATE}.txt 8 | echo "x" > /dev/ttyACM0 9 | sleep 2 10 | # ( ./devreader.sh > $CAPTURE &) 11 | ( ./devreader.sh | ./time-stamp.pl > $CAPTURE &) 12 | sleep $DURATION 13 | echo "x" > /dev/ttyACM0 14 | sleep 1 15 | killall devreader.sh 16 | head -n10 $CAPTURE 17 | wc -l $CAPTURE 18 | lines=`wc -l $CAPTURE | cut -d' ' -f1` 19 | echo "$lines / $DURATION" | bc 20 | -------------------------------------------------------------------------------- /src/visualize-signal-with-noise-test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | cat ../test/signal-with-noise.csv | 4 | ./freq-split-smooth.pl > ../test/freq-split-smooth.out 5 | 6 | cat ../test/signal-with-noise.csv | 7 | ./freq-split-smooth.pl --skipsmooth > ../test/freq-split-no-smooth.out 8 | 9 | R --vanilla <<'EOF' 10 | signals = read.csv("../test/signal.csv", col.names=c('low','high')); 11 | r1 = read.csv("../test/freq-split-smooth.out", col.names=c('low','high')); 12 | r2 = read.csv("../test/freq-split-no-smooth.out", col.names=c('low','high')); 13 | 14 | png("plotted.png", 1024, 1024); 15 | 16 | par(mfrow=c(2,1)); 17 | 18 | plot(1:length(signals$low), signals$low, col="blue", type="l") 19 | lines(1:length(r2$low), r2$low, col="gray") 20 | lines(1:length(r1$low), r1$low, col="black") 21 | 22 | 23 | plot(1:length(signals$high), signals$high, col="blue", type="l") 24 | lines(1:length(r2$high), r2$high, col="gray") 25 | lines(1:length(r1$high), r1$high, col="black") 26 | 27 | dev.off(); 28 | EOF 29 | 30 | firefox ./plotted.png 31 | -------------------------------------------------------------------------------- /src/chan1-filter.pl: -------------------------------------------------------------------------------- 1 | #!/usr/bin/perl 2 | use strict; 3 | use warnings; 4 | 5 | # turn off output buffering 6 | $| = 1; 7 | 8 | # C, 00, 00, 0, 0, 0, 0, -0.1136117, -0.1485806, -0.1476560, -0.1508017, -0.2974134, -0.2481733, -0.2990923, -0.9263688 9 | 10 | my $valid_row_regex = qr/ 11 | (?C),\s*.* 12 | (?[0-9A-F]{2}),\s* 13 | (?[0-9A-F]{2}),\s* 14 | (?[01]),\s* 15 | (?[01]),\s* 16 | (?[01]),\s* 17 | (?[01]),\s* 18 | (?-?[0-9]+(?:\.[0-9]*)),\s* 19 | (?-?[0-9]+(?:\.[0-9]*)),\s* 20 | (?-?[0-9]+(?:\.[0-9]*)),\s* 21 | (?-?[0-9]+(?:\.[0-9]*)),\s* 22 | (?-?[0-9]+(?:\.[0-9]*)),\s* 23 | (?-?[0-9]+(?:\.[0-9]*)),\s* 24 | (?-?[0-9]+(?:\.[0-9]*)),\s* 25 | (?-?[0-9]+(?:\.[0-9]*))\s* 26 | /x; 27 | 28 | while () { 29 | 30 | # parse the data 31 | if ( $_ =~ m/$valid_row_regex/ ) { 32 | print $+{chan1}, "\n"; 33 | } 34 | else { 35 | warn "unrecognized data string:\n", $_, "\n"; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/chan2-filter.pl: -------------------------------------------------------------------------------- 1 | #!/usr/bin/perl 2 | use strict; 3 | use warnings; 4 | 5 | # turn off output buffering 6 | $| = 1; 7 | 8 | # C, 00, 00, 0, 0, 0, 0, -0.1136117, -0.1485806, -0.1476560, -0.1508017, -0.2974134, -0.2481733, -0.2990923, -0.9263688 9 | 10 | my $valid_row_regex = qr/ 11 | (?C),\s*.* 12 | (?[0-9A-F]{2}),\s* 13 | (?[0-9A-F]{2}),\s* 14 | (?[01]),\s* 15 | (?[01]),\s* 16 | (?[01]),\s* 17 | (?[01]),\s* 18 | (?-?[0-9]+(?:\.[0-9]*)),\s* 19 | (?-?[0-9]+(?:\.[0-9]*)),\s* 20 | (?-?[0-9]+(?:\.[0-9]*)),\s* 21 | (?-?[0-9]+(?:\.[0-9]*)),\s* 22 | (?-?[0-9]+(?:\.[0-9]*)),\s* 23 | (?-?[0-9]+(?:\.[0-9]*)),\s* 24 | (?-?[0-9]+(?:\.[0-9]*)),\s* 25 | (?-?[0-9]+(?:\.[0-9]*))\s* 26 | /x; 27 | 28 | while () { 29 | 30 | # parse the data 31 | if ( $_ =~ m/$valid_row_regex/ ) { 32 | print $+{chan1}, ',', $+{chan2}, "\n"; 33 | } 34 | else { 35 | warn "unrecognized data string:\n", $_, "\n"; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /test/generate-test-signals.R: -------------------------------------------------------------------------------- 1 | #!/usr/bin/Rscript 2 | totalDuration = 20.0; # seconds 3 | signalAmplitude = 1e-3; # volts, peak to peak 4 | samplingRate = 250; # samples per second 5 | 6 | noiseAmplitude = 0.1e-3; # volts 7 | 8 | lowSignalParams = list( 9 | duration = c(1.0, 1.0), # seconds 10 | startTime = c(5, 10), # seconds 11 | frequency = 11 # Hz 12 | ); 13 | highSignalParams = list( 14 | duration = c(1.0, 1.0), # seconds 15 | startTime = c(10, 15), # seconds 16 | frequency = 23 # Hz 17 | ); 18 | 19 | time = (0:floor(totalDuration * samplingRate)) / samplingRate; 20 | 21 | # generate the noise 22 | set.seed(1234); 23 | noise = rnorm(length(time), sd=noiseAmplitude); 24 | 25 | # generate the pure signal (as a sine wave for the given totalDuration) 26 | generateSignal = function(params) { 27 | signal = (1:length(time)); # generate an array of numbers 28 | signal = signal * 0; # reset the array to all zeros 29 | for (i in 1:length(params$duration)) { 30 | signalStartingSample = floor( 31 | params$startTime[i] * samplingRate); 32 | signalEndingSample = floor( 33 | (params$startTime[i] + params$duration[i]) * 34 | samplingRate); 35 | signal[signalStartingSample:signalEndingSample] = 36 | (signalAmplitude / 2) * 37 | sin( 38 | 2 * pi * params$frequency * 39 | time[signalStartingSample:signalEndingSample] 40 | ); 41 | } 42 | return(signal); 43 | } 44 | 45 | lowSignal = generateSignal(lowSignalParams); 46 | highSignal = generateSignal(highSignalParams); 47 | 48 | signalsPlusNoise = lowSignal + highSignal + noise; 49 | 50 | # write out the resulting file 51 | write.table(data.frame(low=lowSignal, high=highSignal), "signal.csv", 52 | row.names=F, col.names=F, sep=","); 53 | write.table(signalsPlusNoise, "signal-with-noise.csv", row.names=F, col.names=F); 54 | -------------------------------------------------------------------------------- /src/filter_coefs.yaml: -------------------------------------------------------------------------------- 1 | x_filter: 2 | in_coef: [0.00477952850316041, -0.017827843732432628, 0.02133463381903281, 0.0, -0.02133463381903281, 0.017827843732432624, -0.00477952850316041] 3 | out_coef: [-5.691356164442124, 13.6929827386752, -17.818776437449383, 13.22611247371713, -5.3100044118982845, 0.901263913122391] 4 | y_filter: 5 | in_coef: [0.004089453810784436, -0.013354414837795602, 0.014847937497981823, 0.0, -0.014847937497981823, 0.013354414837795602, -0.004089453810784436] 6 | out_coef: [-4.950151699635935, 11.074514320959294, -14.093607485952553, 10.750243222185905, -4.6644901480190075, 0.9147439680658163] 7 | baseline_filter: 8 | in_coef: [0.000689782274448917, -0.0025092973678783304, 0.002971490889697712, 0.0, -0.002971490889697712, 0.0025092973678783304, -0.000689782274448917] 9 | out_coef: [-5.447102955119436, 12.87540194547167, -16.826181095869945, 12.811807423618697, -5.3934270892516425, 0.9852556650491906] 10 | smooth_filter: 11 | in_coef: [0.003406439119826039, -0.003139475150201823, -0.003139475150201823, 0.003406439119826039] 12 | out_coef: [-2.9137129783371485, 2.8426746259167683, -0.9284277196403714] 13 | broad_bandpass_filter: 14 | in_coef: [1.6909095781919097e-06, 0.0, -1.5218186203726985e-05, -4.248935548711965e-20, 6.087274481490756e-05, -4.2489355487119655e-19, -0.00014203640456813882, -1.593350830766987e-19, 0.00021305460685212612, -5.158473314608121e-19, -0.0002130546068522077, -1.593350830766987e-19, 0.00014203640456812524, -2.1244677743559827e-19, -6.087274481490794e-05, -4.248935548711965e-20, 1.5218186203726985e-05, 0.0, -1.6909095781919097e-06] 15 | out_coef: [-12.488419783387014, 75.5259633299214, -293.59256131504253, 821.4082163802806, -1755.3376196749905, 2967.871867129326, -4057.9677927582748, 4547.419553523908, -4207.072207090399, 3220.7606344046203, -2035.594853027713, 1054.4216977096867, -441.6983742975524, 146.40647224590808, -37.074934176982985, 6.760223378255063, -0.7929598071891343, 0.04510354827065592] 16 | -------------------------------------------------------------------------------- /src/generate_filter_coef.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | import numpy as np 3 | from scipy import signal 4 | from matplotlib import pyplot as plt 5 | 6 | nyquest_freq = 250./2; 7 | 8 | # 7-14 Hz elliptic bandpass filter 9 | x_filter = signal.iirdesign( 10 | wp = [7./nyquest_freq, 14./nyquest_freq], 11 | ws = [4./nyquest_freq, 20./nyquest_freq], 12 | gstop=40, gpass=3, ftype='ellip' 13 | ) 14 | 15 | # 20-26 Hz elliptic bandpass filter 16 | y_filter = signal.iirdesign( 17 | wp = [20.0/nyquest_freq, 26.0/nyquest_freq], 18 | ws = [16./nyquest_freq, 32./nyquest_freq], 19 | gstop=40, gpass=3, ftype='ellip' 20 | ) 21 | 22 | # 16.5-17.5 Hz elliptic bandpass filter 23 | baseline_filter = signal.iirdesign( 24 | wp = [16.5/nyquest_freq, 17.5/nyquest_freq], 25 | ws = [15.5/nyquest_freq, 18.5/nyquest_freq], 26 | gstop=40, gpass=3, ftype='ellip' 27 | ) 28 | 29 | # 5 Hz elliptic lowpass filter 30 | smooth_filter = signal.iirdesign( 31 | wp = 5./ nyquest_freq, 32 | ws = 10./ nyquest_freq, gstop=40, 33 | gpass=3, ftype='ellip' 34 | ) 35 | 36 | # 15-35 Hz band-pass filter 37 | broad_bandpass_filter = signal.iirdesign( 38 | wp = [15/nyquest_freq, 35/nyquest_freq], 39 | ws = [5/nyquest_freq, 45/nyquest_freq], 40 | gstop=40, gpass=3, ftype='butterworth' 41 | ) 42 | 43 | 44 | fig = plt.figure() 45 | for filter in [x_filter, y_filter, baseline_filter, smooth_filter, broad_bandpass_filter]: 46 | w,h = signal.freqz(*filter) 47 | plt.plot(w*(nyquest_freq/max(w)), np.abs(h)) 48 | plt.xlim(0,40) 49 | plt.xlabel('frequency (Hz)'); 50 | plt.ylabel('response'); 51 | fig.savefig("test.png") 52 | 53 | with open('filter_coefs.yaml','wt') as f: 54 | f.write('x_filter:\n'); 55 | f.write(' in_coef: ' + str(x_filter[0].tolist()) + '\n'); 56 | f.write(' out_coef: ' + str(x_filter[1][1:].tolist()) + '\n'); 57 | f.write('y_filter:\n'); 58 | f.write(' in_coef: ' + str(y_filter[0].tolist()) + '\n'); 59 | f.write(' out_coef: ' + str(y_filter[1][1:].tolist()) + '\n'); 60 | f.write('baseline_filter:\n'); 61 | f.write(' in_coef: ' + str(baseline_filter[0].tolist()) + '\n'); 62 | f.write(' out_coef: ' + str(baseline_filter[1][1:].tolist()) + '\n'); 63 | f.write('smooth_filter:\n'); 64 | f.write(' in_coef: ' + str(smooth_filter[0].tolist()) + '\n'); 65 | f.write(' out_coef: ' + str(smooth_filter[1][1:].tolist()) + '\n'); 66 | f.write('broad_bandpass_filter:\n'); 67 | f.write(' in_coef: ' + str(broad_bandpass_filter[0].tolist()) + '\n'); 68 | f.write(' out_coef: ' + str(broad_bandpass_filter[1][1:].tolist()) + '\n'); 69 | 70 | -------------------------------------------------------------------------------- /src/score_trial.pl: -------------------------------------------------------------------------------- 1 | #!/usr/bin/perl 2 | use strict; 3 | use warnings; 4 | 5 | use Getopt::Long; 6 | 7 | my $shuffle = 0; 8 | my $lacks_basechan = 0; 9 | 10 | GetOptions("shuffle" => \$shuffle); 11 | GetOptions("lacks_basechan" => \$lacks_basechan); 12 | 13 | our $without_basechan = qr/ 14 | (?-?[0-9]+(?:\.[0-9]*)),\s* 15 | (?-?[0-9]+(?:\.[0-9]*)),\s* 16 | (?<_x>[0-9]*),\s*(?[0-9]*),\s* 17 | (?<_y>[0-9]*),\s*(?[0-9]*),\s* 18 | (?