Repository: pwr-Solaar/Solaar

Issue:
tray.py:375:_add_device:AssertionError
**Information**
- Solaar version (`solaar --version` or `git describe --tags` if cloned from this repository): **1.0.7**
- Distribution: **Fedora 34**
- Kernel version (ex. `uname -srmo`): **5.14.11-200.fc34.x86_64**
- Output of `solaar show`:

<details>

```
Lightspeed Receiver
  Device path  : /dev/hidraw4
  USB id       : 046d:C53D
  Serial       : DC7B41CE
    Firmware   : 43.01.B0009
    Bootloader : 02.10
    Other      : AA.D2
  Has 1 paired device(s) out of a maximum of 1.
  Notifications: wireless, software present (0x000900)
  Device activity counters: (empty)

  1: G613 Wireless Mechanical Gaming Keyboard
     Device path  : None
     WPID         : 4065
     Codename     : G613
     Kind         : keyboard
     Protocol     : HID++ 4.2
     Polling rate : 1 ms (1000Hz)
     Serial number: A4D7BD66
     Model ID:      B34F40650000
     Unit ID:       3195F95E
        Bootloader: BOT 46.00.B0006
          Firmware: MPK 05.02.B0021
             Other: 
     Supports 32 HID++ 2.0 features:
         0: ROOT                   {0000}   
         1: FEATURE SET            {0001}   
         2: DEVICE FW VERSION      {0003}   
            Firmware: Bootloader BOT 46.00.B0006 00006E86A7BD
            Firmware: Firmware MPK 05.02.B0021 40656E86A7BD
            Firmware: Other   
            Unit ID: 3195F95E  Model ID: B34F40650000  Transport IDs: {'btleid': 'B34F', 'wpid': '4065'}
         3: DEVICE NAME            {0005}   
            Name: G613 Wireless Mechanical Gaming Keyboard
            Kind: keyboard
         4: WIRELESS DEVICE STATUS {1D4B}   
         5: RESET                  {0020}   
         6: DEVICE FRIENDLY NAME   {0007}   
            Friendly Name: G613
         7: BATTERY STATUS         {1000}   
            Battery: 80%, discharging, next level 50%.
         8: CHANGE HOST            {1814}   
            Change Host        : 1:vcostago-mobl3
         9: HOSTS INFO             {1815}   
            Host 0 (paired): vcostago-mobl3
            Host 1 (paired): vcostago-mobl3
        10: REPROG CONTROLS V4     {1B04}   
        11: REPORT HID USAGE       {1BC0}   
        12: ENCRYPTION             {4100}   
        13: KEYBOARD DISABLE BY USAGE {4522}   
        14: KEYBOARD LAYOUT 2      {4540}   
        15: GKEY                   {8010}   
            Divert G Keys (saved): False
            Divert G Keys        : False
        16: REPORT RATE            {8060}   
            Polling Rate (ms): 1
            Polling Rate (ms) (saved): 1
            Polling Rate (ms)        : 1
        17: DFUCONTROL SIGNED      {00C2}   
        18: DEVICE RESET           {1802}   internal, hidden
        19: unknown:1803           {1803}   internal, hidden
        20: CONFIG DEVICE PROPS    {1806}   internal, hidden
        21: unknown:1813           {1813}   internal, hidden
        22: OOBSTATE               {1805}   internal, hidden
        23: unknown:1830           {1830}   internal, hidden
        24: unknown:1890           {1890}   internal, hidden
        25: unknown:1891           {1891}   internal, hidden
        26: unknown:18A1           {18A1}   internal, hidden
        27: unknown:1DF3           {1DF3}   internal, hidden
        28: unknown:1E00           {1E00}   hidden
        29: unknown:1EB0           {1EB0}   internal, hidden
        30: unknown:1861           {1861}   internal, hidden
        31: unknown:18B1           {18B1}   internal, hidden
     Has 2 reprogrammable keys:
         0: Host Switch Channel 1     , default: HostSwitch Channel 1        => HostSwitch Channel 1      
             divertable, persistently divertable, pos:1, group:0, group mask:empty
             reporting: default
         1: Host Switch Channel 2     , default: HostSwitch Channel 2        => HostSwitch Channel 2      
             divertable, persistently divertable, pos:2, group:0, group mask:empty
             reporting: default
     Battery: 80%, discharging, next level 50%.

Nano Receiver
  Device path  : /dev/hidraw12
  USB id       : 046d:C537
  Serial       : 946F9D4F
    Firmware   : 34.00.B0002
    Bootloader : 02.14
  Has 1 paired device(s) out of a maximum of 1.
  Notifications: wireless, software present (0x000900)

  1: Wireless Gaming Mouse
     Device path  : /dev/hidraw13
     WPID         : 402C
     Codename     : G602
     Kind         : mouse
     Protocol     : HID++ 2.0
     Polling rate : 2 ms (500Hz)
     Serial number: 5D5335B3
     Model ID:      000000000000
     Unit ID:       00000000
          Firmware: RQM 47.00.B0010
        Bootloader: BL  02.00.B0001
          Hardware: 72
     The power switch is located on the base.
     Supports 23 HID++ 2.0 features:
         0: ROOT                   {0000}   
         1: FEATURE SET            {0001}   
         2: DEVICE FW VERSION      {0003}   
            Firmware: Firmware RQM 47.00.B0010 402C
            Firmware: Bootloader BL  02.00.B0001 402C
            Firmware: Hardware  72 
            Unit ID: 00000000  Model ID: 000000000000  Transport IDs: {}
         3: DEVICE NAME            {0005}   
            Name: Wireless Gaming Mouse
            Kind: mouse
         4: DFUCONTROL LEGACY      {00C0}   
         5: BATTERY STATUS         {1000}   
            Battery: 50%, discharging, next level 20%.
         6: WIRELESS DEVICE STATUS {1D4B}   
         7: unknown:1DF3           {1DF3}   internal, hidden
         8: unknown:1E00           {1E00}   hidden
         9: unknown:1E80           {1E80}   internal, hidden
        10: unknown:1F03           {1F03}   internal, hidden
        11: VERTICAL SCROLLING     {2100}   
            Roller type: standard
            Ratchet per turn: 24
            Scroll lines: 0
        12: MOUSE POINTER          {2200}   
            DPI: 1000
            Acceleration: low
            Override OS ballistics
            No vertical tuning, standard mice
        13: ADJUSTABLE DPI         {2201}   
        14: PER KEY LIGHTING       {8080}   
        15: REPORT RATE            {8060}   
            Polling Rate (ms): 2
            Polling Rate (ms) (saved): 2
            Polling Rate (ms)        : 2
        16: COLOR LED EFFECTS      {8070}   
        17: unknown:1810           {1810}   internal, hidden
        18: unknown:1830           {1830}   internal, hidden
        19: unknown:1850           {1850}   internal, hidden
        20: unknown:1860           {1860}   internal, hidden
        21: unknown:1890           {1890}   internal, hidden
        22: unknown:18A0           {18A0}   internal, hidden
     Battery: 50%, discharging, next level 20%.
```
</details>

**Describe the bug**
This is a repost of an automatically reported bug from Fedora. I will update when reporter provides details.

**To Reproduce**
Steps to reproduce the behavior:
1. N/A, will update when reporter provides details

**Additional context**
Downstream bug report: https://bugzilla.redhat.com/show_bug.cgi?id=2015360 .


Discussion:
Hmm.   I'm not sure how the assertion failed but it is possible to rewrite the code to be more robust.
Apologies, I forgot to include the stack trace from the downstream report:
```
Truncated backtrace:
tray.py:375:_add_device:AssertionError

Traceback (most recent call last):
  File "/usr/lib/python3.9/site-packages/solaar/ui/__init__.py", line 177, in _status_changed
    tray.update(device)
  File "/usr/lib/python3.9/site-packages/solaar/ui/tray.py", line 535, in update
    index = _add_device(device)
  File "/usr/lib/python3.9/site-packages/solaar/ui/tray.py", line 375, in _add_device
    assert path == receiver_path
AssertionError

Local variables in innermost frame:
device: <Device(1,4065,G613 Wireless Mechanical Gaming Keyboard,A4D7BD66)>
receiver_path: '/dev/hidraw4'
index: 1
idx: 0
path: '/dev/hidraw7'
_ignore: None
number: None
```

Resolving patch:
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
@@ -351,28 +351,27 @@ 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
-    # not true for wired devices - assert device.receiver
-    receiver_path = device.receiver.path if device.receiver is not None else device.path
-    # not true for wired devices - assert receiver_path
-
-    index = None
-    for idx, (path, _ignore, _ignore, _ignore) in enumerate(_devices_info):
-        if path == receiver_path:
-            # the first entry matching the receiver serial should be for the receiver itself
-            index = idx + 1
-            break
 
-    if index is None:
-        index = len(_devices_info)
-    elif device.receiver:
-        # proper ordering (according to device.number) for a receiver's devices
-        while True:
+    index = 0
+    receiver_path = device.receiver.path if device.receiver is not None else device.path
+    if device.receiver is not None:  # if receiver insert into devices for the receiver in device number order
+        for idx, (path, _ignore, _ignore, _ignore) in enumerate(_devices_info):
+            if path and path == receiver_path:
+                index = idx + 1  # the first entry matching the receiver serial should be for the receiver itself
+                break
+        while index < len(_devices_info):
             path, number, _ignore, _ignore = _devices_info[index]
             if path == _RECEIVER_SEPARATOR[0]:
                 break
-            assert path == receiver_path
             assert number != device.number
             if number > device.number:
                 break
@@ -382,9 +381,7 @@ def _add_device(device):
     assert len(new_device_info) == len(_RECEIVER_SEPARATOR)
     _devices_info.insert(index, new_device_info)
 
-    # label_prefix = b'\xE2\x94\x84 '.decode('utf-8')
     label_prefix = '   '
-
     new_menu_item = Gtk.ImageMenuItem.new_with_label((label_prefix if device.number else '') + device.name)
     new_menu_item.set_image(Gtk.Image())
     new_menu_item.show_all()
@@ -408,23 +405,20 @@ def _remove_device(index):
 
 
 def _add_receiver(receiver):
-    index = len(_devices_info)
+    index = 0
 
     new_receiver_info = (receiver.path, None, receiver.name, None)
     assert len(new_receiver_info) == len(_RECEIVER_SEPARATOR)
     _devices_info.append(new_receiver_info)
 
     new_menu_item = Gtk.ImageMenuItem.new_with_label(receiver.name)
-    _menu.insert(new_menu_item, index)
     icon_set = _icons.device_icon_set(receiver.name)
     new_menu_item.set_image(Gtk.Image().new_from_icon_set(icon_set, _MENU_ICON_SIZE))
     new_menu_item.show_all()
     new_menu_item.connect('activate', _window_popup, receiver.path)
+    _menu.insert(new_menu_item, index)
 
-    _devices_info.append(_RECEIVER_SEPARATOR)
-    separator = Gtk.SeparatorMenuItem.new()
-    separator.set_visible(True)
-    _menu.insert(separator, index + 1)
+    _add_separator(index)
 
     return 0
 


Test patch: