Skip to content

12. Skin Electronics

Building on my Open Source Hardware project — where a machine became a tool for reflection and presence — I wanted to move this exploration closer to the body. This week, I looked at skin not just as a surface, but as a sensitive site of touch, perception, and awareness, exploring how skin-based electronics might support somatic awareness and care.

Skin Sync — Skin-Based Electronics for Somatic Interaction — Pattarporn (Porpla) Kittisapkajon

Research

Somantic Healing and the Lack of Touch

Somatic healing addresses the effects of touch deprivation—often referred to as “skin hunger”—by supporting reconnection with the body through intentional, safe touch or self-touch. Touch plays a critical role in nervous system regulation and emotional wellbeing, helping reduce stress and restore a sense of safety.

A lack of touch has been linked to increased anxiety, depression, and social disconnection. Gentle, attuned touch activates the parasympathetic nervous system, releases oxytocin, and lowers stress hormones such as cortisol, supporting both emotional and physical regulation.

While not all somatic practices involve direct contact, some approaches use guided touch to help individuals process experiences that are difficult to access through words alone. When human touch is unavailable or inappropriate, self-touch and other embodied practices—such as movement, pressure, or sensory engagement—can offer alternative ways to meet this fundamental need.

References:

Trauma Recovery & Somatic Touch Therapy. Psychology Today.

The Power of Pleasant Touch. Somatic Therapy Partners.

References & Inspiration

  • Richard Shusterman — Somaesthetics
    Focus on bodily awareness through sensation and self-touch rather than visual or cognitive interaction.
    https://www.shusterman.net/somaesthetics

  • Kristina Höök — Somaesthetic Interaction Design
    Informed the use of slow, responsive feedback that supports presence and emotional attunement over efficiency or performance.
    https://www.digitalfutures.kth.se/project/kristina-hook/

  • Soma Lab (KTH Royal Institute of Technology)
    Reinforced the approach of using body-based interaction and sensory feedback as tools for self-awareness and regulation.
    https://www.youtube.com/watch?v=IwBTNAq8Qy8

  • Erin Manning — The Minor Gesture
    Shaped the use of sound and rhythm to amplify subtle movement rather than control or direct the body.
    https://www.dukeupress.edu/the-minor-gesture

Concept

describe what you see in this image

Ritual Loop of Touch, Sound, and Attention — Pattarporn (Porpla) Kittisapkajon

Process and workflow

describe what you see in this image

System Logic for Continuous Touch — Pattarporn (Porpla) Kittisapkajon

Prototyping

1. Pressure Sensor

I started by making a pressure sensor, following Emma Pareschi’s tutorial. While it worked mechanically, the interaction felt a bit forced. I also had trouble getting a reliable touch threshold in the code—the sensor readings were inconsistent and hard to calibrate.

Because of that, I decided to switch to a capacitive sensor instead. Capacitive sensing felt more natural for touch-based interaction and was much easier to tune in code, especially for detecting subtle contact rather than pressure.

describe what you see in this image

Capacitive Sensing and Pressure Matrix Tutorial Electronics by Emma Pareschi

describe what you see in this image

Pressure Matrix Prototype by Pattaraporn (Porpla) Kittisapkajon

Code Example — Arduino

int row0 = A0;  //first row pin
int row1 = A1;  //second row pin
int row2 = A2;  //third row pin

int col0 = 4;    //first column pin
int col1 = 5;    //second column pin
int col2 = 6;    //third column pin

int incomingValue0 = 0; //variable to save the sensor reading
int incomingValue1 = 0; //variable to save the sensor reading
int incomingValue2 = 0; //variable to save the sensor reading

int incomingValue3 = 0; //variable to save the sensor reading
int incomingValue4 = 0; //variable to save the sensor reading
int incomingValue5 = 0; //variable to save the sensor reading

int incomingValue6 = 0; //variable to save the sensor reading
int incomingValue7 = 0; //variable to save the sensor reading
int incomingValue8 = 0; //variable to save the sensor reading

void setup() {

  // set all rows to INPUT (high impedance):
   pinMode(row0, INPUT_PULLUP);
    pinMode(row1, INPUT_PULLUP);
    pinMode(row2, INPUT_PULLUP);

//set the firt column as output
  pinMode(col0, OUTPUT);
  pinMode(col1, OUTPUT);
  pinMode(col2, OUTPUT);

  //open serial communication
  Serial.begin(9600);

}

void loop() {

  // FIRST BLOCK OF READINGS----------------------------
  //set the col0 to low (GND)
  digitalWrite(col0, LOW);
  digitalWrite(col1, HIGH);
  digitalWrite(col2, HIGH);

  //read the three rows pins
    incomingValue0 = analogRead(row0);
    incomingValue1 = analogRead(row1);
    incomingValue2 = analogRead(row2);
  // --------------------------------------------------

  //set the col1 to low (GND)
  digitalWrite(col0, HIGH);
  digitalWrite(col1, LOW);
  digitalWrite(col2, HIGH);

    incomingValue3 = analogRead(row0);
    incomingValue4 = analogRead(row1);
    incomingValue5 = analogRead(row2);

  //set the col2 to low (GND)
  digitalWrite(col0, HIGH);
  digitalWrite(col1, HIGH);
  digitalWrite(col2, LOW);

    incomingValue6 = analogRead(row0);
    incomingValue7 = analogRead(row1);
    incomingValue8 = analogRead(row2);

  // Print the incoming values of the grid:

    Serial.print(incomingValue0);
    Serial.print("\t");
    Serial.print(incomingValue1);
    Serial.print("\t");
    Serial.print(incomingValue2);
    Serial.print("\t");
    Serial.print(incomingValue3);
    Serial.print("\t");
    Serial.print(incomingValue4);
    Serial.print("\t");
    Serial.print(incomingValue5);
    Serial.print("\t");
    Serial.print(incomingValue6);
    Serial.print("\t");
    Serial.print(incomingValue7);
    Serial.print("\t");
    Serial.println(incomingValue8);

delay(10); //wait millisecond
}

Code Example — Processing

<!doctype html>
<html>
<head>
  <meta charset="utf-8" />
  <title>3x3 Sensor Viz (p5.js + Web Serial)</title>
  <script src="https://cdn.jsdelivr.net/npm/p5@1.9.4/lib/p5.min.js"></script>
</head>
<body>
<script>
let port, reader;
let connectBtn;
const rows = 3, cols = 3;
const maxSensors = rows * cols;
let sensorValue = new Array(maxSensors).fill(0);
let cellSize;

function setup() {
  createCanvas(600, 600);
  cellSize = width / cols;

  connectBtn = createButton("Connect Serial");
  connectBtn.mousePressed(connectSerial);

  textAlign(CENTER, CENTER);
}

function draw() {
  background(0);

  for (let i = 0; i < maxSensors; i++) {
    let col = Math.floor(i / rows);
    let row = i % rows;

    let val = sensorValue[i]; // 0..255
    fill(val);
    noStroke();

    let d = map(val, 0, 255, cellSize, 5);

    ellipse(
      col * cellSize + cellSize / 2,
      row * cellSize + cellSize / 2,
     d, d
    );

    // optional label
    fill(255);
    textSize(12);
    text(i, col * cellSize + cellSize / 2, row * cellSize + cellSize / 2);
     }
}

async function connectSerial() {
  if (!("serial" in navigator)) {
    alert("Web Serial not supported. Use Chrome/Edge.");
    return;
  }
  port = await navigator.serial.requestPort();
  await port.open({ baudRate: 115200 });

  const decoder = new TextDecoderStream();
  port.readable.pipeTo(decoder.writable);
  reader = decoder.readable.getReader();

  readLoop();
}

async function readLoop() {
  let buffer = "";
  while (true) {
    const { value, done } = await reader.read();
    if (done) break;
    buffer += value;

// process full lines
let lines = buffer.split("\n");
buffer = lines.pop(); // keep last partial line

for (const line of lines) {
  const trimmed = line.trim();
  if (!trimmed) continue;
  const parts = trimmed.split("\t").map(x => parseInt(x, 10));
  if (parts.length === maxSensors) {
    for (let i = 0; i < maxSensors; i++) {
      // map ESP32 0..4095 to 0..255
      let mapped = map(parts[i], 0, 4095, 0, 255);
      sensorValue[i] = constrain(mapped, 0, 255);
    }
  }
}
 }
}
</script>
</body>
</html>

2. Capacitive Sensor

describe what you see in this image

Prototype Setup: Body, Sensors, and Ausio Interface — Pattarporn (Porpla) Kittisapkajon

Code Example — Arduino

#include <CapacitiveSensor.h>
#include <Wire.h>
#include <MPU6050.h>

// ======================================================
// SKINSYNC — Capacitive + IMU (UNO) -> Serial for p5.js
//
// Capacitive wiring:
//   send pin 4, receive pin 2, 100k resistor between 4 and 2
//
// IMU wiring (MPU-6050 / GY-521) on UNO:
//   VCC->5V, GND->GND, SDA->A4, SCL->A5
//
// Sends over Serial (9600):
//   T, V, M
//
// T = Touch gate (0 = not touched, 1 = touched)
// V = Smoothed capacitive sensor reading
// M = Motion intensity 0..100 (from IMU acceleration magnitude)
// ======================================================

// ---------- Capacitive sensor ----------
CapacitiveSensor cs_4_2(4, 2);

// ---------- IMU ----------
MPU6050 mpu;

// ---------- Serial ----------
const long BAUD = 9600;

// ---------- Capacitive tuning ----------
const byte  CAP_SAMPLES   = 6;      // lower = faster, noisier (4–10)
const byte  LOOP_DELAY_MS = 5;      // 2–10
const float V_ALPHA       = 0.45;   // smoothing for V (0.30–0.60)

const float BASE_ALPHA      = 0.02; // baseline drift when idle
const long  BASE_LOCK_DELTA = 6;    // only update baseline if delta small

long DELTA_ON  = 10;  // touch ON when delta >= this
long DELTA_OFF = 8;   // touch OFF when delta <= this

// ---------- Motion tuning (IMU) ----------
// M is derived from "linear acceleration magnitude" (approx).
// Larger M means you're moving more.
//
// MOTION_ALPHA smooths motion output (higher = snappier).
const float MOTION_ALPHA = 0.35;  // 0.2–0.6

// How sensitive motion is:
// - Lower MOTION_MIN / lower MOTION_MAX = more sensitive
// - If M sticks low, reduce MOTION_MAX
// Units are in "g" (roughly), because we compute in g's.
float MOTION_MIN_G = 0.02;  // ignore tiny noise
float MOTION_MAX_G = 0.35;  // strong movement -> 100 (TUNE THIS)

// ---------- State ----------
bool  touched = false;
float vSmooth = 0.0;
float base    = 0.0;

// For motion smoothing
float motionSmoothG = 0.0;

// Debug (prints extra values)
const bool DEBUG = false;

// ---------- Helpers ----------
long readCap() {
  return cs_4_2.capacitiveSensor(CAP_SAMPLES);
}

float clampf(float x, float a, float b) {
  if (x < a) return a;
  if (x > b) return b;
  return x;
}

void setup() {
  Serial.begin(BAUD);

  // Disable capacitive autocal so it doesn't fight our baseline logic
  cs_4_2.set_CS_AutocaL_Millis(0xFFFFFFFF);

  // IMU init
  Wire.begin();
  mpu.initialize();

  // Optional: check IMU connection
  if (!mpu.testConnection()) {
    Serial.println("IMU:0 (MPU6050 not found)"); // p5 can ignore this line
  }

  delay(300);

  // Initialize baseline + smoothing for capacitive
  long sum = 0;
  const int N = 30;
  for (int i = 0; i < N; i++) {
    sum += readCap();
    delay(5);
  }
  float initV = sum / (float)N;
  vSmooth = initV;
  base    = initV;

  motionSmoothG = 0.0;
}

void loop() {
  // ======================================================
  // 1) CAPACITIVE READ -> V (smoothed)
  // ======================================================
  long raw = readCap();

  vSmooth = (1.0f - V_ALPHA) * vSmooth + V_ALPHA * (float)raw;
  long V = (long)(vSmooth + 0.5f);

  // delta = V - baseline
  long b = (long)(base + 0.5f);
  long delta = V - b;
  if (delta < 0) delta = 0;

  // Touch gate with hysteresis
  bool newTouched = touched;
  if (!touched && delta >= DELTA_ON)  newTouched = true;
  if ( touched && delta <= DELTA_OFF) newTouched = false;
  touched = newTouched;

  // Update baseline only when idle (prevents chasing touch)
  if (!touched && delta <= BASE_LOCK_DELTA) {
    base = (1.0f - BASE_ALPHA) * base + BASE_ALPHA * (float)V;
  }

  int T = touched ? 1 : 0;

  // ======================================================
  // 2) IMU READ -> motion magnitude -> M (0..100)
  // ======================================================
  int16_t ax, ay, az;
  mpu.getAcceleration(&ax, &ay, &az);

  // Convert raw accel to "g" (MPU6050 default: 16384 LSB per g at ±2g)
  // NOTE: az includes gravity when still, so we subtract ~1g from magnitude later.
  float axg = (float)ax / 16384.0f;
  float ayg = (float)ay / 16384.0f;
  float azg = (float)az / 16384.0f;

  // Acceleration magnitude (includes gravity ~1g when still)
  float mag = sqrt(axg * axg + ayg * ayg + azg * azg);

  // Remove gravity approximately: when still mag ~1.0
  float lin = fabs(mag - 1.0f);

  // Smooth motion
  motionSmoothG = (1.0f - MOTION_ALPHA) * motionSmoothG + MOTION_ALPHA * lin;

  // Map motion to 0..100
  float mClamped = clampf(motionSmoothG, MOTION_MIN_G, MOTION_MAX_G);
  int M = (int)( (mClamped - MOTION_MIN_G) * 100.0f / (MOTION_MAX_G - MOTION_MIN_G) + 0.5f );
  M = constrain(M, 0, 100);

  // Optional behavior: if not touched, set M = 0
  // (so tempo resets when hand not touching)
  if (!touched) M = 0;

  // ======================================================
  // 3) SEND TO p5.js
  // ======================================================
  Serial.print("T:"); Serial.print(T);
  Serial.print(" V:"); Serial.print(V);
  Serial.print(" M:"); Serial.print(M);

  if (DEBUG) {
    Serial.print(" d:"); Serial.print(delta);
    Serial.print(" axg:"); Serial.print(axg, 3);
    Serial.print(" ayg:"); Serial.print(ayg, 3);
    Serial.print(" azg:"); Serial.print(azg, 3);
    Serial.print(" lin:"); Serial.print(lin, 3);
  }

  Serial.println();
  delay(LOOP_DELAY_MS);
}

Code Example — p5.js

describe what you see in this image

Perceptual Feedback Interface — Pattarporn (Porpla) Kittisapkajon

[Link to Code Example](https://editor.p5js.org/pkittisapkajon/full/ENhubcN96)