ESP32 IMU Bridge

This project bridges IMU data through an ESP32 to a PC, with optional gripper/relay control, real-time serial monitoring, CSV export, and signal-quality visualization.

Features

  • Reads WIT-format IMU UART frames on the ESP32: acceleration, gyro, angle, and temperature.
  • Streams custom binary packets from the ESP32 to the PC over USB serial at 50 Hz.
  • Allows the PC to control the gripper with simple keyboard commands:
    • o: OPEN
    • c: CLOSE
    • s: STOP
  • Generates statistics, CSV files, and plots from either live serial data or saved text logs.
  • Estimates world-frame linear acceleration and velocity from IMU acceleration and Euler angles.
  • Uses a physical switch on GPIO27 to trigger one-shot gripper open/close pulses.

Files

File Description
main.py Main MicroPython program for the ESP32. It reads the IMU, controls DIO, and streams USB serial packets.
esp_bridge.py PC-side Python wrapper class for integrating the ESP32 bridge into other programs.
pc_reader.py PC-side real-time serial reader with keyboard gripper commands.
visualise.py Sampling, statistics, CSV export, plotting, linear-acceleration estimation, and velocity integration tool.
test.py ESP32 UART pin scan/debug helper.
imu_data.csv Example or exported IMU sample data.
rcd.txt Example text log from pc_reader.py.
imu_quality*.png Visualization output images.

Hardware Wiring

IMU UART

IMU ESP32
TX yellow wire GPIO16 / RX2
RX green wire GPIO17 / TX2
GND GND

The default IMU baud rate is 921600.

Digital Inputs

Default digital input pins in main.py:

DIN_PINS = (25, 26, 27, 32)

These inputs use Pin.PULL_UP, so normally:

  • floating/open reads as 1
  • connected to ground/closed reads as 0

GPIO27 is used as the physical toggle switch:

  • OFF -> ON: triggers a CLOSE pulse
  • ON -> OFF: triggers an OPEN pulse

Digital Outputs / Relay

Default output pins in main.py:

DOUT_PINS = (18, 19)

The relay is low-level triggered:

  • GPIO HIGH: relay off
  • GPIO LOW: relay on

Current output logic:

  • GPIO18 / IN1 LOW: CLOSE
  • GPIO19 / IN2 LOW: OPEN
  • both GPIOs HIGH: STOP

ESP32 Setup

  1. Flash MicroPython firmware to the ESP32.
  2. Upload main.py to the ESP32 root filesystem.
  3. Reset the ESP32.

After reset, the program stays quiet for about 3 seconds, then starts streaming binary packets over USB serial.

Example upload commands:

mpremote connect /dev/ttyUSB0 fs cp main.py :main.py
mpremote connect /dev/ttyUSB0 reset

Replace /dev/ttyUSB0 with your actual ESP32 serial port if needed.

PC Environment

Python 3 is recommended.

Install dependencies:

python3 -m pip install pyserial matplotlib numpy

If you only use pc_reader.py or esp_bridge.py, pyserial is enough.

Real-Time Reading And Control

Run:

python3 pc_reader.py /dev/ttyUSB0 --baud 115200

The output should look similar to:

#     793   50.0Hz DIN=[0, 1, 0, 0] DOUT=[1, 0] ACC=[...] GYRO=[...] ANGLE=[...]

Type a command in the terminal and press Enter:

o
c
s

These commands mean OPEN, CLOSE, and STOP.

Python API Usage

esp_bridge.py provides an ESP32Bridge class for use in other PC-side Python programs:

from esp_bridge import ESP32Bridge

with ESP32Bridge("/dev/ttyUSB0") as bridge:
    bridge.open_gripper()
    data = bridge.read_packet_with_frequency()
    print(data)
    bridge.stop_gripper()

Continuous reading example:

from esp_bridge import ESP32Bridge

with ESP32Bridge("/dev/ttyUSB0") as bridge:
    for packet in bridge.iter_packets(seconds=10):
        print(packet["freq_hz"], packet["acc_g"], packet["angle_deg"])

Sampling, Export, And Visualization

Sample from serial for 10 seconds, save a CSV file, and generate a plot:

python3 visualise.py --port /dev/ttyUSB0 --baud 115200 --seconds 10 --csv imu_data.csv --output imu_quality.png

Save files only without opening a plot window:

python3 visualise.py --port /dev/ttyUSB0 --baud 115200 --seconds 10 --csv imu_data.csv --output imu_quality.png --no-show

Read from a pc_reader.py text log and plot it:

python3 visualise.py --file rcd.txt --output imu_quality.png

visualise.py now adds derived motion columns before saving or plotting:

Column Description
lin_acc_x_ms2, lin_acc_y_ms2, lin_acc_z_ms2 Estimated world-frame linear acceleration in m/s^2 after gravity and bias compensation.
vel_x_ms, vel_y_ms, vel_z_ms Estimated world-frame velocity in m/s from integrating linear acceleration.

The generated plot contains six stacked views:

  • raw acceleration
  • linear acceleration
  • velocity
  • gyro
  • angle
  • packet frequency

CSV files are saved under csv/, PNG files are saved under png/, and other output extensions are saved under output/. If the target file already exists, visualise.py appends a timestamp to avoid overwriting it, for example:

png/imu_quality_20260701_131902.png

Linear Acceleration And Velocity

The raw accelerometer output includes both motion acceleration and the support-force/gravity-related acceleration measured by the IMU. When the sensor is placed horizontally and kept still, it will still measure about 1g on the vertical axis. This is expected: the table support force prevents free fall, and the accelerometer senses that proper acceleration.

Because of this, visualise.py removes the static 1g component before integrating velocity. The current calculation is:

  1. Read raw acceleration in the IMU/body frame: acc_x_g, acc_y_g, acc_z_g.

  2. Convert Euler angles to a body-to-world rotation matrix:

    R = Rz(yaw) @ Ry(pitch) @ Rx(roll)
    
  3. Rotate body-frame acceleration into the world frame:

    acc_world_g = R @ acc_body_g
    
  4. Subtract the world Z-axis support-force/gravity component:

    linear_acc_world_g = acc_world_g - [0, 0, gravity_sign]
    

    The default call uses gravity_sign=1.0, so a horizontally placed, still sensor is expected to have approximately +1g on the world Z axis before compensation.

  5. Convert from g to m/s^2 with 9.81.

  6. Estimate a small acceleration bias from the first bias_seconds seconds, default 1.0 second, and subtract it.

  7. Apply an acceleration deadband, default 0.15 m/s^2, to reduce small stationary noise.

  8. Integrate acceleration to velocity with a small decay factor, default vel_decay=0.995, to limit drift.

This velocity estimate is useful for short-duration motion checks and relative comparisons. It will drift over time because low-cost IMU acceleration, attitude error, and numerical integration all accumulate error. Keep the sensor still for the first second when possible so the initial bias estimate is meaningful.

Packet Protocol

The ESP32-to-PC binary packet format is defined in main.py, pc_reader.py, and esp_bridge.py:

PACKET_FORMAT = "<2sBBBI10hB"

Field layout:

Field Type Description
sync 2s Fixed bytes A5 5A
version B Currently 2
din_mask B 4-channel digital input bit mask
dout_mask B 2-channel digital output bit mask
time_ms I ESP32 ticks_ms() value
raw_values 10h acc xyz, gyro xyz, angle xyz, temperature
checksum B Low 8 bits of the sum of all previous bytes

Raw value scaling:

Data Scaling
Acceleration raw * 16 / 32768, unit g
Gyro raw * 2000 / 32768, unit deg/s
Angle raw * 180 / 32768, unit deg
Temperature raw / 100, unit deg C

Common Parameters

The most useful hardware and timing parameters are in main.py:

IMU_RX_PIN = 16
IMU_TX_PIN = 17
DIN_PINS = (25, 26, 27, 32)
DOUT_PINS = (18, 19)
IMU_BAUD = 921600
SEND_PERIOD_MS = 20
SWITCH_PULSE_MS = 2000
USE_REAL_DOUT = True

Before testing a real relay, you can set:

USE_REAL_DOUT = False

In this mode, the PC can still see DOUT state changes, but the ESP32 will not actually drive GPIO18/GPIO19.

Troubleshooting

Serial Port Not Found

On Linux, list possible serial ports with:

ls /dev/ttyUSB* /dev/ttyACM*

If you have permission issues, add your user to the dialout group or temporarily run the serial tool with sudo.

Frequency Is Not 50 Hz

Check the following:

  • The ESP32 has entered the main.py loop.
  • The PC-side baud rate is 115200.
  • The IMU-to-ESP32 UART baud rate is 921600.
  • The USB serial cable and power supply are stable.

Data Is All Zero Or Angle Does Not Update

Check the IMU wiring and output format. The ESP32 parser currently handles WIT frames:

  • 0x51: acceleration
  • 0x52: gyro
  • 0x53: angle

The program treats an angle frame as the point where one full IMU update is complete.

OPEN/CLOSE Direction Is Reversed

Depending on the relay and motor wiring, swap the relay inputs connected to GPIO18/GPIO19, or adjust the OPEN/CLOSE output logic in set_gripper().

Suggested Workflow

  1. Use pc_reader.py to confirm the PC receives stable 50 Hz packets.
  2. Type o, c, and s to confirm DOUT state changes.
  3. Enable real relay output with USE_REAL_DOUT = True when ready.
  4. Use visualise.py to sample data and generate CSV/plot outputs for checking noise, frequency, and angle stability.
Description
No description provided
Readme 2.9 MiB
Languages
Python 100%