Skip to content

Fixes LOQ model fancurves#347

Draft
qquique wants to merge 5 commits intojohnfanv2:mainfrom
qquique:loq-fancurves
Draft

Fixes LOQ model fancurves#347
qquique wants to merge 5 commits intojohnfanv2:mainfrom
qquique:loq-fancurves

Conversation

@qquique
Copy link

@qquique qquique commented Aug 7, 2025

Hi me again :), tldr of this PR :

  • Fix for fancurves for LOQ Model (Tested on LZCN model), reads and sets values when in Custom Mode for CPU Fan (min temp, max temp, rpm), GPU Fan (min temp, max temp, rpm), IC (min temp, max temp), and works!.
  • WMI methods to read/write current Fan Indices (read details)

Now the details :

When using the Lenovo Vantage Application when setting the custom mode and modifying its levels (points) :

  • Only changes position indices in an array that has the same size of the Fancurve.
  • Each element value of that array points to the element that contains the speed in the current Fancurve selected.
  • The Fancurve is selected depending on the thermal mode (quiet/balanced/performance/custom), the dGPU (by vendor id) and a PRID (value in DSDT) that I think is the mode of the laptop (hybrid, iGPU, dGPU, and another that wakes up the dGPU when used according to the Vantage app)

It can be queried with the following powershell script :

$classInstance = Get-CimInstance -Namespace root/wmi -ClassName LENOVO_FAN_METHOD
$classInstance | Invoke-CimMethod -MethodName Fan_Get_Table -Arguments @{FanID=0;SensorID=0}
FanTable        : {2, 2, 3, 4, 5, 6, 7, 8 ,9 ,9}
FanTableSize    : 10
SensorTable     : {2, 2, 3, 4, 5, 6 ,7, 8, 9 ,9}
SensorTableSize : 10
ReturnValue     : True

In this output the value 2 is repeated because I changed the first point of the fancurve to the same level as the second point (1400, 1400). This method is implemented now in function wmi_read_fancurve_idx. FanId and SensorId parameters doesnt make any difference.

There are many Fan Curves defined and can be queried with the following powershell :

$classInstance = Get-CimInstance -Namespace root/wmi -ClassName LENOVO_FAN_TABLE_DATA
$classInstance
Active                      : True
CurrentFanMaxSpeed          : 3900
CurrentFanMinSpeed          : 0
DesignMaxFanSpeedNumber     : 10
EndOnlyUpwardAdjustNumber   : 10
Fan_Id                      : 1
FanSpeedStep                : 100
FanTable_Data               : {1400, 1700, 1900, 2200, 2500, 2700, 2900, 3100, 3500, 3900}
FanTable_Len                : 10
InstanceName                : ACPI\PNP0C14\GMZN_0
MaxSensorTemperature        : 100
MinSensorTemperature        : 0
Mode                        : 3
Reserved                    : 0
Sensor_ID                   : 4
SensorTable_Data            : {60, 64, 68, 72, 76, 80, 84, 93, 99, 100}
SensorTable_Len             : 10
SensorTemperatureStep       : 1
StartOnlyUpwardAdjustNumber : 0
PSComputerName              :
...

This output is just one example, In Windows it shows 15 I assume it gets only the Fan Table specific for the laptop model, In the ACPI DSDT there are 4 Tables of 15 each one. Also the temps here are not used there are other tables with temps selected also by the thermal mode, dgpu and PRID logic.

We dont care too much about the Fan Curves predefined, maybe as a reference, however Vantage app doesnt use the 10th one that has the highest temp 120 for the IC sensor (the index doesnt start at 0 as C arrays it starts at 1), you can see in the output of Get_Fan_Table uses index 9 for the 10th position. This can be a warning when changing those values.

The WMI call that the Vantage App uses LENOVO_FAN_METHOD.Fan_Set_Table related to the ACPI/DSDT SFAN Method only works with the indices mapping, doesnt set directly RPM's it obtains them from its internal tables. This method is implemented in wmi_write_fancurve_idx.

The function wmi_read/write_fancurve_idx can be used as new feature on the variables exposed by the driver in hwmon or acpi/firmware (maybe) and also on the python client, if someone wants to develop it.

Besides the modes Quiet (1), Balanced (2), Performance (3) and Custom (255) there is a Extreme Mode (224).

So following the DSDT code as a guide of how to obtain the rpm's, I replaced the old ec_read/write_fancurve_loq to reset the index mapping to an ordered list (1-10) that represent the speed RPM's that are being passed as arguments (via cmd line, legion.py, legion_gui.py), sets the correct values on the offsets on the 3 groups (cpu, gpu, ic), and activates a flag to tell the device/ec to use the new values.

Note:

  • When changing the Power Profile to quiet/normal/performance with Fn+Q or echo "balanced" > /sys/firmware/acpi/platform_profile, the values are reset to the bios/dsdt defaults and the 3 sets of custom values are not changed, so when querying cat /sys/kernel/debug/legion/fancurve it will appear the custom values but not the current ones, I think that when changing powermodes also we have to call wmi_write_fancurve_idx so we reset the indices, and the acpi/dsdt set the default speeds and temps of the current powermode.

Test Output of Fancurve (Custom Mode, modes set in Vantage App):

Fan curve points size: 10
u(speed_of_unit)|speed1[u]|speed2[u]|speed1[pwm]|speed2[pwm]|acceleration|deceleration|cpu_min_temp|cpu_max_temp|gpu_min_temp|gpu_max_temp|ic_min_temp|ic_max_temp
3        14      14      35      35      0       0       0       70      0       48      0       42
3        14      14      35      35      0       0       60      73      45      55      38      63
3        17      17      43      43      0       0       70      77      57      60      59      70
3        19      19      48      48      0       0       75      80      59      65      60      72
3        22      22      56      56      0       0       78      84      62      77      60      72
3        25      25      63      63      0       0       80      84      70      82      60      72
3        29      29      73      73      0       0       82      92      80      87      60      78
3        31      31      79      79      0       0       87      95      80      87      76      90
3        35      35      89      89      0       0       92      99      80      87      89      105
3        35      35      89      89      0       0       94      100     85      100     90      120

Changing the CPU and GPU fans

# cpu
export HWMONPATH=/sys/class/hwmon/hwmon5
echo 65 >$HWMONPATH/pwm1_auto_point2_temp_hyst
echo 72 >$HWMONPATH/pwm1_auto_point2_temp
echo 35 >$HWMONPATH/pwm1_auto_point2_pwm

# gpu
echo 46 >$HWMONPATH/pwm2_auto_point2_temp_hyst
echo 56 >$HWMONPATH/pwm2_auto_point2_temp
echo 35 >$HWMONPATH/pwm2_auto_point2_pwm

output of fancurve

u(speed_of_unit)|speed1[u]|speed2[u]|speed1[pwm]|speed2[pwm]|acceleration|deceleration|cpu_min_temp|cpu_max_temp|gpu_min_temp|gpu_max_temp|ic_min_temp|ic_max_temp
3        14      14      35      35      0       0       0       60      0       45      0       42
3        13      13      33      33      0       0       65      72      46      56      38      63
3        17      17      43      43      0       0       70      77      57      60      59      70
3        19      19      48      48      0       0       75      80      59      65      60      72
3        22      22      56      56      0       0       78      84      62      77      60      72
3        25      25      63      63      0       0       80      84      70      82      60      72
3        29      29      73      73      0       0       82      92      80      87      60      78
3        31      31      79      79      0       0       87      95      80      87      76      90
3        35      35      89      89      0       0       92      99      80      87      89      105
3        35      35      89      89      0       0       94      100     85      100     90      120

  • because of the issue with pwm rounding/truncation: 35 is converted to 33 🤷‍♂️

@qquique
Copy link
Author

qquique commented Oct 10, 2025

Hi, with the data from the comments in #352 , I added a commit to simplify the use for the models LZCN and NZCN :

  • NZCN and LZCN have a common ec memory area with just info for the cpu min temp, max temp, and rpm duplicated.
  • Modifying via ec does nothing on that area, so it stays just for read.
  • There is no WMI function between the two models to set the values of cpu, gpu ic individually for each point so far that I read both models dsl's, LZCN can modify via ec in an specific ec memory area as my previous commit but I'm disregarding that commit to simplify the use.
  • There is a common WMI function for LZCN and NZCN to trigger the selection of a fancurve from the bios/acpi data so I exposed it in the hwmon path as auto_points_defaults. When in balanced-performance mode (0xff, 255) a echo 0 > auto_points_defaults will trigger the bios selection of fancurve rpms and temps according to its internal tables for the powermode.

Example:

  • press Fn+Q now in Normal Mode (White)
  • echo 0 > auto_points_defaults does nothing
u(speed_of_unit)|speed1[u]|speed2[u]|speed1[pwm]|speed2[pwm]|acceleration|deceleration|cpu_min_temp|cpu_max_temp|gpu_min_temp|gpu_max_temp|ic_min_temp|ic_max_temp
3        0       0       0       0       0       0       0       84      0       0       0       0
3        35      35      89      89      0       0       60      84      0       0       0       0
3        35      35      89      89      0       0       80      92      0       0       0       0
3        35      35      89      89      0       0       80      92      0       0       0       0
3        35      35      89      89      0       0       80      92      0       0       0       0
3        35      35      89      89      0       0       80      92      0       0       0       0
3        35      35      89      89      0       0       80      92      0       0       0       0
3        35      35      89      89      0       0       80      92      0       0       0       0
3        35      35      89      89      0       0       82      99      0       0       0       0
3        35      35      89      89      0       0       94      100     0       0       0       0
  • change to balanced-performance
  • echo 0 > auto_points_defaults , can be any value, changes to one of the fancurve of the fan tables that it has in the bios/acpi
u(speed_of_unit)|speed1[u]|speed2[u]|speed1[pwm]|speed2[pwm]|acceleration|deceleration|cpu_min_temp|cpu_max_temp|gpu_min_temp|gpu_max_temp|ic_min_temp|ic_max_temp
3        14      14      35      35      0       0       0       60      0       0       0       0
3        17      17      43      43      0       0       58      70      0       0       0       0
3        19      19      48      48      0       0       70      73      0       0       0       0
3        22      22      56      56      0       0       70      77      0       0       0       0
3        25      25      63      63      0       0       70      79      0       0       0       0
3        27      27      68      68      0       0       70      82      0       0       0       0
3        29      29      73      73      0       0       70      85      0       0       0       0
3        31      31      79      79      0       0       70      92      0       0       0       0
3        35      35      89      89      0       0       70      97      0       0       0       0
3        35      35      89      89      0       0       94      100     0       0       0       0
$ sensors legion_hwmon-isa-0000
legion_hwmon-isa-0000
Adapter: ISA adapter
Fan 1:           1400 RPM  (max = 10000 RPM)
Fan 2:           1400 RPM  (max = 10000 RPM)
CPU Temperature:  +42.0°C
GPU Temperature:  +35.0°C
IC Temperature:    +0.0°C
pwm1:                 N/A  (mode = pwm)
Edit : There is a WMI Method that can return the selected fan curve from the internal fan tables, but I cant make it work, maybe someone knows how, keep getting ACPI Error 4100:

fwts

\_SB_.GZFD._WDG (8 of 34)
  GUID: 87FB2A6D-D802-48E7-9208-4576C5F5C8D8
  WMI Block:
    Flags          : 0x01 (Expensive)
    Object ID      : A7
    Instance       : 0x0f
PASSED: Test 1, 87FB2A6D-D802-48E7-9208-4576C5F5C8D8 has associated query method
\_SB_.GZFD.WQA7
PASSED: Test 1, 87FB2A6D-D802-48E7-9208-4576C5F5C8D8 has more than zero
instances

dsl

            Method (WQA7, 1, NotSerialized)
            {
                Return (SFTW (Arg0))
            }

            Method (SFTW, 1, NotSerialized)
            {
                If (((PRID == Zero) || (PRID == One)))
                {
                    If ((DGID == 0x02))
                    {
                        CopyObject (FNT2, Local1)
                    }
                    Else
                    {
                        CopyObject (FNT3, Local1)
                    }
                }
                ElseIf ((PRID == 0x03))
                {
                    CopyObject (FNT1, Local1)
                }
                Else
                {
                    CopyObject (FNT0, Local1)
                }

                Local0 = DerefOf (Local1 [ToInteger (Arg0)])
                FNIM = DerefOf (Local0 [Zero])
                FNID = DerefOf (Local0 [One])
                FNLE = DerefOf (Local0 [0x02])
                FNS0 = DerefOf (Local0 [0x03])
                FNS1 = DerefOf (Local0 [0x04])
                FNS2 = DerefOf (Local0 [0x05])
                FNS3 = DerefOf (Local0 [0x06])
                FNS4 = DerefOf (Local0 [0x07])
                FNS5 = DerefOf (Local0 [0x08])
                FNS6 = DerefOf (Local0 [0x09])
                FNS7 = DerefOf (Local0 [0x0A])
                FNS8 = DerefOf (Local0 [0x0B])
                FNS9 = DerefOf (Local0 [0x0C])
                SEID = DerefOf (Local0 [0x0D])
                STLE = DerefOf (Local0 [0x0E])
                SST0 = DerefOf (Local0 [0x0F])
                SST1 = DerefOf (Local0 [0x10])
                SST2 = DerefOf (Local0 [0x11])
                SST3 = DerefOf (Local0 [0x12])
                SST4 = DerefOf (Local0 [0x13])
                SST5 = DerefOf (Local0 [0x14])
                SST6 = DerefOf (Local0 [0x15])
                SST7 = DerefOf (Local0 [0x16])
                SST8 = DerefOf (Local0 [0x17])
                SST9 = DerefOf (Local0 [0x18])
                SOU1 = DerefOf (Local0 [0x19])
                SOU2 = DerefOf (Local0 [0x1A])
                CFMS = DerefOf (Local0 [0x1B])
                SOU3 = DerefOf (Local0 [0x1C])
                SOU4 = DerefOf (Local0 [0x1D])
                CFIS = DerefOf (Local0 [0x1E])
                FSSP = DerefOf (Local0 [0x1F])
                MST1 = DerefOf (Local0 [0x20])
                MST2 = DerefOf (Local0 [0x21])
                MSTP = DerefOf (Local0 [0x22])
                Return (FACT) /* \_SB_.GZFD.FACT */
            }
struct WMIFanTableReadLoq { // FACT table
	u16 FNIM;
	u16 FNID;
	u32 FNLE;
	u16 FNS0;
	u16 FNS1;
	u16 FNS2;
	u16 FNS3;
	u16 FNS4;
	u16 FNS5;
	u16 FNS6;
	u16 FNS7;
	u16 FNS8;
	u16 FNS9;
	u32 SEID;
	u32 STLE;
	u16 SST0;
	u16 SST1;
	u16 SST2;
	u16 SST3;
	u16 SST4;
	u16 SST5;
	u16 SST6;
	u16 SST7;
	u16 SST8;
	u16 SST9;
	u8  SOU1;
	u8  SOU2;
	u16 CFMS;
	u8  SOU3;
	u8  SOU4;
	u16 CFIS;
	u16 FSSP;
	u16 MST1;
	u16 MST2;
	u16 MSTP;
} __packed;

static ssize_t wmi_read_fancurve_idx(const struct model_config *model,
					struct fancurve *fancurve)
{
	u8 buffer[10];
	int err;

	u8 input[2] = { 0x09, 0x0 };
	struct acpi_buffer in_buf = {
		.length = sizeof(input),
		.pointer = input,
	};

	err = wmi_exec_ints("87FB2A6D-D802-48E7-9208-4576C5F5C8D8", 0,
					0xa7, &in_buf, buffer, sizeof(buffer));

	if (!err) {
		struct WMIFanTableReadLoq *fantable =
			(struct WMIFanTableReadLoq *)&buffer[0];

		fancurve->current_point_i = 0;
		fancurve->size = fantable->FNLE;
		fancurve->fan_speed_unit = FAN_SPEED_UNIT_RPM_HUNDRED;
		fancurve->points[0].speed1 = fantable->FNS0;
		fancurve->points[1].speed1 = fantable->FNS1;
		fancurve->points[2].speed1 = fantable->FNS2;
		fancurve->points[3].speed1 = fantable->FNS3;
		fancurve->points[4].speed1 = fantable->FNS4;
		fancurve->points[5].speed1 = fantable->FNS5;
		fancurve->points[6].speed1 = fantable->FNS6;
		fancurve->points[7].speed1 = fantable->FNS7;
		fancurve->points[8].speed1 = fantable->FNS8;
		fancurve->points[9].speed1 = fantable->FNS9;

		print_hex_dump(KERN_DEBUG, "legion_laptop fan table idx wmi buffer",
		       DUMP_PREFIX_ADDRESS, 16, 1, buffer, sizeof(buffer),
		       true);
	}
	return err;
}``
</details>

@qquique
Copy link
Author

qquique commented Oct 11, 2025

In Vantage App when the Custom Mode is selected, there is a drop down control to the right that says "Resets" and gives the options of powermodes : "Quiet, Balanced, Performance". It means that when in Custom Mode/balanced-performance 255 it can use any of the default fancurves from "Quiet, Balanced, Performance" modes. This last commit does that.

  • echo "balanced-performance" > platform_profile
  • echo 1 > auto_points_defaults : Quiet
u(speed_of_unit)|speed1[u]|speed2[u]|speed1[pwm]|speed2[pwm]|acceleration|deceleration|cpu_min_temp|cpu_max_temp|gpu_min_temp|gpu_max_temp|ic_min_temp|ic_max_temp
3        0       0       0       0       0       0       0       70      0       0       0       0
3        14      14      35      35      0       0       68      86      0       0       0       0
3        17      17      43      43      0       0       84      92      0       0       0       0
3        19      19      48      48      0       0       84      92      0       0       0       0
3        22      22      56      56      0       0       84      92      0       0       0       0
3        25      25      63      63      0       0       84      92      0       0       0       0
3        29      29      73      73      0       0       84      92      0       0       0       0
3        31      31      79      79      0       0       84      92      0       0       0       0
3        35      35      89      89      0       0       88      99      0       0       0       0
3        35      35      89      89      0       0       94      100     0       0       0       0
  • echo 2 > auto_points_defaults : Balanced
u(speed_of_unit)|speed1[u]|speed2[u]|speed1[pwm]|speed2[pwm]|acceleration|deceleration|cpu_min_temp|cpu_max_temp|gpu_min_temp|gpu_max_temp|ic_min_temp|ic_max_temp
3        0       0       0       0       0       0       0       70      0       0       0       0
3        14      14      35      35      0       0       60      73      0       0       0       0
3        17      17      43      43      0       0       70      77      0       0       0       0
3        19      19      48      48      0       0       75      80      0       0       0       0
3        22      22      56      56      0       0       78      84      0       0       0       0
3        25      25      63      63      0       0       80      84      0       0       0       0
3        29      29      73      73      0       0       82      92      0       0       0       0
3        31      31      79      79      0       0       87      95      0       0       0       0
3        35      35      89      89      0       0       92      99      0       0       0       0
3        35      35      89      89      0       0       94      100     0       0       0       0
  • echo 3 > auto_points_defaults : Performance
u(speed_of_unit)|speed1[u]|speed2[u]|speed1[pwm]|speed2[pwm]|acceleration|deceleration|cpu_min_temp|cpu_max_temp|gpu_min_temp|gpu_max_temp|ic_min_temp|ic_max_temp
3        14      14      35      35      0       0       0       60      0       0       0       0
3        17      17      43      43      0       0       50      64      0       0       0       0
3        19      19      48      48      0       0       56      68      0       0       0       0
3        22      22      56      56      0       0       60      72      0       0       0       0
3        25      25      63      63      0       0       65      76      0       0       0       0
3        27      27      68      68      0       0       70      80      0       0       0       0
3        29      29      73      73      0       0       75      84      0       0       0       0
3        31      31      79      79      0       0       80      93      0       0       0       0
3        35      35      89      89      0       0       87      99      0       0       0       0
3        35      35      89      89      0       0       94      100     0       0       0       0
  • echo 255 > auto_points_defaults : Custom
u(speed_of_unit)|speed1[u]|speed2[u]|speed1[pwm]|speed2[pwm]|acceleration|deceleration|cpu_min_temp|cpu_max_temp|gpu_min_temp|gpu_max_temp|ic_min_temp|ic_max_temp
3        14      14      35      35      0       0       0       60      0       0       0       0
3        17      17      43      43      0       0       58      70      0       0       0       0
3        19      19      48      48      0       0       70      73      0       0       0       0
3        22      22      56      56      0       0       70      77      0       0       0       0
3        25      25      63      63      0       0       70      79      0       0       0       0
3        27      27      68      68      0       0       70      82      0       0       0       0
3        29      29      73      73      0       0       70      85      0       0       0       0
3        31      31      79      79      0       0       70      92      0       0       0       0
3        35      35      89      89      0       0       70      97      0       0       0       0
3        35      35      89      89      0       0       94      100     0       0       0       0
  • echo 224 > auto_points_defaults : Extreme
u(speed_of_unit)|speed1[u]|speed2[u]|speed1[pwm]|speed2[pwm]|acceleration|deceleration|cpu_min_temp|cpu_max_temp|gpu_min_temp|gpu_max_temp|ic_min_temp|ic_max_temp
3        14      14      35      35      0       0       0       60      0       0       0       0
3        17      17      43      43      0       0       50      64      0       0       0       0
3        19      19      48      48      0       0       56      68      0       0       0       0
3        22      22      56      56      0       0       60      72      0       0       0       0
3        25      25      63      63      0       0       65      76      0       0       0       0
3        27      27      68      68      0       0       70      80      0       0       0       0
3        29      29      73      73      0       0       75      84      0       0       0       0
3        31      31      79      79      0       0       80      93      0       0       0       0
3        35      35      89      89      0       0       87      99      0       0       0       0
3        35      35      89      89      0       0       94      100     0       0       0       0

@alfrix
Copy link

alfrix commented Dec 10, 2025

This is needed to make the 15IRX10 model to work, could be added to the whitelist after this is merged

@qquique
Copy link
Author

qquique commented Dec 12, 2025

This is needed to make the 15IRX10 model to work, could be added to the whitelist after this is merged

Add an issue with the info required / model / dmidecode as others, and also add the info that I asked on #352 to see If this PR can work for your model.

- Offsets for reading CPU,GPU groups LZCN, NZCN, R3CN models.
- Adds function wmi_write_fancurve_defaults to call WMI method SFAN
  to trigger the selection of fan curve.
- Expose function wmi_write_fancurve_defaults as an entry in hwmon
  path 'auto_points_defaults' to select default fancurve from other
  powermodes when in custom mode. Only for tested models.
- Adds compatibility for R3CN model.
- Use of last range of fan curve rpm's. Vantage App limited it,
  LegionSpace allows it.
@qquique qquique changed the title Fixes LOQ model fancurves, new WMI functions Fixes LOQ model fancurves Dec 15, 2025
@qquique
Copy link
Author

qquique commented Dec 15, 2025

I squashed all the commits on one, @alfrix , @VoSed can you please test.

@VoSed
Copy link

VoSed commented Dec 15, 2025

Works as expected

@alfrix
Copy link

alfrix commented Dec 16, 2025

missing some fixes

diff --git a/kernel_module/legion-laptop.c b/kernel_module/legion-laptop.c
index f57b8af..683f219 100644
--- a/kernel_module/legion-laptop.c
+++ b/kernel_module/legion-laptop.c
@@ -2454,7 +2454,7 @@ static ssize_t fancurve_print_seqfile(const struct fancurve *fancurve,
 		const struct fancurve_point *point = &fancurve->points[i];
 
 		fancurve_get_speed_pwm(fancurve, i, 0, &speed_pwm1);
-		fancurve_get_speed_pwm(fancurve, i, 0, &speed_pwm2);
+		fancurve_get_speed_pwm(fancurve, i, 1, &speed_pwm2);
 
 		seq_printf(
 			s,
diff --git a/python/legion_linux/legion_linux/legion.py b/python/legion_linux/legion_linux/legion.py
index df551cb..d63660a 100755
--- a/python/legion_linux/legion_linux/legion.py
+++ b/python/legion_linux/legion_linux/legion.py
@@ -872,10 +872,10 @@ class FanCurveIO(Feature):
         return self._read_file(file_path)
 
     def get_fan_1_speed_rpm(self, point_id):
-        return round(self.get_fan_1_speed_pwm(point_id)/255.0*self.get_fan_1_max_rpm(), ndigits=2)
+        return round(((self.get_fan_1_speed_pwm(point_id) * self.get_fan_1_max_rpm() + (100 * 255) - 1) // (100 * 255)) * 100 , ndigits=2)
 
     def get_fan_2_speed_rpm(self, point_id):
-        return round(self.get_fan_2_speed_pwm(point_id)/255.0*self.get_fan_2_max_rpm(), ndigits=2)
+        return round(((self.get_fan_2_speed_pwm(point_id) * self.get_fan_2_max_rpm() + (100 * 255) - 1) // (100 * 255)) * 100 , ndigits=2)
 
     def get_lower_cpu_temperature(self, point_id):
         point_id = self._validate_point_id(point_id)

also we are missing the GPU and IC temps

- User can query its current value, 0 if is not set.
- When powermode changes Fn+q or via sysfs is reset to 0.
This reads the FNT* table configuraton that is used by the SFAN Method,
provides values for:
- CPU RPM's, Max Temps
- GPU RPM's, Max Temps
- IC Max Temps
- others

The temps not necessarily match the ones used,
the tables that are used for temps are in FI* that
only SFAN can access. The RPM's should match.
@qquique
Copy link
Author

qquique commented Dec 17, 2025

  1. speed_pwm2 done, it is an error not related to the PR

  2. the rounding/truncation should be in another PR because has to be tested for owners of others laptop models, specially the older models, and should be talked/considered in the PR 261.

  3. the gpu and ic temps, as I mentioned in the PR and is also in your hexdump, the ec memory common area for these models LZCN, NZCN, R3CN only shows min and max CPU temps, and rpms. That being said, see point 4.

  4. I figured out how to query the fantable data, I added a new commit with that. The data from the FNT Table, as mentioned in my initial comment in this PR, the temps there, that are only max temps ,are not being used by the SFAN method, the ones used are located in others FI* tables. The temp values in the FNT table for a powermode can match some others not, in my model only the values of quiet mode doesnt match.

  5. The last commit allows to create fancurves as in the Vantage app.

# in Custom Powermode
$ echo "balanced-performance" > /sys/firmware/acpi/platform_profile
$ sudo cat /sys/kernel/debug/legion/fancurve | tail -n 30

# First part what it reads from EC, shows what is running currently after changing mode from Balanced mode.
# Second part what it read from the WMI call that extracts the configuration of the FNT table for the current powermode, that is "balanced-powermode" (255) 

u(speed_of_unit)|speed1[u]|speed2[u]|speed1[pwm]|speed2[pwm]|acceleration|deceleration|cpu_min_temp|cpu_max_temp|gpu_min_temp|gpu_max_temp|ic_min_temp|ic_max_temp
3	 14	 14	 35	 35	 0	 0	 0	 60	 0	 0	 0	 0
3	 39	 39	 99	 99	 0	 0	 58	 84	 0	 0	 0	 0
3	 39	 39	 99	 99	 0	 0	 70	 97	 0	 0	 0	 0
3	 39	 39	 99	 99	 0	 0	 70	 97	 0	 0	 0	 0
3	 39	 39	 99	 99	 0	 0	 70	 97	 0	 0	 0	 0
3	 39	 39	 99	 99	 0	 0	 70	 97	 0	 0	 0	 0
3	 39	 39	 99	 99	 0	 0	 70	 97	 0	 0	 0	 0
3	 39	 39	 99	 99	 0	 0	 70	 97	 0	 0	 0	 0
3	 39	 39	 99	 99	 0	 0	 70	 99	 0	 0	 0	 0
3	 39	 39	 99	 99	 0	 0	 94	 100	 0	 0	 0	 0
=====================
Current fan curve in hardware (WMI; might be empty)
Fan curve current point id: 0
Fan curve points size: 10
u(speed_of_unit)|speed1[u]|speed2[u]|speed1[pwm]|speed2[pwm]|acceleration|deceleration|cpu_min_temp|cpu_max_temp|gpu_min_temp|gpu_max_temp|ic_min_temp|ic_max_temp
3	 14	 14	 35	 35	 0	 0	 0	 60	 0	 48	 0	 42
3	 17	 17	 43	 43	 0	 0	 0	 70	 0	 60	 0	 63
3	 19	 19	 48	 48	 0	 0	 0	 73	 0	 66	 0	 74
3	 22	 22	 56	 56	 0	 0	 0	 77	 0	 70	 0	 76
3	 25	 25	 63	 63	 0	 0	 0	 79	 0	 74	 0	 78
3	 27	 27	 68	 68	 0	 0	 0	 82	 0	 77	 0	 80
3	 29	 29	 73	 73	 0	 0	 0	 85	 0	 81	 0	 90
3	 31	 31	 79	 79	 0	 0	 0	 92	 0	 85	 0	 95
3	 35	 35	 89	 89	 0	 0	 0	 97	 0	 89	 0	 105
3	 39	 39	 99	 99	 0	 0	 0	 100	 0	 100	 0	 120

# I want to use the rpms of quiet mode,

$ cat /path/to/hwmon/auto_points_default
0
# Is not set 
$ echo 1 >  /path/to/hwmon/auto_points_default
$ cat /path/to/hwmon/auto_points_default
1
# Now is set
# Checking if it was applied on EC .
$ sudo cat /sys/kernel/debug/legion/fancurve | tail -n 30
u(speed_of_unit)|speed1[u]|speed2[u]|speed1[pwm]|speed2[pwm]|acceleration|deceleration|cpu_min_temp|cpu_max_temp|gpu_min_temp|gpu_max_temp|ic_min_temp|ic_max_temp
3	 0	 0	 0	 0	 0	 0	 0	 70	 0	 0	 0	 0
3	 14	 14	 35	 35	 0	 0	 68	 86	 0	 0	 0	 0
3	 17	 17	 43	 43	 0	 0	 84	 92	 0	 0	 0	 0
3	 19	 19	 48	 48	 0	 0	 84	 92	 0	 0	 0	 0
3	 22	 22	 56	 56	 0	 0	 84	 92	 0	 0	 0	 0
3	 25	 25	 63	 63	 0	 0	 84	 92	 0	 0	 0	 0
3	 29	 29	 73	 73	 0	 0	 84	 92	 0	 0	 0	 0
3	 31	 31	 79	 79	 0	 0	 84	 92	 0	 0	 0	 0
3	 35	 35	 89	 89	 0	 0	 88	 99	 0	 0	 0	 0
3	 39	 39	 99	 99	 0	 0	 94	 100	 0	 0	 0	 0
=====================
Current fan curve in hardware (WMI; might be empty)
Fan curve current point id: 0
Fan curve points size: 10
u(speed_of_unit)|speed1[u]|speed2[u]|speed1[pwm]|speed2[pwm]|acceleration|deceleration|cpu_min_temp|cpu_max_temp|gpu_min_temp|gpu_max_temp|ic_min_temp|ic_max_temp
3	 0	 0	 0	 0	 0	 0	 0	 70	 0	 48	 0	 42
3	 14	 14	 35	 35	 0	 0	 0	 74	 0	 55	 0	 63
3	 17	 17	 43	 43	 0	 0	 0	 78	 0	 58	 0	 65
3	 19	 19	 48	 48	 0	 0	 0	 82	 0	 60	 0	 68
3	 22	 22	 56	 56	 0	 0	 0	 86	 0	 78	 0	 76
3	 25	 25	 63	 63	 0	 0	 0	 89	 0	 80	 0	 77
3	 29	 29	 73	 73	 0	 0	 0	 92	 0	 83	 0	 87
3	 31	 31	 79	 79	 0	 0	 0	 95	 0	 85	 0	 97
3	 35	 35	 89	 89	 0	 0	 0	 99	 0	 87	 0	 105
3	 39	 39	 99	 99	 0	 0	 0	 100	 0	 100	 0	 120

# looking at the sensor
legion_hwmon-isa-0000
Adapter: ISA adapter
Fan 1:              0 RPM  (max = 10000 RPM)
Fan 2:              0 RPM  (max = 10000 RPM)
CPU Temperature:  +44.0°C
GPU Temperature:  +35.0°C
IC Temperature:    +0.0°C
pwm1:                 N/A  (mode = pwm)

# Now I want  to apply a "curve" to those rpm's as in the Vantage App .
$ cat /path/to/hwmon/fancurve_indices
1,2,3,4,5,6,7,8,9,10

$ echo "2,2,3,3,3,4,5,7,8,9" > /path/to/hwmon/fancurve_indices
$ cat /path/to/hwmon/fancurve_indices
2,2,3,3,3,4,5,7,8,9

$ sudo cat /sys/kernel/debug/legion/fancurve | tail -n 30
# first part what is running in the ec
# second part, you still see the original config of the fan table 
# so you can choose again what indice to use, if you want to change the curve
u(speed_of_unit)|speed1[u]|speed2[u]|speed1[pwm]|speed2[pwm]|acceleration|deceleration|cpu_min_temp|cpu_max_temp|gpu_min_temp|gpu_max_temp|ic_min_temp|ic_max_temp
3	 14	 14	 35	 35	 0	 0	 0	 70	 0	 0	 0	 0
3	 14	 14	 35	 35	 0	 0	 68	 86	 0	 0	 0	 0
3	 17	 17	 43	 43	 0	 0	 84	 92	 0	 0	 0	 0
3	 17	 17	 43	 43	 0	 0	 84	 92	 0	 0	 0	 0
3	 17	 17	 43	 43	 0	 0	 84	 92	 0	 0	 0	 0
3	 19	 19	 48	 48	 0	 0	 84	 92	 0	 0	 0	 0
3	 22	 22	 56	 56	 0	 0	 84	 92	 0	 0	 0	 0
3	 29	 29	 73	 73	 0	 0	 84	 92	 0	 0	 0	 0
3	 31	 31	 79	 79	 0	 0	 88	 99	 0	 0	 0	 0
3	 35	 35	 89	 89	 0	 0	 94	 100	 0	 0	 0	 0
=====================
Current fan curve in hardware (WMI; might be empty)
Fan curve current point id: 0
Fan curve points size: 10
u(speed_of_unit)|speed1[u]|speed2[u]|speed1[pwm]|speed2[pwm]|acceleration|deceleration|cpu_min_temp|cpu_max_temp|gpu_min_temp|gpu_max_temp|ic_min_temp|ic_max_temp
3	 0	 0	 0	 0	 0	 0	 0	 70	 0	 48	 0	 42
3	 14	 14	 35	 35	 0	 0	 0	 74	 0	 55	 0	 63
3	 17	 17	 43	 43	 0	 0	 0	 78	 0	 58	 0	 65
3	 19	 19	 48	 48	 0	 0	 0	 82	 0	 60	 0	 68
3	 22	 22	 56	 56	 0	 0	 0	 86	 0	 78	 0	 76
3	 25	 25	 63	 63	 0	 0	 0	 89	 0	 80	 0	 77
3	 29	 29	 73	 73	 0	 0	 0	 92	 0	 83	 0	 87
3	 31	 31	 79	 79	 0	 0	 0	 95	 0	 85	 0	 97
3	 35	 35	 89	 89	 0	 0	 0	 99	 0	 87	 0	 105
3	 39	 39	 99	 99	 0	 0	 0	 100	 0	 100	 0	 120

# sensor
legion_hwmon-isa-0000
Adapter: ISA adapter
Fan 1:           1400 RPM  (max = 10000 RPM)
Fan 2:           1400 RPM  (max = 10000 RPM)
CPU Temperature:  +44.0°C
GPU Temperature:  +35.0°C
IC Temperature:    +0.0°C
pwm1:                 N/A  (mode = pwm)

Expose in hwmon sysfs fancurve_indices, returns the current indices of
the fan table that is used by the bios. Also allows to be modified when
in Custom powermode (255)

Read
cat /path/to/hwmon/fancurve_indices
1,2,3,4,5,6,7,8,9,10

Write
echo "2,2,3,4,5,6,7,8,9,10" > /path/to/hwmon/fancurve_indices

It will use the rpm's from indice 2 in indice 1, similar to
what the Vantage App does. Values allowed from 1 to 10.

It takes in consideration the /path/to/hwmon/auto_points_defaults.

example: If you are in custom mode "balanced-performance" (255), and you
set to use the default fancurve of quiet powermode :

echo 1 > /path/to/hwmon/auto_points_defaults
cat /path/to/hwmon/fancurve_indices
1,2,3,4,5,6,7,8,9,10
echo "1,2,3,4,4,4,7,8,9" > /path/to/hwmon/fancurve_indices

will create a curve with the rpm's of quiet powermode.
@qquique qquique marked this pull request as draft January 14, 2026 06:49
@qquique
Copy link
Author

qquique commented Jan 14, 2026

changing to draft, I will create small PR's for each part

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants