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
This commit is contained in:
Kovid Goyal 2006-11-07 04:10:11 +00:00
parent 5a57a2c022
commit 1c8319a5a4
19 changed files with 1778 additions and 1117 deletions

5
MANIFEST.in Normal file
View File

@ -0,0 +1,5 @@
include libprs500 *.py
include scripts *.py
include README
include docs/pdf/api.pdf
recursive-include docs/html *

8
Makefile.distrib Normal file
View File

@ -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

13
README
View File

@ -4,13 +4,16 @@ Requirements:
1) Python >= 2.5 1) Python >= 2.5
2) PyUSB >= 0.3.4 (http://sourceforge.net/projects/pyusb/) 2) PyUSB >= 0.3.4 (http://sourceforge.net/projects/pyusb/)
Installation:
As root
python setup.py install
Usage: 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" 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 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. rules file to suit your distribution.
To see the listing Usage information is provided when you run the script prs500.py
./communicate.py /path/to/see
If the path does not exist, it will throw an Exception.

View File

@ -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)

View File

@ -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.

50
epydoc-pdf.conf Normal file
View File

@ -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: <a href="somewhere">My Cool Project</a>
# 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

View File

@ -1,19 +1,51 @@
[epydoc] # Epydoc section marker (required by ConfigParser) [epydoc] # Epydoc section marker (required by ConfigParser)
# Information about the project. # Information about the project.
name: My Cool Project name: libprs500
url: http://cool.project/ #url: http://
# The list of modules to document. Modules can be named using # The list of modules to document. Modules can be named using
# dotted names, module filenames, or package directory names. # dotted names, module filenames, or package directory names.
# This option may be repeated. # This option may be repeated.
modules: usb, struct modules: libprs500, scripts/prs500.py, usb, struct
modules: prstypes.py, communicate.py, errors.py
# Write html output to the directory "apidocs" # Write html output to the directory "apidocs"
output: html output: html
target: apidocs/ target: docs/html
# Include all automatically generated graphs. These graphs are frames: no
# generated using Graphviz dot.
graph: all # 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: <a href="somewhere">My Cool Project</a>
# 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

View File

@ -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"

25
libprs500/__init__.py Normal file
View File

@ -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<http://pyusb.berlios.de/>}. 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 <kovid@kovidgoyal.net>"

366
libprs500/communicate.py Executable file
View File

