Showing posts with label lcd. Show all posts
Showing posts with label lcd. Show all posts

2014-06-01

Raspberry Pi Tweet Controlled RGB LCD

I've written another article over on Tuts.

The article shows how to use a Raspberry Pi with an RGB LCD to monitor tweets. Tweets containing specific keywords are displayed in defined colours. It explains how to create a Twitter application to use the stream API to push data to a multi-threaded Python program.


A second follow-up article to "How to Build a Tweet Controlled RGB LCD" is planned. This will add a web interface to the program so you can change what you're following and the colour mappings.

2013-09-07

Build a Raspberry Pi Moisture Sensor to Monitor Your Plants

*** NOTE: ControlMyPi shutting down ***

Here's a snippet from a detailed tutorial I've written for Tuts+

 ....You will be able to monitor the sensor locally on the LCD or remotely, via ControlMyPi.com, and receive daily emails if the moisture drops below a specified level.

Along the way I will:

  • wire up and read a value from an analog sensor over SPI using a breadboard
  • format the sensor reading nicely in the console
  • display the sensor reading on an RGB LCD display
  • have the Raspberry Pi send an email with the sensor reading
  • easily monitor the sensor and some historic readings on the web

Read the whole tutorial here: Build a Raspberry Pi Moisture Sensor to Monitor Your Plants


2013-03-21

Raspberry Pi parking camera with distance sensor

This build brings together a few other projects to make something potentially quite useful for a change - a parking camera with distance sensor. The feed from the webcam is shown on the LCD with a distance read-out underneath. As you get closer to the object (my hand in the videos) a circle is overlaid on the video which gets larger as you move closer. Once you get to within 30cm of the object the word "STOP" is overlaid and everything turns red.

Here it is in action:

And a close-up of the screen:


The project is made up of the following:

Camera: Microsoft Lifecam Cinema - I've used this with lots of Raspberry Pi projects - it works nicely and has a good microphone too.

Distance sensorSharp GP2Y0A02YK0F - my article Raspberry Pi distance measuring sensor with LCD output explains how to put this together.

I'm also using an Adafruit Pi Cobbler to breakout the header onto the breadboard.




For the software I'm using Pygame. Thankfully the camera supports a 176x144 resolution and since my screen is 176x220 this fits perfectly. So, after some initializing there is a main loop which simply: blits the image from the camera, reads from the distance sensor, draws the circle and text. Finally update() is called to send this to the framebuffer.

import pygame
import pygame.camera
import os
import mcp3008

BLACK = 0,0,0
GREEN = 0,255,0
RED = 255,0,0

if not os.getenv('SDL_FBDEV'):
    os.putenv('SDL_FBDEV', '/dev/fb1')

if not os.getenv('SDL_VIDEODRIVER'):
    os.putenv('SDL_VIDEODRIVER', 'fbcon')

pygame.init()
lcd = pygame.display.set_mode((176, 220))
pygame.mouse.set_visible(False)
lcd.fill(BLACK)
pygame.display.update()

pygame.camera.init()
 
size = (176,144)
cam = pygame.camera.Camera('/dev/video0', size, 'RGB')

cam.start()

font_big = pygame.font.Font(None, 50)
surf = pygame.Surface(size)
while True:
    lcd.fill(BLACK)
    cam.get_image(surf)
    lcd.blit(surf, (0,0))

    cm = mcp3008.read_2Y0A02_sensor(7)
    colour = GREEN
    if cm < 30:
        colour = RED
        text_surface = font_big.render('STOP', True, colour)
        rect = text_surface.get_rect(center=(88,72))
        lcd.blit(text_surface, rect)

    if cm < 140:
        pygame.draw.circle(lcd, colour, (88,72), (150-cm)/2, 3)
    
    text_surface = font_big.render('%dcm'%cm, True, colour)
    rect = text_surface.get_rect(center=(88,180))
    lcd.blit(text_surface, rect)

    pygame.display.update()


Finally here's the mcp3008 module which is imported above. NOTE: Since the LCD is using SPI 0.0 I have used SPI 0.1 for the mcp3008. You'll see this in the code below:

import spidev

spi = spidev.SpiDev()
spi.open(0,1)

# read SPI data from MCP3008 chip, 8 possible adc's (0 thru 7)
def readadc(adcnum):
    if ((adcnum > 7) or (adcnum < 0)):
        return -1
    r = spi.xfer2([1,(8+adcnum)<<4,0])
    adcout = ((r[1]&3) << 8) + r[2]
    return adcout

def read_3v3(adcnum):
    r = readadc(adcnum)
    v = (r/1023.0)*3.3
    return v

def read_2Y0A02_sensor(adcnum):
    r = []
    for i in range (0,10):
        r.append(readadc(adcnum))
    a = sum(r)/10.0
    v = (a/1023.0)*3.3
    d = 16.2537 * v**4 - 129.893 * v**3 + 382.268 * v**2 - 512.611 * v + 306.439
    cm = int(round(d))
    return cm


2013-03-17

Raspberry Pi playing video on 2.2" LCD.

I used the software and followed the guide here: https://github.com/notro/fbtft - Then I resized and rotated a video from my blog to fit the display.




