Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
118 changes: 79 additions & 39 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,31 +1,36 @@
# 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
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 .
```
Expand All @@ -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%). |
BSD 3-Clause License
See the `LICENSE` file for full terms.
22 changes: 2 additions & 20 deletions pyectool/__init__.py
Original file line number Diff line number Diff line change
@@ -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",
]
43 changes: 34 additions & 9 deletions pyectool/__init__.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -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: ...
22 changes: 17 additions & 5 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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 = "[email protected]" },
]
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]"
Expand Down Expand Up @@ -49,4 +61,4 @@ ignore = [
isort.required-imports = ["from __future__ import annotations"]

[tool.ruff.lint.per-file-ignores]
"tests/**" = ["T20"]
"tests/**" = ["T20"]
5 changes: 3 additions & 2 deletions src/bindings/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -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})
Loading