diff --git a/src/calibre/devices/smart_device_app/driver.py b/src/calibre/devices/smart_device_app/driver.py
index ba6a959c0e..903cd4cd56 100644
--- a/src/calibre/devices/smart_device_app/driver.py
+++ b/src/calibre/devices/smart_device_app/driver.py
@@ -89,6 +89,7 @@ class SMART_DEVICE_APP(DeviceConfig, DevicePlugin):
SEND_NOOP_EVERY_NTH_PROBE = 5
DISCONNECT_AFTER_N_SECONDS = 30*60 # 30 minutes
+
opcodes = {
'NOOP' : 12,
'OK' : 0,
@@ -163,11 +164,13 @@ class SMART_DEVICE_APP(DeviceConfig, DevicePlugin):
OPT_COLLECTIONS = 8
OPT_AUTODISCONNECT = 10
+
def __init__(self, path):
self.sync_lock = threading.RLock()
self.noop_counter = 0
self.debug_start_time = time.time()
self.debug_time = time.time()
+ self.client_can_stream_books = False
def _debug(self, *args):
if not DEBUG:
@@ -343,7 +346,7 @@ class SMART_DEVICE_APP(DeviceConfig, DevicePlugin):
# recv seems to return a pointer into some internal buffer.
# Things get trashed if we don't make a copy of the data.
self.device_socket.settimeout(self.MAX_CLIENT_COMM_TIMEOUT)
- v = self.device_socket.recv(self.BASE_PACKET_LEN)
+ v = self.device_socket.recv(2)
self.device_socket.settimeout(None)
if len(v) == 0:
return '' # documentation says the socket is broken permanently.
@@ -382,7 +385,7 @@ class SMART_DEVICE_APP(DeviceConfig, DevicePlugin):
raise
time.sleep(0.1) # lets not hammer the OS too hard
- def _call_client(self, op, arg, print_debug_info=True):
+ def _call_client(self, op, arg, print_debug_info=True, wait_for_response=True):
if op != 'NOOP':
self.noop_counter = 0
extra_debug = self.settings().extra_customization[self.OPT_EXTRA_DEBUG]
@@ -398,7 +401,28 @@ class SMART_DEVICE_APP(DeviceConfig, DevicePlugin):
if print_debug_info and extra_debug:
self._debug('send string', s)
self.device_socket.settimeout(self.MAX_CLIENT_COMM_TIMEOUT)
- self._send_byte_string((b'%d' % len(s))+s)
+ self._send_byte_string((b'%d' % len(s)) + s)
+ if not wait_for_response:
+ return None, None
+ return self._receive_from_client(print_debug_info=print_debug_info)
+ except socket.timeout:
+ self._debug('timeout communicating with device')
+ self._close_device_socket()
+ raise TimeoutError('Device did not respond in reasonable time')
+ except socket.error:
+ self._debug('device went away')
+ self._close_device_socket()
+ raise ControlError(desc='Device closed the network connection')
+ except:
+ self._debug('other exception')
+ traceback.print_exc()
+ self._close_device_socket()
+ raise
+ raise ControlError(desc='Device responded with incorrect information')
+
+ def _receive_from_client(self, print_debug_info=True):
+ extra_debug = self.settings().extra_customization[self.OPT_EXTRA_DEBUG]
+ try:
v = self._read_string_from_net()
self.device_socket.settimeout(None)
if print_debug_info and extra_debug:
@@ -434,9 +458,13 @@ class SMART_DEVICE_APP(DeviceConfig, DevicePlugin):
book_metadata.size = length
infile.seek(0)
self._debug(lpath, length)
- self._call_client('SEND_BOOK', {'lpath': lpath, 'length': length,
+ opcode, result = self._call_client('SEND_BOOK', {'lpath': lpath, 'length': length,
'metadata': book_metadata, 'thisBook': this_book,
- 'totalBooks': total_books}, print_debug_info=False)
+ 'totalBooks': total_books,
+ 'willStreamBooks': self.client_can_stream_books},
+ print_debug_info=False,
+ wait_for_response=(not self.client_can_stream_books))
+
self._set_known_metadata(book_metadata)
pos = 0
failed = False
@@ -449,9 +477,10 @@ class SMART_DEVICE_APP(DeviceConfig, DevicePlugin):
b = b64encode(b)
opcode, result = self._call_client('BOOK_DATA',
{'lpath': lpath, 'position': pos, 'data': b},
- print_debug_info=False)
+ print_debug_info=False,
+ wait_for_response=(not self.client_can_stream_books))
pos += blen
- if opcode != 'OK':
+ if not self.client_can_stream_books and opcode != 'OK':
self._debug('protocol error', opcode)
failed = True
break
@@ -466,6 +495,10 @@ class SMART_DEVICE_APP(DeviceConfig, DevicePlugin):
return self.OPT_PASSWORD
elif opt_string == 'autostart':
return self.OPT_AUTOSTART
+ elif opt_string == 'use_fixed_port':
+ return self.OPT_USE_PORT
+ elif opt_string == 'port_number':
+ return self.OPT_PORT_NUMBER
else:
return None
@@ -510,6 +543,19 @@ class SMART_DEVICE_APP(DeviceConfig, DevicePlugin):
self.device_socket = None
self.is_connected = False
+ def _attach_to_port(self, port):
+ try:
+ self._debug('try port', port)
+ self.listen_socket.bind(('', port))
+ except socket.error:
+ self._debug('socket error on port', port)
+ port = 0
+ except:
+ self._debug('Unknown exception while allocating listen socket')
+ traceback.print_exc()
+ raise
+ return port
+
# The public interface methods.
@synchronous('sync_lock')
@@ -560,7 +606,7 @@ class SMART_DEVICE_APP(DeviceConfig, DevicePlugin):
if attempts >= self.MAX_UNSUCCESSFUL_CONNECTS:
self._debug('too many connection attempts from', peer)
self._close_device_socket()
- raise InitialConnectionError(_('Too many connection attempts from %s')%peer)
+ raise InitialConnectionError(_('Too many connection attempts from %s') % peer)
else:
self.connection_attempts[peer] = attempts + 1
except InitialConnectionError:
@@ -619,6 +665,8 @@ class SMART_DEVICE_APP(DeviceConfig, DevicePlugin):
self._close_device_socket()
return False
self._debug('CC version #:', result.get('ccVersionNumber', 'unknown'))
+ self.client_can_stream_books = result.get('canStreamBooks', False)
+ self._debug('CC can stream', self.client_can_stream_books);
self.max_book_packet_len = result.get('maxBookContentPacketLen',
self.BASE_PACKET_LEN)
exts = result.get('acceptedExtensions', None)
@@ -725,12 +773,16 @@ class SMART_DEVICE_APP(DeviceConfig, DevicePlugin):
self._debug(oncard)
if oncard is not None:
return CollectionsBookList(None, None, None)
- opcode, result = self._call_client('GET_BOOK_COUNT', {})
+ opcode, result = self._call_client('GET_BOOK_COUNT', {'canStream':True})
bl = CollectionsBookList(None, self.PREFIX, self.settings)
if opcode == 'OK':
count = result['count']
+ will_stream = 'willStream' in result;
for i in range(0, count):
- opcode, result = self._call_client('GET_BOOK_METADATA', {'index': i},
+ if will_stream:
+ opcode, result = self._receive_from_client(print_debug_info=False)
+ else:
+ opcode, result = self._call_client('GET_BOOK_METADATA', {'index': i},
print_debug_info=False)
if opcode == 'OK':
if '_series_sort_' in result:
@@ -760,11 +812,10 @@ class SMART_DEVICE_APP(DeviceConfig, DevicePlugin):
# probably need to send two booklists, one with calibre's data that is
# given back by "books", and one that has been plugboarded.
self._call_client('SEND_BOOKLISTS', { 'count': len(booklists[0]),
- 'collections': coldict} )
+ 'collections': coldict})
for i,book in enumerate(booklists[0]):
if not self._metadata_already_on_device(book):
self._set_known_metadata(book)
- self._debug('syncing book', book.lpath)
opcode, result = self._call_client('SEND_BOOK_METADATA',
{'index': i, 'data': book},
print_debug_info=False)
@@ -802,19 +853,19 @@ class SMART_DEVICE_APP(DeviceConfig, DevicePlugin):
paths.append((lpath, length))
# No need to deal with covers. The client will get the thumbnails
# in the mi structure
- self.report_progress((i+1) / float(len(files)), _('Transferring books to device...'))
+ self.report_progress((i + 1) / float(len(files)), _('Transferring books to device...'))
self.report_progress(1.0, _('Transferring books to device...'))
- self._debug('finished uploading %d books'%(len(files)))
+ self._debug('finished uploading %d books' % (len(files)))
return paths
@synchronous('sync_lock')
def add_books_to_metadata(self, locations, metadata, booklists):
- self._debug('adding metadata for %d books'%(len(metadata)))
+ self._debug('adding metadata for %d books' % (len(metadata)))
metadata = iter(metadata)
for i, location in enumerate(locations):
- self.report_progress((i+1) / float(len(locations)),
+ self.report_progress((i + 1) / float(len(locations)),
_('Adding books to device metadata listing...'))
info = metadata.next()
lpath = location[0]
@@ -846,14 +897,14 @@ class SMART_DEVICE_APP(DeviceConfig, DevicePlugin):
self._debug(paths)
for i, path in enumerate(paths):
path = self._strip_prefix(path)
- self.report_progress((i+1) / float(len(paths)), _('Removing books from device metadata listing...'))
+ self.report_progress((i + 1) / float(len(paths)), _('Removing books from device metadata listing...'))
for bl in booklists:
for book in bl:
if path == book.path:
bl.remove_book(book)
self._set_known_metadata(book, remove=True)
self.report_progress(1.0, _('Removing books from device metadata listing...'))
- self._debug('finished removing metadata for %d books'%(len(paths)))
+ self._debug('finished removing metadata for %d books' % (len(paths)))
@synchronous('sync_lock')
@@ -864,7 +915,7 @@ class SMART_DEVICE_APP(DeviceConfig, DevicePlugin):
while not eof:
opcode, result = self._call_client('GET_BOOK_FILE_SEGMENT',
{'lpath' : path, 'position': position},
- print_debug_info=False )
+ print_debug_info=False)
if opcode == 'OK':
if not result['eof']:
data = b64decode(result['data'])
@@ -904,57 +955,71 @@ class SMART_DEVICE_APP(DeviceConfig, DevicePlugin):
self.max_book_packet_len = 0
self.noop_counter = 0
self.connection_attempts = {}
+ self.client_can_stream_books = False
+
+ message = None
try:
self.listen_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
except:
- self._debug('creation of listen socket failed')
- return
+ message = 'creation of listen socket failed'
+ self._debug(message)
+ return message
i = 0
- while i < 100: # try up to 100 random port numbers
- if self.settings().extra_customization[self.OPT_USE_PORT]:
- i = 100
- try:
- port = int(self.settings().extra_customization[self.OPT_PORT_NUMBER])
- except:
- port = 0
- else:
- i += 1
- port = random.randint(8192, 32000)
+
+ if self.settings().extra_customization[self.OPT_USE_PORT]:
try:
- self._debug('try port', port)
- self.listen_socket.bind(('', port))
- break
- except socket.error:
- port = 0
+ opt_port = int(self.settings().extra_customization[self.OPT_PORT_NUMBER])
except:
- self._debug('Unknown exception while allocating listen socket')
- traceback.print_exc()
- raise
- if port == 0:
- self._debug('Failed to allocate a port');
- self.listen_socket.close()
- self.listen_socket = None
- self.is_connected = False
- return
+ message = _('Invalid port in options: %s')% \
+ self.settings().extra_customization[self.OPT_PORT_NUMBER]
+ self.debug(message)
+ self.listen_socket.close()
+ self.listen_socket = None
+ self.is_connected = False
+ return message
+
+ port = self._attach_to_port(opt_port)
+ if port == 0:
+ message = 'Failed to connect to port %d'%opt_port
+ self._debug(message);
+ self.listen_socket.close()
+ self.listen_socket = None
+ self.is_connected = False
+ return message
+ else:
+ while i < 100: # try up to 100 random port numbers
+ i += 1
+ port = self._attach_to_port(random.randint(8192, 32000))
+ if port != 0:
+ break;
+ if port == 0:
+ message = _('Failed to allocate a random port')
+ self._debug(message);
+ self.listen_socket.close()
+ self.listen_socket = None
+ self.is_connected = False
+ return message
try:
self.listen_socket.listen(0)
except:
- self._debug('listen on socket failed', port)
+ message = 'listen on port %d failed' % port
+ self._debug(message)
self.listen_socket.close()
self.listen_socket = None
self.is_connected = False
- return
+ return message
try:
do_zeroconf(publish_zeroconf, port)
except:
- self._debug('registration with bonjour failed')
+ message = 'registration with bonjour failed'
+ self._debug(message)
self.listen_socket.close()
self.listen_socket = None
self.is_connected = False
- return
+ return message
self._debug('listening on port', port)
self.port = port
@@ -975,7 +1040,7 @@ class SMART_DEVICE_APP(DeviceConfig, DevicePlugin):
@synchronous('sync_lock')
def start_plugin(self):
- self.startup_on_demand()
+ return self.startup_on_demand()
@synchronous('sync_lock')
def stop_plugin(self):
diff --git a/src/calibre/gui2/actions/device.py b/src/calibre/gui2/actions/device.py
index 1151d1976a..d978f9723f 100644
--- a/src/calibre/gui2/actions/device.py
+++ b/src/calibre/gui2/actions/device.py
@@ -238,11 +238,20 @@ class ConnectShareAction(InterfaceAction):
def set_smartdevice_action_state(self):
from calibre.utils.mdns import get_external_ip
- running = self.gui.device_manager.is_running('smartdevice')
+ dm = self.gui.device_manager
+
+ running = dm.is_running('smartdevice')
if not running:
text = self.share_conn_menu.DEVICE_MSGS[0]
else:
- text = self.share_conn_menu.DEVICE_MSGS[1] + ' [%s]'%get_external_ip()
+ use_fixed_port = dm.get_option('smartdevice', 'use_fixed_port')
+ port_number = dm.get_option('smartdevice', 'port_number')
+ if use_fixed_port:
+ text = self.share_conn_menu.DEVICE_MSGS[1] + ' [%s port %s]'%(
+ get_external_ip(), port_number)
+ else:
+ text = self.share_conn_menu.DEVICE_MSGS[1] + ' [%s]'%get_external_ip()
+
icon = 'green' if running else 'red'
ac = self.share_conn_menu.control_smartdevice_action
ac.setIcon(QIcon(I('dot_%s.png'%icon)))
diff --git a/src/calibre/gui2/device.py b/src/calibre/gui2/device.py
index a029e0be8f..e319e95535 100644
--- a/src/calibre/gui2/device.py
+++ b/src/calibre/gui2/device.py
@@ -554,7 +554,7 @@ class DeviceManager(Thread): # {{{
# will switch to the device thread before calling the plugin.
def start_plugin(self, name):
- self._call_request(name, 'start_plugin')
+ return self._call_request(name, 'start_plugin')
def stop_plugin(self, name):
self._call_request(name, 'stop_plugin')
diff --git a/src/calibre/gui2/dialogs/smartdevice.py b/src/calibre/gui2/dialogs/smartdevice.py
index 35e6bdf3a0..b83db2bd18 100644
--- a/src/calibre/gui2/dialogs/smartdevice.py
+++ b/src/calibre/gui2/dialogs/smartdevice.py
@@ -7,6 +7,7 @@ __copyright__ = '2008, Kovid Goyal
' + + _('Check this box if you want calibre to use a fixed network ' + 'port. Normally you will not need to do this. However, if ' + 'your device consistently fails to connect to calibre, ' + 'try checking this box.') + '
') + + self.fixed_port.setToolTip('' + + _('A port number must be a 4-digit integer less than 32,000. No ' + 'two network applications on the same computer can use ' + 'the same port number. If calibre says that it fails to connect ' + 'to the port, try a different number.') + '
') + self.show_password.stateChanged[int].connect(self.toggle_password) + self.use_fixed_port.stateChanged[int].connect(self.use_fixed_port_changed) self.device_manager = parent.device_manager @@ -37,8 +52,22 @@ class SmartdeviceDialog(QDialog, Ui_Dialog): pw = self.device_manager.get_option('smartdevice', 'password') if pw: self.password_box.setText(pw) + + use_fixed_port = self.device_manager.get_option('smartdevice', 'use_fixed_port') + port_number = self.device_manager.get_option('smartdevice', 'port_number') + self.fixed_port.setText(port_number) + self.use_fixed_port.setChecked(use_fixed_port); + if not use_fixed_port: + self.fixed_port.setEnabled(False); + + if pw: + self.password_box.setText(pw) + self.resize(self.sizeHint()) + def use_fixed_port_changed(self, state): + self.fixed_port.setEnabled(state == Qt.Checked) + def toggle_password(self, state): self.password_box.setEchoMode(QLineEdit.Password if state == Qt.Unchecked else QLineEdit.Normal) @@ -48,6 +77,16 @@ class SmartdeviceDialog(QDialog, Ui_Dialog): unicode(self.password_box.text())) self.device_manager.set_option('smartdevice', 'autostart', self.autostart_box.isChecked()) - self.device_manager.start_plugin('smartdevice') - QDialog.accept(self) + self.device_manager.set_option('smartdevice', 'use_fixed_port', + self.use_fixed_port.isChecked()) + self.device_manager.set_option('smartdevice', 'port_number', + unicode(self.fixed_port.text())) + + message = self.device_manager.start_plugin('smartdevice') + + if not self.device_manager.is_running('smartdevice'): + error_dialog(self, _('Problem starting smartdevice'), + _('The snart device driver did not start. It said "%s"')%message, show=True) + else: + QDialog.accept(self) diff --git a/src/calibre/gui2/dialogs/smartdevice.ui b/src/calibre/gui2/dialogs/smartdevice.ui index 830c79b787..60f1c07be4 100644 --- a/src/calibre/gui2/dialogs/smartdevice.ui +++ b/src/calibre/gui2/dialogs/smartdevice.ui @@ -71,6 +71,36 @@ +