diff --git a/src/hhd/controller/const.py b/src/hhd/controller/const.py index 5343d9a5..7c5672d8 100644 --- a/src/hhd/controller/const.py +++ b/src/hhd/controller/const.py @@ -301,4 +301,6 @@ "is_attached_right", # Commands "steam", + # IMU + "imu_powersave", ] diff --git a/src/hhd/controller/physical/imu.py b/src/hhd/controller/physical/imu.py index 4f0998c9..f1fa91a1 100644 --- a/src/hhd/controller/physical/imu.py +++ b/src/hhd/controller/physical/imu.py @@ -28,6 +28,8 @@ class DeviceInfo(NamedTuple): dev: str axis: Sequence[ScanElement] sysfs: str + freqs: dict[str, float] + min_freqs: dict[str, float | None] ACCEL_NAMES = ["accel_3d"] @@ -113,34 +115,36 @@ def prepare_dev( write_sysfs(sensor_dir, "buffer/enable", 0) # Set sampling frequency + target_freqs = {} + min_freqs = {} if freq is not None: for a, f in zip(attr, freq): sfn = os.path.join(sensor_dir, f"in_{a}_sampling_frequency") if os.path.isfile(sfn): try: - write_sysfs(sensor_dir, f"in_{a}_sampling_frequency", f) - except Exception as e: - logger.error(f"Could not set sampling frequency for {a}:\n{e}") - try: - # Select closest higher frequency instead - sfn = os.path.join( - sensor_dir, f"in_{a}_sampling_frequency_available" - ) - if os.path.isfile(sfn): - freqs = map( + # Select closest higher frequency instead + sfn = os.path.join( + sensor_dir, f"in_{a}_sampling_frequency_available" + ) + if os.path.isfile(sfn): + freqs = list( + map( float, read_sysfs( sensor_dir, f"in_{a}_sampling_frequency_available" ).split(), ) - f = next((x for x in freqs if x >= f), None) - if f: - write_sysfs(sensor_dir, f"in_{a}_sampling_frequency", f) - logger.info( - f"Selected higher sampling frequency {f} for {a}" - ) - except Exception as e: - logger.error(f"Could not set higher sampling frequency for {a}:\n{e}") + ) + f = next((x for x in freqs if x >= f), f) + min_freqs[a] = min(freqs) if freqs else None + else: + min_freqs[a] = None + + write_sysfs(sensor_dir, f"in_{a}_sampling_frequency", f) + target_freqs[a] = f + logger.info(f"Selected higher frequency {f} for {a}") + except Exception as e: + logger.error(f"Could not set sampling frequency for {a}:\n{e}") # Set scale if scales is not None: @@ -231,7 +235,57 @@ def prepare_dev( write_sysfs(sensor_dir, "buffer/enable", 1) axis_arr = tuple(axis[i] for i in sorted(axis)) - return DeviceInfo(dev, axis_arr, sensor_dir) + return DeviceInfo(dev, axis_arr, sensor_dir, target_freqs, min_freqs) + + +def set_powersave(dev: DeviceInfo, state: bool, update_trigger: bool): + freqs = dev.min_freqs if state else dev.freqs + + for a, f in freqs.items(): + if ( + a + and f + and os.path.isfile(os.path.join(dev.sysfs, f"in_{a}_sampling_frequency")) + ): + try: + write_sysfs(dev.sysfs, f"in_{a}_sampling_frequency", f) + logger.info( + f"Set {'powersave' if state else 'normal'} frequency {f} for {a}" + ) + except Exception as e: + logger.error( + f"Could not set {'powersave' if state else 'normal'} frequency for {a}:\n{e}" + ) + + if not update_trigger: + return + + # Find trigger + trig = None + for fn in os.listdir("/sys/bus/iio/devices"): + if not fn.startswith("trigger"): + continue + with open(os.path.join("/sys/bus/iio/devices", fn, "name"), "r") as f: + if f.read().strip() == "hhd": + trig = fn + break + + if not trig: + return + + freq = next(iter([f for f in freqs if f]), None) + if not freq: + return + + try: + write_sysfs(os.path.dirname(trig), "sampling_frequency", freq) + logger.info( + f"Set {'powersave' if state else 'normal'} trigger frequency {freq}" + ) + except Exception as e: + logger.error( + f"Could not set {'powersave' if state else 'normal'} trigger frequency:\n{e}" + ) def close_dev(dev: DeviceInfo): @@ -310,6 +364,18 @@ def close(self, exit: bool): self.fd = -1 return True + def consume(self, events: Sequence[Event]) -> None: + if not self.dev: + return + + powersave = None + for e in events: + if e["type"] == "configuration" and e["code"] == "imu_powersave": + powersave = bool(e["value"]) + + if powersave is not None: + set_powersave(self.dev, powersave, self.update_trigger) + def produce(self, fds: Sequence[int]) -> Sequence[Event]: if self.fd not in fds or not self.dev: return [] diff --git a/src/hhd/controller/virtual/sd/__init__.py b/src/hhd/controller/virtual/sd/__init__.py index 9d6df129..f27ccf45 100644 --- a/src/hhd/controller/virtual/sd/__init__.py +++ b/src/hhd/controller/virtual/sd/__init__.py @@ -54,6 +54,7 @@ def __init__( name, touchpad: bool = False, sync_gyro: bool = True, + gyro_detection: bool = True, ) -> None: self.available = False self.dev = None @@ -65,6 +66,9 @@ def __init__( self.report = bytearray(64) self.i = 0 self.last_rep = None + self.gyro_enabled = False + self.gyro_detection = gyro_detection + self.send_powersave = True def open(self) -> Sequence[int]: self.available = False @@ -87,6 +91,9 @@ def open(self) -> Sequence[int]: self.dev = cached.dev if self.dev and self.dev.fd: self.fd = self.dev.fd + self.gyro_enabled = cached.gyro_enabled + if not self.gyro_enabled: + self.send_powersave = True else: logger.warning(f"Throwing away cached Steamdeck Controller.") cached.close(True, in_cache=True) @@ -136,7 +143,6 @@ def produce(self, fds: Sequence[int]) -> Sequence[Event]: # Process queued events out: Sequence[Event] = [] - assert self.dev while ev := self.dev.read_event(): match ev["type"]: case "open": @@ -241,26 +247,39 @@ def produce(self, fds: Sequence[int]) -> Sequence[Event]: "weak_magnitude": right / (2**16 - 1), } ) - case 0xea: + case 0xEA: # Touchpad stuff pass case 0x8F: # logger.info(f"SD Received Haptics ({time.perf_counter()*1000:.3f}ms):\n{ev['data'].hex().rstrip(' 0')}") pass case 0x87: + rnum = ev["data"][4] + ss = [] + for i in range(0, rnum, 3): + rtype = ev["data"][5 + i] + rdata = int.from_bytes( + ev["data"][6 + i : 8 + i], + byteorder="little", + signed=False, + ) + ss.append( + f"{SD_SETTINGS[rtype] if rtype < len(SD_SETTINGS) else "UKNOWN"} ({rtype:02d}): {rdata:02x}" + ) + + if rtype == 80 and self.gyro_detection: + should_enable = rdata == 0xAA00 + if should_enable != self.gyro_enabled: + self.gyro_enabled = should_enable + out.append( + { + "type": "configuration", + "code": "imu_powersave", + "value": not should_enable, + } + ) + if DEBUG_MODE: - rnum = ev["data"][4] - ss = [] - for i in range(0, rnum, 3): - rtype = ev["data"][5 + i] - rdata = int.from_bytes( - ev["data"][6 + i : 8 + i], - byteorder="little", - signed=False, - ) - ss.append( - f"{SD_SETTINGS[rtype] if rtype < len(SD_SETTINGS) else "UKNOWN"} ({rtype:02d}): {rdata:02x}" - ) mlen = max(map(len, ss)) logger.info( f"SD Received Settings (n={rnum // 3}):{''.join(map(lambda x: '\n > ' + ' '*(mlen - len(x)) + x, ss))}" @@ -278,6 +297,16 @@ def produce(self, fds: Sequence[int]) -> Sequence[Event]: case _: logger.warning(f"SD UKN_EVENT: {ev}") + if self.send_powersave: + self.send_powersave = False + out.append( + { + "type": "configuration", + "code": "imu_powersave", + "value": True, + } + ) + return out def consume(self, events: Sequence[Event]): @@ -288,7 +317,7 @@ def consume(self, events: Sequence[Event]): # To fix gyro to mouse in latest steam # only send updates when gyro sends a timestamp - send = not self.sync_gyro + send = not self.sync_gyro or not self.gyro_enabled curr = time.perf_counter() new_rep = bytearray(self.report) @@ -355,7 +384,7 @@ def consume(self, events: Sequence[Event]): # If the IMU breaks, smoothly re-enable the controller failover = self.last_imu + MAX_IMU_SYNC_DELAY < curr - if self.sync_gyro and failover and not self.imu_failed: + if self.gyro_enabled and self.sync_gyro and failover and not self.imu_failed: self.imu_failed = True logger.error( f"IMU Did not send information for {MAX_IMU_SYNC_DELAY}s. Disabling Gyro Sync." diff --git a/src/hhd/device/ayaneo/base.py b/src/hhd/device/ayaneo/base.py index 68d389ac..3e1fdcf3 100644 --- a/src/hhd/device/ayaneo/base.py +++ b/src/hhd/device/ayaneo/base.py @@ -624,6 +624,8 @@ def prepare(m): d_vend.consume(evs) for d in d_outs: d.consume(evs) + if d_imu: + d_imu.consume(evs) t = time.perf_counter() elapsed = t - start diff --git a/src/hhd/device/generic/base.py b/src/hhd/device/generic/base.py index a8fbf398..c962d15c 100644 --- a/src/hhd/device/generic/base.py +++ b/src/hhd/device/generic/base.py @@ -276,6 +276,8 @@ def prepare(m): d_rgb.consume(evs) for d in d_outs: d.consume(evs) + if d_imu: + d_imu.consume(evs) t = time.perf_counter() elapsed = t - start diff --git a/src/hhd/device/gpd/win/base.py b/src/hhd/device/gpd/win/base.py index 035426b2..969b3e35 100644 --- a/src/hhd/device/gpd/win/base.py +++ b/src/hhd/device/gpd/win/base.py @@ -391,6 +391,8 @@ def prepare(m): for d in d_outs: d.consume(evs) + if d_imu: + d_imu.consume(evs) # If unbounded, the total number of events per second is the sum of all # events generated by the producers. diff --git a/src/hhd/device/orange_pi/base.py b/src/hhd/device/orange_pi/base.py index 0bb15752..8a64bd23 100644 --- a/src/hhd/device/orange_pi/base.py +++ b/src/hhd/device/orange_pi/base.py @@ -221,6 +221,8 @@ def prepare(m): d_rgb.consume(evs) for d in d_outs: d.consume(evs) + if d_imu: + d_imu.consume(evs) # If unbounded, the total number of events per second is the sum of all # events generated by the producers. diff --git a/src/hhd/device/oxp/base.py b/src/hhd/device/oxp/base.py index 37aa41d7..20c68c43 100644 --- a/src/hhd/device/oxp/base.py +++ b/src/hhd/device/oxp/base.py @@ -693,6 +693,8 @@ def prepare(m): d.consume(evs) for d in d_outs: d.consume(evs) + if d_imu: + d_imu.consume(evs) t = time.perf_counter() elapsed = t - start diff --git a/src/hhd/device/rog_ally/base.py b/src/hhd/device/rog_ally/base.py index 90e7b2e0..803202cd 100644 --- a/src/hhd/device/rog_ally/base.py +++ b/src/hhd/device/rog_ally/base.py @@ -469,6 +469,8 @@ def prepare(m): for d in d_outs: d.consume(evs) + if d_imu: + d_imu.consume(evs) if d_vend.mouse_mode and d_kbd_grabbed and d_kbd_1.dev: try: