Build a simple EMG-based system using the Muscle BioAmp Shield and Arduino Uno to control a servo claw โ just like prosthetic hands!
EMG measures electrical signals produced by muscle contractions. These signals help detect muscle activation and control assistive tech.
An Arduino-compatible EMG shield with plug-and-play sensors, accelerometers, OLEDs, and servo support.
Align shield pins properly before pressing it in.
Apply NuPrep gel โ rub gently โ wipe clean.
Connect the BioAmp cable exactly as shown โ do NOT put electrodes yet.
Place electrodes on forearm as shown below:
Using EMG BioAmp Band
Plug the servo claw into the BioAmp Shield ports. Servo moves automatically based on muscle activation.
Paste the following code into Arduino IDE and upload:
#if defined(ESP32)
// For ESP32 Servo library
#include <ESP32Servo.h>
#else
// Arduino Servo library
#include <Servo.h>
#endif
// Samples per second
#define SAMPLE_RATE 500
// Make sure to set the same baud rate on your Serial Monitor/Plotter
#define BAUD_RATE 115200
// Change if not using A0 analog pin
#define INPUT_PIN A0
// envelopeee buffer size
// High value -> smooth but less responsive
// Low value -> not smooth but responsive
#define BUFFER_SIZE 64
// Servo pin (Change as per your connection)
#define SERVO_PIN 2 // Pin2 for Muscle BioAmp Shield v0.3
// EMG Threshold value, different for each user
// Check by plotting EMG envelopee data on Serial plotter
#define EMG_THRESHOLD 47
// Servo open & close angles
#define SERVO_OPEN 10
#define SERVO_CLOSE 90
// EMG Envelope baseline value
// Minimum value without flexing hand
#define EMG_ENVELOPE_BASELINE 4
// EMG Envelope divider
// Minimum value increase required to turn on a single LED
#define EMG_ENVELOPE_DIVIDER 4
Servo servo;
// Servo toggle flag
bool flag = 0;
// Last gesture timestamp
unsigned long lastGestureTime = 0;
// Delay between two actions
unsigned long gestureDelay = 240;
int circular_buffer[BUFFER_SIZE];
int data_index, sum;
// Muscle BioAmp Shield v0.3 LED pin numbers in-order
int led_bar[] = {8, 9, 10, 11, 12, 13};
int total_leds = sizeof(led_bar) / sizeof(led_bar[0]);
void setup() {
// Serial connection begin
Serial.begin(BAUD_RATE);
// Initialize all the led_bar
for (int i = 0; i < total_leds; i++) {
pinMode(led_bar[i], OUTPUT);
}
servo.attach(SERVO_PIN);
servo.write(0);
}
void loop() {
// Calculate elapsed time
static unsigned long past = 0;
unsigned long present = micros();
unsigned long interval = present - past;
past = present;
// Run timer
static long timer = 0;
timer -= interval;
// Sample and get envelope
if(timer < 0) {
timer += 1000000 / SAMPLE_RATE;
// RAW EMG Values
int sensor_value = analogRead(INPUT_PIN);
// Filtered EMG
int signal = EMGFilter(sensor_value);
// EMG envelopee
int envelope = getEnvelope(abs(signal));
// Update LED bar graph
for(int i = 0; i<=total_leds; i++){
if(i>(envelope/EMG_ENVELOPE_DIVIDER - EMG_ENVELOPE_BASELINE)){
digitalWrite(led_bar[i], LOW);
} else {
digitalWrite(led_bar[i], HIGH);
}
}
if(envelope > EMG_THRESHOLD) {
if((millis() - lastGestureTime) > gestureDelay){
if(flag == 1){
servo.write(SERVO_OPEN);
flag = 0;
lastGestureTime = millis();
}
else {
servo.write(SERVO_CLOSE);
flag = 1;
lastGestureTime = millis();
}
}
}
// EMG Raw signal
Serial.print(signal);
// Data seprator
Serial.print(",");
// EMG envelopeee
Serial.println(envelope);
}
}
// envelope detection algorithm
int getEnvelope(int abs_emg){
sum -= circular_buffer[data_index];
sum += abs_emg;
circular_buffer[data_index] = abs_emg;
data_index = (data_index + 1) % BUFFER_SIZE;
return (sum/BUFFER_SIZE) * 2;
}
float EMGFilter(float input)
{
float output = input;
{
static float z1, z2; // filter section state
float x = output - 0.05159732*z1 - 0.36347401*z2;
output = 0.01856301*x + 0.03712602*z1 + 0.01856301*z2;
z2 = z1;
z1 = x;
}
{
static float z1, z2; // filter section state
float x = output - -0.53945795*z1 - 0.39764934*z2;
output = 1.00000000*x + -2.00000000*z1 + 1.00000000*z2;
z2 = z1;
z1 = x;
}
{
static float z1, z2; // filter section state
float x = output - 0.47319594*z1 - 0.70744137*z2;
output = 1.00000000*x + 2.00000000*z1 + 1.00000000*z2;
z2 = z1;
z1 = x;
}
{
static float z1, z2; // filter section state
float x = output - -1.00211112*z1 - 0.74520226*z2;
output = 1.00000000*x + -2.00000000*z1 + 1.00000000*z2;
z2 = z1;
z1 = x;
}
return output;
}
Keep laptop unplugged and remove the charger .
Flex your muscles โ envelope rises โ servo toggles. Stronger flex = higher EMG = faster servo movement.
In this project, you will record EEG signals using the BioAmp EXG Pill and an arduino Dev Board, extract brainwave bandpowers using FFT, and classify emotional states like Focused, Calm, Drowsy, Daydreaming, Hyper-aware. The output appears live in the Serial Monitor.
Create a new sketch in Arduino IDE and paste the following code:
#include <arduinoFFT.h>
// ================= EEG + FFT Config =====================
#define SAMPLE_RATE 512
#define FFT_SIZE 256
#define BAUD_RATE 115200
#define INPUT_PIN A2
#define LED_PIN 2
// ================= Frequency Bands ======================
#define DELTA_LOW 0.5
#define DELTA_HIGH 4.0
#define THETA_LOW 4.0
#define THETA_HIGH 8.0
#define ALPHA_LOW 8.0
#define ALPHA_HIGH 13.0
#define BETA_LOW 13.0
#define BETA_HIGH 30.0
#define GAMMA_LOW 30.0
#define GAMMA_HIGH 45.0
// Less smoothing โ responds faster
#define SMOOTHING_FACTOR 0.35
const float EPSILON = 1e-6f;
// ================= Structs ==============================
typedef struct {
float delta, theta, alpha, beta, gamma, total;
} BandpowerResults;
typedef struct {
float delta = 0, theta = 0, alpha = 0, beta = 0, gamma = 0, total = 0;
} SmoothedBandpower;
SmoothedBandpower smoothedPowers;
// ================= FFT Buffers ==========================
double vReal[FFT_SIZE];
double vImag[FFT_SIZE];
ArduinoFFT<double> FFT(vReal, vImag, FFT_SIZE, SAMPLE_RATE);
// ================= Timing ================================
unsigned long lastEmotionTime = 0;
unsigned long sampling_period_us;
// ================= Functions =============================
void smoothBandpower(BandpowerResults *raw, SmoothedBandpower *s) {
s->delta = SMOOTHING_FACTOR * raw->delta + (1 - SMOOTHING_FACTOR) * s->delta;
s->theta = SMOOTHING_FACTOR * raw->theta + (1 - SMOOTHING_FACTOR) * s->theta;
s->alpha = SMOOTHING_FACTOR * raw->alpha + (1 - SMOOTHING_FACTOR) * s->alpha;
s->beta = SMOOTHING_FACTOR * raw->beta + (1 - SMOOTHING_FACTOR) * s->beta;
s->gamma = SMOOTHING_FACTOR * raw->gamma + (1 - SMOOTHING_FACTOR) * s->gamma;
s->total = SMOOTHING_FACTOR * raw->total + (1 - SMOOTHING_FACTOR) * s->total;
}
BandpowerResults calculateBandpower(double *powerSpectrum, double binWidth) {
BandpowerResults results = {0};
for (int i = 1; i < FFT_SIZE / 2; i++) {
float freq = i * binWidth;
float power = powerSpectrum[i];
results.total += power;
if (freq >= DELTA_LOW && freq < DELTA_HIGH) results.delta += power;
else if (freq >= THETA_LOW && freq < THETA_HIGH) results.theta += power;
else if (freq >= ALPHA_LOW && freq < ALPHA_HIGH) results.alpha += power;
else if (freq >= BETA_LOW && freq < BETA_HIGH) results.beta += power;
else if (freq >= GAMMA_LOW && freq < GAMMA_HIGH) results.gamma += power;
}
return results;
}
// ================= Emotion Classification ==================
void printEmotion() {
float d = (smoothedPowers.delta / (smoothedPowers.total + EPSILON)) * 100;
float t = (smoothedPowers.theta / (smoothedPowers.total + EPSILON)) * 100;
float a = (smoothedPowers.alpha / (smoothedPowers.total + EPSILON)) * 100;
float b = (smoothedPowers.beta / (smoothedPowers.total + EPSILON)) * 100;
float g = (smoothedPowers.gamma / (smoothedPowers.total + EPSILON)) * 100;
String emotion = "Neutral";
if (smoothedPowers.total < 0.001) {
emotion = "No Signal";
}
else if (d > 60 && b < 10 && g < 10) {
emotion = "Drowsy";
}
else if (t > a && t > b && t > 15) {
emotion = "Daydreaming";
}
else if (b > 25 && b > a) {
emotion = "Focused";
}
else if (a > 25 && a > b) {
emotion = "Calm";
}
else if (g > 15) {
emotion = "Hyper-aware";
}
Serial.print("Emotion: "); Serial.println(emotion);
Serial.print("Delta: "); Serial.print(d, 1);
Serial.print(" | Theta: "); Serial.print(t, 1);
Serial.print(" | Alpha: "); Serial.print(a, 1);
Serial.print(" | Beta: "); Serial.print(b, 1);
Serial.print(" | Gamma: "); Serial.println(g, 1);
}
// ================= Setup ==================
void setup() {
Serial.begin(115200);
pinMode(INPUT_PIN, INPUT);
pinMode(LED_PIN, OUTPUT);
sampling_period_us = round(1000000 * (1.0 / SAMPLE_RATE));
Serial.println("EEG Emotion Detection (Improved) Started...");
}
// ================= Loop ==================
void loop() {
unsigned long microseconds;
// Sampling
for (int i = 0; i < FFT_SIZE; i++) {
microseconds = micros();
vReal[i] = analogRead(INPUT_PIN);
vImag[i] = 0;
while (micros() - microseconds < sampling_period_us);
}
// FFT Processing
FFT.windowing(FFT_WIN_TYP_HAMMING, FFT_FORWARD);
FFT.compute(FFT_FORWARD);
FFT.complexToMagnitude();
double binWidth = (1.0 * SAMPLE_RATE) / FFT_SIZE;
BandpowerResults raw = calculateBandpower(vReal, binWidth);
smoothBandpower(&raw, &smoothedPowers);
double b_pct = (smoothedPowers.beta / (smoothedPowers.total + EPSILON)) * 100;
digitalWrite(LED_PIN, (b_pct > 20.0 ? HIGH : LOW));
if (millis() - lastEmotionTime > 5000) {
printEmotion();
lastEmotionTime = millis();
}
}
Open Tools โ Serial Monitor in Arduino IDE and set the baud rate to 115200.
You will see outputs like:
In this project, you will create a fun game controller that lets you play the Chrome Dino Game using your eye blinks. Using EOG (Electrooculography) signals from your eyes, the system detects blinks and triggers a keyboard SPACE press โ making the Dino jump!
Electrooculography measures the electrical potential produced by eye movement. The cornea is positively charged, the retina is negatively charged โ creating the electrooculogram. When you blink, this voltage changes, and we detect it using the BioAmp EXG Pill.
HARDWARE:
SOFTWARE:
A complete portable neuroscience lab for EEG, EMG, ECG and EOG recordings โ designed for HCI and BCI projects.
Carefully align the pins and stack the Muscle BioAmp Shield on top of Arduino Uno.
Connect the BioAmp EXG Pill to the A2 port using the STEMMA cable.
Plug the BioAmp Cable into the EXG Pill exactly as shown.
Apply NuPrep Gel โ rub gently โ clean with wet wipe. This reduces skin impedance and improves EOG signal quality.
About NuPrep: Removes dry skin and improves conductivity.
#include <Arduino.h>
#include <Keyboard.h> // HID keyboard library for Arduino R4
#include <math.h>
// #define DEBUG // Uncomment this line to enable debugging
// ----------------- USER CONFIGURATION -----------------
#define SAMPLE_RATE 512 // samples per second
#define BAUD_RATE 115200
#define INPUT_PIN A2
#define LED_PIN LED_BUILTIN
// Envelope Configuration for EOG detection
#define ENVELOPE_WINDOW_MS 100 // Smoothing window in milliseconds
#define ENVELOPE_WINDOW_SIZE ((ENVELOPE_WINDOW_MS * SAMPLE_RATE) / 1000)
// Blink Detection Thresholds - adjust these based on your setup
const int BlinkLowerThreshold = 30;
const int BlinkUpperThreshold = 50;
// Circular buffer for timing-based sampling
#define BUFFER_SIZE 64
float eogCircBuffer[BUFFER_SIZE];
int writeIndex = 0;
int readIndex = 0;
int samplesAvailable = 0;
// Single Blink Detection Configuration
const unsigned long BLINK_DEBOUNCE_MS = 300; // minimum time between blinks to prevent double-triggering
unsigned long lastBlinkTime = 0; // time of most recent blink
float currentEOGEnvelope = 0;
// HID Command Cooldown to prevent rapid-fire commands
const unsigned long HID_COOLDOWN_MS = 250; // 250ms between space commands (allows ~4 jumps per second)
unsigned long lastHIDCommandTime = 0;
// EOG Envelope Processing Variables
float eogEnvelopeBuffer[ENVELOPE_WINDOW_SIZE] = {0};
int eogEnvelopeIndex = 0;
float eogEnvelopeSum = 0;
// Game Statistics
unsigned long totalBlinks = 0;
unsigned long gameStartTime = 0;
// EOG Statistics for debug display
#define SEGMENT_SEC 1
#define SAMPLES_PER_SEGMENT (SAMPLE_RATE * SEGMENT_SEC)
float eogBuffer[SAMPLES_PER_SEGMENT] = {0};
uint16_t segmentIndex = 0;
unsigned long lastSegmentTimeMs = 0;
float eogAvg = 0, eogMin = 0, eogMax = 0;
bool segmentStatsReady = false;
// --- Filter Functions ---
// Band-Stop Butterworth IIR digital filter, generated using filter_gen.py.
// Sampling rate: 512.0 Hz, frequency: [48.0, 52.0] Hz.
// Filter is order 2, implemented as second-order sections (biquads).
// Reference: https://docs.scipy.org/doc/scipy/reference/generated/scipy.signal.butter.html
float Notch(float input)
{
float output = input;
{
static float z1, z2; // filter section state
float x = output - -1.58696045*z1 - 0.96505858*z2;
output = 0.96588529*x + -1.57986211*z1 + 0.96588529*z2;
z2 = z1;
z1 = x;
}
{
static float z1, z2; // filter section state
float x = output - -1.62761184*z1 - 0.96671306*z2;
output = 1.00000000*x + -1.63566226*z1 + 1.00000000*z2;
z2 = z1;
z1 = x;
}
return output;
}
// High-Pass Butterworth IIR digital filter, generated using filter_gen.py.
// Sampling rate: 512.0 Hz, frequency: 5.0 Hz.
// Filter is order 2, implemented as second-order sections (biquads).
// Reference: https://docs.scipy.org/doc/scipy/reference/generated/scipy.signal.butter.html
float EOGFilter(float input)
{
float output = input;
{
static float z1, z2; // filter section state
float x = output - -1.91327599*z1 - 0.91688335*z2;
output = 0.95753983*x + -1.91507967*z1 + 0.95753983*z2;
z2 = z1;
z1 = x;
}
return output;
}
float updateEOGEnvelope(float sample)
{
float absSample = fabs(sample);
// Update circular buffer and running sum
eogEnvelopeSum -= eogEnvelopeBuffer[eogEnvelopeIndex];
eogEnvelopeSum += absSample;
eogEnvelopeBuffer[eogEnvelopeIndex] = absSample;
eogEnvelopeIndex = (eogEnvelopeIndex + 1) % ENVELOPE_WINDOW_SIZE;
return eogEnvelopeSum / ENVELOPE_WINDOW_SIZE; // Return moving average
}
// HID Keyboard Functions
void sendSpaceBar() {
unsigned long nowMs = millis();
if ((nowMs - lastHIDCommandTime) >= HID_COOLDOWN_MS) {
Keyboard.press(' '); // Press space bar
delay(30); // Hold key for 30ms (shorter for gaming responsiveness)
Keyboard.release(' ');
lastHIDCommandTime = nowMs;
totalBlinks++;
Serial.print("JUMP! Blink #");
Serial.println(totalBlinks);
// LED feedback - quick single flash
digitalWrite(LED_PIN, HIGH);
delay(50);
digitalWrite(LED_PIN, LOW);
}
}
void setup() {
Serial.begin(BAUD_RATE);
delay(100);
pinMode(INPUT_PIN, INPUT);
pinMode(LED_PIN, OUTPUT);
// Initialize HID Keyboard
Keyboard.begin();
// LED startup sequence - game ready indicator
for(int i = 0; i < 3; i++) {
digitalWrite(LED_PIN, HIGH);
delay(200);
digitalWrite(LED_PIN, LOW);
delay(200);
}
gameStartTime = millis();
lastSegmentTimeMs = millis(); // Initialize the segment timer
Serial.println("=================================");
Serial.println("Arduino R4 EOG Dino Game Controller");
Serial.println("=================================");
Serial.println("Single Blink = Space Bar (Jump)");
Serial.println("Perfect for Chrome Dino Game!");
Serial.println("");
Serial.println("Instructions:");
Serial.println("1. Open Chrome and go to chrome://dino/");
Serial.println("2. Or disconnect internet and try to browse");
Serial.println("3. Press spacebar once to start game");
Serial.println("4. Then use blinks to jump!");
Serial.println("");
Serial.println("Starting EOG monitoring at 512 Hz...");
Serial.println("Ready to play! ๐ฆ");
}
void loop() {
static unsigned long lastMicros = 0;
static long timer = 0;
digitalWrite(LED_PIN, LOW); // Default LED state
// Timing-based sampling for 512 Hz
unsigned long currentMicros = micros();
long interval = (long)(currentMicros - lastMicros);
lastMicros = currentMicros;
timer -= interval;
const long period = 1000000L / SAMPLE_RATE;
while (timer < 0) {
timer += period;
int raw = analogRead(INPUT_PIN);
float filtered = Notch(raw);
float eog = EOGFilter(filtered);
eogCircBuffer[writeIndex] = eog;
writeIndex = (writeIndex + 1) % BUFFER_SIZE;
if (samplesAvailable < BUFFER_SIZE) {
samplesAvailable++;
}
}
// Process all available samples from circular buffer
while (samplesAvailable > 0) {
float eog = eogCircBuffer[readIndex];
readIndex = (readIndex + 1) % BUFFER_SIZE;
samplesAvailable--;
// Process the sample (envelope calculation)
currentEOGEnvelope = updateEOGEnvelope(eog);
// Add to segment buffer for statistics
if(segmentIndex < SAMPLES_PER_SEGMENT) {
eogBuffer[segmentIndex] = currentEOGEnvelope;
segmentIndex++;
}
}
// Get current time for blink detection logic
unsigned long nowMs = millis();
// ===== SEGMENT STATISTICS PROCESSING =====
if ((nowMs - lastSegmentTimeMs) >= (1000UL * SEGMENT_SEC)) {
if(segmentIndex > 0) {
// Compute min/max/avg for the completed segment
eogMin = eogBuffer[0];
eogMax = eogBuffer[0];
float eogSum = 0;
for (uint16_t i = 0; i < segmentIndex; i++) {
float eogVal = eogBuffer[i];
// EOG statistics
if (eogVal < eogMin) eogMin = eogVal;
if (eogVal > eogMax) eogMax = eogVal;
eogSum += eogVal;
}
eogAvg = eogSum / segmentIndex;
segmentStatsReady = true;
}
lastSegmentTimeMs = nowMs;
segmentIndex = 0;
}
// ===== SINGLE BLINK DETECTION AND SPACE BAR CONTROL =====
if (currentEOGEnvelope > BlinkLowerThreshold &&
currentEOGEnvelope < BlinkUpperThreshold &&
(nowMs - lastBlinkTime) >= BLINK_DEBOUNCE_MS) {
lastBlinkTime = nowMs;
#ifdef DEBUG
Serial.println("Blink detected!");
#endif
// Send space bar immediately for single blink
sendSpaceBar();
}
// ===== PERIODIC STATUS UPDATES =====
static unsigned long lastStatusUpdate = 0;
if ((nowMs - lastStatusUpdate) >= 30000) { // Every 30 seconds
unsigned long gameTimeSeconds = (nowMs - gameStartTime) / 1000;
float blinksPerMinute = (totalBlinks * 60.0) / (gameTimeSeconds + 1);
Serial.println("");
Serial.println("=== Game Stats ===");
Serial.print("Game Time: ");
Serial.print(gameTimeSeconds);
Serial.println(" seconds");
Serial.print("Total Jumps: ");
Serial.println(totalBlinks);
Serial.print("Jump Rate: ");
Serial.print(blinksPerMinute, 1);
Serial.println(" per minute");
Serial.print("Current EOG Level: ");
Serial.println(currentEOGEnvelope);
Serial.println("==================");
Serial.println("");
lastStatusUpdate = nowMs;
}
// ===== REAL-TIME EOG MONITORING (DEBUG) =====
#ifdef DEBUG
static unsigned long lastDebugPrint = 0;
if ((nowMs - lastDebugPrint) >= 1000) { // Every 1 second
if (segmentStatsReady) {
Serial.print("EOG: (Avg: "); Serial.print(eogAvg);
Serial.print(", Min: "); Serial.print(eogMin);
Serial.print(", Max: "); Serial.print(eogMax); Serial.println(")");
} else {
Serial.println("EOG: "); Serial.print(currentEOGEnvelope);
}
lastDebugPrint = nowMs;
}
#endif
}
Keep laptop unplugged and remove the charger.
Open Serial Plotter โ Press SW1 โ Blink โ Observe clear spike signals.
Control the Chrome Dino Game using only your eye blinks ๐
chrome://dino/Stronger blinks = higher accuracy ๐๐๏ธ๐ฆ