Thursday, January 12, 2012

Code to switch the frame from Mass Storage mode to Mini Monitor mode

A picture can only be written to the photo frame when it is in Mini Monitor mode (and it is INITialized). However, when it is found in Mass Monitor mode, it can be switched to Mini Monitor mode by a script. The code reqired is:
dev.ctrl_transfer(0x00|0x80,  0x06, 0xfe, 0xfe, 0xfe )
Settling on the USB bus takes <0.5sec, but give it some extra time.

A stripped down version of a program which takes care of switching and initialization and can be fed with pictures of any size and (almost any) type is following. Note the use of the Image module for image manipulation, and of StringIO to avoid writing and reading temp files to/from disk:

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

import os
import sys
import time
import usb.core
import usb.util
import StringIO
import Image
import struct

def write_jpg2frame(dev, pic):
    """Attach header to picture, pad with zeros if necessary, and send to frame"""
    # create header and stack before picture
    # middle 4 bytes have size of picture
    rawdata = b"\xa5\x5a\x18\x04" + struct.pack('<I', len(pic)) + b"\x48\x00\x00\x00" + pic
    # total transfers must be complete chunks of 16384  = 2 ^14. Complete by padding with zeros
    pad = 16384 - (len(rawdata) % 16384) +1         
    tdata = rawdata + pad * b'\x00'
    ltdata = len(tdata)
    # Syntax: write(self, endpoint, data, interface = None, timeout = None):
    endpoint = 0x02               
    dev.write(endpoint, tdata )
   

def get_known_devices():
    """Return a dict of photo frames"""
    # listed as: Name, idVendor, idProduct, [width , height - in pixel if applicable]
    #
    # Samsung SPF-87H in either mini monitor mode or mass storage mode
    SPF87H_MiniMon   = {'name':"SPF87H Mini Monitor", 'idVendor':0x04e8, 'idProduct':0x2034, 'width':800, 'height':480 }
    SPF87H_MassSto   = {'name':"SPF87H Mass Storage", 'idVendor':0x04e8, 'idProduct':0x2033}
   
    # Samsung SPF-107H (data from web reports - not tested)
    SPF107H_MiniMon  = {'name':"SPF107H Mini Monitor", 'idVendor':0x04e8, 'idProduct':0x2036, 'width':1024, 'height':600 }
    SPF107H_MassSto  = {'name':"SPF107H Mass Storage", 'idVendor':0x04e8, 'idProduct':0x2035}
   
    # Samsung SPF-83H (data from web reports - not tested)
    SPF107H_MiniMon  = {'name':"SPF107H Mini Monitor", 'idVendor':0x04e8, 'idProduct':0x200d, 'width':800, 'height':600 }
    SPF107H_MassSto  = {'name':"SPF107H Mass Storage", 'idVendor':0x04e8, 'idProduct':0x200c}
   
    return    ( SPF87H_MiniMon, SPF87H_MassSto, SPF107H_MiniMon, SPF107H_MassSto, SPF107H_MiniMon, SPF107H_MassSto )
 

def find_device(device):
    """Try to find device on USB bus."""
    return usb.core.find(idVendor=device['idVendor'], idProduct=device['idProduct'])   


def init_device(device0, device1):
    """First try Mini Monitor mode, then Mass storage mode"""
    dev = find_device(device0)
 
    if dev is not None:
        ## found it, trying to init it
        frame_init(dev)
    else:
        # not found device in Mini Monitor mode, trying to find it in Mass Storage mode
        dev = find_device(device1)
        if dev is not None:
            #found it in Mass Storage, trying to switch to Mini Monitor
            frame_switch(dev)
            ts = time.time()
            while True:
                # may need to burn some time
                dev = find_device(device0)
                if dev is not None:
                    #switching successful
                    break
                elif time.time() - ts > 2:
                    print "switching failed. Ending program"
                    sys.exit()
            frame_init(dev)
        else:
            print "Could not find frame in either mode"
            sys.exit()
    return dev

  
def frame_init(dev):
    """Init device so it stays in Mini Monitor mode"""
    # this is the minimum required to keep the frame in Mini Monitor mode!!!
    dev.ctrl_transfer(0xc0, 4 )   
 

def frame_switch(dev):
    """Switch device from Mass Storage to Mini Monitor""" 
    dev.ctrl_transfer(0x00|0x80,  0x06, 0xfe, 0xfe, 0xfe )
    # settling of the bus and frame takes about 0.42 sec
    # give it some extra time, but then still make sure it has settled
    time.sleep(1)

   
def main():
    global dev, known_devices_list
   
    known_devices_list = get_known_devices()

    # define which frame to use, here use Samsung SPF-87H
    device0 = known_devices_list[0] # Mini Monitor mode
    device1 = known_devices_list[1] # Mass Storage mode

    dev = init_device(device0, device1)   
    print "Frame is in Mini Monitor mode and initialized. Sending pictures now"

    image = Image.open("mypicture.jpg")
    #manipulations to consider:
    #  convert
    #  thumbnail
    #  rotate
    #  crop
    image = image.resize((800,480))
    output = StringIO.StringIO()
    image.save(output, "JPEG", quality=94)
    pic  = output.getvalue()
    output.close()
    write_jpg2frame(dev, pic)       
      

if __name__ == "__main__":
    main()

Wednesday, January 11, 2012

pyframe transfer speed sufficient even for video

I was wondering about the transfer speed of pictures to the frame, given that Python is an interpreted language. It turned out to be much faster than expected:

Two very different pictures were loaded by the script, prepared, stored in memory and alternatively transferred to the frame. I used one pair of pictures which were simple and small (<<16384 Bytes), and another one with rather complex and hence larger (ca 100kB after resizing to 800x480) pictures. All are attached to this post. The transfer of the pics resulted in a CPU load of only about 1-2%. Here the measured data:

Picture_Pair_______ Picture Size (B) _______Pictures/sec ___Total Transfer MB/sec
red.jpg/blue.jpg ____ 6631/2536 __________ 28 _____________ 0.46
i244,jpg/i247.jpg ___ 98792/123768 ______ 16 _____________ 1.93

Since a minimum chunk size of 16384 bytes per picture needs to be transferred irrespective of the picture size, the small pictures do not benefit much from their small size with respect to transfer speed. Generally, 20+ Pictures/sec should be achievable.

Since the USB bus can transfer at least 10x as much, and the CPU even more, I conclude that the transfer speed is limited by the frame.

Next I took a video clip and converted each frame into a JPG picture using ffmpeg, which I then tried to transfer to the frame as a fast sequence. Suprisingly, I could not transfer even a single picture out of several hundred, although each picture could be viewed correctly with all photoview programs on my computer. I tried a variety of permutations of the ffmpeg parameters, but without success.

However, reading and rewriting each picture using the Python IMAGE Modul and this code resulted in pictures fit for transfer to the frame:
import Image
filename = "inpicture.jpg" 
image = Image.open(filename)
image.save( "outpicture", "JPEG", quality=95)
Transfering those pictures was possible with a flicker-free frame rate of some 26 pictures/sec (1.7 MB/sec) - the "picture" frame turned into a "video" frame! Reading the pictures from a fast SSD did not improve the speed, which is consistent with the frame itself being the bottleneck.


The test script is this:
#!/usr/bin/python

import sys
import os
import struct
import usb.core
import time

device = "SPF87H Mini Monitor"

dev = usb.core.find(idVendor=0x04e8, idProduct=0x2034)

if dev is None:
    print "Could not find", device, " - Exiting\n"
    sys.exit()

print "Found", device

dev.ctrl_transfer(0xc0, 4 )   

if len(sys.argv) < 3:
    print "I need 2 pictures  - Exiting."
    sys.exit()

filename1 = sys.argv[1]
filename2 = sys.argv[2]
filesize1 = os.path.getsize(filename1)
filesize2 = os.path.getsize(filename2)
print "Pictures to show are:", filename1, "filesize:", filesize1
print "Pictures to show are:", filename2, "filesize:", filesize2

# Open the picture file and read into a string
infile1 = open(filename1, "rb")
pic1 = infile1.read()
infile1.close()

infile2 = open(filename2, "rb")
pic2 = infile2.read()
infile2.close()

# The photo frame expects a header of 12 bytes, followed by the picture data.
# The first 4 and the last 4 bytes are always the same.
# The middle 4 bytes are the picture size (excluding the header) with the least significant byte first
rawdata1 = b"\xa5\x5a\x18\x04" + struct.pack('<I', len(pic1)) + b"\x48\x00\x00\x00" + pic1
rawdata2 = b"\xa5\x5a\x18\x04" + struct.pack('<I', len(pic2)) + b"\x48\x00\x00\x00" + pic2

# The photo frame expects transfers in complete chunks of 16384 bytes (=2^14 bytes).
# If last chunk of rawdata is not complete, then make it complete by padding with zeros.
pad1 = 16384 - (len(rawdata1) % 16384)
tdata1 = rawdata1 + pad1 * b'\x00'

pad2 = 16384 - (len(rawdata2) % 16384)
tdata2 = rawdata2 + pad2 * b'\x00'

# For unknown reasons, some pictures will only transfer successfully, when at least one
# additional zero byte is added. Possibly a firmware bug of the frame?
#tdata1 = tdata1 + b'\x00'
#tdata2 = tdata2 + b'\x00'

# Write the data. Must write to USB endpoint 2
endpoint = 0x02

bytes_written1 = 0
bytes_written2 = 0

ts = time.time()
nr = 100
for i in range(nr):
    bytes_written1 += dev.write(endpoint, tdata1 )
    bytes_written2 += dev.write(endpoint, tdata2 )  
   
te = time.time()

sum = bytes_written1 + bytes_written2
td = te -ts
print "time lapsed writing:", td, "sec"

print "total no of pictures transferred:", nr * 2, ", rate: ", "%02.1f Bilder/sec"% (nr * 2 / td)
print "total no of bytes transferred:", sum, ", rate:",  "%03d kB/sec"%(sum/td/1000.)

sumfs = nr * (filesize1 + filesize2)
print "transfer overhead: %3d%% " % ((100.*sum/sumfs) - 100.)
The test pictures follow, each one 800x480