Skip to content

Commit

Permalink
update custom fan curve implementation
Browse files Browse the repository at this point in the history
  • Loading branch information
aarron-lee committed Mar 12, 2024
1 parent c918b9a commit 355f4b3
Show file tree
Hide file tree
Showing 3 changed files with 215 additions and 134 deletions.
6 changes: 4 additions & 2 deletions main.py
Original file line number Diff line number Diff line change
Expand Up @@ -111,11 +111,13 @@ async def save_fan_settings(self, fanInfo, currentGameId):
if not enable_full_fan_speed:
legion_space.set_full_fan_speed(False)
sleep(0.5)
legion_space.set_fan_curve(active_fan_curve)
legion_space.set_active_fan_curve(active_fan_curve)
else:
legion_space.set_full_fan_speed(True)
elif not customFanCurvesEnabled and settings.supports_custom_fan_curves():
legion_space.set_default_fan_curve()
legion_space.set_tdp_mode("performance")
sleep(0.5)
legion_space.set_tdp_mode("custom")

return True
except Exception as e:
Expand Down
337 changes: 208 additions & 129 deletions py_modules/legion_space.py
Original file line number Diff line number Diff line change
@@ -1,139 +1,218 @@
import subprocess
# import subprocess
import decky_plugin
from time import sleep
from typing import NamedTuple, Sequence

# all credit goes to corando98
# source: https://github.com/corando98/LLG_Dev_scripts/blob/main/LegionSpace.py

def execute_acpi_command(command):
"""
Executes an ACPI command and returns the output.
Uses subprocess for robust command execution.
# def execute_acpi_command(command):
# """
# Executes an ACPI command and returns the output.
# Uses subprocess for robust command execution.

# Args:
# command (str): The ACPI command to be executed.

# Returns:
# str: The output from the ACPI command execution.
# """
# try:
# result = subprocess.run(command, shell=True, check=True, text=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
# return result.stdout.strip()
# except subprocess.CalledProcessError as e:
# decky_plugin.logger.error(f"Error executing command: {e.stderr}")
# return None

# def set_default_fan_curve():
# """
# # Fan ID, Sensor ID, ignored
# 0x00, 0x00,
# # Temperature array length (10; ignored; suspected use)
# 0x0A, 0x00, 0x00, 0x00,
# # Speeds in uint16, except last that is a byte.
# 0x2c, 0x00, # FSS0 44
# 0x30, 0x00, # FSS1 48
# 0x37, 0x00, # FSS2 55
# 0x3c, 0x00, # FSS3 60
# 0x47, 0x00, # FSS4 71
# 0x4f, 0x00, # FSS5 79
# 0x57, 0x00, # FSS6 87
# 0x57, 0x00, # FSS7 87
# 0x64, 0x00, # FSS8 100
# 0x64, 0x00, # FSS9 100
# 0x00, # Null termination (?)
# """
# command = "echo '\_SB.GZFD.WMAB 0 0x06 {0x00, 0x00, 0x0A, 0x00, 0x00, 0x00, 0x2c, 0x00, 0x30, 0x00, 0x37, 0x00, 0x3c, 0x00, 0x47, 0x00, 0x4f, 0x00, 0x57, 0x00, 0x57, 0x00, 0x64, 0x00, 0x64, 0x00, 0x00, 0x0a, 0x00, 0x00, 0x00, 0x0a, 0x00, 0x14, 0x00, 0x1e, 0x00, 0x28, 0x00, 0x32, 0x00, 0x3c, 0x00, 0x46, 0x00, 0x50, 0x00, 0x5a, 0x00, 0x64, 0x00, 0x00}' | tee /proc/acpi/call; cat /proc/acpi/call"
# return execute_acpi_command(command)

# source: hhd-adjustor
# https://github.com/hhd-dev/adjustor/blob/072411bff14bb5996b0fe00da06f36d17f31a389/src/adjustor/core/lenovo.py#L13

def set_full_fan_speed(enable: bool):
decky_plugin.logger.info(f"Setting full fan mode to {enable}.")
return set_feature(0x04020000, int(enable))

def set_active_fan_curve(arr: Sequence[int]):
current_mode = get_tdp_mode()

if current_mode != "custom":
# set custom
set_tdp_mode("custom")

sleep(0.3)

set_fan_curve(arr)

def set_fan_curve(arr: Sequence[int]):
decky_plugin.logger.info(f"Setting fan curve to:\n{arr}")
if len(arr) != 10:
decky_plugin.logger.error(f"Invalid fan curve length: {len(arr)}. Should be 10.")
return False
if any(not isinstance(d, int) for d in arr):
decky_plugin.logger.error(f"Curve has null value, not setting.")
return False

return call(
r"\_SB.GZFD.WMAB",
[
0,
0x06,
bytes(
[
0x00,
0x00,
0x0A,
0x00,
0x00,
0x00,
arr[0],
0x00,
arr[1],
0x00,
arr[2],
0x00,
arr[3],
0x00,
arr[4],
0x00,
arr[5],
0x00,
arr[6],
0x00,
arr[7],
0x00,
arr[8],
0x00,
arr[9],
0x00,
0x00,
0x0A,
0x00,
0x00,
0x00,
0x0A,
0x00,
0x14,
0x00,
0x1E,
0x00,
0x28,
0x00,
0x32,
0x00,
0x3C,
0x00,
0x46,
0x00,
0x50,
0x00,
0x5A,
0x00,
0x64,
0x00,
0x00,
]
),
],
)

def set_tdp_mode(mode):
decky_plugin.logger.info(f"Setting tdp mode to '{mode}'.")
match mode:
case "quiet":
b = 0x01
case "balanced":
b = 0x02
case "performance":
b = 0x03
case "custom":
b = 0xFF
case _:
decky_plugin.logger.error(f"TDP mode '{mode}' is unknown. Not setting.")
return False

return call(r"\_SB.GZFD.WMAA", [0, 0x2C, b])

def get_tdp_mode():
decky_plugin.logger.debug(f"Retrieving TDP Mode.")
if not call(r"\_SB.GZFD.WMAA", [0, 0x2D, 0], risky=False):
decky_plugin.logger.error(f"Failed retrieving TDP Mode.")
return None

Args:
command (str): The ACPI command to be executed.
match read():
case 0x01:
return "quiet"
case 0x02:
return "balanced"
case 0x03:
return "performance"
case 0xFF:
return "custom"
case v:
decky_plugin.logger.error(f"TDP mode '{v}' is unknown")
return None


def call(method: str, args: Sequence[bytes | int], risky: bool = True):
cmd = method
for arg in args:
if isinstance(arg, int):
cmd += f" 0x{arg:02x}"
else:
cmd += f" b{arg.hex()}"

log = decky_plugin.logger.info if risky else decky_plugin.logger.debug
log(f"Executing ACPI call:\n'{cmd}'")

Returns:
str: The output from the ACPI command execution.
"""
try:
result = subprocess.run(command, shell=True, check=True, text=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
return result.stdout.strip()
except subprocess.CalledProcessError as e:
decky_plugin.logger.error(f"Error executing command: {e.stderr}")
with open("/proc/acpi/call", "wb") as f:
f.write(cmd.encode())
return True
except Exception as e:
decky_plugin.logger.error(f"ACPI Call failed with error:\n{e}")
return False

def read():
with open("/proc/acpi/call", "rb") as f:
d = f.read().decode().strip()

if d == "not called\0":
return None

def set_fan_curve(fan_table):
"""
Sets a new fan curve based on the provided fan table array.
The fan table should contain fan speed values that correspond to different temperature thresholds.
Args:
fan_table (list): An array of fan speeds to set the fan curve.
Returns:
str: The output from setting the new fan curve.
"""
# set custom TDP mode
set_smart_fan_mode(0xff)
sleep(0.2)

# Assuming Fan ID and Sensor ID are both 0 (as they are ignored)
fan_id_sensor_id = '0x00, 0x00'

# Assuming the temperature array length and values are ignored but required
temp_array_length = '0x0A, 0x00, 0x00, 0x00' # Length 10 in hex
temp_values = ', '.join([f'0x{temp:02x}, 0x00' for temp in range(10, 101, 10)]) + ', 0x00'

# Fan speed values in uint16 format with null termination
fan_speed_values = ', '.join([f'0x{speed:02x}, 0x00' for speed in fan_table]) + ', 0x00'

# Constructing the full command
command = f"echo '\\_SB.GZFD.WMAB 0 0x06 {{{fan_id_sensor_id}, {temp_array_length}, {fan_speed_values}, {temp_array_length}, {temp_values}}}' | tee /proc/acpi/call; cat /proc/acpi/call"
return execute_acpi_command(command)

def set_default_fan_curve():
"""
# Fan ID, Sensor ID, ignored
0x00, 0x00,
# Temperature array length (10; ignored; suspected use)
0x0A, 0x00, 0x00, 0x00,
# Speeds in uint16, except last that is a byte.
0x2c, 0x00, # FSS0 44
0x30, 0x00, # FSS1 48
0x37, 0x00, # FSS2 55
0x3c, 0x00, # FSS3 60
0x47, 0x00, # FSS4 71
0x4f, 0x00, # FSS5 79
0x57, 0x00, # FSS6 87
0x57, 0x00, # FSS7 87
0x64, 0x00, # FSS8 100
0x64, 0x00, # FSS9 100
0x00, # Null termination (?)
"""
command = "echo '\_SB.GZFD.WMAB 0 0x06 {0x00, 0x00, 0x0A, 0x00, 0x00, 0x00, 0x2c, 0x00, 0x30, 0x00, 0x37, 0x00, 0x3c, 0x00, 0x47, 0x00, 0x4f, 0x00, 0x57, 0x00, 0x57, 0x00, 0x64, 0x00, 0x64, 0x00, 0x00, 0x0a, 0x00, 0x00, 0x00, 0x0a, 0x00, 0x14, 0x00, 0x1e, 0x00, 0x28, 0x00, 0x32, 0x00, 0x3c, 0x00, 0x46, 0x00, 0x50, 0x00, 0x5a, 0x00, 0x64, 0x00, 0x00}' | tee /proc/acpi/call; cat /proc/acpi/call"
return execute_acpi_command(command)

# FFSS Full speed mode set on /off
# echo '\_SB.GZFD.WMAE 0 0x12 0x0104020000' | sudo tee /proc/acpi/call; sudo cat /proc/acpi/call
# echo '\_SB.GZFD.WMAE 0 0x12 0x0004020000' | sudo tee /proc/acpi/call; sudo cat /proc/acpi/call
def set_full_fan_speed(enable):
"""
Enable or disable full fan speed mode.
Args:
enable (bool): True to enable, False to disable.
Returns:
str: The result of the operation.
"""
status = '0x01' if enable else '0x00'
command = f"echo '\\_SB.GZFD.WMAE 0 0x12 {status}04020000' | tee /proc/acpi/call; cat /proc/acpi/call"
return execute_acpi_command(command)

def set_smart_fan_mode(mode_value):
"""
Set the Smart Fan Mode of the system.
The Smart Fan Mode controls the system's cooling behavior. Different modes can be set to
optimize the balance between cooling performance and noise level.
Args:
mode_value (int): The value of the Smart Fan Mode to set.
Known values:
- 0: Quiet Mode - Lower fan speed for quieter operation.
- 1: Balanced Mode - Moderate fan speed for everyday usage.
- 2: Performance Mode - Higher fan speed for intensive tasks.
- 224: Extreme Mode
- 255: Custom Mode - Custom fan curve can be set?.
Returns:
str: The result of the operation. Returns None if an error occurs.
"""
is_already_set = get_smart_fan_mode() == mode_value

if not is_already_set:
command = f"echo '\\_SB.GZFD.WMAA 0 0x2C {mode_value}' | tee /proc/acpi/call; cat /proc/acpi/call"
return execute_acpi_command(command)
return True

def get_smart_fan_mode():
"""
Get the current Smart Fan Mode of the system.
This function retrieves the current setting of the Smart Fan mode as specified in the WMI documentation.
Returns:
str: The current Smart Fan Mode. The return value corresponds to:
- '0': Quiet Mode
- '1': Balanced Mode
- '2': Performance Mode
- '224': Extreme Mode
- '255': Custom Mode
Returns None if an error occurs.
"""
command = "echo '\\_SB.GZFD.WMAA 0 0x2D' | tee /proc/acpi/call; cat /proc/acpi/call"
output = execute_acpi_command(command)
first_newline_position = output.find('\n')
output = output[first_newline_position+1:first_newline_position+5].replace('\x00', '')
return int(output, 16)
if d.startswith("0x") and d.endswith("\0"):
return int(d[:-1], 16)
if d.startswith("{") and d.endswith("}\0"):
bs = d[1:-2].split(", ")
return bytes(int(b, 16) for b in bs)
assert False, f"Return value '{d}' supported yet or was truncated."

def set_feature(id: int, value: int):
return call(
r"\_SB.GZFD.WMAE",
[
0,
0x12,
int.to_bytes(id, length=4, byteorder="little", signed=False)
+ int.to_bytes(value, length=4, byteorder="little", signed=False),
],
)
6 changes: 3 additions & 3 deletions src/components/fan/FanCurveSliders.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ const FanCurveSliders: VFC = () => {
const sliders = Object.entries(activeFanCurve).map(
([temp, fanSpeed], idx) => {
const updateFanCurveValue = (temp: string, fanSpeed: number) => {
if (idx >=6 && fanSpeed < 70) {
if (idx >= 6 && fanSpeed < 70) {
fanSpeed = 70;
}
return dispatch(fanSlice.actions.updateFanCurve({ temp, fanSpeed }));
Expand All @@ -23,8 +23,8 @@ const FanCurveSliders: VFC = () => {
showValue
valueSuffix="%"
step={5}
min={0}
max={100}
min={5}
max={115}
validValues="range"
bottomSeparator="none"
key={idx}
Expand Down

0 comments on commit 355f4b3

Please sign in to comment.