d4visl
Published

Portable and rechargeable Ultraviolet (UV) radiation meter

A lightweight, reliable, and completely portable ultraviolet (UV) radiation meter.

IntermediateFull instructions provided1,411
Portable and rechargeable Ultraviolet (UV) radiation meter

Things used in this project

Hardware components

Arduino Nano R3
Arduino Nano R3
×1
UV sensor
×1
128x64 OLED display module
×1
18650 4.2 V lithium battery
×1
18650 Battery holder
×1
TP4056 charging module
×1
Jumper wires (generic)
Jumper wires (generic)
×1
Tactile Switch, Top Actuated
Tactile Switch, Top Actuated
The one I'm using is quite small. The 3D design is made for a button of this type with dimensions of 6.3x5.5 mm.
×1
Resistor 10k ohm
Resistor 10k ohm
×1

Hand tools and fabrication machines

Soldering iron (generic)
Soldering iron (generic)
PCB Holder, Soldering Iron
PCB Holder, Soldering Iron
Optional, but it will help a lot, especially if you're making more than one like I did.
Solder Wire, 0.02" Diameter
Solder Wire, 0.02" Diameter
Generic works. I'd recommend a 0.5 mm or 1mm soldering wire.

Story

Read more

Custom parts and enclosures

3D printed enclosure

Custom-designed 3D-printed enclosure that fits all the components and the PCB nicely.

Printed Circuit Board Fabrication file (Gerber)

Printed Circuit Board Fabrication file (Gerber)

PCB Easy EDA type file

Schematics

Schematics for the circuit

Understand how the circuit is organized and assembled

Code

UV meter code

C/C++
/*
    UV meter code

    This is the code for the portable UV meter. Most of it is essentially 
    code for the OLED 0.96" display, to draw and print the desired info on it.
    There is also a setup for the tactile switch, and readings of the actual UV sensor

    The circuit:
    * OLED display: SCL attached to A5 and SDA attached to A4
    * UV sensor: sensor's OUT pin is connected to A0
    * Tactile Switch: The tactile switch is connected D12

    Feel free to use this code and modify it. Just keep it "open source" and share it with the community. That's what this project is all about, after all!
    
    Created and modified throughout the whole of 2022
    By Davi Salles Leite, d4visl on the Arduino Hub
    Modifications (dd/mm/yy): 

    https://create.arduino.cc/projecthub/d4visl/portable-and-rechargeable-ultraviolet-uv-radiation-meter-751c2e?auth_token=740cf17a480a86a9b0df5523606d24a4

*/
int i = 0;  // Variable to store values
int button = 12;  // Pin of the tactile switch used to change modes
unsigned long t_1 = millis();   // records millis for the sun animation on the OLED display
unsigned long t_2 = millis();   // same as the above
int mode = 0;   // sets the initial mode

// include the necessary libraries
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>

#define SCREEN_WIDTH 128 // OLED display width,  in pixels
#define SCREEN_HEIGHT 64 // OLED display height, in pixels

// declare an SSD1306 display object connected to I2C
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, -1);

void setup() {
  Serial.begin(9600);

  // initialize OLED display with address 0x3C for 128x64
  if (!display.begin(SSD1306_SWITCHCAPVCC, 0x3C)) {
    Serial.println(F("SSD1306 allocation failed"));
    while (true);
  }

  delay(2000);         // wait for initializing
  display.clearDisplay(); // clear display
  
  pinMode(button, INPUT); // Initialize
}

void loop() {
  
  // Read the tactile switch for input. If high, then the switch is being pressed.
  if (digitalRead(button) == HIGH){
    mode += 1;    // change the value of mode
    delay(500); 
  }
  
  // Change the mode
  if (mode == 0){
    mode_0(); // call mode 0
  }
  else if (mode == 1){
    mode_1(); // call mode 1
  }
  else if (mode == 2){
    mode_2(); // call mode 2
  }
  else{
    mode = 0;  // revert the value of mode to the initial and calls mode 0
    mode_0();
  }
}

