5 Commits

Author SHA1 Message Date
Stephen Horvath
a97148f7a5 Release 0.7 2025-12-25 20:48:18 +10:00
Stephen Horvath
8b10c49ed4 Update Python on Windows CI 2025-12-25 20:15:14 +10:00
Stephen Horvath
f31054c18b Update Gvsbuild on CI 2025-12-25 20:10:22 +10:00
Stephen Horvath
2b50f0816f Implement fan set points 2025-12-25 19:35:33 +10:00
Stephen Horvath
1fc4b94237 Streamline udev rules 2025-11-06 13:58:51 +10:00
14 changed files with 138 additions and 45 deletions

View File

@@ -11,7 +11,7 @@ jobs:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- uses: actions/setup-python@v5 - uses: actions/setup-python@v5
with: with:
python-version: '3.13' python-version: '3.14'
cache: 'pip' cache: 'pip'
- name: Cache GTK4 - name: Cache GTK4
@@ -19,11 +19,11 @@ jobs:
uses: actions/cache@v4 uses: actions/cache@v4
with: with:
path: C:\gtk path: C:\gtk
key: Gvsbuild_2025.9.0 key: Gvsbuild_2025.11.1
- name: Download GTK4 Gvsbuild zip - name: Download GTK4 Gvsbuild zip
if: steps.cache-gtk4.outputs.cache-hit != 'true' if: steps.cache-gtk4.outputs.cache-hit != 'true'
run: Start-BitsTransfer -Source https://github.com/wingtk/gvsbuild/releases/download/2025.9.0/GTK4_Gvsbuild_2025.9.0_x64.zip -Destination Gvsbuild.zip run: Start-BitsTransfer -Source https://github.com/wingtk/gvsbuild/releases/download/2025.11.1/GTK4_Gvsbuild_2025.11.1_x64.zip -Destination Gvsbuild.zip
- name: Extract Gvsbuild zip - name: Extract Gvsbuild zip
if: steps.cache-gtk4.outputs.cache-hit != 'true' if: steps.cache-gtk4.outputs.cache-hit != 'true'

View File

@@ -1,7 +0,0 @@
# CrOS_EC_Python udev rules
# LPC Access
KERNEL=="port", TAG+="uaccess"
# /dev/cros_ec Access
KERNEL=="cros_ec", TAG+="uaccess"

View File

@@ -15,7 +15,9 @@ YAFI is also available on [Flathub](https://flathub.org/en/apps/au.stevetech.yaf
### Linux ### Linux
To allow YAFI to communicate with the EC, you need to copy the [`60-cros_ec_python.rules`](60-cros_ec_python.rules) file to `/etc/udev/rules.d/` and reload the rules with `sudo udevadm control --reload-rules && sudo udevadm trigger`. To allow YAFI to communicate with the EC, you will need to enable user access to the `/dev/cros_ec` device. You can do this by running `echo KERNEL=="cros_ec", TAG+="uaccess" | sudo tee /etc/udev/rules.d/60-yafi.rules`, and then reload the rules with `sudo udevadm control --reload-rules && sudo udevadm trigger`.
You can also do this by running `curl -Lfs yafi.stevetech.au/udev.sh | sudo sh` which will run the [`add-udev-rules.sh`](add-udev-rules.sh) script.
### Windows ### Windows
@@ -84,7 +86,7 @@ It is possible to run YAFI on Windows using [gvsbuild](https://github.com/wingtk
### `[Errno 13] Permission denied: '/dev/cros_ec'` ### `[Errno 13] Permission denied: '/dev/cros_ec'`
This error occurs when the udev rules are not installed or not working. Make sure you have copied the `60-cros_ec_python.rules` file to `/etc/udev/rules.d/` and reloaded the rules with `sudo udevadm control --reload-rules && sudo udevadm trigger`. This error occurs when the udev rules are not installed or not working. Make sure you have installed the udev rules as described in the [Linux Installation](#linux) section.
### `Could not auto detect device, check you have the required permissions, or specify manually.` ### `Could not auto detect device, check you have the required permissions, or specify manually.`

7
add-udev-rules.sh Executable file
View File

@@ -0,0 +1,7 @@
#!/bin/bash
set -e
echo Installing udev rules for YAFI to /etc/udev/rules.d/60-yafi.rules
echo KERNEL=="cros_ec", TAG+="uaccess" > /etc/udev/rules.d/60-yafi.rules
udevadm control --reload-rules
udevadm trigger
echo udev rules installed successfully.

View File

@@ -19,6 +19,8 @@
<p>You will need to install the udev rules to allow non-root access to the EC device. See the README for more information.</p> <p>You will need to install the udev rules to allow non-root access to the EC device. See the README for more information.</p>
<p>Alternatively, you can run <code>curl -Lfs yafi.stevetech.au/udev.sh | sudo sh</code> to install the udev rules.</p>
<p>YAFI is not affiliated with Framework Computer Inc. in any way.</p> <p>YAFI is not affiliated with Framework Computer Inc. in any way.</p>
</description> </description>
@@ -86,6 +88,12 @@
</screenshots> </screenshots>
<releases> <releases>
<release version="0.7" date="2025-12-25">
<url type="details">https://github.com/Steve-Tech/YAFI/releases/tag/0.7</url>
<description>
<p>YAFI now supports modifying fan set points.</p>
</description>
</release>
<release version="0.6" date="2025-09-28"> <release version="0.6" date="2025-09-28">
<url type="details">https://github.com/Steve-Tech/YAFI/releases/tag/0.6</url> <url type="details">https://github.com/Steve-Tech/YAFI/releases/tag/0.6</url>
<description> <description>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 86 KiB

After

Width:  |  Height:  |  Size: 161 KiB

View File

@@ -1,5 +1,5 @@
project('yafi', project('yafi',
version: '0.6', version: '0.7',
meson_version: '>= 1.0.0', meson_version: '>= 1.0.0',
default_options: [ 'warning_level=2', 'werror=false', ], default_options: [ 'warning_level=2', 'werror=false', ],
) )

View File

@@ -1,6 +1,6 @@
[project] [project]
name = "yafi" name = "yafi"
version = "0.6" version = "0.7"
authors = [ authors = [
{ name="Steve-Tech" } { name="Steve-Tech" }
] ]
@@ -8,7 +8,7 @@ description = "Yet Another Framework Interface"
readme = "README.md" readme = "README.md"
requires-python = ">=3.10" requires-python = ">=3.10"
dependencies = [ dependencies = [
"cros_ec_python >= 0.2.0", "cros_ec_python >= 0.3.0",
"PyGObject" "PyGObject"
] ]
classifiers = [ classifiers = [

View File

@@ -7,8 +7,8 @@
"sources": [ "sources": [
{ {
"type": "file", "type": "file",
"url": "https://files.pythonhosted.org/packages/33/11/c23a7acaa333589921a2f524517eb719dfb72628153ae22fcdf2e9052ac6/cros_ec_python-0.2.0-py3-none-any.whl", "url": "https://files.pythonhosted.org/packages/6c/7a/10d978a02bbe37530490cfd14e0994c433dc29c81b3afcdbde453d512528/cros_ec_python-0.3.0-py3-none-any.whl",
"sha256": "d38e493fbcaf23bc4b613d1342a036cecc6506284afc74f37013a3eac85a01b9" "sha256": "aeb14ebdbd60ec6d6a4b11df1482a295466da4a908a468d168efd4cc141e7e3d"
} }
] ]
} }

View File

@@ -143,7 +143,7 @@ class YafiApplication(Adw.Application):
developers=["Stephen Horvath https://github.com/Steve-Tech"], developers=["Stephen Horvath https://github.com/Steve-Tech"],
issue_url="https://github.com/Steve-Tech/YAFI/issues", issue_url="https://github.com/Steve-Tech/YAFI/issues",
license_type=Gtk.License.GPL_2_0, license_type=Gtk.License.GPL_2_0,
version="0.6", version="0.7",
website="https://github.com/Steve-Tech/YAFI", website="https://github.com/Steve-Tech/YAFI",
) )
about.add_acknowledgement_section(None, ["Framework Computer Inc. https://frame.work/"]) about.add_acknowledgement_section(None, ["Framework Computer Inc. https://frame.work/"])

View File

@@ -27,11 +27,17 @@ import cros_ec_python.exceptions as ec_exceptions
class ThermalsPage(Gtk.Box): class ThermalsPage(Gtk.Box):
__gtype_name__ = 'ThermalsPage' __gtype_name__ = 'ThermalsPage'
first_run = True
fan_rpm = Gtk.Template.Child() fan_rpm = Gtk.Template.Child()
fan_mode = Gtk.Template.Child() fan_mode = Gtk.Template.Child()
fan_set_rpm = Gtk.Template.Child() fan_set_rpm = Gtk.Template.Child()
fan_set_percent = Gtk.Template.Child() fan_set_percent = Gtk.Template.Child()
fan_percent_scale = Gtk.Template.Child() fan_percent_scale = Gtk.Template.Child()
fan_set_points = Gtk.Template.Child()
set_points = []
ec_set_points_supported = False
ec_set_points = []
temperatures = Gtk.Template.Child() temperatures = Gtk.Template.Child()
temp_items = [] temp_items = []
@@ -40,10 +46,40 @@ class ThermalsPage(Gtk.Box):
super().__init__(**kwargs) super().__init__(**kwargs)
def setup(self, app): def setup(self, app):
# Temperature sensors
while temp_child := self.temperatures.get_last_child():
self.temperatures.remove(temp_child)
self.temp_items.clear()
try:
ec_temp_sensors = ec_commands.thermal.get_temp_sensors(app.cros_ec)
except ec_exceptions.ECError as e:
if e.ec_status == ec_exceptions.EcStatus.EC_RES_INVALID_COMMAND:
# Generate some labels if the command is not supported
ec_temp_sensors = {}
temps = ec_commands.memmap.get_temps(app.cros_ec)
for i, temp in enumerate(temps):
ec_temp_sensors[f"Sensor {i}"] = (temp, None)
else:
raise e
for key, value in ec_temp_sensors.items():
off_row = Adw.ActionRow(title=key, subtitle=f"{value[0]}°C")
off_row.add_css_class("property")
self.temperatures.append(off_row)
self.temp_items.append(off_row)
self._update_thermals(app)
# Don't let the user change the fans if they can't get back to auto # Don't let the user change the fans if they can't get back to auto
if ec_commands.general.get_cmd_versions( if ec_commands.general.get_cmd_versions(
app.cros_ec, ec_commands.thermal.EC_CMD_THERMAL_AUTO_FAN_CTRL app.cros_ec, ec_commands.thermal.EC_CMD_THERMAL_AUTO_FAN_CTRL
): ):
self.ec_set_points_supported = ec_commands.general.check_cmd_version(
app.cros_ec, ec_commands.thermal.EC_CMD_THERMAL_GET_THRESHOLD, 1
) and ec_commands.general.check_cmd_version(
app.cros_ec, ec_commands.thermal.EC_CMD_THERMAL_SET_THRESHOLD, 1
)
def handle_fan_mode(mode): def handle_fan_mode(mode):
match mode: match mode:
@@ -51,13 +87,18 @@ class ThermalsPage(Gtk.Box):
self.fan_set_rpm.set_visible(False) self.fan_set_rpm.set_visible(False)
self.fan_set_percent.set_visible(False) self.fan_set_percent.set_visible(False)
ec_commands.thermal.thermal_auto_fan_ctrl(app.cros_ec) ec_commands.thermal.thermal_auto_fan_ctrl(app.cros_ec)
self.fan_set_points.set_visible(self.ec_set_points_supported)
case 1: # Percent case 1: # Percent
self.fan_set_points.set_visible(False)
self.fan_set_rpm.set_visible(False) self.fan_set_rpm.set_visible(False)
self.fan_set_percent.set_visible(True) self.fan_set_percent.set_visible(True)
case 2: # RPM case 2: # RPM
self.fan_set_points.set_visible(False)
self.fan_set_rpm.set_visible(True) self.fan_set_rpm.set_visible(True)
self.fan_set_percent.set_visible(False) self.fan_set_percent.set_visible(False)
handle_fan_mode(self.fan_mode.get_selected())
self.fan_mode.connect( self.fan_mode.connect(
"notify::selected", "notify::selected",
lambda combo, _: handle_fan_mode(combo.get_selected()), lambda combo, _: handle_fan_mode(combo.get_selected()),
@@ -81,41 +122,77 @@ class ThermalsPage(Gtk.Box):
): ):
def handle_fan_rpm(entry): def handle_fan_rpm(entry):
rpm = int(entry.get_text()) rpm = int(entry.get_value())
ec_commands.pwm.pwm_set_fan_rpm(app.cros_ec, rpm) ec_commands.pwm.pwm_set_fan_rpm(app.cros_ec, rpm)
self.fan_set_rpm.connect( self.fan_set_rpm.connect(
"notify::text", lambda entry, _: handle_fan_rpm(entry) "notify::value", lambda entry, _: handle_fan_rpm(entry)
) )
else: else:
self.fan_set_rpm.set_sensitive(False) self.fan_set_rpm.set_sensitive(False)
else: else:
self.fan_mode.set_sensitive(False) self.fan_mode.set_sensitive(False)
# Temperature sensors # Set points
while temp_child := self.temperatures.get_last_child(): if self.ec_set_points_supported and self.first_run:
self.temperatures.remove(temp_child) def handle_set_point(entry, key):
self.temp_items.clear() index = entry.ec_index
temp = int(entry.get_value())
# Don't allow an off temp higher than max temp and vice versa
match key:
case "temp_fan_off":
if temp > self.ec_set_points[index]["temp_fan_max"]:
entry.set_value(self.ec_set_points[index]["temp_fan_off"])
return
case "temp_fan_max":
if temp < self.ec_set_points[index]["temp_fan_off"]:
entry.set_value(self.ec_set_points[index]["temp_fan_max"])
return
self.ec_set_points[entry.ec_index][key] = temp
ec_commands.thermal.thermal_set_thresholds(
app.cros_ec, index,
self.ec_set_points[index]
)
try: for i, sensor in enumerate(ec_temp_sensors):
ec_temp_sensors = ec_commands.thermal.get_temp_sensors(app.cros_ec) ec_set_point = ec_commands.thermal.thermal_get_thresholds(app.cros_ec, i)
except ec_exceptions.ECError as e: self.ec_set_points.append(ec_set_point)
if e.ec_status == ec_exceptions.EcStatus.EC_RES_INVALID_COMMAND: off_row = Adw.SpinRow(
# Generate some labels if the command is not supported title=f"Fan On - {sensor}",
ec_temp_sensors = {} subtitle=f"Turn fan on when above temp (°C)",
temps = ec_commands.memmap.get_temps(app.cros_ec) )
for i, temp in enumerate(temps): off_row.ec_index = i
ec_temp_sensors[f"Sensor {i}"] = (temp, None) # 0K to 65535K for 16bit unsigned range
else: # Actually the EC takes 32bits, but let's keep it like this for sanity
raise e off_row.set_adjustment(Gtk.Adjustment(
lower=-273,
upper=65_262,
page_increment=10,
step_increment=1,
value=ec_set_point["temp_fan_off"],
))
off_row.connect(
"notify::value", lambda entry, _: handle_set_point(entry, "temp_fan_off")
)
max_row = Adw.SpinRow(
title=f"Fan Max - {sensor}",
subtitle=f"Max fan speed when above temp (°C)",
)
max_row.ec_index = i
max_row.set_adjustment(Gtk.Adjustment(
lower=-273,
upper=65_262,
page_increment=10,
step_increment=1,
value=ec_set_point["temp_fan_max"],
))
max_row.connect(
"notify::value", lambda entry, _: handle_set_point(entry, "temp_fan_max")
)
self.fan_set_points.add_row(off_row)
self.fan_set_points.add_row(max_row)
for key, value in ec_temp_sensors.items(): self.first_run = False
new_row = Adw.ActionRow(title=key, subtitle=f"{value[0]}°C")
new_row.add_css_class("property")
self.temperatures.append(new_row)
self.temp_items.append(new_row)
self._update_thermals(app)
# Schedule _update_thermals to run every second # Schedule _update_thermals to run every second
GLib.timeout_add_seconds(1, self._update_thermals, app) GLib.timeout_add_seconds(1, self._update_thermals, app)

View File

@@ -1,5 +1,5 @@
<?xml version='1.0' encoding='UTF-8'?> <?xml version='1.0' encoding='UTF-8'?>
<!-- Created with Cambalache 0.96.1 --> <!-- Created with Cambalache 0.96.3 -->
<interface> <interface>
<!-- interface-name thermals.ui --> <!-- interface-name thermals.ui -->
<!-- interface-description The Thermals page for YAFI --> <!-- interface-description The Thermals page for YAFI -->
@@ -92,6 +92,12 @@
<property name="visible">False</property> <property name="visible">False</property>
</object> </object>
</child> </child>
<child>
<object class="AdwExpanderRow" id="fan_set_points">
<property name="selectable">False</property>
<property name="title">Fan Set Points</property>
</object>
</child>
<style> <style>
<class name="boxed-list"/> <class name="boxed-list"/>
</style> </style>

View File

@@ -1,9 +1,9 @@
<?xml version='1.0' encoding='UTF-8' standalone='no'?> <?xml version='1.0' encoding='UTF-8' standalone='no'?>
<!DOCTYPE cambalache-project SYSTEM "cambalache-project.dtd"> <!DOCTYPE cambalache-project SYSTEM "cambalache-project.dtd">
<!-- Created with Cambalache 0.96.1 --> <!-- Created with Cambalache 0.96.3 -->
<cambalache-project version="0.96.0" target_tk="gtk-4.0"> <cambalache-project version="0.96.0" target_tk="gtk-4.0">
<ui template-class="YafiWindow" filename="yafi.ui" sha256="9d1b2f030e4a816eb0b1aa53ae1d80c5b50a2f4646e32c7a64803eb6f6ed3947"/> <ui template-class="YafiWindow" filename="yafi.ui" sha256="9d1b2f030e4a816eb0b1aa53ae1d80c5b50a2f4646e32c7a64803eb6f6ed3947"/>
<ui template-class="ThermalsPage" filename="thermals.ui" sha256="e301e65649005315ff60d250b60a47f6250ad6feb27db104051fcf0143cde173"/> <ui template-class="ThermalsPage" filename="thermals.ui" sha256="89f5b68da04abad587d8b949d18357cd956313680e663b10e5d42697f9bfbf6e"/>
<ui template-class="LedsPage" filename="leds.ui" sha256="abc3ee759974a5c92feb48cc258dbe7271d0402facf71fd5e779f2bb1a277e16"/> <ui template-class="LedsPage" filename="leds.ui" sha256="abc3ee759974a5c92feb48cc258dbe7271d0402facf71fd5e779f2bb1a277e16"/>
<ui template-class="BatteryLimiterPage" filename="battery-limiter.ui" sha256="b5d41b19cb1fb7ca5b4bcfae43244e54111f5e8d8c51d95448d6a92b5185d2c4"/> <ui template-class="BatteryLimiterPage" filename="battery-limiter.ui" sha256="b5d41b19cb1fb7ca5b4bcfae43244e54111f5e8d8c51d95448d6a92b5185d2c4"/>
<ui template-class="HardwarePage" filename="hardware.ui" sha256="37ea282198d9f60435f80e4adf8256cd2249e590dcad4b63af634d828673f1bf"/> <ui template-class="HardwarePage" filename="hardware.ui" sha256="37ea282198d9f60435f80e4adf8256cd2249e590dcad4b63af634d828673f1bf"/>

Binary file not shown.