diff --git a/lib/logitech_receiver/device.py b/lib/logitech_receiver/device.py index 3fb7092b..f2f88c9e 100644 --- a/lib/logitech_receiver/device.py +++ b/lib/logitech_receiver/device.py @@ -766,6 +766,13 @@ class Device: if no_reply: return None + # The device echoes our exact sub-device function+swid byte in + # MessageEvent responses. Match on that to reject cross-contamination + # from late-arriving responses to other function calls on the same + # feature (e.g. GetRGBZoneInfo response showing up on a later + # GetHostModeState read). + expected_sub_func_sw = (sub_function & 0xF0) | sw_id + # Read ACK + MessageEvent response request_started = time.time() ack_received = False @@ -782,7 +789,7 @@ class Device: break if (func_sw >> 4) == 0x01 and (func_sw & 0x0F) == 0: # MessageEvent arrived before ACK — validate it's for our request - if self._is_bridge_response_for(reply_data, sub_feat_idx): + if self._is_bridge_response_for(reply_data, sub_feat_idx, expected_sub_func_sw): if logger.isEnabledFor(logging.DEBUG): logger.debug("bridge idx=%d fn=0x%02X -> OK", sub_feat_idx, sub_function) return self._parse_bridge_response(reply_data) @@ -800,7 +807,7 @@ class Device: if len(reply_data) >= 2 and reply_data[0] == bridge_idx: func_sw = reply_data[1] if (func_sw >> 4) == 0x01 and (func_sw & 0x0F) == 0: - if self._is_bridge_response_for(reply_data, sub_feat_idx): + if self._is_bridge_response_for(reply_data, sub_feat_idx, expected_sub_func_sw): if logger.isEnabledFor(logging.DEBUG): logger.debug("bridge idx=%d fn=0x%02X -> OK", sub_feat_idx, sub_function) return self._parse_bridge_response(reply_data) @@ -824,12 +831,19 @@ class Device: return False @staticmethod - def _is_bridge_response_for(reply_data, expected_sub_feat_idx): + def _is_bridge_response_for(reply_data, expected_sub_feat_idx, expected_sub_func_sw=None): """Check if a bridge MessageEvent is a response for our specific sub-feature request. Accepts both normal responses (sub_feat_idx matches) and error responses (sub_feat_idx=0xFF with original feat_idx in next byte). Unsolicited notifications (sub_cpl=0xFF) are rejected. + + If `expected_sub_func_sw` is provided, also matches on the echoed + sub-device function byte (`(function << 4) | sw_id`). This prevents + cross-talk between different function calls on the SAME feature, which + can happen when a late-arriving response for one function gets picked + up by a later request on the same feature (observed on G522 where a + GetRGBZoneInfo response contaminated a subsequent GetHostModeState). """ if len(reply_data) < 6: return False @@ -839,9 +853,15 @@ class Device: if sub_cpl != 0x00: return False if sub_feat_idx == expected_sub_feat_idx: + if expected_sub_func_sw is not None and len(reply_data) >= 7: + if reply_data[6] != expected_sub_func_sw: + return False return True # Error response: sub_feat_idx=0xFF, next byte is the original feat_idx that errored if sub_feat_idx == 0xFF and len(reply_data) >= 7 and reply_data[6] == expected_sub_feat_idx: + if expected_sub_func_sw is not None and len(reply_data) >= 8: + if reply_data[7] != expected_sub_func_sw: + return False return True return False diff --git a/lib/logitech_receiver/settings_templates.py b/lib/logitech_receiver/settings_templates.py index 709478c4..b9fc1da0 100644 --- a/lib/logitech_receiver/settings_templates.py +++ b/lib/logitech_receiver/settings_templates.py @@ -2077,9 +2077,9 @@ class HeadsetRGBColor(settings.Setting): # Tight format observed on G522: [count, zone_ids...] with no reserved gap. # The protocol doc shows 3-byte + 1-byte reserved gaps before zone IDs, but # the G522 sub-device packs them immediately after the count. + # Don't filter zone_id==0 — on some devices 0 is a valid zone ID. We trust + # the device to report sane zone IDs and let the device reject nonsense. tight = list(resp[1 : 1 + zone_count]) if 1 <= zone_count <= len(resp) - 1 else [] - # Filter out zero bytes that are just padding (zone IDs should be non-zero). - tight = [z for z in tight if z != 0] if tight and len(tight) == zone_count: logger.info( "HeadsetRGBColor: discovered %d zone(s) %s (tight format, raw resp=%s)", @@ -2091,7 +2091,6 @@ class HeadsetRGBColor(settings.Setting): return tight # Try the doc's format (with reserved gap) as fallback gap = list(resp[5 : 5 + zone_count]) if len(resp) >= 5 + zone_count else [] - gap = [z for z in gap if z != 0] if gap and len(gap) == zone_count: logger.info( "HeadsetRGBColor: discovered %d zone(s) %s (doc format, raw resp=%s)",