diff --git a/docs/devices.md b/docs/devices.md index 2e341676..be86a861 100644 --- a/docs/devices.md +++ b/docs/devices.md @@ -49,7 +49,7 @@ a tuple of known feature settings (from lib/logitech/settings_templates.py). | USB ID | Kind | Max Paired Devices | ------------|------------|--------------------| -| 046d:c517 | Nano | 1 | +| 046d:c517 | 27Mhz old | 2-4? | | 046d:c518 | Nano | 1 | | 046d:c51a | Nano | 1 | | 046d:c51b | Nano | 1 | @@ -68,7 +68,8 @@ a tuple of known feature settings (from lib/logitech/settings_templates.py). | 064d:c53f | Lightspeed | 1 | | 17ef:6042 | Nano | 1 | - +* The receiver with usb Id 046d:c517 is old, 27 MHz receiver, supporting only + subset of HID++ 1.0 protocol. Only hardware pairing supported. ### Keyboards (Unifying): @@ -215,7 +216,10 @@ setting is useful only to disable smooth scrolling. | MK550 | | | | | | MK700 | 2008 | 1.0 | yes | FN swap, reprog keys | | MK710 | | 1.0 | yes | FN swap, reprog keys | +| EX100 keyboard | 6500 | 1.0 | yes | | +| EX100 mouse | 3f00 | 1.0 | yes | | +* The EX100 is old, pre-unifying set, supporting only part of HID++ 1.0 features [solaar]: https://github.com/pwr-Solaar/Solaar [logitech]: https://www.logitech.com diff --git a/docs/devices/ex100.txt b/docs/devices/ex100.txt new file mode 100644 index 00000000..d7319d02 --- /dev/null +++ b/docs/devices/ex100.txt @@ -0,0 +1,120 @@ +./scan-registers.sh ff /dev/hidraw4 +# Old notification flags: 000100 +>> ( 0.035) [10 FF 8100 000100] '\x10\xff\x81\x00\x00\x01\x00' +<< ( 0.015) [10 FF 8101 000000] '\x10\xff\x81\x01\x00\x00\x00' +>> ( 0.020) [10 FF 8101 000200] '\x10\xff\x81\x01\x00\x02\x00' +<< ( 0.030) [10 FF 8102 000000] '\x10\xff\x81\x02\x00\x00\x00' +>> ( 0.036) [10 FF 8102 000200] '\x10\xff\x81\x02\x00\x02\x00' +-- +<< ( 0.142) [10 FF 8109 000000] '\x10\xff\x81\t\x00\x00\x00' +>> ( 0.148) [10 FF 8109 010000] '\x10\xff\x81\t\x01\x00\x00' +-- +<< ( 1.790) [10 FF 8170 000000] '\x10\xff\x81p\x00\x00\x00' +>> ( 1.796) [10 FF 8170 012100] '\x10\xff\x81p\x01!\x00' +<< ( 1.806) [10 FF 8171 000000] '\x10\xff\x81q\x00\x00\x00' +>> ( 1.812) [10 FF 8171 011200] '\x10\xff\x81q\x01\x12\x00' +-- +<< ( 1.838) [10 FF 8173 000000] '\x10\xff\x81s\x00\x00\x00' +>> ( 1.844) [10 FF 8173 643F00] '\x10\xff\x81sd?\x00' +-- +<< ( 2.046) [10 FF 8180 000000] '\x10\xff\x81\x80\x00\x00\x00' +>> ( 2.052) [10 FF 8180 030000] '\x10\xff\x81\x80\x03\x00\x00' +-- +<< ( 3.326) [10 FF 81D0 000000] '\x10\xff\x81\xd0\x00\x00\x00' +>> ( 3.332) [10 FF 81D0 000000] '\x10\xff\x81\xd0\x00\x00\x00' + +devices +01 mouse +Red button pressed +>> ( 1676.106) [10 01 0810 000000] '\x10\x01\x08\x10\x00\x00\x00' +>> ( 1676.114) [10 01 4600 000021] '\x10\x01F\x00\x00\x00!' +>> ( 1676.122) [10 FF 4600 211100] '\x10\xffF\x00!\x11\x00' + +Power lewel? +?? Input: 10 01 81 07 00 00 00 +<< ( 1739.032) [10 01 8107 000000] '\x10\x01\x81\x07\x00\x00\x00' +>> ( 1739.040) [10 01 8107 030000] '\x10\x01\x81\x07\x03\x00\x00' + [10 01 8107 070000] '\x10\x01\x81\x07\x07\x00\x00' + +power change +>> ( 2441.563) [10 01 0703 000000] '\x10\x01\x07\x03\x00\x00\x00' +>> ( 100.159) [10 01 0707 000000] '\x10\x01\x07\x07\x00\x00\x00' + +enable power event +<< ( 59.190) [10 01 8000 100000] '\x10\x01\x80\x00\x10\x00\x00' +>> ( 59.193) [10 01 8000 000000] '\x10\x01\x80\x00\x00\x00\x00' + + +03 keyboard + +Power level? +?? Input: 10 03 81 07 00 00 00 +<< ( 1777.961) [10 03 8107 000000] '\x10\x03\x81\x07\x00\x00\x00' +>> ( 1777.967) [10 03 8107 070000] '\x10\x03\x81\x07\x07\x00\x00' + +power on +>> ( 1571.805) [10 03 0810 000000] '\x10\x03\x08\x10\x00\x00\x00' +>> ( 1574.709) [10 03 0800 000000] '\x10\x03\x08\x00\x00\x00\x00' + +red button pressed +>> ( 1619.043) [10 03 0810 000000] '\x10\x03\x08\x10\x00\x00\x00' +>> ( 1619.051) [10 03 4600 000011] '\x10\x03F\x00\x00\x00\x11' +>> ( 1619.059) [10 FF 4600 221100] '\x10\xffF\x00"\x11\x00' +>> ( 1621.747) [10 03 0800 000000] '\x10\x03\x08\x00\x00\x00\x00' + +Fn pressed +>> ( 1651.715) [10 03 032C 100000] '\x10\x03\x03,\x10\x00\x00' +>> ( 1652.170) [10 03 0300 000000] '\x10\x03\x03\x00\x00\x00\x00' + + +$ bin/solaar probe +Nano Receiver + Device path : /dev/hidraw3 + USB id : 046d:c517 + Serial : None + Has 2 paired device(s) out of a maximum of 6. + Notifications: wireless (0x000100) + Register Dump + Notification Register 0x00: 0x000100 + Connection State 0x02: 0x000200 + Device Activity 0xb3: None + Pairing Register 0xb5 0x00: None + Pairing Register 0xb5 0x10: None + Pairing Register 0xb5 0x20: None + Pairing Register 0xb5 0x30: None + Pairing Name 0xb5 0x40: None + Pairing Register 0xb5 0x01: None + Pairing Register 0xb5 0x11: None + Pairing Register 0xb5 0x21: None + Pairing Register 0xb5 0x31: None + Pairing Name 0xb5 0x41: None + Pairing Register 0xb5 0x02: None + Pairing Register 0xb5 0x12: None + Pairing Register 0xb5 0x22: None + Pairing Register 0xb5 0x32: None + Pairing Name 0xb5 0x42: None + Pairing Register 0xb5 0x03: None + Pairing Register 0xb5 0x13: None + Pairing Register 0xb5 0x23: None + Pairing Register 0xb5 0x33: None + Pairing Name 0xb5 0x43: None + Pairing Register 0xb5 0x04: None + Pairing Register 0xb5 0x14: None + Pairing Register 0xb5 0x24: None + Pairing Register 0xb5 0x34: None + Pairing Name 0xb5 0x44: None + Pairing Register 0xb5 0x05: None + Pairing Register 0xb5 0x15: None + Pairing Register 0xb5 0x25: None + Pairing Register 0xb5 0x35: None + Pairing Name 0xb5 0x45: None + Firmware 0xf1 0x00: None + Firmware 0xf1 0x01: None + Firmware 0xf1 0x02: None + Firmware 0xf1 0x03: None + Firmware 0xf1 0x04: None + +Battery status: +1.9V critical +2.3V low +2.5V full diff --git a/lib/logitech_receiver/base_usb.py b/lib/logitech_receiver/base_usb.py index 416997e6..10f6fc4b 100644 --- a/lib/logitech_receiver/base_usb.py +++ b/lib/logitech_receiver/base_usb.py @@ -27,6 +27,7 @@ from __future__ import absolute_import, division, print_function, unicode_litera ## should this last be changed so that may_unpair is used for all receivers? writing to _R.receiver_pairing doesn't seem right # re_pairs determines whether a receiver pairs by replacing existing pairings, default to False ## currently only one receiver is so marked - should there be more? +# ex100_wpid_fix enable workarounds for EX100 and possible other old 27Mhz receivers _DRIVER = ('hid-generic', 'generic-usb', 'logitech-djreceiver') @@ -86,6 +87,18 @@ _lightspeed_receiver = lambda product_id: { 'name': 'Lightspeed Receiver' } +_ex100_receiver = lambda product_id: { + 'vendor_id': 0x046d, + 'product_id': product_id, + 'usb_interface': 1, + 'hid_driver': _DRIVER, # noqa: F821 + 'name': 'EX100 Receiver 27 Mhz', + 'max_devices': 4, + 'may_unpair': False, + 're_pairs': True, + 'ex100_wpid_fix': True +} + # standard Unifying receivers (marked with the orange Unifying logo) UNIFYING_RECEIVER_C52B = _unifying_receiver(0xc52b) UNIFYING_RECEIVER_C532 = _unifying_receiver(0xc532) @@ -93,8 +106,10 @@ UNIFYING_RECEIVER_C532 = _unifying_receiver(0xc532) # Nano receviers that support the Unifying protocol NANO_RECEIVER_ADVANCED = _nano_receiver(0xc52f) +# ex100 old style receiver pre-unifyimg protocol +EX100_27MHZ_RECEIVER_C517 = _ex100_receiver(0xc517) + # 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) @@ -119,7 +134,7 @@ ALL = ( UNIFYING_RECEIVER_C52B, UNIFYING_RECEIVER_C532, NANO_RECEIVER_ADVANCED, - NANO_RECEIVER_C517, + EX100_27MHZ_RECEIVER_C517, NANO_RECEIVER_C518, NANO_RECEIVER_C51A, NANO_RECEIVER_C51B, diff --git a/lib/logitech_receiver/descriptors.py b/lib/logitech_receiver/descriptors.py index 598e71f2..845ef56c 100644 --- a/lib/logitech_receiver/descriptors.py +++ b/lib/logitech_receiver/descriptors.py @@ -293,6 +293,13 @@ _D( wpid='3622', registers=(_R.battery_status, ), ) +_D( + 'Wireless Keyboard EX100', + codename='EX100', + protocol=1.0, + wpid='6500', + registers=(_R.battery_status, ), +) # Mice @@ -523,6 +530,14 @@ _D( wpid='6822', registers=(_R.battery_status, ), ) +_D( + 'Wireless Mouse EX100', + codename='EX100m', + protocol=1.0, + wpid='3F00', + registers=(_R.battery_status, ), + # settings=[ _RS.smooth_scroll(), ], # command accepted, but no change in whell action +) # Trackballs diff --git a/lib/logitech_receiver/hidpp10.py b/lib/logitech_receiver/hidpp10.py index b48e2573..028be437 100644 --- a/lib/logitech_receiver/hidpp10.py +++ b/lib/logitech_receiver/hidpp10.py @@ -64,14 +64,26 @@ POWER_SWITCH_LOCATION = _NamedInts( # - the rest work only on devices as far as we can tell right now # In the future would be useful to have separate enums for receiver and device notification flags, # but right now we don't know enough. +# additional flags taken from https://drive.google.com/file/d/0BxbRzx7vEV7eNDBheWY0UHM5dEU/view?usp=sharing NOTIFICATION_FLAG = _NamedInts( + numpad_numerical_keys=0x800000, + f_lock_status=0x400000, + roller_H=0x200000, battery_status=0x100000, # send battery charge notifications (0x07 or 0x0D) + mouse_extra_buttons=0x080000, + roller_V=0x040000, 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 + reserved5=0x008000, + reserved4=0x004000, + reserved3=0x002000, + reserved2=0x001000, software_present=0x000800, # .. no idea + reserved1=0x000400, keyboard_illumination=0x000200, # illumination brightness level changes (by pressing keys) wireless=0x000100, # notify when the device wireless goes on/off-line + mx_air_3d_gesture=0x000001, ) ERROR = _NamedInts( @@ -116,6 +128,25 @@ REGISTERS = _NamedInts( notifications=0x00, firmware=0xF1, ) +# Flags taken from https://drive.google.com/file/d/0BxbRzx7vEV7eNDBheWY0UHM5dEU/view?usp=sharing +DEVICE_FEATURES = _NamedInts( + reserved1=0x010000, + special_buttons=0x020000, + enhanced_key_usage=0x040000, + fast_fw_rev=0x080000, + reserved2=0x100000, + reserved3=0x200000, + scroll_accel=0x400000, + buttons_control_resolution=0x800000, + inhibit_lock_key_sound=0x000001, + reserved4=0x000002, + mx_air_3d_engine=0x000004, + host_control_leds=0x000008, + reserved5=0x000010, + reserved6=0x000020, + reserved7=0x000040, + reserved8=0x000080, +) # # functions @@ -316,3 +347,19 @@ def set_notification_flags(device, *flag_bits): assert flag_bits & 0x00FFFFFF == flag_bits result = write_register(device, REGISTERS.notifications, _int2bytes(flag_bits, 3)) return result is not None + + +def get_device_features(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 + + flags = read_register(device, REGISTERS.mouse_button_flags) + if flags is not None: + assert len(flags) == 3 + return _bytes2int(flags) diff --git a/lib/logitech_receiver/notifications.py b/lib/logitech_receiver/notifications.py index 92e7c11c..1a3ba0db 100644 --- a/lib/logitech_receiver/notifications.py +++ b/lib/logitech_receiver/notifications.py @@ -160,8 +160,8 @@ def _process_hidpp10_custom_notification(device, status, n): # message layout: 10 ix <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) + charge, status_text, next_charge = _hidpp10.parse_battery_status(n.sub_id, data) + status.set_battery_info(charge, status_text, next_charge) return True if n.sub_id == _R.keyboard_illumination: @@ -201,6 +201,9 @@ def _process_hidpp10_notification(device, status, n): if protocol_name: if _log.isEnabledFor(_DEBUG): wpid = _strhex(n.data[2:3] + n.data[1:2]) + # workaround for short EX100 wpids + if protocol_name == '27 MHz': + wpid = _strhex(n.data[2:3]) + '00' assert wpid == device.wpid, '%s wpid mismatch, got %s' % (device, wpid) flags = ord(n.data[:1]) & 0xF0 diff --git a/lib/logitech_receiver/receiver.py b/lib/logitech_receiver/receiver.py index 1cce415b..68cebcd9 100644 --- a/lib/logitech_receiver/receiver.py +++ b/lib/logitech_receiver/receiver.py @@ -88,6 +88,14 @@ class PairedDevice(object): 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 + # fix EX100 wpid + if receiver.ex100_wpid_fix: # EX100 receiver + self.wpid = _strhex(link_notification.data[2:3]) + '00' + # workaround for EX100 switched kind + if self.wpid == '3F00': + kind = 2 + if self.wpid == '6500': + kind = 1 self._kind = _hidpp10.DEVICE_KIND[kind] else: # force a reading of the wpid @@ -97,6 +105,20 @@ class PairedDevice(object): self.wpid = _strhex(pair_info[3:5]) kind = ord(pair_info[7:8]) & 0x0F self._kind = _hidpp10.DEVICE_KIND[kind] + elif receiver.ex100_wpid_fix: + # ex100 receiver, fill fake device_info with known wpid's + # accordingly to drivers/hid/hid-logitech-dj.c + # index 1 or 2 always mouse, index 3 always the keyboard, + # index 4 is used for an optional separate numpad + if number == 1: # mouse + self.wpid = '3F00' + self._kind = _hidpp10.DEVICE_KIND[2] + elif number == 3: # keyboard + self.wpid = '6500' + self._kind = _hidpp10.DEVICE_KIND[1] + else: # unknown device number on EX100 + _log.error('failed to set fake EX100 wpid for device %d of %s', number, receiver) + raise _base.NoSuchDevice(number=number, receiver=receiver, error='Unknown EX100 device') else: # unifying protocol not supported, must be a Nano receiver device_info = self.receiver.read_register(_R.receiver_info, 0x04) @@ -365,6 +387,7 @@ class Receiver(object): self._str = '<%s(%s,%s%s)>' % ( self.name.replace(' ', ''), self.path, '' if isinstance(self.handle, int) else 'T', self.handle ) + self.ex100_wpid_fix = product_info.get('ex100_wpid_fix', False) self._firmware = None self._devices = {} diff --git a/lib/solaar/cli/show.py b/lib/solaar/cli/show.py index 05c52512..65dad417 100644 --- a/lib/solaar/cli/show.py +++ b/lib/solaar/cli/show.py @@ -96,6 +96,13 @@ def _print_device(dev): print(' Notifications: %s (0x%06X).' % (', '.join(notification_names), notification_flags)) else: print(' Notifications: (none).') + device_features = _hidpp10.get_device_features(dev) + if device_features is not None: + if device_features: + device_features_names = _hidpp10.DEVICE_FEATURES.flag_names(device_features) + print(' Features: %s (0x%06X)' % (', '.join(device_features_names), device_features)) + else: + print(' Features: (none)') if dev.online and dev.features: print(' Supports %d HID++ 2.0 features:' % len(dev.features))