Translate

Saturday, July 4, 2020

A family size UV sterilizer - the electronic


At this point what is needed is a timer to control the lights and also a switch used for safety and to start the operations.

The timer

The original UV-C strip light used is powered by a USB 5V charger, in the end a timer is needed to power the lights for a specific amount of time (to be determined, see Tests section).
A possible candidate is actually the UV Box timer designed time ago but it is probably overshooting for this application.
To simplify and assuming the time will be fixed rather than dynamic (i.e. no need to change constantly the exposure) a timer can be based on a Trinket that control a relay and something to indicate how long the lamps are working like a LED strip, maybe using one with RGB LED (WS2812B LED strip)
Here the shopping list for the electronic :
  • Trinket 5V 
  • Relay
  • WS2812B LED strip (Neopixel Ring or Bar)
  • Power supply
Since the strip is powered by the USB 5V would be possible to control it via a transistor rather than a relay but in case more than one strip is needed, the circuit could need to use 12V UV-C LED strips and in this case the relay is better, is a better approach, more generic.

It happens that just recently I did work on a project using a 5V Trinket (Rotary Dial) and I did prepare a PCB for it, PCB that can be fully converted to become a timer.

Schematic


Let see the schematic, directly copied from the Rotary Dial project.


If compared with the Rotary Dial schematic, some components are missing.
We will need only one switch as input, the power supply (12V) will be routed to one relay contact and only the Neopixel will be used as mean to communicate the status. 
With some jumper and small modifications will be possible to use the PCB of the Rotary Dial project, much better than use a breadboard and a point to point building.

Also the Rotary Dial case will be re-used for the project, only the top of the case will be changed in order to accommodate the Neopixel ring.

The final arrangement

And here the final arrangement for the basic UV sterilizer to be tested.
I did change the single line 5V UV LEDs with two 12V lines UV LED and powered everything with a 12V 2A wall transformer.
I screwed the controller to one side and for fun I did find an interesting 3D project to host the Adafruit Neopixel 16 LED Ring : https://www.thingiverse.com/thing:1183672
Then I used a micro-switch to control the lights and timer.
In this way the usage is really simple and intuitive.

Is enough to open the box, put whatever you want to sterilize and close it.
The timer (15 minutes for now) starts as well as the UV LEDs.
The LEDs in the beacon are all red and every minute one change blue.
When all the ring is blue the 15 minutes are terminated and the ring become green indicating the end of the cycle, turning off the UV LEDs.

When the door is opened the Ring LED are turned off.
Of course if during the cycle the door is opened, the UV LEDs are turned off as well as the Ring LEDs.

So let see some pictures of the ready for test UV Box. 

The circuit mounted on the mailbox

The double strip of UV LEDs lit


 
A closed look to the circuit

  
The micro-switch
 





The Ring LED enclosure.
The upper part is covered (inside) on tin foil to better reflect the light.
The effect is amazing !
 

 



  

The code


The code is quite trivial, based on a state machine.
Here it is :

// UV sterilizer by TheFwGuy - July 2020
// The program use a Neopixel strip as feedback about a timer for activating UV lights
//
// A switch is hooked to the door.
// When the door is closed, the switch close contacts (thus reading zero)
// When the door is open the switch is open (thus reading Vcc)
#include <Adafruit_NeoPixel.h>
#ifdef __AVR__
 #include <avr/power.h> // Required for 16 MHz Adafruit Trinket
#endif
// ----- Defines 
#define LED_PIN    1    // Used for Neopixel
#define SW1        0    // Door switch (marked on PCB as Pulse) - the input is pulled up
#define SW2        2    // N/A  (marked on PCB as Signalling)
#define RELAY      4    // Relay

#define UV_START  0 
#define UV_IDLE   1
#define UV_TIMING 2
#define UV_END    3
#define LED_COUNT 16
//#define END_EXPOSURE_TIME  1200000       // Time for exposure - ~20 minutes
#define END_EXPOSURE_TIME  900000          // Time for exposure - ~15 minutes
//#define END_EXPOSURE_TIME  60000         // Time for exposure - ~1 minute
#define CHANGE_PIXEL_TIME  60000           // Time for change pixel - ~1 minute
//#define CHANGE_PIXEL_TIME  3750           // Time for change pixel - ~3.75 second
// ------ Global variables
Adafruit_NeoPixel strip(LED_COUNT, LED_PIN, NEO_GRB + NEO_KHZ800);
// Argument 1 = Number of pixels in NeoPixel strip
// Argument 2 = Arduino pin number (most are valid)
// Argument 3 = Pixel type flags, add together as needed:
//   NEO_KHZ800  800 KHz bitstream (most NeoPixel products w/WS2812 LEDs)
//   NEO_KHZ400  400 KHz (classic 'v1' (not v2) FLORA pixels, WS2811 drivers)
//   NEO_GRB     Pixels are wired for GRB bitstream (most NeoPixel products)
//   NEO_RGB     Pixels are wired for RGB bitstream (v1 FLORA pixels, not v2)
//   NEO_RGBW    Pixels are wired for RGBW bitstream (NeoPixel RGBW products)
char uv_status;                     // State machine 
int count_up;
unsigned long currentMillis = 0;    // stores the value of millis() in each iteration of loop()
unsigned long digitTimerMillis = 0; // Main timer
unsigned long changeCountMillis = 0; // Change color pixel timer
// setup() function -- runs once at startup --------------------------------
void setup() {
  // These lines are specifically to support the Adafruit Trinket 5V 16 MHz.
  // Any other board, you can remove this part (but no harm leaving it):
#if defined(__AVR_ATtiny85__) && (F_CPU == 16000000)
  clock_prescale_set(clock_div_1);
#endif
  // END of Trinket-specific code.
  uv_status = UV_START;
  // Set input pins for rotary dial
  pinMode(SW1,INPUT);
  pinMode(SW2,INPUT);
  pinMode(RELAY, OUTPUT);
  
  strip.begin();           // INITIALIZE NeoPixel strip object (REQUIRED)
  strip.show();            // Turn OFF all pixels ASAP
  strip.setBrightness(250); // Set BRIGHTNESS to about 1/5 (max = 255)
  // Test strip
  colorWipe(strip.Color(  0, 255,   0), 80); // Green
  colorWipe(strip.Color(  255, 0,   0), 80); // Green
  colorWipe(strip.Color(  0,   0,   255), 80); // Blue
   
}

// loop() function -- runs repeatedly as long as board is on ---------------
void loop() {
  currentMillis = millis();    // Update time base
     
  uv_state_machine();
  
}
void uv_state_machine () {
  switch (uv_status) {
    case UV_START:
    default:
      colorWipe(strip.Color(  0, 0,  0), 0); // Turn off everything
      count_up = 1;
      uv_status = UV_IDLE;
      break;
    case UV_IDLE:
      // If reading switch is low, it means the door is closed
      if (!readSwitch(SW1) ) { 
        colorWipe(strip.Color(255,   0,   0), 5); // Red
        // Set timer
        digitTimerMillis = currentMillis;     // Initialize main timer
        changeCountMillis = currentMillis;    // Initialize change pixel timer
        // Turn on light
        digitalWrite(RELAY, HIGH);
        uv_status = UV_TIMING;
      }
      break;  
    case UV_TIMING:
      // If reading switch is close, it means the door is closed - stop if goes open
      if (readSwitch(SW1) ) {
        // Door open ! Turn off everything
        digitalWrite(RELAY, LOW);
        uv_status = UV_START;
      } else {
        // Door close - timer management
        if (currentMillis - digitTimerMillis > END_EXPOSURE_TIME)
        {
           // Door open ! Turn off everything
           digitalWrite(RELAY, LOW);
           colorWipe(strip.Color(  0, 255,   0), 10); // Green
           uv_status = UV_END;
        } else {
          // Main timer is running
          if (count_up < LED_COUNT) 
          {
            if (currentMillis - changeCountMillis > CHANGE_PIXEL_TIME)
            {
               changeCountMillis = currentMillis;    // Re-Initialize change pixel timer
               colorNumber(strip.Color(  0, 0, 255), 0, count_up);
               count_up++;
            }  
            
          }
        }
      }
      break;  
    case UV_END:
      // Wait for the switch to go open
      if (readSwitch(SW1) ) {
         uv_status = UV_START;
      }      
      break;  
   }  
}
// The function read a button with some debouncing - return true if the pin goes high.
// returns false otherwise
boolean readSwitch(int pin)
{
   if(digitalRead(pin))
   {
      delay(1);
      if(digitalRead(pin))
      {
         return true;
      }
   }
   return false;
}
// Some functions of our own for creating animated effects -----------------
// Fill strip pixels one after another with a color. Strip is NOT cleared
// first; anything there will be covered pixel by pixel. Pass in color
// (as a single 'packed' 32-bit value, which you can get by calling
// strip.Color(red, green, blue) as shown in the loop() function above),
// and a delay time (in milliseconds) between pixels.
void colorWipe(uint32_t color, int wait) {
  for(int i=0; i<strip.numPixels(); i++) { // For each pixel in strip...
    strip.setPixelColor(i, color);         //  Set pixel's color (in RAM)
    strip.show();                          //  Update strip to match
    delay(wait);                           //  Pause for a moment
  }
}
// Display a number (from 0 to 8)
void colorNumber(uint32_t color, int wait, char number) {
  for(int i=0; i<number; i++) { // For each pixel in strip...
    strip.setPixelColor(i, color);         //  Set pixel's color (in RAM)
    strip.show();                          //  Update strip to match
    delay(wait);                           //  Pause for a moment
  }
}

// Theater-marquee-style chasing lights. Pass in a color (32-bit value,
// a la strip.Color(r,g,b) as mentioned above), and a delay time (in ms)
// between frames.
void theaterChase(uint32_t color, int wait) {
  for(int a=0; a<10; a++) {  // Repeat 10 times...
    for(int b=0; b<3; b++) { //  'b' counts from 0 to 2...
      strip.clear();         //   Set all pixels in RAM to 0 (off)
      // 'c' counts up from 'b' to end of strip in steps of 3...
      for(int c=b; c<strip.numPixels(); c += 3) {
        strip.setPixelColor(c, color); // Set pixel 'c' to value 'color'
      }
      strip.show(); // Update strip with new contents
      delay(wait);  // Pause for a moment
    }
  }
}
// Rainbow cycle along whole strip. Pass delay time (in ms) between frames.
void rainbow(int wait) {
  // Hue of first pixel runs 5 complete loops through the color wheel.
  // Color wheel has a range of 65536 but it's OK if we roll over, so
  // just count from 0 to 5*65536. Adding 256 to firstPixelHue each time
  // means we'll make 5*65536/256 = 1280 passes through this outer loop:
  for(long firstPixelHue = 0; firstPixelHue < 5*65536; firstPixelHue += 256) {
    for(int i=0; i<strip.numPixels(); i++) { // For each pixel in strip...
      // Offset pixel hue by an amount to make one full revolution of the
      // color wheel (range of 65536) along the length of the strip
      // (strip.numPixels() steps):
      int pixelHue = firstPixelHue + (i * 65536L / strip.numPixels());
      // strip.ColorHSV() can take 1 or 3 arguments: a hue (0 to 65535) or
      // optionally add saturation and value (brightness) (each 0 to 255).
      // Here we're using just the single-argument hue variant. The result
      // is passed through strip.gamma32() to provide 'truer' colors
      // before assigning to each pixel:
      strip.setPixelColor(i, strip.gamma32(strip.ColorHSV(pixelHue)));
    }
    strip.show(); // Update strip with new contents
    delay(wait);  // Pause for a moment
  }
}
// Rainbow-enhanced theater marquee. Pass delay time (in ms) between frames.
void theaterChaseRainbow(int wait) {
  int firstPixelHue = 0;     // First pixel starts at red (hue 0)
  for(int a=0; a<30; a++) {  // Repeat 30 times...
    for(int b=0; b<3; b++) { //  'b' counts from 0 to 2...
      strip.clear();         //   Set all pixels in RAM to 0 (off)
      // 'c' counts up from 'b' to end of strip in increments of 3...
      for(int c=b; c<strip.numPixels(); c += 3) {
        // hue of pixel 'c' is offset by an amount to make one full
        // revolution of the color wheel (range 65536) along the length
        // of the strip (strip.numPixels() steps):
        int      hue   = firstPixelHue + c * 65536L / strip.numPixels();
        uint32_t color = strip.gamma32(strip.ColorHSV(hue)); // hue -> RGB
        strip.setPixelColor(c, color); // Set pixel 'c' to value 'color'
      }
      strip.show();                // Update strip with new contents
      delay(wait);                 // Pause for a moment
      firstPixelHue += 65536 / 90; // One cycle of color wheel over 90 frames
    }
  }
}




No comments:

Post a Comment