calibre/libprs500/prstypes.py
Kovid Goyal ddc2e5bbdd Fixed connection errors due to timeouts in the GUI (Basically, increased the timeout in PRS500Device.open to 10s to allow device to initialize USB handler.
Refactored device display code to work with changed implementation of book list parsing in libprs500
2006-12-04 05:25:56 +00:00

725 lines
24 KiB
Python
Executable File

## Copyright (C) 2006 Kovid Goyal kovid@kovidgoyal.net
## This program is free software; you can redistribute it and/or modify
## it under the terms of the GNU General Public License as published by
## the Free Software Foundation; either version 2 of the License, or
## (at your option) any later version.
##
## This program is distributed in the hope that it will be useful,
## but WITHOUT ANY WARRANTY; without even the implied warranty of
## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
## GNU General Public License for more details.
##
## You should have received a copy of the GNU General Public License along
## with this program; if not, write to the Free Software Foundation, Inc.,
## 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
"""
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
from errors import PacketError
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
class TransferBuffer(list):
"""
Represents raw (unstructured) data packets sent over the usb bus.
C{TransferBuffer} is a wrapper around the tuples used by L{PyUSB<usb>} 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.
@param fmt: See U{struct<http://docs.python.org/lib/module-struct.html>}
@param start: Position in buffer at which to write encoded data
"""
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 val.__class__.__name__ != 'str': 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. """
number = field(start=0, fmt=DWORD)
"""
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
"""
type = field(start=4, fmt=DDWORD) #: Known types are 0x00 and 0x01. Acknowledge commands are always type 0x00
length = field(start=12, fmt=DWORD) #: Length of the data part of this packet
@apply
def data():
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, buffer):
self[16:] = buffer
self.length = len(buffer)
return property(**locals())
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 ShortCommand(Command):
""" A L{Command} whoose data section is 4 bytes long """
SIZE = 20 #: Packet size in bytes
command = field(start=16, fmt=DWORD) #: Usually carries additional information
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 EndSession(ShortCommand):
""" Ask device to change status to 'USB connected' i.e., tell the device that the present sequence of commands is complete """
NUMBER=0x1 #: Command number
def __init__(self):
ShortCommand.__init__(self, number=EndSession.NUMBER, type=0x01, command=0x00)
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(ShortCommand):
NUMBER = 0x107 #: Command number
def __init__(self, size=0x028000):
ShortCommand.__init__(self, number=SetBulkSize.NUMBER, type=0x01, command=size)
class UnlockDevice(ShortCommand):
NUMBER = 0x106 #: Command number
def __init__(self, key=0x312d):
ShortCommand.__init__(self, number=UnlockDevice.NUMBER, type=0x01, command=key)
class LongCommand(Command):
""" A L{Command} whoose 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
@apply
def command():
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(**locals())
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))
self.path_length = len(path)
self.path = path
self.type = 0x01
self.length = len(self)-16
self.number = number
class FreeSpaceQuery(PathCommand):
""" Query the free space available """
NUMBER = 0x53 #; Command number
def __init__(self, path):
PathCommand.__init__(self, path, FreeSpaceQuery.NUMBER)
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=0x0101 #: 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
@apply
def mode():
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(**locals())
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
rnumber = field(start=16, fmt=DWORD) #: Response number, the command number of a command packet sent sometime before this packet was received
code = field(start=20, fmt=DWORD) #: Used to indicate error conditions. A value of 0 means there was no error
data_size = field(start=28, fmt=DWORD) #: Used to indicate the size of the next bulk read
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))
@apply
def data():
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(**locals())
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
IS_UNMOUNTED = 0xffffffc8 #: Queried path is not mounted (i.e. a removed storage card/stick)
IS_EOL = 0xfffffffa #: There are no more entries in the list
PATH_NOT_FOUND = 0xffffffd7 #: Queried path is not found
@apply
def is_file():
""" True iff queried path is a file """
def fget(self):
return self.code == ListResponse.IS_FILE
return property(**locals())
@apply
def is_invalid():
""" True iff queried path is invalid """
def fget(self):
return self.code == ListResponse.IS_INVALID
return property(**locals())
@apply
def path_not_found():
""" True iff queried path is not found """
def fget(self):
return self.code == ListResponse.PATH_NOT_FOUND
return property(**locals())
@apply
def is_unmounted():
""" True iff queried path is unmounted (i.e. removed storage card) """
def fget(self):
return self.code == ListResponse.IS_UNMOUNTED
return property(**locals())
@apply
def is_eol():
""" True iff there are no more items in the list """
def fget(self):
return self.code == ListResponse.IS_EOL
return property(**locals())
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")
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
wtime = field(start=32, fmt=DWORD) #: Modification time
permissions = field(start=36, fmt=DWORD) #: 0 = default permissions, 4 = read only
@apply
def is_dir():
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(**locals())
@apply
def is_readonly():
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(**locals())
class USBProtocolVersion(Answer):
version = field(start=16, fmt=DDWORD)
class IdAnswer(Answer):
""" Defines the structure of packets that contain identifiers for queries. """
@apply
def id():
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(**locals())
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 FreeSpaceAnswer(Answer):
total = field(start=24, fmt=DDWORD)
free_space = field(start=32, 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)
@apply
def is_dir():
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(**locals())