/* 
  Mode 0 
  Mode 0 shows the following information:
    * Analog value measured by the sensor - This varies between 0-1023, 
    but the maximum value ever registered during testing was 350
    * A graph to demonstrate the analog value through a visual aid
    * Animation of the sun, for aesthetic purposes
   
*/
void mode_0(){
  
  /* This is an animation of the sun. It starts by drawing a filled in circle in the top left, 
  then every time this mode is called, it draws 4 circumferences in a progressively outwards motion, to represent the sunlight moving away from the sun. 
  This animation is repeated every few seconds, and pauses from time to time. 
  millis() was used to keep track of this time.*/
  if (millis() - t_1 > 300){
    i += 1;
    t_1 = millis();
    display.clearDisplay();
     
    if (i >= 4){
      i = 0;
    }
    if (millis() - t_2 < 2000){
      display.drawCircle(0,0,24,WHITE);
      display.drawCircle(0,0,28,WHITE);
    }
    else{
      if (millis() - t_2 > 7000){
        t_2 = millis();
      }
      
      display.drawCircle(0,0,16 + (i * 2),WHITE);
      display.drawCircle(0,0,20 + (i * 2),WHITE);
      
      if (i < 2){
      display.drawCircle(0,0,24 + (i * 2), WHITE);
      }
      
      if (i < 1){
      display.drawCircle(0,0,28 + (i * 2),WHITE);   
      }  
    }
      
    display.fillCircle(0,0,20,WHITE); // this is the circle on the top left
    
    display.drawRoundRect(105, 10, 20, 45, 3, WHITE); // body of the graphic

    // Set all the desired specs, such as color and size
    display.setTextColor(WHITE);
    display.setTextSize(1);
    
    // Set cursor to desired position to print a part of the graphic
    display.setCursor(106,0);
    display.println("MAX");
    
    // Set cursor to desired position to print a part of the graphic
    display.setCursor(106,57);
    display.println("MIN");
      
    // Set specs for text
    display.setTextColor(WHITE);
    display.setTextSize(3);
    
    // Set cursor to desired position to print text
    display.setCursor(SCREEN_WIDTH / 2 - 10, 0);
    display.println("UV");

    float media = 0; // variable used to calculate the mean of a series of 5 values

    // read the sensor 5 times and find the sum of the values
    for (int j = 0;  j < 5; j++){
      int sensor_value = analogRead(A0); // read the sensor pin
      media += sensor_value; // add value to media
    }

    Serial.println(media); // print media to serial monitor 

    // find the mean of the measured values, and round the result, to diminish the effect of current oscillations
    media = media/5; 
    media = round(media);

    // print mean to serial monitor
    Serial.print("Media:");
    Serial.println(media);

    // set the cursor depending on the number of algarisms, so that the number is centralized
    if (media <= 9){
      display.setCursor(63,30);
    }
    else if(media <= 99){
      display.setCursor(54,30);
    }
    else{
      display.setCursor(42,30);
    }
    
    display.print(int(media)); // print the rounded mean sensor value to the display
      
    display.setTextSize(1); // change text size

    // draw 4 lines to separate the levels of the graphic  
    for (int j = 9; j <= 36 ; j += 9 ){
      display.fillRect(105, 55 - j, 20, 1, WHITE); // what is a line if not a rectangle of height = 1 pixel?
    }

    // fill the levels of the graphic, according to the media (rounded mean value of the sensor's readings)
    if(media > 0){
      // "Very high" on the UV index scale
      if(media >= 290){ 
        display.fillRoundRect(105, 10, 20, 45, 3, WHITE);
      }   
      // "High" on the UV index scale
      else if(media >= 256){
        display.fillRoundRect(105, 19, 20, 36, 3, WHITE);
      }
      // "Moderate" on the UV index scale
      else if(media >= 178){
        display.fillRoundRect(105, 28, 20, 27, 3, WHITE);
      }
      // "Low" on the UV index scale
      else if(media >= 121){
        display.fillRoundRect(105, 37, 20, 18, 3, WHITE);
        }
      // Any UV radiation detected (0 < UV < 1)
      else{
        display.fillRoundRect(105, 46, 20, 9, 3, WHITE);
      }
    }
      
    display.display(); // display all the drawn information
  }
}

/*
 
 Mode 1
 Mode 1's purpose is to showcase the approximation of the UV index, along with a visual aid (graph), 
 and the risk of harm from unprotected sun exposure, for the average adult.
 
*/

void mode_1(){
  float media = 0; // variable used to calculate the mean of a series of 5 values

  // clear display and change cursor position and text size
  display.clearDisplay(); 
  display.setCursor(0,0);
  display.setTextSize(1);
  display.println("UV INDEX"); // print desired text

  display.drawTriangle(93,59,66,5,120,5, WHITE); // draw body of the graph

  // draw the lines of the graph
  for (int j = 0; j < 9; j ++){
    display.drawTriangle(93, 59, 66 + (j*3), 5 + (j*6), 120 - (j*3), 5 + (j*6), WHITE);
  }
  
  // read the sensor 5 times and find the sum of the values
  for (int j = 0;  j < 5; j++){
    int sensor_value = analogRead(A0);
    media += sensor_value;
  }
  
  Serial.println(media); // print media to serial monitor

  // find the mean of the measured values, and round the result, to diminish the effect of current oscillations
  media = media / 5;
  media = round(media);

  // print mean to serial monitor
  Serial.print("Media:");
  Serial.println(media);

  // display the value of the UV index, risk of harm from unprotected sun exposure, for the average adult , and fill the graph, according to the according to the media (rounded mean value of the sensor's readings)
  if (media > 5){
    if (media < 69){
      display.fillTriangle(93,59,90,53,96,53, WHITE); // fill the graph
      cursor0(); // set cursor to standardised position
      display.println("<1"); // UV index
      cursor1(); // set cursor to standardised position
      display.println("Baixo"); // risk of harm
    }
    else if(media <= 121){
      display.fillTriangle(93,59,90,53,96,53, WHITE); // fill the graph
      cursor0(); // set cursor to standardised position
      display.println("1"); // UV index
      cursor1(); // set cursor to standardised position
      display.println("Baixo");  // risk of harm
    }
    else if(media <= 178){
      display.fillTriangle(93,59,87,47,99,47, WHITE); // fill the graph
      cursor0(); // set cursor to standardised position
      display.println("2"); // UV index
      cursor1(); // set cursor to standardised position
      display.println("Baixo");  // risk of harm
    }
    else if(media <= 211){
      display.fillTriangle(93,59,84,41,102,41, WHITE); // fill the graph
      cursor0(); // set cursor to standardised position
      display.println("3"); // UV index
      cursor2(); // set cursor to standardised position
      display.println("Moderado"); // risk of harm
    }
    else if(media <= 237){
      display.fillTriangle(93,59,81,35,105,35, WHITE); // fill the graph
      cursor0(); // set cursor to standardised position
      display.println("4"); // UV index
      cursor2(); // set cursor to standardised position
      display.println("Moderado"); // risk of harm
    }
    else if(media <= 256){
      display.fillTriangle(93,59,78,29,108,29, WHITE); // fill the graph
      cursor0(); // set cursor to standardised position
      display.println("5"); // UV index
      cursor2(); // set cursor to standardised position
      display.println("Moderado"); // risk of harm
    }
    else if(media <= 273){
      display.fillTriangle(93,59,75,23,111,23, WHITE); // fill the graph
      cursor0(); // set cursor to standardised position
      display.println("6"); // UV index
      cursor1(); // set cursor to standardised position
      display.println("Alto"); // risk of harm
    }
    else if(media <= 290){
      display.fillTriangle(93,59,72,17,114,17, WHITE); // fill the graph
      cursor0(); // set cursor to standardised position
      display.println("7"); // UV index
      cursor1(); // set cursor to standardised position
      display.println("Alto"); // risk of harm
    }
    else if(media <= 311){
      display.fillTriangle(93,59,69,11,117,11, WHITE); // fill the graph
      cursor0(); // set cursor to standardised position
      display.println("8"); // UV index
      cursor2(); // set cursor to standardised position
      display.println("Muito Alto"); // risk of harm
    }
    else if(media > 311){
      display.fillTriangle(93,59,66,5,120,5, WHITE); // fill the graph
      cursor0(); // set cursor to standardised position
      display.println("9+"); // UV index
      cursor2(); // set cursor to standardised position
      display.println("Muito Alto"); // risk of harm
    }
  }
  else{
    cursor0(); // set cursor to standardised position
    display.println("0"); // UV index
    cursor1(); // set cursor to standardised position
    display.println("Baixo"); //  risk of harm
  }
  display.display(); // display the drawn info
}

/*
 Mode 2
 Mode 2 presents the user with a url (reduced with tinyurl.com) that they can use to  access a google drive folder.
 This folder contains manuals and guides, explaining exactly how to use and maintain the UV meter. It also contains a project log and tutorial, in portuguese, 
 along with other informations about the device, and a link to access the arduino hub project tutorial (in english). There is also a photo gallery.
 */
void mode_2(){
  display.clearDisplay(); // clear display

  // change the text size and cursor position
  display.setTextSize(2);
  display.setCursor(30,0);
  display.println("Acesse"); // print text
  
  // change the text size and cursor position
  display.setTextSize(1);
  display.setCursor(14,32);
  display.drawRoundRect(9, 28, 110, 16, 3, WHITE); // draw rectangle to contain the url
  display.print("tinyurl.com/senuv"); // print text
  display.display(); // display the drawn info
  
}

// standardised cursor position for printing the UV index
void cursor0(){
  display.setCursor(10,15);
  display.setTextSize(4);
}

// standardised cursor position for printingthe risk of harm
void cursor1(){
  display.setCursor(5,50);
  display.setTextSize(1); 
}

// other standardised cursor position for printing the risk of harm
void cursor2(){
  display.setCursor(0,50);
  display.setTextSize(1);
}

Credits

d4visl

d4visl

0 projects • 0 followers

Comments