Pages: [1]   Go Down
Author Topic: ATTiny85 4minute shower timer  (Read 1029 times)
0 Members and 1 Guest are viewing this topic.
Brisbane, Australia
Offline Offline
Edison Member
*
Karma: 29
Posts: 1071
View Profile
 Bigger Bigger  Smaller Smaller  Reset Reset

Hi,

Running out of hot water at home inspired this quick and dirty shower timer for my boys.  To keep cost and size down I've done this with the ATTiny85 using the Arduino IDE. 

The enclosure has 4 LEDs, an IP67 weatherproof pushbutton, and a piezo buzzer.  Because the LEDs and peizo use the 5 available IO on the ATtiny, the push button activates the reset and the sketch remembers the last state so it knows what to do when the button is pressed (either holding & ready, or counting down) in EEPROM using a simple circular queue for wear levelling.  If it is inactive for 2mins the ATTiny drops to a deep sleep so I avoided the need for an off/on switch - the button on the top does it all.
Code:
#include <EEPROM.h>                                                // used to remember current state in between resets
#include <avr/sleep.h>                                             // used to deep sleep the device
//
/*
  ____  _                           _____ _                     
 / ___|| |__   _____      _____ _ _|_   _(_)_ __ ___   ___ _ __
 \___ \| '_ \ / _ \ \ /\ / / _ \ '__|| | | | '_ ` _ \ / _ \ '__|
  ___) | | | | (_) \ V  V /  __/ |   | | | | | | | | |  __/ |   
 |____/|_| |_|\___/ \_/\_/ \___|_|   |_| |_|_| |_| |_|\___|_|   
                                                         v1.0
 
 ATTiny85 shower timer for a 4 minute shower.
 Single switch brings reset to GND to enable actions.
 
 Controls 4 indicator LEDs and a piezo buzzer. 
 Holds with all LEDs on with first reset, counts down from 4mins on subsequent reset.
 
 After 2mins without activity, the device drops into a deep sleep.  When awoken
 from sleep, holds at 4mins in preparation for a countdown.
 
 Revision history
 1.0  Initial
 
 ATTiny85 connections
 Leg  Function
 1    Reset, pull-up resistor to +5V and switch
 2    D3 LED4
 3    D4 piezo buzzer
 4    GND
 5    D0 LED1
 6    D1 LED2
 7    D2 LED3
 8    +5V
 
 attiny85
            reset -+---+- +5V power
         LED4 pb3 -+   +- pb2 LED3
 piezo buzzer pb4 -+   +- pb1 LED2
              GND -+---+- pb0 LED1
 */

// pin definitions
const int buzzerPin = 4;                                            // piezo


// storage constants
// used to 'zero' out unused EEPROM locations
#define EEPROMblank 255                 
// largest EEPROM cell ID (ATTiny85)
#define EEPROMmax 511

// state variables
int timerState = 1;                                                // 0 = waiting on 4, 1 = counting down
boolean firstRun = false;                                          // true if EEPROM has been freshly initialised
int EEPROMidx = 0;                                                 // where to write in EEPROM

// --------------------------------------------------------------------------------
//
void setup() {
  for(int i = 0; i<4; i++) {
    pinMode(i, OUTPUT);
    digitalWrite(i, LOW);
  }
  pinMode(buzzerPin, OUTPUT);

  // extract current state from EEPROM
  if(!loadValues()) {                                           // if there's no value stored there's work to do
    initialiseEEPROM();                                         // setup EEPROM for use by this sketch ongoing
    saveValues();                                               // timerState value & EEPROMidx have default values since EEPROM load failed
  }

  if(!loadValues()) {                                           // EEPROM error state
    while (true) {
      digitalWrite(0, millis() % 1000 < 250);                   // alternate LEDs, forever
      digitalWrite(1, millis() % 1000 > 250 && millis() % 1000 < 500);
      digitalWrite(2, millis() % 1000 > 500 && millis() % 1000 < 750);
      digitalWrite(3, millis() % 1000 > 750);
      delay(200);
    }
  }

  // switch states now
  timerState++;
  if(timerState>1) timerState = 0;
  saveValues();

  // one ping only vasily
  digitalWrite(buzzerPin, HIGH);
  delay(300);
  digitalWrite(buzzerPin, LOW);

} // setup()

// --------------------------------------------------------------------------------
//
void loop() {
  int BeepCount = 0;
  int minuteCounter = 0;
  unsigned long BeepTimer = 0L;
  for(int i = 0; i<4; i++) digitalWrite(i, HIGH);                // start with all 4 LEDs lit
  if(timerState) {                                               // counting down
    while(true) {
      // flash LED for currently counting down minute
      if(millis() < 60000) digitalWrite(0, millis() % 1000 < 500); // first minute
      if(millis() > 60000 && millis() < 120000) digitalWrite(1, millis() % 1000 < 500); // 2nd minute
      if(millis() > 120000 && millis() < 180000) digitalWrite(2, millis() % 1000 < 500); // 3rd minute
      if(millis() > 180000 && millis() < 240000) digitalWrite(3, millis() % 1000 < 500); // 4th & final minute

      // beep for the number of minutes remaining
      if(millis() / 60000 > minuteCounter) {
        minuteCounter++;
        BeepCount = 4-minuteCounter;
        BeepTimer = millis();
        digitalWrite(buzzerPin, HIGH);
      }
      if(BeepCount) {
        if(millis() > BeepTimer + 300) {                        // time to toggle buzzer
          digitalWrite(buzzerPin, !digitalRead(buzzerPin));
          BeepTimer = millis();
          if(!digitalRead(buzzerPin)) BeepCount--;              // if we turned it off, that's one less to do now
        }
      }

      if(millis() > 240000) {
        // and if it's all over, 10 seconds of beeps
        BeepTimer = millis();
        while(millis() < BeepTimer + 10000) digitalWrite(buzzerPin, (millis() % 500 < 250));
        // longbeep
        digitalWrite(buzzerPin, HIGH);
        delay(3000);
        digitalWrite(buzzerPin, LOW);
        while(true) {                                             // and hang here, forever.
          if(millis() > BeepTimer + 120000) bedTimeNow();         // unless it's for longer than 2mins, then sleep
        }
      }
    }
  } else {                                                        // holding, ready to countdown on next reset
    while(true) {                                                 // and hang here, forever.
      if(millis() > 120000) bedTimeNow();                         // unless it's for longer than 2mins, then sleep
    }
  }
}

