Defined the minimum interface that a device backend must satisfy to be used in the GUI. See device.py.

This commit is contained in:
Kovid Goyal 2007-04-05 04:56:47 +00:00
parent ecaa9e813b
commit 011fc40780
6 changed files with 174 additions and 65 deletions

View File

@ -15,14 +15,10 @@
"""
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.get_device_information()
('Sony Reader', 'PRS-500/U', '1.0.00.21081', 'application/x-bbeb-book')
The public interface for device backends is defined in libprs500.device.
There is also a script L{prs500} that provides a command-line interface to
libprs500. See the script
the SONY Reader. See the script
for more usage examples. A GUI is available via the command prs500-gui.
The packet structure used by the SONY Reader USB protocol is defined

View File

@ -47,16 +47,16 @@ class book_metadata_field(object):
class Book(object):
""" Provides a view onto the XML element that represents a book """
title = book_metadata_field("title")
author = book_metadata_field("author", \
title = book_metadata_field("title")
author = book_metadata_field("author", \
formatter=lambda x: x if x.strip() else "Unknown")
mime = book_metadata_field("mime")
rpath = book_metadata_field("path")
id = book_metadata_field("id", formatter=int)
sourceid = book_metadata_field("sourceid", formatter=int)
size = book_metadata_field("size", formatter=int)
mime = book_metadata_field("mime")
rpath = book_metadata_field("path")
id = book_metadata_field("id", formatter=int)
sourceid = book_metadata_field("sourceid", formatter=int)
size = book_metadata_field("size", formatter=int)
# When setting this attribute you must use an epoch
datetime = book_metadata_field("date", \
datetime = book_metadata_field("date", \
formatter=lambda x: time.strptime(x, "%a, %d %b %Y %H:%M:%S %Z"),
setter=lambda x: time.strftime("%a, %d %b %Y %H:%M:%S GMT", time.gmtime(x)))

View File

@ -22,7 +22,7 @@ import StringIO, sys, time, os
from optparse import OptionParser
from libprs500 import __version__ as VERSION
from libprs500.communicate import PRS500Device
from libprs500.prs500 import PRS500
from terminfo import TerminalController
from libprs500.errors import ArgumentError, DeviceError, DeviceLocked
@ -197,7 +197,7 @@ def main():
command = args[0]
args = args[1:]
dev = PRS500Device(key=options.key, log_packets=options.log_packets)
dev = PRS500(key=options.key, log_packets=options.log_packets)
try:
if command == "df":
total = dev.total_space(end_session=False)

114
src/libprs500/device.py Normal file
View File

@ -0,0 +1,114 @@
## 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.
"""
Define the minimum interface that a device backend must satisfy to be used in
the GUI. A device backend must subclass the L{Device} class. See prs500.py for
a backend that implement the Device interface for the SONY PRS500 Reader.
"""
class Device(object):
"""
Defines the interface that should be implemented by backends that
communicate with an ebook reader.
The C{end_session} variables are used for USB session management. Sometimes
the front-end needs to call several methods one after another, in which case
the USB session should not be closed after each method call.
"""
# Ordered list of supported formats
FORMATS = ["lrf", "rtf", "pdf", "txt"]
VENDOR_ID = 0x0000
PRODICT_ID = 0x0000
def __init__(self, key='-1', log_packets=False, report_progress=None) :
"""
@param key: The key to unlock the device
@param log_packets: If true the packet stream to/from the device is logged
@param report_progress: Function that is called with a % progress
(number between 0 and 100) for various tasks
If it is called with -1 that means that the
task does not have any progress information
"""
raise NotImplementedError()
def get_device_information(self, end_session=True):
"""
Ask device for device information. See L{DeviceInfoQuery}.
@return: (device name, device version, software version on device, mime type)
"""
raise NotImplementedError()
def total_space(self, end_session=True):
"""
Get total space available on the mountpoints:
1. Main memory
2. Memory Stick
3. SD Card
@return: A 3 element list with total space in bytes of (1, 2, 3). If a
particular device doesn't have any of these locations it should return 0.
"""
raise NotImplementedError()
def free_space(self, end_session=True):
"""
Get free space available on the mountpoints:
1. Main memory
2. Memory Stick
3. SD Card
@return: A 3 element list with free space in bytes of (1, 2, 3). If a
particular device doesn't have any of these locations it should return 0.
"""
raise NotImplementedError()
def books(self, oncard=False, end_session=True):
"""
Return a list of ebooks on the device.
@param oncard: If True return a list of ebooks on the storage card,
otherwise return list of ebooks in main memory of device
@return: A list of Books. Each Book object must have the fields:
title, author, size, datetime (a time tuple), path, thumbnail (can be None).
"""
raise NotImplementedError()
def add_book(self, infile, name, info, booklists, oncard=False, \
sync_booklists=False, end_session=True):
"""
Add a book to the device. If oncard is True then the book is copied
to the card rather than main memory.
@param infile: The source file, should be opened in "rb" mode
@param name: The name of the book file when uploaded to the
device. The extension of name must be one of
the supported formats for this device.
@param info: A dictionary that must have the keys "title", "authors", "cover".
C{info["cover"]} should be a three element tuple (width, height, data)
where data is the image data in JPEG format as a string
@param booklists: A tuple containing the result of calls to
(L{books}(oncard=False), L{books}(oncard=True)).
"""
raise NotImplementedError()
def remove_book(self, paths, booklists, end_session=True):
"""
Remove the books specified by C{paths} from the device. The metadata
cache on the device must also be updated.
@param booklists: A tuple containing the result of calls to
(L{books}(oncard=False), L{books}(oncard=True)).
"""
raise NotImplementedError()

View File

@ -26,8 +26,7 @@ from PyQt4.QtGui import QPixmap, QErrorMessage, QLineEdit, \
QMessageBox, QFileDialog, QIcon, QDialog, QInputDialog
from PyQt4.Qt import qDebug, qFatal, qWarning, qCritical
from libprs500.communicate import PRS500Device as device
from libprs500.books import fix_ids
from libprs500.prs500 import PRS500 as device
from libprs500.errors import *
from libprs500.gui import import_ui, installErrorHandler, Error, _Warning, \
extension, APP_TITLE
@ -165,17 +164,10 @@ class Main(QObject, Ui_MainWindow):
self.library_model.delete(self.library_view.selectionModel()\
.selectedRows())
else:
self.status("Deleting files from device")
self.status("Deleting books and updating metadata on device")
paths = self.device_view.model().delete(rows)
for path in paths:
self.status("Deleting "+path[path.rfind("/")+1:])
self.dev.del_file(path, end_session=False)
fix_ids(self.reader_model.booklist, self.card_model.booklist)
self.status("Syncing media list to reader")
self.dev.upload_book_list(self.reader_model.booklist)
if len(self.card_model.booklist):
self.status("Syncing media list to card")
self.dev.upload_book_list(self.card_model.booklist)
self.dev.remove_book(paths, (self.reader_model.booklist, \
self.card_model.booklist), end_session=False)
self.update_availabe_space()
self.model_modified()
self.show_book(self.current_view.currentIndex(), QModelIndex())

View File

@ -43,7 +43,7 @@
"""
Contains the logic for communication with the device (a SONY PRS-500).
The public interface of class L{PRS500Device} defines the
The public interface of class L{PRS500} defines the
methods for performing various tasks.
"""
import sys
@ -53,6 +53,7 @@ from tempfile import TemporaryFile
from array import array
from functools import wraps
from libprs500.device import Device
from libprs500.libusb import Error as USBError
from libprs500.libusb import get_device_by_id
from libprs500.prstypes import *
@ -60,38 +61,9 @@ from libprs500.errors import *
from libprs500.books import BookList, fix_ids
from libprs500 import __author__ as AUTHOR
MINIMUM_COL_WIDTH = 12 #: Minimum width of columns in ls output
# Protocol versions libprs500 has been tested with
KNOWN_USB_PROTOCOL_VERSIONS = [0x3030303030303130L]
class Device(object):
""" Contains device independent methods """
_packet_number = 0 #: Keep track of the packet number for packet tracing
def log_packet(self, packet, header, stream=sys.stderr):
"""
Log C{packet} to stream C{stream}.
Header should be a small word describing the type of packet.
"""
self._packet_number += 1
print >> stream, str(self._packet_number), header, "Type:", \
packet.__class__.__name__
print >> stream, packet
print >> stream, "--"
@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))
class File(object):
"""
@ -117,14 +89,14 @@ class File(object):
return self.name
class PRS500Device(Device):
class PRS500(Device):
"""
Contains the logic for performing various tasks on the reader.
Implements the backend for communication with the SONY Reader.
Each method decorated by C{safe} performs a task.
"""
VENDOR_ID = 0x054c #: SONY Vendor Id
VENDOR_ID = 0x054c #: SONY Vendor Id
PRODUCT_ID = 0x029b #: Product Id for the PRS-500
INTERFACE_ID = 0 #: The interface we use to talk to the device
BULK_IN_EP = 0x81 #: Endpoint for Bulk reads
@ -136,7 +108,32 @@ class PRS500Device(Device):
# Ordered list of supported formats
FORMATS = ["lrf", "rtf", "pdf", "txt"]
# Height for thumbnails of books/images on the device
THUMBNAIL_HEIGHT = 68
THUMBNAIL_HEIGHT = 68
_packet_number = 0 #: Keep track of the packet number for packet tracing
def log_packet(self, packet, header, stream=sys.stderr):
"""
Log C{packet} to stream C{stream}.
Header should be a small word describing the type of packet.
"""
self._packet_number += 1
print >> stream, str(self._packet_number), header, "Type:", \
packet.__class__.__name__
print >> stream, packet
print >> stream, "--"
@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))
@classmethod
def signature(cls):
@ -202,7 +199,6 @@ class PRS500Device(Device):
If it is called with -1 that means that the
task does not have any progress information
"""
Device.__init__(self)
self.device = get_device_by_id(self.VENDOR_ID, self.PRODUCT_ID)
# Handle that is used to communicate with device. Setup in L{open}
self.handle = None
@ -237,8 +233,6 @@ class PRS500Device(Device):
Requires write privileges to the device file.
Also initialize the device.
See the source code for the sequenceof initialization commands.
@todo: Implement unlocking of the device
"""
self.device = get_device_by_id(self.VENDOR_ID, self.PRODUCT_ID)
if not self.device:
@ -790,6 +784,19 @@ class PRS500Device(Device):
self.get_file(self.MEDIA_XML, tfile, end_session=False)
return BookList(prefix=prefix, root=root, sfile=tfile)
@safe
def remove_book(self, paths, booklists, end_session=True):
"""
Remove the books specified by paths from the device. The metadata
cache on the device should also be updated.
"""
for path in paths:
self.del_file(path, end_session=False)
fix_ids(booklists[0], booklists[1])
self.upload_book_list(booklists[0], end_session=False)
if len(booklists[1]):
self.upload_book_list(booklists[1], end_session=False)
@safe
def add_book(self, infile, name, info, booklists, oncard=False, \
sync_booklists=False, end_session=True):