mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-08-11 09:13:57 -04:00
Remove legacy code
This commit is contained in:
parent
34efc911ae
commit
9d697e8146
@ -1,7 +1,7 @@
|
|||||||
__license__ = 'GPL v3'
|
__license__ = 'GPL v3'
|
||||||
__copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
|
__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.
|
For usage information run the script.
|
||||||
"""
|
"""
|
||||||
@ -275,7 +275,7 @@ def main():
|
|||||||
elif command == "cp":
|
elif command == "cp":
|
||||||
usage="usage: %prog cp [options] source destination\nCopy files to/from the device\n\n"+\
|
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"+\
|
"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"+\
|
"where mountpoint is one of / or card:/\n\n"+\
|
||||||
"source must point to a file for which you have read permissions\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"
|
"destination must point to a file or directory for which you have write permissions"
|
||||||
@ -286,7 +286,7 @@ def main():
|
|||||||
if len(args) != 2:
|
if len(args) != 2:
|
||||||
parser.print_help()
|
parser.print_help()
|
||||||
return 1
|
return 1
|
||||||
if args[0].startswith("prs500:"):
|
if args[0].startswith("dev:"):
|
||||||
outfile = args[1]
|
outfile = args[1]
|
||||||
path = args[0][7:]
|
path = args[0][7:]
|
||||||
if path.endswith("/"): path = path[:-1]
|
if path.endswith("/"): path = path[:-1]
|
||||||
@ -300,7 +300,7 @@ def main():
|
|||||||
return 1
|
return 1
|
||||||
dev.get_file(path, outfile)
|
dev.get_file(path, outfile)
|
||||||
outfile.close()
|
outfile.close()
|
||||||
elif args[1].startswith("prs500:"):
|
elif args[1].startswith("dev:"):
|
||||||
try:
|
try:
|
||||||
infile = open(args[0], "rb")
|
infile = open(args[0], "rb")
|
||||||
except IOError as e:
|
except IOError as e:
|
@ -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
|
|
@ -1,6 +0,0 @@
|
|||||||
__license__ = 'GPL v3'
|
|
||||||
__copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
|
|
||||||
|
|
||||||
'''
|
|
||||||
Device driver for the Sony Reader PRS 500
|
|
||||||
'''
|
|
@ -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'))
|
|
@ -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>"
|
|
@ -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()
|
|
@ -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)
|
|
||||||
|
|
@ -18,7 +18,6 @@ from cStringIO import StringIO
|
|||||||
import xml.dom.minidom as dom
|
import xml.dom.minidom as dom
|
||||||
from functools import wraps
|
from functools import wraps
|
||||||
|
|
||||||
from calibre.devices.prs500.prstypes import field
|
|
||||||
from calibre.ebooks.metadata import MetaInformation, string_to_authors
|
from calibre.ebooks.metadata import MetaInformation, string_to_authors
|
||||||
|
|
||||||
BYTE = "<B" #: Unsigned char little endian encoded in 1 byte
|
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
|
DWORD = "<I" #: Unsigned integer little endian encoded in 4 bytes
|
||||||
QWORD = "<Q" #: Unsigned long long little endian encoded in 8 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):
|
class versioned_field(field):
|
||||||
def __init__(self, vfield, version, start=0, fmt=WORD):
|
def __init__(self, vfield, version, start=0, fmt=WORD):
|
||||||
field.__init__(self, start=start, fmt=fmt)
|
field.__init__(self, start=start, fmt=fmt)
|
||||||
|
@ -16,7 +16,7 @@ from calibre import CurrentDir
|
|||||||
|
|
||||||
entry_points = {
|
entry_points = {
|
||||||
'console_scripts': [ \
|
'console_scripts': [ \
|
||||||
'ebook-device = calibre.devices.prs500.cli.main:main',
|
'ebook-device = calibre.devices.cli:main',
|
||||||
'ebook-meta = calibre.ebooks.metadata.cli:main',
|
'ebook-meta = calibre.ebooks.metadata.cli:main',
|
||||||
'ebook-convert = calibre.ebooks.conversion.cli:main',
|
'ebook-convert = calibre.ebooks.conversion.cli:main',
|
||||||
'markdown-calibre = calibre.ebooks.markdown.markdown:main',
|
'markdown-calibre = calibre.ebooks.markdown.markdown:main',
|
||||||
@ -299,7 +299,7 @@ class PostInstall:
|
|||||||
return 0
|
return 0
|
||||||
;;
|
;;
|
||||||
cp )
|
cp )
|
||||||
if [[ ${cur} == prs500:* ]]; then
|
if [[ ${cur} == dev:* ]]; then
|
||||||
COMPREPLY=( $(_ebook_device_ls "${cur:7}") )
|
COMPREPLY=( $(_ebook_device_ls "${cur:7}") )
|
||||||
return 0
|
return 0
|
||||||
else
|
else
|
||||||
@ -307,20 +307,20 @@ class PostInstall:
|
|||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
;;
|
;;
|
||||||
prs500 )
|
dev )
|
||||||
COMPREPLY=( $(compgen -W "cp ls rm mkdir touch cat info books df" "${cur}") )
|
COMPREPLY=( $(compgen -W "cp ls rm mkdir touch cat info books df" "${cur}") )
|
||||||
return 0
|
return 0
|
||||||
;;
|
;;
|
||||||
* )
|
* )
|
||||||
if [[ ${cur} == prs500:* ]]; then
|
if [[ ${cur} == dev:* ]]; then
|
||||||
COMPREPLY=( $(_ebook_device_ls "${cur:7}") )
|
COMPREPLY=( $(_ebook_device_ls "${cur:7}") )
|
||||||
return 0
|
return 0
|
||||||
else
|
else
|
||||||
if [[ ${prev} == prs500:* ]]; then
|
if [[ ${prev} == dev:* ]]; then
|
||||||
_filedir
|
_filedir
|
||||||
return 0
|
return 0
|
||||||
else
|
else
|
||||||
COMPREPLY=( $(compgen -W "prs500:" "${cur}") )
|
COMPREPLY=( $(compgen -W "dev:" "${cur}") )
|
||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
return 0
|
return 0
|
||||||
|
@ -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/*']},
|
|
||||||
)
|
|
||||||
|
|
Loading…
x
Reference in New Issue
Block a user