diff --git a/arduino/tests/emg_serial_test/emg_serial_test.ino b/arduino/tests/emg_serial_test/emg_serial_test.ino new file mode 100644 index 0000000..edce673 --- /dev/null +++ b/arduino/tests/emg_serial_test/emg_serial_test.ino @@ -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); +} diff --git a/package.json b/package.json index 01e3cf0..386481c 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 52bd311..01f6402 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -84,8 +84,8 @@ importers: specifier: 0.83.4 version: 0.83.4(@babel/core@7.28.4)(@types/react@19.2.14)(react@19.2.0) react-native-ble-plx: - specifier: ^3.2.0 - version: 3.5.0(react-native@0.83.4(@babel/core@7.28.4)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0) + specifier: ^3.5.1 + version: 3.5.1(react-native@0.83.4(@babel/core@7.28.4)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0) react-native-gesture-handler: specifier: ~2.30.1 version: 2.30.1(react-native@0.83.4(@babel/core@7.28.4)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0) @@ -3582,8 +3582,8 @@ packages: react-is@19.2.0: resolution: {integrity: sha512-x3Ax3kNSMIIkyVYhWPyO09bu0uttcAIoecO/um/rKGQ4EltYWVYtyiGkS/3xMynrbVQdS69Jhlv8FXUEZehlzA==} - react-native-ble-plx@3.5.0: - resolution: {integrity: sha512-PeSnRswHLwLRVMQkOfDaRICtrGmo94WGKhlSC09XmHlqX2EuYgH+vNJpGcLkd8lyiYpEJyf8wlFAdj9Akliwmw==} + react-native-ble-plx@3.5.1: + resolution: {integrity: sha512-SxksmrUt9jG6DOarrrdkb5c/HBLSfZOKauo/9VQSSi3WJA4bmF78GkrtXrgSoGNk0m1ksacFTjB5DuL39xZq/g==} engines: {node: '>= 18.0.0'} peerDependencies: react: '*' @@ -8540,7 +8540,7 @@ snapshots: react-is@19.2.0: {} - react-native-ble-plx@3.5.0(react-native@0.83.4(@babel/core@7.28.4)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0): + react-native-ble-plx@3.5.1(react-native@0.83.4(@babel/core@7.28.4)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0): dependencies: react: 19.2.0 react-native: 0.83.4(@babel/core@7.28.4)(@types/react@19.2.14)(react@19.2.0)