python

Вывод температуры, пробок и курса валют на светодиодную матрицу Raspberry Pi

  • четверг, 26 февраля 2015 г. в 02:11:16
http://habrahabr.ru/post/251439/

Есть под рукой Raspberry Pi c подключенной к нему вот такой штукой:



Ещё есть кнопочка. Вот и появилось желание по нажатию кнопочки выводить на светодиодную матрицу что-то полезное, а не баловство. А еще подучить питон — ООП, потоки, парсинг и прочее. Можно сказать, что это мой первый полезный проект на питоне. Так что данная статья будет одновременно полезна для тех, кто хочет сделать домашний информер, и, кроме того, надеюсь, поучительная.

Для корректной работы матрицы с питоном нам потребуется вот такая штука. А информацию для вывода будем грузить с


Эти сервисы взяты для примера, полностью бесплатны. Можно использовать что угодно другое. При желании можно вывести и баллы пробок по Яндексу, но их не так-то просто получить.

Получение баллов Яндекс.Пробок
Нам понадобится:
веб-сервер з PHP – к примеру, www.penguintutor.com/linux/light-webserver;
xvfb – sudo apt-get install xvfb;
CutyCapt – cutycapt.sourceforge.net/;
Результат: картинка с заторами и текстовый файлик со значением баллов заторов.

save.php

<?php
  $f = "@/probki.txt";
  $fileHandle = fopen($f, 'w') or die("Unable to open the ".$f);
  fwrite($fileHandle, $_GET["val"]);
  fclose($fileHandle);
  echo file_get_contents($f);
?>

probki.html

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
  <html xmlns="http://www.w3.org/1999/xhtml">
  <head>
    <title>Пробки</title>
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
    <script src="//api-maps.yandex.ru/2.1/?lang=ru_RU" type="text/javascript"></script>
    <style>
      html, body, #map {
        width: 100%; height: 100%; padding: 0; margin: 0;
      }
    </style>
    <script type="text/javascript"> ymaps.ready(init); function init () {
      var myMap = new ymaps.Map("map", {
        //Это Киев. Поставьте здесь значение которое Вам нужно.
        center: [50.47,30.54],
        zoom: 12,
        controls: []
      });
 
      // Создадим провайдер пробок "Сейчас" с включенным слоем инфоточек.
      var actualProvider = new ymaps.traffic.provider.Actual({}, { infoLayerShown: true });
      // И потом добавим его на карту.
      actualProvider.setMap(myMap);
 
      actualProvider.state.events.add("change", function () {
        var jamlevel = actualProvider.state.get("level");
        if (jamlevel!== null) {
          //Здесь основная фишка - если вставить вместо пути картинки PHP файлик, то браузер обратится к нему, пытаясь отобразить картинку.
          document.getElementById("val").src="save.php?val="+jamlevel;
        }
      });
    }
  </script>
  </head>
  <body bgcolor="#555555">
    <!-- тут выведем погоду -->
    <div style="position: absolute;left: 30px; top: 0px; z-index: 2;">
      <a href="http://clck.yandex.ru/redir/dtype=stred/pid=7/cid=1228/*http://pogoda.yandex.ru/kyiv"><img src="http://info.weather.yandex.net/kyiv/2_white.uk.png" border="0" alt="$
      <img width="1" height="1" src="http://clck.yandex.ru/click/dtype=stred/pid=7/cid=1227/*http://img.yandex.ru/i/pix.gif" alt="" border="0"/></a>
    </div>
    <!-- а тут виведем красивую картинку про заторы -->
    <div style="position: absolute; right: 30px; top: 0px; z-index: 3;">
      <img src="http://info.maps.yandex.net/traffic/kiev/tends_200.png" alt="Пробки на Яндекс.Картах" border="0"/>
    </div>
    <div id="map">
    </div><!-- тут наша невидимая PHP картинка -->
    <img id="val" src="" style="display:none" />
  </body>
</html>

Теперь, если открыть этот файлик в браузере, мы увидим карту с пробками, виджет погоды и пробок, а самое главное — в папке "@" появится файлик probki.txt со значением пробок и их картинка. После этого можем заставить наш сервер открывать эту страницу автоматически по заданному расписанию. Итак, добавим в cron следующее:

/usr/bin/xvfb-run -a -s "-screen 0 1600x1200x16" /usr/bin/CutyCapt --url=http://mywebsite/probki.html --out=/var/www/mywebsite/@/probki.jpg --javascript=on --delay=5000 --min-width=1600 --min-height=1200

Для уменьшения количества обращений к API можно кешировать результаты допустим раз в пол часа и по нажатии кнопки читать закешированное значение.

Кэширование погоды, курса валют и пробок
#!/bin/bash
###########################################################
echo $(date +%F/%T%Z) "UpdateInfo started" > /var/log/updateinfo
rm -f /var/www/mywebsite/@/currrate.xml
/usr/bin/wget http://bank-ua.com/export/currrate.xml -P /var/www/mywebsite/@/ -q -N
echo $(date +%F/%T%Z) "UpdateInfo currate" >> /var/log/updateinfo
rm -f /var/www/mywebsite/@/weather.json
/usr/bin/wget "http://api.worldweatheronline.com/free/v1/weather.ashx?q=Kyyiv&format=json&extra=localObsTime&num_of_days=5&includelocation=yes&lang=uk&key=13a4e16719a757403c5db6f4a8f3067e4534b4d8" -O /var/www/mywebsite/@/weather.json -q -N
echo $(date +%F/%T%Z) "UpdateInfo weather" >> /var/log/updateinfo
rm -f /var/www/mywebsite/@/probki.jpg
rm -f /var/www/mywebsite/@/probki.txt
/usr/bin/xvfb-run -a -s "-screen 0 1600x1200x16" /usr/bin/CutyCapt --url=http://mywebsite/probki.php --out=/var/www/mywebsite/@/probki.jpg --javascript=on --delay=5000 --min-width=1600 --min-height=1200 
echo $(date +%F/%T%Z) "UpdateInfo Traffic" >> /var/log/updateinfo

Теперь приступим к самому интересному. Для того, чтобы подключенные к распи кнопки начали что-то делать, необходимо написать скрипт-демон, который бы реагировал на изменения состояния GPIO. Рассмотрим пример такого скрипта, написанного на Python, который будет обрабатывать длинные и короткие нажатия каждой кнопки и их комбинаций.

Пусть имеем четыре кнопки. Я писал обработку кнопок последовательно, без цикла. Так проще и я не думаю, что у кого-то будет так много кнопок, что придется писать цикл.

Логика проста. Мы переводим указанный пин GPIO в режим считывания высокого напряжения. Далее мы входим в бесконечный цикл с небольшой паузой, в котором запоминаем нынешние значения пинов и сравниваем с предыдущими. Если значение отличается — статус кнопки изменился. А добавив еще и таймер, мы можем сделать триггер длинного нажатия кнопки по такой логике: если кнопка была нажата, а сейчас не нажата, и со времени нажатия прошло более 2 секунд, то реагируем на длинные нажатия кнопки.

Точно так же можно обрабатывать комбинации кнопок, добавив условие для двух (или более) кнопок одновременно. Итак, следующий скрипт обрабатывает длинные и короткие нажатия на четыре кнопки и одновременное нажатие на первую и вторую.

buttondaemon.py
# This Python file uses the following encoding: utf-8

import RPi.GPIO as GPIO
import time
import os
import sys
import logging

logger = logging.getLogger('buttons_daemon')
hdlr = logging.FileHandler('/MyLogs/main.log')
formatter = logging.Formatter('%(asctime)s %(levelname)s %(message)s')
hdlr.setFormatter(formatter)
logger.addHandler(hdlr)
logger.setLevel(logging.WARNING)
suffix = " > /dev/null&"

#adjust for where your switch is connected
button1Pin = 18
button2Pin = 13
button3Pin = 16
button4Pin = 15
prev_input1 = 0
prev_input2 = 0
prev_input3 = 0
prev_input4 = 0
btimer12 = 0
btimer1= 0
btimer2= 0
btimer3= 0
btimer4= 0

GPIO.setmode(GPIO.BOARD)
GPIO.setup(button1Pin,GPIO.IN, pull_up_down=GPIO.PUD_DOWN)
GPIO.setup(button2Pin,GPIO.IN, pull_up_down=GPIO.PUD_DOWN)
GPIO.setup(button3Pin,GPIO.IN, pull_up_down=GPIO.PUD_DOWN)
GPIO.setup(button4Pin,GPIO.IN, pull_up_down=GPIO.PUD_DOWN)

while True:
  #assuming the script to call is long enough we can ignore bouncing
  input1 = GPIO.input(button1Pin)
  input2 = GPIO.input(button2Pin)
  input3 = GPIO.input(button3Pin)
  input4 = GPIO.input(button4Pin)

  if (input1):
    btimer1 += 1
  if (input2):
    btimer2 += 1
  if (input3):
    btimer3 += 1
  if (input4):
    btimer4 += 1
    
  #Some button up
  if ((not input1) and (not input2) and (not input3) and (not input4) and ( (prev_input1) or (prev_input2) or (prev_input3) or (prev_input4) ) ):

    #Button 12
    if ((prev_input1) and (prev_input2) and (not prev_input3) and (not prev_input4) ):
      if ((btimer1>20) and (btimer2>20)):
        logger.warning("Button12 long");
        os.system("sudo /leds+buttons/button12long"+suffix)
      else:
        logger.warning("Button12");
        os.system("sudo /leds+buttons/button12"+suffix)

    #Button 1
    if ((prev_input1) and (not prev_input2) and (not prev_input3) and (not prev_input4) ):
      if (btimer1>20):
        logger.warning("Button1 long");
        os.system("sudo /leds+buttons/button1long"+suffix)
      else:
        logger.warning("Button1");
        os.system("sudo /leds+buttons/button1"+suffix)

    #Button 2
    if ((prev_input2) and (not prev_input1) and (not prev_input3) and (not prev_input4) ):
      if (btimer2>20):
        logger.warning("Button2 long");
        os.system("sudo /leds+buttons/button2long"+suffix)
      else:
        logger.warning("Button2");
        os.system("sudo /leds+buttons/button2"+suffix)

    #Button 3
    if ((prev_input3) and (not prev_input2) and (not prev_input1) and (not prev_input4) ):
      if (btimer3>20):
        logger.warning("Button3 long");
        os.system("sudo /leds+buttons/button3long"+suffix)
      else:
        logger.warning("Button3");
        os.system("sudo /leds+buttons/button3"+suffix)

    #Button 4
    if ((prev_input4) and (not prev_input2) and (not prev_input3) and (not prev_input1) ):
      if (btimer4>20):
        logger.warning("Button4 long");
        os.system("sudo /leds+buttons/button4long"+suffix)
      else:
        logger.warning("Button4");
        os.system("sudo /leds+buttons/button4"+suffix)

    btimer1=0
    btimer2=0
    btimer3=0
    btimer4=0
    
  prev_input1 = input1
  prev_input2 = input2
  prev_input3 = input3
  prev_input4 = input4
  time.sleep(0.2)

При нажатии определенной кнопки или комбинации будет вызываться соответствующий баш-скрипт из папки /leds+buttons. Создадим файлы-пустышки с соответствующим именем как в скрипте. Пример такого файла (button1long):

#!/bin/bash
echo Button 1 pressed | wall
#Do something
exit

Теперь из нашего питоновского скрипта сделаем демон. Создадим файлик в папке /etc/init.d под названием buttondaemon

buttondaemon
#! /bin/sh
# /etc/init.d/buttondaemon
 
### BEGIN INIT INFO
# Provides: buttondaemon
# Required-Start: $remote_fs $syslog
# Required-Stop: $remote_fs $syslog
# Default-Start: 2 3 4 5
# Default-Stop: 0 1 6
# Short-Description: Daemon to control button events
# Description: Daemon which starts python script to control GPIO button events.
### END INIT INFO
 
# If you want a command to always run, put it here
 
# Carry out specific functions when asked to by the system
case "$1" in
start)
echo "Starting button daemon"
# run application you want to start
sudo python /leds+buttons/buttondaemon.py&
;;
stop)
echo "Stopping button daemon"
# kill application you want to stop
kill -9 $(ps -ef | grep -v "grep" | grep buttondaemon.py | awk '{ print $2 }') &> /dev/null
;;
*)
echo "Usage: /etc/init.d/buttondaemon {start|stop}"
exit 1
;;
esac
 
exit 0

Команда start запускает наш питоновский скрипт, а команда stop его останавливает. Раздадим всем скриптам нужные права. Теперь можем испытать, набрав в консоли:

sudo /etc/init.d/buttondaemon start

и пожмакаем кнопки, смотря что будет отображаться на экране. Вуаля!

Теперь нам осталось вывести что-нибудь на LED-матрицу. Создадим питон-скрипт, который будет отрабатывать по нажатию кнопки. По непонятной причине urlget отрабатывает минимум секунды за две, при том что та же ссылка открывается браузером моментально. Для того, чтоб видеть начало работы скрипта, а не втыкать 2 секунды в пустую матрицу, скрипт будет показывать анимацию пока грузятся данные.

Сначала покажем текущую температуру и осадки. Чтоб понять, что на экране температура, в правом верхнем углу покажем точку, символизирующую знак градуса.

Чтоб показать двухзначные числа на экране компактненько, нарисуем циферки в размере 3х6:

digits.py
#!/usr/bin/env python
# -*- coding: utf-8 -*- 

rows = 6
columns = 3

symbol = [[[0 for x in range(columns)] for x in range(rows)] for x in range(10)] 

symbol[1] = [
        [0,0,1],
        [0,1,1],
        [0,0,1],
        [0,0,1],
        [0,0,1],
        [0,0,1],
]
symbol[2] = [
        [1,1,1],
        [0,0,1],
        [1,1,1],
        [1,0,0],
        [1,0,0],
        [1,1,1],
]
symbol[3] = [
        [1,1,1],
        [0,0,1],
        [0,1,1],
        [0,0,1],
        [0,0,1],
        [1,1,1],
]
symbol[4] = [
        [1,0,0],
        [1,0,0],
        [1,0,1],
        [1,1,1],
        [0,0,1],
        [0,0,1],
]
symbol[5] = [
        [1,1,1],
        [1,0,0],
        [1,1,1],
        [0,0,1],
        [0,0,1],
        [1,1,1],
]
symbol[6] = [
        [1,1,1],
        [1,0,0],
        [1,1,1],
        [1,0,1],
        [1,0,1],
        [1,1,1],
]
symbol[7] = [
        [1,1,1],
        [0,0,1],
        [0,1,0],
        [0,1,0],
        [0,1,0],
        [0,1,0],
]
symbol[8] = [
        [1,1,1],
        [1,0,1],
        [0,1,0],
        [1,0,1],
        [1,0,1],
        [1,1,1],
]
symbol[9] = [
        [1,1,1],
        [1,0,1],
        [1,0,1],
        [1,1,1],
        [0,0,1],
        [1,1,1],
]
symbol[0] = [
        [1,1,1],
        [1,0,1],
        [1,0,1],
        [1,0,1],
        [1,0,1],
        [1,1,1],
]

Про осадки отдельно. Хотелось показать их на одном экране с температурой. Поэтому они отображаются в виде точек количеством от нуля до трех в левом верхнем углу.

Все запросы траффика и курса валют выполнимв это время на фоне в параллельных потоках. Смена данных на матрице анимируется сдвигом. Если загрузить какие-то данные не удалось, вместо них будет показан грустный смайлик.

informer.py
#!/usr/bin/env python
# -*- coding: utf-8 -*- 

import max7219.led as led
import max7219.font as font
import threading, sys, time
import digits
import urllib2
import json
import xml.etree.ElementTree as ET

debug=0

def log(message):
  if debug==1:
    print(message)

device = led.matrix(cascaded=1)

device.brightness(0)

hourglass = [
        [0,1,1,1,1,1,1,0],
        [0,1,0,0,0,0,1,0],
        [0,0,1,0,0,1,0,0],
        [0,0,0,1,1,0,0,0],
        [0,0,0,1,1,0,0,0],
        [0,0,1,0,0,1,0,0],
        [0,1,0,0,0,0,1,0],
        [0,1,1,1,1,1,1,0],
  ]

jopa = [
        [0,0,1,1,1,1,1,0],
        [0,1,1,1,1,1,1,1],
        [0,1,0,0,1,0,0,1],
        [0,1,1,1,1,1,1,1],
        [0,0,1,1,0,1,1,0],
        [0,0,1,1,1,1,1,0],
        [0,0,1,1,1,1,1,0],
        [0,0,1,0,1,0,1,0],
  ]

sad = [
        [0,0,1,1,1,1,0,0],
        [0,1,0,0,0,0,1,0],
        [1,0,1,0,0,1,0,1],
        [1,0,0,0,0,0,0,1],
        [1,0,0,1,1,0,0,1],
        [1,0,1,0,0,1,0,1],
        [0,1,0,0,0,0,1,0],
        [0,0,1,1,1,1,0,0],
  ]

#Thread with wait animation
class TAnimationThread (threading.Thread):
  def __init__(self, threadID):
    threading.Thread.__init__(self)
    self.threadID = threadID
    self.Continue=True
  def run(self):
    log("Starting animation")
    counter=0
    while (self.Continue):
      DrawAnimation(counter)
      counter+=1
      if counter==7:
        counter=0
      time.sleep(0.8)
  def stop(self):
    self.Continue = False
    log("Stopping animation")
def DrawAnimation(counter):
  #draw
  log("Drawing animation"+str(counter))
  if counter==0:
    for j in range(8):
      for i in range(8):
        if hourglass[j][i] == 1:
          device.pixel(i, 7-j, 1, redraw=False)
  elif counter>0 and counter<5:
    device.pixel(1+counter, 1, 1, redraw=False)
  elif counter==5 or counter==6:
    device.pixel(counter-2, 2, 1, redraw=False)
  else:
    device.clear()
  device.flush()
  

def fetch_traffic(url):
  global TrafficData
  try:
    response = urllib2.urlopen(url)
    TrafficData = int(response.read())
    log("Traffic fetched")
  except BaseException:
    TrafficData=-100
  log(TrafficData)
def fetch_currrate(url):
  global CurrencyData
  try:
    response = urllib2.urlopen(url)
    root = ET.fromstring(response.read())
    CurrencyData=float(root[16][5].text)/100
    log("Currency fetched")
  except BaseException:
    CurrencyData=-100
  log(CurrencyData)
  
#Let's begin  
log("Starting animation thread")
AnimationThread = TAnimationThread(1)
AnimationThread.start()

log("Starting traffic thread")
TrafficThread = threading.Thread(target=fetch_traffic, args=("http://tarasius.name/@/probki.txt",))
TrafficThread.start()

CurrencyThread = threading.Thread(target=fetch_currrate, args=("http://tarasius.name/@/currrate.xml",))
CurrencyThread.start()

# Get and show temperature
log("Starting weather fetch")
try:
  response = urllib2.urlopen("http://tarasius.name/@/weather.json")
  data = json.loads(response.read())
  response.close()
  temp = int(data["data"]["current_condition"][0]["temp_C"])
  precip = float(data["data"]["current_condition"][0]["precipMM"])
  temp1 = abs(temp)/10
  temp2 = abs(temp)%10
except BaseException:
  temp=-100
  precip=-100
  temp1=-100
  temp2=-100
log("Stopping animation thread")
AnimationThread.stop()
device.clear()
device.flush()

m = [[0 for x in range(8)] for x in range(64)]
if precip==-100:
#cloudcover was unavailable
  for j in range(8):
    for i in range(8):
      if sad[i][j] == 1:
	     m[j][7-i]=1
#draw cloudcover
elif precip>1.5:
  m[0][7] = 1
  m[2][7] = 1
  m[4][7] = 1
elif precip<=1.5 and precip>0.5:
  m[1][7] = 1
  m[3][7] = 1
elif precip<=0.5 and precip>0.1:
  m[2][7] = 1

if temp>-100:
  if temp<0:
    #Draw minus
    m[0][3] = 1
    if temp1==0:
      m[1][3]=1
      m[2][3]=1
      m[3][3]=1    
  if temp1<>0:
    #Draw first digit
    for j in range(digits.rows):
      for i in range(digits.columns):
        if digits.symbol[temp1][j][i] == 1:
          m[i+1][5-j] = 1
  #draw second digit
  for j in range(digits.rows):
    for i in range(digits.columns):
      if digits.symbol[temp2][j][i] == 1:
        m[i+5][5-j] = 1
  #draw celsium sign
  m[7][7] = 1

log("Drawing temperature")
for i in range(8):
  for j in range(8):
    if m[j][i]==1:
	  device.pixel(j, i, 1, redraw=False)

device.flush()
time.sleep(5)

#Show traffic
#Draw first digit
TrafficThread.join()
if TrafficData==10:
  for j in range(8):
    for i in range(8):
      if jopa[j][i] == 1:
	     m[10+i][j]=1
elif TrafficData==-100:
  for j in range(8):
    for i in range(8):
      if sad[j][i] == 1:
	     m[10+i][7-j]=1
else:
  #draw car
  m[10][6]=1
  m[10][3]=1
  m[11][7]=1
  m[11][6]=1
  m[11][5]=1
  m[11][4]=1
  m[11][3]=1
  m[11][2]=1
  m[12][5]=1
  m[12][4]=1
  
  TrafficData = TrafficData%10
  #draw second digit
  for j in range(digits.rows):
    for i in range(digits.columns):
      if digits.symbol[TrafficData][j][i] == 1:
        m[i+15][5-j]=1

log("Drawing traffic")
for step in range(9):
  device.clear()
  for i in range(8):
    for j in range(8):
      if m[i+step+2][j]==1:
        device.pixel(i, j, 1, redraw=False)
  device.flush()
  time.sleep(0.2)

time.sleep(5)

#Show currency  
CurrencyThread.join()
if CurrencyData==-100:
  for j in range(8):
    for i in range(8):
      if sad[j][i] == 1:
	     m[22+i][7-j]=1 
else:
  cur=str(CurrencyData)
  for d in range(5):
    if cur[d]==".":
      digit=-1
    else:
      digit=int(cur[d])   
    if digit==-1:
      m[22+d*5][0]=1
    else:
      for j in range(digits.rows):
        for i in range(digits.columns):
          if digits.symbol[digit][j][i] == 1:
            m[i+21+d*5][5-j]=1
log("Drawing currency")
for step in range(36):
  device.clear()
  for i in range(8):
    for j in range(8):
      if m[i+step+11][j]==1:
        device.pixel(i, j, 1, redraw=False)
  device.flush()
  time.sleep(0.2)

device.clear()
device.flush()
sys.exit()

Протестировав, что всё работает, повесим вызов информера на кнопку:

#!/bin/bash
echo "Button 1 pressed" | wall
python /leds+buttons/informer.py > /dev/null&
exit

Итогом всего этого должно получится что-то такое — 7 градусов, ясно, пробки 2 балла, курс евро к гривне 32.02:

Спасибо за внимание!