Pi Time


Figure 1 : Raspberry Pi Clock

This project started out as a display cabinet demo, but with changes to the undergraduate curriculum its not quite on message i.e. too much hardware :(, therefore, decided to turn it into a clock for my office :). Since it used a Pi2 seemed a little wasteful to use it as just a 'clock', so thought i would have a play with databases i.e. SQLite. When you stop and think about, its strange that no one has put a quad core processor and a database into a clock, its such an obvious oversight :). The system has:

Table of Contents

Analogue Display - Digital to Analogue Converters
Real time clock
Digital display
New and Improved OO
LCD display
DHT11 temperature and humidity sensor
Speaker
Microphone & Light sensors - Analogue to Digital Converters
Threads and Locks
PIR and Radar
Data logger
Google drive & Camera


Figure 2 : analogue display

The time is displayed on six analogue meters, two each for hours, minutes and seconds. Figure 2 shows the seconds digits, displaying the value 18. Got a big box of these meters, they were used in electronics practicals to teach the fundamentals of current (I) and resistance (R), now surplus to requirements. Six down, just need to find another project to use the other 50, perhaps some type of FFT :). To drive these meters i'm using some old DACs: ZN428 (LINK), these date back to an old Z80 module we taught many moons ago. If i was building this from scratch would have chosen a different DAC, but again have a big box full. The main issue with these DACs is they need an output driver and were designed to fit onto an 8bit bus, so interfacing to the Pi is a little messy. Could of gone with an Ad-Hoc bus made from GPIO, but that would of needed a lot of wires i.e. linking the Pi to the bread board (16-ish). Therefore, decided to off load this to a pair of PCF8474 (LINK) 8bit I2C GPIO expanders, now only need two linking wires (SDA, SCL). This localised the DAC wiring to the bread board i.e. modular. The ZN428 is a classic R-2R ladder (LINK) with an onchip 8bit latch. The standard uni-polar setup is shown in figure 3 below.


Figure 3 : DAC circuit

In this configuration the ladder uses its internal 2.5V ref. Normally you would chose the values of R1 and R2 (non-inverting amps feedback Rs) to scale the output voltage up, to maximise voltage output swing. However, as i'm using these DACs to generate a current i.e. to move the analogue meter's needle, i'm not fussed about the actual output voltage level, just need something to generate a current. Therefore, it doesn't matter if the voltage output is 2.5V or 5V, as i would just select an appropriate resistor to place in series with the meter i.e. to achieve a full scale deflection. To keep things simple and to reduce power, went for 2.5V max analogue out. This also means that i can reconfigure the op-amp as a voltage follower, rather than a non-inverting amp i.e. a gain of one. Also, you normally have joys trying to use an op-amp like this from a single rail supply, a good discussion of virtual grounds etc can be found here (LINK). Note, you still need the op-amp as a current driver, the DAC's Aout doesn't have enough power to drive the meter directly. The op-amp used in this system is your standard LM324 (LINK) a quad operational amplifier, wiring for the DACs and op-amps is shown in figure 4.


Figure 4 : DAC and Op-Amp wiring

From figure 4 you can spot that the op-amps are wired as followers, driving a 220 ohm resistor in series with the meter to ground (blue wires). The maximum voltage output is 2.5V, therefore, the maximum current in the meters is approx 2.5/220 = 11mA, which is about right for the 10mA meters. For reasons that i will come back to, i wired up eight ZN428 DACs, six for the display and two for other stuff. To control these DACs i used one PCF8574 to drive the common 8bit data bus, and the other to control the enable signal of each DAC, as shown in figure 3. These GPIO expanders are shown in figure 5. If you follow the wiring colour scheme through figures 3 and 4 you will spot that:


Figure 5 : PCF8574 GPIO expanders

Note, lacing cord good, cable ties bad :). To update a DAC's output you simply need to drive the required 8bit value onto the parallel data bus and pulse the appropriate enable line low. Unfortunately, this setup is not a easy as it may sound. These meters had been used for a long time in the lab, so they are just a 'bit' out of calibration. This isn't too much of an issue when your using them to measure a current, but when you need to move the meter's needle such that it points exactly to the values 0,1,2,3,4,5,6,7,8,9 then you have to individually calibrate each DAC output. Initially i tried to do this by modifying values in the code, or using a command line interface, but this was _very_ slow. This initial code is shown below:

import smbus	

I2C_DATA_ADDR   = 0x38	#I2C DATA bus base address
I2C_ENABLE_ADDR = 0x39	#I2C ENABLE bus base address

DAC_EN = [0x7F, 0xBF, 0xDF, 0xF7, 0xFB, 0xFD, 0xFE]

DAC_VOLTAGE = [[0, 23, 47, 69, 93, 115, 132, 156, 174, 196],
               [0, 27, 52, 77, 102, 124, 144, 167, 190, 212],
               [0, 28, 53, 77, 102, 125, 144, 168, 189, 211],
               [0, 28, 52, 76, 101, 124, 146, 170, 192, 213],
               [0, 25, 50, 74, 97, 120, 139, 163, 184, 207],
               [0, 27, 49, 71, 98, 123, 146, 170, 192, 214]]

bus = smbus.SMBus(1)	#enable I2C bus

digit = 1
value = 1

bus.write_byte( I2C_DATA_ADDR, 0 )    	  # clear port
bus.write_byte( I2C_ENABLE_ADDR, 255 )    # clear port

bus.write_byte( I2C_DATA_ADDR, DAC_VOLTAGE[digit][value] )  
bus.write_byte( I2C_ENABLE_ADDR, DAC_EN[digit] )  
bus.write_byte( I2C_ENABLE_ADDR, 255 )

After calibration the display worked well, but i found that the meter's needle position was very sensitive to power supply and orientation changes, therefore, you needed to constantly re-calibrate. Eventually decided to write a small GUI python program as shown in figure 6, to quickly capture calibration data.


Figure 6 : Calibration GUI

This GUI allows you to select the meter (HH:MM:SS) and digit (0 - 9), then in real time adjust the slide bar (0 - 255) to set that meter's required output. When this program is exited these values are written to a configuration file that can be cut-&-pasted into main program. The code for this GUI is available here (LINK). Once the required DAC outputs have been determined i just need to determine the time and therefore the digits for HH:MM:SS. This is calculated by the code below, updating the global variables: hoursHigh, hoursLow, minutesHigh, minutesLow, secondsHigh, secondsLow. Not the best solution, but it works.

def updateTime():
    global hoursHigh, hoursLow, minutesHigh, minutesLow, secondsHigh, secondsLow

    localtime = time.asctime( time.localtime( time.time() ) )
    localtime = localtime.replace("  ", " ") 

    dateString = localtime.split(" ", 5)

    day = dateString[0]

    timeString = dateString[3].split(":", 3)
    #print timeString

    hours = str(timeString[0])
    hoursHigh = int(hours[0])
    hoursLow  = int(hours[1]) 

    minutes = str(timeString[1])
    minutesHigh = int(minutes[0])
    minutesLow  = int(minutes[1])

    seconds = str(timeString[2])
    secondsHigh = int(seconds[0])
    secondsLow = int(seconds[1])
	
    #print str(hoursHigh) + " " + str(hoursLow) + " " + str(minutesHigh) + " " + str(minutesLow) + " " + str(secondsHigh) + " " + str(secondsLow)

This function is called every 0.5 seconds and compares the updated values for seconds, minutes and hours with the current values. If they differ the appropriate DAC is updated. The system's main loop is shown in the code below:

while True:
    updateTime()

    if secondsLow != secondsLowOld:
        secondsLowOld = secondsLow
        updateDigit( 0, secondsLow)

        if secondsHigh != secondsHighOld:
            secondsHighOld = secondsHigh
            updateDigit( 1, secondsHigh)
       
            if minutesLow != minutesLowOld:
                minutesLowOld = minutesLow 
                updateDigit( 2, minutesLow)
	          
                if minutesHigh != minutesHighOld:
                    minutesHighOld = minutesHigh
                    updateDigit( 3, minutesHigh)

                    if hoursLow != hoursLowOld:
                        hoursLowOld = hoursLow 
                        updateDigit( 4, hoursLow)

                        if hoursHigh != hoursHighOld:
                            hoursHighOld = hoursHigh
                            updateDigit( 5, hoursHigh)

    time.sleep(0.5)
 
except IOError:
    pass
finally:
    pass

The six meters are mounted using 3D printed brackets, connected together using 8mm brass rod. Unfortunately, after going through my backups i can not find the original Openscad files (first made in 2015), all i could find are the STL files shown in figure 7, available here: (LINK). Don't think these are the final versions, but you should be able to stitch together the frame from these, but to be honest probably easier to start a fresh. Some pictures of the final clock frame are shown in figure 8.




Figure 7 : STL modles






Figure 8 : clock frame

Real Time Clock

To ensure that the Pi always has the correct time, even after a power failure, i attached a real time clock and temperature sensor module RPI-RTC-TC (LINK)(LINK) as shown in figure 9. Simply plugs onto the GPIO header, the good thing about this one is that it has pass through headers allowing you access to the power (+5V, +3.3V and 0V) and I2C bus.




Figure 9 : Real Time Clock (RTC) module

To configure the Pi to use this module as its RTC add the following to the end of /boot/config.txt:

dtoverlay=i2c-rtc,ds1307

Next add to the end of the file /etc/modules:

rtc-ds1307

