From d4c8121539f5b80e1fc667ee5aa4b1e2e7ee39ff Mon Sep 17 00:00:00 2001
From: "Peter F. Patel-Schneider" <pfpschneider@gmail.com>
Date: Tue, 30 Jan 2024 18:47:10 -0500
Subject: [PATCH] device: limited support for extended adjustable dpi

---
 lib/logitech_receiver/settings_templates.py | 41 ++++++++++++++++-----
 1 file changed, 31 insertions(+), 10 deletions(-)

diff --git a/lib/logitech_receiver/settings_templates.py b/lib/logitech_receiver/settings_templates.py
index fdbf50e632..947e5afc80 100644
--- a/lib/logitech_receiver/settings_templates.py
+++ b/lib/logitech_receiver/settings_templates.py
@@ -757,7 +757,7 @@ def build(cls, setting_class, device):
 
 class DpiSlidingXY(_RawXYProcessing):
     def activate_action(self):
-        self.dpiSetting = next(filter(lambda s: s.name == "dpi", self.device.settings), None)
+        self.dpiSetting = next(filter(lambda s: s.name == "dpi" or s.name == "dpi_extended", self.device.settings), None)
         self.dpiChoices = list(self.dpiSetting.choices)
         self.otherDpiIdx = self.device.persister.get("_dpi-sliding", -1) if self.device.persister else -1
         if not isinstance(self.otherDpiIdx, int) or self.otherDpiIdx < 0 or self.otherDpiIdx >= len(self.dpiChoices):
@@ -817,7 +817,7 @@ def move_action(self, dx, dy):
 
 class MouseGesturesXY(_RawXYProcessing):
     def activate_action(self):
-        self.dpiSetting = next(filter(lambda s: s.name == "dpi", self.device.settings), None)
+        self.dpiSetting = next(filter(lambda s: s.name == "dpi" or s.name == "dpi_extended", self.device.settings), None)
         self.fsmState = "idle"
         self.initialize_data()
 
@@ -960,26 +960,35 @@ class AdjustableDpi(_Setting):
     feature = _F.ADJUSTABLE_DPI
     rw_options = {"read_fnid": 0x20, "write_fnid": 0x30}
     choices_universe = _NamedInts.range(200, 4000, str, 50)
+    sensor_list_bytes_ignore = 1
 
     class validator_class(_ChoicesV):
         @classmethod
         def build(cls, setting_class, device):
-            # [1] getSensorDpiList(sensorIdx)
-            reply = device.feature_request(_F.ADJUSTABLE_DPI, 0x10)
+            # [1] getSensorDpiList(sensorIdx) - works for both features
+            reply = device.feature_request(_F.ADJUSTABLE_DPI, 0x10, 0x00, 0x00)
             assert reply, "Oops, DPI list cannot be retrieved!"
+            dpi_bytes = reply[setting_class.sensor_list_bytes_ignore :]
+            i = 1
+            while _bytes2int(dpi_bytes[-2:]) != 0:
+                reply = device.feature_request(_F.ADJUSTABLE_DPI, 0x10, 0x00, i)
+                assert reply, "Oops, DPI list cannot be retrieved!"
+                dpi_bytes += reply[setting_class.sensor_list_bytes_ignore :]
             dpi_list = []
-            step = None
-            for val in _unpack("!7H", reply[1 : 1 + 14]):
+            i = 0
+            while i < len(dpi_bytes):
+                val = _bytes2int(dpi_bytes[i : i + 2])
                 if val == 0:
                     break
                 if val >> 13 == 0b111:
-                    assert step is None and len(dpi_list) == 1, "Invalid DPI list item: %r" % val
                     step = val & 0x1FFF
+                    last = _bytes2int(dpi_bytes[i + 2 : i + 4])
+                    assert len(dpi_list) > 0 and last > dpi_list[-1], "Invalid DPI list item: %r" % val
+                    dpi_list += range(dpi_list[-1] + step, last + 1, step)
+                    i += 4
                 else:
                     dpi_list.append(val)
-            if step:
-                assert len(dpi_list) == 2, "Invalid DPI list range: %r" % dpi_list
-                dpi_list = range(dpi_list[0], dpi_list[1] + 1, step)
+                    i += 2
             return cls(choices=_NamedInts.list(dpi_list), byte_count=3) if dpi_list else None
 
         def validate_read(self, reply_bytes):  # special validator to use default DPI if needed
@@ -991,6 +1000,17 @@ def validate_read(self, reply_bytes):  # special validator to use default DPI if
             return valid_value
 
 
+class ExtendedAdjustableDpi(AdjustableDpi):
+    # the extended version allows for two dimensions, longer dpi descriptions
+    # still assume only one sensor (and X only?)
+    # [5] getSensorDpiParameters(sensorIdx) → sensorIdx, dpiX, defaultDpiX, dpiY, defaultDpiY, lod
+    # [6] setSensorDpiParameters(sensorIdx, dpiX, dpiY, lod) → sensorIdx, dpiX, dpiY, lod
+    name = "dpi_extended"
+    feature = _F.EXTENDED_ADJUSTABLE_DPI
+    rw_options = {"read_fnid": 0x50, "write_fnid": 0x60}
+    sensor_list_bytes_ignore = 3
+
+
 class SpeedChange(_Setting):
     """Implements the ability to switch Sensitivity by clicking on the DPI_Change button."""
 
@@ -1522,6 +1542,7 @@ def build(cls, device):
     ExtendedReportRate,
     PointerSpeed,  # simple
     AdjustableDpi,  # working
+    ExtendedAdjustableDpi,
     SpeedChange,
     #    Backlight,  # not working - disabled temporarily
     Backlight2,  # working