Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/hhd/controller/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -301,4 +301,6 @@
"is_attached_right",
# Commands
"steam",
# IMU
"imu_powersave",
]
104 changes: 85 additions & 19 deletions src/hhd/controller/physical/imu.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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):
Expand Down Expand Up @@ -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 []
Expand Down
61 changes: 45 additions & 16 deletions src/hhd/controller/virtual/sd/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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)
Expand Down Expand Up @@ -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":
Expand Down Expand Up @@ -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))}"
Expand All @@ -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]):
Expand All @@ -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)
Expand Down Expand Up @@ -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."
Expand Down
2 changes: 2 additions & 0 deletions src/hhd/device/ayaneo/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions src/hhd/device/generic/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions src/hhd/device/gpd/win/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
2 changes: 2 additions & 0 deletions src/hhd/device/orange_pi/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
2 changes: 2 additions & 0 deletions src/hhd/device/oxp/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions src/hhd/device/rog_ally/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down