Skip to content

Commit 20c3cb8

Browse files
committed
Firmware: Add settings page
Show fwupd firmware settings alongside updates, including allowed values and pending reboot state. Test=Edited settings through an isolated D-Bus service with native GTK and libfwupd; checked constraints, errors and Back navigation during a write.
1 parent 88cf2cb commit 20c3cb8

6 files changed

Lines changed: 263 additions & 2 deletions

File tree

‎README.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
You'll need the following dependencies:
99

1010
* libswitchboard-3-dev
11-
* libfwupd-dev
11+
* libfwupd-dev (>= 1.8.4, for the BIOS settings API)
1212
* libgranite-7-dev
1313
* libgtk-4-dev
1414
* libgtop2-dev

‎meson.build‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ datadir = join_paths(prefix, get_option('datadir'))
1414
libdir = join_paths(prefix, get_option('libdir'))
1515

1616
glib_dep = dependency('glib-2.0', version: '>=2.64.0')
17-
fwupd_dep = dependency('fwupd')
17+
fwupd_dep = dependency('fwupd', version: '>=1.8.4')
1818
appstream_dep = dependency ('appstream', version: '>=0.12.10')
1919

2020
add_project_arguments(

‎src/Plug.vala‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,7 @@ public class About.Plug : Switchboard.Plug {
142142
search_results.set ("%s → %s".printf (display_name, _("About This Device")), HARDWARE);
143143

144144
search_results.set ("%s → %s".printf (display_name, _("Firmware")), FIRMWARE);
145+
search_results.set ("%s → %s → %s".printf (display_name, _("Firmware"), _("Firmware Settings…")), FIRMWARE);
145146
search_results.set ("%s → %s → %s".printf (display_name, _("Firmware"), _("Restart to Firmware Setup…")), FIRMWARE);
146147

147148
search_results.set ("%s → %s".printf (display_name, _("Drivers")), DRIVERS);
Lines changed: 249 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,249 @@
1+
/*
2+
* SPDX-License-Identifier: GPL-3.0-or-later
3+
*/
4+
5+
public class About.FirmwareSettingsView : Adw.NavigationPage {
6+
private Fwupd.Client client = new Fwupd.Client ();
7+
private Gtk.Box settings_box;
8+
private Gtk.Label status_label;
9+
private Gtk.Label reboot_label;
10+
private Gtk.Button refresh_button;
11+
private bool busy;
12+
13+
construct {
14+
title = _("Firmware Settings");
15+
16+
var back_button = new Granite.BackButton (_("All Updates")) {
17+
action_name = "navigation.pop",
18+
halign = START
19+
};
20+
21+
refresh_button = new Gtk.Button.with_label (_("Refresh"));
22+
var header_bar = new Gtk.HeaderBar () {
23+
show_title_buttons = false,
24+
title_widget = new Gtk.Label (title)
25+
};
26+
header_bar.pack_start (back_button);
27+
header_bar.pack_end (refresh_button);
28+
29+
status_label = new Gtk.Label ("") {
30+
wrap = true,
31+
xalign = 0
32+
};
33+
reboot_label = new Gtk.Label (_("Restart required to apply firmware settings.")) {
34+
wrap = true,
35+
xalign = 0,
36+
visible = false
37+
};
38+
settings_box = new Gtk.Box (VERTICAL, 12);
39+
40+
var content = new Gtk.Box (VERTICAL, 12) {
41+
margin_top = 12,
42+
margin_bottom = 12,
43+
margin_start = 12,
44+
margin_end = 12
45+
};
46+
content.append (status_label);
47+
content.append (reboot_label);
48+
content.append (settings_box);
49+
50+
var toolbarview = new Adw.ToolbarView () {
51+
content = new Gtk.ScrolledWindow () { child = content },
52+
top_bar_style = RAISED_BORDER
53+
};
54+
toolbarview.add_top_bar (header_bar);
55+
child = toolbarview;
56+
57+
refresh_button.clicked.connect (() => load.begin ());
58+
}
59+
60+
public async void load () {
61+
if (busy) {
62+
return;
63+
}
64+
65+
set_busy (true);
66+
yield read_settings ();
67+
set_busy (false);
68+
}
69+
70+
private void set_busy (bool value) {
71+
busy = value;
72+
settings_box.sensitive = !value;
73+
refresh_button.sensitive = !value;
74+
}
75+
76+
private async void read_settings () {
77+
while (settings_box.get_first_child () != null) {
78+
settings_box.remove (settings_box.get_first_child ());
79+
}
80+
81+
status_label.label = _("Loading firmware settings…");
82+
reboot_label.visible = false;
83+
84+
try {
85+
yield client.connect_async (null);
86+
yield client.set_feature_flags_async (Fwupd.FeatureFlags.ALLOW_AUTHENTICATION, null);
87+
var settings = yield client.get_bios_settings_async (null);
88+
foreach (var setting in settings) {
89+
// fwupd exposes this status even when other values are hidden.
90+
if (setting.get_name () == "pending_reboot") {
91+
reboot_label.visible |= setting.get_current_value () == "1";
92+
continue;
93+
}
94+
95+
add_setting (setting);
96+
}
97+
98+
status_label.label = settings_box.get_first_child () == null
99+
? _("Firmware settings are not available on this device.")
100+
: _("Authentication may be required to change firmware settings.");
101+
} catch (Error e) {
102+
status_label.label = _("Unable to load firmware settings: %s").printf (e.message);
103+
}
104+
}
105+
106+
private void add_setting (Fwupd.BiosSetting setting) {
107+
var current = setting.get_current_value ();
108+
var editable = !setting.get_read_only () && current != null && setting.get_id () != null;
109+
var box = new Gtk.Box (VERTICAL, 6);
110+
var name_label = new Gtk.Label (setting.get_name () ?? setting.get_id ()) {
111+
xalign = 0,
112+
wrap = true
113+
};
114+
box.append (name_label);
115+
if (setting.get_description () != null) {
116+
box.append (new Gtk.Label (setting.get_description ()) {
117+
xalign = 0,
118+
wrap = true
119+
});
120+
}
121+
122+
var controls = new Gtk.Box (HORIZONTAL, 6);
123+
Gtk.Entry? entry = null;
124+
Gtk.DropDown? dropdown = null;
125+
126+
if (editable && setting.get_kind () == Fwupd.BiosSettingKind.ENUMERATION) {
127+
var values = new Gtk.StringList (null);
128+
uint selected = Gtk.INVALID_LIST_POSITION;
129+
var possible = setting.get_possible_values ();
130+
for (uint i = 0; i < possible.length; i++) {
131+
values.append (possible[i]);
132+
if (possible[i] == current) {
133+
selected = i;
134+
}
135+
}
136+
dropdown = new Gtk.DropDown (values, null) {
137+
selected = selected,
138+
hexpand = true
139+
};
140+
dropdown.update_relation (Gtk.AccessibleRelation.LABELLED_BY, name_label, null, -1);
141+
controls.append (dropdown);
142+
} else if (editable && (setting.get_kind () == Fwupd.BiosSettingKind.INTEGER ||
143+
setting.get_kind () == Fwupd.BiosSettingKind.STRING)) {
144+
entry = new Gtk.Entry () {
145+
text = current,
146+
hexpand = true
147+
};
148+
entry.update_relation (Gtk.AccessibleRelation.LABELLED_BY, name_label, null, -1);
149+
controls.append (entry);
150+
var constraints = setting.get_kind () == Fwupd.BiosSettingKind.INTEGER
151+
? _("Range: %s–%s; increment: %s").printf (
152+
setting.get_lower_bound ().to_string (), setting.get_upper_bound ().to_string (),
153+
setting.get_scalar_increment ().to_string ())
154+
: _("Length: %s–%s bytes").printf (
155+
setting.get_lower_bound ().to_string (), setting.get_upper_bound ().to_string ());
156+
box.append (new Gtk.Label (constraints) { xalign = 0, wrap = true });
157+
} else {
158+
controls.append (new Gtk.Label (current ?? _("Unavailable")) {
159+
xalign = 0,
160+
wrap = true,
161+
selectable = true
162+
});
163+
box.append (new Gtk.Label (_("Not Editable")) { xalign = 0 });
164+
}
165+
166+
if (entry != null || dropdown != null) {
167+
var apply_button = new Gtk.Button.with_label (_("Apply")) { sensitive = false };
168+
apply_button.update_property (Gtk.AccessibleProperty.LABEL, _("Apply %s").printf (name_label.label), -1);
169+
controls.append (apply_button);
170+
if (entry != null) {
171+
entry.changed.connect (() => {
172+
apply_button.sensitive = entry.text != current && valid_value (setting, entry.text);
173+
});
174+
} else {
175+
dropdown.notify["selected"].connect (() => {
176+
var item = dropdown.selected_item as Gtk.StringObject;
177+
apply_button.sensitive = item != null && item.string != current;
178+
});
179+
}
180+
181+
apply_button.clicked.connect (() => {
182+
var value = entry != null ? entry.text : ((Gtk.StringObject) dropdown.selected_item).string;
183+
if (setting.get_kind () == Fwupd.BiosSettingKind.INTEGER) {
184+
value = uint64.parse (value, 10).to_string ();
185+
}
186+
save.begin (setting.get_id (), value);
187+
});
188+
}
189+
190+
box.append (controls);
191+
settings_box.append (box);
192+
}
193+
194+
private bool valid_value (Fwupd.BiosSetting setting, string value) {
195+
var lower = setting.get_lower_bound ();
196+
var upper = setting.get_upper_bound ();
197+
if (setting.get_kind () == Fwupd.BiosSettingKind.STRING) {
198+
return value.length >= lower && value.length <= upper;
199+
}
200+
201+
// Avoid floating point widgets: firmware integers can span all of uint64.
202+
uint64 number;
203+
if (value.length == 0) {
204+
return false;
205+
}
206+
for (int i = 0; i < value.length; i++) {
207+
if (!value[i].isdigit ()) {
208+
return false;
209+
}
210+
}
211+
if (!uint64.try_parse (value, out number, null, 10) || number < lower || number > upper) {
212+
return false;
213+
}
214+
var increment = setting.get_scalar_increment ();
215+
return increment == 0 || (number - lower) % increment == 0;
216+
}
217+
218+
private async void save (string id, string value) {
219+
if (busy) {
220+
return;
221+
}
222+
223+
set_busy (true);
224+
status_label.label = _("Applying firmware setting…");
225+
try {
226+
var changes = new HashTable<string, string> (str_hash, str_equal);
227+
changes.insert (id, value);
228+
yield client.modify_bios_setting_async ((owned) changes, null);
229+
} catch (Error e) {
230+
var window = get_root () as Gtk.Window;
231+
if (window != null && window.visible) {
232+
var dialog = new Granite.MessageDialog (
233+
_("Unable to change firmware setting"),
234+
e.message,
235+
new ThemedIcon ("dialog-error"),
236+
Gtk.ButtonsType.CLOSE
237+
) {
238+
transient_for = window
239+
};
240+
dialog.response.connect (dialog.destroy);
241+
dialog.present ();
242+
}
243+
}
244+
245+
// Re-fetch the daemon's cached settings, including after a failed write.
246+
yield read_settings ();
247+
set_busy (false);
248+
}
249+
}

‎src/Views/FirmwareView.vala‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,16 @@ public class About.FirmwareView : Switchboard.SettingsPage {
8080

8181
child = frame;
8282

83+
var firmware_settings_view = new FirmwareSettingsView ();
84+
navigation_view.add (firmware_settings_view);
85+
var firmware_settings_button = add_button (_("Firmware Settings…"));
86+
firmware_settings_button.clicked.connect (() => {
87+
if (navigation_view.visible_page != firmware_settings_view) {
88+
navigation_view.push (firmware_settings_view);
89+
}
90+
firmware_settings_view.load.begin ();
91+
});
92+
8393
if (LoginManager.get_instance ().can_reboot_to_firmware_setup ()) {
8494
var reboot_to_firmware_setup_button = add_button (_("Restart to Firmware Setup…"));
8595
reboot_to_firmware_setup_button.clicked.connect (reboot_to_firmware_setup_clicked);

‎src/meson.build‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ plug_files = files(
77
'Utils/ARMPartDecoder.vala',
88
'Views' / 'DriversView.vala',
99
'Views/FirmwareReleaseView.vala',
10+
'Views/FirmwareSettingsView.vala',
1011
'Views/FirmwareView.vala',
1112
'Views/HardwareView.vala',
1213
'Views/OperatingSystemView.vala',

0 commit comments

Comments
 (0)