Siddhant Kumar
Project 044 Β· Environment

UV Index Public Monitor.

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.

Beginner 8–12 hours 34 min read SensorDisplayHealth
Jump to source Bill of materials
UV Index Public Monitor β€” reference build illustration MCU VCC Β· GND Β· SIG Β· NC
Difficulty
Beginner
Build time
8–12 hours
Indicative cost
β‚Ή3,800 – β‚Ή5,200
Platform
ESP32 DevKit V1 (ESP-WROOM-32)
Category
Environment
Last updated
28 July 2026
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.

A city skyline at night
A public UV display in a park or beach shows the real local UV where a citywide forecast cannot. Photograph sourced from Wikimedia Commons β€” Smart city.jpg. Reused under the licence stated on that page; please check it before republishing.

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

SettingHow it is used
Parks, playgrounds and beachesPublic-health signage that tells families the real UV and safe time at the exact spot they are enjoying, prompting sun protection.
Schools and sports groundsLive UV at the field so PE and outdoor activity can be timed and protected, especially for children.
High-altitude and snow locationsViewpoints and ski areas where thin air and reflective snow drive UV far above valley forecasts.
Workplace sun-safetyOutdoor 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

AttributeValue
Difficulty levelBeginner
Estimated completion time8–12 hours
Indicative build costβ‚Ή3,800 – β‚Ή5,200
Primary disciplineEnvironment
Reference platformESP32 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.

ComponentKey specificationQtyApprox. 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 DAC1β‚Ή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 controller1β‚Ή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 eye1β‚Ή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 ID1β‚Ή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 frame1β‚Ή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 protection1β‚Ή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 discharge1β‚Ή450
UV sensor (erythemal)
Choose one that reports UVA/UVB or UV index; calibrate if possible
LTR390 / VEML6075 / Si1145-class UV sensor; erythemal/UVI output1β‚Ή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 glazing1β‚Ή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 sensor1β‚Ή120
Outdoor pedestal/housingWeatherproof housing, sensor on top facing the sky, display at eye level1β‚Ή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

PartSpecificationSupplyInterfaceReference
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 DAC3.3 V logic / 5 V USBUART, SPI, IΒ²C, IΒ²S, CAN, PWMDatasheet
2.4β€³ ILI9341 SPI TFT (240 Γ— 320)262 K colour, 40 MHz SPI, optional resistive touch controller3.3 VSPIDatasheet
BH1750 digital ambient light sensor1–65535 lx, 16-bit, Β±20 %, spectral response close to the human eye2.4–3.6 VIΒ²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 ID3.0–5.5 V1-Wire (multi-drop)Datasheet
20 W 12 V polycrystalline solar panelVmp 17.5 V, Imp 1.14 A, Voc 21.6 V, 350 Γ— 290 mm, aluminium frame12 V nominalMC4 / screw terminalsDatasheet
TP4056 Li-ion charger + DW01 protection1 A programmable CC/CV charge to 4.2 V Β±1 %, over-discharge and short protection4.5–5.5 V inmicro-USB / padsDatasheet
18650 Li-ion cell 3400 mAh + holder3.7 V nominal, 4.2 V full, 3400 mAh, ~12.6 Wh, 2 C discharge3.0–4.2 VHolder / spot-welded tabsDatasheet

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.

LoadSupply railTypical current (mA)Notes
ESP32 DevKit V1 (ESP-WROOM-32)3.3 V logic / 5 V USB160Wi-Fi transmit bursts peak near 500 mA β€” size the regulator accordingly.
2.4β€³ ILI9341 SPI TFT (240 Γ— 320)3.3 V90Backlight is most of the current β€” PWM it for battery builds.
BH1750 digital ambient light sensor2.4–3.6 V0.19Far more linear than an LDR β€” use it whenever you need real lux, not a relative value.
DS18B20 waterproof temperature probe3.0–5.5 V1.5Dozens can share one GPIO β€” you address them by ROM code.
20 W 12 V polycrystalline solar panel12 V nominal1140Rated watts assume 1000 W/mΒ² β€” plan for 60–70 % of nameplate in real installs.
TP4056 Li-ion charger + DW01 protection4.5–5.5 V in1000Buy 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.json under 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 dialout group: sudo usermod -aG dialout $USER and 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

LibraryWhy it is neededInstall
WiFi (ESP32 core) bundledStation/AP connection management for the ESP32.Bundled with the ESP32 Arduino core
PubSubClient 2.8Lightweight MQTT 3.1.1 client for constrained devices.Library Manager β†’ "PubSubClient" by Nick O'Leary
BH1750 1.3.0Digital lux readings with selectable resolution modes.Library Manager β†’ "BH1750" by Christopher Laws
Adafruit Unified Sensor 1.1.xCommon sensor event abstraction; a dependency of most Adafruit drivers.Library Manager β†’ "Adafruit Unified Sensor"
ArduinoJson 7.xZero-allocation JSON serialisation and parsing.Library Manager β†’ "ArduinoJson" by Benoit Blanchon
Preferences (NVS) bundledWear-levelled key/value storage in ESP32 flash for settings.Bundled with the ESP32 core
NTPClient / configTime bundledWall-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.

UV Index Public Monitor β€” system block diagramFunctional block diagram of the UV Index Public Monitor system. Measure skyUV sensorerythemal β†’ UVIVisible (opt)BH1750 cross-checkInterpretESP32UVI + burn-timeCategorycolour bandShowBright displayindex + adviceNetwork (opt)Parks mapcity UVLogdaily profilerightrightnone
UV Index Public Monitor β€” system block diagram

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.

UV Index Public Monitor β€” wiring schematicConnection schematic showing which controller pin drives each peripheral. Sensors / InputsControllerActuators / OutputsESP32 DevKit V1(ESP-WROOM-32)3.3 V logic / 5 V USBUV sensorGPIO 21/22Erythemal UV / UVI(IΒ²C)BH1750 (opt)GPIO 21/22Visible light(cross-check)DS18B20GPIO 4Enclosure temp(sensor health)Bright displayGPIO 18/19/23/5Public UV readoutWi-Fi (opt)β€”Networked UV map +loggingTP4056VIN / 3V3 regSolar-charged supplySolar panelTP4056 IN6 V panel β†’ charger
UV Index Public Monitor β€” wiring schematic
PeripheralPeripheral pinController pinSignal
UV sensorSDA/SCLGPIO 21/22Erythemal UV / UVI (IΒ²C)
BH1750 (opt)SDA/SCLGPIO 21/22Visible light (cross-check)
DS18B20DQGPIO 4Enclosure temp (sensor health)
Bright displaySPI/parallelGPIO 18/19/23/5Public UV readout
Wi-Fi (opt)on-chipβ€”Networked UV map + logging
TP4056OUTVIN / 3V3 regSolar-charged supply
Solar panel+/–TP4056 IN6 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.
A small monochrome OLED display module
A bright, sun-readable display presents the UV index, colour band and safe-exposure advice at a glance. Photograph sourced from Wikimedia Commons β€” OLED display module.jpg. Reused under the licence stated on that page; please check it before republishing.

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.

UV Index Public Monitor β€” architecture stackLayered architecture from hardware to user interface. Hardware layerESP32 DevKit V1 (ESP-WROOM-32) Β· BH1750 digital ambient light sensor Β·DS18B20 waterproof temperature probeDriver layerwifi Β· pubsub Β· bh1750lib Β· unifiedApplication logicsampling loop Β· filtering Β· thresholds Β· state machineTransport layerOptional Wi-Fi + MQTT β†’ city parks UV map Β· TLS Β· retry and backoffPresentation layerdashboard Β· mobile notifications Β· historical charts
UV Index Public Monitor β€” architecture stack

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

plainUV 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

plainSafe-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)

plainReflective 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.

UV Index Public Monitor β€” firmware flowchartControl flow through the main program loop. Read UV sensorConvert to UV indexCompute burn-time + colourbandUV index high (β‰₯ 8)?Show strong-protection adviceShow standard adviceShow strong-protection adviceShow standard adviceUpdate display; log; (network)Wait interval
UV Index Public Monitor β€” firmware flowchart

Assembly Instructions

Build on a breadboard first and only commit to solder once the whole system has run for an hour without a fault.

  1. 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.

  2. 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.

  3. 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.

  1. 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.ino
    struct 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.
  2. 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.

cppuv-public-monitor.ino
/* ═══════════════════════════════════════════════════════════════
   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);
}
float uvi = erythemal / 25.0fConverts the calibrated erythemal irradiance to the standard UV index by its defining constant, so the displayed number is the real, comparable index.
bool suspect = (lux > 20000.0f && uvi < 0.5f)Cross-checks UV against visible light: bright daylight with near-zero UV means the UV sensor is shaded, dirty or faulty, so the display flags itself rather than handing out falsely reassuring advice.
if (burn < 999) displayBurn(burn)Shows the average time-to-burn only when there is a real burn risk, keeping the public advice concrete and actionable at high UV and uncluttered at low UV.
if (uvi > dayPeakUVI)Tracks the day's peak UV and when it occurred, building the per-site daily profile that the network logs and maps.
mqtt.publish("uv/site/1/reading"Publishes the site's live UV, band and peak so a city parks map and daily-profile record can aggregate many displays.

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.

  1. Reference UV

    Compare the node against a calibrated UV-index meter across a range (morning to midday) and set UV_CAL so they agree.

  2. 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.

  3. 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

UV Index Public Monitor β€” network topologyPath taken by telemetry from field node to end user. Edge nodesGatewayCloudClientsUV displayESP32 + sensorOther parksmore displaysWi-Fi 2.4 GHzRouterper siteMQTT 1883Broker + parks maplive + daily profileParks mapcity UVPhonehigh-UV alerts
UV Index Public Monitor β€” network topology

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 / endpointDirectionPayload
uv/site/1/readingnode β†’ brokerUVI, band, burn-time, daily peak, suspect flag
uv/site/1/statusnode β†’ brokerbattery, calibration date, sensor health
uv/site/1/configbroker β†’ nodecalibration, 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.

TestWhat you should see
Compare to a UV meter at middayDisplayed UV index matches the reference after calibration
Move through the dayIndex rises to a midday peak and falls; daily peak/time logged
Shade only the UV sensor in daylightCross-check flags "sensor check"; advice not falsely reassuring
Read the display in direct sunIndex, colour and advice remain legible
Check burn-time at low and extreme UVSensible clamped values with average-guidance framing
Run a solar day/night cycleBattery 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.

jsonuv-reading.json
{
  "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.

An ESP32 development board with the ESP-WROOM-32 module and USB connector
ESP32 module converting the UV sensor reading into the standard index and plain-language guidance. Photograph sourced from Wikimedia Commons β€” ESP32 Espressif ESP-WROOM-32 Dev Board.jpg. Reused under the licence stated on that page; please check it before republishing.

Troubleshooting: Common Errors & Fixes

UV reads low on a clearly sunny day

Likely cause. Sensor shaded/dirty, or window blocks UV

Fix. Re-site for full open sky; clean the window; confirm the window passes UV; the cross-check should flag this

Display unreadable in sunlight

Likely cause. Low-brightness panel

Fix. Use a high-nit display or large LEDs; reduce glare with placement/glazing

Index disagrees with a reference meter

Likely cause. Uncalibrated sensor

Fix. Calibrate UV_CAL against a trusted UV-index meter across a range

Burn-time looks alarmingly precise

Likely cause. Presenting an average as a personal guarantee

Fix. Frame as average guidance for typical skin; clamp extremes; state individual variation

Display browns out at midday

Likely cause. Bright display draw exceeds solar/battery

Fix. Give the display its own headroom; oversize the panel/battery; dim slightly if needed

The sketch will not upload β€” "Failed to connect" or "avrdude: stk500_recv()"

Likely cause. The bootloader is not being reached: wrong port, wrong board, a serial monitor holding the port open, or a USB cable that only carries power.

Fix. Close every serial monitor, confirm Tools β†’ Board and Port, and swap to a known data-capable USB cable. On an ESP32 hold BOOT while the IDE prints "Connecting…", then release. If a peripheral is wired to the UART pins (GPIO 1/3 on ESP32, D0/D1 on Uno) unplug it β€” it fights the programmer.

The board resets in a loop, or the serial monitor prints "Brownout detector was triggered"

Likely cause. The supply cannot deliver peak current. Wi-Fi transmit bursts, relay coils and servos all pull far more than their average draw.

Fix. Power peripherals from a separate regulated supply with a common ground rather than from the board 5 V pin. Add a 470–1000 Β΅F electrolytic capacitor across the supply near the load, and use a real power adapter rather than a laptop USB port.

Serial monitor shows garbage characters

Likely cause. Baud rate mismatch between Serial.begin() and the monitor, or a floating/shared UART line.

Fix. Set the monitor to 115200 to match the sketch. If it still garbles, the crystal or the USB bridge is being confused by noise β€” shorten the cable and keep motor wiring away from the USB lead.

An IΒ²C device is not detected

Likely cause. Wrong address, missing pull-ups, swapped SDA/SCL, or a bus too long for the pull-up value.

Fix. Run an IΒ²C scanner sketch first β€” it should print the device address. Most breakout boards include 4.7 kΞ© pull-ups, but if you have chained four of them the parallel resistance is too low; remove the pull-ups from all but one board. Keep the bus under 30 cm at 100 kHz.

Wi-Fi connects but MQTT never does (state -2)

Likely cause. Wrong broker address or port, a firewall in the way, or the broker requiring credentials the sketch is not sending.

Fix. Test from a laptop on the same network first: mosquitto_sub -h <broker> -t "#" -v. If that works, the problem is on the device β€” check the IP literal, port 1883 (or 8883 for TLS), and that client.setServer() runs before connect(). PubSubClient state codes are documented in its header.

Readings arrive for a while and then stop

Likely cause. The Wi-Fi or MQTT session dropped and the sketch never reconnects, or the broker dropped the client on keep-alive timeout.

Fix. Never assume the link stays up. Check WiFi.status() and client.connected() at the top of every loop and reconnect with exponential backoff. Add a watchdog so a wedged network stack reboots the device instead of going silent.

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 a millis() 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_t where 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

Why not just use the weather app's UV index?

The app forecasts a whole city. Your exact spot can differ a lot β€” shade, altitude, or reflective sand/snow β€” and this monitor measures the real UV where people actually are, which is what protects them.

How does it work out a burn time?

A UV index unit is a fixed dose rate, and skin burns after a characteristic dose. So time-to-burn is roughly inversely proportional to the index. It is shown as an average for typical skin, not a personal guarantee.

What is the biggest thing to get right?

Siting. The sensor must see the whole open sky through a UV-passing window and be kept clean. A shaded or dirty sensor reads low and tells people the sun is safe when it is not β€” the one dangerous failure mode.

Does it need internet?

No. It works fully stand-alone, showing the index and advice from its own sensor. Networking only adds a city parks map and daily-profile logging as a bonus.

Is a cheap UV sensor accurate enough?

It is indicative, and better with a reference calibration. For public awareness β€” showing that UV is high and prompting protection at the right place and time β€” it does the job, and it flags itself if it looks shaded or faulty.

References & Learning Resources

These are the primary sources worth reading in full. Manufacturer datasheets always outrank forum posts when the two disagree.

  1. WHO β€” UV index and sun protectionWHO
  2. Ultraviolet index β€” definition and categoriesReference
  3. Erythema and minimal erythemal doseReference
  4. LTR390 / VEML6075 UV sensors (datasheets)Manufacturer
  5. UV, altitude and surface reflectanceReference