yapf: change code style to yapf
Signed-off-by: Filipe Laíns <lains@archlinux.org>
This commit is contained in:
parent
cab523e122
commit
72a8d311bc
56
bin/solaar
56
bin/solaar
|
|
@ -22,36 +22,38 @@ from __future__ import absolute_import, unicode_literals
|
|||
|
||||
|
||||
def init_paths():
|
||||
"""Make the app work in the source tree."""
|
||||
import sys
|
||||
import os.path as _path
|
||||
"""Make the app work in the source tree."""
|
||||
import sys
|
||||
import os.path as _path
|
||||
|
||||
# Python 2 need conversion from utf-8 filenames
|
||||
# Python 3 might have problems converting back to UTF-8 in case of Unicode surrogates
|
||||
try:
|
||||
if sys.version_info < (3,):
|
||||
decoded_path = sys.path[0].decode(sys.getfilesystemencoding())
|
||||
else:
|
||||
decoded_path = sys.path[0]
|
||||
sys.path[0].encode(sys.getfilesystemencoding())
|
||||
# Python 2 need conversion from utf-8 filenames
|
||||
# Python 3 might have problems converting back to UTF-8 in case of Unicode surrogates
|
||||
try:
|
||||
if sys.version_info < (3, ):
|
||||
decoded_path = sys.path[0].decode(sys.getfilesystemencoding())
|
||||
else:
|
||||
decoded_path = sys.path[0]
|
||||
sys.path[0].encode(sys.getfilesystemencoding())
|
||||
|
||||
except UnicodeError:
|
||||
sys.stderr.write('ERROR: Solaar cannot recognize encoding of filesystem path, this may happen because non UTF-8 characters in the pathname.\n')
|
||||
sys.exit(1)
|
||||
except UnicodeError:
|
||||
sys.stderr.write(
|
||||
'ERROR: Solaar cannot recognize encoding of filesystem path, this may happen because non UTF-8 characters in the pathname.\n'
|
||||
)
|
||||
sys.exit(1)
|
||||
|
||||
prefix = _path.normpath(_path.join(_path.realpath(decoded_path), '..'))
|
||||
src_lib = _path.join(prefix, 'lib')
|
||||
share_lib = _path.join(prefix, 'share', 'solaar', 'lib')
|
||||
for location in src_lib, share_lib:
|
||||
init_py = _path.join(location, 'solaar', '__init__.py')
|
||||
# print ("sys.path[0]: checking", init_py)
|
||||
if _path.exists(init_py):
|
||||
# print ("sys.path[0]: found", location, "replacing", sys.path[0])
|
||||
sys.path[0] = location
|
||||
break
|
||||
prefix = _path.normpath(_path.join(_path.realpath(decoded_path), '..'))
|
||||
src_lib = _path.join(prefix, 'lib')
|
||||
share_lib = _path.join(prefix, 'share', 'solaar', 'lib')
|
||||
for location in src_lib, share_lib:
|
||||
init_py = _path.join(location, 'solaar', '__init__.py')
|
||||
# print ("sys.path[0]: checking", init_py)
|
||||
if _path.exists(init_py):
|
||||
# print ("sys.path[0]: found", location, "replacing", sys.path[0])
|
||||
sys.path[0] = location
|
||||
break
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
init_paths()
|
||||
import solaar.gtk
|
||||
solaar.gtk.main()
|
||||
init_paths()
|
||||
import solaar.gtk
|
||||
solaar.gtk.main()
|
||||
|
|
|
|||
|
|
@ -22,22 +22,24 @@ from __future__ import absolute_import, unicode_literals
|
|||
|
||||
|
||||
def init_paths():
|
||||
"""Make the app work in the source tree."""
|
||||
import sys
|
||||
import os.path as _path
|
||||
"""Make the app work in the source tree."""
|
||||
import sys
|
||||
import os.path as _path
|
||||
|
||||
prefix = _path.normpath(_path.join(_path.realpath(sys.path[0]), '..'))
|
||||
src_lib = _path.join(prefix, 'lib')
|
||||
share_lib = _path.join(prefix, 'share', 'solaar', 'lib')
|
||||
for location in src_lib, share_lib:
|
||||
init_py = _path.join(location, 'solaar', '__init__.py')
|
||||
if _path.exists(init_py):
|
||||
sys.path[0] = location
|
||||
break
|
||||
prefix = _path.normpath(_path.join(_path.realpath(sys.path[0]), '..'))
|
||||
src_lib = _path.join(prefix, 'lib')
|
||||
share_lib = _path.join(prefix, 'share', 'solaar', 'lib')
|
||||
for location in src_lib, share_lib:
|
||||
init_py = _path.join(location, 'solaar', '__init__.py')
|
||||
if _path.exists(init_py):
|
||||
sys.path[0] = location
|
||||
break
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
print ('WARNING: solaar-cli is deprecated; use solaar with the usual arguments')
|
||||
init_paths()
|
||||
import solaar.cli
|
||||
solaar.cli.run()
|
||||
print(
|
||||
'WARNING: solaar-cli is deprecated; use solaar with the usual arguments'
|
||||
)
|
||||
init_paths()
|
||||
import solaar.cli
|
||||
solaar.cli.run()
|
||||
|
|
|
|||
|
|
@ -16,7 +16,6 @@
|
|||
## You should have received a copy of the GNU General Public License along
|
||||
## with this program; if not, write to the Free Software Foundation, Inc.,
|
||||
## 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||
|
||||
"""Generic Human Interface Device API."""
|
||||
|
||||
from __future__ import absolute_import, division, print_function, unicode_literals
|
||||
|
|
@ -24,14 +23,14 @@ from __future__ import absolute_import, division, print_function, unicode_litera
|
|||
__version__ = '0.9'
|
||||
|
||||
from hidapi.udev import (
|
||||
enumerate,
|
||||
open,
|
||||
close,
|
||||
open_path,
|
||||
monitor_glib,
|
||||
read,
|
||||
write,
|
||||
get_manufacturer,
|
||||
get_product,
|
||||
get_serial,
|
||||
)
|
||||
enumerate,
|
||||
open,
|
||||
close,
|
||||
open_path,
|
||||
monitor_glib,
|
||||
read,
|
||||
write,
|
||||
get_manufacturer,
|
||||
get_product,
|
||||
get_serial,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -31,10 +31,10 @@ import hidapi as _hid
|
|||
#
|
||||
|
||||
try:
|
||||
read_packet = raw_input
|
||||
read_packet = raw_input
|
||||
except NameError:
|
||||
# Python 3 equivalent of raw_input
|
||||
read_packet = input
|
||||
# Python 3 equivalent of raw_input
|
||||
read_packet = input
|
||||
|
||||
interactive = os.isatty(0)
|
||||
prompt = '?? Input: ' if interactive else ''
|
||||
|
|
@ -42,18 +42,18 @@ start_time = time.time()
|
|||
|
||||
strhex = lambda d: hexlify(d).decode('ascii').upper()
|
||||
try:
|
||||
unicode
|
||||
# this is certanly Python 2
|
||||
is_string = lambda d: isinstance(d, unicode)
|
||||
# no easy way to distinguish between b'' and '' :(
|
||||
# or (isinstance(d, str) \
|
||||
# and not any((chr(k) in d for k in range(0x00, 0x1F))) \
|
||||
# and not any((chr(k) in d for k in range(0x80, 0xFF))) \
|
||||
# )
|
||||
unicode
|
||||
# this is certanly Python 2
|
||||
is_string = lambda d: isinstance(d, unicode)
|
||||
# no easy way to distinguish between b'' and '' :(
|
||||
# or (isinstance(d, str) \
|
||||
# and not any((chr(k) in d for k in range(0x00, 0x1F))) \
|
||||
# and not any((chr(k) in d for k in range(0x80, 0xFF))) \
|
||||
# )
|
||||
except:
|
||||
# this is certanly Python 3
|
||||
# In Py3, unicode and str are equal (the unicode object does not exist)
|
||||
is_string = lambda d: isinstance(d, str)
|
||||
# this is certanly Python 3
|
||||
# In Py3, unicode and str are equal (the unicode object does not exist)
|
||||
is_string = lambda d: isinstance(d, str)
|
||||
|
||||
#
|
||||
#
|
||||
|
|
@ -63,196 +63,215 @@ from threading import Lock
|
|||
print_lock = Lock()
|
||||
del Lock
|
||||
|
||||
|
||||
def _print(marker, data, scroll=False):
|
||||
t = time.time() - start_time
|
||||
if is_string(data):
|
||||
s = marker + ' ' + data
|
||||
else:
|
||||
hexs = strhex(data)
|
||||
s = '%s (% 8.3f) [%s %s %s %s] %s' % (marker, t, hexs[0:2], hexs[2:4], hexs[4:8], hexs[8:], repr(data))
|
||||
t = time.time() - start_time
|
||||
if is_string(data):
|
||||
s = marker + ' ' + data
|
||||
else:
|
||||
hexs = strhex(data)
|
||||
s = '%s (% 8.3f) [%s %s %s %s] %s' % (marker, t, hexs[0:2], hexs[2:4],
|
||||
hexs[4:8], hexs[8:], repr(data))
|
||||
|
||||
with print_lock:
|
||||
# allow only one thread at a time to write to the console, otherwise
|
||||
# the output gets garbled, especially with ANSI codes.
|
||||
with print_lock:
|
||||
# allow only one thread at a time to write to the console, otherwise
|
||||
# the output gets garbled, especially with ANSI codes.
|
||||
|
||||
if interactive and scroll:
|
||||
# scroll the entire screen above the current line up by 1 line
|
||||
sys.stdout.write('\033[s' # save cursor position
|
||||
'\033[S' # scroll up
|
||||
'\033[A' # cursor up
|
||||
'\033[L' # insert 1 line
|
||||
'\033[G') # move cursor to column 1
|
||||
sys.stdout.write(s)
|
||||
if interactive and scroll:
|
||||
# restore cursor position
|
||||
sys.stdout.write('\033[u')
|
||||
else:
|
||||
sys.stdout.write('\n')
|
||||
if interactive and scroll:
|
||||
# scroll the entire screen above the current line up by 1 line
|
||||
sys.stdout.write('\033[s' # save cursor position
|
||||
'\033[S' # scroll up
|
||||
'\033[A' # cursor up
|
||||
'\033[L' # insert 1 line
|
||||
'\033[G') # move cursor to column 1
|
||||
sys.stdout.write(s)
|
||||
if interactive and scroll:
|
||||
# restore cursor position
|
||||
sys.stdout.write('\033[u')
|
||||
else:
|
||||
sys.stdout.write('\n')
|
||||
|
||||
# flush stdout manually...
|
||||
# because trying to open stdin/out unbuffered programmatically
|
||||
# works much too differently in Python 2/3
|
||||
sys.stdout.flush()
|
||||
# flush stdout manually...
|
||||
# because trying to open stdin/out unbuffered programmatically
|
||||
# works much too differently in Python 2/3
|
||||
sys.stdout.flush()
|
||||
|
||||
|
||||
def _error(text, scroll=False):
|
||||
_print('!!', text, scroll)
|
||||
_print('!!', text, scroll)
|
||||
|
||||
|
||||
def _continuous_read(handle, timeout=2000):
|
||||
while True:
|
||||
try:
|
||||
reply = _hid.read(handle, 128, timeout)
|
||||
except OSError as e:
|
||||
_error("Read failed, aborting: " + str(e), True)
|
||||
break
|
||||
assert reply is not None
|
||||
if reply:
|
||||
_print('>>', reply, True)
|
||||
while True:
|
||||
try:
|
||||
reply = _hid.read(handle, 128, timeout)
|
||||
except OSError as e:
|
||||
_error("Read failed, aborting: " + str(e), True)
|
||||
break
|
||||
assert reply is not None
|
||||
if reply:
|
||||
_print('>>', reply, True)
|
||||
|
||||
|
||||
def _validate_input(line, hidpp=False):
|
||||
try:
|
||||
data = unhexlify(line.encode('ascii'))
|
||||
except Exception as e:
|
||||
_error("Invalid input: " + str(e))
|
||||
return None
|
||||
try:
|
||||
data = unhexlify(line.encode('ascii'))
|
||||
except Exception as e:
|
||||
_error("Invalid input: " + str(e))
|
||||
return None
|
||||
|
||||
if hidpp:
|
||||
if len(data) < 4:
|
||||
_error("Invalid HID++ request: need at least 4 bytes")
|
||||
return None
|
||||
if data[:1] not in b'\x10\x11':
|
||||
_error("Invalid HID++ request: first byte must be 0x10 or 0x11")
|
||||
return None
|
||||
if data[1:2] not in b'\xFF\x01\x02\x03\x04\x05\x06':
|
||||
_error("Invalid HID++ request: second byte must be 0xFF or one of 0x01..0x06")
|
||||
return None
|
||||
if data[:1] == b'\x10':
|
||||
if len(data) > 7:
|
||||
_error("Invalid HID++ request: maximum length of a 0x10 request is 7 bytes")
|
||||
return None
|
||||
while len(data) < 7:
|
||||
data = (data + b'\x00' * 7)[:7]
|
||||
elif data[:1] == b'\x11':
|
||||
if len(data) > 20:
|
||||
_error("Invalid HID++ request: maximum length of a 0x11 request is 20 bytes")
|
||||
return None
|
||||
while len(data) < 20:
|
||||
data = (data + b'\x00' * 20)[:20]
|
||||
if hidpp:
|
||||
if len(data) < 4:
|
||||
_error("Invalid HID++ request: need at least 4 bytes")
|
||||
return None
|
||||
if data[:1] not in b'\x10\x11':
|
||||
_error("Invalid HID++ request: first byte must be 0x10 or 0x11")
|
||||
return None
|
||||
if data[1:2] not in b'\xFF\x01\x02\x03\x04\x05\x06':
|
||||
_error(
|
||||
"Invalid HID++ request: second byte must be 0xFF or one of 0x01..0x06"
|
||||
)
|
||||
return None
|
||||
if data[:1] == b'\x10':
|
||||
if len(data) > 7:
|
||||
_error(
|
||||
"Invalid HID++ request: maximum length of a 0x10 request is 7 bytes"
|
||||
)
|
||||
return None
|
||||
while len(data) < 7:
|
||||
data = (data + b'\x00' * 7)[:7]
|
||||
elif data[:1] == b'\x11':
|
||||
if len(data) > 20:
|
||||
_error(
|
||||
"Invalid HID++ request: maximum length of a 0x11 request is 20 bytes"
|
||||
)
|
||||
return None
|
||||
while len(data) < 20:
|
||||
data = (data + b'\x00' * 20)[:20]
|
||||
|
||||
return data
|
||||
return data
|
||||
|
||||
|
||||
def _open(args):
|
||||
device = args.device
|
||||
if args.hidpp and not device:
|
||||
for d in _hid.enumerate(vendor_id=0x046d):
|
||||
if d.driver == 'logitech-djreceiver':
|
||||
device = d.path
|
||||
break
|
||||
if not device:
|
||||
sys.exit("!! No HID++ receiver found.")
|
||||
if not device:
|
||||
sys.exit("!! Device path required.")
|
||||
device = args.device
|
||||
if args.hidpp and not device:
|
||||
for d in _hid.enumerate(vendor_id=0x046d):
|
||||
if d.driver == 'logitech-djreceiver':
|
||||
device = d.path
|
||||
break
|
||||
if not device:
|
||||
sys.exit("!! No HID++ receiver found.")
|
||||
if not device:
|
||||
sys.exit("!! Device path required.")
|
||||
|
||||
print (".. Opening device", device)
|
||||
handle = _hid.open_path(device)
|
||||
if not handle:
|
||||
sys.exit("!! Failed to open %s, aborting." % device)
|
||||
print(".. Opening device", device)
|
||||
handle = _hid.open_path(device)
|
||||
if not handle:
|
||||
sys.exit("!! Failed to open %s, aborting." % device)
|
||||
|
||||
print (".. Opened handle %r, vendor %r product %r serial %r." % (
|
||||
handle,
|
||||
_hid.get_manufacturer(handle),
|
||||
_hid.get_product(handle),
|
||||
_hid.get_serial(handle)))
|
||||
if args.hidpp:
|
||||
if _hid.get_manufacturer(handle) != b'Logitech':
|
||||
sys.exit("!! Only Logitech devices support the HID++ protocol.")
|
||||
print (".. HID++ validation enabled.")
|
||||
else:
|
||||
if (_hid.get_manufacturer(handle) == b'Logitech' and
|
||||
b'Receiver' in _hid.get_product(handle)):
|
||||
args.hidpp = True
|
||||
print (".. Logitech receiver detected, HID++ validation enabled.")
|
||||
print(".. Opened handle %r, vendor %r product %r serial %r." %
|
||||
(handle, _hid.get_manufacturer(handle), _hid.get_product(handle),
|
||||
_hid.get_serial(handle)))
|
||||
if args.hidpp:
|
||||
if _hid.get_manufacturer(handle) != b'Logitech':
|
||||
sys.exit("!! Only Logitech devices support the HID++ protocol.")
|
||||
print(".. HID++ validation enabled.")
|
||||
else:
|
||||
if (_hid.get_manufacturer(handle) == b'Logitech'
|
||||
and b'Receiver' in _hid.get_product(handle)):
|
||||
args.hidpp = True
|
||||
print(".. Logitech receiver detected, HID++ validation enabled.")
|
||||
|
||||
return handle
|
||||
|
||||
return handle
|
||||
|
||||
#
|
||||
#
|
||||
#
|
||||
|
||||
|
||||
def _parse_arguments():
|
||||
import argparse
|
||||
arg_parser = argparse.ArgumentParser()
|
||||
arg_parser.add_argument('--history', help="history file (default ~/.hidconsole-history)")
|
||||
arg_parser.add_argument('--hidpp', action='store_true', help="ensure input data is a valid HID++ request")
|
||||
arg_parser.add_argument('device', nargs='?', help="linux device to connect to (/dev/hidrawX); "
|
||||
"may be omitted if --hidpp is given, in which case it looks for the first Logitech receiver")
|
||||
return arg_parser.parse_args()
|
||||
import argparse
|
||||
arg_parser = argparse.ArgumentParser()
|
||||
arg_parser.add_argument(
|
||||
'--history', help="history file (default ~/.hidconsole-history)")
|
||||
arg_parser.add_argument('--hidpp',
|
||||
action='store_true',
|
||||
help="ensure input data is a valid HID++ request")
|
||||
arg_parser.add_argument(
|
||||
'device',
|
||||
nargs='?',
|
||||
help="linux device to connect to (/dev/hidrawX); "
|
||||
"may be omitted if --hidpp is given, in which case it looks for the first Logitech receiver"
|
||||
)
|
||||
return arg_parser.parse_args()
|
||||
|
||||
|
||||
def main():
|
||||
args = _parse_arguments()
|
||||
handle = _open(args)
|
||||
args = _parse_arguments()
|
||||
handle = _open(args)
|
||||
|
||||
if interactive:
|
||||
print (".. Press ^C/^D to exit, or type hex bytes to write to the device.")
|
||||
if interactive:
|
||||
print(
|
||||
".. Press ^C/^D to exit, or type hex bytes to write to the device."
|
||||
)
|
||||
|
||||
import readline
|
||||
if args.history is None:
|
||||
import os.path
|
||||
args.history = os.path.join(os.path.expanduser('~'), '.hidconsole-history')
|
||||
try:
|
||||
readline.read_history_file(args.history)
|
||||
except:
|
||||
# file may not exist yet
|
||||
pass
|
||||
import readline
|
||||
if args.history is None:
|
||||
import os.path
|
||||
args.history = os.path.join(os.path.expanduser('~'),
|
||||
'.hidconsole-history')
|
||||
try:
|
||||
readline.read_history_file(args.history)
|
||||
except:
|
||||
# file may not exist yet
|
||||
pass
|
||||
|
||||
try:
|
||||
from threading import Thread
|
||||
t = Thread(target=_continuous_read, args=(handle,))
|
||||
t.daemon = True
|
||||
t.start()
|
||||
try:
|
||||
from threading import Thread
|
||||
t = Thread(target=_continuous_read, args=(handle, ))
|
||||
t.daemon = True
|
||||
t.start()
|
||||
|
||||
if interactive:
|
||||
# move the cursor at the bottom of the screen
|
||||
sys.stdout.write('\033[300B') # move cusor at most 300 lines down, don't scroll
|
||||
if interactive:
|
||||
# move the cursor at the bottom of the screen
|
||||
sys.stdout.write(
|
||||
'\033[300B') # move cusor at most 300 lines down, don't scroll
|
||||
|
||||
while t.is_alive():
|
||||
line = read_packet(prompt)
|
||||
line = line.strip().replace(' ', '')
|
||||
# print ("line", line)
|
||||
if not line:
|
||||
continue
|
||||
while t.is_alive():
|
||||
line = read_packet(prompt)
|
||||
line = line.strip().replace(' ', '')
|
||||
# print ("line", line)
|
||||
if not line:
|
||||
continue
|
||||
|
||||
data = _validate_input(line, args.hidpp)
|
||||
if data is None:
|
||||
continue
|
||||
data = _validate_input(line, args.hidpp)
|
||||
if data is None:
|
||||
continue
|
||||
|
||||
_print('<<', data)
|
||||
_hid.write(handle, data)
|
||||
# wait for some kind of reply
|
||||
if args.hidpp and not interactive:
|
||||
rlist, wlist, xlist = _select([handle], [], [], 1)
|
||||
if data[1:2] == b'\xFF':
|
||||
# the receiver will reply very fast, in a few milliseconds
|
||||
time.sleep(0.010)
|
||||
else:
|
||||
# the devices might reply quite slow
|
||||
time.sleep(0.700)
|
||||
except EOFError:
|
||||
if interactive:
|
||||
print ("")
|
||||
else:
|
||||
time.sleep(1)
|
||||
_print('<<', data)
|
||||
_hid.write(handle, data)
|
||||
# wait for some kind of reply
|
||||
if args.hidpp and not interactive:
|
||||
rlist, wlist, xlist = _select([handle], [], [], 1)
|
||||
if data[1:2] == b'\xFF':
|
||||
# the receiver will reply very fast, in a few milliseconds
|
||||
time.sleep(0.010)
|
||||
else:
|
||||
# the devices might reply quite slow
|
||||
time.sleep(0.700)
|
||||
except EOFError:
|
||||
if interactive:
|
||||
print("")
|
||||
else:
|
||||
time.sleep(1)
|
||||
|
||||
finally:
|
||||
print (".. Closing handle %r" % handle)
|
||||
_hid.close(handle)
|
||||
if interactive:
|
||||
readline.write_history_file(args.history)
|
||||
finally:
|
||||
print(".. Closing handle %r" % handle)
|
||||
_hid.close(handle)
|
||||
if interactive:
|
||||
readline.write_history_file(args.history)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
main()
|
||||
|
|
|
|||
|
|
@ -16,7 +16,6 @@
|
|||
## You should have received a copy of the GNU General Public License along
|
||||
## with this program; if not, write to the Free Software Foundation, Inc.,
|
||||
## 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||
|
||||
"""Generic Human Interface Device API.
|
||||
|
||||
It is currently a partial pure-Python implementation of the native HID API
|
||||
|
|
@ -35,170 +34,173 @@ from select import select as _select
|
|||
from pyudev import Context as _Context, Monitor as _Monitor, Device as _Device
|
||||
from pyudev import DeviceNotFoundError
|
||||
|
||||
|
||||
native_implementation = 'udev'
|
||||
|
||||
|
||||
# the tuple object we'll expose when enumerating devices
|
||||
from collections import namedtuple
|
||||
DeviceInfo = namedtuple('DeviceInfo', [
|
||||
'path',
|
||||
'vendor_id',
|
||||
'product_id',
|
||||
'serial',
|
||||
'release',
|
||||
'manufacturer',
|
||||
'product',
|
||||
'interface',
|
||||
'driver',
|
||||
])
|
||||
'path',
|
||||
'vendor_id',
|
||||
'product_id',
|
||||
'serial',
|
||||
'release',
|
||||
'manufacturer',
|
||||
'product',
|
||||
'interface',
|
||||
'driver',
|
||||
])
|
||||
del namedtuple
|
||||
|
||||
|
||||
#
|
||||
# exposed API
|
||||
# docstrings mostly copied from hidapi.h
|
||||
#
|
||||
|
||||
|
||||
def init():
|
||||
"""This function is a no-op, and exists only to match the native hidapi
|
||||
"""This function is a no-op, and exists only to match the native hidapi
|
||||
implementation.
|
||||
|
||||
:returns: ``True``.
|
||||
"""
|
||||
return True
|
||||
return True
|
||||
|
||||
|
||||
def exit():
|
||||
"""This function is a no-op, and exists only to match the native hidapi
|
||||
"""This function is a no-op, and exists only to match the native hidapi
|
||||
implementation.
|
||||
|
||||
:returns: ``True``.
|
||||
"""
|
||||
return True
|
||||
return True
|
||||
|
||||
|
||||
# The filter is used to determine whether this is a device of interest to Solaar
|
||||
def _match(action, device, filter):
|
||||
vendor_id=filter.get('vendor_id')
|
||||
product_id=filter.get('product_id')
|
||||
interface_number=filter.get('usb_interface')
|
||||
hid_driver=filter.get('hid_driver')
|
||||
vendor_id = filter.get('vendor_id')
|
||||
product_id = filter.get('product_id')
|
||||
interface_number = filter.get('usb_interface')
|
||||
hid_driver = filter.get('hid_driver')
|
||||
|
||||
usb_device = device.find_parent('usb', 'usb_device')
|
||||
# print ("* parent", action, device, "usb:", usb_device)
|
||||
if not usb_device:
|
||||
return
|
||||
usb_device = device.find_parent('usb', 'usb_device')
|
||||
# print ("* parent", action, device, "usb:", usb_device)
|
||||
if not usb_device:
|
||||
return
|
||||
|
||||
vid = usb_device.get('ID_VENDOR_ID')
|
||||
pid = usb_device.get('ID_MODEL_ID')
|
||||
if vid is None or pid is None:
|
||||
return # there are reports that sometimes the usb_device isn't set up right so be defensive
|
||||
if not ((vendor_id is None or vendor_id == int(vid, 16)) and
|
||||
(product_id is None or product_id == int(pid, 16))):
|
||||
return
|
||||
vid = usb_device.get('ID_VENDOR_ID')
|
||||
pid = usb_device.get('ID_MODEL_ID')
|
||||
if vid is None or pid is None:
|
||||
return # there are reports that sometimes the usb_device isn't set up right so be defensive
|
||||
if not ((vendor_id is None or vendor_id == int(vid, 16)) and
|
||||
(product_id is None or product_id == int(pid, 16))):
|
||||
return
|
||||
|
||||
if action == 'add':
|
||||
hid_device = device.find_parent('hid')
|
||||
if not hid_device:
|
||||
return
|
||||
hid_driver_name = hid_device.get('DRIVER')
|
||||
# print ("** found hid", action, device, "hid:", hid_device, hid_driver_name)
|
||||
if hid_driver:
|
||||
if isinstance(hid_driver, tuple):
|
||||
if hid_driver_name not in hid_driver:
|
||||
return
|
||||
elif hid_driver_name != hid_driver:
|
||||
return
|
||||
if action == 'add':
|
||||
hid_device = device.find_parent('hid')
|
||||
if not hid_device:
|
||||
return
|
||||
hid_driver_name = hid_device.get('DRIVER')
|
||||
# print ("** found hid", action, device, "hid:", hid_device, hid_driver_name)
|
||||
if hid_driver:
|
||||
if isinstance(hid_driver, tuple):
|
||||
if hid_driver_name not in hid_driver:
|
||||
return
|
||||
elif hid_driver_name != hid_driver:
|
||||
return
|
||||
|
||||
intf_device = device.find_parent('usb', 'usb_interface')
|
||||
# print ("*** usb interface", action, device, "usb_interface:", intf_device)
|
||||
if interface_number is None:
|
||||
usb_interface = None if intf_device is None else intf_device.attributes.asint('bInterfaceNumber')
|
||||
else:
|
||||
usb_interface = None if intf_device is None else intf_device.attributes.asint('bInterfaceNumber')
|
||||
if usb_interface is None or interface_number != usb_interface:
|
||||
return
|
||||
intf_device = device.find_parent('usb', 'usb_interface')
|
||||
# print ("*** usb interface", action, device, "usb_interface:", intf_device)
|
||||
if interface_number is None:
|
||||
usb_interface = None if intf_device is None else intf_device.attributes.asint(
|
||||
'bInterfaceNumber')
|
||||
else:
|
||||
usb_interface = None if intf_device is None else intf_device.attributes.asint(
|
||||
'bInterfaceNumber')
|
||||
if usb_interface is None or interface_number != usb_interface:
|
||||
return
|
||||
|
||||
attrs = usb_device.attributes
|
||||
d_info = DeviceInfo(path=device.device_node,
|
||||
vendor_id=vid[-4:],
|
||||
product_id=pid[-4:],
|
||||
serial=hid_device.get('HID_UNIQ'),
|
||||
release=attrs.get('bcdDevice'),
|
||||
manufacturer=attrs.get('manufacturer'),
|
||||
product=attrs.get('product'),
|
||||
interface=usb_interface,
|
||||
driver=hid_driver_name)
|
||||
return d_info
|
||||
attrs = usb_device.attributes
|
||||
d_info = DeviceInfo(path=device.device_node,
|
||||
vendor_id=vid[-4:],
|
||||
product_id=pid[-4:],
|
||||
serial=hid_device.get('HID_UNIQ'),
|
||||
release=attrs.get('bcdDevice'),
|
||||
manufacturer=attrs.get('manufacturer'),
|
||||
product=attrs.get('product'),
|
||||
interface=usb_interface,
|
||||
driver=hid_driver_name)
|
||||
return d_info
|
||||
|
||||
elif action == 'remove':
|
||||
# print (dict(device), dict(usb_device))
|
||||
elif action == 'remove':
|
||||
# print (dict(device), dict(usb_device))
|
||||
|
||||
d_info = DeviceInfo(path=device.device_node,
|
||||
vendor_id=vid[-4:],
|
||||
product_id=pid[-4:],
|
||||
serial=None,
|
||||
release=None,
|
||||
manufacturer=None,
|
||||
product=None,
|
||||
interface=None,
|
||||
driver=None)
|
||||
return d_info
|
||||
d_info = DeviceInfo(path=device.device_node,
|
||||
vendor_id=vid[-4:],
|
||||
product_id=pid[-4:],
|
||||
serial=None,
|
||||
release=None,
|
||||
manufacturer=None,
|
||||
product=None,
|
||||
interface=None,
|
||||
driver=None)
|
||||
return d_info
|
||||
|
||||
|
||||
def monitor_glib(callback, *device_filters):
|
||||
from gi.repository import GLib
|
||||
from gi.repository import GLib
|
||||
|
||||
c = _Context()
|
||||
c = _Context()
|
||||
|
||||
# already existing devices
|
||||
# for device in c.list_devices(subsystem='hidraw'):
|
||||
# # print (device, dict(device), dict(device.attributes))
|
||||
# for filter in device_filters:
|
||||
# d_info = _match('add', device, *filter)
|
||||
# if d_info:
|
||||
# GLib.idle_add(callback, 'add', d_info)
|
||||
# break
|
||||
# already existing devices
|
||||
# for device in c.list_devices(subsystem='hidraw'):
|
||||
# # print (device, dict(device), dict(device.attributes))
|
||||
# for filter in device_filters:
|
||||
# d_info = _match('add', device, *filter)
|
||||
# if d_info:
|
||||
# GLib.idle_add(callback, 'add', d_info)
|
||||
# break
|
||||
|
||||
m = _Monitor.from_netlink(c)
|
||||
m.filter_by(subsystem='hidraw')
|
||||
m = _Monitor.from_netlink(c)
|
||||
m.filter_by(subsystem='hidraw')
|
||||
|
||||
def _process_udev_event(monitor, condition, cb, filters):
|
||||
if condition == GLib.IO_IN:
|
||||
event = monitor.receive_device()
|
||||
if event:
|
||||
action, device = event
|
||||
# print ("***", action, device)
|
||||
if action == 'add':
|
||||
for filter in filters:
|
||||
d_info = _match(action, device, filter)
|
||||
if d_info:
|
||||
GLib.idle_add(cb, action, d_info)
|
||||
break
|
||||
elif action == 'remove':
|
||||
# the GLib notification does _not_ match!
|
||||
pass
|
||||
return True
|
||||
def _process_udev_event(monitor, condition, cb, filters):
|
||||
if condition == GLib.IO_IN:
|
||||
event = monitor.receive_device()
|
||||
if event:
|
||||
action, device = event
|
||||
# print ("***", action, device)
|
||||
if action == 'add':
|
||||
for filter in filters:
|
||||
d_info = _match(action, device, filter)
|
||||
if d_info:
|
||||
GLib.idle_add(cb, action, d_info)
|
||||
break
|
||||
elif action == 'remove':
|
||||
# the GLib notification does _not_ match!
|
||||
pass
|
||||
return True
|
||||
|
||||
try:
|
||||
# io_add_watch_full may not be available...
|
||||
GLib.io_add_watch_full(m, GLib.PRIORITY_LOW, GLib.IO_IN, _process_udev_event, callback, device_filters)
|
||||
# print ("did io_add_watch_full")
|
||||
except AttributeError:
|
||||
try:
|
||||
# and the priority parameter appeared later in the API
|
||||
GLib.io_add_watch(m, GLib.PRIORITY_LOW, GLib.IO_IN, _process_udev_event, callback, device_filters)
|
||||
# print ("did io_add_watch with priority")
|
||||
except:
|
||||
GLib.io_add_watch(m, GLib.IO_IN, _process_udev_event, callback, device_filters)
|
||||
# print ("did io_add_watch")
|
||||
try:
|
||||
# io_add_watch_full may not be available...
|
||||
GLib.io_add_watch_full(m, GLib.PRIORITY_LOW, GLib.IO_IN,
|
||||
_process_udev_event, callback, device_filters)
|
||||
# print ("did io_add_watch_full")
|
||||
except AttributeError:
|
||||
try:
|
||||
# and the priority parameter appeared later in the API
|
||||
GLib.io_add_watch(m, GLib.PRIORITY_LOW, GLib.IO_IN,
|
||||
_process_udev_event, callback, device_filters)
|
||||
# print ("did io_add_watch with priority")
|
||||
except:
|
||||
GLib.io_add_watch(m, GLib.IO_IN, _process_udev_event, callback,
|
||||
device_filters)
|
||||
# print ("did io_add_watch")
|
||||
|
||||
m.start()
|
||||
m.start()
|
||||
|
||||
|
||||
def enumerate(usb_id):
|
||||
"""Enumerate the HID Devices.
|
||||
"""Enumerate the HID Devices.
|
||||
|
||||
List all the HID devices attached to the system, optionally filtering by
|
||||
vendor_id, product_id, and/or interface_number.
|
||||
|
|
@ -206,48 +208,48 @@ def enumerate(usb_id):
|
|||
:returns: a list of matching ``DeviceInfo`` tuples.
|
||||
"""
|
||||
|
||||
for dev in _Context().list_devices(subsystem='hidraw'):
|
||||
dev_info = _match('add', dev, usb_id)
|
||||
if dev_info:
|
||||
yield dev_info
|
||||
for dev in _Context().list_devices(subsystem='hidraw'):
|
||||
dev_info = _match('add', dev, usb_id)
|
||||
if dev_info:
|
||||
yield dev_info
|
||||
|
||||
|
||||
def open(vendor_id, product_id, serial=None):
|
||||
"""Open a HID device by its Vendor ID, Product ID and optional serial number.
|
||||
"""Open a HID device by its Vendor ID, Product ID and optional serial number.
|
||||
|
||||
If no serial is provided, the first device with the specified IDs is opened.
|
||||
|
||||
:returns: an opaque device handle, or ``None``.
|
||||
"""
|
||||
for device in enumerate(vendor_id, product_id):
|
||||
if serial is None or serial == device.serial:
|
||||
return open_path(device.path)
|
||||
for device in enumerate(vendor_id, product_id):
|
||||
if serial is None or serial == device.serial:
|
||||
return open_path(device.path)
|
||||
|
||||
|
||||
def open_path(device_path):
|
||||
"""Open a HID device by its path name.
|
||||
"""Open a HID device by its path name.
|
||||
|
||||
:param device_path: the path of a ``DeviceInfo`` tuple returned by
|
||||
enumerate().
|
||||
|
||||
:returns: an opaque device handle, or ``None``.
|
||||
"""
|
||||
assert device_path
|
||||
assert device_path.startswith('/dev/hidraw')
|
||||
return _os.open(device_path, _os.O_RDWR | _os.O_SYNC)
|
||||
assert device_path
|
||||
assert device_path.startswith('/dev/hidraw')
|
||||
return _os.open(device_path, _os.O_RDWR | _os.O_SYNC)
|
||||
|
||||
|
||||
def close(device_handle):
|
||||
"""Close a HID device.
|
||||
"""Close a HID device.
|
||||
|
||||
:param device_handle: a device handle returned by open() or open_path().
|
||||
"""
|
||||
assert device_handle
|
||||
_os.close(device_handle)
|
||||
assert device_handle
|
||||
_os.close(device_handle)
|
||||
|
||||
|
||||
def write(device_handle, data):
|
||||
"""Write an Output report to a HID device.
|
||||
"""Write an Output report to a HID device.
|
||||
|
||||
:param device_handle: a device handle returned by open() or open_path().
|
||||
:param data: the data bytes to send including the report number as the
|
||||
|
|
@ -267,26 +269,28 @@ def write(device_handle, data):
|
|||
one exists. If it does not, it will send the data through
|
||||
the Control Endpoint (Endpoint 0).
|
||||
"""
|
||||
assert device_handle
|
||||
assert data
|
||||
assert isinstance(data, bytes), (repr(data), type(data))
|
||||
retrycount = 0
|
||||
bytes_written = 0
|
||||
while(retrycount < 3):
|
||||
try:
|
||||
bytes_written = _os.write(device_handle, data)
|
||||
retrycount += 1
|
||||
except IOError as e:
|
||||
if e.errno == _errno.EPIPE:
|
||||
sleep(0.1)
|
||||
else:
|
||||
break
|
||||
if bytes_written != len(data):
|
||||
raise IOError(_errno.EIO, 'written %d bytes out of expected %d' % (bytes_written, len(data)))
|
||||
assert device_handle
|
||||
assert data
|
||||
assert isinstance(data, bytes), (repr(data), type(data))
|
||||
retrycount = 0
|
||||
bytes_written = 0
|
||||
while (retrycount < 3):
|
||||
try:
|
||||
bytes_written = _os.write(device_handle, data)
|
||||
retrycount += 1
|
||||
except IOError as e:
|
||||
if e.errno == _errno.EPIPE:
|
||||
sleep(0.1)
|
||||
else:
|
||||
break
|
||||
if bytes_written != len(data):
|
||||
raise IOError(
|
||||
_errno.EIO,
|
||||
'written %d bytes out of expected %d' % (bytes_written, len(data)))
|
||||
|
||||
|
||||
def read(device_handle, bytes_count, timeout_ms=-1):
|
||||
"""Read an Input report from a HID device.
|
||||
"""Read an Input report from a HID device.
|
||||
|
||||
:param device_handle: a device handle returned by open() or open_path().
|
||||
:param bytes_count: maximum number of bytes to read.
|
||||
|
|
@ -301,59 +305,61 @@ def read(device_handle, bytes_count, timeout_ms=-1):
|
|||
:returns: the data packet read, an empty bytes string if a timeout was
|
||||
reached, or None if there was an error while reading.
|
||||
"""
|
||||
assert device_handle
|
||||
timeout = None if timeout_ms < 0 else timeout_ms / 1000.0
|
||||
rlist, wlist, xlist = _select([device_handle], [], [device_handle], timeout)
|
||||
assert device_handle
|
||||
timeout = None if timeout_ms < 0 else timeout_ms / 1000.0
|
||||
rlist, wlist, xlist = _select([device_handle], [], [device_handle],
|
||||
timeout)
|
||||
|
||||
if xlist:
|
||||
assert xlist == [device_handle]
|
||||
raise IOError(_errno.EIO, 'exception on file descriptor %d' % device_handle)
|
||||
if xlist:
|
||||
assert xlist == [device_handle]
|
||||
raise IOError(_errno.EIO,
|
||||
'exception on file descriptor %d' % device_handle)
|
||||
|
||||
if rlist:
|
||||
assert rlist == [device_handle]
|
||||
data = _os.read(device_handle, bytes_count)
|
||||
assert data is not None
|
||||
assert isinstance(data, bytes), (repr(data), type(data))
|
||||
return data
|
||||
else:
|
||||
return b''
|
||||
if rlist:
|
||||
assert rlist == [device_handle]
|
||||
data = _os.read(device_handle, bytes_count)
|
||||
assert data is not None
|
||||
assert isinstance(data, bytes), (repr(data), type(data))
|
||||
return data
|
||||
else:
|
||||
return b''
|
||||
|
||||
|
||||
_DEVICE_STRINGS = {
|
||||
0: 'manufacturer',
|
||||
1: 'product',
|
||||
2: 'serial',
|
||||
0: 'manufacturer',
|
||||
1: 'product',
|
||||
2: 'serial',
|
||||
}
|
||||
|
||||
|
||||
def get_manufacturer(device_handle):
|
||||
"""Get the Manufacturer String from a HID device.
|
||||
"""Get the Manufacturer String from a HID device.
|
||||
|
||||
:param device_handle: a device handle returned by open() or open_path().
|
||||
"""
|
||||
return get_indexed_string(device_handle, 0)
|
||||
return get_indexed_string(device_handle, 0)
|
||||
|
||||
|
||||
def get_product(device_handle):
|
||||
"""Get the Product String from a HID device.
|
||||
"""Get the Product String from a HID device.
|
||||
|
||||
:param device_handle: a device handle returned by open() or open_path().
|
||||
"""
|
||||
return get_indexed_string(device_handle, 1)
|
||||
return get_indexed_string(device_handle, 1)
|
||||
|
||||
|
||||
def get_serial(device_handle):
|
||||
"""Get the serial number from a HID device.
|
||||
"""Get the serial number from a HID device.
|
||||
|
||||
:param device_handle: a device handle returned by open() or open_path().
|
||||
"""
|
||||
serial = get_indexed_string(device_handle, 2)
|
||||
if serial is not None:
|
||||
return ''.join(hex(ord(c)) for c in serial)
|
||||
serial = get_indexed_string(device_handle, 2)
|
||||
if serial is not None:
|
||||
return ''.join(hex(ord(c)) for c in serial)
|
||||
|
||||
|
||||
def get_indexed_string(device_handle, index):
|
||||
"""Get a string from a HID device, based on its string index.
|
||||
"""Get a string from a HID device, based on its string index.
|
||||
|
||||
Note: currently not working in the ``hidraw`` native implementation.
|
||||
|
||||
|
|
@ -362,28 +368,28 @@ def get_indexed_string(device_handle, index):
|
|||
:returns: the value corresponding to index, or None if no value found
|
||||
:rtype: bytes or NoneType
|
||||
"""
|
||||
try:
|
||||
key = _DEVICE_STRINGS[index]
|
||||
except KeyError:
|
||||
return None
|
||||
try:
|
||||
key = _DEVICE_STRINGS[index]
|
||||
except KeyError:
|
||||
return None
|
||||
|
||||
assert device_handle
|
||||
stat = _os.fstat(device_handle)
|
||||
try:
|
||||
dev = _Device.from_device_number(_Context(), 'char', stat.st_rdev)
|
||||
except (DeviceNotFoundError, ValueError):
|
||||
return None
|
||||
assert device_handle
|
||||
stat = _os.fstat(device_handle)
|
||||
try:
|
||||
dev = _Device.from_device_number(_Context(), 'char', stat.st_rdev)
|
||||
except (DeviceNotFoundError, ValueError):
|
||||
return None
|
||||
|
||||
hid_dev = dev.find_parent('hid')
|
||||
if hid_dev:
|
||||
assert 'HID_ID' in hid_dev
|
||||
bus, _ignore, _ignore = hid_dev['HID_ID'].split(':')
|
||||
hid_dev = dev.find_parent('hid')
|
||||
if hid_dev:
|
||||
assert 'HID_ID' in hid_dev
|
||||
bus, _ignore, _ignore = hid_dev['HID_ID'].split(':')
|
||||
|
||||
if bus == '0003': # USB
|
||||
usb_dev = dev.find_parent('usb', 'usb_device')
|
||||
assert usb_dev
|
||||
return usb_dev.attributes.get(key)
|
||||
if bus == '0003': # USB
|
||||
usb_dev = dev.find_parent('usb', 'usb_device')
|
||||
assert usb_dev
|
||||
return usb_dev.attributes.get(key)
|
||||
|
||||
elif bus == '0005': # BLUETOOTH
|
||||
# TODO
|
||||
pass
|
||||
elif bus == '0005': # BLUETOOTH
|
||||
# TODO
|
||||
pass
|
||||
|
|
|
|||
|
|
@ -16,7 +16,6 @@
|
|||
## You should have received a copy of the GNU General Public License along
|
||||
## with this program; if not, write to the Free Software Foundation, Inc.,
|
||||
## 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||
|
||||
"""Low-level interface for devices connected through a Logitech Universal
|
||||
Receiver (UR).
|
||||
|
||||
|
|
@ -43,10 +42,8 @@ _log.setLevel(logging.root.level)
|
|||
|
||||
del logging
|
||||
|
||||
|
||||
__version__ = '0.9'
|
||||
|
||||
|
||||
from .common import strhex
|
||||
from .base import NoReceiver, NoSuchDevice, DeviceUnreachable
|
||||
from .receiver import Receiver, PairedDevice
|
||||
|
|
|
|||
|
|
@ -29,7 +29,6 @@ from logging import getLogger, DEBUG as _DEBUG
|
|||
_log = getLogger(__name__)
|
||||
del getLogger
|
||||
|
||||
|
||||
from .common import strhex as _strhex, KwException as _KwException, pack as _pack
|
||||
from . import hidpp10 as _hidpp10
|
||||
from . import hidpp20 as _hidpp20
|
||||
|
|
@ -46,13 +45,11 @@ _MAX_READ_SIZE = 32
|
|||
|
||||
# mapping from report_id to message length
|
||||
report_lengths = {
|
||||
0x10: _SHORT_MESSAGE_SIZE,
|
||||
0x11: _LONG_MESSAGE_SIZE,
|
||||
0x20: _MEDIUM_MESSAGE_SIZE,
|
||||
0x21: _MAX_READ_SIZE
|
||||
0x10: _SHORT_MESSAGE_SIZE,
|
||||
0x11: _LONG_MESSAGE_SIZE,
|
||||
0x20: _MEDIUM_MESSAGE_SIZE,
|
||||
0x21: _MAX_READ_SIZE
|
||||
}
|
||||
|
||||
|
||||
"""Default timeout on read (in seconds)."""
|
||||
DEFAULT_TIMEOUT = 4
|
||||
# the receiver itself should reply very fast, within 500ms
|
||||
|
|
@ -66,22 +63,24 @@ _PING_TIMEOUT = DEFAULT_TIMEOUT * 2
|
|||
# Exceptions that may be raised by this API.
|
||||
#
|
||||
|
||||
|
||||
class NoReceiver(_KwException):
|
||||
"""Raised when trying to talk through a previously open handle, when the
|
||||
"""Raised when trying to talk through a previously open handle, when the
|
||||
receiver is no longer available. Should only happen if the receiver is
|
||||
physically disconnected from the machine, or its kernel driver module is
|
||||
unloaded."""
|
||||
pass
|
||||
pass
|
||||
|
||||
|
||||
class NoSuchDevice(_KwException):
|
||||
"""Raised when trying to reach a device number not paired to the receiver."""
|
||||
pass
|
||||
"""Raised when trying to reach a device number not paired to the receiver."""
|
||||
pass
|
||||
|
||||
|
||||
class DeviceUnreachable(_KwException):
|
||||
"""Raised when a request is made to an unreachable (turned off) device."""
|
||||
pass
|
||||
"""Raised when a request is made to an unreachable (turned off) device."""
|
||||
pass
|
||||
|
||||
|
||||
#
|
||||
#
|
||||
|
|
@ -89,23 +88,26 @@ class DeviceUnreachable(_KwException):
|
|||
|
||||
from .base_usb import ALL as _RECEIVER_USB_IDS
|
||||
|
||||
|
||||
def receivers():
|
||||
"""List all the Linux devices exposed by the UR attached to the machine."""
|
||||
for receiver_usb_id in _RECEIVER_USB_IDS:
|
||||
for d in _hid.enumerate(receiver_usb_id):
|
||||
yield d
|
||||
"""List all the Linux devices exposed by the UR attached to the machine."""
|
||||
for receiver_usb_id in _RECEIVER_USB_IDS:
|
||||
for d in _hid.enumerate(receiver_usb_id):
|
||||
yield d
|
||||
|
||||
|
||||
def notify_on_receivers_glib(callback):
|
||||
"""Watch for matching devices and notifies the callback on the GLib thread."""
|
||||
_hid.monitor_glib(callback, *_RECEIVER_USB_IDS)
|
||||
"""Watch for matching devices and notifies the callback on the GLib thread."""
|
||||
_hid.monitor_glib(callback, *_RECEIVER_USB_IDS)
|
||||
|
||||
|
||||
#
|
||||
#
|
||||
#
|
||||
|
||||
|
||||
def open_path(path):
|
||||
"""Checks if the given Linux device path points to the right UR device.
|
||||
"""Checks if the given Linux device path points to the right UR device.
|
||||
|
||||
:param path: the Linux device path.
|
||||
|
||||
|
|
@ -117,39 +119,39 @@ def open_path(path):
|
|||
:returns: an open receiver handle if this is the right Linux device, or
|
||||
``None``.
|
||||
"""
|
||||
return _hid.open_path(path)
|
||||
return _hid.open_path(path)
|
||||
|
||||
|
||||
def open():
|
||||
"""Opens the first Logitech Unifying Receiver found attached to the machine.
|
||||
"""Opens the first Logitech Unifying Receiver found attached to the machine.
|
||||
|
||||
:returns: An open file handle for the found receiver, or ``None``.
|
||||
"""
|
||||
for rawdevice in receivers():
|
||||
handle = open_path(rawdevice.path)
|
||||
if handle:
|
||||
return handle
|
||||
for rawdevice in receivers():
|
||||
handle = open_path(rawdevice.path)
|
||||
if handle:
|
||||
return handle
|
||||
|
||||
|
||||
def close(handle):
|
||||
"""Closes a HID device handle."""
|
||||
if handle:
|
||||
try:
|
||||
if isinstance(handle, int):
|
||||
_hid.close(handle)
|
||||
else:
|
||||
handle.close()
|
||||
# _log.info("closed receiver handle %r", handle)
|
||||
return True
|
||||
except:
|
||||
# _log.exception("closing receiver handle %r", handle)
|
||||
pass
|
||||
"""Closes a HID device handle."""
|
||||
if handle:
|
||||
try:
|
||||
if isinstance(handle, int):
|
||||
_hid.close(handle)
|
||||
else:
|
||||
handle.close()
|
||||
# _log.info("closed receiver handle %r", handle)
|
||||
return True
|
||||
except:
|
||||
# _log.exception("closing receiver handle %r", handle)
|
||||
pass
|
||||
|
||||
return False
|
||||
return False
|
||||
|
||||
|
||||
def write(handle, devnumber, data):
|
||||
"""Writes some data to the receiver, addressed to a certain device.
|
||||
"""Writes some data to the receiver, addressed to a certain device.
|
||||
|
||||
:param handle: an open UR handle.
|
||||
:param devnumber: attached device number.
|
||||
|
|
@ -161,27 +163,29 @@ def write(handle, devnumber, data):
|
|||
been physically removed from the machine, or the kernel driver has been
|
||||
unloaded. The handle will be closed automatically.
|
||||
"""
|
||||
# the data is padded to either 5 or 18 bytes
|
||||
assert data is not None
|
||||
assert isinstance(data, bytes), (repr(data), type(data))
|
||||
# the data is padded to either 5 or 18 bytes
|
||||
assert data is not None
|
||||
assert isinstance(data, bytes), (repr(data), type(data))
|
||||
|
||||
if len(data) > _SHORT_MESSAGE_SIZE - 2 or data[:1] == b'\x82':
|
||||
wdata = _pack('!BB18s', 0x11, devnumber, data)
|
||||
else:
|
||||
wdata = _pack('!BB5s', 0x10, devnumber, data)
|
||||
if _log.isEnabledFor(_DEBUG):
|
||||
_log.debug("(%s) <= w[%02X %02X %s %s]", handle, ord(wdata[:1]), devnumber, _strhex(wdata[2:4]), _strhex(wdata[4:]))
|
||||
if len(data) > _SHORT_MESSAGE_SIZE - 2 or data[:1] == b'\x82':
|
||||
wdata = _pack('!BB18s', 0x11, devnumber, data)
|
||||
else:
|
||||
wdata = _pack('!BB5s', 0x10, devnumber, data)
|
||||
if _log.isEnabledFor(_DEBUG):
|
||||
_log.debug("(%s) <= w[%02X %02X %s %s]", handle, ord(wdata[:1]),
|
||||
devnumber, _strhex(wdata[2:4]), _strhex(wdata[4:]))
|
||||
|
||||
try:
|
||||
_hid.write(int(handle), wdata)
|
||||
except Exception as reason:
|
||||
_log.error("write failed, assuming handle %r no longer available", handle)
|
||||
close(handle)
|
||||
raise NoReceiver(reason=reason)
|
||||
try:
|
||||
_hid.write(int(handle), wdata)
|
||||
except Exception as reason:
|
||||
_log.error("write failed, assuming handle %r no longer available",
|
||||
handle)
|
||||
close(handle)
|
||||
raise NoReceiver(reason=reason)
|
||||
|
||||
|
||||
def read(handle, timeout=DEFAULT_TIMEOUT):
|
||||
"""Read some data from the receiver. Usually called after a write (feature
|
||||
"""Read some data from the receiver. Usually called after a write (feature
|
||||
call), to get the reply.
|
||||
|
||||
:param: handle open handle to the receiver
|
||||
|
|
@ -193,25 +197,26 @@ def read(handle, timeout=DEFAULT_TIMEOUT):
|
|||
been physically removed from the machine, or the kernel driver has been
|
||||
unloaded. The handle will be closed automatically.
|
||||
"""
|
||||
reply = _read(handle, timeout)
|
||||
if reply:
|
||||
return reply[1:]
|
||||
reply = _read(handle, timeout)
|
||||
if reply:
|
||||
return reply[1:]
|
||||
|
||||
|
||||
# sanity checks on message report id and size
|
||||
def check_message(data) :
|
||||
assert isinstance(data, bytes), (repr(data), type(data))
|
||||
report_id = ord(data[:1])
|
||||
if report_id in report_lengths: # is this an HID++ or DJ message?
|
||||
if report_lengths.get(report_id) == len(data):
|
||||
return True
|
||||
else:
|
||||
_log.warn("unexpected message size: report_id %02X message %s" % (report_id, _strhex(data)))
|
||||
return False
|
||||
def check_message(data):
|
||||
assert isinstance(data, bytes), (repr(data), type(data))
|
||||
report_id = ord(data[:1])
|
||||
if report_id in report_lengths: # is this an HID++ or DJ message?
|
||||
if report_lengths.get(report_id) == len(data):
|
||||
return True
|
||||
else:
|
||||
_log.warn("unexpected message size: report_id %02X message %s" %
|
||||
(report_id, _strhex(data)))
|
||||
return False
|
||||
|
||||
|
||||
def _read(handle, timeout):
|
||||
"""Read an incoming packet from the receiver.
|
||||
"""Read an incoming packet from the receiver.
|
||||
|
||||
:returns: a tuple of (report_id, devnumber, data), or `None`.
|
||||
|
||||
|
|
@ -219,98 +224,103 @@ def _read(handle, timeout):
|
|||
been physically removed from the machine, or the kernel driver has been
|
||||
unloaded. The handle will be closed automatically.
|
||||
"""
|
||||
try:
|
||||
# convert timeout to milliseconds, the hidapi expects it
|
||||
timeout = int(timeout * 1000)
|
||||
data = _hid.read(int(handle), _MAX_READ_SIZE, timeout)
|
||||
except Exception as reason:
|
||||
_log.error("read failed, assuming handle %r no longer available", handle)
|
||||
close(handle)
|
||||
raise NoReceiver(reason=reason)
|
||||
try:
|
||||
# convert timeout to milliseconds, the hidapi expects it
|
||||
timeout = int(timeout * 1000)
|
||||
data = _hid.read(int(handle), _MAX_READ_SIZE, timeout)
|
||||
except Exception as reason:
|
||||
_log.error("read failed, assuming handle %r no longer available",
|
||||
handle)
|
||||
close(handle)
|
||||
raise NoReceiver(reason=reason)
|
||||
|
||||
if data and check_message(data): # ignore messages that fail check
|
||||
report_id = ord(data[:1])
|
||||
devnumber = ord(data[1:2])
|
||||
if data and check_message(data): # ignore messages that fail check
|
||||
report_id = ord(data[:1])
|
||||
devnumber = ord(data[1:2])
|
||||
|
||||
if _log.isEnabledFor(_DEBUG):
|
||||
_log.debug("(%s) => r[%02X %02X %s %s]", handle, report_id, devnumber, _strhex(data[2:4]), _strhex(data[4:]))
|
||||
if _log.isEnabledFor(_DEBUG):
|
||||
_log.debug("(%s) => r[%02X %02X %s %s]", handle, report_id,
|
||||
devnumber, _strhex(data[2:4]), _strhex(data[4:]))
|
||||
|
||||
return report_id, devnumber, data[2:]
|
||||
|
||||
return report_id, devnumber, data[2:]
|
||||
|
||||
#
|
||||
#
|
||||
#
|
||||
|
||||
|
||||
def _skip_incoming(handle, ihandle, notifications_hook):
|
||||
"""Read anything already in the input buffer.
|
||||
"""Read anything already in the input buffer.
|
||||
|
||||
Used by request() and ping() before their write.
|
||||
"""
|
||||
|
||||
while True:
|
||||
try:
|
||||
# read whatever is already in the buffer, if any
|
||||
data = _hid.read(ihandle, _MAX_READ_SIZE, 0)
|
||||
except Exception as reason:
|
||||
_log.error("read failed, assuming receiver %s no longer available", handle)
|
||||
close(handle)
|
||||
raise NoReceiver(reason=reason)
|
||||
while True:
|
||||
try:
|
||||
# read whatever is already in the buffer, if any
|
||||
data = _hid.read(ihandle, _MAX_READ_SIZE, 0)
|
||||
except Exception as reason:
|
||||
_log.error("read failed, assuming receiver %s no longer available",
|
||||
handle)
|
||||
close(handle)
|
||||
raise NoReceiver(reason=reason)
|
||||
|
||||
if data:
|
||||
if check_message(data): # only process messages that pass check
|
||||
report_id = ord(data[:1])
|
||||
if notifications_hook:
|
||||
n = make_notification(ord(data[1:2]), data[2:])
|
||||
if n:
|
||||
notifications_hook(n)
|
||||
else:
|
||||
# nothing in the input buffer, we're done
|
||||
return
|
||||
if data:
|
||||
if check_message(data): # only process messages that pass check
|
||||
report_id = ord(data[:1])
|
||||
if notifications_hook:
|
||||
n = make_notification(ord(data[1:2]), data[2:])
|
||||
if n:
|
||||
notifications_hook(n)
|
||||
else:
|
||||
# nothing in the input buffer, we're done
|
||||
return
|
||||
|
||||
|
||||
def make_notification(devnumber, data):
|
||||
"""Guess if this is a notification (and not just a request reply), and
|
||||
"""Guess if this is a notification (and not just a request reply), and
|
||||
return a Notification tuple if it is."""
|
||||
|
||||
sub_id = ord(data[:1])
|
||||
if sub_id & 0x80 == 0x80:
|
||||
# this is either a HID++1.0 register r/w, or an error reply
|
||||
return
|
||||
sub_id = ord(data[:1])
|
||||
if sub_id & 0x80 == 0x80:
|
||||
# this is either a HID++1.0 register r/w, or an error reply
|
||||
return
|
||||
|
||||
# DJ input records are not notifications
|
||||
# it would be better to check for report_id 0x20 but that information is not sent here
|
||||
if len(data) == _MEDIUM_MESSAGE_SIZE-2 and (sub_id < 0x10):
|
||||
return
|
||||
# DJ input records are not notifications
|
||||
# it would be better to check for report_id 0x20 but that information is not sent here
|
||||
if len(data) == _MEDIUM_MESSAGE_SIZE - 2 and (sub_id < 0x10):
|
||||
return
|
||||
|
||||
address = ord(data[1:2])
|
||||
if (
|
||||
# standard HID++ 1.0 notification, SubId may be 0x40 - 0x7F
|
||||
(sub_id >= 0x40) or
|
||||
# custom HID++1.0 battery events, where SubId is 0x07/0x0D
|
||||
(sub_id in (0x07, 0x0D) and len(data) == 5 and data[4:5] == b'\x00') or
|
||||
# custom HID++1.0 illumination event, where SubId is 0x17
|
||||
(sub_id == 0x17 and len(data) == 5) or
|
||||
# HID++ 2.0 feature notifications have the SoftwareID 0
|
||||
(address & 0x0F == 0x00)):
|
||||
return _HIDPP_Notification(devnumber, sub_id, address, data[2:])
|
||||
|
||||
address = ord(data[1:2])
|
||||
if (
|
||||
# standard HID++ 1.0 notification, SubId may be 0x40 - 0x7F
|
||||
(sub_id >= 0x40)
|
||||
or
|
||||
# custom HID++1.0 battery events, where SubId is 0x07/0x0D
|
||||
(sub_id in (0x07, 0x0D) and len(data) == 5 and data[4:5] == b'\x00')
|
||||
or
|
||||
# custom HID++1.0 illumination event, where SubId is 0x17
|
||||
(sub_id == 0x17 and len(data) == 5)
|
||||
or
|
||||
# HID++ 2.0 feature notifications have the SoftwareID 0
|
||||
(address & 0x0F == 0x00)
|
||||
):
|
||||
return _HIDPP_Notification(devnumber, sub_id, address, data[2:])
|
||||
|
||||
from collections import namedtuple
|
||||
_HIDPP_Notification = namedtuple('_HIDPP_Notification', ('devnumber', 'sub_id', 'address', 'data'))
|
||||
_HIDPP_Notification.__str__ = lambda self: 'Notification(%d,%02X,%02X,%s)' % (self.devnumber, self.sub_id, self.address, _strhex(self.data))
|
||||
_HIDPP_Notification = namedtuple('_HIDPP_Notification',
|
||||
('devnumber', 'sub_id', 'address', 'data'))
|
||||
_HIDPP_Notification.__str__ = lambda self: 'Notification(%d,%02X,%02X,%s)' % (
|
||||
self.devnumber, self.sub_id, self.address, _strhex(self.data))
|
||||
_HIDPP_Notification.__unicode__ = _HIDPP_Notification.__str__
|
||||
DJ_NOTIFICATION_LENGTH = _MEDIUM_MESSAGE_SIZE - 4 # to allow easy distinguishing of DJ notifications
|
||||
DJ_NOTIFICATION_LENGTH = _MEDIUM_MESSAGE_SIZE - 4 # to allow easy distinguishing of DJ notifications
|
||||
del namedtuple
|
||||
|
||||
#
|
||||
#
|
||||
#
|
||||
|
||||
|
||||
def request(handle, devnumber, request_id, *params):
|
||||
"""Makes a feature call to a device and waits for a matching reply.
|
||||
"""Makes a feature call to a device and waits for a matching reply.
|
||||
|
||||
This function will wait for a matching reply indefinitely.
|
||||
|
||||
|
|
@ -321,174 +331,190 @@ def request(handle, devnumber, request_id, *params):
|
|||
:returns: the reply data, or ``None`` if some error occurred.
|
||||
"""
|
||||
|
||||
# import inspect as _inspect
|
||||
# print ('\n '.join(str(s) for s in _inspect.stack()))
|
||||
# import inspect as _inspect
|
||||
# print ('\n '.join(str(s) for s in _inspect.stack()))
|
||||
|
||||
assert isinstance(request_id, int)
|
||||
if devnumber != 0xFF and request_id < 0x8000:
|
||||
# For HID++ 2.0 feature requests, randomize the SoftwareId to make it
|
||||
# easier to recognize the reply for this request. also, always set the
|
||||
# most significant bit (8) in SoftwareId, to make notifications easier
|
||||
# to distinguish from request replies.
|
||||
# This only applies to peripheral requests, ofc.
|
||||
request_id = (request_id & 0xFFF0) | 0x08 | _random_bits(3)
|
||||
assert isinstance(request_id, int)
|
||||
if devnumber != 0xFF and request_id < 0x8000:
|
||||
# For HID++ 2.0 feature requests, randomize the SoftwareId to make it
|
||||
# easier to recognize the reply for this request. also, always set the
|
||||
# most significant bit (8) in SoftwareId, to make notifications easier
|
||||
# to distinguish from request replies.
|
||||
# This only applies to peripheral requests, ofc.
|
||||
request_id = (request_id & 0xFFF0) | 0x08 | _random_bits(3)
|
||||
|
||||
timeout = _RECEIVER_REQUEST_TIMEOUT if devnumber == 0xFF else _DEVICE_REQUEST_TIMEOUT
|
||||
# be extra patient on long register read
|
||||
if request_id & 0xFF00 == 0x8300:
|
||||
timeout *= 2
|
||||
timeout = _RECEIVER_REQUEST_TIMEOUT if devnumber == 0xFF else _DEVICE_REQUEST_TIMEOUT
|
||||
# be extra patient on long register read
|
||||
if request_id & 0xFF00 == 0x8300:
|
||||
timeout *= 2
|
||||
|
||||
if params:
|
||||
params = b''.join(_pack('B', p) if isinstance(p, int) else p for p in params)
|
||||
else:
|
||||
params = b''
|
||||
# if _log.isEnabledFor(_DEBUG):
|
||||
# _log.debug("(%s) device %d request_id {%04X} params [%s]", handle, devnumber, request_id, _strhex(params))
|
||||
request_data = _pack('!H', request_id) + params
|
||||
if params:
|
||||
params = b''.join(
|
||||
_pack('B', p) if isinstance(p, int) else p for p in params)
|
||||
else:
|
||||
params = b''
|
||||
# if _log.isEnabledFor(_DEBUG):
|
||||
# _log.debug("(%s) device %d request_id {%04X} params [%s]", handle, devnumber, request_id, _strhex(params))
|
||||
request_data = _pack('!H', request_id) + params
|
||||
|
||||
ihandle = int(handle)
|
||||
notifications_hook = getattr(handle, 'notifications_hook', None)
|
||||
_skip_incoming(handle, ihandle, notifications_hook)
|
||||
write(ihandle, devnumber, request_data)
|
||||
ihandle = int(handle)
|
||||
notifications_hook = getattr(handle, 'notifications_hook', None)
|
||||
_skip_incoming(handle, ihandle, notifications_hook)
|
||||
write(ihandle, devnumber, request_data)
|
||||
|
||||
# we consider timeout from this point
|
||||
request_started = _timestamp()
|
||||
delta = 0
|
||||
# we consider timeout from this point
|
||||
request_started = _timestamp()
|
||||
delta = 0
|
||||
|
||||
while delta < timeout:
|
||||
reply = _read(handle, timeout)
|
||||
while delta < timeout:
|
||||
reply = _read(handle, timeout)
|
||||
|
||||
if reply:
|
||||
report_id, reply_devnumber, reply_data = reply
|
||||
if reply_devnumber == devnumber:
|
||||
if report_id == 0x10 and reply_data[:1] == b'\x8F' and reply_data[1:3] == request_data[:2]:
|
||||
error = ord(reply_data[3:4])
|
||||
if reply:
|
||||
report_id, reply_devnumber, reply_data = reply
|
||||
if reply_devnumber == devnumber:
|
||||
if report_id == 0x10 and reply_data[:1] == b'\x8F' and reply_data[
|
||||
1:3] == request_data[:2]:
|
||||
error = ord(reply_data[3:4])
|
||||
|
||||
# if error == _hidpp10.ERROR.resource_error: # device unreachable
|
||||
# _log.warn("(%s) device %d error on request {%04X}: unknown device", handle, devnumber, request_id)
|
||||
# raise DeviceUnreachable(number=devnumber, request=request_id)
|
||||
# if error == _hidpp10.ERROR.resource_error: # device unreachable
|
||||
# _log.warn("(%s) device %d error on request {%04X}: unknown device", handle, devnumber, request_id)
|
||||
# raise DeviceUnreachable(number=devnumber, request=request_id)
|
||||
|
||||
# if error == _hidpp10.ERROR.unknown_device: # unknown device
|
||||
# _log.error("(%s) device %d error on request {%04X}: unknown device", handle, devnumber, request_id)
|
||||
# raise NoSuchDevice(number=devnumber, request=request_id)
|
||||
# if error == _hidpp10.ERROR.unknown_device: # unknown device
|
||||
# _log.error("(%s) device %d error on request {%04X}: unknown device", handle, devnumber, request_id)
|
||||
# raise NoSuchDevice(number=devnumber, request=request_id)
|
||||
|
||||
if _log.isEnabledFor(_DEBUG):
|
||||
_log.debug("(%s) device 0x%02X error on request {%04X}: %d = %s",
|
||||
handle, devnumber, request_id, error, _hidpp10.ERROR[error])
|
||||
return
|
||||
if _log.isEnabledFor(_DEBUG):
|
||||
_log.debug(
|
||||
"(%s) device 0x%02X error on request {%04X}: %d = %s",
|
||||
handle, devnumber, request_id, error,
|
||||
_hidpp10.ERROR[error])
|
||||
return
|
||||
|
||||
if reply_data[:1] == b'\xFF' and reply_data[1:3] == request_data[:2]:
|
||||
# a HID++ 2.0 feature call returned with an error
|
||||
error = ord(reply_data[3:4])
|
||||
_log.error("(%s) device %d error on feature request {%04X}: %d = %s",
|
||||
handle, devnumber, request_id, error, _hidpp20.ERROR[error])
|
||||
raise _hidpp20.FeatureCallError(number=devnumber, request=request_id, error=error, params=params)
|
||||
if reply_data[:1] == b'\xFF' and reply_data[
|
||||
1:3] == request_data[:2]:
|
||||
# a HID++ 2.0 feature call returned with an error
|
||||
error = ord(reply_data[3:4])
|
||||
_log.error(
|
||||
"(%s) device %d error on feature request {%04X}: %d = %s",
|
||||
handle, devnumber, request_id, error,
|
||||
_hidpp20.ERROR[error])
|
||||
raise _hidpp20.FeatureCallError(number=devnumber,
|
||||
request=request_id,
|
||||
error=error,
|
||||
params=params)
|
||||
|
||||
if reply_data[:2] == request_data[:2]:
|
||||
if request_id & 0xFE00 == 0x8200:
|
||||
# long registry r/w should return a long reply
|
||||
assert report_id == 0x11
|
||||
elif request_id & 0xFE00 == 0x8000:
|
||||
# short registry r/w should return a short reply
|
||||
assert report_id == 0x10
|
||||
if reply_data[:2] == request_data[:2]:
|
||||
if request_id & 0xFE00 == 0x8200:
|
||||
# long registry r/w should return a long reply
|
||||
assert report_id == 0x11
|
||||
elif request_id & 0xFE00 == 0x8000:
|
||||
# short registry r/w should return a short reply
|
||||
assert report_id == 0x10
|
||||
|
||||
if devnumber == 0xFF:
|
||||
if request_id == 0x83B5 or request_id == 0x81F1:
|
||||
# these replies have to match the first parameter as well
|
||||
if reply_data[2:3] == params[:1]:
|
||||
return reply_data[2:]
|
||||
else:
|
||||
# hm, not matching my request, and certainly not a notification
|
||||
continue
|
||||
else:
|
||||
return reply_data[2:]
|
||||
else:
|
||||
return reply_data[2:]
|
||||
else:
|
||||
# a reply was received, but did not match our request in any way
|
||||
# reset the timeout starting point
|
||||
request_started = _timestamp()
|
||||
if devnumber == 0xFF:
|
||||
if request_id == 0x83B5 or request_id == 0x81F1:
|
||||
# these replies have to match the first parameter as well
|
||||
if reply_data[2:3] == params[:1]:
|
||||
return reply_data[2:]
|
||||
else:
|
||||
# hm, not matching my request, and certainly not a notification
|
||||
continue
|
||||
else:
|
||||
return reply_data[2:]
|
||||
else:
|
||||
return reply_data[2:]
|
||||
else:
|
||||
# a reply was received, but did not match our request in any way
|
||||
# reset the timeout starting point
|
||||
request_started = _timestamp()
|
||||
|
||||
if notifications_hook:
|
||||
n = make_notification(reply_devnumber, reply_data)
|
||||
if n:
|
||||
notifications_hook(n)
|
||||
# elif _log.isEnabledFor(_DEBUG):
|
||||
# _log.debug("(%s) ignoring reply %02X [%s]", handle, reply_devnumber, _strhex(reply_data))
|
||||
# elif _log.isEnabledFor(_DEBUG):
|
||||
# _log.debug("(%s) ignoring reply %02X [%s]", handle, reply_devnumber, _strhex(reply_data))
|
||||
if notifications_hook:
|
||||
n = make_notification(reply_devnumber, reply_data)
|
||||
if n:
|
||||
notifications_hook(n)
|
||||
# elif _log.isEnabledFor(_DEBUG):
|
||||
# _log.debug("(%s) ignoring reply %02X [%s]", handle, reply_devnumber, _strhex(reply_data))
|
||||
# elif _log.isEnabledFor(_DEBUG):
|
||||
# _log.debug("(%s) ignoring reply %02X [%s]", handle, reply_devnumber, _strhex(reply_data))
|
||||
|
||||
delta = _timestamp() - request_started
|
||||
# if _log.isEnabledFor(_DEBUG):
|
||||
# _log.debug("(%s) still waiting for reply, delta %f", handle, delta)
|
||||
delta = _timestamp() - request_started
|
||||
# if _log.isEnabledFor(_DEBUG):
|
||||
# _log.debug("(%s) still waiting for reply, delta %f", handle, delta)
|
||||
|
||||
_log.warn("timeout (%0.2f/%0.2f) on device %d request {%04X} params [%s]",
|
||||
delta, timeout, devnumber, request_id, _strhex(params))
|
||||
# raise DeviceUnreachable(number=devnumber, request=request_id)
|
||||
_log.warn("timeout (%0.2f/%0.2f) on device %d request {%04X} params [%s]",
|
||||
delta, timeout, devnumber, request_id, _strhex(params))
|
||||
# raise DeviceUnreachable(number=devnumber, request=request_id)
|
||||
|
||||
|
||||
def ping(handle, devnumber):
|
||||
"""Check if a device is connected to the receiver.
|
||||
"""Check if a device is connected to the receiver.
|
||||
|
||||
:returns: The HID protocol supported by the device, as a floating point number, if the device is active.
|
||||
"""
|
||||
if _log.isEnabledFor(_DEBUG):
|
||||
_log.debug("(%s) pinging device %d", handle, devnumber)
|
||||
if _log.isEnabledFor(_DEBUG):
|
||||
_log.debug("(%s) pinging device %d", handle, devnumber)
|
||||
|
||||
# import inspect as _inspect
|
||||
# print ('\n '.join(str(s) for s in _inspect.stack()))
|
||||
# import inspect as _inspect
|
||||
# print ('\n '.join(str(s) for s in _inspect.stack()))
|
||||
|
||||
assert devnumber != 0xFF
|
||||
assert devnumber > 0x00
|
||||
assert devnumber < 0x0F
|
||||
assert devnumber != 0xFF
|
||||
assert devnumber > 0x00
|
||||
assert devnumber < 0x0F
|
||||
|
||||
# randomize the SoftwareId and mark byte to be able to identify the ping
|
||||
# reply, and set most significant (0x8) bit in SoftwareId so that the reply
|
||||
# is always distinguishable from notifications
|
||||
request_id = 0x0018 | _random_bits(3)
|
||||
request_data = _pack('!HBBB', request_id, 0, 0, _random_bits(8))
|
||||
# randomize the SoftwareId and mark byte to be able to identify the ping
|
||||
# reply, and set most significant (0x8) bit in SoftwareId so that the reply
|
||||
# is always distinguishable from notifications
|
||||
request_id = 0x0018 | _random_bits(3)
|
||||
request_data = _pack('!HBBB', request_id, 0, 0, _random_bits(8))
|
||||
|
||||
ihandle = int(handle)
|
||||
notifications_hook = getattr(handle, 'notifications_hook', None)
|
||||
_skip_incoming(handle, ihandle, notifications_hook)
|
||||
write(ihandle, devnumber, request_data)
|
||||
ihandle = int(handle)
|
||||
notifications_hook = getattr(handle, 'notifications_hook', None)
|
||||
_skip_incoming(handle, ihandle, notifications_hook)
|
||||
write(ihandle, devnumber, request_data)
|
||||
|
||||
# we consider timeout from this point
|
||||
request_started = _timestamp()
|
||||
delta = 0
|
||||
# we consider timeout from this point
|
||||
request_started = _timestamp()
|
||||
delta = 0
|
||||
|
||||
while delta < _PING_TIMEOUT:
|
||||
reply = _read(handle, _PING_TIMEOUT)
|
||||
while delta < _PING_TIMEOUT:
|
||||
reply = _read(handle, _PING_TIMEOUT)
|
||||
|
||||
if reply:
|
||||
report_id, reply_devnumber, reply_data = reply
|
||||
if reply_devnumber == devnumber:
|
||||
if reply_data[:2] == request_data[:2] and reply_data[4:5] == request_data[-1:]:
|
||||
# HID++ 2.0+ device, currently connected
|
||||
return ord(reply_data[2:3]) + ord(reply_data[3:4]) / 10.0
|
||||
if reply:
|
||||
report_id, reply_devnumber, reply_data = reply
|
||||
if reply_devnumber == devnumber:
|
||||
if reply_data[:2] == request_data[:2] and reply_data[
|
||||
4:5] == request_data[-1:]:
|
||||
# HID++ 2.0+ device, currently connected
|
||||
return ord(reply_data[2:3]) + ord(reply_data[3:4]) / 10.0
|
||||
|
||||
if report_id == 0x10 and reply_data[:1] == b'\x8F' and reply_data[1:3] == request_data[:2]:
|
||||
assert reply_data[-1:] == b'\x00'
|
||||
error = ord(reply_data[3:4])
|
||||
if report_id == 0x10 and reply_data[:1] == b'\x8F' and reply_data[
|
||||
1:3] == request_data[:2]:
|
||||
assert reply_data[-1:] == b'\x00'
|
||||
error = ord(reply_data[3:4])
|
||||
|
||||
if error == _hidpp10.ERROR.invalid_SubID__command: # a valid reply from a HID++ 1.0 device
|
||||
return 1.0
|
||||
if error == _hidpp10.ERROR.invalid_SubID__command: # a valid reply from a HID++ 1.0 device
|
||||
return 1.0
|
||||
|
||||
if error == _hidpp10.ERROR.resource_error: # device unreachable
|
||||
return
|
||||
if error == _hidpp10.ERROR.resource_error: # device unreachable
|
||||
return
|
||||
|
||||
if error == _hidpp10.ERROR.unknown_device: # no paired device with that number
|
||||
_log.error("(%s) device %d error on ping request: unknown device", handle, devnumber)
|
||||
raise NoSuchDevice(number=devnumber, request=request_id)
|
||||
if error == _hidpp10.ERROR.unknown_device: # no paired device with that number
|
||||
_log.error(
|
||||
"(%s) device %d error on ping request: unknown device",
|
||||
handle, devnumber)
|
||||
raise NoSuchDevice(number=devnumber,
|
||||
request=request_id)
|
||||
|
||||
if notifications_hook:
|
||||
n = make_notification(reply_devnumber, reply_data)
|
||||
if n:
|
||||
notifications_hook(n)
|
||||
# elif _log.isEnabledFor(_DEBUG):
|
||||
# _log.debug("(%s) ignoring reply %02X [%s]", handle, reply_devnumber, _strhex(reply_data))
|
||||
if notifications_hook:
|
||||
n = make_notification(reply_devnumber, reply_data)
|
||||
if n:
|
||||
notifications_hook(n)
|
||||
# elif _log.isEnabledFor(_DEBUG):
|
||||
# _log.debug("(%s) ignoring reply %02X [%s]", handle, reply_devnumber, _strhex(reply_data))
|
||||
|
||||
delta = _timestamp() - request_started
|
||||
delta = _timestamp() - request_started
|
||||
|
||||
_log.warn("(%s) timeout (%0.2f/%0.2f) on device %d ping", handle, delta, _PING_TIMEOUT, devnumber)
|
||||
# raise DeviceUnreachable(number=devnumber, request=request_id)
|
||||
_log.warn("(%s) timeout (%0.2f/%0.2f) on device %d ping", handle, delta,
|
||||
_PING_TIMEOUT, devnumber)
|
||||
# raise DeviceUnreachable(number=devnumber, request=request_id)
|
||||
|
|
|
|||
|
|
@ -22,7 +22,6 @@
|
|||
|
||||
from __future__ import absolute_import, division, print_function, unicode_literals
|
||||
|
||||
|
||||
_DRIVER = ('hid-generic', 'generic-usb', 'logitech-djreceiver')
|
||||
|
||||
# max_devices is only used for receivers that do not support reading from _R.receiver_info offset 0x03, default to 1
|
||||
|
|
@ -32,117 +31,117 @@ _DRIVER = ('hid-generic', 'generic-usb', 'logitech-djreceiver')
|
|||
## currently only one receiver is so marked - should there be more?
|
||||
|
||||
_unifying_receiver = lambda product_id: {
|
||||
'vendor_id':0x046d,
|
||||
'product_id':product_id,
|
||||
'usb_interface':2,
|
||||
'hid_driver':_DRIVER,
|
||||
'name':'Unifying Receiver'
|
||||
'vendor_id': 0x046d,
|
||||
'product_id': product_id,
|
||||
'usb_interface': 2,
|
||||
'hid_driver': _DRIVER,
|
||||
'name': 'Unifying Receiver'
|
||||
}
|
||||
|
||||
_nano_receiver = lambda product_id: {
|
||||
'vendor_id':0x046d,
|
||||
'product_id':product_id,
|
||||
'usb_interface':1,
|
||||
'hid_driver':_DRIVER,
|
||||
'name':'Nano Receiver',
|
||||
'may_unpair': False,
|
||||
're_pairs': True
|
||||
'vendor_id': 0x046d,
|
||||
'product_id': product_id,
|
||||
'usb_interface': 1,
|
||||
'hid_driver': _DRIVER,
|
||||
'name': 'Nano Receiver',
|
||||
'may_unpair': False,
|
||||
're_pairs': True
|
||||
}
|
||||
|
||||
_nano_receiver_max2 = lambda product_id: {
|
||||
'vendor_id':0x046d,
|
||||
'product_id':product_id,
|
||||
'usb_interface':1,
|
||||
'hid_driver':_DRIVER,
|
||||
'name':'Nano Receiver',
|
||||
'max_devices': 2,
|
||||
'may_unpair': False,
|
||||
're_pairs': True
|
||||
'vendor_id': 0x046d,
|
||||
'product_id': product_id,
|
||||
'usb_interface': 1,
|
||||
'hid_driver': _DRIVER,
|
||||
'name': 'Nano Receiver',
|
||||
'max_devices': 2,
|
||||
'may_unpair': False,
|
||||
're_pairs': True
|
||||
}
|
||||
|
||||
_nano_receiver_maxn = lambda product_id, max: {
|
||||
'vendor_id':0x046d,
|
||||
'product_id':product_id,
|
||||
'usb_interface':1,
|
||||
'hid_driver':_DRIVER,
|
||||
'name':'Nano Receiver',
|
||||
'max_devices': max,
|
||||
'may_unpair': False,
|
||||
're_pairs': True
|
||||
'vendor_id': 0x046d,
|
||||
'product_id': product_id,
|
||||
'usb_interface': 1,
|
||||
'hid_driver': _DRIVER,
|
||||
'name': 'Nano Receiver',
|
||||
'max_devices': max,
|
||||
'may_unpair': False,
|
||||
're_pairs': True
|
||||
}
|
||||
|
||||
_lenovo_receiver = lambda product_id: {
|
||||
'vendor_id':0x17ef,
|
||||
'product_id':product_id,
|
||||
'usb_interface':1,
|
||||
'hid_driver':_DRIVER,
|
||||
'name':'Nano Receiver'
|
||||
'vendor_id': 0x17ef,
|
||||
'product_id': product_id,
|
||||
'usb_interface': 1,
|
||||
'hid_driver': _DRIVER,
|
||||
'name': 'Nano Receiver'
|
||||
}
|
||||
|
||||
_lightspeed_receiver = lambda product_id: {
|
||||
'vendor_id':0x046d,
|
||||
'product_id':product_id,
|
||||
'usb_interface':2,
|
||||
'hid_driver':_DRIVER,
|
||||
'name':'Lightspeed Receiver'
|
||||
'vendor_id': 0x046d,
|
||||
'product_id': product_id,
|
||||
'usb_interface': 2,
|
||||
'hid_driver': _DRIVER,
|
||||
'name': 'Lightspeed Receiver'
|
||||
}
|
||||
|
||||
# standard Unifying receivers (marked with the orange Unifying logo)
|
||||
UNIFYING_RECEIVER_C52B = _unifying_receiver(0xc52b)
|
||||
UNIFYING_RECEIVER_C532 = _unifying_receiver(0xc532)
|
||||
UNIFYING_RECEIVER_C52B = _unifying_receiver(0xc52b)
|
||||
UNIFYING_RECEIVER_C532 = _unifying_receiver(0xc532)
|
||||
|
||||
# Nano receviers that support the Unifying protocol
|
||||
NANO_RECEIVER_ADVANCED = _nano_receiver(0xc52f)
|
||||
NANO_RECEIVER_ADVANCED = _nano_receiver(0xc52f)
|
||||
|
||||
# Nano receivers that don't support the Unifying protocol
|
||||
NANO_RECEIVER_C517 = _nano_receiver_maxn(0xc517,6)
|
||||
NANO_RECEIVER_C518 = _nano_receiver(0xc518)
|
||||
NANO_RECEIVER_C51A = _nano_receiver(0xc51a)
|
||||
NANO_RECEIVER_C51B = _nano_receiver(0xc51b)
|
||||
NANO_RECEIVER_C521 = _nano_receiver(0xc521)
|
||||
NANO_RECEIVER_C525 = _nano_receiver(0xc525)
|
||||
NANO_RECEIVER_C526 = _nano_receiver(0xc526)
|
||||
NANO_RECEIVER_C52e = _nano_receiver(0xc52e)
|
||||
NANO_RECEIVER_C531 = _nano_receiver(0xc531)
|
||||
NANO_RECEIVER_C534 = _nano_receiver_max2(0xc534)
|
||||
NANO_RECEIVER_C537 = _nano_receiver(0xc537)
|
||||
NANO_RECEIVER_6042 = _lenovo_receiver(0x6042)
|
||||
NANO_RECEIVER_C517 = _nano_receiver_maxn(0xc517, 6)
|
||||
NANO_RECEIVER_C518 = _nano_receiver(0xc518)
|
||||
NANO_RECEIVER_C51A = _nano_receiver(0xc51a)
|
||||
NANO_RECEIVER_C51B = _nano_receiver(0xc51b)
|
||||
NANO_RECEIVER_C521 = _nano_receiver(0xc521)
|
||||
NANO_RECEIVER_C525 = _nano_receiver(0xc525)
|
||||
NANO_RECEIVER_C526 = _nano_receiver(0xc526)
|
||||
NANO_RECEIVER_C52e = _nano_receiver(0xc52e)
|
||||
NANO_RECEIVER_C531 = _nano_receiver(0xc531)
|
||||
NANO_RECEIVER_C534 = _nano_receiver_max2(0xc534)
|
||||
NANO_RECEIVER_C537 = _nano_receiver(0xc537)
|
||||
NANO_RECEIVER_6042 = _lenovo_receiver(0x6042)
|
||||
|
||||
# Lightspeed receivers
|
||||
LIGHTSPEED_RECEIVER_C539 = _lightspeed_receiver(0xc539)
|
||||
LIGHTSPEED_RECEIVER_C53a = _lightspeed_receiver(0xc53a)
|
||||
LIGHTSPEED_RECEIVER_C53f = _lightspeed_receiver(0xc53f)
|
||||
LIGHTSPEED_RECEIVER_C53d = _lightspeed_receiver(0xc53d)
|
||||
LIGHTSPEED_RECEIVER_C539 = _lightspeed_receiver(0xc539)
|
||||
LIGHTSPEED_RECEIVER_C53a = _lightspeed_receiver(0xc53a)
|
||||
LIGHTSPEED_RECEIVER_C53f = _lightspeed_receiver(0xc53f)
|
||||
LIGHTSPEED_RECEIVER_C53d = _lightspeed_receiver(0xc53d)
|
||||
|
||||
del _DRIVER, _unifying_receiver, _nano_receiver, _lenovo_receiver, _lightspeed_receiver
|
||||
|
||||
|
||||
ALL = (
|
||||
UNIFYING_RECEIVER_C52B,
|
||||
UNIFYING_RECEIVER_C532,
|
||||
NANO_RECEIVER_ADVANCED,
|
||||
NANO_RECEIVER_C517,
|
||||
NANO_RECEIVER_C518,
|
||||
NANO_RECEIVER_C51A,
|
||||
NANO_RECEIVER_C51B,
|
||||
NANO_RECEIVER_C521,
|
||||
NANO_RECEIVER_C525,
|
||||
NANO_RECEIVER_C526,
|
||||
NANO_RECEIVER_C52e,
|
||||
NANO_RECEIVER_C531,
|
||||
NANO_RECEIVER_C534,
|
||||
NANO_RECEIVER_C537,
|
||||
NANO_RECEIVER_6042,
|
||||
LIGHTSPEED_RECEIVER_C539,
|
||||
LIGHTSPEED_RECEIVER_C53a,
|
||||
LIGHTSPEED_RECEIVER_C53f,
|
||||
LIGHTSPEED_RECEIVER_C53d,
|
||||
)
|
||||
UNIFYING_RECEIVER_C52B,
|
||||
UNIFYING_RECEIVER_C532,
|
||||
NANO_RECEIVER_ADVANCED,
|
||||
NANO_RECEIVER_C517,
|
||||
NANO_RECEIVER_C518,
|
||||
NANO_RECEIVER_C51A,
|
||||
NANO_RECEIVER_C51B,
|
||||
NANO_RECEIVER_C521,
|
||||
NANO_RECEIVER_C525,
|
||||
NANO_RECEIVER_C526,
|
||||
NANO_RECEIVER_C52e,
|
||||
NANO_RECEIVER_C531,
|
||||
NANO_RECEIVER_C534,
|
||||
NANO_RECEIVER_C537,
|
||||
NANO_RECEIVER_6042,
|
||||
LIGHTSPEED_RECEIVER_C539,
|
||||
LIGHTSPEED_RECEIVER_C53a,
|
||||
LIGHTSPEED_RECEIVER_C53f,
|
||||
LIGHTSPEED_RECEIVER_C53d,
|
||||
)
|
||||
|
||||
|
||||
def product_information(usb_id):
|
||||
if isinstance(usb_id,str):
|
||||
usb_id = int(usb_id,16)
|
||||
for r in ALL:
|
||||
if usb_id == r.get('product_id'):
|
||||
return r
|
||||
return { }
|
||||
if isinstance(usb_id, str):
|
||||
usb_id = int(usb_id, 16)
|
||||
for r in ALL:
|
||||
if usb_id == r.get('product_id'):
|
||||
return r
|
||||
return {}
|
||||
|
|
|
|||
|
|
@ -24,70 +24,71 @@ from __future__ import absolute_import, division, print_function, unicode_litera
|
|||
from binascii import hexlify as _hexlify
|
||||
from struct import pack, unpack
|
||||
try:
|
||||
unicode
|
||||
# if Python2, unicode_literals will mess our first (un)pack() argument
|
||||
_pack_str = pack
|
||||
_unpack_str = unpack
|
||||
pack = lambda x, *args: _pack_str(str(x), *args)
|
||||
unpack = lambda x, *args: _unpack_str(str(x), *args)
|
||||
unicode
|
||||
# if Python2, unicode_literals will mess our first (un)pack() argument
|
||||
_pack_str = pack
|
||||
_unpack_str = unpack
|
||||
pack = lambda x, *args: _pack_str(str(x), *args)
|
||||
unpack = lambda x, *args: _unpack_str(str(x), *args)
|
||||
|
||||
is_string = lambda d: isinstance(d, unicode) or isinstance(d, str)
|
||||
# no easy way to distinguish between b'' and '' :(
|
||||
# or (isinstance(d, str) \
|
||||
# and not any((chr(k) in d for k in range(0x00, 0x1F))) \
|
||||
# and not any((chr(k) in d for k in range(0x80, 0xFF))) \
|
||||
# )
|
||||
is_string = lambda d: isinstance(d, unicode) or isinstance(d, str)
|
||||
# no easy way to distinguish between b'' and '' :(
|
||||
# or (isinstance(d, str) \
|
||||
# and not any((chr(k) in d for k in range(0x00, 0x1F))) \
|
||||
# and not any((chr(k) in d for k in range(0x80, 0xFF))) \
|
||||
# )
|
||||
except:
|
||||
# this is certanly Python 3
|
||||
# In Py3, unicode and str are equal (the unicode object does not exist)
|
||||
is_string = lambda d: isinstance(d, str)
|
||||
# this is certanly Python 3
|
||||
# In Py3, unicode and str are equal (the unicode object does not exist)
|
||||
is_string = lambda d: isinstance(d, str)
|
||||
|
||||
#
|
||||
#
|
||||
#
|
||||
|
||||
|
||||
class NamedInt(int):
|
||||
"""An reqular Python integer with an attached name.
|
||||
"""An reqular Python integer with an attached name.
|
||||
|
||||
Caution: comparison with strings will also match this NamedInt's name
|
||||
(case-insensitive)."""
|
||||
def __new__(cls, value, name):
|
||||
assert is_string(name)
|
||||
obj = int.__new__(cls, value)
|
||||
obj.name = str(name)
|
||||
return obj
|
||||
|
||||
def __new__(cls, value, name):
|
||||
assert is_string(name)
|
||||
obj = int.__new__(cls, value)
|
||||
obj.name = str(name)
|
||||
return obj
|
||||
def bytes(self, count=2):
|
||||
return int2bytes(self, count)
|
||||
|
||||
def bytes(self, count=2):
|
||||
return int2bytes(self, count)
|
||||
def __eq__(self, other):
|
||||
if isinstance(other, NamedInt):
|
||||
return int(self) == int(other) and self.name == other.name
|
||||
if isinstance(other, int):
|
||||
return int(self) == int(other)
|
||||
if is_string(other):
|
||||
return self.name.lower() == other.lower()
|
||||
# this should catch comparisons with bytes in Py3
|
||||
if other is not None:
|
||||
raise TypeError('Unsupported type ' + str(type(other)))
|
||||
|
||||
def __eq__(self, other):
|
||||
if isinstance(other, NamedInt):
|
||||
return int(self) == int(other) and self.name == other.name
|
||||
if isinstance(other, int):
|
||||
return int(self) == int(other)
|
||||
if is_string(other):
|
||||
return self.name.lower() == other.lower()
|
||||
# this should catch comparisons with bytes in Py3
|
||||
if other is not None:
|
||||
raise TypeError('Unsupported type ' + str(type(other)))
|
||||
def __ne__(self, other):
|
||||
return not self.__eq__(other)
|
||||
|
||||
def __ne__(self, other):
|
||||
return not self.__eq__(other)
|
||||
def __hash__(self):
|
||||
return int(self)
|
||||
|
||||
def __hash__(self):
|
||||
return int(self)
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
__unicode__ = __str__
|
||||
__unicode__ = __str__
|
||||
|
||||
def __repr__(self):
|
||||
return 'NamedInt(%d, %r)' % (int(self), self.name)
|
||||
def __repr__(self):
|
||||
return 'NamedInt(%d, %r)' % (int(self), self.name)
|
||||
|
||||
|
||||
class NamedInts(object):
|
||||
"""An ordered set of NamedInt values.
|
||||
"""An ordered set of NamedInt values.
|
||||
|
||||
Indexing can be made by int or string, and will return the corresponding
|
||||
NamedInt if it exists in this set, or `None`.
|
||||
|
|
@ -99,194 +100,194 @@ class NamedInts(object):
|
|||
if the value already exists in the set (int or string), ValueError will be
|
||||
raised.
|
||||
"""
|
||||
__slots__ = ('__dict__', '_values', '_indexed', '_fallback')
|
||||
__slots__ = ('__dict__', '_values', '_indexed', '_fallback')
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
def _readable_name(n):
|
||||
if not is_string(n):
|
||||
raise TypeError("expected (unicode) string, got " + str(type(n)))
|
||||
return n.replace('__', '/').replace('_', ' ')
|
||||
def __init__(self, **kwargs):
|
||||
def _readable_name(n):
|
||||
if not is_string(n):
|
||||
raise TypeError("expected (unicode) string, got " +
|
||||
str(type(n)))
|
||||
return n.replace('__', '/').replace('_', ' ')
|
||||
|
||||
# print (repr(kwargs))
|
||||
values = {k: NamedInt(v, _readable_name(k)) for (k, v) in kwargs.items()}
|
||||
self.__dict__ = values
|
||||
self._values = sorted(list(values.values()))
|
||||
self._indexed = {int(v): v for v in self._values}
|
||||
# assert len(values) == len(self._indexed), "(%d) %r\n=> (%d) %r" % (len(values), values, len(self._indexed), self._indexed)
|
||||
self._fallback = None
|
||||
# print (repr(kwargs))
|
||||
values = {
|
||||
k: NamedInt(v, _readable_name(k))
|
||||
for (k, v) in kwargs.items()
|
||||
}
|
||||
self.__dict__ = values
|
||||
self._values = sorted(list(values.values()))
|
||||
self._indexed = {int(v): v for v in self._values}
|
||||
# assert len(values) == len(self._indexed), "(%d) %r\n=> (%d) %r" % (len(values), values, len(self._indexed), self._indexed)
|
||||
self._fallback = None
|
||||
|
||||
@classmethod
|
||||
def list(cls, items, name_generator=lambda x: str(x)):
|
||||
values = {name_generator(x): x for x in items}
|
||||
return NamedInts(**values)
|
||||
@classmethod
|
||||
def list(cls, items, name_generator=lambda x: str(x)):
|
||||
values = {name_generator(x): x for x in items}
|
||||
return NamedInts(**values)
|
||||
|
||||
@classmethod
|
||||
def range(cls, from_value, to_value, name_generator=lambda x: str(x), step=1):
|
||||
values = {name_generator(x): x for x in range(from_value, to_value + 1, step)}
|
||||
return NamedInts(**values)
|
||||
@classmethod
|
||||
def range(cls,
|
||||
from_value,
|
||||
to_value,
|
||||
name_generator=lambda x: str(x),
|
||||
step=1):
|
||||
values = {
|
||||
name_generator(x): x
|
||||
for x in range(from_value, to_value + 1, step)
|
||||
}
|
||||
return NamedInts(**values)
|
||||
|
||||
def flag_names(self, value):
|
||||
unknown_bits = value
|
||||
for k in self._indexed:
|
||||
assert bin(k).count('1') == 1
|
||||
if k & value == k:
|
||||
unknown_bits &= ~k
|
||||
yield str(self._indexed[k])
|
||||
def flag_names(self, value):
|
||||
unknown_bits = value
|
||||
for k in self._indexed:
|
||||
assert bin(k).count('1') == 1
|
||||
if k & value == k:
|
||||
unknown_bits &= ~k
|
||||
yield str(self._indexed[k])
|
||||
|
||||
if unknown_bits:
|
||||
yield 'unknown:%06X' % unknown_bits
|
||||
if unknown_bits:
|
||||
yield 'unknown:%06X' % unknown_bits
|
||||
|
||||
def __getitem__(self, index):
|
||||
if isinstance(index, int):
|
||||
if index in self._indexed:
|
||||
return self._indexed[int(index)]
|
||||
if self._fallback and isinstance(index, int):
|
||||
value = NamedInt(index, self._fallback(index))
|
||||
self._indexed[index] = value
|
||||
self._values = sorted(self._values + [value])
|
||||
return value
|
||||
def __getitem__(self, index):
|
||||
if isinstance(index, int):
|
||||
if index in self._indexed:
|
||||
return self._indexed[int(index)]
|
||||
if self._fallback and isinstance(index, int):
|
||||
value = NamedInt(index, self._fallback(index))
|
||||
self._indexed[index] = value
|
||||
self._values = sorted(self._values + [value])
|
||||
return value
|
||||
|
||||
elif is_string(index):
|
||||
if index in self.__dict__:
|
||||
return self.__dict__[index]
|
||||
elif is_string(index):
|
||||
if index in self.__dict__:
|
||||
return self.__dict__[index]
|
||||
|
||||
elif isinstance(index, slice):
|
||||
if index.start is None and index.stop is None:
|
||||
return self._values[:]
|
||||
elif isinstance(index, slice):
|
||||
if index.start is None and index.stop is None:
|
||||
return self._values[:]
|
||||
|
||||
v_start = int(self._values[0]) if index.start is None else int(index.start)
|
||||
v_stop = (self._values[-1] + 1) if index.stop is None else int(index.stop)
|
||||
v_start = int(self._values[0]) if index.start is None else int(
|
||||
index.start)
|
||||
v_stop = (self._values[-1] +
|
||||
1) if index.stop is None else int(index.stop)
|
||||
|
||||
if v_start > v_stop or v_start > self._values[-1] or v_stop <= self._values[0]:
|
||||
return []
|
||||
if v_start > v_stop or v_start > self._values[
|
||||
-1] or v_stop <= self._values[0]:
|
||||
return []
|
||||
|
||||
if v_start <= self._values[0] and v_stop > self._values[-1]:
|
||||
return self._values[:]
|
||||
if v_start <= self._values[0] and v_stop > self._values[-1]:
|
||||
return self._values[:]
|
||||
|
||||
start_index = 0
|
||||
stop_index = len(self._values)
|
||||
for i, value in enumerate(self._values):
|
||||
if value < v_start:
|
||||
start_index = i + 1
|
||||
elif index.stop is None:
|
||||
break
|
||||
if value >= v_stop:
|
||||
stop_index = i
|
||||
break
|
||||
start_index = 0
|
||||
stop_index = len(self._values)
|
||||
for i, value in enumerate(self._values):
|
||||
if value < v_start:
|
||||
start_index = i + 1
|
||||
elif index.stop is None:
|
||||
break
|
||||
if value >= v_stop:
|
||||
stop_index = i
|
||||
break
|
||||
|
||||
return self._values[start_index:stop_index]
|
||||
return self._values[start_index:stop_index]
|
||||
|
||||
def __setitem__(self, index, name):
|
||||
assert isinstance(index, int), type(index)
|
||||
if isinstance(name, NamedInt):
|
||||
assert int(index) == int(name), repr(index) + ' ' + repr(name)
|
||||
value = name
|
||||
elif is_string(name):
|
||||
value = NamedInt(index, name)
|
||||
else:
|
||||
raise TypeError('name must be a string')
|
||||
def __setitem__(self, index, name):
|
||||
assert isinstance(index, int), type(index)
|
||||
if isinstance(name, NamedInt):
|
||||
assert int(index) == int(name), repr(index) + ' ' + repr(name)
|
||||
value = name
|
||||
elif is_string(name):
|
||||
value = NamedInt(index, name)
|
||||
else:
|
||||
raise TypeError('name must be a string')
|
||||
|
||||
if str(value) in self.__dict__:
|
||||
raise ValueError('%s (%d) already known' % (value, int(value)))
|
||||
if int(value) in self._indexed:
|
||||
raise ValueError('%d (%s) already known' % (int(value), value))
|
||||
if str(value) in self.__dict__:
|
||||
raise ValueError('%s (%d) already known' % (value, int(value)))
|
||||
if int(value) in self._indexed:
|
||||
raise ValueError('%d (%s) already known' % (int(value), value))
|
||||
|
||||
self._values = sorted(self._values + [value])
|
||||
self.__dict__[str(value)] = value
|
||||
self._indexed[int(value)] = value
|
||||
self._values = sorted(self._values + [value])
|
||||
self.__dict__[str(value)] = value
|
||||
self._indexed[int(value)] = value
|
||||
|
||||
def __contains__(self, value):
|
||||
if isinstance(value, int):
|
||||
return value in self._indexed
|
||||
elif is_string(value):
|
||||
return value in self.__dict__
|
||||
def __contains__(self, value):
|
||||
if isinstance(value, int):
|
||||
return value in self._indexed
|
||||
elif is_string(value):
|
||||
return value in self.__dict__
|
||||
|
||||
def __iter__(self):
|
||||
for v in self._values:
|
||||
yield v
|
||||
def __iter__(self):
|
||||
for v in self._values:
|
||||
yield v
|
||||
|
||||
def __len__(self):
|
||||
return len(self._values)
|
||||
def __len__(self):
|
||||
return len(self._values)
|
||||
|
||||
def __repr__(self):
|
||||
return 'NamedInts(%s)' % ', '.join(repr(v) for v in self._values)
|
||||
def __repr__(self):
|
||||
return 'NamedInts(%s)' % ', '.join(repr(v) for v in self._values)
|
||||
|
||||
|
||||
def strhex(x):
|
||||
assert x is not None
|
||||
"""Produce a hex-string representation of a sequence of bytes."""
|
||||
return _hexlify(x).decode('ascii').upper()
|
||||
assert x is not None
|
||||
"""Produce a hex-string representation of a sequence of bytes."""
|
||||
return _hexlify(x).decode('ascii').upper()
|
||||
|
||||
|
||||
def bytes2int(x):
|
||||
"""Convert a bytes string to an int.
|
||||
"""Convert a bytes string to an int.
|
||||
The bytes are assumed to be in most-significant-first order.
|
||||
"""
|
||||
assert isinstance(x, bytes)
|
||||
assert len(x) < 9
|
||||
qx = (b'\x00' * 8) + x
|
||||
result, = unpack('!Q', qx[-8:])
|
||||
# assert x == int2bytes(result, len(x))
|
||||
return result
|
||||
assert isinstance(x, bytes)
|
||||
assert len(x) < 9
|
||||
qx = (b'\x00' * 8) + x
|
||||
result, = unpack('!Q', qx[-8:])
|
||||
# assert x == int2bytes(result, len(x))
|
||||
return result
|
||||
|
||||
|
||||
def int2bytes(x, count=None):
|
||||
"""Convert an int to a bytes representation.
|
||||
"""Convert an int to a bytes representation.
|
||||
The bytes are ordered in most-significant-first order.
|
||||
If 'count' is not given, the necessary number of bytes is computed.
|
||||
"""
|
||||
assert isinstance(x, int)
|
||||
result = pack('!Q', x)
|
||||
assert isinstance(result, bytes)
|
||||
# assert x == bytes2int(result)
|
||||
assert isinstance(x, int)
|
||||
result = pack('!Q', x)
|
||||
assert isinstance(result, bytes)
|
||||
# assert x == bytes2int(result)
|
||||
|
||||
if count is None:
|
||||
return result.lstrip(b'\x00')
|
||||
if count is None:
|
||||
return result.lstrip(b'\x00')
|
||||
|
||||
assert isinstance(count, int)
|
||||
assert count > 0
|
||||
assert x.bit_length() <= count * 8
|
||||
return result[-count:]
|
||||
assert isinstance(count, int)
|
||||
assert count > 0
|
||||
assert x.bit_length() <= count * 8
|
||||
return result[-count:]
|
||||
|
||||
|
||||
class KwException(Exception):
|
||||
"""An exception that remembers all arguments passed to the constructor.
|
||||
"""An exception that remembers all arguments passed to the constructor.
|
||||
They can be later accessed by simple member access.
|
||||
"""
|
||||
def __init__(self, **kwargs):
|
||||
super(KwException, self).__init__(kwargs)
|
||||
def __init__(self, **kwargs):
|
||||
super(KwException, self).__init__(kwargs)
|
||||
|
||||
def __getattr__(self, k):
|
||||
try:
|
||||
return super(KwException, self).__getattr__(k)
|
||||
except AttributeError:
|
||||
return self.args[0][k]
|
||||
def __getattr__(self, k):
|
||||
try:
|
||||
return super(KwException, self).__getattr__(k)
|
||||
except AttributeError:
|
||||
return self.args[0][k]
|
||||
|
||||
|
||||
from collections import namedtuple
|
||||
|
||||
"""Firmware information."""
|
||||
FirmwareInfo = namedtuple('FirmwareInfo', [
|
||||
'kind',
|
||||
'name',
|
||||
'version',
|
||||
'extras'])
|
||||
|
||||
FirmwareInfo = namedtuple('FirmwareInfo',
|
||||
['kind', 'name', 'version', 'extras'])
|
||||
"""Reprogrammable keys information."""
|
||||
ReprogrammableKeyInfo = namedtuple('ReprogrammableKeyInfo', [
|
||||
'index',
|
||||
'key',
|
||||
'task',
|
||||
'flags'])
|
||||
ReprogrammableKeyInfo = namedtuple('ReprogrammableKeyInfo',
|
||||
['index', 'key', 'task', 'flags'])
|
||||
|
||||
ReprogrammableKeyInfoV4 = namedtuple('ReprogrammableKeyInfoV4', [
|
||||
'index',
|
||||
'key',
|
||||
'task',
|
||||
'flags',
|
||||
'pos',
|
||||
'group',
|
||||
'group_mask',
|
||||
'remapped'])
|
||||
'index', 'key', 'task', 'flags', 'pos', 'group', 'group_mask', 'remapped'
|
||||
])
|
||||
|
||||
del namedtuple
|
||||
|
|
|
|||
|
|
@ -19,7 +19,6 @@
|
|||
|
||||
from __future__ import absolute_import, division, print_function, unicode_literals
|
||||
|
||||
|
||||
from .common import NamedInts as _NamedInts
|
||||
from .hidpp10 import REGISTERS as _R, DEVICE_KIND as _DK
|
||||
from .settings_templates import RegisterSettings as _RS, FeatureSettings as _FS
|
||||
|
|
@ -30,67 +29,88 @@ from .settings_templates import RegisterSettings as _RS, FeatureSettings as _FS
|
|||
|
||||
from collections import namedtuple
|
||||
_DeviceDescriptor = namedtuple('_DeviceDescriptor',
|
||||
('name', 'kind', 'wpid', 'codename', 'protocol', 'registers', 'settings', 'persister'))
|
||||
('name', 'kind', 'wpid', 'codename', 'protocol',
|
||||
'registers', 'settings', 'persister'))
|
||||
del namedtuple
|
||||
|
||||
DEVICES = {}
|
||||
|
||||
def _D(name, codename=None, kind=None, wpid=None, protocol=None, registers=None, settings=None, persister=None):
|
||||
assert name
|
||||
|
||||
if kind is None:
|
||||
kind = (_DK.mouse if 'Mouse' in name
|
||||
else _DK.keyboard if 'Keyboard' in name
|
||||
else _DK.numpad if 'Number Pad' in name
|
||||
else _DK.touchpad if 'Touchpad' in name
|
||||
else _DK.trackball if 'Trackball' in name
|
||||
else None)
|
||||
assert kind is not None, 'descriptor for %s does not have kind set' % name
|
||||
def _D(name,
|
||||
codename=None,
|
||||
kind=None,
|
||||
wpid=None,
|
||||
protocol=None,
|
||||
registers=None,
|
||||
settings=None,
|
||||
persister=None):
|
||||
assert name
|
||||
|
||||
# heuristic: the codename is the last word in the device name
|
||||
if codename is None and ' ' in name:
|
||||
codename = name.split(' ')[-1]
|
||||
assert codename is not None, 'descriptor for %s does not have codename set' % name
|
||||
if kind is None:
|
||||
kind = (_DK.mouse if 'Mouse' in name else
|
||||
_DK.keyboard if 'Keyboard' in name else _DK.numpad
|
||||
if 'Number Pad' in name else _DK.touchpad if 'Touchpad' in
|
||||
name else _DK.trackball if 'Trackball' in name else None)
|
||||
assert kind is not None, 'descriptor for %s does not have kind set' % name
|
||||
|
||||
if protocol is not None:
|
||||
# ? 2.0 devices should not have any registers
|
||||
_kind = lambda s : s._rw.kind if hasattr(s, '_rw') else s._rw_kind
|
||||
if protocol < 2.0:
|
||||
assert settings is None or all(_kind(s) == 1 for s in settings)
|
||||
else:
|
||||
assert registers is None
|
||||
assert settings is None or all(_kind(s) == 2 for s in settings)
|
||||
# heuristic: the codename is the last word in the device name
|
||||
if codename is None and ' ' in name:
|
||||
codename = name.split(' ')[-1]
|
||||
assert codename is not None, 'descriptor for %s does not have codename set' % name
|
||||
|
||||
if wpid:
|
||||
for w in wpid if isinstance(wpid, tuple) else (wpid, ):
|
||||
if protocol > 1.0:
|
||||
assert w[0:1] == '4', '%s has protocol %0.1f, wpid %s' % (name, protocol, w)
|
||||
else:
|
||||
if w[0:1] == '1':
|
||||
assert kind == _DK.mouse, '%s has protocol %0.1f, wpid %s' % (name, protocol, w)
|
||||
elif w[0:1] == '2':
|
||||
assert kind in (_DK.keyboard, _DK.numpad), '%s has protocol %0.1f, wpid %s' % (name, protocol, w)
|
||||
if protocol is not None:
|
||||
# ? 2.0 devices should not have any registers
|
||||
_kind = lambda s: s._rw.kind if hasattr(s, '_rw') else s._rw_kind
|
||||
if protocol < 2.0:
|
||||
assert settings is None or all(_kind(s) == 1 for s in settings)
|
||||
else:
|
||||
assert registers is None
|
||||
assert settings is None or all(_kind(s) == 2 for s in settings)
|
||||
|
||||
device_descriptor = _DeviceDescriptor(name=name, kind=kind,
|
||||
wpid=wpid, codename=codename, protocol=protocol,
|
||||
registers=registers, settings=settings, persister=persister)
|
||||
if wpid:
|
||||
for w in wpid if isinstance(wpid, tuple) else (wpid, ):
|
||||
if protocol > 1.0:
|
||||
assert w[0:1] == '4', '%s has protocol %0.1f, wpid %s' % (
|
||||
name, protocol, w)
|
||||
else:
|
||||
if w[0:1] == '1':
|
||||
assert kind == _DK.mouse, '%s has protocol %0.1f, wpid %s' % (
|
||||
name, protocol, w)
|
||||
elif w[0:1] == '2':
|
||||
assert kind in (
|
||||
_DK.keyboard,
|
||||
_DK.numpad), '%s has protocol %0.1f, wpid %s' % (
|
||||
name, protocol, w)
|
||||
|
||||
assert codename not in DEVICES, 'duplicate codename in device descriptors: %s' % (DEVICES[codename], )
|
||||
DEVICES[codename] = device_descriptor
|
||||
device_descriptor = _DeviceDescriptor(name=name,
|
||||
kind=kind,
|
||||
wpid=wpid,
|
||||
codename=codename,
|
||||
protocol=protocol,
|
||||
registers=registers,
|
||||
settings=settings,
|
||||
persister=persister)
|
||||
|
||||
if wpid:
|
||||
if not isinstance(wpid, tuple):
|
||||
wpid = (wpid, )
|
||||
assert codename not in DEVICES, 'duplicate codename in device descriptors: %s' % (
|
||||
DEVICES[codename], )
|
||||
DEVICES[codename] = device_descriptor
|
||||
|
||||
if wpid:
|
||||
if not isinstance(wpid, tuple):
|
||||
wpid = (wpid, )
|
||||
|
||||
for w in wpid:
|
||||
assert w not in DEVICES, 'duplicate wpid in device descriptors: %s' % (
|
||||
DEVICES[w], )
|
||||
DEVICES[w] = device_descriptor
|
||||
|
||||
for w in wpid:
|
||||
assert w not in DEVICES, 'duplicate wpid in device descriptors: %s' % (DEVICES[w], )
|
||||
DEVICES[w] = device_descriptor
|
||||
|
||||
#
|
||||
#
|
||||
#
|
||||
|
||||
_PERFORMANCE_MX_DPIS = _NamedInts.range(0x81, 0x8F, lambda x: str((x - 0x80) * 100))
|
||||
_PERFORMANCE_MX_DPIS = _NamedInts.range(0x81, 0x8F, lambda x: str(
|
||||
(x - 0x80) * 100))
|
||||
|
||||
#
|
||||
#
|
||||
|
|
@ -145,242 +165,378 @@ _PERFORMANCE_MX_DPIS = _NamedInts.range(0x81, 0x8F, lambda x: str((x - 0x80) * 1
|
|||
|
||||
_D('Wireless Keyboard K230', protocol=2.0, wpid='400D')
|
||||
_D('Wireless Keyboard K270(unifying)', protocol=2.0, wpid='4003')
|
||||
_D('Wireless Keyboard MK270', protocol=2.0, wpid='4023',
|
||||
settings=[
|
||||
_FS.fn_swap()
|
||||
],
|
||||
)
|
||||
_D('Wireless Keyboard K270', protocol=1.0,
|
||||
registers=(_R.battery_status, ),
|
||||
)
|
||||
_D('Wireless Keyboard MK300', protocol=1.0, wpid='8521',
|
||||
registers=(_R.battery_status, ),
|
||||
)
|
||||
_D(
|
||||
'Wireless Keyboard MK270',
|
||||
protocol=2.0,
|
||||
wpid='4023',
|
||||
settings=[_FS.fn_swap()],
|
||||
)
|
||||
_D(
|
||||
'Wireless Keyboard K270',
|
||||
protocol=1.0,
|
||||
registers=(_R.battery_status, ),
|
||||
)
|
||||
_D(
|
||||
'Wireless Keyboard MK300',
|
||||
protocol=1.0,
|
||||
wpid='8521',
|
||||
registers=(_R.battery_status, ),
|
||||
)
|
||||
|
||||
_D('Wireless Keyboard MK320', protocol=1.0, wpid='200F',
|
||||
registers=(_R.battery_status, ),
|
||||
)
|
||||
_D(
|
||||
'Wireless Keyboard MK320',
|
||||
protocol=1.0,
|
||||
wpid='200F',
|
||||
registers=(_R.battery_status, ),
|
||||
)
|
||||
_D('Wireless Keyboard MK330')
|
||||
_D('Wireless Compact Keyboard K340', protocol=1.0, wpid='2007',
|
||||
registers=(_R.battery_status, ),
|
||||
)
|
||||
_D('Wireless Wave Keyboard K350', protocol=1.0, wpid='200A',
|
||||
registers=(_R.battery_status, ),
|
||||
)
|
||||
_D('Wireless Keyboard K360', protocol=2.0, wpid='4004',
|
||||
settings=[
|
||||
_FS.fn_swap()
|
||||
],
|
||||
)
|
||||
_D('Wireless Keyboard K375s', protocol=2.0, wpid='4061',
|
||||
settings=[
|
||||
_FS.k375s_fn_swap()
|
||||
],
|
||||
)
|
||||
_D('Wireless Touch Keyboard K400', protocol=2.0, wpid=('400E', '4024'),
|
||||
settings=[
|
||||
_FS.fn_swap()
|
||||
],
|
||||
)
|
||||
_D('Wireless Touch Keyboard K400 Plus', codename='K400 Plus', protocol=2.0, wpid='404D',
|
||||
settings=[
|
||||
_FS.new_fn_swap(),
|
||||
_FS.reprogrammable_keys(),
|
||||
_FS.disable_keyboard_keys(),
|
||||
],
|
||||
)
|
||||
_D('Wireless Keyboard K520', protocol=1.0, wpid='2011',
|
||||
registers=(_R.battery_status, ),
|
||||
settings=[
|
||||
_RS.fn_swap(),
|
||||
],
|
||||
)
|
||||
_D('Number Pad N545', protocol=1.0, wpid='2006',
|
||||
registers=(_R.battery_status, ),
|
||||
)
|
||||
_D(
|
||||
'Wireless Compact Keyboard K340',
|
||||
protocol=1.0,
|
||||
wpid='2007',
|
||||
registers=(_R.battery_status, ),
|
||||
)
|
||||
_D(
|
||||
'Wireless Wave Keyboard K350',
|
||||
protocol=1.0,
|
||||
wpid='200A',
|
||||
registers=(_R.battery_status, ),
|
||||
)
|
||||
_D(
|
||||
'Wireless Keyboard K360',
|
||||
protocol=2.0,
|
||||
wpid='4004',
|
||||
settings=[_FS.fn_swap()],
|
||||
)
|
||||
_D(
|
||||
'Wireless Keyboard K375s',
|
||||
protocol=2.0,
|
||||
wpid='4061',
|
||||
settings=[_FS.k375s_fn_swap()],
|
||||
)
|
||||
_D(
|
||||
'Wireless Touch Keyboard K400',
|
||||
protocol=2.0,
|
||||
wpid=('400E', '4024'),
|
||||
settings=[_FS.fn_swap()],
|
||||
)
|
||||
_D(
|
||||
'Wireless Touch Keyboard K400 Plus',
|
||||
codename='K400 Plus',
|
||||
protocol=2.0,
|
||||
wpid='404D',
|
||||
settings=[
|
||||
_FS.new_fn_swap(),
|
||||
_FS.reprogrammable_keys(),
|
||||
_FS.disable_keyboard_keys(),
|
||||
],
|
||||
)
|
||||
_D(
|
||||
'Wireless Keyboard K520',
|
||||
protocol=1.0,
|
||||
wpid='2011',
|
||||
registers=(_R.battery_status, ),
|
||||
settings=[
|
||||
_RS.fn_swap(),
|
||||
],
|
||||
)
|
||||
_D(
|
||||
'Number Pad N545',
|
||||
protocol=1.0,
|
||||
wpid='2006',
|
||||
registers=(_R.battery_status, ),
|
||||
)
|
||||
_D('Wireless Keyboard MK550')
|
||||
_D('Wireless Keyboard MK700', protocol=1.0, wpid='2008',
|
||||
registers=(_R.battery_status, ),
|
||||
settings=[
|
||||
_RS.fn_swap(),
|
||||
],
|
||||
)
|
||||
_D('Wireless Solar Keyboard K750', protocol=2.0, wpid='4002',
|
||||
settings=[
|
||||
_FS.fn_swap()
|
||||
],
|
||||
)
|
||||
_D('Wireless Multi-Device Keyboard K780', protocol=4.5, wpid='405B',
|
||||
settings=[
|
||||
_FS.new_fn_swap()
|
||||
],
|
||||
)
|
||||
_D('Wireless Illuminated Keyboard K800', protocol=1.0, wpid='2010',
|
||||
registers=(_R.battery_status, _R.three_leds, ),
|
||||
settings=[
|
||||
_RS.fn_swap(),
|
||||
_RS.hand_detection(),
|
||||
],
|
||||
)
|
||||
_D('Wireless Illuminated Keyboard K800 new', codename='K800 new', protocol=4.5, wpid='406E',
|
||||
settings=[
|
||||
_FS.fn_swap()
|
||||
],
|
||||
)
|
||||
_D('Illuminated Living-Room Keyboard K830', protocol=2.0, wpid='4032',
|
||||
settings=[
|
||||
_FS.new_fn_swap()
|
||||
],
|
||||
)
|
||||
_D(
|
||||
'Wireless Keyboard MK700',
|
||||
protocol=1.0,
|
||||
wpid='2008',
|
||||
registers=(_R.battery_status, ),
|
||||
settings=[
|
||||
_RS.fn_swap(),
|
||||
],
|
||||
)
|
||||
_D(
|
||||
'Wireless Solar Keyboard K750',
|
||||
protocol=2.0,
|
||||
wpid='4002',
|
||||
settings=[_FS.fn_swap()],
|
||||
)
|
||||
_D(
|
||||
'Wireless Multi-Device Keyboard K780',
|
||||
protocol=4.5,
|
||||
wpid='405B',
|
||||
settings=[_FS.new_fn_swap()],
|
||||
)
|
||||
_D(
|
||||
'Wireless Illuminated Keyboard K800',
|
||||
protocol=1.0,
|
||||
wpid='2010',
|
||||
registers=(
|
||||
_R.battery_status,
|
||||
_R.three_leds,
|
||||
),
|
||||
settings=[
|
||||
_RS.fn_swap(),
|
||||
_RS.hand_detection(),
|
||||
],
|
||||
)
|
||||
_D(
|
||||
'Wireless Illuminated Keyboard K800 new',
|
||||
codename='K800 new',
|
||||
protocol=4.5,
|
||||
wpid='406E',
|
||||
settings=[_FS.fn_swap()],
|
||||
)
|
||||
_D(
|
||||
'Illuminated Living-Room Keyboard K830',
|
||||
protocol=2.0,
|
||||
wpid='4032',
|
||||
settings=[_FS.new_fn_swap()],
|
||||
)
|
||||
_D('Craft Advanced Keyboard', codename='Craft', protocol=4.5, wpid='4066')
|
||||
|
||||
_D('Wireless Keyboard S510', codename='S510', protocol=1.0, wpid='3622',
|
||||
registers=(_R.battery_status, ),
|
||||
)
|
||||
_D(
|
||||
'Wireless Keyboard S510',
|
||||
codename='S510',
|
||||
protocol=1.0,
|
||||
wpid='3622',
|
||||
registers=(_R.battery_status, ),
|
||||
)
|
||||
|
||||
# Mice
|
||||
|
||||
_D('Wireless Mouse M150', protocol=2.0, wpid='4022')
|
||||
_D('Wireless Mouse M175', protocol=2.0, wpid='4008')
|
||||
_D('Wireless Mouse M185 new', codename='M185n', protocol=4.5, wpid='4054',
|
||||
settings=[
|
||||
_FS.lowres_smooth_scroll(),
|
||||
_FS.pointer_speed(),
|
||||
])
|
||||
_D('Wireless Mouse M185 new',
|
||||
codename='M185n',
|
||||
protocol=4.5,
|
||||
wpid='4054',
|
||||
settings=[
|
||||
_FS.lowres_smooth_scroll(),
|
||||
_FS.pointer_speed(),
|
||||
])
|
||||
# Apparently Logitech uses wpid 4055 for three different mice
|
||||
# That's not so strange, as M185 is used on both Unifying-ready and non-Unifying-ready mice
|
||||
_D('Wireless Mouse M185/M235/M310', codename='M185/M235/M310', protocol=4.5, wpid='4055',
|
||||
settings=[
|
||||
_FS.lowres_smooth_scroll(),
|
||||
_FS.pointer_speed(),
|
||||
])
|
||||
_D('Wireless Mouse M185/M235/M310',
|
||||
codename='M185/M235/M310',
|
||||
protocol=4.5,
|
||||
wpid='4055',
|
||||
settings=[
|
||||
_FS.lowres_smooth_scroll(),
|
||||
_FS.pointer_speed(),
|
||||
])
|
||||
_D('Wireless Mouse M185', protocol=2.0, wpid='4038')
|
||||
_D('Wireless Mouse M187', protocol=2.0, wpid='4019')
|
||||
_D('Wireless Mouse M215', protocol=1.0, wpid='1020')
|
||||
_D('Wireless Mouse M305', protocol=1.0, wpid='101F',
|
||||
registers=(_R.battery_status, ),
|
||||
settings=[
|
||||
_RS.side_scroll(),
|
||||
],
|
||||
)
|
||||
_D('Wireless Mouse M310', protocol=1.0, wpid='1024',
|
||||
registers=(_R.battery_status, ),
|
||||
)
|
||||
_D(
|
||||
'Wireless Mouse M305',
|
||||
protocol=1.0,
|
||||
wpid='101F',
|
||||
registers=(_R.battery_status, ),
|
||||
settings=[
|
||||
_RS.side_scroll(),
|
||||
],
|
||||
)
|
||||
_D(
|
||||
'Wireless Mouse M310',
|
||||
protocol=1.0,
|
||||
wpid='1024',
|
||||
registers=(_R.battery_status, ),
|
||||
)
|
||||
_D('Wireless Mouse M315')
|
||||
_D('Wireless Mouse M317')
|
||||
_D('Wireless Mouse M325', protocol=2.0, wpid='400A',
|
||||
settings=[
|
||||
_FS.hi_res_scroll(),
|
||||
])
|
||||
_D('Wireless Mouse M325',
|
||||
protocol=2.0,
|
||||
wpid='400A',
|
||||
settings=[
|
||||
_FS.hi_res_scroll(),
|
||||
])
|
||||
_D('Wireless Mouse M345', protocol=2.0, wpid='4017')
|
||||
_D('Wireless Mouse M350', protocol=1.0, wpid='101C',
|
||||
registers=(_R.battery_charge, ),
|
||||
)
|
||||
_D(
|
||||
'Wireless Mouse M350',
|
||||
protocol=1.0,
|
||||
wpid='101C',
|
||||
registers=(_R.battery_charge, ),
|
||||
)
|
||||
_D('Wireless Mouse Pebble M350', codename='Pebble', protocol=2.0, wpid='4080')
|
||||
_D('Wireless Mouse M505', codename='M505/B605', protocol=1.0, wpid='101D',
|
||||
registers=(_R.battery_charge, ),
|
||||
settings=[
|
||||
_RS.smooth_scroll(),
|
||||
_RS.side_scroll(),
|
||||
],
|
||||
)
|
||||
_D('Wireless Mouse M510', protocol=1.0, wpid='1025',
|
||||
registers=(_R.battery_status, ),
|
||||
settings=[
|
||||
_RS.smooth_scroll(),
|
||||
_RS.side_scroll(),
|
||||
],
|
||||
)
|
||||
_D('Wireless Mouse M510', codename='M510v2', protocol=2.0, wpid='4051',
|
||||
settings=[
|
||||
_FS.lowres_smooth_scroll(),
|
||||
])
|
||||
_D(
|
||||
'Wireless Mouse M505',
|
||||
codename='M505/B605',
|
||||
protocol=1.0,
|
||||
wpid='101D',
|
||||
registers=(_R.battery_charge, ),
|
||||
settings=[
|
||||
_RS.smooth_scroll(),
|
||||
_RS.side_scroll(),
|
||||
],
|
||||
)
|
||||
_D(
|
||||
'Wireless Mouse M510',
|
||||
protocol=1.0,
|
||||
wpid='1025',
|
||||
registers=(_R.battery_status, ),
|
||||
settings=[
|
||||
_RS.smooth_scroll(),
|
||||
_RS.side_scroll(),
|
||||
],
|
||||
)
|
||||
_D('Wireless Mouse M510',
|
||||
codename='M510v2',
|
||||
protocol=2.0,
|
||||
wpid='4051',
|
||||
settings=[
|
||||
_FS.lowres_smooth_scroll(),
|
||||
])
|
||||
_D('Couch Mouse M515', protocol=2.0, wpid='4007')
|
||||
_D('Wireless Mouse M525', protocol=2.0, wpid='4013')
|
||||
_D('Multi Device Silent Mouse M585/M590', codename='M585/M590', protocol=4.5, wpid='406B',
|
||||
settings=[
|
||||
_FS.lowres_smooth_scroll(),
|
||||
_FS.pointer_speed(),
|
||||
],
|
||||
)
|
||||
_D(
|
||||
'Multi Device Silent Mouse M585/M590',
|
||||
codename='M585/M590',
|
||||
protocol=4.5,
|
||||
wpid='406B',
|
||||
settings=[
|
||||
_FS.lowres_smooth_scroll(),
|
||||
_FS.pointer_speed(),
|
||||
],
|
||||
)
|
||||
_D('Touch Mouse M600', protocol=2.0, wpid='401A')
|
||||
_D('Marathon Mouse M705 (M-R0009)', codename='M705 (M-R0009)', protocol=1.0, wpid='101B',
|
||||
registers=(_R.battery_charge, ),
|
||||
settings=[
|
||||
_RS.smooth_scroll(),
|
||||
_RS.side_scroll(),
|
||||
],
|
||||
)
|
||||
_D('Marathon Mouse M705 (M-R0073)', codename='M705 (M-R0073)', protocol=4.5, wpid='406D',
|
||||
settings=[
|
||||
_FS.hires_smooth_invert(),
|
||||
_FS.hires_smooth_resolution(),
|
||||
_FS.pointer_speed(),
|
||||
])
|
||||
_D(
|
||||
'Marathon Mouse M705 (M-R0009)',
|
||||
codename='M705 (M-R0009)',
|
||||
protocol=1.0,
|
||||
wpid='101B',
|
||||
registers=(_R.battery_charge, ),
|
||||
settings=[
|
||||
_RS.smooth_scroll(),
|
||||
_RS.side_scroll(),
|
||||
],
|
||||
)
|
||||
_D('Marathon Mouse M705 (M-R0073)',
|
||||
codename='M705 (M-R0073)',
|
||||
protocol=4.5,
|
||||
wpid='406D',
|
||||
settings=[
|
||||
_FS.hires_smooth_invert(),
|
||||
_FS.hires_smooth_resolution(),
|
||||
_FS.pointer_speed(),
|
||||
])
|
||||
_D('Zone Touch Mouse T400')
|
||||
_D('Touch Mouse T620', protocol=2.0)
|
||||
_D('Logitech Cube', kind=_DK.mouse, protocol=2.0)
|
||||
_D('Anywhere Mouse MX', codename='Anywhere MX', protocol=1.0, wpid='1017',
|
||||
registers=(_R.battery_charge, ),
|
||||
settings=[
|
||||
_RS.smooth_scroll(),
|
||||
_RS.side_scroll(),
|
||||
],
|
||||
)
|
||||
_D('Anywhere Mouse MX 2', codename='Anywhere MX 2', protocol=4.5, wpid='404A',
|
||||
settings=[
|
||||
_FS.hires_smooth_invert(),
|
||||
_FS.hires_smooth_resolution(),
|
||||
],
|
||||
)
|
||||
_D('Performance Mouse MX', codename='Performance MX', protocol=1.0, wpid='101A',
|
||||
registers=(_R.battery_status, _R.three_leds, ),
|
||||
settings=[
|
||||
_RS.dpi(choices=_PERFORMANCE_MX_DPIS),
|
||||
_RS.smooth_scroll(),
|
||||
_RS.side_scroll(),
|
||||
],
|
||||
)
|
||||
_D(
|
||||
'Anywhere Mouse MX',
|
||||
codename='Anywhere MX',
|
||||
protocol=1.0,
|
||||
wpid='1017',
|
||||
registers=(_R.battery_charge, ),
|
||||
settings=[
|
||||
_RS.smooth_scroll(),
|
||||
_RS.side_scroll(),
|
||||
],
|
||||
)
|
||||
_D(
|
||||
'Anywhere Mouse MX 2',
|
||||
codename='Anywhere MX 2',
|
||||
protocol=4.5,
|
||||
wpid='404A',
|
||||
settings=[
|
||||
_FS.hires_smooth_invert(),
|
||||
_FS.hires_smooth_resolution(),
|
||||
],
|
||||
)
|
||||
_D(
|
||||
'Performance Mouse MX',
|
||||
codename='Performance MX',
|
||||
protocol=1.0,
|
||||
wpid='101A',
|
||||
registers=(
|
||||
_R.battery_status,
|
||||
_R.three_leds,
|
||||
),
|
||||
settings=[
|
||||
_RS.dpi(choices=_PERFORMANCE_MX_DPIS),
|
||||
_RS.smooth_scroll(),
|
||||
_RS.side_scroll(),
|
||||
],
|
||||
)
|
||||
|
||||
_D('Wireless Mouse MX Master', codename='MX Master', protocol=4.5, wpid='4041',
|
||||
settings=[
|
||||
_FS.hires_smooth_invert(),
|
||||
_FS.hires_smooth_resolution(),
|
||||
],
|
||||
)
|
||||
_D(
|
||||
'Wireless Mouse MX Master',
|
||||
codename='MX Master',
|
||||
protocol=4.5,
|
||||
wpid='4041',
|
||||
settings=[
|
||||
_FS.hires_smooth_invert(),
|
||||
_FS.hires_smooth_resolution(),
|
||||
],
|
||||
)
|
||||
|
||||
_D('Wireless Mouse MX Master 2S', codename='MX Master 2S', protocol=4.5,wpid='4069',
|
||||
settings=[
|
||||
_FS.hires_smooth_invert(),
|
||||
_FS.hires_smooth_resolution(),
|
||||
],
|
||||
)
|
||||
_D(
|
||||
'Wireless Mouse MX Master 2S',
|
||||
codename='MX Master 2S',
|
||||
protocol=4.5,
|
||||
wpid='4069',
|
||||
settings=[
|
||||
_FS.hires_smooth_invert(),
|
||||
_FS.hires_smooth_resolution(),
|
||||
],
|
||||
)
|
||||
|
||||
_D('Wireless Mouse MX Vertical', codename='MX Vertical', protocol=4.5, wpid='407B')
|
||||
_D('Wireless Mouse MX Vertical',
|
||||
codename='MX Vertical',
|
||||
protocol=4.5,
|
||||
wpid='407B')
|
||||
|
||||
_D('G7 Cordless Laser Mouse', codename='G7', protocol=1.0, wpid='1002',
|
||||
registers=(_R.battery_status, ),
|
||||
)
|
||||
_D('G700 Gaming Mouse', codename='G700', protocol=1.0, wpid='1023',
|
||||
registers=(_R.battery_status, _R.three_leds, ),
|
||||
settings=[
|
||||
_RS.smooth_scroll(),
|
||||
_RS.side_scroll(),
|
||||
],
|
||||
)
|
||||
_D('G700s Gaming Mouse', codename='G700s', protocol=1.0, wpid='102A',
|
||||
registers=(_R.battery_status, _R.three_leds, ),
|
||||
settings=[
|
||||
_RS.smooth_scroll(),
|
||||
_RS.side_scroll(),
|
||||
],
|
||||
)
|
||||
_D('LX5 Cordless Mouse', codename='LX5', protocol=1.0, wpid='5612',
|
||||
registers=(_R.battery_status, ),
|
||||
)
|
||||
_D('Wireless Mouse M30', codename='M30', protocol=1.0, wpid='6822',
|
||||
registers=(_R.battery_status, ),
|
||||
)
|
||||
_D(
|
||||
'G7 Cordless Laser Mouse',
|
||||
codename='G7',
|
||||
protocol=1.0,
|
||||
wpid='1002',
|
||||
registers=(_R.battery_status, ),
|
||||
)
|
||||
_D(
|
||||
'G700 Gaming Mouse',
|
||||
codename='G700',
|
||||
protocol=1.0,
|
||||
wpid='1023',
|
||||
registers=(
|
||||
_R.battery_status,
|
||||
_R.three_leds,
|
||||
),
|
||||
settings=[
|
||||
_RS.smooth_scroll(),
|
||||
_RS.side_scroll(),
|
||||
],
|
||||
)
|
||||
_D(
|
||||
'G700s Gaming Mouse',
|
||||
codename='G700s',
|
||||
protocol=1.0,
|
||||
wpid='102A',
|
||||
registers=(
|
||||
_R.battery_status,
|
||||
_R.three_leds,
|
||||
),
|
||||
settings=[
|
||||
_RS.smooth_scroll(),
|
||||
_RS.side_scroll(),
|
||||
],
|
||||
)
|
||||
_D(
|
||||
'LX5 Cordless Mouse',
|
||||
codename='LX5',
|
||||
protocol=1.0,
|
||||
wpid='5612',
|
||||
registers=(_R.battery_status, ),
|
||||
)
|
||||
_D(
|
||||
'Wireless Mouse M30',
|
||||
codename='M30',
|
||||
protocol=1.0,
|
||||
wpid='6822',
|
||||
registers=(_R.battery_status, ),
|
||||
)
|
||||
|
||||
# Trackballs
|
||||
|
||||
|
|
@ -396,57 +552,109 @@ _D('Wireless Touchpad', codename='Wireless Touch', protocol=2.0, wpid='4011')
|
|||
# A wpid is necessary to properly identify them.
|
||||
#
|
||||
|
||||
_D('VX Nano Cordless Laser Mouse', codename='VX Nano', protocol=1.0, wpid=('100B', '100F'),
|
||||
registers=(_R.battery_charge, ),
|
||||
settings=[
|
||||
_RS.smooth_scroll(),
|
||||
_RS.side_scroll(),
|
||||
],
|
||||
)
|
||||
_D('V450 Nano Cordless Laser Mouse', codename='V450 Nano', protocol=1.0, wpid='1011',
|
||||
registers=(_R.battery_charge, ),
|
||||
)
|
||||
_D('V550 Nano Cordless Laser Mouse', codename='V550 Nano', protocol=1.0, wpid='1013',
|
||||
registers=(_R.battery_charge, ),
|
||||
settings=[
|
||||
_RS.smooth_scroll(),
|
||||
_RS.side_scroll(),
|
||||
],
|
||||
)
|
||||
_D(
|
||||
'VX Nano Cordless Laser Mouse',
|
||||
codename='VX Nano',
|
||||
protocol=1.0,
|
||||
wpid=('100B', '100F'),
|
||||
registers=(_R.battery_charge, ),
|
||||
settings=[
|
||||
_RS.smooth_scroll(),
|
||||
_RS.side_scroll(),
|
||||
],
|
||||
)
|
||||
_D(
|
||||
'V450 Nano Cordless Laser Mouse',
|
||||
codename='V450 Nano',
|
||||
protocol=1.0,
|
||||
wpid='1011',
|
||||
registers=(_R.battery_charge, ),
|
||||
)
|
||||
_D(
|
||||
'V550 Nano Cordless Laser Mouse',
|
||||
codename='V550 Nano',
|
||||
protocol=1.0,
|
||||
wpid='1013',
|
||||
registers=(_R.battery_charge, ),
|
||||
settings=[
|
||||
_RS.smooth_scroll(),
|
||||
_RS.side_scroll(),
|
||||
],
|
||||
)
|
||||
|
||||
# Mini receiver mice
|
||||
|
||||
_D('MX610 Laser Cordless Mouse', codename='MX610', protocol=1.0, wpid='1001',
|
||||
registers=(_R.battery_status, ),
|
||||
)
|
||||
_D('MX620 Laser Cordless Mouse', codename='MX620', protocol=1.0, wpid=('100A', '1016'),
|
||||
registers=(_R.battery_charge, ),
|
||||
)
|
||||
_D('MX610 Left-Handled Mouse', codename='MX610L', protocol=1.0, wpid='1004',
|
||||
registers=(_R.battery_status, ),
|
||||
)
|
||||
_D('V400 Laser Cordless Mouse', codename='V400', protocol=1.0, wpid='1003',
|
||||
registers=(_R.battery_status, ),
|
||||
)
|
||||
_D('V450 Laser Cordless Mouse', codename='V450', protocol=1.0, wpid='1005',
|
||||
registers=(_R.battery_status, ),
|
||||
)
|
||||
_D('VX Revolution', codename='VX Revolution', kind=_DK.mouse, protocol=1.0, wpid=('1006', '100D'),
|
||||
registers=(_R.battery_charge, ),
|
||||
)
|
||||
_D('MX Air', codename='MX Air', protocol=1.0, kind=_DK.mouse, wpid=('1007', '100E'),
|
||||
registers=(_R.battery_charge, ),
|
||||
)
|
||||
_D('MX Revolution', codename='MX Revolution', protocol=1.0, kind=_DK.mouse, wpid=('1008', '100C'),
|
||||
registers=(_R.battery_charge, ),
|
||||
)
|
||||
_D('MX 1100 Cordless Laser Mouse', codename='MX 1100', protocol=1.0, kind=_DK.mouse, wpid='1014',
|
||||
registers=(_R.battery_charge, ),
|
||||
settings=[
|
||||
_RS.smooth_scroll(),
|
||||
_RS.side_scroll(),
|
||||
],
|
||||
)
|
||||
_D(
|
||||
'MX610 Laser Cordless Mouse',
|
||||
codename='MX610',
|
||||
protocol=1.0,
|
||||
wpid='1001',
|
||||
registers=(_R.battery_status, ),
|
||||
)
|
||||
_D(
|
||||
'MX620 Laser Cordless Mouse',
|
||||
codename='MX620',
|
||||
protocol=1.0,
|
||||
wpid=('100A', '1016'),
|
||||
registers=(_R.battery_charge, ),
|
||||
)
|
||||
_D(
|
||||
'MX610 Left-Handled Mouse',
|
||||
codename='MX610L',
|
||||
protocol=1.0,
|
||||
wpid='1004',
|
||||
registers=(_R.battery_status, ),
|
||||
)
|
||||
_D(
|
||||
'V400 Laser Cordless Mouse',
|
||||
codename='V400',
|
||||
protocol=1.0,
|
||||
wpid='1003',
|
||||
registers=(_R.battery_status, ),
|
||||
)
|
||||
_D(
|
||||
'V450 Laser Cordless Mouse',
|
||||
codename='V450',
|
||||
protocol=1.0,
|
||||
wpid='1005',
|
||||
registers=(_R.battery_status, ),
|
||||
)
|
||||
_D(
|
||||
'VX Revolution',
|
||||
codename='VX Revolution',
|
||||
kind=_DK.mouse,
|
||||
protocol=1.0,
|
||||
wpid=('1006', '100D'),
|
||||
registers=(_R.battery_charge, ),
|
||||
)
|
||||
_D(
|
||||
'MX Air',
|
||||
codename='MX Air',
|
||||
protocol=1.0,
|
||||
kind=_DK.mouse,
|
||||
wpid=('1007', '100E'),
|
||||
registers=(_R.battery_charge, ),
|
||||
)
|
||||
_D(
|
||||
'MX Revolution',
|
||||
codename='MX Revolution',
|
||||
protocol=1.0,
|
||||
kind=_DK.mouse,
|
||||
wpid=('1008', '100C'),
|
||||
registers=(_R.battery_charge, ),
|
||||
)
|
||||
_D(
|
||||
'MX 1100 Cordless Laser Mouse',
|
||||
codename='MX 1100',
|
||||
protocol=1.0,
|
||||
kind=_DK.mouse,
|
||||
wpid='1014',
|
||||
registers=(_R.battery_charge, ),
|
||||
settings=[
|
||||
_RS.smooth_scroll(),
|
||||
_RS.side_scroll(),
|
||||
],
|
||||
)
|
||||
|
||||
# Some exotics...
|
||||
|
||||
|
|
|
|||
|
|
@ -23,12 +23,9 @@ from logging import getLogger # , DEBUG as _DEBUG
|
|||
_log = getLogger(__name__)
|
||||
del getLogger
|
||||
|
||||
|
||||
from .common import (strhex as _strhex,
|
||||
bytes2int as _bytes2int,
|
||||
int2bytes as _int2bytes,
|
||||
NamedInts as _NamedInts,
|
||||
FirmwareInfo as _FirmwareInfo)
|
||||
from .common import (strhex as _strhex, bytes2int as _bytes2int, int2bytes as
|
||||
_int2bytes, NamedInts as _NamedInts, FirmwareInfo as
|
||||
_FirmwareInfo)
|
||||
from .hidpp20 import FIRMWARE_KIND, BATTERY_STATUS
|
||||
|
||||
#
|
||||
|
|
@ -36,26 +33,24 @@ from .hidpp20 import FIRMWARE_KIND, BATTERY_STATUS
|
|||
# documentation, some of them guessed.
|
||||
#
|
||||
|
||||
DEVICE_KIND = _NamedInts(
|
||||
keyboard=0x01,
|
||||
mouse=0x02,
|
||||
numpad=0x03,
|
||||
presenter=0x04,
|
||||
trackball=0x08,
|
||||
touchpad=0x09)
|
||||
DEVICE_KIND = _NamedInts(keyboard=0x01,
|
||||
mouse=0x02,
|
||||
numpad=0x03,
|
||||
presenter=0x04,
|
||||
trackball=0x08,
|
||||
touchpad=0x09)
|
||||
|
||||
POWER_SWITCH_LOCATION = _NamedInts(
|
||||
base=0x01,
|
||||
top_case=0x02,
|
||||
edge_of_top_right_corner=0x03,
|
||||
top_left_corner=0x05,
|
||||
bottom_left_corner=0x06,
|
||||
top_right_corner=0x07,
|
||||
bottom_right_corner=0x08,
|
||||
top_edge=0x09,
|
||||
right_edge=0x0A,
|
||||
left_edge=0x0B,
|
||||
bottom_edge=0x0C)
|
||||
POWER_SWITCH_LOCATION = _NamedInts(base=0x01,
|
||||
top_case=0x02,
|
||||
edge_of_top_right_corner=0x03,
|
||||
top_left_corner=0x05,
|
||||
bottom_left_corner=0x06,
|
||||
top_right_corner=0x07,
|
||||
bottom_right_corner=0x08,
|
||||
top_edge=0x09,
|
||||
right_edge=0x0A,
|
||||
left_edge=0x0B,
|
||||
bottom_edge=0x0C)
|
||||
|
||||
# Some flags are used both by devices and receivers. The Logitech documentation
|
||||
# mentions that the first and last (third) byte are used for devices while the
|
||||
|
|
@ -70,261 +65,259 @@ POWER_SWITCH_LOCATION = _NamedInts(
|
|||
# In the future would be useful to have separate enums for receiver and device notification flags,
|
||||
# but right now we don't know enough.
|
||||
NOTIFICATION_FLAG = _NamedInts(
|
||||
battery_status= 0x100000, # send battery charge notifications (0x07 or 0x0D)
|
||||
keyboard_sleep_raw= 0x020000, # system control keys such as Sleep
|
||||
keyboard_multimedia_raw=0x010000, # consumer controls such as Mute and Calculator
|
||||
# reserved_r1b4= 0x001000, # unknown, seen on a unifying receiver
|
||||
software_present= 0x000800, # .. no idea
|
||||
keyboard_illumination= 0x000200, # illumination brightness level changes (by pressing keys)
|
||||
wireless= 0x000100, # notify when the device wireless goes on/off-line
|
||||
)
|
||||
battery_status=0x100000, # send battery charge notifications (0x07 or 0x0D)
|
||||
keyboard_sleep_raw=0x020000, # system control keys such as Sleep
|
||||
keyboard_multimedia_raw=
|
||||
0x010000, # consumer controls such as Mute and Calculator
|
||||
# reserved_r1b4= 0x001000, # unknown, seen on a unifying receiver
|
||||
software_present=0x000800, # .. no idea
|
||||
keyboard_illumination=
|
||||
0x000200, # illumination brightness level changes (by pressing keys)
|
||||
wireless=0x000100, # notify when the device wireless goes on/off-line
|
||||
)
|
||||
|
||||
ERROR = _NamedInts(
|
||||
invalid_SubID__command=0x01,
|
||||
invalid_address=0x02,
|
||||
invalid_value=0x03,
|
||||
connection_request_failed=0x04,
|
||||
too_many_devices=0x05,
|
||||
already_exists=0x06,
|
||||
busy=0x07,
|
||||
unknown_device=0x08,
|
||||
resource_error=0x09,
|
||||
request_unavailable=0x0A,
|
||||
unsupported_parameter_value=0x0B,
|
||||
wrong_pin_code=0x0C)
|
||||
ERROR = _NamedInts(invalid_SubID__command=0x01,
|
||||
invalid_address=0x02,
|
||||
invalid_value=0x03,
|
||||
connection_request_failed=0x04,
|
||||
too_many_devices=0x05,
|
||||
already_exists=0x06,
|
||||
busy=0x07,
|
||||
unknown_device=0x08,
|
||||
resource_error=0x09,
|
||||
request_unavailable=0x0A,
|
||||
unsupported_parameter_value=0x0B,
|
||||
wrong_pin_code=0x0C)
|
||||
|
||||
PAIRING_ERRORS = _NamedInts(
|
||||
device_timeout=0x01,
|
||||
device_not_supported=0x02,
|
||||
too_many_devices=0x03,
|
||||
sequence_timeout=0x06)
|
||||
|
||||
BATTERY_APPOX = _NamedInts(
|
||||
empty = 0,
|
||||
critical = 5,
|
||||
low = 20,
|
||||
good = 50,
|
||||
full = 90)
|
||||
PAIRING_ERRORS = _NamedInts(device_timeout=0x01,
|
||||
device_not_supported=0x02,
|
||||
too_many_devices=0x03,
|
||||
sequence_timeout=0x06)
|
||||
|
||||
BATTERY_APPOX = _NamedInts(empty=0, critical=5, low=20, good=50, full=90)
|
||||
"""Known registers.
|
||||
Devices usually have a (small) sub-set of these. Some registers are only
|
||||
applicable to certain device kinds (e.g. smooth_scroll only applies to mice."""
|
||||
REGISTERS = _NamedInts(
|
||||
# only apply to receivers
|
||||
receiver_connection=0x02,
|
||||
receiver_pairing=0xB2,
|
||||
devices_activity=0x2B3,
|
||||
receiver_info=0x2B5,
|
||||
# only apply to receivers
|
||||
receiver_connection=0x02,
|
||||
receiver_pairing=0xB2,
|
||||
devices_activity=0x2B3,
|
||||
receiver_info=0x2B5,
|
||||
|
||||
# only apply to devices
|
||||
mouse_button_flags=0x01,
|
||||
keyboard_hand_detection=0x01,
|
||||
battery_status=0x07,
|
||||
keyboard_fn_swap=0x09,
|
||||
battery_charge=0x0D,
|
||||
keyboard_illumination=0x17,
|
||||
three_leds=0x51,
|
||||
mouse_dpi=0x63,
|
||||
# only apply to devices
|
||||
mouse_button_flags=0x01,
|
||||
keyboard_hand_detection=0x01,
|
||||
battery_status=0x07,
|
||||
keyboard_fn_swap=0x09,
|
||||
battery_charge=0x0D,
|
||||
keyboard_illumination=0x17,
|
||||
three_leds=0x51,
|
||||
mouse_dpi=0x63,
|
||||
|
||||
# apply to both
|
||||
notifications=0x00,
|
||||
firmware=0xF1,
|
||||
)
|
||||
# apply to both
|
||||
notifications=0x00,
|
||||
firmware=0xF1,
|
||||
)
|
||||
|
||||
#
|
||||
# functions
|
||||
#
|
||||
|
||||
|
||||
def read_register(device, register_number, *params):
|
||||
assert device, 'tried to read register %02X from invalid device %s' % (register_number, device)
|
||||
# support long registers by adding a 2 in front of the register number
|
||||
request_id = 0x8100 | (int(register_number) & 0x2FF)
|
||||
return device.request(request_id, *params)
|
||||
assert device, 'tried to read register %02X from invalid device %s' % (
|
||||
register_number, device)
|
||||
# support long registers by adding a 2 in front of the register number
|
||||
request_id = 0x8100 | (int(register_number) & 0x2FF)
|
||||
return device.request(request_id, *params)
|
||||
|
||||
|
||||
def write_register(device, register_number, *value):
|
||||
assert device, 'tried to write register %02X to invalid device %s' % (register_number, device)
|
||||
# support long registers by adding a 2 in front of the register number
|
||||
request_id = 0x8000 | (int(register_number) & 0x2FF)
|
||||
return device.request(request_id, *value)
|
||||
assert device, 'tried to write register %02X to invalid device %s' % (
|
||||
register_number, device)
|
||||
# support long registers by adding a 2 in front of the register number
|
||||
request_id = 0x8000 | (int(register_number) & 0x2FF)
|
||||
return device.request(request_id, *value)
|
||||
|
||||
|
||||
def get_battery(device):
|
||||
assert device
|
||||
assert device.kind is not None
|
||||
if not device.online:
|
||||
return
|
||||
assert device
|
||||
assert device.kind is not None
|
||||
if not device.online:
|
||||
return
|
||||
"""Reads a device's battery level, if provided by the HID++ 1.0 protocol."""
|
||||
if device.protocol and device.protocol >= 2.0:
|
||||
# let's just assume HID++ 2.0 devices do not provide the battery info in a register
|
||||
return
|
||||
|
||||
"""Reads a device's battery level, if provided by the HID++ 1.0 protocol."""
|
||||
if device.protocol and device.protocol >= 2.0:
|
||||
# let's just assume HID++ 2.0 devices do not provide the battery info in a register
|
||||
return
|
||||
for r in (REGISTERS.battery_status, REGISTERS.battery_charge):
|
||||
if r in device.registers:
|
||||
reply = read_register(device, r)
|
||||
if reply:
|
||||
return parse_battery_status(r, reply)
|
||||
return
|
||||
|
||||
for r in (REGISTERS.battery_status, REGISTERS.battery_charge):
|
||||
if r in device.registers:
|
||||
reply = read_register(device, r)
|
||||
if reply:
|
||||
return parse_battery_status(r, reply)
|
||||
return
|
||||
# the descriptor does not tell us which register this device has, try them both
|
||||
reply = read_register(device, REGISTERS.battery_charge)
|
||||
if reply:
|
||||
# remember this for the next time
|
||||
device.registers.append(REGISTERS.battery_charge)
|
||||
return parse_battery_status(REGISTERS.battery_charge, reply)
|
||||
|
||||
# the descriptor does not tell us which register this device has, try them both
|
||||
reply = read_register(device, REGISTERS.battery_charge)
|
||||
if reply:
|
||||
# remember this for the next time
|
||||
device.registers.append(REGISTERS.battery_charge)
|
||||
return parse_battery_status(REGISTERS.battery_charge, reply)
|
||||
|
||||
reply = read_register(device, REGISTERS.battery_status)
|
||||
if reply:
|
||||
# remember this for the next time
|
||||
device.registers.append(REGISTERS.battery_status)
|
||||
return parse_battery_status(REGISTERS.battery_status, reply)
|
||||
reply = read_register(device, REGISTERS.battery_status)
|
||||
if reply:
|
||||
# remember this for the next time
|
||||
device.registers.append(REGISTERS.battery_status)
|
||||
return parse_battery_status(REGISTERS.battery_status, reply)
|
||||
|
||||
|
||||
def parse_battery_status(register, reply):
|
||||
if register == REGISTERS.battery_charge:
|
||||
charge = ord(reply[:1])
|
||||
status_byte = ord(reply[2:3]) & 0xF0
|
||||
status_text = (BATTERY_STATUS.discharging if status_byte == 0x30
|
||||
else BATTERY_STATUS.recharging if status_byte == 0x50
|
||||
else BATTERY_STATUS.full if status_byte == 0x90
|
||||
else None)
|
||||
return charge, status_text, None
|
||||
if register == REGISTERS.battery_charge:
|
||||
charge = ord(reply[:1])
|
||||
status_byte = ord(reply[2:3]) & 0xF0
|
||||
status_text = (BATTERY_STATUS.discharging if status_byte == 0x30 else
|
||||
BATTERY_STATUS.recharging if status_byte == 0x50 else
|
||||
BATTERY_STATUS.full if status_byte == 0x90 else None)
|
||||
return charge, status_text, None
|
||||
|
||||
if register == REGISTERS.battery_status:
|
||||
status_byte = ord(reply[:1])
|
||||
charge = (BATTERY_APPOX.full if status_byte == 7 # full
|
||||
else BATTERY_APPOX.good if status_byte == 5 # good
|
||||
else BATTERY_APPOX.low if status_byte == 3 # low
|
||||
else BATTERY_APPOX.critical if status_byte == 1 # critical
|
||||
# pure 'charging' notifications may come without a status
|
||||
else BATTERY_APPOX.empty)
|
||||
if register == REGISTERS.battery_status:
|
||||
status_byte = ord(reply[:1])
|
||||
charge = (
|
||||
BATTERY_APPOX.full if status_byte == 7 # full
|
||||
else BATTERY_APPOX.good if status_byte == 5 # good
|
||||
else BATTERY_APPOX.low if status_byte == 3 # low
|
||||
else BATTERY_APPOX.critical if status_byte == 1 # critical
|
||||
# pure 'charging' notifications may come without a status
|
||||
else BATTERY_APPOX.empty)
|
||||
|
||||
charging_byte = ord(reply[1:2])
|
||||
if charging_byte == 0x00:
|
||||
status_text = BATTERY_STATUS.discharging
|
||||
elif charging_byte & 0x21 == 0x21:
|
||||
status_text = BATTERY_STATUS.recharging
|
||||
elif charging_byte & 0x22 == 0x22:
|
||||
status_text = BATTERY_STATUS.full
|
||||
else:
|
||||
_log.warn("could not parse 0x07 battery status: %02X (level %02X)", charging_byte, status_byte)
|
||||
status_text = None
|
||||
charging_byte = ord(reply[1:2])
|
||||
if charging_byte == 0x00:
|
||||
status_text = BATTERY_STATUS.discharging
|
||||
elif charging_byte & 0x21 == 0x21:
|
||||
status_text = BATTERY_STATUS.recharging
|
||||
elif charging_byte & 0x22 == 0x22:
|
||||
status_text = BATTERY_STATUS.full
|
||||
else:
|
||||
_log.warn("could not parse 0x07 battery status: %02X (level %02X)",
|
||||
charging_byte, status_byte)
|
||||
status_text = None
|
||||
|
||||
if charging_byte & 0x03 and status_byte == 0:
|
||||
# some 'charging' notifications may come with no battery level information
|
||||
charge = None
|
||||
if charging_byte & 0x03 and status_byte == 0:
|
||||
# some 'charging' notifications may come with no battery level information
|
||||
charge = None
|
||||
|
||||
# Return None for next charge level as this is not in HID++ 1.0 spec
|
||||
return charge, status_text, None
|
||||
# Return None for next charge level as this is not in HID++ 1.0 spec
|
||||
return charge, status_text, None
|
||||
|
||||
|
||||
def get_firmware(device):
|
||||
assert device
|
||||
assert device
|
||||
|
||||
firmware = [None, None, None]
|
||||
firmware = [None, None, None]
|
||||
|
||||
reply = read_register(device, REGISTERS.firmware, 0x01)
|
||||
if not reply:
|
||||
# won't be able to read any of it now...
|
||||
return
|
||||
reply = read_register(device, REGISTERS.firmware, 0x01)
|
||||
if not reply:
|
||||
# won't be able to read any of it now...
|
||||
return
|
||||
|
||||
fw_version = _strhex(reply[1:3])
|
||||
fw_version = '%s.%s' % (fw_version[0:2], fw_version[2:4])
|
||||
reply = read_register(device, REGISTERS.firmware, 0x02)
|
||||
if reply:
|
||||
fw_version += '.B' + _strhex(reply[1:3])
|
||||
fw = _FirmwareInfo(FIRMWARE_KIND.Firmware, '', fw_version, None)
|
||||
firmware[0] = fw
|
||||
fw_version = _strhex(reply[1:3])
|
||||
fw_version = '%s.%s' % (fw_version[0:2], fw_version[2:4])
|
||||
reply = read_register(device, REGISTERS.firmware, 0x02)
|
||||
if reply:
|
||||
fw_version += '.B' + _strhex(reply[1:3])
|
||||
fw = _FirmwareInfo(FIRMWARE_KIND.Firmware, '', fw_version, None)
|
||||
firmware[0] = fw
|
||||
|
||||
reply = read_register(device, REGISTERS.firmware, 0x04)
|
||||
if reply:
|
||||
bl_version = _strhex(reply[1:3])
|
||||
bl_version = '%s.%s' % (bl_version[0:2], bl_version[2:4])
|
||||
bl = _FirmwareInfo(FIRMWARE_KIND.Bootloader, '', bl_version, None)
|
||||
firmware[1] = bl
|
||||
reply = read_register(device, REGISTERS.firmware, 0x04)
|
||||
if reply:
|
||||
bl_version = _strhex(reply[1:3])
|
||||
bl_version = '%s.%s' % (bl_version[0:2], bl_version[2:4])
|
||||
bl = _FirmwareInfo(FIRMWARE_KIND.Bootloader, '', bl_version, None)
|
||||
firmware[1] = bl
|
||||
|
||||
reply = read_register(device, REGISTERS.firmware, 0x03)
|
||||
if reply:
|
||||
o_version = _strhex(reply[1:3])
|
||||
o_version = '%s.%s' % (o_version[0:2], o_version[2:4])
|
||||
o = _FirmwareInfo(FIRMWARE_KIND.Other, '', o_version, None)
|
||||
firmware[2] = o
|
||||
reply = read_register(device, REGISTERS.firmware, 0x03)
|
||||
if reply:
|
||||
o_version = _strhex(reply[1:3])
|
||||
o_version = '%s.%s' % (o_version[0:2], o_version[2:4])
|
||||
o = _FirmwareInfo(FIRMWARE_KIND.Other, '', o_version, None)
|
||||
firmware[2] = o
|
||||
|
||||
if any(firmware):
|
||||
return tuple(f for f in firmware if f)
|
||||
if any(firmware):
|
||||
return tuple(f for f in firmware if f)
|
||||
|
||||
|
||||
def set_3leds(device, battery_level=None, charging=None, warning=None):
|
||||
assert device
|
||||
assert device.kind is not None
|
||||
if not device.online:
|
||||
return
|
||||
assert device
|
||||
assert device.kind is not None
|
||||
if not device.online:
|
||||
return
|
||||
|
||||
if REGISTERS.three_leds not in device.registers:
|
||||
return
|
||||
if REGISTERS.three_leds not in device.registers:
|
||||
return
|
||||
|
||||
if battery_level is not None:
|
||||
if battery_level < BATTERY_APPOX.critical:
|
||||
# 1 orange, and force blink
|
||||
v1, v2 = 0x22, 0x00
|
||||
warning = True
|
||||
elif battery_level < BATTERY_APPOX.low:
|
||||
# 1 orange
|
||||
v1, v2 = 0x22, 0x00
|
||||
elif battery_level < BATTERY_APPOX.good:
|
||||
# 1 green
|
||||
v1, v2 = 0x20, 0x00
|
||||
elif battery_level < BATTERY_APPOX.full:
|
||||
# 2 greens
|
||||
v1, v2 = 0x20, 0x02
|
||||
else:
|
||||
# all 3 green
|
||||
v1, v2 = 0x20, 0x22
|
||||
if warning:
|
||||
# set the blinking flag for the leds already set
|
||||
v1 |= (v1 >> 1)
|
||||
v2 |= (v2 >> 1)
|
||||
elif charging:
|
||||
# blink all green
|
||||
v1, v2 = 0x30,0x33
|
||||
elif warning:
|
||||
# 1 red
|
||||
v1, v2 = 0x02, 0x00
|
||||
else:
|
||||
# turn off all leds
|
||||
v1, v2 = 0x11, 0x11
|
||||
if battery_level is not None:
|
||||
if battery_level < BATTERY_APPOX.critical:
|
||||
# 1 orange, and force blink
|
||||
v1, v2 = 0x22, 0x00
|
||||
warning = True
|
||||
elif battery_level < BATTERY_APPOX.low:
|
||||
# 1 orange
|
||||
v1, v2 = 0x22, 0x00
|
||||
elif battery_level < BATTERY_APPOX.good:
|
||||
# 1 green
|
||||
v1, v2 = 0x20, 0x00
|
||||
elif battery_level < BATTERY_APPOX.full:
|
||||
# 2 greens
|
||||
v1, v2 = 0x20, 0x02
|
||||
else:
|
||||
# all 3 green
|
||||
v1, v2 = 0x20, 0x22
|
||||
if warning:
|
||||
# set the blinking flag for the leds already set
|
||||
v1 |= (v1 >> 1)
|
||||
v2 |= (v2 >> 1)
|
||||
elif charging:
|
||||
# blink all green
|
||||
v1, v2 = 0x30, 0x33
|
||||
elif warning:
|
||||
# 1 red
|
||||
v1, v2 = 0x02, 0x00
|
||||
else:
|
||||
# turn off all leds
|
||||
v1, v2 = 0x11, 0x11
|
||||
|
||||
write_register(device, REGISTERS.three_leds, v1, v2)
|
||||
write_register(device, REGISTERS.three_leds, v1, v2)
|
||||
|
||||
|
||||
def get_notification_flags(device):
|
||||
assert device
|
||||
assert device
|
||||
|
||||
# Avoid a call if the device is not online,
|
||||
# or the device does not support registers.
|
||||
if device.kind is not None:
|
||||
# peripherals with protocol >= 2.0 don't support registers
|
||||
if device.protocol and device.protocol >= 2.0:
|
||||
return
|
||||
# Avoid a call if the device is not online,
|
||||
# or the device does not support registers.
|
||||
if device.kind is not None:
|
||||
# peripherals with protocol >= 2.0 don't support registers
|
||||
if device.protocol and device.protocol >= 2.0:
|
||||
return
|
||||
|
||||
flags = read_register(device, REGISTERS.notifications)
|
||||
if flags is not None:
|
||||
assert len(flags) == 3
|
||||
return _bytes2int(flags)
|
||||
flags = read_register(device, REGISTERS.notifications)
|
||||
if flags is not None:
|
||||
assert len(flags) == 3
|
||||
return _bytes2int(flags)
|
||||
|
||||
|
||||
def set_notification_flags(device, *flag_bits):
|
||||
assert device
|
||||
assert device
|
||||
|
||||
# Avoid a call if the device is not online,
|
||||
# or the device does not support registers.
|
||||
if device.kind is not None:
|
||||
# peripherals with protocol >= 2.0 don't support registers
|
||||
if device.protocol and device.protocol >= 2.0:
|
||||
return
|
||||
# Avoid a call if the device is not online,
|
||||
# or the device does not support registers.
|
||||
if device.kind is not None:
|
||||
# peripherals with protocol >= 2.0 don't support registers
|
||||
if device.protocol and device.protocol >= 2.0:
|
||||
return
|
||||
|
||||
flag_bits = sum(int(b) for b in flag_bits)
|
||||
assert flag_bits & 0x00FFFFFF == flag_bits
|
||||
result = write_register(device, REGISTERS.notifications, _int2bytes(flag_bits, 3))
|
||||
return result is not None
|
||||
flag_bits = sum(int(b) for b in flag_bits)
|
||||
assert flag_bits & 0x00FFFFFF == flag_bits
|
||||
result = write_register(device, REGISTERS.notifications,
|
||||
_int2bytes(flag_bits, 3))
|
||||
return result is not None
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -23,30 +23,42 @@ from __future__ import absolute_import, division, print_function, unicode_litera
|
|||
|
||||
import gettext as _gettext
|
||||
|
||||
|
||||
try:
|
||||
unicode
|
||||
_ = lambda x: _gettext.gettext(x).decode('UTF-8')
|
||||
ngettext = lambda *x: _gettext.ngettext(*x).decode('UTF-8')
|
||||
unicode
|
||||
_ = lambda x: _gettext.gettext(x).decode('UTF-8')
|
||||
ngettext = lambda *x: _gettext.ngettext(*x).decode('UTF-8')
|
||||
except:
|
||||
_ = _gettext.gettext
|
||||
ngettext = _gettext.ngettext
|
||||
|
||||
_ = _gettext.gettext
|
||||
ngettext = _gettext.ngettext
|
||||
|
||||
# A few common strings, not always accessible as such in the code.
|
||||
|
||||
_DUMMY = (
|
||||
# approximative battery levels
|
||||
_("empty"), _("critical"), _("low"), _("good"), _("full"),
|
||||
# approximative battery levels
|
||||
_("empty"),
|
||||
_("critical"),
|
||||
_("low"),
|
||||
_("good"),
|
||||
_("full"),
|
||||
|
||||
# battery charging statuses
|
||||
_("discharging"), _("recharging"), _("almost full"), _("charged"),
|
||||
_("slow recharge"), _("invalid battery"), _("thermal error"),
|
||||
# battery charging statuses
|
||||
_("discharging"),
|
||||
_("recharging"),
|
||||
_("almost full"),
|
||||
_("charged"),
|
||||
_("slow recharge"),
|
||||
_("invalid battery"),
|
||||
_("thermal error"),
|
||||
|
||||
# pairing errors
|
||||
_("device timeout"), _("device not supported"), _("too many devices"), _("sequence timeout"),
|
||||
# pairing errors
|
||||
_("device timeout"),
|
||||
_("device not supported"),
|
||||
_("too many devices"),
|
||||
_("sequence timeout"),
|
||||
|
||||
# firmware kinds
|
||||
_("Firmware"), _("Bootloader"), _("Hardware"), _("Other"),
|
||||
|
||||
)
|
||||
# firmware kinds
|
||||
_("Firmware"),
|
||||
_("Bootloader"),
|
||||
_("Hardware"),
|
||||
_("Other"),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -24,92 +24,96 @@ import threading as _threading
|
|||
|
||||
# for both Python 2 and 3
|
||||
try:
|
||||
from Queue import Queue as _Queue
|
||||
from Queue import Queue as _Queue
|
||||
except ImportError:
|
||||
from queue import Queue as _Queue
|
||||
from queue import Queue as _Queue
|
||||
|
||||
from logging import getLogger, DEBUG as _DEBUG, INFO as _INFO
|
||||
_log = getLogger(__name__)
|
||||
del getLogger
|
||||
|
||||
|
||||
from . import base as _base
|
||||
|
||||
#
|
||||
#
|
||||
#
|
||||
|
||||
|
||||
class _ThreadedHandle(object):
|
||||
"""A thread-local wrapper with different open handles for each thread.
|
||||
"""A thread-local wrapper with different open handles for each thread.
|
||||
|
||||
Closing a ThreadedHandle will close all handles.
|
||||
"""
|
||||
|
||||
__slots__ = ('path', '_local', '_handles', '_listener')
|
||||
__slots__ = ('path', '_local', '_handles', '_listener')
|
||||
|
||||
def __init__(self, listener, path, handle):
|
||||
assert listener is not None
|
||||
assert path is not None
|
||||
assert handle is not None
|
||||
assert isinstance(handle, int)
|
||||
def __init__(self, listener, path, handle):
|
||||
assert listener is not None
|
||||
assert path is not None
|
||||
assert handle is not None
|
||||
assert isinstance(handle, int)
|
||||
|
||||
self._listener = listener
|
||||
self.path = path
|
||||
self._local = _threading.local()
|
||||
# take over the current handle for the thread doing the replacement
|
||||
self._local.handle = handle
|
||||
self._handles = [handle]
|
||||
self._listener = listener
|
||||
self.path = path
|
||||
self._local = _threading.local()
|
||||
# take over the current handle for the thread doing the replacement
|
||||
self._local.handle = handle
|
||||
self._handles = [handle]
|
||||
|
||||
def _open(self):
|
||||
handle = _base.open_path(self.path)
|
||||
if handle is None:
|
||||
_log.error("%r failed to open new handle", self)
|
||||
else:
|
||||
# if _log.isEnabledFor(_DEBUG):
|
||||
# _log.debug("%r opened new handle %d", self, handle)
|
||||
self._local.handle = handle
|
||||
self._handles.append(handle)
|
||||
return handle
|
||||
def _open(self):
|
||||
handle = _base.open_path(self.path)
|
||||
if handle is None:
|
||||
_log.error("%r failed to open new handle", self)
|
||||
else:
|
||||
# if _log.isEnabledFor(_DEBUG):
|
||||
# _log.debug("%r opened new handle %d", self, handle)
|
||||
self._local.handle = handle
|
||||
self._handles.append(handle)
|
||||
return handle
|
||||
|
||||
def close(self):
|
||||
if self._local:
|
||||
self._local = None
|
||||
handles, self._handles = self._handles, []
|
||||
if _log.isEnabledFor(_DEBUG):
|
||||
_log.debug("%r closing %s", self, handles)
|
||||
for h in handles:
|
||||
_base.close(h)
|
||||
def close(self):
|
||||
if self._local:
|
||||
self._local = None
|
||||
handles, self._handles = self._handles, []
|
||||
if _log.isEnabledFor(_DEBUG):
|
||||
_log.debug("%r closing %s", self, handles)
|
||||
for h in handles:
|
||||
_base.close(h)
|
||||
|
||||
@property
|
||||
def notifications_hook(self):
|
||||
if self._listener:
|
||||
assert isinstance(self._listener, _threading.Thread)
|
||||
if _threading.current_thread() == self._listener:
|
||||
return self._listener._notifications_hook
|
||||
@property
|
||||
def notifications_hook(self):
|
||||
if self._listener:
|
||||
assert isinstance(self._listener, _threading.Thread)
|
||||
if _threading.current_thread() == self._listener:
|
||||
return self._listener._notifications_hook
|
||||
|
||||
def __del__(self):
|
||||
self._listener = None
|
||||
self.close()
|
||||
def __del__(self):
|
||||
self._listener = None
|
||||
self.close()
|
||||
|
||||
def __index__(self):
|
||||
if self._local:
|
||||
try:
|
||||
return self._local.handle
|
||||
except:
|
||||
return self._open()
|
||||
__int__ = __index__
|
||||
def __index__(self):
|
||||
if self._local:
|
||||
try:
|
||||
return self._local.handle
|
||||
except:
|
||||
return self._open()
|
||||
|
||||
def __str__(self):
|
||||
if self._local:
|
||||
return str(int(self))
|
||||
__unicode__ = __str__
|
||||
__int__ = __index__
|
||||
|
||||
def __repr__(self):
|
||||
return '<_ThreadedHandle(%s)>' % self.path
|
||||
def __str__(self):
|
||||
if self._local:
|
||||
return str(int(self))
|
||||
|
||||
__unicode__ = __str__
|
||||
|
||||
def __repr__(self):
|
||||
return '<_ThreadedHandle(%s)>' % self.path
|
||||
|
||||
def __bool__(self):
|
||||
return bool(self._local)
|
||||
|
||||
__nonzero__ = __bool__
|
||||
|
||||
def __bool__(self):
|
||||
return bool(self._local)
|
||||
__nonzero__ = __bool__
|
||||
|
||||
#
|
||||
#
|
||||
|
|
@ -129,102 +133,105 @@ _EVENT_READ_TIMEOUT = 0.4 # in seconds
|
|||
|
||||
|
||||
class EventsListener(_threading.Thread):
|
||||
"""Listener thread for notifications from the Unifying Receiver.
|
||||
"""Listener thread for notifications from the Unifying Receiver.
|
||||
|
||||
Incoming packets will be passed to the callback function in sequence.
|
||||
"""
|
||||
def __init__(self, receiver, notifications_callback):
|
||||
super(EventsListener, self).__init__(name=self.__class__.__name__ + ':' + receiver.path.split('/')[2])
|
||||
def __init__(self, receiver, notifications_callback):
|
||||
super(EventsListener, self).__init__(name=self.__class__.__name__ +
|
||||
':' + receiver.path.split('/')[2])
|
||||
|
||||
self.daemon = True
|
||||
self._active = False
|
||||
self.daemon = True
|
||||
self._active = False
|
||||
|
||||
self.receiver = receiver
|
||||
self._queued_notifications = _Queue(16)
|
||||
self._notifications_callback = notifications_callback
|
||||
self.receiver = receiver
|
||||
self._queued_notifications = _Queue(16)
|
||||
self._notifications_callback = notifications_callback
|
||||
|
||||
# self.tick_period = 0
|
||||
# self.tick_period = 0
|
||||
|
||||
def run(self):
|
||||
self._active = True
|
||||
def run(self):
|
||||
self._active = True
|
||||
|
||||
# replace the handle with a threaded one
|
||||
self.receiver.handle = _ThreadedHandle(self, self.receiver.path, self.receiver.handle)
|
||||
# get the right low-level handle for this thread
|
||||
ihandle = int(self.receiver.handle)
|
||||
if _log.isEnabledFor(_INFO):
|
||||
_log.info("started with %s (%d)", self.receiver, ihandle)
|
||||
# replace the handle with a threaded one
|
||||
self.receiver.handle = _ThreadedHandle(self, self.receiver.path,
|
||||
self.receiver.handle)
|
||||
# get the right low-level handle for this thread
|
||||
ihandle = int(self.receiver.handle)
|
||||
if _log.isEnabledFor(_INFO):
|
||||
_log.info("started with %s (%d)", self.receiver, ihandle)
|
||||
|
||||
self.has_started()
|
||||
self.has_started()
|
||||
|
||||
# last_tick = 0
|
||||
# the first idle read -- delay it a bit, and make sure to stagger
|
||||
# idle reads for multiple receivers
|
||||
# idle_reads = _IDLE_READS + (ihandle % 5) * 2
|
||||
# last_tick = 0
|
||||
# the first idle read -- delay it a bit, and make sure to stagger
|
||||
# idle reads for multiple receivers
|
||||
# idle_reads = _IDLE_READS + (ihandle % 5) * 2
|
||||
|
||||
while self._active:
|
||||
if self._queued_notifications.empty():
|
||||
try:
|
||||
# _log.debug("read next notification")
|
||||
n = _base.read(ihandle, _EVENT_READ_TIMEOUT)
|
||||
except _base.NoReceiver:
|
||||
_log.warning("receiver disconnected")
|
||||
self.receiver.close()
|
||||
break
|
||||
while self._active:
|
||||
if self._queued_notifications.empty():
|
||||
try:
|
||||
# _log.debug("read next notification")
|
||||
n = _base.read(ihandle, _EVENT_READ_TIMEOUT)
|
||||
except _base.NoReceiver:
|
||||
_log.warning("receiver disconnected")
|
||||
self.receiver.close()
|
||||
break
|
||||
|
||||
if n:
|
||||
n = _base.make_notification(*n)
|
||||
else:
|
||||
# deliver any queued notifications
|
||||
n = self._queued_notifications.get()
|
||||
if n:
|
||||
n = _base.make_notification(*n)
|
||||
else:
|
||||
# deliver any queued notifications
|
||||
n = self._queued_notifications.get()
|
||||
|
||||
if n:
|
||||
# if _log.isEnabledFor(_DEBUG):
|
||||
# _log.debug("%s: processing %s", self.receiver, n)
|
||||
try:
|
||||
self._notifications_callback(n)
|
||||
except:
|
||||
_log.exception("processing %s", n)
|
||||
if n:
|
||||
# if _log.isEnabledFor(_DEBUG):
|
||||
# _log.debug("%s: processing %s", self.receiver, n)
|
||||
try:
|
||||
self._notifications_callback(n)
|
||||
except:
|
||||
_log.exception("processing %s", n)
|
||||
|
||||
# elif self.tick_period:
|
||||
# idle_reads -= 1
|
||||
# if idle_reads <= 0:
|
||||
# idle_reads = _IDLE_READS
|
||||
# now = _timestamp()
|
||||
# if now - last_tick >= self.tick_period:
|
||||
# last_tick = now
|
||||
# self.tick(now)
|
||||
# elif self.tick_period:
|
||||
# idle_reads -= 1
|
||||
# if idle_reads <= 0:
|
||||
# idle_reads = _IDLE_READS
|
||||
# now = _timestamp()
|
||||
# if now - last_tick >= self.tick_period:
|
||||
# last_tick = now
|
||||
# self.tick(now)
|
||||
|
||||
del self._queued_notifications
|
||||
self.has_stopped()
|
||||
del self._queued_notifications
|
||||
self.has_stopped()
|
||||
|
||||
def stop(self):
|
||||
"""Tells the listener to stop as soon as possible."""
|
||||
self._active = False
|
||||
def stop(self):
|
||||
"""Tells the listener to stop as soon as possible."""
|
||||
self._active = False
|
||||
|
||||
def has_started(self):
|
||||
"""Called right after the thread has started, and before it starts
|
||||
def has_started(self):
|
||||
"""Called right after the thread has started, and before it starts
|
||||
reading notification packets."""
|
||||
pass
|
||||
pass
|
||||
|
||||
def has_stopped(self):
|
||||
"""Called right before the thread stops."""
|
||||
pass
|
||||
def has_stopped(self):
|
||||
"""Called right before the thread stops."""
|
||||
pass
|
||||
|
||||
# def tick(self, timestamp):
|
||||
# """Called about every tick_period seconds."""
|
||||
# pass
|
||||
# def tick(self, timestamp):
|
||||
# """Called about every tick_period seconds."""
|
||||
# pass
|
||||
|
||||
def _notifications_hook(self, n):
|
||||
# Only consider unhandled notifications that were sent from this thread,
|
||||
# i.e. triggered by a callback handling a previous notification.
|
||||
assert _threading.current_thread() == self
|
||||
if self._active: # and _threading.current_thread() == self:
|
||||
# if _log.isEnabledFor(_DEBUG):
|
||||
# _log.debug("queueing unhandled %s", n)
|
||||
if not self._queued_notifications.full():
|
||||
self._queued_notifications.put(n)
|
||||
def _notifications_hook(self, n):
|
||||
# Only consider unhandled notifications that were sent from this thread,
|
||||
# i.e. triggered by a callback handling a previous notification.
|
||||
assert _threading.current_thread() == self
|
||||
if self._active: # and _threading.current_thread() == self:
|
||||
# if _log.isEnabledFor(_DEBUG):
|
||||
# _log.debug("queueing unhandled %s", n)
|
||||
if not self._queued_notifications.full():
|
||||
self._queued_notifications.put(n)
|
||||
|
||||
def __bool__(self):
|
||||
return bool(self._active and self.receiver)
|
||||
__nonzero__ = __bool__
|
||||
def __bool__(self):
|
||||
return bool(self._active and self.receiver)
|
||||
|
||||
__nonzero__ = __bool__
|
||||
|
|
|
|||
|
|
@ -26,7 +26,6 @@ from logging import getLogger, DEBUG as _DEBUG, INFO as _INFO
|
|||
_log = getLogger(__name__)
|
||||
del getLogger
|
||||
|
||||
|
||||
from .i18n import _
|
||||
from .common import strhex as _strhex, unpack as _unpack
|
||||
from . import hidpp10 as _hidpp10
|
||||
|
|
@ -41,302 +40,329 @@ _F = _hidpp20.FEATURE
|
|||
#
|
||||
#
|
||||
|
||||
|
||||
def process(device, notification):
|
||||
assert device
|
||||
assert notification
|
||||
assert device
|
||||
assert notification
|
||||
|
||||
assert hasattr(device, 'status')
|
||||
status = device.status
|
||||
assert status is not None
|
||||
assert hasattr(device, 'status')
|
||||
status = device.status
|
||||
assert status is not None
|
||||
|
||||
if device.kind is None:
|
||||
return _process_receiver_notification(device, status, notification)
|
||||
if device.kind is None:
|
||||
return _process_receiver_notification(device, status, notification)
|
||||
|
||||
return _process_device_notification(device, status, notification)
|
||||
|
||||
return _process_device_notification(device, status, notification)
|
||||
|
||||
#
|
||||
#
|
||||
#
|
||||
|
||||
|
||||
def _process_receiver_notification(receiver, status, n):
|
||||
# supposedly only 0x4x notifications arrive for the receiver
|
||||
assert n.sub_id & 0x40 == 0x40
|
||||
# supposedly only 0x4x notifications arrive for the receiver
|
||||
assert n.sub_id & 0x40 == 0x40
|
||||
|
||||
# pairing lock notification
|
||||
if n.sub_id == 0x4A:
|
||||
status.lock_open = bool(n.address & 0x01)
|
||||
reason = (_("pairing lock is open") if status.lock_open else _("pairing lock is closed"))
|
||||
if _log.isEnabledFor(_INFO):
|
||||
_log.info("%s: %s", receiver, reason)
|
||||
# pairing lock notification
|
||||
if n.sub_id == 0x4A:
|
||||
status.lock_open = bool(n.address & 0x01)
|
||||
reason = (_("pairing lock is open")
|
||||
if status.lock_open else _("pairing lock is closed"))
|
||||
if _log.isEnabledFor(_INFO):
|
||||
_log.info("%s: %s", receiver, reason)
|
||||
|
||||
status[_K.ERROR] = None
|
||||
if status.lock_open:
|
||||
status.new_device = None
|
||||
status[_K.ERROR] = None
|
||||
if status.lock_open:
|
||||
status.new_device = None
|
||||
|
||||
pair_error = ord(n.data[:1])
|
||||
if pair_error:
|
||||
status[_K.ERROR] = error_string = _hidpp10.PAIRING_ERRORS[pair_error]
|
||||
status.new_device = None
|
||||
_log.warn("pairing error %d: %s", pair_error, error_string)
|
||||
pair_error = ord(n.data[:1])
|
||||
if pair_error:
|
||||
status[
|
||||
_K.ERROR] = error_string = _hidpp10.PAIRING_ERRORS[pair_error]
|
||||
status.new_device = None
|
||||
_log.warn("pairing error %d: %s", pair_error, error_string)
|
||||
|
||||
status.changed(reason=reason)
|
||||
return True
|
||||
status.changed(reason=reason)
|
||||
return True
|
||||
|
||||
_log.warn("%s: unhandled notification %s", receiver, n)
|
||||
|
||||
_log.warn("%s: unhandled notification %s", receiver, n)
|
||||
|
||||
#
|
||||
#
|
||||
#
|
||||
|
||||
|
||||
def _process_device_notification(device, status, n):
|
||||
# incoming packets with SubId >= 0x80 are supposedly replies from
|
||||
# HID++ 1.0 requests, should never get here
|
||||
assert n.sub_id & 0x80 == 0
|
||||
# incoming packets with SubId >= 0x80 are supposedly replies from
|
||||
# HID++ 1.0 requests, should never get here
|
||||
assert n.sub_id & 0x80 == 0
|
||||
|
||||
# 0x40 to 0x7F appear to be HID++ 1.0 or DJ notifications
|
||||
if n.sub_id >= 0x40:
|
||||
if len(n.data) == _DJ_NOTIFICATION_LENGTH :
|
||||
return _process_dj_notification(device, status, n)
|
||||
else:
|
||||
return _process_hidpp10_notification(device, status, n)
|
||||
# 0x40 to 0x7F appear to be HID++ 1.0 or DJ notifications
|
||||
if n.sub_id >= 0x40:
|
||||
if len(n.data) == _DJ_NOTIFICATION_LENGTH:
|
||||
return _process_dj_notification(device, status, n)
|
||||
else:
|
||||
return _process_hidpp10_notification(device, status, n)
|
||||
|
||||
# At this point, we need to know the device's protocol, otherwise it's
|
||||
# possible to not know how to handle it.
|
||||
assert device.protocol is not None
|
||||
# At this point, we need to know the device's protocol, otherwise it's
|
||||
# possible to not know how to handle it.
|
||||
assert device.protocol is not None
|
||||
|
||||
# some custom battery events for HID++ 1.0 devices
|
||||
if device.protocol < 2.0:
|
||||
return _process_hidpp10_custom_notification(device, status, n)
|
||||
# some custom battery events for HID++ 1.0 devices
|
||||
if device.protocol < 2.0:
|
||||
return _process_hidpp10_custom_notification(device, status, n)
|
||||
|
||||
# assuming 0x00 to 0x3F are feature (HID++ 2.0) notifications
|
||||
assert device.features
|
||||
try:
|
||||
feature = device.features[n.sub_id]
|
||||
except IndexError:
|
||||
_log.warn("%s: notification from invalid feature index %02X: %s", device, n.sub_id, n)
|
||||
return False
|
||||
# assuming 0x00 to 0x3F are feature (HID++ 2.0) notifications
|
||||
assert device.features
|
||||
try:
|
||||
feature = device.features[n.sub_id]
|
||||
except IndexError:
|
||||
_log.warn("%s: notification from invalid feature index %02X: %s",
|
||||
device, n.sub_id, n)
|
||||
return False
|
||||
|
||||
return _process_feature_notification(device, status, n, feature)
|
||||
return _process_feature_notification(device, status, n, feature)
|
||||
|
||||
|
||||
def _process_dj_notification(device, status, n) :
|
||||
if _log.isEnabledFor(_DEBUG):
|
||||
_log.debug("%s (%s) DJ notification %s", device, device.protocol, n)
|
||||
def _process_dj_notification(device, status, n):
|
||||
if _log.isEnabledFor(_DEBUG):
|
||||
_log.debug("%s (%s) DJ notification %s", device, device.protocol, n)
|
||||
|
||||
if n.sub_id == 0x40:
|
||||
# do all DJ paired notifications also show up as HID++ 1.0 notifications?
|
||||
if _log.isEnabledFor(_INFO):
|
||||
_log.info("%s: ignoring DJ unpaired: %s", device, n)
|
||||
return True
|
||||
if n.sub_id == 0x40:
|
||||
# do all DJ paired notifications also show up as HID++ 1.0 notifications?
|
||||
if _log.isEnabledFor(_INFO):
|
||||
_log.info("%s: ignoring DJ unpaired: %s", device, n)
|
||||
return True
|
||||
|
||||
if n.sub_id == 0x41:
|
||||
# do all DJ paired notifications also show up as HID++ 1.0 notifications?
|
||||
if _log.isEnabledFor(_INFO):
|
||||
_log.info("%s: ignoring DJ paired: %s", device, n)
|
||||
return True
|
||||
if n.sub_id == 0x41:
|
||||
# do all DJ paired notifications also show up as HID++ 1.0 notifications?
|
||||
if _log.isEnabledFor(_INFO):
|
||||
_log.info("%s: ignoring DJ paired: %s", device, n)
|
||||
return True
|
||||
|
||||
if n.sub_id == 0x42:
|
||||
if _log.isEnabledFor(_INFO):
|
||||
_log.info("%s: ignoring DJ connection: %s", device, n)
|
||||
return True
|
||||
if n.sub_id == 0x42:
|
||||
if _log.isEnabledFor(_INFO):
|
||||
_log.info("%s: ignoring DJ connection: %s", device, n)
|
||||
return True
|
||||
|
||||
_log.warn("%s: unrecognized DJ %s", device, n)
|
||||
_log.warn("%s: unrecognized DJ %s", device, n)
|
||||
|
||||
|
||||
def _process_hidpp10_custom_notification(device, status, n):
|
||||
if _log.isEnabledFor(_DEBUG):
|
||||
_log.debug("%s (%s) custom notification %s", device, device.protocol, n)
|
||||
if _log.isEnabledFor(_DEBUG):
|
||||
_log.debug("%s (%s) custom notification %s", device, device.protocol,
|
||||
n)
|
||||
|
||||
if n.sub_id in (_R.battery_status, _R.battery_charge):
|
||||
# message layout: 10 ix <register> <xx> <yy> <zz> <00>
|
||||
assert n.data[-1:] == b'\x00'
|
||||
data = chr(n.address).encode() + n.data
|
||||
charge, status_text = _hidpp10.parse_battery_status(n.sub_id, data)
|
||||
status.set_battery_info(charge, status_text, None)
|
||||
return True
|
||||
if n.sub_id in (_R.battery_status, _R.battery_charge):
|
||||
# message layout: 10 ix <register> <xx> <yy> <zz> <00>
|
||||
assert n.data[-1:] == b'\x00'
|
||||
data = chr(n.address).encode() + n.data
|
||||
charge, status_text = _hidpp10.parse_battery_status(n.sub_id, data)
|
||||
status.set_battery_info(charge, status_text, None)
|
||||
return True
|
||||
|
||||
if n.sub_id == _R.keyboard_illumination:
|
||||
# message layout: 10 ix 17("address") <??> <?> <??> <light level 1=off..5=max>
|
||||
# TODO anything we can do with this?
|
||||
if _log.isEnabledFor(_INFO):
|
||||
_log.info("illumination event: %s", n)
|
||||
return True
|
||||
if n.sub_id == _R.keyboard_illumination:
|
||||
# message layout: 10 ix 17("address") <??> <?> <??> <light level 1=off..5=max>
|
||||
# TODO anything we can do with this?
|
||||
if _log.isEnabledFor(_INFO):
|
||||
_log.info("illumination event: %s", n)
|
||||
return True
|
||||
|
||||
_log.warn("%s: unrecognized %s", device, n)
|
||||
_log.warn("%s: unrecognized %s", device, n)
|
||||
|
||||
|
||||
def _process_hidpp10_notification(device, status, n):
|
||||
# unpair notification
|
||||
if n.sub_id == 0x40:
|
||||
if n.address == 0x02:
|
||||
# device un-paired
|
||||
status.clear()
|
||||
device.wpid = None
|
||||
device.status = None
|
||||
if device.number in device.receiver:
|
||||
del device.receiver[device.number]
|
||||
status.changed(active=False, alert=_ALERT.ALL, reason=_("unpaired"))
|
||||
else:
|
||||
_log.warn("%s: disconnection with unknown type %02X: %s", device, n.address, n)
|
||||
return True
|
||||
# unpair notification
|
||||
if n.sub_id == 0x40:
|
||||
if n.address == 0x02:
|
||||
# device un-paired
|
||||
status.clear()
|
||||
device.wpid = None
|
||||
device.status = None
|
||||
if device.number in device.receiver:
|
||||
del device.receiver[device.number]
|
||||
status.changed(active=False,
|
||||
alert=_ALERT.ALL,
|
||||
reason=_("unpaired"))
|
||||
else:
|
||||
_log.warn("%s: disconnection with unknown type %02X: %s", device,
|
||||
n.address, n)
|
||||
return True
|
||||
|
||||
# wireless link notification
|
||||
if n.sub_id == 0x41:
|
||||
protocol_name = ('Bluetooth' if n.address == 0x01
|
||||
else '27 MHz' if n.address == 0x02
|
||||
else 'QUAD or eQUAD' if n.address == 0x03
|
||||
else 'eQUAD step 4 DJ' if n.address == 0x04
|
||||
else 'DFU Lite' if n.address == 0x05
|
||||
else 'eQUAD step 4 Lite' if n.address == 0x06
|
||||
else 'eQUAD step 4 Gaming' if n.address == 0x07
|
||||
else 'eQUAD step 4 for gamepads' if n.address == 0x08
|
||||
else 'eQUAD nano Lite' if n.address == 0x0A
|
||||
else 'Lightspeed 1' if n.address == 0x0C
|
||||
else 'Lightspeed 1_1' if n.address == 0x0D
|
||||
else None)
|
||||
if protocol_name:
|
||||
if _log.isEnabledFor(_DEBUG):
|
||||
wpid = _strhex(n.data[2:3] + n.data[1:2])
|
||||
assert wpid == device.wpid, "%s wpid mismatch, got %s" % (device, wpid)
|
||||
# wireless link notification
|
||||
if n.sub_id == 0x41:
|
||||
protocol_name = (
|
||||
'Bluetooth' if n.address == 0x01 else '27 MHz'
|
||||
if n.address == 0x02 else 'QUAD or eQUAD' if n.address == 0x03 else
|
||||
'eQUAD step 4 DJ' if n.address == 0x04 else 'DFU Lite' if n.
|
||||
address == 0x05 else 'eQUAD step 4 Lite' if n.address ==
|
||||
0x06 else 'eQUAD step 4 Gaming' if n.address ==
|
||||
0x07 else 'eQUAD step 4 for gamepads' if n.address ==
|
||||
0x08 else 'eQUAD nano Lite' if n.address ==
|
||||
0x0A else 'Lightspeed 1' if n.address ==
|
||||
0x0C else 'Lightspeed 1_1' if n.address == 0x0D else None)
|
||||
if protocol_name:
|
||||
if _log.isEnabledFor(_DEBUG):
|
||||
wpid = _strhex(n.data[2:3] + n.data[1:2])
|
||||
assert wpid == device.wpid, "%s wpid mismatch, got %s" % (
|
||||
device, wpid)
|
||||
|
||||
flags = ord(n.data[:1]) & 0xF0
|
||||
link_encrypted = bool(flags & 0x20)
|
||||
link_established = not (flags & 0x40)
|
||||
if _log.isEnabledFor(_DEBUG):
|
||||
sw_present = bool(flags & 0x10)
|
||||
has_payload = bool(flags & 0x80)
|
||||
_log.debug("%s: %s connection notification: software=%s, encrypted=%s, link=%s, payload=%s",
|
||||
device, protocol_name, sw_present, link_encrypted, link_established, has_payload)
|
||||
status[_K.LINK_ENCRYPTED] = link_encrypted
|
||||
status.changed(active=link_established)
|
||||
else:
|
||||
_log.warn("%s: connection notification with unknown protocol %02X: %s", device.number, n.address, n)
|
||||
flags = ord(n.data[:1]) & 0xF0
|
||||
link_encrypted = bool(flags & 0x20)
|
||||
link_established = not (flags & 0x40)
|
||||
if _log.isEnabledFor(_DEBUG):
|
||||
sw_present = bool(flags & 0x10)
|
||||
has_payload = bool(flags & 0x80)
|
||||
_log.debug(
|
||||
"%s: %s connection notification: software=%s, encrypted=%s, link=%s, payload=%s",
|
||||
device, protocol_name, sw_present, link_encrypted,
|
||||
link_established, has_payload)
|
||||
status[_K.LINK_ENCRYPTED] = link_encrypted
|
||||
status.changed(active=link_established)
|
||||
else:
|
||||
_log.warn(
|
||||
"%s: connection notification with unknown protocol %02X: %s",
|
||||
device.number, n.address, n)
|
||||
|
||||
return True
|
||||
return True
|
||||
|
||||
if n.sub_id == 0x49:
|
||||
# raw input event? just ignore it
|
||||
# if n.address == 0x01, no idea what it is, but they keep on coming
|
||||
# if n.address == 0x03, appears to be an actual input event,
|
||||
# because they only come when input happents
|
||||
return True
|
||||
if n.sub_id == 0x49:
|
||||
# raw input event? just ignore it
|
||||
# if n.address == 0x01, no idea what it is, but they keep on coming
|
||||
# if n.address == 0x03, appears to be an actual input event,
|
||||
# because they only come when input happents
|
||||
return True
|
||||
|
||||
# power notification
|
||||
if n.sub_id == 0x4B:
|
||||
if n.address == 0x01:
|
||||
if _log.isEnabledFor(_DEBUG):
|
||||
_log.debug("%s: device powered on", device)
|
||||
reason = status.to_string() or _("powered on")
|
||||
status.changed(active=True, alert=_ALERT.NOTIFICATION, reason=reason)
|
||||
else:
|
||||
_log.warn("%s: unknown %s", device, n)
|
||||
return True
|
||||
# power notification
|
||||
if n.sub_id == 0x4B:
|
||||
if n.address == 0x01:
|
||||
if _log.isEnabledFor(_DEBUG):
|
||||
_log.debug("%s: device powered on", device)
|
||||
reason = status.to_string() or _("powered on")
|
||||
status.changed(active=True,
|
||||
alert=_ALERT.NOTIFICATION,
|
||||
reason=reason)
|
||||
else:
|
||||
_log.warn("%s: unknown %s", device, n)
|
||||
return True
|
||||
|
||||
_log.warn("%s: unrecognized %s", device, n)
|
||||
_log.warn("%s: unrecognized %s", device, n)
|
||||
|
||||
|
||||
def _process_feature_notification(device, status, n, feature):
|
||||
if feature == _F.BATTERY_STATUS:
|
||||
if n.address == 0x00:
|
||||
discharge_level = ord(n.data[:1])
|
||||
discharge_level = None if discharge_level == 0 else discharge_level
|
||||
discharge_next_level = ord(n.data[1:2])
|
||||
battery_status = ord(n.data[2:3])
|
||||
status.set_battery_info(discharge_level, _hidpp20.BATTERY_STATUS[battery_status], discharge_next_level)
|
||||
else:
|
||||
_log.warn("%s: unknown BATTERY %s", device, n)
|
||||
return True
|
||||
if feature == _F.BATTERY_STATUS:
|
||||
if n.address == 0x00:
|
||||
discharge_level = ord(n.data[:1])
|
||||
discharge_level = None if discharge_level == 0 else discharge_level
|
||||
discharge_next_level = ord(n.data[1:2])
|
||||
battery_status = ord(n.data[2:3])
|
||||
status.set_battery_info(discharge_level,
|
||||
_hidpp20.BATTERY_STATUS[battery_status],
|
||||
discharge_next_level)
|
||||
else:
|
||||
_log.warn("%s: unknown BATTERY %s", device, n)
|
||||
return True
|
||||
|
||||
if feature == _F.BATTERY_VOLTAGE:
|
||||
if n.address == 0x00:
|
||||
level, status, voltage, _ignore, _ignore =_hidpp20.decipher_voltage(n.data)
|
||||
status.set_battery_info(level, status, None, voltage)
|
||||
else:
|
||||
_log.warn("%s: unknown VOLTAGE %s", device, n)
|
||||
return True
|
||||
if feature == _F.BATTERY_VOLTAGE:
|
||||
if n.address == 0x00:
|
||||
level, status, voltage, _ignore, _ignore = _hidpp20.decipher_voltage(
|
||||
n.data)
|
||||
status.set_battery_info(level, status, None, voltage)
|
||||
else:
|
||||
_log.warn("%s: unknown VOLTAGE %s", device, n)
|
||||
return True
|
||||
|
||||
# TODO: what are REPROG_CONTROLS_V{2,3}?
|
||||
if feature == _F.REPROG_CONTROLS:
|
||||
if n.address == 0x00:
|
||||
if _log.isEnabledFor(_INFO):
|
||||
_log.info("%s: reprogrammable key: %s", device, n)
|
||||
else:
|
||||
_log.warn("%s: unknown REPROGRAMMABLE KEYS %s", device, n)
|
||||
return True
|
||||
# TODO: what are REPROG_CONTROLS_V{2,3}?
|
||||
if feature == _F.REPROG_CONTROLS:
|
||||
if n.address == 0x00:
|
||||
if _log.isEnabledFor(_INFO):
|
||||
_log.info("%s: reprogrammable key: %s", device, n)
|
||||
else:
|
||||
_log.warn("%s: unknown REPROGRAMMABLE KEYS %s", device, n)
|
||||
return True
|
||||
|
||||
if feature == _F.WIRELESS_DEVICE_STATUS:
|
||||
if n.address == 0x00:
|
||||
if _log.isEnabledFor(_DEBUG):
|
||||
_log.debug("wireless status: %s", n)
|
||||
if n.data[0:3] == b'\x01\x01\x01':
|
||||
status.changed(active=True, alert=_ALERT.NOTIFICATION, reason='powered on')
|
||||
else:
|
||||
_log.warn("%s: unknown WIRELESS %s", device, n)
|
||||
else:
|
||||
_log.warn("%s: unknown WIRELESS %s", device, n)
|
||||
return True
|
||||
if feature == _F.WIRELESS_DEVICE_STATUS:
|
||||
if n.address == 0x00:
|
||||
if _log.isEnabledFor(_DEBUG):
|
||||
_log.debug("wireless status: %s", n)
|
||||
if n.data[0:3] == b'\x01\x01\x01':
|
||||
status.changed(active=True,
|
||||
alert=_ALERT.NOTIFICATION,
|
||||
reason='powered on')
|
||||
else:
|
||||
_log.warn("%s: unknown WIRELESS %s", device, n)
|
||||
else:
|
||||
_log.warn("%s: unknown WIRELESS %s", device, n)
|
||||
return True
|
||||
|
||||
if feature == _F.SOLAR_DASHBOARD:
|
||||
if n.data[5:9] == b'GOOD':
|
||||
charge, lux, adc = _unpack('!BHH', n.data[:5])
|
||||
# guesstimate the battery voltage, emphasis on 'guess'
|
||||
# status_text = '%1.2fV' % (adc * 2.67793237653 / 0x0672)
|
||||
status_text = _hidpp20.BATTERY_STATUS.discharging
|
||||
if feature == _F.SOLAR_DASHBOARD:
|
||||
if n.data[5:9] == b'GOOD':
|
||||
charge, lux, adc = _unpack('!BHH', n.data[:5])
|
||||
# guesstimate the battery voltage, emphasis on 'guess'
|
||||
# status_text = '%1.2fV' % (adc * 2.67793237653 / 0x0672)
|
||||
status_text = _hidpp20.BATTERY_STATUS.discharging
|
||||
|
||||
if n.address == 0x00:
|
||||
status[_K.LIGHT_LEVEL] = None
|
||||
status.set_battery_info(charge, status_text, None)
|
||||
elif n.address == 0x10:
|
||||
status[_K.LIGHT_LEVEL] = lux
|
||||
if lux > 200:
|
||||
status_text = _hidpp20.BATTERY_STATUS.recharging
|
||||
status.set_battery_info(charge, status_text, None)
|
||||
elif n.address == 0x20:
|
||||
if _log.isEnabledFor(_DEBUG):
|
||||
_log.debug("%s: Light Check button pressed", device)
|
||||
status.changed(alert=_ALERT.SHOW_WINDOW)
|
||||
# first cancel any reporting
|
||||
# device.feature_request(_F.SOLAR_DASHBOARD)
|
||||
# trigger a new report chain
|
||||
reports_count = 15
|
||||
reports_period = 2 # seconds
|
||||
device.feature_request(_F.SOLAR_DASHBOARD, 0x00, reports_count, reports_period)
|
||||
else:
|
||||
_log.warn("%s: unknown SOLAR CHARGE %s", device, n)
|
||||
else:
|
||||
_log.warn("%s: SOLAR CHARGE not GOOD? %s", device, n)
|
||||
return True
|
||||
if n.address == 0x00:
|
||||
status[_K.LIGHT_LEVEL] = None
|
||||
status.set_battery_info(charge, status_text, None)
|
||||
elif n.address == 0x10:
|
||||
status[_K.LIGHT_LEVEL] = lux
|
||||
if lux > 200:
|
||||
status_text = _hidpp20.BATTERY_STATUS.recharging
|
||||
status.set_battery_info(charge, status_text, None)
|
||||
elif n.address == 0x20:
|
||||
if _log.isEnabledFor(_DEBUG):
|
||||
_log.debug("%s: Light Check button pressed", device)
|
||||
status.changed(alert=_ALERT.SHOW_WINDOW)
|
||||
# first cancel any reporting
|
||||
# device.feature_request(_F.SOLAR_DASHBOARD)
|
||||
# trigger a new report chain
|
||||
reports_count = 15
|
||||
reports_period = 2 # seconds
|
||||
device.feature_request(_F.SOLAR_DASHBOARD, 0x00, reports_count,
|
||||
reports_period)
|
||||
else:
|
||||
_log.warn("%s: unknown SOLAR CHARGE %s", device, n)
|
||||
else:
|
||||
_log.warn("%s: SOLAR CHARGE not GOOD? %s", device, n)
|
||||
return True
|
||||
|
||||
if feature == _F.TOUCHMOUSE_RAW_POINTS:
|
||||
if n.address == 0x00:
|
||||
if _log.isEnabledFor(_INFO):
|
||||
_log.info("%s: TOUCH MOUSE points %s", device, n)
|
||||
elif n.address == 0x10:
|
||||
touch = ord(n.data[:1])
|
||||
button_down = bool(touch & 0x02)
|
||||
mouse_lifted = bool(touch & 0x01)
|
||||
if _log.isEnabledFor(_INFO):
|
||||
_log.info("%s: TOUCH MOUSE status: button_down=%s mouse_lifted=%s", device, button_down, mouse_lifted)
|
||||
else:
|
||||
_log.warn("%s: unknown TOUCH MOUSE %s", device, n)
|
||||
return True
|
||||
if feature == _F.TOUCHMOUSE_RAW_POINTS:
|
||||
if n.address == 0x00:
|
||||
if _log.isEnabledFor(_INFO):
|
||||
_log.info("%s: TOUCH MOUSE points %s", device, n)
|
||||
elif n.address == 0x10:
|
||||
touch = ord(n.data[:1])
|
||||
button_down = bool(touch & 0x02)
|
||||
mouse_lifted = bool(touch & 0x01)
|
||||
if _log.isEnabledFor(_INFO):
|
||||
_log.info(
|
||||
"%s: TOUCH MOUSE status: button_down=%s mouse_lifted=%s",
|
||||
device, button_down, mouse_lifted)
|
||||
else:
|
||||
_log.warn("%s: unknown TOUCH MOUSE %s", device, n)
|
||||
return True
|
||||
|
||||
if feature == _F.HIRES_WHEEL:
|
||||
if (n.address == 0x00):
|
||||
if _log.isEnabledFor(_INFO):
|
||||
flags, delta_v = _unpack('>bh', n.data[:3])
|
||||
high_res = (flags & 0x10) != 0
|
||||
periods = flags & 0x0f
|
||||
_log.info("%s: WHEEL: res: %d periods: %d delta V:%-3d", device, high_res, periods, delta_v)
|
||||
return True
|
||||
elif (n.address == 0x10):
|
||||
if _log.isEnabledFor(_INFO):
|
||||
flags = ord(n.data[:1])
|
||||
ratchet = flags & 0x01
|
||||
_log.info("%s: WHEEL: ratchet: %d", device, ratchet)
|
||||
return True
|
||||
else:
|
||||
_log.warn("%s: unknown WHEEL %s", device, n)
|
||||
return True
|
||||
if feature == _F.HIRES_WHEEL:
|
||||
if (n.address == 0x00):
|
||||
if _log.isEnabledFor(_INFO):
|
||||
flags, delta_v = _unpack('>bh', n.data[:3])
|
||||
high_res = (flags & 0x10) != 0
|
||||
periods = flags & 0x0f
|
||||
_log.info("%s: WHEEL: res: %d periods: %d delta V:%-3d",
|
||||
device, high_res, periods, delta_v)
|
||||
return True
|
||||
elif (n.address == 0x10):
|
||||
if _log.isEnabledFor(_INFO):
|
||||
flags = ord(n.data[:1])
|
||||
ratchet = flags & 0x01
|
||||
_log.info("%s: WHEEL: ratchet: %d", device, ratchet)
|
||||
return True
|
||||
else:
|
||||
_log.warn("%s: unknown WHEEL %s", device, n)
|
||||
return True
|
||||
|
||||
_log.warn("%s: unrecognized %s for feature %s (index %02X)", device, n, feature, n.sub_id)
|
||||
_log.warn("%s: unrecognized %s for feature %s (index %02X)", device, n,
|
||||
feature, n.sub_id)
|
||||
|
|
|
|||
|
|
@ -25,7 +25,6 @@ from logging import getLogger, INFO as _INFO
|
|||
_log = getLogger(__name__)
|
||||
del getLogger
|
||||
|
||||
|
||||
from .i18n import _
|
||||
from . import base as _base
|
||||
from . import hidpp10 as _hidpp10
|
||||
|
|
@ -41,516 +40,552 @@ _R = _hidpp10.REGISTERS
|
|||
#
|
||||
#
|
||||
|
||||
|
||||
class PairedDevice(object):
|
||||
def __init__(self, receiver, number, link_notification=None):
|
||||
assert receiver
|
||||
self.receiver = receiver
|
||||
def __init__(self, receiver, number, link_notification=None):
|
||||
assert receiver
|
||||
self.receiver = receiver
|
||||
|
||||
assert number > 0 and number <= receiver.max_devices
|
||||
# Device number, 1..6 for unifying devices, 1 otherwise.
|
||||
self.number = number
|
||||
# 'device active' flag; requires manual management.
|
||||
self.online = None
|
||||
assert number > 0 and number <= receiver.max_devices
|
||||
# Device number, 1..6 for unifying devices, 1 otherwise.
|
||||
self.number = number
|
||||
# 'device active' flag; requires manual management.
|
||||
self.online = None
|
||||
|
||||
# the Wireless PID is unique per device model
|
||||
self.wpid = None
|
||||
self.descriptor = None
|
||||
# the Wireless PID is unique per device model
|
||||
self.wpid = None
|
||||
self.descriptor = None
|
||||
|
||||
# mouse, keyboard, etc (see _hidpp10.DEVICE_KIND)
|
||||
self._kind = None
|
||||
# Unifying peripherals report a codename.
|
||||
self._codename = None
|
||||
# the full name of the model
|
||||
self._name = None
|
||||
# HID++ protocol version, 1.0 or 2.0
|
||||
self._protocol = None
|
||||
# serial number (an 8-char hex string)
|
||||
self._serial = None
|
||||
# mouse, keyboard, etc (see _hidpp10.DEVICE_KIND)
|
||||
self._kind = None
|
||||
# Unifying peripherals report a codename.
|
||||
self._codename = None
|
||||
# the full name of the model
|
||||
self._name = None
|
||||
# HID++ protocol version, 1.0 or 2.0
|
||||
self._protocol = None
|
||||
# serial number (an 8-char hex string)
|
||||
self._serial = None
|
||||
|
||||
self._firmware = None
|
||||
self._keys = None
|
||||
self._registers = None
|
||||
self._settings = None
|
||||
self._feature_settings_checked = False
|
||||
self._firmware = None
|
||||
self._keys = None
|
||||
self._registers = None
|
||||
self._settings = None
|
||||
self._feature_settings_checked = False
|
||||
|
||||
# Misc stuff that's irrelevant to any functionality, but may be
|
||||
# displayed in the UI and caching it here helps.
|
||||
self._polling_rate = None
|
||||
self._power_switch = None
|
||||
# Misc stuff that's irrelevant to any functionality, but may be
|
||||
# displayed in the UI and caching it here helps.
|
||||
self._polling_rate = None
|
||||
self._power_switch = None
|
||||
|
||||
# if _log.isEnabledFor(_DEBUG):
|
||||
# _log.debug("new PairedDevice(%s, %s, %s)", receiver, number, link_notification)
|
||||
# if _log.isEnabledFor(_DEBUG):
|
||||
# _log.debug("new PairedDevice(%s, %s, %s)", receiver, number, link_notification)
|
||||
|
||||
if link_notification is not None:
|
||||
self.online = not bool(ord(link_notification.data[0:1]) & 0x40)
|
||||
self.wpid = _strhex(link_notification.data[2:3] + link_notification.data[1:2])
|
||||
# assert link_notification.address == (0x04 if unifying else 0x03)
|
||||
kind = ord(link_notification.data[0:1]) & 0x0F
|
||||
self._kind = _hidpp10.DEVICE_KIND[kind]
|
||||
else:
|
||||
# force a reading of the wpid
|
||||
pair_info = receiver.read_register(_R.receiver_info, 0x20 + number - 1)
|
||||
if pair_info:
|
||||
# may be either a Unifying receiver, or an Unifying-ready receiver
|
||||
self.wpid = _strhex(pair_info[3:5])
|
||||
kind = ord(pair_info[7:8]) & 0x0F
|
||||
self._kind = _hidpp10.DEVICE_KIND[kind]
|
||||
self._polling_rate = ord(pair_info[2:3])
|
||||
if link_notification is not None:
|
||||
self.online = not bool(ord(link_notification.data[0:1]) & 0x40)
|
||||
self.wpid = _strhex(link_notification.data[2:3] +
|
||||
link_notification.data[1:2])
|
||||
# assert link_notification.address == (0x04 if unifying else 0x03)
|
||||
kind = ord(link_notification.data[0:1]) & 0x0F
|
||||
self._kind = _hidpp10.DEVICE_KIND[kind]
|
||||
else:
|
||||
# force a reading of the wpid
|
||||
pair_info = receiver.read_register(_R.receiver_info,
|
||||
0x20 + number - 1)
|
||||
if pair_info:
|
||||
# may be either a Unifying receiver, or an Unifying-ready receiver
|
||||
self.wpid = _strhex(pair_info[3:5])
|
||||
kind = ord(pair_info[7:8]) & 0x0F
|
||||
self._kind = _hidpp10.DEVICE_KIND[kind]
|
||||
self._polling_rate = ord(pair_info[2:3])
|
||||
|
||||
else:
|
||||
# unifying protocol not supported, must be a Nano receiver
|
||||
device_info = self.receiver.read_register(_R.receiver_info, 0x04)
|
||||
if device_info is None:
|
||||
_log.error("failed to read Nano wpid for device %d of %s", number, receiver)
|
||||
raise _base.NoSuchDevice(number=number, receiver=receiver, error="read Nano wpid")
|
||||
else:
|
||||
# unifying protocol not supported, must be a Nano receiver
|
||||
device_info = self.receiver.read_register(
|
||||
_R.receiver_info, 0x04)
|
||||
if device_info is None:
|
||||
_log.error("failed to read Nano wpid for device %d of %s",
|
||||
number, receiver)
|
||||
raise _base.NoSuchDevice(number=number,
|
||||
receiver=receiver,
|
||||
error="read Nano wpid")
|
||||
|
||||
self.wpid = _strhex(device_info[3:5])
|
||||
self._polling_rate = 0
|
||||
self._power_switch = '(' + _("unknown") + ')'
|
||||
self.wpid = _strhex(device_info[3:5])
|
||||
self._polling_rate = 0
|
||||
self._power_switch = '(' + _("unknown") + ')'
|
||||
|
||||
# the wpid is necessary to properly identify wireless link on/off notifications
|
||||
# also it gets set to None on this object when the device is unpaired
|
||||
assert self.wpid is not None, "failed to read wpid: device %d of %s" % (number, receiver)
|
||||
# the wpid is necessary to properly identify wireless link on/off notifications
|
||||
# also it gets set to None on this object when the device is unpaired
|
||||
assert self.wpid is not None, "failed to read wpid: device %d of %s" % (
|
||||
number, receiver)
|
||||
|
||||
self.descriptor = _DESCRIPTORS.get(self.wpid)
|
||||
if self.descriptor is None:
|
||||
# Last chance to correctly identify the device; many Nano receivers
|
||||
# do not support this call.
|
||||
codename = self.receiver.read_register(_R.receiver_info, 0x40 + self.number - 1)
|
||||
if codename:
|
||||
codename_length = ord(codename[1:2])
|
||||
codename = codename[2:2 + codename_length]
|
||||
self._codename = codename.decode('ascii')
|
||||
self.descriptor = _DESCRIPTORS.get(self._codename)
|
||||
self.descriptor = _DESCRIPTORS.get(self.wpid)
|
||||
if self.descriptor is None:
|
||||
# Last chance to correctly identify the device; many Nano receivers
|
||||
# do not support this call.
|
||||
codename = self.receiver.read_register(_R.receiver_info,
|
||||
0x40 + self.number - 1)
|
||||
if codename:
|
||||
codename_length = ord(codename[1:2])
|
||||
codename = codename[2:2 + codename_length]
|
||||
self._codename = codename.decode('ascii')
|
||||
self.descriptor = _DESCRIPTORS.get(self._codename)
|
||||
|
||||
if self.descriptor:
|
||||
self._name = self.descriptor.name
|
||||
self._protocol = self.descriptor.protocol
|
||||
if self._codename is None:
|
||||
self._codename = self.descriptor.codename
|
||||
if self._kind is None:
|
||||
self._kind = self.descriptor.kind
|
||||
if self.descriptor:
|
||||
self._name = self.descriptor.name
|
||||
self._protocol = self.descriptor.protocol
|
||||
if self._codename is None:
|
||||
self._codename = self.descriptor.codename
|
||||
if self._kind is None:
|
||||
self._kind = self.descriptor.kind
|
||||
|
||||
if self._protocol is not None:
|
||||
self.features = None if self._protocol < 2.0 else _hidpp20.FeaturesArray(self)
|
||||
else:
|
||||
# may be a 2.0 device; if not, it will fix itself later
|
||||
self.features = _hidpp20.FeaturesArray(self)
|
||||
if self._protocol is not None:
|
||||
self.features = None if self._protocol < 2.0 else _hidpp20.FeaturesArray(
|
||||
self)
|
||||
else:
|
||||
# may be a 2.0 device; if not, it will fix itself later
|
||||
self.features = _hidpp20.FeaturesArray(self)
|
||||
|
||||
@property
|
||||
def protocol(self):
|
||||
if self._protocol is None and self.online is not False:
|
||||
self._protocol = _base.ping(self.receiver.handle, self.number)
|
||||
# if the ping failed, the peripheral is (almost) certainly offline
|
||||
self.online = self._protocol is not None
|
||||
@property
|
||||
def protocol(self):
|
||||
if self._protocol is None and self.online is not False:
|
||||
self._protocol = _base.ping(self.receiver.handle, self.number)
|
||||
# if the ping failed, the peripheral is (almost) certainly offline
|
||||
self.online = self._protocol is not None
|
||||
|
||||
# if _log.isEnabledFor(_DEBUG):
|
||||
# _log.debug("device %d protocol %s", self.number, self._protocol)
|
||||
return self._protocol or 0
|
||||
# if _log.isEnabledFor(_DEBUG):
|
||||
# _log.debug("device %d protocol %s", self.number, self._protocol)
|
||||
return self._protocol or 0
|
||||
|
||||
@property
|
||||
def codename(self):
|
||||
if self._codename is None:
|
||||
codename = self.receiver.read_register(_R.receiver_info, 0x40 + self.number - 1)
|
||||
if codename:
|
||||
codename_length = ord(codename[1:2])
|
||||
codename = codename[2:2 + codename_length]
|
||||
self._codename = codename.decode('ascii')
|
||||
# if _log.isEnabledFor(_DEBUG):
|
||||
# _log.debug("device %d codename %s", self.number, self._codename)
|
||||
else:
|
||||
self._codename = '? (%s)' % self.wpid
|
||||
return self._codename
|
||||
@property
|
||||
def codename(self):
|
||||
if self._codename is None:
|
||||
codename = self.receiver.read_register(_R.receiver_info,
|
||||
0x40 + self.number - 1)
|
||||
if codename:
|
||||
codename_length = ord(codename[1:2])
|
||||
codename = codename[2:2 + codename_length]
|
||||
self._codename = codename.decode('ascii')
|
||||
# if _log.isEnabledFor(_DEBUG):
|
||||
# _log.debug("device %d codename %s", self.number, self._codename)
|
||||
else:
|
||||
self._codename = '? (%s)' % self.wpid
|
||||
return self._codename
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
if self._name is None:
|
||||
if self.online and self.protocol >= 2.0:
|
||||
self._name = _hidpp20.get_name(self)
|
||||
return self._name or self.codename or ('Unknown device %s' % self.wpid)
|
||||
@property
|
||||
def name(self):
|
||||
if self._name is None:
|
||||
if self.online and self.protocol >= 2.0:
|
||||
self._name = _hidpp20.get_name(self)
|
||||
return self._name or self.codename or ('Unknown device %s' % self.wpid)
|
||||
|
||||
@property
|
||||
def kind(self):
|
||||
if self._kind is None:
|
||||
pair_info = self.receiver.read_register(_R.receiver_info, 0x20 + self.number - 1)
|
||||
if pair_info:
|
||||
kind = ord(pair_info[7:8]) & 0x0F
|
||||
self._kind = _hidpp10.DEVICE_KIND[kind]
|
||||
self._polling_rate = ord(pair_info[2:3])
|
||||
elif self.online and self.protocol >= 2.0:
|
||||
self._kind = _hidpp20.get_kind(self)
|
||||
return self._kind or '?'
|
||||
@property
|
||||
def kind(self):
|
||||
if self._kind is None:
|
||||
pair_info = self.receiver.read_register(_R.receiver_info,
|
||||
0x20 + self.number - 1)
|
||||
if pair_info:
|
||||
kind = ord(pair_info[7:8]) & 0x0F
|
||||
self._kind = _hidpp10.DEVICE_KIND[kind]
|
||||
self._polling_rate = ord(pair_info[2:3])
|
||||
elif self.online and self.protocol >= 2.0:
|
||||
self._kind = _hidpp20.get_kind(self)
|
||||
return self._kind or '?'
|
||||
|
||||
@property
|
||||
def firmware(self):
|
||||
if self._firmware is None and self.online:
|
||||
if self.protocol >= 2.0:
|
||||
self._firmware = _hidpp20.get_firmware(self)
|
||||
else:
|
||||
self._firmware = _hidpp10.get_firmware(self)
|
||||
return self._firmware or ()
|
||||
@property
|
||||
def firmware(self):
|
||||
if self._firmware is None and self.online:
|
||||
if self.protocol >= 2.0:
|
||||
self._firmware = _hidpp20.get_firmware(self)
|
||||
else:
|
||||
self._firmware = _hidpp10.get_firmware(self)
|
||||
return self._firmware or ()
|
||||
|
||||
@property
|
||||
def serial(self):
|
||||
if self._serial is None:
|
||||
serial = self.receiver.read_register(_R.receiver_info, 0x30 + self.number - 1)
|
||||
if serial:
|
||||
ps = ord(serial[9:10]) & 0x0F
|
||||
self._power_switch = _hidpp10.POWER_SWITCH_LOCATION[ps]
|
||||
else:
|
||||
# some Nano receivers?
|
||||
serial = self.receiver.read_register(0x2D5)
|
||||
@property
|
||||
def serial(self):
|
||||
if self._serial is None:
|
||||
serial = self.receiver.read_register(_R.receiver_info,
|
||||
0x30 + self.number - 1)
|
||||
if serial:
|
||||
ps = ord(serial[9:10]) & 0x0F
|
||||
self._power_switch = _hidpp10.POWER_SWITCH_LOCATION[ps]
|
||||
else:
|
||||
# some Nano receivers?
|
||||
serial = self.receiver.read_register(0x2D5)
|
||||
|
||||
if serial:
|
||||
self._serial = _strhex(serial[1:5])
|
||||
else:
|
||||
# fallback...
|
||||
self._serial = self.receiver.serial
|
||||
return self._serial or '?'
|
||||
if serial:
|
||||
self._serial = _strhex(serial[1:5])
|
||||
else:
|
||||
# fallback...
|
||||
self._serial = self.receiver.serial
|
||||
return self._serial or '?'
|
||||
|
||||
@property
|
||||
def power_switch_location(self):
|
||||
if self._power_switch is None:
|
||||
ps = self.receiver.read_register(_R.receiver_info, 0x30 + self.number - 1)
|
||||
if ps is not None:
|
||||
ps = ord(ps[9:10]) & 0x0F
|
||||
self._power_switch = _hidpp10.POWER_SWITCH_LOCATION[ps]
|
||||
else:
|
||||
self._power_switch = '(unknown)'
|
||||
return self._power_switch
|
||||
@property
|
||||
def power_switch_location(self):
|
||||
if self._power_switch is None:
|
||||
ps = self.receiver.read_register(_R.receiver_info,
|
||||
0x30 + self.number - 1)
|
||||
if ps is not None:
|
||||
ps = ord(ps[9:10]) & 0x0F
|
||||
self._power_switch = _hidpp10.POWER_SWITCH_LOCATION[ps]
|
||||
else:
|
||||
self._power_switch = '(unknown)'
|
||||
return self._power_switch
|
||||
|
||||
@property
|
||||
def polling_rate(self):
|
||||
if self._polling_rate is None:
|
||||
pair_info = self.receiver.read_register(_R.receiver_info, 0x20 + self.number - 1)
|
||||
if pair_info:
|
||||
self._polling_rate = ord(pair_info[2:3])
|
||||
else:
|
||||
self._polling_rate = 0
|
||||
return self._polling_rate
|
||||
@property
|
||||
def polling_rate(self):
|
||||
if self._polling_rate is None:
|
||||
pair_info = self.receiver.read_register(_R.receiver_info,
|
||||
0x20 + self.number - 1)
|
||||
if pair_info:
|
||||
self._polling_rate = ord(pair_info[2:3])
|
||||
else:
|
||||
self._polling_rate = 0
|
||||
return self._polling_rate
|
||||
|
||||
@property
|
||||
def keys(self):
|
||||
if self._keys is None:
|
||||
if self.online and self.protocol >= 2.0:
|
||||
self._keys = _hidpp20.get_keys(self) or ()
|
||||
return self._keys
|
||||
@property
|
||||
def keys(self):
|
||||
if self._keys is None:
|
||||
if self.online and self.protocol >= 2.0:
|
||||
self._keys = _hidpp20.get_keys(self) or ()
|
||||
return self._keys
|
||||
|
||||
@property
|
||||
def registers(self):
|
||||
if self._registers is None:
|
||||
if self.descriptor and self.descriptor.registers:
|
||||
self._registers = list(self.descriptor.registers)
|
||||
else:
|
||||
self._registers = []
|
||||
return self._registers
|
||||
@property
|
||||
def registers(self):
|
||||
if self._registers is None:
|
||||
if self.descriptor and self.descriptor.registers:
|
||||
self._registers = list(self.descriptor.registers)
|
||||
else:
|
||||
self._registers = []
|
||||
return self._registers
|
||||
|
||||
@property
|
||||
def settings(self):
|
||||
if self._settings is None:
|
||||
if self.descriptor and self.descriptor.settings:
|
||||
self._settings = [s(self) for s in self.descriptor.settings]
|
||||
self._settings = [s for s in self._settings if s is not None]
|
||||
else:
|
||||
self._settings = []
|
||||
if not self._feature_settings_checked:
|
||||
self._feature_settings_checked =_check_feature_settings(self, self._settings)
|
||||
return self._settings
|
||||
@property
|
||||
def settings(self):
|
||||
if self._settings is None:
|
||||
if self.descriptor and self.descriptor.settings:
|
||||
self._settings = [s(self) for s in self.descriptor.settings]
|
||||
self._settings = [s for s in self._settings if s is not None]
|
||||
else:
|
||||
self._settings = []
|
||||
if not self._feature_settings_checked:
|
||||
self._feature_settings_checked = _check_feature_settings(
|
||||
self, self._settings)
|
||||
return self._settings
|
||||
|
||||
def enable_notifications(self, enable=True):
|
||||
"""Enable or disable device (dis)connection notifications on this
|
||||
def enable_notifications(self, enable=True):
|
||||
"""Enable or disable device (dis)connection notifications on this
|
||||
receiver."""
|
||||
if not bool(self.receiver) or self.protocol >= 2.0:
|
||||
return False
|
||||
if not bool(self.receiver) or self.protocol >= 2.0:
|
||||
return False
|
||||
|
||||
if enable:
|
||||
set_flag_bits = ( _hidpp10.NOTIFICATION_FLAG.battery_status
|
||||
| _hidpp10.NOTIFICATION_FLAG.keyboard_illumination
|
||||
| _hidpp10.NOTIFICATION_FLAG.wireless
|
||||
| _hidpp10.NOTIFICATION_FLAG.software_present )
|
||||
else:
|
||||
set_flag_bits = 0
|
||||
ok = _hidpp10.set_notification_flags(self, set_flag_bits)
|
||||
if ok is None:
|
||||
_log.warn("%s: failed to %s device notifications", self, 'enable' if enable else 'disable')
|
||||
if enable:
|
||||
set_flag_bits = (_hidpp10.NOTIFICATION_FLAG.battery_status
|
||||
| _hidpp10.NOTIFICATION_FLAG.keyboard_illumination
|
||||
| _hidpp10.NOTIFICATION_FLAG.wireless
|
||||
| _hidpp10.NOTIFICATION_FLAG.software_present)
|
||||
else:
|
||||
set_flag_bits = 0
|
||||
ok = _hidpp10.set_notification_flags(self, set_flag_bits)
|
||||
if ok is None:
|
||||
_log.warn("%s: failed to %s device notifications", self,
|
||||
'enable' if enable else 'disable')
|
||||
|
||||
flag_bits = _hidpp10.get_notification_flags(self)
|
||||
flag_names = None if flag_bits is None else tuple(_hidpp10.NOTIFICATION_FLAG.flag_names(flag_bits))
|
||||
if _log.isEnabledFor(_INFO):
|
||||
_log.info("%s: device notifications %s %s", self, 'enabled' if enable else 'disabled', flag_names)
|
||||
return flag_bits if ok else None
|
||||
flag_bits = _hidpp10.get_notification_flags(self)
|
||||
flag_names = None if flag_bits is None else tuple(
|
||||
_hidpp10.NOTIFICATION_FLAG.flag_names(flag_bits))
|
||||
if _log.isEnabledFor(_INFO):
|
||||
_log.info("%s: device notifications %s %s", self,
|
||||
'enabled' if enable else 'disabled', flag_names)
|
||||
return flag_bits if ok else None
|
||||
|
||||
def request(self, request_id, *params):
|
||||
return _base.request(self.receiver.handle, self.number, request_id, *params)
|
||||
def request(self, request_id, *params):
|
||||
return _base.request(self.receiver.handle, self.number, request_id,
|
||||
*params)
|
||||
|
||||
read_register = _hidpp10.read_register
|
||||
write_register = _hidpp10.write_register
|
||||
read_register = _hidpp10.read_register
|
||||
write_register = _hidpp10.write_register
|
||||
|
||||
def feature_request(self, feature, function=0x00, *params):
|
||||
if self.protocol >= 2.0:
|
||||
return _hidpp20.feature_request(self, feature, function, *params)
|
||||
def feature_request(self, feature, function=0x00, *params):
|
||||
if self.protocol >= 2.0:
|
||||
return _hidpp20.feature_request(self, feature, function, *params)
|
||||
|
||||
def ping(self):
|
||||
"""Checks if the device is online, returns True of False"""
|
||||
protocol = _base.ping(self.receiver.handle, self.number)
|
||||
self.online = protocol is not None
|
||||
if protocol is not None:
|
||||
self._protocol = protocol
|
||||
return self.online
|
||||
def ping(self):
|
||||
"""Checks if the device is online, returns True of False"""
|
||||
protocol = _base.ping(self.receiver.handle, self.number)
|
||||
self.online = protocol is not None
|
||||
if protocol is not None:
|
||||
self._protocol = protocol
|
||||
return self.online
|
||||
|
||||
def __index__(self):
|
||||
return self.number
|
||||
__int__ = __index__
|
||||
def __index__(self):
|
||||
return self.number
|
||||
|
||||
def __eq__(self, other):
|
||||
return other is not None and self.kind == other.kind and self.wpid == other.wpid
|
||||
__int__ = __index__
|
||||
|
||||
def __ne__(self, other):
|
||||
return other is None or self.kind != other.kind or self.wpid != other.wpid
|
||||
def __eq__(self, other):
|
||||
return other is not None and self.kind == other.kind and self.wpid == other.wpid
|
||||
|
||||
def __hash__(self):
|
||||
return self.wpid.__hash__()
|
||||
def __ne__(self, other):
|
||||
return other is None or self.kind != other.kind or self.wpid != other.wpid
|
||||
|
||||
__bool__ = __nonzero__ = lambda self: self.wpid is not None and self.number in self.receiver
|
||||
def __hash__(self):
|
||||
return self.wpid.__hash__()
|
||||
|
||||
__bool__ = __nonzero__ = lambda self: self.wpid is not None and self.number in self.receiver
|
||||
|
||||
def __str__(self):
|
||||
return '<PairedDevice(%d,%s,%s,%s)>' % (
|
||||
self.number, self.wpid, self.codename or '?', self.serial)
|
||||
|
||||
__unicode__ = __repr__ = __str__
|
||||
|
||||
def __str__(self):
|
||||
return '<PairedDevice(%d,%s,%s,%s)>' % (self.number, self.wpid, self.codename or '?', self.serial)
|
||||
__unicode__ = __repr__ = __str__
|
||||
|
||||
#
|
||||
#
|
||||
#
|
||||
|
||||
|
||||
class Receiver(object):
|
||||
"""A Unifying Receiver instance.
|
||||
"""A Unifying Receiver instance.
|
||||
|
||||
The paired devices are available through the sequence interface.
|
||||
"""
|
||||
number = 0xFF
|
||||
kind = None
|
||||
number = 0xFF
|
||||
kind = None
|
||||
|
||||
def __init__(self, handle, device_info):
|
||||
assert handle
|
||||
self.handle = handle
|
||||
assert device_info
|
||||
self.path = device_info.path
|
||||
# USB product id, used for some Nano receivers
|
||||
self.product_id = device_info.product_id
|
||||
product_info = _product_information(self.product_id)
|
||||
if not product_info:
|
||||
raise Exception("Unknown receiver type", self.product_id)
|
||||
def __init__(self, handle, device_info):
|
||||
assert handle
|
||||
self.handle = handle
|
||||
assert device_info
|
||||
self.path = device_info.path
|
||||
# USB product id, used for some Nano receivers
|
||||
self.product_id = device_info.product_id
|
||||
product_info = _product_information(self.product_id)
|
||||
if not product_info:
|
||||
raise Exception("Unknown receiver type", self.product_id)
|
||||
|
||||
# read the serial immediately, so we can find out max_devices
|
||||
serial_reply = self.read_register(_R.receiver_info, 0x03)
|
||||
if serial_reply :
|
||||
self.serial = _strhex(serial_reply[1:5])
|
||||
self.max_devices = ord(serial_reply[6:7])
|
||||
# TODO _properly_ figure out which receivers do and which don't support unpairing
|
||||
# This code supposes that receivers that don't unpair support a pairing request for device index 0
|
||||
self.may_unpair = self.write_register(_R.receiver_pairing) is None
|
||||
else: # handle receivers that don't have a serial number specially (i.e., c534)
|
||||
self.serial = None
|
||||
self.max_devices = product_info.get('max_devices',1)
|
||||
self.may_unpair = product_info.get('may_unpair',False)
|
||||
# read the serial immediately, so we can find out max_devices
|
||||
serial_reply = self.read_register(_R.receiver_info, 0x03)
|
||||
if serial_reply:
|
||||
self.serial = _strhex(serial_reply[1:5])
|
||||
self.max_devices = ord(serial_reply[6:7])
|
||||
# TODO _properly_ figure out which receivers do and which don't support unpairing
|
||||
# This code supposes that receivers that don't unpair support a pairing request for device index 0
|
||||
self.may_unpair = self.write_register(_R.receiver_pairing) is None
|
||||
else: # handle receivers that don't have a serial number specially (i.e., c534)
|
||||
self.serial = None
|
||||
self.max_devices = product_info.get('max_devices', 1)
|
||||
self.may_unpair = product_info.get('may_unpair', False)
|
||||
|
||||
self.name = product_info.get('name','')
|
||||
self.re_pairs = product_info.get('re_pairs',False)
|
||||
self._str = '<%s(%s,%s%s)>' % (self.name.replace(' ', ''), self.path, '' if isinstance(self.handle, int) else 'T', self.handle)
|
||||
self.name = product_info.get('name', '')
|
||||
self.re_pairs = product_info.get('re_pairs', False)
|
||||
self._str = '<%s(%s,%s%s)>' % (self.name.replace(
|
||||
' ', ''), self.path, '' if isinstance(self.handle, int) else 'T',
|
||||
self.handle)
|
||||
|
||||
self._firmware = None
|
||||
self._devices = {}
|
||||
self._remaining_pairings = None
|
||||
self._firmware = None
|
||||
self._devices = {}
|
||||
self._remaining_pairings = None
|
||||
|
||||
def close(self):
|
||||
handle, self.handle = self.handle, None
|
||||
self._devices.clear()
|
||||
return (handle and _base.close(handle))
|
||||
def close(self):
|
||||
handle, self.handle = self.handle, None
|
||||
self._devices.clear()
|
||||
return (handle and _base.close(handle))
|
||||
|
||||
def __del__(self):
|
||||
self.close()
|
||||
def __del__(self):
|
||||
self.close()
|
||||
|
||||
@property
|
||||
def firmware(self):
|
||||
if self._firmware is None and self.handle:
|
||||
self._firmware = _hidpp10.get_firmware(self)
|
||||
return self._firmware
|
||||
@property
|
||||
def firmware(self):
|
||||
if self._firmware is None and self.handle:
|
||||
self._firmware = _hidpp10.get_firmware(self)
|
||||
return self._firmware
|
||||
|
||||
# how many pairings remain (None for unknown, -1 for unlimited)
|
||||
def remaining_pairings(self,cache=True):
|
||||
if self._remaining_pairings is None or not cache:
|
||||
ps = self.read_register(_R.receiver_connection)
|
||||
if ps is not None:
|
||||
ps = ord(ps[2:3])
|
||||
self._remaining_pairings = ps-5 if ps >= 5 else -1
|
||||
return self._remaining_pairings
|
||||
# how many pairings remain (None for unknown, -1 for unlimited)
|
||||
def remaining_pairings(self, cache=True):
|
||||
if self._remaining_pairings is None or not cache:
|
||||
ps = self.read_register(_R.receiver_connection)
|
||||
if ps is not None:
|
||||
ps = ord(ps[2:3])
|
||||
self._remaining_pairings = ps - 5 if ps >= 5 else -1
|
||||
return self._remaining_pairings
|
||||
|
||||
def enable_notifications(self, enable=True):
|
||||
"""Enable or disable device (dis)connection notifications on this
|
||||
def enable_notifications(self, enable=True):
|
||||
"""Enable or disable device (dis)connection notifications on this
|
||||
receiver."""
|
||||
if not self.handle:
|
||||
return False
|
||||
if not self.handle:
|
||||
return False
|
||||
|
||||
if enable:
|
||||
set_flag_bits = ( _hidpp10.NOTIFICATION_FLAG.battery_status
|
||||
| _hidpp10.NOTIFICATION_FLAG.wireless
|
||||
| _hidpp10.NOTIFICATION_FLAG.software_present )
|
||||
else:
|
||||
set_flag_bits = 0
|
||||
ok = _hidpp10.set_notification_flags(self, set_flag_bits)
|
||||
if ok is None:
|
||||
_log.warn("%s: failed to %s receiver notifications", self, 'enable' if enable else 'disable')
|
||||
return None
|
||||
if enable:
|
||||
set_flag_bits = (_hidpp10.NOTIFICATION_FLAG.battery_status
|
||||
| _hidpp10.NOTIFICATION_FLAG.wireless
|
||||
| _hidpp10.NOTIFICATION_FLAG.software_present)
|
||||
else:
|
||||
set_flag_bits = 0
|
||||
ok = _hidpp10.set_notification_flags(self, set_flag_bits)
|
||||
if ok is None:
|
||||
_log.warn("%s: failed to %s receiver notifications", self,
|
||||
'enable' if enable else 'disable')
|
||||
return None
|
||||
|
||||
flag_bits = _hidpp10.get_notification_flags(self)
|
||||
flag_names = None if flag_bits is None else tuple(_hidpp10.NOTIFICATION_FLAG.flag_names(flag_bits))
|
||||
if _log.isEnabledFor(_INFO):
|
||||
_log.info("%s: receiver notifications %s => %s", self, 'enabled' if enable else 'disabled', flag_names)
|
||||
return flag_bits
|
||||
flag_bits = _hidpp10.get_notification_flags(self)
|
||||
flag_names = None if flag_bits is None else tuple(
|
||||
_hidpp10.NOTIFICATION_FLAG.flag_names(flag_bits))
|
||||
if _log.isEnabledFor(_INFO):
|
||||
_log.info("%s: receiver notifications %s => %s", self,
|
||||
'enabled' if enable else 'disabled', flag_names)
|
||||
return flag_bits
|
||||
|
||||
def notify_devices(self):
|
||||
"""Scan all devices."""
|
||||
if self.handle:
|
||||
if not self.write_register(_R.receiver_connection, 0x02):
|
||||
_log.warn("%s: failed to trigger device link notifications", self)
|
||||
def notify_devices(self):
|
||||
"""Scan all devices."""
|
||||
if self.handle:
|
||||
if not self.write_register(_R.receiver_connection, 0x02):
|
||||
_log.warn("%s: failed to trigger device link notifications",
|
||||
self)
|
||||
|
||||
def register_new_device(self, number, notification=None):
|
||||
if self._devices.get(number) is not None:
|
||||
raise IndexError("%s: device number %d already registered" % (self, number))
|
||||
def register_new_device(self, number, notification=None):
|
||||
if self._devices.get(number) is not None:
|
||||
raise IndexError("%s: device number %d already registered" %
|
||||
(self, number))
|
||||
|
||||
assert notification is None or notification.devnumber == number
|
||||
assert notification is None or notification.sub_id == 0x41
|
||||
assert notification is None or notification.devnumber == number
|
||||
assert notification is None or notification.sub_id == 0x41
|
||||
|
||||
try:
|
||||
dev = PairedDevice(self, number, notification)
|
||||
assert dev.wpid
|
||||
if _log.isEnabledFor(_INFO):
|
||||
_log.info("%s: found new device %d (%s)", self, number, dev.wpid)
|
||||
self._devices[number] = dev
|
||||
return dev
|
||||
except _base.NoSuchDevice:
|
||||
_log.exception("register_new_device")
|
||||
try:
|
||||
dev = PairedDevice(self, number, notification)
|
||||
assert dev.wpid
|
||||
if _log.isEnabledFor(_INFO):
|
||||
_log.info("%s: found new device %d (%s)", self, number,
|
||||
dev.wpid)
|
||||
self._devices[number] = dev
|
||||
return dev
|
||||
except _base.NoSuchDevice:
|
||||
_log.exception("register_new_device")
|
||||
|
||||
_log.warning("%s: looked for device %d, not found", self, number)
|
||||
self._devices[number] = None
|
||||
_log.warning("%s: looked for device %d, not found", self, number)
|
||||
self._devices[number] = None
|
||||
|
||||
def set_lock(self, lock_closed=True, device=0, timeout=0):
|
||||
if self.handle:
|
||||
action = 0x02 if lock_closed else 0x01
|
||||
reply = self.write_register(_R.receiver_pairing, action, device, timeout)
|
||||
if reply:
|
||||
return True
|
||||
_log.warn("%s: failed to %s the receiver lock", self, 'close' if lock_closed else 'open')
|
||||
def set_lock(self, lock_closed=True, device=0, timeout=0):
|
||||
if self.handle:
|
||||
action = 0x02 if lock_closed else 0x01
|
||||
reply = self.write_register(_R.receiver_pairing, action, device,
|
||||
timeout)
|
||||
if reply:
|
||||
return True
|
||||
_log.warn("%s: failed to %s the receiver lock", self,
|
||||
'close' if lock_closed else 'open')
|
||||
|
||||
def count(self):
|
||||
count = self.read_register(_R.receiver_connection)
|
||||
return 0 if count is None else ord(count[1:2])
|
||||
def count(self):
|
||||
count = self.read_register(_R.receiver_connection)
|
||||
return 0 if count is None else ord(count[1:2])
|
||||
|
||||
# def has_devices(self):
|
||||
# return len(self) > 0 or self.count() > 0
|
||||
# def has_devices(self):
|
||||
# return len(self) > 0 or self.count() > 0
|
||||
|
||||
def request(self, request_id, *params):
|
||||
if bool(self):
|
||||
return _base.request(self.handle, 0xFF, request_id, *params)
|
||||
def request(self, request_id, *params):
|
||||
if bool(self):
|
||||
return _base.request(self.handle, 0xFF, request_id, *params)
|
||||
|
||||
read_register = _hidpp10.read_register
|
||||
write_register = _hidpp10.write_register
|
||||
read_register = _hidpp10.read_register
|
||||
write_register = _hidpp10.write_register
|
||||
|
||||
def __iter__(self):
|
||||
for number in range(1, 1 + self.max_devices):
|
||||
if number in self._devices:
|
||||
dev = self._devices[number]
|
||||
else:
|
||||
dev = self.__getitem__(number)
|
||||
if dev is not None:
|
||||
yield dev
|
||||
def __iter__(self):
|
||||
for number in range(1, 1 + self.max_devices):
|
||||
if number in self._devices:
|
||||
dev = self._devices[number]
|
||||
else:
|
||||
dev = self.__getitem__(number)
|
||||
if dev is not None:
|
||||
yield dev
|
||||
|
||||
def __getitem__(self, key):
|
||||
if not bool(self):
|
||||
return None
|
||||
def __getitem__(self, key):
|
||||
if not bool(self):
|
||||
return None
|
||||
|
||||
dev = self._devices.get(key)
|
||||
if dev is not None:
|
||||
return dev
|
||||
dev = self._devices.get(key)
|
||||
if dev is not None:
|
||||
return dev
|
||||
|
||||
if not isinstance(key, int):
|
||||
raise TypeError('key must be an integer')
|
||||
if key < 1 or key > self.max_devices:
|
||||
raise IndexError(key)
|
||||
if not isinstance(key, int):
|
||||
raise TypeError('key must be an integer')
|
||||
if key < 1 or key > self.max_devices:
|
||||
raise IndexError(key)
|
||||
|
||||
return self.register_new_device(key)
|
||||
return self.register_new_device(key)
|
||||
|
||||
def __delitem__(self, key):
|
||||
self._unpair_device(key, False)
|
||||
def __delitem__(self, key):
|
||||
self._unpair_device(key, False)
|
||||
|
||||
def _unpair_device(self, key, force=False):
|
||||
key = int(key)
|
||||
def _unpair_device(self, key, force=False):
|
||||
key = int(key)
|
||||
|
||||
if self._devices.get(key) is None:
|
||||
raise IndexError(key)
|
||||
if self._devices.get(key) is None:
|
||||
raise IndexError(key)
|
||||
|
||||
dev = self._devices[key]
|
||||
if not dev:
|
||||
if key in self._devices:
|
||||
del self._devices[key]
|
||||
return
|
||||
dev = self._devices[key]
|
||||
if not dev:
|
||||
if key in self._devices:
|
||||
del self._devices[key]
|
||||
return
|
||||
|
||||
if self.re_pairs and not force:
|
||||
# invalidate the device, but these receivers don't unpair per se
|
||||
dev.online = False
|
||||
dev.wpid = None
|
||||
if key in self._devices:
|
||||
del self._devices[key]
|
||||
_log.warn("%s removed device %s", self, dev)
|
||||
else:
|
||||
reply = self.write_register(_R.receiver_pairing, 0x03, key)
|
||||
if reply:
|
||||
# invalidate the device
|
||||
dev.online = False
|
||||
dev.wpid = None
|
||||
if key in self._devices:
|
||||
del self._devices[key]
|
||||
_log.warn("%s unpaired device %s", self, dev)
|
||||
else:
|
||||
_log.error("%s failed to unpair device %s", self, dev)
|
||||
raise IndexError(key)
|
||||
if self.re_pairs and not force:
|
||||
# invalidate the device, but these receivers don't unpair per se
|
||||
dev.online = False
|
||||
dev.wpid = None
|
||||
if key in self._devices:
|
||||
del self._devices[key]
|
||||
_log.warn("%s removed device %s", self, dev)
|
||||
else:
|
||||
reply = self.write_register(_R.receiver_pairing, 0x03, key)
|
||||
if reply:
|
||||
# invalidate the device
|
||||
dev.online = False
|
||||
dev.wpid = None
|
||||
if key in self._devices:
|
||||
del self._devices[key]
|
||||
_log.warn("%s unpaired device %s", self, dev)
|
||||
else:
|
||||
_log.error("%s failed to unpair device %s", self, dev)
|
||||
raise IndexError(key)
|
||||
|
||||
def __len__(self):
|
||||
return len([d for d in self._devices.values() if d is not None])
|
||||
def __len__(self):
|
||||
return len([d for d in self._devices.values() if d is not None])
|
||||
|
||||
def __contains__(self, dev):
|
||||
if isinstance(dev, int):
|
||||
return self._devices.get(dev) is not None
|
||||
def __contains__(self, dev):
|
||||
if isinstance(dev, int):
|
||||
return self._devices.get(dev) is not None
|
||||
|
||||
return self.__contains__(dev.number)
|
||||
return self.__contains__(dev.number)
|
||||
|
||||
def __eq__(self, other):
|
||||
return other is not None and self.kind == other.kind and self.path == other.path
|
||||
def __eq__(self, other):
|
||||
return other is not None and self.kind == other.kind and self.path == other.path
|
||||
|
||||
def __ne__(self, other):
|
||||
return other is None or self.kind != other.kind or self.path != other.path
|
||||
def __ne__(self, other):
|
||||
return other is None or self.kind != other.kind or self.path != other.path
|
||||
|
||||
def __hash__(self):
|
||||
return self.path.__hash__()
|
||||
def __hash__(self):
|
||||
return self.path.__hash__()
|
||||
|
||||
def __str__(self):
|
||||
return self._str
|
||||
__unicode__ = __repr__ = __str__
|
||||
def __str__(self):
|
||||
return self._str
|
||||
|
||||
__bool__ = __nonzero__ = lambda self: self.handle is not None
|
||||
__unicode__ = __repr__ = __str__
|
||||
|
||||
@classmethod
|
||||
def open(self, device_info):
|
||||
"""Opens a Logitech Receiver found attached to the machine, by Linux device path.
|
||||
__bool__ = __nonzero__ = lambda self: self.handle is not None
|
||||
|
||||
@classmethod
|
||||
def open(self, device_info):
|
||||
"""Opens a Logitech Receiver found attached to the machine, by Linux device path.
|
||||
|
||||
:returns: An open file handle for the found receiver, or ``None``.
|
||||
"""
|
||||
try:
|
||||
handle = _base.open_path(device_info.path)
|
||||
if handle:
|
||||
return Receiver(handle, device_info)
|
||||
except OSError as e:
|
||||
_log.exception("open %s", device_info)
|
||||
if e.errno == _errno.EACCES:
|
||||
raise
|
||||
except:
|
||||
_log.exception("open %s", device_info)
|
||||
try:
|
||||
handle = _base.open_path(device_info.path)
|
||||
if handle:
|
||||
return Receiver(handle, device_info)
|
||||
except OSError as e:
|
||||
_log.exception("open %s", device_info)
|
||||
if e.errno == _errno.EACCES:
|
||||
raise
|
||||
except:
|
||||
_log.exception("open %s", device_info)
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
|
|
@ -22,490 +22,488 @@
|
|||
|
||||
from __future__ import absolute_import, division, print_function, unicode_literals
|
||||
|
||||
|
||||
from .common import NamedInts as _NamedInts
|
||||
|
||||
# <controls.xml awk -F\" '/<Control /{sub(/^LD_FINFO_(CTRLID_)?/, "", $2);printf("\t%s=0x%04X,\n", $2, $4)}' | sort -t= -k2
|
||||
CONTROL = _NamedInts(
|
||||
Volume_Up=0x0001,
|
||||
Volume_Down=0x0002,
|
||||
Mute=0x0003,
|
||||
Play__Pause=0x0004,
|
||||
Next=0x0005,
|
||||
Previous=0x0006,
|
||||
Stop=0x0007,
|
||||
Application_Switcher=0x0008,
|
||||
BURN=0x0009,
|
||||
Calculator=0x000A,
|
||||
CALENDAR=0x000B,
|
||||
CLOSE=0x000C,
|
||||
EJECT=0x000D,
|
||||
Mail=0x000E,
|
||||
HELP_AS_HID=0x000F,
|
||||
HELP_AS_F1=0x0010,
|
||||
LAUNCH_WORD_PROC=0x0011,
|
||||
LAUNCH_SPREADSHEET=0x0012,
|
||||
LAUNCH_PRESENTATION=0x0013,
|
||||
UNDO_AS_CTRL_Z=0x0014,
|
||||
UNDO_AS_HID=0x0015,
|
||||
REDO_AS_CTRL_Y=0x0016,
|
||||
REDO_AS_HID=0x0017,
|
||||
PRINT_AS_CTRL_P=0x0018,
|
||||
PRINT_AS_HID=0x0019,
|
||||
SAVE_AS_CTRL_S=0x001A,
|
||||
SAVE_AS_HID=0x001B,
|
||||
PRESET_A=0x001C,
|
||||
PRESET_B=0x001D,
|
||||
PRESET_C=0x001E,
|
||||
PRESET_D=0x001F,
|
||||
FAVORITES=0x0020,
|
||||
GADGETS=0x0021,
|
||||
MY_HOME=0x0022,
|
||||
GADGETS_AS_WIN_G=0x0023,
|
||||
MAXIMIZE_AS_HID=0x0024,
|
||||
MAXIMIZE_AS_WIN_SHIFT_M=0x0025,
|
||||
MINIMIZE_AS_HID=0x0026,
|
||||
MINIMIZE_AS_WIN_M=0x0027,
|
||||
MEDIA_PLAYER=0x0028,
|
||||
MEDIA_CENTER_LOGI=0x0029,
|
||||
MEDIA_CENTER_MSFT=0x002A, # Should not be used as it is not reprogrammable under Windows
|
||||
CUSTOM_MENU=0x002B,
|
||||
MESSENGER=0x002C,
|
||||
MY_DOCUMENTS=0x002D,
|
||||
MY_MUSIC=0x002E,
|
||||
WEBCAM=0x002F,
|
||||
MY_PICTURES=0x0030,
|
||||
MY_VIDEOS=0x0031,
|
||||
MY_COMPUTER_AS_HID=0x0032,
|
||||
MY_COMPUTER_AS_WIN_E=0x0033,
|
||||
LAUNC_PICTURE_VIEWER=0x0035,
|
||||
ONE_TOUCH_SEARCH=0x0036,
|
||||
PRESET_1=0x0037,
|
||||
PRESET_2=0x0038,
|
||||
PRESET_3=0x0039,
|
||||
PRESET_4=0x003A,
|
||||
RECORD=0x003B,
|
||||
INTERNET_REFRESH=0x003C,
|
||||
ROTATE_RIGHT=0x003D,
|
||||
Search=0x003E, # SEARCH
|
||||
SHUFFLE=0x003F,
|
||||
SLEEP=0x0040,
|
||||
INTERNET_STOP=0x0041,
|
||||
SYNCHRONIZE=0x0042,
|
||||
ZOOM=0x0043,
|
||||
ZOOM_IN_AS_HID=0x0044,
|
||||
ZOOM_IN_AS_CTRL_WHEEL=0x0045,
|
||||
ZOOM_IN_AS_CLTR_PLUS=0x0046,
|
||||
ZOOM_OUT_AS_HID=0x0047,
|
||||
ZOOM_OUT_AS_CTRL_WHEEL=0x0048,
|
||||
ZOOM_OUT_AS_CLTR_MINUS=0x0049,
|
||||
ZOOM_RESET=0x004A,
|
||||
ZOOM_FULL_SCREEN=0x004B,
|
||||
PRINT_SCREEN=0x004C,
|
||||
PAUSE_BREAK=0x004D,
|
||||
SCROLL_LOCK=0x004E,
|
||||
CONTEXTUAL_MENU=0x004F,
|
||||
Left_Button=0x0050, # LEFT_CLICK
|
||||
Right_Button=0x0051, # RIGHT_CLICK
|
||||
Middle_Button=0x0052, # MIDDLE_BUTTON
|
||||
Back_Button=0x0053, # from M510v2 was BACK_AS_BUTTON_4
|
||||
Back=0x0054, # BACK_AS_HID
|
||||
BACK_AS_ALT_WIN_ARROW=0x0055,
|
||||
Forward_Button=0x0056, # from M510v2 was FORWARD_AS_BUTTON_5
|
||||
FORWARD_AS_HID=0x0057,
|
||||
FORWARD_AS_ALT_WIN_ARROW=0x0058,
|
||||
BUTTON_6=0x0059,
|
||||
LEFT_SCROLL_AS_BUTTON_7=0x005A,
|
||||
Left_Tilt=0x005B, # from M510v2 was LEFT_SCROLL_AS_AC_PAN
|
||||
RIGHT_SCROLL_AS_BUTTON_8=0x005C,
|
||||
Right_Tilt=0x005D, # from M510v2 was RIGHT_SCROLL_AS_AC_PAN
|
||||
BUTTON_9=0x005E,
|
||||
BUTTON_10=0x005F,
|
||||
BUTTON_11=0x0060,
|
||||
BUTTON_12=0x0061,
|
||||
BUTTON_13=0x0062,
|
||||
BUTTON_14=0x0063,
|
||||
BUTTON_15=0x0064,
|
||||
BUTTON_16=0x0065,
|
||||
BUTTON_17=0x0066,
|
||||
BUTTON_18=0x0067,
|
||||
BUTTON_19=0x0068,
|
||||
BUTTON_20=0x0069,
|
||||
BUTTON_21=0x006A,
|
||||
BUTTON_22=0x006B,
|
||||
BUTTON_23=0x006C,
|
||||
BUTTON_24=0x006D,
|
||||
Show_Desktop=0x006E, # Show_Desktop
|
||||
Lock_PC=0x006F,
|
||||
FN_F1=0x0070,
|
||||
FN_F2=0x0071,
|
||||
FN_F3=0x0072,
|
||||
FN_F4=0x0073,
|
||||
FN_F5=0x0074,
|
||||
FN_F6=0x0075,
|
||||
FN_F7=0x0076,
|
||||
FN_F8=0x0077,
|
||||
FN_F9=0x0078,
|
||||
FN_F10=0x0079,
|
||||
FN_F11=0x007A,
|
||||
FN_F12=0x007B,
|
||||
FN_F13=0x007C,
|
||||
FN_F14=0x007D,
|
||||
FN_F15=0x007E,
|
||||
FN_F16=0x007F,
|
||||
FN_F17=0x0080,
|
||||
FN_F18=0x0081,
|
||||
FN_F19=0x0082,
|
||||
IOS_HOME=0x0083,
|
||||
ANDROID_HOME=0x0084,
|
||||
ANDROID_MENU=0x0085,
|
||||
ANDROID_SEARCH=0x0086,
|
||||
ANDROID_BACK=0x0087,
|
||||
HOME_COMBO=0x0088,
|
||||
LOCK_COMBO=0x0089,
|
||||
IOS_VIRTUAL_KEYBOARD=0x008A,
|
||||
IOS_LANGUAGE_SWICH=0x008B,
|
||||
MAC_EXPOSE=0x008C,
|
||||
MAC_DASHBOARD=0x008D,
|
||||
WIN7_SNAP_LEFT=0x008E,
|
||||
WIN7_SNAP_RIGHT=0x008F,
|
||||
Minimize_Window=0x0090, # WIN7_MINIMIZE_AS_WIN_ARROW
|
||||
Maximize_Window=0x0091, # WIN7_MAXIMIZE_AS_WIN_ARROW
|
||||
WIN7_STRETCH_UP=0x0092,
|
||||
WIN7_MONITOR_SWITCH_AS_WIN_SHIFT_LEFTARROW=0x0093,
|
||||
WIN7_MONITOR_SWITCH_AS_WIN_SHIFT_RIGHTARROW=0x0094,
|
||||
Switch_Screen=0x0095, # WIN7_SHOW_PRESENTATION_MODE
|
||||
WIN7_SHOW_MOBILITY_CENTER=0x0096,
|
||||
ANALOG_HSCROLL=0x0097,
|
||||
METRO_APPSWITCH=0x009F,
|
||||
METRO_APPBAR=0x00A0,
|
||||
METRO_CHARMS=0x00A1,
|
||||
CALC_VKEYBOARD=0x00A2,
|
||||
METRO_SEARCH=0x00A3,
|
||||
COMBO_SLEEP=0x00A4,
|
||||
METRO_SHARE=0x00A5,
|
||||
METRO_SETTINGS=0x00A6,
|
||||
METRO_DEVICES=0x00A7,
|
||||
METRO_START_SCREEN=0x00A9,
|
||||
ZOOMIN=0x00AA,
|
||||
ZOOMOUT=0x00AB,
|
||||
BACK_HSCROLL=0x00AC,
|
||||
SHOW_DESKTOP_HPP=0x00AE,
|
||||
Fn_Left_Click=0x00B7, # from K400 Plus
|
||||
# https://docs.google.com/document/u/0/d/1YvXICgSe8BcBAuMr4Xu_TutvAxaa-RnGfyPFWBWzhkc/export?format=docx
|
||||
# Extract to csv. Eliminate extra linefeeds and spaces.
|
||||
# awk -F, '/0x/{gsub(" \\+ ","_",$2); gsub("/","__",$2); gsub(" -","_Down",$2); gsub(" \\+","_Up",$2); gsub("[()\"-]","",$2); gsub(" ","_",$2); printf("\t%s=0x%04X,\n", $2, $1)}' < controls.cvs
|
||||
Second_Left_Click=0x00B8, # Second_LClick / on K400 Plus
|
||||
Fn_Second_Left_Click=0x00B9, # Fn_Second_LClick
|
||||
MultiPlatform_App_Switch=0x00BA,
|
||||
MultiPlatform_Home=0x00BB,
|
||||
MultiPlatform_Menu=0x00BC,
|
||||
MultiPlatform_Back=0x00BD,
|
||||
MultiPlatform_Insert=0x00BE,
|
||||
Screen_Capture__Print_Screen=0x00BF, # on Craft Keyboard
|
||||
Fn_Down=0x00C0,
|
||||
Fn_Up=0x00C1,
|
||||
Multiplatform_Lock=0x00C2,
|
||||
App_Switch_Gesture=0x00C3, # Thumb_Button on MX Master
|
||||
Smart_Shift=0x00C4, # Top_Button on MX Master
|
||||
Microphone=0x00C5,
|
||||
Wifi=0x00C6,
|
||||
Brightness_Down=0x00C7,
|
||||
Brightness_Up=0x00C8,
|
||||
Display_out__project_screen_=0x00C9,
|
||||
View_Open_Apps=0x00CA,
|
||||
View_all_apps=0x00CB,
|
||||
Switch_App=0x00CC,
|
||||
Fn_inversion_change=0x00CD,
|
||||
MultiPlatform_back=0x00CE,
|
||||
Multiplatform_forward=0x00CF,
|
||||
Multiplatform_gesture_button=0x00D0,
|
||||
Host_Switch_channel_1=0x00D1,
|
||||
Host_Switch_channel_2=0x00D2,
|
||||
Host_Switch_channel_3=0x00D3,
|
||||
Multiplatform_search=0x00D4,
|
||||
Multiplatform_Home__Mission_Control=0x00D5,
|
||||
Multiplatform_Menu__Show__Hide_Virtual_Keyboard__Launchpad=0x00D6,
|
||||
Virtual_Gesture_Button=0x00D7,
|
||||
Cursor_Button_Long_press=0x00D8,
|
||||
Next_Button_Shortpress=0x00D9, # Next_Button
|
||||
Next_Button_Longpress=0x00DA,
|
||||
Back_Button_Shortpress=0x00DB, # Back
|
||||
Back_Button_Longpress=0x00DC,
|
||||
Multi_Platform_Language_Switch=0x00DD,
|
||||
F_Lock=0x00DE,
|
||||
Switch_Highlight=0x00DF,
|
||||
Mission_Control__Task_View=0x00E0, # Switch_Workspaces on Craft Keyboard
|
||||
Dashboard_Launchpad__Action_Center=0x00E1, # Application_Launcher on Craft Keyboard
|
||||
Backlight_Down=0x00E2,
|
||||
Backlight_Up=0x00E3,
|
||||
Previous_Fn=0x00E4, # Reprogrammable_Previous_Track / on Craft Keyboard
|
||||
Play__Pause_Fn=0x00E5, # Reprogrammable_Play__Pause / on Craft Keyboard
|
||||
Next_Fn=0x00E6, # Reprogrammable_Next_Track / on Craft Keyboard
|
||||
Mute_Fn=0x00E7, # Reprogrammable_Mute / on Craft Keyboard
|
||||
Volume_Down_Fn=0x00E8, # Reprogrammable_Volume_Down / on Craft Keyboard
|
||||
Volume_Up_Fn=0x00E9, # Reprogrammable_Volume_Up / on Craft Keyboard
|
||||
App_Contextual_Menu__Right_Click=0x00EA, # Context_Menu on Craft Keyboard
|
||||
Right_Arrow=0x00EB,
|
||||
Left_Arrow=0x00EC,
|
||||
DPI_Change=0x00ED,
|
||||
New_Tab=0x00EE,
|
||||
F2=0x00EF,
|
||||
F3=0x00F0,
|
||||
F4=0x00F1,
|
||||
F5=0x00F2,
|
||||
F6=0x00F3,
|
||||
F7=0x00F4,
|
||||
F8=0x00F5,
|
||||
F1=0x00F6,
|
||||
Next_Color_Effect=0x00F7,
|
||||
Increase_Color_Effect_Speed=0x00F8,
|
||||
Decrease_Color_Effect_Speed=0x00F9,
|
||||
Load_Lighting_Custom_Profile=0x00FA,
|
||||
Laser_button_short_press=0x00FB,
|
||||
Laser_button_long_press=0x00FC,
|
||||
DPI_switch=0x00FD,
|
||||
MultiPlatform_Home__Show_Desktop=0x00FE,
|
||||
MultiPlatform_App_Switch__Show_Dashboard=0x00FF,
|
||||
MultiPlatform_App_Switch_2=0x0100, # MultiPlatform_App_Switch
|
||||
Fn_Inversion__Hot_Key=0x0101,
|
||||
LeftAndRightClick=0x0102,
|
||||
LED_TOGGLE=0x013B, #
|
||||
Volume_Up=0x0001,
|
||||
Volume_Down=0x0002,
|
||||
Mute=0x0003,
|
||||
Play__Pause=0x0004,
|
||||
Next=0x0005,
|
||||
Previous=0x0006,
|
||||
Stop=0x0007,
|
||||
Application_Switcher=0x0008,
|
||||
BURN=0x0009,
|
||||
Calculator=0x000A,
|
||||
CALENDAR=0x000B,
|
||||
CLOSE=0x000C,
|
||||
EJECT=0x000D,
|
||||
Mail=0x000E,
|
||||
HELP_AS_HID=0x000F,
|
||||
HELP_AS_F1=0x0010,
|
||||
LAUNCH_WORD_PROC=0x0011,
|
||||
LAUNCH_SPREADSHEET=0x0012,
|
||||
LAUNCH_PRESENTATION=0x0013,
|
||||
UNDO_AS_CTRL_Z=0x0014,
|
||||
UNDO_AS_HID=0x0015,
|
||||
REDO_AS_CTRL_Y=0x0016,
|
||||
REDO_AS_HID=0x0017,
|
||||
PRINT_AS_CTRL_P=0x0018,
|
||||
PRINT_AS_HID=0x0019,
|
||||
SAVE_AS_CTRL_S=0x001A,
|
||||
SAVE_AS_HID=0x001B,
|
||||
PRESET_A=0x001C,
|
||||
PRESET_B=0x001D,
|
||||
PRESET_C=0x001E,
|
||||
PRESET_D=0x001F,
|
||||
FAVORITES=0x0020,
|
||||
GADGETS=0x0021,
|
||||
MY_HOME=0x0022,
|
||||
GADGETS_AS_WIN_G=0x0023,
|
||||
MAXIMIZE_AS_HID=0x0024,
|
||||
MAXIMIZE_AS_WIN_SHIFT_M=0x0025,
|
||||
MINIMIZE_AS_HID=0x0026,
|
||||
MINIMIZE_AS_WIN_M=0x0027,
|
||||
MEDIA_PLAYER=0x0028,
|
||||
MEDIA_CENTER_LOGI=0x0029,
|
||||
MEDIA_CENTER_MSFT=
|
||||
0x002A, # Should not be used as it is not reprogrammable under Windows
|
||||
CUSTOM_MENU=0x002B,
|
||||
MESSENGER=0x002C,
|
||||
MY_DOCUMENTS=0x002D,
|
||||
MY_MUSIC=0x002E,
|
||||
WEBCAM=0x002F,
|
||||
MY_PICTURES=0x0030,
|
||||
MY_VIDEOS=0x0031,
|
||||
MY_COMPUTER_AS_HID=0x0032,
|
||||
MY_COMPUTER_AS_WIN_E=0x0033,
|
||||
LAUNC_PICTURE_VIEWER=0x0035,
|
||||
ONE_TOUCH_SEARCH=0x0036,
|
||||
PRESET_1=0x0037,
|
||||
PRESET_2=0x0038,
|
||||
PRESET_3=0x0039,
|
||||
PRESET_4=0x003A,
|
||||
RECORD=0x003B,
|
||||
INTERNET_REFRESH=0x003C,
|
||||
ROTATE_RIGHT=0x003D,
|
||||
Search=0x003E, # SEARCH
|
||||
SHUFFLE=0x003F,
|
||||
SLEEP=0x0040,
|
||||
INTERNET_STOP=0x0041,
|
||||
SYNCHRONIZE=0x0042,
|
||||
ZOOM=0x0043,
|
||||
ZOOM_IN_AS_HID=0x0044,
|
||||
ZOOM_IN_AS_CTRL_WHEEL=0x0045,
|
||||
ZOOM_IN_AS_CLTR_PLUS=0x0046,
|
||||
ZOOM_OUT_AS_HID=0x0047,
|
||||
ZOOM_OUT_AS_CTRL_WHEEL=0x0048,
|
||||
ZOOM_OUT_AS_CLTR_MINUS=0x0049,
|
||||
ZOOM_RESET=0x004A,
|
||||
ZOOM_FULL_SCREEN=0x004B,
|
||||
PRINT_SCREEN=0x004C,
|
||||
PAUSE_BREAK=0x004D,
|
||||
SCROLL_LOCK=0x004E,
|
||||
CONTEXTUAL_MENU=0x004F,
|
||||
Left_Button=0x0050, # LEFT_CLICK
|
||||
Right_Button=0x0051, # RIGHT_CLICK
|
||||
Middle_Button=0x0052, # MIDDLE_BUTTON
|
||||
Back_Button=0x0053, # from M510v2 was BACK_AS_BUTTON_4
|
||||
Back=0x0054, # BACK_AS_HID
|
||||
BACK_AS_ALT_WIN_ARROW=0x0055,
|
||||
Forward_Button=0x0056, # from M510v2 was FORWARD_AS_BUTTON_5
|
||||
FORWARD_AS_HID=0x0057,
|
||||
FORWARD_AS_ALT_WIN_ARROW=0x0058,
|
||||
BUTTON_6=0x0059,
|
||||
LEFT_SCROLL_AS_BUTTON_7=0x005A,
|
||||
Left_Tilt=0x005B, # from M510v2 was LEFT_SCROLL_AS_AC_PAN
|
||||
RIGHT_SCROLL_AS_BUTTON_8=0x005C,
|
||||
Right_Tilt=0x005D, # from M510v2 was RIGHT_SCROLL_AS_AC_PAN
|
||||
BUTTON_9=0x005E,
|
||||
BUTTON_10=0x005F,
|
||||
BUTTON_11=0x0060,
|
||||
BUTTON_12=0x0061,
|
||||
BUTTON_13=0x0062,
|
||||
BUTTON_14=0x0063,
|
||||
BUTTON_15=0x0064,
|
||||
BUTTON_16=0x0065,
|
||||
BUTTON_17=0x0066,
|
||||
BUTTON_18=0x0067,
|
||||
BUTTON_19=0x0068,
|
||||
BUTTON_20=0x0069,
|
||||
BUTTON_21=0x006A,
|
||||
BUTTON_22=0x006B,
|
||||
BUTTON_23=0x006C,
|
||||
BUTTON_24=0x006D,
|
||||
Show_Desktop=0x006E, # Show_Desktop
|
||||
Lock_PC=0x006F,
|
||||
FN_F1=0x0070,
|
||||
FN_F2=0x0071,
|
||||
FN_F3=0x0072,
|
||||
FN_F4=0x0073,
|
||||
FN_F5=0x0074,
|
||||
FN_F6=0x0075,
|
||||
FN_F7=0x0076,
|
||||
FN_F8=0x0077,
|
||||
FN_F9=0x0078,
|
||||
FN_F10=0x0079,
|
||||
FN_F11=0x007A,
|
||||
FN_F12=0x007B,
|
||||
FN_F13=0x007C,
|
||||
FN_F14=0x007D,
|
||||
FN_F15=0x007E,
|
||||
FN_F16=0x007F,
|
||||
FN_F17=0x0080,
|
||||
FN_F18=0x0081,
|
||||
FN_F19=0x0082,
|
||||
IOS_HOME=0x0083,
|
||||
ANDROID_HOME=0x0084,
|
||||
ANDROID_MENU=0x0085,
|
||||
ANDROID_SEARCH=0x0086,
|
||||
ANDROID_BACK=0x0087,
|
||||
HOME_COMBO=0x0088,
|
||||
LOCK_COMBO=0x0089,
|
||||
IOS_VIRTUAL_KEYBOARD=0x008A,
|
||||
IOS_LANGUAGE_SWICH=0x008B,
|
||||
MAC_EXPOSE=0x008C,
|
||||
MAC_DASHBOARD=0x008D,
|
||||
WIN7_SNAP_LEFT=0x008E,
|
||||
WIN7_SNAP_RIGHT=0x008F,
|
||||
Minimize_Window=0x0090, # WIN7_MINIMIZE_AS_WIN_ARROW
|
||||
Maximize_Window=0x0091, # WIN7_MAXIMIZE_AS_WIN_ARROW
|
||||
WIN7_STRETCH_UP=0x0092,
|
||||
WIN7_MONITOR_SWITCH_AS_WIN_SHIFT_LEFTARROW=0x0093,
|
||||
WIN7_MONITOR_SWITCH_AS_WIN_SHIFT_RIGHTARROW=0x0094,
|
||||
Switch_Screen=0x0095, # WIN7_SHOW_PRESENTATION_MODE
|
||||
WIN7_SHOW_MOBILITY_CENTER=0x0096,
|
||||
ANALOG_HSCROLL=0x0097,
|
||||
METRO_APPSWITCH=0x009F,
|
||||
METRO_APPBAR=0x00A0,
|
||||
METRO_CHARMS=0x00A1,
|
||||
CALC_VKEYBOARD=0x00A2,
|
||||
METRO_SEARCH=0x00A3,
|
||||
COMBO_SLEEP=0x00A4,
|
||||
METRO_SHARE=0x00A5,
|
||||
METRO_SETTINGS=0x00A6,
|
||||
METRO_DEVICES=0x00A7,
|
||||
METRO_START_SCREEN=0x00A9,
|
||||
ZOOMIN=0x00AA,
|
||||
ZOOMOUT=0x00AB,
|
||||
BACK_HSCROLL=0x00AC,
|
||||
SHOW_DESKTOP_HPP=0x00AE,
|
||||
Fn_Left_Click=0x00B7, # from K400 Plus
|
||||
# https://docs.google.com/document/u/0/d/1YvXICgSe8BcBAuMr4Xu_TutvAxaa-RnGfyPFWBWzhkc/export?format=docx
|
||||
# Extract to csv. Eliminate extra linefeeds and spaces.
|
||||
# awk -F, '/0x/{gsub(" \\+ ","_",$2); gsub("/","__",$2); gsub(" -","_Down",$2); gsub(" \\+","_Up",$2); gsub("[()\"-]","",$2); gsub(" ","_",$2); printf("\t%s=0x%04X,\n", $2, $1)}' < controls.cvs
|
||||
Second_Left_Click=0x00B8, # Second_LClick / on K400 Plus
|
||||
Fn_Second_Left_Click=0x00B9, # Fn_Second_LClick
|
||||
MultiPlatform_App_Switch=0x00BA,
|
||||
MultiPlatform_Home=0x00BB,
|
||||
MultiPlatform_Menu=0x00BC,
|
||||
MultiPlatform_Back=0x00BD,
|
||||
MultiPlatform_Insert=0x00BE,
|
||||
Screen_Capture__Print_Screen=0x00BF, # on Craft Keyboard
|
||||
Fn_Down=0x00C0,
|
||||
Fn_Up=0x00C1,
|
||||
Multiplatform_Lock=0x00C2,
|
||||
App_Switch_Gesture=0x00C3, # Thumb_Button on MX Master
|
||||
Smart_Shift=0x00C4, # Top_Button on MX Master
|
||||
Microphone=0x00C5,
|
||||
Wifi=0x00C6,
|
||||
Brightness_Down=0x00C7,
|
||||
Brightness_Up=0x00C8,
|
||||
Display_out__project_screen_=0x00C9,
|
||||
View_Open_Apps=0x00CA,
|
||||
View_all_apps=0x00CB,
|
||||
Switch_App=0x00CC,
|
||||
Fn_inversion_change=0x00CD,
|
||||
MultiPlatform_back=0x00CE,
|
||||
Multiplatform_forward=0x00CF,
|
||||
Multiplatform_gesture_button=0x00D0,
|
||||
Host_Switch_channel_1=0x00D1,
|
||||
Host_Switch_channel_2=0x00D2,
|
||||
Host_Switch_channel_3=0x00D3,
|
||||
Multiplatform_search=0x00D4,
|
||||
Multiplatform_Home__Mission_Control=0x00D5,
|
||||
Multiplatform_Menu__Show__Hide_Virtual_Keyboard__Launchpad=0x00D6,
|
||||
Virtual_Gesture_Button=0x00D7,
|
||||
Cursor_Button_Long_press=0x00D8,
|
||||
Next_Button_Shortpress=0x00D9, # Next_Button
|
||||
Next_Button_Longpress=0x00DA,
|
||||
Back_Button_Shortpress=0x00DB, # Back
|
||||
Back_Button_Longpress=0x00DC,
|
||||
Multi_Platform_Language_Switch=0x00DD,
|
||||
F_Lock=0x00DE,
|
||||
Switch_Highlight=0x00DF,
|
||||
Mission_Control__Task_View=0x00E0, # Switch_Workspaces on Craft Keyboard
|
||||
Dashboard_Launchpad__Action_Center=
|
||||
0x00E1, # Application_Launcher on Craft Keyboard
|
||||
Backlight_Down=0x00E2,
|
||||
Backlight_Up=0x00E3,
|
||||
Previous_Fn=0x00E4, # Reprogrammable_Previous_Track / on Craft Keyboard
|
||||
Play__Pause_Fn=0x00E5, # Reprogrammable_Play__Pause / on Craft Keyboard
|
||||
Next_Fn=0x00E6, # Reprogrammable_Next_Track / on Craft Keyboard
|
||||
Mute_Fn=0x00E7, # Reprogrammable_Mute / on Craft Keyboard
|
||||
Volume_Down_Fn=0x00E8, # Reprogrammable_Volume_Down / on Craft Keyboard
|
||||
Volume_Up_Fn=0x00E9, # Reprogrammable_Volume_Up / on Craft Keyboard
|
||||
App_Contextual_Menu__Right_Click=0x00EA, # Context_Menu on Craft Keyboard
|
||||
Right_Arrow=0x00EB,
|
||||
Left_Arrow=0x00EC,
|
||||
DPI_Change=0x00ED,
|
||||
New_Tab=0x00EE,
|
||||
F2=0x00EF,
|
||||
F3=0x00F0,
|
||||
F4=0x00F1,
|
||||
F5=0x00F2,
|
||||
F6=0x00F3,
|
||||
F7=0x00F4,
|
||||
F8=0x00F5,
|
||||
F1=0x00F6,
|
||||
Next_Color_Effect=0x00F7,
|
||||
Increase_Color_Effect_Speed=0x00F8,
|
||||
Decrease_Color_Effect_Speed=0x00F9,
|
||||
Load_Lighting_Custom_Profile=0x00FA,
|
||||
Laser_button_short_press=0x00FB,
|
||||
Laser_button_long_press=0x00FC,
|
||||
DPI_switch=0x00FD,
|
||||
MultiPlatform_Home__Show_Desktop=0x00FE,
|
||||
MultiPlatform_App_Switch__Show_Dashboard=0x00FF,
|
||||
MultiPlatform_App_Switch_2=0x0100, # MultiPlatform_App_Switch
|
||||
Fn_Inversion__Hot_Key=0x0101,
|
||||
LeftAndRightClick=0x0102,
|
||||
LED_TOGGLE=0x013B, #
|
||||
)
|
||||
CONTROL._fallback = lambda x: 'unknown:%04X' % x
|
||||
|
||||
# <tasks.xml awk -F\" '/<Task /{gsub(/ /, "_", $6); printf("\t%s=0x%04X,\n", $6, $4)}'
|
||||
TASK = _NamedInts(
|
||||
Volume_Up=0x0001,
|
||||
Volume_Down=0x0002,
|
||||
Mute=0x0003,
|
||||
# Multimedia tasks:
|
||||
Play__Pause=0x0004,
|
||||
Next=0x0005,
|
||||
Previous=0x0006,
|
||||
Stop=0x0007,
|
||||
Volume_Up=0x0001,
|
||||
Volume_Down=0x0002,
|
||||
Mute=0x0003,
|
||||
# Multimedia tasks:
|
||||
Play__Pause=0x0004,
|
||||
Next=0x0005,
|
||||
Previous=0x0006,
|
||||
Stop=0x0007,
|
||||
Application_Switcher=0x0008,
|
||||
BurnMediaPlayer=0x0009,
|
||||
Calculator=0x000A,
|
||||
Calendar=0x000B,
|
||||
Close_Application=0x000C,
|
||||
Eject=0x000D,
|
||||
Email=0x000E,
|
||||
Help=0x000F,
|
||||
OffDocument=0x0010,
|
||||
OffSpreadsheet=0x0011,
|
||||
OffPowerpnt=0x0012,
|
||||
Undo=0x0013,
|
||||
Redo=0x0014,
|
||||
Print=0x0015,
|
||||
Save=0x0016,
|
||||
SmartKeySet=0x0017,
|
||||
Favorites=0x0018,
|
||||
GadgetsSet=0x0019,
|
||||
HomePage=0x001A,
|
||||
WindowsRestore=0x001B,
|
||||
WindowsMinimize=0x001C,
|
||||
Music=0x001D, # also known as MediaPlayer
|
||||
|
||||
Application_Switcher=0x0008,
|
||||
BurnMediaPlayer=0x0009,
|
||||
Calculator=0x000A,
|
||||
Calendar=0x000B,
|
||||
Close_Application=0x000C,
|
||||
Eject=0x000D,
|
||||
Email=0x000E,
|
||||
Help=0x000F,
|
||||
OffDocument=0x0010,
|
||||
OffSpreadsheet=0x0011,
|
||||
OffPowerpnt=0x0012,
|
||||
Undo=0x0013,
|
||||
Redo=0x0014,
|
||||
Print=0x0015,
|
||||
Save=0x0016,
|
||||
SmartKeySet=0x0017,
|
||||
Favorites=0x0018,
|
||||
GadgetsSet=0x0019,
|
||||
HomePage=0x001A,
|
||||
WindowsRestore=0x001B,
|
||||
WindowsMinimize=0x001C,
|
||||
Music=0x001D, # also known as MediaPlayer
|
||||
|
||||
# Both 0x001E and 0x001F are known as MediaCenterSet
|
||||
Media_Center_Logitech=0x001E,
|
||||
Media_Center_Microsoft=0x001F,
|
||||
|
||||
UserMenu=0x0020,
|
||||
Messenger=0x0021,
|
||||
PersonalFolders=0x0022,
|
||||
MyMusic=0x0023,
|
||||
Webcam=0x0024,
|
||||
PicturesFolder=0x0025,
|
||||
MyVideos=0x0026,
|
||||
My_Computer=0x0027,
|
||||
PictureAppSet=0x0028,
|
||||
Search=0x0029, # also known as AdvSmartSearch
|
||||
RecordMediaPlayer=0x002A,
|
||||
BrowserRefresh=0x002B,
|
||||
RotateRight=0x002C,
|
||||
Search_Files=0x002D, # SearchForFiles
|
||||
MM_SHUFFLE=0x002E,
|
||||
Sleep=0x002F, # also known as StandBySet
|
||||
BrowserStop=0x0030,
|
||||
OneTouchSync=0x0031,
|
||||
ZoomSet=0x0032,
|
||||
ZoomBtnInSet2=0x0033,
|
||||
ZoomBtnInSet=0x0034,
|
||||
ZoomBtnOutSet2=0x0035,
|
||||
ZoomBtnOutSet=0x0036,
|
||||
ZoomBtnResetSet=0x0037,
|
||||
Left_Click=0x0038, # LeftClick
|
||||
Right_Click=0x0039, # RightClick
|
||||
Mouse_Middle_Button=0x003A, # from M510v2 was MiddleMouseButton
|
||||
Back=0x003B,
|
||||
Mouse_Back_Button=0x003C, # from M510v2 was BackEx
|
||||
BrowserForward=0x003D,
|
||||
Mouse_Forward_Button=0x003E, # from M510v2 was BrowserForwardEx
|
||||
Mouse_Scroll_Left_Button_=0x003F, # from M510v2 was HorzScrollLeftSet
|
||||
Mouse_Scroll_Right_Button=0x0040, # from M510v2 was HorzScrollRightSet
|
||||
QuickSwitch=0x0041,
|
||||
BatteryStatus=0x0042,
|
||||
Show_Desktop=0x0043, # ShowDesktop
|
||||
WindowsLock=0x0044,
|
||||
FileLauncher=0x0045,
|
||||
FolderLauncher=0x0046,
|
||||
GotoWebAddress=0x0047,
|
||||
GenericMouseButton=0x0048,
|
||||
KeystrokeAssignment=0x0049,
|
||||
LaunchProgram=0x004A,
|
||||
MinMaxWindow=0x004B,
|
||||
VOLUMEMUTE_NoOSD=0x004C,
|
||||
New=0x004D,
|
||||
Copy=0x004E,
|
||||
CruiseDown=0x004F,
|
||||
CruiseUp=0x0050,
|
||||
Cut=0x0051,
|
||||
Do_Nothing=0x0052,
|
||||
PageDown=0x0053,
|
||||
PageUp=0x0054,
|
||||
Paste=0x0055,
|
||||
SearchPicture=0x0056,
|
||||
Reply=0x0057,
|
||||
PhotoGallerySet=0x0058,
|
||||
MM_REWIND=0x0059,
|
||||
MM_FASTFORWARD=0x005A,
|
||||
Send=0x005B,
|
||||
ControlPanel=0x005C,
|
||||
UniversalScroll=0x005D,
|
||||
AutoScroll=0x005E,
|
||||
GenericButton=0x005F,
|
||||
MM_NEXT=0x0060,
|
||||
MM_PREVIOUS=0x0061,
|
||||
Do_Nothing_One=0x0062, # also known as Do_Nothing
|
||||
SnapLeft=0x0063,
|
||||
SnapRight=0x0064,
|
||||
WinMinRestore=0x0065,
|
||||
WinMaxRestore=0x0066,
|
||||
WinStretch=0x0067,
|
||||
SwitchMonitorLeft=0x0068,
|
||||
SwitchMonitorRight=0x0069,
|
||||
ShowPresentation=0x006A,
|
||||
ShowMobilityCenter=0x006B,
|
||||
HorzScrollNoRepeatSet=0x006C,
|
||||
TouchBackForwardHorzScroll=0x0077,
|
||||
MetroAppSwitch=0x0078,
|
||||
MetroAppBar=0x0079,
|
||||
MetroCharms=0x007A,
|
||||
Calculator_VKEY=0x007B, # also known as Calculator
|
||||
MetroSearch=0x007C,
|
||||
MetroStartScreen=0x0080,
|
||||
MetroShare=0x007D,
|
||||
MetroSettings=0x007E,
|
||||
MetroDevices=0x007F,
|
||||
MetroBackLeftHorz=0x0082,
|
||||
MetroForwRightHorz=0x0083,
|
||||
Win8_Back=0x0084, # also known as MetroCharms
|
||||
Win8_Forward=0x0085, # also known as AppSwitchBar
|
||||
Win8Charm_Appswitch_GifAnimation=0x0086,
|
||||
Win8BackHorzLeft=0x008B, # also known as Back
|
||||
Win8ForwardHorzRight=0x008C, # also known as BrowserForward
|
||||
MetroSearch2=0x0087,
|
||||
MetroShare2=0x0088,
|
||||
MetroSettings2=0x008A,
|
||||
MetroDevices2=0x0089,
|
||||
Win8MetroWin7Forward=0x008D, # also known as MetroStartScreen
|
||||
Win8ShowDesktopWin7Back=0x008E, # also known as ShowDesktop
|
||||
MetroApplicationSwitch=0x0090, # also known as MetroStartScreen
|
||||
ShowUI=0x0092,
|
||||
# https://docs.google.com/document/d/1Dpx_nWRQAZox_zpZ8SNc9nOkSDE9svjkghOCbzopabc/edit
|
||||
# Extract to csv. Eliminate extra linefeeds and spaces. Turn / into __ and space into _
|
||||
# awk -F, '/0x/{gsub(" \\+ ","_",$2); gsub("_-","_Down",$2); gsub("_\\+","_Up",$2); gsub("[()\"-]","",$2); gsub(" ","_",$2); printf("\t%s=0x%04X,\n", $2, $1)}' < tasks.csv > tasks.py
|
||||
Switch_Presentation__Switch_Screen=0x0093, # on K400 Plus
|
||||
Minimize_Window=0x0094,
|
||||
Maximize_Window=0x0095, # on K400 Plus
|
||||
MultiPlatform_App_Switch=0x0096,
|
||||
MultiPlatform_Home=0x0097,
|
||||
MultiPlatform_Menu=0x0098,
|
||||
MultiPlatform_Back=0x0099,
|
||||
Switch_Language=0x009A, # Mac_switch_language
|
||||
Screen_Capture=0x009B, # Mac_screen_Capture, on Craft Keyboard
|
||||
Gesture_Button=0x009C,
|
||||
Smart_Shift=0x009D,
|
||||
AppExpose=0x009E,
|
||||
Smart_Zoom=0x009F,
|
||||
Lookup=0x00A0,
|
||||
Microphone_on__off=0x00A1,
|
||||
Wifi_on__off=0x00A2,
|
||||
Brightness_Down=0x00A3,
|
||||
Brightness_Up=0x00A4,
|
||||
Display_Out=0x00A5,
|
||||
View_Open_Apps=0x00A6,
|
||||
View_All_Open_Apps=0x00A7,
|
||||
AppSwitch=0x00A8,
|
||||
Gesture_Button_Navigation=0x00A9, # Mouse_Thumb_Button on MX Master
|
||||
Fn_inversion=0x00AA,
|
||||
Multiplatform_Back=0x00AB,
|
||||
Multiplatform_Forward=0x00AC,
|
||||
Multiplatform_Gesture_Button=0x00AD,
|
||||
HostSwitch_Channel_1=0x00AE,
|
||||
HostSwitch_Channel_2=0x00AF,
|
||||
HostSwitch_Channel_3=0x00B0,
|
||||
Multiplatform_Search=0x00B1,
|
||||
Multiplatform_Home__Mission_Control=0x00B2,
|
||||
Multiplatform_Menu__Launchpad=0x00B3,
|
||||
Virtual_Gesture_Button=0x00B4,
|
||||
Cursor=0x00B5,
|
||||
Keyboard_Right_Arrow=0x00B6,
|
||||
SW_Custom_Highlight=0x00B7,
|
||||
Keyboard_Left_Arrow=0x00B8,
|
||||
TBD=0x00B9,
|
||||
Multiplatform_Language_Switch=0x00BA,
|
||||
SW_Custom_Highlight_2=0x00BB,
|
||||
Fast_Forward=0x00BC,
|
||||
Fast_Backward=0x00BD,
|
||||
Switch_Highlighting=0x00BE,
|
||||
Mission_Control__Task_View=0x00BF, # Switch_Workspace on Craft Keyboard
|
||||
Dashboard_Launchpad__Action_Center=0x00C0, # Application_Launcher on Craft Keyboard
|
||||
Backlight_Down=0x00C1, # Backlight_Down_FW_internal_function
|
||||
Backlight_Up=0x00C2, # Backlight_Up_FW_internal_function
|
||||
Right_Click__App_Contextual_Menu=0x00C3, # Context_Menu on Craft Keyboard
|
||||
DPI_Change=0x00C4,
|
||||
New_Tab=0x00C5,
|
||||
F2=0x00C6,
|
||||
F3=0x00C7,
|
||||
F4=0x00C8,
|
||||
F5=0x00C9,
|
||||
F6=0x00CA,
|
||||
F7=0x00CB,
|
||||
F8=0x00CC,
|
||||
F1=0x00CD,
|
||||
Laser_Button=0x00CE,
|
||||
Laser_Button_Long_Press=0x00CF,
|
||||
Start_Presentation=0x00D0,
|
||||
Blank_Screen=0x00D1,
|
||||
DPI_Switch=0x00D2, # AdjustDPI on MX Vertical
|
||||
Home__Show_Desktop=0x00D3,
|
||||
App_Switch__Dashboard=0x00D4,
|
||||
App_Switch=0x00D5,
|
||||
Fn_Inversion=0x00D6,
|
||||
LeftAndRightClick=0x00D7,
|
||||
LedToggle=0x00DD, #
|
||||
# Both 0x001E and 0x001F are known as MediaCenterSet
|
||||
Media_Center_Logitech=0x001E,
|
||||
Media_Center_Microsoft=0x001F,
|
||||
UserMenu=0x0020,
|
||||
Messenger=0x0021,
|
||||
PersonalFolders=0x0022,
|
||||
MyMusic=0x0023,
|
||||
Webcam=0x0024,
|
||||
PicturesFolder=0x0025,
|
||||
MyVideos=0x0026,
|
||||
My_Computer=0x0027,
|
||||
PictureAppSet=0x0028,
|
||||
Search=0x0029, # also known as AdvSmartSearch
|
||||
RecordMediaPlayer=0x002A,
|
||||
BrowserRefresh=0x002B,
|
||||
RotateRight=0x002C,
|
||||
Search_Files=0x002D, # SearchForFiles
|
||||
MM_SHUFFLE=0x002E,
|
||||
Sleep=0x002F, # also known as StandBySet
|
||||
BrowserStop=0x0030,
|
||||
OneTouchSync=0x0031,
|
||||
ZoomSet=0x0032,
|
||||
ZoomBtnInSet2=0x0033,
|
||||
ZoomBtnInSet=0x0034,
|
||||
ZoomBtnOutSet2=0x0035,
|
||||
ZoomBtnOutSet=0x0036,
|
||||
ZoomBtnResetSet=0x0037,
|
||||
Left_Click=0x0038, # LeftClick
|
||||
Right_Click=0x0039, # RightClick
|
||||
Mouse_Middle_Button=0x003A, # from M510v2 was MiddleMouseButton
|
||||
Back=0x003B,
|
||||
Mouse_Back_Button=0x003C, # from M510v2 was BackEx
|
||||
BrowserForward=0x003D,
|
||||
Mouse_Forward_Button=0x003E, # from M510v2 was BrowserForwardEx
|
||||
Mouse_Scroll_Left_Button_=0x003F, # from M510v2 was HorzScrollLeftSet
|
||||
Mouse_Scroll_Right_Button=0x0040, # from M510v2 was HorzScrollRightSet
|
||||
QuickSwitch=0x0041,
|
||||
BatteryStatus=0x0042,
|
||||
Show_Desktop=0x0043, # ShowDesktop
|
||||
WindowsLock=0x0044,
|
||||
FileLauncher=0x0045,
|
||||
FolderLauncher=0x0046,
|
||||
GotoWebAddress=0x0047,
|
||||
GenericMouseButton=0x0048,
|
||||
KeystrokeAssignment=0x0049,
|
||||
LaunchProgram=0x004A,
|
||||
MinMaxWindow=0x004B,
|
||||
VOLUMEMUTE_NoOSD=0x004C,
|
||||
New=0x004D,
|
||||
Copy=0x004E,
|
||||
CruiseDown=0x004F,
|
||||
CruiseUp=0x0050,
|
||||
Cut=0x0051,
|
||||
Do_Nothing=0x0052,
|
||||
PageDown=0x0053,
|
||||
PageUp=0x0054,
|
||||
Paste=0x0055,
|
||||
SearchPicture=0x0056,
|
||||
Reply=0x0057,
|
||||
PhotoGallerySet=0x0058,
|
||||
MM_REWIND=0x0059,
|
||||
MM_FASTFORWARD=0x005A,
|
||||
Send=0x005B,
|
||||
ControlPanel=0x005C,
|
||||
UniversalScroll=0x005D,
|
||||
AutoScroll=0x005E,
|
||||
GenericButton=0x005F,
|
||||
MM_NEXT=0x0060,
|
||||
MM_PREVIOUS=0x0061,
|
||||
Do_Nothing_One=0x0062, # also known as Do_Nothing
|
||||
SnapLeft=0x0063,
|
||||
SnapRight=0x0064,
|
||||
WinMinRestore=0x0065,
|
||||
WinMaxRestore=0x0066,
|
||||
WinStretch=0x0067,
|
||||
SwitchMonitorLeft=0x0068,
|
||||
SwitchMonitorRight=0x0069,
|
||||
ShowPresentation=0x006A,
|
||||
ShowMobilityCenter=0x006B,
|
||||
HorzScrollNoRepeatSet=0x006C,
|
||||
TouchBackForwardHorzScroll=0x0077,
|
||||
MetroAppSwitch=0x0078,
|
||||
MetroAppBar=0x0079,
|
||||
MetroCharms=0x007A,
|
||||
Calculator_VKEY=0x007B, # also known as Calculator
|
||||
MetroSearch=0x007C,
|
||||
MetroStartScreen=0x0080,
|
||||
MetroShare=0x007D,
|
||||
MetroSettings=0x007E,
|
||||
MetroDevices=0x007F,
|
||||
MetroBackLeftHorz=0x0082,
|
||||
MetroForwRightHorz=0x0083,
|
||||
Win8_Back=0x0084, # also known as MetroCharms
|
||||
Win8_Forward=0x0085, # also known as AppSwitchBar
|
||||
Win8Charm_Appswitch_GifAnimation=0x0086,
|
||||
Win8BackHorzLeft=0x008B, # also known as Back
|
||||
Win8ForwardHorzRight=0x008C, # also known as BrowserForward
|
||||
MetroSearch2=0x0087,
|
||||
MetroShare2=0x0088,
|
||||
MetroSettings2=0x008A,
|
||||
MetroDevices2=0x0089,
|
||||
Win8MetroWin7Forward=0x008D, # also known as MetroStartScreen
|
||||
Win8ShowDesktopWin7Back=0x008E, # also known as ShowDesktop
|
||||
MetroApplicationSwitch=0x0090, # also known as MetroStartScreen
|
||||
ShowUI=0x0092,
|
||||
# https://docs.google.com/document/d/1Dpx_nWRQAZox_zpZ8SNc9nOkSDE9svjkghOCbzopabc/edit
|
||||
# Extract to csv. Eliminate extra linefeeds and spaces. Turn / into __ and space into _
|
||||
# awk -F, '/0x/{gsub(" \\+ ","_",$2); gsub("_-","_Down",$2); gsub("_\\+","_Up",$2); gsub("[()\"-]","",$2); gsub(" ","_",$2); printf("\t%s=0x%04X,\n", $2, $1)}' < tasks.csv > tasks.py
|
||||
Switch_Presentation__Switch_Screen=0x0093, # on K400 Plus
|
||||
Minimize_Window=0x0094,
|
||||
Maximize_Window=0x0095, # on K400 Plus
|
||||
MultiPlatform_App_Switch=0x0096,
|
||||
MultiPlatform_Home=0x0097,
|
||||
MultiPlatform_Menu=0x0098,
|
||||
MultiPlatform_Back=0x0099,
|
||||
Switch_Language=0x009A, # Mac_switch_language
|
||||
Screen_Capture=0x009B, # Mac_screen_Capture, on Craft Keyboard
|
||||
Gesture_Button=0x009C,
|
||||
Smart_Shift=0x009D,
|
||||
AppExpose=0x009E,
|
||||
Smart_Zoom=0x009F,
|
||||
Lookup=0x00A0,
|
||||
Microphone_on__off=0x00A1,
|
||||
Wifi_on__off=0x00A2,
|
||||
Brightness_Down=0x00A3,
|
||||
Brightness_Up=0x00A4,
|
||||
Display_Out=0x00A5,
|
||||
View_Open_Apps=0x00A6,
|
||||
View_All_Open_Apps=0x00A7,
|
||||
AppSwitch=0x00A8,
|
||||
Gesture_Button_Navigation=0x00A9, # Mouse_Thumb_Button on MX Master
|
||||
Fn_inversion=0x00AA,
|
||||
Multiplatform_Back=0x00AB,
|
||||
Multiplatform_Forward=0x00AC,
|
||||
Multiplatform_Gesture_Button=0x00AD,
|
||||
HostSwitch_Channel_1=0x00AE,
|
||||
HostSwitch_Channel_2=0x00AF,
|
||||
HostSwitch_Channel_3=0x00B0,
|
||||
Multiplatform_Search=0x00B1,
|
||||
Multiplatform_Home__Mission_Control=0x00B2,
|
||||
Multiplatform_Menu__Launchpad=0x00B3,
|
||||
Virtual_Gesture_Button=0x00B4,
|
||||
Cursor=0x00B5,
|
||||
Keyboard_Right_Arrow=0x00B6,
|
||||
SW_Custom_Highlight=0x00B7,
|
||||
Keyboard_Left_Arrow=0x00B8,
|
||||
TBD=0x00B9,
|
||||
Multiplatform_Language_Switch=0x00BA,
|
||||
SW_Custom_Highlight_2=0x00BB,
|
||||
Fast_Forward=0x00BC,
|
||||
Fast_Backward=0x00BD,
|
||||
Switch_Highlighting=0x00BE,
|
||||
Mission_Control__Task_View=0x00BF, # Switch_Workspace on Craft Keyboard
|
||||
Dashboard_Launchpad__Action_Center=
|
||||
0x00C0, # Application_Launcher on Craft Keyboard
|
||||
Backlight_Down=0x00C1, # Backlight_Down_FW_internal_function
|
||||
Backlight_Up=0x00C2, # Backlight_Up_FW_internal_function
|
||||
Right_Click__App_Contextual_Menu=0x00C3, # Context_Menu on Craft Keyboard
|
||||
DPI_Change=0x00C4,
|
||||
New_Tab=0x00C5,
|
||||
F2=0x00C6,
|
||||
F3=0x00C7,
|
||||
F4=0x00C8,
|
||||
F5=0x00C9,
|
||||
F6=0x00CA,
|
||||
F7=0x00CB,
|
||||
F8=0x00CC,
|
||||
F1=0x00CD,
|
||||
Laser_Button=0x00CE,
|
||||
Laser_Button_Long_Press=0x00CF,
|
||||
Start_Presentation=0x00D0,
|
||||
Blank_Screen=0x00D1,
|
||||
DPI_Switch=0x00D2, # AdjustDPI on MX Vertical
|
||||
Home__Show_Desktop=0x00D3,
|
||||
App_Switch__Dashboard=0x00D4,
|
||||
App_Switch=0x00D5,
|
||||
Fn_Inversion=0x00D6,
|
||||
LeftAndRightClick=0x00D7,
|
||||
LedToggle=0x00DD, #
|
||||
)
|
||||
TASK._fallback = lambda x: 'unknown:%04X' % x
|
||||
# hidpp 4.5 info from https://lekensteyn.nl/files/logitech/x1b04_specialkeysmsebuttons.html
|
||||
KEY_FLAG = _NamedInts(
|
||||
virtual=0x80,
|
||||
persistently_divertable=0x40,
|
||||
divertable=0x20,
|
||||
reprogrammable=0x10,
|
||||
FN_sensitive=0x08,
|
||||
nonstandard=0x04,
|
||||
is_FN=0x02,
|
||||
mse=0x01
|
||||
)
|
||||
KEY_FLAG = _NamedInts(virtual=0x80,
|
||||
persistently_divertable=0x40,
|
||||
divertable=0x20,
|
||||
reprogrammable=0x10,
|
||||
FN_sensitive=0x08,
|
||||
nonstandard=0x04,
|
||||
is_FN=0x02,
|
||||
mse=0x01)
|
||||
|
||||
DISABLE = _NamedInts(
|
||||
Caps_Lock=0x01,
|
||||
Num_Lock=0x02,
|
||||
Scroll_Lock=0x04,
|
||||
Insert=0x08,
|
||||
Win=0x10, # aka Super
|
||||
Caps_Lock=0x01,
|
||||
Num_Lock=0x02,
|
||||
Scroll_Lock=0x04,
|
||||
Insert=0x08,
|
||||
Win=0x10, # aka Super
|
||||
)
|
||||
DISABLE._fallback = lambda x: 'unknown:%02X' % x
|
||||
|
|
|
|||
|
|
@ -25,7 +25,6 @@ from logging import getLogger, DEBUG as _DEBUG
|
|||
_log = getLogger(__name__)
|
||||
del getLogger
|
||||
|
||||
|
||||
from .i18n import _, ngettext
|
||||
from .common import NamedInts as _NamedInts, NamedInt as _NamedInt
|
||||
from . import hidpp10 as _hidpp10
|
||||
|
|
@ -37,19 +36,23 @@ _R = _hidpp10.REGISTERS
|
|||
#
|
||||
#
|
||||
|
||||
ALERT = _NamedInts(NONE=0x00, NOTIFICATION=0x01, SHOW_WINDOW=0x02, ATTENTION=0x04, ALL=0xFF)
|
||||
ALERT = _NamedInts(NONE=0x00,
|
||||
NOTIFICATION=0x01,
|
||||
SHOW_WINDOW=0x02,
|
||||
ATTENTION=0x04,
|
||||
ALL=0xFF)
|
||||
|
||||
KEYS = _NamedInts(
|
||||
BATTERY_LEVEL=1,
|
||||
BATTERY_CHARGING=2,
|
||||
BATTERY_STATUS=3,
|
||||
LIGHT_LEVEL=4,
|
||||
LINK_ENCRYPTED=5,
|
||||
NOTIFICATION_FLAGS=6,
|
||||
ERROR=7,
|
||||
BATTERY_NEXT_LEVEL=8,
|
||||
BATTERY_VOLTAGE=9,
|
||||
)
|
||||
BATTERY_LEVEL=1,
|
||||
BATTERY_CHARGING=2,
|
||||
BATTERY_STATUS=3,
|
||||
LIGHT_LEVEL=4,
|
||||
LINK_ENCRYPTED=5,
|
||||
NOTIFICATION_FLAGS=6,
|
||||
ERROR=7,
|
||||
BATTERY_NEXT_LEVEL=8,
|
||||
BATTERY_VOLTAGE=9,
|
||||
)
|
||||
|
||||
# If the battery charge is under this percentage, trigger an attention event
|
||||
# (blink systray icon/notification/whatever).
|
||||
|
|
@ -64,286 +67,331 @@ _LONG_SLEEP = 15 * 60 # seconds
|
|||
#
|
||||
#
|
||||
|
||||
|
||||
def attach_to(device, changed_callback):
|
||||
assert device
|
||||
assert changed_callback
|
||||
assert device
|
||||
assert changed_callback
|
||||
|
||||
if not hasattr(device, 'status') or device.status is None:
|
||||
if device.kind is None:
|
||||
device.status = ReceiverStatus(device, changed_callback)
|
||||
else:
|
||||
device.status = DeviceStatus(device, changed_callback)
|
||||
|
||||
if not hasattr(device, 'status') or device.status is None:
|
||||
if device.kind is None:
|
||||
device.status = ReceiverStatus(device, changed_callback)
|
||||
else:
|
||||
device.status = DeviceStatus(device, changed_callback)
|
||||
|
||||
#
|
||||
#
|
||||
#
|
||||
|
||||
|
||||
class ReceiverStatus(dict):
|
||||
"""The 'runtime' status of a receiver, mostly about the pairing process --
|
||||
"""The 'runtime' status of a receiver, mostly about the pairing process --
|
||||
is the pairing lock open or closed, any pairing errors, etc.
|
||||
"""
|
||||
def __init__(self, receiver, changed_callback):
|
||||
assert receiver
|
||||
self._receiver = receiver
|
||||
def __init__(self, receiver, changed_callback):
|
||||
assert receiver
|
||||
self._receiver = receiver
|
||||
|
||||
assert changed_callback
|
||||
self._changed_callback = changed_callback
|
||||
assert changed_callback
|
||||
self._changed_callback = changed_callback
|
||||
|
||||
# self.updated = 0
|
||||
# self.updated = 0
|
||||
|
||||
self.lock_open = False
|
||||
self.new_device = None
|
||||
self.lock_open = False
|
||||
self.new_device = None
|
||||
|
||||
self[KEYS.ERROR] = None
|
||||
self[KEYS.ERROR] = None
|
||||
|
||||
def __str__(self):
|
||||
count = len(self._receiver)
|
||||
return (_("No paired devices.") if count == 0 else
|
||||
ngettext("%(count)s paired device.", "%(count)s paired devices.", count) % { 'count': count })
|
||||
__unicode__ = __str__
|
||||
def __str__(self):
|
||||
count = len(self._receiver)
|
||||
return (_("No paired devices.") if count == 0 else ngettext(
|
||||
"%(count)s paired device.", "%(count)s paired devices.", count) % {
|
||||
'count': count
|
||||
})
|
||||
|
||||
def changed(self, alert=ALERT.NOTIFICATION, reason=None):
|
||||
# self.updated = _timestamp()
|
||||
self._changed_callback(self._receiver, alert=alert, reason=reason)
|
||||
__unicode__ = __str__
|
||||
|
||||
def changed(self, alert=ALERT.NOTIFICATION, reason=None):
|
||||
# self.updated = _timestamp()
|
||||
self._changed_callback(self._receiver, alert=alert, reason=reason)
|
||||
|
||||
# def poll(self, timestamp):
|
||||
# r = self._receiver
|
||||
# assert r
|
||||
#
|
||||
# if _log.isEnabledFor(_DEBUG):
|
||||
# _log.debug("polling status of %s", r)
|
||||
#
|
||||
# # make sure to read some stuff that may be read later by the UI
|
||||
# r.serial, r.firmware, None
|
||||
#
|
||||
# # get an update of the notification flags
|
||||
# # self[KEYS.NOTIFICATION_FLAGS] = _hidpp10.get_notification_flags(r)
|
||||
|
||||
# def poll(self, timestamp):
|
||||
# r = self._receiver
|
||||
# assert r
|
||||
#
|
||||
# if _log.isEnabledFor(_DEBUG):
|
||||
# _log.debug("polling status of %s", r)
|
||||
#
|
||||
# # make sure to read some stuff that may be read later by the UI
|
||||
# r.serial, r.firmware, None
|
||||
#
|
||||
# # get an update of the notification flags
|
||||
# # self[KEYS.NOTIFICATION_FLAGS] = _hidpp10.get_notification_flags(r)
|
||||
|
||||
#
|
||||
#
|
||||
#
|
||||
|
||||
|
||||
class DeviceStatus(dict):
|
||||
"""Holds the 'runtime' status of a peripheral -- things like
|
||||
"""Holds the 'runtime' status of a peripheral -- things like
|
||||
active/inactive, battery charge, lux, etc. It updates them mostly by
|
||||
processing incoming notification events from the device itself.
|
||||
"""
|
||||
def __init__(self, device, changed_callback):
|
||||
assert device
|
||||
self._device = device
|
||||
def __init__(self, device, changed_callback):
|
||||
assert device
|
||||
self._device = device
|
||||
|
||||
assert changed_callback
|
||||
self._changed_callback = changed_callback
|
||||
assert changed_callback
|
||||
self._changed_callback = changed_callback
|
||||
|
||||
# is the device active?
|
||||
self._active = None
|
||||
# is the device active?
|
||||
self._active = None
|
||||
|
||||
# timestamp of when this status object was last updated
|
||||
self.updated = 0
|
||||
# timestamp of when this status object was last updated
|
||||
self.updated = 0
|
||||
|
||||
def to_string(self):
|
||||
def _items():
|
||||
comma = False
|
||||
def to_string(self):
|
||||
def _items():
|
||||
comma = False
|
||||
|
||||
battery_level = self.get(KEYS.BATTERY_LEVEL)
|
||||
if battery_level is not None:
|
||||
if isinstance(battery_level, _NamedInt):
|
||||
yield _("Battery: %(level)s") % { 'level': _(str(battery_level)) }
|
||||
else:
|
||||
yield _("Battery: %(percent)d%%") % { 'percent': battery_level }
|
||||
battery_level = self.get(KEYS.BATTERY_LEVEL)
|
||||
if battery_level is not None:
|
||||
if isinstance(battery_level, _NamedInt):
|
||||
yield _("Battery: %(level)s") % {
|
||||
'level': _(str(battery_level))
|
||||
}
|
||||
else:
|
||||
yield _("Battery: %(percent)d%%") % {
|
||||
'percent': battery_level
|
||||
}
|
||||
|
||||
battery_status = self.get(KEYS.BATTERY_STATUS)
|
||||
if battery_status is not None:
|
||||
yield ' (%s)' % _(str(battery_status))
|
||||
battery_status = self.get(KEYS.BATTERY_STATUS)
|
||||
if battery_status is not None:
|
||||
yield ' (%s)' % _(str(battery_status))
|
||||
|
||||
comma = True
|
||||
comma = True
|
||||
|
||||
light_level = self.get(KEYS.LIGHT_LEVEL)
|
||||
if light_level is not None:
|
||||
if comma: yield ', '
|
||||
yield _("Lighting: %(level)s lux") % { 'level': light_level }
|
||||
light_level = self.get(KEYS.LIGHT_LEVEL)
|
||||
if light_level is not None:
|
||||
if comma: yield ', '
|
||||
yield _("Lighting: %(level)s lux") % {'level': light_level}
|
||||
|
||||
return ''.join(i for i in _items())
|
||||
return ''.join(i for i in _items())
|
||||
|
||||
def __repr__(self):
|
||||
return '{' + ', '.join('\'%s\': %r' % (k, v) for k, v in self.items()) + '}'
|
||||
def __repr__(self):
|
||||
return '{' + ', '.join('\'%s\': %r' % (k, v)
|
||||
for k, v in self.items()) + '}'
|
||||
|
||||
def __bool__(self):
|
||||
return bool(self._active)
|
||||
__nonzero__ = __bool__
|
||||
def __bool__(self):
|
||||
return bool(self._active)
|
||||
|
||||
def set_battery_info(self, level, status, nextLevel=None, voltage=None, timestamp=None):
|
||||
if _log.isEnabledFor(_DEBUG):
|
||||
_log.debug("%s: battery %s, %s", self._device, level, status)
|
||||
__nonzero__ = __bool__
|
||||
|
||||
if level is None:
|
||||
# Some notifications may come with no battery level info, just
|
||||
# charging state info, so do our best to infer a level (even if it is just the last level)
|
||||
# It is not always possible to do this well
|
||||
if status == _hidpp20.BATTERY_STATUS.full:
|
||||
level = _hidpp10.BATTERY_APPOX.full
|
||||
elif status in (_hidpp20.BATTERY_STATUS.almost_full, _hidpp20.BATTERY_STATUS.recharging):
|
||||
level = _hidpp10.BATTERY_APPOX.good
|
||||
elif status == _hidpp20.BATTERY_STATUS.slow_recharge:
|
||||
level = _hidpp10.BATTERY_APPOX.low
|
||||
else:
|
||||
level = self.get(KEYS.BATTERY_LEVEL)
|
||||
else:
|
||||
assert isinstance(level, int)
|
||||
def set_battery_info(self,
|
||||
level,
|
||||
status,
|
||||
nextLevel=None,
|
||||
voltage=None,
|
||||
timestamp=None):
|
||||
if _log.isEnabledFor(_DEBUG):
|
||||
_log.debug("%s: battery %s, %s", self._device, level, status)
|
||||
|
||||
# TODO: this is also executed when pressing Fn+F7 on K800.
|
||||
old_level, self[KEYS.BATTERY_LEVEL] = self.get(KEYS.BATTERY_LEVEL), level
|
||||
old_status, self[KEYS.BATTERY_STATUS] = self.get(KEYS.BATTERY_STATUS), status
|
||||
self[KEYS.BATTERY_NEXT_LEVEL] = nextLevel
|
||||
if voltage is not None:
|
||||
self[KEYS.BATTERY_VOLTAGE] = voltage
|
||||
if level is None:
|
||||
# Some notifications may come with no battery level info, just
|
||||
# charging state info, so do our best to infer a level (even if it is just the last level)
|
||||
# It is not always possible to do this well
|
||||
if status == _hidpp20.BATTERY_STATUS.full:
|
||||
level = _hidpp10.BATTERY_APPOX.full
|
||||
elif status in (_hidpp20.BATTERY_STATUS.almost_full,
|
||||
_hidpp20.BATTERY_STATUS.recharging):
|
||||
level = _hidpp10.BATTERY_APPOX.good
|
||||
elif status == _hidpp20.BATTERY_STATUS.slow_recharge:
|
||||
level = _hidpp10.BATTERY_APPOX.low
|
||||
else:
|
||||
level = self.get(KEYS.BATTERY_LEVEL)
|
||||
else:
|
||||
assert isinstance(level, int)
|
||||
|
||||
charging = status in (_hidpp20.BATTERY_STATUS.recharging, _hidpp20.BATTERY_STATUS.almost_full,
|
||||
_hidpp20.BATTERY_STATUS.full, _hidpp20.BATTERY_STATUS.slow_recharge)
|
||||
old_charging, self[KEYS.BATTERY_CHARGING] = self.get(KEYS.BATTERY_CHARGING), charging
|
||||
# TODO: this is also executed when pressing Fn+F7 on K800.
|
||||
old_level, self[KEYS.BATTERY_LEVEL] = self.get(
|
||||
KEYS.BATTERY_LEVEL), level
|
||||
old_status, self[KEYS.BATTERY_STATUS] = self.get(
|
||||
KEYS.BATTERY_STATUS), status
|
||||
self[KEYS.BATTERY_NEXT_LEVEL] = nextLevel
|
||||
if voltage is not None:
|
||||
self[KEYS.BATTERY_VOLTAGE] = voltage
|
||||
|
||||
changed = old_level != level or old_status != status or old_charging != charging
|
||||
alert, reason = ALERT.NONE, None
|
||||
charging = status in (_hidpp20.BATTERY_STATUS.recharging,
|
||||
_hidpp20.BATTERY_STATUS.almost_full,
|
||||
_hidpp20.BATTERY_STATUS.full,
|
||||
_hidpp20.BATTERY_STATUS.slow_recharge)
|
||||
old_charging, self[KEYS.BATTERY_CHARGING] = self.get(
|
||||
KEYS.BATTERY_CHARGING), charging
|
||||
|
||||
if _hidpp20.BATTERY_OK(status) and ( level is None or level > _BATTERY_ATTENTION_LEVEL ):
|
||||
self[KEYS.ERROR] = None
|
||||
else:
|
||||
_log.warn("%s: battery %d%%, ALERT %s", self._device, level, status)
|
||||
if self.get(KEYS.ERROR) != status:
|
||||
self[KEYS.ERROR] = status
|
||||
# only show the notification once
|
||||
alert = ALERT.NOTIFICATION | ALERT.ATTENTION
|
||||
if isinstance(level, _NamedInt):
|
||||
reason = _("Battery: %(level)s (%(status)s)") % { 'level': _(level), 'status': _(status) }
|
||||
else:
|
||||
reason = _("Battery: %(percent)d%% (%(status)s)") % { 'percent': level, 'status': status.name }
|
||||
changed = old_level != level or old_status != status or old_charging != charging
|
||||
alert, reason = ALERT.NONE, None
|
||||
|
||||
if changed or reason:
|
||||
# update the leds on the device, if any
|
||||
_hidpp10.set_3leds(self._device, level, charging=charging, warning=bool(alert))
|
||||
self.changed(active=True, alert=alert, reason=reason, timestamp=timestamp)
|
||||
if _hidpp20.BATTERY_OK(status) and (level is None or
|
||||
level > _BATTERY_ATTENTION_LEVEL):
|
||||
self[KEYS.ERROR] = None
|
||||
else:
|
||||
_log.warn("%s: battery %d%%, ALERT %s", self._device, level,
|
||||
status)
|
||||
if self.get(KEYS.ERROR) != status:
|
||||
self[KEYS.ERROR] = status
|
||||
# only show the notification once
|
||||
alert = ALERT.NOTIFICATION | ALERT.ATTENTION
|
||||
if isinstance(level, _NamedInt):
|
||||
reason = _("Battery: %(level)s (%(status)s)") % {
|
||||
'level': _(level),
|
||||
'status': _(status)
|
||||
}
|
||||
else:
|
||||
reason = _("Battery: %(percent)d%% (%(status)s)") % {
|
||||
'percent': level,
|
||||
'status': status.name
|
||||
}
|
||||
|
||||
# Retrieve and regularize battery status
|
||||
def read_battery(self, timestamp=None):
|
||||
if self._active:
|
||||
d = self._device
|
||||
assert d
|
||||
if changed or reason:
|
||||
# update the leds on the device, if any
|
||||
_hidpp10.set_3leds(self._device,
|
||||
level,
|
||||
charging=charging,
|
||||
warning=bool(alert))
|
||||
self.changed(active=True,
|
||||
alert=alert,
|
||||
reason=reason,
|
||||
timestamp=timestamp)
|
||||
|
||||
if d.protocol < 2.0:
|
||||
battery = _hidpp10.get_battery(d)
|
||||
self.set_battery_keys(battery)
|
||||
return
|
||||
# Retrieve and regularize battery status
|
||||
def read_battery(self, timestamp=None):
|
||||
if self._active:
|
||||
d = self._device
|
||||
assert d
|
||||
|
||||
battery = _hidpp20.get_battery(d)
|
||||
if battery is None:
|
||||
v = _hidpp20.get_voltage(d)
|
||||
if v is not None:
|
||||
level, status, voltage, _ignore, _ignore = v
|
||||
self.set_battery_keys( (level, status, None), voltage)
|
||||
return
|
||||
if d.protocol < 2.0:
|
||||
battery = _hidpp10.get_battery(d)
|
||||
self.set_battery_keys(battery)
|
||||
return
|
||||
|
||||
# Really unnecessary, if the device has SOLAR_DASHBOARD it should be
|
||||
# broadcasting it's battery status anyway, it will just take a little while.
|
||||
# However, when the device has just been detected, it will not show
|
||||
# any battery status for a while (broadcasts happen every 90 seconds).
|
||||
if battery is None and d.features and _hidpp20.FEATURE.SOLAR_DASHBOARD in d.features:
|
||||
d.feature_request(_hidpp20.FEATURE.SOLAR_DASHBOARD, 0x00, 1, 1)
|
||||
return
|
||||
self.set_battery_keys(battery)
|
||||
battery = _hidpp20.get_battery(d)
|
||||
if battery is None:
|
||||
v = _hidpp20.get_voltage(d)
|
||||
if v is not None:
|
||||
level, status, voltage, _ignore, _ignore = v
|
||||
self.set_battery_keys((level, status, None), voltage)
|
||||
return
|
||||
|
||||
def set_battery_keys(self, battery, voltage=None) :
|
||||
if battery is not None:
|
||||
level, status, nextLevel = battery
|
||||
self.set_battery_info(level, status, nextLevel, voltage)
|
||||
elif KEYS.BATTERY_STATUS in self:
|
||||
self[KEYS.BATTERY_STATUS] = None
|
||||
self[KEYS.BATTERY_CHARGING] = None
|
||||
self.changed()
|
||||
# Really unnecessary, if the device has SOLAR_DASHBOARD it should be
|
||||
# broadcasting it's battery status anyway, it will just take a little while.
|
||||
# However, when the device has just been detected, it will not show
|
||||
# any battery status for a while (broadcasts happen every 90 seconds).
|
||||
if battery is None and d.features and _hidpp20.FEATURE.SOLAR_DASHBOARD in d.features:
|
||||
d.feature_request(_hidpp20.FEATURE.SOLAR_DASHBOARD, 0x00, 1, 1)
|
||||
return
|
||||
self.set_battery_keys(battery)
|
||||
|
||||
def changed(self, active=None, alert=ALERT.NONE, reason=None, timestamp=None):
|
||||
assert self._changed_callback
|
||||
d = self._device
|
||||
# assert d # may be invalid when processing the 'unpaired' notification
|
||||
timestamp = timestamp or _timestamp()
|
||||
def set_battery_keys(self, battery, voltage=None):
|
||||
if battery is not None:
|
||||
level, status, nextLevel = battery
|
||||
self.set_battery_info(level, status, nextLevel, voltage)
|
||||
elif KEYS.BATTERY_STATUS in self:
|
||||
self[KEYS.BATTERY_STATUS] = None
|
||||
self[KEYS.BATTERY_CHARGING] = None
|
||||
self.changed()
|
||||
|
||||
if active is not None:
|
||||
d.online = active
|
||||
was_active, self._active = self._active, active
|
||||
if active:
|
||||
if not was_active:
|
||||
# Make sure to set notification flags on the device, they
|
||||
# get cleared when the device is turned off (but not when the device
|
||||
# goes idle, and we can't tell the difference right now).
|
||||
if d.protocol < 2.0:
|
||||
self[KEYS.NOTIFICATION_FLAGS] = d.enable_notifications()
|
||||
def changed(self,
|
||||
active=None,
|
||||
alert=ALERT.NONE,
|
||||
reason=None,
|
||||
timestamp=None):
|
||||
assert self._changed_callback
|
||||
d = self._device
|
||||
# assert d # may be invalid when processing the 'unpaired' notification
|
||||
timestamp = timestamp or _timestamp()
|
||||
|
||||
# If we've been inactive for a long time, forget anything
|
||||
# about the battery.
|
||||
if self.updated > 0 and timestamp - self.updated > _LONG_SLEEP:
|
||||
self[KEYS.BATTERY_LEVEL] = None
|
||||
self[KEYS.BATTERY_STATUS] = None
|
||||
self[KEYS.BATTERY_CHARGING] = None
|
||||
if active is not None:
|
||||
d.online = active
|
||||
was_active, self._active = self._active, active
|
||||
if active:
|
||||
if not was_active:
|
||||
# Make sure to set notification flags on the device, they
|
||||
# get cleared when the device is turned off (but not when the device
|
||||
# goes idle, and we can't tell the difference right now).
|
||||
if d.protocol < 2.0:
|
||||
self[KEYS.NOTIFICATION_FLAGS] = d.enable_notifications(
|
||||
)
|
||||
|
||||
# Devices lose configuration when they are turned off,
|
||||
# make sure they're up-to-date.
|
||||
if _log.isEnabledFor(_DEBUG):
|
||||
_log.debug("%s pushing device settings %s", d, d.settings)
|
||||
for s in d.settings:
|
||||
s.apply()
|
||||
# If we've been inactive for a long time, forget anything
|
||||
# about the battery.
|
||||
if self.updated > 0 and timestamp - self.updated > _LONG_SLEEP:
|
||||
self[KEYS.BATTERY_LEVEL] = None
|
||||
self[KEYS.BATTERY_STATUS] = None
|
||||
self[KEYS.BATTERY_CHARGING] = None
|
||||
|
||||
if self.get(KEYS.BATTERY_LEVEL) is None:
|
||||
self.read_battery(timestamp)
|
||||
else:
|
||||
if was_active:
|
||||
battery = self.get(KEYS.BATTERY_LEVEL)
|
||||
self.clear()
|
||||
# If we had a known battery level before, assume it's not going
|
||||
# to change much while the device is offline.
|
||||
if battery is not None:
|
||||
self[KEYS.BATTERY_LEVEL] = battery
|
||||
# Devices lose configuration when they are turned off,
|
||||
# make sure they're up-to-date.
|
||||
if _log.isEnabledFor(_DEBUG):
|
||||
_log.debug("%s pushing device settings %s", d,
|
||||
d.settings)
|
||||
for s in d.settings:
|
||||
s.apply()
|
||||
|
||||
if self.updated == 0 and active == True:
|
||||
# if the device is active on the very first status notification,
|
||||
# (meaning just when the program started or a new receiver was just
|
||||
# detected), pop-up a notification about it
|
||||
alert |= ALERT.NOTIFICATION
|
||||
self.updated = timestamp
|
||||
if self.get(KEYS.BATTERY_LEVEL) is None:
|
||||
self.read_battery(timestamp)
|
||||
else:
|
||||
if was_active:
|
||||
battery = self.get(KEYS.BATTERY_LEVEL)
|
||||
self.clear()
|
||||
# If we had a known battery level before, assume it's not going
|
||||
# to change much while the device is offline.
|
||||
if battery is not None:
|
||||
self[KEYS.BATTERY_LEVEL] = battery
|
||||
|
||||
# if _log.isEnabledFor(_DEBUG):
|
||||
# _log.debug("device %d changed: active=%s %s", d.number, self._active, dict(self))
|
||||
self._changed_callback(d, alert, reason)
|
||||
if self.updated == 0 and active == True:
|
||||
# if the device is active on the very first status notification,
|
||||
# (meaning just when the program started or a new receiver was just
|
||||
# detected), pop-up a notification about it
|
||||
alert |= ALERT.NOTIFICATION
|
||||
self.updated = timestamp
|
||||
|
||||
# def poll(self, timestamp):
|
||||
# d = self._device
|
||||
# if not d:
|
||||
# _log.error("polling status of invalid device")
|
||||
# return
|
||||
#
|
||||
# if self._active:
|
||||
# if _log.isEnabledFor(_DEBUG):
|
||||
# _log.debug("polling status of %s", d)
|
||||
#
|
||||
# # read these from the device, the UI may need them later
|
||||
# d.protocol, d.serial, d.firmware, d.kind, d.name, d.settings, None
|
||||
#
|
||||
# # make sure we know all the features of the device
|
||||
# # if d.features:
|
||||
# # d.features[:]
|
||||
#
|
||||
# # devices may go out-of-range while still active, or the computer
|
||||
# # may go to sleep and wake up without the devices available
|
||||
# if timestamp - self.updated > _STATUS_TIMEOUT:
|
||||
# if d.ping():
|
||||
# timestamp = self.updated = _timestamp()
|
||||
# else:
|
||||
# self.changed(active=False, reason='out of range')
|
||||
#
|
||||
# # if still active, make sure we know the battery level
|
||||
# if KEYS.BATTERY_LEVEL not in self:
|
||||
# self.read_battery(timestamp)
|
||||
#
|
||||
# elif timestamp - self.updated > _STATUS_TIMEOUT:
|
||||
# if d.ping():
|
||||
# self.changed(active=True)
|
||||
# else:
|
||||
# self.updated = _timestamp()
|
||||
# if _log.isEnabledFor(_DEBUG):
|
||||
# _log.debug("device %d changed: active=%s %s", d.number, self._active, dict(self))
|
||||
self._changed_callback(d, alert, reason)
|
||||
|
||||
# def poll(self, timestamp):
|
||||
# d = self._device
|
||||
# if not d:
|
||||
# _log.error("polling status of invalid device")
|
||||
# return
|
||||
#
|
||||
# if self._active:
|
||||
# if _log.isEnabledFor(_DEBUG):
|
||||
# _log.debug("polling status of %s", d)
|
||||
#
|
||||
# # read these from the device, the UI may need them later
|
||||
# d.protocol, d.serial, d.firmware, d.kind, d.name, d.settings, None
|
||||
#
|
||||
# # make sure we know all the features of the device
|
||||
# # if d.features:
|
||||
# # d.features[:]
|
||||
#
|
||||
# # devices may go out-of-range while still active, or the computer
|
||||
# # may go to sleep and wake up without the devices available
|
||||
# if timestamp - self.updated > _STATUS_TIMEOUT:
|
||||
# if d.ping():
|
||||
# timestamp = self.updated = _timestamp()
|
||||
# else:
|
||||
# self.changed(active=False, reason='out of range')
|
||||
#
|
||||
# # if still active, make sure we know the battery level
|
||||
# if KEYS.BATTERY_LEVEL not in self:
|
||||
# self.read_battery(timestamp)
|
||||
#
|
||||
# elif timestamp - self.updated > _STATUS_TIMEOUT:
|
||||
# if d.ping():
|
||||
# self.changed(active=True)
|
||||
# else:
|
||||
# self.updated = _timestamp()
|
||||
|
|
|
|||
|
|
@ -19,7 +19,6 @@
|
|||
|
||||
from __future__ import absolute_import, division, print_function, unicode_literals
|
||||
|
||||
|
||||
import argparse as _argparse
|
||||
import sys as _sys
|
||||
|
||||
|
|
@ -27,54 +26,76 @@ from logging import getLogger, DEBUG as _DEBUG
|
|||
_log = getLogger(__name__)
|
||||
del getLogger
|
||||
|
||||
|
||||
from solaar import NAME
|
||||
|
||||
#
|
||||
#
|
||||
#
|
||||
|
||||
|
||||
def _create_parser():
|
||||
parser = _argparse.ArgumentParser(prog=NAME.lower(), add_help=False,
|
||||
epilog='For details on individual actions, run `%s <action> --help`.' % NAME.lower())
|
||||
subparsers = parser.add_subparsers(title='actions',
|
||||
help='optional action to perform')
|
||||
parser = _argparse.ArgumentParser(
|
||||
prog=NAME.lower(),
|
||||
add_help=False,
|
||||
epilog='For details on individual actions, run `%s <action> --help`.' %
|
||||
NAME.lower())
|
||||
subparsers = parser.add_subparsers(title='actions',
|
||||
help='optional action to perform')
|
||||
|
||||
sp = subparsers.add_parser('show', help='show information about devices')
|
||||
sp.add_argument('device', nargs='?', default='all',
|
||||
help='device to show information about; may be a device number (1..6), a serial, '
|
||||
'a substring of a device\'s name, or "all" (the default)')
|
||||
sp.set_defaults(action='show')
|
||||
sp = subparsers.add_parser('show', help='show information about devices')
|
||||
sp.add_argument(
|
||||
'device',
|
||||
nargs='?',
|
||||
default='all',
|
||||
help=
|
||||
'device to show information about; may be a device number (1..6), a serial, '
|
||||
'a substring of a device\'s name, or "all" (the default)')
|
||||
sp.set_defaults(action='show')
|
||||
|
||||
sp = subparsers.add_parser('probe', help='probe a receiver (debugging use only)')
|
||||
sp.add_argument('receiver', nargs='?',
|
||||
help='select a certain receiver when more than one is present')
|
||||
sp.set_defaults(action='probe')
|
||||
sp = subparsers.add_parser('probe',
|
||||
help='probe a receiver (debugging use only)')
|
||||
sp.add_argument(
|
||||
'receiver',
|
||||
nargs='?',
|
||||
help='select a certain receiver when more than one is present')
|
||||
sp.set_defaults(action='probe')
|
||||
|
||||
sp = subparsers.add_parser('config', help='read/write device-specific settings',
|
||||
epilog='Please note that configuration only works on active devices.')
|
||||
sp.add_argument('device',
|
||||
help='device to configure; may be a device number (1..6), a device serial, '
|
||||
'or at least 3 characters of a device\'s name')
|
||||
sp.add_argument('setting', nargs='?',
|
||||
help='device-specific setting; leave empty to list available settings')
|
||||
sp.add_argument('value', nargs='?',
|
||||
help='new value for the setting')
|
||||
sp.set_defaults(action='config')
|
||||
sp = subparsers.add_parser(
|
||||
'config',
|
||||
help='read/write device-specific settings',
|
||||
epilog='Please note that configuration only works on active devices.')
|
||||
sp.add_argument(
|
||||
'device',
|
||||
help=
|
||||
'device to configure; may be a device number (1..6), a device serial, '
|
||||
'or at least 3 characters of a device\'s name')
|
||||
sp.add_argument(
|
||||
'setting',
|
||||
nargs='?',
|
||||
help='device-specific setting; leave empty to list available settings')
|
||||
sp.add_argument('value', nargs='?', help='new value for the setting')
|
||||
sp.set_defaults(action='config')
|
||||
|
||||
sp = subparsers.add_parser('pair', help='pair a new device',
|
||||
epilog='The Logitech Unifying Receiver supports up to 6 paired devices at the same time.')
|
||||
sp.add_argument('receiver', nargs='?',
|
||||
help='select a certain receiver when more than one is present')
|
||||
sp.set_defaults(action='pair')
|
||||
sp = subparsers.add_parser(
|
||||
'pair',
|
||||
help='pair a new device',
|
||||
epilog=
|
||||
'The Logitech Unifying Receiver supports up to 6 paired devices at the same time.'
|
||||
)
|
||||
sp.add_argument(
|
||||
'receiver',
|
||||
nargs='?',
|
||||
help='select a certain receiver when more than one is present')
|
||||
sp.set_defaults(action='pair')
|
||||
|
||||
sp = subparsers.add_parser('unpair', help='unpair a device')
|
||||
sp.add_argument('device',
|
||||
help='device to unpair; may be a device number (1..6), a serial, '
|
||||
'or a substring of a device\'s name.')
|
||||
sp.set_defaults(action='unpair')
|
||||
sp = subparsers.add_parser('unpair', help='unpair a device')
|
||||
sp.add_argument(
|
||||
'device',
|
||||
help='device to unpair; may be a device number (1..6), a serial, '
|
||||
'or a substring of a device\'s name.')
|
||||
sp.set_defaults(action='unpair')
|
||||
|
||||
return parser, subparsers.choices
|
||||
return parser, subparsers.choices
|
||||
|
||||
|
||||
_cli_parser, actions = _create_parser()
|
||||
|
|
@ -82,88 +103,89 @@ print_help = _cli_parser.print_help
|
|||
|
||||
|
||||
def _receivers(dev_path=None):
|
||||
from logitech_receiver import Receiver
|
||||
from logitech_receiver.base import receivers
|
||||
for dev_info in receivers():
|
||||
if dev_path is not None and dev_path != dev_info.path:
|
||||
continue
|
||||
try:
|
||||
r = Receiver.open(dev_info)
|
||||
if _log.isEnabledFor(_DEBUG):
|
||||
_log.debug("[%s] => %s", dev_info.path, r)
|
||||
if r:
|
||||
yield r
|
||||
except Exception as e:
|
||||
_log.exception('opening ' + str(dev_info))
|
||||
_sys.exit("%s: error: %s" % (NAME, str(e)))
|
||||
from logitech_receiver import Receiver
|
||||
from logitech_receiver.base import receivers
|
||||
for dev_info in receivers():
|
||||
if dev_path is not None and dev_path != dev_info.path:
|
||||
continue
|
||||
try:
|
||||
r = Receiver.open(dev_info)
|
||||
if _log.isEnabledFor(_DEBUG):
|
||||
_log.debug("[%s] => %s", dev_info.path, r)
|
||||
if r:
|
||||
yield r
|
||||
except Exception as e:
|
||||
_log.exception('opening ' + str(dev_info))
|
||||
_sys.exit("%s: error: %s" % (NAME, str(e)))
|
||||
|
||||
|
||||
def _find_receiver(receivers, name):
|
||||
assert receivers
|
||||
assert name
|
||||
assert receivers
|
||||
assert name
|
||||
|
||||
for r in receivers:
|
||||
if name in r.name.lower() or (r.serial is not None and name == r.serial.lower()):
|
||||
return r
|
||||
for r in receivers:
|
||||
if name in r.name.lower() or (r.serial is not None
|
||||
and name == r.serial.lower()):
|
||||
return r
|
||||
|
||||
|
||||
def _find_device(receivers, name):
|
||||
assert receivers
|
||||
assert name
|
||||
assert receivers
|
||||
assert name
|
||||
|
||||
number = None
|
||||
if len(name) == 1:
|
||||
try:
|
||||
number = int(name)
|
||||
except:
|
||||
pass
|
||||
else:
|
||||
assert not (number < 0)
|
||||
if number > 6: number = None
|
||||
number = None
|
||||
if len(name) == 1:
|
||||
try:
|
||||
number = int(name)
|
||||
except:
|
||||
pass
|
||||
else:
|
||||
assert not (number < 0)
|
||||
if number > 6: number = None
|
||||
|
||||
for r in receivers:
|
||||
if number and number <= r.max_devices:
|
||||
dev = r[number]
|
||||
if dev:
|
||||
return dev
|
||||
for r in receivers:
|
||||
if number and number <= r.max_devices:
|
||||
dev = r[number]
|
||||
if dev:
|
||||
return dev
|
||||
|
||||
for dev in r:
|
||||
if (name == dev.serial.lower() or
|
||||
name == dev.codename.lower() or
|
||||
name == str(dev.kind).lower() or
|
||||
name in dev.name.lower()):
|
||||
return dev
|
||||
for dev in r:
|
||||
if (name == dev.serial.lower() or name == dev.codename.lower()
|
||||
or name == str(dev.kind).lower()
|
||||
or name in dev.name.lower()):
|
||||
return dev
|
||||
|
||||
raise Exception("no device found matching '%s'" % name)
|
||||
raise Exception("no device found matching '%s'" % name)
|
||||
|
||||
|
||||
def run(cli_args=None, hidraw_path=None):
|
||||
if cli_args:
|
||||
action = cli_args[0]
|
||||
args = _cli_parser.parse_args(cli_args)
|
||||
else:
|
||||
args = _cli_parser.parse_args()
|
||||
# Python 3 has an undocumented 'feature' that breaks parsing empty args
|
||||
# http://bugs.python.org/issue16308
|
||||
if not 'cmd' in args:
|
||||
_cli_parser.print_usage(_sys.stderr)
|
||||
_sys.stderr.write('%s: error: too few arguments\n' % NAME.lower())
|
||||
_sys.exit(2)
|
||||
action = args.action
|
||||
assert action in actions
|
||||
if cli_args:
|
||||
action = cli_args[0]
|
||||
args = _cli_parser.parse_args(cli_args)
|
||||
else:
|
||||
args = _cli_parser.parse_args()
|
||||
# Python 3 has an undocumented 'feature' that breaks parsing empty args
|
||||
# http://bugs.python.org/issue16308
|
||||
if not 'cmd' in args:
|
||||
_cli_parser.print_usage(_sys.stderr)
|
||||
_sys.stderr.write('%s: error: too few arguments\n' % NAME.lower())
|
||||
_sys.exit(2)
|
||||
action = args.action
|
||||
assert action in actions
|
||||
|
||||
try:
|
||||
c = list(_receivers(hidraw_path))
|
||||
if not c:
|
||||
raise Exception('Logitech receiver not found')
|
||||
try:
|
||||
c = list(_receivers(hidraw_path))
|
||||
if not c:
|
||||
raise Exception('Logitech receiver not found')
|
||||
|
||||
from importlib import import_module
|
||||
m = import_module('.' + action, package=__name__)
|
||||
m.run(c, args, _find_receiver, _find_device)
|
||||
except AssertionError as e:
|
||||
from traceback import extract_tb
|
||||
tb_last = extract_tb(_sys.exc_info()[2])[-1]
|
||||
_sys.exit('%s: assertion failed: %s line %d' % (NAME.lower(), tb_last[0], tb_last[1]))
|
||||
except Exception as e:
|
||||
from traceback import format_exc
|
||||
_sys.exit('%s: error: %s' % (NAME.lower(), format_exc()))
|
||||
from importlib import import_module
|
||||
m = import_module('.' + action, package=__name__)
|
||||
m.run(c, args, _find_receiver, _find_device)
|
||||
except AssertionError as e:
|
||||
from traceback import extract_tb
|
||||
tb_last = extract_tb(_sys.exc_info()[2])[-1]
|
||||
_sys.exit('%s: assertion failed: %s line %d' %
|
||||
(NAME.lower(), tb_last[0], tb_last[1]))
|
||||
except Exception as e:
|
||||
from traceback import format_exc
|
||||
_sys.exit('%s: error: %s' % (NAME.lower(), format_exc()))
|
||||
|
|
|
|||
|
|
@ -19,109 +19,118 @@
|
|||
|
||||
from __future__ import absolute_import, division, print_function, unicode_literals
|
||||
|
||||
|
||||
from solaar import configuration as _configuration
|
||||
from logitech_receiver import settings as _settings
|
||||
|
||||
|
||||
def _print_setting(s, verbose=True):
|
||||
print ('#', s.label)
|
||||
if verbose:
|
||||
if s.description:
|
||||
print ('#', s.description.replace('\n', ' '))
|
||||
if s.kind == _settings.KIND.toggle:
|
||||
print ('# possible values: on/true/t/yes/y/1 or off/false/f/no/n/0')
|
||||
elif s.choices:
|
||||
print ('# possible values: one of [', ', '.join(str(v) for v in s.choices), '], or higher/lower/highest/max/lowest/min')
|
||||
else:
|
||||
# wtf?
|
||||
pass
|
||||
value = s.read(cached=False)
|
||||
if value is None:
|
||||
print (s.name, '= ? (failed to read from device)')
|
||||
else:
|
||||
print (s.name, '= %r' % value)
|
||||
print('#', s.label)
|
||||
if verbose:
|
||||
if s.description:
|
||||
print('#', s.description.replace('\n', ' '))
|
||||
if s.kind == _settings.KIND.toggle:
|
||||
print(
|
||||
'# possible values: on/true/t/yes/y/1 or off/false/f/no/n/0')
|
||||
elif s.choices:
|
||||
print('# possible values: one of [',
|
||||
', '.join(str(v) for v in s.choices),
|
||||
'], or higher/lower/highest/max/lowest/min')
|
||||
else:
|
||||
# wtf?
|
||||
pass
|
||||
value = s.read(cached=False)
|
||||
if value is None:
|
||||
print(s.name, '= ? (failed to read from device)')
|
||||
else:
|
||||
print(s.name, '= %r' % value)
|
||||
|
||||
|
||||
def run(receivers, args, find_receiver, find_device):
|
||||
assert receivers
|
||||
assert args.device
|
||||
assert receivers
|
||||
assert args.device
|
||||
|
||||
device_name = args.device.lower()
|
||||
dev = find_device(receivers, device_name)
|
||||
device_name = args.device.lower()
|
||||
dev = find_device(receivers, device_name)
|
||||
|
||||
if not dev.ping():
|
||||
raise Exception('%s is offline' % dev.name)
|
||||
if not dev.ping():
|
||||
raise Exception('%s is offline' % dev.name)
|
||||
|
||||
if not dev.settings:
|
||||
raise Exception('no settings for %s' % dev.name)
|
||||
if not dev.settings:
|
||||
raise Exception('no settings for %s' % dev.name)
|
||||
|
||||
_configuration.attach_to(dev)
|
||||
_configuration.attach_to(dev)
|
||||
|
||||
if not args.setting:
|
||||
print (dev.name, '(%s) [%s:%s]' % (dev.codename, dev.wpid, dev.serial))
|
||||
for s in dev.settings:
|
||||
print ('')
|
||||
_print_setting(s)
|
||||
return
|
||||
if not args.setting:
|
||||
print(dev.name, '(%s) [%s:%s]' % (dev.codename, dev.wpid, dev.serial))
|
||||
for s in dev.settings:
|
||||
print('')
|
||||
_print_setting(s)
|
||||
return
|
||||
|
||||
setting_name = args.setting.lower()
|
||||
setting = None
|
||||
for s in dev.settings:
|
||||
if setting_name == s.name.lower():
|
||||
setting = s
|
||||
break
|
||||
if setting is None:
|
||||
raise Exception("no setting '%s' for %s" % (args.setting, dev.name))
|
||||
setting_name = args.setting.lower()
|
||||
setting = None
|
||||
for s in dev.settings:
|
||||
if setting_name == s.name.lower():
|
||||
setting = s
|
||||
break
|
||||
if setting is None:
|
||||
raise Exception("no setting '%s' for %s" % (args.setting, dev.name))
|
||||
|
||||
if args.value is None:
|
||||
_print_setting(setting)
|
||||
return
|
||||
if args.value is None:
|
||||
_print_setting(setting)
|
||||
return
|
||||
|
||||
if setting.kind == _settings.KIND.toggle:
|
||||
value = args.value
|
||||
try:
|
||||
value = bool(int(value))
|
||||
except:
|
||||
if value.lower() in ('true', 'yes', 'on', 't', 'y'):
|
||||
value = True
|
||||
elif value.lower() in ('false', 'no', 'off', 'f', 'n'):
|
||||
value = False
|
||||
else:
|
||||
raise Exception("don't know how to interpret '%s' as boolean" % value)
|
||||
if setting.kind == _settings.KIND.toggle:
|
||||
value = args.value
|
||||
try:
|
||||
value = bool(int(value))
|
||||
except:
|
||||
if value.lower() in ('true', 'yes', 'on', 't', 'y'):
|
||||
value = True
|
||||
elif value.lower() in ('false', 'no', 'off', 'f', 'n'):
|
||||
value = False
|
||||
else:
|
||||
raise Exception("don't know how to interpret '%s' as boolean" %
|
||||
value)
|
||||
|
||||
elif setting.choices:
|
||||
value = args.value.lower()
|
||||
elif setting.choices:
|
||||
value = args.value.lower()
|
||||
|
||||
if value in ('higher', 'lower'):
|
||||
old_value = setting.read()
|
||||
if old_value is None:
|
||||
raise Exception("could not read current value of '%s'" % setting.name)
|
||||
if value in ('higher', 'lower'):
|
||||
old_value = setting.read()
|
||||
if old_value is None:
|
||||
raise Exception("could not read current value of '%s'" %
|
||||
setting.name)
|
||||
|
||||
if value == 'lower':
|
||||
lower_values = setting.choices[:old_value]
|
||||
value = lower_values[-1] if lower_values else setting.choices[:][0]
|
||||
elif value == 'higher':
|
||||
higher_values = setting.choices[old_value + 1:]
|
||||
value = higher_values[0] if higher_values else setting.choices[:][-1]
|
||||
elif value in ('highest', 'max'):
|
||||
value = setting.choices[:][-1]
|
||||
elif value in ('lowest', 'min'):
|
||||
value = setting.choices[:][0]
|
||||
elif value not in setting.choices:
|
||||
raise Exception("possible values for '%s' are: [%s]" % (setting.name, ', '.join(str(v) for v in setting.choices)))
|
||||
value = setting.choices[value]
|
||||
if value == 'lower':
|
||||
lower_values = setting.choices[:old_value]
|
||||
value = lower_values[
|
||||
-1] if lower_values else setting.choices[:][0]
|
||||
elif value == 'higher':
|
||||
higher_values = setting.choices[old_value + 1:]
|
||||
value = higher_values[
|
||||
0] if higher_values else setting.choices[:][-1]
|
||||
elif value in ('highest', 'max'):
|
||||
value = setting.choices[:][-1]
|
||||
elif value in ('lowest', 'min'):
|
||||
value = setting.choices[:][0]
|
||||
elif value not in setting.choices:
|
||||
raise Exception(
|
||||
"possible values for '%s' are: [%s]" %
|
||||
(setting.name, ', '.join(str(v) for v in setting.choices)))
|
||||
value = setting.choices[value]
|
||||
|
||||
elif setting.kind == _settings.KIND.range:
|
||||
try:
|
||||
value = int(args.value)
|
||||
except ValueError:
|
||||
raise Exception("can't interpret '%s' as integer" % args.value)
|
||||
elif setting.kind == _settings.KIND.range:
|
||||
try:
|
||||
value = int(args.value)
|
||||
except ValueError:
|
||||
raise Exception("can't interpret '%s' as integer" % args.value)
|
||||
|
||||
else:
|
||||
raise Exception("NotImplemented")
|
||||
else:
|
||||
raise Exception("NotImplemented")
|
||||
|
||||
result = setting.write(value)
|
||||
if result is None:
|
||||
raise Exception("failed to set '%s' = '%s' [%r]" % (setting.name, str(value), value))
|
||||
_print_setting(setting, False)
|
||||
result = setting.write(value)
|
||||
if result is None:
|
||||
raise Exception("failed to set '%s' = '%s' [%r]" %
|
||||
(setting.name, str(value), value))
|
||||
_print_setting(setting, False)
|
||||
|
|
|
|||
|
|
@ -19,80 +19,85 @@
|
|||
|
||||
from __future__ import absolute_import, division, print_function, unicode_literals
|
||||
|
||||
|
||||
from time import time as _timestamp
|
||||
|
||||
from logitech_receiver import (
|
||||
base as _base,
|
||||
hidpp10 as _hidpp10,
|
||||
status as _status,
|
||||
notifications as _notifications,
|
||||
)
|
||||
base as _base,
|
||||
hidpp10 as _hidpp10,
|
||||
status as _status,
|
||||
notifications as _notifications,
|
||||
)
|
||||
|
||||
|
||||
def run(receivers, args, find_receiver, _ignore):
|
||||
assert receivers
|
||||
assert receivers
|
||||
|
||||
if args.receiver:
|
||||
receiver_name = args.receiver.lower()
|
||||
receiver = find_receiver(receiver_name)
|
||||
if not receiver:
|
||||
raise Exception("no receiver found matching '%s'" % receiver_name)
|
||||
else:
|
||||
receiver = receivers[0]
|
||||
if args.receiver:
|
||||
receiver_name = args.receiver.lower()
|
||||
receiver = find_receiver(receiver_name)
|
||||
if not receiver:
|
||||
raise Exception("no receiver found matching '%s'" % receiver_name)
|
||||
else:
|
||||
receiver = receivers[0]
|
||||
|
||||
assert receiver
|
||||
receiver.status = _status.ReceiverStatus(receiver, lambda *args, **kwargs: None)
|
||||
assert receiver
|
||||
receiver.status = _status.ReceiverStatus(receiver,
|
||||
lambda *args, **kwargs: None)
|
||||
|
||||
# check if it's necessary to set the notification flags
|
||||
old_notification_flags = _hidpp10.get_notification_flags(receiver) or 0
|
||||
if not (old_notification_flags & _hidpp10.NOTIFICATION_FLAG.wireless):
|
||||
_hidpp10.set_notification_flags(receiver, old_notification_flags | _hidpp10.NOTIFICATION_FLAG.wireless)
|
||||
# check if it's necessary to set the notification flags
|
||||
old_notification_flags = _hidpp10.get_notification_flags(receiver) or 0
|
||||
if not (old_notification_flags & _hidpp10.NOTIFICATION_FLAG.wireless):
|
||||
_hidpp10.set_notification_flags(
|
||||
receiver,
|
||||
old_notification_flags | _hidpp10.NOTIFICATION_FLAG.wireless)
|
||||
|
||||
# get all current devices
|
||||
known_devices = [dev.number for dev in receiver]
|
||||
# get all current devices
|
||||
known_devices = [dev.number for dev in receiver]
|
||||
|
||||
class _HandleWithNotificationHook(int):
|
||||
def notifications_hook(self, n):
|
||||
assert n
|
||||
if n.devnumber == 0xFF:
|
||||
_notifications.process(receiver, n)
|
||||
elif n.sub_id == 0x41: # allow for other protocols! (was and n.address == 0x04)
|
||||
if n.devnumber not in known_devices:
|
||||
receiver.status.new_device = receiver[n.devnumber]
|
||||
elif receiver.re_pairs:
|
||||
del receiver[n.devnumber] # get rid of information on device re-paired away
|
||||
receiver.status.new_device = receiver[n.devnumber]
|
||||
class _HandleWithNotificationHook(int):
|
||||
def notifications_hook(self, n):
|
||||
assert n
|
||||
if n.devnumber == 0xFF:
|
||||
_notifications.process(receiver, n)
|
||||
elif n.sub_id == 0x41: # allow for other protocols! (was and n.address == 0x04)
|
||||
if n.devnumber not in known_devices:
|
||||
receiver.status.new_device = receiver[n.devnumber]
|
||||
elif receiver.re_pairs:
|
||||
del receiver[
|
||||
n.
|
||||
devnumber] # get rid of information on device re-paired away
|
||||
receiver.status.new_device = receiver[n.devnumber]
|
||||
|
||||
timeout = 20 # seconds
|
||||
receiver.handle = _HandleWithNotificationHook(receiver.handle)
|
||||
timeout = 20 # seconds
|
||||
receiver.handle = _HandleWithNotificationHook(receiver.handle)
|
||||
|
||||
receiver.set_lock(False, timeout=timeout)
|
||||
print ('Pairing: turn your new device on (timing out in', timeout, 'seconds).')
|
||||
receiver.set_lock(False, timeout=timeout)
|
||||
print('Pairing: turn your new device on (timing out in', timeout,
|
||||
'seconds).')
|
||||
|
||||
# the lock-open notification may come slightly later, wait for it a bit
|
||||
pairing_start = _timestamp()
|
||||
patience = 5 # seconds
|
||||
# the lock-open notification may come slightly later, wait for it a bit
|
||||
pairing_start = _timestamp()
|
||||
patience = 5 # seconds
|
||||
|
||||
while receiver.status.lock_open or _timestamp() - pairing_start < patience:
|
||||
n = _base.read(receiver.handle)
|
||||
if n:
|
||||
n = _base.make_notification(*n)
|
||||
if n:
|
||||
receiver.handle.notifications_hook(n)
|
||||
while receiver.status.lock_open or _timestamp() - pairing_start < patience:
|
||||
n = _base.read(receiver.handle)
|
||||
if n:
|
||||
n = _base.make_notification(*n)
|
||||
if n:
|
||||
receiver.handle.notifications_hook(n)
|
||||
|
||||
if not (old_notification_flags & _hidpp10.NOTIFICATION_FLAG.wireless):
|
||||
# only clear the flags if they weren't set before, otherwise a
|
||||
# concurrently running Solaar app might stop working properly
|
||||
_hidpp10.set_notification_flags(receiver, old_notification_flags)
|
||||
|
||||
if receiver.status.new_device:
|
||||
dev = receiver.status.new_device
|
||||
print ('Paired device %d: %s (%s) [%s:%s]' % (dev.number, dev.name, dev.codename, dev.wpid, dev.serial))
|
||||
else:
|
||||
error = receiver.status.get(_status.KEYS.ERROR)
|
||||
if error :
|
||||
raise Exception("pairing failed: %s" % error)
|
||||
else :
|
||||
print ('Paired a device') # this is better than an error
|
||||
if not (old_notification_flags & _hidpp10.NOTIFICATION_FLAG.wireless):
|
||||
# only clear the flags if they weren't set before, otherwise a
|
||||
# concurrently running Solaar app might stop working properly
|
||||
_hidpp10.set_notification_flags(receiver, old_notification_flags)
|
||||
|
||||
if receiver.status.new_device:
|
||||
dev = receiver.status.new_device
|
||||
print('Paired device %d: %s (%s) [%s:%s]' %
|
||||
(dev.number, dev.name, dev.codename, dev.wpid, dev.serial))
|
||||
else:
|
||||
error = receiver.status.get(_status.KEYS.ERROR)
|
||||
if error:
|
||||
raise Exception("pairing failed: %s" % error)
|
||||
else:
|
||||
print('Paired a device') # this is better than an error
|
||||
|
|
|
|||
|
|
@ -19,52 +19,65 @@
|
|||
|
||||
from __future__ import absolute_import, division, print_function, unicode_literals
|
||||
|
||||
|
||||
from time import time as _timestamp
|
||||
|
||||
from logitech_receiver.common import strhex as _strhex
|
||||
|
||||
from logitech_receiver import (
|
||||
base as _base,
|
||||
hidpp10 as _hidpp10,
|
||||
status as _status,
|
||||
notifications as _notifications,
|
||||
)
|
||||
base as _base,
|
||||
hidpp10 as _hidpp10,
|
||||
status as _status,
|
||||
notifications as _notifications,
|
||||
)
|
||||
|
||||
_R = _hidpp10.REGISTERS
|
||||
|
||||
from solaar.cli.show import _print_receiver
|
||||
|
||||
|
||||
def run(receivers, args, find_receiver, _ignore):
|
||||
assert receivers
|
||||
assert receivers
|
||||
|
||||
if args.receiver:
|
||||
receiver_name = args.receiver.lower()
|
||||
receiver = find_receiver(receiver_name)
|
||||
if not receiver:
|
||||
raise Exception("no receiver found matching '%s'" % receiver_name)
|
||||
else:
|
||||
receiver = receivers[0]
|
||||
if args.receiver:
|
||||
receiver_name = args.receiver.lower()
|
||||
receiver = find_receiver(receiver_name)
|
||||
if not receiver:
|
||||
raise Exception("no receiver found matching '%s'" % receiver_name)
|
||||
else:
|
||||
receiver = receivers[0]
|
||||
|
||||
assert receiver
|
||||
assert receiver
|
||||
|
||||
_print_receiver(receiver)
|
||||
_print_receiver(receiver)
|
||||
|
||||
print (' Register Dump')
|
||||
register = receiver.read_register(_R.notifications)
|
||||
print(" Notification Register %#04x: %s" % (_R.notifications%0x100,'0x'+_strhex(register) if register else "None"))
|
||||
register = receiver.read_register(_R.receiver_connection)
|
||||
print(" Connection State %#04x: %s" % (_R.receiver_connection%0x100,'0x'+_strhex(register) if register else "None"))
|
||||
register = receiver.read_register(_R.devices_activity)
|
||||
print(" Device Activity %#04x: %s" % (_R.devices_activity%0x100,'0x'+_strhex(register) if register else "None"))
|
||||
print(' Register Dump')
|
||||
register = receiver.read_register(_R.notifications)
|
||||
print(" Notification Register %#04x: %s" %
|
||||
(_R.notifications % 0x100,
|
||||
'0x' + _strhex(register) if register else "None"))
|
||||
register = receiver.read_register(_R.receiver_connection)
|
||||
print(" Connection State %#04x: %s" %
|
||||
(_R.receiver_connection % 0x100,
|
||||
'0x' + _strhex(register) if register else "None"))
|
||||
register = receiver.read_register(_R.devices_activity)
|
||||
print(" Device Activity %#04x: %s" %
|
||||
(_R.devices_activity % 0x100,
|
||||
'0x' + _strhex(register) if register else "None"))
|
||||
|
||||
for device in range(0,6):
|
||||
for sub_reg in [ 0x0, 0x10, 0x20, 0x30 ] :
|
||||
register = receiver.read_register(_R.receiver_info, sub_reg + device)
|
||||
print(" Pairing Register %#04x %#04x: %s" % (_R.receiver_info%0x100,sub_reg + device,'0x'+_strhex(register) if register else "None"))
|
||||
register = receiver.read_register(_R.receiver_info, 0x40 + device)
|
||||
print(" Pairing Name %#04x %#02x: %s" % (_R.receiver_info%0x100,0x40 + device,register[2:2+ord(register[1:2])] if register else "None"))
|
||||
for device in range(0, 6):
|
||||
for sub_reg in [0x0, 0x10, 0x20, 0x30]:
|
||||
register = receiver.read_register(_R.receiver_info,
|
||||
sub_reg + device)
|
||||
print(" Pairing Register %#04x %#04x: %s" %
|
||||
(_R.receiver_info % 0x100, sub_reg + device,
|
||||
'0x' + _strhex(register) if register else "None"))
|
||||
register = receiver.read_register(_R.receiver_info, 0x40 + device)
|
||||
print(" Pairing Name %#04x %#02x: %s" %
|
||||
(_R.receiver_info % 0x100, 0x40 + device,
|
||||
register[2:2 + ord(register[1:2])] if register else "None"))
|
||||
|
||||
for sub_reg in range(0,5):
|
||||
register = receiver.read_register(_R.firmware, sub_reg)
|
||||
print(" Firmware %#04x %#04x: %s" % (_R.firmware%0x100,sub_reg,'0x'+_strhex(register) if register else "None"))
|
||||
for sub_reg in range(0, 5):
|
||||
register = receiver.read_register(_R.firmware, sub_reg)
|
||||
print(" Firmware %#04x %#04x: %s" %
|
||||
(_R.firmware % 0x100, sub_reg,
|
||||
'0x' + _strhex(register) if register else "None"))
|
||||
|
|
|
|||
|
|
@ -19,220 +19,244 @@
|
|||
|
||||
from __future__ import absolute_import, division, print_function, unicode_literals
|
||||
|
||||
|
||||
from logitech_receiver import (
|
||||
hidpp10 as _hidpp10,
|
||||
hidpp20 as _hidpp20,
|
||||
special_keys as _special_keys,
|
||||
settings_templates as _settings_templates
|
||||
)
|
||||
from logitech_receiver import (hidpp10 as _hidpp10, hidpp20 as _hidpp20,
|
||||
special_keys as _special_keys,
|
||||
settings_templates as _settings_templates)
|
||||
from logitech_receiver.common import NamedInt as _NamedInt
|
||||
|
||||
|
||||
def _print_receiver(receiver):
|
||||
paired_count = receiver.count()
|
||||
paired_count = receiver.count()
|
||||
|
||||
print (receiver.name)
|
||||
print (' Device path :', receiver.path)
|
||||
print (' USB id : 046d:%s' % receiver.product_id)
|
||||
print (' Serial :', receiver.serial)
|
||||
if receiver.firmware:
|
||||
for f in receiver.firmware:
|
||||
print (' %-11s: %s' % (f.kind, f.version))
|
||||
print(receiver.name)
|
||||
print(' Device path :', receiver.path)
|
||||
print(' USB id : 046d:%s' % receiver.product_id)
|
||||
print(' Serial :', receiver.serial)
|
||||
if receiver.firmware:
|
||||
for f in receiver.firmware:
|
||||
print(' %-11s: %s' % (f.kind, f.version))
|
||||
|
||||
print (' Has', paired_count, 'paired device(s) out of a maximum of %d.' % receiver.max_devices)
|
||||
if receiver.remaining_pairings() and receiver.remaining_pairings() >= 0 :
|
||||
print (' Has %d successful pairing(s) remaining.' % receiver.remaining_pairings() )
|
||||
print(' Has', paired_count,
|
||||
'paired device(s) out of a maximum of %d.' % receiver.max_devices)
|
||||
if receiver.remaining_pairings() and receiver.remaining_pairings() >= 0:
|
||||
print(' Has %d successful pairing(s) remaining.' %
|
||||
receiver.remaining_pairings())
|
||||
|
||||
notification_flags = _hidpp10.get_notification_flags(receiver)
|
||||
if notification_flags is not None:
|
||||
if notification_flags:
|
||||
notification_names = _hidpp10.NOTIFICATION_FLAG.flag_names(notification_flags)
|
||||
print (' Notifications: %s (0x%06X)' % (', '.join(notification_names), notification_flags))
|
||||
else:
|
||||
print (' Notifications: (none)')
|
||||
notification_flags = _hidpp10.get_notification_flags(receiver)
|
||||
if notification_flags is not None:
|
||||
if notification_flags:
|
||||
notification_names = _hidpp10.NOTIFICATION_FLAG.flag_names(
|
||||
notification_flags)
|
||||
print(' Notifications: %s (0x%06X)' %
|
||||
(', '.join(notification_names), notification_flags))
|
||||
else:
|
||||
print(' Notifications: (none)')
|
||||
|
||||
activity = receiver.read_register(_hidpp10.REGISTERS.devices_activity)
|
||||
if activity:
|
||||
activity = [(d, ord(activity[d - 1:d])) for d in range(1, receiver.max_devices)]
|
||||
activity_text = ', '.join(('%d=%d' % (d, a)) for d, a in activity if a > 0)
|
||||
print (' Device activity counters:', activity_text or '(empty)')
|
||||
activity = receiver.read_register(_hidpp10.REGISTERS.devices_activity)
|
||||
if activity:
|
||||
activity = [(d, ord(activity[d - 1:d]))
|
||||
for d in range(1, receiver.max_devices)]
|
||||
activity_text = ', '.join(
|
||||
('%d=%d' % (d, a)) for d, a in activity if a > 0)
|
||||
print(' Device activity counters:', activity_text or '(empty)')
|
||||
|
||||
|
||||
def _battery_text(level):
|
||||
if level is None:
|
||||
return 'N/A'
|
||||
elif isinstance(level, _NamedInt):
|
||||
return str(level)
|
||||
else:
|
||||
return '%d%%' % level
|
||||
|
||||
def _battery_text(level) :
|
||||
if level is None:
|
||||
return 'N/A'
|
||||
elif isinstance(level, _NamedInt):
|
||||
return str(level)
|
||||
else:
|
||||
return '%d%%' % level
|
||||
|
||||
def _print_device(dev):
|
||||
assert dev
|
||||
# check if the device is online
|
||||
dev.ping()
|
||||
assert dev
|
||||
# check if the device is online
|
||||
dev.ping()
|
||||
|
||||
print (' %d: %s' % (dev.number, dev.name))
|
||||
print (' Codename :', dev.codename)
|
||||
print (' Kind :', dev.kind)
|
||||
print (' Wireless PID :', dev.wpid)
|
||||
if dev.protocol:
|
||||
print (' Protocol : HID++ %1.1f' % dev.protocol)
|
||||
else:
|
||||
print (' Protocol : unknown (device is offline)')
|
||||
if dev.polling_rate:
|
||||
print (' Polling rate :', dev.polling_rate, 'ms (%dHz)' % (1000 // dev.polling_rate))
|
||||
print (' Serial number:', dev.serial)
|
||||
if dev.firmware:
|
||||
for fw in dev.firmware:
|
||||
print (' %11s:' % fw.kind, (fw.name + ' ' + fw.version).strip())
|
||||
print(' %d: %s' % (dev.number, dev.name))
|
||||
print(' Codename :', dev.codename)
|
||||
print(' Kind :', dev.kind)
|
||||
print(' Wireless PID :', dev.wpid)
|
||||
if dev.protocol:
|
||||
print(' Protocol : HID++ %1.1f' % dev.protocol)
|
||||
else:
|
||||
print(' Protocol : unknown (device is offline)')
|
||||
if dev.polling_rate:
|
||||
print(' Polling rate :', dev.polling_rate,
|
||||
'ms (%dHz)' % (1000 // dev.polling_rate))
|
||||
print(' Serial number:', dev.serial)
|
||||
if dev.firmware:
|
||||
for fw in dev.firmware:
|
||||
print(' %11s:' % fw.kind,
|
||||
(fw.name + ' ' + fw.version).strip())
|
||||
|
||||
if dev.power_switch_location:
|
||||
print (' The power switch is located on the %s.' % dev.power_switch_location)
|
||||
if dev.power_switch_location:
|
||||
print(' The power switch is located on the %s.' %
|
||||
dev.power_switch_location)
|
||||
|
||||
if dev.online:
|
||||
notification_flags = _hidpp10.get_notification_flags(dev)
|
||||
if notification_flags is not None:
|
||||
if notification_flags:
|
||||
notification_names = _hidpp10.NOTIFICATION_FLAG.flag_names(notification_flags)
|
||||
print (' Notifications: %s (0x%06X).' % (', '.join(notification_names), notification_flags))
|
||||
else:
|
||||
print (' Notifications: (none).')
|
||||
if dev.online:
|
||||
notification_flags = _hidpp10.get_notification_flags(dev)
|
||||
if notification_flags is not None:
|
||||
if notification_flags:
|
||||
notification_names = _hidpp10.NOTIFICATION_FLAG.flag_names(
|
||||
notification_flags)
|
||||
print(' Notifications: %s (0x%06X).' %
|
||||
(', '.join(notification_names), notification_flags))
|
||||
else:
|
||||
print(' Notifications: (none).')
|
||||
|
||||
if dev.online and dev.features:
|
||||
print (' Supports %d HID++ 2.0 features:' % len(dev.features))
|
||||
dev.persister = None # Give the device a fake persister
|
||||
dev_settings = []
|
||||
_settings_templates.check_feature_settings(dev, dev_settings)
|
||||
for index, feature in enumerate(dev.features):
|
||||
feature = dev.features[index]
|
||||
flags = dev.request(0x0000, feature.bytes(2))
|
||||
flags = 0 if flags is None else ord(flags[1:2])
|
||||
flags = _hidpp20.FEATURE_FLAG.flag_names(flags)
|
||||
print (' %2d: %-22s {%04X} %s' % (index, feature, feature, ', '.join(flags)))
|
||||
if feature == _hidpp20.FEATURE.HIRES_WHEEL:
|
||||
wheel = _hidpp20.get_hires_wheel(dev)
|
||||
if wheel:
|
||||
multi, has_invert, has_switch, inv, res, target, ratchet = wheel
|
||||
print(" Multiplier: %s" % multi)
|
||||
if has_invert:
|
||||
print(" Has invert")
|
||||
if inv:
|
||||
print(" Inverse wheel motion")
|
||||
else:
|
||||
print(" Normal wheel motion")
|
||||
if has_switch:
|
||||
print(" Has ratchet switch")
|
||||
if ratchet:
|
||||
print(" Normal wheel mode")
|
||||
else:
|
||||
print(" Free wheel mode")
|
||||
if res:
|
||||
print(" High resolution mode")
|
||||
else:
|
||||
print(" Low resolution mode")
|
||||
if target:
|
||||
print(" HID++ notification")
|
||||
else:
|
||||
print(" HID notification")
|
||||
elif feature == _hidpp20.FEATURE.MOUSE_POINTER:
|
||||
mouse_pointer = _hidpp20.get_mouse_pointer_info(dev)
|
||||
if mouse_pointer:
|
||||
print(" DPI: %s" % mouse_pointer['dpi'])
|
||||
print(" Acceleration: %s" % mouse_pointer['acceleration'])
|
||||
if mouse_pointer['suggest_os_ballistics']:
|
||||
print(" Use OS ballistics")
|
||||
else:
|
||||
print(" Override OS ballistics")
|
||||
if mouse_pointer['suggest_vertical_orientation']:
|
||||
print(" Provide vertical tuning, trackball")
|
||||
else:
|
||||
print(" No vertical tuning, standard mice")
|
||||
if feature == _hidpp20.FEATURE.VERTICAL_SCROLLING:
|
||||
vertical_scrolling_info = _hidpp20.get_vertical_scrolling_info(dev)
|
||||
if vertical_scrolling_info:
|
||||
print(" Roller type: %s" % vertical_scrolling_info['roller'])
|
||||
print(" Ratchet per turn: %s" % vertical_scrolling_info['ratchet'])
|
||||
print(" Scroll lines: %s" % vertical_scrolling_info['lines'])
|
||||
elif feature == _hidpp20.FEATURE.HI_RES_SCROLLING:
|
||||
scrolling_mode, scrolling_resolution = _hidpp20.get_hi_res_scrolling_info(dev)
|
||||
if scrolling_mode:
|
||||
print(" Hi-res scrolling enabled")
|
||||
else:
|
||||
print(" Hi-res scrolling disabled")
|
||||
if scrolling_resolution:
|
||||
print(" Hi-res scrolling multiplier: %s" % scrolling_resolution)
|
||||
elif feature == _hidpp20.FEATURE.POINTER_SPEED:
|
||||
pointer_speed = _hidpp20.get_pointer_speed_info(dev)
|
||||
if pointer_speed:
|
||||
print(" Pointer Speed: %s" % pointer_speed)
|
||||
elif feature == _hidpp20.FEATURE.LOWRES_WHEEL:
|
||||
wheel_status = _hidpp20.get_lowres_wheel_status(dev)
|
||||
if wheel_status:
|
||||
print(" Wheel Reports: %s" % wheel_status)
|
||||
elif feature == _hidpp20.FEATURE.NEW_FN_INVERSION:
|
||||
inverted, default_inverted = _hidpp20.get_new_fn_inversion(dev)
|
||||
print(" Fn-swap:", "enabled" if inverted else "disabled")
|
||||
print(" Fn-swap default:", "enabled" if default_inverted else "disabled")
|
||||
for setting in dev_settings:
|
||||
if setting.feature == feature:
|
||||
v = setting.read(False)
|
||||
print(" %s: %s" % (setting.label, v) )
|
||||
if dev.online and dev.features:
|
||||
print(' Supports %d HID++ 2.0 features:' % len(dev.features))
|
||||
dev.persister = None # Give the device a fake persister
|
||||
dev_settings = []
|
||||
_settings_templates.check_feature_settings(dev, dev_settings)
|
||||
for index, feature in enumerate(dev.features):
|
||||
feature = dev.features[index]
|
||||
flags = dev.request(0x0000, feature.bytes(2))
|
||||
flags = 0 if flags is None else ord(flags[1:2])
|
||||
flags = _hidpp20.FEATURE_FLAG.flag_names(flags)
|
||||
print(' %2d: %-22s {%04X} %s' %
|
||||
(index, feature, feature, ', '.join(flags)))
|
||||
if feature == _hidpp20.FEATURE.HIRES_WHEEL:
|
||||
wheel = _hidpp20.get_hires_wheel(dev)
|
||||
if wheel:
|
||||
multi, has_invert, has_switch, inv, res, target, ratchet = wheel
|
||||
print(" Multiplier: %s" % multi)
|
||||
if has_invert:
|
||||
print(" Has invert")
|
||||
if inv:
|
||||
print(" Inverse wheel motion")
|
||||
else:
|
||||
print(" Normal wheel motion")
|
||||
if has_switch:
|
||||
print(" Has ratchet switch")
|
||||
if ratchet:
|
||||
print(" Normal wheel mode")
|
||||
else:
|
||||
print(" Free wheel mode")
|
||||
if res:
|
||||
print(" High resolution mode")
|
||||
else:
|
||||
print(" Low resolution mode")
|
||||
if target:
|
||||
print(" HID++ notification")
|
||||
else:
|
||||
print(" HID notification")
|
||||
elif feature == _hidpp20.FEATURE.MOUSE_POINTER:
|
||||
mouse_pointer = _hidpp20.get_mouse_pointer_info(dev)
|
||||
if mouse_pointer:
|
||||
print(" DPI: %s" % mouse_pointer['dpi'])
|
||||
print(" Acceleration: %s" %
|
||||
mouse_pointer['acceleration'])
|
||||
if mouse_pointer['suggest_os_ballistics']:
|
||||
print(" Use OS ballistics")
|
||||
else:
|
||||
print(" Override OS ballistics")
|
||||
if mouse_pointer['suggest_vertical_orientation']:
|
||||
print(" Provide vertical tuning, trackball")
|
||||
else:
|
||||
print(" No vertical tuning, standard mice")
|
||||
if feature == _hidpp20.FEATURE.VERTICAL_SCROLLING:
|
||||
vertical_scrolling_info = _hidpp20.get_vertical_scrolling_info(
|
||||
dev)
|
||||
if vertical_scrolling_info:
|
||||
print(" Roller type: %s" %
|
||||
vertical_scrolling_info['roller'])
|
||||
print(" Ratchet per turn: %s" %
|
||||
vertical_scrolling_info['ratchet'])
|
||||
print(" Scroll lines: %s" %
|
||||
vertical_scrolling_info['lines'])
|
||||
elif feature == _hidpp20.FEATURE.HI_RES_SCROLLING:
|
||||
scrolling_mode, scrolling_resolution = _hidpp20.get_hi_res_scrolling_info(
|
||||
dev)
|
||||
if scrolling_mode:
|
||||
print(" Hi-res scrolling enabled")
|
||||
else:
|
||||
print(" Hi-res scrolling disabled")
|
||||
if scrolling_resolution:
|
||||
print(" Hi-res scrolling multiplier: %s" %
|
||||
scrolling_resolution)
|
||||
elif feature == _hidpp20.FEATURE.POINTER_SPEED:
|
||||
pointer_speed = _hidpp20.get_pointer_speed_info(dev)
|
||||
if pointer_speed:
|
||||
print(" Pointer Speed: %s" % pointer_speed)
|
||||
elif feature == _hidpp20.FEATURE.LOWRES_WHEEL:
|
||||
wheel_status = _hidpp20.get_lowres_wheel_status(dev)
|
||||
if wheel_status:
|
||||
print(" Wheel Reports: %s" % wheel_status)
|
||||
elif feature == _hidpp20.FEATURE.NEW_FN_INVERSION:
|
||||
inverted, default_inverted = _hidpp20.get_new_fn_inversion(dev)
|
||||
print(" Fn-swap:",
|
||||
"enabled" if inverted else "disabled")
|
||||
print(" Fn-swap default:",
|
||||
"enabled" if default_inverted else "disabled")
|
||||
for setting in dev_settings:
|
||||
if setting.feature == feature:
|
||||
v = setting.read(False)
|
||||
print(" %s: %s" % (setting.label, v))
|
||||
|
||||
if dev.online and dev.keys:
|
||||
print (' Has %d reprogrammable keys:' % len(dev.keys))
|
||||
for k in dev.keys:
|
||||
flags = _special_keys.KEY_FLAG.flag_names(k.flags)
|
||||
# TODO: add here additional variants for other REPROG_CONTROLS
|
||||
if dev.keys.keyversion == 1:
|
||||
print (' %2d: %-26s => %-27s %s' % (k.index, k.key, k.task, ', '.join(flags)))
|
||||
if dev.keys.keyversion == 4:
|
||||
print (' %2d: %-26s, default: %-27s => %-26s' % (k.index, k.key, k.task, k.remapped))
|
||||
print (' %s, pos:%d, group:%1d, gmask:%d' % ( ', '.join(flags), k.pos, k.group, k.group_mask))
|
||||
if dev.online:
|
||||
battery = _hidpp20.get_battery(dev)
|
||||
if battery is None:
|
||||
battery = _hidpp10.get_battery(dev)
|
||||
if battery is not None:
|
||||
level, status, nextLevel = battery
|
||||
text = _battery_text(level)
|
||||
nextText = '' if nextLevel is None else ', next level ' +_battery_text(nextLevel)
|
||||
print (' Battery: %s, %s%s.' % (text, status, nextText))
|
||||
else:
|
||||
battery_voltage = _hidpp20.get_voltage(dev)
|
||||
if battery_voltage :
|
||||
(level, status, voltage, charge_sts, charge_type) = battery_voltage
|
||||
print (' Battery: %smV, %s, %s.' % (voltage, status, level))
|
||||
else:
|
||||
print (' Battery status unavailable.')
|
||||
else:
|
||||
print (' Battery: unknown (device is offline).')
|
||||
if dev.online and dev.keys:
|
||||
print(' Has %d reprogrammable keys:' % len(dev.keys))
|
||||
for k in dev.keys:
|
||||
flags = _special_keys.KEY_FLAG.flag_names(k.flags)
|
||||
# TODO: add here additional variants for other REPROG_CONTROLS
|
||||
if dev.keys.keyversion == 1:
|
||||
print(' %2d: %-26s => %-27s %s' %
|
||||
(k.index, k.key, k.task, ', '.join(flags)))
|
||||
if dev.keys.keyversion == 4:
|
||||
print(' %2d: %-26s, default: %-27s => %-26s' %
|
||||
(k.index, k.key, k.task, k.remapped))
|
||||
print(' %s, pos:%d, group:%1d, gmask:%d' %
|
||||
(', '.join(flags), k.pos, k.group, k.group_mask))
|
||||
if dev.online:
|
||||
battery = _hidpp20.get_battery(dev)
|
||||
if battery is None:
|
||||
battery = _hidpp10.get_battery(dev)
|
||||
if battery is not None:
|
||||
level, status, nextLevel = battery
|
||||
text = _battery_text(level)
|
||||
nextText = '' if nextLevel is None else ', next level ' + _battery_text(
|
||||
nextLevel)
|
||||
print(' Battery: %s, %s%s.' % (text, status, nextText))
|
||||
else:
|
||||
battery_voltage = _hidpp20.get_voltage(dev)
|
||||
if battery_voltage:
|
||||
(level, status, voltage, charge_sts,
|
||||
charge_type) = battery_voltage
|
||||
print(' Battery: %smV, %s, %s.' % (voltage, status, level))
|
||||
else:
|
||||
print(' Battery status unavailable.')
|
||||
else:
|
||||
print(' Battery: unknown (device is offline).')
|
||||
|
||||
|
||||
def run(receivers, args, find_receiver, find_device):
|
||||
assert receivers
|
||||
assert args.device
|
||||
assert receivers
|
||||
assert args.device
|
||||
|
||||
device_name = args.device.lower()
|
||||
device_name = args.device.lower()
|
||||
|
||||
if device_name == 'all':
|
||||
for r in receivers:
|
||||
_print_receiver(r)
|
||||
count = r.count()
|
||||
if count:
|
||||
for dev in r:
|
||||
print ('')
|
||||
_print_device(dev)
|
||||
count -= 1
|
||||
if not count:
|
||||
break
|
||||
print ('')
|
||||
return
|
||||
if device_name == 'all':
|
||||
for r in receivers:
|
||||
_print_receiver(r)
|
||||
count = r.count()
|
||||
if count:
|
||||
for dev in r:
|
||||
print('')
|
||||
_print_device(dev)
|
||||
count -= 1
|
||||
if not count:
|
||||
break
|
||||
print('')
|
||||
return
|
||||
|
||||
dev = find_receiver(receivers, device_name)
|
||||
if dev:
|
||||
_print_receiver(dev)
|
||||
return
|
||||
dev = find_receiver(receivers, device_name)
|
||||
if dev:
|
||||
_print_receiver(dev)
|
||||
return
|
||||
|
||||
dev = find_device(receivers, device_name)
|
||||
assert dev
|
||||
_print_device(dev)
|
||||
dev = find_device(receivers, device_name)
|
||||
assert dev
|
||||
_print_device(dev)
|
||||
|
|
|
|||
|
|
@ -21,19 +21,22 @@ from __future__ import absolute_import, division, print_function, unicode_litera
|
|||
|
||||
|
||||
def run(receivers, args, find_receiver, find_device):
|
||||
assert receivers
|
||||
assert args.device
|
||||
assert receivers
|
||||
assert args.device
|
||||
|
||||
device_name = args.device.lower()
|
||||
dev = find_device(receivers, device_name)
|
||||
device_name = args.device.lower()
|
||||
dev = find_device(receivers, device_name)
|
||||
|
||||
if not dev.receiver.may_unpair:
|
||||
print('Receiver for %s [%s:%s] does not unpair, but attempting anyway' % (dev.name,dev.wpid,dev.serial))
|
||||
if not dev.receiver.may_unpair:
|
||||
print(
|
||||
'Receiver for %s [%s:%s] does not unpair, but attempting anyway' %
|
||||
(dev.name, dev.wpid, dev.serial))
|
||||
|
||||
try:
|
||||
# query these now, it's last chance to get them
|
||||
number, codename, wpid, serial = dev.number, dev.codename, dev.wpid, dev.serial
|
||||
dev.receiver._unpair_device(number, True) # force an unpair
|
||||
print ('Unpaired %d: %s (%s) [%s:%s]' % (number, dev.name, codename, wpid, serial))
|
||||
except Exception as e:
|
||||
raise Exception('failed to unpair device %s: %s' % (dev.name, e))
|
||||
try:
|
||||
# query these now, it's last chance to get them
|
||||
number, codename, wpid, serial = dev.number, dev.codename, dev.wpid, dev.serial
|
||||
dev.receiver._unpair_device(number, True) # force an unpair
|
||||
print('Unpaired %d: %s (%s) [%s:%s]' %
|
||||
(number, dev.name, codename, wpid, serial))
|
||||
except Exception as e:
|
||||
raise Exception('failed to unpair device %s: %s' % (dev.name, e))
|
||||
|
|
|
|||
|
|
@ -25,106 +25,109 @@ from logging import getLogger, DEBUG as _DEBUG, INFO as _INFO
|
|||
_log = getLogger(__name__)
|
||||
del getLogger
|
||||
|
||||
_XDG_CONFIG_HOME = _os.environ.get('XDG_CONFIG_HOME') or _path.expanduser(_path.join('~', '.config'))
|
||||
_XDG_CONFIG_HOME = _os.environ.get('XDG_CONFIG_HOME') or _path.expanduser(
|
||||
_path.join('~', '.config'))
|
||||
_file_path = _path.join(_XDG_CONFIG_HOME, 'solaar', 'config.json')
|
||||
|
||||
|
||||
from solaar import __version__
|
||||
_KEY_VERSION = '_version'
|
||||
_KEY_NAME = '_name'
|
||||
_configuration = {}
|
||||
|
||||
|
||||
|
||||
def _load():
|
||||
if _path.isfile(_file_path):
|
||||
loaded_configuration = {}
|
||||
try:
|
||||
with open(_file_path, 'r') as config_file:
|
||||
loaded_configuration = _json_load(config_file)
|
||||
except:
|
||||
_log.error("failed to load from %s", _file_path)
|
||||
if _path.isfile(_file_path):
|
||||
loaded_configuration = {}
|
||||
try:
|
||||
with open(_file_path, 'r') as config_file:
|
||||
loaded_configuration = _json_load(config_file)
|
||||
except:
|
||||
_log.error("failed to load from %s", _file_path)
|
||||
|
||||
# loaded_configuration.update(_configuration)
|
||||
_configuration.clear()
|
||||
_configuration.update(loaded_configuration)
|
||||
# loaded_configuration.update(_configuration)
|
||||
_configuration.clear()
|
||||
_configuration.update(loaded_configuration)
|
||||
|
||||
if _log.isEnabledFor(_DEBUG):
|
||||
_log.debug("load => %s", _configuration)
|
||||
if _log.isEnabledFor(_DEBUG):
|
||||
_log.debug("load => %s", _configuration)
|
||||
|
||||
_cleanup(_configuration)
|
||||
_configuration[_KEY_VERSION] = __version__
|
||||
return _configuration
|
||||
_cleanup(_configuration)
|
||||
_configuration[_KEY_VERSION] = __version__
|
||||
return _configuration
|
||||
|
||||
|
||||
def save():
|
||||
# don't save if the configuration hasn't been loaded
|
||||
if _KEY_VERSION not in _configuration:
|
||||
return
|
||||
# don't save if the configuration hasn't been loaded
|
||||
if _KEY_VERSION not in _configuration:
|
||||
return
|
||||
|
||||
dirname = _os.path.dirname(_file_path)
|
||||
if not _path.isdir(dirname):
|
||||
try:
|
||||
_os.makedirs(dirname)
|
||||
except:
|
||||
_log.error("failed to create %s", dirname)
|
||||
return False
|
||||
dirname = _os.path.dirname(_file_path)
|
||||
if not _path.isdir(dirname):
|
||||
try:
|
||||
_os.makedirs(dirname)
|
||||
except:
|
||||
_log.error("failed to create %s", dirname)
|
||||
return False
|
||||
|
||||
_cleanup(_configuration)
|
||||
_cleanup(_configuration)
|
||||
|
||||
try:
|
||||
with open(_file_path, 'w') as config_file:
|
||||
_json_save(_configuration, config_file, skipkeys=True, indent=2, sort_keys=True)
|
||||
try:
|
||||
with open(_file_path, 'w') as config_file:
|
||||
_json_save(_configuration,
|
||||
config_file,
|
||||
skipkeys=True,
|
||||
indent=2,
|
||||
sort_keys=True)
|
||||
|
||||
if _log.isEnabledFor(_INFO):
|
||||
_log.info("saved %s to %s", _configuration, _file_path)
|
||||
return True
|
||||
except:
|
||||
_log.error("failed to save to %s", _file_path)
|
||||
if _log.isEnabledFor(_INFO):
|
||||
_log.info("saved %s to %s", _configuration, _file_path)
|
||||
return True
|
||||
except:
|
||||
_log.error("failed to save to %s", _file_path)
|
||||
|
||||
|
||||
def _cleanup(d):
|
||||
# remove None values from the dict
|
||||
for key in list(d.keys()):
|
||||
value = d.get(key)
|
||||
if value is None:
|
||||
del d[key]
|
||||
elif isinstance(value, dict):
|
||||
_cleanup(value)
|
||||
# remove None values from the dict
|
||||
for key in list(d.keys()):
|
||||
value = d.get(key)
|
||||
if value is None:
|
||||
del d[key]
|
||||
elif isinstance(value, dict):
|
||||
_cleanup(value)
|
||||
|
||||
|
||||
def _device_key(device):
|
||||
return '%s:%s' % (device.wpid, device.serial)
|
||||
return '%s:%s' % (device.wpid, device.serial)
|
||||
|
||||
|
||||
class _DeviceEntry(dict):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(_DeviceEntry, self).__init__(*args, **kwargs)
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(_DeviceEntry, self).__init__(*args, **kwargs)
|
||||
|
||||
def __setitem__(self, key, value):
|
||||
super(_DeviceEntry, self).__setitem__(key, value)
|
||||
save()
|
||||
def __setitem__(self, key, value):
|
||||
super(_DeviceEntry, self).__setitem__(key, value)
|
||||
save()
|
||||
|
||||
|
||||
def _device_entry(device):
|
||||
if not _configuration:
|
||||
_load()
|
||||
if not _configuration:
|
||||
_load()
|
||||
|
||||
device_key = _device_key(device)
|
||||
c = _configuration.get(device_key) or {}
|
||||
device_key = _device_key(device)
|
||||
c = _configuration.get(device_key) or {}
|
||||
|
||||
if not isinstance(c, _DeviceEntry):
|
||||
c[_KEY_NAME] = device.name
|
||||
c = _DeviceEntry(c)
|
||||
_configuration[device_key] = c
|
||||
if not isinstance(c, _DeviceEntry):
|
||||
c[_KEY_NAME] = device.name
|
||||
c = _DeviceEntry(c)
|
||||
_configuration[device_key] = c
|
||||
|
||||
return c
|
||||
return c
|
||||
|
||||
|
||||
def attach_to(device):
|
||||
"""Apply the last saved configuration to a device."""
|
||||
if not _configuration:
|
||||
_load()
|
||||
"""Apply the last saved configuration to a device."""
|
||||
if not _configuration:
|
||||
_load()
|
||||
|
||||
persister = _device_entry(device)
|
||||
device.persister = persister
|
||||
persister = _device_entry(device)
|
||||
device.persister = persister
|
||||
|
|
|
|||
|
|
@ -22,7 +22,6 @@ from __future__ import absolute_import, division, print_function, unicode_litera
|
|||
|
||||
import importlib
|
||||
|
||||
|
||||
from solaar import __version__, NAME
|
||||
import solaar.i18n as _i18n
|
||||
import solaar.cli as _cli
|
||||
|
|
@ -31,96 +30,131 @@ import solaar.cli as _cli
|
|||
#
|
||||
#
|
||||
|
||||
|
||||
def _require(module, os_package, gi=None, gi_package=None, gi_version=None):
|
||||
try:
|
||||
if gi is not None:
|
||||
gi.require_version(gi_package,gi_version)
|
||||
return importlib.import_module(module)
|
||||
except (ImportError, ValueError):
|
||||
import sys
|
||||
sys.exit("%s: missing required system package %s" % (NAME, os_package))
|
||||
try:
|
||||
if gi is not None:
|
||||
gi.require_version(gi_package, gi_version)
|
||||
return importlib.import_module(module)
|
||||
except (ImportError, ValueError):
|
||||
import sys
|
||||
sys.exit("%s: missing required system package %s" % (NAME, os_package))
|
||||
|
||||
|
||||
prefer_symbolic_battery_icons = False
|
||||
|
||||
|
||||
def _parse_arguments():
|
||||
import argparse
|
||||
arg_parser = argparse.ArgumentParser(prog=NAME.lower())
|
||||
arg_parser.add_argument('-d', '--debug', action='count', default=0,
|
||||
help='print logging messages, for debugging purposes (may be repeated for extra verbosity)')
|
||||
arg_parser.add_argument('-D', '--hidraw', action='store', dest='hidraw_path', metavar='PATH',
|
||||
help='unifying receiver to use; the first detected receiver if unspecified. Example: /dev/hidraw2')
|
||||
arg_parser.add_argument('--restart-on-wake-up', action='store_true',
|
||||
help='restart Solaar on sleep wake-up (experimental)')
|
||||
arg_parser.add_argument('-w', '--window', choices=('show','hide','only'), help='start with window showing / hidden / only (no tray icon)')
|
||||
arg_parser.add_argument('-b', '--battery-icons', choices=('regular','symbolic'), help='prefer regular / symbolic icons')
|
||||
arg_parser.add_argument('-V', '--version', action='version', version='%(prog)s ' + __version__)
|
||||
arg_parser.add_argument('--help-actions', action='store_true',
|
||||
help='print help for the optional actions')
|
||||
arg_parser.add_argument('action', nargs=argparse.REMAINDER, choices=_cli.actions,
|
||||
help='optional actions to perform')
|
||||
import argparse
|
||||
arg_parser = argparse.ArgumentParser(prog=NAME.lower())
|
||||
arg_parser.add_argument(
|
||||
'-d',
|
||||
'--debug',
|
||||
action='count',
|
||||
default=0,
|
||||
help=
|
||||
'print logging messages, for debugging purposes (may be repeated for extra verbosity)'
|
||||
)
|
||||
arg_parser.add_argument(
|
||||
'-D',
|
||||
'--hidraw',
|
||||
action='store',
|
||||
dest='hidraw_path',
|
||||
metavar='PATH',
|
||||
help=
|
||||
'unifying receiver to use; the first detected receiver if unspecified. Example: /dev/hidraw2'
|
||||
)
|
||||
arg_parser.add_argument(
|
||||
'--restart-on-wake-up',
|
||||
action='store_true',
|
||||
help='restart Solaar on sleep wake-up (experimental)')
|
||||
arg_parser.add_argument(
|
||||
'-w',
|
||||
'--window',
|
||||
choices=('show', 'hide', 'only'),
|
||||
help='start with window showing / hidden / only (no tray icon)')
|
||||
arg_parser.add_argument('-b',
|
||||
'--battery-icons',
|
||||
choices=('regular', 'symbolic'),
|
||||
help='prefer regular / symbolic icons')
|
||||
arg_parser.add_argument('-V',
|
||||
'--version',
|
||||
action='version',
|
||||
version='%(prog)s ' + __version__)
|
||||
arg_parser.add_argument('--help-actions',
|
||||
action='store_true',
|
||||
help='print help for the optional actions')
|
||||
arg_parser.add_argument('action',
|
||||
nargs=argparse.REMAINDER,
|
||||
choices=_cli.actions,
|
||||
help='optional actions to perform')
|
||||
|
||||
args = arg_parser.parse_args()
|
||||
args = arg_parser.parse_args()
|
||||
|
||||
if args.help_actions:
|
||||
_cli.print_help()
|
||||
return
|
||||
if args.help_actions:
|
||||
_cli.print_help()
|
||||
return
|
||||
|
||||
if args.window is None:
|
||||
args.window = 'show' # default behaviour is to show main window
|
||||
if args.window is None:
|
||||
args.window = 'show' # default behaviour is to show main window
|
||||
|
||||
global prefer_symbolic_battery_icons
|
||||
prefer_symbolic_battery_icons = True if args.battery_icons == 'symbolic' else False
|
||||
global prefer_symbolic_battery_icons
|
||||
prefer_symbolic_battery_icons = True if args.battery_icons == 'symbolic' else False
|
||||
|
||||
import logging
|
||||
if args.debug > 0:
|
||||
log_level = logging.WARNING - 10 * args.debug
|
||||
log_format='%(asctime)s,%(msecs)03d %(levelname)8s [%(threadName)s] %(name)s: %(message)s'
|
||||
logging.basicConfig(level=max(log_level, logging.DEBUG), format=log_format, datefmt='%H:%M:%S')
|
||||
else:
|
||||
logging.root.addHandler(logging.NullHandler())
|
||||
logging.root.setLevel(logging.ERROR)
|
||||
import logging
|
||||
if args.debug > 0:
|
||||
log_level = logging.WARNING - 10 * args.debug
|
||||
log_format = '%(asctime)s,%(msecs)03d %(levelname)8s [%(threadName)s] %(name)s: %(message)s'
|
||||
logging.basicConfig(level=max(log_level, logging.DEBUG),
|
||||
format=log_format,
|
||||
datefmt='%H:%M:%S')
|
||||
else:
|
||||
logging.root.addHandler(logging.NullHandler())
|
||||
logging.root.setLevel(logging.ERROR)
|
||||
|
||||
if not args.action:
|
||||
if logging.root.isEnabledFor(logging.INFO):
|
||||
logging.info("language %s (%s), translations path %s", _i18n.language, _i18n.encoding, _i18n.path)
|
||||
if not args.action:
|
||||
if logging.root.isEnabledFor(logging.INFO):
|
||||
logging.info("language %s (%s), translations path %s",
|
||||
_i18n.language, _i18n.encoding, _i18n.path)
|
||||
|
||||
return args
|
||||
return args
|
||||
|
||||
|
||||
def main():
|
||||
_require('pyudev', 'python3-pyudev')
|
||||
_require('pyudev', 'python3-pyudev')
|
||||
|
||||
# handle ^C in console
|
||||
import signal
|
||||
signal.signal(signal.SIGINT, signal.SIG_DFL)
|
||||
# handle ^C in console
|
||||
import signal
|
||||
signal.signal(signal.SIGINT, signal.SIG_DFL)
|
||||
|
||||
args = _parse_arguments()
|
||||
if not args: return
|
||||
if args.action:
|
||||
# if any argument, run comandline and exit
|
||||
return _cli.run(args.action, args.hidraw_path)
|
||||
args = _parse_arguments()
|
||||
if not args: return
|
||||
if args.action:
|
||||
# if any argument, run comandline and exit
|
||||
return _cli.run(args.action, args.hidraw_path)
|
||||
|
||||
gi = _require('gi', 'python3-gi or python3-gobject')
|
||||
_require('gi.repository.Gtk', 'gir1.2-gtk-3.0', gi, 'Gtk', '3.0')
|
||||
gi = _require('gi', 'python3-gi or python3-gobject')
|
||||
_require('gi.repository.Gtk', 'gir1.2-gtk-3.0', gi, 'Gtk', '3.0')
|
||||
|
||||
try:
|
||||
import solaar.ui as ui
|
||||
import solaar.listener as listener
|
||||
listener.setup_scanner(ui.status_changed, ui.error_dialog)
|
||||
try:
|
||||
import solaar.ui as ui
|
||||
import solaar.listener as listener
|
||||
listener.setup_scanner(ui.status_changed, ui.error_dialog)
|
||||
|
||||
import solaar.upower as _upower
|
||||
if args.restart_on_wake_up:
|
||||
_upower.watch(listener.start_all, listener.stop_all)
|
||||
else:
|
||||
_upower.watch(lambda: listener.ping_all(True))
|
||||
import solaar.upower as _upower
|
||||
if args.restart_on_wake_up:
|
||||
_upower.watch(listener.start_all, listener.stop_all)
|
||||
else:
|
||||
_upower.watch(lambda: listener.ping_all(True))
|
||||
|
||||
# main UI event loop
|
||||
ui.run_loop(listener.start_all, listener.stop_all, args.window!='only', args.window!='hide')
|
||||
except Exception as e:
|
||||
import sys
|
||||
from traceback import format_exc
|
||||
sys.exit('%s: error: %s' % (NAME.lower(), format_exc()))
|
||||
# main UI event loop
|
||||
ui.run_loop(listener.start_all, listener.stop_all,
|
||||
args.window != 'only', args.window != 'hide')
|
||||
except Exception as e:
|
||||
import sys
|
||||
from traceback import format_exc
|
||||
sys.exit('%s: error: %s' % (NAME.lower(), format_exc()))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
main()
|
||||
|
|
|
|||
|
|
@ -25,22 +25,27 @@ from solaar import NAME as _NAME
|
|||
#
|
||||
#
|
||||
|
||||
|
||||
def _find_locale_path(lc_domain):
|
||||
import os.path as _path
|
||||
import os.path as _path
|
||||
|
||||
import sys as _sys
|
||||
prefix_share = _path.normpath(_path.join(_path.realpath(_sys.path[0]), '..'))
|
||||
src_share = _path.normpath(_path.join(_path.realpath(_sys.path[0]), '..', 'share'))
|
||||
del _sys
|
||||
import sys as _sys
|
||||
prefix_share = _path.normpath(
|
||||
_path.join(_path.realpath(_sys.path[0]), '..'))
|
||||
src_share = _path.normpath(
|
||||
_path.join(_path.realpath(_sys.path[0]), '..', 'share'))
|
||||
del _sys
|
||||
|
||||
from glob import glob as _glob
|
||||
from glob import glob as _glob
|
||||
|
||||
for location in prefix_share, src_share:
|
||||
mo_files = _glob(_path.join(location, 'locale', '*', 'LC_MESSAGES', lc_domain + '.mo'))
|
||||
if mo_files:
|
||||
return _path.join(location, 'locale')
|
||||
for location in prefix_share, src_share:
|
||||
mo_files = _glob(
|
||||
_path.join(location, 'locale', '*', 'LC_MESSAGES',
|
||||
lc_domain + '.mo'))
|
||||
if mo_files:
|
||||
return _path.join(location, 'locale')
|
||||
|
||||
# del _path
|
||||
# del _path
|
||||
|
||||
|
||||
import locale
|
||||
|
|
@ -58,9 +63,9 @@ _gettext.textdomain(_LOCALE_DOMAIN)
|
|||
_gettext.install(_LOCALE_DOMAIN)
|
||||
|
||||
try:
|
||||
unicode
|
||||
_ = lambda x: _gettext.gettext(x).decode('UTF-8')
|
||||
ngettext = lambda *x: _gettext.ngettext(*x).decode('UTF-8')
|
||||
unicode
|
||||
_ = lambda x: _gettext.gettext(x).decode('UTF-8')
|
||||
ngettext = lambda *x: _gettext.ngettext(*x).decode('UTF-8')
|
||||
except:
|
||||
_ = _gettext.gettext
|
||||
ngettext = _gettext.ngettext
|
||||
_ = _gettext.gettext
|
||||
ngettext = _gettext.ngettext
|
||||
|
|
|
|||
|
|
@ -24,34 +24,32 @@ from logging import getLogger, INFO as _INFO, WARNING as _WARNING
|
|||
_log = getLogger(__name__)
|
||||
del getLogger
|
||||
|
||||
|
||||
from solaar.i18n import _
|
||||
from . import configuration
|
||||
from logitech_receiver import (
|
||||
Receiver,
|
||||
listener as _listener,
|
||||
status as _status,
|
||||
notifications as _notifications
|
||||
)
|
||||
from logitech_receiver import (Receiver, listener as _listener, status as
|
||||
_status, notifications as _notifications)
|
||||
|
||||
#
|
||||
#
|
||||
#
|
||||
|
||||
from collections import namedtuple
|
||||
_GHOST_DEVICE = namedtuple('_GHOST_DEVICE', ('receiver', 'number', 'name', 'kind', 'status', 'online'))
|
||||
_GHOST_DEVICE = namedtuple(
|
||||
'_GHOST_DEVICE',
|
||||
('receiver', 'number', 'name', 'kind', 'status', 'online'))
|
||||
_GHOST_DEVICE.__bool__ = lambda self: False
|
||||
_GHOST_DEVICE.__nonzero__ = _GHOST_DEVICE.__bool__
|
||||
del namedtuple
|
||||
|
||||
|
||||
def _ghost(device):
|
||||
return _GHOST_DEVICE(
|
||||
receiver=device.receiver,
|
||||
number=device.number,
|
||||
name=device.name,
|
||||
kind=device.kind,
|
||||
status=None,
|
||||
online=False)
|
||||
return _GHOST_DEVICE(receiver=device.receiver,
|
||||
number=device.number,
|
||||
name=device.name,
|
||||
kind=device.kind,
|
||||
status=None,
|
||||
online=False)
|
||||
|
||||
|
||||
#
|
||||
#
|
||||
|
|
@ -63,188 +61,201 @@ def _ghost(device):
|
|||
|
||||
|
||||
class ReceiverListener(_listener.EventsListener):
|
||||
"""Keeps the status of a Receiver.
|
||||
"""Keeps the status of a Receiver.
|
||||
"""
|
||||
def __init__(self, receiver, status_changed_callback):
|
||||
super(ReceiverListener, self).__init__(receiver, self._notifications_handler)
|
||||
# no reason to enable polling yet
|
||||
# self.tick_period = _POLL_TICK
|
||||
# self._last_tick = 0
|
||||
def __init__(self, receiver, status_changed_callback):
|
||||
super(ReceiverListener, self).__init__(receiver,
|
||||
self._notifications_handler)
|
||||
# no reason to enable polling yet
|
||||
# self.tick_period = _POLL_TICK
|
||||
# self._last_tick = 0
|
||||
|
||||
assert status_changed_callback
|
||||
self.status_changed_callback = status_changed_callback
|
||||
_status.attach_to(receiver, self._status_changed)
|
||||
assert status_changed_callback
|
||||
self.status_changed_callback = status_changed_callback
|
||||
_status.attach_to(receiver, self._status_changed)
|
||||
|
||||
def has_started(self):
|
||||
if _log.isEnabledFor(_INFO):
|
||||
_log.info("%s: notifications listener has started (%s)", self.receiver, self.receiver.handle)
|
||||
notification_flags = self.receiver.enable_notifications()
|
||||
self.receiver.status[_status.KEYS.NOTIFICATION_FLAGS] = notification_flags
|
||||
self.receiver.notify_devices()
|
||||
self._status_changed(self.receiver) #, _status.ALERT.NOTIFICATION)
|
||||
def has_started(self):
|
||||
if _log.isEnabledFor(_INFO):
|
||||
_log.info("%s: notifications listener has started (%s)",
|
||||
self.receiver, self.receiver.handle)
|
||||
notification_flags = self.receiver.enable_notifications()
|
||||
self.receiver.status[
|
||||
_status.KEYS.NOTIFICATION_FLAGS] = notification_flags
|
||||
self.receiver.notify_devices()
|
||||
self._status_changed(self.receiver) #, _status.ALERT.NOTIFICATION)
|
||||
|
||||
def has_stopped(self):
|
||||
r, self.receiver = self.receiver, None
|
||||
assert r is not None
|
||||
if _log.isEnabledFor(_INFO):
|
||||
_log.info("%s: notifications listener has stopped", r)
|
||||
def has_stopped(self):
|
||||
r, self.receiver = self.receiver, None
|
||||
assert r is not None
|
||||
if _log.isEnabledFor(_INFO):
|
||||
_log.info("%s: notifications listener has stopped", r)
|
||||
|
||||
# because udev is not notifying us about device removal,
|
||||
# make sure to clean up in _all_listeners
|
||||
_all_listeners.pop(r.path, None)
|
||||
# because udev is not notifying us about device removal,
|
||||
# make sure to clean up in _all_listeners
|
||||
_all_listeners.pop(r.path, None)
|
||||
|
||||
r.status = _("The receiver was unplugged.")
|
||||
if r:
|
||||
try:
|
||||
r.close()
|
||||
except:
|
||||
_log.exception("closing receiver %s" % r.path)
|
||||
self.status_changed_callback(r) #, _status.ALERT.NOTIFICATION)
|
||||
r.status = _("The receiver was unplugged.")
|
||||
if r:
|
||||
try:
|
||||
r.close()
|
||||
except:
|
||||
_log.exception("closing receiver %s" % r.path)
|
||||
self.status_changed_callback(r) #, _status.ALERT.NOTIFICATION)
|
||||
|
||||
# def tick(self, timestamp):
|
||||
# if not self.tick_period:
|
||||
# raise Exception("tick() should not be called without a tick_period: %s", self)
|
||||
#
|
||||
# # not necessary anymore, we're now using udev monitor to watch for receiver status
|
||||
# # if self._last_tick > 0 and timestamp - self._last_tick > _POLL_TICK * 2:
|
||||
# # # if we missed a couple of polls, most likely the computer went into
|
||||
# # # sleep, and we have to reinitialize the receiver again
|
||||
# # _log.warn("%s: possible sleep detected, closing this listener", self.receiver)
|
||||
# # self.stop()
|
||||
# # return
|
||||
#
|
||||
# self._last_tick = timestamp
|
||||
#
|
||||
# try:
|
||||
# # read these in case they haven't been read already
|
||||
# # self.receiver.serial, self.receiver.firmware
|
||||
# if self.receiver.status.lock_open:
|
||||
# # don't mess with stuff while pairing
|
||||
# return
|
||||
#
|
||||
# self.receiver.status.poll(timestamp)
|
||||
#
|
||||
# # Iterating directly through the reciver would unnecessarily probe
|
||||
# # all possible devices, even unpaired ones.
|
||||
# # Checking for each device number in turn makes sure only already
|
||||
# # known devices are polled.
|
||||
# # This is okay because we should have already known about them all
|
||||
# # long before the first poll() happents, through notifications.
|
||||
# for number in range(1, 6):
|
||||
# if number in self.receiver:
|
||||
# dev = self.receiver[number]
|
||||
# if dev and dev.status is not None:
|
||||
# dev.status.poll(timestamp)
|
||||
# except Exception as e:
|
||||
# _log.exception("polling", e)
|
||||
# def tick(self, timestamp):
|
||||
# if not self.tick_period:
|
||||
# raise Exception("tick() should not be called without a tick_period: %s", self)
|
||||
#
|
||||
# # not necessary anymore, we're now using udev monitor to watch for receiver status
|
||||
# # if self._last_tick > 0 and timestamp - self._last_tick > _POLL_TICK * 2:
|
||||
# # # if we missed a couple of polls, most likely the computer went into
|
||||
# # # sleep, and we have to reinitialize the receiver again
|
||||
# # _log.warn("%s: possible sleep detected, closing this listener", self.receiver)
|
||||
# # self.stop()
|
||||
# # return
|
||||
#
|
||||
# self._last_tick = timestamp
|
||||
#
|
||||
# try:
|
||||
# # read these in case they haven't been read already
|
||||
# # self.receiver.serial, self.receiver.firmware
|
||||
# if self.receiver.status.lock_open:
|
||||
# # don't mess with stuff while pairing
|
||||
# return
|
||||
#
|
||||
# self.receiver.status.poll(timestamp)
|
||||
#
|
||||
# # Iterating directly through the reciver would unnecessarily probe
|
||||
# # all possible devices, even unpaired ones.
|
||||
# # Checking for each device number in turn makes sure only already
|
||||
# # known devices are polled.
|
||||
# # This is okay because we should have already known about them all
|
||||
# # long before the first poll() happents, through notifications.
|
||||
# for number in range(1, 6):
|
||||
# if number in self.receiver:
|
||||
# dev = self.receiver[number]
|
||||
# if dev and dev.status is not None:
|
||||
# dev.status.poll(timestamp)
|
||||
# except Exception as e:
|
||||
# _log.exception("polling", e)
|
||||
|
||||
def _status_changed(self, device, alert=_status.ALERT.NONE, reason=None):
|
||||
assert device is not None
|
||||
if _log.isEnabledFor(_INFO):
|
||||
if device.kind is None:
|
||||
_log.info("status_changed %s: %s, %s (%X) %s", device,
|
||||
'present' if bool(device) else 'removed',
|
||||
device.status, alert, reason or '')
|
||||
else:
|
||||
_log.info("status_changed %s: %s %s, %s (%X) %s", device,
|
||||
'paired' if bool(device) else 'unpaired',
|
||||
'online' if device.online else 'offline',
|
||||
device.status, alert, reason or '')
|
||||
def _status_changed(self, device, alert=_status.ALERT.NONE, reason=None):
|
||||
assert device is not None
|
||||
if _log.isEnabledFor(_INFO):
|
||||
if device.kind is None:
|
||||
_log.info("status_changed %s: %s, %s (%X) %s", device,
|
||||
'present' if bool(device) else 'removed',
|
||||
device.status, alert, reason or '')
|
||||
else:
|
||||
_log.info("status_changed %s: %s %s, %s (%X) %s", device,
|
||||
'paired' if bool(device) else 'unpaired',
|
||||
'online' if device.online else 'offline',
|
||||
device.status, alert, reason or '')
|
||||
|
||||
if device.kind is None:
|
||||
assert device == self.receiver
|
||||
# the status of the receiver changed
|
||||
self.status_changed_callback(device, alert, reason)
|
||||
return
|
||||
if device.kind is None:
|
||||
assert device == self.receiver
|
||||
# the status of the receiver changed
|
||||
self.status_changed_callback(device, alert, reason)
|
||||
return
|
||||
|
||||
assert device.receiver == self.receiver
|
||||
if not device:
|
||||
# Device was unpaired, and isn't valid anymore.
|
||||
# We replace it with a ghost so that the UI has something to work
|
||||
# with while cleaning up.
|
||||
_log.warn("device %s was unpaired, ghosting", device)
|
||||
device = _ghost(device)
|
||||
assert device.receiver == self.receiver
|
||||
if not device:
|
||||
# Device was unpaired, and isn't valid anymore.
|
||||
# We replace it with a ghost so that the UI has something to work
|
||||
# with while cleaning up.
|
||||
_log.warn("device %s was unpaired, ghosting", device)
|
||||
device = _ghost(device)
|
||||
|
||||
self.status_changed_callback(device, alert, reason)
|
||||
self.status_changed_callback(device, alert, reason)
|
||||
|
||||
if not device:
|
||||
# the device was just unpaired, need to update the
|
||||
# status of the receiver as well
|
||||
self.status_changed_callback(self.receiver)
|
||||
if not device:
|
||||
# the device was just unpaired, need to update the
|
||||
# status of the receiver as well
|
||||
self.status_changed_callback(self.receiver)
|
||||
|
||||
def _notifications_handler(self, n):
|
||||
assert self.receiver
|
||||
# if _log.isEnabledFor(_DEBUG):
|
||||
# _log.debug("%s: handling %s", self.receiver, n)
|
||||
if n.devnumber == 0xFF:
|
||||
# a receiver notification
|
||||
_notifications.process(self.receiver, n)
|
||||
return
|
||||
def _notifications_handler(self, n):
|
||||
assert self.receiver
|
||||
# if _log.isEnabledFor(_DEBUG):
|
||||
# _log.debug("%s: handling %s", self.receiver, n)
|
||||
if n.devnumber == 0xFF:
|
||||
# a receiver notification
|
||||
_notifications.process(self.receiver, n)
|
||||
return
|
||||
|
||||
# a device notification
|
||||
if not(0 < n.devnumber <= self.receiver.max_devices):
|
||||
if _log.isEnabledFor(_WARNING):
|
||||
_log.warning(_("Unexpected device number (%s) in notification %s." % (n.devnumber, n)))
|
||||
return
|
||||
already_known = n.devnumber in self.receiver
|
||||
# a device notification
|
||||
if not (0 < n.devnumber <= self.receiver.max_devices):
|
||||
if _log.isEnabledFor(_WARNING):
|
||||
_log.warning(
|
||||
_("Unexpected device number (%s) in notification %s." %
|
||||
(n.devnumber, n)))
|
||||
return
|
||||
already_known = n.devnumber in self.receiver
|
||||
|
||||
# FIXME: hacky fix for kernel/hardware race condition
|
||||
# If the device was just turned on or woken up from sleep, it may not
|
||||
# be ready to receive commands. The "payload" bit of the wireless
|
||||
# status notification seems to tell us this. If this is the case, we
|
||||
# must wait a short amount of time to avoid causing a broken pipe
|
||||
# error.
|
||||
device_ready = not bool(ord(n.data[0:1]) & 0x80) or n.sub_id != 0x41
|
||||
if not device_ready:
|
||||
time.sleep(0.01)
|
||||
# FIXME: hacky fix for kernel/hardware race condition
|
||||
# If the device was just turned on or woken up from sleep, it may not
|
||||
# be ready to receive commands. The "payload" bit of the wireless
|
||||
# status notification seems to tell us this. If this is the case, we
|
||||
# must wait a short amount of time to avoid causing a broken pipe
|
||||
# error.
|
||||
device_ready = not bool(ord(n.data[0:1]) & 0x80) or n.sub_id != 0x41
|
||||
if not device_ready:
|
||||
time.sleep(0.01)
|
||||
|
||||
if n.sub_id == 0x40 and not already_known:
|
||||
return # disconnecting something that is not known - nothing to do
|
||||
if n.sub_id == 0x40 and not already_known:
|
||||
return # disconnecting something that is not known - nothing to do
|
||||
|
||||
if n.sub_id == 0x41:
|
||||
if not already_known:
|
||||
dev = self.receiver.register_new_device(n.devnumber, n)
|
||||
elif self.receiver.status.lock_open and self.receiver.re_pairs and not ord(n.data[0:1]) & 0x40:
|
||||
dev = self.receiver[n.devnumber]
|
||||
del self.receiver[n.devnumber] # get rid of information on device re-paired away
|
||||
self._status_changed(dev) # signal that this device has changed
|
||||
dev = self.receiver.register_new_device(n.devnumber, n)
|
||||
self.receiver.status.new_device = self.receiver[n.devnumber]
|
||||
else:
|
||||
dev = self.receiver[n.devnumber]
|
||||
else:
|
||||
dev = self.receiver[n.devnumber]
|
||||
if n.sub_id == 0x41:
|
||||
if not already_known:
|
||||
dev = self.receiver.register_new_device(n.devnumber, n)
|
||||
elif self.receiver.status.lock_open and self.receiver.re_pairs and not ord(
|
||||
n.data[0:1]) & 0x40:
|
||||
dev = self.receiver[n.devnumber]
|
||||
del self.receiver[
|
||||
n.
|
||||
devnumber] # get rid of information on device re-paired away
|
||||
self._status_changed(
|
||||
dev) # signal that this device has changed
|
||||
dev = self.receiver.register_new_device(n.devnumber, n)
|
||||
self.receiver.status.new_device = self.receiver[n.devnumber]
|
||||
else:
|
||||
dev = self.receiver[n.devnumber]
|
||||
else:
|
||||
dev = self.receiver[n.devnumber]
|
||||
|
||||
if not dev:
|
||||
_log.warn("%s: received %s for invalid device %d: %r", self.receiver, n, n.devnumber, dev)
|
||||
return
|
||||
if not dev:
|
||||
_log.warn("%s: received %s for invalid device %d: %r",
|
||||
self.receiver, n, n.devnumber, dev)
|
||||
return
|
||||
|
||||
# Apply settings every time the device connects
|
||||
if n.sub_id == 0x41:
|
||||
if _log.isEnabledFor(_INFO):
|
||||
_log.info("%s triggered new device %s (%s)", n, dev, dev.kind)
|
||||
# If there are saved configs, bring the device's settings up-to-date.
|
||||
# They will be applied when the device is marked as online.
|
||||
configuration.attach_to(dev)
|
||||
_status.attach_to(dev, self._status_changed)
|
||||
# the receiver changed status as well
|
||||
self._status_changed(self.receiver)
|
||||
# Apply settings every time the device connects
|
||||
if n.sub_id == 0x41:
|
||||
if _log.isEnabledFor(_INFO):
|
||||
_log.info("%s triggered new device %s (%s)", n, dev, dev.kind)
|
||||
# If there are saved configs, bring the device's settings up-to-date.
|
||||
# They will be applied when the device is marked as online.
|
||||
configuration.attach_to(dev)
|
||||
_status.attach_to(dev, self._status_changed)
|
||||
# the receiver changed status as well
|
||||
self._status_changed(self.receiver)
|
||||
|
||||
assert dev
|
||||
assert dev.status is not None
|
||||
_notifications.process(dev, n)
|
||||
if self.receiver.status.lock_open and not already_known:
|
||||
# this should be the first notification after a device was paired
|
||||
assert n.sub_id == 0x41 and n.address == 0x04
|
||||
if _log.isEnabledFor(_INFO):
|
||||
_log.info("%s: pairing detected new device", self.receiver)
|
||||
self.receiver.status.new_device = dev
|
||||
elif dev.online is None:
|
||||
dev.ping()
|
||||
assert dev
|
||||
assert dev.status is not None
|
||||
_notifications.process(dev, n)
|
||||
if self.receiver.status.lock_open and not already_known:
|
||||
# this should be the first notification after a device was paired
|
||||
assert n.sub_id == 0x41 and n.address == 0x04
|
||||
if _log.isEnabledFor(_INFO):
|
||||
_log.info("%s: pairing detected new device", self.receiver)
|
||||
self.receiver.status.new_device = dev
|
||||
elif dev.online is None:
|
||||
dev.ping()
|
||||
|
||||
def __str__(self):
|
||||
return '<ReceiverListener(%s,%s)>' % (self.receiver.path,
|
||||
self.receiver.handle)
|
||||
|
||||
__unicode__ = __str__
|
||||
|
||||
def __str__(self):
|
||||
return '<ReceiverListener(%s,%s)>' % (self.receiver.path, self.receiver.handle)
|
||||
__unicode__ = __str__
|
||||
|
||||
#
|
||||
#
|
||||
|
|
@ -256,103 +267,106 @@ _all_listeners = {}
|
|||
|
||||
|
||||
def _start(device_info):
|
||||
assert _status_callback
|
||||
receiver = Receiver.open(device_info)
|
||||
if receiver:
|
||||
rl = ReceiverListener(receiver, _status_callback)
|
||||
rl.start()
|
||||
_all_listeners[device_info.path] = rl
|
||||
return rl
|
||||
assert _status_callback
|
||||
receiver = Receiver.open(device_info)
|
||||
if receiver:
|
||||
rl = ReceiverListener(receiver, _status_callback)
|
||||
rl.start()
|
||||
_all_listeners[device_info.path] = rl
|
||||
return rl
|
||||
|
||||
_log.warn("failed to open %s", device_info)
|
||||
_log.warn("failed to open %s", device_info)
|
||||
|
||||
|
||||
def start_all():
|
||||
# just in case this it called twice in a row...
|
||||
stop_all()
|
||||
# just in case this it called twice in a row...
|
||||
stop_all()
|
||||
|
||||
if _log.isEnabledFor(_INFO):
|
||||
_log.info("starting receiver listening threads")
|
||||
for device_info in _base.receivers():
|
||||
_process_receiver_event('add', device_info)
|
||||
if _log.isEnabledFor(_INFO):
|
||||
_log.info("starting receiver listening threads")
|
||||
for device_info in _base.receivers():
|
||||
_process_receiver_event('add', device_info)
|
||||
|
||||
|
||||
def stop_all():
|
||||
listeners = list(_all_listeners.values())
|
||||
_all_listeners.clear()
|
||||
listeners = list(_all_listeners.values())
|
||||
_all_listeners.clear()
|
||||
|
||||
if listeners:
|
||||
if _log.isEnabledFor(_INFO):
|
||||
_log.info("stopping receiver listening threads %s", listeners)
|
||||
if listeners:
|
||||
if _log.isEnabledFor(_INFO):
|
||||
_log.info("stopping receiver listening threads %s", listeners)
|
||||
|
||||
for l in listeners:
|
||||
l.stop()
|
||||
for l in listeners:
|
||||
l.stop()
|
||||
|
||||
configuration.save()
|
||||
configuration.save()
|
||||
|
||||
if listeners:
|
||||
for l in listeners:
|
||||
l.join()
|
||||
|
||||
if listeners:
|
||||
for l in listeners:
|
||||
l.join()
|
||||
|
||||
# ping all devices to find out whether they are connected
|
||||
# after a resume, the device may have been off
|
||||
# so mark its saved status to ensure that the status is pushed to the device when it comes back
|
||||
def ping_all(resuming = False):
|
||||
for l in _all_listeners.values():
|
||||
count = l.receiver.count()
|
||||
if count:
|
||||
for dev in l.receiver:
|
||||
if resuming:
|
||||
dev.status._active = False
|
||||
dev.ping()
|
||||
l._status_changed(dev)
|
||||
count -= 1
|
||||
if not count:
|
||||
break
|
||||
def ping_all(resuming=False):
|
||||
for l in _all_listeners.values():
|
||||
count = l.receiver.count()
|
||||
if count:
|
||||
for dev in l.receiver:
|
||||
if resuming:
|
||||
dev.status._active = False
|
||||
dev.ping()
|
||||
l._status_changed(dev)
|
||||
count -= 1
|
||||
if not count:
|
||||
break
|
||||
|
||||
|
||||
from logitech_receiver import base as _base
|
||||
_status_callback = None
|
||||
_error_callback = None
|
||||
|
||||
|
||||
def setup_scanner(status_changed_callback, error_callback):
|
||||
global _status_callback, _error_callback
|
||||
assert _status_callback is None, 'scanner was already set-up'
|
||||
global _status_callback, _error_callback
|
||||
assert _status_callback is None, 'scanner was already set-up'
|
||||
|
||||
_status_callback = status_changed_callback
|
||||
_error_callback = error_callback
|
||||
_status_callback = status_changed_callback
|
||||
_error_callback = error_callback
|
||||
|
||||
_base.notify_on_receivers_glib(_process_receiver_event)
|
||||
_base.notify_on_receivers_glib(_process_receiver_event)
|
||||
|
||||
|
||||
# receiver add/remove events will start/stop listener threads
|
||||
def _process_receiver_event(action, device_info):
|
||||
assert action is not None
|
||||
assert device_info is not None
|
||||
assert _error_callback
|
||||
assert action is not None
|
||||
assert device_info is not None
|
||||
assert _error_callback
|
||||
|
||||
if _log.isEnabledFor(_INFO):
|
||||
_log.info("receiver event %s %s", action, device_info)
|
||||
if _log.isEnabledFor(_INFO):
|
||||
_log.info("receiver event %s %s", action, device_info)
|
||||
|
||||
# whatever the action, stop any previous receivers at this path
|
||||
l = _all_listeners.pop(device_info.path, None)
|
||||
if l is not None:
|
||||
assert isinstance(l, ReceiverListener)
|
||||
l.stop()
|
||||
# whatever the action, stop any previous receivers at this path
|
||||
l = _all_listeners.pop(device_info.path, None)
|
||||
if l is not None:
|
||||
assert isinstance(l, ReceiverListener)
|
||||
l.stop()
|
||||
|
||||
if action == 'add':
|
||||
# a new receiver device was detected
|
||||
try:
|
||||
_start(device_info)
|
||||
except OSError:
|
||||
# permission error, ignore this path for now
|
||||
# If receiver has extended ACL but not writable then it is for another seat.
|
||||
# (It would be easier to use pylibacl but adding the pylibacl dependencies
|
||||
# for this special case is not good.)
|
||||
try:
|
||||
import subprocess, re
|
||||
output = subprocess.check_output(['/usr/bin/getfacl', '-p', device_info.path])
|
||||
if not re.search(b'user:.+:',output) :
|
||||
_error_callback('permissions', device_info.path)
|
||||
except:
|
||||
_error_callback('permissions', device_info.path)
|
||||
if action == 'add':
|
||||
# a new receiver device was detected
|
||||
try:
|
||||
_start(device_info)
|
||||
except OSError:
|
||||
# permission error, ignore this path for now
|
||||
# If receiver has extended ACL but not writable then it is for another seat.
|
||||
# (It would be easier to use pylibacl but adding the pylibacl dependencies
|
||||
# for this special case is not good.)
|
||||
try:
|
||||
import subprocess, re
|
||||
output = subprocess.check_output(
|
||||
['/usr/bin/getfacl', '-p', device_info.path])
|
||||
if not re.search(b'user:.+:', output):
|
||||
_error_callback('permissions', device_info.path)
|
||||
except:
|
||||
_error_callback('permissions', device_info.path)
|
||||
|
|
|
|||
|
|
@ -27,44 +27,45 @@ del getLogger
|
|||
from threading import Thread as _Thread
|
||||
|
||||
try:
|
||||
from Queue import Queue as _Queue
|
||||
from Queue import Queue as _Queue
|
||||
except ImportError:
|
||||
from queue import Queue as _Queue
|
||||
from queue import Queue as _Queue
|
||||
|
||||
#
|
||||
#
|
||||
#
|
||||
|
||||
|
||||
class TaskRunner(_Thread):
|
||||
def __init__(self, name):
|
||||
super(TaskRunner, self).__init__(name=name)
|
||||
self.daemon = True
|
||||
self.queue = _Queue(16)
|
||||
self.alive = False
|
||||
def __init__(self, name):
|
||||
super(TaskRunner, self).__init__(name=name)
|
||||
self.daemon = True
|
||||
self.queue = _Queue(16)
|
||||
self.alive = False
|
||||
|
||||
def __call__(self, function, *args, **kwargs):
|
||||
task = (function, args, kwargs)
|
||||
self.queue.put(task)
|
||||
def __call__(self, function, *args, **kwargs):
|
||||
task = (function, args, kwargs)
|
||||
self.queue.put(task)
|
||||
|
||||
def stop(self):
|
||||
self.alive = False
|
||||
self.queue.put(None)
|
||||
def stop(self):
|
||||
self.alive = False
|
||||
self.queue.put(None)
|
||||
|
||||
def run(self):
|
||||
self.alive = True
|
||||
def run(self):
|
||||
self.alive = True
|
||||
|
||||
if _log.isEnabledFor(_DEBUG):
|
||||
_log.debug("started")
|
||||
if _log.isEnabledFor(_DEBUG):
|
||||
_log.debug("started")
|
||||
|
||||
while self.alive:
|
||||
task = self.queue.get()
|
||||
if task:
|
||||
function, args, kwargs = task
|
||||
assert function
|
||||
try:
|
||||
function(*args, **kwargs)
|
||||
except:
|
||||
_log.exception("calling %s", function)
|
||||
while self.alive:
|
||||
task = self.queue.get()
|
||||
if task:
|
||||
function, args, kwargs = task
|
||||
assert function
|
||||
try:
|
||||
function(*args, **kwargs)
|
||||
except:
|
||||
_log.exception("calling %s", function)
|
||||
|
||||
if _log.isEnabledFor(_DEBUG):
|
||||
_log.debug("stopped")
|
||||
if _log.isEnabledFor(_DEBUG):
|
||||
_log.debug("stopped")
|
||||
|
|
|
|||
|
|
@ -19,14 +19,12 @@
|
|||
|
||||
from __future__ import absolute_import, division, print_function, unicode_literals
|
||||
|
||||
|
||||
from logging import getLogger, DEBUG as _DEBUG
|
||||
_log = getLogger(__name__)
|
||||
del getLogger
|
||||
|
||||
from gi.repository import GLib, Gtk
|
||||
|
||||
|
||||
from solaar.i18n import _
|
||||
|
||||
#
|
||||
|
|
@ -41,43 +39,50 @@ GLib.threads_init()
|
|||
#
|
||||
#
|
||||
|
||||
|
||||
def _error_dialog(reason, object):
|
||||
_log.error("error: %s %s", reason, object)
|
||||
_log.error("error: %s %s", reason, object)
|
||||
|
||||
if reason == 'permissions':
|
||||
title = _("Permissions error")
|
||||
text = _("Found a Logitech Receiver (%s), but did not have permission to open it.") % object + \
|
||||
'\n\n' + \
|
||||
_("If you've just installed Solaar, try removing the receiver and plugging it back in.")
|
||||
elif reason == 'unpair':
|
||||
title = _("Unpairing failed")
|
||||
text = _("Failed to unpair %{device} from %{receiver}.").format(device=object.name, receiver=object.receiver.name) + \
|
||||
'\n\n' + \
|
||||
_("The receiver returned an error, with no further details.")
|
||||
else:
|
||||
raise Exception("ui.error_dialog: don't know how to handle (%s, %s)", reason, object)
|
||||
if reason == 'permissions':
|
||||
title = _("Permissions error")
|
||||
text = _("Found a Logitech Receiver (%s), but did not have permission to open it.") % object + \
|
||||
'\n\n' + \
|
||||
_("If you've just installed Solaar, try removing the receiver and plugging it back in.")
|
||||
elif reason == 'unpair':
|
||||
title = _("Unpairing failed")
|
||||
text = _("Failed to unpair %{device} from %{receiver}.").format(device=object.name, receiver=object.receiver.name) + \
|
||||
'\n\n' + \
|
||||
_("The receiver returned an error, with no further details.")
|
||||
else:
|
||||
raise Exception("ui.error_dialog: don't know how to handle (%s, %s)",
|
||||
reason, object)
|
||||
|
||||
assert title
|
||||
assert text
|
||||
assert title
|
||||
assert text
|
||||
|
||||
m = Gtk.MessageDialog(None, Gtk.DialogFlags.MODAL, Gtk.MessageType.ERROR, Gtk.ButtonsType.CLOSE, text)
|
||||
m.set_title(title)
|
||||
m.run()
|
||||
m.destroy()
|
||||
m = Gtk.MessageDialog(None, Gtk.DialogFlags.MODAL, Gtk.MessageType.ERROR,
|
||||
Gtk.ButtonsType.CLOSE, text)
|
||||
m.set_title(title)
|
||||
m.run()
|
||||
m.destroy()
|
||||
|
||||
|
||||
def error_dialog(reason, object):
|
||||
assert reason is not None
|
||||
GLib.idle_add(_error_dialog, reason, object)
|
||||
assert reason is not None
|
||||
GLib.idle_add(_error_dialog, reason, object)
|
||||
|
||||
|
||||
#
|
||||
#
|
||||
#
|
||||
|
||||
_task_runner = None
|
||||
|
||||
|
||||
def ui_async(function, *args, **kwargs):
|
||||
if _task_runner:
|
||||
_task_runner(function, *args, **kwargs)
|
||||
if _task_runner:
|
||||
_task_runner(function, *args, **kwargs)
|
||||
|
||||
|
||||
#
|
||||
#
|
||||
|
|
@ -87,65 +92,70 @@ from . import notify, tray, window
|
|||
|
||||
|
||||
def _startup(app, startup_hook, use_tray, show_window):
|
||||
if _log.isEnabledFor(_DEBUG):
|
||||
_log.debug("startup registered=%s, remote=%s", app.get_is_registered(), app.get_is_remote())
|
||||
if _log.isEnabledFor(_DEBUG):
|
||||
_log.debug("startup registered=%s, remote=%s", app.get_is_registered(),
|
||||
app.get_is_remote())
|
||||
|
||||
from solaar.tasks import TaskRunner as _TaskRunner
|
||||
global _task_runner
|
||||
_task_runner = _TaskRunner('AsyncUI')
|
||||
_task_runner.start()
|
||||
from solaar.tasks import TaskRunner as _TaskRunner
|
||||
global _task_runner
|
||||
_task_runner = _TaskRunner('AsyncUI')
|
||||
_task_runner.start()
|
||||
|
||||
notify.init()
|
||||
if use_tray:
|
||||
tray.init(lambda _ignore: window.destroy())
|
||||
window.init(show_window, use_tray)
|
||||
notify.init()
|
||||
if use_tray:
|
||||
tray.init(lambda _ignore: window.destroy())
|
||||
window.init(show_window, use_tray)
|
||||
|
||||
startup_hook()
|
||||
startup_hook()
|
||||
|
||||
|
||||
def _activate(app):
|
||||
if _log.isEnabledFor(_DEBUG):
|
||||
_log.debug("activate")
|
||||
if app.get_windows():
|
||||
window.popup()
|
||||
else:
|
||||
app.add_window(window._window)
|
||||
if _log.isEnabledFor(_DEBUG):
|
||||
_log.debug("activate")
|
||||
if app.get_windows():
|
||||
window.popup()
|
||||
else:
|
||||
app.add_window(window._window)
|
||||
|
||||
|
||||
def _command_line(app, command_line):
|
||||
if _log.isEnabledFor(_DEBUG):
|
||||
_log.debug("command_line %s", command_line.get_arguments())
|
||||
if _log.isEnabledFor(_DEBUG):
|
||||
_log.debug("command_line %s", command_line.get_arguments())
|
||||
|
||||
return 0
|
||||
return 0
|
||||
|
||||
|
||||
def _shutdown(app, shutdown_hook):
|
||||
if _log.isEnabledFor(_DEBUG):
|
||||
_log.debug("shutdown")
|
||||
if _log.isEnabledFor(_DEBUG):
|
||||
_log.debug("shutdown")
|
||||
|
||||
shutdown_hook()
|
||||
shutdown_hook()
|
||||
|
||||
# stop the async UI processor
|
||||
global _task_runner
|
||||
_task_runner.stop()
|
||||
_task_runner = None
|
||||
# stop the async UI processor
|
||||
global _task_runner
|
||||
_task_runner.stop()
|
||||
_task_runner = None
|
||||
|
||||
tray.destroy()
|
||||
notify.uninit()
|
||||
tray.destroy()
|
||||
notify.uninit()
|
||||
|
||||
|
||||
def run_loop(startup_hook, shutdown_hook, use_tray, show_window, args=None):
|
||||
assert use_tray or show_window, 'need either tray or visible window'
|
||||
# from gi.repository.Gio import ApplicationFlags as _ApplicationFlags
|
||||
APP_ID = 'io.github.pwr.solaar'
|
||||
application = Gtk.Application.new(APP_ID, 0) # _ApplicationFlags.HANDLES_COMMAND_LINE)
|
||||
assert use_tray or show_window, 'need either tray or visible window'
|
||||
# from gi.repository.Gio import ApplicationFlags as _ApplicationFlags
|
||||
APP_ID = 'io.github.pwr.solaar'
|
||||
application = Gtk.Application.new(
|
||||
APP_ID, 0) # _ApplicationFlags.HANDLES_COMMAND_LINE)
|
||||
|
||||
application.connect('startup', lambda app, startup_hook:_startup(app,startup_hook,use_tray,show_window), startup_hook)
|
||||
application.connect('command-line', _command_line)
|
||||
application.connect('activate', _activate)
|
||||
application.connect('shutdown', _shutdown, shutdown_hook)
|
||||
application.connect(
|
||||
'startup', lambda app, startup_hook: _startup(
|
||||
app, startup_hook, use_tray, show_window), startup_hook)
|
||||
application.connect('command-line', _command_line)
|
||||
application.connect('activate', _activate)
|
||||
application.connect('shutdown', _shutdown, shutdown_hook)
|
||||
|
||||
application.run(args)
|
||||
|
||||
application.run(args)
|
||||
|
||||
#
|
||||
#
|
||||
|
|
@ -155,20 +165,20 @@ from logitech_receiver.status import ALERT
|
|||
|
||||
|
||||
def _status_changed(device, alert, reason):
|
||||
assert device is not None
|
||||
if _log.isEnabledFor(_DEBUG):
|
||||
_log.debug("status changed: %s (%s) %s", device, alert, reason)
|
||||
assert device is not None
|
||||
if _log.isEnabledFor(_DEBUG):
|
||||
_log.debug("status changed: %s (%s) %s", device, alert, reason)
|
||||
|
||||
tray.update(device)
|
||||
if alert & ALERT.ATTENTION:
|
||||
tray.attention(reason)
|
||||
tray.update(device)
|
||||
if alert & ALERT.ATTENTION:
|
||||
tray.attention(reason)
|
||||
|
||||
need_popup = alert & ALERT.SHOW_WINDOW
|
||||
window.update(device, need_popup)
|
||||
need_popup = alert & ALERT.SHOW_WINDOW
|
||||
window.update(device, need_popup)
|
||||
|
||||
if alert & (ALERT.NOTIFICATION | ALERT.ATTENTION):
|
||||
notify.show(device, reason)
|
||||
if alert & (ALERT.NOTIFICATION | ALERT.ATTENTION):
|
||||
notify.show(device, reason)
|
||||
|
||||
|
||||
def status_changed(device, alert=ALERT.NONE, reason=None):
|
||||
GLib.idle_add(_status_changed, device, alert, reason)
|
||||
GLib.idle_add(_status_changed, device, alert, reason)
|
||||
|
|
|
|||
|
|
@ -32,64 +32,68 @@ _dialog = None
|
|||
|
||||
|
||||
def _create():
|
||||
about = Gtk.AboutDialog()
|
||||
about = Gtk.AboutDialog()
|
||||
|
||||
about.set_program_name(NAME)
|
||||
about.set_version(__version__)
|
||||
about.set_comments(_("Shows status of devices connected\nthrough wireless Logitech receivers."))
|
||||
about.set_program_name(NAME)
|
||||
about.set_version(__version__)
|
||||
about.set_comments(
|
||||
_("Shows status of devices connected\nthrough wireless Logitech receivers."
|
||||
))
|
||||
|
||||
about.set_logo_icon_name(NAME.lower())
|
||||
about.set_logo_icon_name(NAME.lower())
|
||||
|
||||
about.set_copyright('© 2012-2013 Daniel Pavel')
|
||||
about.set_license_type(Gtk.License.GPL_2_0)
|
||||
about.set_copyright('© 2012-2013 Daniel Pavel')
|
||||
about.set_license_type(Gtk.License.GPL_2_0)
|
||||
|
||||
about.set_authors(('Daniel Pavel http://github.com/pwr',))
|
||||
try:
|
||||
about.add_credit_section(_("GUI design"), ('Julien Gascard', 'Daniel Pavel'))
|
||||
about.add_credit_section(_("Testing"), (
|
||||
'Douglas Wagner',
|
||||
'Julien Gascard',
|
||||
'Peter Wu http://www.lekensteyn.nl/logitech-unifying.html',
|
||||
))
|
||||
about.add_credit_section(_("Logitech documentation"), (
|
||||
'Julien Danjou http://julien.danjou.info/blog/2012/logitech-unifying-upower',
|
||||
'Nestor Lopez Casado http://drive.google.com/folderview?id=0BxbRzx7vEV7eWmgwazJ3NUFfQ28',
|
||||
))
|
||||
except TypeError:
|
||||
# gtk3 < ~3.6.4 has incorrect gi bindings
|
||||
import logging
|
||||
logging.exception("failed to fully create the about dialog")
|
||||
except:
|
||||
# the Gtk3 version may be too old, and the function does not exist
|
||||
import logging
|
||||
logging.exception("failed to fully create the about dialog")
|
||||
about.set_authors(('Daniel Pavel http://github.com/pwr', ))
|
||||
try:
|
||||
about.add_credit_section(_("GUI design"),
|
||||
('Julien Gascard', 'Daniel Pavel'))
|
||||
about.add_credit_section(_("Testing"), (
|
||||
'Douglas Wagner',
|
||||
'Julien Gascard',
|
||||
'Peter Wu http://www.lekensteyn.nl/logitech-unifying.html',
|
||||
))
|
||||
about.add_credit_section(_("Logitech documentation"), (
|
||||
'Julien Danjou http://julien.danjou.info/blog/2012/logitech-unifying-upower',
|
||||
'Nestor Lopez Casado http://drive.google.com/folderview?id=0BxbRzx7vEV7eWmgwazJ3NUFfQ28',
|
||||
))
|
||||
except TypeError:
|
||||
# gtk3 < ~3.6.4 has incorrect gi bindings
|
||||
import logging
|
||||
logging.exception("failed to fully create the about dialog")
|
||||
except:
|
||||
# the Gtk3 version may be too old, and the function does not exist
|
||||
import logging
|
||||
logging.exception("failed to fully create the about dialog")
|
||||
|
||||
about.set_translator_credits('\n'.join((
|
||||
'gogo (croatian)',
|
||||
'Papoteur, David Geiger, Damien Lallement (français)',
|
||||
'Michele Olivo (italiano)',
|
||||
'Adrian Piotrowicz (polski)',
|
||||
'Drovetto, JrBenito (Portuguese-BR)',
|
||||
'Daniel Pavel (română)',
|
||||
'Daniel Zippert, Emelie Snecker (svensk)',
|
||||
'Dimitriy Ryazantcev (Russian)',
|
||||
)))
|
||||
about.set_translator_credits('\n'.join((
|
||||
'gogo (croatian)',
|
||||
'Papoteur, David Geiger, Damien Lallement (français)',
|
||||
'Michele Olivo (italiano)',
|
||||
'Adrian Piotrowicz (polski)',
|
||||
'Drovetto, JrBenito (Portuguese-BR)',
|
||||
'Daniel Pavel (română)',
|
||||
'Daniel Zippert, Emelie Snecker (svensk)',
|
||||
'Dimitriy Ryazantcev (Russian)',
|
||||
)))
|
||||
|
||||
about.set_website('http://pwr-solaar.github.io/Solaar/')
|
||||
about.set_website_label(NAME)
|
||||
about.set_website('http://pwr-solaar.github.io/Solaar/')
|
||||
about.set_website_label(NAME)
|
||||
|
||||
about.connect('response', lambda x, y: x.hide())
|
||||
about.connect('response', lambda x, y: x.hide())
|
||||
|
||||
def _hide(dialog, event):
|
||||
dialog.hide()
|
||||
return True
|
||||
about.connect('delete-event', _hide)
|
||||
def _hide(dialog, event):
|
||||
dialog.hide()
|
||||
return True
|
||||
|
||||
return about
|
||||
about.connect('delete-event', _hide)
|
||||
|
||||
return about
|
||||
|
||||
|
||||
def show_window(trigger=None):
|
||||
global _dialog
|
||||
if _dialog is None:
|
||||
_dialog = _create()
|
||||
_dialog.present()
|
||||
global _dialog
|
||||
if _dialog is None:
|
||||
_dialog = _create()
|
||||
_dialog.present()
|
||||
|
|
|
|||
|
|
@ -25,30 +25,31 @@ from gi.repository import Gtk, Gdk
|
|||
# _log = getLogger(__name__)
|
||||
# del getLogger
|
||||
|
||||
|
||||
from solaar.i18n import _
|
||||
|
||||
#
|
||||
#
|
||||
#
|
||||
|
||||
|
||||
def make(name, label, function, stock_id=None, *args):
|
||||
action = Gtk.Action(name, label, label, None)
|
||||
action.set_icon_name(name)
|
||||
if stock_id is not None:
|
||||
action.set_stock_id(stock_id)
|
||||
if function:
|
||||
action.connect('activate', function, *args)
|
||||
return action
|
||||
action = Gtk.Action(name, label, label, None)
|
||||
action.set_icon_name(name)
|
||||
if stock_id is not None:
|
||||
action.set_stock_id(stock_id)
|
||||
if function:
|
||||
action.connect('activate', function, *args)
|
||||
return action
|
||||
|
||||
|
||||
def make_toggle(name, label, function, stock_id=None, *args):
|
||||
action = Gtk.ToggleAction(name, label, label, None)
|
||||
action.set_icon_name(name)
|
||||
if stock_id is not None:
|
||||
action.set_stock_id(stock_id)
|
||||
action.connect('activate', function, *args)
|
||||
return action
|
||||
action = Gtk.ToggleAction(name, label, label, None)
|
||||
action.set_icon_name(name)
|
||||
if stock_id is not None:
|
||||
action.set_stock_id(stock_id)
|
||||
action.connect('activate', function, *args)
|
||||
return action
|
||||
|
||||
|
||||
#
|
||||
#
|
||||
|
|
@ -62,49 +63,55 @@ def make_toggle(name, label, function, stock_id=None, *args):
|
|||
# action.set_sensitive(notify.available)
|
||||
# toggle_notifications = make_toggle('notifications', 'Notifications', _toggle_notifications)
|
||||
|
||||
|
||||
from .about import show_window as _show_about_window
|
||||
from solaar import NAME
|
||||
about = make('help-about', _("About") + ' ' + NAME, _show_about_window, stock_id=Gtk.STOCK_ABOUT)
|
||||
about = make('help-about',
|
||||
_("About") + ' ' + NAME,
|
||||
_show_about_window,
|
||||
stock_id=Gtk.STOCK_ABOUT)
|
||||
|
||||
#
|
||||
#
|
||||
#
|
||||
|
||||
from . import pair_window
|
||||
def pair(window, receiver):
|
||||
assert receiver
|
||||
assert receiver.kind is None
|
||||
|
||||
pair_dialog = pair_window.create(receiver)
|
||||
pair_dialog.set_transient_for(window)
|
||||
pair_dialog.set_destroy_with_parent(True)
|
||||
pair_dialog.set_modal(True)
|
||||
pair_dialog.set_type_hint(Gdk.WindowTypeHint.DIALOG)
|
||||
pair_dialog.set_position(Gtk.WindowPosition.CENTER)
|
||||
pair_dialog.present()
|
||||
|
||||
def pair(window, receiver):
|
||||
assert receiver
|
||||
assert receiver.kind is None
|
||||
|
||||
pair_dialog = pair_window.create(receiver)
|
||||
pair_dialog.set_transient_for(window)
|
||||
pair_dialog.set_destroy_with_parent(True)
|
||||
pair_dialog.set_modal(True)
|
||||
pair_dialog.set_type_hint(Gdk.WindowTypeHint.DIALOG)
|
||||
pair_dialog.set_position(Gtk.WindowPosition.CENTER)
|
||||
pair_dialog.present()
|
||||
|
||||
|
||||
from ..ui import error_dialog
|
||||
|
||||
|
||||
def unpair(window, device):
|
||||
assert device
|
||||
assert device.kind is not None
|
||||
assert device
|
||||
assert device.kind is not None
|
||||
|
||||
qdialog = Gtk.MessageDialog(window, 0,
|
||||
Gtk.MessageType.QUESTION, Gtk.ButtonsType.NONE,
|
||||
_("Unpair") + ' ' + device.name + ' ?')
|
||||
qdialog.set_icon_name('remove')
|
||||
qdialog.add_button(Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL)
|
||||
qdialog.add_button(_("Unpair"), Gtk.ResponseType.ACCEPT)
|
||||
choice = qdialog.run()
|
||||
qdialog.destroy()
|
||||
if choice == Gtk.ResponseType.ACCEPT:
|
||||
receiver = device.receiver
|
||||
assert receiver
|
||||
device_number = device.number
|
||||
qdialog = Gtk.MessageDialog(window, 0, Gtk.MessageType.QUESTION,
|
||||
Gtk.ButtonsType.NONE,
|
||||
_("Unpair") + ' ' + device.name + ' ?')
|
||||
qdialog.set_icon_name('remove')
|
||||
qdialog.add_button(Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL)
|
||||
qdialog.add_button(_("Unpair"), Gtk.ResponseType.ACCEPT)
|
||||
choice = qdialog.run()
|
||||
qdialog.destroy()
|
||||
if choice == Gtk.ResponseType.ACCEPT:
|
||||
receiver = device.receiver
|
||||
assert receiver
|
||||
device_number = device.number
|
||||
|
||||
try:
|
||||
del receiver[device_number]
|
||||
except:
|
||||
# _log.exception("unpairing %s", device)
|
||||
error_dialog('unpair', device)
|
||||
try:
|
||||
del receiver[device_number]
|
||||
except:
|
||||
# _log.exception("unpairing %s", device)
|
||||
error_dialog('unpair', device)
|
||||
|
|
|
|||
|
|
@ -30,221 +30,238 @@ from logitech_receiver.settings import KIND as _SETTING_KIND
|
|||
#
|
||||
#
|
||||
|
||||
def _read_async(setting, force_read, sbox, device_is_online):
|
||||
def _do_read(s, force, sb, online):
|
||||
v = s.read(not force)
|
||||
GLib.idle_add(_update_setting_item, sb, v, online, priority=99)
|
||||
|
||||
_ui_async(_do_read, setting, force_read, sbox, device_is_online)
|
||||
def _read_async(setting, force_read, sbox, device_is_online):
|
||||
def _do_read(s, force, sb, online):
|
||||
v = s.read(not force)
|
||||
GLib.idle_add(_update_setting_item, sb, v, online, priority=99)
|
||||
|
||||
_ui_async(_do_read, setting, force_read, sbox, device_is_online)
|
||||
|
||||
|
||||
def _write_async(setting, value, sbox):
|
||||
_ignore, failed, spinner, control = sbox.get_children()
|
||||
control.set_sensitive(False)
|
||||
failed.set_visible(False)
|
||||
spinner.set_visible(True)
|
||||
spinner.start()
|
||||
_ignore, failed, spinner, control = sbox.get_children()
|
||||
control.set_sensitive(False)
|
||||
failed.set_visible(False)
|
||||
spinner.set_visible(True)
|
||||
spinner.start()
|
||||
|
||||
def _do_write(s, v, sb):
|
||||
v = setting.write(v)
|
||||
GLib.idle_add(_update_setting_item, sb, v, True, priority=99)
|
||||
def _do_write(s, v, sb):
|
||||
v = setting.write(v)
|
||||
GLib.idle_add(_update_setting_item, sb, v, True, priority=99)
|
||||
|
||||
_ui_async(_do_write, setting, value, sbox)
|
||||
_ui_async(_do_write, setting, value, sbox)
|
||||
|
||||
|
||||
def _write_async_key_value(setting, key, value, sbox):
|
||||
_ignore, failed, spinner, control = sbox.get_children()
|
||||
control.set_sensitive(False)
|
||||
failed.set_visible(False)
|
||||
spinner.set_visible(True)
|
||||
spinner.start()
|
||||
def _write_async_key_value(setting, key, value, sbox):
|
||||
_ignore, failed, spinner, control = sbox.get_children()
|
||||
control.set_sensitive(False)
|
||||
failed.set_visible(False)
|
||||
spinner.set_visible(True)
|
||||
spinner.start()
|
||||
|
||||
def _do_write_key_value(s, k, v, sb):
|
||||
v = setting.write_key_value(k, v)
|
||||
GLib.idle_add(_update_setting_item, sb, {k: v}, True, priority=99)
|
||||
def _do_write_key_value(s, k, v, sb):
|
||||
v = setting.write_key_value(k, v)
|
||||
GLib.idle_add(_update_setting_item, sb, {k: v}, True, priority=99)
|
||||
|
||||
_ui_async(_do_write_key_value, setting, key, value, sbox)
|
||||
_ui_async(_do_write_key_value, setting, key, value, sbox)
|
||||
|
||||
|
||||
#
|
||||
#
|
||||
#
|
||||
|
||||
|
||||
def _create_toggle_control(setting):
|
||||
def _switch_notify(switch, _ignore, s):
|
||||
if switch.get_sensitive():
|
||||
_write_async(s, switch.get_active() == True, switch.get_parent())
|
||||
def _switch_notify(switch, _ignore, s):
|
||||
if switch.get_sensitive():
|
||||
_write_async(s, switch.get_active() == True, switch.get_parent())
|
||||
|
||||
c = Gtk.Switch()
|
||||
c.connect('notify::active', _switch_notify, setting)
|
||||
return c
|
||||
|
||||
c = Gtk.Switch()
|
||||
c.connect('notify::active', _switch_notify, setting)
|
||||
return c
|
||||
|
||||
def _create_choice_control(setting):
|
||||
def _combo_notify(cbbox, s):
|
||||
if cbbox.get_sensitive():
|
||||
_write_async(s, cbbox.get_active_id(), cbbox.get_parent())
|
||||
def _combo_notify(cbbox, s):
|
||||
if cbbox.get_sensitive():
|
||||
_write_async(s, cbbox.get_active_id(), cbbox.get_parent())
|
||||
|
||||
c = Gtk.ComboBoxText()
|
||||
# TODO i18n text entries
|
||||
for entry in setting.choices:
|
||||
c.append(str(entry), str(entry))
|
||||
c.connect('changed', _combo_notify, setting)
|
||||
return c
|
||||
|
||||
c = Gtk.ComboBoxText()
|
||||
# TODO i18n text entries
|
||||
for entry in setting.choices:
|
||||
c.append(str(entry), str(entry))
|
||||
c.connect('changed', _combo_notify, setting)
|
||||
return c
|
||||
|
||||
def _create_map_choice_control(setting):
|
||||
def _map_value_notify_key(cbbox, s):
|
||||
setting, valueBox = s
|
||||
key_choice = int(cbbox.get_active_id())
|
||||
if cbbox.get_sensitive():
|
||||
valueBox.remove_all()
|
||||
_map_populate_value_box(valueBox, setting, key_choice)
|
||||
def _map_value_notify_key(cbbox, s):
|
||||
setting, valueBox = s
|
||||
key_choice = int(cbbox.get_active_id())
|
||||
if cbbox.get_sensitive():
|
||||
valueBox.remove_all()
|
||||
_map_populate_value_box(valueBox, setting, key_choice)
|
||||
|
||||
def _map_value_notify_value(cbbox, s):
|
||||
setting, keyBox = s
|
||||
key_choice = keyBox.get_active_id()
|
||||
if key_choice is not None and cbbox.get_sensitive() and cbbox.get_active_id():
|
||||
if setting._value.get(key_choice) != int(cbbox.get_active_id()):
|
||||
setting._value[key_choice] = int(cbbox.get_active_id())
|
||||
_write_async_key_value(setting, key_choice, setting._value[key_choice], cbbox.get_parent().get_parent())
|
||||
def _map_value_notify_value(cbbox, s):
|
||||
setting, keyBox = s
|
||||
key_choice = keyBox.get_active_id()
|
||||
if key_choice is not None and cbbox.get_sensitive(
|
||||
) and cbbox.get_active_id():
|
||||
if setting._value.get(key_choice) != int(cbbox.get_active_id()):
|
||||
setting._value[key_choice] = int(cbbox.get_active_id())
|
||||
_write_async_key_value(setting, key_choice,
|
||||
setting._value[key_choice],
|
||||
cbbox.get_parent().get_parent())
|
||||
|
||||
def _map_populate_value_box(valueBox, setting, key_choice):
|
||||
choices = None
|
||||
choices = setting.choices[key_choice]
|
||||
current = setting._value.get(str(key_choice)) # just in case the persisted value is missing some keys
|
||||
if choices:
|
||||
# TODO i18n text entries
|
||||
for choice in choices:
|
||||
valueBox.append(str(int(choice)), str(choice))
|
||||
if current is not None:
|
||||
valueBox.set_active_id(str(int(current)))
|
||||
def _map_populate_value_box(valueBox, setting, key_choice):
|
||||
choices = None
|
||||
choices = setting.choices[key_choice]
|
||||
current = setting._value.get(
|
||||
str(key_choice
|
||||
)) # just in case the persisted value is missing some keys
|
||||
if choices:
|
||||
# TODO i18n text entries
|
||||
for choice in choices:
|
||||
valueBox.append(str(int(choice)), str(choice))
|
||||
if current is not None:
|
||||
valueBox.set_active_id(str(int(current)))
|
||||
|
||||
c = Gtk.HBox(homogeneous=False, spacing=6)
|
||||
keyBox = Gtk.ComboBoxText()
|
||||
valueBox = Gtk.ComboBoxText()
|
||||
c.pack_start(keyBox, False, False, 0)
|
||||
c.pack_end(valueBox, False, False, 0)
|
||||
# TODO i18n text entries
|
||||
for entry in setting.choices:
|
||||
keyBox.append(str(int(entry)), str(entry))
|
||||
keyBox.set_active(0)
|
||||
keyBox.connect('changed', _map_value_notify_key, (setting, valueBox))
|
||||
_map_populate_value_box(valueBox, setting, int(keyBox.get_active_id()))
|
||||
valueBox.connect('changed', _map_value_notify_value, (setting, keyBox))
|
||||
return c
|
||||
|
||||
c = Gtk.HBox(homogeneous=False, spacing=6)
|
||||
keyBox = Gtk.ComboBoxText()
|
||||
valueBox = Gtk.ComboBoxText()
|
||||
c.pack_start(keyBox, False, False, 0)
|
||||
c.pack_end(valueBox, False, False, 0)
|
||||
# TODO i18n text entries
|
||||
for entry in setting.choices:
|
||||
keyBox.append(str(int(entry)), str(entry))
|
||||
keyBox.set_active(0)
|
||||
keyBox.connect('changed', _map_value_notify_key, (setting,valueBox))
|
||||
_map_populate_value_box(valueBox, setting, int(keyBox.get_active_id()))
|
||||
valueBox.connect('changed', _map_value_notify_value, (setting,keyBox))
|
||||
return c
|
||||
|
||||
def _create_slider_control(setting):
|
||||
class SliderControl:
|
||||
__slots__ = ('gtk_range', 'timer', 'setting')
|
||||
def __init__(self, setting):
|
||||
self.setting = setting
|
||||
self.timer = None
|
||||
class SliderControl:
|
||||
__slots__ = ('gtk_range', 'timer', 'setting')
|
||||
|
||||
self.gtk_range = Gtk.Scale()
|
||||
self.gtk_range.set_range(*self.setting.range)
|
||||
self.gtk_range.set_round_digits(0)
|
||||
self.gtk_range.set_digits(0)
|
||||
self.gtk_range.set_increments(1, 5)
|
||||
self.gtk_range.connect('value-changed',
|
||||
lambda _, c: c._changed(),
|
||||
self)
|
||||
def __init__(self, setting):
|
||||
self.setting = setting
|
||||
self.timer = None
|
||||
|
||||
def _write(self):
|
||||
_write_async(self.setting,
|
||||
int(self.gtk_range.get_value()),
|
||||
self.gtk_range.get_parent())
|
||||
self.timer.cancel()
|
||||
self.gtk_range = Gtk.Scale()
|
||||
self.gtk_range.set_range(*self.setting.range)
|
||||
self.gtk_range.set_round_digits(0)
|
||||
self.gtk_range.set_digits(0)
|
||||
self.gtk_range.set_increments(1, 5)
|
||||
self.gtk_range.connect('value-changed', lambda _, c: c._changed(),
|
||||
self)
|
||||
|
||||
def _changed(self):
|
||||
if self.gtk_range.get_sensitive():
|
||||
if self.timer:
|
||||
self.timer.cancel()
|
||||
self.timer = _Timer(0.5, lambda: GLib.idle_add(self._write))
|
||||
self.timer.start()
|
||||
def _write(self):
|
||||
_write_async(self.setting, int(self.gtk_range.get_value()),
|
||||
self.gtk_range.get_parent())
|
||||
self.timer.cancel()
|
||||
|
||||
def _changed(self):
|
||||
if self.gtk_range.get_sensitive():
|
||||
if self.timer:
|
||||
self.timer.cancel()
|
||||
self.timer = _Timer(0.5, lambda: GLib.idle_add(self._write))
|
||||
self.timer.start()
|
||||
|
||||
control = SliderControl(setting)
|
||||
return control.gtk_range
|
||||
|
||||
control = SliderControl(setting)
|
||||
return control.gtk_range
|
||||
|
||||
#
|
||||
#
|
||||
#
|
||||
|
||||
|
||||
def _create_sbox(s):
|
||||
sbox = Gtk.HBox(homogeneous=False, spacing=6)
|
||||
sbox.pack_start(Gtk.Label(s.label), False, False, 0)
|
||||
sbox = Gtk.HBox(homogeneous=False, spacing=6)
|
||||
sbox.pack_start(Gtk.Label(s.label), False, False, 0)
|
||||
|
||||
spinner = Gtk.Spinner()
|
||||
spinner.set_tooltip_text(_("Working") + '...')
|
||||
spinner = Gtk.Spinner()
|
||||
spinner.set_tooltip_text(_("Working") + '...')
|
||||
|
||||
failed = Gtk.Image.new_from_icon_name('dialog-warning', Gtk.IconSize.SMALL_TOOLBAR)
|
||||
failed.set_tooltip_text(_("Read/write operation failed."))
|
||||
failed = Gtk.Image.new_from_icon_name('dialog-warning',
|
||||
Gtk.IconSize.SMALL_TOOLBAR)
|
||||
failed.set_tooltip_text(_("Read/write operation failed."))
|
||||
|
||||
if s.kind == _SETTING_KIND.toggle:
|
||||
control = _create_toggle_control(s)
|
||||
sbox.pack_end(control, False, False, 0)
|
||||
elif s.kind == _SETTING_KIND.choice:
|
||||
control = _create_choice_control(s)
|
||||
sbox.pack_end(control, False, False, 0)
|
||||
elif s.kind == _SETTING_KIND.range:
|
||||
control = _create_slider_control(s)
|
||||
sbox.pack_end(control, True, True, 0)
|
||||
elif s.kind == _SETTING_KIND.map_choice:
|
||||
control = _create_map_choice_control(s)
|
||||
sbox.pack_end(control, True, True, 0)
|
||||
elif s.kind == _SETTING_KIND.multiple_toggle:
|
||||
# ugly temporary hack!
|
||||
choices = {k : [False, True] for k in s._validator.options}
|
||||
class X:
|
||||
def __init__(self, obj, ext):
|
||||
self.obj = obj
|
||||
self.ext = ext
|
||||
def __getattr__(self, attr):
|
||||
try:
|
||||
return self.ext[attr]
|
||||
except KeyError:
|
||||
return getattr(self.obj, attr)
|
||||
control = _create_map_choice_control(X(s, {'choices': choices}))
|
||||
sbox.pack_end(control, True, True, 0)
|
||||
else:
|
||||
raise Exception("NotImplemented")
|
||||
if s.kind == _SETTING_KIND.toggle:
|
||||
control = _create_toggle_control(s)
|
||||
sbox.pack_end(control, False, False, 0)
|
||||
elif s.kind == _SETTING_KIND.choice:
|
||||
control = _create_choice_control(s)
|
||||
sbox.pack_end(control, False, False, 0)
|
||||
elif s.kind == _SETTING_KIND.range:
|
||||
control = _create_slider_control(s)
|
||||
sbox.pack_end(control, True, True, 0)
|
||||
elif s.kind == _SETTING_KIND.map_choice:
|
||||
control = _create_map_choice_control(s)
|
||||
sbox.pack_end(control, True, True, 0)
|
||||
elif s.kind == _SETTING_KIND.multiple_toggle:
|
||||
# ugly temporary hack!
|
||||
choices = {k: [False, True] for k in s._validator.options}
|
||||
|
||||
control.set_sensitive(False) # the first read will enable it
|
||||
sbox.pack_end(spinner, False, False, 0)
|
||||
sbox.pack_end(failed, False, False, 0)
|
||||
class X:
|
||||
def __init__(self, obj, ext):
|
||||
self.obj = obj
|
||||
self.ext = ext
|
||||
|
||||
if s.description:
|
||||
sbox.set_tooltip_text(s.description)
|
||||
def __getattr__(self, attr):
|
||||
try:
|
||||
return self.ext[attr]
|
||||
except KeyError:
|
||||
return getattr(self.obj, attr)
|
||||
|
||||
sbox.show_all()
|
||||
spinner.start() # the first read will stop it
|
||||
failed.set_visible(False)
|
||||
control = _create_map_choice_control(X(s, {'choices': choices}))
|
||||
sbox.pack_end(control, True, True, 0)
|
||||
else:
|
||||
raise Exception("NotImplemented")
|
||||
|
||||
return sbox
|
||||
control.set_sensitive(False) # the first read will enable it
|
||||
sbox.pack_end(spinner, False, False, 0)
|
||||
sbox.pack_end(failed, False, False, 0)
|
||||
|
||||
if s.description:
|
||||
sbox.set_tooltip_text(s.description)
|
||||
|
||||
sbox.show_all()
|
||||
spinner.start() # the first read will stop it
|
||||
failed.set_visible(False)
|
||||
|
||||
return sbox
|
||||
|
||||
|
||||
def _update_setting_item(sbox, value, is_online=True):
|
||||
_ignore, failed, spinner, control = sbox.get_children() # depends on box layout
|
||||
spinner.set_visible(False)
|
||||
spinner.stop()
|
||||
_ignore, failed, spinner, control = sbox.get_children(
|
||||
) # depends on box layout
|
||||
spinner.set_visible(False)
|
||||
spinner.stop()
|
||||
|
||||
if value is None:
|
||||
control.set_sensitive(False)
|
||||
failed.set_visible(is_online)
|
||||
return
|
||||
if value is None:
|
||||
control.set_sensitive(False)
|
||||
failed.set_visible(is_online)
|
||||
return
|
||||
|
||||
failed.set_visible(False)
|
||||
if isinstance(control, Gtk.Switch):
|
||||
control.set_active(value)
|
||||
elif isinstance(control, Gtk.ComboBoxText):
|
||||
control.set_active_id(str(value))
|
||||
elif isinstance(control, Gtk.Scale):
|
||||
control.set_value(int(value))
|
||||
elif isinstance(control, Gtk.HBox):
|
||||
kbox, vbox = control.get_children() # depends on box layout
|
||||
if value.get(kbox.get_active_id()):
|
||||
vbox.set_active_id(str(value.get(kbox.get_active_id())))
|
||||
else:
|
||||
raise Exception("NotImplemented")
|
||||
control.set_sensitive(True)
|
||||
|
||||
failed.set_visible(False)
|
||||
if isinstance(control, Gtk.Switch):
|
||||
control.set_active(value)
|
||||
elif isinstance(control, Gtk.ComboBoxText):
|
||||
control.set_active_id(str(value))
|
||||
elif isinstance(control, Gtk.Scale):
|
||||
control.set_value(int(value))
|
||||
elif isinstance(control, Gtk.HBox):
|
||||
kbox, vbox = control.get_children() # depends on box layout
|
||||
if value.get(kbox.get_active_id()):
|
||||
vbox.set_active_id(str(value.get(kbox.get_active_id())))
|
||||
else:
|
||||
raise Exception("NotImplemented")
|
||||
control.set_sensitive(True)
|
||||
|
||||
#
|
||||
#
|
||||
|
|
@ -254,57 +271,58 @@ def _update_setting_item(sbox, value, is_online=True):
|
|||
_box = None
|
||||
_items = {}
|
||||
|
||||
|
||||
def create():
|
||||
global _box
|
||||
assert _box is None
|
||||
_box = Gtk.VBox(homogeneous=False, spacing=8)
|
||||
_box._last_device = None
|
||||
return _box
|
||||
global _box
|
||||
assert _box is None
|
||||
_box = Gtk.VBox(homogeneous=False, spacing=8)
|
||||
_box._last_device = None
|
||||
return _box
|
||||
|
||||
|
||||
def update(device, is_online=None):
|
||||
assert _box is not None
|
||||
assert device
|
||||
device_id = (device.receiver.path, device.number)
|
||||
if is_online is None:
|
||||
is_online = bool(device.online)
|
||||
assert _box is not None
|
||||
assert device
|
||||
device_id = (device.receiver.path, device.number)
|
||||
if is_online is None:
|
||||
is_online = bool(device.online)
|
||||
|
||||
# if the device changed since last update, clear the box first
|
||||
if device_id != _box._last_device:
|
||||
_box.set_visible(False)
|
||||
_box._last_device = device_id
|
||||
# if the device changed since last update, clear the box first
|
||||
if device_id != _box._last_device:
|
||||
_box.set_visible(False)
|
||||
_box._last_device = device_id
|
||||
|
||||
# hide controls belonging to other devices
|
||||
for k, sbox in _items.items():
|
||||
sbox = _items[k]
|
||||
sbox.set_visible(k[0:2] == device_id)
|
||||
# hide controls belonging to other devices
|
||||
for k, sbox in _items.items():
|
||||
sbox = _items[k]
|
||||
sbox.set_visible(k[0:2] == device_id)
|
||||
|
||||
for s in device.settings:
|
||||
k = (device_id[0], device_id[1], s.name)
|
||||
if k in _items:
|
||||
sbox = _items[k]
|
||||
else:
|
||||
sbox = _items[k] = _create_sbox(s)
|
||||
_box.pack_start(sbox, False, False, 0)
|
||||
for s in device.settings:
|
||||
k = (device_id[0], device_id[1], s.name)
|
||||
if k in _items:
|
||||
sbox = _items[k]
|
||||
else:
|
||||
sbox = _items[k] = _create_sbox(s)
|
||||
_box.pack_start(sbox, False, False, 0)
|
||||
|
||||
_read_async(s, False, sbox, is_online)
|
||||
_read_async(s, False, sbox, is_online)
|
||||
|
||||
_box.set_visible(True)
|
||||
_box.set_visible(True)
|
||||
|
||||
|
||||
def clean(device):
|
||||
"""Remove the controls for a given device serial.
|
||||
"""Remove the controls for a given device serial.
|
||||
Needed after the device has been unpaired.
|
||||
"""
|
||||
assert _box is not None
|
||||
device_id = (device.receiver.path, device.number)
|
||||
for k in list(_items.keys()):
|
||||
if k[0:2] == device_id:
|
||||
_box.remove(_items[k])
|
||||
del _items[k]
|
||||
assert _box is not None
|
||||
device_id = (device.receiver.path, device.number)
|
||||
for k in list(_items.keys()):
|
||||
if k[0:2] == device_id:
|
||||
_box.remove(_items[k])
|
||||
del _items[k]
|
||||
|
||||
|
||||
def destroy():
|
||||
global _box
|
||||
_box = None
|
||||
_items.clear()
|
||||
global _box
|
||||
_box = None
|
||||
_items.clear()
|
||||
|
|
|
|||
|
|
@ -46,92 +46,110 @@ TRAY_ATTENTION = 'solaar-attention'
|
|||
|
||||
|
||||
def _look_for_application_icons():
|
||||
import os.path as _path
|
||||
from os import environ as _environ
|
||||
import os.path as _path
|
||||
from os import environ as _environ
|
||||
|
||||
import sys as _sys
|
||||
if _log.isEnabledFor(_DEBUG):
|
||||
_log.debug("sys.path[0] = %s", _sys.path[0])
|
||||
prefix_share = _path.normpath(_path.join(_path.realpath(_sys.path[0]), '..'))
|
||||
src_share = _path.normpath(_path.join(_path.realpath(_sys.path[0]), '..', 'share'))
|
||||
local_share = _environ.get('XDG_DATA_HOME', _path.expanduser(_path.join('~', '.local', 'share')))
|
||||
data_dirs = _environ.get('XDG_DATA_DIRS', '/usr/local/share:/usr/share')
|
||||
repo_share = _path.normpath(_path.join(_path.dirname(__file__), '..', '..', '..', 'share'))
|
||||
setuptools_share = _path.normpath(_path.join(_path.dirname(__file__), '..', '..', 'share'))
|
||||
del _sys
|
||||
import sys as _sys
|
||||
if _log.isEnabledFor(_DEBUG):
|
||||
_log.debug("sys.path[0] = %s", _sys.path[0])
|
||||
prefix_share = _path.normpath(
|
||||
_path.join(_path.realpath(_sys.path[0]), '..'))
|
||||
src_share = _path.normpath(
|
||||
_path.join(_path.realpath(_sys.path[0]), '..', 'share'))
|
||||
local_share = _environ.get(
|
||||
'XDG_DATA_HOME', _path.expanduser(_path.join('~', '.local', 'share')))
|
||||
data_dirs = _environ.get('XDG_DATA_DIRS', '/usr/local/share:/usr/share')
|
||||
repo_share = _path.normpath(
|
||||
_path.join(_path.dirname(__file__), '..', '..', '..', 'share'))
|
||||
setuptools_share = _path.normpath(
|
||||
_path.join(_path.dirname(__file__), '..', '..', 'share'))
|
||||
del _sys
|
||||
|
||||
share_solaar = [prefix_share] + list(_path.join(x, 'solaar') for x in [src_share, local_share, setuptools_share, repo_share] + data_dirs.split(':'))
|
||||
for location in share_solaar:
|
||||
location = _path.join(location, 'icons')
|
||||
if _log.isEnabledFor(_DEBUG):
|
||||
_log.debug("looking for icons in %s", location)
|
||||
share_solaar = [prefix_share] + list(
|
||||
_path.join(x, 'solaar')
|
||||
for x in [src_share, local_share, setuptools_share, repo_share] +
|
||||
data_dirs.split(':'))
|
||||
for location in share_solaar:
|
||||
location = _path.join(location, 'icons')
|
||||
if _log.isEnabledFor(_DEBUG):
|
||||
_log.debug("looking for icons in %s", location)
|
||||
|
||||
if _path.exists(_path.join(location, TRAY_ATTENTION + '.svg')):
|
||||
yield location
|
||||
if _path.exists(_path.join(location, TRAY_ATTENTION + '.svg')):
|
||||
yield location
|
||||
|
||||
del _environ
|
||||
# del _path
|
||||
del _environ
|
||||
# del _path
|
||||
|
||||
|
||||
_default_theme = None
|
||||
_use_symbolic_icons = False
|
||||
|
||||
|
||||
def _init_icon_paths():
|
||||
global _default_theme
|
||||
if _default_theme:
|
||||
return
|
||||
global _default_theme
|
||||
if _default_theme:
|
||||
return
|
||||
|
||||
_default_theme = Gtk.IconTheme.get_default()
|
||||
for p in _look_for_application_icons():
|
||||
_default_theme.prepend_search_path(p)
|
||||
if _log.isEnabledFor(_DEBUG):
|
||||
_log.debug("icon theme paths: %s", _default_theme.get_search_path())
|
||||
_default_theme = Gtk.IconTheme.get_default()
|
||||
for p in _look_for_application_icons():
|
||||
_default_theme.prepend_search_path(p)
|
||||
if _log.isEnabledFor(_DEBUG):
|
||||
_log.debug("icon theme paths: %s", _default_theme.get_search_path())
|
||||
|
||||
if gtk.prefer_symbolic_battery_icons:
|
||||
if _default_theme.has_icon('battery-good-symbolic'):
|
||||
global _use_symbolic_icons
|
||||
_use_symbolic_icons = True
|
||||
return
|
||||
else:
|
||||
_log.warning("failed to detect symbolic icons")
|
||||
if not _default_theme.has_icon('battery-good'):
|
||||
_log.warning("failed to detect icons")
|
||||
|
||||
if gtk.prefer_symbolic_battery_icons:
|
||||
if _default_theme.has_icon('battery-good-symbolic'):
|
||||
global _use_symbolic_icons
|
||||
_use_symbolic_icons = True
|
||||
return
|
||||
else:
|
||||
_log.warning("failed to detect symbolic icons")
|
||||
if not _default_theme.has_icon('battery-good'):
|
||||
_log.warning("failed to detect icons")
|
||||
|
||||
#
|
||||
#
|
||||
#
|
||||
|
||||
|
||||
def battery(level=None, charging=False):
|
||||
icon_name = _battery_icon_name(level, charging)
|
||||
if not _default_theme.has_icon(icon_name):
|
||||
_log.warning("icon %s not found in current theme", icon_name)
|
||||
return TRAY_OKAY # use Solaar icon if battery icon not available
|
||||
elif _log.isEnabledFor(_DEBUG):
|
||||
_log.debug("battery icon for %s:%s = %s", level, charging, icon_name)
|
||||
return icon_name
|
||||
icon_name = _battery_icon_name(level, charging)
|
||||
if not _default_theme.has_icon(icon_name):
|
||||
_log.warning("icon %s not found in current theme", icon_name)
|
||||
return TRAY_OKAY # use Solaar icon if battery icon not available
|
||||
elif _log.isEnabledFor(_DEBUG):
|
||||
_log.debug("battery icon for %s:%s = %s", level, charging, icon_name)
|
||||
return icon_name
|
||||
|
||||
|
||||
# return first res where val >= guard
|
||||
# _first_res(val,((guard,res),...))
|
||||
def _first_res(val,pairs):
|
||||
return next((res for guard,res in pairs if val >= guard),None)
|
||||
def _first_res(val, pairs):
|
||||
return next((res for guard, res in pairs if val >= guard), None)
|
||||
|
||||
|
||||
def _battery_icon_name(level, charging):
|
||||
_init_icon_paths()
|
||||
_init_icon_paths()
|
||||
|
||||
if level is None or level < 0:
|
||||
return 'battery-missing' + ( '-symbolic' if _use_symbolic_icons else '' )
|
||||
if level is None or level < 0:
|
||||
return 'battery-missing' + ('-symbolic' if _use_symbolic_icons else '')
|
||||
|
||||
level_name = _first_res(level, ((90, 'full'), (50, 'good'), (20, 'low'),
|
||||
(5, 'caution'), (0, 'empty')))
|
||||
return 'battery-%s%s%s' % (level_name, '-charging' if charging else '',
|
||||
'-symbolic' if _use_symbolic_icons else '')
|
||||
|
||||
level_name = _first_res(level,((90,'full'), (50,'good'), (20,'low'), (5,'caution'), (0,'empty')))
|
||||
return 'battery-%s%s%s' % (level_name, '-charging' if charging else '', '-symbolic' if _use_symbolic_icons else '')
|
||||
|
||||
#
|
||||
#
|
||||
#
|
||||
|
||||
|
||||
def lux(level=None):
|
||||
if level is None or level < 0:
|
||||
return 'light_unknown'
|
||||
return 'light_%03d' % (20 * ((level + 50) // 100))
|
||||
if level is None or level < 0:
|
||||
return 'light_unknown'
|
||||
return 'light_%03d' % (20 * ((level + 50) // 100))
|
||||
|
||||
|
||||
#
|
||||
#
|
||||
|
|
@ -139,65 +157,66 @@ def lux(level=None):
|
|||
|
||||
_ICON_SETS = {}
|
||||
|
||||
|
||||
def device_icon_set(name='_', kind=None):
|
||||
icon_set = _ICON_SETS.get(name)
|
||||
if icon_set is None:
|
||||
icon_set = Gtk.IconSet.new()
|
||||
_ICON_SETS[name] = icon_set
|
||||
icon_set = _ICON_SETS.get(name)
|
||||
if icon_set is None:
|
||||
icon_set = Gtk.IconSet.new()
|
||||
_ICON_SETS[name] = icon_set
|
||||
|
||||
# names of possible icons, in reverse order of likelihood
|
||||
# the theme will hopefully pick up the most appropriate
|
||||
names = ['preferences-desktop-peripherals']
|
||||
if kind:
|
||||
if str(kind) == 'numpad':
|
||||
names += ('input-keyboard', 'input-dialpad')
|
||||
elif str(kind) == 'touchpad':
|
||||
names += ('input-mouse', 'input-tablet')
|
||||
elif str(kind) == 'trackball':
|
||||
names += ('input-mouse',)
|
||||
names += ('input-' + str(kind),)
|
||||
# names += (name.replace(' ', '-'),)
|
||||
# names of possible icons, in reverse order of likelihood
|
||||
# the theme will hopefully pick up the most appropriate
|
||||
names = ['preferences-desktop-peripherals']
|
||||
if kind:
|
||||
if str(kind) == 'numpad':
|
||||
names += ('input-keyboard', 'input-dialpad')
|
||||
elif str(kind) == 'touchpad':
|
||||
names += ('input-mouse', 'input-tablet')
|
||||
elif str(kind) == 'trackball':
|
||||
names += ('input-mouse', )
|
||||
names += ('input-' + str(kind), )
|
||||
# names += (name.replace(' ', '-'),)
|
||||
|
||||
source = Gtk.IconSource.new()
|
||||
for n in names:
|
||||
source.set_icon_name(n)
|
||||
icon_set.add_source(source)
|
||||
icon_set.names = names
|
||||
source = Gtk.IconSource.new()
|
||||
for n in names:
|
||||
source.set_icon_name(n)
|
||||
icon_set.add_source(source)
|
||||
icon_set.names = names
|
||||
|
||||
return icon_set
|
||||
return icon_set
|
||||
|
||||
|
||||
def device_icon_file(name, kind=None, size=_LARGE_SIZE):
|
||||
_init_icon_paths()
|
||||
_init_icon_paths()
|
||||
|
||||
icon_set = device_icon_set(name, kind)
|
||||
assert icon_set
|
||||
for n in reversed(icon_set.names):
|
||||
if _default_theme.has_icon(n):
|
||||
return _default_theme.lookup_icon(n, size, 0).get_filename()
|
||||
icon_set = device_icon_set(name, kind)
|
||||
assert icon_set
|
||||
for n in reversed(icon_set.names):
|
||||
if _default_theme.has_icon(n):
|
||||
return _default_theme.lookup_icon(n, size, 0).get_filename()
|
||||
|
||||
|
||||
def device_icon_name(name, kind=None):
|
||||
_init_icon_paths()
|
||||
_init_icon_paths()
|
||||
|
||||
icon_set = device_icon_set(name, kind)
|
||||
assert icon_set
|
||||
for n in reversed(icon_set.names):
|
||||
if _default_theme.has_icon(n):
|
||||
return n
|
||||
icon_set = device_icon_set(name, kind)
|
||||
assert icon_set
|
||||
for n in reversed(icon_set.names):
|
||||
if _default_theme.has_icon(n):
|
||||
return n
|
||||
|
||||
|
||||
def icon_file(name, size=_LARGE_SIZE):
|
||||
_init_icon_paths()
|
||||
_init_icon_paths()
|
||||
|
||||
# has_icon() somehow returned False while lookup_icon returns non-None.
|
||||
# I guess it happens because share/solaar/icons/ has no hicolor and
|
||||
# resolution subdirs
|
||||
theme_icon = _default_theme.lookup_icon(name, size, 0)
|
||||
if theme_icon:
|
||||
file_name = theme_icon.get_filename()
|
||||
# if _log.isEnabledFor(_DEBUG):
|
||||
# _log.debug("icon %s(%d) => %s", name, size, file_name)
|
||||
return file_name
|
||||
# has_icon() somehow returned False while lookup_icon returns non-None.
|
||||
# I guess it happens because share/solaar/icons/ has no hicolor and
|
||||
# resolution subdirs
|
||||
theme_icon = _default_theme.lookup_icon(name, size, 0)
|
||||
if theme_icon:
|
||||
file_name = theme_icon.get_filename()
|
||||
# if _log.isEnabledFor(_DEBUG):
|
||||
# _log.debug("icon %s(%d) => %s", name, size, file_name)
|
||||
return file_name
|
||||
|
||||
_log.warn("icon %s(%d) not found in current theme", name, size)
|
||||
_log.warn("icon %s(%d) not found in current theme", name, size)
|
||||
|
|
|
|||
|
|
@ -21,7 +21,6 @@
|
|||
|
||||
from __future__ import absolute_import, division, print_function, unicode_literals
|
||||
|
||||
|
||||
from solaar.i18n import _
|
||||
|
||||
#
|
||||
|
|
@ -29,125 +28,121 @@ from solaar.i18n import _
|
|||
#
|
||||
|
||||
try:
|
||||
import gi
|
||||
gi.require_version('Notify', '0.7')
|
||||
# this import is allowed to fail, in which case the entire feature is unavailable
|
||||
from gi.repository import Notify, GLib
|
||||
import gi
|
||||
gi.require_version('Notify', '0.7')
|
||||
# this import is allowed to fail, in which case the entire feature is unavailable
|
||||
from gi.repository import Notify, GLib
|
||||
|
||||
# assumed to be working since the import succeeded
|
||||
available = True
|
||||
# assumed to be working since the import succeeded
|
||||
available = True
|
||||
|
||||
except (ValueError, ImportError):
|
||||
available = False
|
||||
available = False
|
||||
|
||||
if available:
|
||||
from logging import getLogger, INFO as _INFO
|
||||
_log = getLogger(__name__)
|
||||
del getLogger
|
||||
from logging import getLogger, INFO as _INFO
|
||||
_log = getLogger(__name__)
|
||||
del getLogger
|
||||
|
||||
from solaar import NAME
|
||||
from . import icons as _icons
|
||||
from solaar import NAME
|
||||
from . import icons as _icons
|
||||
|
||||
# cache references to shown notifications here, so if another status comes
|
||||
# while its notification is still visible we don't create another one
|
||||
_notifications = {}
|
||||
# cache references to shown notifications here, so if another status comes
|
||||
# while its notification is still visible we don't create another one
|
||||
_notifications = {}
|
||||
|
||||
def init():
|
||||
"""Init the notifications system."""
|
||||
global available
|
||||
if available:
|
||||
if not Notify.is_initted():
|
||||
if _log.isEnabledFor(_INFO):
|
||||
_log.info("starting desktop notifications")
|
||||
try:
|
||||
return Notify.init(NAME)
|
||||
except:
|
||||
_log.exception("initializing desktop notifications")
|
||||
available = False
|
||||
return available and Notify.is_initted()
|
||||
def init():
|
||||
"""Init the notifications system."""
|
||||
global available
|
||||
if available:
|
||||
if not Notify.is_initted():
|
||||
if _log.isEnabledFor(_INFO):
|
||||
_log.info("starting desktop notifications")
|
||||
try:
|
||||
return Notify.init(NAME)
|
||||
except:
|
||||
_log.exception("initializing desktop notifications")
|
||||
available = False
|
||||
return available and Notify.is_initted()
|
||||
|
||||
def uninit():
|
||||
if available and Notify.is_initted():
|
||||
if _log.isEnabledFor(_INFO):
|
||||
_log.info("stopping desktop notifications")
|
||||
_notifications.clear()
|
||||
Notify.uninit()
|
||||
|
||||
def uninit():
|
||||
if available and Notify.is_initted():
|
||||
if _log.isEnabledFor(_INFO):
|
||||
_log.info("stopping desktop notifications")
|
||||
_notifications.clear()
|
||||
Notify.uninit()
|
||||
# def toggle(action):
|
||||
# if action.get_active():
|
||||
# init()
|
||||
# else:
|
||||
# uninit()
|
||||
# action.set_sensitive(available)
|
||||
# return action.get_active()
|
||||
|
||||
def alert(reason, icon=None):
|
||||
assert reason
|
||||
|
||||
# def toggle(action):
|
||||
# if action.get_active():
|
||||
# init()
|
||||
# else:
|
||||
# uninit()
|
||||
# action.set_sensitive(available)
|
||||
# return action.get_active()
|
||||
if available and Notify.is_initted():
|
||||
n = _notifications.get(NAME)
|
||||
if n is None:
|
||||
n = _notifications[NAME] = Notify.Notification()
|
||||
|
||||
# we need to use the filename here because the notifications daemon
|
||||
# is an external application that does not know about our icon sets
|
||||
icon_file = _icons.icon_file(NAME.lower()) if icon is None \
|
||||
else _icons.icon_file(icon)
|
||||
|
||||
def alert(reason, icon=None):
|
||||
assert reason
|
||||
n.update(NAME, reason, icon_file)
|
||||
n.set_urgency(Notify.Urgency.NORMAL)
|
||||
n.set_hint("desktop-entry", GLib.Variant('s', NAME.lower()))
|
||||
|
||||
if available and Notify.is_initted():
|
||||
n = _notifications.get(NAME)
|
||||
if n is None:
|
||||
n = _notifications[NAME] = Notify.Notification()
|
||||
try:
|
||||
# if _log.isEnabledFor(_DEBUG):
|
||||
# _log.debug("showing %s", n)
|
||||
n.show()
|
||||
except Exception:
|
||||
_log.exception("showing %s", n)
|
||||
|
||||
# we need to use the filename here because the notifications daemon
|
||||
# is an external application that does not know about our icon sets
|
||||
icon_file = _icons.icon_file(NAME.lower()) if icon is None \
|
||||
else _icons.icon_file(icon)
|
||||
def show(dev, reason=None, icon=None):
|
||||
"""Show a notification with title and text."""
|
||||
if available and Notify.is_initted():
|
||||
summary = dev.name
|
||||
|
||||
n.update(NAME, reason, icon_file)
|
||||
n.set_urgency(Notify.Urgency.NORMAL)
|
||||
n.set_hint("desktop-entry", GLib.Variant('s', NAME.lower()))
|
||||
# if a notification with same name is already visible, reuse it to avoid spamming
|
||||
n = _notifications.get(summary)
|
||||
if n is None:
|
||||
n = _notifications[summary] = Notify.Notification()
|
||||
|
||||
try:
|
||||
# if _log.isEnabledFor(_DEBUG):
|
||||
# _log.debug("showing %s", n)
|
||||
n.show()
|
||||
except Exception:
|
||||
_log.exception("showing %s", n)
|
||||
if reason:
|
||||
message = reason
|
||||
elif dev.status is None:
|
||||
message = _("unpaired")
|
||||
elif bool(dev.status):
|
||||
message = dev.status.to_string() or _("connected")
|
||||
else:
|
||||
message = _("offline")
|
||||
|
||||
# we need to use the filename here because the notifications daemon
|
||||
# is an external application that does not know about our icon sets
|
||||
icon_file = _icons.device_icon_file(dev.name, dev.kind) if icon is None \
|
||||
else _icons.icon_file(icon)
|
||||
|
||||
def show(dev, reason=None, icon=None):
|
||||
"""Show a notification with title and text."""
|
||||
if available and Notify.is_initted():
|
||||
summary = dev.name
|
||||
n.update(summary, message, icon_file)
|
||||
urgency = Notify.Urgency.LOW if dev.status else Notify.Urgency.NORMAL
|
||||
n.set_urgency(urgency)
|
||||
n.set_hint("desktop-entry", GLib.Variant('s', NAME.lower()))
|
||||
|
||||
# if a notification with same name is already visible, reuse it to avoid spamming
|
||||
n = _notifications.get(summary)
|
||||
if n is None:
|
||||
n = _notifications[summary] = Notify.Notification()
|
||||
|
||||
if reason:
|
||||
message = reason
|
||||
elif dev.status is None:
|
||||
message = _("unpaired")
|
||||
elif bool(dev.status):
|
||||
message = dev.status.to_string() or _("connected")
|
||||
else:
|
||||
message = _("offline")
|
||||
|
||||
# we need to use the filename here because the notifications daemon
|
||||
# is an external application that does not know about our icon sets
|
||||
icon_file = _icons.device_icon_file(dev.name, dev.kind) if icon is None \
|
||||
else _icons.icon_file(icon)
|
||||
|
||||
n.update(summary, message, icon_file)
|
||||
urgency = Notify.Urgency.LOW if dev.status else Notify.Urgency.NORMAL
|
||||
n.set_urgency(urgency)
|
||||
n.set_hint("desktop-entry", GLib.Variant('s', NAME.lower()))
|
||||
|
||||
try:
|
||||
# if _log.isEnabledFor(_DEBUG):
|
||||
# _log.debug("showing %s", n)
|
||||
n.show()
|
||||
except Exception:
|
||||
_log.exception("showing %s", n)
|
||||
try:
|
||||
# if _log.isEnabledFor(_DEBUG):
|
||||
# _log.debug("showing %s", n)
|
||||
n.show()
|
||||
except Exception:
|
||||
_log.exception("showing %s", n)
|
||||
|
||||
else:
|
||||
init = lambda: False
|
||||
uninit = lambda: None
|
||||
# toggle = lambda action: False
|
||||
alert = lambda reason: None
|
||||
show = lambda dev, reason=None: None
|
||||
init = lambda: False
|
||||
uninit = lambda: None
|
||||
# toggle = lambda action: False
|
||||
alert = lambda reason: None
|
||||
show = lambda dev, reason=None: None
|
||||
|
|
|
|||
|
|
@ -25,7 +25,6 @@ from logging import getLogger, DEBUG as _DEBUG
|
|||
_log = getLogger(__name__)
|
||||
del getLogger
|
||||
|
||||
|
||||
from solaar.i18n import _
|
||||
from . import icons as _icons
|
||||
from logitech_receiver.status import KEYS as _K
|
||||
|
|
@ -38,183 +37,201 @@ _STATUS_CHECK = 500 # milliseconds
|
|||
|
||||
|
||||
def _create_page(assistant, kind, header=None, icon_name=None, text=None):
|
||||
p = Gtk.VBox(False, 8)
|
||||
assistant.append_page(p)
|
||||
assistant.set_page_type(p, kind)
|
||||
p = Gtk.VBox(False, 8)
|
||||
assistant.append_page(p)
|
||||
assistant.set_page_type(p, kind)
|
||||
|
||||
if header:
|
||||
item = Gtk.HBox(False, 16)
|
||||
p.pack_start(item, False, True, 0)
|
||||
if header:
|
||||
item = Gtk.HBox(False, 16)
|
||||
p.pack_start(item, False, True, 0)
|
||||
|
||||
label = Gtk.Label(header)
|
||||
label.set_alignment(0, 0)
|
||||
label.set_line_wrap(True)
|
||||
item.pack_start(label, True, True, 0)
|
||||
label = Gtk.Label(header)
|
||||
label.set_alignment(0, 0)
|
||||
label.set_line_wrap(True)
|
||||
item.pack_start(label, True, True, 0)
|
||||
|
||||
if icon_name:
|
||||
icon = Gtk.Image.new_from_icon_name(icon_name, Gtk.IconSize.DIALOG)
|
||||
icon.set_alignment(1, 0)
|
||||
item.pack_start(icon, False, False, 0)
|
||||
if icon_name:
|
||||
icon = Gtk.Image.new_from_icon_name(icon_name, Gtk.IconSize.DIALOG)
|
||||
icon.set_alignment(1, 0)
|
||||
item.pack_start(icon, False, False, 0)
|
||||
|
||||
if text:
|
||||
label = Gtk.Label(text)
|
||||
label.set_alignment(0, 0)
|
||||
label.set_line_wrap(True)
|
||||
p.pack_start(label, False, False, 0)
|
||||
if text:
|
||||
label = Gtk.Label(text)
|
||||
label.set_alignment(0, 0)
|
||||
label.set_line_wrap(True)
|
||||
p.pack_start(label, False, False, 0)
|
||||
|
||||
p.show_all()
|
||||
return p
|
||||
p.show_all()
|
||||
return p
|
||||
|
||||
|
||||
def _check_lock_state(assistant, receiver, count=2):
|
||||
if not assistant.is_drawable():
|
||||
if _log.isEnabledFor(_DEBUG):
|
||||
_log.debug("assistant %s destroyed, bailing out", assistant)
|
||||
return False
|
||||
if not assistant.is_drawable():
|
||||
if _log.isEnabledFor(_DEBUG):
|
||||
_log.debug("assistant %s destroyed, bailing out", assistant)
|
||||
return False
|
||||
|
||||
if receiver.status.get(_K.ERROR):
|
||||
# receiver.status.new_device = _fake_device(receiver)
|
||||
_pairing_failed(assistant, receiver, receiver.status.pop(_K.ERROR))
|
||||
return False
|
||||
if receiver.status.get(_K.ERROR):
|
||||
# receiver.status.new_device = _fake_device(receiver)
|
||||
_pairing_failed(assistant, receiver, receiver.status.pop(_K.ERROR))
|
||||
return False
|
||||
|
||||
if receiver.status.new_device:
|
||||
device, receiver.status.new_device = receiver.status.new_device, None
|
||||
_pairing_succeeded(assistant, receiver, device)
|
||||
return False
|
||||
if receiver.status.new_device:
|
||||
device, receiver.status.new_device = receiver.status.new_device, None
|
||||
_pairing_succeeded(assistant, receiver, device)
|
||||
return False
|
||||
|
||||
if not receiver.status.lock_open:
|
||||
if count > 0:
|
||||
# the actual device notification may arrive after the lock was paired,
|
||||
# so have a little patience
|
||||
GLib.timeout_add(_STATUS_CHECK, _check_lock_state, assistant, receiver, count - 1)
|
||||
else:
|
||||
_pairing_failed(assistant, receiver, 'failed to open pairing lock')
|
||||
return False
|
||||
if not receiver.status.lock_open:
|
||||
if count > 0:
|
||||
# the actual device notification may arrive after the lock was paired,
|
||||
# so have a little patience
|
||||
GLib.timeout_add(_STATUS_CHECK, _check_lock_state, assistant,
|
||||
receiver, count - 1)
|
||||
else:
|
||||
_pairing_failed(assistant, receiver, 'failed to open pairing lock')
|
||||
return False
|
||||
|
||||
return True
|
||||
return True
|
||||
|
||||
|
||||
def _prepare(assistant, page, receiver):
|
||||
index = assistant.get_current_page()
|
||||
if _log.isEnabledFor(_DEBUG):
|
||||
_log.debug("prepare %s %d %s", assistant, index, page)
|
||||
index = assistant.get_current_page()
|
||||
if _log.isEnabledFor(_DEBUG):
|
||||
_log.debug("prepare %s %d %s", assistant, index, page)
|
||||
|
||||
if index == 0:
|
||||
if receiver.set_lock(False, timeout=_PAIRING_TIMEOUT):
|
||||
assert receiver.status.new_device is None
|
||||
assert receiver.status.get(_K.ERROR) is None
|
||||
spinner = page.get_children()[-1]
|
||||
spinner.start()
|
||||
GLib.timeout_add(_STATUS_CHECK, _check_lock_state, assistant, receiver)
|
||||
assistant.set_page_complete(page, True)
|
||||
else:
|
||||
GLib.idle_add(_pairing_failed, assistant, receiver, 'the pairing lock did not open')
|
||||
else:
|
||||
assistant.remove_page(0)
|
||||
if index == 0:
|
||||
if receiver.set_lock(False, timeout=_PAIRING_TIMEOUT):
|
||||
assert receiver.status.new_device is None
|
||||
assert receiver.status.get(_K.ERROR) is None
|
||||
spinner = page.get_children()[-1]
|
||||
spinner.start()
|
||||
GLib.timeout_add(_STATUS_CHECK, _check_lock_state, assistant,
|
||||
receiver)
|
||||
assistant.set_page_complete(page, True)
|
||||
else:
|
||||
GLib.idle_add(_pairing_failed, assistant, receiver,
|
||||
'the pairing lock did not open')
|
||||
else:
|
||||
assistant.remove_page(0)
|
||||
|
||||
|
||||
def _finish(assistant, receiver):
|
||||
if _log.isEnabledFor(_DEBUG):
|
||||
_log.debug("finish %s", assistant)
|
||||
assistant.destroy()
|
||||
receiver.status.new_device = None
|
||||
if receiver.status.lock_open:
|
||||
receiver.set_lock()
|
||||
else:
|
||||
receiver.status[_K.ERROR] = None
|
||||
if _log.isEnabledFor(_DEBUG):
|
||||
_log.debug("finish %s", assistant)
|
||||
assistant.destroy()
|
||||
receiver.status.new_device = None
|
||||
if receiver.status.lock_open:
|
||||
receiver.set_lock()
|
||||
else:
|
||||
receiver.status[_K.ERROR] = None
|
||||
|
||||
|
||||
def _pairing_failed(assistant, receiver, error):
|
||||
if _log.isEnabledFor(_DEBUG):
|
||||
_log.debug("%s fail: %s", receiver, error)
|
||||
if _log.isEnabledFor(_DEBUG):
|
||||
_log.debug("%s fail: %s", receiver, error)
|
||||
|
||||
assistant.commit()
|
||||
assistant.commit()
|
||||
|
||||
header = _("Pairing failed") + ': ' + _(str(error)) + '.'
|
||||
if 'timeout' in str(error):
|
||||
text = _("Make sure your device is within range, and has a decent battery charge.")
|
||||
elif str(error) == 'device not supported':
|
||||
text = _("A new device was detected, but it is not compatible with this receiver.")
|
||||
elif 'many' in str(error):
|
||||
text = _("The receiver only supports %d paired device(s).")
|
||||
else:
|
||||
text = _("No further details are available about the error.")
|
||||
_create_page(assistant, Gtk.AssistantPageType.SUMMARY, header, 'dialog-error', text)
|
||||
header = _("Pairing failed") + ': ' + _(str(error)) + '.'
|
||||
if 'timeout' in str(error):
|
||||
text = _(
|
||||
"Make sure your device is within range, and has a decent battery charge."
|
||||
)
|
||||
elif str(error) == 'device not supported':
|
||||
text = _(
|
||||
"A new device was detected, but it is not compatible with this receiver."
|
||||
)
|
||||
elif 'many' in str(error):
|
||||
text = _("The receiver only supports %d paired device(s).")
|
||||
else:
|
||||
text = _("No further details are available about the error.")
|
||||
_create_page(assistant, Gtk.AssistantPageType.SUMMARY, header,
|
||||
'dialog-error', text)
|
||||
|
||||
assistant.next_page()
|
||||
assistant.commit()
|
||||
assistant.next_page()
|
||||
assistant.commit()
|
||||
|
||||
|
||||
def _pairing_succeeded(assistant, receiver, device):
|
||||
assert device
|
||||
if _log.isEnabledFor(_DEBUG):
|
||||
_log.debug("%s success: %s", receiver, device)
|
||||
assert device
|
||||
if _log.isEnabledFor(_DEBUG):
|
||||
_log.debug("%s success: %s", receiver, device)
|
||||
|
||||
page = _create_page(assistant, Gtk.AssistantPageType.SUMMARY)
|
||||
page = _create_page(assistant, Gtk.AssistantPageType.SUMMARY)
|
||||
|
||||
header = Gtk.Label(_("Found a new device:"))
|
||||
header.set_alignment(0.5, 0)
|
||||
page.pack_start(header, False, False, 0)
|
||||
header = Gtk.Label(_("Found a new device:"))
|
||||
header.set_alignment(0.5, 0)
|
||||
page.pack_start(header, False, False, 0)
|
||||
|
||||
device_icon = Gtk.Image()
|
||||
icon_set = _icons.device_icon_set(device.name, device.kind)
|
||||
device_icon.set_from_icon_set(icon_set, Gtk.IconSize.LARGE)
|
||||
device_icon.set_alignment(0.5, 1)
|
||||
page.pack_start(device_icon, True, True, 0)
|
||||
device_icon = Gtk.Image()
|
||||
icon_set = _icons.device_icon_set(device.name, device.kind)
|
||||
device_icon.set_from_icon_set(icon_set, Gtk.IconSize.LARGE)
|
||||
device_icon.set_alignment(0.5, 1)
|
||||
page.pack_start(device_icon, True, True, 0)
|
||||
|
||||
device_label = Gtk.Label()
|
||||
device_label.set_markup('<b>%s</b>' % device.name)
|
||||
device_label.set_alignment(0.5, 0)
|
||||
page.pack_start(device_label, True, True, 0)
|
||||
device_label = Gtk.Label()
|
||||
device_label.set_markup('<b>%s</b>' % device.name)
|
||||
device_label.set_alignment(0.5, 0)
|
||||
page.pack_start(device_label, True, True, 0)
|
||||
|
||||
hbox = Gtk.HBox(False, 8)
|
||||
hbox.pack_start(Gtk.Label(' '), False, False, 0)
|
||||
hbox.set_property('expand', False)
|
||||
hbox.set_property('halign', Gtk.Align.CENTER)
|
||||
page.pack_start(hbox, False, False, 0)
|
||||
hbox = Gtk.HBox(False, 8)
|
||||
hbox.pack_start(Gtk.Label(' '), False, False, 0)
|
||||
hbox.set_property('expand', False)
|
||||
hbox.set_property('halign', Gtk.Align.CENTER)
|
||||
page.pack_start(hbox, False, False, 0)
|
||||
|
||||
def _check_encrypted(dev):
|
||||
if assistant.is_drawable():
|
||||
if device.status.get(_K.LINK_ENCRYPTED) == False:
|
||||
hbox.pack_start(Gtk.Image.new_from_icon_name('security-low', Gtk.IconSize.MENU), False, False, 0)
|
||||
hbox.pack_start(Gtk.Label(_("The wireless link is not encrypted") + '!'), False, False, 0)
|
||||
hbox.show_all()
|
||||
else:
|
||||
return True
|
||||
GLib.timeout_add(_STATUS_CHECK, _check_encrypted, device)
|
||||
def _check_encrypted(dev):
|
||||
if assistant.is_drawable():
|
||||
if device.status.get(_K.LINK_ENCRYPTED) == False:
|
||||
hbox.pack_start(
|
||||
Gtk.Image.new_from_icon_name('security-low',
|
||||
Gtk.IconSize.MENU), False,
|
||||
False, 0)
|
||||
hbox.pack_start(
|
||||
Gtk.Label(_("The wireless link is not encrypted") + '!'),
|
||||
False, False, 0)
|
||||
hbox.show_all()
|
||||
else:
|
||||
return True
|
||||
|
||||
page.show_all()
|
||||
GLib.timeout_add(_STATUS_CHECK, _check_encrypted, device)
|
||||
|
||||
assistant.next_page()
|
||||
assistant.commit()
|
||||
page.show_all()
|
||||
|
||||
assistant.next_page()
|
||||
assistant.commit()
|
||||
|
||||
|
||||
def create(receiver):
|
||||
assert receiver is not None
|
||||
assert receiver.kind is None
|
||||
assert receiver is not None
|
||||
assert receiver.kind is None
|
||||
|
||||
assistant = Gtk.Assistant()
|
||||
assistant.set_title(_('%(receiver_name)s: pair new device') % { 'receiver_name': receiver.name })
|
||||
assistant.set_icon_name('list-add')
|
||||
assistant = Gtk.Assistant()
|
||||
assistant.set_title(
|
||||
_('%(receiver_name)s: pair new device') %
|
||||
{'receiver_name': receiver.name})
|
||||
assistant.set_icon_name('list-add')
|
||||
|
||||
assistant.set_size_request(400, 240)
|
||||
assistant.set_resizable(False)
|
||||
assistant.set_role('pair-device')
|
||||
assistant.set_size_request(400, 240)
|
||||
assistant.set_resizable(False)
|
||||
assistant.set_role('pair-device')
|
||||
|
||||
page_text = _("If the device is already turned on, turn if off and on again.")
|
||||
if receiver.remaining_pairings() and receiver.remaining_pairings() >= 0:
|
||||
page_text += _("\n\nThis receiver has %d pairing(s) remaining.")%receiver.remaining_pairings()
|
||||
page_text += _("\nCancelling at this point will not use up a pairing.")
|
||||
page_text = _(
|
||||
"If the device is already turned on, turn if off and on again.")
|
||||
if receiver.remaining_pairings() and receiver.remaining_pairings() >= 0:
|
||||
page_text += _("\n\nThis receiver has %d pairing(s) remaining."
|
||||
) % receiver.remaining_pairings()
|
||||
page_text += _("\nCancelling at this point will not use up a pairing.")
|
||||
|
||||
page_intro = _create_page(assistant, Gtk.AssistantPageType.PROGRESS,
|
||||
_("Turn on the device you want to pair."), 'preferences-desktop-peripherals',
|
||||
page_text)
|
||||
spinner = Gtk.Spinner()
|
||||
spinner.set_visible(True)
|
||||
page_intro.pack_end(spinner, True, True, 24)
|
||||
page_intro = _create_page(assistant, Gtk.AssistantPageType.PROGRESS,
|
||||
_("Turn on the device you want to pair."),
|
||||
'preferences-desktop-peripherals', page_text)
|
||||
spinner = Gtk.Spinner()
|
||||
spinner.set_visible(True)
|
||||
page_intro.pack_end(spinner, True, True, 24)
|
||||
|
||||
assistant.connect('prepare', _prepare, receiver)
|
||||
assistant.connect('cancel', _finish, receiver)
|
||||
assistant.connect('close', _finish, receiver)
|
||||
assistant.connect('prepare', _prepare, receiver)
|
||||
assistant.connect('cancel', _finish, receiver)
|
||||
assistant.connect('close', _finish, receiver)
|
||||
|
||||
return assistant
|
||||
return assistant
|
||||
|
|
|
|||
|
|
@ -29,7 +29,6 @@ from time import time as _timestamp
|
|||
from gi.repository import Gtk, GLib
|
||||
from gi.repository.Gdk import ScrollDirection
|
||||
|
||||
|
||||
from solaar import NAME
|
||||
from solaar.i18n import _
|
||||
from logitech_receiver.status import KEYS as _K
|
||||
|
|
@ -40,7 +39,7 @@ from .window import popup as _window_popup, toggle as _window_toggle
|
|||
# constants
|
||||
#
|
||||
|
||||
_TRAY_ICON_SIZE = 32 # pixels
|
||||
_TRAY_ICON_SIZE = 32 # pixels
|
||||
_MENU_ICON_SIZE = Gtk.IconSize.LARGE_TOOLBAR
|
||||
_RECEIVER_SEPARATOR = ('~', None, None, None)
|
||||
|
||||
|
|
@ -48,418 +47,429 @@ _RECEIVER_SEPARATOR = ('~', None, None, None)
|
|||
#
|
||||
#
|
||||
|
||||
|
||||
def _create_menu(quit_handler):
|
||||
menu = Gtk.Menu()
|
||||
menu = Gtk.Menu()
|
||||
|
||||
# per-device menu entries will be generated as-needed
|
||||
# per-device menu entries will be generated as-needed
|
||||
|
||||
no_receiver = Gtk.MenuItem.new_with_label(_("No Logitech receiver found"))
|
||||
no_receiver.set_sensitive(False)
|
||||
menu.append(no_receiver)
|
||||
menu.append(Gtk.SeparatorMenuItem.new())
|
||||
no_receiver = Gtk.MenuItem.new_with_label(_("No Logitech receiver found"))
|
||||
no_receiver.set_sensitive(False)
|
||||
menu.append(no_receiver)
|
||||
menu.append(Gtk.SeparatorMenuItem.new())
|
||||
|
||||
from .action import about, make
|
||||
menu.append(about.create_menu_item())
|
||||
menu.append(make('application-exit', _("Quit"), quit_handler, stock_id=Gtk.STOCK_QUIT).create_menu_item())
|
||||
del about, make
|
||||
from .action import about, make
|
||||
menu.append(about.create_menu_item())
|
||||
menu.append(
|
||||
make('application-exit',
|
||||
_("Quit"),
|
||||
quit_handler,
|
||||
stock_id=Gtk.STOCK_QUIT).create_menu_item())
|
||||
del about, make
|
||||
|
||||
menu.show_all()
|
||||
menu.show_all()
|
||||
|
||||
return menu
|
||||
return menu
|
||||
|
||||
|
||||
_last_scroll = 0
|
||||
|
||||
|
||||
def _scroll(tray_icon, event, direction=None):
|
||||
if direction is None:
|
||||
direction = event.direction
|
||||
now = event.time / 1000.0
|
||||
else:
|
||||
now = None
|
||||
if direction is None:
|
||||
direction = event.direction
|
||||
now = event.time / 1000.0
|
||||
else:
|
||||
now = None
|
||||
|
||||
if direction != ScrollDirection.UP and direction != ScrollDirection.DOWN:
|
||||
# ignore all other directions
|
||||
return
|
||||
if direction != ScrollDirection.UP and direction != ScrollDirection.DOWN:
|
||||
# ignore all other directions
|
||||
return
|
||||
|
||||
if len(_devices_info) < 4:
|
||||
# don't bother with scrolling when there's only one receiver
|
||||
# with only one device (3 = [receiver, device, separator])
|
||||
return
|
||||
if len(_devices_info) < 4:
|
||||
# don't bother with scrolling when there's only one receiver
|
||||
# with only one device (3 = [receiver, device, separator])
|
||||
return
|
||||
|
||||
# scroll events come way too fast (at least 5-6 at once)
|
||||
# so take a little break between them
|
||||
global _last_scroll
|
||||
now = now or _timestamp()
|
||||
if now - _last_scroll < 0.33: # seconds
|
||||
return
|
||||
_last_scroll = now
|
||||
# scroll events come way too fast (at least 5-6 at once)
|
||||
# so take a little break between them
|
||||
global _last_scroll
|
||||
now = now or _timestamp()
|
||||
if now - _last_scroll < 0.33: # seconds
|
||||
return
|
||||
_last_scroll = now
|
||||
|
||||
# if _log.isEnabledFor(_DEBUG):
|
||||
# _log.debug("scroll direction %s", direction)
|
||||
# if _log.isEnabledFor(_DEBUG):
|
||||
# _log.debug("scroll direction %s", direction)
|
||||
|
||||
global _picked_device
|
||||
candidate = None
|
||||
global _picked_device
|
||||
candidate = None
|
||||
|
||||
if _picked_device is None:
|
||||
for info in _devices_info:
|
||||
# pick first peripheral found
|
||||
if info[1] is not None:
|
||||
candidate = info
|
||||
break
|
||||
else:
|
||||
found = False
|
||||
for info in _devices_info:
|
||||
if not info[1]:
|
||||
# only conside peripherals
|
||||
continue
|
||||
# compare peripherals
|
||||
if info[0:2] == _picked_device[0:2]:
|
||||
if direction == ScrollDirection.UP and candidate:
|
||||
# select previous device
|
||||
break
|
||||
found = True
|
||||
else:
|
||||
if found:
|
||||
candidate = info
|
||||
if direction == ScrollDirection.DOWN:
|
||||
break
|
||||
# if direction is up, but no candidate found before _picked,
|
||||
# let it run through all candidates, will get stuck with the last one
|
||||
else:
|
||||
if direction == ScrollDirection.DOWN:
|
||||
# only use the first one, in case no candidates are after _picked
|
||||
if candidate is None:
|
||||
candidate = info
|
||||
else:
|
||||
candidate = info
|
||||
if _picked_device is None:
|
||||
for info in _devices_info:
|
||||
# pick first peripheral found
|
||||
if info[1] is not None:
|
||||
candidate = info
|
||||
break
|
||||
else:
|
||||
found = False
|
||||
for info in _devices_info:
|
||||
if not info[1]:
|
||||
# only conside peripherals
|
||||
continue
|
||||
# compare peripherals
|
||||
if info[0:2] == _picked_device[0:2]:
|
||||
if direction == ScrollDirection.UP and candidate:
|
||||
# select previous device
|
||||
break
|
||||
found = True
|
||||
else:
|
||||
if found:
|
||||
candidate = info
|
||||
if direction == ScrollDirection.DOWN:
|
||||
break
|
||||
# if direction is up, but no candidate found before _picked,
|
||||
# let it run through all candidates, will get stuck with the last one
|
||||
else:
|
||||
if direction == ScrollDirection.DOWN:
|
||||
# only use the first one, in case no candidates are after _picked
|
||||
if candidate is None:
|
||||
candidate = info
|
||||
else:
|
||||
candidate = info
|
||||
|
||||
# if the last _picked_device is gone, clear it
|
||||
# the candidate will be either the first or last one remaining,
|
||||
# depending on the scroll direction
|
||||
if not found:
|
||||
_picked_device = None
|
||||
# if the last _picked_device is gone, clear it
|
||||
# the candidate will be either the first or last one remaining,
|
||||
# depending on the scroll direction
|
||||
if not found:
|
||||
_picked_device = None
|
||||
|
||||
_picked_device = candidate or _picked_device
|
||||
if _log.isEnabledFor(_DEBUG):
|
||||
_log.debug("scroll: picked %s", _picked_device)
|
||||
_update_tray_icon()
|
||||
_picked_device = candidate or _picked_device
|
||||
if _log.isEnabledFor(_DEBUG):
|
||||
_log.debug("scroll: picked %s", _picked_device)
|
||||
_update_tray_icon()
|
||||
|
||||
|
||||
try:
|
||||
import gi
|
||||
try:
|
||||
gi.require_version('AyatanaAppIndicator3', '0.1')
|
||||
ayatana_appindicator_found = True
|
||||
except ValueError:
|
||||
try:
|
||||
gi.require_version('AppIndicator3', '0.1')
|
||||
ayatana_appindicator_found = False
|
||||
except ValueError:
|
||||
# treat unavailable versions the same as unavailable packages
|
||||
raise ImportError
|
||||
import gi
|
||||
try:
|
||||
gi.require_version('AyatanaAppIndicator3', '0.1')
|
||||
ayatana_appindicator_found = True
|
||||
except ValueError:
|
||||
try:
|
||||
gi.require_version('AppIndicator3', '0.1')
|
||||
ayatana_appindicator_found = False
|
||||
except ValueError:
|
||||
# treat unavailable versions the same as unavailable packages
|
||||
raise ImportError
|
||||
|
||||
if ayatana_appindicator_found:
|
||||
from gi.repository import AyatanaAppIndicator3 as AppIndicator3
|
||||
else:
|
||||
from gi.repository import AppIndicator3
|
||||
if ayatana_appindicator_found:
|
||||
from gi.repository import AyatanaAppIndicator3 as AppIndicator3
|
||||
else:
|
||||
from gi.repository import AppIndicator3
|
||||
|
||||
if _log.isEnabledFor(_DEBUG):
|
||||
_log.debug("using %sAppIndicator3" % ('Ayatana ' if ayatana_appindicator_found else ''))
|
||||
if _log.isEnabledFor(_DEBUG):
|
||||
_log.debug("using %sAppIndicator3" %
|
||||
('Ayatana ' if ayatana_appindicator_found else ''))
|
||||
|
||||
# Defense against AppIndicator3 bug that treats files in current directory as icon files
|
||||
# https://bugs.launchpad.net/ubuntu/+source/libappindicator/+bug/1363277
|
||||
def _icon_file(icon_name):
|
||||
if not os.path.isfile(icon_name):
|
||||
return icon_name
|
||||
icon_info = Gtk.IconTheme.get_default().lookup_icon(icon_name,_TRAY_ICON_SIZE,0)
|
||||
return icon_info.get_filename() if icon_info else icon_name
|
||||
# Defense against AppIndicator3 bug that treats files in current directory as icon files
|
||||
# https://bugs.launchpad.net/ubuntu/+source/libappindicator/+bug/1363277
|
||||
def _icon_file(icon_name):
|
||||
if not os.path.isfile(icon_name):
|
||||
return icon_name
|
||||
icon_info = Gtk.IconTheme.get_default().lookup_icon(
|
||||
icon_name, _TRAY_ICON_SIZE, 0)
|
||||
return icon_info.get_filename() if icon_info else icon_name
|
||||
|
||||
def _create(menu):
|
||||
theme_paths = Gtk.IconTheme.get_default().get_search_path()
|
||||
def _create(menu):
|
||||
theme_paths = Gtk.IconTheme.get_default().get_search_path()
|
||||
|
||||
ind = AppIndicator3.Indicator.new_with_path(
|
||||
'indicator-solaar',
|
||||
_icon_file(_icons.TRAY_INIT),
|
||||
AppIndicator3.IndicatorCategory.HARDWARE,
|
||||
':'.join(theme_paths))
|
||||
ind.set_title(NAME)
|
||||
ind.set_status(AppIndicator3.IndicatorStatus.ACTIVE)
|
||||
ind.set_attention_icon_full(_icon_file(_icons.TRAY_ATTENTION), '')
|
||||
# ind.set_label(NAME, NAME)
|
||||
ind = AppIndicator3.Indicator.new_with_path(
|
||||
'indicator-solaar', _icon_file(_icons.TRAY_INIT),
|
||||
AppIndicator3.IndicatorCategory.HARDWARE, ':'.join(theme_paths))
|
||||
ind.set_title(NAME)
|
||||
ind.set_status(AppIndicator3.IndicatorStatus.ACTIVE)
|
||||
ind.set_attention_icon_full(_icon_file(_icons.TRAY_ATTENTION), '')
|
||||
# ind.set_label(NAME, NAME)
|
||||
|
||||
ind.set_menu(menu)
|
||||
ind.connect('scroll-event', _scroll)
|
||||
ind.set_menu(menu)
|
||||
ind.connect('scroll-event', _scroll)
|
||||
|
||||
return ind
|
||||
return ind
|
||||
|
||||
def _destroy(indicator):
|
||||
indicator.set_status(AppIndicator3.IndicatorStatus.PASSIVE)
|
||||
|
||||
def _destroy(indicator):
|
||||
indicator.set_status(AppIndicator3.IndicatorStatus.PASSIVE)
|
||||
def _update_tray_icon():
|
||||
if _picked_device:
|
||||
_ignore, _ignore, name, device_status = _picked_device
|
||||
battery_level = device_status.get(_K.BATTERY_LEVEL)
|
||||
battery_charging = device_status.get(_K.BATTERY_CHARGING)
|
||||
tray_icon_name = _icons.battery(battery_level, battery_charging)
|
||||
|
||||
description = '%s: %s' % (name, device_status.to_string())
|
||||
else:
|
||||
# there may be a receiver, but no peripherals
|
||||
tray_icon_name = _icons.TRAY_OKAY if _devices_info else _icons.TRAY_INIT
|
||||
|
||||
def _update_tray_icon():
|
||||
if _picked_device:
|
||||
_ignore, _ignore, name, device_status = _picked_device
|
||||
battery_level = device_status.get(_K.BATTERY_LEVEL)
|
||||
battery_charging = device_status.get(_K.BATTERY_CHARGING)
|
||||
tray_icon_name = _icons.battery(battery_level, battery_charging)
|
||||
description_lines = _generate_description_lines()
|
||||
description = '\n'.join(description_lines).rstrip('\n')
|
||||
|
||||
description = '%s: %s' % (name, device_status.to_string())
|
||||
else:
|
||||
# there may be a receiver, but no peripherals
|
||||
tray_icon_name = _icons.TRAY_OKAY if _devices_info else _icons.TRAY_INIT
|
||||
# icon_file = _icons.icon_file(icon_name, _TRAY_ICON_SIZE)
|
||||
_icon.set_icon_full(_icon_file(tray_icon_name), description)
|
||||
|
||||
description_lines = _generate_description_lines()
|
||||
description = '\n'.join(description_lines).rstrip('\n')
|
||||
def _update_menu_icon(image_widget, icon_name):
|
||||
image_widget.set_from_icon_name(icon_name, _MENU_ICON_SIZE)
|
||||
# icon_file = _icons.icon_file(icon_name, _MENU_ICON_SIZE)
|
||||
# image_widget.set_from_file(icon_file)
|
||||
# image_widget.set_pixel_size(_TRAY_ICON_SIZE)
|
||||
|
||||
# icon_file = _icons.icon_file(icon_name, _TRAY_ICON_SIZE)
|
||||
_icon.set_icon_full(_icon_file(tray_icon_name), description)
|
||||
|
||||
|
||||
def _update_menu_icon(image_widget, icon_name):
|
||||
image_widget.set_from_icon_name(icon_name, _MENU_ICON_SIZE)
|
||||
# icon_file = _icons.icon_file(icon_name, _MENU_ICON_SIZE)
|
||||
# image_widget.set_from_file(icon_file)
|
||||
# image_widget.set_pixel_size(_TRAY_ICON_SIZE)
|
||||
|
||||
|
||||
def attention(reason=None):
|
||||
if _icon.get_status() != AppIndicator3.IndicatorStatus.ATTENTION:
|
||||
_icon.set_attention_icon_full(_icon_file(_icons.TRAY_ATTENTION), reason or '')
|
||||
_icon.set_status(AppIndicator3.IndicatorStatus.ATTENTION)
|
||||
GLib.timeout_add(10 * 1000, _icon.set_status, AppIndicator3.IndicatorStatus.ACTIVE)
|
||||
def attention(reason=None):
|
||||
if _icon.get_status() != AppIndicator3.IndicatorStatus.ATTENTION:
|
||||
_icon.set_attention_icon_full(_icon_file(_icons.TRAY_ATTENTION),
|
||||
reason or '')
|
||||
_icon.set_status(AppIndicator3.IndicatorStatus.ATTENTION)
|
||||
GLib.timeout_add(10 * 1000, _icon.set_status,
|
||||
AppIndicator3.IndicatorStatus.ACTIVE)
|
||||
|
||||
except ImportError:
|
||||
|
||||
if _log.isEnabledFor(_DEBUG):
|
||||
_log.debug("using StatusIcon")
|
||||
if _log.isEnabledFor(_DEBUG):
|
||||
_log.debug("using StatusIcon")
|
||||
|
||||
def _create(menu):
|
||||
icon = Gtk.StatusIcon.new_from_icon_name(_icons.TRAY_INIT)
|
||||
icon.set_name(NAME)
|
||||
icon.set_title(NAME)
|
||||
icon.set_tooltip_text(NAME)
|
||||
icon.connect('activate', _window_toggle)
|
||||
icon.connect('scroll-event', _scroll)
|
||||
icon.connect('popup-menu',
|
||||
lambda icon, button, time: menu.popup(None, None, icon.position_menu, icon, button, time))
|
||||
def _create(menu):
|
||||
icon = Gtk.StatusIcon.new_from_icon_name(_icons.TRAY_INIT)
|
||||
icon.set_name(NAME)
|
||||
icon.set_title(NAME)
|
||||
icon.set_tooltip_text(NAME)
|
||||
icon.connect('activate', _window_toggle)
|
||||
icon.connect('scroll-event', _scroll)
|
||||
icon.connect(
|
||||
'popup-menu', lambda icon, button, time: menu.popup(
|
||||
None, None, icon.position_menu, icon, button, time))
|
||||
|
||||
return icon
|
||||
return icon
|
||||
|
||||
def _destroy(icon):
|
||||
icon.set_visible(False)
|
||||
|
||||
def _destroy(icon):
|
||||
icon.set_visible(False)
|
||||
def _update_tray_icon():
|
||||
tooltip_lines = _generate_tooltip_lines()
|
||||
tooltip = '\n'.join(tooltip_lines).rstrip('\n')
|
||||
_icon.set_tooltip_markup(tooltip)
|
||||
|
||||
if _picked_device:
|
||||
_ignore, _ignore, name, device_status = _picked_device
|
||||
battery_level = device_status.get(_K.BATTERY_LEVEL)
|
||||
battery_charging = device_status.get(_K.BATTERY_CHARGING)
|
||||
tray_icon_name = _icons.battery(battery_level, battery_charging)
|
||||
else:
|
||||
# there may be a receiver, but no peripherals
|
||||
tray_icon_name = _icons.TRAY_OKAY if _devices_info else _icons.TRAY_ATTENTION
|
||||
_icon.set_from_icon_name(tray_icon_name)
|
||||
|
||||
def _update_tray_icon():
|
||||
tooltip_lines = _generate_tooltip_lines()
|
||||
tooltip = '\n'.join(tooltip_lines).rstrip('\n')
|
||||
_icon.set_tooltip_markup(tooltip)
|
||||
def _update_menu_icon(image_widget, icon_name):
|
||||
image_widget.set_from_icon_name(icon_name, _MENU_ICON_SIZE)
|
||||
|
||||
if _picked_device:
|
||||
_ignore, _ignore, name, device_status = _picked_device
|
||||
battery_level = device_status.get(_K.BATTERY_LEVEL)
|
||||
battery_charging = device_status.get(_K.BATTERY_CHARGING)
|
||||
tray_icon_name = _icons.battery(battery_level, battery_charging)
|
||||
else:
|
||||
# there may be a receiver, but no peripherals
|
||||
tray_icon_name = _icons.TRAY_OKAY if _devices_info else _icons.TRAY_ATTENTION
|
||||
_icon.set_from_icon_name(tray_icon_name)
|
||||
_icon_before_attention = None
|
||||
|
||||
def _blink(count):
|
||||
global _icon_before_attention
|
||||
if count % 2:
|
||||
_icon.set_from_icon_name(_icons.TRAY_ATTENTION)
|
||||
else:
|
||||
_icon.set_from_icon_name(_icon_before_attention)
|
||||
|
||||
def _update_menu_icon(image_widget, icon_name):
|
||||
image_widget.set_from_icon_name(icon_name, _MENU_ICON_SIZE)
|
||||
if count > 0:
|
||||
GLib.timeout_add(1000, _blink, count - 1)
|
||||
else:
|
||||
_icon_before_attention = None
|
||||
|
||||
def attention(reason=None):
|
||||
global _icon_before_attention
|
||||
if _icon_before_attention is None:
|
||||
_icon_before_attention = _icon.get_icon_name()
|
||||
GLib.idle_add(_blink, 9)
|
||||
|
||||
_icon_before_attention = None
|
||||
|
||||
def _blink(count):
|
||||
global _icon_before_attention
|
||||
if count % 2:
|
||||
_icon.set_from_icon_name(_icons.TRAY_ATTENTION)
|
||||
else:
|
||||
_icon.set_from_icon_name(_icon_before_attention)
|
||||
|
||||
if count > 0:
|
||||
GLib.timeout_add(1000, _blink, count - 1)
|
||||
else:
|
||||
_icon_before_attention = None
|
||||
|
||||
def attention(reason=None):
|
||||
global _icon_before_attention
|
||||
if _icon_before_attention is None:
|
||||
_icon_before_attention = _icon.get_icon_name()
|
||||
GLib.idle_add(_blink, 9)
|
||||
|
||||
#
|
||||
#
|
||||
#
|
||||
|
||||
|
||||
def _generate_tooltip_lines():
|
||||
if not _devices_info:
|
||||
yield '<b>%s</b>: ' % NAME + _("no receiver")
|
||||
return
|
||||
if not _devices_info:
|
||||
yield '<b>%s</b>: ' % NAME + _("no receiver")
|
||||
return
|
||||
|
||||
yield from _generate_description_lines()
|
||||
yield from _generate_description_lines()
|
||||
|
||||
|
||||
def _generate_description_lines():
|
||||
if not _devices_info:
|
||||
yield _("no receiver")
|
||||
return
|
||||
if not _devices_info:
|
||||
yield _("no receiver")
|
||||
return
|
||||
|
||||
for _ignore, number, name, status in _devices_info:
|
||||
if number is None: # receiver
|
||||
continue
|
||||
for _ignore, number, name, status in _devices_info:
|
||||
if number is None: # receiver
|
||||
continue
|
||||
|
||||
p = status.to_string()
|
||||
if p: # does it have any properties to print?
|
||||
yield '<b>%s</b>' % name
|
||||
if status:
|
||||
yield '\t%s' % p
|
||||
else:
|
||||
yield '\t%s <small>(' % p + _("offline") + ')</small>'
|
||||
else:
|
||||
if status:
|
||||
yield '<b>%s</b> <small>(' % name + _("no status") + ')</small>'
|
||||
else:
|
||||
yield '<b>%s</b> <small>(' % name + _("offline") + ')</small>'
|
||||
yield ''
|
||||
p = status.to_string()
|
||||
if p: # does it have any properties to print?
|
||||
yield '<b>%s</b>' % name
|
||||
if status:
|
||||
yield '\t%s' % p
|
||||
else:
|
||||
yield '\t%s <small>(' % p + _("offline") + ')</small>'
|
||||
else:
|
||||
if status:
|
||||
yield '<b>%s</b> <small>(' % name + _(
|
||||
"no status") + ')</small>'
|
||||
else:
|
||||
yield '<b>%s</b> <small>(' % name + _("offline") + ')</small>'
|
||||
yield ''
|
||||
|
||||
|
||||
def _pick_device_with_lowest_battery():
|
||||
if not _devices_info:
|
||||
return None
|
||||
if not _devices_info:
|
||||
return None
|
||||
|
||||
picked = None
|
||||
picked_level = 1000
|
||||
picked = None
|
||||
picked_level = 1000
|
||||
|
||||
for info in _devices_info:
|
||||
if info[1] is None: # is receiver/separator
|
||||
continue
|
||||
level = info[-1].get(_K.BATTERY_LEVEL)
|
||||
# print ("checking %s -> %s", info, level)
|
||||
if level is not None and picked_level > level:
|
||||
picked = info
|
||||
picked_level = level or 0
|
||||
for info in _devices_info:
|
||||
if info[1] is None: # is receiver/separator
|
||||
continue
|
||||
level = info[-1].get(_K.BATTERY_LEVEL)
|
||||
# print ("checking %s -> %s", info, level)
|
||||
if level is not None and picked_level > level:
|
||||
picked = info
|
||||
picked_level = level or 0
|
||||
|
||||
if _log.isEnabledFor(_DEBUG):
|
||||
_log.debug("picked device with lowest battery: %s", picked)
|
||||
if _log.isEnabledFor(_DEBUG):
|
||||
_log.debug("picked device with lowest battery: %s", picked)
|
||||
|
||||
return picked
|
||||
return picked
|
||||
|
||||
|
||||
#
|
||||
#
|
||||
#
|
||||
|
||||
|
||||
def _add_device(device):
|
||||
assert device
|
||||
assert device.receiver
|
||||
receiver_path = device.receiver.path
|
||||
assert receiver_path
|
||||
assert device
|
||||
assert device.receiver
|
||||
receiver_path = device.receiver.path
|
||||
assert receiver_path
|
||||
|
||||
index = None
|
||||
for idx, (path, _ignore, _ignore, _ignore) in enumerate(_devices_info):
|
||||
if path == receiver_path:
|
||||
# the first entry matching the receiver serial should be for the receiver itself
|
||||
index = idx + 1
|
||||
break
|
||||
assert index is not None
|
||||
index = None
|
||||
for idx, (path, _ignore, _ignore, _ignore) in enumerate(_devices_info):
|
||||
if path == receiver_path:
|
||||
# the first entry matching the receiver serial should be for the receiver itself
|
||||
index = idx + 1
|
||||
break
|
||||
assert index is not None
|
||||
|
||||
# proper ordering (according to device.number) for a receiver's devices
|
||||
while True:
|
||||
path, number, _ignore, _ignore = _devices_info[index]
|
||||
if path == _RECEIVER_SEPARATOR[0]:
|
||||
break
|
||||
assert path == receiver_path
|
||||
assert number != device.number
|
||||
if number > device.number:
|
||||
break
|
||||
index = index + 1
|
||||
# proper ordering (according to device.number) for a receiver's devices
|
||||
while True:
|
||||
path, number, _ignore, _ignore = _devices_info[index]
|
||||
if path == _RECEIVER_SEPARATOR[0]:
|
||||
break
|
||||
assert path == receiver_path
|
||||
assert number != device.number
|
||||
if number > device.number:
|
||||
break
|
||||
index = index + 1
|
||||
|
||||
new_device_info = (receiver_path, device.number, device.name, device.status)
|
||||
assert len(new_device_info) == len(_RECEIVER_SEPARATOR)
|
||||
_devices_info.insert(index, new_device_info)
|
||||
new_device_info = (receiver_path, device.number, device.name,
|
||||
device.status)
|
||||
assert len(new_device_info) == len(_RECEIVER_SEPARATOR)
|
||||
_devices_info.insert(index, new_device_info)
|
||||
|
||||
# label_prefix = b'\xE2\x94\x84 '.decode('utf-8')
|
||||
label_prefix = ' '
|
||||
# label_prefix = b'\xE2\x94\x84 '.decode('utf-8')
|
||||
label_prefix = ' '
|
||||
|
||||
new_menu_item = Gtk.ImageMenuItem.new_with_label(label_prefix + device.name)
|
||||
new_menu_item.set_image(Gtk.Image())
|
||||
new_menu_item.show_all()
|
||||
new_menu_item.connect('activate', _window_popup, receiver_path, device.number)
|
||||
_menu.insert(new_menu_item, index)
|
||||
new_menu_item = Gtk.ImageMenuItem.new_with_label(label_prefix +
|
||||
device.name)
|
||||
new_menu_item.set_image(Gtk.Image())
|
||||
new_menu_item.show_all()
|
||||
new_menu_item.connect('activate', _window_popup, receiver_path,
|
||||
device.number)
|
||||
_menu.insert(new_menu_item, index)
|
||||
|
||||
return index
|
||||
return index
|
||||
|
||||
|
||||
def _remove_device(index):
|
||||
assert index is not None
|
||||
assert index is not None
|
||||
|
||||
menu_items = _menu.get_children()
|
||||
_menu.remove(menu_items[index])
|
||||
menu_items = _menu.get_children()
|
||||
_menu.remove(menu_items[index])
|
||||
|
||||
removed_device = _devices_info.pop(index)
|
||||
global _picked_device
|
||||
if _picked_device and _picked_device[0:2] == removed_device[0:2]:
|
||||
# the current pick was unpaired
|
||||
_picked_device = None
|
||||
removed_device = _devices_info.pop(index)
|
||||
global _picked_device
|
||||
if _picked_device and _picked_device[0:2] == removed_device[0:2]:
|
||||
# the current pick was unpaired
|
||||
_picked_device = None
|
||||
|
||||
|
||||
def _add_receiver(receiver):
|
||||
index = len(_devices_info)
|
||||
index = len(_devices_info)
|
||||
|
||||
new_receiver_info = (receiver.path, None, receiver.name, None)
|
||||
assert len(new_receiver_info) == len(_RECEIVER_SEPARATOR)
|
||||
_devices_info.append(new_receiver_info)
|
||||
new_receiver_info = (receiver.path, None, receiver.name, None)
|
||||
assert len(new_receiver_info) == len(_RECEIVER_SEPARATOR)
|
||||
_devices_info.append(new_receiver_info)
|
||||
|
||||
new_menu_item = Gtk.ImageMenuItem.new_with_label(receiver.name)
|
||||
_menu.insert(new_menu_item, index)
|
||||
icon_set = _icons.device_icon_set(receiver.name)
|
||||
new_menu_item.set_image(Gtk.Image().new_from_icon_set(icon_set, _MENU_ICON_SIZE))
|
||||
new_menu_item.show_all()
|
||||
new_menu_item.connect('activate', _window_popup, receiver.path)
|
||||
new_menu_item = Gtk.ImageMenuItem.new_with_label(receiver.name)
|
||||
_menu.insert(new_menu_item, index)
|
||||
icon_set = _icons.device_icon_set(receiver.name)
|
||||
new_menu_item.set_image(Gtk.Image().new_from_icon_set(
|
||||
icon_set, _MENU_ICON_SIZE))
|
||||
new_menu_item.show_all()
|
||||
new_menu_item.connect('activate', _window_popup, receiver.path)
|
||||
|
||||
_devices_info.append(_RECEIVER_SEPARATOR)
|
||||
separator = Gtk.SeparatorMenuItem.new()
|
||||
separator.set_visible(True)
|
||||
_menu.insert(separator, index + 1)
|
||||
_devices_info.append(_RECEIVER_SEPARATOR)
|
||||
separator = Gtk.SeparatorMenuItem.new()
|
||||
separator.set_visible(True)
|
||||
_menu.insert(separator, index + 1)
|
||||
|
||||
return 0
|
||||
return 0
|
||||
|
||||
|
||||
def _remove_receiver(receiver):
|
||||
index = 0
|
||||
found = False
|
||||
index = 0
|
||||
found = False
|
||||
|
||||
# remove all entries in devices_info that match this receiver
|
||||
while index < len(_devices_info):
|
||||
path, _ignore, _ignore, _ignore = _devices_info[index]
|
||||
if path == receiver.path:
|
||||
found = True
|
||||
_remove_device(index)
|
||||
elif found and path == _RECEIVER_SEPARATOR[0]:
|
||||
# the separator after this receiver
|
||||
_remove_device(index)
|
||||
break
|
||||
else:
|
||||
index += 1
|
||||
# remove all entries in devices_info that match this receiver
|
||||
while index < len(_devices_info):
|
||||
path, _ignore, _ignore, _ignore = _devices_info[index]
|
||||
if path == receiver.path:
|
||||
found = True
|
||||
_remove_device(index)
|
||||
elif found and path == _RECEIVER_SEPARATOR[0]:
|
||||
# the separator after this receiver
|
||||
_remove_device(index)
|
||||
break
|
||||
else:
|
||||
index += 1
|
||||
|
||||
|
||||
def _update_menu_item(index, device):
|
||||
assert device
|
||||
assert device.status is not None
|
||||
assert device
|
||||
assert device.status is not None
|
||||
|
||||
menu_items = _menu.get_children()
|
||||
menu_item = menu_items[index]
|
||||
menu_items = _menu.get_children()
|
||||
menu_item = menu_items[index]
|
||||
|
||||
level = device.status.get(_K.BATTERY_LEVEL)
|
||||
charging = device.status.get(_K.BATTERY_CHARGING)
|
||||
icon_name = _icons.battery(level, charging)
|
||||
level = device.status.get(_K.BATTERY_LEVEL)
|
||||
charging = device.status.get(_K.BATTERY_CHARGING)
|
||||
icon_name = _icons.battery(level, charging)
|
||||
|
||||
image_widget = menu_item.get_image()
|
||||
image_widget.set_sensitive(bool(device.online))
|
||||
_update_menu_icon(image_widget, icon_name)
|
||||
|
||||
image_widget = menu_item.get_image()
|
||||
image_widget.set_sensitive(bool(device.online))
|
||||
_update_menu_icon(image_widget, icon_name)
|
||||
|
||||
#
|
||||
#
|
||||
|
|
@ -476,73 +486,77 @@ _devices_info = []
|
|||
_menu = None
|
||||
_icon = None
|
||||
|
||||
|
||||
def init(_quit_handler):
|
||||
global _menu, _icon
|
||||
assert _menu is None
|
||||
_menu = _create_menu(_quit_handler)
|
||||
assert _icon is None
|
||||
_icon = _create(_menu)
|
||||
global _menu, _icon
|
||||
assert _menu is None
|
||||
_menu = _create_menu(_quit_handler)
|
||||
assert _icon is None
|
||||
_icon = _create(_menu)
|
||||
|
||||
|
||||
def destroy():
|
||||
global _icon, _menu, _devices_info
|
||||
if _icon is not None:
|
||||
i, _icon = _icon, None
|
||||
_destroy(i)
|
||||
i = None
|
||||
global _icon, _menu, _devices_info
|
||||
if _icon is not None:
|
||||
i, _icon = _icon, None
|
||||
_destroy(i)
|
||||
i = None
|
||||
|
||||
_icon = None
|
||||
_menu = None
|
||||
_devices_info = None
|
||||
_icon = None
|
||||
_menu = None
|
||||
_devices_info = None
|
||||
|
||||
|
||||
def update(device=None):
|
||||
if _icon is None:
|
||||
return
|
||||
if _icon is None:
|
||||
return
|
||||
|
||||
if device is not None:
|
||||
if device.kind is None:
|
||||
# receiver
|
||||
is_alive = bool(device)
|
||||
receiver_path = device.path
|
||||
if is_alive:
|
||||
index = None
|
||||
for idx, (path, _ignore, _ignore, _ignore) in enumerate(_devices_info):
|
||||
if path == receiver_path:
|
||||
index = idx
|
||||
break
|
||||
if device is not None:
|
||||
if device.kind is None:
|
||||
# receiver
|
||||
is_alive = bool(device)
|
||||
receiver_path = device.path
|
||||
if is_alive:
|
||||
index = None
|
||||
for idx, (path, _ignore, _ignore,
|
||||
_ignore) in enumerate(_devices_info):
|
||||
if path == receiver_path:
|
||||
index = idx
|
||||
break
|
||||
|
||||
if index is None:
|
||||
_add_receiver(device)
|
||||
else:
|
||||
_remove_receiver(device)
|
||||
if index is None:
|
||||
_add_receiver(device)
|
||||
else:
|
||||
_remove_receiver(device)
|
||||
|
||||
else:
|
||||
# peripheral
|
||||
is_paired = bool(device)
|
||||
receiver_path = device.receiver.path
|
||||
index = None
|
||||
for idx, (path, number, _ignore, _ignore) in enumerate(_devices_info):
|
||||
if path == receiver_path and number == device.number:
|
||||
index = idx
|
||||
else:
|
||||
# peripheral
|
||||
is_paired = bool(device)
|
||||
receiver_path = device.receiver.path
|
||||
index = None
|
||||
for idx, (path, number, _ignore,
|
||||
_ignore) in enumerate(_devices_info):
|
||||
if path == receiver_path and number == device.number:
|
||||
index = idx
|
||||
|
||||
if is_paired:
|
||||
if index is None:
|
||||
index = _add_device(device)
|
||||
_update_menu_item(index, device)
|
||||
else:
|
||||
# was just unpaired
|
||||
if index:
|
||||
_remove_device(index)
|
||||
if is_paired:
|
||||
if index is None:
|
||||
index = _add_device(device)
|
||||
_update_menu_item(index, device)
|
||||
else:
|
||||
# was just unpaired
|
||||
if index:
|
||||
_remove_device(index)
|
||||
|
||||
menu_items = _menu.get_children()
|
||||
no_receivers_index = len(_devices_info)
|
||||
menu_items[no_receivers_index].set_visible(not _devices_info)
|
||||
menu_items[no_receivers_index + 1].set_visible(not _devices_info)
|
||||
menu_items = _menu.get_children()
|
||||
no_receivers_index = len(_devices_info)
|
||||
menu_items[no_receivers_index].set_visible(not _devices_info)
|
||||
menu_items[no_receivers_index + 1].set_visible(not _devices_info)
|
||||
|
||||
global _picked_device
|
||||
if (not _picked_device or _last_scroll == 0) and device is not None and device.kind is not None:
|
||||
# if it's just a receiver update, it's unlikely the picked device would change
|
||||
_picked_device = _pick_device_with_lowest_battery()
|
||||
global _picked_device
|
||||
if (not _picked_device or _last_scroll
|
||||
== 0) and device is not None and device.kind is not None:
|
||||
# if it's just a receiver update, it's unlikely the picked device would change
|
||||
_picked_device = _pick_device_with_lowest_battery()
|
||||
|
||||
_update_tray_icon()
|
||||
_update_tray_icon()
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -28,61 +28,74 @@ del getLogger
|
|||
#
|
||||
|
||||
_suspend_callback = None
|
||||
|
||||
|
||||
def _suspend():
|
||||
if _suspend_callback:
|
||||
if _log.isEnabledFor(_INFO):
|
||||
_log.info("received suspend event")
|
||||
_suspend_callback()
|
||||
if _suspend_callback:
|
||||
if _log.isEnabledFor(_INFO):
|
||||
_log.info("received suspend event")
|
||||
_suspend_callback()
|
||||
|
||||
|
||||
_resume_callback = None
|
||||
|
||||
|
||||
def _resume():
|
||||
if _resume_callback:
|
||||
if _log.isEnabledFor(_INFO):
|
||||
_log.info("received resume event")
|
||||
_resume_callback()
|
||||
if _resume_callback:
|
||||
if _log.isEnabledFor(_INFO):
|
||||
_log.info("received resume event")
|
||||
_resume_callback()
|
||||
|
||||
|
||||
def _suspend_or_resume(suspend):
|
||||
_suspend() if suspend else _resume()
|
||||
_suspend() if suspend else _resume()
|
||||
|
||||
|
||||
def watch(on_resume_callback=None, on_suspend_callback=None):
|
||||
"""Register callback for suspend/resume events.
|
||||
"""Register callback for suspend/resume events.
|
||||
They are called only if the system DBus is running, and the UPower daemon is available."""
|
||||
global _resume_callback, _suspend_callback
|
||||
_suspend_callback = on_suspend_callback
|
||||
_resume_callback = on_resume_callback
|
||||
global _resume_callback, _suspend_callback
|
||||
_suspend_callback = on_suspend_callback
|
||||
_resume_callback = on_resume_callback
|
||||
|
||||
|
||||
try:
|
||||
import dbus
|
||||
import dbus
|
||||
|
||||
_UPOWER_BUS = 'org.freedesktop.UPower'
|
||||
_UPOWER_INTERFACE = 'org.freedesktop.UPower'
|
||||
_LOGIND_BUS = 'org.freedesktop.login1'
|
||||
_LOGIND_INTERFACE = 'org.freedesktop.login1.Manager'
|
||||
_UPOWER_BUS = 'org.freedesktop.UPower'
|
||||
_UPOWER_INTERFACE = 'org.freedesktop.UPower'
|
||||
_LOGIND_BUS = 'org.freedesktop.login1'
|
||||
_LOGIND_INTERFACE = 'org.freedesktop.login1.Manager'
|
||||
|
||||
# integration into the main GLib loop
|
||||
from dbus.mainloop.glib import DBusGMainLoop
|
||||
DBusGMainLoop(set_as_default=True)
|
||||
# integration into the main GLib loop
|
||||
from dbus.mainloop.glib import DBusGMainLoop
|
||||
DBusGMainLoop(set_as_default=True)
|
||||
|
||||
bus = dbus.SystemBus()
|
||||
assert bus
|
||||
bus = dbus.SystemBus()
|
||||
assert bus
|
||||
|
||||
bus.add_signal_receiver(_suspend, signal_name='Sleeping',
|
||||
dbus_interface=_UPOWER_INTERFACE, bus_name=_UPOWER_BUS)
|
||||
bus.add_signal_receiver(_suspend,
|
||||
signal_name='Sleeping',
|
||||
dbus_interface=_UPOWER_INTERFACE,
|
||||
bus_name=_UPOWER_BUS)
|
||||
|
||||
bus.add_signal_receiver(_resume, signal_name='Resuming',
|
||||
dbus_interface=_UPOWER_INTERFACE, bus_name=_UPOWER_BUS)
|
||||
bus.add_signal_receiver(_resume,
|
||||
signal_name='Resuming',
|
||||
dbus_interface=_UPOWER_INTERFACE,
|
||||
bus_name=_UPOWER_BUS)
|
||||
|
||||
bus.add_signal_receiver(_suspend_or_resume,'PrepareForSleep',
|
||||
dbus_interface=_LOGIND_INTERFACE, bus_name=_LOGIND_BUS)
|
||||
bus.add_signal_receiver(_suspend_or_resume,
|
||||
'PrepareForSleep',
|
||||
dbus_interface=_LOGIND_INTERFACE,
|
||||
bus_name=_LOGIND_BUS)
|
||||
|
||||
if _log.isEnabledFor(_INFO):
|
||||
_log.info("connected to system dbus, watching for suspend/resume events")
|
||||
if _log.isEnabledFor(_INFO):
|
||||
_log.info(
|
||||
"connected to system dbus, watching for suspend/resume events")
|
||||
|
||||
except:
|
||||
# Either:
|
||||
# - the dbus library is not available
|
||||
# - the system dbus is not running
|
||||
_log.warn("failed to register suspend/resume callbacks")
|
||||
pass
|
||||
# Either:
|
||||
# - the dbus library is not available
|
||||
# - the system dbus is not running
|
||||
_log.warn("failed to register suspend/resume callbacks")
|
||||
pass
|
||||
|
|
|
|||
89
setup.py
89
setup.py
|
|
@ -14,56 +14,61 @@ NAME = 'Solaar'
|
|||
|
||||
|
||||
def _data_files():
|
||||
from os.path import dirname as _dirname
|
||||
from os.path import dirname as _dirname
|
||||
|
||||
yield 'share/solaar/icons', _glob('share/solaar/icons/solaar*.svg')
|
||||
yield 'share/solaar/icons', _glob('share/solaar/icons/light_*.png')
|
||||
yield 'share/icons/hicolor/scalable/apps', ['share/solaar/icons/solaar.svg']
|
||||
yield 'share/solaar/icons', _glob('share/solaar/icons/solaar*.svg')
|
||||
yield 'share/solaar/icons', _glob('share/solaar/icons/light_*.png')
|
||||
yield 'share/icons/hicolor/scalable/apps', [
|
||||
'share/solaar/icons/solaar.svg'
|
||||
]
|
||||
|
||||
for mo in _glob('share/locale/*/LC_MESSAGES/solaar.mo'):
|
||||
yield _dirname(mo), [mo]
|
||||
for mo in _glob('share/locale/*/LC_MESSAGES/solaar.mo'):
|
||||
yield _dirname(mo), [mo]
|
||||
|
||||
yield 'share/applications', ['share/applications/solaar.desktop']
|
||||
yield autostart_path, ['share/autostart/solaar.desktop']
|
||||
yield '/etc/udev/rules.d', ['rules.d/42-logitech-unify-permissions.rules']
|
||||
yield 'share/applications', ['share/applications/solaar.desktop']
|
||||
yield autostart_path, ['share/autostart/solaar.desktop']
|
||||
yield '/etc/udev/rules.d', ['rules.d/42-logitech-unify-permissions.rules']
|
||||
|
||||
del _dirname
|
||||
del _dirname
|
||||
|
||||
|
||||
setup(name=NAME.lower(),
|
||||
version=__version__,
|
||||
description='Linux devices manager for the Logitech Unifying Receiver.',
|
||||
long_description='''
|
||||
setup(
|
||||
name=NAME.lower(),
|
||||
version=__version__,
|
||||
description='Linux devices manager for the Logitech Unifying Receiver.',
|
||||
long_description='''
|
||||
Solaar is a Linux device manager for Logitech's Unifying Receiver peripherals.
|
||||
It is able to pair/unpair devices with the receiver, for many devices show
|
||||
battery status, and show and modify some of the modifiable features of devices.
|
||||
'''.strip(),
|
||||
author='Daniel Pavel',
|
||||
license='GPLv2',
|
||||
url='http://pwr-solaar.github.io/Solaar/',
|
||||
classifiers=[
|
||||
'Development Status :: 4 - Beta',
|
||||
'Environment :: X11 Applications :: GTK',
|
||||
'Environment :: Console',
|
||||
'Intended Audience :: End Users/Desktop',
|
||||
'License :: DFSG approved',
|
||||
'License :: OSI Approved :: GNU General Public License v2 (GPLv2)',
|
||||
'Natural Language :: English',
|
||||
'Programming Language :: Python :: 3 :: Only',
|
||||
'Operating System :: POSIX :: Linux',
|
||||
'Topic :: Utilities',
|
||||
],
|
||||
author='Daniel Pavel',
|
||||
license='GPLv2',
|
||||
url='http://pwr-solaar.github.io/Solaar/',
|
||||
classifiers=[
|
||||
'Development Status :: 4 - Beta',
|
||||
'Environment :: X11 Applications :: GTK',
|
||||
'Environment :: Console',
|
||||
'Intended Audience :: End Users/Desktop',
|
||||
'License :: DFSG approved',
|
||||
'License :: OSI Approved :: GNU General Public License v2 (GPLv2)',
|
||||
'Natural Language :: English',
|
||||
'Programming Language :: Python :: 3 :: Only',
|
||||
'Operating System :: POSIX :: Linux',
|
||||
'Topic :: Utilities',
|
||||
],
|
||||
platforms=['linux'],
|
||||
|
||||
platforms=['linux'],
|
||||
|
||||
# sudo apt install python-gi python3-gi \
|
||||
# gir1.2-gtk-3.0 gir1.2-notify-0.7 gir1.2-ayatanaappindicator3-0.1
|
||||
# os_requires=['gi.repository.GObject (>= 2.0)', 'gi.repository.Gtk (>= 3.0)'],
|
||||
|
||||
python_requires='>=3.2',
|
||||
install_requires=['pyudev (>= 0.13)', ],
|
||||
package_dir={'': 'lib'},
|
||||
packages=['hidapi', 'logitech_receiver', 'solaar', 'solaar.ui', 'solaar.cli'],
|
||||
data_files=list(_data_files()),
|
||||
scripts=_glob('bin/*'),
|
||||
)
|
||||
# sudo apt install python-gi python3-gi \
|
||||
# gir1.2-gtk-3.0 gir1.2-notify-0.7 gir1.2-ayatanaappindicator3-0.1
|
||||
# os_requires=['gi.repository.GObject (>= 2.0)', 'gi.repository.Gtk (>= 3.0)'],
|
||||
python_requires='>=3.2',
|
||||
install_requires=[
|
||||
'pyudev (>= 0.13)',
|
||||
],
|
||||
package_dir={'': 'lib'},
|
||||
packages=[
|
||||
'hidapi', 'logitech_receiver', 'solaar', 'solaar.ui', 'solaar.cli'
|
||||
],
|
||||
data_files=list(_data_files()),
|
||||
scripts=_glob('bin/*'),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -6,17 +6,18 @@ from __future__ import absolute_import
|
|||
|
||||
|
||||
def init_paths():
|
||||
"""Make the app work in the source tree."""
|
||||
import sys
|
||||
import os.path as _path
|
||||
"""Make the app work in the source tree."""
|
||||
import sys
|
||||
import os.path as _path
|
||||
|
||||
src_lib = _path.normpath(_path.join(_path.realpath(sys.path[0]), '..', 'lib'))
|
||||
init_py = _path.join(src_lib, 'hidapi', '__init__.py')
|
||||
if _path.exists(init_py):
|
||||
sys.path[0] = src_lib
|
||||
src_lib = _path.normpath(
|
||||
_path.join(_path.realpath(sys.path[0]), '..', 'lib'))
|
||||
init_py = _path.join(src_lib, 'hidapi', '__init__.py')
|
||||
if _path.exists(init_py):
|
||||
sys.path[0] = src_lib
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
init_paths()
|
||||
from hidapi import hidconsole
|
||||
hidconsole.main()
|
||||
init_paths()
|
||||
from hidapi import hidconsole
|
||||
hidconsole.main()
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
from __future__ import absolute_import, division, print_function, unicode_literals
|
||||
|
||||
import sys
|
||||
sys.path += (sys.path[0] + '/../lib',)
|
||||
sys.path += (sys.path[0] + '/../lib', )
|
||||
|
||||
import hidapi
|
||||
from logitech.unifying_receiver.base import DEVICE_UNIFYING_RECEIVER
|
||||
|
|
@ -13,11 +13,8 @@ from logitech.unifying_receiver.base import DEVICE_NANO_RECEIVER
|
|||
|
||||
|
||||
def print_event(action, device):
|
||||
print ("~~~~ device [%s] %s" % (action, device))
|
||||
print("~~~~ device [%s] %s" % (action, device))
|
||||
|
||||
|
||||
hidapi.monitor(print_event,
|
||||
DEVICE_UNIFYING_RECEIVER,
|
||||
DEVICE_UNIFYING_RECEIVER_2,
|
||||
DEVICE_NANO_RECEIVER
|
||||
)
|
||||
hidapi.monitor(print_event, DEVICE_UNIFYING_RECEIVER,
|
||||
DEVICE_UNIFYING_RECEIVER_2, DEVICE_NANO_RECEIVER)
|
||||
|
|
|
|||
Loading…
Reference in New Issue