adjustor 3.5.1__tar.gz → 3.5.3__tar.gz

Sign up to get free protection for your applications and to get access to all the features.
Files changed (48) hide show
  1. {adjustor-3.5.1/src/adjustor.egg-info → adjustor-3.5.3}/PKG-INFO +1 -1
  2. {adjustor-3.5.1 → adjustor-3.5.3}/pyproject.toml +1 -1
  3. adjustor-3.5.3/src/adjustor/core/fan/__main__.py +7 -0
  4. adjustor-3.5.3/src/adjustor/core/fan/alg.py +157 -0
  5. adjustor-3.5.3/src/adjustor/core/fan/core.py +194 -0
  6. adjustor-3.5.3/src/adjustor/core/fan/utils.py +93 -0
  7. adjustor-3.5.3/src/adjustor/drivers/__init__.py +0 -0
  8. {adjustor-3.5.1 → adjustor-3.5.3}/src/adjustor/drivers/amd/__init__.py +51 -56
  9. {adjustor-3.5.1 → adjustor-3.5.3}/src/adjustor/drivers/amd/settings.yml +54 -54
  10. {adjustor-3.5.1 → adjustor-3.5.3}/src/adjustor/drivers/asus/__init__.py +0 -1
  11. {adjustor-3.5.1 → adjustor-3.5.3}/src/adjustor/drivers/general/__init__.py +69 -0
  12. {adjustor-3.5.1 → adjustor-3.5.3}/src/adjustor/events.py +7 -3
  13. {adjustor-3.5.1 → adjustor-3.5.3/src/adjustor.egg-info}/PKG-INFO +1 -1
  14. {adjustor-3.5.1 → adjustor-3.5.3}/src/adjustor.egg-info/SOURCES.txt +5 -0
  15. {adjustor-3.5.1 → adjustor-3.5.3}/LICENSE +0 -0
  16. {adjustor-3.5.1 → adjustor-3.5.3}/MANIFEST.in +0 -0
  17. {adjustor-3.5.1 → adjustor-3.5.3}/readme.md +0 -0
  18. {adjustor-3.5.1 → adjustor-3.5.3}/setup.cfg +0 -0
  19. {adjustor-3.5.1 → adjustor-3.5.3}/src/adjustor/__init__.py +0 -0
  20. {adjustor-3.5.1 → adjustor-3.5.3}/src/adjustor/__main__.py +0 -0
  21. {adjustor-3.5.1 → adjustor-3.5.3}/src/adjustor/core/__init__.py +0 -0
  22. {adjustor-3.5.1 → adjustor-3.5.3}/src/adjustor/core/acpi.py +0 -0
  23. {adjustor-3.5.1 → adjustor-3.5.3}/src/adjustor/core/alib.py +0 -0
  24. {adjustor-3.5.1 → adjustor-3.5.3}/src/adjustor/core/const.py +0 -0
  25. {adjustor-3.5.1/src/adjustor/drivers → adjustor-3.5.3/src/adjustor/core/fan}/__init__.py +0 -0
  26. {adjustor-3.5.1 → adjustor-3.5.3}/src/adjustor/core/lenovo.py +0 -0
  27. {adjustor-3.5.1 → adjustor-3.5.3}/src/adjustor/core/platform.py +0 -0
  28. {adjustor-3.5.1 → adjustor-3.5.3}/src/adjustor/drivers/amd/power-profiles-daemon.dbus.xml.in +0 -0
  29. {adjustor-3.5.1 → adjustor-3.5.3}/src/adjustor/drivers/amd/ppd.py +0 -0
  30. {adjustor-3.5.1 → adjustor-3.5.3}/src/adjustor/drivers/asus/settings.yml +0 -0
  31. {adjustor-3.5.1 → adjustor-3.5.3}/src/adjustor/drivers/general/settings.yml +0 -0
  32. {adjustor-3.5.1 → adjustor-3.5.3}/src/adjustor/drivers/lenovo/__init__.py +0 -0
  33. {adjustor-3.5.1 → adjustor-3.5.3}/src/adjustor/drivers/lenovo/settings.yml +0 -0
  34. {adjustor-3.5.1 → adjustor-3.5.3}/src/adjustor/drivers/smu/__init__.py +0 -0
  35. {adjustor-3.5.1 → adjustor-3.5.3}/src/adjustor/drivers/smu/qam.yml +0 -0
  36. {adjustor-3.5.1 → adjustor-3.5.3}/src/adjustor/drivers/smu/smu.yml +0 -0
  37. {adjustor-3.5.1 → adjustor-3.5.3}/src/adjustor/fuse/__init__.py +0 -0
  38. {adjustor-3.5.1 → adjustor-3.5.3}/src/adjustor/fuse/driver.py +0 -0
  39. {adjustor-3.5.1 → adjustor-3.5.3}/src/adjustor/fuse/gpu.py +0 -0
  40. {adjustor-3.5.1 → adjustor-3.5.3}/src/adjustor/fuse/utils.py +0 -0
  41. {adjustor-3.5.1 → adjustor-3.5.3}/src/adjustor/hhd.py +0 -0
  42. {adjustor-3.5.1 → adjustor-3.5.3}/src/adjustor/i18n.py +0 -0
  43. {adjustor-3.5.1 → adjustor-3.5.3}/src/adjustor/settings.yml +0 -0
  44. {adjustor-3.5.1 → adjustor-3.5.3}/src/adjustor.egg-info/dependency_links.txt +0 -0
  45. {adjustor-3.5.1 → adjustor-3.5.3}/src/adjustor.egg-info/entry_points.txt +0 -0
  46. {adjustor-3.5.1 → adjustor-3.5.3}/src/adjustor.egg-info/requires.txt +0 -0
  47. {adjustor-3.5.1 → adjustor-3.5.3}/src/adjustor.egg-info/top_level.txt +0 -0
  48. {adjustor-3.5.1 → adjustor-3.5.3}/usr/share/dbus-1/system.d/hhd-net.hadess.PowerProfiles.conf +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: adjustor
