Contents β 26 sections
Project Overview
Measures real UV at a specific park or beach and shows the live UV index and a plain-language safe-exposure time on a bright public display.
The UV index on a weather app is a forecast for a whole city, and it can be badly wrong for the spot you are actually standing in: a shaded plaza, a high-altitude viewpoint, a beach with sand and water bouncing extra UV back at you, a cloudy morning that clears at noon. UV is what causes sunburn, premature skin ageing and, over years, skin cancer β and it is invisible, so people routinely under- or over-estimate it. This monitor measures the actual UV at one public place in real time and shows it on a bright, glanceable display with something more useful than a number: roughly how long until an unprotected person of average skin would start to burn.
The sensing is straightforward and the honesty is in the interpretation. A UV sensor measures the erythemal (sunburn-weighted) UV irradiance, which converts to the standard UV index β the familiar 0-to-11+ scale where each unit is a fixed amount of skin-reddening UV. From the UV index and a typical skin type, the monitor estimates a safe-exposure time: at UV index 3 you might have the better part of an hour before an average unprotected person burns, at UV index 10 only a few minutes. It shows the index, a colour band (green/yellow/orange/red/purple, matching the international standard), and that burn-time in plain words, so a parent at a playground or a swimmer at a beach gets advice they can act on rather than a number they have to interpret.
Because it lives in a public space, it is built to be seen and to run itself: a bright display readable in full sun, solar power so it needs no mains at a remote park or beach, and optional connectivity to log the day's UV profile and feed a network of such displays across a city's parks. It is careful to state its limits β a single low-cost UV sensor is indicative, must be sited in the open and kept clean, and burn-time is an average not a personal guarantee β but as a piece of public-health infrastructure it does something apps cannot: it tells you the truth about the sun where you are, right now, and nudges people to cover up before they get hurt.
What this project does
- Measures real erythemal UV and converts it to the standard UV index
- Estimates a plain-language safe-exposure (burn) time from the index
- Shows the index, the international colour band and advice on a bright display
- Logs the day's UV profile (peak, timing) for a place
- Runs on solar so it needs no mains at a park or beach
- Optionally networks many displays into a city parks map
- States its limits honestly β indicative, sited and cleaned, average advice
Real-World Applications
| Setting | How it is used |
|---|---|
| Parks, playgrounds and beaches | Public-health signage that tells families the real UV and safe time at the exact spot they are enjoying, prompting sun protection. |
| Schools and sports grounds | Live UV at the field so PE and outdoor activity can be timed and protected, especially for children. |
| High-altitude and snow locations | Viewpoints and ski areas where thin air and reflective snow drive UV far above valley forecasts. |
| Workplace sun-safety | Outdoor worksites displaying live UV to trigger break and cover-up policies. |
Deployment contexts where a build of this kind earns its keep.
Features & Capabilities
- Location-true UV, not a citywide forecast
- Standard UV index and the international colour categories
- Actionable burn-time advice in plain language
- Bright, sun-readable public display
- Solar, self-contained, park/beach-ready
- Optional networking for a city parks UV map
- Daily UV-profile logging
Difficulty, Time & Required Skills
| Attribute | Value |
|---|---|
| Difficulty level | Beginner |
| Estimated completion time | 8β12 hours |
| Indicative build cost | βΉ3,800 β βΉ5,200 |
| Primary discipline | Environment |
| Reference platform | ESP32 DevKit V1 (ESP-WROOM-32) |
Skills you should have (or will pick up)
- Reading a UV sensor and converting irradiance to UV index
- Estimating erythemal (burn) exposure time from the index and skin type
- Driving a bright, sun-readable display
- Solar power design for a public installation
- Optional networking of public displays
Bill of Materials
Every part below is commonly available from Indian and international hobby-electronics suppliers. Prices are indicative 2026 retail figures in Indian rupees and will drift β treat them as a budgeting guide, not a quotation.
| Component | Key specification | Qty | Approx. cost |
|---|---|---|---|
| ESP32 DevKit V1 (ESP-WROOM-32) Wi-Fi transmit bursts peak near 500 mA β size the regulator accordingly. | Dual-core Xtensa LX6 @ 240 MHz, 520 KB SRAM, 4 MB flash, Wi-Fi 802.11 b/g/n + BLE 4.2, 34 GPIO, 18Γ 12-bit ADC, 2Γ 8-bit DAC | 1 | βΉ450 |
| 2.4β³ ILI9341 SPI TFT (240 Γ 320) Backlight is most of the current β PWM it for battery builds. | 262 K colour, 40 MHz SPI, optional resistive touch controller | 1 | βΉ750 |
| BH1750 digital ambient light sensor Far more linear than an LDR β use it whenever you need real lux, not a relative value. | 1β65535 lx, 16-bit, Β±20 %, spectral response close to the human eye | 1 | βΉ140 |
| DS18B20 waterproof temperature probe Dozens can share one GPIO β you address them by ROM code. | β55 to +125 Β°C, Β±0.5 Β°C from β10 to +85 Β°C, 9β12-bit resolution, unique 64-bit ROM ID | 1 | βΉ160 |
| 20 W 12 V polycrystalline solar panel Rated watts assume 1000 W/mΒ² β plan for 60β70 % of nameplate in real installs. | Vmp 17.5 V, Imp 1.14 A, Voc 21.6 V, 350 Γ 290 mm, aluminium frame | 1 | βΉ1,200 |
| TP4056 Li-ion charger + DW01 protection Buy the version *with* protection ICs β the bare charger will over-discharge your cell. | 1 A programmable CC/CV charge to 4.2 V Β±1 %, over-discharge and short protection | 1 | βΉ45 |
| 18650 Li-ion cell 3400 mAh + holder Never charge below 0 Β°C; always use a protected cell or a BMS. | 3.7 V nominal, 4.2 V full, 3400 mAh, ~12.6 Wh, 2 C discharge | 1 | βΉ450 |
| UV sensor (erythemal) Choose one that reports UVA/UVB or UV index; calibrate if possible | LTR390 / VEML6075 / Si1145-class UV sensor; erythemal/UVI output | 1 | βΉ350 |
| Bright sun-readable display Must be legible in direct sunlight β the whole point | High-nit TFT, large LED digits, or an outdoor LED matrix behind UV-stable glazing | 1 | βΉ1,200 |
| UV-transparent sensor window Ordinary acrylic blocks UV β use a UV-passing material | A cap/window that passes UV (many plastics block it) protecting the sensor | 1 | βΉ120 |
| Outdoor pedestal/housing | Weatherproof housing, sensor on top facing the sky, display at eye level | 1 | βΉ900 |
Estimated total: βΉ5,765, excluding tools, shipping and consumables.
Tools and consumables
- Soldering iron (temperature controlled, 350 Β°C) with 0.8 mm 60/40 or lead-free solder
- Digital multimeter β continuity, DC volts and current ranges
- Wire strippers, flush cutters and a small set of precision screwdrivers
- Heat-shrink tubing and a heat gun (or a lighter, carefully)
- A laptop with a USB port and the toolchain listed above
Hardware Specifications
| Part | Specification | Supply | Interface | Reference |
|---|---|---|---|---|
| ESP32 DevKit V1 (ESP-WROOM-32) | Dual-core Xtensa LX6 @ 240 MHz, 520 KB SRAM, 4 MB flash, Wi-Fi 802.11 b/g/n + BLE 4.2, 34 GPIO, 18Γ 12-bit ADC, 2Γ 8-bit DAC | 3.3 V logic / 5 V USB | UART, SPI, IΒ²C, IΒ²S, CAN, PWM | Datasheet |
| 2.4β³ ILI9341 SPI TFT (240 Γ 320) | 262 K colour, 40 MHz SPI, optional resistive touch controller | 3.3 V | SPI | Datasheet |
| BH1750 digital ambient light sensor | 1β65535 lx, 16-bit, Β±20 %, spectral response close to the human eye | 2.4β3.6 V | IΒ²C (0x23/0x5C) | Datasheet |
| DS18B20 waterproof temperature probe | β55 to +125 Β°C, Β±0.5 Β°C from β10 to +85 Β°C, 9β12-bit resolution, unique 64-bit ROM ID | 3.0β5.5 V | 1-Wire (multi-drop) | Datasheet |
| 20 W 12 V polycrystalline solar panel | Vmp 17.5 V, Imp 1.14 A, Voc 21.6 V, 350 Γ 290 mm, aluminium frame | 12 V nominal | MC4 / screw terminals | Datasheet |
| TP4056 Li-ion charger + DW01 protection | 1 A programmable CC/CV charge to 4.2 V Β±1 %, over-discharge and short protection | 4.5β5.5 V in | micro-USB / pads | Datasheet |
| 18650 Li-ion cell 3400 mAh + holder | 3.7 V nominal, 4.2 V full, 3400 mAh, ~12.6 Wh, 2 C discharge | 3.0β4.2 V | Holder / spot-welded tabs | Datasheet |
Consolidated electrical and interface specifications for every active part in the build.
Power Budget & Supply Sizing
Add up the typical active current of every part, then size the supply with at least 50 % headroom so transmit bursts and motor inrush never brown out the controller.
| Load | Supply rail | Typical current (mA) | Notes |
|---|---|---|---|
| ESP32 DevKit V1 (ESP-WROOM-32) | 3.3 V logic / 5 V USB | 160 | Wi-Fi transmit bursts peak near 500 mA β size the regulator accordingly. |
| 2.4β³ ILI9341 SPI TFT (240 Γ 320) | 3.3 V | 90 | Backlight is most of the current β PWM it for battery builds. |
| BH1750 digital ambient light sensor | 2.4β3.6 V | 0.19 | Far more linear than an LDR β use it whenever you need real lux, not a relative value. |
| DS18B20 waterproof temperature probe | 3.0β5.5 V | 1.5 | Dozens can share one GPIO β you address them by ROM code. |
| 20 W 12 V polycrystalline solar panel | 12 V nominal | 1140 | Rated watts assume 1000 W/mΒ² β plan for 60β70 % of nameplate in real installs. |
| TP4056 Li-ion charger + DW01 protection | 4.5β5.5 V in | 1000 | Buy the version *with* protection ICs β the bare charger will over-discharge your cell. |
Summed typical draw is 2391.69 mA. With a 1.5Γ design margin the supply should deliver at least 3600 mA continuously at the stated rail voltage.
Software Requirements & Development Environment
Reference toolchain: Arduino IDE 2.3.x with the ESP32 board package 3.x (or PlatformIO on VS Code). Anything newer normally works; anything older may lack the board definitions used here.
- Install the Arduino IDE 2.3.x (or PlatformIO if you prefer a real editor and dependency locking).
- Add
https://espressif.github.io/arduino-esp32/package_esp32_index.jsonunder File β Preferences β Additional Board Manager URLs, then install esp32 from the Boards Manager. - Set the correct port under Tools β Port. On Linux add yourself to the
dialoutgroup:sudo usermod -aG dialout $USERand log out and back in. - Open the Serial Monitor at 115200 baud β every sketch here logs its state there.
- Keep File β Preferences β Show verbose output during: compilation switched on while you are debugging build errors.
Required libraries
| Library | Why it is needed | Install |
|---|---|---|
| WiFi (ESP32 core) bundled | Station/AP connection management for the ESP32. | Bundled with the ESP32 Arduino core |
| PubSubClient 2.8 | Lightweight MQTT 3.1.1 client for constrained devices. | Library Manager β "PubSubClient" by Nick O'Leary |
| BH1750 1.3.0 | Digital lux readings with selectable resolution modes. | Library Manager β "BH1750" by Christopher Laws |
| Adafruit Unified Sensor 1.1.x | Common sensor event abstraction; a dependency of most Adafruit drivers. | Library Manager β "Adafruit Unified Sensor" |
| ArduinoJson 7.x | Zero-allocation JSON serialisation and parsing. | Library Manager β "ArduinoJson" by Benoit Blanchon |
| Preferences (NVS) bundled | Wear-levelled key/value storage in ESP32 flash for settings. | Bundled with the ESP32 core |
| NTPClient / configTime bundled | Wall-clock time from an NTP server for timestamping. | Bundled (`configTime()` on ESP32) |
Block Diagram
The block diagram shows the functional decomposition of the system β what senses, what decides, what acts, and where the data ends up.
Circuit Diagram & Wiring
Every signal line in the build is shown below, followed by a pin-by-pin connection table you can work through with a multimeter in hand.
| Peripheral | Peripheral pin | Controller pin | Signal |
|---|---|---|---|
| UV sensor | SDA/SCL | GPIO 21/22 | Erythemal UV / UVI (IΒ²C) |
| BH1750 (opt) | SDA/SCL | GPIO 21/22 | Visible light (cross-check) |
| DS18B20 | DQ | GPIO 4 | Enclosure temp (sensor health) |
| Bright display | SPI/parallel | GPIO 18/19/23/5 | Public UV readout |
| Wi-Fi (opt) | on-chip | β | Networked UV map + logging |
| TP4056 | OUT | VIN / 3V3 reg | Solar-charged supply |
| Solar panel | +/β | TP4056 IN | 6 V panel β charger |
Wire one row at a time and tick it off β most "it does not work" reports trace back to a single swapped pair.
Wiring explanation
- Mount the UV sensor on top of the housing facing straight up at the open sky, behind a UV-transparent window β ordinary acrylic blocks UV and would make the reading meaningless.
- Keep the sensor clear of any shade, overhang or nearby wall that blocks part of the sky or reflects onto it.
- Drive the bright display on its own supply headroom; a large LED display can draw significant current and must not brown out the sensor node.
- Add an enclosure-temperature sensor so you can flag over-heating, which affects both the sensor and the display.
- Angle the solar panel for the site and size it for the display's daytime draw plus overnight standby.
System Architecture
Read the stack from the bottom up: physical hardware, the firmware that drives it, the transport that moves data off the device, and the software a human actually looks at.
Working Principle
The quantity that matters for health is not raw UV power but erythemally-weighted UV β UV weighted by how effectively each wavelength reddens skin, since shorter-wavelength UVB burns far more per watt than UVA. The internationally standardised UV index is built directly on this: it is the erythemal irradiance scaled so that each index unit equals a fixed amount of skin-reddening UV (25 mW/mΒ² of erythemal irradiance per unit). That standardisation is what lets a single number mean the same thing everywhere and map to the familiar colour categories β low (green) through extreme (purple) β that public-health bodies use worldwide. A good UV sensor either reports the index directly or an erythemal irradiance the firmware converts.
The monitor's value over a forecast is locality and truth. A citywide UV forecast cannot know that this particular spot is shaded until noon, sits at altitude where thinner air lets more UV through, or is a beach where sand and water reflect additional UV onto people. By measuring the sky at the actual location, the display reflects the sun people are really under β which can differ substantially from the app in their pocket. This demands correct siting: the sensor must see the whole open sky, behind a UV-transparent window, unshaded and clean, because a sensor under an overhang or a dusty cover reads low and would hand out dangerously reassuring advice.
What turns the index into action is the safe-exposure (burn) time. Because a UV index unit is a fixed dose rate, and skin has a characteristic dose at which it starts to redden (the minimal erythemal dose, which depends on skin type), you can estimate how long until an unprotected average person begins to burn: high index means the dose accumulates fast, so the time is short; low index means it is long. The display shows this in plain language β "about X minutes to burn without protection" β alongside the index and colour, which is far more actionable to a passer-by than a bare number. Crucially, it is framed as an average for a typical skin type, because the minimal erythemal dose varies several-fold between very fair and very dark skin, and the display says so rather than implying a personal guarantee.
Everything else serves getting that message to people reliably in a public place. The display must be genuinely readable in direct sunlight β the environment it exists to describe is the one that washes out ordinary screens β so it uses a high-brightness panel or large LEDs. It runs on solar so it can stand at a remote beach or hilltop with no mains. And optionally it joins a network of such displays, logging each site's daily UV profile (when the peak occurs, how high it reaches) and feeding a city map so people can see, and planners can study, how UV varies across a city's public spaces. It is modest infrastructure with a real public-health job: making an invisible, cumulative hazard visible at the moment and place people are exposed to it.
The maths behind it
UV index from erythemal irradiance
The UV index is defined from erythemal irradiance E_er:
UVI = E_er (mW/mΒ²) / 25
i.e. 1 index unit = 25 mW/mΒ² of skin-reddening UV.
A sensor giving E_er (or UVA/UVB it converts) yields UVI
directly. Categories: 0β2 low, 3β5 moderate, 6β7 high,
8β10 very high, 11+ extreme.
Safe-exposure (burn) time
Skin burns after a minimal erythemal dose (MED),
which depends on skin type. As a rate:
dose_rate β UVI
t_burn (min) β MED_factor / UVI
A common rule of thumb for average (type IIβIII) skin:
t_burn β 200 / (UVI Γ skin_factor) minutes
Stronger for fair skin, longer for darker skin. Display it
as AVERAGE guidance, never a personal guarantee.
Reflective and altitude enhancement (why local matters)
Local UV can exceed a valley forecast because:
β’ altitude: +~10β12% UV per 1000 m of elevation
β’ snow reflects up to ~80% of UV back onto you
β’ sand ~15%, water ~10% add to direct UV
A sensor measures the ACTUAL local UV including these,
which a citywide forecast cannot capture.
Program Flowchart
The firmware is a single cooperative loop. Nothing blocks for long, so networking, sensing and the user interface all stay responsive.
Assembly Instructions
Build on a breadboard first and only commit to solder once the whole system has run for an hour without a fault.
Site and mount the UV sensor correctly
Fix the UV sensor on top of the housing facing straight up, behind a UV-transparent window, with an unobstructed view of the whole sky β no overhangs, branches or walls in its hemisphere.
Confirm the window material actually passes UV; many clear plastics block it and would silently kill the reading.
Fit the bright display
Mount a high-brightness display or large LED digits at eye level, behind UV-stable glazing, angled to avoid direct glare while staying legible in full sun.
Give the display its own current headroom so it never browns out the sensor node.
Set up solar and optional networking
Angle and size the solar panel for the display's daytime draw plus standby; add Wi-Fi if joining a parks network and logging daily profiles.
Step-by-Step Implementation Guide
Work through these in order. Each step ends in something you can observe, so a failure is always localised to the step you just finished.
Read UV and compute index, band and burn-time
Read the UV sensor, convert to UV index, map to the international colour category, and estimate an average safe-exposure time, clamping sensibly at the extremes.
cppuv-advice.inostruct UVInfo { float uvi; const char *band; uint32_t burnMin; const char *advice; }; const char* uvBand(float uvi) { if (uvi < 3) return "LOW"; if (uvi < 6) return "MODERATE"; if (uvi < 8) return "HIGH"; if (uvi < 11) return "VERY HIGH"; return "EXTREME"; } // Average burn time for typical (type IIβIII) skin; guidance only. uint32_t burnMinutes(float uvi) { if (uvi < 0.5f) return 999; // effectively no burn risk float t = 200.0f / (uvi * 1.0f); // skin_factor = 1 (average) return (uint32_t)constrain(t, 5.0f, 999.0f); } const char* uvAdvice(float uvi) { if (uvi < 3) return "Enjoy β minimal protection needed"; if (uvi < 6) return "Hat + sunscreen; seek shade midday"; if (uvi < 8) return "Cover up, SPF30+, shade 11-3"; if (uvi < 11) return "Avoid sun midday; full protection"; return "Stay in shade; extreme risk"; } UVInfo uvInfo(float erythemal_mWm2) { float uvi = erythemal_mWm2 / 25.0f; // definition of the UV index return { uvi, uvBand(uvi), burnMinutes(uvi), uvAdvice(uvi) }; }float uvi = erythemal_mWm2 / 25.0fApplies the exact definition of the UV index β 25 mW/mΒ² of erythemal irradiance per index unit β turning the sensor's measurement into the standard scale.const char* uvBand(float uvi)Maps the index to the internationally standardised colour categories, so the display speaks the same language as every public UV forecast.float t = 200.0f / (uvi * 1.0f)Estimates the average time to burn as inversely proportional to the index, since a UV index unit is a fixed dose rate β higher index, faster dose, shorter time.constrain(t, 5.0f, 999.0f)Clamps the estimate to sensible bounds so extreme UV never shows an alarmingly precise "2 minutes" and near-zero UV does not show an infinite time.Drive the display and (optionally) the network
Show the big index number, the colour band, and the burn-time and advice; log the reading; and if networked, publish the site's UV so a parks map and daily-profile record update.
Complete Source Code
The listing below is complete and compiles as written β there are no elided sections. Read the annotations under each block before you upload it.
/* βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
UV Index Public Monitor β ESP32 + UV sensor + bright display
Measures real erythemal UV at a public place, shows the UV index,
international colour band and an average safe-exposure time, logs the
daily profile, and optionally feeds a city parks UV map. Solar.
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ */
#include <WiFi.h>
#include <PubSubClient.h>
#include <Wire.h>
#include <BH1750.h>
#include <Preferences.h>
// Replace with your UV sensor's driver (LTR390 / VEML6075 / Si1145).
#include "UvSensor.h"
#define PIN_TFT_DC 16
#define INTERVAL_MS 30000UL // update every 30 s
UvSensor uv;
BH1750 vis; // visible-light cross-check
Preferences prefs;
WiFiClient net;
PubSubClient mqtt(net);
float UV_CAL; // reference-calibration scale
float dayPeakUVI = 0; int dayPeakHour = -1;
const char* uvBand(float u){
if(u<3)return "LOW"; if(u<6)return "MODERATE"; if(u<8)return "HIGH";
if(u<11)return "VERY HIGH"; return "EXTREME";
}
uint32_t burnMinutes(float u){
if(u<0.5f)return 999;
float t=200.0f/(u*1.0f); return (uint32_t)constrain(t,5.0f,999.0f);
}
const char* uvAdvice(float u){
if(u<3)return "Minimal protection needed";
if(u<6)return "Hat + sunscreen; shade midday";
if(u<8)return "Cover up, SPF30+, shade 11-3";
if(u<11)return "Avoid midday sun; full protection";
return "Stay in shade; extreme risk";
}
void showDisplay(float uvi, const char* band, uint32_t burn, const char* adv,
bool suspect) {
// Big index, colour band, burn-time and advice on a bright panel.
displayClear();
displayBigNumber(uvi);
displayBand(band); // coloured per UVI category
if (burn < 999) displayBurn(burn); // "~%u min to burn"
displayAdvice(adv);
if (suspect) displayFlag("sensor check"); // shaded/dirty/faulty
}
void setup() {
Serial.begin(115200);
Wire.begin(21,22);
uv.begin();
vis.begin(BH1750::CONTINUOUS_HIGH_RES_MODE);
prefs.begin("uv",true); UV_CAL=prefs.getFloat("cal",1.0f); prefs.end();
displayInit();
WiFi.begin(WIFI_SSID,WIFI_PASS);
mqtt.setServer(MQTT_HOST,1883);
}
void loop() {
if(!mqtt.connected()&&WiFi.status()==WL_CONNECTED) mqtt.connect("uv-1");
mqtt.loop();
float erythemal = uv.readErythemal_mWm2() * UV_CAL;
float uvi = erythemal / 25.0f; // UV index definition
float lux = vis.readLightLevel();
// Cross-check: bright daylight but ~zero UV => sensor shaded/dirty/faulty.
bool suspect = (lux > 20000.0f && uvi < 0.5f);
const char* band = uvBand(uvi);
uint32_t burn = burnMinutes(uvi);
const char* adv = uvAdvice(uvi);
showDisplay(uvi, band, burn, adv, suspect);
// daily profile
int hr = nowHour();
if (uvi > dayPeakUVI) { dayPeakUVI = uvi; dayPeakHour = hr; }
if (WiFi.status()==WL_CONNECTED) {
char b[200];
snprintf(b,sizeof b,
"{\"site\":1,\"uvi\":%.1f,\"band\":\"%s\",\"burn_min\":%u,"
"\"peak\":%.1f,\"suspect\":%d}",
uvi, band, burn, dayPeakUVI, suspect?1:0);
mqtt.publish("uv/site/1/reading", b);
}
delay(INTERVAL_MS);
}
Configuration & Calibration
Configuration steps
- Set UV_CAL from a reference calibration so the displayed index matches a trusted UV meter.
- Choose the skin-type factor and burn-time framing text; always present it as average guidance.
- Set the display brightness/day-night behaviour and the update interval.
- Enable Wi-Fi/MQTT and daily-profile logging if joining a parks network.
Calibration procedure
An uncalibrated sensor produces confident, precise, wrong numbers. Do this once per physical unit and record the constants.
Reference UV
Compare the node against a calibrated UV-index meter across a range (morning to midday) and set UV_CAL so they agree.
Siting check
Confirm the sensor sees the full open sky and that the window passes UV; verify readings track the sun's arc and are not clipped by shade.
Cross-check logic
Validate the visible-light cross-check by shading only the UV sensor and confirming the display flags "sensor check".
Network Architecture & Connectivity
Communication protocol
Each display optionally publishes its live UV index, band and daily peak. Networking is a bonus β the display works fully stand-alone β so a park with no connectivity still protects the people in front of it.
| Topic / endpoint | Direction | Payload |
|---|---|---|
uv/site/1/reading | node β broker | UVI, band, burn-time, daily peak, suspect flag |
uv/site/1/status | node β broker | battery, calibration date, sensor health |
uv/site/1/config | broker β node | calibration, display settings |
Message contract between the device and the broker.
Cloud platform configuration
A broker feeds a city parks map of live UV and archives each site's daily profile, useful for public-health messaging and for studying how UV varies across a city's open spaces.
Dashboard setup
A parks map coloured by live UV index, with per-site daily curves showing when and how high UV peaks at each location.
Mobile app integration
Optional alerts when a favourite park hits very-high/extreme UV, prompting sun protection or a change of plans.
Security considerations
- Sign readings so a public map cannot be spoofed with false "safe" UV values.
- Keep the stand-alone display fully functional without the network β connectivity must never gate the public-health message.
- Flag and surface any "sensor check" condition so a mis-sited display is corrected quickly.
Testing Procedure & Expected Output
Test from the bottom up. Confirm power, then each sensor in isolation, then the integrated loop β the first failing step tells you exactly where to look.
| Test | What you should see |
|---|---|
| Compare to a UV meter at midday | Displayed UV index matches the reference after calibration |
| Move through the day | Index rises to a midday peak and falls; daily peak/time logged |
| Shade only the UV sensor in daylight | Cross-check flags "sensor check"; advice not falsely reassuring |
| Read the display in direct sun | Index, colour and advice remain legible |
| Check burn-time at low and extreme UV | Sensible clamped values with average-guidance framing |
| Run a solar day/night cycle | Battery covers the bright display's daytime draw; recovers overnight |
Bench-test checklist. If a row fails, stop and fix it before moving on.
Expected output
The display shows a large UV-index number, the colour band, and a burn-time with plain advice; if networked, a parks map shows each site's live UV and daily peak.
{
"site": 1,
"uvi": 9.2,
"band": "VERY HIGH",
"burn_min": 22,
"peak": 9.6,
"suspect": 0
}
Here the live UV index is 9.2 (Very High), with an average burn time of about 22 minutes and a day-peak of 9.6 β a clear, local, actionable public-health message the citywide forecast could not give.
Troubleshooting: Common Errors & Fixes
Performance Optimisation
- Update every ~30 s β UV changes slowly enough that faster is unnecessary and costs display power.
- Dim or duty-cycle the bright display when no one is around (e.g. at night) to save solar budget.
- Do the trivial index/burn-time maths on-device; there is no heavy computation here.
- Cache the day's peak locally so the profile survives brief resets.
- Replace every
delay()with amillis()comparison β blocking delays are the single most common cause of dropped readings. - Sample sensors on a fixed cadence and publish on a slower one; you almost never need to transmit at the sampling rate.
- Move networking into its own FreeRTOS task so a slow DNS lookup cannot stall the control loop.
- Use
uint8_t/uint16_twhere the range allows; on an 8-bit AVR a 32-bit add costs four times as much. - Batch several samples into one MQTT publish. Radio time, not CPU time, dominates the energy budget.
- Set the MQTT keep-alive to a value that matches your reporting interval so the broker does not churn reconnections.
- For battery builds use deep sleep between samples: an ESP32 drops from ~160 mA awake to about 10 Β΅A asleep, which is the difference between days and months of runtime.
Safety Precautions
- Site for full open sky and keep the sensor clean β a low reading gives dangerously reassuring advice.
- Present burn-time as average guidance for a typical skin type; individuals, especially fair-skinned and children, vary and need more caution.
- Make clear the display is a public-health aid, not a substitute for personal sun-protection judgement.
- Use UV-stable materials for the housing and glazing so the installation itself does not degrade in the sun it measures.
- Lithium cells vent and burn when abused. Only use protected cells or a proper BMS, never charge below 0 Β°C, and never leave a charging pack unattended on a wooden desk.
- Wear eye protection when soldering or cutting, and solder in a ventilated space β rosin flux fumes are a respiratory irritant.
- Power the circuit through a bench supply with a current limit while you are testing. A 300 mA limit turns a wiring mistake into a beep instead of a dead board.
- Disconnect power before changing any wiring. Hot-plugging a sensor onto a live bus is the fastest way to lose a controller.
Maintenance
- Clean the sensor window regularly; dust and grime cut UV and bias the reading low.
- Re-verify calibration against a reference periodically; UV sensors can drift.
- Check the display legibility and brightness, and the solar panel cleanliness.
- Confirm the siting is still clear as trees grow or structures change around the installation.
- Re-check every screw terminal and header after the first week β thermal cycling loosens connections that felt tight on day one.
- Log pack voltage. When resting voltage after a full charge drops below about 4.0 V, the cell is near end of life β replace it.
- Wash the panel every few weeks in dusty conditions; a visible dust film costs 15β25 % of the harvest.
- Keep the broker and dashboard containers patched, and rotate device credentials at least once a year.
- Recalibrate at the interval given in the calibration section, and keep the constants in a text file next to the firmware β not only in flash.
- Keep a short logbook of firmware versions and what changed. Six months later you will not remember why that constant is 1.083.
Future Improvements & Upgrades
A working v1 is a platform, not a finish line. These are the upgrades that add the most capability for the least rework.
- Add a skin-type selector (button) so a user can get advice tuned to their skin.
- Add spoken/multilingual advice and larger accessibility features for public spaces.
- Correlate logged UV with local ozone and cloud data to explain and forecast site UV.
- Integrate with park PA/signage to broadcast sun-safety reminders at high-UV times.
- Design a proper PCB. Once the breadboard version has run for a month, moving to a two-layer board removes the intermittent-contact failures that dominate prototype faults.
- Add over-the-air firmware updates so you never have to physically reach a deployed node again.
- Add persistent local storage (microSD or the on-chip flash) so a network outage does not create a hole in your data.
- Move configuration out of the source: a captive-portal setup page or a JSON config file makes the build reusable without a recompile.
- Add a battery and solar option so the unit survives a power cut and can be sited away from a socket.
- Write a small test harness that feeds synthetic sensor values through the decision logic, so you can validate thresholds without physically triggering the event.
Frequently Asked Questions
References & Learning Resources
These are the primary sources worth reading in full. Manufacturer datasheets always outrank forum posts when the two disagree.