diff --git a/README.md b/README.md index 4f879be..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,45 +41,80 @@ 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 "import pyectool; print(pyectool.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 "import pyectool; print(pyectool.is_on_ac())" +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`. + +--- + +## Usage + +### Create an EC controller instance + +```python +from pyectool import ECController + +ec = ECController() ``` -### Available Functions +### Available Methods + + +| 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 + +--- + +## License -| 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 +BSD 3-Clause License +See the `LICENSE` file for full terms. 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..910d0b8 100644 --- a/pyectool/__init__.pyi +++ b/pyectool/__init__.pyi @@ -3,12 +3,37 @@ 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 ECTempInfo(dict[str, int | str]): + sensor_name: str + sensor_type: int + temp: int + 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: ... + 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_sensors(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/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 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..23a519f --- /dev/null +++ b/src/bindings/ECController.cc @@ -0,0 +1,157 @@ +#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; + 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; + } + + throw std::runtime_error(msg + " (" + reason + ", code " + std::to_string(code) + ")"); +} + +int ECController::hello() { + int ret = ec_hello(); + return ret; +} + +// ----------------------------------------------------------------------------- +// Top-level Power Functions +// ----------------------------------------------------------------------------- + +bool ECController::is_on_ac() { + int ac; + int ret = ec_is_on_ac(&ac); + handle_error(ret, "Failed to read AC status"); + 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 +// ----------------------------------------------------------------------------- + +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::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"); +} + +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_sensors() { + int val = 0; + int ret = ec_get_num_temp_sensors(&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_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; +} + +int ECController::get_max_temp() { + int temp = 0; + int ret = ec_get_max_temp(&temp); + handle_error(ret, "Failed to get max temperature"); + 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; +} + +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 new file mode 100644 index 0000000..eab9f96 --- /dev/null +++ b/src/bindings/ECController.h @@ -0,0 +1,33 @@ +#pragma once +#include +#include +#include +#include "libectool.h" + +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); + 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_sensors(); + 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 new file mode 100644 index 0000000..896ecb1 --- /dev/null +++ b/src/bindings/PyECController.cc @@ -0,0 +1,124 @@ +#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; +} + +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("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); +#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..c8d7788 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,642 @@ 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); + ret = ec_readmem(EC_MEMMAP_THERMAL_VERSION, sizeof(val), &val); + if (ret <= 0 || val == 0) + return 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 val; + + // 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; } -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(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; + 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 +// ----------------------------------------------------------------------------- +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; - 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; + 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 rv; + + return 0; } // ----------------------------------------------------------------------------- -// Top-level endpoint functions +// Top-level Power 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; - uint8_t flags = read_mapped_mem8(EC_MEMMAP_BATT_FLAG); - bool ac_present = (flags & EC_BATT_FLAG_AC_PRESENT); + if (!ac_present) + return EC_ERR_INVALID_PARAM; + ret = libectool_init(); + if (ret < 0) + return EC_ERR_INIT; + + ret = ec_readmem(EC_MEMMAP_BATT_FLAG, sizeof(flags), &flags); + + if (ret <= 0) { + libectool_release(); + return EC_ERR_READMEM; + } + + *ac_present = !!(flags & EC_BATT_FLAG_AC_PRESENT); libectool_release(); + 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 +// ----------------------------------------------------------------------------- + +int ec_get_num_fans(int *val) { + int ret, idx; + uint16_t fan_val; + 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); - return ac_present; + if (ret <= 0) + return EC_ERR_READMEM; + + if ((int)fan_val == EC_FAN_SPEED_NOT_PRESENT) + break; + } + + *val = idx; + libectool_release(); + return 0; } -void auto_fan_control() { - if (libectool_init() < 0) - fprintf(stderr, "Failed initializing EC connection\n"); +int ec_enable_fan_auto_ctrl(int fan_idx) { + int ret, cmdver; + int num_fans; + struct ec_params_auto_fan_ctrl_v1 p_v1; - int rv = ec_command(EC_CMD_THERMAL_AUTO_FAN_CTRL, 0, NULL, 0, NULL, 0); + ret = libectool_init(); + if (ret < 0) + return EC_ERR_INIT; - if (rv < 0) - fprintf(stderr, "Failed to enable auto fan control\n"); + cmdver = 1; + if (!ec_cmd_version_supported(EC_CMD_THERMAL_AUTO_FAN_CTRL, cmdver)) { + libectool_release(); + return EC_ERR_UNSUPPORTED_VER; + } + + ec_get_num_fans(&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_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; } -void set_fan_duty(int duty) { - if (libectool_init() < 0) - fprintf(stderr, "Failed initializing EC connection\n"); +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; + + ret = libectool_init(); + if (ret < 0) + return EC_ERR_INIT; + + ec_get_num_fans(&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; - int rv; - if (duty < 0 || duty > 100) { - fprintf(stderr, "Error: Fan duty cycle must be between 0 and 100.\n"); - return; + if (percent < 0 || percent > 100) + return EC_ERR_INVALID_PARAM; + + ret = libectool_init(); + if (ret < 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) { + 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; + + ec_get_num_fans(&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_v0.percent = duty; - rv = 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"); + 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; } -// Get the maximum temperature from all sensors -float get_max_temperature() { - if (libectool_init() < 0) - fprintf(stderr, "Failed initializing EC connection\n"); +int ec_set_all_fans_rpm(int target_rpm) { + int ret; + struct ec_params_pwm_set_fan_target_rpm_v0 p_v0; - float max_temp = -1.0f; - int mtemp, temp; - int id; + 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; + uint16_t val; + + if (!rpm) + return EC_ERR_INVALID_PARAM; + + ret = libectool_init(); + if (ret < 0) + return EC_ERR_INIT; + + ec_get_num_fans(&num_fans); + + if (fan_idx < 0 || fan_idx >= num_fans) { + libectool_release(); + return EC_ERR_INVALID_PARAM; + } + + ret = ec_readmem(EC_MEMMAP_FAN + 2 * fan_idx, sizeof(val), &val); + if (ret <= 0) + return EC_ERR_READMEM; + + switch (val) { + case EC_FAN_SPEED_NOT_PRESENT: + *rpm = -1; + break; + case EC_FAN_SPEED_STALLED: + *rpm = -2; + break; + default: + *rpm = val; + } + + libectool_release(); + return 0; +} + +int ec_get_all_fans_rpm(int *rpms, int rpms_size, int *num_fans_out) { + int i, ret, num_fans; + uint16_t val; + + if (!rpms || !num_fans_out) + return EC_ERR_INVALID_PARAM; + + ret = libectool_init(); + if (ret < 0) + return EC_ERR_INIT; + + ec_get_num_fans(&num_fans); + *num_fans_out = num_fans; + + for (i = 0; i < num_fans && i < rpms_size; i++) { + 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: + rpms[i] = -1; + break; + case EC_FAN_SPEED_STALLED: + rpms[i] = -2; + break; + default: + rpms[i] = val; + } + + } + + libectool_release(); + return 0; +} + +// ----------------------------------------------------------------------------- +// Top-level temperature Functions +// ----------------------------------------------------------------------------- +int ec_get_num_temp_sensors(int *val) { + int id, mtemp, ret; + int count = 0; + + if (!val) + 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); + 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; + continue; default: - temp = K_TO_C(mtemp + EC_TEMP_SENSOR_OFFSET); + count++; + } + } + + libectool_release(); + + *val = count; + return 0; +} + +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) + 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); + + 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++; } + } + + libectool_release(); + + if (num_sensors_out) + *num_sensors_out = count; - if (temp > max_temp) { - max_temp = temp; + return 0; +} + +int ec_get_max_temp(int *max_temp) { + if (!max_temp) + return EC_ERR_INVALID_PARAM; + + int ret = libectool_init(); + if (ret < 0) + return EC_ERR_INIT; + + int t = -1; + 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: + 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_temp(int *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; + int t = -1; 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; +} + +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 531bf8f..b08dd74 100644 --- a/src/include/libectool.h +++ b/src/include/libectool.h @@ -3,20 +3,61 @@ #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_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; +}; + +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(); // 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_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); +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_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_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); +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; diff --git a/tests/test_pyectool.py b/tests/test_pyectool.py new file mode 100644 index 0000000..07cbe37 --- /dev/null +++ b/tests/test_pyectool.py @@ -0,0 +1,131 @@ +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) + + 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() + 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}"