The B.U.R.N. Meter: Rechargeable Arduino UV Index Meter

by billyen33 in Circuits > Arduino

1659 Views, 9 Favorites, 0 Comments

The B.U.R.N. Meter: Rechargeable Arduino UV Index Meter

BURN Meter.jpg
img_5179.jpg
img_5182.jpg
img_5184_720.jpg

Since summer rolled around and people are starting to flock to beaches again during these post-pandemic times, I decided to make a portable UV index meter for my more delicate-skinned friends so they can sunbathe responsbily - by keeping an eye on how much UV radiation they are exposing themselves to at the moment! There are some other portable UV index meter projects online, but none of them feature a rechargable battery and most have clunky designs that makes it hard to see the screen and point the UV sensor up at the same time, so I took it to myself to build one.

B.U.R.N. Meter is an Arduino Teensy based UV index meter with a ST7789 display capable of measuring UV index and onboard battery voltage in real time. The device features the following:

  • Colored display that shows the current UV index based on an ultraviolet photodiode reading
  • Live texts with recommended duration user can stay outside before getting sunburned
  • Graphics displaying the remaining battery capacity
  • Dynamic image that changes color based on the measured radiation level

Supplies

Components:

Tools needed:

  • Hot glue
  • 3D printer
  • Wire stripper/cutter
  • Soldering iron and solder
  • Small M2 Phillips screwdriver (or equivalent if using a different fastener from the one above)
  • Electronics tape (optional but recommended)
  • Laser cutter (optional, can substitute with a saw or something that can cut acrylic)

Overall Circuit

circuit.png

Since I have a spare Arduino Teensy lying around, I decided to use it for the brain of the UV meter. It features a ARM Cortex-M7 processor at 600 MHz, 3 SPI ports and a whopping 14 analog inputs, which is way more than I need, but it is small, relatively low-powered, and easy to interface using the Arduino IDE with the Teensyduino add-on, so like electricity, I went with the path of least resistance and sacrificed it for this project. I also happenned to have a full-color ST7789 LCD screen from a previous project, so I took it as an opportunity to spice up the boring OLED display format that most people used for projects like this with a cool feature - a face that turns more red as the radiation level increases (more on that later). The ST7789 talks to the Teensy using a 4-wire SPI interface (SCL, SDA, RES, and DC), and while the manufacturer featured pull-down resistors on their recommended wiring diagram, I was able to do without since it's the only thing my Teensy is talking to with those pins.

On the sensing side, I opted for a cheap yet trusty GUVA-S12SD UV light sensor breakout board from Adafruit, which features a "true" UV photodiode that outputs a voltage that is equal to 4.3 times the diode current (in uA). This means that if the diode current is 1uA (9 mW/cm^2), the output voltage is 4.3V, which gives me an easy way to convert its analog voltage to a UV index value (just output voltage divide by 0.1). The sensor can also be powered by 3.3V and gives a consistent reading even when the input voltage changes, making it a reliable choice for sensing ambient UV light intensity.

In order to provide power for the rest of the circuit and also allow users to recharge the battery (because let's face it, disposable batteries stinks), I used a standard 3.7v lithium polymer (LiPo) battery from Amazon and a TP4056 LiPo battery charger board with an integrated micro USB port. This particular TP4056 breakout board is useful since it takes advantage of the STDBY and CHRG pins on the TP4056 IC and includes a red LED that turns on when charging the battery (CHRG is pulled low) and a blue LED that turns on when charging is complete (STDBY pulled low), which I was able to somewhat take advantage of in my design. Unfortunately, the output voltage from the TP4056 board is not regulated to 3.3V or 5V, so I have to use a S7V8F3 voltage regulator to create the clean 3.3V supply that my circuit needs. Because we don't want the meter to be on all the time, a rocker switch is added between the outputs of the charging board and the voltage regulator. The switch I have happens to be able to switch two circuits off, so I soldered it to both OUT- and OUT+, but if you want to save yourself some time, just soldering one of those would do the trick.

Lastly, since the Teensy has so many analog inputs, I connected A3 (or pin 17) to a the positive side of the LiPo battery so I can monitor its voltage. Since the battery voltage could go up to 4.2V and the Teensy's pins are only 3.3V tolerant, I used a voltage divider to cut that input voltage in half (so the maximum voltage A3 can be exposed to is just 2.1V) and adjusted my code to account for that change.

Software

Screenshot 2022-08-13 180156.png

The program I wrote for B.U.R.N. Meter requires the use of the Adafruit GFX library and Adafruit ST7789 library, both of which can be downloaded either within the Arduino IDE itself under Tools --> Manage Libraries or from their GitHub page linked above. The GitHub repository containing my code, all the CAD files you need to 3D print the components, and the circuit diagram is linked here. While most of you are just going to clone my repository, upload it into your Teensy, and call it a day, below is a more detailed description of what is going on from the software standpoint for the outliers who want to learn more and possibly edit the code for your specific application. All the code for this project is done in C/C++ in the Arduino IDE.

Burn_Meter.ino

#include "TFT_display.h"
#include "battery.h"
#include "UV_sensor.h"
float last_percent = 100;
float last_index = 100;
float percent_now;
float index_now;


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


void loop() {
  percent_now = bat_percentage();
  index_now = UV_index();


  //only update screen if there are changes in reading
  if (percent_now != last_percent){
    display_battery(percent_now);
    last_percent = percent_now;
  }
  if (index_now != last_index){
    display_texts(index_now);
    display_pic(index_now);
    last_index = index_now;
  }
  delay(1000);
}

This is the main code that the Teensy loops through every second (as denoted by the delay(1000) line at the bottom of the loop() function). The Arduino IDE has a built-in setup(void) function that runs once whenever your Teensy first turns on (so when you flip the rocker switch) and a loop() function that essentially acts like a while(1) loop and continuously runs. If you want your UV index to update faster, you can decrease 1000 to say, 500 (the unit is in milliseconds, so that would make it loop twice a second), but I found that to be unnecessary, and increasing updating frequency will drain the battery more quickly.

When I first wrote the code, I realized that everything is updating on the screen every cycle, which creates a miniscule but nonetheless annoying lag on the display that I really didn't like, so I added some if statements in loop() to only update part of the screen at a time depending on what has changed. This means that the portion of the screen displaying battery voltage will remain constant when the UV index changes, and vice versa, creating a smoother user experience.

TFT_display.cpp

The majority of this code is fairly standard and follows the basic Adafruit GFX and ST7789 library notation (), so I'm not going to go too much into that to avoid writing a whole book on creating graphics layouts (see this great tutorial with sample code from Simple Projects if you want to dive deeper into using ST7789 with Arduino). However, I do want to highlight the display_pic and adjust_redness functions I wrote since they could use more explanation.

void display_pic(int index){
  //Case 2: Multi Colored Images/Icons
  int h = 69,w = 60, row, col, buffidx=0;
  uint16_t color;
  for (row=0; row<h; row++) { // For each scanline...
    for (col=0; col<w; col++) { // For each pixel...
      //To read from Flash Memory, pgm_read_XXX is required.
      //Since image is stored as uint16_t, pgm_read_word is used as it uses 16bit address
      // adjust color based on index to make face more red if UV is strong
      color = adjust_redness(pgm_read_word(face + buffidx), index); //face is the bitmap we are using
      tft.drawPixel(180+col, 60+row, color);
      buffidx++;
    } // end pixel
  }
}


uint16_t adjust_redness(uint16_t color, int index){
  // color is a 16 bit RGB number in format RRRRR-GGGGGG-BBBBB
  // index is the UV index we get from our sensor
  int r = color >> 11;
  int new_r = r+(r*index/5);
  if (new_r > 0b11111){
    new_r = 0b11111; //make sure we don't overflow these bits
  }
  uint16_t new_color = (color & 0b0000011111111111) | (new_r << 11); 
  return new_color; //return hue-adjusted index
}

As I mentioned above, I wanted to take full advantage of the RGB screen that the ST7789 display provides, so I followed this Instructable from theSTEMpedia to generate a 60 px by 69 px RGB bitmap of a person's face on the screen. However handsome the face is, it is a bit boring to just have a static image on the display, so I wrote a function that scales the redness of the image based on the UV index (adjust_redness). It took a lot of trial and error and some binary operations, but in the end I found a scale that works well for me. A bitmap is just a collection of 16-bit numbers, each number representing the color of a single pixel. The most significant 5 bits of the 16-bit number represent how red the pixel is (0-31 in decimal), the next 6 most significant bits represent how green it is (0-63 in decimal) , and the last 5 bits represents how blue it is (again, 0-31). By adjusting the ratio of how red, green, and blue the pixel is, we can make any color show up on the screen. Depending on whose face you use, you can change the scaling factor in the line int new_r = r+(r*index/5) from 5 to something else in order to make the color difference more obvious (increasing it would make the face less red, decreasing makes it more red). One thing to note regarding choosing a image to display is that it must have a black background, otherwise the background color would change as well, making it look fairly bizarre (a black background has an r of 0, so r+(r*index/5) would remain 0 no matter what index is).

battery.cpp

While reading the battery voltage itself is very straightforward (just analogRead times the ratio of the maximum 10-bit ADC value over the reference supply voltage, which I measured to be ~3.36V with a multimeter), converting it to an approximate battery percentage is a very difficult task. It is impossible to know exactly what the remaining energy in a LiPo battery is just using the voltage, since it is dependent on a number of factors like temperature and discharge current, so it requires more complicated devices like coulomb counters to truly monitor. Instead of going down this potentially expensive route, I chose to use a known voltage chart for a similar 3.7v LiPo battery and linearly interpolated it to create a piecewise function model that calculates percentage based on voltage. This might not be the most accurate, since each battery is sure to have a slightly different voltage chart, but it seems to be accurate enough as I see the percentage value drop over time fairly consistently. I do notice that the battery never shows up as 100% full even right after charging, but a +/- 10% error is good enough for a simple graphical display of how much longer you can go without charging the meter.

#include "battery.h"
#include <Arduino.h>


float read_voltage(int pin){
  int a = analogRead(pin);
  //int a = adc_read(pin);
  return 3.36*a/1023;
}


float bat_percentage(){
  float v = 2*read_voltage(A3); // take into account the voltage divider loss
  float c = 0; //capacity in percentage
  // Use a series of linear interpolation for rough estimate of capacity for 1S LiPo battery
  // Source: https://blog.ampow.com/lipo-voltage-chart/
  if (v > 4.15){
    c = ((4.15-4.2)/(95-100))*(v-4.2)+100;
  }
  else if (v <= 4.15 && v > 4.11){
    c = ((4.11-4.15)/(90-95))*(v-4.15)+95;
  }
  else if (v <= 4.11 && v > 4.08){
    c = ((4.08-4.11)/(85-90))*(v-4.11)+90;
  }
  else if (v <= 4.08 && v > 4.02){
    c = ((4.02-4.08)/(80-85))*(v-4.08)+85;
  }
  else if (v <= 4.02 && v > 3.98){
    c = ((3.98-4.02)/(75-80))*(v-4.02)+80;
  }
  else if (v <= 3.98 && v > 3.95){
    c = ((3.95-3.98)/(70-75))*(v-3.98)+75;
  }
  else if (v <= 3.95 && v > 3.91){
    c = ((3.91-3.95)/(65-70))*(v-3.95)+70;
  }
  else if (v <= 3.91 && v > 3.87){
    c = ((3.87-3.91)/(60-65))*(v-3.91)+65;
  }
  else if (v <= 3.87 && v > 3.85){
    c = ((3.85-3.87)/(55-60))*(v-3.87)+60;
  }
  else if (v <= 3.85 && v > 3.84){
    c = ((3.84-3.85)/(50-55))*(v-3.85)+55;
  }
  else if (v <= 3.84 && v > 3.82){
    c = ((3.82-3.84)/(45-50))*(v-3.84)+50;
  }
  else if (v <= 3.82 && v > 3.8){
    c = ((3.8-3.82)/(40-45))*(v-3.82)+45;
  }
  else if (v <= 3.8 && v > 3.79){
    c = ((3.79-3.8)/(35-40))*(v-3.8)+40;
  }
  else if (v <= 3.79 && v > 3.77){
    c = ((3.77-3.79)/(30-35))*(v-3.79)+35;
  }
  else if (v <= 3.77 && v > 3.75){
    c = ((3.75-3.77)/(25-30))*(v-3.77)+30;
  }
  else if (v <= 3.75 && v > 3.73){
    c = ((3.73-3.75)/(20-25))*(v-3.75)+25;
  }
  //yellow zone
  else if (v <= 3.73 && v > 3.71){
    c = ((3.71-3.73)/(15-20))*(v-3.73)+20;
  }
  //danger zone
  else if (v <= 3.71 && v > 3.69){
    c = ((3.69-3.71)/(10-15))*(v-3.71)+15;
  }
  else if (v <= 3.69 && v > 3.61){
    c = ((3.61-3.69)/(5-10))*(v-3.69)+10;
  }
  else if (v <= 3.61 && v > 3.27){
    c = ((3.27-3.61)/(0-5))*(v-3.61)+5;
  }
  else if (v <= 3.27){
    c = 0;
  }


  // make sure battery capacity never exceeds 100 or drop below 0
  if (c > 100){
    c = 100;
  }
  if (c < 0){
    c = 0;
  }
  
  return c;
}

Mechanical Design Pt. 1

ExplodedView.png
front_view.png
back_view.png

The full 3D CAD model of the final design can be downloaded from Onshape as SOLIDWORKS or STEP files, and the stl files themselves are available on GitHub. B.U.R.N. Meter's enclosure is dustproof, and it uses a 1/8" acrylic sheet to protect the flimsy LCD screen from direct contact, and it also features a small piece of fused quartz glass plate to cover up the UV sensor. This creates a physical barrier between the sensor and the environment, which prevents small dust particles and sand from entering and potentially shorting out something in the device. This particular fused quartz plate also has a very high UV light transmission (nearly 90%), which means the sensor can still work well behind it. I used self-threading M2 screws as the fastener of choice to secure the electronics to the box since they don't require us to have a particularly good 3D printer capable of printing threaded holes. These screws cut their own threads and are designed for plastic, which fits our application well.

The top of B.U.R.N. meter is sloped at about 42 degrees, meaning the user can look at the screen from a comfortable angle with the opening to the sensor pointing straight up, preventing the edges of the hole from shading the sensor and skewing the UV index reading. I played around with different sloping angles, and this ended up being the most comfortable to me, but depending on your particular physique, a different angle may be better for viewing.

Mechanical Design Pt. 2

side_view.png
port_closeup.png

A large rocker switch makes it easy for users to turn the device on and off, and the recessed port for the rocker switch makes it sit a couple centimeters inside the perimeter of the box, lowering the chances of it being accidentally toggled if someone puts it inside a backpack with many other objects.

Finally, since there need to be an opening on the B.U.R.N. meter to allow the user to plug in a micro USB cord to the battery charger, I made a simple snap fit plug for the port with a hole in it so that it can be attached to the back of the box via a thin string or wire. I've lost a number of these plugs I printed inadvertently since they are so small, so physically tying it to the rest of the device was a game changer. The micro USB port is pretty far inside the device, so the entire head of the charging cord needs to be able to fit through that hole. If your charging cord's head is taller than mine, you will want to either adjust the CAD model when you print or simply scrape off excess material with a small blade to enlarge that hole. The plug should still stay in there since the snapping mechanism engages the sides of the hole, not the top or bottom.

I printed the main housing, back plate, and charging port cover using an Ultimaker 2+ 3D printer and PLA plastic. The main housing and back cover was printed using 0.15 mm nozzle size and skirt plate adhesion with no support (the side with the display opening is the side on the plate), and the port cover was printed using the same setting except that I used 0.1 mm nozzle size instead to get that snap fit precision right. The acrylic cover for the LCD screen was cut using a laser cutter, but really any sheet of acrylic around the size of the opening would do, just make sure to leave enough overlap between the acrylic and the housing for the hot glue to bond to later on.

Assembly - Soldering

img_5169.jpg
img_5170.jpg
img_5173_720.jpg
img_5174.jpg

Most of the electronics components are soldered onto a solder board except for the UV sensor, LCD screen, and rocker switch. To make everything fit, make sure to solder the Teensy and voltage regulator on the bottom of the solder board and the battery charger board on top, centered on the last row of the solder board. I had to solder the voltage regulator on the right of the box so it doesn't get in the way of the rocker switch housing.

Since there is not enough space use Dupont connectors for the LCD and UV sensor, I simply soldered wires between their pin holes and the solder board. If you also do not have helping hands like me, you can tape the chips to a table or stand them up on their wires to make it easier to apply solder. Make sure to push the rocker switch into the enclosure before you solder wires between the solder board and its terminals otherwise you would not be able to fit it through the hole. Depending on your 3D printer's tolerance, you might need to scrape off some excess material to be able to fit the rocker switch through (it is a tight friction fit).

Assembly - Fasteners and Finishing Details

img_5175.jpg
img_5177.jpg
img_5185.jpg

Before securing the UV sensor and LCD screen to their place, you will want to hot glue the quartz plate and acrylic cover to the inside of the housing. Be careful to not get hot glue on the visible part of the quartz plate or acrylic cover (especially the quartz plate because it would block UV light from reaching the sensor). Although the quartz plate from Alpha Nanotech is too big to fit in the housing (it's 30 mm by 30 mm), I found that I could easily break off a piece by hand (make sure to wear gloves and safety goggles for this) and glue one to the housing.

After gluing both cover plates to the housing and waiting for the hot glue to cool, you can now secure the UV sensor and LCD screen to the housing with the 5 mm long screws. Bend the wires so they are out of the way, and then secure the solder board to the housing with the 10 mm long screws. I recommend putting a few layers of eletrical tape between the LiPo battery and the solder board and cutting off the excess header pins sticking up from the bottom side of the board. This will prevent the sharp header pins and wires from scratching or poking holes into the battery. I also secured the battery itself to the solder board with more tape so it doesn't move around when the enclosure is shaken.

Next, take a small length of wire (I used a blue wire to fit the aesthetic of the blue PLA plastic my housing is printed with) and thread it through the hole on the back cover. Tie a knot on one end of the wire so it does not slip out, and thread the other end through the USB port cover. This way when you remove the cover for charging, it will still be attached to the box and won't be accidentally lost (see image above).

Lastly, close up the box by placing the back cover plate over it and screwing in the remaining four M2 x 10mm screws. Now you are ready to measure some UV index!

Charging and Maintenance

img_5183.jpg
img_5186_720.jpg

To charge the B.U.R.N. Meter, simply unplug the micro USB port cover from the box, and insert your micro USB cable. Make sure you see a red light coming out of the port. If you do not see a red light, that means you might not have plugged in your cable correctly and slight adjustments would be needed. When the device is finished charging, you will see a bright blue glow on the back of the B.U.R.N. Meter. The housing would protect the sensitive electronics inside from dust for the most part, but it is not sealed so don't get it soaked in water. Take your meter outside, keep an eye on the index, and bask away!