pwr-Solaar / Solaar

Linux device manager for Logitech devices
https://pwr-solaar.github.io/Solaar
GNU General Public License v2.0
5.54k stars 409 forks source link

[Implementation] Report Rate Control On HID++ 2.0+ Devices #1040

Closed leogx9r closed 3 years ago

leogx9r commented 3 years ago

Information

Greetings,

I'm already aware of the previous efforts to add and as such, control the report/polling rates for HID++ 2.0+ devices, which was removed due to the insufficient documentation available of changing the device to Host-mode which seems to be required for this particular feature.

I've spent a couple hours trying to understand the code base of this project and managed to get it working. The device mode for HID++ 2.0 and beyond can be retrieved or changed by calling 0x8100, labeled as FEATURE.ONBOARD_PROFILES with the function ID 0x20 and 0x10 respectively. Setting the polling rate requires the device to be set in Host mode before it can be adjusted, as such the solution is simply to check what mode the device is in, before changing the polling rate and switch modes if required.

The following patch is what I've managed to hack together in the brief time I spent with this code base. While it probably has mistakes, it's my hope that (while this isn't a pull request due to lacking time to properly respond to such engagements), someone can (properly) implement this feature. This was tested with my Logitech G305, which by default, uses a 1000 Hz polling rate when running the device in onboard mode. I've managed to change the polling rates using the following patch to all supported values, 125 Hz, 250 Hz, 500 Hz and 1000 Hz. The code itself should work on all HID++ 2.0+ devices since it is based off the work here as well as the previous code from pull requests like https://github.com/pwr-Solaar/Solaar/pull/840, https://github.com/pwr-Solaar/Solaar/pull/824, with relevant issues being https://github.com/pwr-Solaar/Solaar/issues/850, https://github.com/pwr-Solaar/Solaar/issues/792

diff --git a/lib/logitech_receiver/hidpp20.py b/lib/logitech_receiver/hidpp20.py
index dd25d97..d42337e 100644
--- a/lib/logitech_receiver/hidpp20.py
+++ b/lib/logitech_receiver/hidpp20.py
@@ -183,6 +183,12 @@ BATTERY_STATUS = _NamedInts(
     thermal_error=0x06
 )

+ONBOARD_MODES = _NamedInts(
+    MODE_NO_CHANGE=0x00,
+    MODE_ONBOARD=0x01,
+    MODE_HOST=0x02
+)
+
 CHARGE_STATUS = _NamedInts(charging=0x00, full=0x01, not_charging=0x02, error=0x07)

 CHARGE_LEVEL = _NamedInts(average=50, full=90, critical=5)
@@ -1331,6 +1337,19 @@ def set_host_name(device, name):
             return response

+def get_onboard_mode(device):
+    state = feature_request(device, FEATURE.ONBOARD_PROFILES, 0x20)
+
+    if state:
+        mode = _unpack('!B', state[:1])[0]
+        return mode
+
+
+def set_onboard_mode(device, mode):
+    state = feature_request(device, FEATURE.ONBOARD_PROFILES, 0x10, mode)
+    return state
+
+
 def get_polling_rate(device):
     state = feature_request(device, FEATURE.REPORT_RATE, 0x10)
     if state:
diff --git a/lib/logitech_receiver/settings_templates.py b/lib/logitech_receiver/settings_templates.py
index 52790dd..99b6862 100644
--- a/lib/logitech_receiver/settings_templates.py
+++ b/lib/logitech_receiver/settings_templates.py
@@ -81,6 +81,7 @@ _HIRES_INV = ('hires-smooth-invert', _('Scroll Wheel Direction'),
               _('Invert direction for vertical scroll with wheel.'))
 _HIRES_RES = ('hires-smooth-resolution', _('Scroll Wheel Resolution'),
               _('High-sensitivity mode for vertical scroll with the wheel.'))
+_REPORT_RATE = ('report_rate', _("Polling Rate (ms)"), _("Frequency of device polling, in milliseconds"))
 _FN_SWAP = ('fn-swap', _('Swap Fx function'),
             _('When set, the F1..F12 keys will activate their special function,\n'
               'and you must hold the FN key to activate their standard function.') + '\n\n' +
@@ -527,6 +528,28 @@ def _feature_adjustable_dpi():
     return _Setting(_DPI, rw, callback=_feature_adjustable_dpi_callback, device_kind=(_DK.mouse, _DK.trackball))

+# Implemented based on code in libratrag
+def _feature_report_rate_callback(device):
+    # Host mode is required for report rate to be adjustable
+    if _hidpp20.get_onboard_mode(device) != _hidpp20.ONBOARD_MODES.MODE_HOST:
+        _hidpp20.set_onboard_mode(device, _hidpp20.ONBOARD_MODES.MODE_HOST)
+
+    reply = device.feature_request(_F.REPORT_RATE, 0x00)
+    assert reply, 'Oops, report rate choices cannot be retrieved!'
+    rate_list = []
+    rate_flags = _bytes2int(reply[0:1])
+    for i in range(0,8):
+        if (rate_flags >> i) & 0x01:
+            rate_list.append(i+1)
+    return _ChoicesV(_NamedInts.list(rate_list), byte_count=1) if rate_list else None
+
+
+def _feature_report_rate():
+    """Report Rate feature"""
+    rw = _FeatureRW(_F.REPORT_RATE, read_fnid=0x10, write_fnid=0x20)
+    return _Setting(_REPORT_RATE, rw, callback=_feature_report_rate_callback, device_kind=(_DK.mouse, ))
+
+
 def _feature_pointer_speed():
     """Pointer Speed feature"""
     # min and max values taken from usb traces of Win software
@@ -756,6 +779,7 @@ _SETTINGS_TABLE = [
     _S(_REPROGRAMMABLE_KEYS, _F.REPROG_CONTROLS_V4, _feature_reprogrammable_keys),
     _S(_DIVERT_KEYS, _F.REPROG_CONTROLS_V4, _feature_divert_keys),
     _S(_DISABLE_KEYS, _F.KEYBOARD_DISABLE_KEYS, _feature_disable_keyboard_keys),
+    _S(_REPORT_RATE, _F.REPORT_RATE, _feature_report_rate),
     _S(_DIVERT_CROWN, _F.CROWN, _feature_divert_crown),
     _S(_DIVERT_GKEYS, _F.GKEY, _feature_divert_gkeys),
     _S(_PLATFORM, _F.MULTIPLATFORM, _feature_multiplatform),
diff --git a/lib/solaar/cli/show.py b/lib/solaar/cli/show.py
index 7a97b36..585e5d7 100644
--- a/lib/solaar/cli/show.py
+++ b/lib/solaar/cli/show.py
@@ -212,8 +212,12 @@ def _print_device(dev, num=None):
                 if ids:
                     unitId, modelId, tid_map = ids
                     print('            Unit ID: %s  Model ID: %s  Transport IDs: %s' % (unitId, modelId, tid_map))
-            elif feature == _hidpp20.FEATURE.REPORT_RATE:
-                print('            Polling Rate (ms): %d' % _hidpp20.get_polling_rate(dev))
+            elif feature == _hidpp20.FEATURE.ONBOARD_PROFILES:
+                if _hidpp20.get_onboard_mode(dev) == _hidpp20.ONBOARD_MODES.MODE_HOST:
+                    mode = 'Host'
+                else:
+                    mode = 'On-Board'
+                print('            Device Mode: %s' % mode)
             elif feature == _hidpp20.FEATURE.BATTERY_STATUS or feature == _hidpp20.FEATURE.BATTERY_VOLTAGE:
                 print('', end='       ')
                 _battery_line(dev)

This is based off the current HEAD on master branch, which at the time of writing is f68a831. Additionally, the show command was modified to show whether the device in question is in host mode or onboard mode.

Below are a couple screenshots of said patch working, measured using evhz,

image image

Respectfully, Leonardo.

pfps commented 3 years ago

Could you create a PR with your changes? This will make it easier to check them out.

One problem is that host profiles is a very complex feature and may be different for different devices. I might be able to check out the differences.

leogx9r commented 3 years ago

Thanks for the acknowledgement. PR was created along with relevant changes for checks to pass.