My Pi Description

My Experiences With the Raspberry Pi -- Tracking My Learning -- My Pi Projects

Friday, October 18, 2013

Can A Python Script With GUI Run From Root? Not A Trivial Question. Answer: Yes

Important Preface: While I mention graphing results of measurements in the next paragraph, this post is about the Graphical User Interface. Even though graphing and Graphical contain the word graph, this post concerns a problem related to the Graphical User's Interface and not any problem graphing results.
My last blog post is about my python program that measures temperature and then graphs the results. When that script runs, the user is faced with many questions to answer. All that typing gets quite tedious after a while so I thought about including a GUI (Graphical User's Interface) to deal with the questions. I started slow and included a pop-up window for the user to select a file name. I used the Tkinter module, included in python 2.7. Seemed simple enough. Of course, due to the RPi GPIO code, the script has to be run as root (prefacing the command with sudo). It didn't work!
I got an error: “Client is not authorized to connect to Server…..”. What!!! This error really floored me. I posted a question on the Stackoverflow forum and was told that I should use gksudo or its cousin gksu instead of sudo (gksudo is related to gksu as sudo is related to su).
What is gksudo/gksu? The Linux manual page says: "Their primary purpose is to run graphical commands that need root without the need to run an X terminal emulator and using su directly." The first part of that sentence sounded to be just what I needed. So, I played around with a simple script and ran it with gksudo. It worked fine. Then, I tried more complex scripts and I noticed they almost worked. Finally, I tried my large temperature measuring program run with gksudo (no GUI code in this script), and IT DID NOT WORK AT ALL.
Now, what? Every time I ran a script using gksudo (or gksu) I received this error message: "Xlib: extension "RANDR" missing on display "1.0". I had no idea what that meant but decided I had to find out how to get around it. Consequently, I did a search on RANDR. I found nothing to explain what RANDR is but I found quite a few forum posts where people asked about the exact same error message. This message pops up in a diverse universe of Linux applications run on a variety of platforms. There were a lot of answers, too. However, none had a solution I could use. Many of the answers said to ignore the error message. In my case, I knew I could not ignore the error - my script fails.
I next posted the question on the Raspberry Pi forum and the Stackoverflow forum. I explained about getting the no permission error with sudo, and the RANDR error with gksudo and the fact that my script fails. My responses on both forums was no response at all.
Finally, my son helped me out by pointing me to a forum post he found that gave me the answer that worked:
To the bottom of this file:
          pi@raspberrypi: ~/.bashrc
Add (first line optional):
          # allows X11 graphics to be run from root
          xhost +
The first line, of course is just a comment. What is xhost + doing here, and why?
The Linux manual page says that xhost is used to add and delete host or user names to the list allowed to make connections to the X server. By saying xhost +, we give every Tom, Dick, Harry, and Edward Snowden permission to connect to the X server. This includes root.
And, what is X server? From the Linux Information Project, "An X server is a program in the X Window System that runs on local machines (i.e., the computers used directly by users) and handles all access to the graphics cards, display screens and input devices (typically a keyboard and mouse) on those computers." The X window system used by the Pi's Linux distribution is X11.
Since we give everybody permission to make connections to the X server (xhost + does that), are we putting our Raspberry Pi in danger? If you have other users that have access to your Pi, they may be able to pop a window into your display space. But, if you are the only user there should be no danger of applying xhost +

Wednesday, October 2, 2013

Graphing Real Temperature Data Using RRDtool and PyRRD

My previous blog post, "RRDtool for Dummies Like Me" was a tutorial on the use of RRDtool and PyRRD to graph data from a python script. For simplicity, I used made-up data. This post talks about plotting real data taken with the Raspberry Pi.
In June and July of this year, I wrote five blog posts about measuring temperature with the Dallas Semiconductor DS18B20 One-wire temperature sensor. The temperature measurements were reported to the terminal screen and my 16x2 LCD display (seen in photo in blog header) so would not be saved. I did modify the script to send the results to a file but have not reported on that effort. Perhaps that would make a worthwhile post for later.
It just seemed natural to want to plot that data to see the change of temperature with time. That led to my investigation of RRDtool and PyRRD, and to the tutorial in my last post. This post combines temperature measurements with plotting in a python script.
I have two DS18uB20 temperature probes in my system. One looks like a small transistor and sits on the breadboard and the other is at the end of a cable. For the graph below, I put the cable sensor in a glass of hot tap water while the breadboard sensor measured ambient air temperature.
The python code follows.
#!/usr/bin/python
# This script is a continuation of my series of temperature measurements
# usinjg the Dallas DS18B20 One-wire temperature sensor.
# See my blog for previous versions.
#
# This version adds graphing the results using RRDtool and PyRRD
#
# MJL -- www.thepiandi.blogspot.com -- 10/3/2013
import os
from datetime import datetime
import time
import subprocess
import sys
from Tkinter import Tk
from tkFileDialog import asksaveasfilename
from LCD_2X16 import *
from pyrrd.rrd import DataSource, RRA, RRD
from pyrrd.graph import DEF, CDEF, VDEF, LINE, AREA, GPRINT, COMMENT
from pyrrd.graph import ColorAttributes
from pyrrd.graph import Graph
def loadmodules():
""" Checks to see if the 1-wire modules are loaded. If not, loades them."""
err = 'error'
while err != '':
modloaded = subprocess.Popen(['lsmod'], stdout = subprocess.PIPE, stderr = subprocess.PIPE)
out, err = modloaded.communicate()
if 'w1_therm' not in out or 'w1_gpio' not in out:
print '1-Wire modules had to be loaded. Waiting 10 seconds'
print
os.system('sudo modprobe w1-gpio')
os.system('sudo modprobe w1-therm')
time.sleep(10)
def read_temp_raw():
""" Uses Popen to open then read contents of w-slave. Returns the contents of the file as a two line list.
If the file cannot be read the 1-wire modules are unloaded and loaded again. This is repeated
three times until the file is read successfully. After three times, the program
is stopped."""
global glitches
trials = 3
catdata = subprocess.Popen(['cat', device_file], stdout = subprocess.PIPE, stderr = subprocess.PIPE)
out, err = catdata.communicate()
while err != "" and trials:
print "Got a glitch - trying to recover"
trials -= 1
time.sleep(1)
os.system('sudo modprobe -r w1-gpio')
os.system('sudo modprobe -r w1-therm')
time.sleep(1)
os.system('sudo modprobe w1-gpio')
os.system('sudo modprobe w1-therm')
time.sleep(10)
glitches += 1
catdata = subprocess.Popen(['cat', device_file], stdout = subprocess.PIPE, stderr = subprocess.PIPE)
out, err = catdata.communicate()
if trials:
out_decode = out.decode('utf-8')
lines = out_decode.split('\n')
return lines
else:
raise(IOError)
def print2display(temperature, sen):
""" Prints the temperature and time to the 16 character
by two line LCD display. Also prints to the stdio"""
time.sleep(.01)
if sen == '0':
lcd_byte(LCD_LINE_1, LCD_CMD) # Print to top line
string1 = "Brdbd: " + str(temperature) + " degF"
time.sleep(.01)
lcd_string(string1)
if sen =='1':
lcd_byte(LCD_LINE_2, LCD_CMD) # Print to bottom line
string1 = "Cable: " + str(temperature) + " degF"
time.sleep(.01)
lcd_string(string1)
def check_switch():
"""Checks to see if the breadboard switch was pressed. If so it generates a keyboard interrupt
as if CTRL-C was pressed. Switch must be held down for at least .2 seconds to be reconized"""
input_switch = GPIO.input(SwitchInput) # Look for breadboard switch
if not input_switch:
time.sleep(.2)
input_switch = GPIO.input(SwitchInput) # Look for breadboard switch again
if not input_switch: # Must still be pressed
print
print "Switch Pressed"
raise(KeyboardInterrupt)
def check_title(title):
"""Makes sure that all of the space characters are escaped with the backspace character"""
cnt_spaces = title.count(' ')
title_good = True
start_pos = 0
while cnt_spaces:
index_space = title.find(' ', start_pos)
if title[index_space -1] != '\\':
title_good = False
start_pos = index_space + 1
cnt_spaces -= 1
return title_good
# --------------------------------------------------------------------------------------------------
# Main Program
glitches = 0
device_id = ('28-00000400d39d' ,'28-000004986cbb')
prompt = 'Enter the measurement time interval in minutes (integer values only): '
prompt1 = 'Enter 0 for sensor on breadboard, 1 for cable sensor, 2 for both: '
prompt2 = 'Enter the maximum number of measurements: '
prompt3 = 'What are we measuring with the breadboard sensor? Default is Breadboard: '
prompt4 = 'What are we measuring with the cable sensor? Default is Cable: '
prompt5 = 'Enter comment if desired (Remember to escape punctuation): '
prompt6 = 'Enter a name for the file. No spaces, extension, or directory: '
prompt7 = 'Enter a title for the graph (Remember to escape spaces and punctuation): '
prompt8 = 'Satisfied with the inputs? Y or N: '
prompt9 = 'OK to proceed? If not, we quit. Y or N: '
prompt10 = 'Graph background color: Enter 0 for black, 1 for white, 2 for both: '
prompt11 = 'Choose height of graph in the range if 100 to 400 pixels: '
short_wait = .1
lcd_byte(0x01, LCD_CMD) # Clear Display
loadmodules() # Check to see if 1-Wire modules are loaded.
# Operator Inputs
good_inputs = False
while not good_inputs:
# Choose which sensor or both
print
sensor = ''
while sensor != '0' and sensor != '1' and sensor !='2':
sensor = raw_input(prompt1)
print
if sensor == '0':
print "You have choosen the breadboard sensor"
elif sensor == '1':
print "You have choosen the cable sensor"
elif sensor == '2':
print "You have choosen both sensors"
# Legend Titles
print
if sensor == '0' or sensor == '2':
measure_what0 = raw_input(prompt3)
if measure_what0 == '':
measure_what0 = 'Breadboard'
if sensor == '1' or sensor == '2':
measure_what1 = raw_input(prompt4)
if measure_what1 == '':
measure_what1 = 'Cable'
# Graph Title
print
title_good = False
while not title_good:
title_it = ''
while not title_it:
title_it = raw_input(prompt7)
title_good = check_title(title_it)
if not title_good:
print "\n\tYou forgot to escape all of the space characters, try again: "
# Comment
print
comment = raw_input(prompt5)
cmt = COMMENT(comment)
# Graph Colors
print
background = ''
while background != '0' and background != '1' and background !='2':
background = raw_input(prompt10)
# Height of Graph
print
how_high = 0
while how_high < 100 or how_high > 400:
try:
how_high = abs(int(raw_input(prompt11)))
except:
pass
# File names
print
directory = '/home/pi/Documents/PythonProjects/TempProbe/TempResults/'
filename = raw_input(prompt6)
graphfile_blk = directory + filename + '_black.png'
graphfile_wht = directory + filename + '_white.png'
if sensor == '0' or sensor == '2':
rrdfile0 = directory + filename + '_bread.rrd'
if sensor == '1' or sensor == '2':
rrdfile1 = directory + filename + '_cable.rrd'
# Maximum number of measurements
print
max_measurements = 0
while not max_measurements:
try:
max_measurements = abs(int(raw_input(prompt2)))
except:
pass
# Measurement Interval
print
measurement_interval = 0
while not measurement_interval:
try:
measurement_interval = abs(int(raw_input(prompt)))
except:
pass
# Satisfied With The Inputs?
print
response = ""
while response != 'y' and response != 'Y' and response != 'n' and response != 'N':
response = raw_input(prompt8)
if response == 'Y' or response == 'y':
good_inputs = True
# Good to Proceed?
print
proceed = ''
while proceed != 'y' and proceed != 'Y' and proceed != 'n' and proceed != 'N':
proceed = raw_input(prompt9)
print
measurement_interval *= 60
start_time = int(time.time() / measurement_interval) * measurement_interval
next_meas_time= start_time + measurement_interval
# Breadboard RRD Setup
if sensor == '0' or sensor == '2':
dataSources = []
roundRobinArchives = []
dataSource = DataSource(dsName='Breadboard', dsType='GAUGE', heartbeat=int(1.5 * measurement_interval))
dataSources.append(dataSource)
roundRobinArchives.append(RRA(cf='LAST', xff=0.5, steps=1, rows=max_measurements))
breadboard = RRD(rrdfile0, step=measurement_interval, ds=dataSources, rra=roundRobinArchives, start=start_time)
breadboard.create(debug=False)
# Cable RRD Setup
if sensor == '1' or sensor == '2':
dataSources = []
roundRobinArchives = []
dataSource = DataSource(dsName='Cable', dsType='GAUGE', heartbeat=int(1.5 * measurement_interval))
dataSources.append(dataSource)
roundRobinArchives.append(RRA(cf='LAST', xff=0.5, steps=1, rows=max_measurements))
cable = RRD(rrdfile1, step=measurement_interval, ds=dataSources, rra=roundRobinArchives, start=start_time)
cable.create(debug=False)
# Breadboard Graph Setup
if sensor == '0' or sensor == '2':
bread_def = DEF(rrdfile=rrdfile0, vname='Bread_data', dsName='Breadboard', cdef='LAST')
bread_line = LINE(defObj=bread_def, color='#00FF00', legend=measure_what0 + ' Temperature')
bread_aver = VDEF(vname='Bread_aver', rpn='%s,AVERAGE' % bread_def.vname)
bread_val = GPRINT(bread_aver, 'Average ' + measure_what0 + ' Temperature: %6.2lf Degrees F')
# Cable Graph Setup
if sensor == '1' or sensor == '2':
cable_def = DEF(rrdfile=rrdfile1, vname='Cable_data', dsName='Cable', cdef='LAST')
cable_line = LINE(defObj=cable_def, color='#FF0000', legend=measure_what1 + ' Temperature')
cable_aver = VDEF(vname='Cable_aver', rpn='%s,AVERAGE' % cable_def.vname)
cable_val = GPRINT(cable_aver, 'Average ' + measure_what1 + ' Temperature: %6.2lf Degrees F')
# Define Graph Colors
# black background:
black_bkgnd = ColorAttributes()
black_bkgnd.back = '#000000'
black_bkgnd.canvas = '#333333'
black_bkgnd.shadea = '#000000'
black_bkgnd.shadeb = '#111111'
black_bkgnd.mgrid = '#CCCCCC'
black_bkgnd.axis = '#FFFFFF'
black_bkgnd.frame = '#0000AA'
black_bkgnd.font = '#FFFFFF'
black_bkgnd.arrow = '#FFFFFF'
# white background:
white_bkgnd = ColorAttributes()
white_bkgnd.back = '#FFFFFF'
white_bkgnd.canvas = '#EEEEEE'
white_bkgnd.shadea = '#000000'
white_bkgnd.shadeb = '#111111'
white_bkgnd.mgrid = '#444444'
white_bkgnd.axis = '#000000'
white_bkgnd.frame = '#0000AA'
white_bkgnd.font = '#000000'
white_bkgnd.arrow = '#000000'
# Let's make some measurements and graph them
try:
if proceed == 'N' or proceed == 'n':
raise(KeyboardInterrupt)
print 'First Measurement Will Be Made At: ' + time.asctime(time.localtime(next_meas_time))
print
while max_measurements:
time_now = time.time()
while time_now < next_meas_time:
check_switch()
time.sleep(0.5)
time_now = time.time()
if sensor == '0' or sensor == '2':
device_file = '/sys/bus/w1/devices/' + device_id[0] + '/w1_slave'
measurement_good = False
while measurement_good == False:
lines = read_temp_raw()
if 'YES' in lines[0]:
equals_pos = lines[1].find('t=')
if equals_pos != -1:
measurement_good = True
temp_string = lines[1][equals_pos +2:]
Deg_C = float(temp_string)/1000.0
Deg_F = round(Deg_C * 1.8 + 32.0, 1)
timenow = datetime.now()
print timenow.strftime("%A, %B %d, %I:%M:%S %p") + (", the Breadboard sensor temperature is %3.1f") %(Deg_F) + u"\xB0" +"F"
print2display(Deg_F, '0')
if measurement_good == False:
time.sleep(short_wait)
breadboard.bufferValue(next_meas_time, str(Deg_F))
breadboard.update(debug = False)
if sensor == '1' or sensor == '2':
device_file = '/sys/bus/w1/devices/' + device_id[1] + '/w1_slave'
measurement_good = False
while measurement_good == False:
lines = read_temp_raw()
if 'YES' in lines[0]:
equals_pos = lines[1].find('t=')
if equals_pos != -1:
measurement_good = True
temp_string = lines[1][equals_pos +2:]
Deg_C = float(temp_string)/1000.0
Deg_F = round(Deg_C * 1.8 + 32.0, 1)
timenow = datetime.now()
print timenow.strftime("%A, %B %d, %I:%M:%S %p") + (", the Cable sensor temperature is %3.1f") %(Deg_F) + u"\xB0" +"F"
print2display(Deg_F, '1')
if measurement_good == False:
time.sleep(short_wait)
cable.bufferValue(next_meas_time, str(Deg_F))
cable.update(debug = False)
next_meas_time += measurement_interval
max_measurements -= 1
gb = Graph(graphfile_blk, start = start_time, end = next_meas_time - measurement_interval, color = black_bkgnd, vertical_label='Degrees\ F', width=600, height=how_high, title=title_it)
gw = Graph(graphfile_wht, start = start_time, end = next_meas_time - measurement_interval, color = white_bkgnd, vertical_label='Degrees\ F', width=600, height=how_high, title=title_it)
if sensor == '0':
gb.data.extend([bread_def, bread_line, bread_aver, bread_val])
gw.data.extend([bread_def, bread_line, bread_aver, bread_val])
if sensor == '1':
gb.data.extend([cable_def, cable_line, cable_aver, cable_val])
gw.data.extend([cable_def, cable_line, cable_aver, cable_val])
if sensor == '2':
gb.data.extend([bread_def, bread_line, bread_aver])
gb.data.extend([cable_def, cable_line, cable_aver])
gb.data.extend([bread_val, cable_val])
gw.data.extend([bread_def, bread_line, bread_aver])
gw.data.extend([cable_def, cable_line, cable_aver])
gw.data.extend([bread_val, cable_val])
if comment:
gb.data.extend([cmt])
gw.data.extend([cmt])
if background == '0' or background == '2':
gb.write()
if background == '1' or background == '2':
gw.write()
except(KeyboardInterrupt):
print
except(IOError):
print
print 'Three tries and we are out of here'
except:
print
print "Unexpected Error: ", sys.exc_info()[1]
print
print "See you later, Glitches = %d" % glitches
lcd_byte(0x01, LCD_CMD) # Clear Display
GPIO.cleanup()
print
print "start time: ", start_time
print "last measurement: ", next_meas_time
run_time = next_meas_time - start_time - measurement_interval
run_days = run_time / 86400
run_hours = run_time % 86400 / 3600
run_minutes = run_time % 86400 % 3600 / 60
total_measurements = run_time / measurement_interval
print
print 'Total Run Time: %2d days, %2d hours, %2d minutes' %(run_days, run_hours, run_minutes)
print 'Total Number of Measurements Per Device: ' + str(total_measurements)
print
The script is an evolution of code I reported on previously. All the One-wire temperature code was discussed in my June and July posts. The plotting code grew out of the code in the last post. The code for the LCD display came from my posts of March and April of this year.
My python script may seem pretty large, but I wanted to make it useful, and to incorporate all of the hardware connected to the Pi that makes sense to use. These are some of the features:
  1. Choose to use either sensor, breadboard or cable, or both sensors.
  2. Choose graph characteristics:
    1. If desired, give the breadboard, and/or cable sensor another name.
    2. Give the graph a title - mandatory
    3. If desired, enter a comment that prints on the graph.
    4. Select graph with either black or white background, or choose to make both graphs. White background is best to print. It's easier on printer ink. I think black looks better on the screen, like as seen in the sample graph.
    5. Enter the height from 100 to 400 pixels. If you are plotting from one sensor of a temperature that does not change much it is better to choose a lower value for the height. Otherwise, RRDtool may repeat the values on the Y axis, like: 70, 70, 70, 71, 71, 71, 72, 72, 72, etc.
    6. Choose a file name. Up to four files will be created with that name: an .rrd file for each sensor chosen, and a .png file for each background color chosen.
  3. Choose the maximum number of measurements to make for each sensor. The script will stop once that number of measurements (for each sensor) is reached. In the future, I may modify the script so that once the maximum is reached, new measurements replace the oldest measurements. Remember, this is how a round robin database works.
  4. Choose the time interval between measurements, in minutes. For each sensor: The first measurement will be taken at the first time corresponding to the time interval. For example, if the time is now 37 minutes past the hour, and 15 minutes is selected as the time interval, the first measurement will be at 45 minutes past the hour. If you select the time interval to be one day, the first measurement will be made at midnight. When making the plot, RRDtool will only place a measurement point at a time corresponding to the nearest measurement interval. Again, as an illustration, if we choose a day as the time interval, and we made a measurement at 8PM, the plot will show the measurement at midnight, and --- unless it is the first measurement, the value will be different. That value is not even the linear interpolation of the measured value. See my past post. Strange, but that is the way the tools work.
  5. After entering all of the user information, you have a chance to review and enter the information again, if you are not satisfied.
  6. You can choose not to proceed after entering the user information. This will stop the script.
  7. As well as plotting the temperature data, the average temperature for for each sensor, is calculated and displayed below the plot.
  8. The current measured temperature value, for each sensor, is displayed on the 16 character by 2 line LCD display.
  9. All measurements are sent to the terminal along with the time of the measurement.
  10. There are three ways to stop the script:
    1. When we have reached the maximum number of measurements.
    2. Pressing CTRL-C from the keyboard (Issuing a keyboard interrupt).
    3. Hitting the switch on the breadboard.
There were two things that did not work out as I wished. The first is that RRDtool/PyRRD makes adding some text to the graph somewhat difficult. The title requires you to escape all spaces and probably some punctuation. A space needs to be "\ " in the variable (title_it). It's so easy to forget to add that backslash that I tried to have the script do that for you. It would seem that the following would work:
      title_it = title_it.replace(" ", "\ ")
It doesn't. It replaces a space with "\\ ". This is a title prints on the graph: This\ is\ a\ title, which looks pretty dumb. The following does not work either, giving the same result:
      title_it = title_it.replace(" ", r"\ ")
Since I had no luck adding the backslashes before the spaces, I have the script, in the function check_title, check that the user added the backslashes. Note that the user type "\ " for each space, but the function looks for "\\" before each space (line 103). That makes sense, the first backslash escapes the second backslash.
Here's the second thing that did now work out: I wanted to handle all of that user input with dialog boxes. Using stdio is a bit clunky and if you decide to change some input, you have to enter everything again. Besides, I wanted to learn to incorporate those features. Python has a nice module called Tkinter which brings the graphic users interface to python and other programming languages.
I experimented with a simple program to bring up a dialog box to enter or choose a file name. It worked just fine. I could run it from a Idle Python shell, or from the stdio. No problem. Then I incorporated some GPIO code which meant I had to run the script as root. Now it failed. It threw an exception saying, "Client is not authorized to connect to the Server .....". By client, I assume it means root. Obviously, if the user is me, it works fine, but not if the user is root.