// --------------------------------------------------------------------------------
// Retrieve saved values from EEPROM, sets the global address value and returns
// true if successful or false as error condition
//
boolean loadValues() {
  // determine if EEPROM has been initialised
  int datacount = 0;
  for(int i = 0; i < EEPROMmax; i++) if(EEPROM.read(i) != EEPROMblank) datacount++;
  if(datacount != 1) return false;                        // error condition code

    for(int idx=0; idx < EEPROMmax; idx++) {
    if(EEPROM.read(idx) != EEPROMblank) {
      EEPROMidx = idx;                                    // store global index value
      timerState = EEPROM.read(EEPROMidx);                // read value
      return true;                                        // all good
    }
  } // for
}

// --------------------------------------------------------------------------------
// Save new values to EEPROM, overwrite previous location for reuse
//
void saveValues() {
  int newIndex = 0;
  if (!firstRun) newIndex = EEPROMidx + 1;
  if(newIndex > EEPROMmax) newIndex = 0;                  // circular buffer
  EEPROM.write(newIndex, timerState);
  // if reset occurs before this next step, data will be lost
  if(!firstRun) EEPROM.write(EEPROMidx, EEPROMblank);    // overwrite previous if not first run after EEPROM init
  EEPROMidx = newIndex;                                  // setup for the next save
  firstRun = false;
}

// --------------------------------------------------------------------------------
// Overwrite all EEPROM locations with with the default value
//
void initialiseEEPROM() {
  for(unsigned int idx=0; idx < EEPROMmax; idx++) EEPROM.write(idx, EEPROMblank);
  firstRun = true;
}


// --------------------------------------------------------------------------------
// Drop into deep sleep
//
void bedTimeNow() {
  // no matter what the mode is currently, make sure it wakes from this to hold
  timerState = 1;
  saveValues();
  // lightshow before sleep
  for(int i=0; i<4; i++) digitalWrite(i, LOW);  // all off
  for(int i=3; i>=0; i--) {
    for(int j=3; j>=0; j--) digitalWrite(j, i==j);
    delay(500);
  }
  // low power all the outputs
  for(int i = 0; i<4; i++) {
    digitalWrite(i, LOW);
    pinMode(i, INPUT);
  }
  digitalWrite(buzzerPin, LOW);
  pinMode(buzzerPin, INPUT);
  // and now put this thing to sleep
  set_sleep_mode(SLEEP_MODE_PWR_DOWN);       // sleep mode is set here
  sleep_enable();                            // and activated here
  sleep_mode();                              // and...sleep.
}
For the one I've put together the LEDs are driven directly from the ATTiny via current limiting resistors and the piezo is controlled via a 2N7000 MOSFET.  The small proto board fits snugly in an enclosure with 4x rechargeable AAA's for power.

The code was thrown together so there's lots of room for enhancement, but hopefully I've commented it thoroughly enough for this to be useful to anyone else looking to do something similar.

Cheers ! Geoff
Logged

"There is no problem so bad you can't make it worse"
- retired astronaut Chris Hadfield

Valencia, Spain
Online Online
Faraday Member
**
Karma: 118
Posts: 4547
View Profile
 Bigger Bigger  Smaller Smaller  Reset Reset

Because the LEDs and peizo use the 5 available IO on the ATtiny, the push button activates the reset and the sketch remembers the last state so it knows what to do when the button is pressed (either holding & ready, or counting down) in EEPROM using a simple circular queue for wear levelling. 

Clever.

(Although 100,000 showers is quite a lot to be worrying about wear levellling smiley )

Logged

No, I don't answer questions sent in private messages...

Brisbane, Australia
Offline Offline
Edison Member
*
Karma: 29
Posts: 1071
View Profile
 Bigger Bigger  Smaller Smaller  Reset Reset

Because the LEDs and peizo use the 5 available IO on the ATtiny, the push button activates the reset and the sketch remembers the last state so it knows what to do when the button is pressed (either holding & ready, or counting down) in EEPROM using a simple circular queue for wear levelling.

Clever.
Thankyou.  I was very pleased with how well it worked too.  The natural short delay caused by coming back from reset acts as an inbuilt debounce of sort - there's no need to worry about testing for multiple switch presses.
(Although 100,000 showers is quite a lot to be worrying about wear levellling smiley )
With at least 2 state changes per shower, that's only 50k writes...and with my boys it's a risk! smiley-wink

Geoff
Logged

"There is no problem so bad you can't make it worse"
- retired astronaut Chris Hadfield

0
Offline Offline
Full Member
***
Karma: 0
Posts: 134
View Profile
WWW
 Bigger Bigger  Smaller Smaller  Reset Reset

The 100K cycles is highly pessimistic (I know the datasheet), a guy tested hat out to find 1.2M erase/write cycles before it failed
http://hackaday.com/2011/05/16/destroying-an-arduinos-eeprom/

Another idea to save pins would be to use this way of wiring LEDs, it allows two per pin: http://www.josepino.com/?led_chaser2

Nice project anyway.
Logged

Pages: [1]   Go Up
Jump to: