The UV Glow Keyring: Stopping Sunburns!!

by inaki.iturriaga in Circuits > Assistive Tech

32 Views, 0 Favorites, 0 Comments

The UV Glow Keyring: Stopping Sunburns!!

0b885ef4-871b-4df4-9f85-8b03a99ee1e5.png

As a natural redhead, I have a complicated relationship with the sun. I don’t really tan, I burn. Fast. The problem is that UV radiation is completely invisible. By the time I feel the heat or see the redness on my skin, the damage is already done. And after not having learnt my lesson by now, getting the worst sunburn in my life, I wanted to build a device that acts as a 'sixth sense', a way to see the invisible radiation hitting my skin in real-time before it becomes a burn.


For the Visualize It contest, I created the UV Glow Keyring. It uses a UV sensor and a microcontroller to measure the invisible Ultraviolet Index (UVI) and translates that data into a subtle glowing ring of light. The higher the UV, the warmer the colors shift, from a safe Green to a scorching Purple.

Supplies

Supplies and Components


To build this, I wanted to keep the footprint as small as possible. I chose the RP2040-Zero because it is tiny, powerful, and runs on 3.3V logic, which matches modern sensors perfectly.

Electronics:

  1. Microcontroller: RP2040-Zero
  2. Sensor: ML8511 UV Sensor
  3. Display: NeoPixel Ring 12
  4. Power: 3.7V LiPo Battery (I used an 804030 for this)
  5. Charging: TP4056 USB-C Charging Module
  6. Switch: Small Slide Switch

Materials:

  1. Filament: White PLA (Crucial for the light diffusion effect at the top)
  2. Wire
  3. Superglue

Tools:

  1. Soldering Iron & Solder
  2. 3D Printer
  3. Wire Strippers




Designing the Case (Autodesk Fusion 360)

Screenshot 2026-01-05 at 11.49.54 PM.png
Screenshot 2026-01-05 at 11.54.08 PM.png
Screenshot 2026-01-05 at 11.58.12 PM.png
Screenshot 2026-01-06 at 12.14.35 AM.png
Screenshot 2026-01-06 at 12.24.40 AM.png

I designed the enclosure entirely in Fusion 360. The goal was to make the device look like a solid piece of white plastic when it's off, but allow light to shine through the solid material when it's on.

I designed the case in three specific parts to manage the light and the electronics:

  1. The Main Case (Bottom): This holds the heavy components—the battery, the charging board, and the RP2040-Zero.
  2. The Baffle (Middle Separator): It sits on top of the electronics and creates a physical wall between the center sensor and the outer LEDs.
  3. The Diffusion Lid (Top): I modeled the top face to be 0.6mm thick (3 layers of printing). This is thick enough to look solid white, but thin enough that the LEDs can shine through it, creating a "phantom" light effect.


The Circuit

Screenshot 2026-01-06 at 1.03.40 AM.png


To verify my connections before soldering, I mapped out the logic in EasyEDA.

The Wiring Map:

  1. Power: The Battery connects to the TP4056 charger. The output goes through a switch to the 5V/VBUS pin of the RP2040.
  2. Sensor (ML8511): Connected to 3.3V (Power), GND, and Pin GP26 (Analog Data). The EN (Enable) pin is tied to 3.3V so the sensor is always active.
  3. LEDs (NeoPixel): Powered by 5V (for maximum brightness) and Data connected to Pin GP0.


Printing the Parts

1767672441803-019b917c-3d57-7736-97f7-3bd9b691758e.png


I printed these parts on an FDM printer using White PLA.

  1. Layer Height: 0.2mm
  2. Infill: 100% (Solid).
  3. Tip: You must use 100% infill for the lid. If you use a grid infill, you will see the grid pattern shadow when the lights turn on. Solid plastic creates a smooth, milky diffusion.

In the photo above, you can see the three stages of the assembly:

  1. Left: The bottom case for the battery.
  2. Middle: The "Baffle" or separator plate with the cable pass-through hole.
  3. Right: The top lid with the diffusion face and the dedicated sensor pupil in the center.

(The files are attached to the project)

Downloads

The Code

The code runs on the RP2040. It reads the analog voltage from the sensor, smooths out the data (averaging 10 readings to prevent flickering), and maps that voltage to the standard UV Index scale.

The visualizer then converts that number into color:

  1. Index 0-2 (Low): Green
  2. Index 3-5 (Moderate): Yellow
  3. Index 6-7 (High): Orange
  4. Index 8-10 (Very High): Red
  5. Index 11+ (Extreme): Purple

Here is the code I wrote for the project:

#include <Adafruit_NeoPixel.h>

// --- CONFIGURATION ---
#define LED_PIN 0 // GP0 on RP2040-Zero
#define NUMPIXELS 12 // 12 LED Ring
#define UV_PIN 26 // GP26 (Analog A0)

Adafruit_NeoPixel pixels(NUMPIXELS, LED_PIN, NEO_GRB + NEO_KHZ800);

// Variables for smoothing sensor jitter
const int numReadings = 10;
int readings[numReadings];
int readIndex = 0;
long total = 0;
int average = 0;

void setup() {
analogReadResolution(12); // Force 12-bit resolution for RP2040
pixels.begin();
pixels.setBrightness(200); // High brightness for outdoor visibility
pixels.show();

// Initialize smoothing array
for (int i = 0; i < numReadings; i++) readings[i] = 0;
}

void loop() {
// 1. Smooth the Sensor Data
total = total - readings[readIndex];
readings[readIndex] = analogRead(UV_PIN);
total = total + readings[readIndex];
readIndex = readIndex + 1;
if (readIndex >= numReadings) readIndex = 0;
average = total / numReadings;

// 2. Convert to Voltage (RP2040 is 3.3V ref)
float voltage = average * (3.3 / 4095.0);

// 3. Map Voltage to UV Index (Based on ML8511 Datasheet)
// 1.0V = UV 0, 2.8V = UV 15
float uvIndex = mapfloat(voltage, 0.99, 2.8, 0.0, 15.0);
if (uvIndex < 0) uvIndex = 0;

// 4. Update the Lights
visualizeUV(uvIndex);
delay(100);
}

float mapfloat(float x, float in_min, float in_max, float out_min, float out_max) {
return (x - in_min) * (out_max - out_min) / (in_max - in_min) + out_min;
}

void visualizeUV(float index) {
uint32_t color;
// Color Logic
if (index < 1) color = pixels.Color(0, 255, 0); // Green
else if (index < 3) color = pixels.Color(200, 255, 0); // Chartreuse
else if (index < 5) color = pixels.Color(255, 200, 0); // Yellow
else if (index < 7) color = pixels.Color(255, 100, 0); // Orange
else if (index < 9) color = pixels.Color(255, 0, 0); // Red
else color = pixels.Color(140, 0, 211); // Purple

// Gauge Logic (How many LEDs to light up)
int ledsLit = map((int)index, 0, 12, 1, NUMPIXELS);
if (index < 0.2) ledsLit = 0;

pixels.clear();
for(int i=0; i<ledsLit; i++) {
pixels.setPixelColor(i, color);
}
pixels.show();
}

Assembly


Since the device is compact, the assembly order is important.

  1. The Bottom Stack: I soldered the battery to the TP4056, and the TP4056 to the RP2040-Zero. These were placed into the bottom of the main case.
  2. The Wiring Pass-through: I soldered long wires to the UV sensor and the LED ring. I threaded these wires through the center hole of the Middle Baffle piece.
  3. The Sandwich:
  4. The Baffle was glued on top of the RP2040, creating a flat platform.
  5. The NeoPixel ring was glued onto the Baffle.
  6. The UV Sensor was placed in the dead center of the ring.
  7. Closing the Lid: Finally, the wires were soldered to the microcontroller below, and the Top Lid was snapped into place. I used a tiny drop of superglue to ensure the UV sensor stayed perfectly aligned with the small hole in the lid.


And that's pretty much it! Hope this helps prevent some nasty sunburns!