Community project

Parkinson’s Tremor Detection Pen

ESP32
Photo of Parkinson’s Tremor Detection Pen
Generated with AI

chat gtp

Published October 10, 2026

This guide builds a portable tremor detection pen that monitors hand movement patterns to support Parkinson's disease assessment. The device uses a 6-axis motion sensor to capture acceleration and rotation data, a force-sensing resistor to measure grip pressure, and an OLED display to show real-time readings. The ESP32 microcontroller processes sensor data and stores measurements for later analysis.

Readers will receive a complete parts list, wiring diagram showing all sensor and control connections, step-by-step assembly instructions, and the full Arduino firmware needed to run the device. The guide covers building the battery power chain with charging protection, calibrating the motion sensor, and testing the display and buttons before first use.

Wiring diagram

Wiring diagram for Parkinson’s Tremor Detection Pen

Gather all the parts

QtyComponent
1

DFRobot SEN0142 Fermion MPU-6050 6 DOF Sensor Breakout

DFRobot SEN0142 MPU-6050 breakout with 3-5 V board input, I2C interface, onboard I2C pull-ups, and i2cdevlib Arduino example coverage.

1

SSD1306 OLED

0.96 inch 128x64 OLED display with I2C interface

1

Push Button

Record/start button

Momentary push button switch

1

Push Button

Event/stop button

Momentary push button switch

1

Buzzer

Piezo buzzer for sound output

1

Interlink FSR 402 force-sensing resistor

A thin two-terminal pressure sensor that changes resistance when the pen is gripped.

1

10 kΩ resistor

10 kΩ, 1/4 W

A fixed resistor that forms the safe pressure-measurement circuit with the force sensor.

1

TP4056 Li-Ion/LiPo charger module with protection

TP4056 single-cell Li-Ion/LiPo linear charger module, 5V USB input, 1A charge current (programmable). Common variants ship with DW01 protection. Pair with battery_lipo_storage for the cell.

1

LiPo 3.7V 1000mAh Battery

Single-cell LiPo pack, nominal 3.7 V, 1000 mAh. Default rechargeable choice for portable ESP32 / Pico projects. Pair with a TP4056 charger for safe USB recharging.

1

MT3608 adjustable boost converter module

A preassembled adjustable converter that raises the battery voltage to a regulated 5 V for the ESP32 board.

Assemble it in 6 steps

1. Place the electronics safely

Keep the ESP32, charger, boost module, and battery outside the narrow writing grip at first. Mount the MPU6050 firmly near the pen tip and tape the round sensing area of the FSR402 where the user naturally grips the pen. A loose motion sensor measures casing movement instead of the writing motion.

  • Use short, flexible wires along the pen body so they do not pull on the sensor while writing.
  • The OLED, two buttons, and buzzer can sit on a small handle enclosure or test board.
  • Do not puncture, bend sharply, crush, or solder directly onto the LiPo battery — damage can cause it to overheat or catch fire.

2. Make the 3.3 V sensor connections

With the ESP32 unplugged, connect MPU6050 VIN to ESP32 3V3 (power), MPU6050 GND to ESP32 GND (ground), MPU6050 SDA to GPIO21 (data), and MPU6050 SCL to GPIO22 (clock). Connect OLED VCC to ESP32 3V3 (power), OLED GND to ESP32 GND (ground), OLED SDA to GPIO21 (data), and OLED SCL to GPIO22 (clock).

  • Both the screen and motion sensor share the same two data wires; this is normal.
  • Keep the sensor and OLED on 3.3 V so their data wires remain safe for the ESP32.
  • Make sure VCC and GND are not swapped — swapped power can damage the screen or motion sensor.

3. Wire the grip-pressure sensor

Connect FSR402 Pin 1 to ESP32 3V3 (power). Connect FSR402 Pin 2 to GPIO34 (pressure signal) and also to one lead of the 10 kΩ resistor (signal junction). Connect the resistor's other lead to ESP32 GND (ground).

  • GPIO34 is input-only, which is exactly what this pressure measurement needs.
  • The FSR's flexible tail is delicate; reinforce it with tape or a connector so repeated writing does not tear it.
  • Do not connect the FSR signal to 5 V — the ESP32 input can be damaged by voltages above 3.3 V.

4. Wire the controls and sounder

Connect the GND pin of the record/start button to ESP32 GND (ground) and its SIGNAL pin to GPIO25 (signal). Connect the GND pin of the event/stop button to ESP32 GND (ground) and its SIGNAL pin to GPIO26 (signal). Connect buzzer GND to ESP32 GND (ground) and buzzer SIGNAL to GPIO27 (sound signal).

  • The firmware treats a quick tap as exactly one press: GPIO25 starts or stops a recording and GPIO26 adds a user-mark event to the USB data stream.
  • If the buzzer has a + marking, connect that marked pin to GPIO27 and its other pin to GND.
  • Do not use a large high-current buzzer directly from GPIO27; use only a small logic-level piezo/active buzzer like the listed part.

5. Build the protected battery power chain

Connect battery +V to TP4056 B+ (battery positive) and battery GND to TP4056 B- (battery negative). Connect TP4056 OUT+ to boost-module IN+ (protected battery power) and TP4056 OUT- to boost-module IN- (protected ground). Before connecting the ESP32, power the boost module from the battery and adjust it with a meter until OUT+ to OUT- reads exactly 5.0 V. Then connect boost OUT+ to ESP32 VIN (5 V power) and boost OUT- to ESP32 GND (ground). Connect the TP4056 IN+ and IN- only to its own 5 V USB charging cable (charging power).

  • Use a TP4056 module with B+/B-/OUT+/OUT- pads, because the OUT pads provide the module's protection path.
  • The ESP32's normal USB connector remains the data link to the Python application.
  • Never connect the TP4056 charging input to the ESP32 3V3 pin or directly to the battery.
  • Set the boost output to exactly 5.0 V before connecting VIN — a higher setting can damage the ESP32.
  • Do not attempt to charge a non-rechargeable battery or an unprotected bare cell.

6. Connect the data cable and check the startup screen

Plug the ESP32's normal USB data cable into the computer running the Python CNN application. This cable provides USB serial communication at 115200 baud. After flashing, the screen should say MPU ready; press GPIO25's button to begin sending 50 motion-and-grip JSON readings each second, and press GPIO26's button to add a user-mark line.

  • The Python application should read one JSON object per line and use only sample lines that contain ax, ay, az, gx, gy, gz, and fsr.
  • This device collects movement and grip features for research or screening support; its output must not be used alone to diagnose Parkinson's disease.
  • Do not connect the computer USB cable and the TP4056 charging cable to an unverified shared power setup until you have checked that all grounds and voltages are correct.

Review all connections

1. Connections between "mpu6050_1" and "ESP32"

Functionmpu6050_1ESP32
powerVIN3V3
groundGNDGND
i2cSDAGPIO 21
i2cSCLGPIO 22

2. Connections between "oled_1" and "ESP32"

Functionoled_1ESP32
powerVCC3V3
groundGNDGND
i2cSDAGPIO 21
i2cSCLGPIO 22

3. Connections between "start_button" and "ESP32"

Functionstart_buttonESP32
groundGNDGND
digitalSIGNALGPIO 25

4. Connections between "mark_button" and "ESP32"

Functionmark_buttonESP32
groundGNDGND
digitalSIGNALGPIO 26

5. Connections between "buzzer_1" and "ESP32"

Functionbuzzer_1ESP32
groundGNDGND
digitalSIGNALGPIO 27

6. Connections between "fsr_1" and "ESP32"

Functionfsr_1ESP32
powerPin 13V3
analogPin 2GPIO 34

7. Connections between "fsr_resistor" and "ESP32"

Functionfsr_resistorESP32
analogLead 1 → Interlink FSR 402 force-sensing resistor Pin 2EXT
groundLead 2GND

8. Connections between "tp4056_1" and "ESP32"