3
- Version: 3.5.1
3
+ Version: 3.5.3
4
4
  Summary: Adjustor, a userspace program for managing the TDP of handheld devices.
5
5
  Author-email: Kapenekakis Antheas <pypi@antheas.dev>
6
6
  Project-URL: Homepage, https://github.com/hhd-dev/adjustor
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "adjustor"
3
- version = "3.5.1"
3
+ version = "3.5.3"
4
4
  authors = [
5
5
  { name="Kapenekakis Antheas", email="pypi@antheas.dev" },
6
6
  ]
@@ -0,0 +1,7 @@
1
+ import sys
2
+
3
+ from .core import fan_pwm_tester
4
+
5
+ if __name__ == "__main__":
6
+ observe_only = "--observe" in sys.argv
7
+ fan_pwm_tester(observe_only=observe_only)
@@ -0,0 +1,157 @@
1
+ # The objective of this fan algorithm is simple: receive a fan curve for a specific
2
+ # power profile, the current system temperature, and the current fan speed.
3
+ # Then, have a single variable memory: the current acceleration.
4
+ # At each point, use a derived jerk to modify the acceleration and move the
5
+ # speed to the desired point smoothly.
6
+
7
+ # Maximum typical transition span of the fan speed (e.g., from 20% to 70%)
8
+ # This transition span should take exactly the seconds specified below
9
+ # E.g., if the fan begins to move from 20% to 70% with 0 acceleration, where
10
+ # the temperature is at 70C, it should take at most 5 seconds.
11
+ SPEED_SPAN = 0.5
12
+
13
+ # The ratio of the curve to begin decelerating the fan change rpm/s^2
14
+ # E.g., when going from 20% to 70%, the fan should begin decelerating
15
+ # at 0.7*(70-20) + 20 = 62% speed.
16
+ DECEL_RATIO = 0.5
17
+
18
+ # The temperature at which the fan should use the high accel speed.
19
+ HIGH_TEMP_EDGE = 65
20
+ HIGH_TEMP_JUNCTION = 75
21
+
22
+ # Allow for a gradual transition if the temperature is low
23
+ # Speed up if not.
24
+ ACCEL_UP_LOWT_T = 30
25
+ ACCEL_UP_HIGH_T = 20
26
+ # Penalize going down to avoid dithering
27
+ ACCEL_DOWN_T = 40
28
+
29
+ JERK_TOLERANCE = 0.9
30
+ MAX_ACCEL = 0.4
31
+ SETPOINT_DEVIATION = 0.01
32
+
33
+ UPDATE_FREQUENCY = 5
34
+ UPDATE_T = 1 / UPDATE_FREQUENCY
35
+ SETPOINT_UPDATE_FREQUENCY = 1
36
+ SETPOINT_UPDATE_T = 1 / SETPOINT_UPDATE_FREQUENCY
37
+ HYSTERESIS_RATIO = 0.25
38
+
39
+
40
+ def _calculate_jerk(speed_span, decel_ratio, freq, time):
41
+ """Calculate the required positive and negative jerks such that the
42
+ fan can cross the speed span (e.g., 50%) in the specified time (e.g., 5s)
43
+ given an update frequency (e.g., 3), and the point at which it should start
44
+ decelerating (e.g., 0.3).
45
+ """
46
+
47
+ jerk_accel = (2 * speed_span) / ((time * freq) ** 2) / ((1 - decel_ratio) ** 2)
48
+ jerk_decel = -(1 - decel_ratio) / decel_ratio * jerk_accel
49
+ return jerk_accel, jerk_decel
50
+
51
+
52
+ def calculate_jerk(t_target: float, increase: bool, junction: bool):
53
+ """Calculate the jerk based on the target temperature and whether the fan
54
+ speed should increase or decrease to reach it.
55
+
56
+ Allow for specifying whether the temperature probe is in the junction or the
57
+ edge, as junction reaches thermal saturation faster and at higher temperatures."""
58
+ if not increase:
59
+ return _calculate_jerk(SPEED_SPAN, DECEL_RATIO, UPDATE_FREQUENCY, ACCEL_DOWN_T)
60
+
61
+ if (junction and t_target > HIGH_TEMP_JUNCTION) or (
62
+ not junction and t_target > HIGH_TEMP_EDGE
63
+ ):
64
+ return _calculate_jerk(
65
+ SPEED_SPAN, DECEL_RATIO, UPDATE_FREQUENCY, ACCEL_UP_HIGH_T
66
+ )
67
+
68
+ return _calculate_jerk(SPEED_SPAN, DECEL_RATIO, UPDATE_FREQUENCY, ACCEL_UP_LOWT_T)
69
+
70
+
71
+ def move_to_setpoint(v_curr, a_curr, jerk_accel, jerk_decel, v_target):
72
+ """Update the current fan speed and acceleration by either using jerk_accel
73
+ which will increase acceleration to meet the target speed or jerk_decel
74
+ which will begin to decrease it to 0.
75
+
76
+ The choice between jerk_accel and jerk_decel is made by calculating the
77
+ minimum negative jerk required to decelerate to a=0 when reaching the target.
78
+ If the minimum jerk is smaller than jerk_decel, we use jerk_accel.
79
+ To avoid overshoots, a tolerance is used to start decelerating a bit earlier
80
+ than when the minimum jerk reaches the value of jerk_decel.
81
+ """
82
+
83
+ # Flip the jerks if the target is lower than the current speed
84
+ diff = v_target - v_curr
85
+ if diff < 0:
86
+ jerk_accel = -jerk_accel
87
+ jerk_decel = -jerk_decel
88
+
89
+ correct_direction = (diff > 0 and a_curr > 0) or (diff < 0 and a_curr < 0)
90
+ non_zero = abs(diff) > 1e-3
91
+
92
+ # Always accelerate if we are on the right direction or speed is zero
93
+ # Start decelerating once we run out of opposite jerk.
94
+ accel = True
95
+ if correct_direction and non_zero:
96
+ min_jerk_neg = -(a_curr**2) / 2 / diff
97
+ accel = abs(min_jerk_neg) < JERK_TOLERANCE * abs(jerk_decel)
98
+
99
+ if accel:
100
+ jerk = jerk_accel
101
+ else:
102
+ jerk = jerk_decel
103
+
104
+ # Calculate the new acceleration
105
+ a_new = a_curr + jerk
106
+ v_new = v_curr + a_new
107
+ return v_new, a_new
108
+
109
+
110
+ def sanitize_fan_values(v: float, a: float):
111
+ return max(0, min(1, v)), max(-MAX_ACCEL, min(MAX_ACCEL, a))
112
+
113
+
114
+ def has_reached_setpoint(v_curr, a_curr, v_target):
115
+ """Check if the current fan speed has reached the target speed.
116
+
117
+ If true, lower the update rate and set a_curr to 0 to avoid dithering.
118
+ """
119
+ return abs(v_curr - v_target) < SETPOINT_DEVIATION
120
+
121
+
122
+ def update_setpoint(temp: float, curr: int, fan_curve: dict[int, float]):
123
+ """Update the setpoint given the current temperature, fan curve, and previous setpoint.
124
+
125
+ Fan curve is a dictionary of increasing temperatures to fan speeds.
126
+ """
127
+
128
+ targets = list(fan_curve.keys())
129
+ assert curr in targets, "Current setpoint not in fan curve"
130
+
131
+ idx = targets.index(curr)
132
+
133
+ # Add some hysterisis to avoid dithering
134
+ if idx > 0:
135
+ prev = targets[idx - 1]
136
+ if temp < prev + (curr - prev) * HYSTERESIS_RATIO:
137
+ return prev
138
+
139
+ if idx < len(targets) - 1:
140
+ next = targets[idx + 1]
141
+ if temp > next - (next - curr) * HYSTERESIS_RATIO:
142
+ return next
143
+
144
+ return curr
145
+
146
+
147
+ def get_initial_setpoint(temp: float, fan_curve: dict[int, float]):
148
+ """Get the initial setpoint given the current temperature and fan curve.
149
+
150
+ Fan curve is a dictionary of increasing temperatures to fan speeds.
151
+ """
152
+
153
+ targets = list(fan_curve.keys())
154
+ for idx, target in enumerate(targets):
155
+ if temp < target:
156
+ return targets[idx - 1] if idx else targets[0]
157
+ return targets[-1]
@@ -0,0 +1,194 @@
1
+ import logging
2
+ import time
3
+ from typing import TypedDict
4
+
5
+ from .alg import (
6
+ SETPOINT_UPDATE_T,
7
+ UPDATE_T,
8
+ calculate_jerk,
9
+ get_initial_setpoint,
10
+ has_reached_setpoint,
11
+ move_to_setpoint,
12
+ sanitize_fan_values,
13
+ update_setpoint,
14
+ )
15
+ from .utils import (
16
+ find_edge_temp,
17
+ find_fans,
18
+ find_tctl_temp,
19
+ read_fan_speed,
20
+ read_temp,
21
+ write_fan_speed,
22
+ )
23
+
24
+ logger = logging.getLogger(__name__)
25
+
26
+
27
+ class FanInfo(TypedDict):
28
+ tctl: str
29
+ edge: str
30
+ fans: list[str]
31
+
32
+
33
+ class FanData(TypedDict):
34
+ a: float
35
+ v: float
36
+ t_target: int
37
+
38
+
39
+ class FanState(TypedDict):
40
+ v_curr: float
41
+ v_target: float
42
+ v_target_pwm: int
43
+ v_rpm: list[int]
44
+ t_junction: float
45
+ t_edge: float
46
+ fan_data: FanData
47
+
48
+
49
+ def get_fan_info() -> FanInfo | None:
50
+ tctl = find_tctl_temp()
51
+ if tctl is None:
52
+ logger.error("Could not find tctl junction temperature.")
53
+ return None
54
+
55
+ edge = find_edge_temp()
56
+ if edge is None:
57
+ logger.error("Could not find edge temperature.")
58
+ return None
59
+
60
+ fans = find_fans()
61
+ if not fans:
62
+ logger.error("Could not find PWM controllable fans.")
63
+ return None
64
+
65
+ return {"tctl": tctl, "edge": edge, "fans": fans}
66
+
67
+
68
+ def calculate_fan_speed(
69
+ temp: float, data: FanData | None, fan_curve: dict[int, float], junction: bool
70
+ ) -> tuple[float, bool, FanData]:
71
+ if data is None:
72
+ # Initialize with best guess
73
+ t_target = get_initial_setpoint(temp, fan_curve)
74
+ v_curr = fan_curve[t_target]
75
+ a_curr = 0
76
+ return v_curr, False, {"a": a_curr, "v": v_curr, "t_target": t_target}
77
+
78
+ # Get values and new temp target setpoint
79
+ v_curr = data["v"]
80
+ a_curr = data["a"]
81
+ t_target = data["t_target"]
82
+ t_target = update_setpoint(temp, t_target, fan_curve)
83
+
84
+ # Pin values if we are in the setpoint
85
+ if has_reached_setpoint(v_curr, a_curr, fan_curve[t_target]):
86
+ a_curr = 0
87
+ v_curr = fan_curve[t_target]
88
+ return v_curr, True, {"a": a_curr, "v": v_curr, "t_target": t_target}
89
+
90
+ v_target = fan_curve[t_target]
91
+ jerk_accel, jerk_decel = calculate_jerk(t_target, v_target > v_curr, junction)
92
+ v_new, a_new = move_to_setpoint(v_curr, a_curr, jerk_accel, jerk_decel, v_target)
93
+ v_new, a_new = sanitize_fan_values(v_new, a_new)
94
+
95
+ return v_new, False, {"a": a_new, "v": v_new, "t_target": t_target}
96
+
97
+
98
+ def set_fans_to_pwm(enable: bool, fan_info: FanInfo):
99
+ for _, fn_enable, _ in fan_info["fans"]:
100
+ with open(fn_enable, "w") as f:
101
+ f.write("1" if enable else "0")
102
+
103
+
104
+ def update_fan_speed(
105
+ state: FanState | None,
106
+ fan_info: FanInfo,
107
+ fan_curve: dict[int, float],
108
+ junction: bool,
109
+ observe_only: bool = False,
110
+ ) -> tuple[bool, FanState]:
111
+ t_edge = read_temp(fan_info["edge"])
112
+ t_junction = read_temp(fan_info["tctl"])
113
+
114
+ t_curr = t_junction if junction else t_edge
115
+ data = state["fan_data"] if state else None
116
+ v_curr, in_setpoint, data = calculate_fan_speed(t_curr, data, fan_curve, junction)
117
+
118
+ v_curr_int = min(255, max(0, int(v_curr * 255)))
119
+ if not observe_only:
120
+ if state is None or state["v_target_pwm"] != v_curr_int:
121
+ for v_fn, _, _ in fan_info["fans"]:
122
+ write_fan_speed(v_fn, v_curr_int)
123
+
124
+ fan_speeds = [read_fan_speed(rpm_fn) for _, _, rpm_fn in fan_info["fans"] if rpm_fn]
125
+ return (
126
+ in_setpoint,
127
+ {
128
+ "v_curr": v_curr,
129
+ "v_target": fan_curve[data["t_target"]],
130
+ "v_target_pwm": v_curr_int,
131
+ "v_rpm": fan_speeds,
132
+ "t_junction": t_junction,
133
+ "t_edge": t_edge,
134
+ "fan_data": data,
135
+ },
136
+ )
137
+
138
+
139
+ def fan_pwm_tester(normal_curve: bool = True, observe_only: bool = False):
140
+ fan_info = get_fan_info()
141
+ if fan_info is None:
142
+ return
143
+
144
+ if normal_curve:
145
+ fan_curve = {
146
+ 50: 0.3,
147
+ 60: 0.35,
148
+ 70: 0.4,
149
+ 80: 0.5,
150
+ 85: 0.6,
151
+ 90: 0.8,
152
+ 100: 0.9,
153
+ }
154
+ fan_curve = {
155
+ 40: 0.2,
156
+ 45: 0.3,
157
+ 50: 0.4,
158
+ 55: 0.45,
159
+ 60: 0.55,
160
+ 65: 0.7,
161
+ 70: 0.8,
162
+ 80: 0.85,
163
+ 90: 0.9,
164
+ 100: 1,
165
+ }
166
+ else:
167
+ fan_curve = {
168
+ 50: 0.3,
169
+ 60: 0.3,
170
+ 65: 0.9,
171
+ 80: 0.9,
172
+ 90: 0.9,
173
+ 100: 1,
174
+ }
175
+
176
+ try:
177
+ if not observe_only:
178
+ set_fans_to_pwm(True, fan_info)
179
+
180
+ MAX_FAN = 5300
181
+
182
+ state = None
183
+ for i in range(10000000):
184
+ in_setpoint, state = update_fan_speed(state, fan_info, fan_curve, False, observe_only=observe_only)
185
+
186
+ print(f"\n> {i:05d}: {'in setpoint' if in_setpoint else 'updating'}{' (observe)' if observe_only else ''}")
187
+ print(f" Junction: {state['t_junction']:.2f}C, Edge: {state['t_edge']:.2f}C")
188
+ print(f" Current: {state['v_curr']*100:.1f}%, Target: {state['v_target']*100:.1f}%")
189
+ print(f" Fan speeds: {' '.join(map(lambda rpm: f"{rpm:4d}rpm/{MAX_FAN}rpm ({100*rpm/MAX_FAN:.1f}%)", state['v_rpm']))}")
190
+ time.sleep(SETPOINT_UPDATE_T if in_setpoint else UPDATE_T)
191
+ except KeyboardInterrupt:
192
+ print("Exiting fan test.")
193
+ finally:
194
+ set_fans_to_pwm(False, fan_info)
@@ -0,0 +1,93 @@
1
+ import os
2
+
3
+ FAN_HWMONS = ["oxpec"]
4
+ HWMON_DIR = "/sys/class/hwmon"
5
+
6
+
7
+ def get_hwmon():
8
+ for dir in os.listdir(HWMON_DIR):
9
+ if dir.startswith("hwmon"):
10
+ yield dir
11
+
12
+
13
+ def find_edge_temp():
14
+ for hwmon in get_hwmon():
15
+ with open(f"{HWMON_DIR}/{hwmon}/name") as f:
16
+ name = f.read().strip()
17
+
18
+ if name != "amdgpu":
19
+ continue
20
+
21
+ # For sanity, check the device has CPUs to avoid hooking an eGPU.
22
+ if not os.path.exists(f"{HWMON_DIR}/{hwmon}/device/local_cpus"):
23
+ continue
24
+
25
+ if not os.path.exists(f"{HWMON_DIR}/{hwmon}/temp1_input"):
26
+ continue
27
+
28
+ return f"{HWMON_DIR}/{hwmon}/temp1_input"
29
+
30
+
31
+ def find_tctl_temp():
32
+ for hwmon in get_hwmon():
33
+ with open(f"{HWMON_DIR}/{hwmon}/name") as f:
34
+ name = f.read().strip()
35
+
36
+ if name != "k10temp":
37
+ continue
38
+
39
+ # For sanity, check the device has CPUs to avoid hooking an eGPU.
40
+ if not os.path.exists(f"{HWMON_DIR}/{hwmon}/device/local_cpus"):
41
+ continue
42
+
43
+ if not os.path.exists(f"{HWMON_DIR}/{hwmon}/temp1_input"):
44
+ continue
45
+
46
+ return f"{HWMON_DIR}/{hwmon}/temp1_input"
47
+
48
+
49
+ def find_fans():
50
+ """Finds tunable fans with endpoints pwmX and pwmX_enable."""
51
+ fans = []
52
+ for hwmon in get_hwmon():
53
+ with open(f"{HWMON_DIR}/{hwmon}/name") as f:
54
+ name = f.read().strip()
55
+
56
+ if name not in FAN_HWMONS:
57
+ continue
58
+
59
+ for fn in os.listdir(f"{HWMON_DIR}/{hwmon}"):
60
+ if (
61
+ fn.startswith("pwm")
62
+ and fn[3:].isdigit()
63
+ and os.path.exists(f"{HWMON_DIR}/{hwmon}/{fn}_enable")
64
+ ):
65
+ idx = fn[3:]
66
+ speed = f"fan{idx}_input"
67
+ if speed in os.listdir(f"{HWMON_DIR}/{hwmon}"):
68
+ speed_fn = f"{HWMON_DIR}/{hwmon}/{speed}"
69
+ else:
70
+ speed_fn = None
71
+ fans.append(
72
+ (
73
+ f"{HWMON_DIR}/{hwmon}/{fn}",
74
+ f"{HWMON_DIR}/{hwmon}/{fn}_enable",
75
+ speed_fn,
76
+ )
77
+ )
78
+
79
+ return fans
80
+
81
+
82
+ def read_temp(path: str) -> float:
83
+ with open(path, "r") as f:
84
+ return int(f.read()) / 1000
85
+
86
+
87
+ def read_fan_speed(path: str) -> int:
88
+ with open(path, "r") as f:
89
+ return int(f.read())
90
+
91
+ def write_fan_speed(path: str, speed: int):
92
+ with open(path, "w") as f:
93
+ f.write(str(speed))
File without changes
@@ -142,11 +142,13 @@ class AmdGPUPlugin(HHDPlugin):
142
142
 
143
143
  if self.ppd_conflict and os.environ.get("HHD_PPD_MASK", None):
144
144
  logger.warning(
145
- "PPD conflict detected but HHD_PPD_MASK is set. Masking PPD."
145
+ "PPD conflict detected but HHD_PPD_MASK is set. Masking PPD/TuneD."
146
146
  )
147
147
  # Mask and disable
148
148
  os.system("systemctl mask power-profiles-daemon.service")
149
149
  os.system("systemctl disable --now power-profiles-daemon.service")
150
+ os.system("systemctl mask tuned.service")
151
+ os.system("systemctl disable --now tuned.service")
150
152
  # Keep going without check to avoid obscure errors
151
153
  self.ppd_conflict = False
152
154
 
@@ -158,20 +160,20 @@ class AmdGPUPlugin(HHDPlugin):
158
160
  }
159
161
 
160
162
  self.initialized = True
161
-
163
+
162
164
  # Initialize frequency settings
163
- manual_freq = sets["enabled"]["children"]["mode"]["modes"]["manual"][
165
+ manual_freq = sets["enabled"]["children"]["gpu_freq"]["modes"]["manual"][
166
+ "children"
167
+ ]["frequency"]
168
+ upper_freq = sets["enabled"]["children"]["gpu_freq"]["modes"]["upper"][
164
169
  "children"
165
- ]["gpu_freq"]["modes"]["manual"]["children"]["frequency"]
166
- upper_freq = sets["enabled"]["children"]["mode"]["modes"]["manual"][
170
+ ]["frequency"]
171
+ min_freq = sets["enabled"]["children"]["gpu_freq"]["modes"]["range"][
167
172
  "children"
168
- ]["gpu_freq"]["modes"]["upper"]["children"]["frequency"]
169
- min_freq = sets["enabled"]["children"]["mode"]["modes"]["manual"]["children"][
170
- "gpu_freq"
171
- ]["modes"]["range"]["children"]["min"]
172
- max_freq = sets["enabled"]["children"]["mode"]["modes"]["manual"]["children"][
173
- "gpu_freq"
174
- ]["modes"]["range"]["children"]["max"]
173
+ ]["min"]
174
+ max_freq = sets["enabled"]["children"]["gpu_freq"]["modes"]["range"][
175
+ "children"
176
+ ]["max"]
175
177
 
176
178
  manual_freq["default"] = ((status.freq_min + status.freq_max) // 200) * 100
177
179
  upper_freq["default"] = status.freq_max
@@ -285,6 +287,10 @@ class AmdGPUPlugin(HHDPlugin):
285
287
  if new_ppd:
286
288
  try:
287
289
  self.proc, self.t = _open_ppd_server(self.emit)
290
+ # Fixup target in case it came before
291
+ if self.proc.stdin and self.target:
292
+ self.proc.stdin.write(f"{self.target}\n".encode())
293
+ self.proc.stdin.flush()
288
294
  except Exception as e:
289
295
  logger.error(f"Failed to open PPD server:\n{e}")
290
296
  self.close_ppd()
@@ -305,7 +311,6 @@ class AmdGPUPlugin(HHDPlugin):
305
311
  try:
306
312
  match self.target:
307
313
  case "balanced":
308
- set_gpu_auto()
309
314
  if self.supports_epp:
310
315
  set_powersave_governor()
311
316
  set_epp_mode("balance_power")
@@ -313,7 +318,6 @@ class AmdGPUPlugin(HHDPlugin):
313
318
  set_cpu_boost(True)
314
319
  set_frequency_scaling(nonlinear=False)
315
320
  case "performance":
316
- set_gpu_auto()
317
321
  if self.supports_epp:
318
322
  set_powersave_governor()
319
323
  set_epp_mode("balance_power")
@@ -321,7 +325,6 @@ class AmdGPUPlugin(HHDPlugin):
321
325
  set_cpu_boost(True)
322
326
  set_frequency_scaling(nonlinear=True)
323
327
  case _: # power
324
- set_gpu_auto()
325
328
  if self.supports_epp:
326
329
  set_powersave_governor()
327
330
  set_epp_mode("power")
@@ -334,52 +337,11 @@ class AmdGPUPlugin(HHDPlugin):
334
337
  # Unless it is set manually, use the default scheduler.
335
338
  self.close_sched()
336
339
  self.old_sched = None
337
- self.old_freq = None
338
340
  self.old_boost = None
339
341
  self.old_epp = None
340
342
  self.old_min_freq = None
341
343
  else:
342
344
  self.old_target = None
343
- new_gpu = conf["tdp.amd_energy.mode.manual.gpu_freq.mode"].to(str)
344
- match new_gpu:
345
- case "manual":
346
- f = conf["tdp.amd_energy.mode.manual.gpu_freq.manual.frequency"].to(
347
- int
348
- )
349
- new_freq = (f, f)
350
- case "upper":
351
- f = conf["tdp.amd_energy.mode.manual.gpu_freq.upper.frequency"].to(
352
- int
353
- )
354
- new_freq = (self.min_freq or f, f)
355
- case "range":
356
- min_f = conf["tdp.amd_energy.mode.manual.gpu_freq.range.min"].to(
357
- int
358
- )
359
- max_f = conf["tdp.amd_energy.mode.manual.gpu_freq.range.max"].to(
360
- int
361
- )
362
- if max_f < min_f:
363
- max_f = min_f
364
- conf["tdp.amd_energy.mode.manual.gpu_freq.range.max"] = min_f
365
- new_freq = (min_f, max_f)
366
- case _:
367
- new_freq = None
368
-
369
- if new_freq != self.old_freq:
370
- self.old_freq = new_freq
371
- self.queue_gpu = curr + APPLY_DELAY
372
-
373
- if self.queue_gpu is not None and curr >= self.queue_gpu:
374
- self.queue_gpu = None
375
- try:
376
- if new_freq:
377
- set_gpu_manual(*new_freq)
378
- else:
379
- set_gpu_auto()
380
- except Exception as e:
381
- logger.error(f"Failed to set GPU mode:\n{e}")
382
-
383
345
  if self.supports_boost:
384
346
  new_boost = conf["tdp.amd_energy.mode.manual.cpu_boost"].to(bool)
385
347
  if new_boost != self.old_boost:
@@ -439,6 +401,39 @@ class AmdGPUPlugin(HHDPlugin):
439
401
  stdout=subprocess.DEVNULL,
440
402
  )
441
403
 
404
+ # Apply GPU settings
405
+ new_gpu = conf["tdp.amd_energy.gpu_freq.mode"].to(str)
406
+ match new_gpu:
407
+ case "manual":
408
+ f = conf["tdp.amd_energy.gpu_freq.manual.frequency"].to(int)
409
+ new_freq = (f, f)
410
+ case "upper":
411
+ f = conf["tdp.amd_energy.gpu_freq.upper.frequency"].to(int)
412
+ new_freq = (self.min_freq or f, f)
413
+ case "range":
414
+ min_f = conf["tdp.amd_energy.gpu_freq.range.min"].to(int)
415
+ max_f = conf["tdp.amd_energy.gpu_freq.range.max"].to(int)
416
+ if max_f < min_f:
417
+ max_f = min_f
418
+ conf["tdp.amd_energy.gpu_freq.range.max"] = min_f
419
+ new_freq = (min_f, max_f)
420
+ case _:
421
+ new_freq = None
422
+
423
+ if new_freq != self.old_freq:
424
+ self.old_freq = new_freq
425
+ self.queue_gpu = curr + APPLY_DELAY
426
+
427
+ if self.queue_gpu is not None and curr >= self.queue_gpu:
428
+ self.queue_gpu = None
429
+ try:
430
+ if new_freq:
431
+ set_gpu_manual(*new_freq)
432
+ else:
433
+ set_gpu_auto()
434
+ except Exception as e:
435
+ logger.error(f"Failed to set GPU mode:\n{e}")
436
+
442
437
  def close_ppd(self):
