Rodney Trusty
Published © GPL3+

Yendor Flex Glove

An e-textile glove that combines an IMU, flex sensors, and touch input to drive reactive LEDs, vibration, and sound.

ExpertFull instructions provided8 hours1,464

Things used in this project

Hardware components

Textile Flex Sensors
×5
Sewable Resistors 110ohm
×5
8 Chan ADC
×1
Sewable MPU6050
×1
Sewable Haptic Motor
×1
Sewable Buzzer
×1
Sewable WS2812 LED
×8
Sewable 4Pos connector
×1
Sewable Xiao SAMD21
×1

Software apps and online services

Digital Fiber Studio CAD

Hand tools and fabrication machines

Industrial Embroidery Machine

Story

Read more

Custom parts and enclosures

Yendor Flex Glove

CAD View in Digital Fiber Studio

Schematics

Yendor Flex Glove

Textile Circuit Diagram

Code

Processing Visualizer

Processing
A GUI that displays the IMU and Flex Sensor Values from the glove
import processing.serial.*;

Serial myPort;
Serial myPressurePort;

boolean useSimulation = false;
int[] rValues = new int[8];

// Sensor values
float yaw = 0, pitch = 0, roll = 0;
float accelX = 0, accelY = 0, accelZ = 0;
int gyroX = 0, gyroY = 0, gyroZ = 0;
int[] fingerRaw = {0, 0, 0, 0, 0};
float[] fingerPercent = {0, 0, 0, 0, 0};
int[] fingerMin = {4096, 4096, 4096, 4096, 4096};
int[] fingerMax = {0, 0, 0, 0, 0};

void setup() {
  fullScreen(P3D); // <-- Use fullscreen

  frameRate(30);
  textAlign(CENTER, CENTER);

  if (!useSimulation) {
    printArray(Serial.list());
    myPort = new Serial(this, "COM8", 115200);
    myPort.bufferUntil('\n');
    myPressurePort = new Serial(this, "COM6", 115200);
    myPressurePort.bufferUntil('\n');
  }
}

void draw() {
  background(20);

  pushMatrix();
  translate(-100, 25);
  scale(1.25); 
  drawTorsoPanel();
  popMatrix();

  pushMatrix();
  translate(-150, 25);
  scale(1.25);          // Scale everything inside this block by 1.5×
  drawSensorGraphics();
  popMatrix();
}

// ---------- Left Side: Torso Panel ----------
void drawTorsoPanel() {
  fill(100, 200, 255);
  textSize(48);
 // text("Digital Fiber", width / 4, 50);

  int pecW = 180, pecH = 180;
  int abW = 100, abH = 100;
  int sideW = 40, gap = 30;

  int centerX = width / 4;
  int pecY = 80;
  int leftPecX = centerX - (pecW + gap / 2);
  int rightPecX = centerX + (gap / 2);
  drawBox(leftPecX, pecY, pecW, pecH, rValues[0]);
  drawBox(rightPecX, pecY, pecW, pecH, rValues[1]);

  int absStartY = pecY + pecH + 60;
  int absXLeft = centerX - (abW + gap / 2);
  int absXRight = centerX + (gap / 2);
  int sideXLeft = absXLeft - sideW - 15;
  int sideXRight = absXRight + abW + 15;
  int absTotalHeight = (3 * abH) + (2 * gap);
  int absCenterY = absStartY;

  drawSidePanel(sideXLeft, absCenterY, sideW, absTotalHeight);
  drawSidePanel(sideXRight, absCenterY, sideW, absTotalHeight);

  for (int i = 0; i < 6; i++) {
    int row = i / 2;
    int col = i % 2;
    int x = (col == 0) ? absXLeft : absXRight;
    int y = absStartY + row * (abH + gap);
    drawBox(x, y, abW, abH, rValues[i + 2]);
  }
}

void drawBox(int x, int y, int w, int h, int brightness) {
  fill(100, 200, 255, brightness);
  rect(x, y, w, h, 20);
}

void drawSidePanel(int x, int y, int w, int h) {
  pushMatrix();
  translate(x + w / 2, y + h / 2);
  if (x > width / 2) scale(-1, 1);
  stroke(100, 200, 255, 150);
  strokeWeight(2);
  noFill();
  int steps = 6;
  float stepY = h / float(steps);
  float zigX = w * 0.6;
  beginShape();
  for (int i = 0; i <= steps; i++) {
    float yStep = -h / 2 + i * stepY;
    float xStep = (i % 2 == 0) ? -zigX / 2 : zigX / 2;
    vertex(xStep, yStep);
  }
  endShape();
  popMatrix();
}

// ---------- Right Side: Sensor Panel ----------
void drawSensorGraphics() {
  int baseY = 50;

  // Yaw/Pitch/Roll sphere
  pushMatrix();
  translate(width * .62, baseY + 100, 0);
  scale(1.5);  

  drawYawPitchRoll(yaw, pitch, roll);
  popMatrix();

  // Finger bars
  drawFingerBars(width / 2 + 60, baseY + 250);

  // Accel and Gyro
  drawAccelGyroBars(width / 2 + 60, baseY + 480);
}

void drawYawPitchRoll(float yaw, float pitch, float roll) {
  rotateX(radians(-pitch));
  rotateY(radians(yaw));
  rotateZ(radians(roll));
  strokeWeight(2);
  stroke(255, 0, 0); line(0, 0, 0, 100, 0, 0);
  stroke(0, 255, 0); line(0, 0, 0, 0, 100, 0);
  stroke(0, 0, 255); line(0, 0, 0, 0, 0, 100);
  fill(150, 150, 255, 100);
  sphere(50);
  fill(255);
  textSize(16);
  text("Yaw: " + nf(yaw, 1, 1), 0, -100);
  text("Pitch: " + nf(pitch, 1, 1), 0, -80);
  text("Roll: " + nf(roll, 1, 1), 0, -60);
}

void drawFingerBars(int xOffset, int yBase) {
  int barWidth = 60, spacing = 20, maxH = 120;

  for (int i = 0; i < 5; i++) {
    float h = map(fingerPercent[i], 0, 100, 0, maxH);
    fill(100, 200, 255);
    rect(xOffset + i * (barWidth + spacing), yBase + maxH - h, barWidth, h);
    fill(255);
    text("F" + (i + 1), xOffset + i * (barWidth + spacing) + barWidth / 2, yBase + maxH + 20);
    text(nf(fingerPercent[i], 0, 1) + "%", xOffset + i * (barWidth + spacing) + barWidth / 2, yBase + maxH + 40);
  }
}

void drawAccelGyroBars(int xOffset, int yBase) {
  int barWidth = 50, spacing = 15, maxH = 100;
  float[] a = {accelX, accelY, accelZ};
  int[] g = {gyroX, gyroY, gyroZ};
  String[] labels = {"AX", "AY", "AZ", "GX", "GY", "GZ"};

  for (int i = 0; i < 3; i++) {
    float h = map(a[i], -10, 20, 0, maxH);
    fill(255, 100, 100);
    rect(xOffset + i * (barWidth + spacing), yBase + maxH - h, barWidth, h);
    fill(255);
    text(labels[i], xOffset + i * (barWidth + spacing) + barWidth / 2, yBase + maxH + 20);
    text(nf(a[i], 1, 1), xOffset + i * (barWidth + spacing) + barWidth / 2, yBase + maxH + 40);
  }

  for (int i = 0; i < 3; i++) {
    float h = map(g[i], -200, 200, 0, maxH);
    fill(100, 255, 100);
    rect(xOffset + (i + 3) * (barWidth + spacing), yBase + maxH - h, barWidth, h);
    fill(255);
    text(labels[i + 3], xOffset + (i + 3) * (barWidth + spacing) + barWidth / 2, yBase + maxH + 20);
    text(g[i], xOffset + (i + 3) * (barWidth + spacing) + barWidth / 2, yBase + maxH + 40);
  }
}

// ---------- Serial ----------
void serialEvent(Serial port) {
  String line = trim(port.readStringUntil('\n'));
  if (line == null) return;

  if (port == myPressurePort) {
    String[] tokens = split(line, ' ');
    if (tokens.length == 8) {
      for (int i = 0; i < 8; i++) rValues[i] = constrain(int(tokens[i]), 0, 255);
    }
  } else if (port == myPort) {
    String[] values = split(line, ',');
    if (values.length == 14) {
      try {
        yaw = float(values[0]);
        pitch = float(values[1]);
        roll = float(values[2]);
        accelX = float(values[3]);
        accelY = float(values[4]);
        accelZ = float(values[5]);
        gyroX = int(values[6]);
        gyroY = int(values[7]);
        gyroZ = int(values[8]);
        for (int i = 0; i < 5; i++) {
          int val = int(values[9 + i]);
          fingerRaw[i] = val;
          if (val < fingerMin[i] && val > 50) fingerMin[i] = val;
          if (val > fingerMax[i] && val < 4095) fingerMax[i] = val;
          fingerPercent[i] = constrain(map(val, fingerMin[i], fingerMax[i], 0, 100), 0, 100);
        }
      } catch (Exception e) {
        println("Sensor parse error: " + e.getMessage());
      }
    }
  }
}

Xiao MCU Code

Arduino
Reads all sensors and reports them to Processing.
#include <Adafruit_NeoPixel.h>
#include <Adafruit_ADS1X15.h>
#include "I2Cdev.h"
#include "MPU6050_6Axis_MotionApps20.h"

// ========== Pin and Sensor Configuration ==========
#define NEOPIXEL_PIN  D6
#define NUM_PIXELS    8
#define CAP_SENSOR    D10
#define BUZZER_PIN    D1
#define VIBRATOR_PIN  D3
#define TRIG_PIN      D8
#define ECHO_PIN      D9

#define CAP_THRESHOLD 200
#define ACCEL_SENSITIVITY 16384.0

// ========== NeoPixel and Sensor Objects ==========
Adafruit_NeoPixel pixels(NUM_PIXELS, NEOPIXEL_PIN, NEO_GRB + NEO_KHZ800);
MPU6050 mpu;
Adafruit_ADS1015 ads1;
Adafruit_ADS1015 ads2;

// ========== Sensor Data Storage ==========
float ypr[3] = {0, 0, 0};        // Yaw, Pitch, Roll
VectorInt16 accel, gyro;         // Accelerometer and Gyro
Quaternion q;
VectorFloat gravity;
int16_t adcValues[5];            // Raw finger sensor values
int capValue = 0;
int distance = 0;

// ========== MPU6050 DMP ==========
bool dmpReady = false;
uint8_t mpuIntStatus;
uint16_t packetSize;
uint8_t fifoBuffer[64];
volatile bool mpuInterrupt = false;
void dmpDataReady() { mpuInterrupt = true; }

// ========== Timing ==========
unsigned long previousMillisLED = 0;
unsigned long previousMillisBuzz = 0;
unsigned long previousMillisVibrate = 0;
unsigned long previousMillisUltrasonic = 0;
unsigned long buzzStartTime = 0;
unsigned long vibrateStartTime = 0;

const long ledInterval = 50;
const long buzzInterval = 2000;
const long buzzDuration = 200;
const long vibrateInterval = 4000;
const long vibrateDuration = 125;
const long ultrasonicInterval = 500;

bool isBuzzing = false;
bool isVibrating = false;
float pulsePosition = 0.0;
bool forward = true;

// ========== Setup ==========
void setup() {
  Serial.begin(115200);
  while (!Serial && millis() < 2000);
  Serial.println("Start");
  delay(1000);
  Serial.println("Start");

  pinMode(BUZZER_PIN, OUTPUT);
  pinMode(VIBRATOR_PIN, OUTPUT);
  pinMode(TRIG_PIN, OUTPUT);
  pinMode(ECHO_PIN, INPUT);
  pinMode(CAP_SENSOR, INPUT);
  pinMode(D2, OUTPUT); digitalWrite(D2, HIGH); // Cap power

  pixels.begin();
  pixels.show();

  Wire.begin();
  Wire.setClock(400000);

  mpu.initialize();
  if (!mpu.testConnection()) {
    Serial.println(F("MPU6050 connection failed!"));
    while (1);
  }

  uint8_t dmpStatus = mpu.dmpInitialize();
  if (dmpStatus != 0) {
    Serial.print(F("DMP failed (code ")); Serial.print(dmpStatus); Serial.println(")");
    while (1);
  }

  mpu.setXGyroOffset(0);
  mpu.setYGyroOffset(0);
  mpu.setZGyroOffset(0);
  mpu.setXAccelOffset(0);
  mpu.setYAccelOffset(0);
  mpu.setZAccelOffset(0);

  mpu.setDMPEnabled(true);
  attachInterrupt(digitalPinToInterrupt(2), dmpDataReady, RISING);
  mpuIntStatus = mpu.getIntStatus();
  dmpReady = true;
  packetSize = mpu.dmpGetFIFOPacketSize();

  if (!ads1.begin(0x48)) Serial.println(F("Failed to init ADS1 (0x48)"));
  if (!ads2.begin(0x49)) Serial.println(F("Failed to init ADS2 (0x49)"));
}

// ========== Loop ==========
void loop() {
  if (dmpReady && mpu.dmpGetCurrentFIFOPacket(fifoBuffer)) {
    mpu.dmpGetQuaternion(&q, fifoBuffer);
    mpu.dmpGetGravity(&gravity, &q);
    mpu.dmpGetYawPitchRoll(ypr, &q, &gravity);
    for (int i = 0; i < 3; i++) ypr[i] *= 180 / M_PI;
    mpu.dmpGetAccel(&accel, fifoBuffer);
    mpu.dmpGetGyro(&gyro, fifoBuffer);
  }

  readCapSensor();
  readUltrasonic();
  readFingerSensors();
  updatePulseLEDs();
  handleBuzzer();
 // handleVibration();
  sendDataToProcessing();
}

// ========== Functions ==========

void readCapSensor() {
  capValue = analogRead(CAP_SENSOR);
}

void readUltrasonic() {
  if (millis() - previousMillisUltrasonic >= ultrasonicInterval) {
    previousMillisUltrasonic = millis();
    digitalWrite(TRIG_PIN, LOW); delayMicroseconds(2);
    digitalWrite(TRIG_PIN, HIGH); delayMicroseconds(10);
    digitalWrite(TRIG_PIN, LOW);
    unsigned long duration = pulseIn(ECHO_PIN, HIGH, 30000);
    distance = duration * 0.034 / 2;
  }
}

void readFingerSensors() {
  adcValues[0] = ads1.readADC_SingleEnded(1);
  adcValues[1] = ads1.readADC_SingleEnded(2);
  adcValues[2] = ads1.readADC_SingleEnded(3);
  adcValues[3] = ads2.readADC_SingleEnded(0);
  adcValues[4] = ads2.readADC_SingleEnded(1);
}

void updatePulseLEDs() {
  if (millis() - previousMillisLED < ledInterval) return;
  previousMillisLED = millis();

  pixels.clear();
  uint8_t baseRed = (capValue < CAP_THRESHOLD) ? 255 : 0;
  uint8_t baseBlue = (capValue < CAP_THRESHOLD) ? 0 : 255;

  for (int i = 0; i < NUM_PIXELS; i++) {
    float d = abs(i - pulsePosition);
    int brightness = int(150 * exp(-d * 1.2));
    pixels.setPixelColor(i, pixels.Color(
      (brightness * baseRed) / 255,
      0,
      (brightness * baseBlue) / 255
    ));
  }

  pixels.show();

  if (forward) {
    pulsePosition += 0.6;
    if (pulsePosition >= NUM_PIXELS - 1) {
      forward = false;
      tone(BUZZER_PIN, 2000);
      buzzStartTime = millis();
      isBuzzing = true;
    }
  } else {
    pulsePosition -= 0.6;
    if (pulsePosition <= 0) forward = true;
  }
}

void handleBuzzer() {
  if (isBuzzing && millis() - buzzStartTime >= buzzDuration) {
    noTone(BUZZER_PIN);
    isBuzzing = false;
  }
}

void handleVibration() {
  if (!isVibrating && millis() - previousMillisVibrate >= vibrateInterval) {
    previousMillisVibrate = millis();
    vibrateStartTime = millis();
    digitalWrite(VIBRATOR_PIN, HIGH);
    isVibrating = true;
  }
  if (isVibrating && millis() - vibrateStartTime >= vibrateDuration) {
    digitalWrite(VIBRATOR_PIN, LOW);
    isVibrating = false;
  }
}
void sendDataToProcessing() {
  Serial.print(ypr[0], 1); Serial.print(",");
  Serial.print(ypr[1], 1); Serial.print(",");
  Serial.print(ypr[2], 1); Serial.print(",");
  Serial.print((float(accel.x) / ACCEL_SENSITIVITY) * 9.81, 2); Serial.print(",");
  Serial.print((float(accel.y) / ACCEL_SENSITIVITY) * 9.81, 2); Serial.print(",");
  Serial.print((float(accel.z) / ACCEL_SENSITIVITY) * 9.81, 2); Serial.print(",");
  Serial.print(gyro.x); Serial.print(",");
  Serial.print(gyro.y); Serial.print(",");
  Serial.print(gyro.z); Serial.print(",");
  for (int i = 0; i < 5; i++) {
    Serial.print(adcValues[i]);
    if (i < 4) Serial.print(",");
  }
  
  Serial.println();
}

Credits

Rodney Trusty
2 projects • 10 followers
Anomaly

Comments