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. 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 The public interface for device backends is defined in libprs500.device.
>>> from libprs500.communicate import PRS500Device
>>> dev = PRS500Device()
>>> dev.get_device_information()
('Sony Reader', 'PRS-500/U', '1.0.00.21081', 'application/x-bbeb-book')
There is also a script L{prs500} that provides a command-line interface to 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. 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 The packet structure used by the SONY Reader USB protocol is defined

View File

@ -22,7 +22,7 @@ import StringIO, sys, time, os
from optparse import OptionParser from optparse import OptionParser
from libprs500 import __version__ as VERSION from libprs500 import __version__ as VERSION
from libprs500.communicate import PRS500Device from libprs500.prs500 import PRS500
from terminfo import TerminalController from terminfo import TerminalController
from libprs500.errors import ArgumentError, DeviceError, DeviceLocked from libprs500.errors import ArgumentError, DeviceError, DeviceLocked
@ -197,7 +197,7 @@ def main():
command = args[0] command = args[0]
args = args[1:] args = args[1:]
dev = PRS500Device(key=options.key, log_packets=options.log_packets) dev = PRS500(key=options.key, log_packets=options.log_packets)
try: try:
if command == "df": if command == "df":
total = dev.total_space(end_session=False) 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 QMessageBox, QFileDialog, QIcon, QDialog, QInputDialog
from PyQt4.Qt import qDebug, qFatal, qWarning, qCritical from PyQt4.Qt import qDebug, qFatal, qWarning, qCritical
from libprs500.communicate import PRS500Device as device from libprs500.prs500 import PRS500 as device
from libprs500.books import fix_ids
from libprs500.errors import * from libprs500.errors import *
from libprs500.gui import import_ui, installErrorHandler, Error, _Warning, \ from libprs500.gui import import_ui, installErrorHandler, Error, _Warning, \
extension, APP_TITLE extension, APP_TITLE
@ -165,17 +164,10 @@ class Main(QObject, Ui_MainWindow):
self.library_model.delete(self.library_view.selectionModel()\ self.library_model.delete(self.library_view.selectionModel()\
.selectedRows()) .selectedRows())
else: else:
self.status("Deleting files from device") self.status("Deleting books and updating metadata on device")
paths = self.device_view.model().delete(rows) paths = self.device_view.model().delete(rows)
for path in paths: self.dev.remove_book(paths, (self.reader_model.booklist, \
self.status("Deleting "+path[path.rfind("/")+1:]) self.card_model.booklist), end_session=False)
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.update_availabe_space() self.update_availabe_space()
self.model_modified() self.model_modified()
self.show_book(self.current_view.currentIndex(), QModelIndex()) 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). 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. methods for performing various tasks.
""" """
import sys import sys
@ -53,6 +53,7 @@ from tempfile import TemporaryFile
from array import array from array import array
from functools import wraps from functools import wraps
from libprs500.device import Device
from libprs500.libusb import Error as USBError from libprs500.libusb import Error as USBError
from libprs500.libusb import get_device_by_id from libprs500.libusb import get_device_by_id
from libprs500.prstypes import * from libprs500.prstypes import *
@ -60,38 +61,9 @@ from libprs500.errors import *
from libprs500.books import BookList, fix_ids from libprs500.books import BookList, fix_ids
from libprs500 import __author__ as AUTHOR from libprs500 import __author__ as AUTHOR
MINIMUM_COL_WIDTH = 12 #: Minimum width of columns in ls output
# Protocol versions libprs500 has been tested with # Protocol versions libprs500 has been tested with
KNOWN_USB_PROTOCOL_VERSIONS = [0x3030303030303130L] 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): class File(object):
""" """
@ -117,10 +89,10 @@ class File(object):
return self.name 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. Each method decorated by C{safe} performs a task.
""" """
@ -138,6 +110,31 @@ class PRS500Device(Device):
# Height for thumbnails of books/images on the device # 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 @classmethod
def signature(cls): def signature(cls):
""" Return a two element tuple (vendor id, product id) """ """ Return a two element tuple (vendor id, product id) """
@ -202,7 +199,6 @@ class PRS500Device(Device):
If it is called with -1 that means that the If it is called with -1 that means that the
task does not have any progress information task does not have any progress information
""" """
Device.__init__(self)
self.device = get_device_by_id(self.VENDOR_ID, self.PRODUCT_ID) self.device = get_device_by_id(self.VENDOR_ID, self.PRODUCT_ID)
# Handle that is used to communicate with device. Setup in L{open} # Handle that is used to communicate with device. Setup in L{open}
self.handle = None self.handle = None
@ -237,8 +233,6 @@ class PRS500Device(Device):
Requires write privileges to the device file. Requires write privileges to the device file.
Also initialize the device. Also initialize the device.
See the source code for the sequenceof initialization commands. 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) self.device = get_device_by_id(self.VENDOR_ID, self.PRODUCT_ID)
if not self.device: if not self.device:
@ -790,6 +784,19 @@ class PRS500Device(Device):
self.get_file(self.MEDIA_XML, tfile, end_session=False) self.get_file(self.MEDIA_XML, tfile, end_session=False)
return BookList(prefix=prefix, root=root, sfile=tfile) 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 @safe
def add_book(self, infile, name, info, booklists, oncard=False, \ def add_book(self, infile, name, info, booklists, oncard=False, \
sync_booklists=False, end_session=True): sync_booklists=False, end_session=True):