From 99802e73cfce282c51e776de96bca274dcabc0ef Mon Sep 17 00:00:00 2001 From: Yasser Date: Sun, 6 Jul 2025 18:24:52 +0300 Subject: [PATCH 01/13] [Refactor] Error Handling, C++ Wrapper, and Pybind11 Bindings (#2) * Refactored C ectool functions to return error codes instead of printing or exiting. * Added standard error codes (e.g. `EC_ERR_INIT`, `EC_ERR_READMEM`). * Introduced a C++ `ECController` class to wrap the C API, throwing C++ exceptions with clear error messages. * Exposed `ECController` to Python using pybind11 bindings. * Updated CMake to support the new C++ and pybind11 build flow. --- pyectool/__init__.py | 22 +--- pyectool/__init__.pyi | 16 ++- src/bindings/CMakeLists.txt | 5 +- src/bindings/ECController.cc | 49 ++++++++ src/bindings/ECController.h | 15 +++ src/bindings/PyECController.cc | 29 +++++ src/bindings/libectool_py.cc | 31 ----- src/core/libectool.cc | 212 +++++++++++++++------------------ src/include/libectool.h | 16 ++- 9 files changed, 212 insertions(+), 183 deletions(-) create mode 100644 src/bindings/ECController.cc create mode 100644 src/bindings/ECController.h create mode 100644 src/bindings/PyECController.cc delete mode 100644 src/bindings/libectool_py.cc diff --git a/pyectool/__init__.py b/pyectool/__init__.py index e1bd152..e4e6df5 100644 --- a/pyectool/__init__.py +++ b/pyectool/__init__.py @@ -1,27 +1,9 @@ from __future__ import annotations -from .libectool_py import ( - __doc__, - __version__, - ascii_mode, - auto_fan_control, - get_max_non_battery_temperature, - get_max_temperature, - init, - is_on_ac, - release, - set_fan_duty, -) +from .libectool_py import __doc__, __version__, ECController __all__ = [ "__doc__", "__version__", - "ascii_mode", - "auto_fan_control", - "get_max_non_battery_temperature", - "get_max_temperature", - "init", - "is_on_ac", - "release", - "set_fan_duty", + "ECController", ] diff --git a/pyectool/__init__.pyi b/pyectool/__init__.pyi index 7337574..1d562ab 100644 --- a/pyectool/__init__.pyi +++ b/pyectool/__init__.pyi @@ -3,12 +3,10 @@ from __future__ import annotations __doc__: str __version__: str -def init() -> None: ... -def release() -> None: ... -def is_on_ac() -> bool: ... -def auto_fan_control() -> None: ... -def set_fan_duty(duty: int) -> None: ... -def get_max_temperature() -> float: ... -def get_max_non_battery_temperature() -> float: ... - -ascii_mode: bool +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 diff --git a/src/bindings/CMakeLists.txt b/src/bindings/CMakeLists.txt index 8b3dd75..788b69b 100644 --- a/src/bindings/CMakeLists.txt +++ b/src/bindings/CMakeLists.txt @@ -1,7 +1,8 @@ # Create the Python module -python_add_library(libectool_py MODULE libectool_py.cc WITH_SOABI) +python_add_library(libectool_py MODULE PyECController.cc ECController.cc + WITH_SOABI) # Link against required libraries target_link_libraries(libectool_py PRIVATE pybind11::headers libectool) -target_include_directories(libectool_py PUBLIC ../include) +target_include_directories(libectool_py PRIVATE . ../include) target_compile_definitions(libectool_py PUBLIC VERSION_INFO=${PROJECT_VERSION}) diff --git a/src/bindings/ECController.cc b/src/bindings/ECController.cc new file mode 100644 index 0000000..96e616a --- /dev/null +++ b/src/bindings/ECController.cc @@ -0,0 +1,49 @@ +#include "ECController.h" +#include "libectool.h" + +void ECController::handle_error(int code, const std::string &msg) { + if (code == 0) return; + + std::string reason; + switch (code) { + case EC_ERR_INIT: reason = "EC initialization failed"; break; + 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; + default: reason = "Unknown error"; break; + } + + throw std::runtime_error(msg + " (" + reason + ", code " + std::to_string(code) + ")"); +} + + +bool ECController::is_on_ac() { + int ac; + int ret = ec_is_on_ac(&ac); + handle_error(ret, "Failed to read AC status"); + return ac; +} + +void ECController::auto_fan_control() { + int ret = ec_auto_fan_control(); + handle_error(ret, "Failed to enable auto fan control"); +} + +void ECController::set_fan_duty(int duty) { + int ret = ec_set_fan_duty(duty); + handle_error(ret, "Failed to set fan duty"); +} + +float ECController::get_max_temperature() { + float t; + int ret = ec_get_max_temperature(&t); + handle_error(ret, "Failed to get max temperature"); + return t; +} + +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; +} diff --git a/src/bindings/ECController.h b/src/bindings/ECController.h new file mode 100644 index 0000000..969ed54 --- /dev/null +++ b/src/bindings/ECController.h @@ -0,0 +1,15 @@ +#pragma once +#include +#include + +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(); + +private: + void handle_error(int code, const std::string &msg); +}; diff --git a/src/bindings/PyECController.cc b/src/bindings/PyECController.cc new file mode 100644 index 0000000..e6d9898 --- /dev/null +++ b/src/bindings/PyECController.cc @@ -0,0 +1,29 @@ +#include +#include "ECController.h" + +#define STRINGIFY(x) #x +#define MACRO_STRINGIFY(x) STRINGIFY(x) + +namespace py = pybind11; + +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"); + +#ifdef VERSION_INFO + m.attr("__version__") = MACRO_STRINGIFY(VERSION_INFO); +#else + m.attr("__version__") = "dev"; +#endif +} diff --git a/src/bindings/libectool_py.cc b/src/bindings/libectool_py.cc deleted file mode 100644 index f77272a..0000000 --- a/src/bindings/libectool_py.cc +++ /dev/null @@ -1,31 +0,0 @@ -#include -#include "libectool.h" - -#define STRINGIFY(x) #x -#define MACRO_STRINGIFY(x) STRINGIFY(x) - -namespace py = pybind11; - -PYBIND11_MODULE(libectool_py, m) { - m.doc() = "Python bindings for ectool"; - - // Optional: expose init/release explicitly - m.def("init", &libectool_init, "Initialize libectool"); - m.def("release", &libectool_release, "Release libectool"); - - // Expose API functions - m.def("is_on_ac", &is_on_ac, "Check if on AC power"); - m.def("auto_fan_control", &auto_fan_control, "Enable automatic fan control"); - m.def("set_fan_duty", &set_fan_duty, py::arg("duty"), "Set fan duty cycle (0-100)"); - m.def("get_max_temperature", &get_max_temperature, "Get max temperature"); - m.def("get_max_non_battery_temperature", &get_max_non_battery_temperature, "Get max non-battery temperature"); - - // Expose global variable ascii_mode - m.attr("ascii_mode") = py::cast(&ascii_mode, py::return_value_policy::reference); - - #ifdef VERSION_INFO - m.attr("__version__") = MACRO_STRINGIFY(VERSION_INFO); - #else - m.attr("__version__") = "dev"; - #endif -} diff --git a/src/core/libectool.cc b/src/core/libectool.cc index 8c8c64f..849cfd3 100644 --- a/src/core/libectool.cc +++ b/src/core/libectool.cc @@ -49,21 +49,18 @@ int libectool_init() /* For non-USB alt interfaces, we need to acquire the GEC lock */ if (!(interfaces & COMM_USB) && acquire_gec_lock(GEC_LOCK_TIMEOUT_SECS) < 0) { - fprintf(stderr, "Could not acquire GEC lock.\n"); return -1; } /* If the interface is set to USB, try that (no lock needed) */ if (interfaces == COMM_USB) { #ifndef _WIN32 if (comm_init_usb(vid, pid)) { - fprintf(stderr, "Couldn't find EC on USB.\n"); /* Release the lock if it was acquired */ release_gec_lock(); return -1; } #endif } else if (comm_init_alt(interfaces, device_name, i2c_bus)) { - fprintf(stderr, "Couldn't find EC\n"); release_gec_lock(); return -1; } @@ -71,7 +68,6 @@ int libectool_init() /* Initialize ring buffers for sending/receiving EC commands */ if (comm_init_buffer()) { - fprintf(stderr, "Couldn't initialize buffers\n"); release_gec_lock(); return -1; } @@ -91,174 +87,158 @@ void libectool_release() #endif } -static uint8_t read_mapped_mem8(uint8_t offset) +int read_mapped_temperature(int id) { int ret; uint8_t val; - ret = ec_readmem(offset, sizeof(val), &val); - if (ret <= 0) { - fprintf(stderr, "failure in %s(): %d\n", __func__, ret); - exit(1); - } - return val; -} + ret = ec_readmem(EC_MEMMAP_THERMAL_VERSION, sizeof(val), &val); + if (ret <= 0 || val == 0) + return EC_TEMP_SENSOR_NOT_PRESENT; -int read_mapped_temperature(int id) -{ - int rv; - - if (!read_mapped_mem8(EC_MEMMAP_THERMAL_VERSION)) { - /* - * The temp_sensor_init() is not called, which implies no - * temp sensor is defined. - */ - rv = EC_TEMP_SENSOR_NOT_PRESENT; - } else if (id < EC_TEMP_SENSOR_ENTRIES) - rv = read_mapped_mem8(EC_MEMMAP_TEMP_SENSOR + id); - else if (read_mapped_mem8(EC_MEMMAP_THERMAL_VERSION) >= 2) - rv = read_mapped_mem8(EC_MEMMAP_TEMP_SENSOR_B + id - - EC_TEMP_SENSOR_ENTRIES); - else { - /* Sensor in second bank, but second bank isn't supported */ - rv = EC_TEMP_SENSOR_NOT_PRESENT; + if (id < EC_TEMP_SENSOR_ENTRIES) { + ret = ec_readmem(EC_MEMMAP_TEMP_SENSOR + id, sizeof(val), &val); + return (ret <= 0) ? EC_TEMP_SENSOR_ERROR : val; } - return rv; + + // Check if second bank is supported + if (val < 2) + return EC_TEMP_SENSOR_NOT_PRESENT; + + ret = ec_readmem( + EC_MEMMAP_TEMP_SENSOR_B + id - EC_TEMP_SENSOR_ENTRIES, + sizeof(val), &val); + return (ret <= 0) ? EC_TEMP_SENSOR_ERROR : val; } // ----------------------------------------------------------------------------- // Top-level endpoint functions // ----------------------------------------------------------------------------- -bool is_on_ac() { - if (libectool_init() < 0) - fprintf(stderr, "Failed initializing EC connection\n"); +int ec_is_on_ac(int *ac_present) { + int ret; + uint8_t flags; + + if (!ac_present) + return EC_ERR_INVALID_PARAM; - uint8_t flags = read_mapped_mem8(EC_MEMMAP_BATT_FLAG); - bool ac_present = (flags & EC_BATT_FLAG_AC_PRESENT); + ret = libectool_init(); + if (ret < 0) + return EC_ERR_INIT; - libectool_release(); + ret = ec_readmem(EC_MEMMAP_BATT_FLAG, sizeof(flags), &flags); - return ac_present; -} + if (ret <= 0) { + libectool_release(); + return EC_ERR_READMEM; + } -void auto_fan_control() { - if (libectool_init() < 0) - fprintf(stderr, "Failed initializing EC connection\n"); + *ac_present = !!(flags & EC_BATT_FLAG_AC_PRESENT); + libectool_release(); + return 0; +} - int rv = ec_command(EC_CMD_THERMAL_AUTO_FAN_CTRL, 0, NULL, 0, NULL, 0); +int ec_auto_fan_control() { + int ret = libectool_init(); + if (ret < 0) + return EC_ERR_INIT; - if (rv < 0) - fprintf(stderr, "Failed to enable auto fan control\n"); + ret = ec_command(EC_CMD_THERMAL_AUTO_FAN_CTRL, 0, NULL, 0, NULL, 0); libectool_release(); + if (ret < 0) + return EC_ERR_EC_COMMAND; + return 0; } -void set_fan_duty(int duty) { - if (libectool_init() < 0) - fprintf(stderr, "Failed initializing EC connection\n"); +int ec_set_fan_duty(int duty) { + if (duty < 0 || duty > 100) + return EC_ERR_INVALID_PARAM; - struct ec_params_pwm_set_fan_duty_v0 p_v0; - int rv; - - if (duty < 0 || duty > 100) { - fprintf(stderr, "Error: Fan duty cycle must be between 0 and 100.\n"); - return; - } + int ret = libectool_init(); + if (ret < 0) + return EC_ERR_INIT; + struct ec_params_pwm_set_fan_duty_v0 p_v0; p_v0.percent = duty; - rv = ec_command(EC_CMD_PWM_SET_FAN_DUTY, 0, &p_v0, sizeof(p_v0), + ret = ec_command(EC_CMD_PWM_SET_FAN_DUTY, 0, &p_v0, sizeof(p_v0), NULL, 0); - if (rv < 0) - fprintf(stderr, "Error: Can't set duty cycle\n"); libectool_release(); + if (ret < 0) + return EC_ERR_EC_COMMAND; + return 0; } -// Get the maximum temperature from all sensors -float get_max_temperature() { - if (libectool_init() < 0) - fprintf(stderr, "Failed initializing EC connection\n"); +int ec_get_max_temperature(float *max_temp) { + if (!max_temp) + return EC_ERR_INVALID_PARAM; - float max_temp = -1.0f; + int ret = libectool_init(); + if (ret < 0) + return EC_ERR_INIT; + + float t = -1.0f; int mtemp, temp; int id; for (id = 0; id < EC_MAX_TEMP_SENSOR_ENTRIES; id++) { mtemp = read_mapped_temperature(id); switch (mtemp) { - case EC_TEMP_SENSOR_NOT_PRESENT: - break; - case EC_TEMP_SENSOR_ERROR: - fprintf(stderr, "Sensor %d error\n", id); - break; - case EC_TEMP_SENSOR_NOT_POWERED: - fprintf(stderr, "Sensor %d disabled\n", id); - break; - case EC_TEMP_SENSOR_NOT_CALIBRATED: - fprintf(stderr, "Sensor %d not calibrated\n", - id); - break; - default: - temp = K_TO_C(mtemp + EC_TEMP_SENSOR_OFFSET); - } - - if (temp > max_temp) { - max_temp = temp; + 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: + temp = K_TO_C(mtemp + EC_TEMP_SENSOR_OFFSET); + if (temp > t) + t = temp; } } + libectool_release(); - return max_temp; + + if (t < 0) + return EC_ERR_READMEM; + *max_temp = t; + return 0; } -float get_max_non_battery_temperature() +int ec_get_max_non_battery_temperature(float *max_temp) { - if (libectool_init() < 0) - fprintf(stderr, "Failed initializing EC connection\n"); + if (!max_temp) + return EC_ERR_INVALID_PARAM; + + int ret = libectool_init(); + if (ret < 0) + return EC_ERR_INIT; struct ec_params_temp_sensor_get_info p; struct ec_response_temp_sensor_get_info r; - int rv; - float max_temp = -1.0f; + float t = -1.0f; int mtemp, temp; - int id; for (p.id = 0; p.id < EC_MAX_TEMP_SENSOR_ENTRIES; p.id++) { - if (read_mapped_temperature(p.id) == EC_TEMP_SENSOR_NOT_PRESENT) + mtemp = read_mapped_temperature(p.id); + if (mtemp < 0) continue; - rv = ec_command(EC_CMD_TEMP_SENSOR_GET_INFO, 0, &p, + ret = ec_command(EC_CMD_TEMP_SENSOR_GET_INFO, 0, &p, sizeof(p), &r, sizeof(r)); - if (rv < 0) + if (ret < 0) continue; - printf("%d: %d %s\n", p.id, r.sensor_type, - r.sensor_name); - - if(strcmp(r.sensor_name, "Battery")){ // not eqaul to battery - mtemp = read_mapped_temperature(p.id); - switch (mtemp) { - case EC_TEMP_SENSOR_NOT_PRESENT: - break; - case EC_TEMP_SENSOR_ERROR: - fprintf(stderr, "Sensor %d error\n", id); - break; - case EC_TEMP_SENSOR_NOT_POWERED: - fprintf(stderr, "Sensor %d disabled\n", id); - break; - case EC_TEMP_SENSOR_NOT_CALIBRATED: - fprintf(stderr, "Sensor %d not calibrated\n", - id); - break; - default: - temp = K_TO_C(mtemp + EC_TEMP_SENSOR_OFFSET); - } + if(strcmp(r.sensor_name, "Battery")){ temp = K_TO_C(mtemp + EC_TEMP_SENSOR_OFFSET); - if (temp > max_temp) { - max_temp = temp; - } + if (temp > t) + t = temp; } } libectool_release(); - return max_temp; + + if (t < 0) + return EC_ERR_READMEM; + *max_temp = t; + return 0; } diff --git a/src/include/libectool.h b/src/include/libectool.h index 531bf8f..ebc6fbf 100644 --- a/src/include/libectool.h +++ b/src/include/libectool.h @@ -3,6 +3,12 @@ #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 + #ifdef __cplusplus extern "C" { #endif @@ -12,11 +18,11 @@ int libectool_init(); void libectool_release(); // API functions to expose -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 ec_is_on_ac(int *ac_present); +int ec_auto_fan_control(); +int ec_set_fan_duty(int duty); +int ec_get_max_temperature(float *max_temp); +int ec_get_max_non_battery_temperature(float *max_temp); /* ASCII mode for printing, default off */ extern int ascii_mode; From b0f86b6dcdb050505bc863e31b0b17b8c32cb056 Mon Sep 17 00:00:00 2001 From: AhmedYasserrr Date: Sun, 6 Jul 2025 23:17:29 +0300 Subject: [PATCH 02/13] 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 03/13] 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 04/13] 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 05/13] 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 06/13] 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 07/13] 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 08/13] 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 09/13] 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 10/13] 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() From 043647c38b95306b0c63c3934e9810c23852a40e Mon Sep 17 00:00:00 2001 From: AhmedYasserrr Date: Thu, 31 Jul 2025 23:37:35 +0300 Subject: [PATCH 11/13] docs: update README --- README.md | 50 +++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 41 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 4f879be..44b815f 100644 --- a/README.md +++ b/README.md @@ -43,7 +43,7 @@ Instead, test from another location, e.g.: ```sh cd .. -sudo python -c "import pyectool; print(pyectool.is_on_ac())" +sudo python -c "from pyectool import ECController; ec = ECController(); print(ec.is_on_ac())" ``` ## VENV INSTALLATION @@ -66,15 +66,47 @@ pip install . ### Test from outside the repo dir ```bash cd .. -sudo env "PATH=$PATH" python -c "import pyectool; print(pyectool.is_on_ac())" +sudo env "PATH=$PATH" python -c "from pyectool import ECController; ec = ECController(); print(ec.is_on_ac())" ``` ### Available Functions -| Function | Description | -| ------------------------------------------ | -------------------------------------------------------------------------------- | -| `auto_fan_control()` | Enables automatic fan control by the EC. | -| `get_max_non_battery_temperature() -> float` | Returns the highest temperature (in °C) from all sensors except the battery. | -| `get_max_temperature() -> float` | Returns the highest temperature (in °C) from all EC sensors including battery. | -| `is_on_ac() -> bool` | Checks whether the device is running on AC power. | -| `set_fan_duty(percent: int)` | Sets the fan duty cycle manually (0–100%). | \ No newline at end of file +All functions are methods of the `ECController` class. Instantiate it like so: + +```python +from pyectool import ECController +ec = ECController() +``` + +Then use the methods as shown below: + +| Method | Description | +| ------------------------------------------------------- | ------------------------------------------------------------------------- | +| `ec.is_on_ac() -> bool` | Returns `True` if the system is on AC power, else `False`. | +| `ec.get_num_fans() -> int` | Returns the number of fan devices detected. | +| `ec.enable_fan_auto_ctrl(fan_idx: int) -> None` | Enables automatic fan control for a specific fan. | +| `ec.enable_all_fans_auto_ctrl() -> None` | Enables automatic control for all fans. | +| `ec.set_fan_duty(percent: int, fan_idx: int) -> None` | Sets fan duty (speed) as a percentage for a specific fan. | +| `ec.set_all_fans_duty(percent: int) -> None` | Sets the same duty percentage for all fans. | +| `ec.set_fan_rpm(target_rpm: int, fan_idx: int) -> None` | Sets a specific RPM target for a specific fan. | +| `ec.set_all_fans_rpm(target_rpm: int) -> None` | Sets the same RPM target for all fans. | +| `ec.get_fan_rpm(fan_idx: int) -> int` | Returns current RPM of a specific fan. | +| `ec.get_all_fans_rpm() -> list[int]` | Returns a list of current RPM values for all fans. | +| `ec.get_num_temp_sensors() -> int` | Returns the total number of temperature sensors detected. | +| `ec.get_temp(sensor_idx: int) -> int` | Returns the temperature (in °C) for the given sensor index. | +| `ec.get_all_temps() -> list[int]` | Returns a list of all sensor temperatures (in °C). | +| `ec.get_max_temp() -> int` | Returns the highest temperature across all sensors. | +| `ec.get_max_non_battery_temp() -> int` | Returns the highest temperature excluding battery-related sensors. | +| `ec.get_temp_info(sensor_idx: int) -> ECTempInfo` | Returns detailed info for a sensor, including name, type, and thresholds. | + +--- + +### `ECTempInfo` + +Returned by `get_temp_info()`, acts like a `dict` with: + +* `sensor_name`: str +* `sensor_type`: int +* `temp`: int +* `temp_fan_off`: int +* `temp_fan_max`: int From 16bbf2038817211c2f6f394d13b5df08d94fcf15 Mon Sep 17 00:00:00 2001 From: Yasser Date: Sun, 3 Aug 2025 17:11:13 +0300 Subject: [PATCH 12/13] Update README.md --- README.md | 74 ++++++++++++++++++++++++++++++------------------------- 1 file changed, 41 insertions(+), 33 deletions(-) diff --git a/README.md b/README.md index 44b815f..7ee3ae9 100644 --- a/README.md +++ b/README.md @@ -1,23 +1,26 @@ # Pyectool -**Pyectool** is a Python package with C++ bindings for interacting with the Embedded Controller (EC) on ChromeOS and Framework devices. It is extracted from and based on [`ectool`](https://gitlab.howett.net/DHowett/ectool) utility, and exposes EC control functions directly to Python programs via a native extension. +**Pyectool** provides Python bindings for interacting with the Embedded Controller (EC) on ChromeOS and Framework devices. +It is extracted from and based on [Dustin Howett's `ectool`](https://gitlab.howett.net/DHowett/ectool) and exposes EC control functions directly to Python via a native C++ extension built with `pybind11`. -## Features +Pyectool also provides a simple way to build the original `ectool` CLI tool, or to build `libectool`—a standalone C library that wrap most of ectool’s functionality, making it reusable in C/C++ projects or accessible from other languages. Both the CLI binary and the library are built automatically during installation. -- Python bindings for EC functionality using `pybind11`. +## Features +- Python-native interface to low-level EC functionality via `pybind11` - Supports fan duty control, temperature reading, AC power status, and more. -- Designed for integration with hardware management or fan control tools. -- Shared core logic with `libectool` for C/C++ integration. +- Designed for hardware monitoring, thermal management, and fan control tooling. +- Bundles the native `ectool` CLI and `libectool` C library alongside the Python package: + * `pyectool/bin/ectool` (ectool CLI) + * `pyectool/lib/libectool.a` (libectool static library) + * `pyectool/include/libectool.h` (libectool C header) --- -## Build & Install (Python Package) - -We use [`scikit-build-core`](https://scikit-build-core.readthedocs.io/en/latest/) to build the C++ extension via CMake. +## Installation ### Prerequisites -Install the required system dependencies: +Install system dependencies: ```sh sudo apt update @@ -25,7 +28,9 @@ sudo apt install -y libusb-1.0-0-dev libftdi1-dev pkg-config ```` ### Clone the repository -## Install system-wide +### Install the package + +#### Option 1: System-wide (not recommended unless you know what you're doing) ```sh sudo pip install . ``` @@ -36,49 +41,45 @@ sudo env "PIP_BREAK_SYSTEM_PACKAGES=1" pip install . ``` (Required on modern distros like Ubuntu 24.04 due to PEP 668.) -### Test from outside the repo dir -After installing, **do not run Python from the `libectool/` directory**, since it contains a `pyectool/` folder that may shadow the installed package. - -Instead, test from another location, e.g.: - -```sh -cd .. -sudo python -c "from pyectool import ECController; ec = ECController(); print(ec.is_on_ac())" -``` - -## VENV INSTALLATION - -If you **don’t** want to touch system Python: - -### Create venv - +#### Option 2: Isolated virtual environment (recommended) ```bash python3 -m venv ~/.venv/pyectool source ~/.venv/pyectool/bin/activate +pip install . ``` -### Install your package +### ⚠️ Important Note + +After installation, **do not run Python from inside the `libectool/` directory**. It contains a `pyectool/` folder that may shadow the installed package. + +Instead, test from a different directory: -Inside the venv: ```bash -pip install . +cd .. +python -c "from pyectool import ECController; ec = ECController(); print(ec.is_on_ac())" ``` -### Test from outside the repo dir + +If you're using a virtual environment and want to preserve its `PATH`, use: ```bash cd .. sudo env "PATH=$PATH" python -c "from pyectool import ECController; ec = ECController(); print(ec.is_on_ac())" ``` +This ensures the correct Python from your virtual environment is used even with `sudo`. + +--- -### Available Functions +## Usage -All functions are methods of the `ECController` class. Instantiate it like so: +### Create an EC controller instance ```python from pyectool import ECController + ec = ECController() ``` -Then use the methods as shown below: +### Available Methods + | Method | Description | | ------------------------------------------------------- | ------------------------------------------------------------------------- | @@ -110,3 +111,10 @@ Returned by `get_temp_info()`, acts like a `dict` with: * `temp`: int * `temp_fan_off`: int * `temp_fan_max`: int + +--- + +## License + +BSD 3-Clause License +See the `LICENSE` file for full terms. From 9de391d52d69466e8d5fcb9cb7a2869ee44f5121 Mon Sep 17 00:00:00 2001 From: AhmedYasserrr Date: Sun, 3 Aug 2025 19:08:43 +0300 Subject: [PATCH 13/13] chore: bump to v0.2.0 and refine project metadata --- pyproject.toml | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index d91681a..8c03a05 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -2,18 +2,30 @@ requires = ["scikit-build-core>=0.10", "pybind11"] build-backend = "scikit_build_core.build" - [project] name = "pyectool" -version = "0.1.0" -description="Python bindings for ectool using pybind11, enabling seamless integration with other applications" +version = "0.2.0" +description="Pyectool provides Python bindings for interacting with the Embedded Controller (EC) on ChromeOS and Framework devices, enabling seamless integration with other applications" readme = "README.md" authors = [ { name = "Ahmed Gamea", email = "ahmed.gamea@ejust.edu.eg" }, ] +license = {file = "LICENSE"} +keywords = ["ectool", "embedded controller", "EC", "pybind11", "bindings"] +requires-python = ">=3.9" +classifiers = [ + "Programming Language :: Python :: 3", + "Programming Language :: C++", + "License :: OSI Approved :: BSD License", + "Operating System :: POSIX :: Linux", +] + +[project.urls] +Homepage = "https://github.com/CCExtractor/libectool" +Issues = "https://github.com/CCExtractor/libectool/issues" [tool.scikit-build] -minimum-version = "build-system.requires" +minimum-version = "0.10" [tool.cibuildwheel] build-frontend = "build[uv]" @@ -49,4 +61,4 @@ ignore = [ isort.required-imports = ["from __future__ import annotations"] [tool.ruff.lint.per-file-ignores] -"tests/**" = ["T20"] +"tests/**" = ["T20"] \ No newline at end of file