Repository: pwr-Solaar/Solaar

Issue:
CLI version no longer interacts with udev properly
**Information**
<!-- Make sure that your issue is not one of the known issues in the Solaar documentation at https://pwr-solaar.github.io/Solaar/ -->
<!-- Do not bother opening an issue for a version older than 1.1.0.  Update to the latest version and see if your issue persists. -->
- Solaar version (`solaar --version` or `git describe --tags` if cloned from this repository): solaar 1.1.2
- Distribution: Linux Mint 20.3 Cinnamon
- Kernel version (ex. `uname -srmo`): `Linux 5.4.0-107-generic x86_64 GNU/Linux`
- Output of `solaar show`:

<details>

```
Solaar version 1.1.2

Unifying Receiver
  Device path  : /dev/hidraw0
  USB id       : 046d:C52B
  Serial       : A4DE9A0E
    Firmware   : 24.11.B0036
    Bootloader : 02.09
    Other      : AA.AC
  Has 1 paired device(s) out of a maximum of 6.
  Notifications: wireless, software present (0x000900)
  Device activity counters: 1=155

  1: Wireless Touch Keyboard K400 Plus
     Device path  : /dev/hidraw1
     WPID         : 404D
     Codename     : K400 Plus
     Kind         : keyboard
     Protocol     : HID++ 4.1
     Polling rate : 8 ms (125Hz)
     Serial number: 70CE825C
     Model ID:      404D00000000
     Unit ID:       563AF230
        Bootloader: BOT 22.02.B0002
          Firmware: RQK 63.02.B0016
             Other: 
     The power switch is located on the top edge.
     Supports 24 HID++ 2.0 features:
         0: ROOT                   {0000}   
         1: FEATURE SET            {0001}   
         2: DEVICE FW VERSION      {0003}   
            Firmware: Bootloader BOT 22.02.B0002 404D6217AD40
            Firmware: Firmware RQK 63.02.B0016 404D6217AD40
            Firmware: Other   
            Unit ID: 563AF230  Model ID: 404D00000000  Transport IDs: {'wpid': '404D'}
         3: DEVICE NAME            {0005}   
            Name: Wireless Touch Keyboard K400 Plus
            Kind: keyboard
         4: WIRELESS DEVICE STATUS {1D4B}   
         5: RESET                  {0020}   
         6: BATTERY STATUS         {1000}   
            Battery: 50%, discharging, next level 20%.
         7: REPROG CONTROLS V4     {1B04}   
            Key/Button Diversion (saved): {Volume Up:Regular, Show Desktop:Regular, Maximize Window:Regular, Switch Screen:Regular, Fn Left Click:Regular, Second Left Click:Regular, Volume Down:Regular, Mute:Regular, My Home:Regular, Play/Pause:Regular, Media Player:Regular, Next:Regular, Previous:Regular, Search:Regular, Sleep:Regular, Contextual Menu:Regular, Application Switcher:Regular, Back:Regular}
            Key/Button Diversion        : {Volume Up:Regular, Show Desktop:Regular, Maximize Window:Regular, Switch Screen:Regular, Fn Left Click:Regular, Second Left Click:Regular, Volume Down:Regular, Mute:Regular, My Home:Regular, Play/Pause:Regular, Media Player:Regular, Next:Regular, Previous:Regular, Search:Regular, Sleep:Regular, Contextual Menu:Regular, Application Switcher:Regular, Back:Regular}
         8: SWAP BUTTON CANCEL     {2005}   
         9: NEW FN INVERSION       {40A2}   
            Fn-swap: enabled
            Fn-swap default: enabled
            Swap Fx function (saved): False
            Swap Fx function        : True
        10: ENCRYPTION             {4100}   
        11: KEYBOARD DISABLE KEYS  {4521}   
            Disable keys (saved): {Caps Lock:False, Win:False, Scroll Lock:False, Insert:False}
            Disable keys        : {Caps Lock:False, Scroll Lock:False, Insert:False, Win:False}
        12: TOUCHPAD RAW XY        {6100}   
        13: GESTURE 2              {6501}   
            Gestures (saved): {'1': True, '10': True, '2': True, '30': True, '34': False, '42': True, '43': True, '45': False, '64': False, '65': False, '67': False, '84': True}
            Gestures        : {'1': True, '2': True, '30': True, '10': True, '45': False, '42': True, '43': True, '64': False, '65': False, '67': False, '84': True, '34': False}
            Gestures Diversion (saved): {'1': False, '10': False, '100': False, '2': False, '44': False, '64': False, '65': False, '67': False, '84': False, '85': False}
            Gestures Diversion        : {'1': False, '2': False, '10': False, '44': False, '64': False, '65': False, '67': False, '84': False, '85': False, '100': False}
            Gesture params (saved): {'4': {'scale': 256}}
            Gesture params        : {'4': {'scale': 256}}
        14: DFUCONTROL UNSIGNED    {00C1}   
        15: unknown:1811           {1811}   internal, hidden
        16: unknown:1830           {1830}   internal, hidden
        17: unknown:1890           {1890}   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:18B0           {18B0}   internal, hidden
        23: unknown:1F11           {1F11}   internal, hidden
     Has 20 reprogrammable keys:
         0: Second Left Click         , default: Left Click                  => Left Click                
             mse, divertable, pos:0, group:0, group mask:empty
             reporting: default
         1: Back                      , default: Mouse Back Button           => Mouse Back Button         
             is FN, FN sensitive, reprogrammable, divertable, pos:1, group:0, group mask:empty
             reporting: default
         2: My Home                   , default: HomePage                    => HomePage                  
             is FN, FN sensitive, reprogrammable, divertable, pos:2, group:0, group mask:empty
             reporting: default
         3: Application Switcher      , default: Application Switcher        => Application Switcher      
             is FN, FN sensitive, reprogrammable, divertable, pos:3, group:0, group mask:empty
             reporting: default
         4: Contextual Menu           , default: Right Click                 => Right Click               
             is FN, FN sensitive, reprogrammable, divertable, pos:4, group:0, group mask:empty
             reporting: default
         5: Search                    , default: Search Files                => Search Files              
             is FN, FN sensitive, reprogrammable, divertable, pos:5, group:0, group mask:empty
             reporting: default
         6: Show Desktop              , default: Show Desktop                => Show Desktop              
             is FN, FN sensitive, reprogrammable, divertable, pos:6, group:0, group mask:empty
             reporting: default
         7: Maximize Window           , default: Maximize Window             => Maximize Window           
             is FN, FN sensitive, reprogrammable, divertable, pos:7, group:0, group mask:empty
             reporting: default
         8: Switch Screen             , default: Switch Presentation/Switch Screen => Switch Presentation/Switch Screen
             is FN, FN sensitive, reprogrammable, divertable, pos:8, group:0, group mask:empty
             reporting: default
         9: Media Player              , default: Music                       => Music                     
             is FN, FN sensitive, reprogrammable, divertable, pos:9, group:0, group mask:empty
             reporting: default
        10: Previous                  , default: Previous                    => Previous                  
             is FN, FN sensitive, divertable, pos:10, group:0, group mask:empty
             reporting: default
        11: Play/Pause                , default: Play/Pause                  => Play/Pause                
             is FN, FN sensitive, divertable, pos:11, group:0, group mask:empty
             reporting: default
        12: Next                      , default: Next                        => Next                      
             is FN, FN sensitive, divertable, pos:12, group:0, group mask:empty
             reporting: default
        13: Fn Left Click             , default: ShowUI                      => ShowUI                    
             divertable, pos:0, group:0, group mask:empty
             reporting: default
        14: Left Button               , default: Left Click                  => Left Click                
             mse, pos:0, group:0, group mask:empty
             reporting: default
        15: Right Button              , default: Right Click                 => Right Click               
             mse, pos:0, group:0, group mask:empty
             reporting: default
        16: Mute                      , default: Mute                        => Mute                      
             nonstandard, divertable, pos:0, group:0, group mask:empty
             reporting: default
        17: Volume Up                 , default: Volume Up                   => Volume Up                 
             nonstandard, divertable, pos:0, group:0, group mask:empty
             reporting: default
        18: Volume Down               , default: Volume Down                 => Volume Down               
             nonstandard, divertable, pos:0, group:0, group mask:empty
             reporting: default
        19: Sleep                     , default: Sleep                       => Sleep                     
             reprogrammable, divertable, pos:0, group:0, group mask:empty
             reporting: default
     Has 17 gesture(s), 1 param(s) and 5 spec(s):
        Track1Finger               Enabled(None): None   Diverted:(None) None
        TapGestures                Enabled(None): None   Diverted:(None) None
        Tap1Finger                 Enabled(   0): True   Diverted:(   0) False
        Tap2Finger                 Enabled(   1): True   Diverted:(   1) False
        TapDrag1Finger             Enabled(   2): True   Diverted:(None) None
        DoubleTap1Finger           Enabled(   3): True   Diverted:(   2) False
        Scroll2FingerStateless     Enabled(None): None   Diverted:(   3) False
        NaturalScrolling           Enabled(   4): False  Diverted:(None) None
        Scroll2FingerHoriz         Enabled(   5): True   Diverted:(None) None
        Scroll2FingerVert          Enabled(   6): True   Diverted:(None) None
        LeftEdgeSwipe1Finger2      Enabled(   7): False  Diverted:(   4) False
        RightEdgeSwipe1Finger2     Enabled(   8): False  Diverted:(   5) False
        TopEdgeSwipe1Finger2       Enabled(   9): False  Diverted:(   6) False
        Zoom2FingerStateless       Enabled(  10): True   Diverted:(   7) False
        FnClickGestureSuppression  Enabled(  11): False  Diverted:(None) None
        TwoFingersPresent          Enabled(None): None   Diverted:(   8) False
        DeviceSpecificRawData      Enabled(None): None   Diverted:(   9) False
        ScaleFactor                Value  (   0): 256 [Default: 256]
        period unit                Spec   (   3): 2048
        DVI field width            Spec   (   1): 8
        field widths               Spec   (   2): 8
        multiplier                 Spec   (   5): None
        resolution                 Spec   (   4): 4
     Battery: 50%, discharging, next level 20%.

```
</details>

**Describe the bug**
While Issue #1535 seems to have been fixed with PR #1536 my udev rule no longer reapplies settings upon keyboard power cycle.

**To Reproduce**
Steps to reproduce the behavior:
<details>

1. Create a script at `/usr/local/bin/forcesolaarconfig.sh` with the contents
```
#!/bin/bash
solaar config 1 fn-swap 0
echo "Solaar configuration applied at: $(date)" >> /tmp/solaar.log
```
Be sure to make it executable with `chmod 0755 forcesolaarconfig.sh`

2. Create a udev rule at `/etc/udev/rules.d/99-forcesolaarconfig.rules` with the contents
```
ACTION=="change", SUBSYSTEM=="power_supply", ENV{POWER_SUPPLY_SERIAL_NUMBER}=="404d*", RUN+="/usr/local/bin/forcesolaarconfig.sh"
```
Where `ENV{POWER_SUPPLY_SERIAL_NUMBER}=="404d*"` can be found by running `udevadm monitor -u -p -s power_supply` and power cycling the keyboard or using the first bit from `MODEL ID` in `solaar show` output.

3. Reload udev rules with `udevadm control --reload-rules`
4. Now run `solaar config 1 fn-swap 0` and press an F key, say F3, to trigger its functionality. Eg; F3 = find
5. Power cycle the keyboard.
6. Notice that pressing F3 no longer triggers the find dialog but (as in my case) the system's application window switcher.
7. Navigating to `/tmp` confirms udev was indeed triggered as `solaar.log` was created containing a timestamp but Solaar settings were not reapplied.

</details>

**Additional context**
I know I was running version 1.0.6 last summer and the above was working as intended.
IIRC, Solaar had a few minor updates between then and a week or so ago and the above was still working just fine.
I _think_ I might have been on 1.1.1 from January up until about a week ago, but I'm not really sure so I can't confirm if above works there or not. Apt didn't have it listed when I was playing around with downgrading and whatnot yesterday.
At any rate, something in the code changed and I'm eager to get it sorted.
Thanks in advance.


Discussion:
There might be a problem with timing here.   Solaar might be sending a device command while the device is still starting up and the command is ignored.  You could try to add a short (1 second?) pause in the script.  You could also run the Solaar config command with debugging turned on `-ddd` and send its output to a file.

You could also look at the responses the device is sending.   Find out which hidraw node your receiver is using by running the Solaar GUI and opening the info pane for the receiver.   THe hidraw node is show as the Path value.  From the Solaar directory run `./tools/hidconsole` in a terminal window.  Make sure that the Solaar GUI is not running.  Then run your script.  The last two lines of output from hidconsole should be the device responding to two commands from Solaar - the last line should be the device saying that it has changed the value for fn-swap and the second-last line should be the device saying what its value for fn-swap was at the time.  So something like
```
11 01 090X 0W00...
11 01 091Y 0V00..
```
Then turn the device off and on.  There should again be two lines like the above.  

Also, make sure that the Solaar that is being run from the udev rule has PR #1536 applied.  (Yes, you've done this, but please make sure.)




Aah, I think I know what is going on.   Solaar config now tries to coordinate with the Solaar GUI, if running, so that they do not become confused as to what settings should be retained.  However, this might not be possible inside a udev rule.   

So please arrange to have debugging output captured from your script and post that here.
> So please arrange to have debugging output captured from your script and post that here.

How exactly?
I just purged the PPA install from my system (was updated yesterday a few hours after PR #1536 was merged by the maintainer).
I cloned the lasted repo from git just now.
Am I to leave the udev rule in etc or should I move it into the rules.d directory in the repo?
I'm a bit confused on how to do this without a "proper" install.

EDIT: FWIW, we're on hidraw1
You should be able to run Solaar from where you cloned it by changing your script to use a file path (<solaar directory>/bin/solaar).   You should also be able to redirect the output from the Solaar command to a file.   So the script should look something like:
```
#!/bin/bash
<Solaar dir>/bin/solaar -ddd config 1 fn-swap 0 &> /tmp/solaar-config.txt
echo "Solaar configuration applied at: $(date)" >> /tmp/solaar.log
```
I expect that there will be an error message in /tmp/solaar-config.txt complaining about not being able to do some GTK action.

Resolving patch:
diff --git a/lib/solaar/cli/config.py b/lib/solaar/cli/config.py
--- a/lib/solaar/cli/config.py
+++ b/lib/solaar/cli/config.py
@@ -187,19 +187,22 @@ def run(receivers, args, find_receiver, find_device):
         _print_setting(setting)
         return
 
-    APP_ID = 'io.github.pwr_solaar.solaar'
-    application = Gtk.Application.new(APP_ID, Gio.ApplicationFlags.HANDLES_COMMAND_LINE)
-    application.register()
+    remote = False
+    if Gtk.init_check()[0]:  # can Gtk be initialized?
+        APP_ID = 'io.github.pwr_solaar.solaar'
+        application = Gtk.Application.new(APP_ID, Gio.ApplicationFlags.HANDLES_COMMAND_LINE)
+        application.register()
+        remote = application.get_is_remote()
 
     # if the Solaar UI is running don't save configuration here
-    result, message, value = set(dev, setting, args, not application.get_is_remote())
+    result, message, value = set(dev, setting, args, not remote)
     if message is not None:
         print(message)
         if result is None:
             raise Exception("%s: failed to set value '%s' [%r]" % (setting.name, str(value), value))
 
     # if the Solaar UI is running tell it to also perform the set
-    if application.get_is_remote():
+    if remote:
         argl = ['config', dev.serial or dev.unitId, setting.name]
         argl.extend([a for a in [args.value_key, args.extra_subkey, args.extra2] if a is not None])
         application.run(_yaml.safe_dump(argl))
diff --git a/lib/solaar/listener.py b/lib/solaar/listener.py
--- a/lib/solaar/listener.py
+++ b/lib/solaar/listener.py
@@ -198,6 +198,12 @@ def _notifications_handler(self, n):
                 _log.debug('Notification %s via device %s being ignored.', n, self.receiver)
             return
 
+        # DJ pairing notification - ignore - hid++ 1.0 pairing notification is all that is needed
+        if n.sub_id == 0x41 and n.report_id == _base.DJ_MESSAGE_ID:
+            if _log.isEnabledFor(_INFO):
+                _log.info('ignoring DJ pairing notification %s', n)
+            return
+
         # a device notification
         if not (0 < n.devnumber <= self.receiver.max_devices):
             if _log.isEnabledFor(_WARNING):
@@ -218,12 +224,7 @@ def _notifications_handler(self, n):
         if n.sub_id == 0x40 and not already_known:
             return  # disconnecting something that is not known - nothing to do
 
-        if n.sub_id == 0x41 and n.report_id == _base.DJ_MESSAGE_ID:
-            # DJ pairing notification - ignore - hid++ 1.0 pairing notification is all that is needed
-            if _log.isEnabledFor(_INFO):
-                _log.info('ignoring DJ pairing notification %s', n)
-            return
-        elif n.sub_id == 0x41:
+        if n.sub_id == 0x41:
             if not already_known:
                 if n.address == 0x0A and not self.receiver.receiver_kind == 'bolt':
                     # some Nanos send a notification even if no new pairing - check that there really is a device there


Test patch: