| 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 | |
|---|
| 7 | import usb |
|---|
| 8 | import pdb |
|---|
| 9 | import PIL.Image |
|---|
| 10 | import array |
|---|
| 11 | import optparse |
|---|
| 12 | import binascii |
|---|
| 13 | import struct |
|---|
| 14 | import sys |
|---|
| 15 | import time |
|---|
| 16 | |
|---|
| 17 | class 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 | |
|---|
| 249 | class 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 | |
|---|
| 289 | class 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 |
|---|
| 308 | def 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 |
|---|
| 321 | def 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 | |
|---|
| 338 | def 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 | |
|---|
| 357 | if __name__ == "__main__": |
|---|
| 358 | sys.exit(main(sys.argv)) |
|---|