Repository: pwr-Solaar/Solaar

Issue:
Does this support MX Keys Mini ?
Hello, I just want to ask if this software supports MX Keys Mini. I checked the list of supported devices but there is only "MX Keys" so I don't really know.

Thank you !


Discussion:
This appears to be one of the new Logitech devices that can connect via a Bolt receiver.  Solaar hasn't been tested with any Bolt receiver, but it should be easy to add them and support should be coming soon.  If the MX Keys Mini is connected via Bluetooth supporting it should require at most a very simple change to Solaar.  
If you have the keyboard then you can help adding support for it in Solaar by finding its bluetooth product ID number.  First find its bluetooth address from `bluetoothctl devices`.  Then find out information about it from `bluetoothctl info <address>`.  The bluetooth product ID number is the hex number after 'p' in the modalias line.
Thanks for answering.
Yes, I have the keyboard connected via Bluetooth. If I understood correctly what you asked, the ID is `B369d0004`. I'm enclosing the full output of `bluetoothctl info <address>` in case of mistake : 
```
Device D2:8C:7A:F0:B7:2B (random)
	Name: MX Keys Mini
	Alias: MX Keys Mini
	Appearance: 0x03c1
	Icon: input-keyboard
	Paired: yes
	Trusted: yes
	Blocked: no
	Connected: yes
	LegacyPairing: no
	UUID: Generic Access Profile    (00001800-0000-1000-8000-00805f9b34fb)
	UUID: Generic Attribute Profile (00001801-0000-1000-8000-00805f9b34fb)
	UUID: Device Information        (0000180a-0000-1000-8000-00805f9b34fb)
	UUID: Battery Service           (0000180f-0000-1000-8000-00805f9b34fb)
	UUID: Human Interface Device    (00001812-0000-1000-8000-00805f9b34fb)
	UUID: Unknown                   (0000fd72-0000-1000-8000-00805f9b34fb)
	UUID: Vendor specific           (00010000-0000-1000-8000-011f2000046d)
	Modalias: usb:v046DpB369d0004
	ManufacturerData Key: 0x0006
	ManufacturerData Value:
  03 00 80                                         ...             
	ServiceData Key: 0000fd72-0000-1000-8000-00805f9b34fb
	ServiceData Value:
  10 01 01 69 b3 01 01 00  
``` 
 
I see that the span of the hex number is a bit ambiguous.  These hex numbers are all 16 bits, so the number is B369.  That number is accepted as a valid product number by Solaar already.

So Solaar might already support this device.  Install a recent version of Solaar (1.0.5 or better, probably) and see whether Solaar accepts the device.  If it doesn't, run Solaar as `solaar -dd` and post the output here.
I'll try, thanks !
I installed Solaar 1.0.7, and it seems to work, thanks ! Maybe you can add it to the list ?
What I did is remove some information from that file.  :-)  There are lots of devices that are supported by Solaar and not mentioned there.   The best thing to do to determine if a device is supported is to run Solaar.
If you have a Bolt receiver could you help out in checking the upcoming changes in Solaar to support it?

Resolving patch:
diff --git a/lib/logitech_receiver/special_keys.py b/lib/logitech_receiver/special_keys.py
--- a/lib/logitech_receiver/special_keys.py
+++ b/lib/logitech_receiver/special_keys.py
@@ -32,7 +32,7 @@
     Stop=0x0007,
     Application_Switcher=0x0008,
     Burn=0x0009,
-    Calculator=0x000A,
+    Calculator=0x000A,  # Craft Keyboard top 4th from right
     Calendar=0x000B,
     Close=0x000C,
     Eject=0x000D,
@@ -130,8 +130,8 @@
     Button_22=0x006B,
     Button_23=0x006C,
     Button_24=0x006D,
-    Show_Desktop=0x006E,  # Show_Desktop
-    Lock_PC=0x006F,
+    Show_Desktop=0x006E,  # Craft Keyboard Fn F5
+    Lock_PC=0x006F,  # Craft Keyboard top 1st from right
     Fn_F1=0x0070,
     Fn_F2=0x0071,
     Fn_F3=0x0072,
@@ -198,7 +198,7 @@
     Multiplatform_Menu=0x00BC,
     Multiplatform_Back=0x00BD,
     Multiplatform_Insert=0x00BE,
-    Screen_Capture__Print_Screen=0x00BF,  # on Craft Keyboard
+    Screen_Capture__Print_Screen=0x00BF,  # Craft Keyboard top 3rd from right
     Fn_Down=0x00C0,
     Fn_Up=0x00C1,
     Multiplatform_Lock=0x00C2,
