root/hodgestar/PythonCode/PicturePocketRevEng/pixart.py

Revision 283, 11.5 kB (checked in by simon, 4 years ago)

Move Picture Pocket reverse engineering attempts into svn.

  • Property svn:mime-type set to text/python-source
  • Property svn:eol-style set to native
Line 
1"""Python script for controlling a Pixart Picture Pocket digital photo frame.
2   """
3
4# TODO: Support having multiple photo frames connected at the same time.
5# TODO: Remove debugging cruft
6
7import usb
8import pdb
9import PIL.Image
10import array
11import optparse
12import binascii
13import struct
14import sys
15import time
16
17class PixartDevice(object):
18    VENDOR_ID = 0x093a
19    PRODUCT_ID = 0x020f
20
21    INTERFACE_ID = 0
22
23    BULK_SEND_EP = 4
24    BULK_RECV_EP = 3
25
26    def __init__(self):
27        self.device = None
28        self.handle = None
29
30        for bus in usb.busses():
31            for device in bus.devices:
32                if device.idVendor == self.VENDOR_ID and \
33                   device.idProduct == self.PRODUCT_ID:
34                    self.device = device
35
36        if self.device is None:
37            raise RuntimeError("Could not find Pixart Picture Pocket")
38
39    def _i2h(self,aBuf):
40        s = binascii.b2a_hex(struct.pack("b"*len(aBuf),*aBuf))
41        return " ".join([s[i:i+8] for i in range(0,len(s),8)])
42
43    def open(self):
44        self.handle = self.device.open()
45        self.handle.claimInterface(self.INTERFACE_ID)
46
47    def close(self) :
48        self.handle.releaseInterface()
49        self.handle = None
50
51    def init(self):
52        # controlMsg(requestType, request, buffer, value=0, index=0, timeout=100) -> bytesWritten|buffer
53        # bulkRead(endpoint, size, timeout=100) -> buffer
54        # bulkWrite(endpoint, buffer, timeout=100) -> bytesWritten
55        print "INIT"
56        self.handle.controlMsg(0x02, 0x01, [], index=0x82)
57        self.handle.controlMsg(0x02, 0x01, [], index=0x07)
58        self.handle.controlMsg(0x02, 0x01, [], index=0x83)
59        self.handle.controlMsg(0x02, 0x01, [], index=0x04)
60
61    def set_mem_position(self,iPos1,iPos2):
62        self.init()
63        print "SET MEM POS: %i, %i" % (iPos1, iPos2)
64
65        # Is iPos1 some sort of command indicator?
66        #  - reads seem to set 0xd4
67        #  - post reads it gets set to 0xd5
68        #  - on disconnect it gets set to 0x5a (this seems to put the device back into display mode)
69        #  - delete uses 0xdb
70        #  - upload sets 0xfa, 0xdb and 0xd5
71
72        for iPosByte in [0x51, iPos1, iPos2, 0x00, 0x00, 0x00, 0x00, 0x00]:
73            bReady = False
74            while not bReady:
75                # print ".",
76                self.handle.bulkWrite(self.BULK_SEND_EP,(0x21,))
77                aBuf = self.handle.bulkRead(self.BULK_RECV_EP,16)
78                aBuf0 = struct.unpack("B",struct.pack("b",aBuf[0]))[0]
79                if aBuf0 == 0x08 or aBuf0 == 0x0a:
80                    bReady = True
81                else:
82                    print " SRP 1:", self._i2h(aBuf)
83
84            self.handle.bulkWrite(self.BULK_SEND_EP,(0x19,iPosByte))
85            time.sleep(0.001)
86
87        bSet = False
88        while not bSet:
89            # print ".",
90            self.handle.bulkWrite(self.BULK_SEND_EP,(0x21,))
91            aBuf = self.handle.bulkRead(self.BULK_RECV_EP,16)
92            aBuf0 = struct.unpack("B",struct.pack("b",aBuf[0]))[0]
93            if aBuf0 == 0x02:
94                bSet = True
95            else:
96                print " SRP 2:", self._i2h(aBuf)
97
98        self.handle.bulkWrite(self.BULK_SEND_EP,(0x19,))
99        time.sleep(0.001)
100        aBuf = self.handle.bulkRead(self.BULK_RECV_EP,16)
101        print "XB:", self._i2h(aBuf)
102
103        time.sleep(0.1)
104
105    def signal_read_start(self,sExpected="09507813 04000000 00ff6900 000000ff",bRetry=True):
106        self.init()
107        print "SIGNAL READ START"
108
109        self.handle.bulkWrite(self.BULK_SEND_EP,(0x01,0x01))
110        self.handle.bulkWrite(self.BULK_SEND_EP,(0x01,))
111
112        aBuf = self.handle.bulkRead(self.BULK_RECV_EP,16)
113        sBuf = self._i2h(aBuf)
114        if sBuf != sBuf: #sExpected:
115            if bRetry:
116                print "RETRYING ..."
117                self.signal_read_complete()
118                self.signal_read_start(sExpected=sExpected,bRetry=False)
119                return
120            else:
121                raise RuntimeError("Unexpected read start reponse: %s (not %s)" % (sBuf,sExpected))
122
123        self.handle.bulkWrite(self.BULK_SEND_EP,(0x0f,0x00))
124        self.handle.bulkWrite(self.BULK_SEND_EP,(0x00,0x2a)) # deletes and writes use 0x28 as the second byte
125        self.handle.bulkWrite(self.BULK_SEND_EP,(0x1a,0x10))
126        self.handle.bulkWrite(self.BULK_SEND_EP,(0x1f,))
127
128    def signal_read_complete(self):
129        print "SIGNAL READ COMPLETE"
130        self.handle.bulkWrite(self.BULK_SEND_EP,(0x00,0x00))
131        self.handle.bulkWrite(self.BULK_SEND_EP,(0x01,0x00))
132
133    def random_read(self):
134        print "--"
135        aData = []
136        for j in range(4,16):
137            print j, "K"
138            sPrev = None
139            bNewLine = False
140
141            self.set_mem_position(0xd4,j)
142            try:
143                self.signal_read_start()
144            except StandardError:
145                print "--- ERROR @ %i K ---" % j
146                self.signal_read_complete()
147                self.set_mem_position(0xd5,0x00)
148                raise
149
150            for i in range(64):
151                iData = self.handle.bulkRead(self.BULK_RECV_EP,16)
152                aData.extend(struct.unpack("B"*len(iData),struct.pack("b"*len(iData),*iData)))
153
154            self.signal_read_complete()
155            self.set_mem_position(0xd5,0x00)
156
157            #print "  First:", aData[0]
158            #print "  Last:", aData[-1]
159
160        if len(aData) > 102*80*2:
161            print "Trimming", len(aData) - 102*80*2, "bytes"
162            del aData[102*80*2:]
163        elif len(aData) < 102*80*2:
164            print "Image data too short!"
165            print "--"
166            return
167
168        oP = Picture.from_native(aData)
169        oP.oI.show()
170
171        print "--"
172
173    def connect_in(self):
174        for bType, bRequest, wValue, wIndex, wLength in [
175            (0x80, 0x06, 0x0100, 0, 0x12),
176            (0x80, 0x06, 0x0200, 0, 0x09),
177            (0x80, 0x06, 0x0200, 0, 0x0213),
178            (0x80, 0x06, 0x0300, 0, 0xff),
179            (0x80, 0x06, 0x0302, 0x0409, 0xff),
180            (0x00, 0x09, 0x0001, 0, []),
181            (0x80, 0x06, 0x0100, 0, 0x40),
182            (0x80, 0x06, 0x0100, 0, 0x12),
183            (0x80, 0x06, 0x0200, 0, 0x09),
184            (0x80, 0x06, 0x0200, 0, 0xff),
185            (0x80, 0x06, 0x0200, 0, 0x0213),
186            (0x80, 0x06, 0x0300, 0, 0xff),
187            (0x80, 0x06, 0x0302, 0x0409, 0xff),
188            (0x80, 0x06, 0x0300, 0, 0xff),
189            (0x80, 0x06, 0x0302, 0x0409, 0xff),
190            (0x80, 0x06, 0x0100, 0, 0x12),
191            (0x80, 0x06, 0x0200, 0, 0x12),
192            (0x80, 0x06, 0x0200, 0, 0x0213),
193        ]:
194            oRes = self.handle.controlMsg(bType, bRequest, wLength, value=wValue, index=wIndex)
195            if type(oRes) is int:
196                s = str(oRes)
197            else:
198                s = self._i2h(oRes)
199            print "C:", bType, bRequest, wValue, wIndex, wLength, " = ", s
200
201    def connect(self):
202        # controlMsg(requestType, request, buffer, value=0, index=0, timeout=100) -> bytesWritten|buffer
203        self.connect_in()
204
205        self.handle.controlMsg(0x01, 0x0b, [])
206        self.handle.controlMsg(0x00, 0x09, [], value=0x01)
207        self.handle.controlMsg(0x01, 0x0b, [])
208        self.handle.controlMsg(0x01, 0x0b, [])
209
210        time.sleep(0.1)
211
212        self.set_mem_position(0xd4,0x00)
213        try:
214            self.signal_read_start(sExpected="09505050 50505050 50505050 50505050")
215        except StandardError:
216            print "--- CONNECT ERROR ---"
217            self.signal_read_complete()
218            self.set_mem_position(0xd5,0x00)
219            raise
220
221        aData = []
222        for i in range(64):
223            s = self._i2h(self.handle.bulkRead(self.BULK_RECV_EP,16))
224            aData.append(s)
225
226        self.signal_read_complete()
227        self.set_mem_position(0xd5,0x00)
228
229        for i, s in enumerate(aData):
230            print "  Connect %i:" % i, s
231
232        time.sleep(0.1)
233
234    def disconnect(self):
235        self.set_mem_position(0x5a,0x00)
236
237    def download_images(self):
238        print "open & connect"
239        self.open()
240        try:
241            self.connect()
242            print "random read"
243            self.random_read()
244        finally:
245            self.disconnect()
246            self.close()
247            print "disconnected & closed"
248
249class Picture(object):
250    WIDTH = 102
251    HEIGHT = 80
252
253    def __init__(self,oI):
254        """oI = PIL Image object. Size should be 102 x 80.
255           """
256        self.oI = oI
257
258    @classmethod
259    def from_native(cls,aBuf):
260        """Read in image from a buffer containing the image in the native Pocket Picture format.
261           
262           Native format is 102 x 80, RGB with 2 bytes per pixel (5-6-5 bits for red, green and blue).
263           """
264        if len(aBuf) != cls.WIDTH * cls.HEIGHT * 2:
265            raise ValueError("Buffer size incorrect.")
266
267        aRgbBuf = array.array("B",[])
268        for i in range(0,len(aBuf),2):
269            iByte1, iByte2 = aBuf[i], aBuf[i+1]
270
271            iRed = (iByte1 & 0xf8) >> 3 # 5 bits
272            iGreen = (iByte1 & 0x07) << 3 | (iByte2 & 0xe0) >> 5 # 6 bits
273            iBlue = iByte2 & 0x1f # 5 bits
274
275            # append rgb values scaled up to 8 bits
276            aRgbBuf.append(iRed << 3) # red * 2**3
277            aRgbBuf.append(iGreen << 2)  # green * 2**2
278            aRgbBuf.append(iBlue << 3) # blue * 2**3
279
280        oI = PIL.Image.frombuffer("RGB",(cls.WIDTH,cls.HEIGHT),aRgbBuf)
281        oI = oI.transpose(PIL.Image.ROTATE_180)
282        return cls(oI)
283
284    def to_native(self):
285        """Return a buffer containing the image in native Pocket Picture format.
286           """
287        pass
288
289class OptionParser(optparse.OptionParser,object):
290    def __init__(self):
291        super(OptionParser,self).__init__(usage="usage: %prog [options]",
292                version="%prog 0.1")
293
294        self.add_option("-c","--collection-folder",
295                        type="string", dest="collection_folder", default=".",
296                        help="Folder holding image collection [.]")
297
298        self.add_option("-d","--download-images",
299                        action="store_true", dest="download_images", default=False,
300                        help="Copy images from the device and save them to the collection folder as PNGs.")
301
302        self.add_option("-u","--upload-images",
303                        action="store_true", dest="upload_images", default=False,
304                        help="Copy images from the collection folder to the device.")
305
306
307# TODO: Remove Function
308def remove_control_bytes(aImg):
309    iBlockLen = 16*68 + 5
310    iNewBlockLen = iBlockLen - 69
311    iOldLen = len(aImg)
312    for iBlockStart in range(0,len(aImg),iNewBlockLen):
313        iBlockEnd = iBlockStart + iBlockLen
314        #for i, x in enumerate(aImg[iBlockStart:iBlockEnd:16]):
315            #if x != 0x1f:
316            #    print iBlockStart, i, x
317        del aImg[iBlockStart:iBlockEnd:16]
318    print "Length decrease:", iOldLen - len(aImg)
319
320# TODO: Remove Function
321def show_image(filename):
322    fImg = file(filename,"rb")
323    aImg = array.array("B",fImg.read())
324    fImg.close()
325
326    #remove_control_bytes(aImg)
327
328    if len(aImg) > 102*80*2:
329        print "Trimming", len(aImg) - 102*80*2, "bytes"
330        del aImg[102*80*2:]
331    elif len(aImg) < 102*80*2:
332        print "Image data too short!"
333        return
334
335    oP = Picture.from_native(aImg)
336    oP.oI.show()
337
338def main(aArgs):
339    oOptParser = OptionParser()
340    oOpts, aArgs = oOptParser.parse_args(aArgs)
341
342    if len(aArgs) != 1:
343        oOptParser.print_help()
344        return 1
345
346    oPD = PixartDevice()
347
348    if oOpts.download_images:
349        aImgs = oPD.download_images()
350        print "Images downloaded."
351
352    if oOpts.upload_images:
353        pass
354
355    return 0
356
357if __name__ == "__main__":
358    sys.exit(main(sys.argv))
Note: See TracBrowser for help on using the browser.