Functiontp4056_1ESP32
powerIN+ → 5 V USB charging cable positiveEXT
groundIN- → 5 V USB charging cable groundEXT
powerOUT+ → MT3608 adjustable boost converter module IN+EXT
groundOUT- → MT3608 adjustable boost converter module IN-EXT

9. Connections between "battery_1" and "ESP32"

Functionbattery_1ESP32
power+V → TP4056 Li-Ion/LiPo charger module with protection B+EXT
groundGND → TP4056 Li-Ion/LiPo charger module with protection B-EXT

10. Connections between "boost_1" and "ESP32"

Functionboost_1ESP32
powerOUT+VIN
groundOUT-GND

Deploy the firmware

#include <Arduino.h>
#include <Wire.h>
#include <Adafruit_MPU6050.h>
#include <Adafruit_Sensor.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>

constexpr int I2C_SDA_PIN = 21;
constexpr int I2C_SCL_PIN = 22;
constexpr int START_BUTTON_PIN = 25;
constexpr int MARK_BUTTON_PIN = 26;
constexpr int BUZZER_PIN = 27;
constexpr int FSR_PIN = 34;
constexpr uint8_t OLED_ADDRESS = 0x3C;
constexpr int SCREEN_WIDTH = 128;
constexpr int SCREEN_HEIGHT = 64;
constexpr uint32_t SAMPLE_INTERVAL_MS = 20;
constexpr uint32_t DISPLAY_INTERVAL_MS = 250;
constexpr uint32_t DEBOUNCE_MS = 35;

Adafruit_MPU6050 mpu;
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, -1);

bool recording = false;
bool mpuReady = false;
float ax = 0.0f, ay = 0.0f, az = 0.0f, gx = 0.0f, gy = 0.0f, gz = 0.0f;
int fsrRaw = 0;
uint32_t lastSampleMs = 0;
uint32_t lastDisplayMs = 0;
uint32_t sampleNumber = 0;

bool lastStartReading = HIGH;
bool stableStartState = HIGH;
uint32_t startChangedMs = 0;
bool lastMarkReading = HIGH;
bool stableMarkState = HIGH;
uint32_t markChangedMs = 0;

void beep(uint16_t frequency, uint16_t durationMs) {
  ledcWriteTone(BUZZER_PIN, frequency);
  delay(durationMs);
  ledcWriteTone(BUZZER_PIN, 0);
}

void drawStatus() {
  display.clearDisplay();
  display.setTextColor(SSD1306_WHITE);
  display.setTextSize(1);
  display.setCursor(0, 0);
  display.println("Parkinson's pen");
  display.drawLine(0, 10, SCREEN_WIDTH - 1, 10, SSD1306_WHITE);
  display.setCursor(0, 16);
  display.print("MPU: ");
  display.println(mpuReady ? "ready" : "not found");
  display.setCursor(0, 28);
  display.print("Grip: ");
  display.println(fsrRaw);
  display.setCursor(0, 40);
  display.print("USB CNN: JSON lines");
  display.setCursor(0, 52);
  display.print(recording ? "REC  #" : "READY");
  if (recording) display.print(sampleNumber);
  display.display();
}

void sendHeader() {
  Serial.println("{\"type\":\"smart_pen\",\"format\":\"jsonl\",\"sample_hz\":50,\"units\":{\"accel\":\"m_s2\",\"gyro\":\"rad_s\"},\"fields\":[\"t_ms\",\"ax\",\"ay\",\"az\",\"gx\",\"gy\",\"gz\",\"fsr\"]}");
}

void sendSample(uint32_t now) {
  Serial.print("{\"t_ms\":"); Serial.print(now);
  Serial.print(",\"ax\":"); Serial.print(ax, 5);
  Serial.print(",\"ay\":"); Serial.print(ay, 5);
  Serial.print(",\"az\":"); Serial.print(az, 5);
  Serial.print(",\"gx\":"); Serial.print(gx, 5);
  Serial.print(",\"gy\":"); Serial.print(gy, 5);
  Serial.print(",\"gz\":"); Serial.print(gz, 5);
  Serial.print(",\"fsr\":"); Serial.print(fsrRaw);
  Serial.println("}");
}

bool pressedEvent(int pin, bool &lastReading, bool &stableState, uint32_t &changedMs, uint32_t now) {
  const bool reading = digitalRead(pin);
  if (reading != lastReading) changedMs = now;
  if ((now - changedMs) >= DEBOUNCE_MS && reading != stableState) {
    stableState = reading;
    lastReading = reading;
    return stableState == LOW;
  }
  lastReading = reading;
  return false;
}

void setup() {
  Serial.begin(115200);
  analogReadResolution(12);
  pinMode(START_BUTTON_PIN, INPUT_PULLUP);
  pinMode(MARK_BUTTON_PIN, INPUT_PULLUP);
  ledcAttach(BUZZER_PIN, 2000, 8);
  ledcWriteTone(BUZZER_PIN, 0);

  Wire.begin(I2C_SDA_PIN, I2C_SCL_PIN);
  mpuReady = mpu.begin(0x68, &Wire);
  if (mpuReady) {
    mpu.setAccelerometerRange(MPU6050_RANGE_8_G);
    mpu.setGyroRange(MPU6050_RANGE_500_DEG);
    mpu.setFilterBandwidth(MPU6050_BAND_21_HZ);
  }

  if (!display.begin(SSD1306_SWITCHCAPVCC, OLED_ADDRESS)) {
    Serial.println("{\"type\":\"error\",\"message\":\"OLED not found at 0x3C\"}");
  }
  if (mpuReady) {
    Serial.println("{\"type\":\"status\",\"message\":\"MPU6050 ready\"}");
    beep(2200, 70);
  } else {
    Serial.println("{\"type\":\"error\",\"message\":\"MPU6050 not found at 0x68\"}");
    beep(450, 250);
  }
  sendHeader();
  drawStatus();
}

void loop() {
  const uint32_t now = millis();
  if (pressedEvent(START_BUTTON_PIN, lastStartReading, stableStartState, startChangedMs, now)) {
    recording = !recording;
    if (recording) {
      sampleNumber = 0;
      Serial.println("{\"type\":\"recording\",\"state\":\"started\"}");
      beep(1800, 70);
    } else {
      Serial.println("{\"type\":\"recording\",\"state\":\"stopped\"}");
      beep(900, 120);
    }
    drawStatus();
  }
  if (pressedEvent(MARK_BUTTON_PIN, lastMarkReading, stableMarkState, markChangedMs, now)) {
    Serial.print("{\"type\":\"event\",\"t_ms\":");
    Serial.print(now);
    Serial.println(",\"name\":\"user_mark\"}");
    beep(2600, 45);
  }
  if (recording && now - lastSampleMs >= SAMPLE_INTERVAL_MS) {
    lastSampleMs = now;
    fsrRaw = analogRead(FSR_PIN);
    if (mpuReady) {
      sensors_event_t acceleration, gyroscope, temperature;
      mpu.getEvent(&acceleration, &gyroscope, &temperature);
      ax = acceleration.acceleration.x;
      ay = acceleration.acceleration.y;
      az = acceleration.acceleration.z;
      gx = gyroscope.gyro.x;
      gy = gyroscope.gyro.y;
      gz = gyroscope.gyro.z;
    } else {
      ax = ay = az = gx = gy = gz = 0.0f;
    }
    sendSample(now);
    ++sampleNumber;
  }
  if (now - lastDisplayMs >= DISPLAY_INTERVAL_MS) {
    lastDisplayMs = now;
    if (!recording) fsrRaw = analogRead(FSR_PIN);
    drawStatus();
  }
}

Remix this project

Make it yours in one click

Open a full copy of this project in your own Schematik workspace — diagram, code, parts, and assembly steps included. Swap the sensor, add features, or redesign the whole thing with AI. The author's original stays untouched.

Open in Schematik