diff --git a/pyectool/__init__.pyi b/pyectool/__init__.pyi index 1d562ab..910d0b8 100644 --- a/pyectool/__init__.pyi +++ b/pyectool/__init__.pyi @@ -3,10 +3,37 @@ 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 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 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_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/src/bindings/ECController.cc b/src/bindings/ECController.cc index 96e616a..23a519f 100644 --- a/src/bindings/ECController.cc +++ b/src/bindings/ECController.cc @@ -10,12 +10,30 @@ 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; } 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; @@ -24,26 +42,116 @@ bool ECController::is_on_ac() { return ac; } -void ECController::auto_fan_control() { - int ret = ec_auto_fan_control(); +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::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_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 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..eab9f96 100644 --- a/src/bindings/ECController.h +++ b/src/bindings/ECController.h @@ -1,14 +1,32 @@ #pragma once #include #include +#include +#include "libectool.h" class ECController { public: + int hello(); + bool is_on_ac(); - void auto_fan_control(); - void set_fan_duty(int duty); - float get_max_temperature(); - float get_max_non_battery_temperature(); + 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 index e6d9898..896ecb1 100644 --- a/src/bindings/PyECController.cc +++ b/src/bindings/PyECController.cc @@ -1,25 +1,120 @@ #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("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(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 849cfd3..c8d7788 100644 --- a/src/core/libectool.cc +++ b/src/core/libectool.cc @@ -111,8 +111,74 @@ 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(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; + + 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 endpoint functions +// Top-level Power Functions // ----------------------------------------------------------------------------- int ec_is_on_ac(int *ac_present) { @@ -138,39 +204,404 @@ int ec_is_on_ac(int *ac_present) { return 0; } -int ec_auto_fan_control() { - int ret = libectool_init(); +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; - ret = ec_command(EC_CMD_THERMAL_AUTO_FAN_CTRL, 0, NULL, 0, NULL, 0); + 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_EC_COMMAND; + 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 ((int)fan_val == EC_FAN_SPEED_NOT_PRESENT) + break; + } + + *val = idx; + libectool_release(); return 0; } -int ec_set_fan_duty(int duty) { - if (duty < 0 || duty > 100) +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; + + 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; + } - int ret = libectool_init(); + 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; +} + +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; - p_v0.percent = duty; - ret = ec_command(EC_CMD_PWM_SET_FAN_DUTY, 0, &p_v0, sizeof(p_v0), - NULL, 0); + + 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_EC_COMMAND; + 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_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_fans_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; + 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: + 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; +} + +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_max_temperature(float *max_temp) { +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; + + return 0; +} + +int ec_get_max_temp(int *max_temp) { if (!max_temp) return EC_ERR_INVALID_PARAM; @@ -178,7 +609,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; @@ -205,7 +636,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; @@ -216,7 +647,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++) { @@ -242,3 +673,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 ebc6fbf..b08dd74 100644 --- a/src/include/libectool.h +++ b/src/include/libectool.h @@ -4,25 +4,60 @@ #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_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 +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_get_max_temperature(float *max_temp); -int ec_get_max_non_battery_temperature(float *max_temp); +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}"