initial implementation of pairing

This commit is contained in:
Daniel Pavel 2012-10-22 10:03:16 +03:00
parent f2dac70131
commit b10ade4430
18 changed files with 554 additions and 293 deletions

103
app/pairing.py Normal file
View File

@ -0,0 +1,103 @@
#
#
#
from logging import getLogger as _Logger
from receiver import DeviceInfo as _DeviceInfo
from logitech.devices.constants import (STATUS, NAMES)
_l = _Logger('pairing')
class State(object):
TICK = 300
PAIR_TIMEOUT = 60 * 1000 / TICK
def __init__(self, watcher):
self._watcher = watcher
self.reset()
def reset(self):
self.success = None
self.detected_device = None
self._countdown = self.PAIR_TIMEOUT
def countdown(self, assistant):
if self._countdown == self.PAIR_TIMEOUT:
self.start_scan()
self._countdown -= 1
return True
self._countdown -= 1
if self._countdown > 0 and self.success is None:
return True
self.stop_scan()
assistant.scan_complete(assistant, self.detected_device)
return False
def start_scan(self):
self.reset()
self._watcher.receiver.events_filter = self.filter_events
reply = self._watcher.receiver.request(0xFF, b'\x80\xB2', b'\x01')
_l.debug("start scan reply %s", repr(reply))
def stop_scan(self):
if self._countdown >= 0:
self._countdown = -1
reply = self._watcher.receiver.request(0xFF, b'\x80\xB2', b'\x02')
_l.debug("stop scan reply %s", repr(reply))
self._watcher.receiver.events_filter = None
def filter_events(self, event):
if event.devnumber == 0xFF:
if event.code == 0x10:
if event.data == b'\x4A\x01\x00\x00\x00':
_l.debug("receiver listening for device wakeup")
return True
if event.data == b'\x4A\x00\x01\x00\x00':
_l.debug("receiver gave up")
self.success = False
return True
return False
if event.devnumber in self._watcher.receiver.devices:
return False
_l.debug("event for new device? %s", event)
if event.code == 0x10 and event.data[0:2] == b'\x41\x04':
state_code = ord(event.data[2:3]) & 0xF0
state = STATUS.UNAVAILABLE if state_code == 0x60 else \
STATUS.CONNECTED if state_code == 0xA0 else \
STATUS.CONNECTED if state_code == 0x20 else \
None
if state is None:
_l.warn("don't know how to handle status 0x%02x: %s", state_code, event)
elif event.devnumber < 1 or event.devnumber > self.max_devices:
_l.warn("got event for invalid device number %d: %s", event.devnumber, event)
else:
dev = _DeviceInfo(self._watcher.receiver, event.devnumber, state)
if state == STATUS.CONNECTED:
n, k = dev.name, dev.kind
_l.debug("detected active device %s", dev)
else:
# we can query the receiver for the device short name
dev_id = self.request(0xFF, b'\x83\xB5', event.data[4:5])
if dev_id:
shortname = str(dev_id[2:].rstrip(b'\x00'))
if shortname in NAMES:
dev._name, dev._kind = NAMES[shortname]
_l.debug("detected new device %s", dev)
else:
_l.warn("could not properly detect inactive device %d: %s", event.devnumber, shortname)
self.detected_device = dev
return True
def unpair(receiver, devnumber):
reply = receiver.request(0xFF, b'\x80\xB2', b'\x03' + chr(devnumber))
_l.debug("unpair %d reply %s", devnumber, repr(reply))

View File

@ -66,7 +66,7 @@ class DeviceInfo(object):
def name(self): def name(self):
if self._name is None: if self._name is None:
if self._status >= STATUS.CONNECTED: if self._status >= STATUS.CONNECTED:
self._name = self.receiver.request(_api.get_device_name, self.number, self.features) self._name = self.receiver.call_api(_api.get_device_name, self.number, self.features)
return self._name or '?' return self._name or '?'
@property @property
@ -77,25 +77,25 @@ class DeviceInfo(object):
def kind(self): def kind(self):
if self._kind is None: if self._kind is None:
if self._status >= STATUS.CONNECTED: if self._status >= STATUS.CONNECTED:
self._kind = self.receiver.request(_api.get_device_kind, self.number, self.features) self._kind = self.receiver.call_api(_api.get_device_kind, self.number, self.features)
return self._kind or '?' return self._kind or '?'
@property @property
def firmware(self): def firmware(self):
if self._firmware is None: if self._firmware is None:
if self._status >= STATUS.CONNECTED: if self._status >= STATUS.CONNECTED:
self._firmware = self.receiver.request(_api.get_device_firmware, self.number, self.features) self._firmware = self.receiver.call_api(_api.get_device_firmware, self.number, self.features)
return self._firmware or () return self._firmware or ()
@property @property
def features(self): def features(self):
if self._features is None: if self._features is None:
if self._status >= STATUS.CONNECTED: if self._status >= STATUS.CONNECTED:
self._features = self.receiver.request(_api.get_device_features, self.number) self._features = self.receiver.call_api(_api.get_device_features, self.number)
return self._features or () return self._features or ()
def ping(self): def ping(self):
return self.receiver.request(_api.ping, self.number) return self.receiver.call_api(_api.ping, self.number)
def process_event(self, code, data): def process_event(self, code, data):
if code == 0x10 and data[:1] == b'\x8F': if code == 0x10 and data[:1] == b'\x8F':
@ -155,12 +155,11 @@ class Receiver(_listener.EventsListener):
self.LOG.info("initializing") self.LOG.info("initializing")
self.devices = {} self.devices = {}
self.events_filter = None
self.events_handler = None self.events_handler = None
init = (_base.request(handle, 0xFF, b'\x81\x00') and if (_base.request(handle, 0xFF, b'\x81\x00') and
_base.request(handle, 0xFF, b'\x80\x00', b'\x00\x01') and _base.request(handle, 0xFF, b'\x80\x00', b'\x00\x01')):
_base.request(handle, 0xFF, b'\x81\x02'))
if init:
self.LOG.info("initialized") self.LOG.info("initialized")
else: else:
self.LOG.warn("initialization failed") self.LOG.warn("initialization failed")
@ -210,12 +209,18 @@ class Receiver(_listener.EventsListener):
def device_name(self): def device_name(self):
return self.NAME return self.NAME
def count_devices(self):
return self.call_api(_api.count_devices)
def _device_changed(self, dev, urgent=False): def _device_changed(self, dev, urgent=False):
self.status_changed.reason = dev self.status_changed.reason = dev
self.status_changed.urgent = urgent self.status_changed.urgent = urgent
self.status_changed.set() self.status_changed.set()
def _events_handler(self, event): def _events_handler(self, event):
if self.events_filter and self.events_filter(event):
return
if event.code == 0x10 and event.data[0:2] == b'\x41\x04': if event.code == 0x10 and event.data[0:2] == b'\x41\x04':
state_code = ord(event.data[2:3]) & 0xF0 state_code = ord(event.data[2:3]) & 0xF0
state = STATUS.UNAVAILABLE if state_code == 0x60 else \ state = STATUS.UNAVAILABLE if state_code == 0x60 else \
@ -239,7 +244,7 @@ class Receiver(_listener.EventsListener):
n, k = dev.name, dev.kind n, k = dev.name, dev.kind
else: else:
# we can query the receiver for the device short name # we can query the receiver for the device short name
dev_id = self.request(_base.request, 0xFF, b'\x83\xB5', event.data[4:5]) dev_id = self.request(0xFF, b'\x83\xB5', event.data[4:5])
if dev_id: if dev_id:
shortname = str(dev_id[2:].rstrip(b'\x00')) shortname = str(dev_id[2:].rstrip(b'\x00'))
if shortname in NAMES: if shortname in NAMES:
@ -258,14 +263,15 @@ class Receiver(_listener.EventsListener):
self.devices = {} self.devices = {}
self.status = STATUS.UNAVAILABLE self.status = STATUS.UNAVAILABLE
return return
self.LOG.warn("don't know how to handle event %s", event)
elif event.devnumber in self.devices: elif event.devnumber in self.devices:
dev = self.devices[event.devnumber] dev = self.devices[event.devnumber]
if dev.process_event(event.code, event.data): if dev.process_event(event.code, event.data):
return return
if self.events_handler: if self.events_handler and self.events_handler(event):
self.events_handler(event) return
self.LOG.warn("don't know how to handle event %s", event)
def __str__(self): def __str__(self):
return 'Receiver(%s,%x,%d:%d)' % (self.path, self._handle, self._active, self._status) return 'Receiver(%s,%x,%d:%d)' % (self.path, self._handle, self._active, self._status)

View File

@ -1,17 +1,19 @@
#!/usr/bin/env python #!/usr/bin/env python
__author__ = "Daniel Pavel <daniel.pavel@gmail.com>"
__version__ = '0.5' __version__ = '0.5'
__license__ = "GPL"
# #
# #
# #
APP_TITLE = 'Solaar' APPNAME = 'Solaar'
if __name__ == '__main__': def _parse_arguments():
import argparse import argparse
arg_parser = argparse.ArgumentParser(prog=APP_TITLE) arg_parser = argparse.ArgumentParser(prog=APPNAME.lower())
arg_parser.add_argument('-v', '--verbose', arg_parser.add_argument('-v', '--verbose',
action='count', default=0, action='count', default=0,
help='increase the logger verbosity (may be repeated)') help='increase the logger verbosity (may be repeated)')
@ -31,59 +33,50 @@ if __name__ == '__main__':
import logging import logging
log_level = logging.root.level - 10 * args.verbose log_level = logging.root.level - 10 * args.verbose
log_format='%(asctime)s.%(msecs)03d %(levelname)8s [%(threadName)s] %(name)s: %(message)s' log_format='%(asctime)s.%(msecs)03d %(levelname)8s [%(threadName)s] %(name)s: %(message)s'
logging.basicConfig(level=log_level if log_level > 0 else 1, format=log_format, datefmt='%H:%M:%S') logging.basicConfig(level=log_level if log_level > 0 else 1,
format=log_format,
datefmt='%H:%M:%S')
from gi.repository import GObject return args
GObject.threads_init()
if __name__ == '__main__':
args = _parse_arguments()
import ui import ui
# check if the notifications are available
args.notifications &= args.systray args.notifications &= args.systray
if args.notifications: if ui.notify.init(APPNAME):
args.notifications &= ui.notify.init(APP_TITLE) ui.action.toggle_notifications.set_active(args.notifications)
else:
ui.action.toggle_notifications = None
import watcher import watcher
tray_icon = None
window = ui.window.create(APP_TITLE,
watcher.DUMMY.NAME,
watcher.DUMMY.max_devices,
args.systray)
window.set_icon_name(APP_TITLE + '-init')
def _ui_update(receiver, tray_icon, window): window = ui.main_window.create(APPNAME,
icon_name = APP_TITLE + '-fail' if receiver.status < 1 else APP_TITLE watcher.DUMMY.NAME,
if window: watcher.DUMMY.max_devices,
GObject.idle_add(ui.window.update, window, receiver, icon_name) args.systray)
if tray_icon:
GObject.idle_add(ui.icon.update, tray_icon, receiver, icon_name)
def _notify(device):
GObject.idle_add(ui.notify.show, device)
w = watcher.Watcher(APP_TITLE,
lambda r: _ui_update(r, tray_icon, window),
_notify if args.notifications else None)
w.start()
if args.systray: if args.systray:
def _toggle_notifications(item): menu_actions = (ui.action.pair,
# logging.debug("toggle notifications %s", item) ui.action.toggle_notifications,
if ui.notify.available: ui.action.about)
if item.get_active(): icon = ui.status_icon.create(window, menu_actions)
ui.notify.init(APP_TITLE)
else:
ui.notify.uninit()
item.set_sensitive(ui.notify.available)
menu = (
('Notifications', _toggle_notifications if args.notifications else None, args.notifications),
)
tray_icon = ui.icon.create(APP_TITLE, (ui.window.toggle, window), menu)
tray_icon.set_from_icon_name(APP_TITLE + '-init')
else: else:
icon = None
window.present() window.present()
w = watcher.Watcher(APPNAME,
lambda r: ui.update(r, icon, window),
ui.notify.show if ui.notify.available else None)
w.start()
import pairing
ui.action.pair.connect('activate', ui.action._pair_device,
window, pairing.State(w))
from gi.repository import Gtk from gi.repository import Gtk
Gtk.main() Gtk.main()

View File

@ -1,3 +1,58 @@
# pass # pass
from . import (notify, icon, window, pair) APPNAME = 'Solaar'
APPVERSION = '0.5'
from . import (notify, status_icon, main_window, pair_window, action)
from gi.repository import (GObject, Gtk)
GObject.threads_init()
def appicon(receiver_status):
return (APPNAME + '-fail' if receiver_status < 0 else
APPNAME + '-init' if receiver_status < 1 else
APPNAME)
_THEME = Gtk.IconTheme.get_default()
def get_icon(name, fallback):
return name if name and _THEME.has_icon(name) else fallback
def icon_file(name):
if name and _THEME.has_icon(name):
return _THEME.lookup_icon(name, 0, 0).get_filename()
return None
def find_children(container, *child_names):
def _iterate_children(widget, names, result, count):
wname = widget.get_name()
if wname in names:
index = names.index(wname)
names[index] = None
result[index] = widget
count -= 1
if count > 0 and isinstance(widget, Gtk.Container):
for w in widget:
count = _iterate_children(w, names, result, count)
if count == 0:
break
return count
names = list(child_names)
count = len(names)
result = [None] * count
_iterate_children(container, names, result, count)
return tuple(result) if count > 1 else result[0]
def update(receiver, icon, window):
GObject.idle_add(action.pair.set_sensitive, receiver.status > 0)
if window:
GObject.idle_add(main_window.update, window, receiver)
if icon:
GObject.idle_add(status_icon.update, icon, receiver)

58
app/ui/action.py Normal file
View File

@ -0,0 +1,58 @@
from gi.repository import Gtk
import ui
def _action(name, label, function, *args):
action = Gtk.Action(name, label, label, None)
action.set_icon_name(name)
if function:
action.connect('activate', function, *args)
return action
def _toggle_action(name, label, function, *args):
action = Gtk.ToggleAction(name, label, label, None)
action.set_icon_name(name)
action.connect('activate', function, *args)
return action
#
#
#
def _toggle_notifications(action):
if action.get_active():
ui.notify.init(ui.APPNAME)
else:
ui.notify.uninit()
action.set_sensitive(ui.notify.available)
toggle_notifications = _toggle_action('notifications', 'Notifications', _toggle_notifications)
def _show_about_window(action):
about = Gtk.AboutDialog()
about.set_icon_name(ui.APPNAME)
about.set_program_name(ui.APPNAME)
about.set_logo_icon_name(ui.APPNAME)
about.set_version(ui.APPVERSION)
about.set_license_type(Gtk.License.GPL_2_0)
about.set_authors(('Daniel Pavel http://github.com/pwr', ))
about.set_website('http://github.com/pwr/Solaar/wiki')
about.run()
about.destroy()
about = _action('help-about', 'About ' + ui.APPNAME, _show_about_window)
def _pair_device(action, window, state):
action.set_sensitive(False)
pair_dialog = ui.pair_window.create(action, state)
# window.present()
# pair_dialog.set_transient_for(parent_window)
# pair_dialog.set_destroy_with_parent(parent_window)
# pair_dialog.set_modal(True)
pair_dialog.present()
pair = _action('add', 'Pair new device', None)

View File

@ -1,79 +0,0 @@
#
#
#
from gi.repository import Gtk
def create(title, click_action=None, actions=None):
icon = Gtk.StatusIcon()
icon.set_title(title)
icon.set_name(title)
if click_action:
if type(click_action) == tuple:
function = click_action[0]
args = click_action[1:]
icon.connect('activate', function, *args)
else:
icon.connect('activate', click_action)
menu = Gtk.Menu()
if actions:
for name, activate, checked in actions:
if checked is None:
item = Gtk.MenuItem(name)
if activate is None:
item.set_sensitive(False)
else:
item.connect('activate', activate)
else:
item = Gtk.CheckMenuItem(name)
if activate is None:
item.set_sensitive(False)
else:
item.set_active(checked or False)
item.connect('toggled', activate)
menu.append(item)
menu.append(Gtk.SeparatorMenuItem())
quit_item = Gtk.MenuItem('Quit')
quit_item.connect('activate', Gtk.main_quit)
menu.append(quit_item)
menu.show_all()
icon.connect('popup_menu',
lambda icon, button, time, menu:
menu.popup(None, None, icon.position_menu, icon, button, time),
menu)
return icon
def update(icon, receiver, icon_name=None):
if icon_name is not None:
icon.set_from_icon_name(icon_name)
if receiver.devices:
lines = []
if receiver.status < 1:
lines += (receiver.status_text, '')
devlist = [receiver.devices[d] for d in range(1, 1 + receiver.max_devices) if d in receiver.devices]
for dev in devlist:
name = '<b>' + dev.name + '</b>'
if dev.status < 1:
lines.append(name + ' (' + dev.status_text + ')')
else:
lines.append(name)
if dev.status > 1:
lines.append(' ' + dev.status_text)
lines.append('')
text = '\n'.join(lines).rstrip('\n')
icon.set_tooltip_markup(text)
else:
icon.set_tooltip_text(receiver.status_text)

View File

@ -4,6 +4,7 @@
from gi.repository import (Gtk, Gdk) from gi.repository import (Gtk, Gdk)
import ui
from logitech.devices.constants import (STATUS, PROPS) from logitech.devices.constants import (STATUS, PROPS)
@ -13,37 +14,10 @@ _STATUS_ICON_SIZE = Gtk.IconSize.DND
_PLACEHOLDER = '~' _PLACEHOLDER = '~'
theme = Gtk.IconTheme.get_default()
def _find_children(container, *child_names):
def _iterate_children(widget, names, result, count):
wname = widget.get_name()
if wname in names:
index = names.index(wname)
names[index] = None
result[index] = widget
count -= 1
if count > 0 and isinstance(widget, Gtk.Container):
for w in widget:
count = _iterate_children(w, names, result, count)
if count == 0:
break
return count
names = list(child_names)
count = len(names)
result = [None] * count
_iterate_children(container, names, result, count)
return result if count > 1 else result[0]
def _update_receiver_box(box, receiver): def _update_receiver_box(box, receiver):
label, buttons = _find_children(box, 'label', 'buttons') label, buttons = ui.find_children(box, 'label', 'buttons')
label.set_text(receiver.status_text or '') label.set_text(receiver.status_text or '')
buttons.set_visible(receiver.status >= STATUS.CONNECTED) # buttons.set_visible(receiver.status >= STATUS.CONNECTED)
def _update_device_box(frame, dev): def _update_device_box(frame, dev):
@ -52,19 +26,16 @@ def _update_device_box(frame, dev):
frame.set_name(_PLACEHOLDER) frame.set_name(_PLACEHOLDER)
return return
icon, label = _find_children(frame, 'icon', 'label') icon, label = ui.find_children(frame, 'icon', 'label')
frame.set_visible(True) frame.set_visible(True)
if frame.get_name() != dev.name: if frame.get_name() != dev.name:
frame.set_name(dev.name) frame.set_name(dev.name)
if theme.has_icon(dev.name): icon.set_from_icon_name(ui.get_icon(dev.name, dev.kind), _DEVICE_ICON_SIZE)
icon.set_from_icon_name(dev.name, _DEVICE_ICON_SIZE)
else:
icon.set_from_icon_name(dev.kind, _DEVICE_ICON_SIZE)
icon.set_tooltip_text(dev.name) icon.set_tooltip_text(dev.name)
label.set_markup('<b>' + dev.name + '</b>') label.set_markup('<b>' + dev.name + '</b>')
status = _find_children(frame, 'status') status = ui.find_children(frame, 'status')
if dev.status < STATUS.CONNECTED: if dev.status < STATUS.CONNECTED:
icon.set_sensitive(False) icon.set_sensitive(False)
icon.set_tooltip_text(dev.status_text) icon.set_tooltip_text(dev.status_text)
@ -111,10 +82,9 @@ def _update_device_box(frame, dev):
light_label.set_text('%d lux' % light_level) light_label.set_text('%d lux' % light_level)
def update(window, receiver, icon_name=None): def update(window, receiver):
if window and window.get_child(): if window and window.get_child():
if icon_name is not None: window.set_icon_name(ui.appicon(receiver.status))
window.set_icon_name(icon_name)
vbox = window.get_child() vbox = window.get_child()
controls = list(vbox.get_children()) controls = list(vbox.get_children())
@ -132,7 +102,7 @@ def update(window, receiver, icon_name=None):
def _receiver_box(name): def _receiver_box(name):
box = _device_box(False, False) box = _device_box(False, False)
icon, status_box = _find_children(box, 'icon', 'status') icon, status_box = ui.find_children(box, 'icon', 'status')
icon.set_from_icon_name(name, _SMALL_DEVICE_ICON_SIZE) icon.set_from_icon_name(name, _SMALL_DEVICE_ICON_SIZE)
icon.set_tooltip_text(name) icon.set_tooltip_text(name)
@ -142,14 +112,10 @@ def _receiver_box(name):
toolbar.set_icon_size(Gtk.IconSize.MENU) toolbar.set_icon_size(Gtk.IconSize.MENU)
toolbar.set_show_arrow(False) toolbar.set_show_arrow(False)
pair_button = Gtk.ToolButton() toolbar.insert(ui.action.pair.create_tool_item(), 0)
pair_button.set_icon_name('add')
pair_button.set_tooltip_text('Pair new device')
pair_button.set_sensitive(False)
toolbar.insert(pair_button, 0)
toolbar.show_all() toolbar.show_all()
toolbar.set_visible(False) # toolbar.set_visible(False)
status_box.pack_end(toolbar, False, False, 0) status_box.pack_end(toolbar, False, False, 0)
return box return box
@ -208,10 +174,26 @@ def _device_box(has_status_icons=True, has_frame=True):
return box return box
def toggle(window, trigger):
# print 'window toggle', window, trigger
if window.get_visible():
position = window.get_position()
window.hide()
window.move(*position)
else:
if trigger and type(trigger) == Gtk.StatusIcon:
x, y = window.get_position()
if x == 0 and y == 0:
x, y, _ = Gtk.StatusIcon.position_menu(Gtk.Menu(), trigger)
window.move(x, y)
window.present()
return True
def create(title, name, max_devices, systray=False): def create(title, name, max_devices, systray=False):
window = Gtk.Window() window = Gtk.Window()
window.set_title(title) window.set_title(title)
# window.set_icon_name(title) window.set_icon_name(ui.appicon(0))
window.set_role('status-window') window.set_role('status-window')
vbox = Gtk.VBox(homogeneous=False, spacing=4) vbox = Gtk.VBox(homogeneous=False, spacing=4)
@ -227,46 +209,17 @@ def create(title, name, max_devices, systray=False):
window.add(vbox) window.add(vbox)
geometry = Gdk.Geometry() geometry = Gdk.Geometry()
geometry.min_width = 260 geometry.min_width = 300
geometry.min_height = 40 geometry.min_height = 40
window.set_geometry_hints(vbox, geometry, Gdk.WindowHints.MIN_SIZE) window.set_geometry_hints(vbox, geometry, Gdk.WindowHints.MIN_SIZE)
window.set_resizable(False) window.set_resizable(False)
window.toggle_visible = lambda i: toggle(window, i)
if systray: if systray:
# def _state_event(w, e):
# if e.new_window_state & Gdk.WindowState.ICONIFIED:
# w.hide()
# w.deiconify()
# return True
# window.connect('window-state-event', _state_event)
window.set_keep_above(True) window.set_keep_above(True)
window.set_deletable(False) window.connect('delete-event', toggle)
# window.set_decorated(False)
# window.set_position(Gtk.WindowPosition.MOUSE)
# ulgy, but hides the minimize icon from the window
window.set_type_hint(Gdk.WindowTypeHint.MENU)
window.set_skip_taskbar_hint(True)
window.set_skip_pager_hint(True)
window.connect('delete-event', lambda w, e: toggle(None, w) or True)
else: else:
# window.set_position(Gtk.WindowPosition.CENTER)
window.connect('delete-event', Gtk.main_quit) window.connect('delete-event', Gtk.main_quit)
return window return window
def toggle(icon, window):
if window.get_visible():
position = window.get_position()
window.hide()
window.move(*position)
else:
if icon:
x, y = window.get_position()
if x == 0 and y == 0:
x, y, _ = Gtk.StatusIcon.position_menu(Gtk.Menu(), icon)
window.move(x, y)
window.present()
return True

View File

@ -7,18 +7,16 @@ import logging
try: try:
from gi.repository import Notify from gi.repository import Notify
from gi.repository import Gtk
import ui
from logitech.devices.constants import STATUS from logitech.devices.constants import STATUS
# necessary because the notifications daemon does not know about our XDG_DATA_DIRS # necessary because the notifications daemon does not know about our XDG_DATA_DIRS
theme = Gtk.IconTheme.get_default()
_icons = {} _icons = {}
def _icon(title): def _icon(title):
if title not in _icons: if title not in _icons:
icon = theme.lookup_icon(title, 0, 0) _icons[title] = ui.icon_file(title)
_icons[title] = icon.get_filename() if icon else None
return _icons.get(title) return _icons.get(title)
@ -28,14 +26,14 @@ try:
_notifications = {} _notifications = {}
def init(app_title=None): def init(app_title):
"""Init the notifications system.""" """Init the notifications system."""
global available global available
if available: if available:
logging.info("starting desktop notifications")
if not Notify.is_initted(): if not Notify.is_initted():
logging.info("starting desktop notifications")
try: try:
return Notify.init(app_title or Notify.get_app_name()) return Notify.init(app_title)
except: except:
logging.exception("initializing desktop notifications") logging.exception("initializing desktop notifications")
available = False available = False
@ -74,4 +72,4 @@ except ImportError:
available = False available = False
init = lambda app_title: False init = lambda app_title: False
uninit = lambda: None uninit = lambda: None
show = lambda status_code, title, text: None show = lambda dev: None

View File

@ -1,15 +0,0 @@
#
#
#
from gi.repository import Gtk
def create(parent_window, title):
window = Gtk.Dialog(title, parent_window, Gtk.DialogFlags.MODAL, buttons=(Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL))
Gtk.Window.set_default_icon_name('add')
window.set_resizable(False)
# window.set_role('pair-device')
return window

122
app/ui/pair_window.py Normal file
View File

@ -0,0 +1,122 @@
#
#
#
import logging
from gi.repository import (Gtk, GObject)
import ui
def _create_page(assistant, text, kind):
p = Gtk.VBox(False, 12)
p.set_border_width(8)
if text:
label = Gtk.Label(text)
label.set_alignment(0, 0)
p.pack_start(label, False, True, 0)
assistant.append_page(p)
assistant.set_page_type(p, kind)
p.show_all()
return p
def _device_confirmed(entry, _2, trigger, assistant, page):
assistant.commit()
assistant.set_page_complete(page, True)
return True
def _finish(assistant, action):
logging.debug("finish %s", assistant)
assistant.destroy()
action.set_sensitive(True)
def _cancel(assistant, action, state):
logging.debug("cancel %s", assistant)
state.stop_scan()
_finish(assistant, action)
def _prepare(assistant, page, state):
index = assistant.get_current_page()
logging.debug("prepare %s %d %s", assistant, index, page)
if index == 0:
state.reset()
GObject.timeout_add(state.TICK, state.countdown, assistant)
spinner = page.get_children()[-1]
spinner.start()
return
assistant.remove_page(0)
state.stop_scan()
def _scan_complete(assistant, device):
if device is None:
page = _create_page(assistant,
'No new device detected.\n'
'\n'
'Make sure your device is within range of the receiver,\nand it has a decent battery charge.\n',
Gtk.AssistantPageType.CONFIRM)
else:
page = _create_page(assistant,
None,
Gtk.AssistantPageType.CONFIRM)
hbox = Gtk.HBox(False, 16)
device_icon = Gtk.Image()
device_icon.set_from_icon_name(ui.get_icon(device.name, device.kind), Gtk.IconSize.DIALOG)
hbox.pack_start(device_icon, False, False, 0)
device_label = Gtk.Label(device.kind + '\n' + device.name)
hbox.pack_start(device_label, False, False, 0)
halign = Gtk.Alignment.new(0.5, 0.5, 0, 1)
halign.add(hbox)
page.pack_start(halign, False, True, 0)
hbox = Gtk.HBox(False, 16)
hbox.pack_start(Gtk.Entry(), False, False, 0)
hbox.pack_start(Gtk.ToggleButton('Test'), False, False, 0)
halign = Gtk.Alignment.new(0.5, 0.5, 0, 1)
halign.add(hbox)
page.pack_start(halign, False, False, 0)
entry_info = Gtk.Label('Use the controls above to confirm\n'
'this is the device you want to pair.')
entry_info.set_sensitive(False)
page.pack_start(entry_info, False, False, 0)
page.show_all()
assistant.set_page_complete(page, True)
assistant.next_page()
def create(action, state):
assistant = Gtk.Assistant()
assistant.set_title(action.get_label())
assistant.set_icon_name(action.get_icon_name())
assistant.set_size_request(440, 240)
assistant.set_resizable(False)
assistant.set_role('pair-device')
page_intro = _create_page(assistant,
'Turn on the device you want to pair.\n'
'\n'
'If the device is already turned on,\nturn if off and on again.',
Gtk.AssistantPageType.INTRO)
spinner = Gtk.Spinner()
spinner.set_visible(True)
page_intro.pack_end(spinner, True, True, 16)
assistant.scan_complete = _scan_complete
assistant.connect('prepare', _prepare, state)
assistant.connect('cancel', _cancel, action, state)
assistant.connect('close', _finish, action)
assistant.connect('apply', _finish, action)
return assistant

56
app/ui/status_icon.py Normal file
View File

@ -0,0 +1,56 @@
#
#
#
from gi.repository import Gtk
import ui
def create(window, menu_actions=None):
icon = Gtk.StatusIcon()
icon.set_title(window.get_title())
icon.set_name(window.get_title())
icon.set_from_icon_name(ui.appicon(0))
icon.connect('activate', window.toggle_visible)
menu = Gtk.Menu()
for action in menu_actions or ():
if action:
menu.append(action.create_menu_item())
quit_action = ui.action._action('exit', 'Quit', Gtk.main_quit)
menu.append(quit_action.create_menu_item())
menu.show_all()
icon.connect('popup_menu',
lambda icon, button, time, menu:
menu.popup(None, None, icon.position_menu, icon, button, time),
menu)
return icon
def update(icon, receiver):
icon.set_from_icon_name(ui.appicon(receiver.status))
if receiver.devices:
lines = []
if receiver.status < 1:
lines += (receiver.status_text, '')
devlist = [receiver.devices[d] for d in range(1, 1 + receiver.max_devices) if d in receiver.devices]
for dev in devlist:
name = '<b>' + dev.name + '</b>'
if dev.status < 1:
lines.append(name + ' (' + dev.status_text + ')')
else:
lines.append(name)
if dev.status > 1:
lines.append(' ' + dev.status_text)
lines.append('')
text = '\n'.join(lines).rstrip('\n')
icon.set_tooltip_markup(text)
else:
icon.set_tooltip_text(receiver.status_text)

View File

@ -4,7 +4,7 @@
from threading import Thread from threading import Thread
import time import time
import logging from logging import getLogger as _Logger
from collections import namedtuple from collections import namedtuple
from logitech.devices.constants import STATUS from logitech.devices.constants import STATUS
@ -16,12 +16,14 @@ _DUMMY_RECEIVER.__nonzero__ = lambda _: False
_DUMMY_RECEIVER.device_name = Receiver.NAME _DUMMY_RECEIVER.device_name = Receiver.NAME
DUMMY = _DUMMY_RECEIVER(Receiver.NAME, Receiver.NAME, STATUS.UNAVAILABLE, 'Receiver not found.', Receiver.max_devices, {}) DUMMY = _DUMMY_RECEIVER(Receiver.NAME, Receiver.NAME, STATUS.UNAVAILABLE, 'Receiver not found.', Receiver.max_devices, {})
_l = _Logger('watcher')
def _sleep(seconds, granularity, breakout=lambda: False): def _sleep(seconds, granularity, breakout=lambda: False):
for index in range(0, int(seconds / granularity)): slept = 0
if breakout(): while slept < seconds and not breakout():
return
time.sleep(granularity) time.sleep(granularity)
slept += granularity
class Watcher(Thread): class Watcher(Thread):
@ -30,77 +32,77 @@ class Watcher(Thread):
""" """
def __init__(self, apptitle, update_ui, notify=None): def __init__(self, apptitle, update_ui, notify=None):
super(Watcher, self).__init__(group=apptitle, name='Watcher') super(Watcher, self).__init__(group=apptitle, name='Watcher')
self.daemon = True
self._active = False self._active = False
self._receiver = DUMMY
self.update_ui = update_ui self.update_ui = update_ui
self.notify = notify or (lambda d: None) self.notify = notify or (lambda d: None)
self.receiver = DUMMY @property
def receiver(self):
return self._receiver
def run(self): def run(self):
self._active = True self._active = True
notify_missing = True notify_missing = True
while self._active: while self._active:
if self.receiver == DUMMY: if self._receiver == DUMMY:
r = Receiver.open() r = Receiver.open()
if r: if r is None:
logging.info("receiver %s ", r)
self.update_ui(r)
self.notify(r)
r.events_handler = self._events_callback
# give it some time to read all devices
r.status_changed.clear()
_sleep(8, 0.4, r.status_changed.is_set)
if r.devices:
logging.info("%d device(s) found", len(r.devices))
for d in r.devices.values():
self.notify(d)
else:
# if no devices found so far, assume none at all
logging.info("no devices found")
r.status = STATUS.CONNECTED
self.receiver = r
notify_missing = True
else:
if notify_missing: if notify_missing:
_sleep(0.8, 0.4, lambda: not self._active) _sleep(0.8, 0.4, lambda: not self._active)
notify_missing = False notify_missing = False
self.update_ui(DUMMY) if self._active:
self.notify(DUMMY) self.update_ui(DUMMY)
self.notify(DUMMY)
_sleep(4, 0.4, lambda: not self._active) _sleep(4, 0.4, lambda: not self._active)
continue continue
_l.info("receiver %s ", r)
self.update_ui(r)
self.notify(r)
if r.count_devices() > 0:
# give it some time to read all devices
r.status_changed.clear()
_sleep(8, 0.4, r.status_changed.is_set)
if r.devices:
_l.info("%d device(s) found", len(r.devices))
for d in r.devices.values():
self.notify(d)
else:
# if no devices found so far, assume none at all
_l.info("no devices found")
r.status = STATUS.CONNECTED
self._receiver = r
notify_missing = True
if self._active: if self._active:
if self.receiver: if self._receiver:
logging.debug("waiting for status_changed") _l.debug("waiting for status_changed")
sc = self.receiver.status_changed sc = self._receiver.status_changed
sc.wait() sc.wait()
sc.clear() sc.clear()
logging.debug("status_changed %s %d", sc.reason, sc.urgent) _l.debug("status_changed %s %d", sc.reason, sc.urgent)
self.update_ui(self.receiver) self.update_ui(self._receiver)
if sc.reason and sc.urgent: if sc.reason and sc.urgent:
self.notify(sc.reason) self.notify(sc.reason)
else: else:
self.receiver = DUMMY self._receiver = DUMMY
self.update_ui(DUMMY) self.update_ui(DUMMY)
self.notify(DUMMY) self.notify(DUMMY)
if self.receiver: if self._receiver:
self.receiver.close() self._receiver.close()
def stop(self): def stop(self):
if self._active: if self._active:
logging.info("stopping %s", self) _l.info("stopping %s", self)
self._active = False self._active = False
if self.receiver: if self._receiver:
# break out of an eventual wait() # break out of an eventual wait()
self.receiver.status_changed.reason = None self._receiver.status_changed.reason = None
self.receiver.status_changed.set() self._receiver.status_changed.set()
self.join()
def _events_callback(self, event):
logging.warn("don't know how to handle event %s", event)

View File

@ -33,7 +33,7 @@ def _module(device_name):
def default_request_status(devinfo, listener=None): def default_request_status(devinfo, listener=None):
if FEATURE.BATTERY in devinfo.features: if FEATURE.BATTERY in devinfo.features:
if listener: if listener:
reply = listener.request(_api.get_device_battery_level, devinfo.number, features=devinfo.features) reply = listener.call_api(_api.get_device_battery_level, devinfo.number, features=devinfo.features)
else: else:
reply = _api.get_device_battery_level(devinfo.handle, devinfo.number, features=devinfo.features) reply = _api.get_device_battery_level(devinfo.handle, devinfo.number, features=devinfo.features)
@ -42,7 +42,7 @@ def default_request_status(devinfo, listener=None):
return STATUS.CONNECTED, {PROPS.BATTERY_LEVEL: discharge, PROPS.BATTERY_STATUS: status} return STATUS.CONNECTED, {PROPS.BATTERY_LEVEL: discharge, PROPS.BATTERY_STATUS: status}
if listener: if listener:
reply = listener.request(_api.ping, devinfo.number) reply = listener.call_api(_api.ping, devinfo.number)
else: else:
reply = _api.ping(devinfo.handle, devinfo.number) reply = _api.ping(devinfo.handle, devinfo.number)

View File

@ -36,7 +36,7 @@ def request_status(devinfo, listener=None):
if listener is None: if listener is None:
reply = _trigger_solar_charge_events(devinfo.handle, devinfo) reply = _trigger_solar_charge_events(devinfo.handle, devinfo)
elif listener: elif listener:
reply = listener.request(_trigger_solar_charge_events, devinfo) reply = listener.call_api(_trigger_solar_charge_events, devinfo)
else: else:
reply = 0 reply = 0

View File

@ -38,22 +38,22 @@ close = _base.close
def get_receiver_info(handle): def get_receiver_info(handle):
serial = None serial = None
reply = _base.request(handle, 0xff, b'\x83\xB5', b'\x03') reply = _base.request(handle, 0xFF, b'\x83\xB5', b'\x03')
if reply and reply[0:1] == b'\x03': if reply and reply[0:1] == b'\x03':
serial = _hexlify(reply[1:5]) serial = _hexlify(reply[1:5])
firmware = '??.??' firmware = '??.??'
reply = _base.request(handle, 0xff, b'\x81\xF1', b'\x01') reply = _base.request(handle, 0xFF, b'\x81\xF1', b'\x01')
if reply and reply[0:1] == b'\x01': if reply and reply[0:1] == b'\x01':
fw_version = _hexlify(reply[1:3]) fw_version = _hexlify(reply[1:3])
firmware = fw_version[0:2] + '.' + fw_version[2:4] firmware = fw_version[0:2] + '.' + fw_version[2:4]
reply = _base.request(handle, 0xff, b'\x81\xF1', b'\x02') reply = _base.request(handle, 0xFF, b'\x81\xF1', b'\x02')
if reply and reply[0:1] == b'\x02': if reply and reply[0:1] == b'\x02':
firmware += '.B' + _hexlify(reply[1:3]) firmware += '.B' + _hexlify(reply[1:3])
bootloader = None bootloader = None
reply = _base.request(handle, 0xff, b'\x81\xF1', b'\x04') reply = _base.request(handle, 0xFF, b'\x81\xF1', b'\x04')
if reply and reply[0:1] == b'\x04': if reply and reply[0:1] == b'\x04':
bl_version = _hexlify(reply[1:3]) bl_version = _hexlify(reply[1:3])
bootloader = bl_version[0:2] + '.' + bl_version[2:4] bootloader = bl_version[0:2] + '.' + bl_version[2:4]
@ -61,6 +61,11 @@ def get_receiver_info(handle):
return (serial, firmware, bootloader) return (serial, firmware, bootloader)
def count_devices(handle):
count = _base.request(handle, 0xFF, b'\x80\x02', b'\x02')
return 0 if count is None else ord(count[1:2])
def request(handle, devnumber, feature, function=b'\x00', params=b'', features=None): def request(handle, devnumber, feature, function=b'\x00', params=b'', features=None):
"""Makes a feature call to the device, and returns the reply data. """Makes a feature call to the device, and returns the reply data.

View File

@ -42,7 +42,7 @@ class EventsListener(Thread):
Incoming packets will be passed to the callback function in sequence, by a Incoming packets will be passed to the callback function in sequence, by a
separate thread. separate thread.
While this listener is running, you must use the request() method to make While this listener is running, you must use the call_api() method to make
regular UR API calls; otherwise the expected API replies are most likely to regular UR API calls; otherwise the expected API replies are most likely to
be captured by the listener and delivered to the callback. be captured by the listener and delivered to the callback.
""" """
@ -107,6 +107,7 @@ class EventsListener(Thread):
self._task_done.set() self._task_done.set()
_base.close(self._handle) _base.close(self._handle)
self._handle = 0
def stop(self): def stop(self):
"""Tells the listener to stop as soon as possible.""" """Tells the listener to stop as soon as possible."""
@ -120,7 +121,10 @@ class EventsListener(Thread):
def handle(self): def handle(self):
return self._handle return self._handle
def request(self, api_function, *args, **kwargs): def request(self, device, feature_function_index, params=b''):
return self.call_api(_base.request, device, feature_function_index, params)
def call_api(self, api_function, *args, **kwargs):
"""Make an UR API request through this listener's receiver. """Make an UR API request through this listener's receiver.
The api_function must have a receiver handle as a first agument, all The api_function must have a receiver handle as a first agument, all

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.6 KiB

After

Width:  |  Height:  |  Size: 8.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 KiB