Skip to content

Commit d5f79db

Browse files
Merge pull request #4 from CCExtractor/dev
[FEATURE] expand fan and temperature control APIs, add tests and docs
2 parents 5dfcd32 + 9de391d commit d5f79db

File tree

12 files changed

+1194
-215
lines changed

12 files changed

+1194
-215
lines changed

README.md

Lines changed: 79 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,36 @@
11
# Pyectool
22

3-
**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.
3+
**Pyectool** provides Python bindings for interacting with the Embedded Controller (EC) on ChromeOS and Framework devices.
4+
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`.
45

5-
## Features
6+
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.
67

7-
- Python bindings for EC functionality using `pybind11`.
8+
## Features
9+
- Python-native interface to low-level EC functionality via `pybind11`
810
- Supports fan duty control, temperature reading, AC power status, and more.
9-
- Designed for integration with hardware management or fan control tools.
10-
- Shared core logic with `libectool` for C/C++ integration.
11+
- Designed for hardware monitoring, thermal management, and fan control tooling.
12+
- Bundles the native `ectool` CLI and `libectool` C library alongside the Python package:
13+
* `pyectool/bin/ectool` (ectool CLI)
14+
* `pyectool/lib/libectool.a` (libectool static library)
15+
* `pyectool/include/libectool.h` (libectool C header)
1116

1217
---
1318

14-
## Build & Install (Python Package)
15-
16-
We use [`scikit-build-core`](https://scikit-build-core.readthedocs.io/en/latest/) to build the C++ extension via CMake.
19+
## Installation
1720

1821
### Prerequisites
1922

20-
Install the required system dependencies:
23+
Install system dependencies:
2124

2225
```sh
2326
sudo apt update
2427
sudo apt install -y libusb-1.0-0-dev libftdi1-dev pkg-config
2528
````
2629
### Clone the repository
2730