@@ -206,8 +206,8 @@
     Smart_Shift=0x00C4,  # Top_Button on MX Master
     Microphone=0x00C5,
     Wifi=0x00C6,
-    Brightness_Down=0x00C7,
-    Brightness_Up=0x00C8,
+    Brightness_Down=0x00C7,  # Craft Keyboard Fn F1
+    Brightness_Up=0x00C8,  # Craft Keyboard Fn F2
     Display_Out__Project_Screen_=0x00C9,
     View_Open_Apps=0x00CA,
     View_All_Apps=0x00CB,
@@ -216,9 +216,9 @@
     MultiPlatform_Back=0x00CE,
     MultiPlatform_Forward=0x00CF,
     MultiPlatform_Gesture_Button=0x00D0,
-    Host_Switch_Channel_1=0x00D1,
-    Host_Switch_Channel_2=0x00D2,
-    Host_Switch_Channel_3=0x00D3,
+    Host_Switch_Channel_1=0x00D1,  # Craft Keyboard
+    Host_Switch_Channel_2=0x00D2,  # Craft Keyboard
+    Host_Switch_Channel_3=0x00D3,  # Craft Keyboard
     MultiPlatform_Search=0x00D4,
     MultiPlatform_Home__Mission_Control=0x00D5,
     MultiPlatform_Menu__Show__Hide_Virtual_Keyboard__Launchpad=0x00D6,
@@ -231,17 +231,17 @@
     Multi_Platform_Language_Switch=0x00DD,
     F_Lock=0x00DE,
     Switch_Highlight=0x00DF,
-    Mission_Control__Task_View=0x00E0,  # Switch_Workspaces on Craft Keyboard
-    Dashboard_Launchpad__Action_Center=0x00E1,  # Application_Launcher on Craft Keyboard
-    Backlight_Down=0x00E2,
-    Backlight_Up=0x00E3,
-    Previous_Fn=0x00E4,  # Reprogrammable_Previous_Track / on Craft Keyboard
-    Play__Pause_Fn=0x00E5,  # Reprogrammable_Play__Pause / on Craft Keyboard
-    Next_Fn=0x00E6,  # Reprogrammable_Next_Track / on Craft Keyboard
-    Mute_Fn=0x00E7,  # Reprogrammable_Mute / on Craft Keyboard
-    Volume_Down_Fn=0x00E8,  # Reprogrammable_Volume_Down / on Craft Keyboard
-    Volume_Up_Fn=0x00E9,  # Reprogrammable_Volume_Up / on Craft Keyboard
-    App_Contextual_Menu__Right_Click=0x00EA,  # Context_Menu on Craft Keyboard
+    Mission_Control__Task_View=0x00E0,  # Craft Keyboard Fn F3 Switch_Workspace
+    Dashboard_Launchpad__Action_Center=0x00E1,  # Craft Keyboard Fn F4 Application_Launcher
+    Backlight_Down=0x00E2,  # Craft Keyboard Fn F6
+    Backlight_Up=0x00E3,  # Craft Keyboard Fn F7
+    Previous_Fn=0x00E4,  # Craft Keyboard Fn F8 Previous_Track
+    Play__Pause_Fn=0x00E5,  # Craft Keyboard Fn F9 Play__Pause
+    Next_Fn=0x00E6,  # Craft Keyboard Fn F10 Next_Track
+    Mute_Fn=0x00E7,  # Craft Keyboard Fn F11 Mute
+    Volume_Down_Fn=0x00E8,  # Craft Keyboard Fn F12 Volume_Down
+    Volume_Up_Fn=0x00E9,  # Craft Keyboard next to F12 Volume_Down
+    App_Contextual_Menu__Right_Click=0x00EA,  # Craft Keyboard top 2nd from right
     Right_Arrow=0x00EB,
     Left_Arrow=0x00EC,
     DPI_Change=0x00ED,
@@ -266,6 +266,11 @@
     Multiplatform_App_Switch_2=0x0100,  # Multiplatform_App_Switch
     Fn_Inversion__Hot_Key=0x0101,
     LeftAndRightClick=0x0102,
+    Show_Desktop_Fn=0x0103,  # MX Keys for Business Fn F5 ; MX Mini Fn F6 Dictation
+    Backlight_Down_Fn=0x0108,  # MX Keys for Business Fn F6 ; MX Mini Fn F7 Emoji
+    Screenshot=0x010A,  # MX Keys for Business top 3rd from right; MX Mini Fn F8 Screenshot
+    Lock_Fn=0x0117,  # MX Mini Lock (on delete key in function row)
+    Backlight_Up_Fn=0x011C,  # MX Keys for Business Fn F7 ; MX Mini Fn F9 Microphone Mute
     LED_Toggle=0x013B,  #
 )
 
diff --git a/lib/solaar/ui/tray.py b/lib/solaar/ui/tray.py
--- a/lib/solaar/ui/tray.py
+++ b/lib/solaar/ui/tray.py
@@ -44,7 +44,6 @@
 
 _TRAY_ICON_SIZE = 48
 _MENU_ICON_SIZE = Gtk.IconSize.LARGE_TOOLBAR
-_RECEIVER_SEPARATOR = ('~', None, None, None)
 
 #
 #
@@ -56,7 +55,7 @@ def _create_menu(quit_handler):
 
     # per-device menu entries will be generated as-needed
 
-    no_receiver = Gtk.MenuItem.new_with_label(_('No Logitech receiver found'))
+    no_receiver = Gtk.MenuItem.new_with_label(_('No Logitech device found'))
     no_receiver.set_sensitive(False)
     menu.append(no_receiver)
     menu.append(Gtk.SeparatorMenuItem.new())
@@ -87,7 +86,7 @@ def _scroll(tray_icon, event, direction=None):
 
     if len(_devices_info) < 4:
         # don't bother with scrolling when there's only one receiver
-        # with only one device (3 = [receiver, device, separator])
+        # with only one or two devices
         return
 
     # scroll events come way too fast (at least 5-6 at once)
@@ -334,7 +333,7 @@ def _pick_device_with_lowest_battery():
     picked_level = 1000
 
     for info in _devices_info:
-        if info[1] is None:  # is receiver/separator
+        if info[1] is None:  # is receiver
             continue
         level = info[-1].get(_K.BATTERY_LEVEL)
         # print ("checking %s -> %s", info, level)
@@ -353,13 +352,6 @@ def _pick_device_with_lowest_battery():
 #
 
 
-def _add_separator(index):
-    _devices_info.insert(index + 1, _RECEIVER_SEPARATOR)
-    separator = Gtk.SeparatorMenuItem.new()
-    separator.set_visible(True)
-    _menu.insert(separator, index + 1)
-
-
 def _add_device(device):
     assert device
 
@@ -372,7 +364,7 @@ def _add_device(device):
                 break
         while index < len(_devices_info):
             path, number, _ignore, _ignore = _devices_info[index]
-            if path == _RECEIVER_SEPARATOR[0]:
+            if not path == receiver_path:
                 break
             assert number != device.number
             if number > device.number:
@@ -380,7 +372,6 @@ def _add_device(device):
             index = index + 1
 
     new_device_info = (receiver_path, device.number, device.name, device.status)
-    assert len(new_device_info) == len(_RECEIVER_SEPARATOR)
     _devices_info.insert(index, new_device_info)
 
     label_prefix = '   '
@@ -407,11 +398,10 @@ def _remove_device(index):
 
 
 def _add_receiver(receiver):
-    index = 0
+    index = len(_devices_info)
 
     new_receiver_info = (receiver.path, None, receiver.name, None)
-    assert len(new_receiver_info) == len(_RECEIVER_SEPARATOR)
-    _devices_info.insert(0, new_receiver_info)
+    _devices_info.insert(index, new_receiver_info)
 
     new_menu_item = Gtk.ImageMenuItem.new_with_label(receiver.name)
     icon_set = _icons.device_icon_set(receiver.name)
@@ -420,25 +410,16 @@ def _add_receiver(receiver):
     new_menu_item.connect('activate', _window_popup, receiver.path)
     _menu.insert(new_menu_item, index)
 
-    _add_separator(index)
-
     return 0
 
 
 def _remove_receiver(receiver):
     index = 0
-    found = False
-
     # remove all entries in devices_info that match this receiver
     while index < len(_devices_info):
         path, _ignore, _ignore, _ignore = _devices_info[index]
         if path == receiver.path:
-            found = True
-            _remove_device(index)
-        elif found and path == _RECEIVER_SEPARATOR[0]:
-            # the separator after this receiver
             _remove_device(index)
-            break
         else:
             index += 1
 
@@ -537,7 +518,6 @@ def update(device=None):
         menu_items = _menu.get_children()
         no_receivers_index = len(_devices_info)
         menu_items[no_receivers_index].set_visible(not _devices_info)
-        menu_items[no_receivers_index + 1].set_visible(not _devices_info)
 
     global _picked_device
     if (not _picked_device or _last_scroll == 0) and device is not None and device.kind is not None:


Test patch: