Skip to the content
PCBKitNew board
2 layers50 × 25 mmAOL PD 2, the printed circuit board in 3D

Wearable device for measuring UV irradiance with multiplatform companion app with dose warnings based on Fitzpatrick phototypes.

Size
50 × 25 mm
Copper layers
2
Thickness
1.6 mm
Parts
33
Nets
32
Pads
98
Tracks, vias
265, 9
Pours
11
Routed
Yes
KiCad
KiCad 9.0 (file 20241229)
Created
21 Aug 2026
Updated
22 Aug 2026

Overview

AOL PD 2 is an open-source printed circuit board by vodjan, from the vodjan/uv-dosimeter repository on GitHub. It is a 2-layer board of 50 × 25 mm with 33 parts on 32 nets, routed with 265 tracks and 9 vias.

It is built around the TLV9064ID, BPW34 and MBR120VLSFT1G-TP. Its parts include 12 resistors, 8 capacitors, 6 diodes, 3 ICs and 1 connector. It belongs with the ESP32 and RISC-V boards of this library.

Main components

3

README

README.md

Wearable UV dosimeter

Wearable device for measuring UV irradiance with multiplatform companion app with dose warnings based on Fitzpatrick phototypes.

This repository is a mirror of Jan Vodstrčil's and Ondřej Pokovič's project made for the Applied Optoelectronics in Medicine subject at CTU FEE. The source repository can be found at CTU FEE's GitLab. The README, docs/ and licenses (see License) have been updated for this public mirror.

Enclosure render

Measures UV irradiance with an ML8511, estimates the angle between the sensor face and the sun from four edge-mounted BPW34 photodiodes, and divides the reading by cos(theta) to recover the irradiance that a sun-facing sensor would have measured. The corrected value is integrated over time into an erythema-weighted dose and compared against the minimal erythema dose for the wearer's skin phototype.

Custom KiCad PCB, ESP32-H2 firmware, and a Flet/Bleak companion app.

MCUESP32-H2-Zero (RISC-V, BLE)
UV sensorML8511, analog output
Angle sensing4x BPW34 into TLV9064 transimpedance amplifiers
ADC12-bit, 11 dB attenuation, factory-calibrated read
Sample rate2 Hz
Offline storage2048 records at 10 s intervals (12 KB LittleFS, ~5.7 h)
LinkBLE GATT, notify-only
AppFlet + Bleak (Linux / Windows / macOS)

Repository layout

PathContents
firmware/Sensor read-out, BLE GATT server, flash ring buffer
app/Companion app: BLE client, dose tracker, UI
hardware/KiCad 9 schematic, PCB, custom symbol and footprint libraries
math/MATLAB derivation of the erythema calibration coefficient

Angle correction

A flat sensor tilted away from the sun by angle theta reads E * cos(theta) instead of E. On a wrist the tilt is arbitrary and changes constantly, so theta has to be measured rather than assumed.

The four BPW34 diodes are tilted 20 deg outward from the PCB normal, one per edge. Their normals are (+/-sin20, 0, cos20) and (0, +/-sin20, cos20). Weighting each normal by its photocurrent and summing approximates the sun direction:

text
s          = ( (R - L) * sin20,  (T - B) * sin20,  (L + R + T + B) * cos20 )
cos(theta) = s_z / |s|

Implemented in sensors.cpp with sin20 = 0.342, cos20 = 0.940. Two consequences of the form of the expression:

  • The dark-current baseline subtracted from each channel cancels in s_x and s_y, and s_z appears in both numerator and denominator, so PD_DARK_OFFSET only needs to be approximately right.
  • |s| is undefined at zero illumination. The implementation returns cos(theta) = 1 when the magnitude is zero.

Below cos(theta) = 0.05 the division is ill-conditioned, so uvCorrected is set to NaN rather than a large number. This propagates through the BLE packet and is treated as zero irradiance by the dose tracker.

Hardware

Two-layer PCB with ground pours on both copper layers and components populated on both sides, built around the ESP32-H2-Zero module and designed in KiCad 9. Schematic: AOL_PD_2.kicad_sch.

PCB layout, top and bottom copper

Schematic

SignalGPIOSchematic net
UV_EN03,3V GPIO
UV1ADC0
LEFT2ADC1
BOTTOM3ADC2
RIGHT4ADC3
TOP5ADC4

GPIO0 drives both the ML8511 EN pin and the divider that biases the photodiode amplifiers, so a single digitalWrite(PIN_UV_EN, HIGH) in setup() brings up the entire analog front end.

ML8511 transfer function. Linear, 0.99 V at 0 mW/cm^2 and 2.8 V at 15 mW/cm^2, giving E = max((V - 0.99) * 15 / 1.81, 0).

Photodiode channels are inverted. The TLV9064 stages are inverting transimpedance amplifiers with the non-inverting input biased to Vb ~= 97 mV, generated by a 330k/10k divider off GPIO0 and buffered by a fifth op-amp section. Increasing photocurrent pulls the output toward GND, so a lower ADC count means more light. Firmware converts with light = max(0, PD_DARK_OFFSET - raw).

The UV channel needs the calibrated ADC read. ESP32 ADC non-linearity near the bottom of the range causes analogRead() to underestimate voltages around the ML8511's 0.99 V floor, which clipped all low readings to zero. analogReadMilliVolts() applies the factory calibration and corrects this. The raw count is still reported over BLE for diagnostics.

PD_DARK_OFFSET in config.h is per-board: cover all four diodes, read the peak count, add margin. Default 110.

Firmware

PlatformIO with the pioarduino platform fork, which supplies arduino-esp32 3.x (ESP-IDF 5.x) for the H2.

bash
cd firmware
pio run -t upload && pio device monitor

On Linux, install 99-platformio-udev.rules first.

The main loop samples all five channels, then either notifies the connected client or appends to the ring buffer, and sleeps 500 ms.

Power management. esp_pm_configure() sets 96 MHz max / 32 MHz min with automatic light sleep, so FreeRTOS sleeps during delay() while the BLE controller schedules its own wake-ups for connection events. This avoids calling esp_light_sleep_start() manually against running BLE tasks. Requires arduino-esp32 3.x, which enables CONFIG_PM_ENABLE and CONFIG_FREERTOS_USE_TICKLESS_IDLE by default; on 2.x the call fails silently and delay() behaves normally.

Offline buffer. While disconnected, one {dt_s, uvCorrected} record is written every 10 s to a LittleFS-backed ring buffer, with head/tail/count indices mirrored in NVS so the buffer survives reset. Records are 6 bytes; 2048 of them cover about 5.7 hours. clear() resets the logical indices without erasing file data, and iteration is deliberately not invalidated by it.

BLE protocol

Advertises as UV Sensor with the service UUID in the advertisement payload; the app connects by service UUID rather than by name.

UUIDProperties
Service4fafc201-1fb5-459e-8fcc-c5c9c331914b
Live databeb5483e-36e1-4688-b7f5-ea07361b26a8notify
Historyd3e52b10-7a4f-4c8d-a9e6-2f1b8c3d5e70notify

Live notification, 22 bytes packed little-endian (struct.unpack('<HfffHHHH', data)):

OffsetTypeField
0uint16rawUv
2floatuvMwCm2
6floatuvCorrected (NaN when cosTheta < 0.05)
10floatcosTheta
14uint16 x4rawLeft, rawBottom, rawRight, rawTop

History transfer, on the second characteristic:

  1. On connect, the firmware waits 4 s for the client to finish GATT discovery and subscribe.
  2. It snapshots the read cursor and clears the buffer before draining it, so a disconnect mid-transfer cannot replay records on the next connection.
  3. Each record is sent as a 6-byte notification (<Hf), paced 20 ms apart to avoid overflowing the stack's notify queue.
  4. End of stream is a single 0x00 byte. The client treats any payload whose length is not 6 bytes as the terminator.

While a client is connected the buffer is not written to, and the record timer is reset on every live packet so the first offline record after disconnect carries a correct dt_s.

App

bash
cd app
pip install -r requirements.txt
python main.py

App home and settings tabs

Home shows UV index, angle-corrected irradiance, and accumulated dose as a percentage of MED. The progress bar transitions green to orange at 50 % and to red at 80 %, with a subtitle estimating time remaining at the current rate. Dose is persisted to client storage along with the date it was accumulated, and restored on launch only if that date is today, so a new calendar day starts from zero.

Settings holds the connect button, a last-sync timestamp, and the Fitzpatrick phototype selector (I-VI, with a reference card sourced from NCBI StatPearls; changing the phototype resets the dose). An "Advanced" switch reveals a developer panel, hidden by default, exposing the five raw ADC counts, cos(theta), uncorrected irradiance, and manual scan/connect/disconnect/reset controls.

On connect, buffered history is replayed through update_historical() before live streaming starts, and a snack bar reports how much offline time was synced. A dose reset performed while disconnected sets a persisted pending_reset flag that suppresses the next replay.

Dose model

K = 0.001719 is computed in sem_project.m as the ratio of two integrals over the ASTM G-173 solar spectrum: irradiance weighted by the McKinlay-Diffey erythema action spectrum, divided by irradiance weighted by the ML8511's spectral response. It therefore converts what this specific sensor measures into what skin responds to.

Per sample, with the factor 10 converting mW/cm^2 to W/m^2:

