From b0f86b6dcdb050505bc863e31b0b17b8c32cb056 Mon Sep 17 00:00:00 2001 From: AhmedYasserrr Date: Sun, 6 Jul 2025 23:17:29 +0300 Subject: [PATCH 1/9] feat: add fan RPM get/set APIs for single and all fans - Implement ec_get_fan_rpm() to read RPM for a specific fan - Implement ec_get_all_fan_rpm() to read RPMs for all fans at once - Implement ec_set_fan_target_rpm() to set RPM for a specific fan - Implement ec_set_all_fan_target_rpm() to set RPM for all fans --- src/core/libectool.cc | 131 +++++++++++++++++++++++++++++++++++++++- src/include/libectool.h | 7 +++ 2 files changed, 137 insertions(+), 1 deletion(-) diff --git a/src/core/libectool.cc b/src/core/libectool.cc index 849cfd3..1b95d91 100644 --- a/src/core/libectool.cc +++ b/src/core/libectool.cc @@ -112,7 +112,7 @@ int read_mapped_temperature(int id) } // ----------------------------------------------------------------------------- -// Top-level endpoint functions +// Top-level Power Functions // ----------------------------------------------------------------------------- int ec_is_on_ac(int *ac_present) { @@ -138,6 +138,10 @@ int ec_is_on_ac(int *ac_present) { return 0; } +// ----------------------------------------------------------------------------- +// Top-level fan control Functions +// ----------------------------------------------------------------------------- + int ec_auto_fan_control() { int ret = libectool_init(); if (ret < 0) @@ -170,6 +174,131 @@ int ec_set_fan_duty(int duty) { return 0; } +int ec_set_fan_rpm(int target_rpm, int fan_idx) { + int ret, cmdver; + int num_fans; + struct ec_params_pwm_set_fan_target_rpm_v1 p_v1; + + if (target_rpm < 0) + return EC_ERR_INVALID_PARAM; + + ret = libectool_init(); + if (ret < 0) + return EC_ERR_INIT; + + num_fans = get_num_fans(); + + if (fan_idx < 0 || fan_idx >= num_fans) { + libectool_release(); + return EC_ERR_INVALID_PARAM; + } + + cmdver = 1; + + if (!ec_cmd_version_supported(EC_CMD_PWM_SET_FAN_TARGET_RPM, cmdver)) { + libectool_release(); + return EC_ERR_UNSUPPORTED_VER; + } + + p_v1.fan_idx = fan_idx; + p_v1.rpm = target_rpm; + + ret = ec_command(EC_CMD_PWM_SET_FAN_TARGET_RPM, cmdver, + &p_v1, sizeof(p_v1), NULL, 0); + libectool_release(); + + return (ret < 0) ? EC_ERR_EC_COMMAND : 0; +} + +int ec_set_all_fan_rpm(int target_rpm) { + int ret; + struct ec_params_pwm_set_fan_target_rpm_v0 p_v0; + + if (target_rpm < 0) + return EC_ERR_INVALID_PARAM; + + ret = libectool_init(); + if (ret < 0) + return EC_ERR_INIT; + + p_v0.rpm = target_rpm; + + ret = ec_command(EC_CMD_PWM_SET_FAN_TARGET_RPM, 0, + &p_v0, sizeof(p_v0), NULL, 0); + + libectool_release(); + return (ret < 0) ? EC_ERR_EC_COMMAND : 0; +} + +int ec_get_fan_rpm(int *rpm, int fan_idx) { + int ret, num_fans; + struct ec_params_pwm_get_fan_rpm p; + struct ec_response_pwm_get_fan_rpm r; + + if (!rpm) + return EC_ERR_INVALID_PARAM; + + ret = libectool_init(); + if (ret < 0) + return EC_ERR_INIT; + + num_fans = get_num_fans(); + + if (fan_idx < 0 || fan_idx >= num_fans) { + libectool_release(); + return EC_ERR_INVALID_PARAM; + } + + p.fan_idx = fan_idx; + + ret = ec_command(EC_CMD_PWM_GET_FAN_RPM, 0, + &p, sizeof(p), + &r, sizeof(r)); + libectool_release(); + + if (ret < 0) + return EC_ERR_EC_COMMAND; + + *rpm = r.rpm; + return 0; +} + +int ec_get_all_fan_rpm(int *rpms, int max_fans, int *num_fans_out) { + int i, ret, num_fans; + + if (!rpms || !num_fans_out) + return EC_ERR_INVALID_PARAM; + + ret = libectool_init(); + if (ret < 0) + return EC_ERR_INIT; + + num_fans = get_num_fans(); + *num_fans_out = num_fans; + + for (i = 0; i < num_fans && i < max_fans; i++) { + struct ec_params_pwm_get_fan_rpm p; + struct ec_response_pwm_get_fan_rpm r; + + p.fan_idx = i; + ret = ec_command(EC_CMD_PWM_GET_FAN_RPM, 0, + &p, sizeof(p), + &r, sizeof(r)); + if (ret < 0) { + libectool_release(); + return EC_ERR_EC_COMMAND; + } + rpms[i] = r.rpm; + } + + libectool_release(); + return 0; +} + +// ----------------------------------------------------------------------------- +// Top-level temperature Functions +// ----------------------------------------------------------------------------- + int ec_get_max_temperature(float *max_temp) { if (!max_temp) return EC_ERR_INVALID_PARAM; diff --git a/src/include/libectool.h b/src/include/libectool.h index ebc6fbf..80989fe 100644 --- a/src/include/libectool.h +++ b/src/include/libectool.h @@ -8,6 +8,7 @@ #define EC_ERR_READMEM -2 #define EC_ERR_EC_COMMAND -3 #define EC_ERR_INVALID_PARAM -4 +#define EC_ERR_UNSUPPORTED_VER -5 #ifdef __cplusplus extern "C" { @@ -19,8 +20,14 @@ void libectool_release(); // API functions to expose int ec_is_on_ac(int *ac_present); + int ec_auto_fan_control(); int ec_set_fan_duty(int duty); +int ec_set_fan_rpm(int target_rpm, int fan_idx); +int ec_set_all_fan_rpm(int target_rpm); +int ec_get_fan_rpm(int *rpm, int fan_idx); +int ec_get_all_fan_rpm(int *rpms, int max_fans, int *num_fans_out); + int ec_get_max_temperature(float *max_temp); int ec_get_max_non_battery_temperature(float *max_temp); From 56b13373320f496390541430a36734c70bfeed5b Mon Sep 17 00:00:00 2001 From: AhmedYasserrr Date: Sun, 13 Jul 2025 23:08:36 +0300 Subject: [PATCH 2/9] feat: add new temperature and fan control APIs, refactor existing functions --- src/core/libectool.cc | 256 ++++++++++++++++++++++++++++++++++++---- src/include/libectool.h | 39 ++++-- 2 files changed, 263 insertions(+), 32 deletions(-) diff --git a/src/core/libectool.cc b/src/core/libectool.cc index 1b95d91..f5b33cb 100644 --- a/src/core/libectool.cc +++ b/src/core/libectool.cc @@ -111,6 +111,35 @@ int read_mapped_temperature(int id) return (ret <= 0) ? EC_TEMP_SENSOR_ERROR : val; } +// ----------------------------------------------------------------------------- +// Top-level General Functions +// ----------------------------------------------------------------------------- +int ec_hello() { + int ret; + struct ec_params_hello p; + struct ec_response_hello r; + + ret = libectool_init(); + if (ret < 0) + return EC_ERR_INIT; + + p.in_data = 0xa0b0c0d0; + + ret = ec_command(EC_CMD_HELLO, 0, + &p, sizeof(p), + &r, sizeof(r)); + libectool_release(); + + if (ret < 0) + return EC_ERR_EC_COMMAND; + + if (r.out_data != 0xa1b2c3d4) { + return EC_ERR_INVALID_RESPONSE; + } + + return 0; +} + // ----------------------------------------------------------------------------- // Top-level Power Functions // ----------------------------------------------------------------------------- @@ -142,36 +171,108 @@ int ec_is_on_ac(int *ac_present) { // Top-level fan control Functions // ----------------------------------------------------------------------------- -int ec_auto_fan_control() { - int ret = libectool_init(); +int ec_enable_fan_auto_ctrl(int fan_idx) { + int ret, cmdver; + int num_fans; + struct ec_params_auto_fan_ctrl_v1 p_v1; + + ret = libectool_init(); if (ret < 0) return EC_ERR_INIT; - ret = ec_command(EC_CMD_THERMAL_AUTO_FAN_CTRL, 0, NULL, 0, NULL, 0); + cmdver = 1; + + if (!ec_cmd_version_supported(EC_CMD_THERMAL_AUTO_FAN_CTRL, cmdver)) { + libectool_release(); + return EC_ERR_UNSUPPORTED_VER; + } + + num_fans = get_num_fans(); + if (fan_idx < 0 || fan_idx >= num_fans) { + libectool_release(); + return EC_ERR_INVALID_PARAM; + } + + p_v1.fan_idx = fan_idx; + ret = ec_command(EC_CMD_THERMAL_AUTO_FAN_CTRL, cmdver, + &p_v1, sizeof(p_v1), + NULL, 0); libectool_release(); + + return (ret < 0) ? EC_ERR_EC_COMMAND : 0; +} + +int ec_enable_all_fans_auto_ctrl() { + int ret; + + ret = libectool_init(); if (ret < 0) - return EC_ERR_EC_COMMAND; - return 0; + return EC_ERR_INIT; + + ret = ec_command(EC_CMD_THERMAL_AUTO_FAN_CTRL, 0, + NULL, 0, + NULL, 0); + libectool_release(); + + return (ret < 0) ? EC_ERR_EC_COMMAND : 0; } -int ec_set_fan_duty(int duty) { - if (duty < 0 || duty > 100) +int ec_set_fan_duty(int percent, int fan_idx) { + int ret, cmdver; + int num_fans; + struct ec_params_pwm_set_fan_duty_v1 p_v1; + + if (percent < 0 || percent > 100) return EC_ERR_INVALID_PARAM; - int ret = libectool_init(); + ret = libectool_init(); if (ret < 0) return EC_ERR_INIT; - struct ec_params_pwm_set_fan_duty_v0 p_v0; - p_v0.percent = duty; - ret = ec_command(EC_CMD_PWM_SET_FAN_DUTY, 0, &p_v0, sizeof(p_v0), - NULL, 0); + num_fans = get_num_fans(); + if (fan_idx < 0 || fan_idx >= num_fans) { + libectool_release(); + return EC_ERR_INVALID_PARAM; + } + + cmdver = 1; + + if (!ec_cmd_version_supported(EC_CMD_PWM_SET_FAN_DUTY, cmdver)) { + libectool_release(); + return EC_ERR_UNSUPPORTED_VER; + } + + p_v1.fan_idx = fan_idx; + p_v1.percent = percent; + + ret = ec_command(EC_CMD_PWM_SET_FAN_DUTY, cmdver, + &p_v1, sizeof(p_v1), NULL, 0); libectool_release(); + + return (ret < 0) ? EC_ERR_EC_COMMAND : 0; +} + +int ec_set_all_fans_duty(int percent) { + int ret; + struct ec_params_pwm_set_fan_duty_v0 p_v0; + + if (percent < 0 || percent > 100) + return EC_ERR_INVALID_PARAM; + + ret = libectool_init(); if (ret < 0) - return EC_ERR_EC_COMMAND; - return 0; + return EC_ERR_INIT; + + p_v0.percent = percent; + + ret = ec_command(EC_CMD_PWM_SET_FAN_DUTY, 0, + &p_v0, sizeof(p_v0), NULL, 0); + + libectool_release(); + + return (ret < 0) ? EC_ERR_EC_COMMAND : 0; } int ec_set_fan_rpm(int target_rpm, int fan_idx) { @@ -210,7 +311,7 @@ int ec_set_fan_rpm(int target_rpm, int fan_idx) { return (ret < 0) ? EC_ERR_EC_COMMAND : 0; } -int ec_set_all_fan_rpm(int target_rpm) { +int ec_set_all_fans_rpm(int target_rpm) { int ret; struct ec_params_pwm_set_fan_target_rpm_v0 p_v0; @@ -263,7 +364,7 @@ int ec_get_fan_rpm(int *rpm, int fan_idx) { return 0; } -int ec_get_all_fan_rpm(int *rpms, int max_fans, int *num_fans_out) { +int ec_get_all_fans_rpm(int *rpms, int rpms_size, int *num_fans_out) { int i, ret, num_fans; if (!rpms || !num_fans_out) @@ -276,7 +377,7 @@ int ec_get_all_fan_rpm(int *rpms, int max_fans, int *num_fans_out) { num_fans = get_num_fans(); *num_fans_out = num_fans; - for (i = 0; i < num_fans && i < max_fans; i++) { + for (i = 0; i < num_fans && i < rpms_size; i++) { struct ec_params_pwm_get_fan_rpm p; struct ec_response_pwm_get_fan_rpm r; @@ -299,7 +400,67 @@ int ec_get_all_fan_rpm(int *rpms, int max_fans, int *num_fans_out) { // Top-level temperature Functions // ----------------------------------------------------------------------------- -int ec_get_max_temperature(float *max_temp) { +int ec_get_temp(int sensor_idx, int *temp_out) { + int mtemp, ret; + + if (!temp_out || sensor_idx < 0 || sensor_idx >= EC_MAX_TEMP_SENSOR_ENTRIES) + return EC_ERR_INVALID_PARAM; + + ret = libectool_init(); + if (ret < 0) + return EC_ERR_INIT; + + mtemp = read_mapped_temperature(sensor_idx); + + switch (mtemp) { + case EC_TEMP_SENSOR_NOT_PRESENT: + case EC_TEMP_SENSOR_ERROR: + case EC_TEMP_SENSOR_NOT_POWERED: + case EC_TEMP_SENSOR_NOT_CALIBRATED: + return EC_ERR_SENSOR_UNAVAILABLE; + default: + mtemp = K_TO_C(mtemp + EC_TEMP_SENSOR_OFFSET); + } + + libectool_release(); + + if (mtemp < 0) + return EC_ERR_READMEM; + *temp_out = mtemp; + + return 0; +} + +int ec_get_all_temps(int *temps_out, int max_len, int *num_sensors_out) { + int id, mtemp, ret; + int count = 0; + + if (!temps_out || max_len < EC_MAX_TEMP_SENSOR_ENTRIES) + return EC_ERR_INVALID_PARAM; + + ret = libectool_init(); + if (ret < 0) + return EC_ERR_INIT; + + for (id = 0; id < EC_MAX_TEMP_SENSOR_ENTRIES; id++) { + mtemp = read_mapped_temperature(id); + if (mtemp >= 0) { + temps_out[id] = K_TO_C(mtemp + EC_TEMP_SENSOR_OFFSET); + count++; + } else { + temps_out[id] = -1; + } + } + + libectool_release(); + + if (num_sensors_out) + *num_sensors_out = count; + + return 0; +} + +int ec_get_max_temp(int *max_temp) { if (!max_temp) return EC_ERR_INVALID_PARAM; @@ -307,7 +468,7 @@ int ec_get_max_temperature(float *max_temp) { if (ret < 0) return EC_ERR_INIT; - float t = -1.0f; + int t = -1; int mtemp, temp; int id; @@ -334,7 +495,7 @@ int ec_get_max_temperature(float *max_temp) { return 0; } -int ec_get_max_non_battery_temperature(float *max_temp) +int ec_get_max_non_battery_temp(int *max_temp) { if (!max_temp) return EC_ERR_INVALID_PARAM; @@ -345,7 +506,7 @@ int ec_get_max_non_battery_temperature(float *max_temp) struct ec_params_temp_sensor_get_info p; struct ec_response_temp_sensor_get_info r; - float t = -1.0f; + int t = -1; int mtemp, temp; for (p.id = 0; p.id < EC_MAX_TEMP_SENSOR_ENTRIES; p.id++) { @@ -371,3 +532,56 @@ int ec_get_max_non_battery_temperature(float *max_temp) *max_temp = t; return 0; } + +int ec_get_temp_info(int sensor_idx, struct ec_temp_info *info_out) { + struct ec_response_temp_sensor_get_info temp_r; + struct ec_params_temp_sensor_get_info temp_p; + struct ec_params_thermal_get_threshold_v1 thresh_p; + struct ec_thermal_config thresh_r; + int mtemp; + int rc; + + if (!info_out || sensor_idx < 0 || sensor_idx >= EC_MAX_TEMP_SENSOR_ENTRIES) + return EC_ERR_INVALID_PARAM; + + int ret = libectool_init(); + if (ret < 0) + return EC_ERR_INIT; + + // Check whether the sensor exists: + mtemp = read_mapped_temperature(sensor_idx); + if (mtemp < 0) + return EC_ERR_SENSOR_UNAVAILABLE; + + // Get sensor info (name, type) + temp_p.id = sensor_idx; + rc = ec_command(EC_CMD_TEMP_SENSOR_GET_INFO, 0, + &temp_p, sizeof(temp_p), + &temp_r, sizeof(temp_r)); + if (rc < 0) + return EC_ERR_EC_COMMAND; + + strncpy(info_out->sensor_name, temp_r.sensor_name, + sizeof(info_out->sensor_name) - 1); + info_out->sensor_name[sizeof(info_out->sensor_name) - 1] = '\0'; + + info_out->sensor_type = temp_r.sensor_type; + + info_out->temp = K_TO_C(mtemp + EC_TEMP_SENSOR_OFFSET); + + thresh_p.sensor_num = sensor_idx; + rc = ec_command(EC_CMD_THERMAL_GET_THRESHOLD, 1, + &thresh_p, sizeof(thresh_p), + &thresh_r, sizeof(thresh_r)); + if (rc < 0) { + // Could not read thresholds. Fill with -1 as invalid values. + info_out->temp_fan_off = -1; + info_out->temp_fan_max = -1; + } else { + info_out->temp_fan_off = K_TO_C(thresh_r.temp_fan_off); + info_out->temp_fan_max = K_TO_C(thresh_r.temp_fan_max); + } + + libectool_release(); + return 0; +} diff --git a/src/include/libectool.h b/src/include/libectool.h index 80989fe..44bda83 100644 --- a/src/include/libectool.h +++ b/src/include/libectool.h @@ -4,32 +4,49 @@ #include // Standard error codes -#define EC_ERR_INIT -1 -#define EC_ERR_READMEM -2 -#define EC_ERR_EC_COMMAND -3 -#define EC_ERR_INVALID_PARAM -4 -#define EC_ERR_UNSUPPORTED_VER -5 +#define EC_ERR_INIT -1 +#define EC_ERR_READMEM -2 +#define EC_ERR_EC_COMMAND -3 +#define EC_ERR_INVALID_PARAM -4 +#define EC_ERR_UNSUPPORTED_VER -5 +#define EC_ERR_INVALID_RESPONSE -6 +#define EC_ERR_SENSOR_UNAVAILABLE -7 #ifdef __cplusplus extern "C" { #endif +struct ec_temp_info { + char sensor_name[32]; + int sensor_type; + int temp; + int temp_fan_off; + int temp_fan_max; +}; + // Library init/release int libectool_init(); void libectool_release(); // API functions to expose +int ec_hello() + int ec_is_on_ac(int *ac_present); -int ec_auto_fan_control(); -int ec_set_fan_duty(int duty); +int ec_enable_fan_auto_ctrl(int fan_idx); +int ec_enable_all_fans_auto_ctrl(); +int ec_set_fan_duty(int percent, int fan_idx); +int ec_set_all_fans_duty(int percent); int ec_set_fan_rpm(int target_rpm, int fan_idx); -int ec_set_all_fan_rpm(int target_rpm); +int ec_set_all_fans_rpm(int target_rpm); int ec_get_fan_rpm(int *rpm, int fan_idx); -int ec_get_all_fan_rpm(int *rpms, int max_fans, int *num_fans_out); +int ec_get_all_fans_rpm(int *rpms, int rpms_size, int *num_fans_out); -int ec_get_max_temperature(float *max_temp); -int ec_get_max_non_battery_temperature(float *max_temp); +int ec_get_temp(int sensor_idx, int *temp_out); +int ec_get_all_temps(int *temps_out, int max_len, int *num_sensors_out); +int ec_get_max_temp(int *max_temp); +int ec_get_max_non_battery_temp(int *max_temp); +int ec_get_temp_info(int sensor_idx, struct ec_temp_info *info_out); /* ASCII mode for printing, default off */ extern int ascii_mode; From f27f997c6805cd176a8ad48002ba084fd2d9a91f Mon Sep 17 00:00:00 2001 From: AhmedYasserrr Date: Tue, 15 Jul 2025 23:53:47 +0300 Subject: [PATCH 3/9] feat: add fan and temperature control APIs --- pyectool/__init__.pyi | 26 ++++++-- src/bindings/ECController.cc | 111 +++++++++++++++++++++++++++++---- src/bindings/ECController.h | 21 +++++-- src/bindings/PyECController.cc | 95 +++++++++++++++++++++++++--- src/core/libectool.cc | 99 ++++++++++++++++++++--------- src/include/libectool.h | 4 +- 6 files changed, 298 insertions(+), 58 deletions(-) diff --git a/pyectool/__init__.pyi b/pyectool/__init__.pyi index 1d562ab..839ee89 100644 --- a/pyectool/__init__.pyi +++ b/pyectool/__init__.pyi @@ -3,10 +3,28 @@ from __future__ import annotations __doc__: str __version__: str +class ECTempInfo(dict[str, int | str]): + sensor_name: str + sensor_type: int + temp: int + temp_fan_off: int + temp_fan_max: int + class ECController: def __init__(self) -> None: ... def is_on_ac(self) -> bool: ... - def auto_fan_control(self) -> None: ... - def set_fan_duty(self, duty: int) -> None: ... - def get_max_temperature(self) -> float: ... - def get_max_non_battery_temperature(self) -> float: ... \ No newline at end of file + def get_num_fans(self) -> int: ... + def enable_fan_auto_ctrl(self, fan_idx: int) -> None: ... + def enable_all_fans_auto_ctrl(self) -> None: ... + def set_fan_duty(self, percent: int, fan_idx: int) -> None: ... + def set_all_fans_duty(self, percent: int) -> None: ... + def set_fan_rpm(self, target_rpm: int, fan_idx: int) -> None: ... + def set_all_fans_rpm(self, target_rpm: int) -> None: ... + def get_fan_rpm(self, fan_idx: int) -> int: ... + def get_all_fans_rpm(self) -> list[int]: ... + def get_num_temp_entries(self) -> int: ... + def get_temp(self, sensor_idx: int) -> int: ... + def get_all_temps(self) -> list[int]: ... + def get_max_temp(self) -> int: ... + def get_max_non_battery_temp(self) -> int: ... + def get_temp_info(self, sensor_idx: int) -> ECTempInfo: ... diff --git a/src/bindings/ECController.cc b/src/bindings/ECController.cc index 96e616a..b84b5f1 100644 --- a/src/bindings/ECController.cc +++ b/src/bindings/ECController.cc @@ -16,6 +16,9 @@ void ECController::handle_error(int code, const std::string &msg) { throw std::runtime_error(msg + " (" + reason + ", code " + std::to_string(code) + ")"); } +// ----------------------------------------------------------------------------- +// Top-level Power Functions +// ----------------------------------------------------------------------------- bool ECController::is_on_ac() { int ac; @@ -24,26 +27,108 @@ bool ECController::is_on_ac() { return ac; } -void ECController::auto_fan_control() { - int ret = ec_auto_fan_control(); +// ----------------------------------------------------------------------------- +// Top-level fan control Functions +// ----------------------------------------------------------------------------- + +int ECController::get_num_fans() { + int val = 0; + int ret = ec_get_num_fans(&val); + handle_error(ret, "Failed to get number of fans"); + return val; +} + +void ECController::enable_fan_auto_ctrl(int fan_idx) { + int ret = ec_enable_fan_auto_ctrl(fan_idx); handle_error(ret, "Failed to enable auto fan control"); } -void ECController::set_fan_duty(int duty) { - int ret = ec_set_fan_duty(duty); +void ECController::enable_all_fans_auto_ctrl() { + int ret = ec_enable_all_fans_auto_ctrl(); + handle_error(ret, "Failed to enable auto control for all fans"); +} + +void ECController::set_fan_duty(int percent, int fan_idx) { + int ret = ec_set_fan_duty(percent, fan_idx); handle_error(ret, "Failed to set fan duty"); } -float ECController::get_max_temperature() { - float t; - int ret = ec_get_max_temperature(&t); +void ECController::set_all_fans_duty(int percent) { + int ret = ec_set_all_fans_duty(percent); + handle_error(ret, "Failed to set duty for all fans"); +} + +void ECController::set_fan_rpm(int target_rpm, int fan_idx) { + int ret = ec_set_fan_rpm(target_rpm, fan_idx); + handle_error(ret, "Failed to set fan RPM"); +} + +void ECController::set_all_fans_rpm(int target_rpm) { + int ret = ec_set_all_fans_rpm(target_rpm); + handle_error(ret, "Failed to set RPM for all fans"); +} + +int ECController::get_fan_rpm(int fan_idx) { + int rpm = 0; + int ret = ec_get_fan_rpm(&rpm, fan_idx); + handle_error(ret, "Failed to get fan RPM"); + return rpm; +} + +std::vector ECController::get_all_fans_rpm() { + int num_fans = get_num_fans(); + std::vector rpms(num_fans); + int num_fans_out = 0; + + int ret = ec_get_all_fans_rpm(rpms.data(), num_fans, &num_fans_out); + handle_error(ret, "Failed to get all fan RPMs"); + return rpms; +} + +// ----------------------------------------------------------------------------- +// Top-level temperature Functions +// ----------------------------------------------------------------------------- +int ECController::get_num_temp_entries() { + int val = 0; + int ret = ec_get_num_temp_entries(&val); + handle_error(ret, "Failed to get number of temp sensors"); + return val; +} + +int ECController::get_temp(int sensor_idx) { + int temp = 0; + int ret = ec_get_temp(sensor_idx, &temp); + handle_error(ret, "Failed to get temperature"); + return temp; +} + +std::vector ECController::get_all_temps() { + int max_entries = get_num_temp_entries(); + std::vector temps(max_entries); + int num_sensors = 0; + + int ret = ec_get_all_temps(temps.data(), max_entries, &num_sensors); + handle_error(ret, "Failed to get all temperatures"); + return temps; +} + +int ECController::get_max_temp() { + int temp = 0; + int ret = ec_get_max_temp(&temp); handle_error(ret, "Failed to get max temperature"); - return t; + return temp; +} + +int ECController::get_max_non_battery_temp() { + int temp = 0; + int ret = ec_get_max_non_battery_temp(&temp); + handle_error(ret, "Failed to get max non-battery temperature"); + return temp; } -float ECController::get_max_non_battery_temperature() { - float t; - int ret = ec_get_max_non_battery_temperature(&t); - handle_error(ret, "Failed to get non-battery temperature"); - return t; +ec_temp_info ECController::get_temp_info(int sensor_idx) { + ec_temp_info info; + int ret = ec_get_temp_info(sensor_idx, &info); + handle_error(ret, "Failed to get temp sensor info"); + return info; } diff --git a/src/bindings/ECController.h b/src/bindings/ECController.h index 969ed54..c63bea6 100644 --- a/src/bindings/ECController.h +++ b/src/bindings/ECController.h @@ -5,10 +5,23 @@ class ECController { public: bool is_on_ac(); - void auto_fan_control(); - void set_fan_duty(int duty); - float get_max_temperature(); - float get_max_non_battery_temperature(); + + int get_num_fans(); + void enable_fan_auto_ctrl(int fan_idx); + void enable_all_fans_auto_ctrl(); + void set_fan_duty(int percent, int fan_idx); + void set_all_fans_duty(int percent); + void set_fan_rpm(int target_rpm, int fan_idx); + void set_all_fans_rpm(int target_rpm); + int get_fan_rpm(int fan_idx); + std::vector get_all_fans_rpm(); + + int get_num_temp_entries(); + int get_temp(int sensor_idx); + std::vector get_all_temps(); + int get_max_temp(); + int get_max_non_battery_temp(); + ec_temp_info get_temp_info(int sensor_idx); private: void handle_error(int code, const std::string &msg); diff --git a/src/bindings/PyECController.cc b/src/bindings/PyECController.cc index e6d9898..820596e 100644 --- a/src/bindings/PyECController.cc +++ b/src/bindings/PyECController.cc @@ -1,25 +1,104 @@ #include +#include #include "ECController.h" +#include "libectool.h" #define STRINGIFY(x) #x #define MACRO_STRINGIFY(x) STRINGIFY(x) namespace py = pybind11; +py::dict temp_info_to_dict(const ec_temp_info& info) { + py::dict d; + d["sensor_name"] = std::string(info.sensor_name); + d["sensor_type"] = info.sensor_type; + d["temp"] = info.temp; + d["temp_fan_off"] = info.temp_fan_off; + d["temp_fan_max"] = info.temp_fan_max; + return d; +} + + PYBIND11_MODULE(libectool_py, m) { m.doc() = "Python bindings for ectool"; py::class_(m, "ECController") .def(py::init<>()) .def("is_on_ac", &ECController::is_on_ac, "Check if on AC power") - .def("auto_fan_control", &ECController::auto_fan_control, "Enable automatic fan control") - .def("set_fan_duty", &ECController::set_fan_duty, - "Set fan duty cycle (0-100)", py::arg("duty")) - .def("get_max_temperature", &ECController::get_max_temperature, - "Get max temperature") - .def("get_max_non_battery_temperature", - &ECController::get_max_non_battery_temperature, - "Get max non-battery temperature"); + + .def("get_num_fans", &ECController::get_num_fans, + "Get number of fans") + + .def("enable_fan_auto_ctrl", + &ECController::enable_fan_auto_ctrl, + "Enable auto control for a fan", + py::arg("fan_idx")) + + .def("enable_all_fans_auto_ctrl", + &ECController::enable_all_fans_auto_ctrl, + "Enable auto control for all fans") + + .def("set_fan_duty", + &ECController::set_fan_duty, + "Set fan duty cycle (0-100)", + py::arg("percent"), py::arg("fan_idx")) + + .def("set_all_fans_duty", + &ECController::set_all_fans_duty, + "Set all fans duty cycle (0-100)", + py::arg("percent")) + + .def("set_fan_rpm", + &ECController::set_fan_rpm, + "Set fan RPM", + py::arg("target_rpm"), py::arg("fan_idx")) + + .def("set_all_fans_rpm", + &ECController::set_all_fans_rpm, + "Set all fans RPM", + py::arg("target_rpm")) + + .def("get_fan_rpm", + &ECController::get_fan_rpm, + "Get single fan RPM", + py::arg("fan_idx")) + + .def("get_all_fans_rpm", + [](ECController &self) { + return py::list(self.get_all_fans_rpm()); + }, + "Get all fans RPM as list") + + .def("get_num_temp_entries", + &ECController::get_num_temp_entries, + "Get number of temperature sensors") + + .def("get_temp", + &ECController::get_temp, + "Get temperature in Celsius for one sensor", + py::arg("sensor_idx")) + + .def("get_all_temps", + [](ECController &self) { + return py::list(self.get_all_temps()); + }, + "Get all temperature values as list") + + .def("get_max_temp", + &ECController::get_max_temp, + "Get maximum temperature across all sensors") + + .def("get_max_non_battery_temp", + &ECController::get_max_non_battery_temp, + "Get maximum non-battery temperature") + + .def("get_temp_info", + [](ECController &self, int sensor_idx) { + ec_temp_info info = self.get_temp_info(sensor_idx); + return temp_info_to_dict(info); + }, + "Get detailed temperature info for a sensor", + py::arg("sensor_idx")); #ifdef VERSION_INFO m.attr("__version__") = MACRO_STRINGIFY(VERSION_INFO); diff --git a/src/core/libectool.cc b/src/core/libectool.cc index f5b33cb..d9095bd 100644 --- a/src/core/libectool.cc +++ b/src/core/libectool.cc @@ -171,6 +171,36 @@ int ec_is_on_ac(int *ac_present) { // Top-level fan control Functions // ----------------------------------------------------------------------------- +int ec_get_num_fans(int *val) { + int ret, fan_val, idx; + struct ec_response_get_features r; + + if (!val) + return EC_ERR_INVALID_PARAM; + + ret = libectool_init(); + if (ret < 0) + return EC_ERR_INIT; + + ret = ec_command(EC_CMD_GET_FEATURES, 0, NULL, 0, &r, sizeof(r)); + if (ret >= 0 && !(r.flags[0] & BIT(EC_FEATURE_PWM_FAN))) + *val = 0; + + for (idx = 0; idx < EC_FAN_SPEED_ENTRIES; idx++) { + ret = ec_readmem(EC_MEMMAP_FAN + 2 * idx, sizeof(fan_val), &fan_val); + + if (ret <= 0) + return EC_ERR_READMEM; + + if (fan_val == EC_FAN_SPEED_NOT_PRESENT) + break; + } + + *val = idx; + libectool_release(); + return 0; +} + int ec_enable_fan_auto_ctrl(int fan_idx) { int ret, cmdver; int num_fans; @@ -187,7 +217,8 @@ int ec_enable_fan_auto_ctrl(int fan_idx) { return EC_ERR_UNSUPPORTED_VER; } - num_fans = get_num_fans(); + ec_get_num_fans(&num_fans); + if (fan_idx < 0 || fan_idx >= num_fans) { libectool_release(); return EC_ERR_INVALID_PARAM; @@ -230,7 +261,7 @@ int ec_set_fan_duty(int percent, int fan_idx) { if (ret < 0) return EC_ERR_INIT; - num_fans = get_num_fans(); + ec_get_num_fans(&num_fans); if (fan_idx < 0 || fan_idx >= num_fans) { libectool_release(); return EC_ERR_INVALID_PARAM; @@ -287,7 +318,7 @@ int ec_set_fan_rpm(int target_rpm, int fan_idx) { if (ret < 0) return EC_ERR_INIT; - num_fans = get_num_fans(); + ec_get_num_fans(&num_fans); if (fan_idx < 0 || fan_idx >= num_fans) { libectool_release(); @@ -332,9 +363,7 @@ int ec_set_all_fans_rpm(int target_rpm) { } int ec_get_fan_rpm(int *rpm, int fan_idx) { - int ret, num_fans; - struct ec_params_pwm_get_fan_rpm p; - struct ec_response_pwm_get_fan_rpm r; + int ret, val, num_fans; if (!rpm) return EC_ERR_INVALID_PARAM; @@ -343,29 +372,34 @@ int ec_get_fan_rpm(int *rpm, int fan_idx) { if (ret < 0) return EC_ERR_INIT; - num_fans = get_num_fans(); + ec_get_num_fans(&num_fans); if (fan_idx < 0 || fan_idx >= num_fans) { libectool_release(); return EC_ERR_INVALID_PARAM; } - p.fan_idx = fan_idx; + ret = ec_readmem(EC_MEMMAP_FAN + 2 * fan_idx, sizeof(val), &val); + if (ret <= 0) + return EC_ERR_READMEM; - ret = ec_command(EC_CMD_PWM_GET_FAN_RPM, 0, - &p, sizeof(p), - &r, sizeof(r)); - libectool_release(); + switch (val) { + case EC_FAN_SPEED_NOT_PRESENT: + val = -1; + break; + case EC_FAN_SPEED_STALLED: + val = -2; + break; + } - if (ret < 0) - return EC_ERR_EC_COMMAND; + libectool_release(); - *rpm = r.rpm; + *rpm = val; return 0; } int ec_get_all_fans_rpm(int *rpms, int rpms_size, int *num_fans_out) { - int i, ret, num_fans; + int i, ret, val, num_fans; if (!rpms || !num_fans_out) return EC_ERR_INVALID_PARAM; @@ -374,22 +408,23 @@ int ec_get_all_fans_rpm(int *rpms, int rpms_size, int *num_fans_out) { if (ret < 0) return EC_ERR_INIT; - num_fans = get_num_fans(); + ec_get_num_fans(&num_fans); *num_fans_out = num_fans; for (i = 0; i < num_fans && i < rpms_size; i++) { - struct ec_params_pwm_get_fan_rpm p; - struct ec_response_pwm_get_fan_rpm r; - - p.fan_idx = i; - ret = ec_command(EC_CMD_PWM_GET_FAN_RPM, 0, - &p, sizeof(p), - &r, sizeof(r)); - if (ret < 0) { - libectool_release(); - return EC_ERR_EC_COMMAND; + ret = ec_readmem(EC_MEMMAP_FAN + 2 * i, sizeof(val), &val); + if (ret <= 0) + return EC_ERR_READMEM; + + switch (val) { + case EC_FAN_SPEED_NOT_PRESENT: + val = -1; + break; + case EC_FAN_SPEED_STALLED: + val = -2; + break; } - rpms[i] = r.rpm; + rpms[i] = val; } libectool_release(); @@ -400,6 +435,14 @@ int ec_get_all_fans_rpm(int *rpms, int rpms_size, int *num_fans_out) { // Top-level temperature Functions // ----------------------------------------------------------------------------- +int ec_get_num_temp_entries(int *val) { + if (!val) + return EC_ERR_INVALID_PARAM; + + *val = EC_MAX_TEMP_SENSOR_ENTRIES; + return 0; +} + int ec_get_temp(int sensor_idx, int *temp_out) { int mtemp, ret; diff --git a/src/include/libectool.h b/src/include/libectool.h index 44bda83..b8c38e6 100644 --- a/src/include/libectool.h +++ b/src/include/libectool.h @@ -29,10 +29,11 @@ int libectool_init(); void libectool_release(); // API functions to expose -int ec_hello() +int ec_hello(); int ec_is_on_ac(int *ac_present); +int ec_get_num_fans(int *val); int ec_enable_fan_auto_ctrl(int fan_idx); int ec_enable_all_fans_auto_ctrl(); int ec_set_fan_duty(int percent, int fan_idx); @@ -42,6 +43,7 @@ int ec_set_all_fans_rpm(int target_rpm); int ec_get_fan_rpm(int *rpm, int fan_idx); int ec_get_all_fans_rpm(int *rpms, int rpms_size, int *num_fans_out); +int ec_get_num_temp_entries(int *val) ; int ec_get_temp(int sensor_idx, int *temp_out); int ec_get_all_temps(int *temps_out, int max_len, int *num_sensors_out); int ec_get_max_temp(int *max_temp); From 1c9572e32dbc4a09f375a20eda07f122cc98d408 Mon Sep 17 00:00:00 2001 From: AhmedYasserrr Date: Sat, 19 Jul 2025 03:00:30 +0300 Subject: [PATCH 4/9] feat: update Python bindings to use py::cast for fan and temperature lists --- src/bindings/ECController.h | 2 ++ src/bindings/PyECController.cc | 4 ++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/bindings/ECController.h b/src/bindings/ECController.h index c63bea6..01e2703 100644 --- a/src/bindings/ECController.h +++ b/src/bindings/ECController.h @@ -1,6 +1,8 @@ #pragma once #include #include +#include +#include "libectool.h" class ECController { public: diff --git a/src/bindings/PyECController.cc b/src/bindings/PyECController.cc index 820596e..a56ee63 100644 --- a/src/bindings/PyECController.cc +++ b/src/bindings/PyECController.cc @@ -65,7 +65,7 @@ PYBIND11_MODULE(libectool_py, m) { .def("get_all_fans_rpm", [](ECController &self) { - return py::list(self.get_all_fans_rpm()); + return py::cast(self.get_all_fans_rpm()); }, "Get all fans RPM as list") @@ -80,7 +80,7 @@ PYBIND11_MODULE(libectool_py, m) { .def("get_all_temps", [](ECController &self) { - return py::list(self.get_all_temps()); + return py::cast(self.get_all_temps()); }, "Get all temperature values as list") From cd2ab19ffab64885d28ff1ad5aee206ff63f4544 Mon Sep 17 00:00:00 2001 From: AhmedYasserrr Date: Fri, 18 Jul 2025 21:52:13 -0700 Subject: [PATCH 5/9] feat: rename temperature entry functions and add tests --- pyectool/__init__.pyi | 2 +- src/bindings/ECController.cc | 7 ++- src/bindings/ECController.h | 2 +- src/bindings/PyECController.cc | 4 +- src/core/libectool.cc | 70 ++++++++++++++++------ src/include/libectool.h | 2 +- tests/test_pyectool.py | 105 +++++++++++++++++++++++++++++++++ 7 files changed, 165 insertions(+), 27 deletions(-) create mode 100644 tests/test_pyectool.py diff --git a/pyectool/__init__.pyi b/pyectool/__init__.pyi index 839ee89..eef7164 100644 --- a/pyectool/__init__.pyi +++ b/pyectool/__init__.pyi @@ -22,7 +22,7 @@ class ECController: def set_all_fans_rpm(self, target_rpm: int) -> None: ... def get_fan_rpm(self, fan_idx: int) -> int: ... def get_all_fans_rpm(self) -> list[int]: ... - def get_num_temp_entries(self) -> int: ... + def get_num_temp_sensors(self) -> int: ... def get_temp(self, sensor_idx: int) -> int: ... def get_all_temps(self) -> list[int]: ... def get_max_temp(self) -> int: ... diff --git a/src/bindings/ECController.cc b/src/bindings/ECController.cc index b84b5f1..07edc5b 100644 --- a/src/bindings/ECController.cc +++ b/src/bindings/ECController.cc @@ -88,9 +88,9 @@ std::vector ECController::get_all_fans_rpm() { // ----------------------------------------------------------------------------- // Top-level temperature Functions // ----------------------------------------------------------------------------- -int ECController::get_num_temp_entries() { +int ECController::get_num_temp_sensors() { int val = 0; - int ret = ec_get_num_temp_entries(&val); + int ret = ec_get_num_temp_sensors(&val); handle_error(ret, "Failed to get number of temp sensors"); return val; } @@ -103,12 +103,13 @@ int ECController::get_temp(int sensor_idx) { } std::vector ECController::get_all_temps() { - int max_entries = get_num_temp_entries(); + int max_entries = get_num_temp_sensors(); std::vector temps(max_entries); int num_sensors = 0; int ret = ec_get_all_temps(temps.data(), max_entries, &num_sensors); handle_error(ret, "Failed to get all temperatures"); + temps.resize(num_sensors); // Trim unused entries return temps; } diff --git a/src/bindings/ECController.h b/src/bindings/ECController.h index 01e2703..e042a68 100644 --- a/src/bindings/ECController.h +++ b/src/bindings/ECController.h @@ -18,7 +18,7 @@ class ECController { int get_fan_rpm(int fan_idx); std::vector get_all_fans_rpm(); - int get_num_temp_entries(); + int get_num_temp_sensors(); int get_temp(int sensor_idx); std::vector get_all_temps(); int get_max_temp(); diff --git a/src/bindings/PyECController.cc b/src/bindings/PyECController.cc index a56ee63..94bcaa8 100644 --- a/src/bindings/PyECController.cc +++ b/src/bindings/PyECController.cc @@ -69,8 +69,8 @@ PYBIND11_MODULE(libectool_py, m) { }, "Get all fans RPM as list") - .def("get_num_temp_entries", - &ECController::get_num_temp_entries, + .def("get_num_temp_sensors", + &ECController::get_num_temp_sensors, "Get number of temperature sensors") .def("get_temp", diff --git a/src/core/libectool.cc b/src/core/libectool.cc index d9095bd..cff9c7e 100644 --- a/src/core/libectool.cc +++ b/src/core/libectool.cc @@ -172,7 +172,8 @@ int ec_is_on_ac(int *ac_present) { // ----------------------------------------------------------------------------- int ec_get_num_fans(int *val) { - int ret, fan_val, idx; + int ret, idx; + uint16_t fan_val; struct ec_response_get_features r; if (!val) @@ -191,8 +192,8 @@ int ec_get_num_fans(int *val) { if (ret <= 0) return EC_ERR_READMEM; - - if (fan_val == EC_FAN_SPEED_NOT_PRESENT) + + if ((int)fan_val == EC_FAN_SPEED_NOT_PRESENT) break; } @@ -363,7 +364,8 @@ int ec_set_all_fans_rpm(int target_rpm) { } int ec_get_fan_rpm(int *rpm, int fan_idx) { - int ret, val, num_fans; + int ret, num_fans; + uint16_t val; if (!rpm) return EC_ERR_INVALID_PARAM; @@ -385,21 +387,22 @@ int ec_get_fan_rpm(int *rpm, int fan_idx) { switch (val) { case EC_FAN_SPEED_NOT_PRESENT: - val = -1; + *rpm = -1; break; case EC_FAN_SPEED_STALLED: - val = -2; + *rpm = -2; break; + default: + *rpm = val; } libectool_release(); - - *rpm = val; return 0; } int ec_get_all_fans_rpm(int *rpms, int rpms_size, int *num_fans_out) { - int i, ret, val, num_fans; + int i, ret, num_fans; + uint16_t val; if (!rpms || !num_fans_out) return EC_ERR_INVALID_PARAM; @@ -418,13 +421,15 @@ int ec_get_all_fans_rpm(int *rpms, int rpms_size, int *num_fans_out) { switch (val) { case EC_FAN_SPEED_NOT_PRESENT: - val = -1; + rpms[i] = -1; break; case EC_FAN_SPEED_STALLED: - val = -2; + rpms[i] = -2; break; + default: + rpms[i] = val; } - rpms[i] = val; + } libectool_release(); @@ -434,12 +439,34 @@ int ec_get_all_fans_rpm(int *rpms, int rpms_size, int *num_fans_out) { // ----------------------------------------------------------------------------- // Top-level temperature Functions // ----------------------------------------------------------------------------- +int ec_get_num_temp_sensors(int *val) { + int id, mtemp, ret; + int count = 0; -int ec_get_num_temp_entries(int *val) { if (!val) return EC_ERR_INVALID_PARAM; - *val = EC_MAX_TEMP_SENSOR_ENTRIES; + ret = libectool_init(); + if (ret < 0) + return EC_ERR_INIT; + + for (id = 0; id < EC_MAX_TEMP_SENSOR_ENTRIES; id++) { + mtemp = read_mapped_temperature(id); + + switch (mtemp) { + case EC_TEMP_SENSOR_NOT_PRESENT: + case EC_TEMP_SENSOR_ERROR: + case EC_TEMP_SENSOR_NOT_POWERED: + case EC_TEMP_SENSOR_NOT_CALIBRATED: + continue; + default: + count++; + } + } + + libectool_release(); + + *val = count; return 0; } @@ -478,7 +505,7 @@ int ec_get_all_temps(int *temps_out, int max_len, int *num_sensors_out) { int id, mtemp, ret; int count = 0; - if (!temps_out || max_len < EC_MAX_TEMP_SENSOR_ENTRIES) + if (!temps_out) return EC_ERR_INVALID_PARAM; ret = libectool_init(); @@ -487,11 +514,16 @@ int ec_get_all_temps(int *temps_out, int max_len, int *num_sensors_out) { for (id = 0; id < EC_MAX_TEMP_SENSOR_ENTRIES; id++) { mtemp = read_mapped_temperature(id); - if (mtemp >= 0) { - temps_out[id] = K_TO_C(mtemp + EC_TEMP_SENSOR_OFFSET); + + switch (mtemp) { + case EC_TEMP_SENSOR_NOT_PRESENT: + case EC_TEMP_SENSOR_ERROR: + case EC_TEMP_SENSOR_NOT_POWERED: + case EC_TEMP_SENSOR_NOT_CALIBRATED: + continue; + default: + temps_out[count] = K_TO_C(mtemp + EC_TEMP_SENSOR_OFFSET); count++; - } else { - temps_out[id] = -1; } } diff --git a/src/include/libectool.h b/src/include/libectool.h index b8c38e6..8d833a6 100644 --- a/src/include/libectool.h +++ b/src/include/libectool.h @@ -43,7 +43,7 @@ int ec_set_all_fans_rpm(int target_rpm); int ec_get_fan_rpm(int *rpm, int fan_idx); int ec_get_all_fans_rpm(int *rpms, int rpms_size, int *num_fans_out); -int ec_get_num_temp_entries(int *val) ; +int ec_get_num_temp_sensors(int *val) ; int ec_get_temp(int sensor_idx, int *temp_out); int ec_get_all_temps(int *temps_out, int max_len, int *num_sensors_out); int ec_get_max_temp(int *max_temp); diff --git a/tests/test_pyectool.py b/tests/test_pyectool.py new file mode 100644 index 0000000..a5659e0 --- /dev/null +++ b/tests/test_pyectool.py @@ -0,0 +1,105 @@ +import subprocess +import re +from pyectool import ECController + +ec = ECController() + +def run_ectool_command(cmd): + result = subprocess.run( + cmd, + stdout=subprocess.PIPE, + stderr=subprocess.DEVNULL, + shell=True, + text=True, + ) + return result.stdout + +def test_is_on_ac(): + result_py = ec.is_on_ac() + out = run_ectool_command("ectool battery") + ectool_result = bool(re.search(r"Flags.*AC_PRESENT", out)) + print(f"[is_on_ac] pyectool={result_py}, ectool={ectool_result}") + assert result_py == ectool_result, f"pyectool.is_on_ac={result_py}, ectool={ectool_result}" + +def test_get_max_temp(): + py_temp = ec.get_max_temp() + raw_out = run_ectool_command("ectool temps all") + raw_temps = re.findall(r"\(= (\d+) C\)", raw_out) + temps = sorted([int(x) for x in raw_temps if int(x) > 0], reverse=True) + ectool_temp = float(round(temps[0], 2)) if temps else -1 + print(f"[get_max_temp] pyectool={py_temp}, ectool={ectool_temp}") + assert abs(py_temp - ectool_temp) <= 1.0, f"pyectool={py_temp}, ectool={ectool_temp}" + +def test_get_max_non_battery_temp(): + raw_out = run_ectool_command("ectool tempsinfo all") + battery_sensors_raw = re.findall(r"\d+ Battery", raw_out, re.MULTILINE) + battery_sensors = [x.split(" ")[0] for x in battery_sensors_raw] + all_sensors = re.findall(r"^\d+", raw_out, re.MULTILINE) + non_battery_sensors = [x for x in all_sensors if x not in battery_sensors] + + temps = [] + for sensor in non_battery_sensors: + out = run_ectool_command(f"ectool temps {sensor}") + matches = re.findall(r"\(= (\d+) C\)", out) + temps.extend([int(x) for x in matches]) + + ectool_temp = float(round(max(temps), 2)) if temps else -1 + py_temp = ec.get_max_non_battery_temp() + print(f"[get_max_non_battery_temp] pyectool={py_temp}, ectool={ectool_temp}") + assert abs(py_temp - ectool_temp) <= 1.0, f"pyectool={py_temp}, ectool={ectool_temp}" + +def test_get_all_temps(): + py_vals = ec.get_all_temps() + raw_out = run_ectool_command("ectool temps all") + ectool_vals = [int(x) for x in re.findall(r"\(= (\d+) C\)", raw_out)] + print(f"[get_all_temps] pyectool={py_vals}, ectool={ectool_vals}") + assert all(abs(p - e) <= 1 for p, e in zip(py_vals, ectool_vals[:len(py_vals)])), "Mismatch in get_all_temps" + +def test_get_temp(): + try: + py_temp = ec.get_temp(0) + raw_out = run_ectool_command("ectool temps 0") + match = re.search(r"\(= (\d+) C\)", raw_out) + ectool_temp = int(match.group(1)) if match else -1 + print(f"[get_temp(0)] pyectool={py_temp}, ectool={ectool_temp}") + assert abs(py_temp - ectool_temp) <= 1 + except Exception as e: + print(f"[get_temp(0)] Skipped due to: {e}") + +def test_get_num_temp_sensors(): + py_val = ec.get_num_temp_sensors() + raw_out = run_ectool_command("ectool temps all") + ectool_vals = [int(x) for x in re.findall(r"\(= (\d+) C\)", raw_out)] + ectool_val = len(ectool_vals) + print(f"[get_num_temp_sensors] pyectool={py_val}, ectool={ectool_val}") + assert abs(py_val == ectool_val) + +def test_get_temp_info(): + py_info = ec.get_temp_info(0) + print(f"[get_temp_info] pyectool={py_info}") + +def test_get_all_fans_rpm(): + py_vals = ec.get_all_fans_rpm() + out = run_ectool_command("ectool pwmgetfanrpm") + ectool_vals = [int(x) for x in re.findall(r"rpm = (\d+)", out)] + print(f"[get_all_fans_rpm] pyectool={py_vals}, ectool={ectool_vals}") + assert all(abs(p - e) <= 20 for p, e in zip(py_vals, ectool_vals)), "Mismatch in fan RPMs" + +def test_get_fan_rpm(): + try: + py_val = ec.get_fan_rpm(0) + out = run_ectool_command("ectool pwmgetfanrpm 0") + match = re.search(r"rpm = (\d+)", out) + ectool_val = int(match.group(1)) if match else -1 + print(f"[get_fan_rpm(0)] pyectool={py_val}, ectool={ectool_val}") + assert abs(py_val - ectool_val) <= 20 + except Exception as e: + print(f"[get_fan_rpm(0)] Skipped due to: {e}") + +def test_get_num_fans(): + py_val = ec.get_num_fans() + out = run_ectool_command("ectool pwmgetnumfans") + match = re.search(r"Number of fans\s*=\s*(\d+)", out) + ectool_val = int(match.group(1)) if match else -1 + print(f"[get_num_fans] pyectool={py_val}, ectool={ectool_val}") + assert py_val == ectool_val, f"Mismatch: pyectool={py_val}, ectool={ectool_val}" From 0b5c75a5501a9691340bdfb3f52ee23d20ce607d Mon Sep 17 00:00:00 2001 From: AhmedYasserrr Date: Mon, 21 Jul 2025 14:15:08 +0300 Subject: [PATCH 6/9] fix: add missing EC error code mappings in handle_error --- src/bindings/ECController.cc | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/bindings/ECController.cc b/src/bindings/ECController.cc index 07edc5b..3e00110 100644 --- a/src/bindings/ECController.cc +++ b/src/bindings/ECController.cc @@ -10,6 +10,16 @@ void ECController::handle_error(int code, const std::string &msg) { case EC_ERR_READMEM: reason = "EC memory read failed"; break; case EC_ERR_EC_COMMAND: reason = "EC command failed"; break; case EC_ERR_INVALID_PARAM: reason = "Invalid parameter"; break; + case EC_ERR_SENSOR_UNAVAILABLE: + reason = "Sensor unavailable or not calibrated/powered"; + break; + case EC_ERR_UNSUPPORTED_VER: + reason = "Unsupported EC command version"; + break; + + case EC_ERR_INVALID_RESPONSE: + reason = "Invalid response from EC"; + break; default: reason = "Unknown error"; break; } From 3bc57e1e8d535b9f9679e509c8a8a6d64c98e03c Mon Sep 17 00:00:00 2001 From: AhmedYasserrr Date: Mon, 21 Jul 2025 16:42:28 +0300 Subject: [PATCH 7/9] feat: add EC hello and charge state functions to ECController and Python bindings --- src/bindings/ECController.cc | 12 +++ src/bindings/ECController.h | 3 + src/bindings/PyECController.cc | 168 ++++++++++++++++++--------------- src/core/libectool.cc | 70 +++++++++++++- src/include/libectool.h | 9 ++ 5 files changed, 184 insertions(+), 78 deletions(-) diff --git a/src/bindings/ECController.cc b/src/bindings/ECController.cc index 3e00110..23a519f 100644 --- a/src/bindings/ECController.cc +++ b/src/bindings/ECController.cc @@ -26,6 +26,11 @@ void ECController::handle_error(int code, const std::string &msg) { throw std::runtime_error(msg + " (" + reason + ", code " + std::to_string(code) + ")"); } +int ECController::hello() { + int ret = ec_hello(); + return ret; +} + // ----------------------------------------------------------------------------- // Top-level Power Functions // ----------------------------------------------------------------------------- @@ -37,6 +42,13 @@ bool ECController::is_on_ac() { return ac; } +ec_charge_state_info ECController::get_charge_state() { + ec_charge_state_info info; + int ret = ec_get_charge_state(&info); + handle_error(ret, "Failed to get charge state"); + return info; +} + // ----------------------------------------------------------------------------- // Top-level fan control Functions // ----------------------------------------------------------------------------- diff --git a/src/bindings/ECController.h b/src/bindings/ECController.h index e042a68..eab9f96 100644 --- a/src/bindings/ECController.h +++ b/src/bindings/ECController.h @@ -6,7 +6,10 @@ class ECController { public: + int hello(); + bool is_on_ac(); + ec_charge_state_info get_charge_state(); int get_num_fans(); void enable_fan_auto_ctrl(int fan_idx); diff --git a/src/bindings/PyECController.cc b/src/bindings/PyECController.cc index 94bcaa8..896ecb1 100644 --- a/src/bindings/PyECController.cc +++ b/src/bindings/PyECController.cc @@ -18,87 +18,103 @@ py::dict temp_info_to_dict(const ec_temp_info& info) { return d; } +py::dict charge_state_to_dict(const ec_charge_state_info& info) { + py::dict d; + d["ac"] = static_cast(info.ac); + d["chg_voltage"] = info.chg_voltage; + d["chg_current"] = info.chg_current; + d["chg_input_current"] = info.chg_input_current; + d["batt_state_of_charge"] = info.batt_state_of_charge; + return d; +} + PYBIND11_MODULE(libectool_py, m) { m.doc() = "Python bindings for ectool"; py::class_(m, "ECController") - .def(py::init<>()) - .def("is_on_ac", &ECController::is_on_ac, "Check if on AC power") - - .def("get_num_fans", &ECController::get_num_fans, - "Get number of fans") - - .def("enable_fan_auto_ctrl", - &ECController::enable_fan_auto_ctrl, - "Enable auto control for a fan", - py::arg("fan_idx")) - - .def("enable_all_fans_auto_ctrl", - &ECController::enable_all_fans_auto_ctrl, - "Enable auto control for all fans") - - .def("set_fan_duty", - &ECController::set_fan_duty, - "Set fan duty cycle (0-100)", - py::arg("percent"), py::arg("fan_idx")) - - .def("set_all_fans_duty", - &ECController::set_all_fans_duty, - "Set all fans duty cycle (0-100)", - py::arg("percent")) - - .def("set_fan_rpm", - &ECController::set_fan_rpm, - "Set fan RPM", - py::arg("target_rpm"), py::arg("fan_idx")) - - .def("set_all_fans_rpm", - &ECController::set_all_fans_rpm, - "Set all fans RPM", - py::arg("target_rpm")) - - .def("get_fan_rpm", - &ECController::get_fan_rpm, - "Get single fan RPM", - py::arg("fan_idx")) - - .def("get_all_fans_rpm", - [](ECController &self) { - return py::cast(self.get_all_fans_rpm()); - }, - "Get all fans RPM as list") - - .def("get_num_temp_sensors", - &ECController::get_num_temp_sensors, - "Get number of temperature sensors") - - .def("get_temp", - &ECController::get_temp, - "Get temperature in Celsius for one sensor", - py::arg("sensor_idx")) - - .def("get_all_temps", - [](ECController &self) { - return py::cast(self.get_all_temps()); - }, - "Get all temperature values as list") - - .def("get_max_temp", - &ECController::get_max_temp, - "Get maximum temperature across all sensors") - - .def("get_max_non_battery_temp", - &ECController::get_max_non_battery_temp, - "Get maximum non-battery temperature") - - .def("get_temp_info", - [](ECController &self, int sensor_idx) { - ec_temp_info info = self.get_temp_info(sensor_idx); - return temp_info_to_dict(info); - }, - "Get detailed temperature info for a sensor", - py::arg("sensor_idx")); + .def(py::init<>()) + .def("hello", &ECController::hello, "Send hello command to EC") + + .def("is_on_ac", &ECController::is_on_ac, "Check if on AC power") + + .def("get_charge_state", [](ECController& self) { + return charge_state_to_dict(self.get_charge_state()); + }, "Get charge state info") + + .def("get_num_fans", &ECController::get_num_fans, + "Get number of fans") + + .def("enable_fan_auto_ctrl", + &ECController::enable_fan_auto_ctrl, + "Enable auto control for a fan", + py::arg("fan_idx")) + + .def("enable_all_fans_auto_ctrl", + &ECController::enable_all_fans_auto_ctrl, + "Enable auto control for all fans") + + .def("set_fan_duty", + &ECController::set_fan_duty, + "Set fan duty cycle (0-100)", + py::arg("percent"), py::arg("fan_idx")) + + .def("set_all_fans_duty", + &ECController::set_all_fans_duty, + "Set all fans duty cycle (0-100)", + py::arg("percent")) + + .def("set_fan_rpm", + &ECController::set_fan_rpm, + "Set fan RPM", + py::arg("target_rpm"), py::arg("fan_idx")) + + .def("set_all_fans_rpm", + &ECController::set_all_fans_rpm, + "Set all fans RPM", + py::arg("target_rpm")) + + .def("get_fan_rpm", + &ECController::get_fan_rpm, + "Get single fan RPM", + py::arg("fan_idx")) + + .def("get_all_fans_rpm", + [](ECController &self) { + return py::cast(self.get_all_fans_rpm()); + }, + "Get all fans RPM as list") + + .def("get_num_temp_sensors", + &ECController::get_num_temp_sensors, + "Get number of temperature sensors") + + .def("get_temp", + &ECController::get_temp, + "Get temperature in Celsius for one sensor", + py::arg("sensor_idx")) + + .def("get_all_temps", + [](ECController &self) { + return py::cast(self.get_all_temps()); + }, + "Get all temperature values as list") + + .def("get_max_temp", + &ECController::get_max_temp, + "Get maximum temperature across all sensors") + + .def("get_max_non_battery_temp", + &ECController::get_max_non_battery_temp, + "Get maximum non-battery temperature") + + .def("get_temp_info", + [](ECController &self, int sensor_idx) { + ec_temp_info info = self.get_temp_info(sensor_idx); + return temp_info_to_dict(info); + }, + "Get detailed temperature info for a sensor", + py::arg("sensor_idx")); #ifdef VERSION_INFO m.attr("__version__") = MACRO_STRINGIFY(VERSION_INFO); diff --git a/src/core/libectool.cc b/src/core/libectool.cc index cff9c7e..2148641 100644 --- a/src/core/libectool.cc +++ b/src/core/libectool.cc @@ -111,6 +111,43 @@ int read_mapped_temperature(int id) return (ret <= 0) ? EC_TEMP_SENSOR_ERROR : val; } +// Charge state parameter count table +#define ST_FLD_SIZE(ST, FLD) sizeof(((struct ST *)0)->FLD) +#define ST_CMD_SIZE ST_FLD_SIZE(struct ec_params_charge_state, cmd) +#define ST_PRM_SIZE(SUBCMD) (ST_CMD_SIZE + ST_FLD_SIZE(struct ec_params_charge_state, SUBCMD)) +#define ST_RSP_SIZE(SUBCMD) ST_FLD_SIZE(struct ec_response_charge_state, SUBCMD) + +static const struct { + uint8_t to_ec_size; + uint8_t from_ec_size; +} cs_paramcount[] = { + [CHARGE_STATE_CMD_GET_STATE] = { ST_CMD_SIZE, ST_RSP_SIZE(get_state) }, + [CHARGE_STATE_CMD_GET_PARAM] = { ST_PRM_SIZE(get_param), ST_RSP_SIZE(get_param) }, + [CHARGE_STATE_CMD_SET_PARAM] = { ST_PRM_SIZE(set_param), 0 }, +}; + +BUILD_ASSERT(ARRAY_SIZE(cs_paramcount) == CHARGE_STATE_NUM_CMDS); + +#undef ST_CMD_SIZE +#undef ST_PRM_SIZE +#undef ST_RSP_SIZE + +// Wrapper to send EC_CMD_CHARGE_STATE with correct sizes +static int cs_do_cmd(struct ec_params_charge_state *to_ec, + struct ec_response_charge_state *from_ec) +{ + int rv; + int cmd = to_ec->cmd; + + if (cmd < 0 || cmd >= CHARGE_STATE_NUM_CMDS) + return 1; + + rv = ec_command(EC_CMD_CHARGE_STATE, 0, + to_ec, cs_paramcount[cmd].to_ec_size, + from_ec, cs_paramcount[cmd].from_ec_size); + return (rv < 0) ? 1 : 0; +} + // ----------------------------------------------------------------------------- // Top-level General Functions // ----------------------------------------------------------------------------- @@ -167,6 +204,35 @@ int ec_is_on_ac(int *ac_present) { return 0; } +int ec_get_charge_state(struct ec_charge_state_info *info_out) { + struct ec_params_charge_state param; + struct ec_response_charge_state resp; + int ret; + + if (!info_out) + return EC_ERR_INVALID_PARAM; + + ret = libectool_init(); + if (ret < 0) + return EC_ERR_INIT; + + param.cmd = CHARGE_STATE_CMD_GET_STATE; + ret = cs_do_cmd(¶m, &resp); + if (ret) { + libectool_release(); + return EC_ERR_EC_COMMAND; + } + + info_out->ac = resp.get_state.ac; + info_out->chg_voltage = resp.get_state.chg_voltage; + info_out->chg_current = resp.get_state.chg_current; + info_out->chg_input_current = resp.get_state.chg_input_current; + info_out->batt_state_of_charge = resp.get_state.batt_state_of_charge; + + libectool_release(); + return 0; +} + // ----------------------------------------------------------------------------- // Top-level fan control Functions // ----------------------------------------------------------------------------- @@ -192,7 +258,7 @@ int ec_get_num_fans(int *val) { if (ret <= 0) return EC_ERR_READMEM; - + if ((int)fan_val == EC_FAN_SPEED_NOT_PRESENT) break; } @@ -429,7 +495,7 @@ int ec_get_all_fans_rpm(int *rpms, int rpms_size, int *num_fans_out) { default: rpms[i] = val; } - + } libectool_release(); diff --git a/src/include/libectool.h b/src/include/libectool.h index 8d833a6..b08dd74 100644 --- a/src/include/libectool.h +++ b/src/include/libectool.h @@ -24,6 +24,14 @@ struct ec_temp_info { int temp_fan_max; }; +struct ec_charge_state_info { + int ac; + int chg_voltage; + int chg_current; + int chg_input_current; + int batt_state_of_charge; +}; + // Library init/release int libectool_init(); void libectool_release(); @@ -32,6 +40,7 @@ void libectool_release(); int ec_hello(); int ec_is_on_ac(int *ac_present); +int ec_get_charge_state(struct ec_charge_state_info *info_out); int ec_get_num_fans(int *val); int ec_enable_fan_auto_ctrl(int fan_idx); From 068409ec9400e8921a4e194a08800e111d1d7a96 Mon Sep 17 00:00:00 2001 From: AhmedYasserrr Date: Mon, 21 Jul 2025 09:11:56 -0700 Subject: [PATCH 8/9] feat: add charge state retrieval to ECController and update related definitions --- pyectool/__init__.pyi | 9 +++++++++ src/core/libectool.cc | 6 +++--- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/pyectool/__init__.pyi b/pyectool/__init__.pyi index eef7164..910d0b8 100644 --- a/pyectool/__init__.pyi +++ b/pyectool/__init__.pyi @@ -10,9 +10,18 @@ class ECTempInfo(dict[str, int | str]): temp_fan_off: int temp_fan_max: int +class ECChargeStateInfo(dict[str, int]): + ac: int + chg_voltage: int + chg_current: int + chg_input_current: int + batt_state_of_charge: int + class ECController: def __init__(self) -> None: ... + def hello(self) -> None: ... def is_on_ac(self) -> bool: ... + def get_charge_state(self) -> ECChargeStateInfo: ... def get_num_fans(self) -> int: ... def enable_fan_auto_ctrl(self, fan_idx: int) -> None: ... def enable_all_fans_auto_ctrl(self) -> None: ... diff --git a/src/core/libectool.cc b/src/core/libectool.cc index 2148641..c8d7788 100644 --- a/src/core/libectool.cc +++ b/src/core/libectool.cc @@ -113,9 +113,9 @@ int read_mapped_temperature(int id) // Charge state parameter count table #define ST_FLD_SIZE(ST, FLD) sizeof(((struct ST *)0)->FLD) -#define ST_CMD_SIZE ST_FLD_SIZE(struct ec_params_charge_state, cmd) -#define ST_PRM_SIZE(SUBCMD) (ST_CMD_SIZE + ST_FLD_SIZE(struct ec_params_charge_state, SUBCMD)) -#define ST_RSP_SIZE(SUBCMD) ST_FLD_SIZE(struct ec_response_charge_state, SUBCMD) +#define ST_CMD_SIZE ST_FLD_SIZE(ec_params_charge_state, cmd) +#define ST_PRM_SIZE(SUBCMD) (ST_CMD_SIZE + ST_FLD_SIZE(ec_params_charge_state, SUBCMD)) +#define ST_RSP_SIZE(SUBCMD) ST_FLD_SIZE(ec_response_charge_state, SUBCMD) static const struct { uint8_t to_ec_size; From 56e1c710b2b6c256b9f4e15c65dae86b541c3f30 Mon Sep 17 00:00:00 2001 From: AhmedYasserrr Date: Thu, 31 Jul 2025 03:17:39 -0700 Subject: [PATCH 9/9] feat: enhance get_temp_info test to validate pyectool output --- tests/test_pyectool.py | 28 +++++++++++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/tests/test_pyectool.py b/tests/test_pyectool.py index a5659e0..07cbe37 100644 --- a/tests/test_pyectool.py +++ b/tests/test_pyectool.py @@ -76,7 +76,33 @@ def test_get_num_temp_sensors(): def test_get_temp_info(): py_info = ec.get_temp_info(0) - print(f"[get_temp_info] pyectool={py_info}") + + tempsinfo_out = run_ectool_command("ectool tempsinfo 0") + temps_out = run_ectool_command("ectool temps 0") + + # Parse ectool tempsinfo + name_match = re.search(r"Sensor name:\s*(\S+)", tempsinfo_out) + type_match = re.search(r"Sensor type:\s*(\d+)", tempsinfo_out) + + # Parse ectool temps + temp_match = re.search(r"= (\d+)\s*C", temps_out) + fan_vals_match = re.search(r"\((\d+)\s*K and (\d+)\s*K\)", temps_out) + + assert name_match and type_match and temp_match and fan_vals_match, "Failed to parse ectool output" + + ectool_info = { + "sensor_name": name_match.group(1), + "sensor_type": int(type_match.group(1)), + "temp": int(temp_match.group(1)), + "temp_fan_off": int(int(fan_vals_match.group(1)) - 273), + "temp_fan_max": int(int(fan_vals_match.group(2)) - 273), + } + + print(f"[get_temp_info] pyectool={py_info}, ectool={ectool_info}") + + # Assert fields match + for key in ectool_info: + assert py_info[key] == ectool_info[key], f"Mismatch in '{key}': pyectool={py_info[key]}, ectool={ectool_info[key]}" def test_get_all_fans_rpm(): py_vals = ec.get_all_fans_rpm()