Repository: pwr-Solaar/Solaar

Issue:
[Implementation] Report Rate Control On HID++ 2.0+ Devices
**Information**

- Solaar version: 1.0.4-64-gf68a831
- Distribution: Arch Linux
- Kernel version: Linux 5.10.3-1 x86_64 GNU/Linux
- Output of `solaar show` for the target device (if applicable):
```
 $ ➜ ./bin/solaar show                 
Lightspeed Receiver
  Device path  : /dev/hidraw4
  USB id       : 046d:C53F
  Serial       : 7E689DA8
    Firmware   : 44.01.B0005
    Bootloader : 00.02
    Other      : AA.DE
  Has 1 paired device(s) out of a maximum of 1.
  Notifications: wireless, software present (0x000900)
  Device activity counters: (empty)

  1: G305 Lightspeed Wireless Gaming Mouse
     Device path  : None
     WPID         : 4074
     Codename     : G305
     Kind         : mouse
     Protocol     : HID++ 4.2
     Polling rate : 8 ms (125Hz)
     Serial number: 522871CF
     Model ID:      407400000000
     Unit ID:       39EAC711
        Bootloader: BOT 69.01.B0014
          Firmware: RQM 68.01.B0014
     The power switch is located on the base.
     Supports 27 HID++ 2.0 features:
         0: ROOT                   {0000}   
         1: FEATURE SET            {0001}   
         2: DEVICE FW VERSION      {0003}   
            Firmware: Bootloader BOT 69.01.B0014 407482F3F4D0
            Firmware: Firmware RQM 68.01.B0014 407482F3F4D0
            Unit ID: 39EAC711  Model ID: 407400000000  Transport IDs: {'wpid': '4074'}
         3: DEVICE NAME            {0005}   
            Name: G305 Lightspeed Wireless Gaming Mouse
            Kind: mouse
         4: WIRELESS DEVICE STATUS {1D4B}   
         5: BATTERY STATUS         {1000}   
            Battery: 90%, discharging, next level 50%.
         6: COLOR LED EFFECTS      {8070}   
         7: ONBOARD PROFILES       {8100}   
            Device Mode: Host
         8: MOUSE BUTTON SPY       {8110}   
         9: REPORT RATE            {8060}   
            Polling Rate (ms): 1
        10: MODE STATUS            {8090}   
        11: DFUCONTROL SIGNED      {00C2}   
        12: DEVICE RESET           {1802}   internal, hidden
        13: unknown:1803           {1803}   internal, hidden
        14: CONFIG DEVICE PROPS    {1806}   internal, hidden
        15: unknown:1811           {1811}   internal, hidden
        16: OOBSTATE               {1805}   internal, hidden
        17: unknown:1830           {1830}   internal, hidden
        18: unknown:1890           {1890}   internal, hidden
        19: unknown:1DF3           {1DF3}   internal, hidden
        20: unknown:1E00           {1E00}   hidden
        21: unknown:1EB0           {1EB0}   internal, hidden
        22: unknown:1861           {1861}   internal, hidden
        23: unknown:18B1           {18B1}   internal, hidden
        24: unknown:1E22           {1E22}   internal, hidden
        25: unknown:1801           {1801}   internal, hidden
        26: ADJUSTABLE DPI         {2201}   
            Sensitivity (DPI): 1600
     Battery: 90%, discharging, next level 50%.
```

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](https://github.com/libratbag/libratbag/blob/master/src/hidpp20.c) 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
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](https://user-images.githubusercontent.com/40126089/103385786-c0f93d00-4af3-11eb-8ed7-0e7eb31aabd6.png)
![image](https://user-images.githubusercontent.com/40126089/103385845-0158bb00-4af4-11eb-9771-28c05d521db0.png)


Respectfully,
 Leonardo.


Discussion:
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.

Resolving patch:
diff --git a/lib/logitech_receiver/hidpp20.py b/lib/logitech_receiver/hidpp20.py
--- a/lib/logitech_receiver/hidpp20.py
+++ b/lib/logitech_receiver/hidpp20.py
@@ -183,6 +183,8 @@
     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)
@@ -1334,6 +1336,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
--- a/lib/logitech_receiver/settings_templates.py
+++ b/lib/logitech_receiver/settings_templates.py
@@ -81,6 +81,7 @@
               _('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
@@ -754,6 +777,7 @@ def _S(name, featureID=None, featureFn=None, registerFn=None, identifier=None):
     _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
--- a/lib/solaar/cli/show.py
+++ b/lib/solaar/cli/show.py
@@ -216,6 +216,12 @@ def _print_device(dev, num=None):
                 print('            Polling Rate (ms): %d' % _hidpp20.get_polling_rate(dev))
             elif feature == _hidpp20.FEATURE.REMAINING_PAIRING:
                 print('            Remaining Pairings: %d' % _hidpp20.get_remaining_pairing(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)


Test patch: