diff --git a/MANIFEST.in b/MANIFEST.in index e6ff0d7411..eb49d4cbac 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,4 +1,5 @@ include libprs500 *.py +include libprs500/gui *.py include scripts *.py include README include docs/pdf/api.pdf diff --git a/libprs500/__init__.py b/libprs500/__init__.py index 15171003fb..4238af5f12 100644 --- a/libprs500/__init__.py +++ b/libprs500/__init__.py @@ -1,3 +1,17 @@ +## 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. """ This package provides an interface to the SONY Reader PRS-500 over USB. diff --git a/libprs500/communicate.py b/libprs500/communicate.py index cbb71a5fe0..38d0c19cdf 100755 --- a/libprs500/communicate.py +++ b/libprs500/communicate.py @@ -184,6 +184,7 @@ class PRS500Device(object): def safe(func): """ Decorator that wraps a call to C{func} to ensure that exceptions are handled correctly. + It also calls L{open} to claim the interface and initialize the Reader if needed. As a convenience, C{safe} automatically sends the a L{USBConnect} after calling func, unless func has a keyword argument named C{end_session} set to C{False}. @@ -192,15 +193,20 @@ class PRS500Device(object): An L{usb.USBError} will cause the library to release control of the USB interface via a call to L{close}. """ def run_session(*args, **kwargs): - dev = args[0] + dev = args[0] res = None try: + if not dev.handle: dev.open() res = func(*args, **kwargs) except ArgumentError, e: if not kwargs.has_key("end_session") or kwargs["end_session"]: dev._send_validated_command(USBConnect()) raise e except usb.USBError, e: + if "No such device" in str(e): + raise DeviceError() + elif "Connection timed out" in str(e): + raise TimeoutError(func.__name__) dev.close() raise e if not kwargs.has_key("end_session") or kwargs["end_session"]: @@ -214,8 +220,8 @@ class PRS500Device(object): 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.device = self.device_descriptor.getDevice() #: The actual device (PyUSB object) + self.handle = None #: Handle that is used to communicate with device. Setup in L{open} self._log_packets = log_packets @classmethod @@ -235,8 +241,7 @@ class PRS500Device(object): """ self.device = self.device_descriptor.getDevice() if not self.device: - print >> sys.stderr, "Unable to find Sony Reader. Is it connected?" - sys.exit(1) + raise DeviceError() self.handle = self.device.open() self.handle.claimInterface(self.device_descriptor.interface_id) self.handle.reset() @@ -492,11 +497,7 @@ class PRS500Device(object): @safe def touch(self, path, end_session=True): - """ - Create a file at path - - @todo: Update file modification time if file already exists - """ + """ Create a file at path """ if path.endswith("/") and len(path) > 1: path = path[:-1] exists, file = self._exists(path) if exists and file.is_dir: @@ -505,11 +506,6 @@ class PRS500Device(object): res = self._send_validated_command(FileCreate(path)) if res.code != 0: raise PathError("Could not create file " + path + ". Response code: " + str(hex(res.code))) -## res = self._send_validated_command(SetFileInfo(path)) -## if res.code != 0: -## raise ProtocolError("Unable to touch " + path + ". Response code: " + hex(res.code)) -## file.wtime = int(time.time()) -## self._bulk_write(file[16:]) @safe @@ -588,38 +584,38 @@ class PRS500Device(object): """ Return a list of all ebooks on the device - @return: A two element tuple. The first element is the books in the main memory and the second is the books on the storage card. - Each element is a list of dictionaries. Important fields in each dictionary are "title", "author", "path" and "thumbnail". + @return: A four element tuple. The first element is the books in the main memory and the second is the books on the storage card. + The first two elements are each a list of dictionaries. If there is no card the second list is empty. + Important fields in each dictionary are "title", "author", "path" and "thumbnail". + The third and fourth elements are the temporary files that hold main.xml and cache.xml """ - buffer = TemporaryFile() - media = self.get_file("/Data/database/cache/media.xml", buffer, end_session=False) + main_xml = TemporaryFile() + media = self.get_file("/Data/database/cache/media.xml", main_xml, end_session=False) parser = make_parser() parser.setFeature(feature_namespaces, 0) finder = FindBooks() parser.setContentHandler(finder) - buffer.seek(0) - parser.parse(buffer) - buffer.close() + main_xml.seek(0) + parser.parse(main_xml) books = finder.books root = "a:/" - buffer = TemporaryFile() + cache_xml = TemporaryFile() cbooks = [] try: - self.get_file("a:/Sony Reader/database/cache.xml", buffer, end_session=False) + self.get_file("a:/Sony Reader/database/cache.xml", cache_xml, end_session=False) except PathError: try: - self.get_file("b:/Sony Reader/database/cache.xml", buffer, end_session=False) + self.get_file("b:/Sony Reader/database/cache.xml", cache_xml, end_session=False) root = "b:/" except PathError: pass - if buffer.tell() > 0: + if cache_xml.tell() > 0: finder = FindBooks(type="cache", root=root) - buffer.seek(0) + cache_xml.seek(0) parser.setContentHandler(finder) - parser.parse(buffer) - buffer.close() + parser.parse(cache_xml) cbooks = finder.books - return books, cbooks + return books, cbooks, main_xml, cache_xml diff --git a/libprs500/errors.py b/libprs500/errors.py index 7d91adccb2..dbc83c80a3 100644 --- a/libprs500/errors.py +++ b/libprs500/errors.py @@ -24,6 +24,16 @@ class ProtocolError(Exception): def __init__(self, msg): Exception.__init__(self, msg) +class TimeoutError(ProtocolError): + """ There was a timeout during communication """ + def __init__(self, func_name): + ProtocolError.__init__(self, "There was a timeout while communicating with the device in function: "+func_name) + +class DeviceError(ProtocolError): + """ Raised when device is not found """ + def __init__(self): + ProtocolError.__init__(self, "Unable to find SONY Reader. Is it connected?") + class PacketError(ProtocolError): """ Errors with creating/interpreting packets """ diff --git a/libprs500/gui/__init__.py b/libprs500/gui/__init__.py new file mode 100644 index 0000000000..82d22e958b --- /dev/null +++ b/libprs500/gui/__init__.py @@ -0,0 +1,16 @@ +## 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. +__docformat__ = "epytext" +__author__ = "Kovid Goyal " diff --git a/libprs500/gui/main.py b/libprs500/gui/main.py new file mode 100644 index 0000000000..712631f093 --- /dev/null +++ b/libprs500/gui/main.py @@ -0,0 +1,88 @@ +## 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. +from main_gui import Ui_MainWindow +from libprs500.communicate import PRS500Device as device +from libprs500.errors import * +from PyQt4.Qt import QThread + + +class DeviceDetector(QThread): + def __init__(self, detected_slot): + QThread.__init__(self) + self.dev = None + self.detected_slot = detected_slot + self.removed = False + + def run(self): + wait = 1 + while None == self.msleep(wait): + wait = 1000 + if self.removed or not self.dev: + self.dev = device() + self.removed = False + self.detected_slot() + + + +class MainWindow(Ui_MainWindow): + + def safe(func): + def call_func(*args, **kwargs): + window = args[0] + res = None + try: + res = func(*args, **kwargs) + except DeviceError: + window.device_removed() + except TimeoutError, e: + print e + window.timeout_error() + return res + return call_func + + @apply + def dev(): + def fget(self): + return self.detector.dev + return property(**locals()) + + def __init__(self, window, log_packets): + Ui_MainWindow.__init__(self) + self.log_packets = log_packets + self.detector = DeviceDetector(self.establish_connection) + self.detector.start() + self.setupUi(window) + window.show() + + def device_removed(self, timeout=False): + """ @todo: implement this """ + self.detector.removed = True + + def timeout_error(self): + """ @todo: update status bar """ + self.detector.sleep(10) + self.device_removed(timeout=True) + + @safe + def establish_connection(self): + mb, cb, mx, cx = self.dev.books() + self.main_books = mb + self.card_books = cb + self.main_xml = mx + self.cache_xml = cx + print self.main_books + self.card_books + + + diff --git a/libprs500/gui/main.ui b/libprs500/gui/main.ui new file mode 100644 index 0000000000..652474e0ed --- /dev/null +++ b/libprs500/gui/main.ui @@ -0,0 +1,136 @@ +## 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. + + MainWindow + + + + 0 + 0 + 885 + 631 + + + + SONY Reader + + + + + 9 + + + 6 + + + + + 0 + + + 6 + + + + + 0 + + + 6 + + + + + ... + + + + + + + false + + + false + + + Search title and author + + + true + + + + + + + + + + 5 + 7 + 0 + 0 + + + + QFrame::Plain + + + false + + + + + + + + + 0 + + + 6 + + + + + QFrame::Plain + + + + + + + TextLabel + + + + + + + + + + + Qt::Horizontal + + + 4 + + + + + + diff --git a/libprs500/gui/main_gui.py b/libprs500/gui/main_gui.py new file mode 100644 index 0000000000..a00ee5cf6e --- /dev/null +++ b/libprs500/gui/main_gui.py @@ -0,0 +1,94 @@ +# -*- coding: utf-8 -*- + +# Form implementation generated from reading ui file 'main.ui' +# +# Created: Thu Nov 16 20:48:21 2006 +# by: PyQt4 UI code generator 4-snapshot-20061112 +# +# WARNING! All changes made in this file will be lost! + +import sys +from PyQt4 import QtCore, QtGui + +class Ui_MainWindow(object): + def setupUi(self, MainWindow): + MainWindow.setObjectName("MainWindow") + MainWindow.resize(QtCore.QSize(QtCore.QRect(0,0,885,631).size()).expandedTo(MainWindow.minimumSizeHint())) + + self.centralwidget = QtGui.QWidget(MainWindow) + self.centralwidget.setObjectName("centralwidget") + + self.gridlayout = QtGui.QGridLayout(self.centralwidget) + self.gridlayout.setMargin(9) + self.gridlayout.setSpacing(6) + self.gridlayout.setObjectName("gridlayout") + + self.vboxlayout = QtGui.QVBoxLayout() + self.vboxlayout.setMargin(0) + self.vboxlayout.setSpacing(6) + self.vboxlayout.setObjectName("vboxlayout") + + self.hboxlayout = QtGui.QHBoxLayout() + self.hboxlayout.setMargin(0) + self.hboxlayout.setSpacing(6) + self.hboxlayout.setObjectName("hboxlayout") + + self.clear = QtGui.QToolButton(self.centralwidget) + self.clear.setObjectName("clear") + self.hboxlayout.addWidget(self.clear) + + self.search = QtGui.QLineEdit(self.centralwidget) + self.search.setAcceptDrops(False) + self.search.setAutoFillBackground(False) + self.search.setFrame(True) + self.search.setObjectName("search") + self.hboxlayout.addWidget(self.search) + self.vboxlayout.addLayout(self.hboxlayout) + + self.listView = QtGui.QListView(self.centralwidget) + + sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Policy(5),QtGui.QSizePolicy.Policy(7)) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.listView.sizePolicy().hasHeightForWidth()) + self.listView.setSizePolicy(sizePolicy) + self.listView.setFrameShadow(QtGui.QFrame.Plain) + self.listView.setAutoScroll(False) + self.listView.setObjectName("listView") + self.vboxlayout.addWidget(self.listView) + self.gridlayout.addLayout(self.vboxlayout,0,1,1,1) + + self.vboxlayout1 = QtGui.QVBoxLayout() + self.vboxlayout1.setMargin(0) + self.vboxlayout1.setSpacing(6) + self.vboxlayout1.setObjectName("vboxlayout1") + + self.treeView = QtGui.QTreeView(self.centralwidget) + self.treeView.setFrameShadow(QtGui.QFrame.Plain) + self.treeView.setObjectName("treeView") + self.vboxlayout1.addWidget(self.treeView) + + self.df = QtGui.QLabel(self.centralwidget) + self.df.setObjectName("df") + self.vboxlayout1.addWidget(self.df) + self.gridlayout.addLayout(self.vboxlayout1,0,0,1,1) + MainWindow.setCentralWidget(self.centralwidget) + + self.statusbar = QtGui.QStatusBar(MainWindow) + self.statusbar.setObjectName("statusbar") + MainWindow.setStatusBar(self.statusbar) + + self.toolBar = QtGui.QToolBar(MainWindow) + self.toolBar.setOrientation(QtCore.Qt.Horizontal) + self.toolBar.setObjectName("toolBar") + MainWindow.addToolBar(self.toolBar) + + self.retranslateUi(MainWindow) + QtCore.QMetaObject.connectSlotsByName(MainWindow) + + def retranslateUi(self, MainWindow): + MainWindow.setWindowTitle(QtGui.QApplication.translate("MainWindow", "MainWindow", None, QtGui.QApplication.UnicodeUTF8)) + self.clear.setText(QtGui.QApplication.translate("MainWindow", "...", None, QtGui.QApplication.UnicodeUTF8)) + self.search.setText(QtGui.QApplication.translate("MainWindow", "Search title and author", None, QtGui.QApplication.UnicodeUTF8)) + self.df.setText(QtGui.QApplication.translate("MainWindow", "TextLabel", None, QtGui.QApplication.UnicodeUTF8)) + diff --git a/libprs500/terminfo.py b/libprs500/terminfo.py index 58ab6eb7d7..7b5e8fba6c 100644 --- a/libprs500/terminfo.py +++ b/libprs500/terminfo.py @@ -1,3 +1,17 @@ +## 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 sys, re class TerminalController: diff --git a/scripts/prs500.py b/scripts/prs500.py index aab445387b..1395bf878e 100755 --- a/scripts/prs500.py +++ b/scripts/prs500.py @@ -1,4 +1,18 @@ #!/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. """ Provides a command-line interface to the SONY Reader PRS-500. @@ -11,7 +25,7 @@ from optparse import OptionParser from libprs500 import VERSION from libprs500.communicate import PRS500Device from libprs500.terminfo import TerminalController -from libprs500.errors import ArgumentError +from libprs500.errors import ArgumentError, DeviceError MINIMUM_COL_WIDTH = 12 #: Minimum width of columns in ls output @@ -170,17 +184,29 @@ def main(): parser.add_option("--log-packets", help="print out packet stream to stdout. "+\ "The numbers in the left column are byte offsets that allow the packet size to be read off easily.", dest="log_packets", action="store_true", default=False) + parser.add_option("--gui", help="Run a Graphical User Interface", dest="gui", action="store_true", default=False) parser.remove_option("-h") parser.disable_interspersed_args() # Allow unrecognized options options, args = parser.parse_args() + if options.gui: + try: + from PyQt4.Qt import QApplication, QMainWindow + from libprs500.gui.main import MainWindow + app = QApplication(args) + window = QMainWindow() + mw = MainWindow(window, options.log_packets) + sys.exit(app.exec_()) + except ImportError, e: + print >>sys.stderr, "You dont have PyQt4 installed:", e + sys.exit(1) if len(args) < 1: parser.print_help() - sys.exit(1) + sys.exit(1) + command = args[0] args = args[1:] dev = PRS500Device(log_packets=options.log_packets) - dev.open() try: if command == "df": data = dev.available_space() @@ -190,7 +216,9 @@ def main(): str(0 if datum[2]==0 else int(100*(datum[2]-datum[1])/(datum[2]*1.)))+"%" print "%-10s\t%s\t%s\t%s\t%s"%(datum[0], total, used, free, percent) elif command == "books": - main, card = dev.books() + main, card, d1, d2 = dev.books() + d1.close() + d2.close() print "Books in main memory:" for book in main: print book @@ -215,7 +243,6 @@ def main(): if len(args) != 1: parser.print_help() sys.exit(1) - dev.open() print ls(dev, args[0], term, color=options.color, recurse=options.recurse, ll=options.ll, human_readable_size=options.hrs, cols=cols), elif command == "info": info(dev) @@ -289,7 +316,7 @@ def main(): parser.print_help() if dev.handle: dev.close() sys.exit(1) - except ArgumentError, e: + except (ArgumentError, DeviceError), e: print >>sys.stderr, e sys.exit(1) finally: diff --git a/setup.py b/setup.py index bcf8f5a059..a51af9ab4d 100644 --- a/setup.py +++ b/setup.py @@ -1,3 +1,17 @@ +## 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. #!/usr/bin/env python from distutils.core import setup from libprs500 import VERSION @@ -7,14 +21,27 @@ setup(name='libprs500', 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. + libprs500 is library to interface with the _Sony Portable Reader:http://Sony.com/reader 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. + copy files from and to the device. It also provides a command line and a graphical user interface via the script prs500.py. """, author='Kovid Goyal', author_email='kovid@kovidgoyal.net', provides=['libprs500'], requires=['pyusb'], - packages = ['libprs500'], - scripts = ['scripts/prs500.py'] + packages = ['libprs500', 'libprs500.gui'], + scripts = ['scripts/prs500.py'], + classifiers = [ + 'Development Status :: 3 - Alpha', + 'Environment :: Console', + 'Environment :: X11 Applications :: Qt', + 'Intended Audience :: Developers', + 'Intended Audience :: End Users/Desktop', + 'License :: OSI Approved :: GNU General Public License (GPL)', + 'Natural Language :: English', + 'Operating System :: POSIX :: Linux', + 'Programming Language :: Python', + 'Topic :: Software Development :: Libraries :: Python Modules', + 'Topic :: System :: Hardware :: Hardware Drivers' + ] )