The original video was 1920x1080 and this screen is 176x220 so I rotated it anti-clockwise and resized it to 220x124 to keep the aspect ratio the same but make best use of the screen. The "transpose=2" does the anti-clockwise rotation.

# First install ffmpeg if you haven't already
sudo apt-get -y install ffmpeg

# Now convert ("experimental" was flagged as required for this h264 movie - try without first)
avconv -i Jeremy.mp4 -s 220:124 -vf transpose=2 -strict experimental j.mp4


Then to play it, make sure to pick the correct framebuffer device - mine is /dev/fb1:
mplayer -vo fbdev2:/dev/fb1 j.mp4

2013-02-24

Live Web Bicycle Dashboard - the code

*** NOTE: ControlMyPi shutting down ***

This post is a walkthrough of the code running on the Raspberry Pi as seen in the previous post: Live Web Bicycle Dashboard using ControlMyPi. This file, and the required mcp3008.py, are available in the examples from ControlMyPi. See "How to connect your pi".

Firstly at the top of the file are a few constants to use with ControlMyPi. The PANEL_FORM defines what ControlMyPi will render on the web site. Each update-able widget has a name so we can push changes up to ControlMyPi as new data is read from the attached devices. For example the 'P','streetview' widget defines a Picture widget. Whenever we want to display a new streetview image we can push the URL up to ControlMyPi and it in turn will push this change out to any browser currently viewing the page.

The last line of the panel defines two buttons and a status text widget. These are used to start and stop recording the telemetry to a file. More about this at the end of this post.

For information about all the available widgets in ControlMyPi go to here: ControlMyPi docs

The last few constants in this section define the URLs used for Google Maps Image APIs. %s substitutions are defined in these strings for us to apply longitude, latitude and heading later on. Also an API_KEY constant is defined here. You can comment this out initially to test but you will need to get a key eventually as the quota for image fetches is quite low without a key. With a key you get 25000 per day for free.

'''
Created on 6 Nov 2012

Bicycle telemetry recorder with Live web dashboard through ControlMyPi.com

See: http://jeremyblythe.blogspot.com
     http://www.controlmypi.com
     Follow me on Twitter for updates: @jerbly
     
@author: Jeremy Blythe
'''

import serial
import subprocess
import mcp3008
import time
from controlmypi import ControlMyPi

JABBER_ID = 'you@your.jabber.host'
JABBER_PASSWORD = 'yourpassword'
SHORT_ID = 'bicycle'
FRIENDLY_NAME = 'Bicycle telemetry system'
PANEL_FORM = [
             [ ['S','locked',''] ],
             [ ['O'] ],
             [ ['P','streetview',''],['P','map',''] ],
             [ ['C'] ],
             [ ['O'] ],
             [ ['L','Speed'],['G','speed','mph',0,0,50], ['L','Height'],['S','height',''] ],
             [ ['C'] ],
             [ ['L','Accelerations'] ],
             [ ['G','accx','X',0,-3,3], ['G','accy','Y',0,-3,3], ['G','accz','Z',1,-3,3] ],
             [ ['L','Trace file'],['B','start_button','Start'],['B','stop_button','Stop'],['S','recording_state','-'] ]
             ]

API_KEY = '&key=YOUR_API_KEY'
STREET_VIEW_URL = 'http://maps.googleapis.com/maps/api/streetview?size=360x300&location=%s,%s&fov=60&heading=%s&pitch=0&sensor=true'+API_KEY
MAP_URL = 'http://maps.googleapis.com/maps/api/staticmap?center=%s,%s&zoom=15&size=360x300&sensor=true&markers=%s,%s'+API_KEY

The GPS class.

The constructor goes through a process of setting the GPS unit into 38400 baud and 10Hz update mode. During testing I noticed that if you don't increase the baud rate to 38400 (up from 9600) then the unit won't go into 10Hz mode. Presumably this is because there's too much data to get out per second at 9600 baud so it's incompatible. Finally I set the unit to only produce RMC and GGA messages - everything but the height is available in the RMC message.

Every time the read method is called on this class a single line is read from the GPS unit. If an RMC or GGA message is found then the info is decoded and the member variables are updated. As you'll see later the design of this whole application hinges around the GPS. The program basically runs as fast as the GPS produces output, since the unit is in 10Hz mode we collect all the data for an update and then there's a short delay until the next set of data. All this happens 10 times per second. During the "data gaps" the serial port 0.01 second time out comes in to play to stop the main loop from freezing allowing us to do things like read key presses from the TextStar display.

