## 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}, L{Responses}, and L{Answers}. 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, time from errors import PacketError DWORD = "} 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} @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} @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}, 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}. """ 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} 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 SetTime(Command): """ Set time on device. All fields refer to time in the GMT time zone. @todo: figure out what the 4 bytes starting at byte 16 are for """ NUMBER = 0x104 unknown = field(start=0x10, fmt=DWORD) #: Haven't figured out what this is for. Seems to always be set to 4294966816L 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 self.unknown = 4294966816L 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] self.second = t[5] if t[5] < 60 else 59 # Hack you should actually update the entire time tree is second is > 59 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="=} 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())