443
438
  if self.proc is not None:
444
439
  self.proc.send_signal(signal.SIGINT)
@@ -1,11 +1,11 @@
1
1
  enabled:
2
- title: Energy Management
2
+ title: Processor Settings
3
3
  type: container
4
4
  tags: [ hide-title, non-essential ]
5
5
  children:
6
6
  mode:
7
7
  type: mode
8
- title: Energy Management
8
+ title: CPU Settings
9
9
  default: auto
10
10
  modes:
11
11
  auto:
@@ -63,56 +63,6 @@ enabled:
63
63
  options:
64
64
  disabled: Disabled
65
65
  enabled: Enabled
66
- gpu_freq:
67
- type: mode
68
- title: GPU Frequency
69
- hint: >-
70
- Pins the GPU to a certain frequency.
71
- Helps in certain games that are CPU or GPU heavy
72
- by shifting power to or from the GPU.
73
- Has a minor effect.
74
- default: auto
75
- modes:
76
- auto:
77
- type: container
78
- title: Auto
79
- hint: >-
80
- Lets the GPU manage its own frequency.
81
- upper:
82
- type: container
83
- title: Upper Limit
84
- hint: >-
85
- Sets the GPU frequency to a maximum.
86
- children:
87
- frequency: &freq
88
- type: int
89
- unit: MHz
90
- min: 300
91
- max: 2000
92
- step: 100
93
- default: 1000
94
- title: Maximum Frequency
95
- range:
96
- type: container
97
- title: Range
98
- hint: >-
99
- Sets the GPU frequency to a range.
100
- children:
101
- min:
102
- <<: *freq
103
- title: Minimum Frequency
104
- max:
105
- <<: *freq
106
- title: Maximum Frequency
107
- manual:
108
- type: container
109
- title: Fixed
110
- hint: >-
111
- Sets the GPU frequency manually.
112
- children:
113
- frequency:
114
- <<: *freq
115
- title: Frequency
116
66
  sched:
117
67
  type: multiple
118
68
  title: Custom Scheduler
@@ -125,9 +75,59 @@ enabled:
125
75
  scx_bpfland: bpfland
126
76
  scx_rusty: rusty
127
77
  default: disabled
78
+ gpu_freq:
79
+ type: mode
80
+ title: GPU Frequency
81
+ hint: >-
82
+ Pins the GPU to a certain frequency.
83
+ Helps in certain games that are CPU or GPU heavy
84
+ by shifting power to or from the GPU.
85
+ Has a minor effect.
86
+ default: auto
87
+ modes:
88
+ auto:
89
+ type: container
90
+ title: Auto
91
+ hint: >-
92
+ Lets the GPU manage its own frequency.
93
+ upper:
94
+ type: container
95
+ title: Max Limit
96
+ hint: >-
97
+ Limits the maximum frequency of the GPU.
98
+ children:
99
+ frequency: &freq
100
+ type: int
101
+ unit: MHz
102
+ min: 300
103
+ max: 2000
104
+ step: 100
105
+ default: 1000
106
+ title: Maximum Frequency
107
+ range:
108
+ type: container
109
+ title: Range
110
+ hint: >-
111
+ Sets the GPU frequency to a range.
112
+ children:
113
+ min:
114
+ <<: *freq
115
+ title: Minimum Frequency
116
+ max:
117
+ <<: *freq
118
+ title: Maximum Frequency
119
+ manual:
120
+ type: container
121
+ title: Fixed
122
+ hint: >-
123
+ Pins the GPU to a certain frequency (not recommended).
124
+ children:
125
+ frequency:
126
+ <<: *freq
127
+ title: Frequency
128
128
 
129
129
  conflict:
130
- title: Energy Management
130
+ title: Processor Settings
131
131
  type: container
132
132
  tags: [ hide-title, non-essential ]
133
133
  children:
@@ -138,7 +138,7 @@ conflict:
138
138
  Energy Management can not be enabled while PPD or TuneD are enabled.
139
139
  `systemctl mask power-profiles-daemon` or `tuned`.
140
140
  enable:
141
- title: Enable Energy Management
141
+ title: Enable Processor Settings
142
142
  type: action
143
143
  tags: [ non-essential ]
144
144
  core:
@@ -255,7 +255,6 @@ class AsusDriverPlugin(HHDPlugin):
255
255
  self.new_tdp = None
256
256
  new_mode = self.new_mode
257
257
  self.new_mode = None
258
- ally_x = self.allyx
259
258
  if new_tdp:
260
259
  # For TDP values received from steam, set the appropriate
261
260
  # mode to get a better experience.
@@ -27,6 +27,7 @@ class GeneralPowerPlugin(HHDPlugin):
27
27
  self.old_sched = None
28
28
  self.sched_proc = None
29
29
  self.ppd_supported = None
30
+ self.tuned_supported = None
30
31
  self.is_steamdeck = is_steamdeck
31
32
  self.ovr_enabled = False
32
33
  self.should_exit = Event()
@@ -54,6 +55,26 @@ class GeneralPowerPlugin(HHDPlugin):
54
55
  except Exception as e:
55
56
  logger.warning(f"powerprofilectl returned with error:\n{e}")
56
57
 
