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

View File

@ -1,17 +1,19 @@
#!/usr/bin/env python
__author__ = "Daniel Pavel <daniel.pavel@gmail.com>"
__version__ = '0.5'
__license__ = "GPL"
#
#
#
APP_TITLE = 'Solaar'
APPNAME = 'Solaar'
if __name__ == '__main__':
def _parse_arguments():
import argparse
arg_parser = argparse.ArgumentParser(prog=APP_TITLE)
arg_parser = argparse.ArgumentParser(prog=APPNAME.lower())
arg_parser.add_argument('-v', '--verbose',
action='count', default=0,
help='increase the logger verbosity (may be repeated)')
@ -31,59 +33,50 @@ if __name__ == '__main__':
import logging
log_level = logging.root.level - 10 * args.verbose
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
GObject.threads_init()
return args
if __name__ == '__main__':
args = _parse_arguments()
import ui
# check if the notifications are available
args.notifications &= args.systray
if args.notifications:
args.notifications &= ui.notify.init(APP_TITLE)
if ui.notify.init(APPNAME):
ui.action.toggle_notifications.set_active(args.notifications)
else:
ui.action.toggle_notifications = None
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):
icon_name = APP_TITLE + '-fail' if receiver.status < 1 else APP_TITLE
if window:
GObject.idle_add(ui.window.update, window, receiver, icon_name)
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()
window = ui.main_window.create(APPNAME,
watcher.DUMMY.NAME,
watcher.DUMMY.max_devices,
args.systray)
if args.systray:
def _toggle_notifications(item):
# logging.debug("toggle notifications %s", item)
if ui.notify.available:
if item.get_active():
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')
menu_actions = (ui.action.pair,
ui.action.toggle_notifications,
ui.action.about)
icon = ui.status_icon.create(window, menu_actions)
else:
icon = None
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
Gtk.main()

View File

@ -1,3 +1,58 @@
# 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)
import ui
from logitech.devices.constants import (STATUS, PROPS)
@ -13,37 +14,10 @@ _STATUS_ICON_SIZE = Gtk.IconSize.DND
_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):
label, buttons = _find_children(box, 'label', 'buttons')
label, buttons = ui.find_children(box, 'label', 'buttons')
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):
@ -52,19 +26,16 @@ def _update_device_box(frame, dev):
frame.set_name(_PLACEHOLDER)
return
icon, label = _find_children(frame, 'icon', 'label')
icon, label = ui.find_children(frame, 'icon', 'label')
frame.set_visible(True)
if frame.get_name() != dev.name:
frame.set_name(dev.name)
if theme.has_icon(dev.name):
icon.set_from_icon_name(dev.name, _DEVICE_ICON_SIZE)
else:
icon.set_from_icon_name(dev.kind, _DEVICE_ICON_SIZE)
icon.set_from_icon_name(ui.get_icon(dev.name, dev.kind), _DEVICE_ICON_SIZE)
icon.set_tooltip_text(dev.name)
label.set_markup('<b>' + dev.name + '</b>')
status = _find_children(frame, 'status')
status = ui.find_children(frame, 'status')
if dev.status < STATUS.CONNECTED:
icon.set_sensitive(False)
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)
def update(window, receiver, icon_name=None):
def update(window, receiver):
if window and window.get_child():
if icon_name is not None:
window.set_icon_name(icon_name)
window.set_icon_name(ui.appicon(receiver.status))
vbox = window.get_child()
controls = list(vbox.get_children())
@ -132,7 +102,7 @@ def update(window, receiver, icon_name=None):
def _receiver_box(name):
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_tooltip_text(name)
@ -142,14 +112,10 @@ def _receiver_box(name):
toolbar.set_icon_size(Gtk.IconSize.MENU)
toolbar.set_show_arrow(False)
pair_button = Gtk.ToolButton()
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.insert(ui.action.pair.create_tool_item(), 0)
toolbar.show_all()
toolbar.set_visible(False)
# toolbar.set_visible(False)
status_box.pack_end(toolbar, False, False, 0)
return box
@ -208,10 +174,26 @@ def _device_box(has_status_icons=True, has_frame=True):
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):
window = Gtk.Window()
window.set_title(title)
# window.set_icon_name(title)
window.set_icon_name(ui.appicon(0))
window.set_role('status-window')
vbox = Gtk.VBox(homogeneous=False, spacing=4)
@ -227,46 +209,17 @@ def create(title, name, max_devices, systray=False):
window.add(vbox)
geometry = Gdk.Geometry()
geometry.min_width = 260
geometry.min_width = 300
geometry.min_height = 40
window.set_geometry_hints(vbox, geometry, Gdk.WindowHints.MIN_SIZE)
window.set_resizable(False)
window.toggle_visible = lambda i: toggle(window, i)
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_deletable(False)
# 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)
window.connect('delete-event', toggle)
else:
# window.set_position(Gtk.WindowPosition.CENTER)
window.connect('delete-event', Gtk.main_quit)
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:
from gi.repository import Notify
from gi.repository import Gtk
import ui
from logitech.devices.constants import STATUS
# necessary because the notifications daemon does not know about our XDG_DATA_DIRS
theme = Gtk.IconTheme.get_default()
_icons = {}
def _icon(title):
if title not in _icons:
icon = theme.lookup_icon(title, 0, 0)
_icons[title] = icon.get_filename() if icon else None
_icons[title] = ui.icon_file(title)
return _icons.get(title)
@ -28,14 +26,14 @@ try:
_notifications = {}
def init(app_title=None):
def init(app_title):
"""Init the notifications system."""
global available
if available:
logging.info("starting desktop notifications")
if not Notify.is_initted():
logging.info("starting desktop notifications")
try:
return Notify.init(app_title or Notify.get_app_name())
return Notify.init(app_title)
except:
logging.exception("initializing desktop notifications")
available = False
@ -74,4 +72,4 @@ except ImportError:
available = False
init = lambda app_title: False
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
import time
import logging
from logging import getLogger as _Logger
from collections import namedtuple
from logitech.devices.constants import STATUS
@ -16,12 +16,14 @@ _DUMMY_RECEIVER.__nonzero__ = lambda _: False
_DUMMY_RECEIVER.device_name = Receiver.NAME
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):
for index in range(0, int(seconds / granularity)):
if breakout():
return
slept = 0
while slept < seconds and not breakout():
time.sleep(granularity)
slept += granularity
class Watcher(Thread):
@ -30,77 +32,77 @@ class Watcher(Thread):
"""
def __init__(self, apptitle, update_ui, notify=None):
super(Watcher, self).__init__(group=apptitle, name='Watcher')
self.daemon = True
self._active = False
self._receiver = DUMMY
self.update_ui = update_ui
self.notify = notify or (lambda d: None)
self.receiver = DUMMY
@property
def receiver(self):
return self._receiver
def run(self):
self._active = True
notify_missing = True
while self._active:
if self.receiver == DUMMY:
if self._receiver == DUMMY:
r = Receiver.open()
if r:
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 r is None:
if notify_missing:
_sleep(0.8, 0.4, lambda: not self._active)
notify_missing = False
self.update_ui(DUMMY)
self.notify(DUMMY)
if self._active:
self.update_ui(DUMMY)
self.notify(DUMMY)
_sleep(4, 0.4, lambda: not self._active)
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.receiver:
logging.debug("waiting for status_changed")
sc = self.receiver.status_changed
if self._receiver:
_l.debug("waiting for status_changed")
sc = self._receiver.status_changed
sc.wait()
sc.clear()
logging.debug("status_changed %s %d", sc.reason, sc.urgent)
self.update_ui(self.receiver)
_l.debug("status_changed %s %d", sc.reason, sc.urgent)
self.update_ui(self._receiver)
if sc.reason and sc.urgent:
self.notify(sc.reason)
else:
self.receiver = DUMMY
self._receiver = DUMMY
self.update_ui(DUMMY)
self.notify(DUMMY)
if self.receiver:
self.receiver.close()
if self._receiver:
self._receiver.close()
def stop(self):
if self._active:
logging.info("stopping %s", self)
_l.info("stopping %s", self)
self._active = False
if self.receiver:
if self._receiver:
# break out of an eventual wait()
self.receiver.status_changed.reason = None
self.receiver.status_changed.set()
self.join()
def _events_callback(self, event):
logging.warn("don't know how to handle event %s", event)
self._receiver.status_changed.reason = None
self._receiver.status_changed.set()

View File

@ -33,7 +33,7 @@ def _module(device_name):
def default_request_status(devinfo, listener=None):
if FEATURE.BATTERY in devinfo.features:
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:
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}
if listener:
reply = listener.request(_api.ping, devinfo.number)
reply = listener.call_api(_api.ping, devinfo.number)
else:
reply = _api.ping(devinfo.handle, devinfo.number)

View File

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

View File

@ -38,22 +38,22 @@ close = _base.close
def get_receiver_info(handle):
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':
serial = _hexlify(reply[1:5])
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':
fw_version = _hexlify(reply[1:3])
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':
firmware += '.B' + _hexlify(reply[1:3])
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':
bl_version = _hexlify(reply[1:3])
bootloader = bl_version[0:2] + '.' + bl_version[2:4]
@ -61,6 +61,11 @@ def get_receiver_info(handle):
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):
"""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
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
be captured by the listener and delivered to the callback.
"""
@ -107,6 +107,7 @@ class EventsListener(Thread):
self._task_done.set()
_base.close(self._handle)
self._handle = 0
def stop(self):
"""Tells the listener to stop as soon as possible."""
@ -120,7 +121,10 @@ class EventsListener(Thread):
def handle(self):
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.
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