@ -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<http://www.usb.org/developers/docs/usb_20_05122006.zip>}
"""
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<Command>} to device and return its L{response<Response>}.
@param command: an object of type Command or one of its derived classes
@param response_type: an object of type 'type'. The return packet from the device is returned as an object of type response_type.
@param timeout: the time to wait for a response from the device, in milliseconds. If there is no response, a L{usb.USBError} is raised.
"""
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<File>}.
The path is the path we are listing, the C{Files} are the files/directories in that path. If it is a recursive
list, then the first element will be (C{path}, children), the next will be (child, its children) and so on.
"""
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

51
libprs500/errors.py Normal file
View File

@ -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"

840
libprs500/prstypes.py Executable file
View File

@ -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<Command>}, L{Responses<Response>}, and L{Answers<Answer>}.
C{Commands} are sent to the device on the control bus, C{Responses} are received from the device,
also on the control bus. C{Answers} and their sub-classes represent data packets sent to/received from
the device via bulk transfers.
Commands are organized as follows: G{classtree Command}
You will typically only use sub-classes of Command.
Responses are organized as follows: G{classtree Response}
Responses inherit Command as they share header structure.
Answers are organized as follows: G{classtree Answer}
"""
import struct
from errors import PacketError
BYTE = "<B" #: Unsigned char little endian encoded in 1 byte
WORD = "<H" #: Unsigned short little endian encoded in 2 bytes
DWORD = "<I" #: Unsigned integer little endian encoded in 4 bytes
DDWORD = "<Q" #: Unsigned long long little endian encoded in 8 bytes
class TransferBuffer(list):
"""
Represents raw (unstructured) data packets sent over the usb bus.
C{TransferBuffer} is a wrapper around the tuples used by L{PyUSB<usb>} for communication.
It has convenience methods to read and write data from the underlying buffer. See
L{TransferBuffer.pack} and L{TransferBuffer.unpack}.
"""
def __init__(self, packet):
"""
Create a L{TransferBuffer} from C{packet} or an empty buffer.
@type packet: integer or listable object
@param packet: If packet is a list, it is copied into the C{TransferBuffer} and then normalized (see L{TransferBuffer._normalize}).
If it is an integer, a zero buffer of that length is created.
"""
if "__len__" in dir(packet):
list.__init__(self, list(packet))
self._normalize()
else: list.__init__(self, [0 for i in range(packet)])
def __add__(self, tb):
""" Return a TransferBuffer rather than a list as the sum """
return TransferBuffer(list.__add__(self, tb))
def __getslice__(self, start, end):
""" Return a TransferBuffer rather than a list as the slice """
return TransferBuffer(list.__getslice__(self, start, end))
def __str__(self):
"""
Return a string representation of this buffer.
Packets are represented as hex strings, in 2-byte pairs, S{<=} 16 bytes to a line. An ASCII representation is included. For example::
0700 0100 0000 0000 0000 0000 0c00 0000 ................
0200 0000 0400 0000 4461 7461 ........Data
"""
ans, ascii = "", ""
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<http://docs.python.org/lib/module-struct.html>}
@param start: Position in buffer from which to decode
"""
end = start + struct.calcsize(fmt)
return struct.unpack(fmt, "".join([ chr(i) for i in list.__getslice__(self, start, end) ]))
def pack(self, val, fmt=DWORD, start=0):
"""
Encode C{val} and write it to buffer.
@param fmt: See U{struct<http://docs.python.org/lib/module-struct.html>}
@param start: Position in buffer at which to write encoded data
"""
self[start:start+struct.calcsize(fmt)] = [ ord(i) for i in struct.pack(fmt, val) ]
def _normalize(self):
""" Replace negative bytes in C{self} by 256 + byte """
for i in range(len(self)):
if self[i] < 0:
self[i] = 256 + self[i]
@classmethod
def phex(cls, num):
"""
Return the hex representation of num without the 0x prefix.
If the hex representation is only 1 digit it is padded to the left with a zero. Used in L{TransferBuffer.__str__}.
"""
index, sign = 2, ""
if num < 0:
index, sign = 3, "-"
h=hex(num)[index:]
if len(h) < 2:
h = "0"+h
return sign + h
class 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="<III")
def fset(self, val):
self.pack(val, start=20, fmt="<III")
return property(**locals())
class ListResponse(Response):
""" Defines the structure of response packets received during list (ll) queries. See L{PathQuery}. """
IS_FILE = 0xffffffd2 #: Queried path is a file
IS_INVALID = 0xfffffff9 #: Queried path is malformed/invalid
IS_UNMOUNTED = 0xffffffc8 #: Queried path is not mounted (i.e. a removed storage card/stick)
IS_EOL = 0xfffffffa #: There are no more entries in the list
PATH_NOT_FOUND = 0xffffffd7 #: Queried path is not found
@apply
def code():
doc =\
""" The response code. Used to indicate conditions like EOL/Error/IsFile etc. C{unsigned int} stored in 4 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 is_file():
""" True iff queried path is a file """
def fget(self):
return self.code == ListResponse.IS_FILE
return property(**locals())
@apply
def is_invalid():
""" True iff queried path is invalid """
def fget(self):
return self.code == ListResponse.IS_INVALID
return property(**locals())
@apply
def path_not_found():
""" True iff queried path is not found """
def fget(self):
return self.code == ListResponse.PATH_NOT_FOUND
return property(**locals())
@apply
def is_unmounted():
""" True iff queried path is unmounted (i.e. removed storage card) """
def fget(self):
return self.code == ListResponse.IS_UNMOUNTED
return property(**locals())
@apply
def is_eol():
""" True iff there are no more items in the list """
def fget(self):
return self.code == ListResponse.IS_EOL
return property(**locals())
class Answer(TransferBuffer):
""" Defines the structure of packets sent to host via a bulk transfer (i.e., bulk reads) """
def __init__(self, packet):
""" @param packet: C{len(packet)} S{>=} 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())

View File

@ -1,101 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE Project SYSTEM "Project-3.9.dtd">
<!-- Project file for project pr5-500 -->
<!-- Saved: 2006-10-31, 09:35:02 -->
<!-- Copyright (C) 2006 Kovid Goyal, kovid@kovidgoyal.net -->
<Project version="3.9">
<ProgLanguage mixed="0">Python</ProgLanguage>
<UIType>Console</UIType>
<Description>Library to communicate with the Sony Reader prs-500 via USB</Description>
<Version></Version>
<Author>Kovid Goyal</Author>
<Email>kovid@kovidgoyal.net</Email>
<Sources>
<Source>
<Name>communicate.py</Name>
</Source>
<Source>
<Name>data.py</Name>
</Source>
<Source>
<Name>prstypes.py</Name>
</Source>
</Sources>
<Forms>
</Forms>
<Translations>
</Translations>
<Interfaces>
</Interfaces>
<Others>
</Others>
<MainScript>
<Name>communicate.py</Name>
</MainScript>
<Vcs>
<VcsType>Subversion</VcsType>
<VcsOptions>(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.</VcsOptions>
<VcsOtherData>(dp0
S'standardLayout'
p1
I01
s.</VcsOtherData>
</Vcs>
<FiletypeAssociations>
<FiletypeAssociation pattern="*.ui.h" type="FORMS" />
<FiletypeAssociation pattern="*.ui" type="FORMS" />
<FiletypeAssociation pattern="*.idl" type="INTERFACES" />
<FiletypeAssociation pattern="*.py" type="SOURCES" />
<FiletypeAssociation pattern="*.ptl" type="SOURCES" />
</FiletypeAssociations>
</Project>

View File

@ -6,22 +6,22 @@
<Project version="3.9"> <Project version="3.9">
<ProgLanguage mixed="0">Python</ProgLanguage> <ProgLanguage mixed="0">Python</ProgLanguage>
<UIType>Console</UIType> <UIType>Console</UIType>
<Description>Library to communicate with the Sony Reader prs-500 via USB</Description> <Description>Library to communicate with the Sony Reader PRS-500 via USB</Description>
<Version></Version> <Version></Version>
<Author>Kovid Goyal</Author> <Author>Kovid Goyal</Author>
<Email>kovid@kovidgoyal.net</Email> <Email>kovid@kovidgoyal.net</Email>
<Sources> <Sources>
<Source> <Source>
<Name>communicate.py</Name> <Name>libprs500/communicate.py</Name>
</Source> </Source>
<Source> <Source>
<Name>data.py</Name> <Name>libprs500/terminfo.py</Name>
</Source> </Source>
<Source> <Source>
<Name>prstypes.py</Name> <Name>libprs500/prstypes.py</Name>
</Source> </Source>
<Source> <Source>
<Name>errors.py</Name> <Name>libprs500/errors.py</Name>
</Source> </Source>
</Sources> </Sources>
<Forms> <Forms>
@ -33,7 +33,7 @@
<Others> <Others>
</Others> </Others>
<MainScript> <MainScript>
<Name>communicate.py</Name> <Name>libprs500/communicate.py</Name>
</MainScript> </MainScript>
<Vcs> <Vcs>
<VcsType>Subversion</VcsType> <VcsType>Subversion</VcsType>

View File

@ -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 = "<B" # Unsigned char little endian encoded in 1 byte
WORD = "<H" # Unsigned short little endian encoded in 2 bytes
DWORD = "<I" # Unsigned integer little endian encoded in 4 bytes
DDWORD = "<Q" # Unsigned long long little endian encoded in 8 bytes
class TransferBuffer(list):
def __init__(self, packet):
"""
packet should be any listable object, or an integer. If it is an integer, a zero buffer of that length is created.
packet is normalized (see TransferBuffer.normalize)
"""
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 thana 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 in the same format as that produced by spike.pl
"""
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:
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<x<5.
"""
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 """
LongCommand.__init__(self, number=0x1000, type=0x00, command=bulk_read_id)
class PathQuery(Command):
""" Defines structure of commands that request information about a path """
# Command.number values used in path queries
PROPERTIES = 0x18 # Ask for file properties
ID = 0x33 # Ask for query id for a directory listing
def __init__(self, path, number=0x18):
Command.__init__(self, 20 + len(path))
self.number=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 """
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 """
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. """
SIZE = 32 # Size of response packets in the SONY protocol
def __init__(self, packet):
""" 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.
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 of data in this response packet. Returned as a list. """
def fget(self):
return self.unpack(start=20, fmt="<III")
def fset(self, val):
self.pack(val, start=20, fmt="<III")
return property(**locals())
class ListResponse(Response):
""" Defines the structure of response packets received during list (ll) queries """
IS_FILE = 0xffffffd2
IS_INVALID = 0xfffffff9
IS_UNMOUNTED = 0xffffffc8
IS_EOL = 0xfffffffa
PATH_NOT_FOUND = 0xffffffd7
@apply
def code():
doc =\
"""
The response code. Used to indicate conditions like EOL/Error/IsFile
fmt=DWORD
"""
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 is_file():
def fget(self):
return self.code == ListResponse.IS_FILE
return property(**locals())
@apply
def is_invalid():
def fget(self):
return self.code == ListResponse.IS_INVALID
return property(**locals())
@apply
def path_not_found():
def fget(self):
return self.code == ListResponse.PATH_NOT_FOUND
return property(**locals())
@apply
def is_unmounted():
def fget(self):
return self.code == ListResponse.IS_UNMOUNTED
return property(**locals())
@apply
def is_eol():
def fget(self):
return self.code == ListResponse.IS_EOL
return property(**locals())
class Answer(TransferBuffer):
""" Defines the structure of packets sent to host via a bulk transfer (i.e., bulk reads) """
def __init__(self, packet):
""" packet must be a listable object of length >= 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())

244
scripts/prs500.py Executable file
View File

@ -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()

20
setup.py Normal file
View File

@ -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']
)

115
spike.pl Executable file
View File

@ -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);
}