Raspberry Pi Projects

Raspberry Pi
  1. Wall clock

Wall clock

This project was born out of my frustration to see wall clocks at a distance without squinting. I also had a Raspberry Pi sitting around not doing anything useful. This project uses a fairly affordable external LCD display (connected to the Raspberry's composite video out port), so doesn't really offer a lot of resolution to play with (656x416 pixels.) Fancier graphics can always be added, but I find that the no-nonsense display is easy to read. Unfortunately the display isn't externally switchable (can't turn on via a relay for instance), so must be manually turned on or just left on.

Here is the Python code (using pygame):

#!/usr/bin/python
# vim: set fileencoding=utf-8
import os
import pygame
import time
import random
import codecs
from datetime import datetime, date, timedelta

# create this file to make this thing stop dead
STOP_FILE = "/tmp/myclock"
# 1st line should just have # (degrees)
# 2nd line is current status (or forecast if you'd like)
WEATHER_FILE = "/tmp/weather"
# text message on first line and date on second line
MSG_FILE = "/tmp/text_message"
# check frequency for above (to see if something changed)
CHECK_FREQ = 10 # in seconds

# colors (R,G,B):
BLACK =  (  0,   0,   0)
WHITE =  (255, 255, 255)
YELLOW = (255, 255,  64)
RED =    (255,   0,   0)
GREEN =  (  0, 255,   0)
CYAN =   ( 41, 239, 239)
ORANGE = (248, 149,   0)

class myclock:
    screen = None
    size_x = 0
    size_y = 0
    last_deg = -1000
    last_msg = ""
    last_msg_dt = ""
    forecast = ""
    dt_msg = None
    cur_time = None
    red_dot = False
    on_mac = False
    
    def __init__(self):
        "Ininitializes a new pygame screen using the framebuffer"
        # Based on "Python GUI in Linux frame buffer"
        # http://www.karoltomala.com/blog/?p=679
        disp_no = os.getenv("DISPLAY")
        if disp_no: # under X windows (on mac or unix) do this:
            print "I'm running under X display={0}".format(disp_no)
            pygame.display.init()
            found = True
            size = (656, 416)
            self.screen = pygame.display.set_mode(size, pygame.RESIZABLE)
            self.on_mac = True
        else:
            # Check which frame buffer drivers are available
            # Start with fbcon since directfb hangs with composite output
            drivers = ['fbcon', 'directfb', 'svgalib']
            found = False
            for driver in drivers:
                # Make sure that SDL_VIDEODRIVER is set
                if not os.getenv('SDL_VIDEODRIVER'):
                    os.putenv('SDL_VIDEODRIVER', driver)
                try:
                    pygame.display.init()
                except pygame.error:
                    print 'Driver: {0} failed.'.format(driver)
                    continue
                found = True
                break
            if found:
                size = (pygame.display.Info().current_w, pygame.display.Info().current_h)
                self.screen = pygame.display.set_mode(size, pygame.FULLSCREEN)
    
        if not found:
            raise Exception('No suitable video driver found!')
        
        print "Display area size: %d x %d" % (size[0], size[1])
        self.size_x = size[0]
        self.size_y = size[1]
        self.xclock = pygame.time.Clock()
        self.dt_msg = datetime.now()
        # Clear the screen to start
        self.screen.fill((0, 0, 0))        
        # Initialise font support
        pygame.font.init()
        # hide the mouse pointer
        pygame.mouse.set_visible(False)
        # Render the screen
        pygame.display.update()

    def __del__(self):
        "Destructor to make sure pygame shuts down, etc."

    def load_data(self):
        "load all the external data"
        if not os.path.exists(WEATHER_FILE):
            self.red_dot = True
            self.last_deg = -1001
        else:
            st = os.stat(WEATHER_FILE)
            tfile = codecs.open(WEATHER_FILE, "r", "utf-8")
            lastmtime = st.st_mtime
            # haven't been updated in a while-->problem?
            if self.cur_time - lastmtime > 2*60*60:
                self.red_dot = True
            else:
                self.red_dot = False
            lines = tfile.readlines()
            self.forecast = ""
            deg = -1002
            if lines.count > 0:
                txt = lines[0].strip()
                try:
                    deg = int(txt)
                except:
                    deg = -1003
                if lines.count > 1:
                    self.forecast = unicode(lines[1].strip())
            self.last_deg = deg

        txt = self.last_msg
        if os.path.exists(MSG_FILE):
            dtstr = ""
            try:
                tfile = open(MSG_FILE, "r")
                txt = tfile.readline()
                dtstr = tfile.readline()
            except:
                pass
            txt = txt.rstrip()
            dtstr = dtstr.rstrip()
            if txt != self.last_msg:
                self.x1 = self.size_x
                self.last_msg_change = self.cur_time
            self.last_msg = txt
            self.last_msg_dt = dtstr
            try:
                self.dt_msg = datetime.strptime(dtstr, '%Y-%m-%d %H:%M:%S')
            except:
                self.dt_msg = datetime.now()
        if txt == "" and self.forecast != "":
            txt = self.forecast
        self.last_msg = txt

    def update_clock(self):
        "Update the display"
        self.screen.fill(BLACK)

        # show day of month
        font = pygame.font.Font(None, 105)
        curt = time.strftime("%e")
        text_surface = font.render(curt, True, GREEN)
        wdday = text_surface.get_width()
        self.screen.blit(text_surface, (self.size_x - wdday, 5))

        # show day of week, month
        font = pygame.font.Font(None, 65)
        curt = time.strftime("%a, %B")
        text_surface = font.render(curt, True, GREEN)
        self.screen.blit(text_surface,
            (self.size_x/2 - text_surface.get_width()/2, 5))
        posy = text_surface.get_height()

        # Show hour and minute
        lt = time.localtime()
        posy += 4
        font = pygame.font.Font(None, 250)
        curt = time.strftime("%k:%M").strip()
        text_surface = font.render(curt, True, WHITE)
        self.screen.blit(text_surface,
            (self.size_x/2 - text_surface.get_width()/2, posy))
        posy += text_surface.get_height()

        # draw a rect around the second bar
        posy += 5
        posx = self.size_x / 2 - (60 * 7) / 2 - 4
        pygame.draw.rect(self.screen, YELLOW, (posx, posy - 2,
            59 * 7 + 4, 7), 2)

        secs = lt[5]
        posx += 2
        posy += 1
        pygame.draw.line(self.screen, YELLOW, (posx, posy),
            (posx + (7 * secs), posy), 4)

        deg = self.last_deg
        if deg > -1000:
            if deg < 1:
                col = CYAN
            elif deg > 17: # 18 or more is warm to me
                col = ORANGE
            else: # eg. 1-17
                col = GREEN
            txt = unicode(str(deg)) + u'°'
            font = pygame.font.Font(None, 105)
            text_surface = font.render(txt, True, col)
            self.screen.blit(text_surface, (0, 5))

        if self.last_msg != "":
            txt = self.last_msg
            dtstr = self.last_msg_dt
            posy += 30
            font = pygame.font.Font(None, 120)
            difdt = datetime.now() - self.dt_msg
            if difdt > timedelta(hours=24):
                if self.forecast != "":
                    txt = "<" + self.forecast + ">"
                    text_surface = font.render(txt, True, CYAN)
                    dtstr = ""
                else:
                    text_surface = font.render(txt, True, WHITE)
            elif difdt > timedelta(hours=5):
                text_surface = font.render(txt, True, WHITE)
            elif difdt > timedelta(hours=3):
                text_surface = font.render(txt, True, YELLOW)
            else:
                text_surface = font.render(txt, True, ORANGE)
            text_rect = text_surface.get_rect()
            w = text_rect[2]
            if w > self.size_x:
                # scroll text by 3 pixels every time through
                self.x1 -= 3
                self.screen.blit(text_surface,(self.x1, posy))
                if self.x1 < -w:
                    self.x1 = self.size_x
            else: # it'll fit - so center
                self.screen.blit(text_surface, (self.size_x/2 - w/2, posy))
            posy += text_surface.get_height()

            if dtstr != "":
                posy += 10
                font = pygame.font.Font(None, 35)
                text_surface = font.render(dtstr, True, GREEN)
                if text_surface.get_width() >= self.size_x:
                    self.screen.blit(text_surface, (0, posy)) # gets cut off
                else:
                    self.screen.blit(text_surface,
                        (self.size_x/2 - text_surface.get_width()/2, posy))

        if self.red_dot: # flash red dot if problem
            if int(self.cur_time) % 2 == 0:
                pygame.draw.circle(self.screen, RED,
                    (self.size_x - 30, self.size_y - 40), 25)

        pygame.display.update()
        #done!

    def runit(self):
        done = False
        try:
            os.unlink(STOP_FILE) # so we don't immediately exit
        except:
            pass
        last_check = time.time() - 2 * CHECK_FREQ
        while not done:
            self.cur_time = lt = time.time()
            if last_check > lt: # time funkiness (system time adjustment?)
                last_check = lt - 5
            if lt - last_check > CHECK_FREQ:
                last_check = lt
                self.load_data()
                if os.path.exists(STOP_FILE):
                    done = True
            self.update_clock()
            for event in pygame.event.get():
                if event.type == pygame.QUIT:
                    done = True
                elif event.type == pygame.KEYDOWN:
                    done = (event.key == pygame.K_ESCAPE)
            self.xclock.tick(10)

# Create an instance of the myclock class
theclock = myclock()
theclock.runit()

I just keep this code in /home/pi/myclock.py on the Raspberry PI.

Since I enjoy the old nixie tubes, I also came up with a version which blits in nixie tube digits instead of the boring font used here. I found I couldn't quite see this as well from a distance, so I scrapped the idea (code still available if you're really interested.)

To start this thing up every time your Raspberry PI boots up add this to the end of your /etc/rc.local file:

cd /home/pi
/usr/bin/python ./myclock.py >/tmp/myclock.out 2>&1 &
exit 0

This program relies on external entities to retrieve the data for display. For example, one could use a simple shell script to retrieve a message from a web site, load a twitter feed message, or one could install a mail filter to extract a specific message from an email. The possibilities are endless...
Note that the first line returned should be the message and the second line is the date and time of the message in my preferred yyyy-mm-dd hh:mm:ss format.

#!/bin/sh
cd /tmp
tfile=msg.$$
curl -o $tfile --connect-timeout 30 \
    "http://www.example.com/getmessage.php" && \
    cnt=`wc -l < $tfile` && \
    [ "$cnt" -eq 2 ] && \
    mv $tfile text_message
rm -f $tfile

To get the weather I use something like this:

#!/usr/bin/php
<?php
# change this:
$LOCATION = "canada/ontario/barrie-4097";
$WOEID = "4097";
$CF = "c"; # f=fahrenheit or c=celsius
// construct the URLs we are going to use
$RSSURL = "http://weather.yahooapis.com/forecastrss?w=$WOEID&u=$CF";
$DEGREE = chr(0xc2).chr(0xb0);
$HOME = getenv("HOME");

$forecast = array();
$cond = array();
$ok_cur = false;
$data = file_get_contents($RSSURL);
if ($data) {
    $domdoc = new DOMDocument;
    $domdoc->loadXML($data);
    $items = $domdoc->getElementsByTagName("condition");
    foreach ($items as $itm) { // should only be one condition here
        $f = array();
        $cond['cur'] = $itm->getAttribute("temp");
        $cond['text'] = $itm->getAttribute("text");
        $ok_cur = true;
        break;
    }
    $items = $domdoc->getElementsByTagName("forecast");
    foreach ($items as $itm) {
        $f = array();
        $f['day'] = $itm->getAttribute("day");
        $f['low'] = $itm->getAttribute("low");
        $f['high'] = $itm->getAttribute("high");
        $f['text'] = $itm->getAttribute("text");
        $forecast[] = $f;
    }
}
if ($ok_cur) {
    print "Now: ".$cond['text'].": ".$cond['cur'].$DEGREE."\n";
}
$fstline = "";
foreach ($forecast as $f) {
    if ($fstline == "") {
        $fstline = $f['text'].": ".$f['low'].$DEGREE;
        if ($f['low'] != $f['high'])
            $fstline .= " to ".$f['high'].$DEGREE;
    }
    print $f['day'].": ".$f['text'].": ".$f['low'].$DEGREE;
    if ($f['low'] != $f['high'])
        print " to ".$f['high'].$DEGREE;
    print "\n";
}
if ($ok_cur) {
    $fp = fopen("/tmp/weather", "w");
    if ($fp) {
        fprintf($fp, "%d\n", $cond['cur']);
        fprintf($fp, "%s\n", $fstline);
        fclose($fp);
    }
}