NEW: Aura

In autumn 2025 we hosted a creative and hands-on FemTech workshop where participants explored self-tracking, biosensing, and the relationship between bodies and technology. Using Raspberry Pi Pico, pulse sensors, and interactive LED feedback, participants built their own personal artifact: a small device that visualizes their live heartbeat. This artifact was named Aura, as an aura is a visual expression of an inner state — often imagined as colored light surrounding a person.

Core concepts
The workshop introduced core concepts of self-tracking and biofeedback. How everyday technologies measure bodies, and how these measurements shape how we understand ourselves. Through step-by-step python coding, sensor calibration, and hardware experimentation, participants learned how heart rate can be detected, interpreted, and expressed through light. The goal was to make invisible bodily processes tangible, visible, and discussable.

Technical
To make this artifact the participants used

  • Raspberry Pi Pico microcontroller
  • Arduino tech heart rate sensor
  • Single NeoPixel RGB LED
  • 3d printed lamp box.

For the workshop they also used a simple push button and a small red LED.

Functionally the artifact could detects a heartbeat, and calculate the user’s BPM and then map the value to a color range—turning pulse data into an immediate visual expression.

To develop the artifact within, we went through the following exercises:

Hello.py

To ease into programming, participants began with a playful introduction to Python using a simple script called Hello.py. The goal of this exercise was not only to learn foundational coding concepts, but also to use code as a way to get to know each other.

Through small, guided steps, the participants explored:

  • Print statements – how the computer outputs text
  • Variables – storing information such as a name or age
  • Input() – asking the user questions
  • If/else statements – making decisions based on user responses
  • For-loops – repeating actions, such as printing “Hurra!”

After trying out the example code, each participant created 3–5 interview questions of their own by combining these elements. They then used their mini-program to “interview” other participants in the room.

What began as a beginner-friendly piece of code quickly turned into a social activity that made programming feel personal, creative, and fun — and helped everyone get comfortable with Python before moving on to sensing, data, and hardware later in the workshop.

The code:

#Print statements
print ("Hello world")

#Use input and a variable 
print ("What is your name?")
myName = input()
print ("It is good to meet you," + myName)

#use a variable and a number OBS strings (always input) and integers
print ("The length of your name is: " + str(len(myName)))
print ("What is your age?")
myAge = input()
print ("On your next birthday you will be " + str(int(myAge) + 1) + ".")

#IF and ELSE 
print ("Do you want me to sing you a birthday song? please write Yes or No")
answer1 = input()

if answer1 == "Yes":
    print ("Happy birthday to you, happy birthday to you, happy birthday dear " + myName + " happy birthday to you")
else:
    print ("Okay")


#FOR LOOP 
print ("Do you want me to say Hurra the number of years?")
answer2 = input()

if answer2 == "Yes":
    for i in range(int(myAge)):
        print ("hurra")
else:
    print ("Okay")

print("HUUUUUURAAAAAAAA")

Led light and button

In the second exercise, participants moved from pure Python programming to physical computing. They learned how to work with a breadboard, basic circuits, and the Raspberry Pi Pico to control an LED and a button.
This hands-on activity introduced fundamental electronics concepts:

  • inputs, outputs
  • digital signals
  • pull-up resistors
  • event-based interaction.

The exercise unfolded in three small coding steps, each building on the previous one.

#CODE 1 
#a button as an input - connects at GPIO 5 + GND
#a red LED as an output - connects at GPIO 1 + GND

a pull-up resistor built into the Pico

They used this code to detect a button press and turn the LED on:

import machine
from machine import Pin
from time import sleep

button = machine.Pin(5, machine.Pin.IN, machine.Pin.PULL_UP)
led = Pin(1, Pin.OUT)

while True:
    if not button.value():
        led.value(1)
        print('Button pressed!')
        sleep(.2)
        led.value(0)
#CODE 2
#a red LED as an output - connects at GPIO 0 + GND 
#The LED could now be turned on or off directly from the computer terminal

from machine import Pin
import time

led = Pin(0, Pin.OUT)

print("Skriv 'on' for at tænde LED'en og 'off' for at slukke den.")

while True:
    command = input("Kommando: ").strip().lower()

    if command == "on":
        led.value(1)
        print("LED tændt 💡")
    elif command == "off":
        led.value(0)
        print("LED slukket 💤")
    else:
        print("Ugyldig kommando. Skriv 'on' eller 'off'.")
#CODE 3
#The final version turned the LED and button into a mini reaction-time game.
#The LED lights up after a random delay, and participants must press the button as quickly as possible
#a button as an input - connects at GPIO 5 + GND
#a red LED as an output - connects at GPIO 1 + GND

import machine
import utime
import urandom

led = machine.Pin(1, machine.Pin.OUT)
button1 = machine.Pin(5, machine.Pin.IN, machine.Pin.PULL_UP)

def button_handler(pin):
    button1.irq(handler=None)
    timer_reaction = utime.ticks_diff(utime.ticks_ms(), timer_start)
    print("Your reaction time was " + str(timer_reaction)+ " milliseconds!" )

led.value(1)
utime.sleep(urandom.uniform(5,10))
led.value(0)
timer_start = utime.ticks_ms()
button1.irq(trigger=machine.Pin.IRQ_RISING, handler=button_handler)

Neopixel

In this exercise, participants moved from guided wiring to independent problem-solving.
They were given only:

  • a NeoPixel LED
  • a button
  • the pin numbers
  • and the starter code

From this, they had to wire the components correctly and explore how microcontrollers handle input (button) and output (LED).

After learning the basics, they built reaction-based games using timing, randomness, and color feedback.

#CODE 1
#detect a button press
#cycle through a list of colors
#update all NeoPixels at once

from neopixel import Neopixel
from machine import Pin
import time
# Initialize button with pull-up resistor
button = Pin(20, Pin.IN, Pin.PULL_UP)
# Initialize NeoPixel strip
num_pixels = 5  # Adjust for your setup
pixels = Neopixel(num_pixels, 0, 28, "RGB")  # Ensure correct GRB order
# Define color sequence
colors = [
    (255, 0, 0),    # Red
    (0, 255, 0),    # Green
    (0, 0, 255),    # Blue
    (255, 255, 0),  # Yellow
    (255, 0, 255)   # Magenta
]
color_index = 0  # Start with the first color
def set_color(color):
    """Set all NeoPixels to the given color."""
    for i in range(num_pixels):
        pixels.set_pixel(i, color)
    pixels.show()
# Set initial color
set_color(colors[color_index])
while True:
    if not button.value():  # Button pressed (LOW)
        print("Button pressed!")
        color_index = (color_index + 1) % len(colors)  # Cycle to the next color
        set_color(colors[color_index])
        time.sleep_ms(500)  # Debounce delay to avoid multiple detections
#CODE 2
#Here we introduced the idea of waiting, an important concept in games and user interaction.
#The LED changes color 3 seconds after the press.

from neopixel import Neopixel
from machine import Pin
import time

# Initialize button with pull-up resistor
button = Pin(20, Pin.IN, Pin.PULL_UP)

# Initialize NeoPixel strip
num_pixels = 5  # Adjust for your setup
pixels = Neopixel(num_pixels, 0, 28, "RGB")  # Ensure correct GRB order

# Define color sequence
colors = [
    (255, 0, 0),    # Red
    (0, 255, 0),    # Green
    (0, 0, 255),    # Blue
    (255, 255, 0),  # Yellow
    (255, 0, 255)   # Magenta
]

color_index = 0  # Start with the first color

def set_color(color):
    """Set all NeoPixels to the given color."""
    for i in range(num_pixels):
        pixels.set_pixel(i, color)
    pixels.show()

# Set initial color
set_color(colors[color_index])

# --- Main loop ---
while True:
    if not button.value():  # Button pressed (LOW)
        print("Button pressed! Skifter farve om 3 sekunder...")
        time.sleep(3)  # ← Delay
        color_index = (color_index + 1) % len(colors)  # Cycle to the next color
        set_color(colors[color_index])
        time.sleep_ms(500)  # Debounce delay
#CODE 3
#random waiting time
#red = "wait"
#green = "GO!"
#measure milliseconds from green to button press
#give feedback using colors (fast, ok, slow)


from neopixel import Neopixel
from machine import Pin
import time, random

# Setup
button = Pin(20, Pin.IN, Pin.PULL_UP)
pixels = Neopixel(5, 0, 28, "RGB")

def set_color(color):
    for i in range(5):
        pixels.set_pixel(i, color)
    pixels.show()

while True:
    print("\nKlar til næste runde...")
    set_color((255, 0, 0))  # Rød = vent
    time.sleep(random.uniform(2, 5))  # Tilfældigt ventetid

    set_color((0, 255, 0))  # Grøn = tryk nu!
    start = time.ticks_ms()

    # Vent på knaptryk
    while button.value():
        pass
    reaction_time = time.ticks_diff(time.ticks_ms(), start)

    print(f"Reaktionstid: {reaction_time} ms")

    # Vis resultat i farver
    if reaction_time < 250:
        set_color((0, 0, 255))  # Blå = lynhurtig!
    elif reaction_time < 500:
        set_color((255, 255, 0))  # Gul = god
    else:
        set_color((255, 0, 0))  # Rød = langsom

    time.sleep(2)
#CODE 4
#Finally, participants extended their code to support two buttons and a competitive game.
#This taught:
#parallel input handling
#determining a winner
#timing under pressure
#visual feedback for each player



from neopixel import Neopixel
from machine import Pin
import time, random
#

# Setup
button1 = Pin(20, Pin.IN, Pin.PULL_UP)  # Spiller 1
button2 = Pin(21, Pin.IN, Pin.PULL_UP)  # Spiller 2
pixels = Neopixel(5, 0, 28, "RGB")

def set_color(color):
    for i in range(5):
        pixels.set_pixel(i, color)
    pixels.show()

while True:
    print("\nKlar til duel!")
    set_color((255, 0, 0))  # Rød = vent
    time.sleep(random.uniform(2, 5))  # Tilfældig pause

    set_color((0, 255, 0))  # Grøn = tryk!
    start = time.ticks_ms()

    winner = None

    while not winner:
        if not button1.value():
            winner = "Spiller 1"
        elif not button2.value():
            winner = "Spiller 2"

    reaction = time.ticks_diff(time.ticks_ms(), start)
    print(f"{winner} vandt! Reaktionstid: {reaction} ms")

    # Vinderen får farve
    if winner == "Spiller 1":
        set_color((0, 0, 255))  # Blå
    else:
        set_color((255, 0, 255))  # Magenta

    time.sleep(3)

The pulse sensor

In the final exercise, participants learned to work with analog sensors by using a pulse sensor to measure heartbeat signals.
This step introduced:

  • how analog input works (ADC)
  • how to process noisy biosignals
  • how to detect peaks (heartbeats)
  • how to compute BPM
  • how to link sensor data to colors on the device

The pulse sensor sits on the fingertip and outputs a small, fluctuating voltage.
Students explored the raw signal first (code 1), then gradually built up pulse detection (code 2) and BPM calculation (code 3).

#CODE 1
# The students began by reading the raw analog values from the sensor to understand its behaviour.

from machine import ADC, Pin
import time

pulse_pin = ADC(Pin(27))

while True:
    val = pulse_pin.read_u16()
    print(val)
    time.sleep(0.05)

# analog sensors produce values (0–65535)
# the heartbeat creates small peaks in the signal
# notice how noise and finger pressure influence readings
#CODE 2

#reducing “chatter” from noisy biological data
#using thresholds to detect a heartbeat “peak”
#blinking an LED in sync with the pulse


import time
from machine import Pin, ADC

ADC_PIN = 27
LED_PIN = "LED"

adc = ADC(Pin(ADC_PIN))
led = Pin(LED_PIN, Pin.OUT)

threshold = 33200   # ~50% of full range
hyst = 500          # smoothing (~1%)
th_hi = threshold + hyst
th_lo = threshold - hyst

# Warm-up
for _ in range(10):
    _ = adc.read_u16()
    time.sleep_ms(5)

while True:
    signal = adc.read_u16()
    print(signal)

    # Hysteresis logic
    if led.value() == 0 and signal > th_hi:
        led.value(1)
    elif led.value() == 1 and signal < th_lo:
        led.value(0)

    time.sleep_ms(20)
#CODE 3

#detecting the moment of a heartbeat
#timestamping biological events
#computing BPM from intervals
#connecting BPM to colors



import time
from machine import Pin, ADC

ADC_PIN = 27
LED_PIN = "LED"

adc = ADC(Pin(ADC_PIN))
led = Pin(LED_PIN, Pin.OUT)

threshold = 33200   # Adjust for your sensor
hyst = 500          # Hysteresis (~1%)
th_hi = threshold + hyst
th_lo = threshold - hyst

# Warm-up
for _ in range(10):
    _ = adc.read_u16()
    time.sleep_ms(5)

beats = []
last_state = 0

while True:
    signal = adc.read_u16()

    # Heartbeat detection
    if led.value() == 0 and signal > th_hi:
        led.value(1)
    elif led.value() == 1 and signal < th_lo:
        led.value(0)

    state = led.value()

    # Rising edge = heartbeat
    if state == 1 and last_state == 0:
        now = time.ticks_ms()
        beats.append(now)

        # Keep last 15 seconds of data
        beats = [b for b in beats if time.ticks_diff(now, b) < 15000]

        # Calculate BPM
        if len(beats) > 1:
            intervals = [time.ticks_diff(beats[i], beats[i-1]) for i in range(1, len(beats))]
            avg_interval = sum(intervals) / len(intervals)
            bpm = 60000 / avg_interval
            print("BPM:", int(bpm))

    last_state = state
    time.sleep_ms(20)

Group discussion & reflection

After working hands-on with the pulse sensor, participants moved into a reflective discussion about data, bodies, and self-tracking.
The goal was to connect the technical experience to the technologies they use in everyday life and to open up critical conversations about what biometric data means.

Participants first discussed in pairs or small groups and then shared reflections in a full-class conversation.

Working with the pulse sensor naturally led to questions like:

  • How does it feel to see your own pulse as data?
  • What does a device “know” about you — and what does it miss?
  • How do commercial self-tracking tools (smartwatches, step counters, sleep trackers, menstrual apps, etc.) shape the way we think about our bodies?

Which forms of self-tracking do you use yourself — consciously or unconsciously?
Examples might included: fitness watches, smartphone health data, menstrual tracking apps, stress or sleep monitoring, social media screen-time reports, location history, or even step counters you never asked for.

When does data help you — and when does it feel like control?
Students discussed moments where data motivated them (sleep, exercise, mood tracking) and moments where numbers felt stressful or limiting. Some talked about using it for running, and some felt more that the apps on their phones were controlling rather than the screentime reminder. Some even mentioned how they turned off the screentimer limit that they had sat for themselves.

Can data tell the truth about who we are — or only a small part of the story?
Participants reflected on the difference between quantitative metrics (pulse, steps, time) and the lived experience behind the number.

How could we use self-tracking in more human, playful, or creative ways?
This question encouraged thinking beyond commercial apps, and said that it could be used for supporting our health care system.
Others mentioned art and community building through self tracking.

Teaching assistant presentations

During the workshop, our teaching assistants — all Computer Science students from the University of Copenhagen — gave short presentations that connected the activities to real university studies.

They introduced the students to bias in technological models, showing how data shapes algorithms and why technologies like self-tracking devices are never neutral.

They also presented small prototypes and artifacts, linking the workshop exercises to the kinds of hands-on projects done in computer science courses, from physical computing to data and interaction design.

Finally, the TAs shared what it’s like to study at KU — the mix of creativity and coding, working in project groups, and exploring how technology affects people. Their presentations helped participants see how the skills they practiced relate to real pathways into tech.

The “final” artifact

A pulse sensor is connected to a microcontroller, which continuously reads the user’s pulse signal and calculates beats per minute (BPM). Based on the measured pulse, a ring of NeoPixel LEDs changes color to indicate different heart-rate zones — from blue at low pulse to red at very high pulse.

The artifact processes the raw analog signal, detects each heartbeat, and computes an average BPM over time for a more stable reading. It then translates the BPM into a clear visual output, enabling users to see their physiological state in real time. This creates an immediate and intuitive connection between the body and the artifact, turning invisible biometric data into a visible, ambient expression.

from machine import Pin, ADC
from neopixel import Neopixel
import time

# --- Hardware setup ---
ADC_PIN = 27
LED_PIN = "LED"

adc = ADC(Pin(ADC_PIN))
led = Pin(LED_PIN, Pin.OUT)

# --- NeoPixel setup ---
num_pixels = 5           # antal LEDs
pixels = Neopixel(num_pixels, 0, 28, "RGB")  # GPIO28 til NeoPixel data
def set_color(color):
    """Sæt alle NeoPixels til en bestemt farve"""
    for i in range(num_pixels):
        pixels.set_pixel(i, color)
    pixels.show()

# --- Farver for forskellige pulsniveauer ---
# (R, G, B)
colors = {
    "low": (0, 0, 255),        # Blå = lav puls
    "normal": (0, 255, 0),     # Grøn = normal
    "elevated": (255, 255, 0), # Gul = let forhøjet
    "high": (255, 165, 0),     # Orange = høj
    "very_high": (255, 0, 0)   # Rød = meget høj
}

# --- Indstillinger til måling ---
threshold = 33200   # Justér efter sensor
hyst = 500          # Hysterese (~1%)
th_hi = threshold + hyst
th_lo = threshold - hyst

# Startopvarmning
for _ in range(10):
    _ = adc.read_u16()
    time.sleep_ms(5)

beats = []            # Tidsstempler for slag
last_state = 0        # LED tilstand
start_time = time.ticks_ms()

# --- Funktion til farveskift baseret på BPM ---
def update_color(bpm):
    """Opdater NeoPixel farve baseret på puls"""
    if bpm < 60:
        set_color(colors["low"])
    elif 60 <= bpm <= 70:
        set_color(colors["normal"])
    elif 71 <= bpm <= 80:
        set_color(colors["elevated"])
    elif 81 <= bpm <= 100:
        set_color(colors["high"])
    else:
        set_color(colors["very_high"])

# --- Main loop ---
while True:
    signal = adc.read_u16()

    # Registrer pulsslag
    if led.value() == 0 and signal > th_hi:
        led.value(1)
    elif led.value() == 1 and signal < th_lo:
        led.value(0)

    state = led.value()

    # Registrer nyt slag (rising edge)
    if state == 1 and last_state == 0:
        now = time.ticks_ms()
        beats.append(now)

        # behold kun de sidste 15 sekunder
        beats = [b for b in beats if time.ticks_diff(now, b) < 15000]

        # Beregn BPM
        if len(beats) > 1:
            intervals = [time.ticks_diff(beats[i], beats[i-1]) for i in range(1, len(beats))]
            avg_interval_ms = sum(intervals) / len(intervals)
            bpm = 60000 / avg_interval_ms
            print("BPM:", int(bpm))

            # Opdater LED-farve efter puls
            update_color(bpm)

    last_state = state
    time.sleep_ms(20)

In the end, the workshop stood out for its safe and welcoming learning environment, where students highlighted the “good atmosphere,” “kind instructors,” and a space “where you felt welcome — even when making mistakes.”

Many experienced their first success with programming, describing the moment “the lamp finally lit up” and the rush of “when it works the first time.”

For several participants, the joy of building something themselves — from “the satisfaction of getting the task to work” to “quickly developing the code further” — became a powerful reminder: I can actually do this.

So if you want to do this – stay tuned! The next workshop will be in April 2026.