mirror of
https://github.com/Steve-Tech/YAFI.git
synced 2026-04-20 01:00:37 +00:00
Compare commits
13 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a97148f7a5 | ||
|
|
8b10c49ed4 | ||
|
|
f31054c18b | ||
|
|
2b50f0816f | ||
|
|
1fc4b94237 | ||
|
|
90a994a685 | ||
|
|
7b123da001 | ||
|
|
c8fd626446 | ||
|
|
0f6fee40ae | ||
|
|
4a788c2395 | ||
|
|
d2f34c9b5a | ||
|
|
4958722edd | ||
|
|
23b54ee397 |
2
.github/FUNDING.yml
vendored
Normal file
2
.github/FUNDING.yml
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
github: [Steve-Tech]
|
||||
thanks_dev: u/gh/steve-tech
|
||||
6
.github/workflows/pyinstaller-windows.yml
vendored
6
.github/workflows/pyinstaller-windows.yml
vendored
@@ -11,7 +11,7 @@ jobs:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.13'
|
||||
python-version: '3.14'
|
||||
cache: 'pip'
|
||||
|
||||
- name: Cache GTK4
|
||||
@@ -19,11 +19,11 @@ jobs:
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: C:\gtk
|
||||
key: Gvsbuild_2025.9.0
|
||||
key: Gvsbuild_2025.11.1
|
||||
|
||||
- name: Download GTK4 Gvsbuild zip
|
||||
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
|
||||
if: steps.cache-gtk4.outputs.cache-hit != 'true'
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
# CrOS_EC_Python udev rules
|
||||
|
||||
# LPC Access
|
||||
KERNEL=="port", TAG+="uaccess"
|
||||
|
||||
# /dev/cros_ec Access
|
||||
KERNEL=="cros_ec", TAG+="uaccess"
|
||||
16
README.md
16
README.md
@@ -11,9 +11,13 @@ You can download the latest release from the [Releases page](https://github.com/
|
||||
|
||||
There are builds for Flatpak, and PyInstaller for portable execution on Linux or Windows.
|
||||
|
||||
YAFI is also available on [Flathub](https://flathub.org/en/apps/au.stevetech.yafi): `flatpak install flathub au.stevetech.yafi`.
|
||||
|
||||
### 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
|
||||
|
||||
@@ -21,6 +25,8 @@ If your Laptop's BIOS supports Framework's EC driver, there is no need to instal
|
||||
|
||||
Otherwise, YAFI supports the [PawnIO](https://pawnio.eu/) driver, and will be automatically used if installed and there is no Framework driver available. YAFI will need to be run as administrator to communicate with the driver.
|
||||
|
||||
Currently the PawnIO driver does not support Framework 13 mainboards with 11th, 12th, or 13th Gen Intel CPUs.
|
||||
|
||||
## Building
|
||||
|
||||
### Flatpak
|
||||
@@ -80,7 +86,7 @@ It is possible to run YAFI on Windows using [gvsbuild](https://github.com/wingtk
|
||||
|
||||
### `[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.`
|
||||
|
||||
@@ -88,3 +94,9 @@ This error occurs when `/dev/cros_ec` is not found, and the `CrOS_EC_Python` lib
|
||||
You can either update your kernel to have a working `cros_ec_dev` driver, or run YAFI as root.
|
||||
|
||||
It can also occur if you do not have a CrOS EC, like on non Framework laptops.
|
||||
|
||||
## Disclaimer
|
||||
|
||||
YAFI is not affiliated with Framework Computer Inc. in any way.
|
||||
|
||||
YAFI is licensed under the [GPL-2.0-or-later license](COPYING), and comes with no warranty or guarantee of any kind. Use at your own risk.
|
||||
|
||||
7
add-udev-rules.sh
Executable file
7
add-udev-rules.sh
Executable 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.
|
||||
@@ -7,7 +7,21 @@
|
||||
<name>Yet Another Framework Interface</name>
|
||||
<summary>YAFI is another GUI for the Framework Laptop Embedded Controller</summary>
|
||||
<description>
|
||||
<p>It is written in Python with a GTK4 Adwaita theme, and uses the `CrOS_EC_Python` library to communicate with the EC.</p>
|
||||
<p>It is written in Python with a GTK4 Adwaita theme, and uses the CrOS_EC_Python library to communicate with the EC.</p>
|
||||
<p>YAFI has the capability for the following features:</p>
|
||||
<ul>
|
||||
<li>Fan control and temperature monitoring</li>
|
||||
<li>LED control</li>
|
||||
<li>Battery statistics</li>
|
||||
<li>Battery limiting</li>
|
||||
<li>Hardware information</li>
|
||||
</ul>
|
||||
|
||||
<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>
|
||||
</description>
|
||||
|
||||
<developer id="au.stevetech">
|
||||
@@ -74,6 +88,12 @@
|
||||
</screenshots>
|
||||
|
||||
<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">
|
||||
<url type="details">https://github.com/Steve-Tech/YAFI/releases/tag/0.6</url>
|
||||
<description>
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 86 KiB After Width: | Height: | Size: 161 KiB |
@@ -1,5 +1,5 @@
|
||||
project('yafi',
|
||||
version: '0.6',
|
||||
version: '0.7',
|
||||
meson_version: '>= 1.0.0',
|
||||
default_options: [ 'warning_level=2', 'werror=false', ],
|
||||
)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[project]
|
||||
name = "yafi"
|
||||
version = "0.6"
|
||||
version = "0.7"
|
||||
authors = [
|
||||
{ name="Steve-Tech" }
|
||||
]
|
||||
@@ -8,7 +8,7 @@ description = "Yet Another Framework Interface"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.10"
|
||||
dependencies = [
|
||||
"cros_ec_python >= 0.2.0",
|
||||
"cros_ec_python >= 0.3.0",
|
||||
"PyGObject"
|
||||
]
|
||||
classifiers = [
|
||||
|
||||
@@ -7,8 +7,8 @@
|
||||
"sources": [
|
||||
{
|
||||
"type": "file",
|
||||
"url": "https://files.pythonhosted.org/packages/33/11/c23a7acaa333589921a2f524517eb719dfb72628153ae22fcdf2e9052ac6/cros_ec_python-0.2.0-py3-none-any.whl",
|
||||
"sha256": "d38e493fbcaf23bc4b613d1342a036cecc6506284afc74f37013a3eac85a01b9"
|
||||
"url": "https://files.pythonhosted.org/packages/6c/7a/10d978a02bbe37530490cfd14e0994c433dc29c81b3afcdbde453d512528/cros_ec_python-0.3.0-py3-none-any.whl",
|
||||
"sha256": "aeb14ebdbd60ec6d6a4b11df1482a295466da4a908a468d168efd4cc141e7e3d"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
21
yafi/leds.py
21
yafi/leds.py
@@ -48,11 +48,19 @@ class LedsPage(Gtk.Box):
|
||||
ec_commands.framework_laptop.set_fp_led_level(app.cros_ec, value)
|
||||
self.led_pwr.set_subtitle(["High", "Medium", "Low"][value])
|
||||
|
||||
current_fp_level = ec_commands.framework_laptop.get_fp_led_level(
|
||||
app.cros_ec
|
||||
).value
|
||||
self.led_pwr_scale.set_value(abs(current_fp_level - 2))
|
||||
self.led_pwr.set_subtitle(["High", "Medium", "Low"][current_fp_level])
|
||||
try:
|
||||
current_fp_level = ec_commands.framework_laptop.get_fp_led_level(
|
||||
app.cros_ec
|
||||
).value
|
||||
self.led_pwr_scale.set_value(abs(current_fp_level - 2))
|
||||
self.led_pwr.set_subtitle(["High", "Medium", "Low"][current_fp_level])
|
||||
except ValueError:
|
||||
# LED isn't a normal value
|
||||
current_fp_level = ec_commands.framework_laptop.get_fp_led_level_int(
|
||||
app.cros_ec
|
||||
)
|
||||
self.led_pwr.set_subtitle(f"Custom ({current_fp_level}%)")
|
||||
|
||||
self.led_pwr_scale.connect("value-changed", handle_led_pwr)
|
||||
except ec_exceptions.ECError as e:
|
||||
if e.ec_status == ec_exceptions.EcStatus.EC_RES_INVALID_COMMAND:
|
||||
@@ -111,6 +119,9 @@ class LedsPage(Gtk.Box):
|
||||
else:
|
||||
raise e
|
||||
|
||||
# Power LED does not support Blue, even though Intel models think they do
|
||||
leds[ec_commands.leds.EcLedId.EC_LED_ID_POWER_LED][2] = 0
|
||||
|
||||
def handle_led_colour(combobox, led_id):
|
||||
colour = combobox.get_selected() - 2
|
||||
match colour:
|
||||
|
||||
@@ -135,14 +135,15 @@ class YafiApplication(Adw.Application):
|
||||
about = Adw.AboutDialog(
|
||||
application_icon="au.stevetech.yafi",
|
||||
application_name="Yet Another Framework Interface",
|
||||
comments="YAFI is another GUI for the Framework Laptop Embedded Controller.\n"
|
||||
+ "It is written in Python with a GTK3 theme, and uses the `CrOS_EC_Python` library to communicate with the EC.",
|
||||
comments="YAFI is another GUI for the Framework Laptop Embedded Controller.\n\n"
|
||||
+ "It is written in Python with a GTK4 Adwaita theme, and uses the CrOS_EC_Python library to communicate with the EC.\n\n"
|
||||
+ "YAFI is not affiliated with Framework Computer Inc. in any way.",
|
||||
copyright="© 2025 Stephen Horvath",
|
||||
developer_name="Stephen Horvath",
|
||||
developers=["Stephen Horvath https://github.com/Steve-Tech"],
|
||||
issue_url="https://github.com/Steve-Tech/YAFI/issues",
|
||||
license_type=Gtk.License.GPL_2_0,
|
||||
version="0.6",
|
||||
version="0.7",
|
||||
website="https://github.com/Steve-Tech/YAFI",
|
||||
)
|
||||
about.add_acknowledgement_section(None, ["Framework Computer Inc. https://frame.work/"])
|
||||
|
||||
125
yafi/thermals.py
125
yafi/thermals.py
@@ -27,11 +27,17 @@ import cros_ec_python.exceptions as ec_exceptions
|
||||
class ThermalsPage(Gtk.Box):
|
||||
__gtype_name__ = 'ThermalsPage'
|
||||
|
||||
first_run = True
|
||||
|
||||
fan_rpm = Gtk.Template.Child()
|
||||
fan_mode = Gtk.Template.Child()
|
||||
fan_set_rpm = Gtk.Template.Child()
|
||||
fan_set_percent = 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()
|
||||
temp_items = []
|
||||
@@ -40,10 +46,40 @@ class ThermalsPage(Gtk.Box):
|
||||
super().__init__(**kwargs)
|
||||
|
||||
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
|
||||
if ec_commands.general.get_cmd_versions(
|
||||
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):
|
||||
match mode:
|
||||
@@ -51,13 +87,18 @@ class ThermalsPage(Gtk.Box):
|
||||
self.fan_set_rpm.set_visible(False)
|
||||
self.fan_set_percent.set_visible(False)
|
||||
ec_commands.thermal.thermal_auto_fan_ctrl(app.cros_ec)
|
||||
self.fan_set_points.set_visible(self.ec_set_points_supported)
|
||||
case 1: # Percent
|
||||
self.fan_set_points.set_visible(False)
|
||||
self.fan_set_rpm.set_visible(False)
|
||||
self.fan_set_percent.set_visible(True)
|
||||
case 2: # RPM
|
||||
self.fan_set_points.set_visible(False)
|
||||
self.fan_set_rpm.set_visible(True)
|
||||
self.fan_set_percent.set_visible(False)
|
||||
|
||||
handle_fan_mode(self.fan_mode.get_selected())
|
||||
|
||||
self.fan_mode.connect(
|
||||
"notify::selected",
|
||||
lambda combo, _: handle_fan_mode(combo.get_selected()),
|
||||
@@ -81,41 +122,77 @@ class ThermalsPage(Gtk.Box):
|
||||
):
|
||||
|
||||
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)
|
||||
|
||||
self.fan_set_rpm.connect(
|
||||
"notify::text", lambda entry, _: handle_fan_rpm(entry)
|
||||
"notify::value", lambda entry, _: handle_fan_rpm(entry)
|
||||
)
|
||||
else:
|
||||
self.fan_set_rpm.set_sensitive(False)
|
||||
else:
|
||||
self.fan_mode.set_sensitive(False)
|
||||
|
||||
# Temperature sensors
|
||||
while temp_child := self.temperatures.get_last_child():
|
||||
self.temperatures.remove(temp_child)
|
||||
self.temp_items.clear()
|
||||
# Set points
|
||||
if self.ec_set_points_supported and self.first_run:
|
||||
def handle_set_point(entry, key):
|
||||
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:
|
||||
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 i, sensor in enumerate(ec_temp_sensors):
|
||||
ec_set_point = ec_commands.thermal.thermal_get_thresholds(app.cros_ec, i)
|
||||
self.ec_set_points.append(ec_set_point)
|
||||
off_row = Adw.SpinRow(
|
||||
title=f"Fan On - {sensor}",
|
||||
subtitle=f"Turn fan on when above temp (°C)",
|
||||
)
|
||||
off_row.ec_index = i
|
||||
# 0K to 65535K for 16bit unsigned range
|
||||
# Actually the EC takes 32bits, but let's keep it like this for sanity
|
||||
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():
|
||||
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)
|
||||
self.first_run = False
|
||||
|
||||
# Schedule _update_thermals to run every second
|
||||
GLib.timeout_add_seconds(1, self._update_thermals, app)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<?xml version='1.0' encoding='UTF-8'?>
|
||||
<!-- Created with Cambalache 0.96.1 -->
|
||||
<!-- Created with Cambalache 0.96.3 -->
|
||||
<interface>
|
||||
<!-- interface-name thermals.ui -->
|
||||
<!-- interface-description The Thermals page for YAFI -->
|
||||
@@ -92,6 +92,12 @@
|
||||
<property name="visible">False</property>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="AdwExpanderRow" id="fan_set_points">
|
||||
<property name="selectable">False</property>
|
||||
<property name="title">Fan Set Points</property>
|
||||
</object>
|
||||
</child>
|
||||
<style>
|
||||
<class name="boxed-list"/>
|
||||
</style>
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
<?xml version='1.0' encoding='UTF-8' standalone='no'?>
|
||||
<!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">
|
||||
<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="BatteryLimiterPage" filename="battery-limiter.ui" sha256="b5d41b19cb1fb7ca5b4bcfae43244e54111f5e8d8c51d95448d6a92b5185d2c4"/>
|
||||
<ui template-class="HardwarePage" filename="hardware.ui" sha256="37ea282198d9f60435f80e4adf8256cd2249e590dcad4b63af634d828673f1bf"/>
|
||||
|
||||
Binary file not shown.
Reference in New Issue
Block a user