Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
257 changes: 257 additions & 0 deletions arduino/tests/emg_serial_test/emg_serial_test.ino
Original file line number Diff line number Diff line change
@@ -0,0 +1,257 @@
#include "esp_adc_cal.h"

#define EMG_PIN 34
#define SAMPLE_RATE_HZ 1000
#define BASELINE_CAL_SAMPLES 500
#define FILTER_ALPHA 0.4
#define STATS_INTERVAL_MS 2000
#define PLOT_DOWNSAMPLE 1

typedef enum {
MODE_RAW,
MODE_CALIBRATED,
MODE_CENTERED,
MODE_FILTERED,
MODE_COUNT
} DisplayMode;

static const char* MODE_NAMES[] = {
"RAW (ADC units)",
"CALIBRATED (mV)",
"CENTERED (mV from baseline)",
"FILTERED (mV, IIR low-pass)"
};

static esp_adc_cal_characteristics_t adcChars;
static int16_t baselineOffset = 0;
static float filteredSample = 0;

static int32_t statsMin = 32767;
static int32_t statsMax = -32768;
static int64_t statsSum = 0;
static int32_t statsCount = 0;
static int32_t statsSumSq = 0;
static unsigned long lastStatsTime = 0;

static DisplayMode currentMode = MODE_FILTERED;
static bool streaming = true;
static bool printHeader = true;

static void calibrateBaseline() {
Serial.println("[CAL] Calibrating baseline (keep muscle relaxed)...");
int64_t sum = 0;
for (int i = 0; i < BASELINE_CAL_SAMPLES; i++) {
sum += esp_adc_cal_raw_to_voltage(analogRead(EMG_PIN), &adcChars);
delay(1);
if (i % 100 == 0) {
Serial.print(" ");
Serial.print(i);
Serial.print("/");
Serial.println(BASELINE_CAL_SAMPLES);
}
}
baselineOffset = (int16_t)(sum / BASELINE_CAL_SAMPLES);
filteredSample = 0;
Serial.print("[CAL] Baseline: ");
Serial.print(baselineOffset);
Serial.println(" mV");
Serial.println("[CAL] Done. Streaming started.");
Serial.println();
}

static void printStats() {
if (statsCount == 0) return;

float mean = (float)statsSum / statsCount;
float rms = sqrtf((float)statsSumSq / statsCount);

Serial.println("┌─────────────────────────────────────────┐");
Serial.print("│ MODE: ");
Serial.print(MODE_NAMES[currentMode]);
Serial.println();

switch (currentMode) {
case MODE_RAW:
Serial.print("│ To mV: raw × 3300/4096 = ");
Serial.print((float)statsMin * 3300.0 / 4096, 1);
Serial.print(" to ");
Serial.print((float)statsMax * 3300.0 / 4096, 1);
Serial.println(" mV");
break;
case MODE_CALIBRATED:
Serial.print("│ Range: ");
Serial.print(statsMin);
Serial.print(" to ");
Serial.print(statsMax);
Serial.println(" mV");
break;
case MODE_CENTERED:
case MODE_FILTERED:
Serial.print("│ Range: ");
Serial.print(statsMin);
Serial.print(" to ");
Serial.print(statsMax);
Serial.println(" mV (centered)");
break;
case MODE_COUNT:
break;
}

Serial.print("│ Min: ");
Serial.println(statsMin);
Serial.print("│ Max: ");
Serial.println(statsMax);
Serial.print("│ Mean: ");
Serial.println(mean, 2);
Serial.print("│ RMS: ");
Serial.println(rms, 2);
Serial.print("│ P-P: ");
Serial.print(statsMax - statsMin);
Serial.println();
Serial.print("│ Samples: ");
Serial.println(statsCount);
Serial.println("└─────────────────────────────────────────┘");
Serial.println();

statsMin = 32767;
statsMax = -32768;
statsSum = 0;
statsSumSq = 0;
statsCount = 0;
}

