Translate

Friday 8 May 2015

Arduino GPS clock with MAX7219 driven 7 segment displays.

This is the start of an on-going time standard project.

The idea is to have three clocks, one MSF clock being received from the NPL on 60KHz, one DCF77 clock on 77.5 KHz and one GPS clock.

This is the first part of the clock, the GPS clock.

I'll be using the Ublox NEO receiver I've used before in the fast GPS-logger project here. The receiver is configured in exactly the same manner as described in that article, so pop along there and learn how to use u-center to configure your receiver.

I purchased some great MAX7219 display boards from eBay. There was a bit of a wait, as they came from China, but they were much less expensive than others, and the build quality is OK.

 Note on some board the CS pin is marked up LOAD.






I'm not going to draw a schematic, as it really is simple. (Leave me a comment if you get stuck!)
The wiring is commented in the code below.





The code is also simple, we're not doing anything particularly clever. There's a bit in the code which writes "no GPS" on the display, and this is defined as a bitmap. The bitmap refers to the segments as follows:

Bit 1(LSB) is G
Bit 2 is F
Bit 3 is E
Bit 4 is D
Bit 5 is C
Bit 6 is B
Bit 7 is A
and finally, bit 8 is the decimal point.






Here's a quick video of the clock in action:


... and here's the code:-

/*
 7-segment GPS clock 
 Version 0.9
 Written by Andy Doswell 2015
 License: The MIT License (See full license at the bottom of this file)
   
 Schematic can be found at www.andydoz.blogspot.com/
 
 You will need the LedControl library from http://playground.arduino.cc/Main/LedControl
 and the very excellent TinyGPS++ library from http://playground.arduino.cc/Main/LedControl
 
 pin 12 is connected to the DataIn 
 pin 11 is connected to the CLK 
 pin 10 is connected to CS
 pin 0 (RX)  is connected to the GPS TX pin

 */
 
#include "LedControl.h"
#include <TinyGPS++.h>

LedControl lc=LedControl(12,11,10,1); //Tells LedControl where our hardware is connected.
TinyGPSPlus gps; //TinyGPS++ class
static const uint32_t GPSBaud = 57600; //Our GPS baudrate. 
int hourTen; //tens of hours
int hourUnit; //units of hours
int minTen; // you get the idea..
int minUnit;
int secTen;
int secUnit;
int centTen; //centiseconds.
int centUnit;
unsigned long timer =0;

void setup() {

  lc.shutdown(0,false); // Wake up the MAX 72xx controller 
  lc.setIntensity(0,8); // Set the display brightness
  lc.clearDisplay(0); //Clear the display
  
  Serial.begin(GPSBaud); // start the comms with the GPS Rx
}

// This contains the bit patterns for the "no GPS" display.

void displayNoGPS() { // displays "noGPS" if the GPS lock isn't valid
  lc.setRow(0,0,B00000000);
  lc.setRow(0,1,B00000000);
  lc.setRow(0,2,B01011011);
  lc.setRow(0,3,B01100111);
  lc.setRow(0,4,B01011110);
  lc.setRow(0,5,B00000000);
  lc.setRow(0,6,B00011101);
  lc.setRow(0,7,B00010101);
}

void displayNoSerial() { // Displays "noSeriAL" in the event of serial comms fail.

  lc.setRow(0,7,B00010101);
  lc.setRow(0,6,B00011101);
  lc.setRow(0,5,B01011011);
  lc.setRow(0,4,B01001111);
  lc.setRow(0,3,B00000101);
  lc.setRow(0,2,B00010000);
  lc.setChar(0,1,'a',false);
  lc.setRow(0,0,B00001110);
}
  
// Displays the time on our LEDs
void displayTime() { 
  if (gps.time.isValid()) {
    timer = millis(); // reset the serial comms timer
    hourUnit = (gps.time.hour()%10);
    hourTen = ((gps.time.hour()/10)%10);
    minUnit = (gps.time.minute()%10);
    minTen = ((gps.time.minute()/10)%10);
    secUnit = (gps.time.second()%10);
    secTen = ((gps.time.second()/10)%10);
    secUnit = (gps.time.second()%10);
    secTen = ((gps.time.second()/10)%10);
    centUnit = (gps.time.centisecond()%10);
    centTen = ((gps.time.centisecond()/10)%10);
    lc.setDigit (0,7,hourTen,false);
    lc.setDigit (0,6,hourUnit,false);
    lc.setDigit (0,5,minTen,false);
    lc.setDigit (0,4,minUnit,false);
    lc.setDigit (0,3,secTen,false);
    lc.setDigit (0,2,secUnit,false);
    lc.setDigit (0,1,centTen,false);
    lc.setDigit (0,0,centUnit,false);
  }
  else
  {
    displayNoGPS();
    delay (2000);
  }
  
}

void loop()
{
  // If the GPS data is OK, then display it. If not display "no GPS"
  while (Serial.available() > 0)
    if (gps.encode(Serial.read())){
      displayTime();
    }
  if (millis() > timer+1000 ) // detects if the serial comms has failed.
  {
    displayNoSerial();
  }
  


  
}

/*
 * Copyright (c) 2015 Andrew Doswell
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHOR(S) OR COPYRIGHT HOLDER(S) BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */


Wednesday 6 May 2015

Happy Birthday!

Wow. 1 year into this website.. 5,354 visitors.. more than half of you from the UK.... more than half of you on Windows devices, most of you using Chrome browser.

Aren't statistics amazing? I never thought I'd have more than a couple of hundred hits at most, let alone have people comment and email and enjoy the youtube videos.

Some of my projects have been built into other projects by people, and their results shared on, and so it goes on... and on ...

Most popular project is the Astronomical clock (or pond pump controller), rapidly followed by the DG7-32 'scope tube (which surprises me!), and the Dehumidifier project.

... anyway, when you sit down tonight, raise a glass with me.

Happy Birthday blog!



Saturday 2 May 2015

Arduino beat detection and lighting controller.

Many years ago, I built up a lighting controller to switch the headlamps on in time with music on a classic Mini that had been turned into a DJ booth in a local nightclub....


... and yes, that's yours truly spinning that fantastic black plastic for your dancing pleasure .

So time has moved on , and so has technology a bit, and rather than slamming many amps into incandescent halogen headlamp bulbs, we can now get some very bright LED lamps, I'm also going to drive one of those RGB strips that are so popular (and inexpensive) these days.

The original controller had a microphone amplifier, which fed a low pass filter centered on 70Hz ish. This was buffered and fed to a rectifier, and buffered again. This fed two time constants, one short and one long. The long time constant would drift up and down with the level of our bass line, and the short time constant would pick out the peaks. The outputs from the two time constants were fed into a comparator, and when the peak (short time constant) output exceeded the long time constant, a "beat" was deemed detected and the output pulsed to "high". This pulse was sent to a PIC micro, which switched on some meaty FETS in varying patterns to switch the lamps on.

So, how can we improve on this? I wish to do away with the analogue beat detection and attempt it in software. I spent many hours tweaking and adjusting the time constants in the last thing to get it to run right, but this could be done in minutes in software.

I've also been searching the internet for other people's solutions. There are many threads on boards, many circuits and many ideas. The only one which would apparently work has now had it's code removed as it's become a commercial project (boo hiss). So we'll build the thing up from scratch.

Filtration. Now we need to filter out those bass notes. I'd really like to perform an FFT here, but, having tried it (and made it work up to a point) there's not enough clock cycles left to do anything else.

There are plenty of designs of sound to light (or light organ) using the MSGEQ7 IC from mixed-signal. Looks ideal. Sparkfun have provided us with a lovely datasheet here. Now, whilst perusing the datasheet, take good note of pin 6. Marked up GND. Now whatever you do , don't connect it to ground. It's an internal voltage reference. It's half of the supply rail (so usually about ~2.5V). Grounding it buggers up the IC. Don't ask me how I know....

So we send this little guy a reset pulse, and then we send him a strobe pulse. After each strobe pulse he dutifully outputs a voltage corresponding to the amplitude of the appropriate frequency band. There are 7 bands.

This output voltage is then sent to the arduino. To detect beats, I'm only interested in the lowest frequencies, which is the 63Hz channel. So I pick off this value and compare it to a default value of 126. This is our AGC value. Id it's greater than our AGC , we call it a beat, and increment our AGC value. If it's less we decrement our AGC... and repeat....

Now the beats are filtered we can flash our lights on and off in time to the music. (you could also implement the same code to measure the BPM of tracks.)

Now, we've also got stored the other values from our MSGEQ7, so we can use those to drive our RGB strip. I had originally averaged the low frequencies for red, mids for green and highs for blue, but it averages out too well to give off shades of white most of the time , so I now only pick individual frequency bands. I've also allowed those bands to be weighted a bit, to prevent it being primarily blue.

Here's the diagram. (Note IC 2 pin 6 isn't grounded!!)


The arduino outputs are used to drive some large FET's. These are capable of many amps of drive should you get really carried away (obviously your power supply and wiring will need to be man enough for the job). Connect the anode of your LED lamp here.
There's also a speed pot and stobe switch. These strobe all the outputs (and also pin 13) on and off together for a stoboscopic effect. This is time limited in the software, so if the DJ gets heavy handed, all of the clientelle won't have a seizure.


 All assembled on perfboard and mounted in a beer-proof die cast box from Hammond.
Completed, with remote control for Strobe and speed.


Did I mention it was important not to ground pin 6? ... I did? Oh good. Don't ground it.

The MSGEQ7 is a fragile little IC. Check, check and double check your wiring before powering up.





I'll post a video up in the very near future of it all set up and working in situ.

Here's the code:-

/* Prom Vaults Ligting Controller
   Version 1.0
   Written by Andy Doswell 2015
   License: The MIT License (See full license at the bottom of this file)
   
   Schematic can be found at www.andydoz.blogspot.com/
   A sound to light controller using the MSGEQ7 filter IC to analyse audio. 
   Software extracts the beat, and uses this to trigger lighting effects. 
   Also has "light organ" and Strobe effects.   
   
   */
int analogPin = 0; // MSGEQ7 OUT 3
int strobePin = 2; // MSGEQ7 STROBE 4
int resetPin = 4; // MSGEQ7 RESET 7 
int channel_1 = 12; // Light output channel 1
int channel_2 = 11; // light output channel 2
int channel_3 = 10; // light output channel 3
int channel_4 = 9; // light output channel 4
int channel_red = 6; // Red output channel
int channel_green = 5; // Green output channel
int channel_blue = 3; // Blue output channel
int channel_strobe = 13; // stoboscope trigger on pin 13
// RGB outputs are on pins 3,5 and 6 respectively
int stroboPin = 7; // Not a stobe as in data , but stobe as in rapidly flashing lights. Active low.
int stroboOutPin = 13; // Output trigger for stroboscope light
int speedPotPin = A1; // Strobe speed pot connected here, and to +5V and GND
int strobeDelay; // delay for strobe effect
int strobeTime = 30000; // maximum time allowable for strobe operartion
unsigned long strobeClock = 0;
int strobeLocked = 0;
int spectrumValue[7]; // sets the array for the output from the MSGEQ7
int filterValue = 80; // cuts out the noise from the input, like a squelch control.
int beatAGC = 126; //set Beat detect AGC to halfway
int AGCInc = 150; //Sets the rate at which the AGC is incremented (attack)
int AGCDec = 1; //Sets the rate at which the AGC is decremented (decay)
int AGCLowLimit = 40; // Sets the lower limit of the beat detector AGC. Has a dramatic effect on beat detection.
int beatDebounce = 200; // bit of a debounce delay for the beat detector
int beatTimer = 100; // counter for the beat debounce
int beatFlag = 0; // set high when the level from the low frequency channel exceeds the AGC level
int pattern = 0; // set default pattern
int beat = 0; // global beat counter for the changing RGB mode
int RGBmode = 0; // default RGB mode
int beatcount = 0; // beat counter for change of RGB output if in RGB mode.
// Gains of each channel, used to alter the "grey scale" or colour balance of the strip
int redGain = 10;
int greenGain = 20;
int blueGain = 10;

void setup() {
  // Read from MSGEQ7 OUT
  pinMode(analogPin, INPUT);
  // Write to MSGEQ7 STROBE and RESET
  pinMode(strobePin, OUTPUT);
  pinMode(resetPin, OUTPUT);
  //pinMode(beatPin, OUTPUT); // beat detector output pin
  pinMode(channel_1 , OUTPUT); // light channel 1
  pinMode(channel_2 , OUTPUT); // light channel 2
  pinMode(channel_3 , OUTPUT); // light channel 3
  pinMode(channel_4 , OUTPUT); // light channel 4
  pinMode(channel_strobe, OUTPUT); // Strobe trigger channel
  pinMode(stroboPin, INPUT_PULLUP); // Strobe switch connected between here and gnd.
  pinMode (speedPotPin, INPUT);
  // Set analogPin's reference voltage
  analogReference(DEFAULT); // 5V
 
  //check all channels
  digitalWrite(channel_1,HIGH);
  delay (500);
  digitalWrite(channel_1,LOW);
  delay (500);
  digitalWrite(channel_2,HIGH);
  delay (500);
  digitalWrite(channel_2,LOW);
  delay (500);
  digitalWrite(channel_3,HIGH);
  delay (500);
  digitalWrite(channel_3,LOW);
  delay (500);
  digitalWrite(channel_4,HIGH);
  delay (500);
  digitalWrite(channel_4,LOW);
  delay (500);
  analogWrite (channel_red,255);
  delay (500);
  analogWrite (channel_red,0);
  delay (500);
  analogWrite (channel_green,255);
  delay (500);
  analogWrite (channel_green,0);
  delay (500);
  analogWrite (channel_blue,255);
  delay (500);
  analogWrite (channel_blue,0);
  delay (500);
  
  // Set startup values for pins
  digitalWrite(resetPin, LOW);
  digitalWrite(strobePin, HIGH);
}
// following are the patterns for the 4 main channels
void display_pattern1()
{
  digitalWrite (channel_1, HIGH);
  digitalWrite (channel_2, HIGH);
  digitalWrite (channel_3, HIGH);
  digitalWrite (channel_4, HIGH);
  delay (30);
  digitalWrite (channel_1, LOW);
  digitalWrite (channel_2, LOW);
  digitalWrite (channel_3, LOW);
  digitalWrite (channel_4, LOW);
}

void display_pattern2()
{
  digitalWrite (channel_1,HIGH); 
  delay (30);
  digitalWrite (channel_1, LOW);
}

void display_pattern3()
{
  digitalWrite(channel_2,HIGH); 
  delay (30);
  digitalWrite(channel_2, LOW);
}

void display_pattern4()
{
  digitalWrite(channel_3,HIGH); 
  delay (30);
  digitalWrite(channel_3, LOW);
}

void display_pattern5()
{
  digitalWrite(channel_4,HIGH); 
  delay (30);
  digitalWrite(channel_4, LOW);
}

void display_pattern6()
{
  digitalWrite (channel_1,HIGH);
  digitalWrite(channel_2,HIGH); 
  delay (30);
  digitalWrite (channel_1,LOW);
  digitalWrite(channel_2,LOW);
}

void display_pattern7()
{
  digitalWrite(channel_3,HIGH);
  digitalWrite(channel_4,HIGH); 
  delay (30);
  digitalWrite(channel_3,LOW);
  digitalWrite(channel_4,LOW);
}

void display_pattern8()
{
  digitalWrite(channel_3,HIGH);
  digitalWrite(channel_2,HIGH); 
  delay (30);
  digitalWrite(channel_3,LOW);
  digitalWrite(channel_2,LOW);
}

void display_pattern9()
{
  digitalWrite(channel_4,HIGH);
  digitalWrite(channel_2,HIGH); 
  delay (30);
  digitalWrite(channel_4,LOW);
  digitalWrite(channel_2,LOW);
}

void display_pattern10()
{
  digitalWrite(channel_4,HIGH);
  digitalWrite(channel_1,HIGH); 
  delay (30);
  digitalWrite(channel_4,LOW);
  digitalWrite(channel_1,LOW);
}
void display_pattern11()
{
  digitalWrite(channel_1,HIGH); 
  delay (30);
}

void display_pattern12()
{
  digitalWrite(channel_2,HIGH); 
  delay (30);
}

void display_pattern13()
{
  digitalWrite(channel_3,HIGH); 
  delay (30);
}

void display_pattern14()
{
  digitalWrite(channel_4,HIGH); 
  delay (30);
}

void display_pattern15()
{
  digitalWrite(channel_1,HIGH);
  digitalWrite(channel_2,HIGH); 
  delay (30);
}

void display_pattern16()
{
  digitalWrite(channel_3,HIGH);
  digitalWrite(channel_4,HIGH); 
  delay (30);
}

void display_pattern17()
{
  digitalWrite(channel_3,HIGH);
  digitalWrite(channel_2,HIGH); 
  delay (30);
}

void display_pattern18()
{
  digitalWrite(channel_4,HIGH);
  digitalWrite(channel_2,HIGH); 
  delay (30);
}

void display_pattern19()
{
  digitalWrite(channel_4,HIGH);
  digitalWrite(channel_1,HIGH); 
  delay (30);
}

void display_pattern20()
{
  digitalWrite(channel_4,HIGH);
  digitalWrite(channel_3,HIGH);
  digitalWrite(channel_2,HIGH);
  digitalWrite(channel_1,HIGH);
  
}

void display_pattern21()
{
  digitalWrite(channel_4,LOW);
  digitalWrite(channel_3,LOW);
  digitalWrite(channel_2,LOW);
  digitalWrite(channel_1,LOW);

  
}

 void strobe() {
      
    if (millis() > (strobeClock+30000)) { // if the strobe has been on for 30 seconds then inhibit
      strobeLocked = 1;
    }
    
    if (strobeLocked == 0) { // if the strobe isn't inhibited 
      
    // everything on 
    digitalWrite(channel_4,HIGH);
    digitalWrite(channel_3,HIGH);
    digitalWrite(channel_2,HIGH);
    digitalWrite(channel_1,HIGH);
    digitalWrite (channel_strobe,HIGH);
    analogWrite (channel_red,255);
    analogWrite (channel_green,255);
    analogWrite (channel_blue,255);
    strobeDelay = analogRead(speedPotPin);
    strobeDelay = map(strobeDelay, 0, 1023, 15, 255);
    delay (strobeDelay); // read the pot and delay for the returned value.
    
    // everything off
    digitalWrite(channel_4,LOW);
    digitalWrite(channel_3,LOW);
    digitalWrite(channel_2,LOW);
    digitalWrite(channel_1,LOW);
    digitalWrite (channel_strobe,LOW);
    analogWrite (channel_red,0);
    analogWrite (channel_green,0);
    analogWrite (channel_blue,0);
    delay (strobeDelay); 
    if (digitalRead (stroboPin) == LOW ) {
    strobe ();
    }

  }  

}

void getSpectrum () { // enables the MSQEQ7 and gets the data
  
   // Set reset pin low to enable strobe
  digitalWrite(resetPin, HIGH);
  digitalWrite(resetPin, LOW);

 
  // Get all 7 spectrum values from the MSGEQ7
  for (int i = 0; i < 7; i++)
  {
    digitalWrite(strobePin, LOW);
    
    delayMicroseconds(30); // Allow output to settle
 
    spectrumValue[i] = analogRead(analogPin);
 
    // Constrain any value above 1023 or below filterValue
    spectrumValue[i] = constrain(spectrumValue[i], filterValue, 1023);
 
    // Remap the value to a number between 0 and 255
    spectrumValue[i] = map(spectrumValue[i], filterValue, 1023, 0, 255);
    
    digitalWrite(strobePin, HIGH);
    
   }
}

void loop() {
   if ((digitalRead(stroboPin) == LOW) && strobeLocked ==0) { // if the strobo is set, and isn't inhibited
     strobeClock = millis ();  
     strobe();
  }
   if ((strobeLocked == 1 && (millis() >= (strobeClock+90000)))) { // if the strobo is inhibited and 90 seconds have passed since it was last activated then unlock 
     strobeLocked = 0;
   }  
   
  getSpectrum (); //gets the current values from the MSGEQ7
  
  beatAGC = constrain (beatAGC,AGCLowLimit,255); // ensures the AGC is between the filter setting and the maximum
     
  if (spectrumValue[0] > beatAGC) {
       beatFlag = 1;
       beatAGC = beatAGC+AGCInc; //increase the AGC if it's been set. Changing the values of the AGCInc & AGCDec (attack and decay) has a quite marked effect on the performance of the beat detector
     }
  else { //decrease the ACG if it hasn't
       beatAGC = beatAGC-AGCDec;
       beatFlag = 0; 
     }
    
   if (beatFlag == 1 && beatTimer <= 0) {
     beatFlag=0;// reset the beat flag
     beatTimer = beatDebounce; // dont allow a trigger again for a number of program cycles
     
       pattern = random(24); // select a random pattern and call it
     
       if (pattern == 0) {
        display_pattern1();
       }   
       if (pattern == 1 ) {
         display_pattern2();
       } 
       if (pattern == 2 ) {
         display_pattern3();
       }   
       if (pattern == 3 ) {
         display_pattern4();
       }
       if (pattern ==4 ) {
         display_pattern5();
       }    
       if (pattern ==5 ) {
         display_pattern6();
       } 
       if (pattern ==6) {
         display_pattern7();
       }
       if (pattern ==7) {
         display_pattern8();
       } 
       if (pattern ==8 ) {
         display_pattern9();
       } 
       if (pattern ==9 ) {
         display_pattern10();
       } 
       if (pattern == 10) {
        display_pattern11();
       }   
       if (pattern == 11 ) {
         display_pattern12();
       } 
       if (pattern == 12 ) {
         display_pattern13();
       }   
       if (pattern == 13 ) {
         display_pattern14();
       }
       if (pattern ==14 ) {
         display_pattern15();
       }    
       if (pattern ==15 ) {
         display_pattern16();
       } 
       if (pattern ==16) {
         display_pattern17();
       }
       if (pattern ==17) {
         display_pattern18();
       } 
       if (pattern ==18 ) {
         display_pattern19();
       } 
       if (pattern ==19 ) {
         display_pattern20();
       } 
       if (pattern ==20 ) {
         display_pattern1();
       } 
       if (pattern ==21 ) {
         display_pattern21();
       } 
       if (pattern ==22 ) {
         display_pattern20();
       } 
       if (pattern ==23 ) {
         display_pattern20();
       } 
       beat ++; // increament the beat counter
       if (beat % 16 ==0) { // every 16 beats change the RGB mode
         beatcount = 5;
         RGBmode = random (3);
       }
       // cycles through primary / tertiary colours every beat
       if (RGBmode == 1 && beatcount == 0) {
           analogWrite (channel_red,255);
           analogWrite (channel_green,0);
           analogWrite (channel_blue,0);
           beatcount = 5;
         }
         if (RGBmode == 1 &&beatcount == 1) {
           analogWrite (channel_green,255);
           analogWrite (channel_red,0);
           analogWrite (channel_blue,0);
           beatcount --;
         }
         if (RGBmode == 1 &&beatcount == 2) {
           analogWrite (channel_blue,255);
           analogWrite (channel_green,0);
           analogWrite (channel_red,0);
           beatcount --;
         }
         if (RGBmode == 1 &&beatcount == 3) {
           analogWrite (channel_red,255);
           analogWrite (channel_green,255);
           analogWrite (channel_blue,0);
           beatcount --;
         }
         if (RGBmode == 1 &&beatcount == 4) {
           analogWrite (channel_red,255);
           analogWrite (channel_green,0);
           analogWrite (channel_blue,255);
           beatcount --;
         }
         if (RGBmode == 1 &&beatcount == 5) {
           analogWrite (channel_red,0);
           analogWrite (channel_green,255);
           analogWrite (channel_blue,255);
           beatcount --;
           }
        // cycles through random colours every beat
       if (RGBmode == 2) {
           analogWrite (channel_red,(random (256)));
           analogWrite (channel_green,(random (256)));
           analogWrite (channel_blue,(random (256)));
       }
         
   }   
     beatTimer --; // provides some debounce to the beat detector
     beatTimer = constrain(beatTimer,-1,beatDebounce);
     if (beatTimer == 0){
      display_pattern21();
     } 
     
     if (RGBmode == 0) { // spectral RGB output (multiplication factor changes the scaling of each colour)
     analogWrite (channel_red,((spectrumValue[1])/10)*redGain);
     analogWrite (channel_green,((spectrumValue[3])/10)*greenGain);
     analogWrite (channel_blue,((spectrumValue[6])/10)*blueGain);
     }
   }
/*
 * Copyright (c) 2015 Andrew Doswell
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHOR(S) OR COPYRIGHT HOLDER(S) BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */

Friday 17 April 2015

Pond Pump Controller Update!

I've been suffering some power outages here, and my clock settings have been lost when power has been restored. It turns out my LIR2032 Lipo battery had failed.

It also had me thinking that when power is restored, our mode settings are lost, it resets them to "Auto", which may not be a good idea.

The solution is to store the settings in EEPROM for future use.

Now if the clock isn't set, then the time will still default to the time the sketch was complied, and the default mode is now set to off, a fail-safe condition. If the clock is still running (which it should be if your Lipo is OK), then the settings are restored from memory. Happy days.

The addition of the EEPROM library (standard fit in the newer revisions of the Arduino IDE) and the use of EEPROM.read and EEPROM.write commands provides an easy solution.

I also now check to see if, when the mode is set to ON, that it's not frozen.

Here's the new code...

// Dawn & Dusk controller with frost protection.
// 5th December 2014. Modified 17th April 2015
// (C) A.G.Doswell 2014 & 2015
//
// Date and time functions using a DS1307 RTC connected via I2C and Wire lib
//
// Designed to control a relay connected to pin A3. Pin goes low during daylight hours and high during night. Relay uses active low, so is
// "On" during the day. This is connected to the fountain pump in my garden.
//
// Time is set using a rotary encoder with integral push button. The Encoder is connected to interrupt pins D2 & D3 (and GND), 
// and the push button to pin analogue 0 (and GND)
// The RTC is connections are: Analogue pin 4 to SDA. Connect analogue pin 5 to SCL.
// A 2 x 16 LCD display is connected as follows (NOTE. This is NOT conventional, as interrupt pins are required for the encoder)
//  Arduino LCD  
//  D4      DB7
//  D5      DB6
//  D6      DB5
//  D7      DB4
//  D12     RS
//  D13     E
// 
// In this revision, there is a Dallas 18B20 Temperature sensor connected to pin 8, enabling frost protection. This is pulled up to +5volts via a 4.7K resistor.
//
// Use: Pressing and holding the button will enter the clock set mode (on release of the button). Clock is set using the rotary encoder. 
// The clock must be set to UTC.
// Pressing and releasing the button quickly will display the current sun rise and sun set times. Pressing the button again will enter the mode select menu. 
// Modes are AUTO: On when the sun rises, off when it sets.
//           AUTO EXTEND : same as above but goes off 1 hour after sunet (nice for summer evenings!) 
//           ON: Permanently ON
//           OFF: Permanently OFF (Who'd have guessed it?)
//
// Change the LATTITUDE and LONGITUDE constant to your location.
// Use the address finder from http://www.hacktronics.com/Tutorials/arduino-1-wire-address-finder.html to find the address of your temperature sensor and enter it where required in DeviceAddress.
// 28th Dec 2014 - Slowed the rate at which the temperature is requested down to once per minute. It has improved the clock display, and solved an issue whereby the temp sensor wasn't always ready to send data, and hung up.
// 10th Jan 2015 - Altered the method of updating the temp to something simpler and more sensible!
// 17th April 2015 - Stored Mode settings in EEPROM
//
// Be sure to visit my website at http://andydoz.blogspot.com

#include <Wire.h>
#include "RTClib.h" // from https://github.com/adafruit/RTClib
#include <LiquidCrystal.h>
#include <Encoder.h> // from http://www.pjrc.com/teensy/td_libs_Encoder.html
#include <TimeLord.h> // from http://swfltek.com/arduino/timelord.html. When adding it to your IDE, rename the file, removing the "-depreciated" 
#include <OneWire.h> // from http://playground.arduino.cc/Learning/OneWire
#include <DallasTemperature.h> // from http://www.hacktronics.com/code/DallasTemperature.zip. When adding this to your IDE, ensure the .h and .cpp files are in the top directory of the library.
#include <EEPROM.h> 

#define ONE_WIRE_BUS 8 // Data wire from temp sensor is plugged into pin 8 on the Arduino
OneWire oneWire(ONE_WIRE_BUS); // Setup a oneWire instance to communicate with any OneWire devices
DeviceAddress outsideThermometer = { 0x28, 0x1A, 0x1A, 0x3E, 0x06, 0x00, 0x00,0xC7 }; // use the address finder from http://www.hacktronics.com/Tutorials/arduino-1-wire-address-finder.html to find the address of your device.
RTC_DS1307 RTC; // Tells the RTC library that we're using a DS1307 RTC
Encoder knob(2, 3); //encoder connected to pins 2 and 3 (and ground)
LiquidCrystal lcd(12, 13, 7, 6, 5, 4); // I used an odd pin combination because I need pin 2 and 3 for the interrupts.
//the variables provide the holding values for the set clock routine
int setyeartemp; 
int setmonthtemp;
int setdaytemp;
int sethourstemp;
int setminstemp;
int setsecs = 0;
int maxday; // maximum number of days in the given month
int TimeMins = 0; // number of seconds since midnight
int TimerMode = 2; //mode 0=Off 1=On 2=Auto
int TimeOut = 10;
int TimeOutCounter;

// These variables are for the push button routine
int buttonstate = 0; //flag to see if the button has been pressed, used internal on the subroutine only
int pushlengthset = 3000; // value for a long push in mS
int pushlength = pushlengthset; // set default pushlength
int pushstart = 0;// sets default push value for the button going low
int pushstop = 0;// sets the default value for when the button goes back high

int knobval; // value for the rotation of the knob
boolean buttonflag = false; // default value for the button flag
float tempC; // Temperature


const int TIMEZONE = 0; //UTC
const float LATITUDE = 51.89, LONGITUDE = -2.04;  // set YOUR position here 
int Sunrise, Sunset; //sunrise and sunset expressed as minute of day (0-1439)
TimeLord myLord; // TimeLord Object, Global variable
byte sunTime[]  = {0, 0, 0, 1, 1, 13}; // 17 Oct 2013
int SunriseHour, SunriseMin, SunsetHour, SunsetMin; //Variables used to make a decent display of our sunset and sunrise time.
DallasTemperature sensors(&oneWire); // Pass our oneWire reference to Dallas Temperature.

void setup () {
    //Serial.begin(57600); //start debug serial interface
    Wire.begin(); //start I2C interface
    RTC.begin(); //start RTC interface
    lcd.begin(16,2); //Start LCD (defined as 16 x 2 characters)
    lcd.clear(); 
    pinMode(A0,INPUT);//push button on encoder connected to A0 (and GND)
    digitalWrite(A0,HIGH); //Pull A0 high
    pinMode(A3,OUTPUT); //Relay connected to A3
    digitalWrite (A3, HIGH); //sets relay off (default condition)
    sensors.begin();
    sensors.setResolution(outsideThermometer, 12); // set the resolution to 12 bits (why not?!)
    
    //Checks to see if the RTC is runnning, and if not, sets the time to the time this sketch was compiled, also sets default mode to 2 and writes to EEPROM
    if (! RTC.isrunning()) {
    RTC.adjust(DateTime(__DATE__, __TIME__));
    EEPROM.write (0,0); // set the auto mode as 2 for default
    }
  
 
   lcd.print("Dawn Dusk Pond");
   lcd.setCursor(0, 1);
   lcd.print("Pump Controller");
   delay (2000);
   lcd.clear();
   lcd.print("Version 1.32");
   lcd.setCursor(0, 1);
   lcd.print("(C) A.G.Doswell ");
   delay (5000);
   lcd.clear();
    //Timelord initialisation
    myLord.TimeZone(TIMEZONE * 60);
    myLord.Position(LATITUDE, LONGITUDE);
    CalcSun ();
    TimerMode = EEPROM.read (0); // Read TimerMode from EEPROM
}
           
void printTemperature(DeviceAddress deviceAddress)
{
  lcd.setCursor (9,1);
  tempC = sensors.getTempC(deviceAddress);
  if (tempC == -127.00) {
    lcd.print("Err");
  } else {
    
    lcd.print(tempC);
    lcd.print((char)0xDF);
    lcd.print("C ");
   
  }
}


void loop () {
    

  
    DateTime now = RTC.now(); //get time from RTC
    //Display current time
    lcd.setCursor (0,0);
    lcd.print(now.day(), DEC);
    lcd.print('/');
    lcd.print(now.month());
    lcd.print('/');
    lcd.print(now.year(), DEC);
    lcd.print(" ");
    lcd.setCursor (0,1);
    lcd.print(now.hour(), DEC);
    lcd.print(':');
    if (now.minute() <10) 
      {
        lcd.print("0");
      }
    lcd.print(now.minute(), DEC);
    lcd.print(':');
    if (now.second() <10) 
      {
        lcd.print("0");
      }
    lcd.print(now.second());
    lcd.print(" ");
    
    //current time in minutes since midnight (used to check against sunrise/sunset easily)
    TimeMins = (now.hour() * 60) + now.minute();
   
    
    //Get and display temp every minute
    if (now.second() == 7) {
      sensors.requestTemperatures(); // Request temperature
      delay (249);
      printTemperature(outsideThermometer); // display on lcd.
     
    }
    
    // Calculate sun times once a day at a minute past midnight
    if (TimeMins == 1) {
      lcd.clear();
      CalcSun ();
    }
    
    if (TimerMode ==2) {
      if (TimeMins >= Sunrise && TimeMins <=Sunset-1 && tempC>=2) { //If it's after sunrise and before sunset, and it's not frozen, switch our relay on
          digitalWrite (A3, LOW);
          lcd.setCursor (13,0);
          lcd.print ("On ");
        }
        else {  //otherwise switch it off
          digitalWrite (A3, HIGH);
          lcd.setCursor (13,0);
          lcd.print ("Off");
        }
      }
       if (TimerMode ==3) {
      if (TimeMins >= Sunrise && TimeMins <=Sunset+60 && tempC>=2) { //If it's after sunrise and before sunset + 1 hour, and it's not frozen, switch our relay on
          digitalWrite (A3, LOW);
          lcd.setCursor (13,0);
          lcd.print ("On ");
        }
        else {  //otherwise switch it off
          digitalWrite (A3, HIGH);
          lcd.setCursor (13,0);
          lcd.print ("Off");
        }
      }
       if (TimerMode ==0) { // Off
         digitalWrite (A3, HIGH);
         lcd.setCursor (13,0);
         lcd.print ("Off");
       }
     
       if (TimerMode ==1 && tempC>=2) { // TimerMode is On, but it's not frozen
         digitalWrite (A3, LOW);
         lcd.setCursor (13,0);
         lcd.print ("On ");
       }
       

    
    pushlength = pushlengthset;
    pushlength = getpushlength ();
    delay (10);
    
    if (pushlength <pushlengthset) {
      lcd.clear ();
      ShortPush ();   
      lcd.clear ();
    }
    
       
       //This runs the setclock routine if the knob is pushed for a long time
       if (pushlength >pushlengthset) {
         lcd.clear();
         DateTime now = RTC.now();
         setyeartemp=now.year(),DEC;
         setmonthtemp=now.month(),DEC;
         setdaytemp=now.day(),DEC;
         sethourstemp=now.hour(),DEC;
         setminstemp=now.minute(),DEC;
         setclock();
         pushlength = pushlengthset;
       };
}

//sets the clock
void setclock (){
   setyear ();
   lcd.clear ();
   setmonth ();
   lcd.clear ();
   setday ();
   lcd.clear ();
   sethours ();
   lcd.clear ();
   setmins ();
   lcd.clear();
   RTC.adjust(DateTime(setyeartemp,setmonthtemp,setdaytemp,sethourstemp,setminstemp,setsecs)); //set the DS1307 RTC
   CalcSun (); //refresh the sunrise and sunset times
   delay (1000);
   
}

// subroutine to return the length of the button push.
int getpushlength () {
  buttonstate = digitalRead(A0);  
       if(buttonstate == LOW && buttonflag==false) {     
              pushstart = millis();
              buttonflag = true;
          };
          
       if (buttonstate == HIGH && buttonflag==true) {
         pushstop = millis ();
         pushlength = pushstop - pushstart;
         buttonflag = false;
       };
       return pushlength;
}
// The following subroutines set the individual clock parameters
int setyear () {
   lcd.setCursor (0,0);
    lcd.print ("Set Year");
    pushlength = pushlengthset;
    pushlength = getpushlength ();
    if (pushlength != pushlengthset) {
      return setyeartemp;
    }

    lcd.setCursor (0,1);
    knob.write(0);
    delay (50);
    knobval=knob.read();
    if (knobval < -1) { //bit of software de-bounce
      knobval = -1;
    }
    if (knobval > 1) {
      knobval = 1;
    }
    setyeartemp=setyeartemp + knobval;
    if (setyeartemp < 2014) { //Year can't be older than currently, it's not a time machine.
      setyeartemp = 2014;
    }
    lcd.print (setyeartemp);
    lcd.print("  "); 
    setyear();
}
  
int setmonth () {

   lcd.setCursor (0,0);
    lcd.print ("Set Month");
    pushlength = pushlengthset;
    pushlength = getpushlength ();
    if (pushlength != pushlengthset) {
      return setmonthtemp;
    }

    lcd.setCursor (0,1);
    knob.write(0);
    delay (50);
    knobval=knob.read();
    if (knobval < -1) {
      knobval = -1;
    }
    if (knobval > 1) {
      knobval = 1;
    }
    setmonthtemp=setmonthtemp + knobval;
    if (setmonthtemp < 1) {// month must be between 1 and 12
      setmonthtemp = 1;
    }
    if (setmonthtemp > 12) {
      setmonthtemp=12;
    }
    lcd.print (setmonthtemp);
    lcd.print("  "); 
    setmonth();
}

int setday () {
  if (setmonthtemp == 4 || setmonthtemp == 5 || setmonthtemp == 9 || setmonthtemp == 11) { //30 days hath September, April June and November
    maxday = 30;
  }
  else {
  maxday = 31; //... all the others have 31
  }
  if (setmonthtemp ==2 && setyeartemp % 4 ==0) { //... Except February alone, and that has 28 days clear, and 29 in a leap year.
    maxday = 29;
  }
  if (setmonthtemp ==2 && setyeartemp % 4 !=0) {
    maxday = 28;
  }
  
   lcd.setCursor (0,0);
    lcd.print ("Set Day");
    pushlength = pushlengthset;
    pushlength = getpushlength ();
    if (pushlength != pushlengthset) {
      return setdaytemp;
    }

    lcd.setCursor (0,1);
    knob.write(0);
    delay (50);
    knobval=knob.read();
    if (knobval < -1) {
      knobval = -1;
    }
    if (knobval > 1) {
      knobval = 1;
    }
    setdaytemp=setdaytemp+ knobval;
    if (setdaytemp < 1) {
      setdaytemp = 1;
    }
    if (setdaytemp > maxday) {
      setdaytemp = maxday;
    }
    lcd.print (setdaytemp);
    lcd.print("  "); 
    setday();
}

int sethours () {
    lcd.setCursor (0,0);
    lcd.print ("Set Hours");
    pushlength = pushlengthset;
    pushlength = getpushlength ();
    if (pushlength != pushlengthset) {
      return sethourstemp;
    }

    lcd.setCursor (0,1);
    knob.write(0);
    delay (50);
    knobval=knob.read();
    if (knobval < -1) {
      knobval = -1;
    }
    if (knobval > 1) {
      knobval = 1;
    }
    sethourstemp=sethourstemp + knobval;
    if (sethourstemp < 1) {
      sethourstemp = 1;
    }
    if (sethourstemp > 23) {
      sethourstemp=23;
    }
    lcd.print (sethourstemp);
    lcd.print("  "); 
    sethours();
}

int setmins () {

   lcd.setCursor (0,0);
    lcd.print ("Set Mins");
    pushlength = pushlengthset;
    pushlength = getpushlength ();
    if (pushlength != pushlengthset) {
      return setminstemp;
    }

    lcd.setCursor (0,1);
    knob.write(0);
    delay (50);
    knobval=knob.read();
    if (knobval < -1) {
      knobval = -1;
    }
    if (knobval > 1) {
      knobval = 1;
    }
    setminstemp=setminstemp + knobval;
    if (setminstemp < 0) {
      setminstemp = 0;
    }
    if (setminstemp > 59) {
      setminstemp=59;
    }
    lcd.print (setminstemp);
    lcd.print("  "); 
    setmins();
}

int setmode () { //Sets the mode of the timer. Auto, On or Off

    lcd.setCursor (0,0);
    lcd.print ("Set Mode");
    pushlength = pushlengthset;
    pushlength = getpushlength ();
    if (pushlength != pushlengthset) {
      EEPROM.write (0,TimerMode); //write the mode setting to EEPROM
      return TimerMode;
    }

    lcd.setCursor (0,1);
    knob.write(0);
    delay (50);
    knobval=knob.read();
    if (knobval < -1) {
      knobval = -1;
    }
    if (knobval > 1) {
      knobval = 1;
    }
    TimerMode=TimerMode + knobval;
    if (TimerMode < 0) {
      TimerMode = 0;
    }
    if (TimerMode > 3) {
      TimerMode=3;
    }
    if (TimerMode == 0) {
    lcd.print("Off        ");
    lcd.print("  "); 
    }
    if (TimerMode == 1) {
    lcd.print("On         ");
    lcd.print(" "); 
    }
    if (TimerMode == 2) {
    lcd.print("Auto       ");
    lcd.print("  "); 
    }
    if (TimerMode == 3) {
    lcd.print("Auto Extend");
    lcd.print("  "); 
    }
    setmode ();
}

int CalcSun () { //Calculates the Sunrise and Sunset times
    DateTime now = RTC.now();
    sunTime[3] = now.day(); // Give Timelord the current date
    sunTime[4] = now.month();
    sunTime[5] = now.year();
    myLord.SunRise(sunTime); // Computes Sun Rise.
    Sunrise = sunTime[2] * 60 + sunTime[1]; // Sunrise returned in minutes past midnight
    SunriseHour = sunTime[2];
    SunriseMin = sunTime [1];
    sunTime[3] = now.day(); // Uses the Time library to give Timelord the current date
    sunTime[4] = now.month();
    sunTime[5] = now.year();
    myLord.SunSet(sunTime); // Computes Sun Set.
    Sunset = sunTime[2] * 60 + sunTime[1]; // Sunset returned in minutes past midnight
    SunsetHour = sunTime[2];
    SunsetMin = sunTime [1];
}

void ShortPush () {
  //This displays the calculated sunrise and sunset times when the knob is pushed for a short time.
for (long Counter = 0; Counter < 604 ; Counter ++) { //returns to the main loop if it's been run 604 times 
                                                     //(don't ask me why I've set 604,it seemed like a good number)
  lcd.setCursor (0,0);
  lcd.print ("Sunrise ");
  lcd.print (SunriseHour);
  lcd.print (":");
  if (SunriseMin <10) 
     {
     lcd.print("0");
     }
  lcd.print (SunriseMin);
  lcd.setCursor (0,1);
  lcd.print ("Sunset ");
  lcd.print (SunsetHour);
  lcd.print (":"); 
    if (SunsetMin <10) 
     {
     lcd.print("0");
     }
  lcd.print (SunsetMin);        

    
  //If the knob is pushed again, enter the mode set menu
  pushlength = pushlengthset;
  pushlength = getpushlength ();
  if (pushlength != pushlengthset) {
    lcd.clear ();
    TimerMode = setmode ();

  }
  
}



}