Finally edit the file /lib/udev/hwclock-set commenting out:

#if [ -e /run/systemd/system ] ; then
#exit 0
#fi

Reboot the Pi, if its connected to the network it will automatically get the correct time, otherwise use the command date with the -s option to set the current time and date. To save this date to the RTC module, at the command prompt enter:

sudo hwclock -w

To check that all is good enter:

sudo hwclock -r

Have tried a copy of different RTC modules, to be honest this was the easiest to setup and most reliable one ive come across. One problem that ive always had with the Raspiberry pi is setting the correct time zone. There seems to be a number of different suggestion on how to do this, i found the command below works, select Europe and then London:

sudo dpkg-reconfigure tzdata

The final DAC board is shown in figure 10. A short video and the clock in action is available here: (LINK)


Figure 10 : DAC board

Digital Display


Figure 11 : 7-segement display

The analogue display is fun, but you can't quickly glance over to it and see the time. To give a more normal display added some 7-segment displays (LINK). Again, could of controlled this display using just GPIO lines i.e. time multiplexed GPIO, as discussed here: (LINK). Using this technique each 7-segment each display is connected to a shared bus, but the common anode/cathode line of each display is controlled by a separate drive transistor, as shown in figure 12. Therefore, you can only update one display at a time, if you do this quick enough persistence of vision gives the illusion that each segment is displaying a constant value. The big advantage of this method is it significantly reduces the number of wires when you have a large number of displays. In this case with six 7-segment displays it would of required 24 GPIO lines, using time multiplexed displays reduces this down to 13 (7 for segments and 6 for enables). The disadvantage of this approach is processor load, to ensure that the displays do not flicker you need to update each display every 1/20 second.


Figure 12 : multiplexed 7 segment LEDs (source)

Multiplexing displays is an ideal job for an Arduino, but, if your using a Pi then you don't want to lockup a core bit bashing. A Raspberry Pi is good general purpose work horse, capable of doing lots of different tasks, but, low level bit flipping isn't perhaps one of its strengths. Therefore, went for a 74hc4511 (LINK) a seven segment driver, these devices contain an 4bit latch and a binary to seven segment decoder, as shown in figure 13. This make the LED displays a fire and forget device, just need to write the four bit digit value to the driver, then its on-chip decoder drives the correct LED segments to display that value.






Figure 13 : 7 segment LED driver

This device has three control signals, however, in this case only need to use the LE signal to update the on-chip latch, the other two are tied high. To select which of the six 7-segment LED displays to update used a 74hc138 (LINK) a 3-to-8 decoder, as shown in figure 14. This device can generate 8 active low control signals based upon its 3bit input bus (A0 - A2). As these bits will _not_ change at the same time (generated by GPIO lines) need to also use the control line E3. This line is held low whilst A0,A1 and A2 are being updated, disabling the decoders outputs, when the address is stable this line is pulsed high to trigger the decoder. This ensures that transitory states produced during address bus changes do not trigger false display updates e.g. from address=010 to address=101. Tried to take a picture of this device, a little tight, shown in figure 15. Wire colour code: Red - +5V, Black - 0V, Purple - address, Grey - Control, White and Green - data, Orange - decoder LE.



Figure 14 : 3-8 decoder


Figure 15 : 3-8 decoder wiring

These decoders are connected to an ad-hoc parallel bus using GPIO, 4bit data bus (A,B,C,D) , 3bit address bus (A0,A1,A2) and a 1bit control bus (E3). The example python code used to control these GPIO lines is shown below, a little cut-and-paste, but it works:

import RPi.GPIO as GPIO
import time 

def zero():
	GPIO.output(26, False)  #A
	GPIO.output(06, False)  #B
	GPIO.output(13, False)  #C
	GPIO.output(19, False)  #D

def one():
	GPIO.output(26, True)
	GPIO.output(06, False)
	GPIO.output(13, False)
	GPIO.output(19, False)

def two():
	GPIO.output(26, False)
	GPIO.output(06, True)
	GPIO.output(13, False)
	GPIO.output(19, False)

def three():
	GPIO.output(26, True)
	GPIO.output(06, True)
	GPIO.output(13, False)
	GPIO.output(19, False)

...

def update( pin ):
	if pin == 0:
		GPIO.output(16, True)  #A0
		GPIO.output(20, False) #A1
		GPIO.output(21, False) #A2
	elif pin == 1:
		GPIO.output(16, False)
		GPIO.output(20, True)
		GPIO.output(21, False)
	elif pin == 3:
		GPIO.output(16, True)
		GPIO.output(20, True)
		GPIO.output(21, False)
	elif pin == 2:
		GPIO.output(16, False)
		GPIO.output(20, False)
		GPIO.output(21, True)
	elif pin == 4:
		GPIO.output(16, True)
		GPIO.output(20, False)
		GPIO.output(21, True)
	elif pin == 5:
		GPIO.output(16, False)
		GPIO.output(20, True)
		GPIO.output(21, True)
	else:
		GPIO.output(16, False)
		GPIO.output(20, False)
		GPIO.output(21, False)
	
	GPIO.output(05, True)  #E3
	time.sleep(0.05)
	GPIO.output(05, False)
	time.sleep(0.05)
	GPIO.output(16, False)
	GPIO.output(20, False)
	GPIO.output(21, False)

7-segment LED displays come in two flavours, common Cathode, or common Anode, as shown in figure 16. As the 74hc4511 decoders are capable of driving the LEDs directly went for common Cathode. The current limiting resistor is 330 ohms.


Figure 16 : LED drivers

This code is integrated into the analogue displays control loop to update the HH:MM digits. The final digital display board is shown in figure 17, the clock display are the four left most 7-segment displays. Note, the large number of 330 ohm resistors, one for each LED segment, these take up the majority of the space. Wire colour code: Red - +5V, Black - 0V, Green - data, Orange - decoder LE, Yellow - output of decoder to 330, Blue - 330 to LED. Digits too bright for camera, looks ok in real life, perhaps need a red strip of acrylic sheet to sharpen up the display. Update 2-6-2018: add a filter made from 3mm acrylic sheet, as shown in figure 18, .dxf file can be downloaded here: (LINK). Camera still a little sensitive, so images don't represent what you see in the real world, but red tinted acrylic does mask out the non-illuminated led segments, making it a lot easier to read.


Figure 17 : digital display board




Figure 18 : digital display board with filter

The RTC module discussed previously also contains a temperature sensor MCP9801 (LINK), could not find any python code online, but looking at the datasheet quick easy to interface to using standard python, test code below:

import smbus
import time

bus = smbus.SMBus(1)

while True:
  raw = bus.read_word_data(0x4F, 0) & 0xFFFF
  value = (((raw << 8) & 0xFF00) | (raw >> 8)) / 256.0
  highDigit = int(value) // 10
  lowDigit = int(value) % 10
  outputString = "Temp: " + str(value) + " " + str(highDigit) + " " str(lowDigit)
  print outputString
  time.sleep(1)

This code was then integrated into the clocks main loop, updating the two right most 7-segment displays shown in figure 17 every 10 seconds. This temperature sensor does read a little hotter than room temperature. It does feel slightly warm to the touch i assume this because its mounted above the Pi, which although not hot will warm the surrounding air.

New and Improved OO

After messing around with the clock for a little while the number of global variables were starting to get a little silly, you just reached a point were you know that the longer you go down this "its only a prototype road" the more messy the code will become and therefore, the more difficult it will be to test and debug. At this point need to start to pull functionality together into classes. Therefore, the new and improved analogue clock, led display and temperature sensor classes are shown below:

import smbus				
import time

class clock():
    def __init__( self, bus ):
        self.I2C_DATA_ADDR   = 0x38	
        self.I2C_ENABLE_ADDR = 0x39	

        self.DAC_EN = [0xFE, 0xFD, 0xFB, 0xF7, 0xEF, 0xDF, 0xBF, 0x7F]

        self.DAC_VOLTAGE = [[0, 27, 49, 71, 98, 123, 146, 170, 192, 214],
                            [0, 25, 50, 74, 97, 120, 139, 163, 184, 207],
                            [0, 28, 52, 76, 101, 124, 146, 170, 192, 213],
                            [0, 28, 53, 77, 102, 125, 144, 168, 189, 211],
                            [0, 27, 52, 77, 102, 124, 144, 167, 190, 212],
                            [0, 23, 47, 69, 93, 115, 132, 156, 174, 196]]
        self.bus = bus

        try:
            self.bus.write_byte( self.I2C_DATA_ADDR, 0 )    	# clear port
        except IOError:
            print("clock: I2C comms error")

        try:
            self.bus.write_byte( self.I2C_ENABLE_ADDR, 255 )    # clear port
        except IOError:
            print("clock: I2C comms error")

        self.newTime = [0,0,0,0,0,0]


    def updateTime( self ):
        localtime = time.asctime( time.localtime( time.time() ) )
        localtime = localtime.replace("  ", " ") 

        dateString = localtime.split(" ", 5)

        day = dateString[0]

        timeString = dateString[3].split(":", 3)
        #print timeString

        hours   = str(timeString[0])
        minutes = str(timeString[1])
        seconds = str(timeString[2])

        self.newTime = [ int(seconds[1]), int(seconds[0]),
                         int(minutes[1]), int(minutes[0]),
                         int(hours[1]),   int(hours[0]) ]

    def getTime( self ):
        self.updateTime()
        return self.newTime

    def updateDisplay( self ):
        self.updateTime()
        for digit in range(6):
            try:
                print digit
                self.bus.write_byte( self.I2C_DATA_ADDR, self.DAC_VOLTAGE[digit][self.newTime[digit]] )    	  
                self.bus.write_byte( self.I2C_ENABLE_ADDR, self.DAC_EN[digit] )  
                self.bus.write_byte( self.I2C_ENABLE_ADDR, 255 ) 
                self.oldTime = self.newTime
            except IOError:
                print("clock: I2C comms error")
		
    def updateDigit( self, digit, value ):
        try:
            self.bus.write_byte( self.I2C_DATA_ADDR, self.DAC_VOLTAGE[digit][value] )    	  
            self.bus.write_byte( self.I2C_ENABLE_ADDR, self.DAC_EN[digit] )  
            self.bus.write_byte( self.I2C_ENABLE_ADDR, 255 ) 
        except IOError:
            print("clock: I2C comms error")
		
# Main program block

def main():
    print "main start"
   
    bus = smbus.SMBus(1) #enable I2C bus
    display = clock( bus )

    while True:
        for i in range(0,6):
            for j in range(0,10):
                display.updateDigit(i, j)
                time.sleep(0.5)	

    while True:
        #print("update")

        display.updateDisplay()
        time.sleep(1)


if __name__ == '__main__':
    try:
        main()
    except KeyboardInterrupt:
        pass
    finally:
        pass

Added a try-except block around the I2C communication, just in case there was a glitch / timeout. This does now block the code on a comms failure, perhaps should change, limit the max number of retires. The added advantage of this style of coding / OO approach, is that basic test code can be integrated into the same files by defining a main function. Found this quite useful for unit testing i.e. for those moments when your not sure if you have broken the software or hardware, re-running a basic unit test focuses your attention on the right area.

import RPi.GPIO as GPIO
import time 

class seven_seg():
    def __init__( self ):
        self.DATA_BUS = [26, 06, 13, 19]
        self.ADDR_BUS = [16, 20, 21]
        self.CONTROL = 5
        self.MASK = [1, 2, 4, 8]
        self.DIGIT = [1, 2, 4, 3, 5, 6]

        GPIO.setwarnings(False)
        GPIO.setmode(GPIO.BCM)

        for pin in self.DATA_BUS:
            GPIO.setup(pin, GPIO.OUT) 
            GPIO.output(pin, False)

        for pin in self.ADDR_BUS:
            GPIO.setup(pin, GPIO.OUT) 
            GPIO.output(pin, False)

        GPIO.setup(self.CONTROL, GPIO.OUT) 
        GPIO.output(self.CONTROL, False)

    def set_data( self, value ):
        for i in range(4):
            GPIO.output(self.DATA_BUS[i], (self.MASK[i] & value)) 

    def set_addr( self, digit ):
        display = self.DIGIT[digit]
        for i in range(3):
            GPIO.output(self.ADDR_BUS[i], (self.MASK[i] & display)) 

        GPIO.output(05, True)
        time.sleep(0.05)
        GPIO.output(05, False)
        time.sleep(0.05)

        for i in range(3):
            GPIO.output(self.ADDR_BUS[i], 0) 

    def update( self, digit, value ):
        self.set_data( value )
        self.set_addr( digit )

def main():   
    display = seven_seg()

    while True:
        for i in range(0,6):
            for j in range(0,10):
                display.update(i, j)
                time.sleep(0.5)	

if __name__ == '__main__':
    try:
        main()
    except KeyboardInterrupt:
        pass
    finally:
        GPIO.cleanup()

Moved away from the cut-&-paste style of GPIO updates to ones using for loops, as shown above. This does make for better looking code, but i suspect it comes at the cost of a longer run time, however, when speed it not critical, perhaps the best approach. Note, one thing i found annoying is that in python array indexes read left-to-right i.e. element 0 is the left most digit, but when you think of a binary number digit 0 is the right most digit, its hard to undo years of brain washing :). When i started to split the code up into different sections did ponder how the OS shared the GPIO and I2C bus i.e. hardware resources, structural hazards etc. Decided to instantiate the I2C bus in the main program and pass this to the classes. However, after playing around with test code the Pi does seem to work ok e.g. it looks like two separate processes can share the I2C bus. As always tried to hide details where possible, push functionality into the classes to help simplify the main program coding.

import smbus
import time			

class mcp9801():
    def __init__( self, bus ):
        self.temperature = 0.0
        self.bus = bus    

    def getTemp( self ):
        try:        
            raw = self.bus.read_word_data(0x4F, 0) & 0xFFFF
        except IOError:
            print("I2C comms error")

        value = (((raw << 8) & 0xFF00) | (raw >> 8)) / 256.0
        return value

    def getTempDigits( self ):
        temp = self.getTemp()
        high = int(temp) // 10
        low = int(temp) % 10
        return [low, high]

def main(): 
    bus = smbus.SMBus(1)				#enable I2C bus 
    sensor = mcp9801( bus )

    while True:
        print sensor.getTemp(), sensor.getTempDigits()
        time.sleep(1)

if __name__ == '__main__':
    try:
        main()
    except KeyboardInterrupt:
        pass

LCD Display

As the Pi will be running headless most of the time i included this LCD display, to allow some user feedback of any error messages etc. Also in normal usage it will display the office's temperature and humidity. These 16x2 displays (LINK) are very common, a good description and python code can be found here: (LINK). To save GPIO pins a I2C adaptor board can be attached, the red board in figure 19, again a good description and python code can be found here: (LINK), very reliable, easy to use code. The 3D printed stand is from Thingiverse (LINK)(LOCAL), a very nice fit. Note, the back light level is important for these displays, initially for the one i was using i thought it was broken i.e. looked blank, then adjusting the back light revealed the message.




Figure 19 : LCD display

To fit this code into the system just cut and pasted the existing code into a class wrapper, as shown below:

import smbus
import time

class lcd():
    def __init__( self, bus ):

        # Define some device parameters
        self.I2C_ADDR  = 0x27 # I2C device address
        self.LCD_WIDTH = 16   # Maximum characters per line

        # Define some device constants
        self.LCD_CHR = 1 # Mode - Sending data
        self.LCD_CMD = 0 # Mode - Sending command

        self.LCD_LINE_1 = 0x80 # LCD RAM address for the 1st line
        self.LCD_LINE_2 = 0xC0 # LCD RAM address for the 2nd line
        self.LCD_LINE_3 = 0x94 # LCD RAM address for the 3rd line
        self.LCD_LINE_4 = 0xD4 # LCD RAM address for the 4th line

        self.LCD_BACKLIGHT  = 0x08  # On
        #self.LCD_BACKLIGHT = 0x00  # Off

        self.ENABLE = 0b00000100 # Enable bit

        # Timing constants
        self.E_PULSE = 0.0005
        self.E_DELAY = 0.0005 

        self.bus = bus

        # Initialise display
        self.lcd_byte(0x33, self.LCD_CMD) # 110011 Initialise
        self.lcd_byte(0x32, self.LCD_CMD) # 110010 Initialise
        self.lcd_byte(0x06, self.LCD_CMD) # 000110 Cursor move direction
        self.lcd_byte(0x0C, self.LCD_CMD) # 001100 Display On,Cursor Off, Blink Off
        self.lcd_byte(0x28, self.LCD_CMD) # 101000 Data length, number of lines, font size
        self.lcd_byte(0x01, self.LCD_CMD) # 000001 Clear display
        time.sleep(self.E_DELAY)

    def lcd_byte(self, bits, mode):
        # Send byte to data pins
        # bits = the data
        # mode = 1 for data
        #        0 for command

        bits_high = mode | (bits & 0xF0) | self.LCD_BACKLIGHT
        bits_low = mode | ((bits<<4) & 0xF0) | self.LCD_BACKLIGHT

        # High bits
        try:
            self.bus.write_byte(self.I2C_ADDR, bits_high)
        except IOError:
            print("I2C comms error")

        self.lcd_toggle_enable(bits_high)

        # Low bits
        try:
            self.bus.write_byte(self.I2C_ADDR, bits_low)
        except IOError:
            print("I2C comms error")

        self.lcd_toggle_enable(bits_low)

    def lcd_toggle_enable(self, bits):
        # Toggle enable
        time.sleep(self.E_DELAY)

        try:
            self.bus.write_byte(self.I2C_ADDR, (bits | self.ENABLE))
        except IOError:
            print("I2C comms error")

        time.sleep(self.E_PULSE)

        try:
            self.bus.write_byte(self.I2C_ADDR,(bits & ~self.ENABLE))
        except IOError:
            print("I2C comms error")

        time.sleep(self.E_DELAY)

    def lcd_string(self, message,line):
        # Send string to display

        message = message.ljust(self.LCD_WIDTH," ")
        self.lcd_byte(line, self.LCD_CMD)

        for i in range(self.LCD_WIDTH):
            self.lcd_byte(ord(message[i]),self.LCD_CHR)
        
    def lcd_update(self, temperature, humidity ):
        line1 = "Temp: " + str(temperature) +" C"
        line2 = "Humidity: " + str(humidity) + " %"

        self.lcd_string(line1, self.LCD_LINE_1)
        self.lcd_string(line2, self.LCD_LINE_2)


# Main program block

def main():
    print "main start"
    temperature = 0.0
    humidity = 0.0

    bus = smbus.SMBus(1)				#enable I2C bus
    display = lcd( bus )

    while True:
        print("update")
        temperature = temperature + 1.0
        humidity = humidity + 2.0

        display.lcd_update( temperature, humidity )
        time.sleep(1)


if __name__ == '__main__':
    try:
        main()
    except KeyboardInterrupt:
        pass
    finally:
        pass

Update 25-6-2018: small update changed the update function to a rolling display so that i could display the date, as shown below, also added the variable self.lcdCount to keep track of the rotation. This function is called by the main loop every 10 seconds.

def lcd_update(self, temperature, humidity ): 
    line1 = "Temp: " + str(temperature) +" C"
    line2 = "Humidity: " + str(humidity) + " %"
        
    dateString = str(datetime.datetime.now()).split()[0].split('-')
    line3 = "Date: " + dateString[2] + "-" + dateString[1] + "-" + dateString[0]

    if self.lcdCount == 0:
        self.lcd_string(line1, self.LCD_LINE_1)
        self.lcd_string(line2, self.LCD_LINE_2)
        self.lcdCount += 1
    elif self.lcdCount == 1:
        self.lcd_string(line2, self.LCD_LINE_1)
        self.lcd_string(line3, self.LCD_LINE_2)
        self.lcdCount += 1            
    else:
        self.lcd_string(line3, self.LCD_LINE_1)
        self.lcd_string(line1, self.LCD_LINE_2)            
        self.lcdCount = 0

DHT11 temperature and humidity sensor

Temperature and humidity are measured using the standard DHT11 sensor (LINK), as shown in figure 20. This is connected to +5V and 0V and uses a single wire protocol for communications. After a little searching found this code on Github: (LINK), the only down side is that sometimes the communication link times out. This could be an issue if it was read in the main loop i.e. could get stuck for a second or so, therefore, display would not update. A simple solution is to run this code as a separate thread and communicate with the main program with a shared variable, function calls, a modified version of the example code from the above link is shown below. The trickiest part was to get the sensor thread to terminate on a keyboard interrupt. There seemed to be a lot of different solutions online, but i could not get these to work reliably, the first working solution called a kill command from within the thread, as shown below.




Figure 20 : DHT11 sensor

import RPi.GPIO as GPIO
import dht11

import time
import datetime

import threading
import signal
import os

# Sensor Thread

class display ( threading.Thread ):
    def stop( self ):
        os.kill(os.getpid(), signal.SIGUSR1)

    def run( self ):
	global temperature, humidity

        # initialize GPIO
        GPIO.setwarnings(False)
        GPIO.setmode(GPIO.BCM)

        sensor = dht11.DHT11(pin=12)

        while True:
            result = sensor.read()
            if result.is_valid():
                #print("Last valid input: " + str(datetime.datetime.now()))
                #print("Temperature: %d C" % result.temperature)
                #print("Humidity: %d %%" % result.humidity)
		
		temperature = result.temperature
		humidity = result.humidity

            time.sleep(1)

# Main program block

temperature = 0
humidity = 0

try:
    lcd.display().start()

    while True:
	updateTime()
	... UPDATE DISPLAYS ..
   
except KeyboardInterrupt:
    lcd.display().stop()
finally:
    pass

This "worked", but perhaps a little messy, needed a couple of read functions and some global declarations. Messed around with different solutions, again could not get the exit condition working all the time, the solution below is a mixture of material i found online (sorry don't have the links). Overall, this is a nicer solution allows a true OO solution, solves the __init__ problem i had in the previous i.e. explicitly calls the super class __init__ from which the sensor class inherits. Communication is now through explicit get methods on local attributes. The original solution provided a new join() function, overwriting the one inherited from the threading library. This didn't work for me, not sure why? Redefining this as the stop() function, then having the main program wait for the thread to complete using the standard join() seemed to be better. The flag stopReg is defined as a thread Event, looking as the documentation (LINK) this may be slight over kill i.e. not using the wait() function, but it works, so kept as is.

import threading

import RPi.GPIO as GPIO
import dht11
import time
import datetime

class sensor( threading.Thread ):
    def __init__( self ):
        super(sensor, self).__init__()
        self.temp = 0.0
        self.hum = 0.0
        self.count = 0
        self.instance = dht11.DHT11(pin=12)
        self.stopReq = threading.Event()

    def getTemp( self ):
        return self.temp

    def getHum( self ):
        return self.hum

    def getTempHum( self ):
        return [self.temp, self.hum]

    def getCount( self ):
        return self.count

    def run ( self ):
        while not self.stopReq.isSet():
            result = self.instance.read()
            if result.is_valid():
                self.temp = result.temperature 
                self.hum = result.humidity 
                self.count = self.count + 1

                #print("Last valid input: " + str(datetime.datetime.now()))
                #print("Temperature: %d C" % self.temp)
                #print("Humidity: %d %%" % self.hum)
                #print self.count

            time.sleep(5)
  
    def stop( self ):
        self.stopReq.set()
        time.sleep(0.5)

def main():
    # initialize GPIO
    GPIO.setwarnings(False)
    GPIO.setmode(GPIO.BCM)

    t = sensor()
    t.start()

    while True:
        try:
            print t.getTemp(), t.getHum(), t.getCount()
            time.sleep(10)
        except KeyboardInterrupt:
            t.stop()  
            t.join()
        finally:
            pass 

if __name__ == "__main__":
    main()

Speaker

This speaker was from the old PiSpeaker amplifier project (LINK). Using the Pi 3.5mm audio output played a .wav file on the quarter hour. As this Pi is operated as a headless node the audio should be set to analogue out, the 3.5mm audio socket. To force the Pi to use this output e.g. to prevent it from using HDMI if connected, at the command line enter:

amixer cset numid=3 1

where the final number specifies the interface : 0=auto, 1=analog, 2=hdmi. To increase or decrease the volume, you can do this using amixer, but the easiest method is to enter at the command line:

alsamixer

This will start a simple GUI allowing you to adjust the volume levels. To play sounds i.e. chimes, wrote a very simple bash script (chime.sh) to start a new process (&), such that the main python program is not blocked (delayed) whilst the sound is playing. This then runs the command line program aplay to play the choosen .wav file, as shown below:

#!/bin/sh
aplay chime.wav &

Again, the trigger for this code is placed in the analogue meter's digit update main loop, quarter hour detection is performed after each minute update and hour chimes are always performed after each low hour digit update, as shown in the example code below.

if newTime[2] != oldTime[2]:
    if (((newTime[2] == 5) and (newTime[3] == 1)) or ((newTime[2] == 5) and (newTime[3] == 4)) or ((newTime[2] == 0) and (newTime[3] == 3))):
        os.system("/home/pi/Clock/chime.sh")

if newTime[4] != oldTime[4]:
    os.system("/home/pi/Clock/chime.sh")

There are quite a few free sound effect sites from which to download the chime sound, after consideration went for this one: (LINK). Going back to the PiSpeakers, they actually work quite nicely, bass frequency performance is a little lacking, but as they are used to play a chime i.e. high frequency sound, they work very well.


Figure 21 : Speaker

Picked up a lot of background hiss when i connected the speakers to the Pi +5V, not surprising, a lot of digital noise on the power rail from the Pi. Tried switching from a 1A to a 2.5A power supply to see if that would give a more stable power supply, no joy. The solution was to put in a simple filter, as shown in figure 22. As the current was low went for a small signal diode (1N4148 250mA), the 100uF cap is a 100V version, to give a bit more peak current when the speaker first starts off. If you listen very carefully there is a very small hiss, but no more than any other audio amplifier, not detectable in normal operation, but i am getting old :).


Figure 22 : Simple filter

Microphone & Light sensors - Analogue to Digital Converters

The reason for including the two extra DACs (discussed in first section) was that I wanted to integrate into the system some analogue sensors i.e. light level using a LDR and sound level using a electret microphone. To convert the analogue signals generated by these sensors you need some sort of analogue to digital (ADC) converter. Could of bought an off the self I2C device, but wanted to test out some alternative solutions. There are a number of different methods:

DAC

A simplified block diagram of this type of ADC is shown in figure 23. Using an operational amplifier comparator the unknown signal (analogue input, Ain) is compared to a signal generated by a digital to analogue converter (Vdac). The comparator acts as a 1-bit ADC, producing a digital output i.e. logic 1 if Ain less than Vdac, or logic 0 if Ain greater than or equal to Vdac, this signal being read by the Pi. In its simplest form the Pi outputs an incrementing count value to the DAC, stopping the count when the comparator indicates that the DAC signal is equal to, or larger than the unknown signal i.e. a logic 0 from the comparator. An improvement on this idea is to use successive approximation, rather than a ramp i.e. halving the error on each count value, as shown in figure 24. A discussion of successive approximation ADCs can be found here: (LINK).


Figure 23 : ADC block diagram


Figure 24 : generating value, ramp (left), successive approximation (right)

The main aim of this ADC is sample the output of the microphone circuit shown in figure 25 below. This circuit uses a cheap electret microphone, sound wave cause a varying current to flow through this microphone, producing a small voltage across R8, around a 100mV. This is then amplified using a LM324 operational amplifier configured as two inverting amplifiers. Could of used a single stage, but wanted to avoid having an amplifier stage with too large a gain i.e. gains around 1000 will tend to start to oscillate. The DC blocking caps started off as 100nF, but i found these did not pass low frequencies i.e. would pass a clap, but not me speaking. To correct this placed a 1uF in parallel i.e. a larger C has a lower impedance at lower frequencies. Note, i guess i could of removed the 100nF, but keep them in as i had already cut them to size, perhaps improves high frequency performance if the electrolytic has a high series R. As this circuit is working off a single rail supply used one operational amplifier to create a virtual ground of 1.6V. This shifts the output to 1.6V with 0V in, allowing the output voltage to swing both up and down with a varying microphone signal i.e. avoid clipping. Note, a virtual ground of 1.6V was chosen as working from a +5V supply the operational amplifiers max output voltage is 3.5V, therefore, this gives +/- 1.6V output voltage swing. The amplified signal is then passed to a peak detector. The output signals from these circuits for a clap and saying "hello" are show in figures 26 and 27.

Figure 25 : microphone amplifier

Figure 26 : microphone amplifier output: clap, left - soft, right - hard

Figure 27 : microphone amplifier output: "hello", left - quiet, right - loud

The microphone signal is intended to detect room activity e.g. sound made when the door is open or closed. It could also be used to implement a clapper e.g. clap twice and the Pi will turn on the lights etc. Therefore, not interested in the frequency components of the signal, rather its amplitude. This is maintained by the peak detector allowing the Pi sample the signal at a slower rate. The signal is passed to a comparator, the reference is supplied by the DAC, a resistor network could be used to provide a little hysteresis, as shown in figure 28, but as space was limited went for the basic circuit.


Figure 28 : comparator + DAC ADC

The code to drive the DAC (ramp or successive approximation) and interface the ADC to the Pi is shown below. Some example conversions are shown in figure 29. For a ramp DAC voltage the conversion time is around 50ms to 150ms i.e. input voltage dependent, which is a bit of problem as the signal produced by a clap can have decayed back to 0V in that time, as shown in figure 27. For a successive approximation DAC voltage the conversion time is around 5ms, conversion time determined by the number of bits. Note, successive approximation plots show are a little slow as i did not comment out the delay, could of improved ramp speed by increasing step size.

import RPi.GPIO as GPIO
import smbus				
import time

class adc():
    def __init__( self, bus, pin, dac ):
        self.I2C_DATA_ADDR   = 0x38	
        self.I2C_ENABLE_ADDR = 0x39	
        self.DAC_EN = [0xBF, 0x7F]

        self.bus = bus
        self.dac = dac

        try:
            self.bus.write_byte( self.I2C_DATA_ADDR, 0 )    	# clear port
        except IOError:
            print("clock: I2C comms error")

        try:
            self.bus.write_byte( self.I2C_ENABLE_ADDR, 255 )    # clear port
        except IOError:
            print("clock: I2C comms error")

        self.COMP_PIN = pin
        GPIO.setwarnings(False)
        GPIO.setmode(GPIO.BCM) 
        GPIO.setup(self.COMP_PIN, GPIO.IN, pull_up_down=GPIO.PUD_UP)

    def update( self, value ):
        try:
            self.bus.write_byte( self.I2C_DATA_ADDR, value )    	  
            self.bus.write_byte( self.I2C_ENABLE_ADDR, self.DAC_EN[self.dac] )  
            #time.sleep(0.001)
            self.bus.write_byte( self.I2C_ENABLE_ADDR, 255 ) 
        except IOError:
            print("clock: I2C comms error")

    def getComp( self ):
        return GPIO.input( self.COMP_PIN )

    def ramp( self ):
        count = 0   
        self.update( 0 ) 
        for i in range(0,255):
            if self.getComp() == False:              
                count = count + 1
                self.update( i ) 
            else:
                break

        return count

    def approx( self ):
        count = 0   
        new = 0
        self.update( 0 ) 
        for i in range(0, 8):
            new = count | 2**(7-i)
	    #print i, count, new

            self.update( new )
            if self.getComp() == False:
                count = new

        return count

		
# Main program block

def main():
    print "main start"
   
    bus = smbus.SMBus(1)
    adc1 = adc( bus, 25, 1 )

    while True:    
        #value = adc1.ramp()
        value = adc1.approx()
        if value > 160:
            print value

        time.sleep( 0.001 )
    

if __name__ == '__main__':
    try:
        main()
    except KeyboardInterrupt:
        pass
    finally:
        pass


Figure 29 : ADC DAC voltage traces claps - top = ramp, middle & bottom = successive approximation

In its present form may have to drop the idea of a DAC based ADC as i really need to sample the microphone signal every 10ms, which is a lot faster than the clocks main control loop. Could run this as a separate thread, but then there is the complexity of sharing the I2C bus used to control the DACs i.e. clock meter DACs and ADC DAC. Will come back to this one later. Perhaps could sync threads, then when the clock is sleeping for 0.5 seconds.

UPDATE: 12/6/2018: ok, going to rewrite the adc code to use threading and locks. In normal operation the main clock program isn't doing that much processing, there are a few hotspots e.g. 9:59:59, where all the analogue meters will need to be updated, but most of the time its only the seconds meter. The other busy point is when the database is updated every minute with sensor values. Therefore, whilst the clock function is suspended, the microphone ADC can be sampling that signal,

PWM

This is very similar to the DAC method, however, the reference signal is generated using a filtered PWM signal. This is quite a common approach in digital systems now days as the R-2R ladder hardware can be relatively expensive to implement in silicon. I first came across this approach when using FPGAs (LINK). The ideal is to us a PWM generator i.e. a feedback shift register, then pass this "square" wave signal through a low pass filter, such that the AC elements are removed, leaving only the DC component. A nice discussion of this technique can be found here: (LINK). Therefore, the same ramp or successive approximation approaches can be used as discussed in the previous section i.e. all that is changed is how the DAC voltage is generated. Some example scope plots of the PWM signal and its filtered output are shown in figure 30. Note, to get a ripple "free" voltage need a reasonably high frequency, something around 1KHz seems to work well for the Pi, "sine" wave test code below:

import RPi.GPIO as GPIO
import time 

GPIO.setwarnings(False)
GPIO.setmode(GPIO.BCM) 
GPIO.setup(10, GPIO.OUT)

PWM = GPIO.PWM(10, 1000)
PWM.start(0) 

try:
    while True:
        for i in range(5,100,5):
            PWM.ChangeDutyCycle(i)
            time.sleep(0.1)
        for i in range(100,0,-5):
            PWM.ChangeDutyCycle(i)
            time.sleep(0.1)
except KeyboardInterrupt:
    pass
finally:
    PWM.stop()
    GPIO.cleanup()

Figure 30 : PWM hardware

PWM using GPIO support worked well, there is a little bit of ripple after the RC filter, but that is only to be expected. The reference voltage generated by this circuit does not have to change that quickly during the ADC conversion so it seemed fine, it is a little slower than the DAC i.e. to allow the capacitor voltage to change, but this approach does significantly reduce hardware.

RC

This only works for sensors that vary their resistance. To illustrate the idea a simple example is shown in figure 26. Initially the capacitor is discharged, it will then start to charge via R2, the sensing element e.g. LDR. The voltage on capacitor Vc is read by a digital input, as the voltage increases the processor repeatedly increments a counter until this voltage reaches the logic 1 threshold of the input. At this point the count is stopped. The count value is therefore proportional to the sensor's resistance e.g. for a low R the capacitor will charge quickly, so a low count, for a high R the capacitor will charge more slowly, so a larger count. To reset the converter the "switch" SW1 changes position, quickly discharging the resistor through a small fixed resistor R2 e.g. 100 ohms.


Figure 26 : simple capacitor charge/discharge

The SW in the previous circuit can be implemented in two ways: GPIO or external transistor. The simplest method is to use a single GPIO line. Initially this line is set to an output, driving a logic 0 to discharge the capacitor. To allow the capacitor to charge this line is redefined in software as an input i.e. a high impedance, allowing the processor to read the voltage on the capacitor. The advantage of this approach is that you only need one GPIO line, the down side is that you are dumping current through the processor. This current can be limited by using a series 100 ohms resistor, but this extra R slows down the discharge and therefore, increases the conversion time. At the cost of an extra GPIO line i used an external transistor as shown in figure 27. This allows the capacitor to be quickly discharged and helps protect the processor's GPIO. The test code for this circuit is shown below. Note, owing to the internal pull-up resistor the input will naturally float high, therefore, you do not need a max time out on the while loop i.e. with the LDR removed the capacitor will still eventually float high (count of roughly 500). The scope screen shots show the discharge pulse (top) and the capacitor rise time (bottom). The left image is with the desk light on, producing a count of 30 i.e. low LDR resistance so higher charging current. The right image is with the desk light off producing a count of 40 i.e. a higher LDR resistance so a lower charging current.

import RPi.GPIO as GPIO				
import time

class ldr():
    def __init__( self, pinI, pinO  ):
        self.count = 0
        self.RESET_PIN = pinO
        self.TEST_PIN = pinI

        GPIO.setwarnings(False) 	
        GPIO.setmode(GPIO.BCM) 		

        GPIO.setup(self.RESET_PIN, GPIO.OUT) 
        GPIO.output(self.RESET_PIN, False) 	

        GPIO.setup(self.TEST_PIN, GPIO.IN, pull_up_down=GPIO.PUD_UP)

    def update( self ):
        self.count = 0
        GPIO.output(self.RESET_PIN, True) 	
        time.sleep(0.001)
        GPIO.output(self.RESET_PIN, False) 

        while GPIO.input(self.TEST_PIN) == 0:
            self.count +=1	

        return self.count 

    def getCount( self ):
        return self.count

