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¶
Ritual Loop of Touch, Sound, and Attention — Pattarporn (Porpla) Kittisapkajon
Process and workflow¶
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.
Capacitive Sensing and Pressure Matrix Tutorial Electronics by Emma Pareschi
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¶
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¶
Perceptual Feedback Interface — Pattarporn (Porpla) Kittisapkajon
[Link to Code Example](https://editor.p5js.org/pkittisapkajon/full/ENhubcN96)





