calibre/communicate.py
2006-11-06 01:18:41 +00:00

405 lines
16 KiB
Python
Executable file

#!/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)