# Main program block

def main():
    print("main program")
    ldr1 = ldr( 23, 24 )
    ldr2 = ldr( 7, 24 )

    while True: 
        countA = ldr1.update()
        countB = ldr2.update()

        outputString = "Count = " + str(countA) + " " + str(countB)
        print outputString

        time.sleep(1) 		
    

if __name__ == '__main__':
    try:
        main()
    except KeyboardInterrupt:
        pass
    finally:
        GPIO.cleanup()

Figure 27 : simple LDR light meter: left - light on, right light off

To improve the accuracy of this sensor a constant current source can be added to this circuit i.e. to give a wider range of count values. The capacitor's voltage charging characteristic is now changed from an exponential rise to a linear ramp, allowing a more accurate conversion for small changes in the values of R. There are a few different circuits to implement this constant current source, my implementation shown in figure 28 is based on this circuit: (LINK). Sort of works, the capacitor voltage is definitely more linear. The left image is with the desk light on, again producing a count of 30. The right image is with the desk light off, now producing a count of 70 i.e. a count difference of 40 rather than the previous 10. Note, uses exactly the same test code.


Figure 28 : improved LDR light meter: left - light on, right light off

Threads and Locks

ADC testing went well, not the fastest ADCs in the world i.e. depending on delay used, successive approximation was around 5ms, but they all worked. This then led to the question how will these sensors be used? One of the original aims for the microphone was to implement a clapper e.g. clap twice to turn on a fan etc. However, this requires the Pi to continuously sample the microphone signal to detect peaks/spikes made by the clap (these last 100ms-ish). A obvious solution was to use threads, but, as both the display meter updates and the ADC use the same DAC interface we have a bit of a structural hazard i.e. during an ADC conversion want to make sure that this code is not interrupted, therefore, implemented access synchronisation via locks (semaphores with a count of 1). To be honest not sure how threading is implemented on a quad core Pi 2, i assume the python code will share the same CPU core, rather than being distributed? Therefore, if the main clock loop is updated every 0.5 seconds, there is a good 500ms of ADC sampling time, which should work ok e.g. clap (100ms) - quiet (100ms) - clap (100ms). Also the clock update element will not take the other full 500ms. The threaded version of the ADC code is shown below. The main modification is that now the main function run continually samples the microphone signal, recording: amplitude, time (time taken) and status (boolean, peak detected). This tuple is stored in an array of 20 samples, implementing a sliding window i.e. records the last 20 samples, each time a new reading is taken the oldest is removed and the new reading is added to the end. The code then scans through this array looking for peaks, these are defined as values above a minimal level and where samples either side of a sample are lower than the current value i.e. the previous and next values (variable peakWindow). If these two attributes are true the status of that sample is set to true i.e. peak detected. Double peaks i.e. two claps, are defined as peaks being separated by more than 150ms, but less than 500ms, if this is detected the variable doublePeakDetected is set to True. The code also records the number of peaks and the max/min/average sample values for that window. All of these attributes can be access by the main program using the getSample function. This function is executed every minute.

import threading

import RPi.GPIO as GPIO
import smbus

import time
import datetime
ool
class adcThreaded( threading.Thread ):
    def __init__( self, bus, pin1, pin2, pin3, dac, lock ):
        super(adcThreaded, self).__init__()
        self.stopReq = threading.Event()
        self.lock = lock

        self.COMP_PIN = pin1
        self.PIR_PIN = pin2
        self.RADAR_PIN = pin3
		
        self.I2C_DATA_ADDR   = 0x38	
        self.I2C_ENABLE_ADDR = 0x39	
        self.DAC_EN = [0xBF, 0x7F]

        self.bus = bus
        self.dac = dac

        self.SAMPLE_SIZE = 20
        self.CLAP_THRESHOLD = 170

        self.sampleWindow = []
        self.sampleCount = 0
        self.sampleNumberOfPeaks = 0
        self.sampleAverageLevel = 0
        self.sampleMaxLevel = 0
        self.sampleMinLevel = 999

        self.doublePeakDetected = False
        self.peakDetected = False
        self.peakWindow = [0,0,0]

        self.pirSensor = False
        self.radarSensor = False

        for i in range(0, self.SAMPLE_SIZE):
            self.sampleWindow.append( [0, 0.0, False] )

        try:
            self.bus.write_byte( self.I2C_DATA_ADDR, 0 )    	# clear port
        except IOError:
            print("clock: I2C comms error")

        try:
            self.bus.write_byte( self.I2C_ENABLE_ADDR, 255 )    # clear port
        except IOError:
            print("clock: I2C comms error")

        GPIO.setwarnings(False)
        GPIO.setmode(GPIO.BCM) 
        GPIO.setup(self.COMP_PIN, GPIO.IN, pull_up_down=GPIO.PUD_UP)
        GPIO.setup(self.PIR_PIN, GPIO.IN, pull_up_down=GPIO.PUD_UP)
        GPIO.setup(self.RADAR_PIN, GPIO.IN, pull_up_down=GPIO.PUD_UP)

    def update( self, value ):       
        try:
            self.bus.write_byte( self.I2C_DATA_ADDR, value )    	  
            self.bus.write_byte( self.I2C_ENABLE_ADDR, self.DAC_EN[self.dac] )  
            time.sleep(0.001)
            self.bus.write_byte( self.I2C_ENABLE_ADDR, 255 ) 
        except IOError:
            print("clock: I2C comms error")

    def getComp( self ):
        return GPIO.input( self.COMP_PIN )

    def getPir( self ):
        return GPIO.input( self.PIR_PIN )

    def getRadar( self ):
        return GPIO.input( self.RADAR_PIN )

    def ramp( self ):
        self.lock.acquire()
        count = 0   
        self.update( 0 ) 
        for i in range(0,255):
            if self.getComp() == False:              
                count = count + 1
                self.update( i ) 
            else:
                break
        self.lock.release()
        return count

    def approx( self ):
        self.lock.acquire()
        count = 0   
        new = 0
        self.update( 0 ) 
        for i in range(0, 8):
            new = count | 2**(7-i)
            self.update( new )
            if self.getComp() == False:
                count = new
        self.lock.release()
        return count

    def run ( self ):
        while not self.stopReq.isSet():

            data = [self.approx(), time.time(), False]

            if self.sampleCount <= self.SAMPLE_SIZE:
                self.sampleWindow = self.sampleWindow[1:] + [data]

                newDoublePeakDetected = False
                newPeakDetected = False
                newNumberOfPeaks = 0
                newAverageLevel = 0
                newMaxLevel = 0
                newMinLevel = 999
 
                for count in range(0, self.SAMPLE_SIZE): 
                    value = self.sampleWindow[count][0] 
                    
                    newAverageLevel += value

                    if value > newMaxLevel:
                        newMaxLevel = value 
                    if value < newMinLevel:
                        newMinLevel = value          

                    self.peakWindow = self.peakWindow[1:] + [value] 

                    if count >= 2:
                        if(self.peakWindow[1] > self.peakWindow[0]) and (self.peakWindow[1] > self.peakWindow[2]) and (self.peakWindow[1] > self.CLAP_THRESHOLD):
                            if self.sampleWindow[count][2] == False:
                                newNumberOfPeaks += 1
                                newPeakDetected = True 
                                self.sampleWindow[count][2] = True

                peakDetected = False
                prevTime = 0.0

                for count in range(0, self.SAMPLE_SIZE): 
                    if self.sampleWindow[count][2]:
                        if peakDetected:
                            if ((self.sampleWindow[count][1] - prevTime) > 0.15) and ((self.sampleWindow[count][1] - prevTime) < 0.5):
                                newDoublePeakDetected = True
                                prevTime = self.sampleWindow[count][1]
                        else:
                            peakDetected = True
                            prevTime = self.sampleWindow[count][1]

            else:
                self.sampleWindow = self.sampleWindow[1:] + [data]
                self.sampleCount += 1

            self.doublePeakDetected = self.doublePeakDetected or newDoublePeakDetected
            self.peakDetected = self.peakDetected or newPeakDetected 
            self.sampleNumberOfPeaks = self.sampleNumberOfPeaks + newNumberOfPeaks

            self.sampleAverageLevel = ((newAverageLevel/self.SAMPLE_SIZE) + self.sampleAverageLevel)/2

            if newMaxLevel > self.sampleMaxLevel:
                self.sampleMaxLevel = newMaxLevel

            if newMinLevel < self.sampleMinLevel:
                self.sampleMinLevel = newMinLevel

            self.pirSensor = self.pirSensor or self.getPir()
            self.radarSensor = self.radarSensor or self.getRadar() 

            #data = [self.doublePeakDetected, self.peakDetected, self.sampleNumberOfPeaks, self.sampleAverageLevel, self.sampleMaxLevel, self.sampleMinLevel]
            #print data
            time.sleep(0.1)

    def getSample( self ):
        data = [self.doublePeakDetected, self.peakDetected, self.sampleNumberOfPeaks, \
                self.sampleAverageLevel, self.sampleMaxLevel, self.sampleMinLevel, \
                self.pirSensor, self.radarSensor]

        self.doublePeakDetected = False
        self.peakDetected = False
        self.sampleNumberOfPeaks = 0
        self.sampleAverageLevel = 0
        self.sampleMaxLevel = 0
        self.sampleMinLevel = 999
        self.pirSensor = False
        self.radarSensor = False 
        return data
 
    def stop( self ):
        self.stopReq.set()
        time.sleep(0.5)

