Aura

A wearable, organ-like interface uses biometric pulse data to translate bodily rhythms to light, proposing embodied domestic infrastructure.

IntermediateWork in progress97
Aura

Things used in this project

Hardware components

WS2812 Addressable LED Strip
Digilent WS2812 Addressable LED Strip
×1
Raspberry Pi Pico W
Raspberry Pi Pico W
×1
LED Light Bulb, Reflector
LED Light Bulb, Reflector
×1
6V Battery
×1
Pulse Sensor (Pulse Sensor Amped)
×1

Software apps and online services

Python
Raspbian
Raspberry Pi Raspbian

Hand tools and fabrication machines

Soldering iron (generic)
Soldering iron (generic)
Solder Wire, Lead Free
Solder Wire, Lead Free
Petri Dish
Mirror

Story

Read more

Schematics

Aura System Diagram

aura electronic circuit

Code

Python Code for Heart Pulse LED Light

Python
import time
import machine
import neopixel

# =========================
# USER SETTINGS
# =========================
DATA_PIN = 14
NUM_PIXELS = 6        
BRIGHTNESS = 0.25

ADC_PIN = 26

# =========================
# LED helpers
# =========================
np = neopixel.NeoPixel(machine.Pin(DATA_PIN), NUM_PIXELS)

def _scale(c, b=BRIGHTNESS):
    return (int(c[0] * b), int(c[1] * b), int(c[2] * b))

def leds_clear():
    for i in range(NUM_PIXELS):
        np[i] = (0, 0, 0)
    np.write()

def leds_flash(color=(255, 30, 30)):
    c = _scale(color)
    for i in range(NUM_PIXELS):
        np[i] = c
    np.write()

def leds_idle(level=0.12):
    # very dim idle so you know it's alive; set to 0.0 to be fully off
    c = _scale((10, 10, 40), BRIGHTNESS * level)
    for i in range(NUM_PIXELS):
        np[i] = c
    np.write()

# =========================
# Pulse sampling + detection
# =========================
adc = machine.ADC(ADC_PIN)

SAMPLE_HZ = 100
DT_MS = 1000 // SAMPLE_HZ

BASELINE_ALPHA = 0.02
PEAK_DECAY = 0.97
THRESH_FRAC = 0.35
THRESH_MIN = 60

REFRACTORY_MS = 350
STALE_MS = 2500

MIN_IBI = 273
MAX_IBI = 2000

baseline = 0.0
peak = 0.0
was_above = False
last_beat_ms = 0

# interval smoothing
intervals = [0] * 8
interval_n = 0
interval_i = 0

def intervals_add(x):
    global interval_n, interval_i
    intervals[interval_i] = x
    interval_i = (interval_i + 1) % len(intervals)
    if interval_n < len(intervals):
        interval_n += 1

def intervals_avg():
    if interval_n == 0:
        return 0
    s = 0
    for i in range(interval_n):
        s += intervals[i]
    return s // interval_n

# =========================
# CONTACT DETECTION (gate)
# =========================
# You will tune these based on your debug "peak" values:
CONTACT_PEAK_ON  = 1200   # need peak above this for a short time -> "touched"
CONTACT_PEAK_OFF = 700    # drop below this for longer -> "not touched"

CONTACT_ON_MS  = 600      # how long signal must be strong to count as touched
CONTACT_OFF_MS = 1200     # how long weak signal to count as not touched

touched = False
touch_candidate_ms = 0
release_candidate_ms = 0

# =========================
# LED pulse timing
# =========================
PULSE_ON_MS = 80
pulse_off_ms = 0

# Only flash on real beats
print("Pulse -> LED (contact-gated) running")
leds_clear()
leds_idle(0.10)

next_debug = time.ticks_add(time.ticks_ms(), 500)

while True:
    t0 = time.ticks_ms()
    now = t0

    raw = adc.read_u16()

    baseline = baseline + BASELINE_ALPHA * (raw - baseline)
    centered = raw - baseline
    abs_c = -centered if centered < 0 else centered

    p = peak * PEAK_DECAY
    peak = abs_c if abs_c > p else p

    threshold = int(peak * THRESH_FRAC)
    if threshold < THRESH_MIN:
        threshold = THRESH_MIN

    # ---- Update "touched" state using peak amplitude + hysteresis ----
    if not touched:
        if peak > CONTACT_PEAK_ON:
            if touch_candidate_ms == 0:
                touch_candidate_ms = now
            elif time.ticks_diff(now, touch_candidate_ms) >= CONTACT_ON_MS:
                touched = True
                touch_candidate_ms = 0
                release_candidate_ms = 0
                # reset beat history on new touch
                interval_n = 0
                last_beat_ms = 0
        else:
            touch_candidate_ms = 0
    else:
        if peak < CONTACT_PEAK_OFF:
            if release_candidate_ms == 0:
                release_candidate_ms = now
            elif time.ticks_diff(now, release_candidate_ms) >= CONTACT_OFF_MS:
                touched = False
                release_candidate_ms = 0
                touch_candidate_ms = 0
                # clear beat history
                interval_n = 0
                last_beat_ms = 0
                leds_idle(0.05)  # or leds_clear()
        else:
            release_candidate_ms = 0

    # ---- Beat detection (ONLY if touched) ----
    beat = False
    bpm = 0

    if touched:
        above = abs_c > threshold

        if above and (not was_above):
            # interval update (if we had a previous beat)
            if last_beat_ms != 0:
                ibi = time.ticks_diff(now, last_beat_ms)
                if MIN_IBI <= ibi <= MAX_IBI:
                    intervals_add(ibi)

            # refractory beat confirm
            if last_beat_ms == 0 or time.ticks_diff(now, last_beat_ms) > REFRACTORY_MS:
                beat = True
                last_beat_ms = now

        was_above = above

        # compute bpm only if we have enough valid intervals
        if interval_n >= 3:
            avg_ibi = intervals_avg()
            if avg_ibi > 0:
                bpm = 60000 // avg_ibi

        # stale reset
        if last_beat_ms != 0 and time.ticks_diff(now, last_beat_ms) > STALE_MS:
            bpm = 0
            interval_n = 0
            last_beat_ms = 0

    else:
        # if not touched, don’t let old state trigger anything
        was_above = False

    # ---- LED behavior ----
    if touched and beat:
        leds_flash((255, 30, 30))
        pulse_off_ms = time.ticks_add(now, PULSE_ON_MS)

    if pulse_off_ms and time.ticks_diff(now, pulse_off_ms) >= 0:
        pulse_off_ms = 0
        # after flash, show a very dim touched idle (or clear)
        if touched:
            leds_idle(0.12)
        else:
            leds_idle(0.05)  # or leds_clear()

    # ---- Debug ----
    if time.ticks_diff(now, next_debug) >= 0:
        next_debug = time.ticks_add(now, 500)
        print("touched=%d peak=%d thr=%d intervals=%d bpm=%d" %
              (1 if touched else 0, int(peak), threshold, interval_n, bpm))

    # keep sample rate stable
    elapsed = time.ticks_diff(time.ticks_ms(), t0)
    sleep_ms = DT_MS - elapsed
    if sleep_ms > 0:
        time.sleep_ms(sleep_ms)

Python Code for Agentic AI System

Python
import network, time, ujson
import machine
import uasyncio as asyncio

# ---------- CONFIG ----------
WIFI_SSID = "YOUR_SSID"
WIFI_PASS = "YOUR_PASSWORD"

ADC_PIN = 26              # GP26 = ADC0
SAMPLE_HZ = 100           # 100 samples/sec is typical for pulse sensors
HTTP_PORT = 80

# ---------- STATE ----------
adc = machine.ADC(ADC_PIN)

state = {
    "ts_ms": 0,
    "raw": 0,
    "filt": 0,
    "beat": False,
    "bpm": 0,
}

# Simple filter / beat detection (starter only)
baseline = 0
last_beat_ms = None
bpm = 0

def wifi_connect():
    wlan = network.WLAN(network.STA_IF)
    wlan.active(True)
    wlan.connect(WIFI_SSID, WIFI_PASS)
    for _ in range(50):
        if wlan.isconnected():
            print("WiFi connected:", wlan.ifconfig())
            return wlan
        time.sleep(0.2)
    raise RuntimeError("WiFi failed")

async def sampler():
    global baseline, last_beat_ms, bpm

    alpha = 0.02           # baseline smoothing
    thresh = 800           # you will tune this (depends on your raw scale / sensor)

    prev_above = False

    while True:
        raw = adc.read_u16()               # 0..65535
        now = time.ticks_ms()

        # Update baseline (very simple DC removal)
        baseline = int((1 - alpha) * baseline + alpha * raw)
        ac = raw - baseline

        # crude beat detect: rising threshold crossing
        above = ac > thresh
        beat = (above and not prev_above)
        prev_above = above

        if beat:
            if last_beat_ms is not None:
                dt = time.ticks_diff(now, last_beat_ms)
                if 250 < dt < 2000:        # 30..240 bpm sanity
                    bpm = int(60000 / dt)
            last_beat_ms = now

        state["ts_ms"] = now
        state["raw"] = raw
        state["filt"] = ac
        state["beat"] = bool(beat)
        state["bpm"] = int(bpm)

        await asyncio.sleep_ms(int(1000 / SAMPLE_HZ))

async def http_server():
    addr = ("0.0.0.0", HTTP_PORT)
    server = await asyncio.start_server(handle_client, *addr)
    print("HTTP server listening on", addr)
    await server.wait_closed()

async def handle_client(reader, writer):
    try:
        line = await reader.readline()
        if not line:
            await writer.aclose()
            return

        # Parse "GET /pulse HTTP/1.1"
        parts = line.decode().split()
        path = parts[1] if len(parts) > 1 else "/"

        # Drain headers
        while True:
            h = await reader.readline()
            if h in (b"\r\n", b"\n", b""):
                break

        if path == "/" or path.startswith("/pulse"):
            body = ujson.dumps(state)
            writer.write("HTTP/1.1 200 OK\r\n")
            writer.write("Content-Type: application/json\r\n")
            writer.write("Access-Control-Allow-Origin: *\r\n")
            writer.write("Content-Length: {}\r\n\r\n".format(len(body)))
            writer.write(body)
        else:
            writer.write("HTTP/1.1 404 Not Found\r\nContent-Length: 0\r\n\r\n")

        await writer.drain()
    finally:
        await writer.aclose()

async def main():
    wifi_connect()
    asyncio.create_task(sampler())
    await http_server()

asyncio.run(main())

Credits

Madlen Elise von Wulffen
2 projects • 1 follower
Ivan Hunga Alho Miguez Garcia
0 projects • 2 followers

Comments