text
E_ery [W/m^2] = uvCorrected * 10 * K
dose  [J/m^2] = sum(E_ery * dt)
UVI           = E_ery * 40

Dose is compared against the lower bound of the MED range for each phototype (200 / 250 / 300 / 450 / 600 / 1000 J/m^2 for types I-VI). NaN samples are treated as zero irradiance, and accumulation pauses on disconnect rather than integrating across the gap.

Status

Working prototype; firmware and app function end to end.

Toolchain notes for the original ESP8266 prototype are kept in docs/esp8266-rtos-sdk-linux.md.

License

Copyright (C) 2026 Jan Vodstrčil and Ondřej Pokovič.

Different parts of this project are released under different reciprocal licenses, each carrying its full text in the directory it applies to.

A file's license is determined by the directory it lives in. math/astmg173.xls is third-party ASTM G-173 reference spectrum data distributed by NREL and is not covered by these terms. See LICENSE for the full breakdown.

Parts

33
QtyValueFootprintReferences
51kResistor_SMD:R_1206_3216Metric_Pad1.30x1.75mm_HandSolderR5, R6, R7, R8, R10
410nCapacitor_SMD:C_0805_2012Metric_Pad1.18x1.45mm_HandSolderC1, C2, C3, C4
4560Resistor_SMD:R_1206_3216Metric_Pad1.30x1.75mm_HandSolderR1, R2, R3, R4
4BPW34KiCad:BPW34D1, D2, D3, D4
2100nCapacitor_SMD:C_0805_2012Metric_Pad1.18x1.45mm_HandSolderC7, C8
210kResistor_SMD:R_1206_3216Metric_Pad1.30x1.75mm_HandSolderR11, R14
210uCapacitor_SMD:C_0805_2012Metric_Pad1.18x1.45mm_HandSolderC5, C6
1330kResistor_SMD:R_1206_3216Metric_Pad1.30x1.75mm_HandSolderR13
1Battery_CellConnector_Wire:SolderWire-0.25sqmm_1x02_P4.5mm_D0.65mm_OD2mmBT1
1Conn_01x05_SocketConnector_PinHeader_2.54mm:PinHeader_1x05_P2.54mm_VerticalJ1
1ESP32-H2-ZeroMoje_wearable:Waveshare_ESP32-H2-ZeroU3
1MBR120VLSFT1GDiode_SMD:D_SOD-123FD7
1MCP73831-2-OTPackage_TO_SOT_SMD:SOT-23-5U4
1Red LEDLED_SMD:LED_1206_3216Metric_Pad1.42x1.75mm_HandSolderD6
1TLV9064Package_SO:SOIC-14_3.9x8.7mm_P1.27mmU1

The 31 most common of 33 parts.

Questions

How big is the AOL PD 2 PCB?

It measures 50 × 25 mm and has 2 copper layers, 1.6 mm thick.

What parts does the AOL PD 2 use?

33 parts, built around the TLV9064ID, BPW34 and MBR120VLSFT1G-TP. The full list of its parts is on this page, with links to each part's symbol and footprint where PCBKit's library has them.

Where can I download the AOL PD 2 KiCad files?

From the vodjan/uv-dosimeter repository on GitHub, at the commit this page was made from: the board is hardware/AOL_PD_2.kicad_pcb and its schematic hardware/AOL_PD_2.kicad_sch. PCBKit opens the board in the browser, with nothing to install.

Can I use the AOL PD 2 design in my own project?

Its repository states no licence, so its author keeps all rights: ask vodjan before reusing the design.

Similar boards

  • 4 layers60 × 60 mmMdrive, the printed circuit board in 3D

    Mdrive

    louismeuret/ROBRO

    127 parts2
  • 4 layers85 × 56 mmRadiohead, the printed circuit board in 3D

    Radiohead

    mpvano/radiohat2

    Software and Documentation for the Radiohat2 Project

    175 parts2MIT
  • 2 layers74.5 × 80 mmPico2v2, the printed circuit board in 3D

    Pico2v2

    J-K-Kowalski/PCSK225-PICO2

    PCSK225 Pico2 SDR Receiver

    99 parts1BSD-3-Clause
  • 2 layers95.4 × 90.9 mmLinear CCD camera V1, the printed circuit board in 3D

    Linear CCD camera V1

    Jed0vcela/Linear-CCD-camera-V1

    Prototype of "scanning" camera using linar CCD sensor ILX555K

    100 parts1
  • 2 layers89.4 × 66.9 mmE foc, the printed circuit board in 3D

    E foc

    embedded-pro/e-foc-hardware

    hardware to be used with https://github.com/embedded-pro/e-foc

    186 parts
  • 2 layers80.8 × 48.4 mmAudioMixer, the printed circuit board in 3D

    AudioMixer

    ThomLalcte/AudioMixer

    59 parts