Sunday, March 11, 2012

Triggered Screenshots

When using a photoframe as a display or a (headless) PC, one might want to update the display at regular intervals, e.g. once per minute to update a clock, but then also at other events, like pressing a key on a keyboard or remote control.

This can be achieved by making the screenshot program listen to UNIX signals. These signals must not be mistaken for signals emitted from GUIs with events like clicking a button, or checking a checkbox. The probably best known of these UNIX signals is SIGINT, which is sent to a program when CTRL-C is pressed, and usually ends the program.

For user defined purposes the signals SIGUSR1 and SIGUSR2 (numerical codes 10 and 12, resp.) have been reserved. In the shell these signals can be send by
kill -SIGUSR1 pid-of-program-to-receive-signal
The tsshot2frame program below will listen to this signal and take a screenshot and send it to the frame when it receives it.

The triggershot program below is just a demo to show how a program can create and send such a signal. In this case the program does it when the key 't' is pressed. Obviously, other events can be used, like key presses on remote control, alarm signals from sensors, etc.
______________________________________________________________________________
#!/usr/bin/python
# -*- coding: UTF-8 -*-

# Program: tsshot2frame
# based on sshot2frame, but allows to be triggered by a SIGUSR1 signal
#
# This triggered-screenshot-to-frame program takes a screenshot from your desktop
# and sends it to the 'Samsung SPF-87H Digital Photo Frame'
#
# The screenshots are taken at regular intervals, but can also be triggered randomly
# by a SIGUSR1 signal, to which this program is listening.
#
# It is an extension of the sshot2frame program found here:
#    http://pyframe.blogspot.com
# Read other posts to understand details not commented here
# Copyright (C) ullix

import sys
import struct
import usb.core
import time
import signal
from PyQt4 import QtGui, QtCore


def takeshot():
    print "tsshot2frame: taking a shot"

    # take a screenshot and store into a pixmap
    #pmap = QtGui.QPixmap.grabWindow(QtGui.QApplication.desktop().winId())
    # if you want a screenshot from only a subset of your desktop, you can define it like this
    pmap = QtGui.QPixmap.grabWindow(QtGui.QApplication.desktop().winId(), x=0, y= 600, width=1200, height=720)

    # next code line is needed only when screenshot does not yet have the proper dimensions for the frame
    # note that distortion will result when aspect ratios of desktop and frame are different!
    # if not needed then inactivate to save cpu cycles
    pmap = pmap.scaled(800,480)

    # create a buffer object and store the pixmap in it as if it were a jpeg file
    buffer = QtCore.QBuffer()
    buffer.open(QtCore.QIODevice.WriteOnly)
    pmap.save(buffer, 'jpeg')
    buffer.close()

    # now get the just saved "file" data into a string, which we will send to the frame
    pic = buffer.data().__str__()

    # wrap pic into write format and write to frame
    rawdata = b"\xa5\x5a\x18\x04" + struct.pack('<I', len(pic)) + b"\x48\x00\x00\x00" + pic
    pad = 16384 - (len(rawdata) % 16384)
    tdata = rawdata + pad * b'\x00'
    tdata = tdata + b'\x00'
    endpoint = 0x02
    bytes_written = dev.write(endpoint, tdata )


def sigusr1_handler(signum, stack):
    """
    Dummy handler for SIGUSR1 signal.
    """
    pass
    #print "tsshot2frame: sigusr1_handler received signal no:", signum

    # Receiving a signal will interrupt the time.sleep() in the main while loop,
    # which will result in a shot being taken immediatel<. Therefor a separate
    # takeshot() is not needed here; it would result in two successive shots
    # being taken
    #takeshot()


#----- main starts here ------------------------------

device = "SPF87H Mini Monitor"
dev = usb.core.find(idVendor=0x04e8, idProduct=0x2034)

if dev is None:
    print "tsshot2frame: Could not find device", device, " - exiting\n"
    sys.exit()
else:
    print "tsshot2frame: Found device", device
    dev.ctrl_transfer(0xc0, 4 )

# Setting the signal handler
signal.signal(signal.SIGUSR1, sigusr1_handler)

# Must have a QApplication running to use the other pyqt4 functions
app  = QtGui.QApplication(sys.argv)

# Take screenshots in regular intervals and send them to the frame;
# screenshots triggered by SIGNALS will come in addition
while True:
    print time.time(),
    takeshot()
    time.sleep(60)
    """
    Remember that receiving a SIGNAL will interrupt time.sleep !
    From the python documentation:
    time.sleep(secs)
    Suspend execution for the given number of seconds. The argument may be a
    floating point number to indicate a more precise sleep time. The actual
    suspension time may be less than that requested because any caught signal
    will terminate the sleep() following execution of that signal’s catching
    routine. Also, the suspension time may be longer than requested by an
    arbitrary amount because of the scheduling of other activity in the system.
    """
Following is the triggershot program:
_______________________________________________________________________________
#!/usr/bin/python
# -*- coding: UTF-8 -*-

# Program: triggershot
# sends the SIGUSR1 signal (numerical value 10) to the
# script tsshot2frame when keypress detected
# Copyright (C) ullix

import time
import signal
import os
import sys
import subprocess
import pygame
import termios
import fcntl

from PyQt4 import QtGui, QtCore


def triggersignal():
    """
    find the pid of our triggered-screen-shot program and send a
    SIGUSR1 to it
    """
    script = "tsshot2frame"

    print time.time(),"trigger: sending SIGUSR1 to ", script

    # execute shell command 'ps -A | grep tsshot2frame' and obtain its output
    p1 = subprocess.Popen(["ps", "-A"], stdout=subprocess.PIPE)
    p2 = subprocess.Popen(["grep", script], stdin=p1.stdout, stdout=subprocess.PIPE)
    output = p2.communicate()[0]
    #print "pipe outsub=",output

    if script in output and '<defunct>' not in output:
        pid = int(output[0:5])
        #print script + " is running, pid: ", pid

    else:
        if '<defunct>' in output:
            #print script + " running but defunct, clear up first"
            os.system("killall " + script ) # clear up if defunct
        else:
            #print script + " not running"
            pass

        pid = subprocess.Popen("./" + script  ).pid
        #pid = subprocess.Popen(script).pid # if script is in path
        time.sleep(2) # give it time to start
        #print script + " restarted, pid: ", pid

    os.kill(pid, signal.SIGUSR1)


def getch():
    # code according to:
    # http://docs.python.org/faq/library#how-do-i-get-a-single-keypress-at-a-time
    fd = sys.stdin.fileno()
    oldterm = termios.tcgetattr(fd)
    newattr = termios.tcgetattr(fd)
    newattr[3] = newattr[3] & ~termios.ICANON & ~termios.ECHO
    termios.tcsetattr(fd, termios.TCSANOW, newattr)

    oldflags = fcntl.fcntl(fd, fcntl.F_GETFL)
    fcntl.fcntl(fd, fcntl.F_SETFL, oldflags | os.O_NONBLOCK)

    c = ""
    try:
        while True:
            # read from stdin as long as there are characters to be read
            # if all read then return
            try:
                c += sys.stdin.read(1)
            except IOError as (errno, msg):
                #print "IOError", errno, msg,
                break
    finally:
        # restore old settings
        termios.tcsetattr(fd, termios.TCSAFLUSH, oldterm)
        fcntl.fcntl(fd, fcntl.F_SETFL, oldflags)

    return c

#----- main starts here ------------------------------

triggersignal()
while True:
    time.sleep(0.3)
    c = getch()
    if "t" in c :
        print "Read character t, triggering screenshot"
        triggersignal()

No comments:

Post a Comment