Package libprs500 :: Module communicate
[hide private]
[frames] | no frames]

Source Code for Module libprs500.communicate

  1  ##    Copyright (C) 2006 Kovid Goyal kovid@kovidgoyal.net 
  2  ##    This program is free software; you can redistribute it and/or modify 
  3  ##    it under the terms of the GNU General Public License as published by 
  4  ##    the Free Software Foundation; either version 2 of the License, or 
  5  ##    (at your option) any later version. 
  6  ## 
  7  ##    This program is distributed in the hope that it will be useful, 
  8  ##    but WITHOUT ANY WARRANTY; without even the implied warranty of 
  9  ##    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the 
 10  ##    GNU General Public License for more details. 
 11  ## 
 12  ##    You should have received a copy of the GNU General Public License along 
 13  ##    with this program; if not, write to the Free Software Foundation, Inc., 
 14  ##    51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 
 15   
 16  ### End point description for PRS-500 procductId=667 
 17  ### Endpoint Descriptor: 
 18  ###        bLength                 7 
 19  ###        bDescriptorType         5 
 20  ###        bEndpointAddress     0x81  EP 1 IN 
 21  ###        bmAttributes            2 
 22  ###          Transfer Type            Bulk 
 23  ###          Synch Type               None 
 24  ###          Usage Type               Data 
 25  ###        wMaxPacketSize     0x0040  1x 64 bytes 
 26  ###        bInterval               0 
 27  ###      Endpoint Descriptor: 
 28  ###        bLength                 7 
 29  ###        bDescriptorType         5 
 30  ###        bEndpointAddress     0x02  EP 2 OUT 
 31  ###        bmAttributes            2 
 32  ###          Transfer Type            Bulk 
 33  ###          Synch Type               None 
 34  ###          Usage Type               Data 
 35  ###        wMaxPacketSize     0x0040  1x 64 bytes 
 36  ###        bInterval               0 
 37  ###  
 38  ### 
 39  ###  Endpoint 0x81 is device->host and endpoint 0x02 is host->device. You can establish Stream pipes to/from these endpoints for Bulk transfers. 
 40  ###  Has two configurations 1 is the USB charging config 2 is the self-powered config.  
 41  ###  I think config management is automatic. Endpoints are the same 
 42  """ 
 43  Contains the logic for communication with the device (a SONY PRS-500). 
 44   
 45  The public interface of class L{PRS500Device} defines the methods for performing various tasks.  
 46  """ 
 47  import usb, sys, os, time 
 48  from array import array 
 49   
 50  from prstypes import * 
 51  from errors import * 
 52   
 53  MINIMUM_COL_WIDTH = 12 #: Minimum width of columns in ls output 
 54  _packet_number = 0     #: Keep track of the packet number of packet tracing 
 55  KNOWN_USB_PROTOCOL_VERSIONS = [0x3030303030303130L] #: Protocol versions libprs500 has been tested with 
 56   
57 -def _log_packet(packet, header, stream=sys.stderr):
58 """ Log C{packet} to stream C{stream}. Header should be a small word describing the type of packet. """ 59 global _packet_number 60 _packet_number += 1 61 print >>stream, str(_packet_number), header, "Type:", packet.__class__.__name__ 62 print >>stream, packet 63 print >>stream, "--"
64
65 -class File(object):
66 """ Wrapper that allows easy access to all information about files/directories """
67 - def __init__(self, file):
68 self.is_dir = file[1].is_dir #: True if self is a directory 69 self.is_readonly = file[1].is_readonly #: True if self is readonly 70 self.size = file[1].file_size #: Size in bytes of self 71 self.ctime = file[1].ctime #: Creation time of self as a epoch 72 self.wtime = file[1].wtime #: Creation time of self as an epoch 73 path = file[0] 74 if path.endswith("/"): path = path[:-1] 75 self.path = path #: Path to self 76 self.name = path[path.rfind("/")+1:].rstrip() #: Name of self
77
78 - def __repr__(self):
79 """ Return path to self """ 80 return "File:"+self.path
81
82 - def __str__(self):
83 return self.name
84 85
86 -class DeviceDescriptor:
87 """ 88 Describes a USB device. 89 90 A description is composed of the Vendor Id, Product Id and Interface Id. 91 See the U{USB spec<http://www.usb.org/developers/docs/usb_20_05122006.zip>} 92 """ 93
94 - def __init__(self, vendor_id, product_id, interface_id) :
95 self.vendor_id = vendor_id 96 self.product_id = product_id 97 self.interface_id = interface_id
98
99 - def getDevice(self) :
100 """ 101 Return the device corresponding to the device descriptor if it is 102 available on a USB bus. Otherwise, return None. Note that the 103 returned device has yet to be claimed or opened. 104 """ 105 buses = usb.busses() 106 for bus in buses : 107 for device in bus.devices : 108 if device.idVendor == self.vendor_id : 109 if device.idProduct == self.product_id : 110 return device 111 return None
112 113
114 -class PRS500Device(object):
115 116 """ 117 Contains the logic for performing various tasks on the reader. 118 119 The implemented tasks are: 120 0. Getting information about the device 121 1. Getting a file from the device 122 2. Listing of directories. See the C{list} method. 123 """ 124 125 SONY_VENDOR_ID = 0x054c #: SONY Vendor Id 126 PRS500_PRODUCT_ID = 0x029b #: Product Id for the PRS-500 127 PRS500_INTERFACE_ID = 0 #: The interface we use to talk to the device 128 PRS500_BULK_IN_EP = 0x81 #: Endpoint for Bulk reads 129 PRS500_BULK_OUT_EP = 0x02 #: Endpoint for Bulk writes 130
131 - def safe(func):
132 """ 133 Decorator that wraps a call to C{func} to ensure that exceptions are handled correctly. 134 135 As a convenience, C{safe} automatically sends the a L{USBConnect} after calling func, unless func has 136 a keyword argument named C{end_session} set to C{False}. 137 138 An L{ArgumentError} will cause the L{USBConnect} command to be sent to the device, unless end_session is set to C{False}. 139 An L{usb.USBError} will cause the library to release control of the USB interface via a call to L{close}. 140 """ 141 def run_session(*args, **kwargs): 142 dev = args[0] 143 res = None 144 try: 145 res = func(*args, **kwargs) 146 except ArgumentError, e: 147 if not kwargs.has_key("end_session") or kwargs["end_session"]: 148 dev._send_validated_command(USBConnect()) 149 raise e 150 except usb.USBError, e: 151 dev.close() 152 raise e 153 if not kwargs.has_key("end_session") or kwargs["end_session"]: 154 dev._send_validated_command(USBConnect()) 155 return res
156 157 return run_session
158
159 - def __init__(self, log_packets=False) :
160 """ @param log_packets: If true the packet stream to/from the device is logged """ 161 self.device_descriptor = DeviceDescriptor(PRS500Device.SONY_VENDOR_ID, 162 PRS500Device.PRS500_PRODUCT_ID, 163 PRS500Device.PRS500_INTERFACE_ID) 164 self.device = self.device_descriptor.getDevice() 165 self.handle = None 166 self._log_packets = log_packets
167 168 @classmethod
169 - def _validate_response(cls, res, type=0x00, number=0x00):
170 """ Raise a ProtocolError if the type and number of C{res} is not the same as C{type} and C{number}. """ 171 if type != res.type or number != res.rnumber: 172 raise ProtocolError("Inavlid response.\ntype: expected="+hex(type)+" actual="+hex(res.type)+ 173 "\nrnumber: expected="+hex(number)+" actual="+hex(res.rnumber))
174
175 - def open(self) :
176 """ 177 Claim an interface on the device for communication. Requires write privileges to the device file. 178 Also initialize the device. See the source code for the sequenceof initialization commands. 179 180 @todo: Implement unlocking of the device 181 @todo: Check this on Mac OSX 182 """ 183 self.device = self.device_descriptor.getDevice() 184 if not self.device: 185 print >> sys.stderr, "Unable to find Sony Reader. Is it connected?" 186 sys.exit(1) 187 self.handle = self.device.open() 188 self.handle.claimInterface(self.device_descriptor.interface_id) 189 self.handle.reset() 190 res = self._send_validated_command(GetUSBProtocolVersion()) 191 if res.code != 0: raise ProtocolError("Unable to get USB Protocol version.") 192 version = self._bulk_read(24, data_type=USBProtocolVersion)[0].version 193 if version not in KNOWN_USB_PROTOCOL_VERSIONS: 194 print >>sys.stderr, "WARNING: Usb protocol version " + hex(version) + " is unknown" 195 res = self._send_validated_command(SetBulkSize(size=0x028000)) 196 if res.code != 0: raise ProtocolError("Unable to set bulk size.") 197 self._send_validated_command(UnlockDevice(key=0x312d)) 198 if res.code != 0: 199 raise ProtocolError("Unlocking of device not implemented. Remove locking and retry.")
200 201
202 - def close(self):
203 """ Release device interface """ 204 self.handle.releaseInterface() 205 self.handle, self.device = None, None 206
207 - def _send_command(self, command, response_type=Response, timeout=100):
208 """ 209 Send L{command<Command>} to device and return its L{response<Response>}. 210 211 @param command: an object of type Command or one of its derived classes 212 @param response_type: an object of type 'type'. The return packet from the device is returned as an object of type response_type. 213 @param timeout: the time to wait for a response from the device, in milliseconds. If there is no response, a L{usb.USBError} is raised. 214 """ 215 if self._log_packets: _log_packet(command, "Command") 216 bytes_sent = self.handle.controlMsg(0x40, 0x80, command) 217 if bytes_sent != len(command): 218 raise ControlError(desc="Could not send control request to device\n" + str(query.query)) 219 response = response_type(self.handle.controlMsg(0xc0, 0x81, Response.SIZE, timeout=timeout)) 220 if self._log_packets: _log_packet(response, "Response") 221 return response
222
223 - def _send_validated_command(self, command, cnumber=None, response_type=Response, timeout=100):
224 """ 225 Wrapper around L{_send_command} that checks if the C{Response.rnumber == cnumber or command.number if cnumber==None}. Also check that 226 C{Response.type == Command.type}. 227 """ 228 if cnumber == None: cnumber = command.number 229 res = self._send_command(command, response_type=response_type, timeout=timeout) 230 PRS500Device._validate_response(res, type=command.type, number=cnumber) 231 return res
232
233 - def _bulk_write(self, data, packet_size=0x1000):
234 """ 235 Send data to device via a bulk transfer. 236 @type data: Any listable type supporting __getslice__ 237 @param packet_size: Size of packets to be sent to device. C{data} is broken up into packets to be sent to device. 238 """ 239 def bulk_write_packet(packet): 240 self.handle.bulkWrite(PRS500Device.PRS500_BULK_OUT_EP, packet) 241 if self._log_packets: _log_packet(Answer(packet), "Answer h->d")
242 243 bytes_left = len(data) 244 if bytes_left + 16 <= packet_size: 245 packet_size = bytes_left +16 246 first_packet = Answer(bytes_left+16) 247 first_packet[16:] = data 248 first_packet.length = len(data) 249 else: 250 first_packet = Answer(packet_size) 251 first_packet[16:] = data[0:packet_size-16] 252 first_packet.length = packet_size-16 253 first_packet.number = 0x10005 254 bulk_write_packet(first_packet) 255 pos = first_packet.length 256 bytes_left -= first_packet.length 257 while bytes_left > 0: 258 endpos = pos + packet_size if pos + packet_size <= len(data) else len(data) 259 bulk_write_packet(data[pos:endpos]) 260 bytes_left -= endpos - pos 261 pos = endpos 262 res = Response(self.handle.controlMsg(0xc0, 0x81, Response.SIZE, timeout=5000)) 263 if self._log_packets: _log_packet(res, "Response") 264 if res.rnumber != 0x10005 or res.code != 0: 265 raise ProtocolError("Sending via Bulk Transfer failed with response:\n"+str(res)) 266 if res.data_size != len(data): 267 raise ProtocolError("Unable to transfer all data to device. Response packet:\n"+str(res)) 268 269
270 - def _bulk_read(self, bytes, command_number=0x00, packet_size=4096, data_type=Answer):
271 """ 272 Read in C{bytes} bytes via a bulk transfer in packets of size S{<=} C{packet_size} 273 @param data_type: an object of type type. The data packet is returned as an object of type C{data_type}. 274 @return: A list of packets read from the device. Each packet is of type data_type 275 @todo: Figure out how to make bulk reads work in OSX 276 """ 277 def bulk_read_packet(data_type=Answer, size=0x1000): 278 data = data_type(self.handle.bulkRead(PRS500Device.PRS500_BULK_IN_EP, size)) 279 if self._log_packets: _log_packet(data, "Answer d->h") 280 return data
281 282 bytes_left = bytes 283 packets = [] 284 while bytes_left > 0: 285 if packet_size > bytes_left: packet_size = bytes_left 286 packet = bulk_read_packet(data_type=data_type, size=packet_size) 287 bytes_left -= len(packet) 288 packets.append(packet) 289 self._send_validated_command(AcknowledgeBulkRead(packets[0].number), cnumber=command_number) 290 return packets 291 292 @safe
293 - def get_device_information(self, end_session=True):
294 """ 295 Ask device for device information. See L{DeviceInfoQuery}. 296 @return: (device name, device version, software version on device, mime type) 297 """ 298 size = self._send_validated_command(DeviceInfoQuery()).data[2] + 16 299 data = self._bulk_read(size, command_number=DeviceInfoQuery.NUMBER, data_type=DeviceInfo)[0] 300 return (data.device_name, data.device_version, data.software_version, data.mime_type)
301 302 @safe
303 - def path_properties(self, path, end_session=True):
304 """ Send command asking device for properties of C{path}. Return L{FileProperties}. """ 305 res = self._send_validated_command(PathQuery(path), response_type=ListResponse) 306 data = self._bulk_read(0x28, data_type=FileProperties, command_number=PathQuery.NUMBER)[0] 307 if path.endswith("/"): path = path[:-1] 308 if res.path_not_found : raise PathError(path + " does not exist on device") 309 if res.is_invalid : raise PathError(path + " is not a valid path") 310 if res.is_unmounted : raise PathError(path + " is not mounted") 311 if res.code not in (0, PathResponseCodes.IS_FILE): 312 raise PathError(path + " has an unknown error. Code: " + hex(res.code)) 313 return data
314 315 @safe
316 - def get_file(self, path, outfile, end_session=True):
317 """ 318 Read the file at path on the device and write it to outfile. For the logic see L{_get_file}. 319 320 The data is fetched in chunks of size S{<=} 32K. Each chunk is make of packets of size S{<=} 4K. See L{FileOpen}, 321 L{FileRead} and L{FileClose} for details on the command packets used. 322 323 @param outfile: file object like C{sys.stdout} or the result of an C{open} call 324 """ 325 if path.endswith("/"): path = path[:-1] # We only copy files 326 file = self.path_properties(path, end_session=False) 327 if file.is_dir: raise PathError("Cannot read as " + path + " is a directory") 328 bytes = file.file_size 329 res = self._send_validated_command(FileOpen(path)) 330 if res.code != 0: 331 raise PathError("Unable to open " + path + " for reading. Response code: " + hex(res.code)) 332 id = self._bulk_read(20, data_type=IdAnswer, command_number=FileOpen.NUMBER)[0].id 333 bytes_left, chunk_size, pos = bytes, 0x8000, 0 334 while bytes_left > 0: 335 if chunk_size > bytes_left: chunk_size = bytes_left 336 res = self._send_validated_command(FileIO(id, pos, chunk_size)) 337 if res.code != 0: 338 self._send_validated_command(FileClose(id)) 339 raise ProtocolError("Error while reading from " + path + ". Response code: " + hex(res.code)) 340 packets = self._bulk_read(chunk_size+16, command_number=FileIO.RNUMBER, packet_size=4096) 341 try: 342 array('B', packets[0][16:]).tofile(outfile) # The first 16 bytes are meta information on the packet stream 343 for i in range(1, len(packets)): 344 array('B', packets[i]).tofile(outfile) 345 except IOError, e: 346 self._send_validated_command(FileClose(id)) 347 raise ArgumentError("File get operation failed. Could not write to local location: " + str(e)) 348 bytes_left -= chunk_size 349 pos += chunk_size 350 self._send_validated_command(FileClose(id))
351 # Not going to check response code to see if close was successful as there's not much we can do if it wasnt 352 353 354 355 @safe
356 - def list(self, path, recurse=False, end_session=True):
357 """ 358 Return a listing of path. See the code for details. See L{DirOpen}, 359 L{DirRead} and L{DirClose} for details on the command packets used. 360 361 @type path: string 362 @param path: The path to list 363 @type recurse: boolean 364 @param recurse: If true do a recursive listing 365 @return: A list of tuples. The first element of each tuple is a path. The second element is a list of L{Files<File>}. 366 The path is the path we are listing, the C{Files} are the files/directories in that path. If it is a recursive 367 list, then the first element will be (C{path}, children), the next will be (child, its children) and so on. If it 368 is not recursive the length of the outermost list will be 1. 369 """ 370 def _list(path): # Do a non recursive listsing of path 371 if not path.endswith("/"): path += "/" # Initially assume path is a directory 372 files = [] 373 candidate = self.path_properties(path, end_session=False) 374 if not candidate.is_dir: 375 path = path[:-1] 376 data = self.path_properties(path, end_session=False) 377 files = [ File((path, data)) ] 378 else: 379 # Get query ID used to ask for next element in list 380 res = self._send_validated_command(DirOpen(path)) 381 if res.code != 0: 382 raise PathError("Unable to open directory " + path + " for reading. Response code: " + hex(res.code)) 383 id = self._bulk_read(0x14, data_type=IdAnswer, command_number=DirOpen.NUMBER)[0].id 384 # Create command asking for next element in list 385 next = DirRead(id) 386 items = [] 387 while True: 388 res = self._send_validated_command(next, response_type=ListResponse) 389 size = res.data_size + 16 390 data = self._bulk_read(size, data_type=ListAnswer, command_number=DirRead.NUMBER)[0] 391 # path_not_found seems to happen if the usb server doesn't have the permissions to access the directory 392 if res.is_eol or res.path_not_found: break 393 elif res.code != 0: 394 raise ProtocolError("Unknown error occured while reading contents of directory " + path + ". Response code: " + haex(res.code)) 395 items.append(data.name) 396 self._send_validated_command(DirClose(id)) # Ignore res.code as we cant do anything if close fails 397 for item in items: 398 ipath = path + item 399 data = self.path_properties(ipath, end_session=False) 400 files.append( File( (ipath, data) ) ) 401 files.sort() 402 return files
403 404 files = _list(path) 405 dirs = [(path, files)] 406 407 for file in files: 408 if recurse and file.is_dir and not file.path.startswith(("/dev","/proc")): 409 dirs[len(dirs):] = self.list(file.path, recurse=True, end_session=False) 410 return dirs 411 412 @safe
413 - def available_space(self, end_session=True):
414 """ 415 Get free space available on the mountpoints: 416 1. /Data/ Device memory 417 2. a:/ Memory Stick 418 3. b:/ SD Card 419 420 @return: A list of tuples. Each tuple has form ("location", free space, total space) 421 """ 422 data = [] 423 for path in ("/Data/", "a:/", "b:/"): 424 res = self._send_validated_command(FreeSpaceQuery(path),timeout=5000) # Timeout needs to be increased as it takes time to read card 425 buffer_size = 16 + res.data[2] 426 pkt = self._bulk_read(buffer_size, data_type=FreeSpaceAnswer, command_number=FreeSpaceQuery.NUMBER)[0] 427 data.append( (path, pkt.free_space, pkt.total) ) 428 return data
429
430 - def _exists(self, path):
431 """ Return (True, FileProperties) if path exists or (False, None) otherwise """ 432 dest = None 433 try: 434 dest = self.path_properties(path, end_session=False) 435 except PathError, e: 436 if "does not exist" in str(e): return (False, None) 437 else: raise e 438 return (True, dest)
439 440 @safe
441 - def touch(self, path, end_session=True):
442 """ 443 Create a file at path 444 445 @todo: Update file modification time if file already exists 446 """ 447 if path.endswith("/") and len(path) > 1: path = path[:-1] 448 exists, file = self._exists(path) 449 if exists and file.is_dir: 450 raise PathError("Cannot touch directories") 451 if not exists: 452 res = self._send_validated_command(FileCreate(path)) 453 if res.code != 0: 454 raise PathError("Could not create file " + path + ". Response code: " + str(hex(res.code)))
455 ## res = self._send_validated_command(SetFileInfo(path)) 456 ## if res.code != 0: 457 ## raise ProtocolError("Unable to touch " + path + ". Response code: " + hex(res.code)) 458 ## file.wtime = int(time.time()) 459 ## self._bulk_write(file[16:]) 460 461 462 @safe
463 - def put_file(self, infile, path, end_session=True):
464 exists, dest = self._exists(path) 465 if exists: 466 if not dest.is_dir: raise PathError("Cannot write to " + path + " as it already exists") 467 if not path.endswith("/"): path += "/" 468 path += os.path.basename(infile.name) 469 exists, dest = self._exists(path) 470 if exists: raise PathError("Cannot write to " + path + " as it already exists") 471 res = self._send_validated_command(FileCreate(path)) 472 if res.code != 0: 473 raise ProtocolError("There was an error creating device:"+path+". Response code: "+hex(res.code)) 474 chunk_size = 0x8000 475 data_left = True 476 res = self._send_validated_command(FileOpen(path, mode=FileOpen.WRITE)) 477 if res.code != 0: 478 raise ProtocolError("Unable to open " + path + " for writing. Response code: " + hex(res.code)) 479 id = self._bulk_read(20, data_type=IdAnswer, command_number=FileOpen.NUMBER)[0].id 480 pos = 0 481 while data_left: 482 data = array('B') 483 try: 484 data.fromfile(infile, chunk_size) 485 except EOFError: 486 data_left = False 487 res = self._send_validated_command(FileIO(id, pos, len(data), mode=FileIO.WNUMBER)) 488 if res.code != 0: 489 raise ProtocolError("Unable to write to " + path + ". Response code: " + hex(res.code)) 490 self._bulk_write(data) 491 pos += len(data) 492 self._send_validated_command(FileClose(id)) # Ignore res.code as cant do anything if close fails 493 file = self.path_properties(path, end_session=False) 494 if file.file_size != pos: 495 raise ProtocolError("Copying to device failed. The file was truncated by " + str(data.file_size - pos) + " bytes")
496 497 @safe
498 - def del_file(self, path, end_session=True):
499 data = self.path_properties(path, end_session=False) 500 if data.is_dir: raise PathError("Cannot delete directories") 501 res = self._send_validated_command(FileDelete(path), response_type=ListResponse) 502 if res.code != 0: 503 raise ProtocolError("Unable to delete " + path + " with response:\n" + str(res))
504 505 @safe
506 - def mkdir(self, path, end_session=True):
507 if not path.endswith("/"): path += "/" 508 error_prefix = "Cannot create directory " + path 509 res = self._send_validated_command(DirCreate(path)).data[0] 510 if res == 0xffffffcc: 511 raise PathError(error_prefix + " as it already exists") 512 elif res == PathResponseCodes.NOT_FOUND: 513 raise PathError(error_prefix + " as " + path[0:path[:-1].rfind("/")] + " does not exist ") 514 elif res == PathResponseCodes.INVALID: 515 raise PathError(error_prefix + " as " + path + " is invalid") 516 elif res != 0: 517 raise PathError(error_prefix + ". Response code: " + hex(res))
518 519 @safe
520 - def rm(self, path, end_session=True):
521 """ Delete path from device if it is a file or an empty directory """ 522 dir = self.path_properties(path, end_session=False) 523 if not dir.is_dir: 524 self.del_file(path, end_session=False) 525 else: 526 if not path.endswith("/"): path += "/" 527 res = self._send_validated_command(DirDelete(path)) 528 if res.code == PathResponseCodes.HAS_CHILDREN: 529 raise PathError("Cannot delete directory " + path + " as it is not empty") 530 if res.code != 0: 531 raise ProtocolError("Failed to delete directory " + path + ". Response code: " + hex(res.code))
532 533 534 535 #dev = PRS500Device(log_packets=False) 536 #dev.open() 537 #print dev.get_file("/etc/sysctl.conf", sys.stdout) 538 #dev.close() 539