Remove legacy code

This commit is contained in:
Kovid Goyal 2012-08-25 09:34:12 +05:30
parent 34efc911ae
commit 9d697e8146
10 changed files with 39 additions and 2650 deletions

View File

@ -1,7 +1,7 @@
__license__ = 'GPL v3'
__copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
"""
Provides a command-line and optional graphical interface to the SONY Reader PRS-500.
Provides a command-line interface to ebook devices.
For usage information run the script.
"""
@ -275,7 +275,7 @@ def main():
elif command == "cp":
usage="usage: %prog cp [options] source destination\nCopy files to/from the device\n\n"+\
"One of source or destination must be a path on the device. \n\nDevice paths have the form\n"+\
"prs500:mountpoint/my/path\n"+\
"dev:mountpoint/my/path\n"+\
"where mountpoint is one of / or card:/\n\n"+\
"source must point to a file for which you have read permissions\n"+\
"destination must point to a file or directory for which you have write permissions"
@ -286,7 +286,7 @@ def main():
if len(args) != 2:
parser.print_help()
return 1
if args[0].startswith("prs500:"):
if args[0].startswith("dev:"):
outfile = args[1]
path = args[0][7:]
if path.endswith("/"): path = path[:-1]
@ -300,7 +300,7 @@ def main():
return 1
dev.get_file(path, outfile)
outfile.close()
elif args[1].startswith("prs500:"):
elif args[1].startswith("dev:"):
try:
infile = open(args[0], "rb")
except IOError as e:

View File

@ -1,368 +0,0 @@
__license__ = 'GPL v3'
__copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
"""
This module provides a thin ctypes based wrapper around libusb.
"""
from ctypes import cdll, POINTER, byref, pointer, Structure as _Structure, \
c_ubyte, c_ushort, c_int, c_char, c_void_p, c_byte, c_uint
from errno import EBUSY, ENOMEM
from calibre import iswindows, isosx, isbsd, load_library
_libusb_name = 'libusb'
PATH_MAX = 511 if iswindows else 1024 if (isosx or isbsd) else 4096
if iswindows:
class Structure(_Structure):
_pack_ = 1
_libusb_name = 'libusb0'
else:
Structure = _Structure
try:
try:
_libusb = load_library(_libusb_name, cdll)
except OSError:
_libusb = cdll.LoadLibrary('libusb-0.1.so.4')
has_library = True
except:
_libusb = None
has_library = False
class DeviceDescriptor(Structure):
_fields_ = [\
('Length', c_ubyte), \
('DescriptorType', c_ubyte), \
('bcdUSB', c_ushort), \
('DeviceClass', c_ubyte), \
('DeviceSubClass', c_ubyte), \
('DeviceProtocol', c_ubyte), \
('MaxPacketSize0', c_ubyte), \
('idVendor', c_ushort), \
('idProduct', c_ushort), \
('bcdDevice', c_ushort), \
('Manufacturer', c_ubyte), \
('Product', c_ubyte), \
('SerialNumber', c_ubyte), \
('NumConfigurations', c_ubyte) \
]
class EndpointDescriptor(Structure):
_fields_ = [\
('Length', c_ubyte), \
('DescriptorType', c_ubyte), \
('EndpointAddress', c_ubyte), \
('Attributes', c_ubyte), \
('MaxPacketSize', c_ushort), \
('Interval', c_ubyte), \
('Refresh', c_ubyte), \
('SynchAddress', c_ubyte), \
('extra', POINTER(c_char)), \
('extralen', c_int)\
]
class InterfaceDescriptor(Structure):
_fields_ = [\
('Length', c_ubyte), \
('DescriptorType', c_ubyte), \
('InterfaceNumber', c_ubyte), \
('AlternateSetting', c_ubyte), \
('NumEndpoints', c_ubyte), \
('InterfaceClass', c_ubyte), \
('InterfaceSubClass', c_ubyte), \
('InterfaceProtocol', c_ubyte), \
('Interface', c_ubyte), \
('endpoint', POINTER(EndpointDescriptor)), \
('extra', POINTER(c_char)), \
('extralen', c_int)\
]
class Interface(Structure):
_fields_ = [\
('altsetting', POINTER(InterfaceDescriptor)), \
('num_altsetting', c_int)\
]
class ConfigDescriptor(Structure):
_fields_ = [\
('Length', c_ubyte), \
('DescriptorType', c_ubyte), \
('TotalLength', c_ushort), \
('NumInterfaces', c_ubyte), \
('Value', c_ubyte), \
('Configuration', c_ubyte), \
('Attributes', c_ubyte), \
('MaxPower', c_ubyte), \
('interface', POINTER(Interface)), \
('extra', POINTER(c_ubyte)), \
('extralen', c_int) \
]
def __str__(self):
ans = ""
for field in self._fields_:
ans += field[0] + ": " + str(eval('self.'+field[0])) + '\n'
return ans.strip()
class Error(Exception):
pass
class Device(Structure):
def open(self):
""" Open device for use. Return a DeviceHandle. """
handle = _libusb.usb_open(byref(self))
if not handle:
raise Error("Cannot open device")
return handle.contents
@dynamic_property
def configurations(self):
doc = """ List of device configurations. See L{ConfigDescriptor} """
def fget(self):
ans = []
for config in range(self.device_descriptor.NumConfigurations):
ans.append(self.config_descriptor[config])
return tuple(ans)
return property(doc=doc, fget=fget)
class Bus(Structure):
@dynamic_property
def device_list(self):
doc = \
"""
Flat list of devices on this bus.
Note: children are not explored
TODO: Check if exploring children is neccessary (e.g. with an external hub)
"""
def fget(self):
if _libusb is None:
return []
if _libusb.usb_find_devices() < 0:
raise Error('Unable to search for USB devices')
ndev = self.devices
ans = []
while ndev:
dev = ndev.contents
ans.append(dev)
ndev = dev.next
return ans
return property(doc=doc, fget=fget)
class DeviceHandle(Structure):
_fields_ = [\
('fd', c_int), \
('bus', POINTER(Bus)), \
('device', POINTER(Device)), \
('config', c_int), \
('interface', c_int), \
('altsetting', c_int), \
('impl_info', c_void_p)
]
def close(self):
""" Close this DeviceHandle """
_libusb.usb_close(byref(self))
def set_configuration(self, config):
"""
Set device configuration. This has to be called on windows before
trying to claim an interface.
@param config: A L{ConfigDescriptor} or a integer (the ConfigurationValue)
"""
try:
num = config.Value
except AttributeError:
num = config
ret = _libusb.usb_set_configuration(byref(self), num)
if ret < 0:
raise Error('Failed to set device configuration to: ' + str(num) + \
'. Error code: ' + str(ret))
def claim_interface(self, num):
"""
Claim interface C{num} on device.
Must be called before doing anything witht the device.
"""
ret = _libusb.usb_claim_interface(byref(self), num)
if -ret == ENOMEM:
raise Error("Insufficient memory to claim interface")
elif -ret == EBUSY:
raise Error('Device busy')
elif ret < 0:
raise Error('Unknown error occurred while trying to claim USB'\
' interface: ' + str(ret))
def control_msg(self, rtype, request, bytes, value=0, index=0, timeout=100):
"""
Perform a control request to the default control pipe on the device.
@param rtype: specifies the direction of data flow, the type
of request, and the recipient.
@param request: specifies the request.
@param bytes: if the transfer is a write transfer, buffer is a sequence
with the transfer data, otherwise, buffer is the number of
bytes to read.
@param value: specific information to pass to the device.
@param index: specific information to pass to the device.
"""
size = 0
try:
size = len(bytes)
except TypeError:
size = bytes
ArrayType = c_byte * size
_libusb.usb_control_msg.argtypes = [POINTER(DeviceHandle), c_int, \
c_int, c_int, c_int, \
POINTER(ArrayType), \
c_int, c_int]
arr = ArrayType()
rsize = _libusb.usb_control_msg(byref(self), rtype, request, \
value, index, byref(arr), \
size, timeout)
if rsize < size:
raise Error('Could not read ' + str(size) + ' bytes on the '\
'control bus. Read: ' + str(rsize) + ' bytes.')
return arr
else:
ArrayType = c_byte * size
_libusb.usb_control_msg.argtypes = [POINTER(DeviceHandle), c_int, \
c_int, c_int, c_int, \
POINTER(ArrayType), \
c_int, c_int]
arr = ArrayType(*bytes)
return _libusb.usb_control_msg(byref(self), rtype, request, \
value, index, byref(arr), \
size, timeout)
def bulk_read(self, endpoint, size, timeout=100):
"""
Read C{size} bytes via a bulk transfer from the device.
"""
ArrayType = c_byte * size
arr = ArrayType()
_libusb.usb_bulk_read.argtypes = [POINTER(DeviceHandle), c_int, \
POINTER(ArrayType), c_int, c_int
]
rsize = _libusb.usb_bulk_read(byref(self), endpoint, byref(arr), \
size, timeout)
if rsize < 0:
raise Error('Could not read ' + str(size) + ' bytes on the '\
'bulk bus. Error code: ' + str(rsize))
if rsize == 0:
raise Error('Device sent zero bytes')
if rsize < size:
arr = arr[:rsize]
return arr
def bulk_write(self, endpoint, bytes, timeout=100):
"""
Send C{bytes} to device via a bulk transfer.
"""
size = len(bytes)
ArrayType = c_byte * size
arr = ArrayType(*bytes)
_libusb.usb_bulk_write.argtypes = [POINTER(DeviceHandle), c_int, \
POINTER(ArrayType), c_int, c_int
]
_libusb.usb_bulk_write(byref(self), endpoint, byref(arr), size, timeout)
def release_interface(self, num):
ret = _libusb.usb_release_interface(pointer(self), num)
if ret < 0:
raise Error('Unknown error occurred while trying to release USB'\
' interface: ' + str(ret))
def reset(self):
ret = _libusb.usb_reset(pointer(self))
if ret < 0:
raise Error('Unknown error occurred while trying to reset '\
'USB device ' + str(ret))
Bus._fields_ = [ \
('next', POINTER(Bus)), \
('previous', POINTER(Bus)), \
('dirname', c_char * (PATH_MAX+1)), \
('devices', POINTER(Device)), \
('location', c_uint), \
('root_dev', POINTER(Device))\
]
Device._fields_ = [ \
('next', POINTER(Device)), \
('previous', POINTER(Device)), \
('filename', c_char * (PATH_MAX+1)), \
('bus', POINTER(Bus)), \
('device_descriptor', DeviceDescriptor), \
('config_descriptor', POINTER(ConfigDescriptor)), \
('dev', c_void_p), \
('devnum', c_ubyte), \
('num_children', c_ubyte), \
('children', POINTER(POINTER(Device)))
]
if _libusb is not None:
try:
_libusb.usb_get_busses.restype = POINTER(Bus)
_libusb.usb_open.restype = POINTER(DeviceHandle)
_libusb.usb_open.argtypes = [POINTER(Device)]
_libusb.usb_close.argtypes = [POINTER(DeviceHandle)]
_libusb.usb_claim_interface.argtypes = [POINTER(DeviceHandle), c_int]
_libusb.usb_claim_interface.restype = c_int
_libusb.usb_release_interface.argtypes = [POINTER(DeviceHandle), c_int]
_libusb.usb_release_interface.restype = c_int
_libusb.usb_reset.argtypes = [POINTER(DeviceHandle)]
_libusb.usb_reset.restype = c_int
_libusb.usb_control_msg.restype = c_int
_libusb.usb_bulk_read.restype = c_int
_libusb.usb_bulk_write.restype = c_int
_libusb.usb_set_configuration.argtypes = [POINTER(DeviceHandle), c_int]
_libusb.usb_set_configuration.restype = c_int
_libusb.usb_init()
except:
_libusb = None
def busses():
""" Get list of USB busses present on system """
if _libusb is None:
raise Error('Could not find libusb.')
if _libusb.usb_find_busses() < 0:
raise Error('Unable to search for USB busses')
if _libusb.usb_find_devices() < 0:
raise Error('Unable to search for USB devices')
ans = []
nbus = _libusb.usb_get_busses()
while nbus:
bus = nbus.contents
ans.append(bus)
nbus = bus.next
return ans
def get_device_by_id(idVendor, idProduct):
""" Return a L{Device} by vendor and prduct ids """
buslist = busses()
for bus in buslist:
devices = bus.device_list
for dev in devices:
if dev.device_descriptor.idVendor == idVendor and \
dev.device_descriptor.idProduct == idProduct:
return dev
def has_library():
return _libusb is not None
def get_devices():
buslist = busses()
ans = []
for bus in buslist:
devices = bus.device_list
for dev in devices:
device = (dev.device_descriptor.idVendor, dev.device_descriptor.idProduct, dev.device_descriptor.bcdDevice)
ans.append(device)
return ans

View File

@ -1,6 +0,0 @@
__license__ = 'GPL v3'
__copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
'''
Device driver for the Sony Reader PRS 500
'''

View File

@ -1,385 +0,0 @@
__license__ = 'GPL v3'
__copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
"""
This module contains the logic for dealing with XML book lists found
in the reader cache.
"""
import xml.dom.minidom as dom
from base64 import b64decode as decode
from base64 import b64encode as encode
import re
from calibre.devices.interface import BookList as _BookList
from calibre.devices import strftime, strptime
MIME_MAP = { \
"lrf":"application/x-sony-bbeb", \
'lrx':'application/x-sony-bbeb', \
"rtf":"application/rtf", \
"pdf":"application/pdf", \
"txt":"text/plain" \
}
def sortable_title(title):
return re.sub('^\s*A\s+|^\s*The\s+|^\s*An\s+', '', title).rstrip()
class book_metadata_field(object):
""" Represents metadata stored as an attribute """
def __init__(self, attr, formatter=None, setter=None):
self.attr = attr
self.formatter = formatter
self.setter = setter
def __get__(self, obj, typ=None):
""" Return a string. String may be empty if self.attr is absent """
return self.formatter(obj.elem.getAttribute(self.attr)) if \
self.formatter else obj.elem.getAttribute(self.attr).strip()
def __set__(self, obj, val):
""" Set the attribute """
val = self.setter(val) if self.setter else val
if not isinstance(val, unicode):
val = unicode(val, 'utf8', 'replace')
obj.elem.setAttribute(self.attr, val)
class Book(object):
""" Provides a view onto the XML element that represents a book """
title = book_metadata_field("title")
authors = book_metadata_field("author", \
formatter=lambda x: x if x and x.strip() else "Unknown")
mime = book_metadata_field("mime")
rpath = book_metadata_field("path")
id = book_metadata_field("id", formatter=int)
sourceid = book_metadata_field("sourceid", formatter=int)
size = book_metadata_field("size", formatter=int)
# When setting this attribute you must use an epoch
datetime = book_metadata_field("date", formatter=strptime, setter=strftime)
@dynamic_property
def title_sorter(self):
doc = '''String to sort the title. If absent, title is returned'''
def fget(self):
src = self.elem.getAttribute('titleSorter').strip()
if not src:
src = self.title
return src
def fset(self, val):
self.elem.setAttribute('titleSorter', sortable_title(unicode(val)))
return property(doc=doc, fget=fget, fset=fset)
@dynamic_property
def thumbnail(self):
doc = \
"""
The thumbnail. Should be a height 68 image.
Setting is not supported.
"""
def fget(self):
th = self.elem.getElementsByTagName(self.prefix + "thumbnail")
if len(th):
for n in th[0].childNodes:
if n.nodeType == n.ELEMENT_NODE:
th = n
break
rc = ""
for node in th.childNodes:
if node.nodeType == node.TEXT_NODE:
rc += node.data
return decode(rc)
return property(fget=fget, doc=doc)
@dynamic_property
def path(self):
doc = """ Absolute path to book on device. Setting not supported. """
def fget(self):
return self.root + self.rpath
return property(fget=fget, doc=doc)
@dynamic_property
def db_id(self):
doc = '''The database id in the application database that this file corresponds to'''
def fget(self):
match = re.search(r'_(\d+)$', self.rpath.rpartition('.')[0])
if match:
return int(match.group(1))
return property(fget=fget, doc=doc)
def __init__(self, node, tags=[], prefix="", root="/Data/media/"):
self.elem = node
self.prefix = prefix
self.root = root
self.tags = tags
def __str__(self):
""" Return a utf-8 encoded string with title author and path information """
return self.title.encode('utf-8') + " by " + \
self.authors.encode('utf-8') + " at " + self.path.encode('utf-8')
def fix_ids(media, cache, *args):
'''
Adjust ids in cache to correspond with media.
'''
media.purge_empty_playlists()
media.reorder_playlists()
if cache.root:
sourceid = media.max_id()
cid = sourceid + 1
for child in cache.root.childNodes:
if child.nodeType == child.ELEMENT_NODE and child.hasAttribute("sourceid"):
child.setAttribute("sourceid", str(sourceid))
child.setAttribute("id", str(cid))
cid += 1
media.set_next_id(str(cid))
class BookList(_BookList):
"""
A list of L{Book}s. Created from an XML file. Can write list
to an XML file.
"""
__getslice__ = None
__setslice__ = None
def __init__(self, root="/Data/media/", sfile=None):
_BookList.__init__(self)
self.tag_order = {}
self.root = self.document = self.proot = None
if sfile:
sfile.seek(0)
src = sfile.read()
try:
src = src.decode('utf8')
except UnicodeDecodeError:
try:
src = src.decode('latin1')
except UnicodeDecodeError:
src = src.decode('cp1252')
src = src.replace('<cache:', '<xs1:').replace('</cache:', '</xs1:').replace('xmlns:cache', 'xmlns:xs1')
self.document = dom.parseString(src.encode('utf8'))
self.root = self.document.documentElement
self.prefix = ''
records = self.root.getElementsByTagName('records')
if records:
self.prefix = 'xs1:'
self.root = records[0]
self.proot = root
for book in self.document.getElementsByTagName(self.prefix + "text"):
id = book.getAttribute('id')
pl = [i.getAttribute('title') for i in self.get_playlists(id)]
self.append(Book(book, root=root, prefix=self.prefix, tags=pl))
def supports_tags(self):
return bool(self.prefix)
def playlists(self):
return self.root.getElementsByTagName(self.prefix+'playlist')
def playlist_items(self):
plitems = []
for pl in self.playlists():
plitems.extend(pl.getElementsByTagName(self.prefix+'item'))
return plitems
def purge_corrupted_files(self):
if not self.root:
return []
corrupted = self.root.getElementsByTagName(self.prefix+'corrupted')
paths = []
proot = self.proot if self.proot.endswith('/') else self.proot + '/'
for c in corrupted:
paths.append(proot + c.getAttribute('path'))
c.parentNode.removeChild(c)
c.unlink()
return paths
def purge_empty_playlists(self):
''' Remove all playlist entries that have no children. '''
for pl in self.playlists():
if not pl.getElementsByTagName(self.prefix + 'item'):
pl.parentNode.removeChild(pl)
pl.unlink()
def _delete_book(self, node):
nid = node.getAttribute('id')
node.parentNode.removeChild(node)
node.unlink()
self.remove_from_playlists(nid)
def delete_book(self, cid):
'''
Remove DOM node corresponding to book with C{id == cid}.
Also remove book from any collections it is part of.
'''
for book in self:
if str(book.id) == str(cid):
self.remove(book)
self._delete_book(book.elem)
break
def remove_book(self, path):
'''
Remove DOM node corresponding to book with C{path == path}.
Also remove book from any collections it is part of.
'''
for book in self:
if path.endswith(book.rpath):
self.remove(book)
self._delete_book(book.elem)
break
def next_id(self):
return self.document.documentElement.getAttribute('nextID')
def set_next_id(self, id):
self.document.documentElement.setAttribute('nextID', str(id))
def max_id(self):
max = 0
for child in self.root.childNodes:
if child.nodeType == child.ELEMENT_NODE and child.hasAttribute("id"):
nid = int(child.getAttribute('id'))
if nid > max:
max = nid
return max
def book_by_path(self, path):
for child in self.root.childNodes:
if child.nodeType == child.ELEMENT_NODE and child.hasAttribute("path"):
if path == child.getAttribute('path'):
return child
return None
def add_book(self, mi, name, size, ctime):
""" Add a node into DOM tree representing a book """
book = self.book_by_path(name)
if book is not None:
self.remove_book(name)
node = self.document.createElement(self.prefix + "text")
mime = MIME_MAP[name[name.rfind(".")+1:].lower()]
cid = self.max_id()+1
sourceid = str(self[0].sourceid) if len(self) else "1"
attrs = {
"title" : mi.title,
'titleSorter' : sortable_title(mi.title),
"author" : mi.format_authors() if mi.format_authors() else _('Unknown'),
"page":"0", "part":"0", "scale":"0", \
"sourceid":sourceid, "id":str(cid), "date":"", \
"mime":mime, "path":name, "size":str(size)
}
for attr in attrs.keys():
node.setAttributeNode(self.document.createAttribute(attr))
node.setAttribute(attr, attrs[attr])
try:
w, h, data = mi.thumbnail
except:
w, h, data = None, None, None
if data:
th = self.document.createElement(self.prefix + "thumbnail")
th.setAttribute("width", str(w))
th.setAttribute("height", str(h))
jpeg = self.document.createElement(self.prefix + "jpeg")
jpeg.appendChild(self.document.createTextNode(encode(data)))
th.appendChild(jpeg)
node.appendChild(th)
self.root.appendChild(node)
book = Book(node, root=self.proot, prefix=self.prefix)
book.datetime = ctime
self.append(book)
self.set_next_id(cid+1)
tags = []
if mi.tags:
tags.extend(mi.tags)
if mi.series:
tags.append(mi.series)
if self.prefix and tags: # Playlists only supportted in main memory
if hasattr(mi, 'tag_order'):
self.tag_order.update(mi.tag_order)
self.set_tags(book, tags)
def playlist_by_title(self, title):
for pl in self.playlists():
if pl.getAttribute('title').lower() == title.lower():
return pl
def add_playlist(self, title):
cid = self.max_id()+1
pl = self.document.createElement(self.prefix+'playlist')
pl.setAttribute('sourceid', '0')
pl.setAttribute('id', str(cid))
pl.setAttribute('title', title)
for child in self.root.childNodes:
try:
if child.getAttribute('id') == '1':
self.root.insertBefore(pl, child)
self.set_next_id(cid+1)
break
except AttributeError:
continue
return pl
def remove_from_playlists(self, id):
for pli in self.playlist_items():
if pli.getAttribute('id') == str(id):
pli.parentNode.removeChild(pli)
pli.unlink()
def set_tags(self, book, tags):
book.tags = tags
self.set_playlists(book.id, tags)
def set_playlists(self, id, collections):
self.remove_from_playlists(id)
for collection in set(collections):
coll = self.playlist_by_title(collection)
if not coll:
coll = self.add_playlist(collection)
item = self.document.createElement(self.prefix+'item')
item.setAttribute('id', str(id))
coll.appendChild(item)
def get_playlists(self, id):
ans = []
for pl in self.playlists():
for item in pl.getElementsByTagName(self.prefix+'item'):
if item.getAttribute('id') == str(id):
ans.append(pl)
continue
return ans
def book_by_id(self, id):
for book in self:
if str(book.id) == str(id):
return book
def reorder_playlists(self):
for title in self.tag_order.keys():
pl = self.playlist_by_title(title)
if not pl:
continue
db_ids = [i.getAttribute('id') for i in pl.childNodes if hasattr(i, 'getAttribute')]
pl_book_ids = [self.book_by_id(i.getAttribute('id')).db_id for i in pl.childNodes if hasattr(i, 'getAttribute')]
map = {}
for i, j in zip(pl_book_ids, db_ids):
map[i] = j
pl_book_ids = [i for i in pl_book_ids if i is not None]
ordered_ids = [i for i in self.tag_order[title] if i in pl_book_ids]
if len(ordered_ids) < len(pl.childNodes):
continue
children = [i for i in pl.childNodes if hasattr(i, 'getAttribute')]
for child in children:
pl.removeChild(child)
child.unlink()
for id in ordered_ids:
item = self.document.createElement(self.prefix+'item')
item.setAttribute('id', str(map[id]))
pl.appendChild(item)
def write(self, stream):
""" Write XML representation of DOM tree to C{stream} """
stream.write(self.document.toxml('utf-8'))

View File

@ -1,9 +0,0 @@
__license__ = 'GPL v3'
__copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
"""
Provides a command-line interface to the SONY Reader PRS-500.
For usage information run the script.
"""
__docformat__ = "epytext"
__author__ = "Kovid Goyal <kovid@kovidgoyal.net>"

View File

@ -1,989 +0,0 @@
__license__ = 'GPL v3'
__copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
### End point description for PRS-500 procductId=667
### Endpoint Descriptor:
### bLength 7
### bDescriptorType 5
### bEndpointAddress 0x81 EP 1 IN
### bmAttributes 2
### Transfer Type Bulk
### Synch Type None
### Usage Type Data
### wMaxPacketSize 0x0040 1x 64 bytes
### bInterval 0
### Endpoint Descriptor:
### bLength 7
### bDescriptorType 5
### bEndpointAddress 0x02 EP 2 OUT
### bmAttributes 2
### Transfer Type Bulk
### Synch Type None
### Usage Type Data
### wMaxPacketSize 0x0040 1x 64 bytes
### bInterval 0
###
###
### Endpoint 0x81 is device->host and endpoint 0x02 is host->device.
### You can establish Stream pipes to/from these endpoints for Bulk transfers.
### Has two configurations 1 is the USB charging config 2 is the self-powered
### config. I think config management is automatic. Endpoints are the same
"""
Contains the logic for communication with the device (a SONY PRS-500).
The public interface of class L{PRS500} defines the
methods for performing various tasks.
"""
import sys, os
from tempfile import TemporaryFile
from array import array
from functools import wraps
from StringIO import StringIO
from threading import RLock
from calibre.devices.interface import DevicePlugin
from calibre.devices.libusb import Error as USBError
from calibre.devices.libusb import get_device_by_id
from calibre.devices.prs500.prstypes import *
from calibre.devices.errors import *
from calibre.devices.prs500.books import BookList, fix_ids
from calibre import __author__, __appname__
from calibre.devices.usbms.deviceconfig import DeviceConfig
# Protocol versions this driver has been tested with
KNOWN_USB_PROTOCOL_VERSIONS = [0x3030303030303130L]
lock = RLock()
class File(object):
"""
Wrapper that allows easy access to all information about files/directories
"""
def __init__(self, _file):
self.is_dir = _file[1].is_dir #: True if self is a directory
self.is_readonly = _file[1].is_readonly #: True if self is readonly
self.size = _file[1].file_size #: Size in bytes of self
self.ctime = _file[1].ctime #: Creation time of self as a epoch
self.wtime = _file[1].wtime #: Creation time of self as an epoch
path = _file[0]
if path.endswith("/"):
path = path[:-1]
self.path = path #: Path to self
self.name = path[path.rfind("/")+1:].rstrip() #: Name of self
def __repr__(self):
""" Return path to self """
return "File:" + self.path
def __str__(self):
return self.name
class PRS500(DeviceConfig, DevicePlugin):
"""
Implements the backend for communication with the SONY Reader.
Each method decorated by C{safe} performs a task.
"""
name = 'PRS-500 Device Interface'
description = _('Communicate with the Sony PRS-500 eBook reader.')
author = _('Kovid Goyal')
supported_platforms = ['windows', 'osx', 'linux']
log_packets = False
VENDOR_ID = 0x054c #: SONY Vendor Id
PRODUCT_ID = 0x029b #: Product Id for the PRS-500
BCD = [0x100]
PRODUCT_NAME = 'PRS-500'
gui_name = PRODUCT_NAME
VENDOR_NAME = 'SONY'
INTERFACE_ID = 0 #: The interface we use to talk to the device
BULK_IN_EP = 0x81 #: Endpoint for Bulk reads
BULK_OUT_EP = 0x02 #: Endpoint for Bulk writes
# Location of media.xml file on device
MEDIA_XML = "/Data/database/cache/media.xml"
# Location of cache.xml on storage card in device
CACHE_XML = "/Sony Reader/database/cache.xml"
# Ordered list of supported formats
FORMATS = ["lrf", "lrx", "rtf", "pdf", "txt"]
# Height for thumbnails of books/images on the device
THUMBNAIL_HEIGHT = 68
# Directory on card to which books are copied
CARD_PATH_PREFIX = __appname__
_packet_number = 0 #: Keep track of the packet number for packet tracing
SUPPORTS_SUB_DIRS = False
MUST_READ_METADATA = True
def log_packet(self, packet, header, stream=sys.stderr):
"""
Log C{packet} to stream C{stream}.
Header should be a small word describing the type of packet.
"""
self._packet_number += 1
print >> stream, str(self._packet_number), header, "Type:", \
packet.__class__.__name__
print >> stream, packet
print >> stream, "--"
@classmethod
def validate_response(cls, res, _type=0x00, number=0x00):
"""
Raise a ProtocolError if the type and number of C{res}
is not the same as C{type} and C{number}.
"""
if _type != res.type or number != res.rnumber:
raise ProtocolError("Inavlid response.\ntype: expected=" + \
hex(_type)+" actual=" + hex(res.type) + \
"\nrnumber: expected=" + hex(number) + \
" actual="+hex(res.rnumber))
@classmethod
def signature(cls):
""" Return a two element tuple (vendor id, product id) """
return (cls.VENDOR_ID, cls.PRODUCT_ID )
def safe(func):
"""
Decorator that wraps a call to C{func} to ensure that
exceptions are handled correctly. It also calls L{open} to claim
the interface and initialize the Reader if needed.
As a convenience, C{safe} automatically sends the a
L{EndSession} after calling func, unless func has
a keyword argument named C{end_session} set to C{False}.
An L{ArgumentError} will cause the L{EndSession} command to
be sent to the device, unless end_session is set to C{False}.
An L{usb.USBError} will cause the library to release control of the
USB interface via a call to L{close}.
"""
@wraps(func)
def run_session(*args, **kwargs):
with lock:
dev = args[0]
res = None
try:
if not hasattr(dev, 'in_session'):
dev.reset()
if not dev.handle:
dev.open()
if not getattr(dev, 'in_session', False):
dev.send_validated_command(BeginEndSession(end=False))
dev.in_session = True
res = func(*args, **kwargs)
except ArgumentError:
if not kwargs.has_key("end_session") or kwargs["end_session"]:
dev.send_validated_command(BeginEndSession(end=True))
dev.in_session = False
raise
except USBError as err:
if "No such device" in str(err):
raise DeviceError()
elif "Connection timed out" in str(err):
dev.close()
raise TimeoutError(func.__name__)
elif "Protocol error" in str(err):
dev.close()
raise ProtocolError("There was an unknown error in the"+\
" protocol. Contact " + __author__)
dev.close()
raise
if not kwargs.has_key("end_session") or kwargs["end_session"]:
dev.send_validated_command(BeginEndSession(end=True))
dev.in_session = False
return res
return run_session
def reset(self, key='-1', log_packets=False, report_progress=None,
detected_device=None) :
"""
@param key: The key to unlock the device
@param log_packets: If true the packet stream to/from the device is logged
@param report_progress: Function that is called with a % progress
(number between 0 and 100) for various tasks
If it is called with -1 that means that the
task does not have any progress information
"""
with lock:
self.device = get_device_by_id(self.VENDOR_ID, self.PRODUCT_ID)
# Handle that is used to communicate with device. Setup in L{open}
self.handle = None
self.in_session = False
self.log_packets = log_packets
self.report_progress = report_progress
if len(key) > 8:
key = key[:8]
elif len(key) < 8:
key += ''.join(['\0' for i in xrange(8 - len(key))])
self.key = key
def reconnect(self):
""" Only recreates the device node and deleted the connection handle """
self.device = get_device_by_id(self.VENDOR_ID, self.PRODUCT_ID)
self.handle = None
@classmethod
def is_connected(cls, helper=None):
"""
This method checks to see whether the device is physically connected.
It does not return any information about the validity of the
software connection. You may need to call L{reconnect} if you keep
getting L{DeviceError}.
"""
try:
return get_device_by_id(cls.VENDOR_ID, cls.PRODUCT_ID) != None
except USBError:
return False
def set_progress_reporter(self, report_progress):
self.report_progress = report_progress
def open(self, connected_device, library_uuid) :
"""
Claim an interface on the device for communication.
Requires write privileges to the device file.
Also initialize the device.
See the source code for the sequence of initialization commands.
"""
with lock:
if not hasattr(self, 'key'):
self.reset()
self.device = get_device_by_id(self.VENDOR_ID, self.PRODUCT_ID)
if not self.device:
raise DeviceError()
configs = self.device.configurations
try:
self.handle = self.device.open()
config = configs[0]
try:
self.handle.set_configuration(configs[0])
except USBError:
self.handle.set_configuration(configs[1])
config = configs[1]
_id = config.interface.contents.altsetting.contents
ed1 = _id.endpoint[0]
ed2 = _id.endpoint[1]
if ed1.EndpointAddress == self.BULK_IN_EP:
red, wed = ed1, ed2
else:
red, wed = ed2, ed1
self.bulk_read_max_packet_size = red.MaxPacketSize
self.bulk_write_max_packet_size = wed.MaxPacketSize
self.handle.claim_interface(self.INTERFACE_ID)
except USBError as err:
raise DeviceBusy(str(err))
# Large timeout as device may still be initializing
res = self.send_validated_command(GetUSBProtocolVersion(), timeout=20000)
if res.code != 0:
raise ProtocolError("Unable to get USB Protocol version.")
version = self._bulk_read(24, data_type=USBProtocolVersion)[0].version
if version not in KNOWN_USB_PROTOCOL_VERSIONS:
print >> sys.stderr, "WARNING: Usb protocol version " + \
hex(version) + " is unknown"
res = self.send_validated_command(SetBulkSize(\
chunk_size = 512*self.bulk_read_max_packet_size, \
unknown = 2))
if res.code != 0:
raise ProtocolError("Unable to set bulk size.")
res = self.send_validated_command(UnlockDevice(key=self.key))#0x312d))
if res.code != 0:
raise DeviceLocked()
res = self.send_validated_command(SetTime())
if res.code != 0:
raise ProtocolError("Could not set time on device")
def eject(self):
pass
def close(self):
""" Release device interface """
with lock:
try:
self.handle.reset()
self.handle.release_interface(self.INTERFACE_ID)
except Exception as err:
print >> sys.stderr, err
self.handle, self.device = None, None
self.in_session = False
def _send_command(self, command, response_type=Response, timeout=1000):
"""
Send L{command<Command>} to device and return its L{response<Response>}.
@param command: an object of type Command or one of its derived classes
@param response_type: an object of type 'type'. The return packet
from the device is returned as an object of type response_type.
@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.
"""
with lock:
if self.log_packets:
self.log_packet(command, "Command")
bytes_sent = self.handle.control_msg(0x40, 0x80, command)
if bytes_sent != len(command):
raise ControlError(desc="Could not send control request to device\n"\
+ str(command))
response = response_type(self.handle.control_msg(0xc0, 0x81, \
Response.SIZE, timeout=timeout))
if self.log_packets:
self.log_packet(response, "Response")
return response
def send_validated_command(self, command, cnumber=None, \
response_type=Response, timeout=1000):
"""
Wrapper around L{_send_command} that checks if the
C{Response.rnumber == cnumber or
command.number if cnumber==None}. Also check that
C{Response.type == Command.type}.
"""
if cnumber == None:
cnumber = command.number
res = self._send_command(command, response_type=response_type, \
timeout=timeout)
self.validate_response(res, _type=command.type, number=cnumber)
return res
def _bulk_write(self, data, packet_size=0x1000):
"""
Send data to device via a bulk transfer.
@type data: Any listable type supporting __getslice__
@param packet_size: Size of packets to be sent to device.
C{data} is broken up into packets to be sent to device.
"""
with lock:
def bulk_write_packet(packet):
self.handle.bulk_write(self.BULK_OUT_EP, packet)
if self.log_packets:
self.log_packet(Answer(packet), "Answer h->d")
bytes_left = len(data)
if bytes_left + 16 <= packet_size:
packet_size = bytes_left +16
first_packet = Answer(bytes_left+16)
first_packet[16:] = data
first_packet.length = len(data)
else:
first_packet = Answer(packet_size)
first_packet[16:] = data[0:packet_size-16]
first_packet.length = packet_size-16
first_packet.number = 0x10005
bulk_write_packet(first_packet)
pos = first_packet.length
bytes_left -= first_packet.length
while bytes_left > 0:
endpos = pos + packet_size if pos + packet_size <= len(data) \
else len(data)
bulk_write_packet(data[pos:endpos])
bytes_left -= endpos - pos
pos = endpos
res = Response(self.handle.control_msg(0xc0, 0x81, Response.SIZE, \
timeout=5000))
if self.log_packets:
self.log_packet(res, "Response")
if res.rnumber != 0x10005 or res.code != 0:
raise ProtocolError("Sending via Bulk Transfer failed with response:\n"\
+str(res))
if res.data_size != len(data):
raise ProtocolError("Unable to transfer all data to device. "+\
"Response packet:\n"\
+str(res))
def _bulk_read(self, bytes, command_number=0x00, packet_size=0x1000, \
data_type=Answer):
"""
Read in C{bytes} bytes via a bulk transfer in
packets of size S{<=} C{packet_size}
@param data_type: an object of type type.
The data packet is returned as an object of type C{data_type}.
@return: A list of packets read from the device.
Each packet is of type data_type
"""
with lock:
msize = self.bulk_read_max_packet_size
def bulk_read_packet(data_type=Answer, size=0x1000):
rsize = size
if size % msize:
rsize = size - size % msize + msize
data = data_type(self.handle.bulk_read(self.BULK_IN_EP, rsize))
if self.log_packets:
self.log_packet(data, "Answer d->h")
if len(data) != size:
raise ProtocolError("Unable to read " + str(size) + " bytes from "\
"device. Read: " + str(len(data)) + " bytes")
return data
bytes_left = bytes
packets = []
while bytes_left > 0:
if packet_size > bytes_left:
packet_size = bytes_left
packet = bulk_read_packet(data_type=data_type, size=packet_size)
bytes_left -= len(packet)
packets.append(packet)
self.send_validated_command(\
AcknowledgeBulkRead(packets[0].number), \
cnumber=command_number)
return packets
@safe
def get_device_information(self, end_session=True):
"""
Ask device for device information. See L{DeviceInfoQuery}.
@return: (device name, device version, software version on device, mime type)
"""
size = self.send_validated_command(DeviceInfoQuery()).data[2] + 16
ans = self._bulk_read(size, command_number=\
DeviceInfoQuery.NUMBER, data_type=DeviceInfo)[0]
return (ans.device_name, ans.device_version, \
ans.software_version, ans.mime_type)
@safe
def path_properties(self, path, end_session=True):
"""
Send command asking device for properties of C{path}.
Return L{FileProperties}.
"""
res = self.send_validated_command(PathQuery(path), \
response_type=ListResponse)
data = self._bulk_read(0x28, data_type=FileProperties, \
command_number=PathQuery.NUMBER)[0]
if path.endswith('/') and path != '/':
path = path[:-1]
if res.path_not_found :
raise PathError(path + " does not exist on device")
if res.is_invalid:
raise PathError(path + " is not a valid path")
if res.is_unmounted:
raise PathError(path + " is not mounted")
if res.permission_denied:
raise PathError('Permission denied for: ' + path + '\nYou can only '+\
'operate on paths starting with /Data, a:/ or b:/')
if res.code not in (0, PathResponseCodes.IS_FILE):
raise PathError(path + " has an unknown error. Code: " + \
hex(res.code))
return data
@safe
def get_file(self, path, outfile, end_session=True):
"""
Read the file at path on the device and write it to outfile.
The data is fetched in chunks of size S{<=} 32K. Each chunk is
made of packets of size S{<=} 4K. See L{FileOpen},
L{FileRead} and L{FileClose} for details on the command packets used.
@param outfile: file object like C{sys.stdout} or the result of an C{open} call
"""
if path.endswith("/"):
path = path[:-1] # We only copy files
cp = self.card_prefix(False)
path = path.replace('card:/', cp if cp else '')
_file = self.path_properties(path, end_session=False)
if _file.is_dir:
raise PathError("Cannot read as " + path + " is a directory")
bytes = _file.file_size
res = self.send_validated_command(FileOpen(path))
if res.code != 0:
raise PathError("Unable to open " + path + \
" for reading. Response code: " + hex(res.code))
_id = self._bulk_read(20, data_type=IdAnswer, \
command_number=FileOpen.NUMBER)[0].id
# The first 16 bytes from the device are meta information on the packet stream
bytes_left, chunk_size = bytes, 512 * self.bulk_read_max_packet_size -16
packet_size, pos = 64 * self.bulk_read_max_packet_size, 0
while bytes_left > 0:
if chunk_size > bytes_left:
chunk_size = bytes_left
res = self.send_validated_command(FileIO(_id, pos, chunk_size))
if res.code != 0:
self.send_validated_command(FileClose(id))
raise ProtocolError("Error while reading from " + path + \
". Response code: " + hex(res.code))
packets = self._bulk_read(chunk_size+16, \
command_number=FileIO.RNUMBER, packet_size=packet_size)
try:
outfile.write("".join(map(chr, packets[0][16:])))
for i in range(1, len(packets)):
outfile.write("".join(map(chr, packets[i])))
except IOError as err:
self.send_validated_command(FileClose(_id))
raise ArgumentError("File get operation failed. " + \
"Could not write to local location: " + str(err))
bytes_left -= chunk_size
pos += chunk_size
if self.report_progress:
self.report_progress(int(100*((1.*pos)/bytes)))
self.send_validated_command(FileClose(_id))
# Not going to check response code to see if close was successful
# as there's not much we can do if it wasnt
@safe
def list(self, path, recurse=False, end_session=True):
"""
Return a listing of path. See the code for details. See L{DirOpen},
L{DirRead} and L{DirClose} for details on the command packets used.
@type path: string
@param path: The path to list
@type recurse: boolean
@param recurse: If true do a recursive listing
@return: A list of tuples. The first element of each tuple is a path.
The second element is a list of L{Files<File>}.
The path is the path we are listing, the C{Files} are the
files/directories in that path. If it is a recursive list, then the first
element will be (C{path}, children), the next will be
(child, its children) and so on. If it is not recursive the length of the
outermost list will be 1.
"""
def _list(path):
""" Do a non recursive listsing of path """
if not path.endswith("/"):
path += "/" # Initially assume path is a directory
cp = self.card_prefix(False)
path = path.replace('card:/', cp if cp else '')
files = []
candidate = self.path_properties(path, end_session=False)
if not candidate.is_dir:
path = path[:-1]
data = self.path_properties(path, end_session=False)
files = [ File((path, data)) ]
else:
# Get query ID used to ask for next element in list
res = self.send_validated_command(DirOpen(path))
if res.code != 0:
raise PathError("Unable to open directory " + path + \
" for reading. Response code: " + hex(res.code))
_id = self._bulk_read(0x14, data_type=IdAnswer, \
command_number=DirOpen.NUMBER)[0].id
# Create command asking for next element in list
next = DirRead(_id)
items = []
while True:
res = self.send_validated_command(next, response_type=ListResponse)
size = res.data_size + 16
data = self._bulk_read(size, data_type=ListAnswer, \
command_number=DirRead.NUMBER)[0]
# path_not_found seems to happen if the usb server
# doesn't have the permissions to access the directory
if res.is_eol or res.path_not_found:
break
elif res.code != 0:
raise ProtocolError("Unknown error occured while "+\
"reading contents of directory " + path + \
". Response code: " + hex(res.code))
items.append(data.name)
self.send_validated_command(DirClose(_id))
# Ignore res.code as we cant do anything if close fails
for item in items:
ipath = path + item
data = self.path_properties(ipath, end_session=False)
files.append( File( (ipath, data) ) )
files.sort()
return files
files = _list(path)
dirs = [(path, files)]
for _file in files:
if recurse and _file.is_dir and not _file.path.startswith(("/dev","/proc")):
dirs[len(dirs):] = self.list(_file.path, recurse=True, end_session=False)
return dirs
@safe
def total_space(self, end_session=True):
"""
Get total space available on the mountpoints:
1. Main memory
2. Memory Stick
3. SD Card
@return: A 3 element list with total space in bytes of (1, 2, 3)
"""
data = []
for path in ("/Data/", "a:/", "b:/"):
# Timeout needs to be increased as it takes time to read card
res = self.send_validated_command(TotalSpaceQuery(path), \
timeout=5000)
buffer_size = 16 + res.data[2]
pkt = self._bulk_read(buffer_size, data_type=TotalSpaceAnswer, \
command_number=TotalSpaceQuery.NUMBER)[0]
data.append( pkt.total )
return data
@safe
def card_prefix(self, end_session=True):
try:
path = 'a:/'
self.path_properties(path, end_session=False)
return path
except PathError:
try:
path = 'b:/'
self.path_properties(path, end_session=False)
return path
except PathError:
return None
@safe
def free_space(self, end_session=True):
"""
Get free space available on the mountpoints:
1. Main memory
2. Memory Stick
3. SD Card
@return: A 3 element list with free space in bytes of (1, 2, 3)
"""
data = []
for path in ("/", "a:/", "b:/"):
# Timeout needs to be increased as it takes time to read card
self.send_validated_command(FreeSpaceQuery(path), \
timeout=5000)
pkt = self._bulk_read(FreeSpaceAnswer.SIZE, \
data_type=FreeSpaceAnswer, \
command_number=FreeSpaceQuery.NUMBER)[0]
data.append( pkt.free )
data = [x for x in data if x != 0]
data.append(0)
return data
def _exists(self, path):
""" Return (True, FileProperties) if path exists or (False, None) otherwise """
dest = None
try:
dest = self.path_properties(path, end_session=False)
except PathError as err:
if "does not exist" in str(err) or "not mounted" in str(err):
return (False, None)
else: raise
return (True, dest)
@safe
def touch(self, path, end_session=True):
"""
Create a file at path
@todo: Update file modification time if it exists.
Opening the file in write mode and then closing it doesn't work.
"""
cp = self.card_prefix(False)
path = path.replace('card:/', cp if cp else '')
if path.endswith("/") and len(path) > 1:
path = path[:-1]
exists, _file = self._exists(path)
if exists and _file.is_dir:
raise PathError("Cannot touch directories")
if not exists:
res = self.send_validated_command(FileCreate(path))
if res.code != 0:
raise PathError("Could not create file " + path + \
". Response code: " + str(hex(res.code)))
@safe
def put_file(self, infile, path, replace_file=False, end_session=True):
"""
Put infile onto the devoce at path
@param infile: An open file object. infile must have a name attribute.
If you are using a StringIO object set its name attribute manually.
@param path: The path on the device at which to put infile.
It should point to an existing directory.
@param replace_file: If True and path points to a file that already exists, it is replaced
"""
pos = infile.tell()
infile.seek(0, 2)
bytes = infile.tell() - pos
start_pos = pos
infile.seek(pos)
cp = self.card_prefix(False)
path = path.replace('card:/', cp if cp else '')
exists, dest = self._exists(path)
if exists:
if dest.is_dir:
if not path.endswith("/"):
path += "/"
path += os.path.basename(infile.name)
return self.put_file(infile, path, replace_file=replace_file, end_session=False)
else:
if not replace_file:
raise PathError("Cannot write to " + \
path + " as it already exists", path=path)
_file = self.path_properties(path, end_session=False)
if _file.file_size > bytes:
self.del_file(path, end_session=False)
self.touch(path, end_session=False)
else: self.touch(path, end_session=False)
chunk_size = 512 * self.bulk_write_max_packet_size
data_left = True
res = self.send_validated_command(FileOpen(path, mode=FileOpen.WRITE))
if res.code != 0:
raise ProtocolError("Unable to open " + path + \
" for writing. Response code: " + hex(res.code))
_id = self._bulk_read(20, data_type=IdAnswer, \
command_number=FileOpen.NUMBER)[0].id
while data_left:
data = array('B')
try:
# Cannot use data.fromfile(infile, chunk_size) as it
# doesn't work in windows w/ python 2.5.1
ind = infile.read(chunk_size)
data.fromstring(ind)
if len(ind) < chunk_size:
raise EOFError
except EOFError:
data_left = False
res = self.send_validated_command(FileIO(_id, pos, len(data), \
mode=FileIO.WNUMBER))
if res.code != 0:
raise ProtocolError("Unable to write to " + \
path + ". Response code: " + hex(res.code))
self._bulk_write(data)
pos += len(data)
if self.report_progress:
self.report_progress( int(100*(pos-start_pos)/(1.*bytes)) )
self.send_validated_command(FileClose(_id))
# Ignore res.code as cant do anything if close fails
_file = self.path_properties(path, end_session=False)
if _file.file_size != pos:
raise ProtocolError("Copying to device failed. The file " +\
"on the device is larger by " + \
str(_file.file_size - pos) + " bytes")
@safe
def del_file(self, path, end_session=True):
""" Delete C{path} from device iff path is a file """
data = self.path_properties(path, end_session=False)
if data.is_dir:
raise PathError("Cannot delete directories")
res = self.send_validated_command(FileDelete(path), \
response_type=ListResponse)
if res.code != 0:
raise ProtocolError("Unable to delete " + path + \
" with response:\n" + str(res))
@safe
def mkdir(self, path, end_session=True):
""" Make directory """
if path.startswith('card:/'):
cp = self.card_prefix(False)
path = path.replace('card:/', cp if cp else '')
if not path.endswith("/"):
path += "/"
error_prefix = "Cannot create directory " + path
res = self.send_validated_command(DirCreate(path)).data[0]
if res == 0xffffffcc:
raise PathError(error_prefix + " as it already exists")
elif res == PathResponseCodes.NOT_FOUND:
raise PathError(error_prefix + " as " + \
path[0:path[:-1].rfind("/")] + " does not exist ")
elif res == PathResponseCodes.INVALID:
raise PathError(error_prefix + " as " + path + " is invalid")
elif res != 0:
raise PathError(error_prefix + ". Response code: " + hex(res))
@safe
def rm(self, path, end_session=True):
""" Delete path from device if it is a file or an empty directory """
cp = self.card_prefix(False)
path = path.replace('card:/', cp if cp else '')
dir = self.path_properties(path, end_session=False)
if not dir.is_dir:
self.del_file(path, end_session=False)
else:
if not path.endswith("/"):
path += "/"
res = self.send_validated_command(DirDelete(path))
if res.code == PathResponseCodes.HAS_CHILDREN:
raise PathError("Cannot delete directory " + path + \
" as it is not empty")
if res.code != 0:
raise ProtocolError("Failed to delete directory " + path + \
". Response code: " + hex(res.code))
@safe
def card(self, end_session=True):
""" Return path prefix to installed card or None """
card = None
try:
if self._exists("a:/")[0]:
card = "a:"
except:
pass
try:
if self._exists("b:/")[0]:
card = "b:"
except:
pass
return card
@safe
def books(self, oncard=False, end_session=True):
"""
Return a list of ebooks on the device.
@param oncard: If True return a list of ebooks on the storage card,
otherwise return list of ebooks in main memory of device
@return: L{BookList}
"""
root = "/Data/media/"
tfile = TemporaryFile()
if oncard:
try:
self.get_file("a:"+self.CACHE_XML, tfile, end_session=False)
root = "a:/"
except PathError:
try:
self.get_file("b:"+self.CACHE_XML, tfile, end_session=False)
root = "b:/"
except PathError: pass
if tfile.tell() == 0:
tfile = None
else:
self.get_file(self.MEDIA_XML, tfile, end_session=False)
bl = BookList(root=root, sfile=tfile)
paths = bl.purge_corrupted_files()
for path in paths:
try:
self.del_file(path, end_session=False)
except PathError: # Incase this is a refetch without a sync in between
continue
return bl
@safe
def remove_books(self, paths, booklists, end_session=True):
"""
Remove the books specified by paths from the device. The metadata
cache on the device should also be updated.
"""
for path in paths:
self.del_file(path, end_session=False)
fix_ids(booklists[0], booklists[1])
self.sync_booklists(booklists, end_session=False)
@safe
def sync_booklists(self, booklists, end_session=True):
'''
Upload bookslists to device.
@param booklists: A tuple containing the result of calls to
(L{books}(oncard=False), L{books}(oncard=True)).
'''
fix_ids(*booklists)
self.upload_book_list(booklists[0], end_session=False)
if booklists[1].root:
self.upload_book_list(booklists[1], end_session=False)
@safe
def upload_books(self, files, names, on_card=False, end_session=True,
metadata=None):
card = self.card(end_session=False)
prefix = card + '/' + self.CARD_PATH_PREFIX +'/' if on_card else '/Data/media/books/'
if on_card and not self._exists(prefix)[0]:
self.mkdir(prefix[:-1], False)
paths, ctimes = [], []
names = iter(names)
infiles = [file if hasattr(file, 'read') else open(file, 'rb') for file in files]
for f in infiles: f.seek(0, 2)
sizes = [f.tell() for f in infiles]
size = sum(sizes)
space = self.free_space(end_session=False)
mspace = space[0]
cspace = space[2] if len(space) > 2 and space[2] >= space[1] else space[1]
if on_card and size > cspace - 1024*1024:
raise FreeSpaceError("There is insufficient free space "+\
"on the storage card")
if not on_card and size > mspace - 2*1024*1024:
raise FreeSpaceError("There is insufficient free space " +\
"in main memory")
for infile in infiles:
infile.seek(0)
name = names.next()
paths.append(prefix+name)
self.put_file(infile, paths[-1], replace_file=True, end_session=False)
ctimes.append(self.path_properties(paths[-1], end_session=False).ctime)
return zip(paths, sizes, ctimes)
@classmethod
def add_books_to_metadata(cls, locations, metadata, booklists):
metadata = iter(metadata)
for location in locations:
info = metadata.next()
path = location[0]
on_card = 1 if path[1] == ':' else 0
name = path.rpartition('/')[2]
name = (cls.CARD_PATH_PREFIX+'/' if on_card else 'books/') + name
booklists[on_card].add_book(info, name, *location[1:])
fix_ids(*booklists)
@safe
def delete_books(self, paths, end_session=True):
for path in paths:
self.del_file(path, end_session=False)
@classmethod
def remove_books_from_metadata(cls, paths, booklists):
for path in paths:
on_card = 1 if path[1] == ':' else 0
booklists[on_card].remove_book(path)
fix_ids(*booklists)
@safe
def add_book(self, infile, name, info, booklists, oncard=False, \
sync_booklists=False, end_session=True):
"""
Add a book to the device. If oncard is True then the book is copied
to the card rather than main memory.
@param infile: The source file, should be opened in "rb" mode
@param name: The name of the book file when uploaded to the
device. The extension of name must be one of
the supported formats for this device.
@param info: A dictionary that must have the keys "title", "authors", "cover".
C{info["cover"]} should be a three element tuple (width, height, data)
where data is the image data in JPEG format as a string
@param booklists: A tuple containing the result of calls to
(L{books}(oncard=False), L{books}(oncard=True)).
"""
infile.seek(0, 2)
size = infile.tell()
infile.seek(0)
card = self.card(end_session=False)
space = self.free_space(end_session=False)
mspace = space[0]
cspace = space[1] if space[1] >= space[2] else space[2]
if oncard and size > cspace - 1024*1024:
raise FreeSpaceError("There is insufficient free space "+\
"on the storage card")
if not oncard and size > mspace - 1024*1024:
raise FreeSpaceError("There is insufficient free space " +\
"in main memory")
prefix = "/Data/media/"
if oncard:
prefix = card + "/"
else: name = "books/"+name
path = prefix + name
self.put_file(infile, path, end_session=False)
ctime = self.path_properties(path, end_session=False).ctime
bkl = booklists[1] if oncard else booklists[0]
bkl.add_book(info, name, size, ctime)
fix_ids(booklists[0], booklists[1])
if sync_booklists:
self.sync_booklists(booklists, end_session=False)
@safe
def upload_book_list(self, booklist, end_session=True):
path = self.MEDIA_XML
if not booklist.prefix:
card = self.card(end_session=True)
if not card:
raise ArgumentError("Cannot upload list to card as "+\
"card is not present")
path = card + self.CACHE_XML
f = StringIO()
booklist.write(f)
f.seek(0)
self.put_file(f, path, replace_file=True, end_session=False)
f.close()

View File

@ -1,861 +0,0 @@
__license__ = 'GPL v3'
__copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
"""
Defines the structure of packets that are sent to/received from the device.
Packet structure is defined using classes and inheritance. Each class is a
view that imposes structure on the underlying data buffer.
The data buffer is encoded in little-endian format, but you don't
have to worry about that if you are using the classes.
The classes have instance variables with getter/setter functions defined
to take care of the encoding/decoding.
The classes are intended to mimic C structs.
There are three kinds of packets. L{Commands<Command>},
L{Responses<Response>}, and L{Answers<Answer>}.
C{Commands} are sent to the device on the control bus,
C{Responses} are received from the device,
also on the control bus. C{Answers} and their sub-classes represent
data packets sent to/received from the device via bulk transfers.
Commands are organized as follows: G{classtree Command}
You will typically only use sub-classes of Command.
Responses are organized as follows: G{classtree Response}
Responses inherit Command as they share header structure.
Answers are organized as follows: G{classtree Answer}
"""
import struct
import time
from datetime import datetime
from calibre.devices.errors import PacketError
WORD = "<H" #: Unsigned integer little endian encoded in 2 bytes
DWORD = "<I" #: Unsigned integer little endian encoded in 4 bytes
DDWORD = "<Q" #: Unsigned long long little endian encoded in 8 bytes
class PathResponseCodes(object):
""" Known response commands to path related commands """
NOT_FOUND = 0xffffffd7
INVALID = 0xfffffff9
IS_FILE = 0xffffffd2
HAS_CHILDREN = 0xffffffcc
PERMISSION_DENIED = 0xffffffd6
class TransferBuffer(list):
"""
Represents raw (unstructured) data packets sent over the usb bus.
C{TransferBuffer} is a wrapper around the tuples used by libusb for communication.
It has convenience methods to read and write data from the underlying buffer. See
L{TransferBuffer.pack} and L{TransferBuffer.unpack}.
"""
def __init__(self, packet):
"""
Create a L{TransferBuffer} from C{packet} or an empty buffer.
@type packet: integer or listable object
@param packet: If packet is a list, it is copied into the C{TransferBuffer} and then normalized (see L{TransferBuffer._normalize}).
If it is an integer, a zero buffer of that length is created.
"""
if "__len__" in dir(packet):
list.__init__(self, list(packet))
self._normalize()
else: list.__init__(self, [0 for i in range(packet)])
def __add__(self, tb):
""" Return a TransferBuffer rather than a list as the sum """
return TransferBuffer(list.__add__(self, tb))
def __getslice__(self, start, end):
""" Return a TransferBuffer rather than a list as the slice """
return TransferBuffer(list.__getslice__(self, start, end))
def __str__(self):
"""
Return a string representation of this buffer.
Packets are represented as hex strings, in 2-byte pairs, S{<=} 16 bytes to a line.
An ASCII representation is included. For example::
0700 0100 0000 0000 0000 0000 0c00 0000 ................
0200 0000 0400 0000 4461 7461 ........Data
"""
ans, ascii = ": ".rjust(10,"0"), ""
for i in range(0, len(self), 2):
for b in range(2):
try:
ans += TransferBuffer.phex(self[i+b])
ascii += chr(self[i+b]) if self[i+b] > 31 and self[i+b] < 127 else "."
except IndexError: break
ans = ans + " "
if (i+2)%16 == 0:
if i+2 < len(self):
ans += " " + ascii + "\n" + (TransferBuffer.phex(i+2)+": ").rjust(10, "0")
ascii = ""
last_line = ans[ans.rfind("\n")+1:]
padding = 50 - len(last_line)
ans += "".ljust(padding) + " " + ascii
return ans.strip()
def unpack(self, fmt=DWORD, start=0):
"""
Return decoded data from buffer.
@param fmt: See U{struct<http://docs.python.org/lib/module-struct.html>}
@param start: Position in buffer from which to decode
"""
end = start + struct.calcsize(fmt)
return struct.unpack(fmt, "".join([ chr(i) for i in list.__getslice__(self, start, end) ]))
def pack(self, val, fmt=DWORD, start=0):
"""
Encode C{val} and write it to buffer. For fmt==WORD val is
adjusted to be in the range 0 <= val < 256**2.
@param fmt: See U{struct<http://docs.python.org/lib/module-struct.html>}
@param start: Position in buffer at which to write encoded data
"""
# struct.py is fussy about packing values into a WORD. The value must be
# between 0 and 65535 or a DeprecationWarning is raised. In the future
# this may become an error, so it's best to take care of wrapping here.
if fmt == WORD:
val = val % 256**2
self[start:start+struct.calcsize(fmt)] = \
[ ord(i) for i in struct.pack(fmt, val) ]
def _normalize(self):
""" Replace negative bytes in C{self} by 256 + byte """
for i in range(len(self)):
if self[i] < 0:
self[i] = 256 + self[i]
@classmethod
def phex(cls, num):
"""
Return the hex representation of num without the 0x prefix.
If the hex representation is only 1 digit it is padded to the left with a zero. Used in L{TransferBuffer.__str__}.
"""
index, sign = 2, ""
if num < 0:
index, sign = 3, "-"
h = hex(num)[index:]
if len(h) < 2:
h = "0"+h
return sign + h
class field(object):
""" A U{Descriptor<http://www.cafepy.com/article/python_attributes_and_methods/python_attributes_and_methods.html>}, that implements access
to protocol packets in a human readable way.
"""
def __init__(self, start=16, fmt=DWORD):
"""
@param start: The byte at which this field is stored in the buffer
@param fmt: The packing format for this field.
See U{struct<http://docs.python.org/lib/module-struct.html>}.
"""
self._fmt, self._start = fmt, start
def __get__(self, obj, typ=None):
return obj.unpack(start=self._start, fmt=self._fmt)[0]
def __set__(self, obj, val):
obj.pack(val, start=self._start, fmt=self._fmt)
def __repr__(self):
typ = ""
if self._fmt == DWORD:
typ = "unsigned int"
if self._fmt == DDWORD:
typ = "unsigned long long"
return "An " + typ + " stored in " + \
str(struct.calcsize(self._fmt)) + \
" bytes starting at byte " + str(self._start)
class stringfield(object):
""" A field storing a variable length string. """
def __init__(self, length_field, start=16):
"""
@param length_field: A U{Descriptor<http://www.cafepy.com/article/python_attributes_and_methods/python_attributes_and_methods.html>}
that returns the length of the string.
@param start: The byte at which this field is stored in the buffer
"""
self._length_field = length_field
self._start = start
def __get__(self, obj, typ=None):
length = str(self._length_field.__get__(obj))
return obj.unpack(start=self._start, fmt="<"+length+"s")[0]
def __set__(self, obj, val):
if isinstance(val, unicode):
val = val.encode('utf8')
else:
val = str(val)
obj.pack(val, start=self._start, fmt="<"+str(len(val))+"s")
def __repr__(self):
return "A string starting at byte " + str(self._start)
class Command(TransferBuffer):
""" Defines the structure of command packets sent to the device. """
# Command number. C{unsigned int} stored in 4 bytes at byte 0.
#
# Command numbers are:
# 0 GetUsbProtocolVersion
# 1 ReqEndSession
# 10 FskFileOpen
# 11 FskFileClose
# 12 FskGetSize
# 13 FskSetSize
# 14 FskFileSetPosition
# 15 FskGetPosition
# 16 FskFileRead
# 17 FskFileWrite
# 18 FskFileGetFileInfo
# 19 FskFileSetFileInfo
# 1A FskFileCreate
# 1B FskFileDelete
# 1C FskFileRename
# 30 FskFileCreateDirectory
# 31 FskFileDeleteDirectory
# 32 FskFileRenameDirectory
# 33 FskDirectoryIteratorNew
# 34 FskDirectoryIteratorDispose
# 35 FskDirectoryIteratorGetNext
# 52 FskVolumeGetInfo
# 53 FskVolumeGetInfoFromPath
# 80 FskFileTerminate
# 100 ConnectDevice
# 101 GetProperty
# 102 GetMediaInfo
# 103 GetFreeSpace
# 104 SetTime
# 105 DeviceBeginEnd
# 106 UnlockDevice
# 107 SetBulkSize
# 110 GetHttpRequest
# 111 SetHttpRespponse
# 112 Needregistration
# 114 GetMarlinState
# 200 ReqDiwStart
# 201 SetDiwPersonalkey
# 202 GetDiwPersonalkey
# 203 SetDiwDhkey
# 204 GetDiwDhkey
# 205 SetDiwChallengeserver
# 206 GetDiwChallengeserver
# 207 GetDiwChallengeclient
# 208 SetDiwChallengeclient
# 209 GetDiwVersion
# 20A SetDiwWriteid
# 20B GetDiwWriteid
# 20C SetDiwSerial
# 20D GetDiwModel
# 20C SetDiwSerial
# 20E GetDiwDeviceid
# 20F GetDiwSerial
# 210 ReqDiwCheckservicedata
# 211 ReqDiwCheckiddata
# 212 ReqDiwCheckserialdata
# 213 ReqDiwFactoryinitialize
# 214 GetDiwMacaddress
# 215 ReqDiwTest
# 216 ReqDiwDeletekey
# 300 UpdateChangemode
# 301 UpdateDeletePartition
# 302 UpdateCreatePartition
# 303 UpdateCreatePartitionWithImage
# 304 UpdateGetPartitionSize
number = field(start=0, fmt=DWORD)
# Known types are 0x00 and 0x01. Acknowledge commands are always type 0x00
type = field(start=4, fmt=DDWORD)
# Length of the data part of this packet
length = field(start=12, fmt=DWORD)
@dynamic_property
def data(self):
doc = \
"""
The data part of this command. Returned/set as/by a TransferBuffer.
Stored at byte 16.
Setting it by default changes self.length to the length of the new
buffer. You may have to reset it to the significant part of the buffer.
You would normally use the C{command} property of
L{ShortCommand} or L{LongCommand} instead.
"""
def fget(self):
return self[16:]
def fset(self, buff):
self[16:] = buff
self.length = len(buff)
return property(doc=doc, fget=fget, fset=fset)
def __init__(self, packet):
"""
@param packet: len(packet) > 15 or packet > 15
"""
if ("__len__" in dir(packet) and len(packet) < 16) or\
("__len__" not in dir(packet) and packet < 16):
raise PacketError(str(self.__class__)[7:-2] + \
" packets must have length atleast 16")
TransferBuffer.__init__(self, packet)
class SetTime(Command):
"""
Set time on device. All fields refer to time in the GMT time zone.
"""
NUMBER = 0x104
# -time.timezone with negative numbers encoded
# as int(0xffffffff +1 -time.timezone/60.)
timezone = field(start=0x10, fmt=DWORD)
year = field(start=0x14, fmt=DWORD) #: year e.g. 2006
month = field(start=0x18, fmt=DWORD) #: month 1-12
day = field(start=0x1c, fmt=DWORD) #: day 1-31
hour = field(start=0x20, fmt=DWORD) #: hour 0-23
minute = field(start=0x24, fmt=DWORD) #: minute 0-59
second = field(start=0x28, fmt=DWORD) #: second 0-59
def __init__(self, t=None):
""" @param t: time as an epoch """
self.number = SetTime.NUMBER
self.type = 0x01
self.length = 0x1c
td = datetime.now() - datetime.utcnow()
tz = int((td.days*24*3600 + td.seconds)/60.)
self.timezone = tz if tz > 0 else 0xffffffff +1 + tz
if not t: t = time.time()
t = time.gmtime(t)
self.year = t[0]
self.month = t[1]
self.day = t[2]
self.hour = t[3]
self.minute = t[4]
# Hack you should actually update the entire time tree if
# second is > 59
self.second = t[5] if t[5] < 60 else 59
class ShortCommand(Command):
""" A L{Command} whose data section is 4 bytes long """
SIZE = 20 #: Packet size in bytes
# Usually carries additional information
command = field(start=16, fmt=DWORD)
def __init__(self, number=0x00, type=0x00, command=0x00):
"""
@param number: L{Command.number}
@param type: L{Command.type}
@param command: L{ShortCommand.command}
"""
Command.__init__(self, ShortCommand.SIZE)
self.number = number
self.type = type
self.length = 4
self.command = command
class DirRead(ShortCommand):
""" The command that asks the device to send the next item in the list """
NUMBER = 0x35 #: Command number
def __init__(self, _id):
""" @param id: The identifier returned as a result of a L{DirOpen} command """
ShortCommand.__init__(self, number=DirRead.NUMBER, type=0x01, \
command=_id)
class DirClose(ShortCommand):
""" Close a previously opened directory """
NUMBER = 0x34 #: Command number
def __init__(self, _id):
""" @param id: The identifier returned as a result of a L{DirOpen} command """
ShortCommand.__init__(self, number=DirClose.NUMBER, type=0x01,
command=_id)
class BeginEndSession(ShortCommand):
"""
Ask device to either start or end a session.
"""
NUMBER = 0x01 #: Command number
def __init__(self, end=True):
command = 0x00 if end else 0x01
ShortCommand.__init__(self, \
number=BeginEndSession.NUMBER, type=0x01, command=command)
class GetUSBProtocolVersion(ShortCommand):
""" Get USB Protocol version used by device """
NUMBER = 0x0 #: Command number
def __init__(self):
ShortCommand.__init__(self, \
number=GetUSBProtocolVersion.NUMBER, \
type=0x01, command=0x00)
class SetBulkSize(Command):
""" Set size for bulk transfers in this session """
NUMBER = 0x107 #: Command number
chunk_size = field(fmt=WORD, start=0x10)
unknown = field(fmt=WORD, start=0x12)
def __init__(self, chunk_size=0x8000, unknown=0x2):
Command.__init__(self, [0 for i in range(24)])
self.number = SetBulkSize.NUMBER
self.type = 0x01
self.chunk_size = chunk_size
self.unknown = unknown
class UnlockDevice(Command):
""" Unlock the device """
NUMBER = 0x106 #: Command number
key = stringfield(8, start=16) #: The key defaults to -1
def __init__(self, key='-1\0\0\0\0\0\0'):
Command.__init__(self, 24)
self.number = UnlockDevice.NUMBER
self.type = 0x01
self.length = 8
self.key = key
class LongCommand(Command):
""" A L{Command} whose data section is 16 bytes long """
SIZE = 32 #: Size in bytes of C{LongCommand} packets
def __init__(self, number=0x00, type=0x00, command=0x00):
"""
@param number: L{Command.number}
@param type: L{Command.type}
@param command: L{LongCommand.command}
"""
Command.__init__(self, LongCommand.SIZE)
self.number = number
self.type = type
self.length = 16
self.command = command
@dynamic_property
def command(self):
doc = \
"""
Usually carries extra information needed for the command
It is a list of C{unsigned integers} of length between 1 and 4. 4
C{unsigned int} stored in 16 bytes at byte 16.
"""
def fget(self):
return self.unpack(start=16, fmt="<"+str(self.length/4)+"I")
def fset(self, val):
if "__len__" not in dir(val): val = (val,)
start = 16
for command in val:
self.pack(command, start=start, fmt=DWORD)
start += struct.calcsize(DWORD)
return property(doc=doc, fget=fget, fset=fset)
class PathCommand(Command):
""" Abstract class that defines structure common to all path related commands. """
path_length = field(start=16, fmt=DWORD) #: Length of the path to follow
path = stringfield(path_length, start=20) #: The path this query is about
def __init__(self, path, number, path_len_at_byte=16):
Command.__init__(self, path_len_at_byte+4+len(path))
if isinstance(path, unicode):
path = path.encode('utf8')
self.path_length = len(path)
self.path = path
self.type = 0x01
self.length = len(self) - 16
self.number = number
class TotalSpaceQuery(PathCommand):
""" Query the total space available on the volume represented by path """
NUMBER = 0x53 #: Command number
def __init__(self, path):
""" @param path: valid values are 'a:', 'b:', '/Data/' """
PathCommand.__init__(self, path, TotalSpaceQuery.NUMBER)
class FreeSpaceQuery(ShortCommand):
""" Query the free space available """
NUMBER = 0x103 #: Command number
def __init__(self, where):
""" @param where: valid values are: 'a:', 'b:', '/' """
c = 0
if where.startswith('a:'): c = 1
elif where.startswith('b:'): c = 2
ShortCommand.__init__(self, \
number=FreeSpaceQuery.NUMBER, type=0x01, command=c)
class DirCreate(PathCommand):
""" Create a directory """
NUMBER = 0x30
def __init__(self, path):
PathCommand.__init__(self, path, DirCreate.NUMBER)
class DirOpen(PathCommand):
""" Open a directory for reading its contents """
NUMBER = 0x33 #: Command number
def __init__(self, path):
PathCommand.__init__(self, path, DirOpen.NUMBER)
class AcknowledgeBulkRead(LongCommand):
""" Must be sent to device after a bulk read """
def __init__(self, bulk_read_id):
"""
bulk_read_id is an integer, the id of the bulk read
we are acknowledging. See L{Answer.id}
"""
LongCommand.__init__(self, number=0x1000, \
type=0x00, command=bulk_read_id)
class DeviceInfoQuery(Command):
""" The command used to ask for device information """
NUMBER = 0x101 #: Command number
def __init__(self):
Command.__init__(self, 16)
self.number = DeviceInfoQuery.NUMBER
self.type = 0x01
class FileClose(ShortCommand):
""" File close command """
NUMBER = 0x11 #: Command number
def __init__(self, _id):
ShortCommand.__init__(self, number=FileClose.NUMBER, \
type=0x01, command=_id)
class FileCreate(PathCommand):
""" Create a file """
NUMBER = 0x1a #: Command number
def __init__(self, path):
PathCommand.__init__(self, path, FileCreate.NUMBER)
class FileDelete(PathCommand):
""" Delete a file """
NUMBER = 0x1B
def __init__(self, path):
PathCommand.__init__(self, path, FileDelete.NUMBER)
class DirDelete(PathCommand):
""" Delete a directory """
NUMBER = 0x31
def __init__(self, path):
PathCommand.__init__(self, path, DirDelete.NUMBER)
class FileOpen(PathCommand):
""" File open command """
NUMBER = 0x10 #: Command number
READ = 0x00 #: Open file in read mode
WRITE = 0x01 #: Open file in write mode
path_length = field(start=20, fmt=DWORD)
path = stringfield(path_length, start=24)
def __init__(self, path, mode=0x00):
PathCommand.__init__(self, path, FileOpen.NUMBER, path_len_at_byte=20)
self.mode = mode
@dynamic_property
def mode(self):
doc = \
"""
The file open mode. Is either L{FileOpen.READ}
or L{FileOpen.WRITE}. C{unsigned int} stored at byte 16.
"""
def fget(self):
return self.unpack(start=16, fmt=DWORD)[0]
def fset(self, val):
self.pack(val, start=16, fmt=DWORD)
return property(doc=doc, fget=fget, fset=fset)
class FileIO(Command):
""" Command to read/write from an open file """
RNUMBER = 0x16 #: Command number to read from a file
WNUMBER = 0x17 #: Command number to write to a file
id = field(start=16, fmt=DWORD) #: The file ID returned by a FileOpen command
offset = field(start=20, fmt=DDWORD) #: offset in the file at which to read
size = field(start=28, fmt=DWORD) #: The number of bytes to reead from file.
def __init__(self, _id, offset, size, mode=0x16):
"""
@param _id: File identifier returned by a L{FileOpen} command
@type id: C{unsigned int}
@param offset: Position in file at which to read
@type offset: C{unsigned long long}
@param size: number of bytes to read
@type size: C{unsigned int}
@param mode: Either L{FileIO.RNUMBER} or L{File.WNUMBER}
"""
Command.__init__(self, 32)
self.number = mode
self.type = 0x01
self.length = 16
self.id = _id
self.offset = offset
self.size = size
class PathQuery(PathCommand):
""" Defines structure of command that requests information about a path """
NUMBER = 0x18 #: Command number
def __init__(self, path):
PathCommand.__init__(self, path, PathQuery.NUMBER)
class SetFileInfo(PathCommand):
""" Set File information """
NUMBER = 0x19 #: Command number
def __init__(self, path):
PathCommand.__init__(self, path, SetFileInfo.NUMBER)
class Response(Command):
"""
Defines the structure of response packets received from the device.
C{Response} inherits from C{Command} as the
first 16 bytes have the same structure.
"""
SIZE = 32 #: Size of response packets in the SONY protocol
# Response number, the command number of a command
# packet sent sometime before this packet was received
rnumber = field(start=16, fmt=DWORD)
# Used to indicate error conditions. A value of 0 means
# there was no error
code = field(start=20, fmt=DWORD)
# Used to indicate the size of the next bulk read
data_size = field(start=28, fmt=DWORD)
def __init__(self, packet):
""" C{len(packet) == Response.SIZE} """
if len(packet) != Response.SIZE:
raise PacketError(str(self.__class__)[7:-2] + \
" packets must have exactly " + \
str(Response.SIZE) + " bytes not " + str(len(packet)))
Command.__init__(self, packet)
if self.number != 0x00001000:
raise PacketError("Response packets must have their number set to " \
+ hex(0x00001000))
@dynamic_property
def data(self):
doc = \
"""
The last 3 DWORDs (12 bytes) of data in this
response packet. Returned as a list of unsigned integers.
"""
def fget(self):
return self.unpack(start=20, fmt="<III")
def fset(self, val):
self.pack(val, start=20, fmt="<III")
return property(doc=doc, fget=fget, fset=fset)
class ListResponse(Response):
"""
Defines the structure of response packets received
during list (ll) queries. See L{PathQuery}.
"""
IS_FILE = 0xffffffd2 #: Queried path is a file
IS_INVALID = 0xfffffff9 #: Queried path is malformed/invalid
# Queried path is not mounted (i.e. a removed storage card/stick)
IS_UNMOUNTED = 0xffffffc8
IS_EOL = 0xfffffffa #: There are no more entries in the list
PATH_NOT_FOUND = 0xffffffd7 #: Queried path is not found
PERMISSION_DENIED = 0xffffffd6 #: Permission denied
@dynamic_property
def is_file(self):
doc = """ True iff queried path is a file """
def fget(self):
return self.code == ListResponse.IS_FILE
return property(doc=doc, fget=fget)
@dynamic_property
def is_invalid(self):
doc = """ True iff queried path is invalid """
def fget(self):
return self.code == ListResponse.IS_INVALID
return property(doc=doc, fget=fget)
@dynamic_property
def path_not_found(self):
doc = """ True iff queried path is not found """
def fget(self):
return self.code == ListResponse.PATH_NOT_FOUND
return property(doc=doc, fget=fget)
@dynamic_property
def permission_denied(self):
doc = """ True iff permission is denied for path operations """
def fget(self):
return self.code == ListResponse.PERMISSION_DENIED
return property(doc=doc, fget=fget)
@dynamic_property
def is_unmounted(self):
doc = """ True iff queried path is unmounted (i.e. removed storage card) """
def fget(self):
return self.code == ListResponse.IS_UNMOUNTED
return property(doc=doc, fget=fget)
@dynamic_property
def is_eol(self):
doc = """ True iff there are no more items in the list """
def fget(self):
return self.code == ListResponse.IS_EOL
return property(doc=doc, fget=fget)
class Answer(TransferBuffer):
"""
Defines the structure of packets sent to host via a
bulk transfer (i.e., bulk reads)
"""
number = field(start=0, fmt=DWORD) #: Answer identifier
length = field(start=12, fmt=DWORD) #: Length of data to follow
def __init__(self, packet):
""" @param packet: C{len(packet)} S{>=} C{16} """
if "__len__" in dir(packet):
if len(packet) < 16 :
raise PacketError(str(self.__class__)[7:-2] + \
" packets must have a length of atleast 16 bytes. "\
"Got initializer of " + str(len(packet)) + " bytes.")
elif packet < 16:
raise PacketError(str(self.__class__)[7:-2] + \
" packets must have a length of atleast 16 bytes")
TransferBuffer.__init__(self, packet)
class FileProperties(Answer):
"""
Defines the structure of packets that contain size, date and
permissions information about files/directories.
"""
file_size = field(start=16, fmt=DDWORD) #: Size in bytes of the file
file_type = field(start=24, fmt=DWORD) #: 1 == file, 2 == dir
ctime = field(start=28, fmt=DWORD) #: Creation time as an epoch
wtime = field(start=32, fmt=DWORD) #: Modification time as an epoch
# 0 = default permissions, 4 = read only
permissions = field(start=36, fmt=DWORD)
@dynamic_property
def is_dir(self):
doc = """True if path points to a directory, False if it points to a file."""
def fget(self):
return (self.file_type == 2)
def fset(self, val):
if val:
val = 2
else:
val = 1
self.file_type = val
return property(doc=doc, fget=fget, fset=fset)
@dynamic_property
def is_readonly(self):
doc = """ Whether this file is readonly."""
def fget(self):
return self.unpack(start=36, fmt=DWORD)[0] != 0
def fset(self, val):
if val:
val = 4
else:
val = 0
self.pack(val, start=36, fmt=DWORD)
return property(doc=doc, fget=fget, fset=fset)
class USBProtocolVersion(Answer):
""" Get USB Protocol version """
version = field(start=16, fmt=DDWORD)
class IdAnswer(Answer):
""" Defines the structure of packets that contain identifiers for queries. """
@dynamic_property
def id(self):
doc = \
"""
The identifier. C{unsigned int} stored in 4 bytes
at byte 16. Should be sent in commands asking
for the next item in the list.
"""
def fget(self):
return self.unpack(start=16, fmt=DWORD)[0]
def fset(self, val):
self.pack(val, start=16, fmt=DWORD)
return property(doc=doc, fget=fget, fset=fset)
class DeviceInfo(Answer):
""" Defines the structure of the packet containing information about the device """
device_name = field(start=16, fmt="<32s")
device_version = field(start=48, fmt="<32s")
software_version = field(start=80, fmt="<24s")
mime_type = field(start=104, fmt="<32s")
class TotalSpaceAnswer(Answer):
total = field(start=24, fmt=DDWORD) #: Total space available
# Supposedly free space available, but it does not work for main memory
free_space = field(start=32, fmt=DDWORD)
class FreeSpaceAnswer(Answer):
SIZE = 24
free = field(start=16, fmt=DDWORD)
class ListAnswer(Answer):
""" Defines the structure of packets that contain items in a list. """
name_length = field(start=20, fmt=DWORD)
name = stringfield(name_length, start=24)
@dynamic_property
def is_dir(self):
doc = \
"""
True if list item points to a directory, False if it points to a file.
C{unsigned int} stored in 4 bytes at byte 16.
"""
def fget(self):
return (self.unpack(start=16, fmt=DWORD)[0] == 2)
def fset(self, val):
if val: val = 2
else: val = 1
self.pack(val, start=16, fmt=DWORD)
return property(doc=doc, fget=fget, fset=fset)

View File

@ -18,7 +18,6 @@ from cStringIO import StringIO
import xml.dom.minidom as dom
from functools import wraps
from calibre.devices.prs500.prstypes import field
from calibre.ebooks.metadata import MetaInformation, string_to_authors
BYTE = "<B" #: Unsigned char little endian encoded in 1 byte
@ -26,6 +25,35 @@ WORD = "<H" #: Unsigned short little endian encoded in 2 bytes
DWORD = "<I" #: Unsigned integer little endian encoded in 4 bytes
QWORD = "<Q" #: Unsigned long long little endian encoded in 8 bytes
class field(object):
""" A U{Descriptor<http://www.cafepy.com/article/python_attributes_and_methods/python_attributes_and_methods.html>}, that implements access
to protocol packets in a human readable way.
"""
def __init__(self, start=16, fmt=DWORD):
"""
@param start: The byte at which this field is stored in the buffer
@param fmt: The packing format for this field.
See U{struct<http://docs.python.org/lib/module-struct.html>}.
"""
self._fmt, self._start = fmt, start
def __get__(self, obj, typ=None):
return obj.unpack(start=self._start, fmt=self._fmt)[0]
def __set__(self, obj, val):
obj.pack(val, start=self._start, fmt=self._fmt)
def __repr__(self):
typ = ""
if self._fmt == DWORD:
typ = "unsigned int"
if self._fmt == QWORD:
typ = "unsigned long long"
return "An " + typ + " stored in " + \
str(struct.calcsize(self._fmt)) + \
" bytes starting at byte " + str(self._start)
class versioned_field(field):
def __init__(self, vfield, version, start=0, fmt=WORD):
field.__init__(self, start=start, fmt=fmt)

View File

@ -16,7 +16,7 @@ from calibre import CurrentDir
entry_points = {
'console_scripts': [ \
'ebook-device = calibre.devices.prs500.cli.main:main',
'ebook-device = calibre.devices.cli:main',
'ebook-meta = calibre.ebooks.metadata.cli:main',
'ebook-convert = calibre.ebooks.conversion.cli:main',
'markdown-calibre = calibre.ebooks.markdown.markdown:main',
@ -299,7 +299,7 @@ class PostInstall:
return 0
;;
cp )
if [[ ${cur} == prs500:* ]]; then
if [[ ${cur} == dev:* ]]; then
COMPREPLY=( $(_ebook_device_ls "${cur:7}") )
return 0
else
@ -307,20 +307,20 @@ class PostInstall:
return 0
fi
;;
prs500 )
dev )
COMPREPLY=( $(compgen -W "cp ls rm mkdir touch cat info books df" "${cur}") )
return 0
;;
* )
if [[ ${cur} == prs500:* ]]; then
if [[ ${cur} == dev:* ]]; then
COMPREPLY=( $(_ebook_device_ls "${cur:7}") )
return 0
else
if [[ ${prev} == prs500:* ]]; then
if [[ ${prev} == dev:* ]]; then
_filedir
return 0
else
COMPREPLY=( $(compgen -W "prs500:" "${cur}") )
COMPREPLY=( $(compgen -W "dev:" "${cur}") )
return 0
fi
return 0

View File

@ -1,21 +0,0 @@
__license__ = 'GPL v3'
__copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
from setuptools import find_packages, setup
# name can be any name. This name will be used to create .egg file.
# name that is used in packages is the one that is used in the trac.ini file.
# use package name as entry_points
setup(
name='TracLibprs500Plugins', version='0.1',
packages=find_packages(exclude=['*.tests*']),
entry_points = """
[trac.plugins]
download = plugins.download
changelog = plugins.Changelog
""",
package_data={'plugins': ['templates/*.html',
'htdocs/css/*.css',
'htdocs/images/*']},
)