58
+ # TuneD
59
+ if self.tuned_supported is None:
60
+ self.tuned_supported = False
61
+ if tuned := shutil.which('tuned-adm'):
62
+ try:
63
+ if os.environ.get("HHD_PPD_MASK", None):
64
+ logger.info("Unmasking TuneD in the case it was masked.")
65
+ os.system('systemctl unmask tuned')
66
+ subprocess.run(
67
+ [tuned],
68
+ check=True,
69
+ stdin=subprocess.DEVNULL,
70
+ stdout=subprocess.DEVNULL,
71
+ stderr=subprocess.DEVNULL,
72
+ )
73
+ self.tuned_supported = True
74
+ except Exception as e:
75
+ logger.warning(f"tuned-adm returned with error:\n{e}")
76
+
77
+
57
78
  if not self.ppd_supported:
58
79
  del sets["children"]["profile"]
59
80
 
@@ -122,6 +143,54 @@ class GeneralPowerPlugin(HHDPlugin):
122
143
  logger.warning(f"powerprofilectl returned with error:\n{e}")
123
144
  self.ppd_supported = False
124
145
 
146
+ # Handle TuneD
147
+ if self.tuned_supported:
148
+ curr = time.time()
149
+ ppd_tuned_mapping = {
150
+ "power-saver": "powersave",
151
+ "balanced": "balanced",
152
+ "performance": "throughput-performance"
153
+ }
154
+ new_profile = ppd_tuned_mapping.get(conf.get("tdp.general.profile", self.target))
155
+ if new_profile != self.target and new_profile and self.target:
156
+ logger.info(f"Setting TuneD profile to '{new_profile}'")
157
+ self.target = new_profile
158
+ try:
159
+ subprocess.run(
160
+ [shutil.which('tuned-adm'), "profile", new_profile],
161
+ check=True,
162
+ stdout=subprocess.PIPE,
163
+ stderr=subprocess.PIPE,
164
+ )
165
+ except Exception as e:
166
+ self.tuned_supported = False
167
+ logger.warning(f"tuned-adm returned with error:\n{e}")
168
+ self.tuned_supported = False
169
+ elif not self.last_check or curr - self.last_check > 2:
170
+ # Update profile every 2 seconds
171
+ self.last_check = curr
172
+ try:
173
+ res = subprocess.run(
174
+ [shutil.which('tuned-adm'), "active"],
175
+ check=True,
176
+ stdout=subprocess.PIPE,
177
+ stderr=subprocess.PIPE,
178
+ )
179
+
180
+ tuned_ppd_mapping = {
181
+ "powersave": "power-saver",
182
+ "balanced": "balanced",
183
+ "throughput-performance": "performance"
184
+ }
185
+ self.target = tuned_ppd_mapping.get(res.stdout.decode().split(":")[1].strip()) # type: ignore
186
+
187
+ if self.target != conf["tdp.general.profile"].to(str):
188
+ conf["tdp.general.profile"] = self.target
189
+ except Exception as e:
190
+ self.tuned_supported = False
191
+ logger.warning(f"powerprofilectl returned with error:\n{e}")
192
+ self.tuned_supported = False
193
+
125
194
  # Handle sched
126
195
  if self.avail_scheds:
127
196
  # Check health and print error
@@ -12,8 +12,12 @@ EVENT_MATCHES: Sequence[tuple[dict[str, Any], str]] = [
12
12
  ({"device_class": b"ac_adapter", "data": 0}, "dc"),
13
13
  ({"device_class": b"ac_adapter", "data": 256}, "ac"),
14
14
  ({"device_class": b"battery"}, "battery"),
15
+ ({"device_class": b"button/power"}, "powerbutton"),
15
16
  # Legion GO TDP event
16
17
  ({"bus_id": b"D320289E-8FEA-"}, "tdp"),
18
+ # GPD Force hibernate thermal event
19
+ # , 'type': 0xf100, 'data': 0x0100 ignore these attrs for now...
20
+ ({"device_class": b"thermal_zone", "bus_id": b"LNXTHERM:00"}, "hibernate-thermal"),
17
21
  ]
18
22
 
19
23
  GUARD_DELAY = 0.5
@@ -43,10 +47,10 @@ def loop_process_events(emit: Emitter, should_exit: TEvent):
43
47
  break
44
48
 
45
49
  if matches:
46
- if etype != "battery":
47
- emit({"type": "acpi", "event": etype}) # type: ignore
50
+ if etype not in ("battery", "powerbutton"):
51
+ emit({"type": "acpi", "event": etype}) # type: ignore
48
52
  found = True
49
53
  break
50
54
 
51
55
  if not found:
52
- logger.info(f"Unknown ACPI event: {ev}")
56
+ logger.info(f"ACPI event: {ev}")
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: adjustor
3
- Version: 3.5.1
3
+ Version: 3.5.3
4
4
  Summary: Adjustor, a userspace program for managing the TDP of handheld devices.
5
5
  Author-email: Kapenekakis Antheas <pypi@antheas.dev>
6
6
  Project-URL: Homepage, https://github.com/hhd-dev/adjustor
@@ -20,6 +20,11 @@ src/adjustor/core/alib.py
20
20
  src/adjustor/core/const.py
21
21
  src/adjustor/core/lenovo.py
22
22
  src/adjustor/core/platform.py
23
+ src/adjustor/core/fan/__init__.py
24
+ src/adjustor/core/fan/__main__.py
25
+ src/adjustor/core/fan/alg.py
26
+ src/adjustor/core/fan/core.py
27
+ src/adjustor/core/fan/utils.py
23
28
  src/adjustor/drivers/__init__.py
24
29
  src/adjustor/drivers/amd/__init__.py
25
30
  src/adjustor/drivers/amd/power-profiles-daemon.dbus.xml.in
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes