adjustor 3.5.2__tar.gz → 3.5.4__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.2/src/adjustor.egg-info → adjustor-3.5.4}/PKG-INFO +1 -1
  2. {adjustor-3.5.2 → adjustor-3.5.4}/pyproject.toml +1 -1
  3. {adjustor-3.5.2 → adjustor-3.5.4}/src/adjustor/core/const.py +32 -0
  4. adjustor-3.5.4/src/adjustor/core/fan/__main__.py +7 -0
  5. adjustor-3.5.4/src/adjustor/core/fan/alg.py +157 -0
  6. adjustor-3.5.4/src/adjustor/core/fan/core.py +194 -0
  7. adjustor-3.5.4/src/adjustor/core/fan/utils.py +93 -0
  8. adjustor-3.5.4/src/adjustor/drivers/__init__.py +0 -0
  9. {adjustor-3.5.2 → adjustor-3.5.4}/src/adjustor/drivers/amd/__init__.py +48 -55
  10. {adjustor-3.5.2 → adjustor-3.5.4}/src/adjustor/drivers/amd/settings.yml +54 -54
  11. {adjustor-3.5.2 → adjustor-3.5.4}/src/adjustor/drivers/asus/__init__.py +4 -2
  12. {adjustor-3.5.2 → adjustor-3.5.4}/src/adjustor/drivers/general/__init__.py +3 -3
  13. {adjustor-3.5.2 → adjustor-3.5.4}/src/adjustor/drivers/lenovo/__init__.py +11 -1
  14. {adjustor-3.5.2 → adjustor-3.5.4}/src/adjustor/drivers/lenovo/settings.yml +6 -0
  15. {adjustor-3.5.2 → adjustor-3.5.4}/src/adjustor/events.py +8 -3
  16. {adjustor-3.5.2 → adjustor-3.5.4/src/adjustor.egg-info}/PKG-INFO +1 -1
  17. {adjustor-3.5.2 → adjustor-3.5.4}/src/adjustor.egg-info/SOURCES.txt +5 -0
  18. {adjustor-3.5.2 → adjustor-3.5.4}/LICENSE +0 -0
  19. {adjustor-3.5.2 → adjustor-3.5.4}/MANIFEST.in +0 -0
  20. {adjustor-3.5.2 → adjustor-3.5.4}/readme.md +0 -0
  21. {adjustor-3.5.2 → adjustor-3.5.4}/setup.cfg +0 -0
  22. {adjustor-3.5.2 → adjustor-3.5.4}/src/adjustor/__init__.py +0 -0
  23. {adjustor-3.5.2 → adjustor-3.5.4}/src/adjustor/__main__.py +0 -0
  24. {adjustor-3.5.2 → adjustor-3.5.4}/src/adjustor/core/__init__.py +0 -0
  25. {adjustor-3.5.2 → adjustor-3.5.4}/src/adjustor/core/acpi.py +0 -0
  26. {adjustor-3.5.2 → adjustor-3.5.4}/src/adjustor/core/alib.py +0 -0
  27. {adjustor-3.5.2/src/adjustor/drivers → adjustor-3.5.4/src/adjustor/core/fan}/__init__.py +0 -0
  28. {adjustor-3.5.2 → adjustor-3.5.4}/src/adjustor/core/lenovo.py +0 -0
  29. {adjustor-3.5.2 → adjustor-3.5.4}/src/adjustor/core/platform.py +0 -0
  30. {adjustor-3.5.2 → adjustor-3.5.4}/src/adjustor/drivers/amd/power-profiles-daemon.dbus.xml.in +0 -0
  31. {adjustor-3.5.2 → adjustor-3.5.4}/src/adjustor/drivers/amd/ppd.py +0 -0
  32. {adjustor-3.5.2 → adjustor-3.5.4}/src/adjustor/drivers/asus/settings.yml +0 -0
  33. {adjustor-3.5.2 → adjustor-3.5.4}/src/adjustor/drivers/general/settings.yml +0 -0
  34. {adjustor-3.5.2 → adjustor-3.5.4}/src/adjustor/drivers/smu/__init__.py +0 -0
  35. {adjustor-3.5.2 → adjustor-3.5.4}/src/adjustor/drivers/smu/qam.yml +0 -0
  36. {adjustor-3.5.2 → adjustor-3.5.4}/src/adjustor/drivers/smu/smu.yml +0 -0
  37. {adjustor-3.5.2 → adjustor-3.5.4}/src/adjustor/fuse/__init__.py +0 -0
  38. {adjustor-3.5.2 → adjustor-3.5.4}/src/adjustor/fuse/driver.py +0 -0
  39. {adjustor-3.5.2 → adjustor-3.5.4}/src/adjustor/fuse/gpu.py +0 -0
  40. {adjustor-3.5.2 → adjustor-3.5.4}/src/adjustor/fuse/utils.py +0 -0
  41. {adjustor-3.5.2 → adjustor-3.5.4}/src/adjustor/hhd.py +0 -0
  42. {adjustor-3.5.2 → adjustor-3.5.4}/src/adjustor/i18n.py +0 -0
  43. {adjustor-3.5.2 → adjustor-3.5.4}/src/adjustor/settings.yml +0 -0
  44. {adjustor-3.5.2 → adjustor-3.5.4}/src/adjustor.egg-info/dependency_links.txt +0 -0
  45. {adjustor-3.5.2 → adjustor-3.5.4}/src/adjustor.egg-info/entry_points.txt +0 -0
  46. {adjustor-3.5.2 → adjustor-3.5.4}/src/adjustor.egg-info/requires.txt +0 -0
  47. {adjustor-3.5.2 → adjustor-3.5.4}/src/adjustor.egg-info/top_level.txt +0 -0
  48. {adjustor-3.5.2 → adjustor-3.5.4}/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.2
3
+ Version: 3.5.4
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.2"
3
+ version = "3.5.4"
4
4
  authors = [
5
5
  { name="Kapenekakis Antheas", email="pypi@antheas.dev" },
6
6
  ]
@@ -1,5 +1,33 @@
1
+ from typing import TypedDict
2
+
1
3
  from .alib import A, D, DeviceParams, AlibParams
2
4
 
5
+
6
+ class DevicePreset(TypedDict):
7
+ tdp_limit: int
8
+ slow_limit: int
9
+ fast_limit: int
10
+ slow_time: int
11
+ stapm_time: int
12
+ temp_target: int
13
+ fan_curve: dict[int, float] | None
14
+
15
+
16
+ class DevideProfile(TypedDict):
17
+ quiet: DevicePreset
18
+ balanced: DevicePreset
19
+ performance: DevicePreset
20
+ # Turbo is custom with max tdp values
21
+ turbo: DevicePreset
22
+
23
+ platform_profile_map: dict[str, int]
24
+ ppd_balanced_min: int
25
+ ppd_performance_min: int
26
+
27
+ alib: dict[str, AlibParams]
28
+ dev: dict[str, DeviceParams]
29
+
30
+
3
31
  PLATFORM_PROFILE_MAP = [
4
32
  ("low-power", 0),
5
33
  ("quiet", 0),
@@ -29,6 +57,7 @@ ALIB_PARAMS_5040: dict[str, AlibParams] = ALIB_PARAMS
29
57
  ALIB_PARAMS_7040: dict[str, AlibParams] = ALIB_PARAMS
30
58
  ALIB_PARAMS_6040: dict[str, AlibParams] = ALIB_PARAMS
31
59
  ALIB_PARAMS_8040: dict[str, AlibParams] = ALIB_PARAMS
60
+ ALIB_PARAMS_HX370: dict[str, AlibParams] = ALIB_PARAMS
32
61
 
33
62
  DEV_PARAMS_30W: dict[str, DeviceParams] = {
34
63
  "stapm_limit": D(0, 4, 15, 30, 40),
@@ -69,12 +98,14 @@ DEV_PARAMS_5000: dict[str, DeviceParams] = DEV_PARAMS_25W
69
98
  DEV_PARAMS_6000: dict[str, DeviceParams] = DEV_PARAMS_30W
70
99
  DEV_PARAMS_7040: dict[str, DeviceParams] = DEV_PARAMS_30W
71
100
  DEV_PARAMS_8040: dict[str, DeviceParams] = DEV_PARAMS_30W
101
+ DEV_PARAMS_HX370: dict[str, DeviceParams] = DEV_PARAMS_30W
72
102
  DEV_PARAMS_LEGO = DEV_PARAMS_30W
73
103
 
74
104
  DEV_DATA: dict[str, tuple[dict[str, DeviceParams], dict[str, AlibParams], bool]] = {
75
105
  "NEO-01": (DEV_PARAMS_28W, ALIB_PARAMS_7040, False),
76
106
  "V3": (DEV_PARAMS_28W, ALIB_PARAMS_8040, False),
77
107
  "83E1": (DEV_PARAMS_LEGO, ALIB_PARAMS_7040, False),
108
+ "ONEXPLAYER F1Pro": (DEV_PARAMS_HX370, ALIB_PARAMS_HX370, False),
78
109
  }
79
110
 
80
111
  CPU_DATA: dict[str, tuple[dict[str, DeviceParams], dict[str, AlibParams]]] = {
@@ -88,4 +119,5 @@ CPU_DATA: dict[str, tuple[dict[str, DeviceParams], dict[str, AlibParams]]] = {
88
119
  "AMD Ryzen 7 7840U": (DEV_PARAMS_7040, ALIB_PARAMS_7040),
89
120
  "AMD Ryzen 7 8840U": (DEV_PARAMS_8040, ALIB_PARAMS_8040),
90
121
  # AMD Athlon Silver 3050e (Win600, will it support tdp?)
122
+ "AMD Ryzen AI 9 HX 370": (DEV_PARAMS_HX370, ALIB_PARAMS_HX370),
91
123
  }
@@ -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
@@ -160,20 +160,20 @@ class AmdGPUPlugin(HHDPlugin):
160
160
  }
161
161
 
162
162
  self.initialized = True
163
-
163
+
164
164
  # Initialize frequency settings
165
- 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"][
166
169
  "children"
167
- ]["gpu_freq"]["modes"]["manual"]["children"]["frequency"]
168
- upper_freq = sets["enabled"]["children"]["mode"]["modes"]["manual"][
170
+ ]["frequency"]
171
+ min_freq = sets["enabled"]["children"]["gpu_freq"]["modes"]["range"][
169
172
  "children"
170
- ]["gpu_freq"]["modes"]["upper"]["children"]["frequency"]
171
- min_freq = sets["enabled"]["children"]["mode"]["modes"]["manual"]["children"][
172
- "gpu_freq"
173
- ]["modes"]["range"]["children"]["min"]
174
- max_freq = sets["enabled"]["children"]["mode"]["modes"]["manual"]["children"][
175
- "gpu_freq"
176
- ]["modes"]["range"]["children"]["max"]
173
+ ]["min"]
174
+ max_freq = sets["enabled"]["children"]["gpu_freq"]["modes"]["range"][
175
+ "children"
176
+ ]["max"]
177
177
 
178
178
  manual_freq["default"] = ((status.freq_min + status.freq_max) // 200) * 100
179
179
  upper_freq["default"] = status.freq_max
@@ -287,6 +287,10 @@ class AmdGPUPlugin(HHDPlugin):
287
287
  if new_ppd:
288
288
  try:
289
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()
290
294
  except Exception as e:
291
295
  logger.error(f"Failed to open PPD server:\n{e}")
292
296
  self.close_ppd()
@@ -307,7 +311,6 @@ class AmdGPUPlugin(HHDPlugin):
307
311
  try:
308
312
  match self.target:
309
313
  case "balanced":
310
- set_gpu_auto()
311
314
  if self.supports_epp:
312
315
  set_powersave_governor()
313
316
  set_epp_mode("balance_power")
@@ -315,7 +318,6 @@ class AmdGPUPlugin(HHDPlugin):
315
318
  set_cpu_boost(True)
316
319
  set_frequency_scaling(nonlinear=False)
317
320
  case "performance":
318
- set_gpu_auto()
319
321
  if self.supports_epp:
320
322
  set_powersave_governor()
321
323
  set_epp_mode("balance_power")
@@ -323,7 +325,6 @@ class AmdGPUPlugin(HHDPlugin):
323
325
  set_cpu_boost(True)
324
326
  set_frequency_scaling(nonlinear=True)
325
327
  case _: # power
326
- set_gpu_auto()
327
328
  if self.supports_epp:
328
329
  set_powersave_governor()
329
330
  set_epp_mode("power")
@@ -336,52 +337,11 @@ class AmdGPUPlugin(HHDPlugin):
336
337
  # Unless it is set manually, use the default scheduler.
337
338
  self.close_sched()
338
339
  self.old_sched = None
339
- self.old_freq = None
340
340
  self.old_boost = None
341
341
  self.old_epp = None
342
342
  self.old_min_freq = None
343
343
  else:
344
344
  self.old_target = None
345
- new_gpu = conf["tdp.amd_energy.mode.manual.gpu_freq.mode"].to(str)
346
- match new_gpu:
347
- case "manual":
348
- f = conf["tdp.amd_energy.mode.manual.gpu_freq.manual.frequency"].to(
349
- int
350
- )
351
- new_freq = (f, f)
352
- case "upper":
353
- f = conf["tdp.amd_energy.mode.manual.gpu_freq.upper.frequency"].to(
354
- int
355
- )
356
- new_freq = (self.min_freq or f, f)
357
- case "range":
358
- min_f = conf["tdp.amd_energy.mode.manual.gpu_freq.range.min"].to(
359
- int
360
- )
361
- max_f = conf["tdp.amd_energy.mode.manual.gpu_freq.range.max"].to(
362
- int
363
- )
364
- if max_f < min_f:
365
- max_f = min_f
366
- conf["tdp.amd_energy.mode.manual.gpu_freq.range.max"] = min_f
367
- new_freq = (min_f, max_f)
368
- case _:
369
- new_freq = None
370
-
371
- if new_freq != self.old_freq:
372
- self.old_freq = new_freq
373
- self.queue_gpu = curr + APPLY_DELAY
374
-
375
- if self.queue_gpu is not None and curr >= self.queue_gpu:
376
- self.queue_gpu = None
377
- try:
378
- if new_freq:
379
- set_gpu_manual(*new_freq)
380
- else:
381
- set_gpu_auto()
382
- except Exception as e:
383
- logger.error(f"Failed to set GPU mode:\n{e}")
384
-
385
345
  if self.supports_boost:
386
346
  new_boost = conf["tdp.amd_energy.mode.manual.cpu_boost"].to(bool)
387
347
  if new_boost != self.old_boost:
@@ -441,6 +401,39 @@ class AmdGPUPlugin(HHDPlugin):
441
401
  stdout=subprocess.DEVNULL,
442
402
  )
443
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
+
444
437
  def close_ppd(self):
445
438
  if self.proc is not None:
446
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:
@@ -225,7 +225,7 @@ class AsusDriverPlugin(HHDPlugin):
225
225
  lim != self.old_conf["charge_limit"].to(str)
226
226
  ):
227
227
  self.queue_charge_limit = curr + APPLY_DELAY
228
-
228
+
229
229
  if self.queue_charge_limit and self.queue_charge_limit < curr:
230
230
  self.queue_charge_limit = None
231
231
  match lim:
@@ -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.
@@ -282,6 +281,9 @@ class AsusDriverPlugin(HHDPlugin):
282
281
  self.sys_tdp = False
283
282
  tdp_reset = True
284
283
 
284
+ if mode is not None and self.startup:
285
+ tdp_reset = True
286
+
285
287
  # Handle EPP for presets
286
288
  if tdp_reset and mode != "custom":
287
289
  match mode:
@@ -64,7 +64,7 @@ class GeneralPowerPlugin(HHDPlugin):
64
64
  logger.info("Unmasking TuneD in the case it was masked.")
65
65
  os.system('systemctl unmask tuned')
66
66
  subprocess.run(
67
- [tuned],
67
+ [tuned,'active'],
68
68
  check=True,
69
69
  stdin=subprocess.DEVNULL,
70
70
  stdout=subprocess.DEVNULL,
@@ -75,7 +75,7 @@ class GeneralPowerPlugin(HHDPlugin):
75
75
  logger.warning(f"tuned-adm returned with error:\n{e}")
76
76
 
77
77
 
78
- if not self.ppd_supported:
78
+ if not self.ppd_supported and not self.tuned_supported:
79
79
  del sets["children"]["profile"]
80
80
 
81
81
  # SchedExt
@@ -188,7 +188,7 @@ class GeneralPowerPlugin(HHDPlugin):
188
188
  conf["tdp.general.profile"] = self.target
189
189
  except Exception as e:
190
190
  self.tuned_supported = False
191
- logger.warning(f"powerprofilectl returned with error:\n{e}")
191
+ logger.warning(f"tuned-adm returned with error:\n{e}")
192
192
  self.tuned_supported = False
193
193
 
194
194
  # Handle sched
@@ -44,6 +44,7 @@ class LenovoDriverPlugin(HHDPlugin):
44
44
  self.old_conf = None
45
45
  self.sys_tdp = False
46
46
  self.fan_curve_set = False
47
+ self.notify_tdp = False
47
48
 
48
49
  bios_version = get_bios_version()
49
50
  logger.info(f"Lenovo BIOS version: {bios_version}")
@@ -326,6 +327,12 @@ class LenovoDriverPlugin(HHDPlugin):
326
327
  # Save current config
327
328
  self.old_conf = conf["tdp.lenovo"]
328
329
 
330
+ if self.notify_tdp:
331
+ self.notify_tdp = False
332
+ print(new_mode)
333
+ if conf.get("tdp.lenovo.tdp_rgb", False):
334
+ self.emit({"type": "special", "event": f"tdp_cycle_{new_mode}"}) # type: ignore
335
+
329
336
  if self.startup:
330
337
  self.startup = False
331
338
 
@@ -333,7 +340,7 @@ class LenovoDriverPlugin(HHDPlugin):
333
340
  for ev in events:
334
341
  if ev["type"] == "tdp":
335
342
  self.new_tdp = ev["tdp"]
336
- self.sys_tdp = ev['tdp'] is not None
343
+ self.sys_tdp = ev["tdp"] is not None
337
344
  if ev["type"] == "ppd":
338
345
  match ev["status"]:
339
346
  case "power":
@@ -342,6 +349,9 @@ class LenovoDriverPlugin(HHDPlugin):
342
349
  self.new_mode = "balanced"
343
350
  case "performance":
344
351
  self.new_mode = "performance"
352
+ print(ev)
353
+ if ev["type"] == "acpi" and ev.get("event", None) == "tdp":
354
+ self.notify_tdp = True
345
355
 
346
356
  def close(self):
347
357
  pass
@@ -143,6 +143,12 @@ children:
143
143
  hint: >-
144
144
  Reset to the original fan curve provided by Lenovo in BIOS V28.
145
145
 
146
+ tdp_rgb:
147
+ type: bool
148
+ title: Show TDP changes with RGB
149
+ tags: [non-essential]
150
+ default: False
151
+
146
152
  charge_limit:
147
153
  tags: [advanced]
148
154
  type: bool
@@ -12,8 +12,13 @@ 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
+ ({"device_class": b"wmi", "bus_id": b"PNP0C14:01"}, "tdp"), # Legion go
19
+ # GPD Force hibernate thermal event
20
+ # , 'type': 0xf100, 'data': 0x0100 ignore these attrs for now...
21
+ ({"device_class": b"thermal_zone", "bus_id": b"LNXTHERM:00"}, "hibernate-thermal"),
17
22
  ]
18
23
 
19
24
  GUARD_DELAY = 0.5
@@ -43,10 +48,10 @@ def loop_process_events(emit: Emitter, should_exit: TEvent):
43
48
  break
44
49
 
45
50
  if matches:
46
- if etype != "battery":
47
- emit({"type": "acpi", "event": etype}) # type: ignore
51
+ if etype not in ("battery", "powerbutton"):
52
+ emit({"type": "acpi", "event": etype}) # type: ignore
48
53
  found = True
49
54
  break
50
55
 
51
56
  if not found:
52
- logger.info(f"Unknown ACPI event: {ev}")
57
+ logger.info(f"ACPI event: {ev}")
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: adjustor
3
- Version: 3.5.2
3
+ Version: 3.5.4
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