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()

2012-07-21

Raspberry Pi 7 segment displays

This post shows how to drive two 7 segment displays from 6 gpio pins on the Raspberry Pi without a continual refresh loop. The software is simple with very low cpu usage - only processing when you want to change the displayed numbers. The electronics are pretty easy too (I'm a novice myself!) It's also a fun way to learn about BCD (Binary-coded decimal) and latches.

Hardware

To put this together I've used:
Basically you take 6 gpio pins through current limiting resistors and then on to the second breadboard. You'll see from the picture below that I have added an LED as a confidence indicator for each gpio pin. I've labelled these in the picture for the latches and data lines that then need to be wired up to the HEF4543Bs:
If you look at the data sheet for the HEF4543B you'll see that the labels I've put on this picture match. These  labels are also used in the software later.
  • LD = Latch disable (for first digit)
  • DD = 8 (BCD bit)
  • DC = 4 (BCD bit)
  • DB = 2 (BCD bit)
  • DA = 1 (BCD bit)
  • LD2 = Latch disable (for second digit)
The gpio pins for the latches are then connected directly to the two ICs, The data pins are connected in parallel to both ICs:
Now follow the data-sheets for the ICs and the displays to connect the IC output pins to the 7 segment displays. The phase input (PH) and the blanking input (BI) should both be connected to ground. I left a longer loop of wire on the blanking input as I was considering using it to blank the display but this would have used another gpio pin. Instead, if you check the data-sheet, you'll find that if you set the data to a decimal number higher than 9 it will blank the display anyway.
Note, this is all 3.3v and I've used another couple of 220ohm resistors to limit the current into the ICs.

Software

To set a number on the display you follow this sequence:
  1. Keep what's on the display now - latch disable = off
  2. Set the data pins for the required number
  3. Disable the latch to take the value to the display - latch disable = on
  4. For timing purposes wait for a short period
  5. Keep what's on the display now - latch disable = off
The Python below counts from 0 to 99 in a continuous loop as seen in the video above.
'''
Created on 7 Jul 2012

@author: Jeremy

numdisplay - counts from 0 to 99 in a loop using two 7 segment displays

Read the blog entry at http://jeremyblythe.blogspot.com for more information
'''
import time
import RPi.GPIO as GPIO


LD = 13
DD = 12
DC = 11
DB = 15
DA = 16
LD2 = 18


GPIO.setup(LD, GPIO.OUT)
GPIO.setup(DD, GPIO.OUT)
GPIO.setup(DC, GPIO.OUT)
GPIO.setup(DB, GPIO.OUT)
GPIO.setup(DA, GPIO.OUT)
GPIO.setup(LD2, GPIO.OUT)

def write_gpo(pin,state):
    GPIO.output(pin,state)

last_tens = None
    
def write(tens,units):
    global last_tens
    if last_tens != tens:
        #tens
        #keep what's on the display now - latch
        write_gpo(LD,False)
        write_digit(tens) 
        #disable the latch to take the value to the display
        write_gpo(LD,True)
        time.sleep(0.1)
        #keep what's on the display now - latch
        write_gpo(LD,False)
        last_tens = tens

    #units
    #keep what's on the display now - latch
    write_gpo(LD2,False)
    write_digit(units) 
    #disable the latch to take the value to the display
    write_gpo(LD2,True)
    time.sleep(0.1)
    #keep what's on the display now - latch
    write_gpo(LD2,False)
         
    
def write_digit(digit):    
        if digit == None:
            #blank
            write_gpo(DD,True)
            write_gpo(DB,True)
        else:
            if digit & 8 > 0:
                write_gpo(DD,True)
            else:
                write_gpo(DD,False)
            
            if digit & 4 > 0:
                write_gpo(DC,True)
            else:
                write_gpo(DC,False)
            
            if digit & 2 > 0:
                write_gpo(DB,True)
            else:
                write_gpo(DB,False)
                
            if digit & 1 > 0:
                write_gpo(DA,True)
            else:
                write_gpo(DA,False)

if __name__ == '__main__':
    t = 0
    u = 0
    while True:
        u=u+1
        if u > 9:
            t=t+1
            u = 0
        if t > 9:
            t = 0
        write(t,u)

        
On my live site (http://jerbly.uk.to/picam) you can enter a number that you want to send to the display. Here's the code to achieve that using the Flask web framework:

'''
Created on 8 Jul 2012

@author: Jeremy

numweb - writes the incoming 2 digit number to a pair of 7 segment displays

Read the blog entry at http://jeremyblythe.blogspot.com for more information
'''
import numdisplay
from datetime import datetime

from flask import Flask
app = Flask(__name__)

@app.route("/num/")
def num(number=None):
    if number == None:
        numdisplay.write(None, None)
    else:
        n = number % 100
        u = n % 10
        t = (n-u) / 10
    numdisplay.write(t, u)
    log(number)
    return "OK"

def log(number):
    with open('/tmp/numweb.log','a') as f:
        f.write('%s - %s\n' % (str(datetime.now()),number))

if __name__ == "__main__":
    app.run('0.0.0.0',8092)

This uses numdisplay.py and adds a very simple web interface. Run this up and go to http://{ip address}:8092/num/{number}. e.g. http://192.168.0.2:8092/num/12 to display 12.

Kits

I'm considering putting some kits together with the resistors, ICs and displays for people to buy if there's enough interest. It may turn out to be more cost effective if I buy some in bulk and then sell on to you. Let me know if you would be interested by leaving a comment below. Thanks.

2012-07-02

Raspberry Pi GPIO and Motion

This post shows how I've set up Motion with the Raspberry Pi GPIOs to indicate motion detection and video production on LEDs and a push button snapshot control. As part of this project I have created a Python service to allow easy scripted control of the GPIOs. This allows you to set up commands you want to run on GPIs and simple command line access to set GPOs high, low or flash. If you're new to these blog posts take a look at: Battery powered, Wireless, Motion detecting Raspberry Pi and Motion Google Drive Uploader and Emailer.

The electronics

I used the Electronic Starter Kit for Raspberry Pi plus a few extra jumper wires to give me 3 LEDs to control and two push button inputs:

The circuit uses the "low to glow" principle - set the GPO low to light the LED. I won't repeat a load of GPIO information available elsewhere, here's a very good article to read: Getting Started with Raspberry Pi GPIO and Python. I'm using 3.3v for the positive rail, pins 11, 12 and 13 for the red, yellow and green LEDs and 7 and 22 for the two buttons.

Flashing an LED from Motion

Motion has a number of configuration options which allow you to run shell commands when specific events occur. For example on_motion_detected, defined as Command to be executed when a motion frame is detected. The Motion web interface allows you to change all of these on the fly without restarting the service - great for testing. The on_motion_detected event does not have a sibling event like "on_motion_not_detected" so you can't easily switch an LED on on the first event and off on the second. Therefore you have to write a little bit of code to light the LED, wait for a short period and then switch it off. This is a common thing to want to do - a momentary indicator.

Another common thing to want to do is to flash an LED continuously, this gives you 3 indications for each LED: On, Off and Flash. But flashing can only be achieved by repeatedly setting the GPO high and low. I wanted to flash the red LED while Motion is in the "event window" this is defined by two events:
  • on_event_startCommand to be executed when an event starts. An event starts at first motion detected after a period of no motion defined by gap.
  • on_event_end: Command to be executed when an event ends after a period of no motion. The period of no motion is defined by option gap.

Taking a Motion snapshot

There are a few controls available for Motion through the action section of its web interface. The snapshot action captures a jpeg and creates a symlink to it called lastsnap.jpg. I then show this snapshot on my web site  (http://jerbly.uk.to/picam) next to the live feed. You can trigger a snapshot from the command line using curl like this:
curl http://localhost:8080/0/action/snapshot
All I needed was a way to run commands when a button is pressed. I needed to capture the GPI state going from False normally to True briefly (while the button is down) and then back to False again when it's released.
So, instead of writing more and more little scripts to handle controlling the GPIOs I wrote a service to do it for me. This allows me to echo simple instructions like "red flash" and "red high" to the service and it takes care of continuously flashing the GPO or setting it high etc.

My GPIO service

The code is available on GitHub here: Jerbly Raspberry Pi code. Download gpioservice.py and the example config file gpioservice.cfg to somewhere on your Raspberry Pi. Then, before you run it up, change the config file. This is very important as the config file determines which pins should be set as inputs and outputs and it also sets the initial mode of the pin (e.g. high). So you want to get this right before you start it up and blow something up!

The service gives you the follow features:
  • Name to pin mapping: so you can send "red flash" or "motor low" commands
  • Common modes: 
    • High: set the pin high
    • Low: set the pin low
    • Flash: flip the pin between high and low every 0.25 second
    • LowShot: set the pin low for 0.25s then leave it high
    • HighShot: set the pin high for 0.25s then leave it low
  • Commands are sent through a named pipe (fifo) which has write permissions for all, this means you don't have to be root to control the GPOs!
  • Run shell commands for button presses on GPIs
The code in gpioservice.py uses two secondary threads alongside the main thread. The main thread is a blocking reader of the named pipe, it just parses the input on the pipe and sets the mode on the GPO objects. The GPO thread sleeps for 0.25s each loop, inverts the current flash state and then runs through each GPO object calling its action method. The action method determines, based on the mode, whether to set the pin high or low. It's this that means you can have flash, lowshot and highshot. The GPI thread sleeps for 0.1s each loop and then calls the action method on all the configured GPI objects. The action method reads the input and uses a state variable to trigger the command to run when the state first changes from False to True and not to continuously call the command if the button is held down. (Other modes may be added to this in the future so you can have "button held" operations).

Setting it all up

My Motion and GPIOs Raspberry Pi is set up to do this:
  • Flash the red LED while in the "event window" (see above)
  • Light the yellow LED when Motion starts making a video
  • Light the green LED when it's finished making the movie and my Motion Google Drive Uploader and Emailer is running.
  • When the upload has finished switch off the yellow and green LEDs
  • When a Motion frame is detected briefly flash the green LED (lowshot)
  • When the first button is pressed take a snapshot
  • When the second button is pressed just lowshot the green LED for testing
Here's the gpioservice.cfg file:
[gpos]
fifo = /tmp/gpopipe

[gpo_pins]
# name = pin, initial_mode {high/low/flash}
red = 11, high
yellow = 12, high
green = 13, high

[gpi_pins]
# name = pin, command
motion_snapshot = 22, curl http://localhost:8080/0/action/snapshot
green_flash = 7, echo "green lowshot" >> /tmp/gpopipe

[options]
debug = False

Note that the fifo is configured at the top. The service takes care of creating this and setting the permissions. Then in the gpi_pins section you can see how to briefly flash the green LED when the button is pressed on pin 7.

Here's the Motion events section from my Motion config file:
# Command to be executed when an event starts. (default: none)
# An event starts at first motion detected after a period of no motion defined by gap
on_event_start echo "red flash" >> /tmp/gpopipe

# Command to be executed when an event ends after a period of no motion
# (default: none). The period of no motion is defined by option gap.
on_event_end echo "red high" >> /tmp/gpopipe

# Command to be executed when a picture (.ppm|.jpg) is saved (default: none)
# To give the filename as an argument to a command append it with %f
; on_picture_save value

# Command to be executed when a motion frame is detected (default: none)
on_motion_detected echo "green lowshot" >> /tmp/gpopipe

# Command to be executed when motion in a predefined area is detected
# Check option 'area_detect'. (default: none)
; on_area_detected value

# Command to be executed when a movie file (.mpg|.avi) is created. (default: none)
# To give the filename as an argument to a command append it with %f
on_movie_start echo "yellow low" >> /tmp/gpopipe

# Command to be executed when a movie file (.mpg|.avi) is closed. (default: none)
# To give the filename as an argument to a command append it with %f
on_movie_end /etc/motion/movie_end %f

# Command to be executed when a camera can't be opened or if it is lost
# NOTE: There is situations when motion doesn't detect a lost camera!
# It depends on the driver, some drivers don't detect a lost camera at all
# Some hang the motion thread. Some even hang the PC! (default: none)
; on_camera_lost value

Note that the on_movie_end setting calls a script that I have placed in /etc/motion to handle controlling the LEDs and uploading to Google Drive:
#!/bin/sh
echo "green low" >> /tmp/gpopipe
/root/py/jerbly/src/uploader.py /etc/motion/uploader.cfg $1
echo "green high" >> /tmp/gpopipe
echo "yellow high" >> /tmp/gpopipe

In Action

This first video shows the green lowshot by pressing the button connected to pin 7:

video

This second video shows the sequence when I trigger a motion event on the camera and start a recording. Note just after I remove the card from the camera there are two motion frame flashes on the green LED too.
video