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 @@
PythonConsole
- Library to communicate with the Sony Reader prs-500 via USB
+ Library to communicate with the Sony Reader PRS-500 via USBKovid Goyalkovid@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.pySubversion
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 << ) {
+ $state= ST_IN;
+ next;
+ } elsif ( $state == ST_INIT ) {
+ next;
+ }
+
+ if ( /^-- URB_FUNCTION_CONTROL_TRANSFER/ ) {
+ $packet{pipe}= 'C';
+ } elsif ( /^-- URB_FUNCTION_BULK_OR_INTERRUPT_TRANSFER/ ) {
+ $packet{pipe}= 'B';
+ } elsif ( /^\s+([0-9a-f]{8}:)\s+(.*)/ ) {
+ my ($offset)= $1;
+ my ($data) = $2;
+ my ($dline);
+
+ unless ( exists $packet{direction} ) {
+ $packet{direction}= ( $state == ST_IN ) ? '<' : '>';
+ $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);
+}
+