28-
## Install system-wide
31+
### Install the package
32+
33+
#### Option 1: System-wide (not recommended unless you know what you're doing)
2934
```sh
3035
sudo pip install .
3136
```
@@ -36,45 +41,80 @@ sudo env "PIP_BREAK_SYSTEM_PACKAGES=1" pip install .
3641
```
3742
(Required on modern distros like Ubuntu 24.04 due to PEP 668.)
3843

39-
### Test from outside the repo dir
40-
After installing, **do not run Python from the `libectool/` directory**, since it contains a `pyectool/` folder that may shadow the installed package.
41-
42-
Instead, test from another location, e.g.:
43-
44-
```sh
45-
cd ..
46-
sudo python -c "import pyectool; print(pyectool.is_on_ac())"
47-
```
48-
49-
## VENV INSTALLATION
50-
51-
If you **don’t** want to touch system Python:
52-
53-
### Create venv
54-
44+
#### Option 2: Isolated virtual environment (recommended)
5545
```bash
5646
python3 -m venv ~/.venv/pyectool
5747
source ~/.venv/pyectool/bin/activate
48+
pip install .
5849
```
5950

60-
### Install your package
51+
### ⚠️ Important Note
52+
53+
After installation, **do not run Python from inside the `libectool/` directory**. It contains a `pyectool/` folder that may shadow the installed package.
54+
55+
Instead, test from a different directory:
6156

62-
Inside the venv:
6357
```bash
64-
pip install .
58+
cd ..
59+
python -c "from pyectool import ECController; ec = ECController(); print(ec.is_on_ac())"
6560
```
66-
### Test from outside the repo dir
61+
62+
If you're using a virtual environment and want to preserve its `PATH`, use:
6763
```bash
6864
cd ..
69-
sudo env "PATH=$PATH" python -c "import pyectool; print(pyectool.is_on_ac())"
65+
sudo env "PATH=$PATH" python -c "from pyectool import ECController; ec = ECController(); print(ec.is_on_ac())"
66+
```
67+
This ensures the correct Python from your virtual environment is used even with `sudo`.
68+
69+
---
70+
71+
## Usage
72+
73+
### Create an EC controller instance
74+
75+
```python
76+
from pyectool import ECController
77+
78+
ec = ECController()
7079
```
7180
72-
### Available Functions
81+
### Available Methods
82+
83+
84+
| Method | Description |
85+
| ------------------------------------------------------- | ------------------------------------------------------------------------- |
86+
| `ec.is_on_ac() -> bool` | Returns `True` if the system is on AC power, else `False`. |
87+
| `ec.get_num_fans() -> int` | Returns the number of fan devices detected. |
88+
| `ec.enable_fan_auto_ctrl(fan_idx: int) -> None` | Enables automatic fan control for a specific fan. |
89+
| `ec.enable_all_fans_auto_ctrl() -> None` | Enables automatic control for all fans. |
90+
| `ec.set_fan_duty(percent: int, fan_idx: int) -> None` | Sets fan duty (speed) as a percentage for a specific fan. |
91+
| `ec.set_all_fans_duty(percent: int) -> None` | Sets the same duty percentage for all fans. |
92+
| `ec.set_fan_rpm(target_rpm: int, fan_idx: int) -> None` | Sets a specific RPM target for a specific fan. |
93+
| `ec.set_all_fans_rpm(target_rpm: int) -> None` | Sets the same RPM target for all fans. |
94+
| `ec.get_fan_rpm(fan_idx: int) -> int` | Returns current RPM of a specific fan. |
95+
| `ec.get_all_fans_rpm() -> list[int]` | Returns a list of current RPM values for all fans. |
96+
| `ec.get_num_temp_sensors() -> int` | Returns the total number of temperature sensors detected. |
97+
| `ec.get_temp(sensor_idx: int) -> int` | Returns the temperature (in °C) for the given sensor index. |
98+
| `ec.get_all_temps() -> list[int]` | Returns a list of all sensor temperatures (in °C). |
99+
| `ec.get_max_temp() -> int` | Returns the highest temperature across all sensors. |
100+
| `ec.get_max_non_battery_temp() -> int` | Returns the highest temperature excluding battery-related sensors. |
101+
| `ec.get_temp_info(sensor_idx: int) -> ECTempInfo` | Returns detailed info for a sensor, including name, type, and thresholds. |
102+
103+
---
104+
105+
### `ECTempInfo`
106+
107+
Returned by `get_temp_info()`, acts like a `dict` with:
108+
109+
* `sensor_name`: str
110+
* `sensor_type`: int
111+
* `temp`: int
112+
* `temp_fan_off`: int
113+
* `temp_fan_max`: int
114+
115+
---
116+
117+
## License
73118
74-
| Function | Description |
75-
| ------------------------------------------ | -------------------------------------------------------------------------------- |
76-
| `auto_fan_control()` | Enables automatic fan control by the EC. |
77-
| `get_max_non_battery_temperature() -> float` | Returns the highest temperature (in °C) from all sensors except the battery. |
78-
| `get_max_temperature() -> float` | Returns the highest temperature (in °C) from all EC sensors including battery. |
79-
| `is_on_ac() -> bool` | Checks whether the device is running on AC power. |
80-
| `set_fan_duty(percent: int)` | Sets the fan duty cycle manually (0–100%). |
119+
BSD 3-Clause License
120+
See the `LICENSE` file for full terms.

pyectool/__init__.py

Lines changed: 2 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,9 @@
11
from __future__ import annotations
22

3-
from .libectool_py import (
4-
__doc__,
5-
__version__,
6-
ascii_mode,
7-
auto_fan_control,
8-
get_max_non_battery_temperature,
9-
get_max_temperature,
10-
init,
11-
is_on_ac,
12-
release,
13-
set_fan_duty,
14-
)
3+
from .libectool_py import __doc__, __version__, ECController
154

165
__all__ = [
176
"__doc__",
187
"__version__",
19-
"ascii_mode",
20-
"auto_fan_control",
21-
"get_max_non_battery_temperature",
22-
"get_max_temperature",
23-
"init",
24-
"is_on_ac",
25-
"release",
26-
"set_fan_duty",
8+
"ECController",
279
]

pyectool/__init__.pyi

Lines changed: 34 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,37 @@ from __future__ import annotations
33
__doc__: str
44
__version__: str
55

6-
def init() -> None: ...
7-
def release() -> None: ...
8-
def is_on_ac() -> bool: ...
9-
def auto_fan_control() -> None: ...
10-
def set_fan_duty(duty: int) -> None: ...
11-
def get_max_temperature() -> float: ...
12-
def get_max_non_battery_temperature() -> float: ...
13-
14-
ascii_mode: bool
6+
class ECTempInfo(dict[str, int | str]):
7+
sensor_name: str
8+
sensor_type: int
9+
temp: int
10+
temp_fan_off: int
11+
temp_fan_max: int
12+
13+
class ECChargeStateInfo(dict[str, int]):
14+
ac: int
15+
chg_voltage: int
16+
chg_current: int
17+
chg_input_current: int
18+
batt_state_of_charge: int
19+
20+
class ECController:
21+
def __init__(self) -> None: ...
22+
def hello(self) -> None: ...
23+
def is_on_ac(self) -> bool: ...
24+
def get_charge_state(self) -> ECChargeStateInfo: ...
25+
def get_num_fans(self) -> int: ...
26+
def enable_fan_auto_ctrl(self, fan_idx: int) -> None: ...
27+
def enable_all_fans_auto_ctrl(self) -> None: ...
28+
def set_fan_duty(self, percent: int, fan_idx: int) -> None: ...
29+
def set_all_fans_duty(self, percent: int) -> None: ...
30+
def set_fan_rpm(self, target_rpm: int, fan_idx: int) -> None: ...
31+
def set_all_fans_rpm(self, target_rpm: int) -> None: ...
32+
def get_fan_rpm(self, fan_idx: int) -> int: ...
33+
def get_all_fans_rpm(self) -> list[int]: ...
34+
def get_num_temp_sensors(self) -> int: ...
35+
def get_temp(self, sensor_idx: int) -> int: ...
36+
def get_all_temps(self) -> list[int]: ...
37+
def get_max_temp(self) -> int: ...
38+
def get_max_non_battery_temp(self) -> int: ...
39+
def get_temp_info(self, sensor_idx: int) -> ECTempInfo: ...

pyproject.toml

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,18 +2,30 @@
22
requires = ["scikit-build-core>=0.10", "pybind11"]
33
build-backend = "scikit_build_core.build"
44

5-
65
[project]
76
name = "pyectool"
8-
version = "0.1.0"
9-
description="Python bindings for ectool using pybind11, enabling seamless integration with other applications"
7+
version = "0.2.0"
8+
description="Pyectool provides Python bindings for interacting with the Embedded Controller (EC) on ChromeOS and Framework devices, enabling seamless integration with other applications"
109
readme = "README.md"
1110
authors = [
1211
{ name = "Ahmed Gamea", email = "[email protected]" },
1312
]
13+
license = {file = "LICENSE"}
14+
keywords = ["ectool", "embedded controller", "EC", "pybind11", "bindings"]
15+
requires-python = ">=3.9"
16+
classifiers = [
17+
"Programming Language :: Python :: 3",
18+
"Programming Language :: C++",
19+
"License :: OSI Approved :: BSD License",
20+
"Operating System :: POSIX :: Linux",
21+
]
22+
23+
[project.urls]
24+
Homepage = "https://github.com/CCExtractor/libectool"
25+
Issues = "https://github.com/CCExtractor/libectool/issues"
1426

1527
[tool.scikit-build]
16-
minimum-version = "build-system.requires"
28+
minimum-version = "0.10"
1729

1830
[tool.cibuildwheel]
1931
build-frontend = "build[uv]"
@@ -49,4 +61,4 @@ ignore = [
4961
isort.required-imports = ["from __future__ import annotations"]
5062

5163
[tool.ruff.lint.per-file-ignores]
52-
"tests/**" = ["T20"]
64+
"tests/**" = ["T20"]

src/bindings/CMakeLists.txt

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
# Create the Python module
2-
python_add_library(libectool_py MODULE libectool_py.cc WITH_SOABI)
2+
python_add_library(libectool_py MODULE PyECController.cc ECController.cc
3+
WITH_SOABI)
34

45
# Link against required libraries
56
target_link_libraries(libectool_py PRIVATE pybind11::headers libectool)
6-
target_include_directories(libectool_py PUBLIC ../include)
7+
target_include_directories(libectool_py PRIVATE . ../include)
78
target_compile_definitions(libectool_py PUBLIC VERSION_INFO=${PROJECT_VERSION})

0 commit comments

Comments
 (0)