static void handleSerialInput() {
while (Serial.available()) {
char c = Serial.read();
switch (c) {
case '1': currentMode = MODE_RAW; printHeader = true; break;
case '2': currentMode = MODE_CALIBRATED; printHeader = true; break;
case '3': currentMode = MODE_CENTERED; printHeader = true; break;
case '4': currentMode = MODE_FILTERED; printHeader = true; break;
case 's':
case 'S':
streaming = !streaming;
Serial.print(streaming ? "[STREAMING ON]" : "[STREAMING OFF]");
Serial.println();
break;
case 'r':
case 'R':
calibrateBaseline();
printHeader = true;
break;
case '?':
case 'h':
case 'H':
Serial.println();
Serial.println("═══════════════════════════════════════");
Serial.println(" EMG Serial Test - Commands");
Serial.println("═══════════════════════════════════════");
Serial.println(" 1 = RAW (ADC 0-4095)");
Serial.println(" 2 = CALIBRATED (mV, esp_adc_cal)");
Serial.println(" 3 = CENTERED (mV - baseline)");
Serial.println(" 4 = FILTERED (mV, IIR low-pass)");
Serial.println(" s = Toggle streaming on/off");
Serial.println(" r = Recalibrate baseline");
Serial.println(" h = This help");
Serial.println("═══════════════════════════════════════");
Serial.print(" Current mode: ");
Serial.println(MODE_NAMES[currentMode]);
Serial.print(" Baseline: ");
Serial.print(baselineOffset);
Serial.println(" mV");
Serial.print(" Filter alpha: ");
Serial.println(FILTER_ALPHA);
Serial.println("═══════════════════════════════════════");
Serial.println();
break;
}
}
}

void setup() {
Serial.begin(115200);
while (!Serial) delay(10);

delay(1000);

Serial.println();
Serial.println("╔═══════════════════════════════════════╗");
Serial.println("║ EMG Click Serial Test (AD8232) ║");
Serial.println("║ MikroElektronika EMG Click ║");
Serial.println("║ ESP32 + GPIO34 (ADC1_CH6) ║");
Serial.println("╚═══════════════════════════════════════╝");
Serial.println();

analogReadResolution(12);
analogSetPinAttenuation(EMG_PIN, ADC_11db);
esp_adc_cal_characterize(ADC_UNIT_1, ADC_ATTEN_DB_11, ADC_WIDTH_BIT_12, 1100, &adcChars);

Serial.print("[ADC] Resolution: 12-bit");
Serial.print(" Attenuation: 11dB (~0-3.6V)");
Serial.print(" eFuse Vref: ");
Serial.print(adcChars.vref);
Serial.println(" mV");
Serial.println();

Serial.println("[PIN] GPIO34 — ADC1 Channel 6 (input-only)");
Serial.println("[RATE] 1000 Hz sampling");
Serial.print("[FILTER] IIR alpha = ");
Serial.println(FILTER_ALPHA);
Serial.println();

Serial.println("Setup complete. Open Serial Plotter (Tools > Serial Plotter)");
Serial.println("for live waveform. Type 'h' for commands.");
Serial.println();

calibrateBaseline();

lastStatsTime = millis();
printHeader = true;
}

void loop() {
handleSerialInput();

int raw = analogRead(EMG_PIN);
int voltageMv = esp_adc_cal_raw_to_voltage(raw, &adcChars);
int16_t centered = (int16_t)(voltageMv - baselineOffset);

filteredSample = FILTER_ALPHA * (float)centered + (1.0 - FILTER_ALPHA) * filteredSample;
int16_t filtered = (int16_t)filteredSample;

int32_t displayValue;
switch (currentMode) {
case MODE_RAW: displayValue = raw; break;
case MODE_CALIBRATED: displayValue = voltageMv; break;
case MODE_CENTERED: displayValue = centered; break;
case MODE_FILTERED: displayValue = filtered; break;
default: displayValue = filtered; break;
}

if (streaming) {
if (printHeader) {
Serial.print("# Mode: ");
Serial.print(MODE_NAMES[currentMode]);
Serial.print(" | Baseline: ");
Serial.print(baselineOffset);
Serial.println(" mV");
printHeader = false;
}

Serial.println(displayValue);
}

statsMin = min(statsMin, displayValue);
statsMax = max(statsMax, displayValue);
statsSum += displayValue;
statsSumSq += (int32_t)displayValue * displayValue;
statsCount++;

unsigned long now = millis();
if (now - lastStatsTime >= STATS_INTERVAL_MS) {
lastStatsTime = now;
printStats();
}

delayMicroseconds(1000);
}
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@
"react": "19.2.0",
"react-dom": "19.2.0",
"react-native": "0.83.4",
"react-native-ble-plx": "^3.2.0",
"react-native-ble-plx": "^3.5.1",
"react-native-gesture-handler": "~2.30.1",
"react-native-reanimated": "~4.2.1",
"react-native-safe-area-context": "~5.6.2",
Expand Down
10 changes: 5 additions & 5 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.