Repository: pwr-Solaar/Solaar

Issue:
SmartShift control for MX Anywhere 3
First, thanks for Solaar-- I honestly don't think I could use this mouse without it :)

**Information**
- Solaar version: 1.0.4-67-g1c6f289
- Distribution: Arch Linux
- Kernel version: Linux 5.10.7-arch1-1 x86_64 GNU/Linux
- Output of `solaar show` for the target device (if applicable):

<details>

```
Wired Devices
  1: MX Anywhere 3
     Device path  : /dev/hidraw4
     USB id       : 046d:B025
     Codename     : MX Anywhere 3
     Kind         : mouse
     Protocol     : HID++ 4.5
     Serial number: ?
     Model ID:      B02540900000
     Unit ID:       0DDCDCA5
        Bootloader: BL1 13.00.B0014
          Firmware: MPM 24.00.B0014
             Other: 
     Supports 30 HID++ 2.0 features:
         0: ROOT                   {0000}   
         1: FEATURE SET            {0001}   
         2: DEVICE FW VERSION      {0003}   
            Firmware: Bootloader BL1 13.00.B0014 000095D7D19A
            Firmware: Firmware MPM 24.00.B0014 B02595D7D19A
            Firmware: Other   
            Unit ID: 0DDCDCA5  Model ID: B02540900000  Transport IDs: {'btleid': 'B025', 'wpid': '4090'}
         3: DEVICE NAME            {0005}   
            Name: MX Anywhere 3
            Kind: mouse
         4: WIRELESS DEVICE STATUS {1D4B}   
         5: RESET                  {0020}   
         6: CRYPTO ID              {0021}   
         7: DEVICE FRIENDLY NAME   {0007}   
         8: UNIFIED BATTERY        {1004}   
         9: REPROG CONTROLS V4     {1B04}   
            Key/Button Actions: {'80': 80, '81': 81, '82': 82, '83': 83, '86': 86, '196': 196}
            Key/Button Diversion: {'82': 0, '83': 0, '86': 0, '196': 0, '215': 0}
        10: CHANGE HOST            {1814}   
            Change Host: 1:command
        11: unknown:2250           {2250}   
        12: ADJUSTABLE DPI         {2201}   
            Sensitivity (DPI): 4000
        13: unknown:2111           {2111}   
        14: HIRES WHEEL            {2121}   
            Multiplier: 15
            Has invert: Normal wheel motion
            Has ratchet switch: Normal wheel mode
            Low resolution mode
            HID notification
            Scroll Wheel Direction: False
            Scroll Wheel Resolution: False
        15: unknown:2251           {2251}   
        16: DFUCONTROL SIGNED      {00C2}   
        17: DEVICE RESET           {1802}   internal, hidden
        18: unknown:1803           {1803}   internal, hidden
        19: CONFIG DEVICE PROPS    {1806}   internal, hidden
        20: unknown:1812           {1812}   internal, hidden
        21: OOBSTATE               {1805}   internal, hidden
        22: unknown:1830           {1830}   internal, hidden
        23: unknown:18A1           {18A1}   internal, hidden
        24: unknown:1E00           {1E00}   hidden
        25: unknown:1EB0           {1EB0}   internal, hidden
        26: unknown:1861           {1861}   internal, hidden
        27: unknown:9300           {9300}   internal, hidden
        28: unknown:9001           {9001}   internal, hidden
        29: unknown:9205           {9205}   internal, hidden
     Has 7 reprogrammable keys:
         0: Left Button               , default: Left Click                  => Left Click                
             mse, analytics key events, pos:0, group:1, group mask:g1
             reporting: default
         1: Right Button              , default: Right Click                 => Right Click               
             mse, analytics key events, pos:0, group:1, group mask:g1
             reporting: default
         2: Middle Button             , default: Mouse Middle Button         => Mouse Middle Button       
             mse, reprogrammable, divertable, raw XY, analytics key events, pos:0, group:2, group mask:g1,g2
             reporting: default
         3: Back Button               , default: Mouse Back Button           => Mouse Back Button         
             mse, reprogrammable, divertable, raw XY, analytics key events, unknown:000800, pos:0, group:2, group mask:g1,g2
             reporting: default
         4: Forward Button            , default: Mouse Forward Button        => Mouse Forward Button      
             mse, reprogrammable, divertable, raw XY, analytics key events, unknown:000800, pos:0, group:2, group mask:g1,g2
             reporting: default
         5: Smart Shift               , default: Smart Shift                 => Smart Shift               
             mse, reprogrammable, divertable, raw XY, analytics key events, pos:0, group:2, group mask:g1,g2
             reporting: default
         6: Virtual Gesture Button    , default: Virtual Gesture Button      => Virtual Gesture Button    
             divertable, virtual, raw XY, force raw XY, pos:0, group:3, group mask:empty
             reporting: default
     Battery: 90%, discharging.
```
</details>

**Is your feature request related to a problem? Please describe.**

The logitech website shows an option for controlling SmartShift on the MX Anywhere 3 [here](https://support.logi.com/hc/en-us/articles/360053831734-Getting-Started-MX-Anywhere-3), but the configuration option isn't currently available in Solaar.

**Describe the solution you'd like**

Getting the option working in Solaar would be amazing. I can help test if necessary.

**Describe alternatives you've considered**

I noticed that other mice seem to use `2110`, which the MX Anywhere 3 doesn't have. It does have a `2111` which I suspect is the same feature but expecting different parameters. I tried patching Solaar to use 2111 for SmartShift and attempted to configure it using that, but attempting to change the value didn't work.

I also tried the other two unknown values that aren't internal/hidden, and 2250 similarly failed to adjust the value while 2251 crashed Solaar.

**Additional context**

Thanks again!



Discussion:
Solaar has this for feature 2110.  Feature 2111 appears to be new, and is not even in the list of HID++ features in Solaar.  It may be possible to get information from Logitech on this feature.  Stay tuned.
PR #1109 should add the ability to change how scrolling works on the MX Anywhere 3.  It's  not exactly the same as in Logitech Options, but should provide equivalent functionality.  Give it a try and report back how it works.

To download and work with Solar
```
git clone https://github.com/pwr-Solaar/Solaar.git
cd Solaar
```
Run Solaar as bin/solaar from this directory.


To run PR #1109, first clone Solaar if you have not already done so and cd to the clone directory.  The first time you download the pull request, fetch it into a new branch and checkout that branch, as in:
```
git fetch origin pull/1109/head:pull_1109
git checkout pull_1109
```
To download a new version of the pull request, fetch it and then set your pull branch
to the new fetch, as in:
```
git checkout pull_1109
git fetch origin pull/1109/head
git reset --hard FETCH_HEAD
```


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
@@ -104,6 +104,7 @@
     POINTER_AXIS_ORIENTATION=0x2006,
     VERTICAL_SCROLLING=0x2100,
     SMART_SHIFT=0x2110,
+    SMART_SHIFT_ENHANCED=0x2111,
     HI_RES_SCROLLING=0x2120,
     HIRES_WHEEL=0x2121,
     LOWRES_WHEEL=0x2130,
@@ -113,6 +114,8 @@
     POINTER_SPEED=0x2205,
     ANGLE_SNAPPING=0x2230,
     SURFACE_TUNING=0x2240,
+    XY_STATS=0x2250,
+    WHEEL_STATS=0x2251,
     HYBRID_TRACKING=0x2400,
     # Keyboard
     FN_INVERSION=0x40A0,
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
@@ -317,36 +317,44 @@ def _feature_hires_smooth_resolution():
     return _Setting(_HIRES_RES, rw, validator, device_kind=(_DK.mouse, _DK.trackball))
 
 
-def _feature_smart_shift():
-    _MIN_SMART_SHIFT_VALUE = 0
-    _MAX_SMART_SHIFT_VALUE = 50
+class _SmartShiftRW(_FeatureRW):
+    MIN_VALUE = 0
+    MAX_VALUE = 50
+
+    def __init__(self, feature, read_fnid, write_fnid):
+        super(_SmartShiftRW, self).__init__(feature, read_fnid, write_fnid)
+
+    def read(self, device):
+        value = super(_SmartShiftRW, self).read(device)
+        if _bytes2int(value[0:1]) == 1:
+            # Mode = Freespin, map to minimum
+            return _int2bytes(_SmartShiftRW.MIN_VALUE, count=1)
+        else:
+            # Mode = smart shift, map to the value, capped at maximum
+            threshold = min(_bytes2int(value[1:2]), _SmartShiftRW.MAX_VALUE)
+            return _int2bytes(threshold, count=1)
 
-    class _SmartShiftRW(_FeatureRW):
-        def __init__(self, feature):
-            super(_SmartShiftRW, self).__init__(feature)
+    def write(self, device, data_bytes):
+        threshold = _bytes2int(data_bytes)
+        # Freespin at minimum
+        mode = 1 if threshold == _SmartShiftRW.MIN_VALUE else 2
+        # Ratchet at maximum
+        if threshold == _SmartShiftRW.MAX_VALUE:
+            threshold = 255
+        data = _int2bytes(mode, count=1) + _int2bytes(threshold, count=1)
+        return super(_SmartShiftRW, self).write(device, data)
 
-        def read(self, device):
-            value = super(_SmartShiftRW, self).read(device)
-            if _bytes2int(value[0:1]) == 1:
-                # Mode = Freespin, map to minimum
-                return _int2bytes(_MIN_SMART_SHIFT_VALUE, count=1)
-            else:
-                # Mode = smart shift, map to the value, capped at maximum
-                threshold = min(_bytes2int(value[1:2]), _MAX_SMART_SHIFT_VALUE)
-                return _int2bytes(threshold, count=1)
 
-        def write(self, device, data_bytes):
-            threshold = _bytes2int(data_bytes)
-            # Freespin at minimum
-            mode = 1 if threshold == _MIN_SMART_SHIFT_VALUE else 2
-            # Ratchet at maximum
-            if threshold == _MAX_SMART_SHIFT_VALUE:
-                threshold = 255
-            data = _int2bytes(mode, count=1) + _int2bytes(threshold, count=1) * 2
-            return super(_SmartShiftRW, self).write(device, data)
+def _feature_smart_shift():
+    validator = _RangeV(_SmartShiftRW.MIN_VALUE, _SmartShiftRW.MAX_VALUE, 1)
+    rw = _SmartShiftRW(_F.SMART_SHIFT, read_fnid=0x00, write_fnid=0x10)
+    return _Setting(_SMART_SHIFT, rw, validator, device_kind=(_DK.mouse, _DK.trackball))
+
 
-    validator = _RangeV(_MIN_SMART_SHIFT_VALUE, _MAX_SMART_SHIFT_VALUE, 1)
-    return _Setting(_SMART_SHIFT, _SmartShiftRW(_F.SMART_SHIFT), validator, device_kind=(_DK.mouse, _DK.trackball))
+def _feature_smart_shift_enhanced():
+    validator = _RangeV(_SmartShiftRW.MIN_VALUE, _SmartShiftRW.MAX_VALUE, 1)
+    rw = _SmartShiftRW(_F.SMART_SHIFT_ENHANCED, read_fnid=0x10, write_fnid=0x20)
+    return _Setting(_SMART_SHIFT, rw, validator, device_kind=(_DK.mouse, _DK.trackball))
 
 
 def _feature_dpi_sliding():
@@ -766,6 +774,7 @@ def _S(name, featureID=None, featureFn=None, registerFn=None, identifier=None):
     _S(_HIRES_INV, _F.HIRES_WHEEL, _feature_hires_smooth_invert),
     _S(_HIRES_RES, _F.HIRES_WHEEL, _feature_hires_smooth_resolution),
     _S(_SMART_SHIFT, _F.SMART_SHIFT, _feature_smart_shift),
+    _S(_SMART_SHIFT, _F.SMART_SHIFT_ENHANCED, _feature_smart_shift_enhanced, identifier='smart_shift_enhanced'),
     _S(_THUMB_SCROLL_MODE, _F.THUMB_WHEEL, _feature_thumb_mode),
     _S(_THUMB_SCROLL_INVERT, _F.THUMB_WHEEL, _feature_thumb_invert),
     _S(_DPI, _F.ADJUSTABLE_DPI, _feature_adjustable_dpi, registerFn=_register_dpi),


Test patch: