首页 > 解决方案 > TclError: 没有显示名称,也没有 $DISPLAY 环境变量

问题描述

我使用 Raspberry Pi 和 DSLR cam 以及 Python、TkInter 和 gPhoto 库开发了自己的 photobooth 软件。photobooth 软件非常灵活,因为它使用配置文件并支持多种类型的输入按钮和 LED 以及实时预览和随机照片库(作为屏幕保护程序)。我还可以操作相机设置以进行实时预览和拍摄最终照片。

大多数时候,该软件运行相当稳定。但是有些事情(主要是晚上一次)我遇到了一个无法修复的未处理异常,并且需要手动重新启动 photobooth 软件。拍摄最终照片后出现未处理的异常。由于未处理的异常随机出现,我无法自己重现它。

未处理的异常总是相同的:

回溯(最近一次通话最后):

主要尝试中的文件“/home/pi/photoBooth/scripts/photoBoothGUI.pyw”,第 476 行:ThreadedClient(Tk()).start()

文件“/usr/lib/python2.7/lib-tk/Tkinter.py”,第 1823 行,在init self.tk = _tkinter.create(screenName, baseName, className, interactive, wantobjects, useTk, sync, use)

TclError: 没有显示名称,也没有 $DISPLAY 环境变量

作为一个快速的'n'dirty解决方案,我已经尝试将大多数可能在try-and-catch-block中抛出异常的函数调用。但这并没有解决上述异常。

这是 photoBoothGui.pyw 的代码:

#!/usr/bin/python
# -*- coding: utf-8 -*-

#Imports
import Queue, threading
import sys, os, traceback
import time, datetime, random

from gPhotoWrapper import GPhotoWrapper
from photoUtils import PhotoUtils

from RPi import GPIO
from Tkinter import Tk, Frame, Label, BOTH, Button, CENTER
from tkFont import Font
from PIL import Image, ImageTk, ImageOps
from configparser import ConfigParser
from enum import Enum
from shutil import copyfile


#Class Definitions

class MessageType(Enum):
    IMAGE = 0
    TEXT = 1
    IMAGE_TEXT = 2

class Message():

    def __init__(self, mType, text = None, fontSize=None, image=None):
        self.mType = mType
        self.text = text
        self.fontSize = fontSize
        self.image = image

class GUI(Frame):

    def __init__(self, parent, queue, config):

        #Init root window and root queue
        Frame.__init__(self, parent)
        self.parent = parent
        self.queue = queue

        #Configure font
        self.labelFont = Font(family=config.get('Font','Family'), size=config.get('Font','SizeTitle'))
        self.labelFont.configure(weight='bold') if config.getboolean('Font','Bold') else None

        #Configure main label    
        self.mainLabel = Label(self, compound=CENTER, font=self.labelFont, text=config.get('RootWindow','AppTitle'), wraplength=1280)
        self.mainLabel.configure(background=config.get('RootWindow','BackgroundColor'))
        self.mainLabel.configure(foreground=config.get('RootWindow','ForegroundColor'))
        self.mainLabel.pack(fill=BOTH, expand=1)
        self.pack(fill=BOTH, expand=1)

        #Configure root window
        self.parent.title(config.get('RootWindow','AppTitle'))
        self.parent.attributes('-fullscreen', config.getboolean('RootWindow','Fullscreen'))
        self.config(cursor='none')


    def processIncoming(self):

        while self.queue.qsize():
            try:
                msg = self.queue.get(0)
                if msg.mType == MessageType.TEXT:
                    self.showText(msg.text, msg.fontSize)
                    self.resetImage()
                elif msg.mType == MessageType.IMAGE:
                    self.showImage(msg.image)
                    self.resetText()
                elif msg.mType == MessageType.IMAGE_TEXT:
                    self.showImage(msg.image)
                    self.showText(msg.text, msg.fontSize)
                else:
                    print('Message Type "' + str(msg.mType) + '" not implemented, yet')
            except:
                pass

    def showImage(self, img):
        try:
            photoImage = ImageTk.PhotoImage(img)
            self.mainLabel.configure(image = photoImage)
            self.mainLabel.image = photoImage
        except:
            pass

    def showText(self, textToShow, fontSize):
        try:
            self.mainLabel.configure(text=textToShow)
            self.labelFont.configure(size=fontSize)
        except:
            pass

    def resetImage(self):
        self.mainLabel.configure(image='')

    def resetText(self):
        self.mainLabel.configure(text='')

    def getMaxImageWidth(self):
        return self.mainLabel.winfo_width();

    def getMaxImageHeight(self):
        return self.mainLabel.winfo_height();

class ClientState(Enum):
    BUSY = 0
    READY_CAPTURE = 1

class ThreadedClient:

    def __init__(self,window):

        #Save a reference to root window
        self.window = window

        #Init config object
        self.config = ConfigParser()
        try:
            if not self.config.read(sys.argv[1]):
                raise IOError('File "' + sys.argv[1] + '" not found!') 
        except IndexError, IOError:
            if not self.config.read(os.path.splitext(os.path.basename(sys.argv[0]))[0]+'.config'):
                print('Loading config file failed.')
                sys.exit()

        #Setup GPhoto Wrapper
        self.photoWrapper = GPhotoWrapper()

        #Setup photo utils
        self.photoUtils = PhotoUtils()

        #Create queue
        self.queue = Queue.Queue()

        #Setup the GUI
        self.gui = GUI(window, self.queue , self.config)

        #Bind keypress event to GUI
        self.gui.parent.bind("<Return>", self.readKeypressCapture)

        #Start periodic call in GUI
        self.periodicCall(self.config.getint('RootWindow','UpdateIntervall'))

        #Setup the GPIO 
        self.setupGPIO()

        #Setup Slideshow thread
        self.setupThreads()

        #Set the application to be ready for taking the first photo
        self.setClientState(ClientState.READY_CAPTURE)

    def start(self):
        self.window.protocol('WM_DELETE_WINDOW', self.close)
        self.window.mainloop()

    def close(self):

        #Cancel background thread
        self.threadSlideshow.cancel()

        #Reset GPIO pins
        GPIO.cleanup()

        #Close GUI
        self.window.destroy()

    def setClientState(self, clientState):
        self.state = clientState

    def periodicCall(self, intervall):

        #Process incoming queue messages
        self.gui.processIncoming()

        #Restart periodic call every x ms
        self.window.after(intervall, self.periodicCall, intervall)

    def setupGPIO(self):

        #Setup GPIO pin layout
        GPIO.setmode(GPIO.BCM)
        GPIO.setwarnings(False)
        bounceTime = self.config.getint('RPi GPIO','Bouncetime')

        #Capturing: Setup GPIO pins and wait for capture events
        GPIO.setup(self.config.getint('RPi GPIO','ButtonCaptureGPIO'), GPIO.IN, pull_up_down=GPIO.PUD_UP)
        GPIO.add_event_detect(self.config.getint('RPi GPIO','ButtonCaptureGPIO'), GPIO.FALLING, callback=self.readButtonCapture, bouncetime=bounceTime)

        if self.config.getboolean('RPi GPIO','EnableRadioCapture'):
            GPIO.setup(self.config.getint('RPi GPIO','ButtonCaptureRadioGPIO'), GPIO.IN, pull_up_down=GPIO.PUD_UP)
            GPIO.add_event_detect(self.config.getint('RPi GPIO','ButtonCaptureRadioGPIO'), GPIO.FALLING, callback=self.readButtonCapture, bouncetime=bounceTime)

        #LED: Setup GPIO pin and initialize LED status to high signal
        if self.config.getboolean('RPi GPIO','EnableCaptureLED'):
            GPIO.setup(self.config.getint('RPi GPIO','LedCaptureGPIO'), GPIO.OUT)
            GPIO.output(self.config.getint('RPi GPIO','LedCaptureGPIO'), GPIO.HIGH)

    def setupThreads(self):

        if self.config.getboolean('Slideshow','EnableSlideshow'):
            self.threadSlideshow = RepeatingTimer(interval=self.config.getint('Slideshow','DisplayDuration'), target=self.showRandomImage)
        else:
            self.threadSlideshow = RepeatingTimer(interval=self.config.getint('Slideshow','DisplayDuration'), target=self.dummy)

    def readButtonCapture(self, channel):
        threading.Thread(target=self.captureImageButtonThread, args=[channel]).start()

    def readKeypressCapture(self, key):
        threading.Thread(target=self.captureImageThread).start()

    def captureImageButtonThread(self, channel):

        try:

            #Check the button to be pressed curtain amount of time 
            timeThreshold = time.time() + (self.config.getint('RPi GPIO','Threshold')/1000)
            while timeThreshold > time.time():
                if GPIO.input(channel): return

            self.captureImageThread()

        except:

            #Show Error Message
            self.queue.put(Message(mType=MessageType.TEXT, text=self.config.get('Text','CamFailed'), fontSize=self.config.getint('Font','SizeMessage')))

    def captureImageThread(self):

        if self.state == ClientState.BUSY: return

        try:

            #Stop background threads (LED and Slideshow)
            self.threadSlideshow.cancel()
            if self.config.getboolean('RPi GPIO','EnableCaptureLED'):
                GPIO.output(self.config.getint('RPi GPIO','LedCaptureGPIO'), GPIO.LOW)

            #Capture image
            self.captureImage()

            #Restart background threads (LED and Slideshow)
            self.threadSlideshow.startAfter(self.config.getint('Slideshow','StartTime'))
            if self.config.getboolean('RPi GPIO','EnableCaptureLED'):
                GPIO.output(self.config.getint('RPi GPIO','LedCaptureGPIO'), GPIO.HIGH)

        except:

            #Show Error Message
            self.queue.put(Message(mType=MessageType.TEXT, text=self.config.get('Text','CamFailed'), fontSize=self.config.getint('Font','SizeMessage')))

    def captureImage(self):

        #Check whether application is ready to capture new image
        if self.state == ClientState.BUSY: return

        #Switch applications' state
        self.setClientState(ClientState.BUSY)

        #Read config values for capture mode
        fontSizeMessage = self.config.getint('Font','SizeMessage')
        fontSizeCountdown = self.config.getint('Font','SizeCountdown')
        isLivePreview = self.config.getboolean('GPhotoParameter','LivePreview')

        #Establish connection to camera
        if self.photoWrapper.connect():

            #Start live preview
            if isLivePreview:
                isoKey = self.config.get('GPhotoParameter','IsoSpeed')
                isoValuePrev = self.config.get('GPhotoParameter','IsoSpeedValuePreview')
                apertureKey = self.config.get('GPhotoParameter','Aperture')
                apertureValuePrev = self.config.get('GPhotoParameter','ApertureValuePreview')
                apertureValueCapt = isoValueCapt = None

                if self.config.getboolean('GPhotoParameter','FixCaptureSettings'):
                    apertureValueCapt = self.config.get('GPhotoParameter','ApertureValueCapture')
                    isoValueCapt = self.config.get('GPhotoParameter','IsoSpeedValueCapture')

                if not self.photoWrapper.startPreview(isoKey, isoValuePrev, isoValueCapt, apertureKey, apertureValuePrev, apertureValueCapt):

                    #Close connection to camera
                    self.photoWrapper.disconnect()

                    #Show Error Message
                    self.queue.put(Message(mType=MessageType.TEXT, text=self.config.get('Text','CamNotReady'), fontSize=fontSizeMessage))

                    #Switch applications' state
                    self.setClientState(ClientState.READY_CAPTURE)

                    return

            #Start countdown for posing
            countdown = self.config.getfloat('Photos','Countdown')
            stepSize = self.config.getfloat('RootWindow','UpdateIntervall')/1000

            while countdown > 0:
                number = int(countdown)
                textMsg = number if number>0 else self.config.get('Text','CamPhotoShot')
                textSize = fontSizeCountdown if number>0 else fontSizeMessage
                if isLivePreview:
                        prevImage = self.photoWrapper.capturePreview()
                        if prevImage != False: self.queue.put(Message(mType=MessageType.IMAGE_TEXT, text=textMsg, fontSize=textSize, image=ImageOps.mirror(prevImage)))
                        else: self.queue.put(Message(mType=MessageType.TEXT, text=textMsg, fontSize=textSize))
                else: self.queue.put(Message(mType=MessageType.TEXT, text=textMsg, fontSize=textSize))
                countdown -= stepSize
                time.sleep(stepSize)

            #Stop live preview
            if not self.photoWrapper.stopPreview():

                #Close connection to camera
                self.photoWrapper.disconnect()

                #Show Error Message
                self.queue.put(Message(mType=MessageType.TEXT, text=self.config.get('Text','CamNotReady'), fontSize=fontSizeMessage))

                #Switch applications' state
                self.setClientState(ClientState.READY_CAPTURE)

                return

            #Capture image
            camFilePath = self.photoWrapper.captureImage()
            if camFilePath:

                #Download image
                self.queue.put(Message(mType=MessageType.TEXT, text=self.config.get('Text','CamPhotoDownload'), fontSize=fontSizeMessage))
                try:
                    unixTimeStamp = self.photoWrapper.getConfigValue(self.config.get('GPhotoParameter','CameraDateTime'))
                    localFilePath = os.path.join(self.config.get('Photos','PathLocal'), self.fileName(unixTimeStamp))
                except:
                    localFilePath = None

                if (localFilePath and self.photoWrapper.downloadImage(camFilePath,localFilePath)):

                    try:

                        #Process image
                        self.queue.put(Message(mType=MessageType.TEXT, text=self.config.get('Text','CamPhotoProcessing'), fontSize=fontSizeMessage))
                        origImage = Image.open(localFilePath)

                        #Resize image in two steps (best trade-off between speed and quality)
                        scaledImage = self.photoUtils.resizeAspectRatio(origImage, self.gui.getMaxImageHeight())
                        previewImage = scaledImage.copy()

                        #Save (resized) image to webservers' photo directory
                        if self.config.getboolean('Photos','CopyToWebServer'):
                            self.queue.put(Message(mType=MessageType.TEXT, text=self.config.get('Text','CamPhotoSave'), fontSize=fontSizeMessage))
                            targetOrig = os.path.join(self.config.get('Photos','PathWebServerOrig'), os.path.basename(localFilePath))
                            targetThumb = os.path.join(self.config.get('Photos','PathWebServerThumb'), os.path.basename(localFilePath))
                            if self.config.getboolean('Photos','SaveScaledImage'):
                                scaledImage = self.photoUtils.resizeFixed(origImage, self.config.getint('Photos','OrigHeight'), self.config.getint('Photos','OrigWidth'))
                                scaledImage.save(targetOrig, quality=self.config.getint('Photos','OrigQuality'))
                            else: copyfile(localFilePath, targetOrig)
                            scaledImage = self.photoUtils.resizeFixed(scaledImage, self.config.getint('Photos','ThumbHeight'), self.config.getint('Photos','ThumbWidth'))
                            scaledImage.save(targetThumb)

                        #Show image
                        self.queue.put(Message(mType=MessageType.IMAGE, image=previewImage))

                    except:

                        self.queue.put(Message(mType=MessageType.TEXT, text=self.config.get('Text','CamPhotoProcessFailed'), fontSize=fontSizeMessage))

                else: self.queue.put(Message(mType=MessageType.TEXT, text=self.config.get('Text','CamPhotoDownloadFailed'), fontSize=fontSizeMessage))
            else: self.queue.put(Message(mType=MessageType.TEXT, text=self.config.get('Text','CamNotReady'), fontSize=fontSizeMessage))
        else: self.queue.put(Message(mType=MessageType.TEXT, text=self.config.get('Text','CamNotDetected'), fontSize=fontSizeMessage))

        #Close connection to camera
        self.photoWrapper.disconnect()

        #Switch applications' state
        self.setClientState(ClientState.READY_CAPTURE)

    def showRandomImage(self):

        #Check client state
        if self.state == ClientState.BUSY: return False
        self.setClientState(ClientState.BUSY)

        #Show loading message
        self.queue.put(Message(mType=MessageType.TEXT, text=self.config.get('Text','PleaseWait'), fontSize=self.config.get('Font','SizeMessage')))

        #Get random image from slideshow directory
        if (self.config.getboolean('Slideshow','EnableAdvertisements')):
            if(random.randint(0,3) > 0):
                filePath = self.config.get('Slideshow','SlideShowFolder')
            else:
                filePath = self.config.get('Slideshow','AdvertisementFolder')
        else:
            filePath = self.config.get('Slideshow','SlideShowFolder')

        if os.listdir(filePath):

            try:

                fileName = random.choice([x for x in os.listdir(filePath) if os.path.isfile(os.path.join(filePath, x))])
                filePathAndName = os.path.join(filePath, fileName)

                #Resize image in two steps (best trade-off between speed and quality)
                scaledImage = self.photoUtils.resizeAspectRatio(Image.open(filePathAndName), self.gui.getMaxImageHeight())

                #Show Image
                self.queue.put(Message(mType=MessageType.IMAGE, image=scaledImage))

            except:

                self.queue.put(Message(mType=MessageType.TEXT, text=self.config.get('RootWindow','AppTitle'), fontSize=self.config.get('Font','SizeTitle')))

        #Change Client State
        self.setClientState(ClientState.READY_CAPTURE)

        return True

    def reset(self):
        if not self.state == ClientState.READY_CAPTURE: return
        self.queue.put(Message(mType=MessageType.TEXT, text=self.config.get('RootWindow','AppTitle'), fontSize=self.config.get('Font','SizeTitle')))

    def fileName(self, unixTimeStamp):
        prefix = self.config.get('Photos','Prefix')
        return prefix + time.strftime('%Y%m%d_%H%M%S', time.localtime(unixTimeStamp)) + '.jpg'

    def dummy():
        return False

class RepeatingTimer(object):

    def __init__(self, interval, target, onCancel=None, *args, **kwargs):
        self.interval = interval
        self.target = target
        self.onCancel = onCancel
        self.args = args
        self.kwargs = kwargs
        self.timer = None
        self.isAlive = False

    def callback(self):
        if self.target(*self.args, **self.kwargs):
            self.isAlive = False
            self.startAfter(self.interval)
        else: self.isAlive = False

    def cancel(self):
        self.isAlive = False
        if not self.timer == None: self.timer.cancel()
        if not self.onCancel == None: self.onCancel(*self.args, **self.kwargs)

    def start(self):
        self.startAfter(self.interval)

    def startAfter(self, interval):
        if self.isAlive: return
        self.timer = threading.Timer(interval, self.callback)
        self.isAlive = True
        self.timer.start()

class Logger(file):

    def __init__(self, name, mode = 'w'):
        self = file.__init__(self, name, mode)

    def writeLine(self, string):
        self.writelines(string + '\n')
        return None

#Main Function

def main():

    while(True):

        try: ThreadedClient(Tk()).start()

        except:

            #Get information from unhandled exception
            exc_type, exc_value, exc_traceback = sys.exc_info()
            exceptionInfo = repr(traceback.format_exception(exc_type, exc_value,exc_traceback))

            #Output unhandled exception to error.log file
            f = Logger(os.path.dirname(sys.argv[0])+'/errors.log','a')
            f.writeLine('')
            f.writeLine('***************************************************')
            f.writeLine('**                   EXCEPTION                   **')
            f.writeLine('***************************************************')
            for line in traceback.format_exception(exc_type, exc_value,exc_traceback): f.writeLine(line)
            f.close()

        else: break

if __name__ == '__main__':   
    main()

获得一些提示如何修复未处理的异常会很棒。

标签: pythonpython-2.7tkinter

解决方案


推荐阅读