def main():

    bus = smbus.SMBus(1)	 
    lock = threading.Lock()
    adc1 = adcThreaded( bus, 25, 27, 22, 1, lock )
    adc1.start()

    while True:
        try:
            value = adc1.getSample()
            print value
            time.sleep(3)
        except KeyboardInterrupt:
            adc1.stop()  
            adc1.join()
        finally:
            pass 

if __name__ == "__main__":
    main()

To ensure that the context switch only occurs after an ADC conversion has completed a lock is defined in the main program and passed to the threaded ADC code when it is initialised, as shown below. Also contained in the ADC code above are the methods used to access the PIR and radar sensors described in the next section. Main reason for doing this was that this data was needed at the same time as the getSample function was called so it placed here, but, not really the OO way.

#ADC 

def approx( self ):
    self.lock.acquire()
    count = 0   
    new = 0
    self.update( 0 ) 
    for i in range(0, 8):
        new = count | 2**(7-i)
        self.update( new )
        if self.getComp() == False:
            count = new
    self.lock.release()
    return count

#MAIN CLOCK LOOP

lock = threading.Lock()
sensors = adcThreaded.adcThreaded( bus, 25, 27, 22, 1, lock )
sensors.start()

while True:
    try:
        newTime = analogueDisplay.getTime()

        lock.acquire()
 
        [MAIN PROGRAM CODE]
                  
        lock.release()
        time.sleep(0.5)

    except KeyboardInterrupt:
        dht11Sensor.stop()  
        sensors.stop()
        dht11Sensor.join()
        sensors.join()
    finally:
        pass 

PIR and Radar

After adding the ADC sensors started to think about if there were any other sensors i could add to the system. Looking through the scap box i had two simple digital proximity sensors: PIR and radar. The passive infra-red (PIR) sensor is a HC-SR501 (LINK)(LINK), as shown in figure 29. A little tricky to setup, but, easy to interface, a simple digital input: 0V=nothing detected, 3.3V=person detected. This input is sampled approximately every 0.1ms - 0.2ms, in the main ADC loop, as discussed in the previous section. Each sample is logically ORed with the variable self.pirSensor, therefore, the main program will read a True if a logic 1 was sampled any time during the 1 minute update rate. This could be improved later to remove noise e.g. false positives, caused by transient spikes/glitches e.g. a counter, minimum pulse length, number of pulses etc.


Figure 29 : PIR sensor

The radar sensor is a RCWL-0516 (LINK)(LINK), as shown in figure 30. There are excellent little sensors able to detect movement over quite a reasonable range with surprising accuracy. Again, these give a digital output, sampled as PIR sensor, updating the variable self.radarSensor, accessed using the getSample function

Figure 30 : radar sensor

The intention behind adding these sensors was to give the clock a burglar alarm function, maybe add a camera, such that when someone is detected in the room the clock will take a series of pictures and upload these to a Google drive.

Data logger

Finally got to the data logger part of the clock, to start with installed SQLite and its graphical front end:

sudo apt-get install sqlite3 
sudo apt-get install sqlitebrowser

Within the main clock code the aim is to write sensor data to the database, recording their values every minute. This is perhaps a slightly overally complex solution, perhaps a simple .csv file based solution would of been better, but it was a chance to assess the performance of SQLite on a Raspberry Pi. To create the database you can use the command terminal.

sqlite3 sensorDatabase.db

This will open a sqlite command shell, to create a new database table within the file sensorDatabase.db enter the following:

BEGIN;
CREATE TABLE sensorData(id INTEGER PRIMARY KEY AUTOINCREMENT, 
                        dht11Temp REAL, dht11Hum REAL, 
                        mcp9801Temp REAL, 
                        ldr1Sensor REAL, ldr2Sensor REAL, 
                        micDoublePeakDetected INTEGER, micPeakDetected INTEGER, micNumberOfPeaks INTEGER, 
                        micAverageLevel INTEGER, micMaxLevel INTEGER, micMinLevel INTEGER, 
                        pirSensor, radarSensor, 
                        date DATE, time TIME);
COMMIT;

Within this shell commands start with a '.' e.g. .help or .quit. Structured Query Language (SQL) is used to interact with the database e.g. create tables, insert, delete and search for data etc. SQL statements are normally written in capital letters (not required, but help readability) and must end with a semicolon ";". To automate this process this can be done in python, very useful when you need to reset the database, as shown below:

import sqlite3 as sql

def main():

    db = sql.connect("sensorDatabase.db")

    cursor = db.cursor()
    cursor.execute("DROP TABLE IF EXISTS sensorData")
    cursor.execute("CREATE TABLE sensorData( id INTEGER PRIMARY KEY AUTOINCREMENT, \
                                             dht11Temp REAL, dht11Hum REAL, \
                                             mcp9801Temp REAL, \
                                             ldr1Sensor REAL, ldr2Sensor REAL, \
                                             micDoublePeakDetected INTEGER, micPeakDetected INTEGER, micNumberOfPeaks INTEGER, \
                                             micAverageLevel INTEGER, micMaxLevel INTEGER, micMinLevel INTEGER, \
                                             pirSensor, radarSensor, \
                                             date DATE, time TIME)" )
    db.commit()

if __name__ == "__main__":
    main()

Sensor data is written to the database every minute from the main clock loop, as shown below, simply define a "cursor", then execute the SQL command and commit. Boolean sensor values e.g. peak and PIR etc, are converted to integers to simplify plotting later.

db = sql.connect("/home/pi/NewClock/sensorDatabase.db")

if newTime[2] != oldTime[2]:
    oldTime[2] = newTime[2]
    analogueDisplay.updateDigit(2, newTime[2])
    ledDisplay.update(0, newTime[2])

    if (((newTime[2] == 5) and (newTime[3] == 1)) or ((newTime[2] == 5) and (newTime[3] == 4)) or ((newTime[2] == 0) and (newTime[3] == 3))):
        os.system("/home/pi/NewClock/chime.sh")

    mcp9801Temp = mcp9801Sensor.getTemp()
    dht11Temp = dht11Sensor.getTemp()
    dht11Hum = dht11Sensor.getHum()
                        
    ldr1Level = ldr1.update()
    ldr2Level = ldr2.update()

    sensorData = sensors.getSample()
    micDoublePeakDetected = 0
    micPeakDetected = 0
    pirDetected = 0
    radarDetected = 0

    if ( sensorData[0] ):
        micDoublePeakDetected = 198 
                  
    if ( sensorData[1] ):
        micPeakDetected = 98 

    if ( sensorData[7] ):
        pirDetected = 198
                  
    if ( sensorData[6] ):
        radarDetected = 98 

    readingData = str(dt.datetime.now()).split()
    readingDate = readingData[0]
    readingTime = readingData[1] 

    cursor = db.cursor()
    cursor.execute("INSERT INTO sensorData( dht11Temp, dht11Hum, mcp9801Temp, \
                                ldr1Sensor, ldr2Sensor, \
                                micDoublePeakDetected, micPeakDetected, micNumberOfPeaks, micAverageLevel, micMaxLevel, micMinLevel, \
                                pirSensor, radarSensor, \
                                date, time) VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)", 
                               (dht11Temp, dht11Hum, mcp9801Temp, ldr1Level, ldr2Level, \
                                micDoublePeakDetected, micPeakDetected, sensorData[2], sensorData[3], sensorData[4], sensorData[5], \
                                pirDetected, radarDetected, \
                                readingDate, readingTime))
    db.commit()

Having used the database a little now it may not be the smallest solution, but it is very convenient and flexible e.g. quickly recalling sensor values for a particular day or time. Speed wise not sure, have not noticed any significant delays, also the database file itself isn't excessively large. The data stored in the database is used to produce a series of graphs which will be tweeted at the end of each day i.e. midnight. There are a lot of different ways of doing this, but went for the tried and tested. To install the required software:

sudo apt-get install python-tweepy
sudo apt-get install gnuplot

I used gnuplot in the sunflower project, kept with it as i find it easy to use, but there are also a range of python libraries for plotting graphs. I believe there is also a python wrapper for gnuplot, but, i went for a "generate config file" approach, this allowed me to regenerate graphs and play around with layout etc at the command line. To extract the "days" data from the database you use the SELECT command, this returns an array of data, one entry for each minute during that day, with each entry containing the 16 sensor data elements. These are then saved to a text file as a .csv, recording the start and end times. The remaining code then writes out the required gnuplot commands to the file plot.gnu which will be used later to generate the three plots. This code is launched as a seperate process from the main clock program, therefore, if anything goes wrong the clock does not stop. The first half of the program upload.py is shown below:

import tweepy
import json

import sqlite3 as sql
import datetime as dt

import os

UPLOAD = True 

db = sql.connect("/home/pi/NewClock/sensorDatabase.db")
cursor = db.cursor()

readingData = str(dt.datetime.now()).split()
readingDate = readingData[0]

searchString = "SELECT * FROM sensorData WHERE date='" + readingDate + "'"  
searchData = cursor.execute( searchString )

dataOutputFile = open("sensorData.csv", "w")
plotOutputFile = open("plot.gnu", "w")

firstTime = 0
lastTime = 0
        
for row in searchData:
    if firstTime == 0:
        firstTime = row[15]
    lastTime = row[15]

    for data in row:
        dataOutputFile.write( str(data) )
        dataOutputFile.write( "," ) 	
    dataOutputFile.write( "\n" ) 

dataOutputFile.close()		

plotOutputFile.write("set term png size 1024, 512\n")
plotOutputFile.write("set key horiz\n")
plotOutputFile.write("set datafile separator \",\"\n")

plotOutputFile.write("set xdata time\n")
plotOutputFile.write("set timefmt \"%H:%M:%S\"\n")

timeString = "set xrange [\"" + str(firstTime) + "\":\"" + str(lastTime) +"\"]\n" 
plotOutputFile.write(timeString)
plotOutputFile.write("set xtics format \"%H:%M\"\n")

plotOutputFile.write("set xlabel \"Time\"\n")

fileName1 = "set output 'plot1-" + readingDate + ".jpg'\n" 
plotOutputFile.write(fileName1)
plotOutputFile.write("set multiplot layout 2,1\n")

plotOutputFile.write("plot 'sensorData.csv' u 16:2 t 'DHT-Temp' w line, 'sensorData.csv' u 16:4 t 'MCP_Temp' w line\n")
plotOutputFile.write("plot 'sensorData.csv' u 16:3 t 'DHT-Humidity' w line\n")
plotOutputFile.write("unset multiplot\n")

fileName2 = "set output 'plot2-" + readingDate + ".jpg'\n" 
plotOutputFile.write(fileName2)
plotOutputFile.write("set multiplot layout 2,1\n")

plotOutputFile.write("plot 'sensorData.csv' u 16:7 t 'DoublePeak' w line, 'sensorData.csv' u 16:8 t 'SinglePeak' w line, 'sensorData.csv' u 16:9 t 'Number' w line\n")
plotOutputFile.write("plot 'sensorData.csv' u 16:10 t 'Average' w line, 'sensorData.csv' u 16:11 t 'Max' w line, 'sensorData.csv' u 16:12 t 'Min' w line\n")
plotOutputFile.write("unset multiplot\n")

fileName3 = "set output 'plot3-" + readingDate + ".jpg'\n" 
plotOutputFile.write(fileName3)
plotOutputFile.write("set multiplot layout 2,1\n")
plotOutputFile.write("plot 'sensorData.csv' u 16:5 t 'LDR1' w line, 'sensorData.csv' u 16:6 t 'LDR2' w line\n")
plotOutputFile.write("plot 'sensorData.csv' u 16:13 t 'PIR' w line, 'sensorData.csv' u 16:14 t 'Radar' w line\n")
plotOutputFile.write("unset multiplot\n")
    
plotOutputFile.close()

os.system("/home/pi/NewClock/plot.sh")

The script plot.sh used to generate the images, this code and example plots are shown below. Note, file names contain the full date so will be unique.

#!/bin/sh
gnuplot /home/pi/NewClock/plot.gnu 1>/home/pi/NewClock/plot.log 2>/home/pi/NewClock/plot.err


Figure 31 : sensor plots

To upload these to twitter i follwed the article in MagPi70: BUILD A TWEETING BABBAGE(LINK)(LINK). Worked out of the box, no real problems, the only additional element was that you do need to register a phone to create the app. The code to upload the images is below, the second half of the program upload.py, follows on from the above code. First it call the script connected.sh to see if it can upload, then uploads the three images. This python code is called via a crontab entry, configured to run this program at 23.58 each day.

os.system("/home/pi/NewClock/connected.sh")
connectionStatusFile = open("/home/pi/NewClock/status", "r")
connectionStatus = connectionStatusFile.read(1)

if connectionStatus == "0":
    outputString = "not connected : " + str(dt.datetime.now())
    print outputString
else:
    #print "connected"
   
    if UPLOAD: 
        with open('/home/pi/NewClock/twitter_auth.json') as file:
            secrets = json.load(file)

        auth = tweepy.OAuthHandler(secrets['consumer_key'], secrets['consumer_secret'])
        auth.set_access_token(secrets['access_token'], secrets['access_token_secret'])
        twitter = tweepy.API(auth)

        fileName1 = "/home/pi/NewClock/plot1-" + readingDate + ".jpg"
        fileName2 = "/home/pi/NewClock/plot2-" + readingDate + ".jpg"
        fileName3 = "/home/pi/NewClock/plot3-" + readingDate + ".jpg"      

        twitter.update_with_media( fileName1, "DHT sensor")
        twitter.update_with_media( fileName2, "Microphone sensor")
        twitter.update_with_media( fileName3, "Light sensor")

connected.sh script pings Google to see if the Pi has an internet connection, returning the result in the file status.

#!/bin/sh

IPADDR="8.8.8.8"
x=`ping -q -w 1 -c 1 $IPADDR | grep loss | cut -d ' ' -f 6`

if test $x = "0%"
then
  #echo connected
  echo 1 > status
else
  #echo not connected
  echo 0 > status
fi

To access this twitter account : (LINK)

Figure 32 : twitter

Google drive & Camera

Final element, thought i would add a camera, take some timelapse or event triggered photos from my window, then upload these to the clock's Google drive. Apparently there is a family of stoats living in the garden, but confess i have not seen them. Used PyDrive in the past and i thought i remembered it being simple to use :). Hmmmm, this one took some time to figure out. Install instructions are here: (LINK). Initially i was running Raspbian Jessie, but there were a couple of out of date packages, messed around for a bit, following the pip error message trial, got it to work in the end, but, later upgraded to Raspbian Stretch. Had to update the google-auth package, but basically followed the instructions online (sorry lost link). Setup instructions for PyDrive are here: (LINK). You need a Google account so setup a Gmail account. Then need to enable the Google Drive API, this is all done through the Google API web interface (LINK). Followed the setup instructions, got lost many a time, but in the end got the required .json file. Note, be careful not to miss the last "/" on the "Authorized redirect URIs", that one caused me to waste a couple of hours. Initially authorisation is done through a web brower on the Pi, but you can create a .yaml file with the client_id and client_secret to automate this process, basic cut and paste from here: (LINK).

Again, implemented the Google image upload as a separate process, called from the clock code using os.system(), such that if thing went wrong the clock would not stop. This code checks for internet connection, then does the authentication process using the .yaml file. It then checks to see if there is a directory whos name is today's date, if there is not one is created, this is the folder images will be uploaded to. Note, on Google drive each folder and file is a "file", identified by an id number, rather than a file name. Also i found the update rate of Google drive very slow when you have a lot of images in one folder e.g. 100+, splitting the images into different folders helped a lot when viewing, deleting etc. The python code used is:

from pydrive.auth import GoogleAuth
from pydrive.drive import GoogleDrive

import os
import time
import datetime as dt

def main():  
    os.system("/home/pi/NewClock/connected.sh")
    connectionStatusFile = open("/home/pi/NewClock/status", "r")
    connectionStatus = connectionStatusFile.read(1)

    if connectionStatus == "0":
        outputString = "not connected : " + str(dt.datetime.now())
        print outputString
    else:
  
        gauth = GoogleAuth( settings_file="settings.yaml")
        drive = GoogleDrive(gauth)

        #file_list = drive.ListFile({'q': "'root' in parents and trashed=false"}).GetList()
        #for file1 in file_list:
        #    print 'title: %s, id: %s' % (file1['title'], file1['id'])

        folderDate = str(dt.datetime.now()).split(' ')
        folderName = folderDate[0]
        
        folderID = 0    
        fileList = drive.ListFile({'q': "mimeType contains 'application/vnd.google-apps.folder' and trashed=false"}).GetList()
        for fileID in fileList:
            #print 'title: %s, id: %s' % (fileID['title'], fileID['id'])   
            if fileID['title'] == folderName:
                #print("folder name exists")
                folderID = fileID
    
        if folderID == 0:
            folderMetaData = {'title': folderName, 'mimeType' : 'application/vnd.google-apps.folder'}
            folderID = drive.CreateFile( folderMetaData )
            folderID.Upload()
    
        for i in range(0,5):
            imageTime = str(dt.datetime.now()).replace(' ', '_').replace(':', '-').split('.')
            imageName = "image_" + imageTime[0] + ".jpg"
    
            commandString = "./captureImage.sh " + imageName
            os.system( commandString )
    
            imageFile = drive.CreateFile({'title': imageName,'parents':[{u'id': folderID['id']}]})
            imageFile.SetContentFile(imageName)
            imageFile.Upload()
        
            time.sleep(2)
    
        os.system("rm ./image*.jpg")

if __name__ == "__main__":
    main()

To capture images from the USB web-camera i installed fswebcam:

sudo apt-get install fswebcam

This is then called using the script captureImage.sh as shown below. Note, the -S skips the first 10 frames found that helped a little with exposure / brightness issues. To find out what image format the web-camera supports run v4l2-ctl --list-formats-ext. Note, its v4"L". The above python code uses this script to take five pictures, each image has the full date and time so are unique file names.

#!/bin/sh

if test $# -eq 0
then
  fswebcam -r 1280x720 -S 10 --no-banner image.jpg
else
  fswebcam -r 1280x720 -S 10 --no-banner $1
fi
Creative Commons Licence

This work is licensed under a Creative Commons Attribution-NonCommercial-NoDerivatives 4.0 International License.

Contact details: email - mike.freeman@york.ac.uk, telephone - 01904 32(5473)

Back