Repository: pwr-Solaar/Solaar

Issue:
Solaar-1.0.3 does not restore settings for M705 mouse
**Information**
- Solaar version (`solaar --version` or `git describe --tags` if cloned from this repository): solaar 1.0.3
- Distribution: Gentoo
- Kernel version (ex. `uname -srmo`): `Linux 5.7.10-x86_64 x86_64 GNU/Linu`
- Output of `solaar show`:
```
Unifying Receiver
  Device path  : /dev/hidraw4
  USB id       : 046d:c52b
  Serial       : FA2297E8
    Firmware   : 12.11.B0032
    Bootloader : 04.16
    Other      : AA.AA
  Has 1 paired device(s) out of a maximum of 6.
  Notifications: wireless, software present (0x000900)
  Device activity counters: 1=18

  1: Marathon Mouse M705 (M-R0073)
     Codename     : M705 (M-R0073)
     Kind         : mouse
     Wireless PID : 406D
     Protocol     : HID++ 4.5
     Polling rate : 8 ms (125Hz)
     Serial number: 2B486C23
        Bootloader: BOT 59.00.B0002
          Firmware: RQM 67.01.B0005
     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}   
         3: DEVICE NAME            {0005}   
         4: WIRELESS DEVICE STATUS {1D4B}   
         5: RESET                  {0020}   
         6: BATTERY STATUS         {1000}   
         7: REPROG CONTROLS V4     {1B04}   
            Actions: {'80': 80, '81': 81, '82': 82, '83': 83, '86': 86, '91': 91, '93': 93}
         8: POINTER SPEED          {2205}   
            Pointer Speed: 1.0
            Sensitivity (Pointer Speed): 256
         9: VERTICAL SCROLLING     {2100}   
            Roller type: 3G
            Ratchet per turn: 24
            Scroll lines: 0
        10: DFUCONTROL SIGNED      {00C2}   
        11: DEVICE RESET           {1802}   internal, hidden
        12: unknown:1803           {1803}   internal, hidden
        13: CONFIG DEVICE PROPS    {1806}   internal, hidden
        14: unknown:1810           {1810}   internal, hidden
        15: unknown:1830           {1830}   internal, hidden
        16: unknown:1890           {1890}   internal, hidden
        17: unknown:18A1           {18A1}   internal, hidden
        18: unknown:1DF3           {1DF3}   internal, hidden
        19: unknown:1E00           {1E00}   hidden
        20: unknown:1EB0           {1EB0}   internal, hidden
        21: unknown:1861           {1861}   internal, hidden
        22: unknown:18B1           {18B1}   internal, hidden
        23: unknown:1850           {1850}   internal, hidden
        24: unknown:1F03           {1F03}   internal, hidden
        25: unknown:18C0           {18C0}   internal, hidden
        26: HIRES WHEEL            {2121}   
            Multiplier: 8
            Has invert
              Normal wheel motion
            Has ratchet switch
              Normal wheel mode
            Low resolution mode
            HID notification
            High Resolution Wheel Invert: False
            Wheel Resolution: False
     Has 7 reprogrammable keys:
         0: Left Button               , default: Left Click                  => Left Button               
             mse, reprogrammable, pos:0, group:1, gmask:1
         1: Right Button              , default: Right Click                 => Right Button              
             mse, reprogrammable, pos:0, group:1, gmask:1
         2: Middle Button             , default: Mouse Middle Button         => Middle Button             
             mse, reprogrammable, divertable, pos:0, group:2, gmask:3
         3: Back Button               , default: Mouse Back Button           => Back Button               
             mse, reprogrammable, divertable, pos:0, group:2, gmask:3
         4: Forward Button            , default: Mouse Forward Button        => Forward Button            
             mse, reprogrammable, divertable, pos:0, group:2, gmask:3
         5: Left Tilt                 , default: Mouse Scroll Left Button    => Left Tilt                 
             mse, reprogrammable, divertable, pos:0, group:2, gmask:3
         6: Right Tilt                , default: Mouse Scroll Right Button   => Right Tilt                
             mse, reprogrammable, divertable, pos:0, group:2, gmask:3
     Battery: 90%, discharging, next level 50%.
```

**Describe the bug**
After the device powers off (for a significant amount of time +2 hours I think?) when it powers back up again, it doesn't restore settings such as the speed of the scrollwheel, or the side buttons.  Restarting solaar restores these settings.  It was suggested I open a new bug on issue #443 if this still happened with 1.0.3.  The only other information I could find matching my problem was a really old redhat bug (for reference: https://bugzilla.redhat.com/show_bug.cgi?id=1701322 ).  Please shout if you need any additional information.

**To Reproduce**
Steps to reproduce the behavior:
1. Start Solaar
2. Scroll at a reasonable speed, configure side buttons to middle click
3. Wait for the device to fall asleep (several hours)
4. Wake the mouse up and scroll/click side buttons.

**Screenshots**
No applicable

**Additional context**
I was asked to provide the output from `solaar -dd` to help diagnose the problem, here's the output across the sleep boundary:
```
...
03:14:41,111    DEBUG [ReceiverListener:hidraw4] logitech_receiver.base: (17) => r[20 01 0200 0001000000000000000000]
03:14:41,197    DEBUG [ReceiverListener:hidraw4] logitech_receiver.base: (17) => r[20 01 0201 0000000000000000000000]
03:14:41,293    DEBUG [ReceiverListener:hidraw4] logitech_receiver.base: (17) => r[20 01 0201 0000100000000000000000]
03:14:41,300    DEBUG [ReceiverListener:hidraw4] logitech_receiver.base: (17) => r[20 01 0200 0000000000000000000000]
03:19:52,047    DEBUG [ReceiverListener:hidraw4] logitech_receiver.base: (17) => r[10 01 4104 526D40]
03:19:52,048     INFO [ReceiverListener:hidraw4] solaar.listener: Notification(1,41,04,526D40) triggered new device <PairedDevice(1,406D,M705 (M-R0073),2B486C23)> (mouse)
03:19:52,048     INFO [ReceiverListener:hidraw4] solaar.listener: status_changed <UnifyingReceiver(/dev/hidraw4,17)>: present, 1 paired device. (0) 
03:19:52,049    DEBUG [ReceiverListener:hidraw4] logitech_receiver.notifications: <PairedDevice(1,406D,M705 (M-R0073),2B486C23)>: eQUAD step 4 DJ connection notification: software=True, encrypted=False, link=False, payload=False
03:19:52,050     INFO [ReceiverListener:hidraw4] solaar.listener: status_changed <PairedDevice(1,406D,M705 (M-R0073),2B486C23)>: paired offline, {'BATTERY LEVEL': 90} (0) 
03:19:52,050    DEBUG [MainThread] solaar.ui: status changed: <UnifyingReceiver(/dev/hidraw4,17)> (NONE) None
03:19:52,051    DEBUG [ReceiverListener:hidraw4] logitech_receiver.base: (17) => r[20 01 4201 0000000000000000000000]
03:19:52,053    DEBUG [ReceiverListener:hidraw4] logitech_receiver.notifications: <PairedDevice(1,406D,M705 (M-R0073),2B486C23)> (4.5) DJ notification Notification(1,42,01,0000000000000000000000)
03:19:52,053     INFO [ReceiverListener:hidraw4] logitech_receiver.notifications: <PairedDevice(1,406D,M705 (M-R0073),2B486C23)>: ignoring DJ connection: Notification(1,42,01,0000000000000000000000)
03:19:52,053    DEBUG [MainThread] solaar.ui.icons: battery icon for 90:None = battery-full

(solaar:1067247): Gtk-CRITICAL **: 03:19:52.054: gtk_widget_get_scale_factor: assertion 'GTK_IS_WIDGET (widget)' failed
03:19:52,056    DEBUG [MainThread] solaar.ui: status changed: <PairedDevice(1,406D,M705 (M-R0073),2B486C23)> (NONE) None
03:19:52,057    DEBUG [MainThread] solaar.ui.icons: battery icon for 90:None = battery-full
03:19:52,057    DEBUG [MainThread] solaar.ui.tray: picked device with lowest battery: ('/dev/hidraw4', 1, 'Marathon Mouse M705 (M-R0073)', {'BATTERY LEVEL': 90})
03:19:52,058    DEBUG [MainThread] solaar.ui.icons: battery icon for 90:None = battery-full

(solaar:1067247): Gtk-CRITICAL **: 03:19:52.058: gtk_widget_get_scale_factor: assertion 'GTK_IS_WIDGET (widget)' failed
03:19:52,059    DEBUG [MainThread] solaar.ui.icons: battery icon for 90:None = battery-full
03:19:52,060    DEBUG [MainThread] solaar.ui.icons: battery icon for 90:None = battery-full
03:19:52,061    DEBUG [AsyncUI] logitech_receiver.settings: hires-smooth-invert: settings read False from <PairedDevice(1,406D,M705 (M-R0073),2B486C23)>
03:19:52,062    DEBUG [AsyncUI] logitech_receiver.settings: hires-smooth-resolution: settings read True from <PairedDevice(1,406D,M705 (M-R0073),2B486C23)>
03:19:52,062    DEBUG [AsyncUI] logitech_receiver.settings: pointer_speed: settings read 256 from <PairedDevice(1,406D,M705 (M-R0073),2B486C23)>
03:19:52,063    DEBUG [AsyncUI] logitech_receiver.settings: reprogrammable-keys: settings read {'80': 80, '81': 81, '82': 82, '83': 82, '86': 82, '91': 83, '93': 86} from <PairedDevice(1,406D,M705 (M-R0073),2B486C23)>
09:20:50,455    DEBUG [ReceiverListener:hidraw4] logitech_receiver.base: (17) => r[20 01 4200 0000000000000000000000]
09:20:50,456    DEBUG [ReceiverListener:hidraw4] logitech_receiver.notifications: <PairedDevice(1,406D,M705 (M-R0073),2B486C23)> (4.5) DJ notification Notification(1,42,00,0000000000000000000000)
09:20:50,456     INFO [ReceiverListener:hidraw4] logitech_receiver.notifications: <PairedDevice(1,406D,M705 (M-R0073),2B486C23)>: ignoring DJ connection: Notification(1,42,00,0000000000000000000000)
09:20:50,457    DEBUG [ReceiverListener:hidraw4] logitech_receiver.base: (17) => r[11 01 0400 01010100000000000000000000000000]
09:20:50,458  WARNING [ReceiverListener:hidraw4] logitech_receiver.notifications: <PairedDevice(1,406D,M705 (M-R0073),2B486C23)>: unrecognized Notification(1,04,00,01010100000000000000000000000000) for feature None (index 04)
09:20:50,459    DEBUG [ReceiverListener:hidraw4] logitech_receiver.base: (17) => r[20 01 0200 00FEDFFF00000000000000]
09:20:50,461    DEBUG [ReceiverListener:hidraw4] logitech_receiver.base: (17) => r[20 01 0200 00FCBFFF00000000000000]
09:20:50,463    DEBUG [ReceiverListener:hidraw4] logitech_receiver.base: (17) => r[20 01 0200 00F8AFFF00000000000000]
09:20:50,471    DEBUG [ReceiverListener:hidraw4] logitech_receiver.base: (17) => r[20 01 0200 00F79FFF00000000000000]
09:20:50,479    DEBUG [ReceiverListener:hidraw4] logitech_receiver.base: (17) => r[20 01 0200 00F3AFFF00000000000000]
09:20:50,487    DEBUG [ReceiverListener:hidraw4] logitech_receiver.base: (17) => r[20 01 0200 00F0AFFF00000000000000]
...
```


Discussion:
I think I see the problem.  The initial device connection notification has a bit set it in that indicates that it is not possible to communicate with the device.  Solaar thus thinks that it is not possible to change the device's settings.  So far so good.

But Solaar doesn't recognize the other notifications that would indicate that the device is connected, partly because these notifications are usually not needed and have caused problems in the past.  I'll see whether they can be used to tell Solaar that it is possible to communicate with the device.

Resolving patch:
diff --git a/lib/logitech_receiver/notifications.py b/lib/logitech_receiver/notifications.py
--- a/lib/logitech_receiver/notifications.py
+++ b/lib/logitech_receiver/notifications.py
@@ -131,7 +131,7 @@ def _process_device_notification(device, status, n):
 
 def _process_dj_notification(device, status, n):
     if _log.isEnabledFor(_DEBUG):
-        _log.debug('%s (%s) DJ notification %s', device, device.protocol, n)
+        _log.debug('%s (%s) DJ %s', device, device.protocol, n)
 
     if n.sub_id == 0x40:
         # do all DJ paired notifications also show up as HID++ 1.0 notifications?
@@ -146,8 +146,10 @@ def _process_dj_notification(device, status, n):
         return True
 
     if n.sub_id == 0x42:
+        connected = not n.address & 0x01
         if _log.isEnabledFor(_INFO):
-            _log.info('%s: ignoring DJ connection: %s', device, n)
+            _log.info('%s: DJ connection: %s %s', device, connected, n)
+        status.changed(active=connected, alert=_ALERT.NONE, reason=_('connected') if connected else _('disconnected'))
         return True
 
     _log.warn('%s: unrecognized DJ %s', device, n)
@@ -253,6 +255,9 @@ def _process_feature_notification(device, status, n, feature):
             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)
+        elif n.address == 0x10:
+            if _log.isEnabledFor(_INFO):
+                _log.info('%s: spurious BATTERY status %s', device, n)
         else:
             _log.warn('%s: unknown BATTERY %s', device, n)
         return True
diff --git a/lib/logitech_receiver/settings.py b/lib/logitech_receiver/settings.py
--- a/lib/logitech_receiver/settings.py
+++ b/lib/logitech_receiver/settings.py
@@ -279,11 +279,13 @@ def write_key_value(self, key, value):
             # Remember the value we're trying to set, even if the write fails.
             # This way even if the device is offline or some other error occurs,
             # the last value we've tried to write is remembered in the configuration.
-            self._value[str(key)] = value
-            if self.persist and self._device.persister:
-                self._device.persister[self.name] = self._value
-
-            data_bytes = self._validator.prepare_write(int(key), value)
+            try:
+                data_bytes = self._validator.prepare_write(int(key), value)
+                self._value[str(key)] = value
+                if self.persist and self._device.persister:
+                    self._device.persister[self.name] = self._value
+            except ValueError:
+                data_bytes = value = None
             if data_bytes is not None:
                 if _log.isEnabledFor(_DEBUG):
                     _log.debug('%s: settings prepare key value write(%s,%s) => %r', self.name, key, value, data_bytes)
@@ -291,7 +293,6 @@ def write_key_value(self, key, value):
                 if not reply:
                     # tell whomever is calling that the write failed
                     return None
-
             return value
 
 


Test patch: