From 1c8319a5a4545ba79cc5c6768588b3d87444871d Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Tue, 7 Nov 2006 04:10:11 +0000 Subject: [PATCH] Added working support for the following commands to libprs500: - ls -lhR --color - cp from device to host - info (get device information) - cat files on device Added a command line interface in prs500.py Added documentation in epytext Added support for distutils --- MANIFEST.in | 5 + Makefile.distrib | 8 + README | 13 +- communicate.py | 405 ------------- decoding | 27 - epydoc-pdf.conf | 50 ++ epydoc.conf | 48 +- errors.py | 32 - libprs500/__init__.py | 25 + libprs500/communicate.py | 366 ++++++++++++ libprs500/errors.py | 51 ++ libprs500/prstypes.py | 840 +++++++++++++++++++++++++++ terminfo.py => libprs500/terminfo.py | 0 pr5-500.e3p | 101 ---- prs-500.e3p | 12 +- prstypes.py | 533 ----------------- scripts/prs500.py | 244 ++++++++ setup.py | 20 + spike.pl | 115 ++++ 19 files changed, 1778 insertions(+), 1117 deletions(-) create mode 100644 MANIFEST.in create mode 100644 Makefile.distrib delete mode 100755 communicate.py delete mode 100644 decoding create mode 100644 epydoc-pdf.conf delete mode 100644 errors.py create mode 100644 libprs500/__init__.py create mode 100755 libprs500/communicate.py create mode 100644 libprs500/errors.py create mode 100755 libprs500/prstypes.py rename terminfo.py => libprs500/terminfo.py (100%) delete mode 100644 pr5-500.e3p delete mode 100755 prstypes.py create mode 100755 scripts/prs500.py create mode 100644 setup.py create mode 100755 spike.pl diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000000..e6ff0d7411 --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,5 @@ +include libprs500 *.py +include scripts *.py +include README +include docs/pdf/api.pdf +recursive-include docs/html * diff --git a/Makefile.distrib b/Makefile.distrib new file mode 100644 index 0000000000..523255b2c2 --- /dev/null +++ b/Makefile.distrib @@ -0,0 +1,8 @@ +all: doc tarball + +tarball: + python setup.py sdist --formats=gztar,zip + +doc: + epydoc --config epydoc.conf + epydoc -v --config epydoc-pdf.conf diff --git a/README b/README index 43046d2b9f..bca84415ff 100644 --- a/README +++ b/README @@ -4,13 +4,16 @@ Requirements: 1) Python >= 2.5 2) PyUSB >= 0.3.4 (http://sourceforge.net/projects/pyusb/) +Installation: +As root +python setup.py install + Usage: -At the moment all that it can do is a simple ls command. Add the following to /etc/udev/rules.d/90-local.rules +Add the following to /etc/udev/rules.d/90-local.rules + BUS=="usb", SYSFS{idProduct}=="029b", SYSFS{idVendor}=="054c", MODE="660", GROUP="plugdev" + and run udevstart to enable access to the reader for non-root users. You may have to adjust the GROUP and the location of the rules file to suit your distribution. -To see the listing -./communicate.py /path/to/see - -If the path does not exist, it will throw an Exception. +Usage information is provided when you run the script prs500.py diff --git a/communicate.py b/communicate.py deleted file mode 100755 index 3bfc9a3c3d..0000000000 --- a/communicate.py +++ /dev/null @@ -1,405 +0,0 @@ -#!/usr/bin/env python -## 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. - -### 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 - -import sys, usb, logging, StringIO, time -from optparse import OptionParser - -from prstypes import * -from errors import * -from terminfo import TerminalController - -#try: -# import psyco -# psyco.full() -#except ImportError: -# print 'Psyco not installed, the program will just run slower' -_term = None - -LOG_PACKETS=False # If True all packets are looged to stdout -MINIMUM_COL_WIDTH = 12 - -class File(object): - def __init__(self, file): - self.is_dir = file[1].is_dir - self.is_readonly = file[1].is_readonly - self.size = file[1].file_size - self.ctime = file[1].ctime - self.wtime = file[1].wtime - path = file[0] - if path.endswith("/"): path = path[:-1] - self.path = path - self.name = path[path.rfind("/")+1:].rstrip() - - def __repr__(self): - return self.path - - @apply - def mode_string(): - doc=""" The mode string for this file. There are only two modes read-only and read-write """ - def fget(self): - mode, x = "-", "-" - if self.is_dir: mode, x = "d", "x" - if self.is_readonly: mode += "r-"+x+"r-"+x+"r-"+x - else: mode += "rw"+x+"rw"+x+"rw"+x - return mode - return property(**locals()) - - @apply - def name_in_color(): - doc=""" The name in ANSI text. Directories are blue, ebooks are green """ - def fget(self): - cname = self.name - blue, green, normal = "", "", "" - if _term: blue, green, normal = _term.BLUE, _term.GREEN, _term.NORMAL - if self.is_dir: cname = blue + self.name + normal - else: - ext = self.name[self.name.rfind("."):] - if ext in (".pdf", ".rtf", ".lrf", ".lrx", ".txt"): cname = green + self.name + normal - return cname - return property(**locals()) - - @apply - def human_readable_size(): - doc=""" File size in human readable form """ - def fget(self): - if self.size < 1024: divisor, suffix = 1, "" - elif self.size < 1024*1024: divisor, suffix = 1024., "M" - elif self.size < 1024*1024*1024: divisor, suffix = 1024*1024, "G" - size = str(self.size/divisor) - if size.find(".") > -1: size = size[:size.find(".")+2] - return size + suffix - return property(**locals()) - - @apply - def modification_time(): - doc=""" Last modified time in the Linux ls -l format """ - def fget(self): - return time.strftime("%Y-%m-%d %H:%M", time.gmtime(self.wtime)) - return property(**locals()) - - @apply - def creation_time(): - doc=""" Last modified time in the Linux ls -l format """ - def fget(self): - return time.strftime("%Y-%m-%d %H:%M", time.gmtime(self.ctime)) - return property(**locals()) - - -class DeviceDescriptor: - def __init__(self, vendor_id, product_id, interface_id) : - self.vendor_id = vendor_id - self.product_id = product_id - self.interface_id = interface_id - - def getDevice(self) : - """ - Return the device corresponding to the device descriptor if it is - available on a USB bus. Otherwise, return None. Note that the - returned device has yet to be claimed or opened. - """ - buses = usb.busses() - for bus in buses : - for device in bus.devices : - if device.idVendor == self.vendor_id : - if device.idProduct == self.product_id : - return device - return None - - - - -class PRS500Device(object): - SONY_VENDOR_ID = 0x054c - PRS500_PRODUCT_ID = 0x029b - PRS500_INTERFACE_ID = 0 - PRS500_BULK_IN_EP = 0x81 - PRS500_BULK_OUT_EP = 0x02 - - def __init__(self) : - self.device_descriptor = DeviceDescriptor(PRS500Device.SONY_VENDOR_ID, - PRS500Device.PRS500_PRODUCT_ID, - PRS500Device.PRS500_INTERFACE_ID) - self.device = self.device_descriptor.getDevice() - self.handle = None - - @classmethod - def _validate_response(cls, res, type=0x00, number=0x00): - 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)) - - def open(self) : - self.device = self.device_descriptor.getDevice() - if not self.device: - print >> sys.stderr, "Unable to find Sony Reader. Is it connected?" - sys.exit(1) - self.handle = self.device.open() - if sys.platform == 'darwin' : - # XXX : For some reason, Mac OS X doesn't set the - # configuration automatically like Linux does. - self.handle.setConfiguration(1) - self.handle.claimInterface(self.device_descriptor.interface_id) - self.handle.reset() - - def close(self): - self.handle.releaseInterface() - self.handle, self.device = None, None - - def _send_command(self, command, response_type=Response, timeout=100): - """ - Send command to device and return its response. - - command -- an object of type Command or one of its derived classes - response_type -- an object of type 'type'. The return packet from the device is returned as an object of type response_type. - timeout -- the time to wait for a response from the device, in milliseconds - """ - if LOG_PACKETS: print "Command\n%s\n--\n"%command - bytes_sent = self.handle.controlMsg(0x40, 0x80, command) - if bytes_sent != len(command): - raise ControlError(desc="Could not send control request to device\n" + str(query.query)) - response = response_type(self.handle.controlMsg(0xc0, 0x81, Response.SIZE, timeout=timeout)) - if LOG_PACKETS: print "Response\n%s\n--\n"%response - return response - - def _send_validated_command(self, command, cnumber=None, response_type=Response, timeout=100): - """ Wrapper around _send_command that checks if the response's rnumber == cnumber or command.number if cnumber==None """ - if cnumber == None: cnumber = command.number - res = self._send_command(command, response_type=response_type, timeout=timeout) - PRS500Device._validate_response(res, type=command.type, number=cnumber) - return res - - def _bulk_read(self, data_type=Answer, size=4096): - data = data_type(self.handle.bulkRead(PRS500Device.PRS500_BULK_IN_EP, size)) - if LOG_PACKETS: print "Answer\n%s\n--\n"%data - return data - - def _read_single_bulk_packet(self, command_number=0x00, data_type=Answer, size=4096): - data = self._bulk_read(data_type=data_type, size=size) - self._send_validated_command(AcknowledgeBulkRead(data.id), cnumber=command_number) - return data - - def _test_bulk_reads(self): - self._send_validated_command( ShortCommand(number=0x00, type=0x01, command=0x00) ) - self._read_single_bulk_packet(command_number=0x00, size=24) - - def _start_session(self): - self.handle.reset() - self._test_bulk_reads() - self._send_validated_command( ShortCommand(number=0x0107, command=0x028000, type=0x01) ) # TODO: Figure out the meaning of this command - self._test_bulk_reads() - self._send_validated_command( ShortCommand(number=0x0106, type=0x01, command=0x312d) ) # TODO: Figure out the meaning of this command - self._send_validated_command( ShortCommand(number=0x01, type=0x01, command=0x01) ) - - def _end_session(self): - self._send_validated_command( ShortCommand(number=0x01, type=0x01, command=0x00) ) - - def _run_session(self, *args): - self._start_session() - res = None - try: - res = args[0](args[1:]) - finally: - self._end_session() - pass - return res - - def _get_path_properties(self, path): - res = self._send_validated_command(PathQuery(path), response_type=ListResponse) - data = self._read_single_bulk_packet(size=0x28, data_type=PathAnswer, command_number=PathQuery.PROPERTIES) - if res.path_not_found : raise PathError(path[:-1] + " does not exist on device") - if res.is_invalid : raise PathError(path[:-1] + " is not a valid path") - if res.is_unmounted : raise PathError(path[:-1] + " is not mounted") - return (res, data) - - def _list(self, args): - path = args[0] - if not path.endswith("/"): path += "/" # Initially assume path is a directory - files = [] - res, data = self._get_path_properties(path) - if res.is_file: - path = path[:-1] - res, data = self._get_path_properties(path) - files = [ (path, data) ] - else: - self._send_validated_command(PathQuery(path, number=PathQuery.ID), response_type=ListResponse) - id = self._read_single_bulk_packet(size=0x14, data_type=IdAnswer, command_number=PathQuery.ID).id - next = ShortCommand.list_command(id=id) - cnumber = next.number - items = [] - while True: - res = self._send_validated_command(next, response_type=ListResponse) - size = res.data[2] + 16 - data = self._read_single_bulk_packet(size=size, data_type=ListAnswer, command_number=cnumber) - # 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 - items.append(data.name) - for item in items: - ipath = path + item - res, data = self._get_path_properties(ipath) - files.append( (ipath, data) ) - files.sort() - return files - - def list(self, path, recurse=False): - files = self._run_session(self._list, path) - files = [ File(file) for file in files ] - 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) - return dirs - - def ls(self, path, recurse=False, color=False, human_readable_size=False, ll=False, cols=0): - def col_split(l, cols): # split list l into columns - rows = len(l) / cols - if len(l) % cols: - rows += 1 - m = [] - for i in range(rows): - m.append(l[i::rows]) - return m - - def row_widths(table): # Calculate widths for each column in the row-wise table - tcols = len(table[0]) - rowwidths = [ 0 for i in range(tcols) ] - for row in table: - c = 0 - for item in row: - rowwidths[c] = len(item) if len(item) > rowwidths[c] else rowwidths[c] - c += 1 - return rowwidths - - output = StringIO.StringIO() - if path.endswith("/"): path = path[:-1] - dirs = self.list(path, recurse) - for dir in dirs: - if recurse: print >>output, dir[0] + ":" - lsoutput, lscoloutput = [], [] - files = dir[1] - maxlen = 0 - if ll: # Calculate column width for size column - for file in files: - size = len(str(file.size)) - if human_readable_size: size = len(file.human_readable_size) - if size > maxlen: maxlen = size - for file in files: - name = file.name - lsoutput.append(name) - if color: name = file.name_in_color - lscoloutput.append(name) - if ll: - size = str(file.size) - if human_readable_size: size = file.human_readable_size - print >>output, file.mode_string, ("%"+str(maxlen)+"s")%size, file.modification_time, name - if not ll and len(lsoutput) > 0: - trytable = [] - for colwidth in range(MINIMUM_COL_WIDTH, cols): - trycols = int(cols/colwidth) - trytable = col_split(lsoutput, trycols) - works = True - for row in trytable: - row_break = False - for item in row: - if len(item) > colwidth - 1: - works, row_break = False, True - break - if row_break: break - if works: break - rowwidths = row_widths(trytable) - trytablecol = col_split(lscoloutput, len(trytable[0])) - for r in range(len(trytable)): - for c in range(len(trytable[r])): - padding = rowwidths[c] - len(trytable[r][c]) - print >>output, trytablecol[r][c], "".ljust(padding), - print >>output - print >>output - listing = output.getvalue().rstrip()+ "\n" - output.close() - return listing - - - -def main(argv): - if _term : cols = _term.COLS - else: cols = 70 - - parser = OptionParser(usage="usage: %prog command [options] args\n\ncommand is one of: ls, get, put or rm\n\n"+ - "For help on a particular command: %prog command") - parser.add_option("--log-packets", help="print out packet stream to stdout", dest="log_packets", action="store_true", default=False) - parser.remove_option("-h") - parser.disable_interspersed_args() # Allow unrecognized options - options, args = parser.parse_args() - global LOG_PACKETS - LOG_PACKETS = options.log_packets - if len(args) < 1: - parser.print_help() - sys.exit(1) - command = args[0] - args = args[1:] - dev = PRS500Device() - if command == "ls": - parser = OptionParser(usage="usage: %prog ls [options] path\n\npath must begin with /,a:/ or b:/") - parser.add_option("--color", help="show ls output in color", dest="color", action="store_true", default=False) - parser.add_option("-l", help="In addition to the name of each file, print the file type, permissions, and timestamp (the modification time unless other times are selected)", dest="ll", action="store_true", default=False) - parser.add_option("-R", help="Recursively list subdirectories encountered. /dev and /proc are omitted", dest="recurse", action="store_true", default=False) - parser.remove_option("-h") - parser.add_option("-h", "--human-readable", help="show sizes in human readable format", dest="hrs", action="store_true", default=False) - options, args = parser.parse_args(args) - if len(args) < 1: - parser.print_help() - sys.exit(1) - dev.open() - try: - print dev.ls(args[0], color=options.color, recurse=options.recurse, ll=options.ll, human_readable_size=options.hrs, cols=cols), - except PathError, e: - print >> sys.stderr, e - sys.exit(1) - finally: - dev.close() - else: - parser.print_help() - sys.exit(1) - -if __name__ == "__main__": - _term = TerminalController() - main(sys.argv) diff --git a/decoding b/decoding deleted file mode 100644 index b54e1ca2f9..0000000000 --- a/decoding +++ /dev/null @@ -1,27 +0,0 @@ -Control packets (see pg 199 in usb1.1 spec) - -00 01 02 03 04 05 06 07 - -00 - Request Type -01 - Request -02,03 - Value -04,05 - Index/Offset -06,07 - Data length - - -Request Type -80 - device to host; type standard; recipient device -c0 - device to host; type vendor ; recipient device -40 - host to device; type vendor ; recipient device - -c0, 40 are used for sony communication - - -Request -06 - GET_DESCRIPTOR -80 - Sony proprietary. goes with 40 on first bit -81 - Sony proprietary. goes with c0 on first bit - - -Data length -Only the 6th byte seems to be used. The value of the 6th byte converted to decimal is the number of bytes to be read/written. diff --git a/epydoc-pdf.conf b/epydoc-pdf.conf new file mode 100644 index 0000000000..70769e7013 --- /dev/null +++ b/epydoc-pdf.conf @@ -0,0 +1,50 @@ +[epydoc] # Epydoc section marker (required by ConfigParser) + +# Information about the project. +name: libprs500 +#url: http:// + +# The list of modules to document. Modules can be named using +# dotted names, module filenames, or package directory names. +# This option may be repeated. +modules: libprs500, scripts/prs500.py, usb, struct + +output: pdf +target: docs/pdf + +frames: no + +# graph +# The list of graph types that should be automatically included +# in the output. Graphs are generated using the Graphviz "dot" +# executable. Graph types include: "classtree", "callgraph", +# "umlclass". Use "all" to include all graph types +graph: classtree + +# css +# The CSS stylesheet for HTML output. Can be the name of a builtin +# stylesheet, or the name of a file. +css: white + +# link +# HTML code for the project link in the navigation bar. If left +# unspecified, the project link will be generated based on the +# project's name and URL. +#link: My Cool Project + +# top +# The "top" page for the documentation. Can be a URL, the name +# of a module or class, or one of the special names "trees.html", +# "indices.html", or "help.html" +top: libprs500 + +# verbosity +# An integer indicating how verbose epydoc should be. The default +# value is 0; negative values will supress warnings and errors; +# positive values will give more verbose output. +#verbosity: 0 + +# separate-classes +# Whether each class should be listed in its own section when +# generating LaTeX or PDF output. +#separate-classes: no diff --git a/epydoc.conf b/epydoc.conf index 221da46165..fcad5a2a8d 100644 --- a/epydoc.conf +++ b/epydoc.conf @@ -1,19 +1,51 @@ [epydoc] # Epydoc section marker (required by ConfigParser) # Information about the project. -name: My Cool Project -url: http://cool.project/ +name: libprs500 +#url: http:// # The list of modules to document. Modules can be named using # dotted names, module filenames, or package directory names. # This option may be repeated. -modules: usb, struct -modules: prstypes.py, communicate.py, errors.py +modules: libprs500, scripts/prs500.py, usb, struct # Write html output to the directory "apidocs" output: html -target: apidocs/ +target: docs/html -# Include all automatically generated graphs. These graphs are -# generated using Graphviz dot. -graph: all +frames: no + +# graph +# The list of graph types that should be automatically included +# in the output. Graphs are generated using the Graphviz "dot" +# executable. Graph types include: "classtree", "callgraph", +# "umlclass". Use "all" to include all graph types +graph: classtree + +# css +# The CSS stylesheet for HTML output. Can be the name of a builtin +# stylesheet, or the name of a file. +css: white + +# link +# HTML code for the project link in the navigation bar. If left +# unspecified, the project link will be generated based on the +# project's name and URL. +#link: My Cool Project + +# top +# The "top" page for the documentation. Can be a URL, the name +# of a module or class, or one of the special names "trees.html", +# "indices.html", or "help.html" +top: libprs500 + +# verbosity +# An integer indicating how verbose epydoc should be. The default +# value is 0; negative values will supress warnings and errors; +# positive values will give more verbose output. +#verbosity: 0 + +# separate-classes +# Whether each class should be listed in its own section when +# generating LaTeX or PDF output. +#separate-classes: no diff --git a/errors.py b/errors.py deleted file mode 100644 index 8ab707dc86..0000000000 --- a/errors.py +++ /dev/null @@ -1,32 +0,0 @@ -#!/usr/bin/env python -from exceptions import Exception - -class ProtocolError(Exception): - """ The base class for all exceptions in this package """ - def __init__(self, msg): - Exception.__init__(self, msg) - -class PacketError(ProtocolError): - """ Errors with creating/interpreting packets """ - def __init__(self, msg): - ProtocolError.__init__(self, msg) - -class PathError(ProtocolError): - def __init__(self, msg): - Exception.__init__(self, msg) - -class ControlError(ProtocolError): - def __init__(self, query=None, response=None, desc=None): - self.query = query - self.response = response - Exception.__init__(self, desc) - - def __str__(self): - if self.query and self.response: - return "Got unexpected response:\n" + \ - "query:\n"+str(self.query.query)+"\n"+\ - "expected:\n"+str(self.query.response)+"\n" +\ - "actual:\n"+str(self.response) - if self.desc: - return self.desc - return "Unknown control error occurred" diff --git a/libprs500/__init__.py b/libprs500/__init__.py new file mode 100644 index 0000000000..cc6b13ca97 --- /dev/null +++ b/libprs500/__init__.py @@ -0,0 +1,25 @@ +""" +This package provides an interface to the SONY Reader PRS-500 over USB. + +The public interface of libprs500 is in L{libprs500.communicate}. To use it + >>> from libprs500.communicate import PRS500Device + >>> dev = PRS500Device() + >>> dev.open() + >>> dev.get_device_information() + ('Sony Reader', 'PRS-500/U', '1.0.00.21081', 'application/x-bbeb-book') + >>> dev.close() + +There is also a script L{prs500} that provides a command-line interface to libprs500. See the script +for more usage examples. + +The packet structure used by the SONY Reader USB protocol is defined in the module L{prstypes}. The communication logic +is defined in the module L{communicate}. + +This package requires U{PyUSB}. In order to use it as a non-root user on Linux, you should have +the following rule in C{/etc/udev/rules.d/90-local.rules} :: + BUS=="usb", SYSFS{idProduct}=="029b", SYSFS{idVendor}=="054c", MODE="660", GROUP="plugdev" +You may have to adjust the GROUP and the location of the rules file to suit your distribution. +""" +VERSION = "0.1" +__docformat__ = "epytext" +__author__ = "Kovid Goyal " diff --git a/libprs500/communicate.py b/libprs500/communicate.py new file mode 100755 index 0000000000..1ee2fe1cba --- /dev/null +++ b/libprs500/communicate.py @@ -0,0 +1,366 @@ +## 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. + +### 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{PRS500Device} defines the methods for performing various tasks. +""" +import usb, sys +from array import array + +from prstypes import AcknowledgeBulkRead, Answer, Command, DeviceInfo, DirOpen, DirRead, DirClose, \ + FileOpen, FileClose, FileRead, IdAnswer, ListAnswer, \ + ListResponse, LongCommand, FileProperties, PathQuery, Response, \ + ShortCommand, DeviceInfoQuery +from errors import * + +MINIMUM_COL_WIDTH = 12 #: Minimum width of columns in ls output + +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 self.path + + +class DeviceDescriptor: + """ + Describes a USB device. + + A description is composed of the Vendor Id, Product Id and Interface Id. + See the U{USB spec} + """ + + def __init__(self, vendor_id, product_id, interface_id) : + self.vendor_id = vendor_id + self.product_id = product_id + self.interface_id = interface_id + + def getDevice(self) : + """ + Return the device corresponding to the device descriptor if it is + available on a USB bus. Otherwise, return None. Note that the + returned device has yet to be claimed or opened. + """ + buses = usb.busses() + for bus in buses : + for device in bus.devices : + if device.idVendor == self.vendor_id : + if device.idProduct == self.product_id : + return device + return None + + +class PRS500Device(object): + + """ + Contains the logic for performing various tasks on the reader. + + The implemented tasks are: + 0. Getting information about the device + 1. Getting a file from the device + 2. Listing of directories. See the C{list} method. + """ + + SONY_VENDOR_ID = 0x054c #: SONY Vendor Id + PRS500_PRODUCT_ID = 0x029b #: Product Id for the PRS-500 + PRS500_INTERFACE_ID = 0 #: The interface we use to talk to the device + PRS500_BULK_IN_EP = 0x81 #: Endpoint for Bulk reads + PRS500_BULK_OUT_EP = 0x02 #: Endpoint for Bulk writes + + def __init__(self, log_packets=False) : + """ @param log_packets: If true the packet stream to/from the device is logged """ + self.device_descriptor = DeviceDescriptor(PRS500Device.SONY_VENDOR_ID, + PRS500Device.PRS500_PRODUCT_ID, + PRS500Device.PRS500_INTERFACE_ID) + self.device = self.device_descriptor.getDevice() + self.handle = None + self._log_packets = log_packets + + @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)) + + def open(self) : + """ + Claim an interface on the device for communication. Requires write privileges to the device file. + + @todo: Check this on Mac OSX + """ + self.device = self.device_descriptor.getDevice() + if not self.device: + print >> sys.stderr, "Unable to find Sony Reader. Is it connected?" + sys.exit(1) + self.handle = self.device.open() + if sys.platform == 'darwin' : + # XXX : For some reason, Mac OS X doesn't set the + # configuration automatically like Linux does. + self.handle.setConfiguration(1) # TODO: Check on Mac OSX + self.handle.claimInterface(self.device_descriptor.interface_id) + self.handle.reset() + + def close(self): + """ Release device interface """ + self.handle.releaseInterface() + self.handle, self.device = None, None + + def _send_command(self, command, response_type=Response, timeout=100): + """ + Send L{command} to device and return its L{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. + """ + if self._log_packets: print "Command\n%s\n--\n"%command + bytes_sent = self.handle.controlMsg(0x40, 0x80, command) + if bytes_sent != len(command): + raise ControlError(desc="Could not send control request to device\n" + str(query.query)) + response = response_type(self.handle.controlMsg(0xc0, 0x81, Response.SIZE, timeout=timeout)) + if self._log_packets: print "Response\n%s\n--\n"%response + return response + + def _send_validated_command(self, command, cnumber=None, response_type=Response, timeout=100): + """ + 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) + PRS500Device._validate_response(res, type=command.type, number=cnumber) + return res + + def _bulk_read_packet(self, data_type=Answer, size=4096): + """ + Read in a data packet via a Bulk Read. + + @param data_type: an object of type type. The data packet is returned as an object of type C{data_type}. + @param size: the expected size of the data packet. + """ + data = data_type(self.handle.bulkRead(PRS500Device.PRS500_BULK_IN_EP, size)) + if self._log_packets: print "Answer\n%s\n--\n"%data + return data + + def _bulk_read(self, bytes, command_number=0x00, packet_size=4096, data_type=Answer): + """ Read in C{bytes} bytes via a bulk transfer in packets of size S{<=} C{packet_size} """ + bytes_left = bytes + packets = [] + while bytes_left > 0: + if packet_size > bytes_left: packet_size = bytes_left + packet = self._bulk_read_packet(data_type=data_type, size=packet_size) + bytes_left -= len(packet) + packets.append(packet) + self._send_validated_command(AcknowledgeBulkRead(packets[0].id), cnumber=command_number) + return packets + + def _test_bulk_reads(self): + """ Carries out a test of bulk reading as part of session initialization. """ + self._send_validated_command( ShortCommand(number=0x00, type=0x01, command=0x00) ) + self._bulk_read(24, command_number=0x00) + + def _start_session(self): + """ + Send the initialization sequence to the device. See the code for details. + This method should be called before any real work is done. Though most things seem to work without it. + """ + self.handle.reset() + self._test_bulk_reads() + self._send_validated_command( ShortCommand(number=0x0107, command=0x028000, type=0x01) ) # TODO: Figure out the meaning of this command + self._test_bulk_reads() + self._send_validated_command( ShortCommand(number=0x0106, type=0x01, command=0x312d) ) # TODO: Figure out the meaning of this command + self._send_validated_command( ShortCommand(number=0x01, type=0x01, command=0x01) ) + + def _end_session(self): + """ Send the end session command to the device. Causes the device to change status from "Do not disconnect" to "USB Connected" """ + self._send_validated_command( ShortCommand(number=0x01, type=0x01, command=0x00) ) + + def _run_session(self, *args): + """ + Wrapper that automatically calls L{_start_session} and L{_end_session}. + + @param args: An array whose first element is the method to call and whose remaining arguments are passed to that mathos as an array. + """ + self._start_session() + res = None + try: + res = args[0](args[1:]) + except ArgumentError, e: + self._end_session() + raise e + self._end_session() + return res + + def _get_device_information(self, args): + """ Ask device for device information. See L{DeviceInfoQuery}. """ + size = self._send_validated_command(DeviceInfoQuery()).data[2] + 16 + data = self._bulk_read(size, command_number=DeviceInfoQuery.NUMBER, data_type=DeviceInfo)[0] + return (data.device_name, data.device_version, data.software_version, data.mime_type) + + def get_device_information(self): + """ Return (device name, device version, software version on device, mime type). See L{_get_device_information} """ + return self._run_session(self._get_device_information) + + def _get_path_properties(self, path): + """ Send command asking device for properties of C{path}. Return (L{Response}, L{Answer}). """ + 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("/"): 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") + return (res, data) + + def get_file(self, path, outfile): + """ + Read the file at path on the device and write it to outfile. For the logic see L{_get_file}. + + @param outfile: file object like C{sys.stdout} or the result of an C{open} call + """ + self._run_session(self._get_file, path, outfile) + + def _get_file(self, args): + """ + Fetch a file from the device and write it to an output stream. + + The data is fetched in chunks of size S{<=} 32K. Each chunk is make of packets of size S{<=} 4K. See L{FileOpen}, + L{FileRead} and L{FileClose} for details on the command packets used. + + @param args: C{path, outfile = arg[0], arg[1]} + """ + path, outfile = args[0], args[1] + if path.endswith("/"): path = path[:-1] # We only copy files + res, data = self._get_path_properties(path) + if data.is_dir: raise PathError("Cannot read as " + path + " is a directory") + bytes = data.file_size + self._send_validated_command(FileOpen(path)) + id = self._bulk_read(20, data_type=IdAnswer, command_number=FileOpen.NUMBER)[0].id + bytes_left, chunk_size, pos = bytes, 0x8000, 0 + while bytes_left > 0: + if chunk_size > bytes_left: chunk_size = bytes_left + res = self._send_validated_command(FileRead(id, pos, chunk_size)) + packets = self._bulk_read(chunk_size+16, command_number=FileRead.NUMBER, packet_size=4096) + try: + array('B', packets[0][16:]).tofile(outfile) # The first 16 bytes are meta information on the packet stream + for i in range(1, len(packets)): + array('B', packets[i]).tofile(outfile) + except IOError, e: + self._send_validated_command(FileClose(id)) + raise ArgumentError("File get operation failed. Could not write to local location: " + str(e)) + bytes_left -= chunk_size + pos += chunk_size + self._send_validated_command(FileClose(id)) + + + def _list(self, args): + """ + Ask the device to list a path. See the code for details. See L{DirOpen}, + L{DirRead} and L{DirClose} for details on the command packets used. + + @param args: C{path=args[0]} + @return: A list of tuples. The first element of each tuple is a string, the path. The second is a L{FileProperties}. + If the path points to a file, the list will have length 1. + """ + path = args[0] + if not path.endswith("/"): path += "/" # Initially assume path is a directory + files = [] + res, data = self._get_path_properties(path) + if res.is_file: + path = path[:-1] + res, data = self._get_path_properties(path) + files = [ (path, data) ] + else: + # Get query ID used to ask for next element in list + self._send_validated_command(DirOpen(path), response_type=ListResponse) + 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[2] + 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 + items.append(data.name) + self._send_validated_command(DirClose(id)) + for item in items: + ipath = path + item + res, data = self._get_path_properties(ipath) + files.append( (ipath, data) ) + files.sort() + return files + + def list(self, path, recurse=False): + """ + Return a listing of path. + + See L{_list} for the communication logic. + + @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}. + 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. + """ + files = self._run_session(self._list, path) + files = [ File(file) for file in files ] + 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) + return dirs diff --git a/libprs500/errors.py b/libprs500/errors.py new file mode 100644 index 0000000000..7d91adccb2 --- /dev/null +++ b/libprs500/errors.py @@ -0,0 +1,51 @@ +## 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 errors that libprs500 generates. + +G{classtree ProtocolError} +""" +from exceptions import Exception + +class ProtocolError(Exception): + """ The base class for all exceptions in this package """ + def __init__(self, msg): + Exception.__init__(self, msg) + +class PacketError(ProtocolError): + """ Errors with creating/interpreting packets """ + +class ArgumentError(ProtocolError): + """ Errors caused by invalid arguments to a public interface function """ + +class PathError(ArgumentError): + """ When a user supplies an incorrect/invalid path """ + +class ControlError(ProtocolError): + """ Errors in Command/Response pairs while communicating with the device """ + def __init__(self, query=None, response=None, desc=None): + self.query = query + self.response = response + Exception.__init__(self, desc) + + def __str__(self): + if self.query and self.response: + return "Got unexpected response:\n" + \ + "query:\n"+str(self.query.query)+"\n"+\ + "expected:\n"+str(self.query.response)+"\n" +\ + "actual:\n"+str(self.response) + if self.desc: + return self.desc + return "Unknown control error occurred" diff --git a/libprs500/prstypes.py b/libprs500/prstypes.py new file mode 100755 index 0000000000..b782530fcb --- /dev/null +++ b/libprs500/prstypes.py @@ -0,0 +1,840 @@ +## 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 +from errors import PacketError + +BYTE = "} 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 = "", "" + 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" + ascii = "" + last_line = ans[ans.rfind("\n")+1:] + padding = 40 - 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 Command(TransferBuffer): + + """ Defines the structure of command packets sent to the device. """ + + 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) + + @apply + def number(): + doc =\ + """ + Command number. C{unsigned int} stored in 4 bytes at byte 0. + + Observed command numbers are: + 1. 0x00 + Test bulk read + 2. 0x01 + End session + 3. 0x0101 + Ask for device information + 4. 0x1000 + Acknowledge + 5. 0x107 + Purpose unknown, occurs in the beginning of sessions duing command testing. Best guess is some sort of OK packet + 6. 0x106 + Purpose unknown, occurs in the beginning of sessions duing command testing. Best guess is some sort of OK packet + 7. 0x18 + Ask for information about a file + 8. 0x33 + Open directory for reading + 9. 0x34 + Close directory + 10. 0x35 + Ask for next item in the directory + 11. 0x10 + File open command + 12. 0x11 + File close command + 13. 0x16 + File read command + """ + def fget(self): + return self.unpack(start=0, fmt=DWORD)[0] + + def fset(self, val): + self.pack(val, start=0, fmt=DWORD) + + return property(**locals()) + + @apply + def type(): + doc =\ + """ Command type. C{unsigned long long} stored in 8 bytes at byte 4. Known types 0x00, 0x01. Not sure what the type means. """ + def fget(self): + return self.unpack(start=4, fmt=DDWORD)[0] + + def fset(self, val): + self.pack(val, start=4, fmt=DDWORD) + + return property(**locals()) + + @apply + def length(): + doc =\ + """ Length in bytes of the data part of the query. C{unsigned int} stored in 4 bytes at byte 12. """ + def fget(self): + return self.unpack(start=12, fmt=DWORD)[0] + + def fset(self, val): + self.pack(val, start=12, fmt=DWORD) + + return property(**locals()) + + @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()) + + +class ShortCommand(Command): + + """ A L{Command} whoose data section is 4 bytes long """ + + SIZE = 20 #: Packet size in bytes + + 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 + + @apply + def command(): + doc =\ + """ The command. Not sure why this is needed in addition to Command.number. C{unsigned int} 4 bytes long 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 DirOpen(Command): + + """ Open a directory for reading its contents """ + NUMBER = 0x33 #: Command number + + def __init__(self, path): + Command.__init__(self, 20 + len(path)) + self.number=DirOpen.NUMBER + self.type = 0x01 + self.length = 4 + len(path) + self.path_length = len(path) + self.path = path + + @apply + def path_length(): + doc =\ + """ The length in bytes of the path to follow. 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()) + + @apply + def path(): + doc =\ + """ The path. Stored as a string at byte 20. """ + + def fget(self): + return self.unpack(start=20, fmt="<"+str(self.path_length)+"s")[0] + + def fset(self, val): + self.pack(val, start=20, fmt="<"+str(self.path_length)+"s") + + return property(**locals()) + +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 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 =\ + """ + The command. Not sure why it is needed in addition to L{Command.number}. + 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 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 FileOpen(Command): + """ File open command """ + NUMBER = 0x10 + READ = 0x00 + WRITE = 0x01 + def __init__(self, path, mode=0x00): + Command.__init__(self, 24 + len(path)) + self.number=FileOpen.NUMBER + self.type = 0x01 + self.length = 8 + len(path) + self.mode = mode + self.path_length = len(path) + self.path = path + + @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()) + + @apply + def path_length(): + doc =\ + """ The length in bytes of the path to follow. C{unsigned int} stored at byte 20. """ + def fget(self): + return self.unpack(start=20, fmt=DWORD)[0] + + def fset(self, val): + self.pack(val, start=20, fmt=DWORD) + + return property(**locals()) + + @apply + def path(): + doc =\ + """ The path. Stored as a string at byte 24. """ + + def fget(self): + return self.unpack(start=24, fmt="<"+str(self.path_length)+"s")[0] + + def fset(self, val): + self.pack(val, start=24, fmt="<"+str(self.path_length)+"s") + + return property(**locals()) + +class FileRead(Command): + """ Command to read from an open file """ + NUMBER = 0x16 #: Command number to read from a file + def __init__(self, id, offset, size): + """ + @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} + """ + Command.__init__(self, 32) + self.number=FileRead.NUMBER + self.type = 0x01 + self.length = 32 + self.id = id + self.offset = offset + self.size = size + + @apply + def id(): + doc =\ + """ The file ID returned by a FileOpen command. C{unsigned int} stored in 4 bytes 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()) + + @apply + def offset(): + doc =\ + """ offset in the file at which to read. C{unsigned long long} stored in 8 bytes at byte 20. """ + def fget(self): + return self.unpack(start=20, fmt=DDWORD)[0] + + def fset(self, val): + self.pack(val, start=20, fmt=DDWORD) + + return property(**locals()) + + @apply + def size(): + doc =\ + """ The number of bytes to read. C{unsigned int} stored in 4 bytes at byte 28. """ + def fget(self): + return self.unpack(start=28, fmt=DWORD)[0] + + def fset(self, val): + self.pack(val, start=28, fmt=DWORD) + + return property(**locals()) + + + + +class PathQuery(Command): + + """ + Defines structure of command that requests information about a path + + >>> print prstypes.PathQuery("/test/path/", number=prstypes.PathQuery.PROPERTIES) + 1800 0000 0100 0000 0000 0000 0f00 0000 ................ + 0b00 0000 2f74 6573 742f 7061 7468 2f ..../test/path/ + """ + NUMBER = 0x18 #: Command number + + def __init__(self, path): + Command.__init__(self, 20 + len(path)) + self.number=PathQuery.NUMBER + self.type = 0x01 + self.length = 4 + len(path) + self.path_length = len(path) + self.path = path + + @apply + def path_length(): + doc =\ + """ The length in bytes of the path to follow. 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()) + + @apply + def path(): + doc =\ + """ The path. Stored as a string at byte 20. """ + + def fget(self): + return self.unpack(start=20, fmt="<"+str(self.path_length)+"s")[0] + + def fset(self, val): + self.pack(val, start=20, fmt="<"+str(self.path_length)+"s") + + return property(**locals()) + + +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 + + 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 rnumber(): + doc =\ + """ + The response number. C{unsigned int} stored in 4 bytes at byte 16. + + It will be the command number from a command that was sent to the device sometime before this response. + """ + 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()) + + @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(packet) < 16 : raise PacketError(str(self.__class__)[7:-2] + " packets must have a length of atleast 16 bytes") + TransferBuffer.__init__(self, packet) + + @apply + def id(): + doc =\ + """ The id of this bulk transfer packet. C{unsigned int} stored in 4 bytes at byte 0. """ + + def fget(self): + return self.unpack(start=0, fmt=DWORD)[0] + + def fset(self, val): + self.pack(val, start=0, fmt=DWORD) + + return property(**locals()) + +class FileProperties(Answer): + + """ Defines the structure of packets that contain size, date and permissions information about files/directories. """ + + @apply + def file_size(): + doc =\ + """ The file size. C{unsigned long long} stored in 8 bytes at byte 16. """ + + def fget(self): + return self.unpack(start=16, fmt=DDWORD)[0] + + def fset(self, val): + self.pack(val, start=16, fmt=DDWORD) + + return property(**locals()) + + @apply + def is_dir(): + doc =\ + """ + True if path points to a directory, False if it points to a file. C{unsigned int} stored in 4 bytes at byte 24. + + Value of 1 == file and 2 == dir + """ + + def fget(self): + return (self.unpack(start=24, fmt=DWORD)[0] == 2) + + def fset(self, val): + if val: val = 2 + else: val = 1 + self.pack(val, start=24, fmt=DWORD) + + return property(**locals()) + + @apply + def ctime(): + doc =\ + """ The creation time of this file/dir as an epoch (seconds since Jan 1970). C{unsigned int} stored in 4 bytes at byte 28. """ + + def fget(self): + return self.unpack(start=28, fmt=DWORD)[0] + + def fset(self, val): + self.pack(val, start=28, fmt=DWORD) + + return property(**locals()) + + @apply + def wtime(): + doc =\ + """ The modification time of this file/dir as an epoch (seconds since Jan 1970). C{unsigned int} stored in 4 bytes at byte 32""" + + def fget(self): + return self.unpack(start=32, fmt=DWORD)[0] + + def fset(self, val): + self.pack(val, start=32, fmt=DWORD) + + return property(**locals()) + + @apply + def is_readonly(): + doc =\ + """ + Whether this file is readonly. C{unsigned int} stored in 4 bytes at byte 36. + + A value of 0 corresponds to read/write and 4 corresponds to read-only. The device doesn't send full permissions information. + """ + + 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 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 """ + + @apply + def device_name(): + """ The name of the device. Stored as a string in 32 bytes starting at byte 16. """ + def fget(self): + src = self.unpack(start=16, fmt="<32s")[0] + return src[0:src.find('\x00')] + return property(**locals()) + + @apply + def device_version(): + """ The device version. Stored as a string in 32 bytes starting at byte 48. """ + def fget(self): + src = self.unpack(start=48, fmt="<32s")[0] + return src[0:src.find('\x00')] + return property(**locals()) + + @apply + def software_version(): + """ Version of the software on the device. Stored as a string in 26 bytes starting at byte 80. """ + def fget(self): + src = self.unpack(start=80, fmt="<26s")[0] + return src[0:src.find('\x00')] + return property(**locals()) + + @apply + def mime_type(): + """ Mime type served by tinyhttp?. Stored as a string in 32 bytes starting at byte 104. """ + def fget(self): + src = self.unpack(start=104, fmt="<32s")[0] + return src[0:src.find('\x00')] + return property(**locals()) + +class ListAnswer(Answer): + + """ Defines the structure of packets that contain items in a list. """ + + @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()) + + @apply + def name_length(): + doc =\ + """ The length in bytes of the list item to follow. C{unsigned int} stored in 4 bytes at byte 20 """ + def fget(self): + return self.unpack(start=20, fmt=DWORD)[0] + + def fset(self, val): + self.pack(val, start=20, fmt=DWORD) + + return property(**locals()) + + @apply + def name(): + doc =\ + """ The name of the list item. Stored as an (ascii?) string at byte 24. """ + + def fget(self): + return self.unpack(start=24, fmt="<"+str(self.name_length)+"s")[0] + + def fset(self, val): + self.pack(val, start=24, fmt="<"+str(self.name_length)+"s") + + return property(**locals()) diff --git a/terminfo.py b/libprs500/terminfo.py similarity index 100% rename from terminfo.py rename to libprs500/terminfo.py diff --git a/pr5-500.e3p b/pr5-500.e3p deleted file mode 100644 index b0514cecbd..0000000000 --- a/pr5-500.e3p +++ /dev/null @@ -1,101 +0,0 @@ - - - - - - - Python - Console - Library to communicate with the Sony Reader prs-500 via USB - - Kovid Goyal - kovid@kovidgoyal.net - - - communicate.py - - - data.py - - - prstypes.py - - - - - - - - - - - - communicate.py - - - Subversion - (dp0 -S'status' -p1 -(lp2 -S'' -p3 -asS'log' -p4 -(lp5 -g3 -asS'global' -p6 -(lp7 -g3 -asS'update' -p8 -(lp9 -g3 -asS'remove' -p10 -(lp11 -g3 -asS'add' -p12 -(lp13 -g3 -asS'tag' -p14 -(lp15 -g3 -asS'export' -p16 -(lp17 -g3 -asS'commit' -p18 -(lp19 -g3 -asS'diff' -p20 -(lp21 -g3 -asS'checkout' -p22 -(lp23 -g3 -asS'history' -p24 -(lp25 -g3 -as. - (dp0 -S'standardLayout' -p1 -I01 -s. - - - - - - - - - diff --git a/prs-500.e3p b/prs-500.e3p index c6c16d6898..92749583b9 100644 --- a/prs-500.e3p +++ b/prs-500.e3p @@ -6,22 +6,22 @@ Python Console - Library to communicate with the Sony Reader prs-500 via USB + Library to communicate with the Sony Reader PRS-500 via USB Kovid Goyal kovid@kovidgoyal.net - communicate.py + libprs500/communicate.py - data.py + libprs500/terminfo.py - prstypes.py + libprs500/prstypes.py - errors.py + libprs500/errors.py @@ -33,7 +33,7 @@ - communicate.py + libprs500/communicate.py Subversion diff --git a/prstypes.py b/prstypes.py deleted file mode 100755 index e6c2e0e2ab..0000000000 --- a/prstypes.py +++ /dev/null @@ -1,533 +0,0 @@ -#!/usr/bin/env python -## 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. - -import struct -from errors import PacketError - -BYTE = " 31 and self[i+b] < 127 else "." - except IndexError: break - ans = ans + " " - if (i+2)%16 == 0: - ans += "\t" + ascii + "\n" - ascii = "" - if len(ascii) > 0: - last_line = ans[ans.rfind("\n")+1:] - padding = 32 - len(last_line) - ans += "".ljust(padding) + "\t\t" + ascii - return ans.strip() - - def unpack(self, fmt=DWORD, start=0): - """ Return decoded data from buffer. See pydoc struct for fmt. start is 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 data and write it to buffer. See pydoc struct fmt. start is 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 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. - """ - index, sign = 2, "" - if num < 0: - index, sign = 3, "-" - h=hex(num)[index:] - if len(h) < 2: - h = "0"+h - return sign + h - - - -class Command(TransferBuffer): - """ Defines the structure of command packets sent to the device. """ - - def __init__(self, packet): - 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) - - @apply - def number(): - doc =\ - """ - Command number - - Observed command numbers are: - 0x00001000 -- Acknowledge - 0x00000107 -- Purpose unknown, occurs in start_session - 0x00000106 -- Purpose unknown, occurs in start_session - """ - def fget(self): - return self.unpack(start=0, fmt=DWORD)[0] - - def fset(self, val): - self.pack(val, start=0, fmt=DWORD) - - return property(**locals()) - - @apply - def type(): - doc =\ - """ - Command type. Known types 0x00, 0x01. Not sure what the type means. - """ - def fget(self): - return self.unpack(start=4, fmt=DDWORD)[0] - - def fset(self, val): - self.pack(val, start=4, fmt=DDWORD) - - return property(**locals()) - - @apply - def length(): - doc =\ - """ Length in bytes of the data part of the query """ - def fget(self): - return self.unpack(start=12, fmt=DWORD)[0] - - def fset(self, val): - self.pack(val, start=12, fmt=DWORD) - - return property(**locals()) - - @apply - def data(): - doc =\ - """ - The data part of this command. Returned/set as/by a TransferBuffer. - - 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. - """ - def fget(self): - return self[16:] - - def fset(self, buffer): - self[16:] = buffer - self.length = len(buffer) - - return property(**locals()) - - -class ShortCommand(Command): - - SIZE = 20 #Packet size in bytes - - def __init__(self, number=0x00, type=0x00, command=0x00): - """ command must be an integer """ - Command.__init__(self, ShortCommand.SIZE) - self.number = number - self.type = type - self.length = 4 - self.command = command - - @classmethod - def list_command(cls, id): - """ Return the command packet used to ask for the next item in the list """ - return ShortCommand(number=0x35, type=0x01, command=id) - - @apply - def command(): - doc =\ - """ - The command. Not sure why this is needed in addition to Command.number - """ - 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 LongCommand(Command): - - SIZE = 32 # Size in bytes of long command packets - - def __init__(self, number=0x00, type=0x00, command=0x00): - """ command must be either an integer or a list of not more than 4 integers """ - Command.__init__(self, LongCommand.SIZE) - self.number = number - self.type = type - self.length = 16 - self.command = command - - @apply - def command(): - doc =\ - """ - The command. - - It should be set to a x-integer list, where 0= 16 """ - if len(packet) < 16 : raise PacketError(str(self.__class__)[7:-2] + " packets must have a length of atleast 16 bytes") - TransferBuffer.__init__(self, packet) - - @apply - def id(): - doc =\ - """ The id of this bulk transfer packet """ - - def fget(self): - return self.unpack(start=0, fmt=DWORD)[0] - - def fset(self, val): - self.pack(val, start=0, fmt=DWORD) - - return property(**locals()) - -class PathAnswer(Answer): - - """ Defines the structure of packets that contain size, date and permissions information about files/directories. """ - - @apply - def file_size(): - doc =\ - """ The file size """ - - def fget(self): - return self.unpack(start=16, fmt=DDWORD)[0] - - def fset(self, val): - self.pack(val, start=16, fmt=DDWORD) - - return property(**locals()) - - @apply - def is_dir(): - doc =\ - """ True if path points to a directory, False if it points to a file """ - - def fget(self): - return (self.unpack(start=24, fmt=DWORD)[0] == 2) - - def fset(self, val): - if val: val = 2 - else: val = 1 - self.pack(val, start=24, fmt=DWORD) - - return property(**locals()) - - @apply - def ctime(): - doc =\ - """ The creation time of this file/dir as an epoch """ - - def fget(self): - return self.unpack(start=28, fmt=DWORD)[0] - - def fset(self, val): - self.pack(val, start=28, fmt=DWORD) - - return property(**locals()) - - @apply - def wtime(): - doc =\ - """ The modification time of this file/dir as an epoch """ - - def fget(self): - return self.unpack(start=32, fmt=DWORD)[0] - - def fset(self, val): - self.pack(val, start=32, fmt=DWORD) - - 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 IdAnswer(Answer): - - """ Defines the structure of packets that contain identifiers for directories. """ - - @apply - def id(): - doc =\ - """ The identifier """ - - 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 ListAnswer(Answer): - - """ Defines the structure of packets that contain items in a list. """ - - @apply - def is_dir(): - doc =\ - """ True if list item points to a directory, False if it points to a file """ - - 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()) - - @apply - def name_length(): - doc =\ - """ The length in bytes of the list item to follow """ - def fget(self): - return self.unpack(start=20, fmt=DWORD)[0] - - def fset(self, val): - self.pack(val, start=20, fmt=DWORD) - - return property(**locals()) - - @apply - def name(): - doc =\ - """ The name of the list item """ - - def fget(self): - return self.unpack(start=24, fmt="<"+str(self.name_length)+"s")[0] - - def fset(self, val): - self.pack(val, start=24, fmt="<"+str(self.name_length)+"s") - - return property(**locals()) diff --git a/scripts/prs500.py b/scripts/prs500.py new file mode 100755 index 0000000000..7ee1edbd48 --- /dev/null +++ b/scripts/prs500.py @@ -0,0 +1,244 @@ +#!/usr/bin/env python +""" +Provides a command-line interface to the SONY Reader PRS-500. + +For usage information run the script. +""" + +import StringIO, sys, time, os +from optparse import OptionParser + +from libprs500 import VERSION +from libprs500.communicate import PRS500Device +from libprs500.terminfo import TerminalController +from libprs500.errors import ArgumentError + + +MINIMUM_COL_WIDTH = 12 #: Minimum width of columns in ls output + +class FileFormatter(object): + def __init__(self, file, term): + self.term = term + self.is_dir = file.is_dir + self.is_readonly = file.is_readonly + self.size = file.size + self.ctime = file.ctime + self.wtime = file.wtime + self.name = file.name + self.path = file.path + + @apply + def mode_string(): + doc=""" The mode string for this file. There are only two modes read-only and read-write """ + def fget(self): + mode, x = "-", "-" + if self.is_dir: mode, x = "d", "x" + if self.is_readonly: mode += "r-"+x+"r-"+x+"r-"+x + else: mode += "rw"+x+"rw"+x+"rw"+x + return mode + return property(**locals()) + + @apply + def name_in_color(): + doc=""" The name in ANSI text. Directories are blue, ebooks are green """ + def fget(self): + cname = self.name + blue, green, normal = "", "", "" + if self.term: blue, green, normal = self.term.BLUE, self.term.GREEN, self.term.NORMAL + if self.is_dir: cname = blue + self.name + normal + else: + ext = self.name[self.name.rfind("."):] + if ext in (".pdf", ".rtf", ".lrf", ".lrx", ".txt"): cname = green + self.name + normal + return cname + return property(**locals()) + + @apply + def human_readable_size(): + doc=""" File size in human readable form """ + def fget(self): + if self.size < 1024: divisor, suffix = 1, "" + elif self.size < 1024*1024: divisor, suffix = 1024., "K" + elif self.size < 1024*1024*1024: divisor, suffix = 1024*1024, "M" + elif self.size < 1024*1024*1024*1024: divisor, suffix = 1024*1024, "G" + size = str(self.size/divisor) + if size.find(".") > -1: size = size[:size.find(".")+2] + return size + suffix + return property(**locals()) + + @apply + def modification_time(): + doc=""" Last modified time in the Linux ls -l format """ + def fget(self): + return time.strftime("%Y-%m-%d %H:%M", time.gmtime(self.wtime)) + return property(**locals()) + + @apply + def creation_time(): + doc=""" Last modified time in the Linux ls -l format """ + def fget(self): + return time.strftime("%Y-%m-%d %H:%M", time.gmtime(self.ctime)) + return property(**locals()) + +def info(dev): + info = dev.get_device_information() + print "Device name: ", info[0] + print "Device version: ", info[1] + print "Software version:", info[2] + print "Mime type: ", info[3] + +def ls(dev, path, term, recurse=False, color=False, human_readable_size=False, ll=False, cols=0): + def col_split(l, cols): # split list l into columns + rows = len(l) / cols + if len(l) % cols: + rows += 1 + m = [] + for i in range(rows): + m.append(l[i::rows]) + return m + + def row_widths(table): # Calculate widths for each column in the row-wise table + tcols = len(table[0]) + rowwidths = [ 0 for i in range(tcols) ] + for row in table: + c = 0 + for item in row: + rowwidths[c] = len(item) if len(item) > rowwidths[c] else rowwidths[c] + c += 1 + return rowwidths + + output = StringIO.StringIO() + if path.endswith("/"): path = path[:-1] + dirs = dev.list(path, recurse) + for dir in dirs: + if recurse: print >>output, dir[0] + ":" + lsoutput, lscoloutput = [], [] + files = dir[1] + maxlen = 0 + if ll: # Calculate column width for size column + for file in files: + size = len(str(file.size)) + if human_readable_size: + file = FileFormatter(file, term) + size = len(file.human_readable_size) + if size > maxlen: maxlen = size + for file in files: + file = FileFormatter(file, term) + name = file.name + lsoutput.append(name) + if color: name = file.name_in_color + lscoloutput.append(name) + if ll: + size = str(file.size) + if human_readable_size: size = file.human_readable_size + print >>output, file.mode_string, ("%"+str(maxlen)+"s")%size, file.modification_time, name + if not ll and len(lsoutput) > 0: + trytable = [] + for colwidth in range(MINIMUM_COL_WIDTH, cols): + trycols = int(cols/colwidth) + trytable = col_split(lsoutput, trycols) + works = True + for row in trytable: + row_break = False + for item in row: + if len(item) > colwidth - 1: + works, row_break = False, True + break + if row_break: break + if works: break + rowwidths = row_widths(trytable) + trytablecol = col_split(lscoloutput, len(trytable[0])) + for r in range(len(trytable)): + for c in range(len(trytable[r])): + padding = rowwidths[c] - len(trytable[r][c]) + print >>output, trytablecol[r][c], "".ljust(padding), + print >>output + print >>output + listing = output.getvalue().rstrip()+ "\n" + output.close() + return listing + +def main(): + term = TerminalController() + cols = term.COLS + + parser = OptionParser(usage="usage: %prog command [options] args\n\ncommand is one of: info, ls, cp, cat or rm\n\n"+ + "For help on a particular command: %prog command", version="libprs500 version: " + VERSION) + parser.add_option("--log-packets", help="print out packet stream to stdout", dest="log_packets", action="store_true", default=False) + parser.remove_option("-h") + parser.disable_interspersed_args() # Allow unrecognized options + options, args = parser.parse_args() + + if len(args) < 1: + parser.print_help() + sys.exit(1) + command = args[0] + args = args[1:] + dev = PRS500Device(log_packets=options.log_packets) + if command == "ls": + parser = OptionParser(usage="usage: %prog ls [options] path\n\npath must begin with /,a:/ or b:/") + parser.add_option("--color", help="show ls output in color", dest="color", action="store_true", default=False) + parser.add_option("-l", help="In addition to the name of each file, print the file type, permissions, and timestamp (the modification time unless other times are selected)", dest="ll", action="store_true", default=False) + parser.add_option("-R", help="Recursively list subdirectories encountered. /dev and /proc are omitted", dest="recurse", action="store_true", default=False) + parser.remove_option("-h") + parser.add_option("-h", "--human-readable", help="show sizes in human readable format", dest="hrs", action="store_true", default=False) + options, args = parser.parse_args(args) + if len(args) < 1: + parser.print_help() + sys.exit(1) + dev.open() + try: + print ls(dev, args[0], term, color=options.color, recurse=options.recurse, ll=options.ll, human_readable_size=options.hrs, cols=cols), + except ArgumentError, e: + print >> sys.stderr, e + sys.exit(1) + finally: + dev.close() + elif command == "info": + dev.open() + try: + info(dev) + finally: dev.close() + elif command == "cp": + parser = OptionParser(usage="usage: %prog cp [options] source destination\n\nsource is a path on the device and must begin with /,a:/ or b:/"+ + "\n\ndestination is a path on your computer and can point to either a file or a directory") + options, args = parser.parse_args(args) + if len(args) < 2: + parser.print_help() + sys.exit(1) + if args[0].endswith("/"): path = args[0][:-1] + else: path = args[0] + outfile = args[1] + if os.path.isdir(outfile): + outfile = os.path.join(outfile, path[path.rfind("/")+1:]) + outfile = open(outfile, "w") + dev.open() + try: + dev.get_file(path, outfile) + except ArgumentError, e: + print >>sys.stderr, e + finally: + dev.close() + outfile.close() + elif command == "cat": + outfile = sys.stdout + parser = OptionParser(usage="usage: %prog cat path\n\npath should point to a file on the device and must begin with /,a:/ or b:/") + options, args = parser.parse_args(args) + if len(args) < 1: + parser.print_help() + sys.exit(1) + if args[0].endswith("/"): path = args[0][:-1] + else: path = args[0] + outfile = sys.stdout + dev.open() + try: + dev.get_file(path, outfile) + except ArgumentError, e: + print >>sys.stderr, e + finally: + dev.close() + else: + parser.print_help() + sys.exit(1) + +if __name__ == "__main__": + main() diff --git a/setup.py b/setup.py new file mode 100644 index 0000000000..bcf8f5a059 --- /dev/null +++ b/setup.py @@ -0,0 +1,20 @@ +#!/usr/bin/env python +from distutils.core import setup +from libprs500 import VERSION + +setup(name='libprs500', + version=VERSION, + description='Library to interface with the Sony Portable Reader 500 over USB.', + long_description = + """ + libprs500 is library to interface with the Sony Portable Reader 500 over USB. + It provides methods to list the contents of the file system on the device as well as + copy files from and to the device. It also provides a command line interface via the script prs500.py. + """, + author='Kovid Goyal', + author_email='kovid@kovidgoyal.net', + provides=['libprs500'], + requires=['pyusb'], + packages = ['libprs500'], + scripts = ['scripts/prs500.py'] + ) diff --git a/spike.pl b/spike.pl new file mode 100755 index 0000000000..8ad723776c --- /dev/null +++ b/spike.pl @@ -0,0 +1,115 @@ +#! /usr/bin/perl -w + +sub ST_INIT { 0; } +sub ST_OUT { 1; } +sub ST_IN { 2; } + +$state= ST_INIT; +$count= 0; + +while (<>) { + $_= &trim($_); + + if ( />>> URB \d+ going down >>>/ ) { + &dump(\%packet) if $count; + $state= ST_OUT; + $count++; + %packet= ( + num => $count + ); + next; + } elsif ( /<<< URB \d+ coming back <<'; + $packet{data}= []; + } + + #$_= <>; + #$_= &trim($_); + + $dline= sprintf("%s %s", $offset, &ascii_rep($data)); + + push (@{$packet{data}}, $dline); + } elsif ( /^\s+SetupPacket/ ) { + $_ = <>; + $packet{setup}= (split(/:\s+/))[1]; + } +} + +&dump(\%packet) if $count; + +0; + +sub dump { + my ($href)= @_; + + printf("%06d\t%s", $href->{num}, $href->{pipe}); + if ( $href->{pipe} eq 'C' ) { + printf("S %s", $href->{setup}); + if ( exists $href->{direction} ) { + print "\n"; + $line= shift(@{$href->{data}}); + printf("\tC%s %s", $href->{direction}, $line); + } + } elsif ( $href->{pipe} eq 'B' ) { + if ( exists $href->{direction} ) { + $line= shift(@{$href->{data}}); + printf("%s %s", $href->{direction}, $line); + } + } else { + warn "unknown pipe"; + } + + foreach $line (@{$href->{data}}) { + printf("\t %s", $line); + } + + print "\n"; +} + +sub trim { + my ($line)= @_; + + $line=~ s/ //g; + $line=~ s/^\d+\s+\d+\.\d+\s+//; + + return $line; +} + +sub ascii_rep { + my (@hexdata)= split(/\s+/, $_[0]); + my ($i)= 0; + my ($compact, $width); + my ($ascii, $byte); + + foreach $byte (@hexdata) { + my ($dec)= hex($byte); + my ($abyte); + + $compact.= $byte; + $compact.= ' ' if ($i%2); + $i++; + + $ascii.= ( $dec > 31 && $dec < 127 ) ? sprintf("%c", $dec) : + '.'; + } + + $width= 40-length($compact); + return sprintf("%s%s %s\n", $compact, ' 'x${width}, $ascii); +} +