Introduction: Universal Clock Suitable for Visually Impaired

I was googling around looking for some sort of device I could make using arduino and stumbled upon eshop with devices for visually impaired. What really shocked me was the price. I mean-I do realize that such sorts of devices are not really mainstream, but still, it was kinda high. So I decided to make something similar while keeping the price as low as possible. I ended up making a clock, stopwatch, egg timer, tape measure and water level detector for under 5 bucks.

Step 1: Basic Principles

First off, I'm sorry to disappoint, but in current design, my device doesn't talk. Instead, it beeps out a specific sequence, telling you the number. It actually is quite like roman numerals. "17" is "XVII". Each numeral up to fifty (that's 1, 5, 10, 50) has corresponding beep sequence in my program. Those sequences vary by pitch and also by number of beeps. So 17 would be X-X-X----V-V----I----I where "X" "V" and "I" are tones of different pitch and dashes ("-") illustrate length of the delay. The way routines communicate is shown in the picture below. Basically, I would like you to notice several things:
-1) Value of null has a specific beep sequence for it.
-2) Buttons and probe (for before mentioned water level detector) are connected via 1 analog pin.
-3) Encoder interrupts the processor, but overflows of encoder related values are handled without jumping out of program.
-4) Encoder needs debouncing. I wasn't able to do it correctly so I borrowed working routine from HERE. ("Another Interrupt Library THAT REALLY WORKS (the Encoder interrupts the processor and debounces like there is no tomorrow"). by raffbuff)

Step 2: Schematics 'n' Stuff

You will need:
---------------------
Arduino duemillanove (to program it) or alternative
Some sort of shield for programming attiny processors with arduino (look it up on instructables; if you're lazy to build one, just hook your attiny up like SO.)
Altoids tin or alternative. (Quite frankly, I used alternative).
Battery holder (I used 2x1AA)
attiny85 (presumably -20PU) and socket (important)
rotary encoder (mechanical, I used P RE-24 rotary encoder, as the estimated lifespan is about 100 000 cycles and it's less than 2 bucks.)
Momentary switch x 2- I used some from an old keyboard, however, I DO NOT recommend you to do so. Although they have really good mechanical debouncing, they are fairly huge and it's not that comfortable to press them with your thumb.
resistor x 6. They need to be same value and it'd probably be good for them to be somewhere in kiloohms range. However, exact value isn't crucial. I used 10kohm.
Piezo speaker. I had some really flat one lying around, but maybe those in plastic housing (cylindrical ones) would be louder. I don't know.
Some paperclips, long wire, 2 old 9V batteries/9V connectors and plastic bottle cap for probe

Tools you will need: Solder, soldering iron, drill (optional), combination pliers, duckt tape, perfboard, scissors, and some sort of glue, I used hot glue and poster clay. Also. Lot of wire.

Step 3: Assembly

First of all, you need to somehow insulate conductive parts of can from electronics. To do so, I used duckt tape (not visible on photos). So, this is the first thing you gotta do. Then cut a piece of universal perfboard and basically resemble schematics given in step 2. while doing so, make sure you don't solder  rotary encoder, buttons and piezo directly to the board. Instead, solder a wire (flexible one, so that it bends nice and smooth) between aforementioned components and socket pins. And don't connect the buttons just yet. Leave wires from "+" and pin 3 (for buttons and probe, that is 4 wires, 2 from both socket pin and "+") unconnected. You'll solder them later. For pullup resistor for buttons, it might generally be a good idea to put a heat shrink tubing over it to prevent short circuit. Same goes for pretty much all resistors, if you decide to not put them on perfboard.
What you should have by now:
Perfboard with attiny socket on it with piezo hanging on 2 wires, rotary encoder hanging on three, with two wires to battery holder and 4 wires not going anywhere (2 from "+" and 2 to pin 3 on socket). Now it's about time to put the components in place. 
Putting the encoder in place
Measure where would you like to put it, mark a hole there and then just drill several holes with your drill in that hole. (What kind of sentence is that? O_o). Basically, just keep drilling until the holes fill the circle you've marked. Then use your mums best scissors to make the individual holes one big hole. Then twisting them, make the big (=final) hole rounder and you're good to go. The encoder should have a tip to make it stable when twisting. (See attached images.) So if it does, just drill an extra hole for this tip. Then unscrew the nut of encoder, place it in position and screw it back using pliers.
Drilling
Simply drill some holes you'll poke the wires through for buttons and probe. The holes for buttons will naturally be on the lid of the can, in the left corner. Just make sure you don't drill the hole right underneath the edge of the buttons where the solder joint will be, otherwise you might run into short circuit. See attached images. Then just solder the buttons and probe. If you didn't put the resistors on the perfboard and want to connect them directly to the buttons instead, now it might be a good time to do so. Secure the buttons in place with some poster clay/hot glue/whatever. Same goes for the perfboard itself, I used a wire that goes round the tin, but that wasn't the most brilliant idea I've had.

Step 4: Probe

To build the probe, just tear open an old 9v battery and glue and solder together some wire, plastic bottle cap and paperclips...

Step 5: Uploading the Code

Go to the Arduino tiny project site and download the version arduino-tiny-0100-0010.zip . Follow included readme.txt file for step by step instructions on how to make it work with your arduino enviroment. (Not really difficult.) Also, you will need to download PinChangeInterrupt-0001.zip from the same website. Also, download the Arduino time library. Installation of libraries is pretty straightforward, in case of confusion refer to this site.


Then turn your arduino into so-called isp by opening arduino ide, selecting file-examples-Arduino as ISP from the dropdown menu and hitting upload button.

Once you have done that, paste following code there, select tools-board-attiny85@1MHZ (Internal oscillator, BOD disabled), place the attiny chip in the programmer socket or breadboard or whatever like mentioned here and proceed to upload.


!!!!IMPORTANT!!!!
If uploading fails, try uploading the arduino as isp sketch from an older version of arduino ide, such as 0022. If Even that fails, you probably have your connections wrong


#define encoderPinA 2
#define encoderPinB 0
#define buttonpin 2
#define loudspeakerpin 1
#include "Time.h"//I did NOT make this library
#include "PinChangeInterruptSimple.h"//And also this library wasn't made by me
volatile  int encoderPos = 0;//position of the encoder, volatile because it changes on interrupt
unsigned int lastReportedPos = 1;   // change management
static boolean rotating=false;      // debounce management
int stepstochangeitem=10; //how many encoder steps are needed for menu item to be changed
boolean A_set = false;              //some debouncing related vars, who knows, I haven't done this part of code
boolean B_set = false;
int items=4;//number of items in menu
int selecteditem=0;
int R=512;//307;//telling which button is pressed-512=pullup resistor has the same value as the button one
int numbertype[]={//roman numerals; if you change this, please also change the numtypesound array abd typecount variable
  50, 10,5,1};
int numbertypesound[]={//pitches of different numerals
  100, 300, 500, 700};
int typecount=4;//number of numerals alltogether
int delaytime=150;
boolean inmenu=true;// are we in menu now?
unsigned long presslength=0;//buttons-for how long has a button been pressed
//time management vars below
long diff=0;//difference between time syncs in millis
long realdiff=0;//same as above, but rounded to half hours
const unsigned long hh=1800000;//half hour in milliseconds
long drift=0;//by how much the realdiff differs from diff
unsigned long lastmillis=0;
unsigned long lastcheckedtime=0;
int onemetervalue=300;//how long (in encoder steps) is one meter
boolean driftenabled=false;//are we correcting time drift?
void setup() {
  pinMode(encoderPinA, INPUT);
  pinMode(encoderPinB, INPUT);
  pinMode(buttonpin, INPUT);
  pinMode(loudspeakerpin, OUTPUT);
  // encoder pin on interrupt 0 (pin 2)
  attachPcInterrupt(2, doEncoderA, CHANGE);
  // encoder pin on interrupt 1 (pin 3)
  attachPcInterrupt(0, doEncoderB, CHANGE); 
  // Serial.begin (9600);
  beepnumber(35);//I just love the sound of that
  /*int tempR=analogRead(buttonpin);
   if (tempR>5)
   {
   R=tempR;
   playSound(1);
   //beepnumber(tempR);
   encoderPos=0;
   selecteditem=0;
   delay(500);
   }*/
}

int setvalue(boolean minutes)//set value; used for clock and egg timer routines
{
  //wrapvalues[]={0,2,1,4};
  encoderPos=0;//reset encoder position
  selecteditem=0;//reset menu position
  int returnvalue=0; //the value to be returned at the end of the function
  int stepnumber=0;//number of current step. each numeral setting=1 step
  int laststepnumber=0;

  while(1)//infinite loop to be broken out of using break command
  {
    inmenu=true;//so that it beeps out menu item, which is here used to determine how many times you want each numeral to be in your desired number
    wrapEncValues();
    inmenu=false;//resets back to appropriate value
    if (stepnumber==0&&minutes==false)//setting hours? Then skip setting 50-value (step 0)
    {
      stepnumber=1;
      laststepnumber=1; 
    }
    if ( laststepnumber==stepnumber)
    {
      if (minutes==false&&stepnumber==2&&returnvalue>=20)//skipping value 5 setting if user is setting hours and has set them to be >20 (setting 25:00 as time wouldn't make sense)
        stepnumber=3;
      if (minutes==true&&stepnumber==1&&returnvalue>=50)//skipping value 10 setting if user is setting minutes and has set them to be >50 (59 is the max value for minutes)
        stepnumber=2;
      beepnumber(numbertype[stepnumber]);
      laststepnumber=-1; 
    }
    if (minutes==false)
    {
      switch (stepnumber)
      {
      case 1://10s  
        items=2;
        break;
      case 2://5s
        if (returnvalue>=20)//20-something value
          stepnumber=3;//skip 5s     
        items=1;
        break;
      case 3://1s
        if (returnvalue>=20)//20-something value
        {
          items=3;
        }
        else
          items=4;
        break;
      }
    }
    else
    {
      switch (stepnumber)
      {
      case 0://50s  
        items=1;
        break;
      case 1://10s
        if (returnvalue>=50)//50-something value
        {
          stepnumber=2;//skip 10s     
          break; 
        }
        items=4;
        break;
      case 2://5s
        items=1;
        break;
      case 3://1s
        items=4;
        break;
      }
    }

    if (buttonpressed()==1)
    {
      returnvalue+=numbertype[stepnumber]*selecteditem;
      stepnumber+=1;
      selecteditem=0;
      laststepnumber=stepnumber;
      if (stepnumber>3)
      {

        inmenu=true;
        stepstochangeitem=10;
        selecteditem=1;//back to menu
        items=4;//not really necessary since "items" allready will have this value by coincidence
        return returnvalue;
        break;
      } 
    }
  }
}
void wrapEncValues()
{
  int beepnum=-1;
  while (encoderPos>stepstochangeitem)
  {
    encoderPos=encoderPos-stepstochangeitem;
    selecteditem+=1;
    beepnum=selecteditem;

  }
  while (encoderPos<0)
  {
    encoderPos=stepstochangeitem+encoderPos;
    selecteditem-=1;
    beepnum=selecteditem;
  }

  if (selecteditem>items)
  {
    selecteditem=0;
    beepnum=selecteditem; 
  }
  if (selecteditem<0)
  {
    selecteditem=items;
    beepnum=selecteditem;
  }
  if (beepnum!=-1&&inmenu==true)
    beepnumber(beepnum);

}
void playSound(int soundtype)//routine for beeping out specific codes (for zero, entering program, alarm and "OK beep" which is also used for separating hours and minutes when displaying time
{
  switch (soundtype)
  {
  case 0://enter
    /* for (int i=0; i<=100; i++)
     {
     tone(loudspeakerpin, i*5);
     delay(5);
     }
     noTone(loudspeakerpin);
     */
    tone(loudspeakerpin, 500,250);
    delay(100);
    tone(loudspeakerpin, 800,250);
    delay(500);
    break;
  case 1://melody
    tone(loudspeakerpin, 200,250);
    delay(100);
    tone(loudspeakerpin, 500,500);
    delay(50);
    break;
  case 2://zero
    tone(loudspeakerpin, 950,250);
    delay(50);
    tone(loudspeakerpin, 250,250);
    break;
  case 3://dot
    delay(100);
    tone(loudspeakerpin, 950,250);
    delay(100);
    noTone(loudspeakerpin);
    tone(loudspeakerpin, 950,250);
    delay(400);
    break;
  }
}

void loop()
{ //Do stuff here
  /*pinMode(13,OUTPUT);
   digitalWrite(13,HIGH);
   delayMicroseconds(100000);
   digitalWrite(13,LOW);*/


  if (driftenabled==true)//auto time correction enabled
  {
    boolean setlastcheckedtime=false;
    while (now()-lastcheckedtime>3600)//more than hour difference since last sync time
    {
      setlastcheckedtime=true;//sync happened
      lastcheckedtime+=3600;
      adjustTime(drift/1000);
    }
    if (setlastcheckedtime== true)//set the time of last sync only if the sync actually happened
   lastcheckedtime=now()-(drift/1000);
  }
  //moved code
  wrapEncValues();
  //if (Serial.read()=='B')
  // Serial.println (encoderPos, DEC);    


  if (inmenu==true&&buttonpressed()==1)
  {
    inmenu=false;
    playSound(0);
    switch (selecteditem)
    {

    case 0://clock
      if (presslength<2000)//press wasn't longer than 2 secs
      {
        beepnumber(hour());
        delay(200);
        playSound(3);
        delay(200);
        beepnumber(minute());
        inmenu=true;
        break;
      }
      else //longer than 2 secs
      {
        if (presslength>5000)//longer than 5 secs
        {
          if (lastmillis!=0)
            driftenabled=true;
          playSound(3);
          //code below calculates drift of internal oscillator by comparing expected delay
          //(which can be easily estimated, since user has to make the sync at :30 or :00) to gained value.
         // Then calculates drift, nominates it to one hour and saves to "drift" variable
          diff=millis()-lastmillis;
          unsigned long compval=hh/2;
          int i;
          for (i=1; i<=10000; i++)
          {
            compval+=hh;
            if (diff<=compval)
              break;
          }
          realdiff=i*hh;
          drift=realdiff-diff;
          drift=(2*drift)/(realdiff/hh);
          lastmillis=millis();
          int hrz=hour();
          int minz;
          if (minute()>15&&minute()<45)//assume the clock isn't off by more than 15 minutes
            minz=30;
          else
          {
            if (minute()>45)
              hrz+=1;
            minz=0;
          }
          setTime(hrz,minz,0,1,1,1);

        } 

        else
        {

          int hrz=setvalue(false);

          int minz=setvalue(true);
          setTime(hrz,minz,0,1,1,1);
          driftenabled=false;
          lastmillis=0;
      lastcheckedtime=now(); 
      }
        inmenu=true;
        break;
      }
      break;
    case 1://tape measure
      encoderPos=0;
      while(1)
      {
        // wrapEncValues();
        if (buttonpressed()==1)
        {
          if (presslength<2000)
          {
            int cms=round(map(abs(encoderPos),0,onemetervalue,0,100));
            beepnumber(cms);
            inmenu=true;
            stepstochangeitem=10;
            selecteditem=1;//back to menu
            items=4;
            encoderPos=0;
            break;
          }
          else//assume user has measured exactly one meter and wants to calibrate the tape measure
          {
            playSound(3);
            onemetervalue=abs(encoderPos);
            encoderPos=0;
            inmenu=true;
            break;
          } 
        } 
      }
      break;
    case 2://water detector
      {
        stepstochangeitem=4;
        items=10;
        selecteditem=5;
        while(1){
          wrapEncValues();
          if (abs(selecteditem-5)>2)//the probe uses same input pin as buttons; therefore, to exit the water detector, user is obliged to twist the encoder wheel
          {
            inmenu=true;
            stepstochangeitem=10;
            encoderPos=0;
            selecteditem=2;//back to menu
            items=4;
            noTone(loudspeakerpin);
            break;
          }
          tone(loudspeakerpin,map(analogRead(buttonpin),0,512,100,1000));//0-256;100-500
        }
        break;
      }
    case 3://stopwatch
      {
        int startingsecs;
        boolean watchrunning=false;
        while (1)
        {
          if(buttonpressed()==1)
          {
            playSound(3);
            if (watchrunning==false)
            {
              watchrunning=true;
              startingsecs=millis()/1000;
            }
            else
            {
              watchrunning=false;
              beepnumber(millis()/1000-startingsecs);
              inmenu=true;
              selecteditem=3;//back to menu
              break;

            }
          }
        }
        break;
      }
    case 4://egg timer
      int minutemins=99;
      while (1)
      {
        int buttoncode=buttonpressed();//so that it doesn't have to be sampled twice
        if (buttoncode==0)
          break;
        if (buttoncode==1)
        {
          minutemins=setvalue(true);
          minutemins=(minute()+minutemins)%60;//to ensure it will work correctly even if hour changes while the egg timer is running (which isn't really possible now when I think about it...)
          playSound(3);
        }
        if (minute()==minutemins)
        {
          playSound(1);
          if (buttoncode==0)
            break;
        } 
      }

      break;

    }
  }
}

//Until further notice, the code below is NOT mine
void doEncoderA(){
  // debounce
  if ( rotating ) delay (1);  // wait a little until the bouncing is done

  // Test transition, did things really change?
  if( digitalRead(encoderPinA) != A_set ) {  // debounce once more
    A_set = !A_set;

    // adjust counter + if A leads B
    if ( A_set && !B_set )
      encoderPos += 1;

    rotating = false;  // no more debouncing until loop() hits again
  }
}

// Interrupt on B changing state, same as A above
void doEncoderB(){
  if ( rotating ) delay (1);
  if( digitalRead(encoderPinB) != B_set ) {
    B_set = !B_set;
    //  adjust counter - 1 if B leads A
    if( B_set && !A_set )
      encoderPos -= 1;

    rotating = false;
  }
}
//from now on, the code is mine again

int buttonpressed()
{
  if (analogRead(buttonpin)>5)//is anything going on at all?
  {
    int returnvalue=-1;
    unsigned long beginms=millis();//variable for later comparation to tell for how long has the button been pressed
    while(1)
    {
      unsigned long lastmillis;

      int value=analogRead(buttonpin);
      if (value>5)//should be 0, but some hysteresion never hurts
      {
        lastmillis=millis();
        if (abs(value-((2*R)/3))<10)//button 2
          returnvalue=0;
        else
          if (abs(value-R)<10)//button 1
          {
            returnvalue=1;
            // R=value;
          }
          else
            if (abs(value-((2*R)/5)*3)<10)//both buttons, doesn't really work
              returnvalue=2;
      }
      else
        if (millis()-lastmillis>20)//to do the debouncing; count button as released once it has been released for more than 20ms
        {
          presslength=millis()-beginms;
          return returnvalue;
          break;
        }
    }
  }
  else
    return -1;
}

int beepnumber(int num) //beeping out the number. Basically works pseudorecursively by dividing the argument by numerals from highest to lowest
//and then feeds the modulo of division back to itself and repeats the whole process
{
  if (num==0)
  {
    playSound(2);//special code for zero
    return 0;
  }
  int initpos=encoderPos;
  int numbertypevalue[]={
    -1, -1, -1 ,-1                                             };
  for(int i=0;i<typecount;i++)
  {
    if (num/numbertype[i]>=1)
    {
      numbertypevalue[i]=floor(num/numbertype[i]);
      num=num%numbertype[i];
    }

    encoderPos=initpos;//basically ignore user rotating the encoder if beeping out the value
    if (numbertypevalue[i]>-1)
    {
      for(int i2=0;i2<numbertypevalue[i];i2++)
      {
        for (int i3=0;i3<4-i;i3++)
        {
          delay(delaytime/2);
          tone(loudspeakerpin, numbertypesound[i],delaytime);
          delay(delaytime);
        }
        delay(delaytime*2); 
      }
    }

  }

}


Step 6: Playing With Our New Device ^^

If you've done everything correctly, it's time to put batteries in. The device should greet you with "beepbeepbeep    beepbeepbeep    beepbeepbeep    beepbeep" sound.
Rotate the encoder to change program. 
Clock-0
Press the primary button to activate. It will then beep out the current time.
Hold the button for over 2 seconds. Then you can set the time. (rotate to select tens of hours, "fives" of hours and individual hours in this order; press the main button to proceed further in selecting.) 
Hold it for 5 seconds to sync the time at :00 or :30 to make the watch more accurate
Tape measure-1
Roll over surface to measure length; then press the main button to know the value
-or-
Measure exactly one meter using this technique. Then hold the button for some time. The tape measure will calibrate.
Water detector-2
attach probe, put it into the cup; listen to the tone. 
After you're done, detach it, twist the encoder around a bit to return to the menu.
Stopwatch-3
Press the main button to start. Press it again to stop counting. The device will beep out the number of seconds since the first button press and return to menu automatically.
Egg timer-4
press the main button; it will beep out value of 50, 10, 5, and 1 in this order as you press button repeatedly. Scroll the encoder wheel to adjust the value (times the number can fit into the desired number, basically, so if you want to set your timer to go off after say 7 minutes, press the button twice to skip 50s and 10s, then turn the wheel to set 5s to 1 , press the button again and turn the wheel to turn 1s to 2.) Sounds confusing, but really isn't that much, trust me!

THANKS FOR READING MY FIRST TUTORIAL AND HOPE YOU LIKED IT!