clean-up and simpler monitoring of receiver state

This commit is contained in:
Daniel Pavel 2012-11-01 13:47:11 +02:00
parent a8a72f7ae5
commit 1d8ac27614
12 changed files with 74 additions and 66 deletions

View File

@ -10,7 +10,7 @@ from logitech.unifying_receiver import base as _base
state = None state = None
class State(object): class State(object):
TICK = 300 TICK = 400
PAIR_TIMEOUT = 60 * 1000 / TICK PAIR_TIMEOUT = 60 * 1000 / TICK
def __init__(self, listener): def __init__(self, listener):

View File

@ -258,7 +258,7 @@ class ReceiverListener(_EventsListener):
self.events_filter = None self.events_filter = None
self.events_handler = None self.events_handler = None
self.status_changed_callback = status_changed_callback or (lambda reason=None, urgent=False: None) self.status_changed_callback = status_changed_callback or (lambda reason, urgent=False: None)
receiver.kind = receiver.name receiver.kind = receiver.name
receiver.devices = {} receiver.devices = {}
@ -354,7 +354,7 @@ class ReceiverListener(_EventsListener):
return True return True
def __str__(self): def __str__(self):
return '<ReceiverListener(%s,%d)>' % (self.path, self.receiver.status) return '<ReceiverListener(%s,%d)>' % (self.receiver.path, self.receiver.status)
@classmethod @classmethod
def open(self, status_changed_callback=None): def open(self, status_changed_callback=None):

View File

@ -1,16 +1,14 @@
#!/usr/bin/env python #!/usr/bin/env python
APPNAME = 'Solaar'
__author__ = "Daniel Pavel <daniel.pavel@gmail.com>" __author__ = "Daniel Pavel <daniel.pavel@gmail.com>"
__version__ = '0.5' __version__ = '0.6'
__license__ = "GPL" __license__ = "GPL"
# #
# #
# #
APPNAME = 'Solaar'
def _parse_arguments(): def _parse_arguments():
import argparse import argparse
arg_parser = argparse.ArgumentParser(prog=APPNAME.lower()) arg_parser = argparse.ArgumentParser(prog=APPNAME.lower())
@ -31,7 +29,7 @@ def _parse_arguments():
args = arg_parser.parse_args() args = arg_parser.parse_args()
import logging import logging
log_level = logging.ERROR - 10 * args.verbose log_level = logging.WARNING - 10 * args.verbose
log_format='%(asctime)s %(levelname)8s [%(threadName)s] %(name)s: %(message)s' log_format='%(asctime)s %(levelname)8s [%(threadName)s] %(name)s: %(message)s'
logging.basicConfig(level=max(log_level, logging.DEBUG), format=log_format) logging.basicConfig(level=max(log_level, logging.DEBUG), format=log_format)
@ -66,6 +64,7 @@ if __name__ == '__main__':
window.present() window.present()
import pairing import pairing
from gi.repository import Gtk, GObject
listener = None listener = None
notify_missing = True notify_missing = True
@ -74,8 +73,11 @@ if __name__ == '__main__':
global listener global listener
receiver = DUMMY if listener is None else listener.receiver receiver = DUMMY if listener is None else listener.receiver
ui.update(receiver, icon, window, reason) ui.update(receiver, icon, window, reason)
if ui.notify.available and reason and urgent: if ui.notify.available and urgent:
ui.notify.show(reason or receiver) ui.notify.show(reason)
if not listener:
listener = None
GObject.timeout_add(3000, check_for_listener)
def check_for_listener(): def check_for_listener():
global listener, notify_missing global listener, notify_missing
@ -85,18 +87,14 @@ if __name__ == '__main__':
pairing.state = None pairing.state = None
if notify_missing: if notify_missing:
status_changed(DUMMY, True) status_changed(DUMMY, True)
ui.notify.show(DUMMY)
notify_missing = False notify_missing = False
else: return True
# print ("opened receiver", listener, listener.receiver)
pairing.state = pairing.State(listener)
notify_missing = True
status_changed(listener.receiver, True)
return True
from gi.repository import Gtk, GObject # print ("opened receiver", listener, listener.receiver)
pairing.state = pairing.State(listener)
notify_missing = True
status_changed(listener.receiver, True)
GObject.timeout_add(5000, check_for_listener)
check_for_listener() check_for_listener()
Gtk.main() Gtk.main()

View File

@ -27,6 +27,8 @@ def icon_file(name):
def find_children(container, *child_names): def find_children(container, *child_names):
assert container is not None
def _iterate_children(widget, names, result, count): def _iterate_children(widget, names, result, count):
wname = widget.get_name() wname = widget.get_name()
if wname in names: if wname in names:

View File

@ -1,3 +1,6 @@
#
#
#
from gi.repository import Gtk from gi.repository import Gtk

View File

@ -183,7 +183,7 @@ def create(title, name, max_devices, systray=False):
geometry = Gdk.Geometry() geometry = Gdk.Geometry()
geometry.min_width = 320 geometry.min_width = 320
geometry.min_height = 20 geometry.min_height = 32
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)
@ -206,10 +206,10 @@ def _update_receiver_box(frame, receiver):
label.set_text(receiver.status_text or '') label.set_text(receiver.status_text or '')
if receiver.status < STATUS.CONNECTED: if receiver.status < STATUS.CONNECTED:
frame._device = None
toolbar.set_sensitive(False) toolbar.set_sensitive(False)
toolbar.get_children()[0].set_active(False) toolbar.get_children()[0].set_active(False)
info_label.set_text('') info_label.set_text('')
frame._device = None
else: else:
toolbar.set_sensitive(True) toolbar.set_sensitive(True)
frame._device = receiver frame._device = receiver
@ -274,20 +274,25 @@ def _update_device_box(frame, dev):
frame.set_visible(True) frame.set_visible(True)
def update(window, receiver, reason): def update(window, receiver, device):
print ("update", receiver, receiver.status, reason) print ("update", receiver, receiver.status, device)
window.set_icon_name(ui.appicon(receiver.status)) window.set_icon_name(ui.appicon(receiver.status))
vbox = window.get_child() vbox = window.get_child()
controls = list(vbox.get_children()) frames = list(vbox.get_children())
if reason == receiver: if id(device) == id(receiver):
_update_receiver_box(controls[0], receiver) _update_receiver_box(frames[0], receiver)
if receiver.status < STATUS.CONNECTED:
for frame in frames[1:]:
frame.set_visible(False)
frame.set_name(_PLACEHOLDER)
frame._device = None
else: else:
frame = controls[reason.number] frame = frames[device.number]
if reason.status == STATUS.UNPAIRED: if device.status == STATUS.UNPAIRED:
frame.set_visible(False) frame.set_visible(False)
frame.set_name(_PLACEHOLDER) frame.set_name(_PLACEHOLDER)
frame._device = None frame._device = None
else: else:
_update_device_box(frame, reason) _update_device_box(frame, device)

View File

@ -68,7 +68,7 @@ try:
logging.exception("showing %s", n) logging.exception("showing %s", n)
except ImportError: except ImportError:
logging.warn("Notify not found in gi.repository, desktop notifications are disabled") logging.warn("desktop notifications disabled")
available = False available = False
init = lambda app_title: False init = lambda app_title: False
uninit = lambda: None uninit = lambda: None

View File

@ -2,7 +2,7 @@
# #
# #
import logging # import logging
from gi.repository import (Gtk, GObject) from gi.repository import (Gtk, GObject)
import ui import ui
@ -31,17 +31,17 @@ def _device_confirmed(entry, _2, trigger, assistant, page):
def _finish(assistant): def _finish(assistant):
logging.debug("finish %s", assistant) # logging.debug("finish %s", assistant)
assistant.destroy() assistant.destroy()
def _cancel(assistant, state): def _cancel(assistant, state):
logging.debug("cancel %s", assistant) # logging.debug("cancel %s", assistant)
state.stop_scan() state.stop_scan()
_finish(assistant) _finish(assistant)
def _prepare(assistant, page, state): def _prepare(assistant, page, state):
index = assistant.get_current_page() index = assistant.get_current_page()
logging.debug("prepare %s %d %s", assistant, index, page) # logging.debug("prepare %s %d %s", assistant, index, page)
if index == 0: if index == 0:
state.reset() state.reset()

View File

@ -71,7 +71,7 @@ if __name__ == '__main__':
t.start() t.start()
while t.is_alive(): while t.is_alive():
line = read_packet ('?? Input: ').strip().replace(' ', '') line = read_packet('?? Input: ').strip().replace(' ', '')
if line: if line:
try: try:
data = unhexlify(line.encode('ascii')) data = unhexlify(line.encode('ascii'))

View File

@ -231,7 +231,7 @@ def get_device(handle, devnumber, features=None):
""" """
if ping(handle, devnumber): if ping(handle, devnumber):
devinfo = PairedDevice(handle, devnumber) devinfo = PairedDevice(handle, devnumber)
# _log.debug("(%d) found device %s", devnumber, devinfo) # _log.debug("found device %s", devinfo)
return devinfo return devinfo
@ -240,7 +240,7 @@ def get_feature_index(handle, devnumber, feature):
:returns: An int, or ``None`` if the feature is not available. :returns: An int, or ``None`` if the feature is not available.
""" """
# _log.debug("(%d) get feature index <%s:%s>", devnumber, _hex(feature), FEATURE_NAME[feature]) # _log.debug("device %d get feature index <%s:%s>", devnumber, _hex(feature), FEATURE_NAME[feature])
if len(feature) != 2: if len(feature) != 2:
raise ValueError("invalid feature <%s>: it must be a two-byte string" % feature) raise ValueError("invalid feature <%s>: it must be a two-byte string" % feature)
@ -251,11 +251,11 @@ def get_feature_index(handle, devnumber, feature):
if feature_index: if feature_index:
# feature_flags = ord(reply[1:2]) & 0xE0 # feature_flags = ord(reply[1:2]) & 0xE0
# if feature_flags: # if feature_flags:
# _log.debug("(%d) feature <%s:%s> has index %d: %s", # _log.debug("device %d feature <%s:%s> has index %d: %s",
# devnumber, _hex(feature), FEATURE_NAME[feature], feature_index, # devnumber, _hex(feature), FEATURE_NAME[feature], feature_index,
# ','.join([FEATURE_FLAGS[k] for k in FEATURE_FLAGS if feature_flags & k])) # ','.join([FEATURE_FLAGS[k] for k in FEATURE_FLAGS if feature_flags & k]))
# else: # else:
# _log.debug("(%d) feature <%s:%s> has index %d", devnumber, _hex(feature), FEATURE_NAME[feature], feature_index) # _log.debug("device %d feature <%s:%s> has index %d", devnumber, _hex(feature), FEATURE_NAME[feature], feature_index)
# only consider active and supported features? # only consider active and supported features?
# if feature_flags: # if feature_flags:
@ -263,7 +263,7 @@ def get_feature_index(handle, devnumber, feature):
return feature_index return feature_index
_log.warn("(%d) feature <%s:%s> not supported by the device", devnumber, _hex(feature), FEATURE_NAME[feature]) _log.warn("device %d feature <%s:%s> not supported by the device", devnumber, _hex(feature), FEATURE_NAME[feature])
raise _FeatureNotSupported(devnumber, feature) raise _FeatureNotSupported(devnumber, feature)
@ -288,13 +288,13 @@ def get_device_features(handle, devnumber):
Their position in the array is the index to be used when requesting that Their position in the array is the index to be used when requesting that
feature on the device. feature on the device.
""" """
# _log.debug("(%d) get device features", devnumber) # _log.debug("device %d get device features", devnumber)
# get the index of the FEATURE_SET # get the index of the FEATURE_SET
# FEATURE.ROOT should always be available for all devices # FEATURE.ROOT should always be available for all devices
fs_index = _base.request(handle, devnumber, FEATURE.ROOT, FEATURE.FEATURE_SET) fs_index = _base.request(handle, devnumber, FEATURE.ROOT, FEATURE.FEATURE_SET)
if fs_index is None: if fs_index is None:
# _l.warn("(%d) FEATURE_SET not available", device) _log.warn("device %d FEATURE_SET not available", devnumber)
return None return None
fs_index = fs_index[:1] fs_index = fs_index[:1]
@ -306,11 +306,11 @@ def get_device_features(handle, devnumber):
if not features_count: if not features_count:
# this can happen if the device disappeard since the fs_index request # this can happen if the device disappeard since the fs_index request
# otherwise we should get at least a count of 1 (the FEATURE_SET we've just used above) # otherwise we should get at least a count of 1 (the FEATURE_SET we've just used above)
_log.debug("(%d) no features available?!", devnumber) _log.debug("device %d no features available?!", devnumber)
return None return None
features_count = ord(features_count[:1]) features_count = ord(features_count[:1])
# _log.debug("(%d) found %d features", devnumber, features_count) # _log.debug("device %d found %d features", devnumber, features_count)
features = [None] * 0x20 features = [None] * 0x20
for index in range(1, 1 + features_count): for index in range(1, 1 + features_count):
@ -322,11 +322,11 @@ def get_device_features(handle, devnumber):
features[index] = feature features[index] = feature
# if feature_flags: # if feature_flags:
# _log.debug("(%d) feature <%s:%s> at index %d: %s", # _log.debug("device %d feature <%s:%s> at index %d: %s",
# devnumber, _hex(feature), FEATURE_NAME[feature], index, # devnumber, _hex(feature), FEATURE_NAME[feature], index,
# ','.join([FEATURE_FLAGS[k] for k in FEATURE_FLAGS if feature_flags & k])) # ','.join([FEATURE_FLAGS[k] for k in FEATURE_FLAGS if feature_flags & k]))
# else: # else:
# _log.debug("(%d) feature <%s:%s> at index %d", devnumber, _hex(feature), FEATURE_NAME[feature], index) # _log.debug("device %d feature <%s:%s> at index %d", devnumber, _hex(feature), FEATURE_NAME[feature], index)
features[0] = FEATURE.ROOT features[0] = FEATURE.ROOT
while features[-1] is None: while features[-1] is None:
@ -369,7 +369,7 @@ def get_device_firmware(handle, devnumber, features=None):
fw_info = _FirmwareInfo(level, FIRMWARE_KIND[-1], '', '', None) fw_info = _FirmwareInfo(level, FIRMWARE_KIND[-1], '', '', None)
fw.append(fw_info) fw.append(fw_info)
# _log.debug("(%d) firmware %s", devnumber, fw_info) # _log.debug("device %d firmware %s", devnumber, fw_info)
return tuple(fw) return tuple(fw)
@ -387,7 +387,7 @@ def get_device_kind(handle, devnumber, features=None):
d_kind = _base.request(handle, devnumber, _pack('!BB', name_fi, 0x20)) d_kind = _base.request(handle, devnumber, _pack('!BB', name_fi, 0x20))
if d_kind: if d_kind:
d_kind = ord(d_kind[:1]) d_kind = ord(d_kind[:1])
# _log.debug("(%d) device type %d = %s", devnumber, d_kind, DEVICE_KIND[d_kind]) # _log.debug("device %d type %d = %s", devnumber, d_kind, DEVICE_KIND[d_kind])
return DEVICE_KIND[d_kind] return DEVICE_KIND[d_kind]
@ -415,7 +415,7 @@ def get_device_name(handle, devnumber, features=None):
break break
d_name = d_name.decode('ascii') d_name = d_name.decode('ascii')
# _log.debug("(%d) device name %s", devnumber, d_name) # _log.debug("device %d name %s", devnumber, d_name)
return d_name return d_name
@ -429,7 +429,7 @@ def get_device_battery_level(handle, devnumber, features=None):
battery = _base.request(handle, devnumber, _pack('!BB', bat_fi, 0)) battery = _base.request(handle, devnumber, _pack('!BB', bat_fi, 0))
if battery: if battery:
discharge, dischargeNext, status = _unpack('!BBB', battery[:3]) discharge, dischargeNext, status = _unpack('!BBB', battery[:3])
_log.debug("(%d) battery %d%% charged, next level %d%% charge, status %d = %s", _log.debug("device %d battery %d%% charged, next level %d%% charge, status %d = %s",
devnumber, discharge, dischargeNext, status, BATTERY_STATUS[status]) devnumber, discharge, dischargeNext, status, BATTERY_STATUS[status])
return (discharge, dischargeNext, BATTERY_STATUS[status]) return (discharge, dischargeNext, BATTERY_STATUS[status])

View File

@ -40,7 +40,7 @@ _MAX_REPLY_SIZE = _MAX_CALL_SIZE
"""Default timeout on read (in ms).""" """Default timeout on read (in ms)."""
DEFAULT_TIMEOUT = 1000 DEFAULT_TIMEOUT = 1500
# #
# #
@ -166,9 +166,9 @@ def write(handle, devnumber, data):
assert _MAX_CALL_SIZE == 20 assert _MAX_CALL_SIZE == 20
# the data is padded to either 5 or 18 bytes # the data is padded to either 5 or 18 bytes
wdata = _pack('!BB18s' if len(data) > 5 else '!BB5s', 0x10, devnumber, data) wdata = _pack('!BB18s' if len(data) > 5 else '!BB5s', 0x10, devnumber, data)
_log.debug("(%d) <= w[10 %02X %s %s]", devnumber, devnumber, _hex(wdata[2:4]), _hex(wdata[4:])) _log.debug("<= w[10 %02X %s %s]", devnumber, _hex(wdata[2:4]), _hex(wdata[4:]))
if not _hid.write(handle, wdata): if not _hid.write(handle, wdata):
_log.warn("(%d) write failed, assuming receiver %X no longer available", devnumber, handle) _log.warn("write failed, assuming receiver %X no longer available", handle)
close(handle) close(handle)
raise _NoReceiver raise _NoReceiver
@ -191,19 +191,19 @@ def read(handle, timeout=DEFAULT_TIMEOUT):
""" """
data = _hid.read(handle, _MAX_REPLY_SIZE, timeout) data = _hid.read(handle, _MAX_REPLY_SIZE, timeout)
if data is None: if data is None:
_log.warn("(-) read failed, assuming receiver %X no longer available", handle) _log.warn("read failed, assuming receiver %X no longer available", handle)
close(handle) close(handle)
raise _NoReceiver raise _NoReceiver
if data: if data:
if len(data) < _MIN_REPLY_SIZE: if len(data) < _MIN_REPLY_SIZE:
_log.warn("(%d) => r[%s] read packet too short: %d bytes", ord(data[1:2]), _hex(data), len(data)) _log.warn("=> r[%s] read packet too short: %d bytes", _hex(data), len(data))
data += b'\x00' * (_MIN_REPLY_SIZE - len(data)) data += b'\x00' * (_MIN_REPLY_SIZE - len(data))
if len(data) > _MAX_REPLY_SIZE: if len(data) > _MAX_REPLY_SIZE:
_log.warn("(%d) => r[%s] read packet too long: %d bytes", ord(data[1:2]), _hex(data), len(data)) _log.warn("=> r[%s] read packet too long: %d bytes", _hex(data), len(data))
code = ord(data[:1]) code = ord(data[:1])
devnumber = ord(data[1:2]) devnumber = ord(data[1:2])
_log.debug("(%d) => r[%02X %02X %s %s]", devnumber, code, devnumber, _hex(data[2:4]), _hex(data[4:])) _log.debug("=> r[%02X %02X %s %s]", code, devnumber, _hex(data[2:4]), _hex(data[4:]))
return code, devnumber, data[2:] return code, devnumber, data[2:]
# _l.log(_LOG_LEVEL, "(-) => r[]") # _l.log(_LOG_LEVEL, "(-) => r[]")
@ -236,7 +236,7 @@ def request(handle, devnumber, feature_index_function, params=b'', features=None
if type(params) == int: if type(params) == int:
params = _pack('!B', params) params = _pack('!B', params)
# _log.debug("(%d) request {%s} params [%s]", devnumber, _hex(feature_index_function), _hex(params)) # _log.debug("device %d request {%s} params [%s]", devnumber, _hex(feature_index_function), _hex(params))
if len(feature_index_function) != 2: if len(feature_index_function) != 2:
raise ValueError('invalid feature_index_function {%s}: it must be a two-byte string' % _hex(feature_index_function)) raise ValueError('invalid feature_index_function {%s}: it must be a two-byte string' % _hex(feature_index_function))
@ -263,7 +263,7 @@ def request(handle, devnumber, feature_index_function, params=b'', features=None
if reply_devnumber != devnumber: if reply_devnumber != devnumber:
# this message not for the device we're interested in # this message not for the device we're interested in
# _l.log(_LOG_LEVEL, "(%d) request got reply for unexpected device %d: [%s]", devnumber, reply_devnumber, _hex(reply_data)) # _l.log(_LOG_LEVEL, "device %d request got reply for unexpected device %d: [%s]", devnumber, reply_devnumber, _hex(reply_data))
# worst case scenario, this is a reply for a concurrent request # worst case scenario, this is a reply for a concurrent request
# on this receiver # on this receiver
if _unhandled: if _unhandled:
@ -272,18 +272,18 @@ def request(handle, devnumber, feature_index_function, params=b'', features=None
if reply_code == 0x10 and reply_data[:1] == b'\x8F' and reply_data[1:3] == feature_index_function: if reply_code == 0x10 and reply_data[:1] == b'\x8F' and reply_data[1:3] == feature_index_function:
# device not present # device not present
_log.debug("(%d) request ping failed on {%s} call: [%s]", devnumber, _hex(feature_index_function), _hex(reply_data)) _log.debug("device %d request ping failed on {%s} call: [%s]", devnumber, _hex(feature_index_function), _hex(reply_data))
return None return None
if reply_code == 0x10 and reply_data[:1] == b'\x8F': if reply_code == 0x10 and reply_data[:1] == b'\x8F':
# device not present # device not present
_log.debug("(%d) request ping failed: [%s]", devnumber, _hex(reply_data)) _log.debug("request ping failed: [%s]", devnumber, _hex(reply_data))
return None return None
if reply_code == 0x11 and reply_data[0] == b'\xFF' and reply_data[1:3] == feature_index_function: if reply_code == 0x11 and reply_data[0] == b'\xFF' and reply_data[1:3] == feature_index_function:
# the feature call returned with an error # the feature call returned with an error
error_code = ord(reply_data[3]) error_code = ord(reply_data[3])
_log.warn("(%d) request feature call error %d = %s: %s", devnumber, error_code, ERROR_NAME[error_code], _hex(reply_data)) _log.warn("device %d request feature call error %d = %s: %s", devnumber, error_code, ERROR_NAME[error_code], _hex(reply_data))
feature_index = ord(feature_index_function[:1]) feature_index = ord(feature_index_function[:1])
feature_function = feature_index_function[1:2] feature_function = feature_index_function[1:2]
feature = None if features is None else features[feature_index] if feature_index < len(features) else None feature = None if features is None else features[feature_index] if feature_index < len(features) else None
@ -291,14 +291,14 @@ def request(handle, devnumber, feature_index_function, params=b'', features=None
if reply_code == 0x11 and reply_data[:2] == feature_index_function: if reply_code == 0x11 and reply_data[:2] == feature_index_function:
# a matching reply # a matching reply
# _log.debug("(%d) matched reply with feature-index-function [%s]", devnumber, _hex(reply_data[2:])) # _log.debug("device %d matched reply with feature-index-function [%s]", devnumber, _hex(reply_data[2:]))
return reply_data[2:] return reply_data[2:]
if reply_code == 0x10 and devnumber == 0xFF and reply_data[:2] == feature_index_function: if reply_code == 0x10 and devnumber == 0xFF and reply_data[:2] == feature_index_function:
# direct calls to the receiver (device 0xFF) may also return successfully with reply code 0x10 # direct calls to the receiver (device 0xFF) may also return successfully with reply code 0x10
# _log.debug("(%d) matched reply with feature-index-function [%s]", devnumber, _hex(reply_data[2:])) # _log.debug("device %d matched reply with feature-index-function [%s]", devnumber, _hex(reply_data[2:]))
return reply_data[2:] return reply_data[2:]
# _log.debug("(%d) unmatched reply {%s} (expected {%s})", devnumber, _hex(reply_data[:2]), _hex(feature_index_function)) # _log.debug("device %d unmatched reply {%s} (expected {%s})", devnumber, _hex(reply_data[:2]), _hex(feature_index_function))
if _unhandled: if _unhandled:
_unhandled(reply_code, reply_devnumber, reply_data) _unhandled(reply_code, reply_devnumber, reply_data)

View File

@ -21,7 +21,7 @@ _log = getLogger('LUR').getChild('listener')
del getLogger del getLogger
_READ_EVENT_TIMEOUT = int(_base.DEFAULT_TIMEOUT) # ms _READ_EVENT_TIMEOUT = int(_base.DEFAULT_TIMEOUT / 2) # ms
def _event_dispatch(listener, callback): def _event_dispatch(listener, callback):
while listener._active: # or not listener._events.empty(): while listener._active: # or not listener._events.empty():