class GPS:
    def __init__(self):
        self.height = '0'
        self.time_stamp = ''
        self.active = False
        self.lat = None
        self.lat_dir = None
        self.lon = None
        self.lon_dir = None
        self.speed = None
        self.heading = None
        self.date = ''
        # Connect to GPS at default 9600 baud
        self.ser = serial.Serial('/dev/ttyAMA0',9600,timeout=0.01)
        # Switch GPS to faster baud
        self.send_and_get_ack('251',',38400')
        # Assume success - close and re-open serial port at new speed
        self.ser.close()
        self.ser = serial.Serial('/dev/ttyAMA0',38400,timeout=0.01)
        # Set GPS into RMC and GGA only mode
        self.send_and_get_ack('314',',0,1,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0')
        # Set GPS into 10Hz mode
        self.send_and_get_ack('220',',100')

    def checksum(self,cmd):
        calc_cksum = 0
        for s in cmd:
            calc_cksum ^= ord(s)
        return '$'+cmd+'*'+hex(calc_cksum)[2:]

    def send_and_get_ack(self,cmdno,cmdstr):
        '''Send the cmd and wait for the ack'''
        #$PMTK001,604,3*32
        #PMTK001,Cmd,Flag 
        #Cmd: The command / packet type the acknowledge responds. 
        #Flag: .0. = Invalid command / packet. 
        #.1. = Unsupported command / packet type 
        #.2. = Valid command / packet, but action failed 
        #.3. = Valid command / packet, and action succeeded 
        cmd = 'PMTK%s%s' % (cmdno,cmdstr)
        msg = self.checksum(cmd)+chr(13)+chr(10)
        #print '>>>%s' % cmd
        self.ser.write(msg)
        ack = False
        timeout = 300
        while (not ack) and timeout > 0:
            line = str(self.ser.readline())
            if line.startswith('$PMTK001'):
                tokens = line.split(',')
                ack = tokens[2][0] == '3'
                #print '<<<%s success=%s' % (line,ack)
            timeout -= 1
        return ack

    def read(self):
        '''Read the GPS'''
        line = str(self.ser.readline())
        #print line
    
        if line.startswith('$GPGGA'):
            # $GPGGA,210612.300,5128.5791,N,00058.5165,W,1,8,1.18,41.9,M,47.3,M,,*79
            # 9 = Height in metres
            tokens = line.split(',')
            if len(tokens) < 15:
                return
            try:
                self.height = tokens[9]
            except ValueError as e:
                print e    
        elif line.startswith('$GPRMC'):
            # $GPRMC,105215.000,A,5128.5775,N,00058.5070,W,0.12,103.43,211012,,,A*78
            # 1 = Time
            # 2 = (A)ctive or (V)oid
            # 3 = Latitude
            # 5 = Longitude
            # 7 = Speed in knots
            # 8 = Compass heading
            # 9 = Date
            #Divide minutes by 60 and add to degrees. West and South = negative
            #Multiply knots my 1.15078 to get mph.
            tokens = line.split(',')
            if len(tokens) < 10:
                return
            try:
                self.time_stamp = tokens[1]
                self.active = tokens[2] == 'A'
                self.lat = tokens[3]
                self.lat_dir = tokens[4]
                self.lon = tokens[5]
                self.lon_dir = tokens[6]
                self.speed = tokens[7]
                self.heading = tokens[8]
                self.date = tokens[9]
                if self.active:
                    self.lat = float(self.lat[:2]) + float(self.lat[2:])/60.0
                    if self.lat_dir == 'S':
                        self.lat = -self.lat
                    self.lon = float(self.lon[:3]) + float(self.lon[3:])/60.0
                    if self.lon_dir == 'W':
                        self.lon = -self.lon
                    self.speed = float(self.speed) * 1.15078
            except ValueError as e:
                print e

The TextStar class.

The TextStar serial LCD is a great little handy device not just for the 16x2 display but also the 4 buttons around the edge of the display for input. Normally I connect this straight into the serial pins on the Raspberry Pi, but in this case I have the GPS connected there. So, I'm using a USB to TTL serial converter. If you have a standard USB to RS232 converter you can use that too, just be sure to set the TextStar into RS232 mode.

The constructor opens the serial port through the USB and throws the first few inputs away. Something I noticed during testing is that when the TextStar starts up it spews out a few characters which could spoil the key reading code later. So, the start up routine reads up to 16 characters after a 3 second delay from opening the port. Also, I set up the "on_rec_button" event here. This is the method to call if the record toggle button is pressed.

As well as updating the display with the GPS and Accelerometer info this class displays the currently assigned wired ethernet address and ppp address. This is really useful as it allows you to plug in to a network and easily find the address you've been assigned so you can then ssh in. Secondly it's a confidence check that the 3g is working as you'll see the ppp address.

The key reading routine uses the 'a' button to rotate through the pages of info and the 'c' button to call the 'on_rec_button' event which is used to toggle recording.
        
class TextStar:
    def __init__(self, on_rec_button):
        self.LCD_UPDATE_DELAY = 5
        self.lcd_update = 0
        self.page = 0
        self.ser = serial.Serial('/dev/ttyUSB0',115200,timeout=0.01)
        # Throw away first few key presses after waiting for the screen to start up
        time.sleep(3)
        self.ser.read(16)
        self.on_rec_button = on_rec_button
    
    def get_addr(self,interface):
        try:
            s = subprocess.check_output(["ip","addr","show",interface])
            return s.split('\n')[2].strip().split(' ')[1].split('/')[0]
        except:
            return '?.?.?.?'
    
    def write_ip_addresses(self):
        self.ser.write(chr(254)+'P'+chr(1)+chr(1))
        self.ser.write('e'+self.get_addr('eth0').rjust(15)+'p'+self.get_addr('ppp0').rjust(15))

    def update(self,gps,acc,rec):
        self.lcd_update += 1
        if self.lcd_update > self.LCD_UPDATE_DELAY:
            self.lcd_update = 0
            self.ser.write(chr(254)+'P'+chr(1)+chr(1))
            
            if not gps.active and (self.page == 0 or self.page == 1):
                self.ser.write('NO FIX: '+gps.date+' '+rec.recording)
                self.ser.write(gps.time_stamp.ljust(16))
            elif self.page == 0:
                self.ser.write(('%.8f' % gps.lat).rjust(14)+" "+rec.recording)
                self.ser.write(('%.8f' % gps.lon).rjust(14)+"  ")
            elif self.page == 1:
                #0.069 223.03 48.9 -0.010 0.010 0.980
                self.ser.write('{: .3f}{:>9} '.format(gps.speed,gps.height))
                if acc:
                    self.ser.write('{: .2f}{: .2f}{: .2f}'.format(*acc))
                else:
                    self.ser.write(' '*16)

    def read_key(self):
        key = str(self.ser.read(1))
        if key != '' and key in 'abcd':
            self.lcd_update = self.LCD_UPDATE_DELAY
            if key == 'c':
                self.on_rec_button()
            elif key == 'a':
                self.page += 1
                if self.page > 2:
                    self.page = 0
                elif self.page == 2:
                    self.write_ip_addresses()

The Recorder class

The current status is logged to file every time there's a new reading from the GPS. So that's 10 times per second. If the Accelerometer is not used then dashes replace the X,Y and Z readings. Likewise if there is no GPS lock only the date and time is logged and dashes replace the rest of the data.

The recorder has a reference to the ControlMyPi connection and uses this to push an update showing the recording state. This is either "Recording" or "Stopped" and when it's stopped the generated file name is shown.
        
class Recorder:
    def __init__(self,gps,cmp):
        self.gps = gps
        self.cmp = cmp
        self.recording = 's'
        self.rec_file = None

    def start(self):
        if self.recording == 's':
            self.recording = 'r'
            self.rec_file = open("/home/pi/gps-"+self.gps.date+self.gps.time_stamp+".log", "a")
            self.cmp.update_status( {'recording_state':'Recording'} )
    
    def stop(self):
        if self.recording == 'r':
            self.recording = 's'
            self.rec_file.close()
            self.cmp.update_status( {'recording_state':'Stopped - [%s]' % self.rec_file.name} )

    def update(self,acc):
        if self.recording == 'r':
            if acc:
                acc_str = '%.2f %.2f %.2f' % acc
            else:
                acc_str = '- - -'
                
            if self.gps.active:
                self.rec_file.write('%s %s %.8f %.8f %.3f %s %s %s\n' % (self.gps.date, self.gps.time_stamp, self.gps.lat, self.gps.lon, self.gps.speed, self.gps.heading, self.gps.height, acc_str))
            else:
                self.rec_file.write('%s %s - - - - - %s\n' % (self.gps.date, self.gps.time_stamp, acc_str))

The Accelerometer class

The MCP3008 is used to read the three voltages from the 3 axis accelerometer, convert them to digital readings and retrieve them over SPI. Details of this technique are written up here: Raspberry Pi hardware SPI analog inputs using the MCP3008. As the comment in the code states there's a little tuning to be done to get good readings.
class Accelerometer:
    def read_accelerometer(self):
        '''Read the 3 axis accelerometer using the MCP3008. 
           Each axis is tuned to show +1g when oriented towards the ground, this will be different
           for everyone and dependent on physical factors - mostly how flat it's mounted.
           The result is rounded to 2 decimal places as there is too much noise to be more
           accurate than this.
           Returns a tuple (X,Y,Z).'''  
        x = mcp3008.readadc(0)
        y = mcp3008.readadc(1)
        z = mcp3008.readadc(2)
        return ( round((x-504)/102.0,2) , round((y-507)/105.0,2) , round((z-515)/102.0,2) )

Construction and Events

In this section the objects are created and a couple of call-back events are assigned. If you don't have an Accelerometer set acc to None. Also, if you don't have a TextStar LCD set lcd to None. Notably the two call-backs are to handle incoming button events from either ControlMyPi or the TextStar keys.

# Start the GPS
gps = GPS()

# Create the Accelerometer object. Change to acc=None if you don't have an accelerometer.
acc = Accelerometer()

# Control My Pi
def on_control_message(conn, key, value):
    if key == 'start_button':    
        rec.start()
    elif key == 'stop_button':
        rec.stop()

conn = ControlMyPi(JABBER_ID, JABBER_PASSWORD, SHORT_ID, FRIENDLY_NAME, PANEL_FORM, on_control_message)

# Recording
rec = Recorder(gps, conn)

def on_rec_button():
    if rec.recording == 's':
        rec.start()
    else:
        rec.stop()    

# Start the TextStar LCD. Change to lcd=None if you don't have a TextStar LCD.
lcd = TextStar(on_rec_button)

The main loop

Finally, after connecting to ControlMyPi, the main loop starts. Here we call the readers and the updaters. We read from the GPS, Accelerometer and TextStar keypad every loop. We keep track of the time stamp we're on and when we move to the next 10th of a second we call the updaters.

The updaters are the LCD, ControlMyPi and the Recorder. The Recorder writes to the file every change of time stamp provided it's in record mode. The LCD and ControlMyPi write less frequently, a simple counter is used to action every n'th update.

You'll notice there is no explicit yield in the main loop. It's quite normal to put a time.sleep(n) into the main loop to stop tight-looping. In this case we're using the blocking serial port reads with their timeouts to yield.

ControlMyPi only needs to be updated with new information, if the GPS is not locked (active) then we don't bother to send the old information again. Instead we send a "NOT LOCKED" message. The status dict is simply filled with the widget names that we want to update and the new values. This dict is then sent to ControlMyPi with the call to update_status.
if conn.start_control():
    try:
        conn.update_status( {'recording_state':'Stopped'} )
        # Start main loop
        old_time_stamp = 'old'
        CMP_UPDATE_DELAY = 50
        cmp_update = 0
        
        while True:
            #Read the 3 axis accelerometer
            if acc:
                xyz = acc.read_accelerometer()
            else:
                xyz = None
                
            #Read GPS
            gps.read()
                    
            #Update ControlMyPi, LCD and Recorder if we have a new reading 
            if gps.time_stamp != old_time_stamp:
                if lcd:
                    lcd.update(gps, xyz, rec)      

                # Don't update ControlMyPi every tick, it'll be too much - approx. 5 seconds is
                # about right as it gives the browser time to fetch the streetview and map 
                cmp_update += 1
                if cmp_update > CMP_UPDATE_DELAY:
                    cmp_update = 0
                    status = {}
                    if xyz:
                        status['accx'] = xyz[0]
                        status['accy'] = xyz[1]
                        status['accz'] = xyz[2]
                    
                    if gps.active:
                        status['locked'] = 'GPS locked'
                        slat = str(gps.lat)
                        slon = str(gps.lon)
                        status['streetview'] = STREET_VIEW_URL % (slat,slon,gps.heading)
                        status['map'] = MAP_URL % (slat,slon,slat,slon)
                        status['speed'] = int(round(gps.speed))
                        status['height'] = '{:>9}'.format(gps.height)
                        conn.update_status(status)
                    else:
                        status['locked'] = 'GPS NOT LOCKED'
                        conn.update_status(status)
              
                #Update recorder every tick
                rec.update(xyz)
        
                old_time_stamp = gps.time_stamp
        
            #Read keypad
            if lcd:        
                lcd.read_key()
    finally:
        conn.stop_control()
else:
    print("FAILED TO CONNECT")


That's it! If you're brave enough to try this yourself there are a few points where I've left some commented-out print statements for debug. This and some simpler projects are available in a zip on ControlMyPi here: How to connect your pi.

Have fun.

2013-02-19

Live Web Bicycle Dashboard using ControlMyPi


*** NOTE: ControlMyPi shutting down ***

This post shows how I set up a Live Web Dashboard from a Raspberry Pi as seen in the video above. In case you haven't worked it out, what's going on here is the Raspberry Pi is using 3G to send GPS and accelerometer data up to ControlMyPi. Users can then log in to ControlMyPi and watch the Live data displayed on the dashboard. In this case I'm using Google StreetView and Maps to show the current position and heading. Gauges are used to show speed and X,Y and Z accelerations.

(Special thanks to Rasathus for making this video for me!)

Click here to watch the "as live" replay of bicycle telemetry!


The diagram below shows the data flow:
There's a lot going on here so in this first post I'll explain how it all fits together and in the following post (or maybe posts) I'll go through the code.

3G dongle

There are quite a few guides out there for setting up 3G dongles on Raspberry Pis. Some use what now seems to be an unsupported script called sakis3g - I tried this first but was uneasy about using something that the author appears to have taken down. In fact all I had to do was install usb-modeswitch and wvdial. usb-modeswitch automatically detects your dongle and switches it into TTY mode. Simply use apt-get to install it.

wvdial is used to make the PPP connection. After using apt-get again to install it I followed the instructions on Linux Forums to get my connection up and running. 

Start-up sequence: There's probably a better Linuxy way of doing this (maybe someone can comment to let me know) but I needed to be in control of the order that things started up to help with TTY discovery and smooth networking. I'm using a TextStar serial LCD through a USB to serial converter and my code expects to find it on /dev/ttyUSB0 so I really don't want the 3G dongle to appear on USB0. Also, I found that if my code starts trying to use the network before the PPP link is up it doesn't work and you have to stop and start the program. This is no good if you're out on your bicycle somewhere. So I wrote a small boot up script which is run from /etc/rc.local it guides the user through this sequence:

  1. Remove 3G dongle and power up Raspberry Pi
  2. Raspberry Pi boots up and runs rc.local
  3. LCD shows "Insert dongle and press 'A'"
  4. User plugs in dongle and waits for Blue LED before pressing the 'A' button on the TextStar
  5. LCD shows "Starting 3G please wait"
  6. wvdial is started
  7. Few seconds delay to wait for the network to come good
  8. Start up Bicycle Telemetry app
This works nicely and means I can get the system up and running anywhere.

GPS

I'm using the Adafruit Ultimate GPS Breakout - 66 channel w/10 Hz updates - Version 3. I chose this one because having a 10Hz update fitted well with the code design. As you'll see when I publish the code, the main loop is timed off the updates from the GPS. Although I'm not updating the Web Dashboard as quickly as this I am logging this information to file 10 times a second.

The breakout board is simply connected up to the TTL serial pins on the Raspberry Pi and then it appears on /dev/ttyAMA0. When I wrote the code I didn't realise that gpsd existed so I have some code which decodes the serial stream directly, it's pretty simple though. Also I can't see how you can send the settings commands through to the GPS module from gpsd - I'm sending commands to switch the baud rate to 38400 and put it in 10Hz update mode (see PMTK_SET_NMEA_UPDATERATE in this pdf). Without these settings it defaults to 9600 baud and 1Hz updates.




3 Axis Accelerometer

Another Adafruit board is used here - the ADXL335 this senses up to 3g in X,Y and Z directions. I've then used an MCP3008 to convert the analog voltage outputs to 3 digital readings available over SPI. This uses the same technique (and code) from my article: Raspberry Pi hardware SPI analog inputs using the MCP3008.










ControlMyPi

I have created the cloud app ControlMyPi to make projects like this not only possible, but easy. A client library is used on the Raspberry Pi and all communication between it and ControlMyPi are over XMPP (also known as Jabber) protocol. This is the instant messaging protocol used by Google Talk as well. By using XMPP you're not hampered by firewalls and such but you do have near real-time messaging.

ControlMyPi also makes it simple to serve your live data to multiple web clients using "push". Updates from your Pi are routed through ControlMyPi and "pushed" to the web browser of anyone viewing the dashboard without refreshing pages.

More information about ControlMyPi is in my previous article: Control My Pi - Easy web remote control for your Raspberry Pi projects. Just as a teaser for the next post here is the panel definition code for the Bicycle Dashboard:

[
[ ['S','locked',''] ],
[ ['O'] ],
[ ['P','streetview',''],['P','map',''] ],
[ ['C'] ],
[ ['O'] ],
[ ['L','Speed'],['G','speed','mph',0,0,50], ['L','Height'],['S','height',''] ],
[ ['C'] ],
[ ['L','Accelerations'] ],
[ ['G','accx','X',0,-3,3], ['G','accy','Y',0,-3,3], ['G','accz','Z',1,-3,3] ],
[ ['L','Trace file'],['B','start_button','Start'],['B','stop_button','Stop'],['S','recording_state','-'] ]
]

Here I'm using Picture widgets (P) for 'streetview' and 'map'. ControlMyPi allows you to push an update to an image by sending a url from your script, so the Google Maps Image APIs work very well. I'll show exactly how this is done in code in the next post.

Since I can't be riding around 24x7 I have set up a script which plays back recorded data from a few journeys. This script is running on a Raspberry Pi and sending out the data to ControlMyPi at the correct speed so it's a pretty good simulation. I've set it up as a public panel so you can access it from the front page - or this link: Replay of bicycle telemetry.



The next post will guide you through the code for all of this, coming soon...


2012-09-08

Raspberry Pi distance measuring sensor with LCD output

Measure distances from the Sharp GP2Y0A02YK0F sensor using an MCP3008 ADC and hardware SPI.


The Sharp GP2Y0A02YK0F can be powered from the 5V supply on the Raspberry Pi. The Analog output is less than 3V and so can easily work with the logic level circuit. If you buy one of these look for the cable that goes with it to save you some bother.

This project builds on two previous projects in this blog. For the Analog to Digital SPI electronics and Python code first go here: Raspberry Pi hardware SPI analog inputs using the MCP3008. For the TextStar LCD first read this: Raspberry Pi with TextStar Serial LCD Display.

In the first video below the sensor is held in a clamp at the very top left of the picture. You can just about make out the display showing the distance to my hand. At the other end of the table there's a chair so when I lift my hand up it shows the distance to that instead - about 118 cm. The second video is a close-up of the screen while I move my hand forward and backwards in front of the sensor. Again the jumps to 118 cm are when I raise a lower my hand. 



All the code is available on github. You'll need mcp3008.py and distance-screen.py to run this project. The code assumes that you have the sensor output connected to CH1 on the MCP3008. The main routine to look at in the code is the translation from the sensor output to a distance. In the datasheet there's a graph which plots distance against voltage output:
I compared this against my sensor by laying a tape measure on the table and looking at the voltage output - it matched perfectly. I was about to start working out a formula to fit this curve when I found a good set of comments on the Sparkfun page including a magic formula!

Here's my Python routine which is called 10 times a second using the on_tick handler from the screen driver:

def write_distance():
    display.position_cursor(1, 1)
    r = []
    for i in range (0,10):
        r.append(mcp3008.readadc(1))
    a = sum(r)/10.0
    v = (a/1023.0)*3.3
    d = 16.2537 * v**4 - 129.893 * v**3 + 382.268 * v**2 - 512.611 * v + 306.439
    cm = int(round(d))
    val = '%d cm' % cm
    percent = int(cm/1.5)
    display.ser.write(str(val).ljust(16))
    display.capped_bar(16, percent)


  • Firstly, take ten readings and find the average (a) - this smooths things out a little.
  • Convert this to a voltage (v)
  • Use the magic formula to get the distance (d)
  • Round this to the nearest centimetre (cm)
  • Then write this to the display along with a 16 character capped bar graph
So the code happily reads from the sensor 100 times a second and does the calculation and screen update 10 times a second. The CPU usage hovers around 2% in top.

2012-09-05

Raspberry Pi hardware SPI analog inputs using the MCP3008

A hardware SPI remake of the bit-banged Adafruit project:  Analog Inputs for Raspberry Pi Using the MCP3008.


Take a look at the Adafruit project and particularly the datasheet for the MCP3008 - what we're making is a hardware volume control using a 10K potentiometer. Instead of using the GPIO pins and bit-banging the SPI protocol I'm using the proper SPI pins and the hardware driver.

Hardware


The connections from the cobbler to the MCP3008 are as follows:
  • MCP3008 VDD -> 3.3V (red)
  • MCP3008 VREF -> 3.3V (red)
  • MCP3008 AGND -> GND (orange)
  • MCP3008 CLK -> SCLK (yellow)
  • MCP3008 DOUT -> MISO (green)
  • MCP3008 DIN -> MOSI (yellow)
  • MCP3008 CS -> CE0 (red)
  • MCP3008 DGND -> GND (orange)

Operating system and packages

This project was built using Occidentalis v0.2 from Adafruit which takes the hassle out of fiddling with Linux. It comes with the hardware SPI driver ready to go. (It also has super-simple wifi setup if you have a dongle like the Edimax EW-7811UN). A couple of packages are required to complete this project. Firstly the mp3 player:
sudo apt-get install mpg321

and secondly the Python wrapper for SPI:
cd ~

git clone git://github.com/doceme/py-spidev

cd py-spidev/

sudo python setup.py install

Talking to the MCP3008

With the bit-banging code you're in control of the chip-select, clock, in and out pins and so you can effectively write and read a single bit at a time. When you use the hardware driver you talk using 8-bit words and it takes care of the lower level protocol for you. This changes the challenge slightly because the MCP3008 uses 10-bits for the values giving a range from 0 to 1023. Thankfully the Python wrapper is excellent and the datasheet has good documentation:

So from the diagram above you can see that to read the value on CH0 we need to send 24 bits:

.... ...s S210 xxxx xxxx xxxx
0000 0001 1000 0000 0000 0000

Let's say the current value is 742 which is 10 1110 0110 in 10-bit binary. This is what is returned in B9 to B0 in the diagram. The driver and Python wrapper returns this as 3 8-bit bytes as seen above, the mildly confusing thing is that you'll get some bits set where the question marks are in the diagram which you have to ignore (if you were sending a continuous stream these would be more useful). The 24 bits returned will be something like this:

???? ???? ???? ?n98 7654 3210
0110 0111 1000 0010 1110 0110

The python wrapper will give you 3 ints in a list:

[103,130,230]

So, we ignore the first int and then mask out all the bits apart from the last two of the second int by anding it with 3 (0000 0011). In this case you get 0000 0010. We then shift this left by 8 positions to give 10 0000 0000 and then add the third int to give 10 1110 0110 which is 742 decimal. Here's the Python for that:

# read SPI data from MCP3008 chip, 8 possible adc's (0 thru 7)
def readadc(adcnum):
        if ((adcnum > 7) or (adcnum < 0)):
                return -1
        r = spi.xfer2([1,(8+adcnum)<<4,0])
        adcout = ((r[1]&3) << 8) + r[2]
        return adcout
The rest of the code is pretty much the same as the Adafruit example. My version is available on git here: https://github.com/jerbly/Pi/blob/master/raspi-adc-pot.py

Run it

Start up mpg321 with your favourite mp3 in the background and then run the Python code:
mpg321 song.mp3 &

sudo python raspi-adc-pot.py
Turn the pot to adjust the volume.


Extra goodies

Combine this with the TextStar screen and the Python code from my previous blog entry: Raspberry Pi with TextStar serial LCD to have a nice bar graph for the pot position. Just use this method on one of the pages and in the on_tick handler so it updates every 0.1 seconds:

# Add this to the display class
    def capped_bar(self, length, percent):
        self.ser.write(ESC+'b'+chr(length)+chr(percent))

s = spidev.SpiDev()
s.open(0,0)

def get_val():
    r = s.xfer2([1,128,0])
    v = ((r[1]&3) << 8) + r[2]
    return v

def write_pots():
    display.position_cursor(1, 1)
    val = get_val()
    percent = int(val/10.23)
    display.ser.write(str(val).ljust(16))
    display.capped_bar(16, percent)


2012-07-28

Raspberry Pi with TextStar Serial LCD Display

The TextStar Serial LCD Display from Cat's Whisker Technologies is a nice little 16x2 display. It's quite a sophisticated screen including 4 buttons to transmit characters too. It's a perfect companion for the Raspberry Pi as you can run it straight off the 3.3v supply and TTL serial pins without the need for an RS232 converter like a MAX232. It couldn't be easier!

As the video shows I have written some software to use the 4 buttons as page buttons.

  • A: Date and time
  • B: Shows the latest number entry on jerbly.uk.to - my other Raspberry Pi
  • C: Twitter feed from @Raspberry_Pi
  • D: eth0 and wlan0 ip addresses
The ip address screen is really useful if you are set up for DHCP. Just plug in and it shows you the address needed to access your Raspberry Pi on the network.

Connecting it up

Simply connect the 3.3v, ground, UART TX and RX pins from the Raspberry Pi to the pins on the TextStar as show in these photos:


Setup the OS

By default there is a VT100 terminal running on /dev/AMA0 which will interfere with the software. Simply edit /etc/inittab and comment out the line like so:
On Raspbian:
#T0:23:respawn:/sbin/getty -L ttyAMA0 115200 vt100
On Arch:
#c2:2345:respawn:/sbin/agetty -8 -s 115200 ttyAMA0
and then reboot.

Code

Cat's Whisker Technologies have good documentation for the display on their web site. Follow their instructions to configure your display for TTL and set the Baud rate so it matches your python code. I've used 9600 as this is way fast enough. I have implemented some of the special commands to control scrolling and so on as you can see in the code below. The code is also available on GitHub: screen.py


'''
Created on 21 Jul 2012

@author: Jeremy Blythe

screen - Manages the Textstar 16x2 4 button display

Read the blog entry at http://jeremyblythe.blogspot.com for more information
'''
import serial
import datetime
import time
import subprocess
import twitter
import urllib2
import json

CLEAR = chr(12)
ESC = chr(254)
BLOCK = chr(154)

POLL_TICKS = 15
REFRESH_TICKS = 300

class Display():
    """ Manages the 16x2 4 button display:
        on_tick called every 0.1 seconds as part of the main loop after the button read
        on_poll called every 1.5 seconds
        on_page called when a new page has been selected
        on_refresh called every 30 seconds"""
    def __init__(self,on_page=None,on_poll=None,on_tick=None,on_refresh=None):
        # Start the serial port
        self.ser = serial.Serial('/dev/ttyAMA0',9600,timeout=0.1)
        # Callbacks
        self.on_page = on_page
        self.on_poll = on_poll
        self.on_tick = on_tick
        self.on_refresh = on_refresh

        self.page = 'a'
        self.poll = POLL_TICKS
        self.refresh = REFRESH_TICKS

    def position_cursor(self,line,column):
        self.ser.write(ESC+'P'+chr(line)+chr(column))
    
    def scroll_down(self):
        self.ser.write(ESC+'O'+chr(0))
    
    def window_home(self):
        self.ser.write(ESC+'G'+chr(1))

    def clear(self):
        self.ser.write(CLEAR)

    def run(self):
        #show initial page
        display.ser.write('  Starting....  ')
        if self.on_page != None:
            self.on_page()
        #main loop
        while True:
            key = str(self.ser.read(1))
            if key != '' and key in 'abcd':
                self.page = key
                self.refresh = REFRESH_TICKS
                self.poll = POLL_TICKS
                if self.on_page != None:
                    self.on_page()
            else:
                self.refresh-=1
                if self.refresh == 0:
                    self.refresh = REFRESH_TICKS
                    if self.on_refresh != None:
                        self.on_refresh()
                        
                self.poll-=1
                if self.poll == 0:
                    self.poll = POLL_TICKS
                    if self.on_poll != None:
                        self.on_poll()
                        
                if self.on_tick != None:
                    self.on_tick()


display = None
# Start twitter
twitter_api = twitter.Api()

def write_datetime():
    display.position_cursor(1, 1)
    dt=str(datetime.datetime.now())
    display.ser.write('   '+dt[:10]+'   '+'    '+dt[11:19]+'    ')

def get_addr(interface):
    try:
        s = subprocess.check_output(["ip","addr","show",interface])
        return s.split('\n')[2].strip().split(' ')[1].split('/')[0]
    except:
        return '?.?.?.?'

def write_ip_addresses():
    display.position_cursor(1, 1)
    display.ser.write('e'+get_addr('eth0').rjust(15)+'w'+get_addr('wlan0').rjust(15))

def write_twitter():
    display.position_cursor(1, 1)
    try:
        statuses = twitter_api.GetUserTimeline('Raspberry_Pi')
        twitter_out = BLOCK
        for s in statuses:
            twitter_out+=s.text.encode('ascii','ignore')+BLOCK
        display.ser.write(twitter_out[:256])
    except:
        display.ser.write('twitter failed'.ljust(256))

def write_recent_numbers():
    display.position_cursor(1, 1)
    try:
        result = urllib2.urlopen("http://jerbly.uk.to/get_recent_visitors").read()
        j = json.loads(result)
        if len(j) > 0:
            entry = str(j[0]['numbers'][-1:])+' '+j[0]['countryName']
            display.ser.write(entry.ljust(32))
        else:
            display.ser.write('No entries found'.ljust(32))
    except:
        display.ser.write('jerbly.uk.to    failed'.ljust(32))

# Callbacks
def on_page():
    display.clear()
    display.window_home()
    if display.page == 'a':
        write_datetime()
    elif display.page == 'b':
        write_recent_numbers()
    elif display.page == 'c':
        write_twitter()
    elif display.page == 'd':
        write_ip_addresses()
        
def on_poll():
    if display.page == 'c':
        display.scroll_down()

def on_tick():
    if display.page == 'a':
        write_datetime()

def on_refresh():
    if display.page == 'b':
        write_recent_numbers()
    elif display.page == 'c':
        write_twitter()
    elif display.page == 'd':
        write_ip_addresses()

display = Display(on_page, on_poll, on_tick, on_refresh)            
display.run()