From c71ca76235b5bcf08f26b581fdd74deee21d6c7b Mon Sep 17 00:00:00 2001 From: Richard Hughes Date: Sun, 13 Feb 2022 21:11:39 +0000 Subject: [PATCH] Add a plugin to set GPIOs for the duration of an update --- .lgtm.yml | 2 +- contrib/ci/build_windows.sh | 1 + contrib/ci/check-abi | 1 + contrib/freebsd/Makefile | 1 + contrib/fwupd.spec.in | 1 + libfwupdplugin/fu-udev-device.c | 2 +- meson.build | 6 + meson_options.txt | 1 + plugins/gpio/README.md | 34 +++++ plugins/gpio/fu-gpio-device.c | 211 ++++++++++++++++++++++++++++++++ plugins/gpio/fu-gpio-device.h | 17 +++ plugins/gpio/fu-plugin-gpio.c | 164 +++++++++++++++++++++++++ plugins/gpio/gpio.quirk | 3 + plugins/gpio/meson.build | 41 +++++++ plugins/meson.build | 1 + snap/snapcraft.yaml | 1 + 16 files changed, 485 insertions(+), 2 deletions(-) create mode 100644 plugins/gpio/README.md create mode 100644 plugins/gpio/fu-gpio-device.c create mode 100644 plugins/gpio/fu-gpio-device.h create mode 100644 plugins/gpio/fu-plugin-gpio.c create mode 100644 plugins/gpio/gpio.quirk create mode 100644 plugins/gpio/meson.build diff --git a/.lgtm.yml b/.lgtm.yml index 19f03b580..06280a179 100644 --- a/.lgtm.yml +++ b/.lgtm.yml @@ -24,7 +24,7 @@ extraction: - "export LD_LIBRARY_PATH=$LGTM_WORKSPACE/installdir/usr/lib:$LD_LIBRARY_PATH" configure: command: - - "meson setup _lgtm_build_dir -Defi_binary=false -Dplugin_uefi_capsule_splash=false -Ddocs=none" + - "meson setup _lgtm_build_dir -Defi_binary=false -Dplugin_uefi_capsule_splash=false -Dplugin_gpio=false -Ddocs=none" index: build_command: - "ninja -C _lgtm_build_dir" diff --git a/contrib/ci/build_windows.sh b/contrib/ci/build_windows.sh index 8d273295d..cefa80d22 100755 --- a/contrib/ci/build_windows.sh +++ b/contrib/ci/build_windows.sh @@ -44,6 +44,7 @@ meson .. \ -Doffline=false \ -Dplugin_emmc=false \ -Dplugin_amt=false \ + -Dplugin_gpio=false \ -Dplugin_mtd=false \ -Dintrospection=false \ -Dplugin_thunderbolt=false \ diff --git a/contrib/ci/check-abi b/contrib/ci/check-abi index 1d7d637f6..fc408fb64 100755 --- a/contrib/ci/check-abi +++ b/contrib/ci/check-abi @@ -87,6 +87,7 @@ def build_install(revision): "--prefix=/usr", "--libdir=lib", "-Db_coverage=false", + "-Dplugin_gpio=false", "-Dgtkdoc=false", "-Ddocs=none", "-Dgusb:docs=false", diff --git a/contrib/freebsd/Makefile b/contrib/freebsd/Makefile index 882afd19a..a80bb7188 100644 --- a/contrib/freebsd/Makefile +++ b/contrib/freebsd/Makefile @@ -44,6 +44,7 @@ MESON_ARGS= -Dgudev=false \ -Dplugin_amt=false \ -Dplugin_dell=false \ -Dplugin_emmc=false \ + -Dplugin_gpio=false \ -Dplugin_nvme=false \ -Dplugin_parade_lspcon=false \ -Dplugin_redfish=false \ diff --git a/contrib/fwupd.spec.in b/contrib/fwupd.spec.in index b8ef3288a..aa010cd13 100644 --- a/contrib/fwupd.spec.in +++ b/contrib/fwupd.spec.in @@ -434,6 +434,7 @@ done %{_libdir}/fwupd-plugins-%{fwupdplugin_version}/libfu_plugin_ep963x.so %{_libdir}/fwupd-plugins-%{fwupdplugin_version}/libfu_plugin_fastboot.so %{_libdir}/fwupd-plugins-%{fwupdplugin_version}/libfu_plugin_fresco_pd.so +%{_libdir}/fwupd-plugins-%{fwupdplugin_version}/libfu_plugin_gpio.so %{_libdir}/fwupd-plugins-%{fwupdplugin_version}/libfu_plugin_hailuck.so %{_libdir}/fwupd-plugins-%{fwupdplugin_version}/libfu_plugin_iommu.so %{_libdir}/fwupd-plugins-%{fwupdplugin_version}/libfu_plugin_jabra.so diff --git a/libfwupdplugin/fu-udev-device.c b/libfwupdplugin/fu-udev-device.c index d17a03e4e..97a4ad5bb 100644 --- a/libfwupdplugin/fu-udev-device.c +++ b/libfwupdplugin/fu-udev-device.c @@ -1114,7 +1114,7 @@ fu_udev_device_set_physical_id(FuUdevDevice *self, const gchar *subsystems, GErr } else if (g_strcmp0(subsystem, "usb") == 0 || g_strcmp0(subsystem, "mmc") == 0 || g_strcmp0(subsystem, "i2c") == 0 || g_strcmp0(subsystem, "platform") == 0 || g_strcmp0(subsystem, "scsi") == 0 || g_strcmp0(subsystem, "mtd") == 0 || - g_strcmp0(subsystem, "block") == 0) { + g_strcmp0(subsystem, "block") == 0 || g_strcmp0(subsystem, "gpio") == 0) { tmp = g_udev_device_get_property(udev_device, "DEVPATH"); if (tmp == NULL) { g_set_error_literal(error, diff --git a/meson.build b/meson.build index 9afa2a8b8..ce216c97a 100644 --- a/meson.build +++ b/meson.build @@ -383,6 +383,12 @@ if cc.has_header('linux/ipmi.h') else have_linux_ipmi = false endif +if cc.has_header_symbol('linux/gpio.h', 'GPIO_V2_LINE_FLAG_OUTPUT') + have_linux_gpio = true + conf.set('HAVE_LINUX_GPIO_H', '1') +else + have_linux_gpio = false +endif if cc.has_header_symbol('linux/ipmi_msgdefs.h', 'IPMI_DEVICE_IN_FW_UPDATE_ERR') conf.set('HAVE_IPMI_DEVICE_IN_FW_UPDATE_ERR', '1') endif diff --git a/meson_options.txt b/meson_options.txt index 1b8b68aa2..7414ffa82 100644 --- a/meson_options.txt +++ b/meson_options.txt @@ -24,6 +24,7 @@ option('plugin_dummy', type : 'boolean', value : false, description : 'enable th option('plugin_emmc', type : 'boolean', value : true, description : 'enable eMMC support') option('plugin_ep963x', type : 'boolean', value : true, description : 'enable EP963x support') option('plugin_fastboot', type : 'boolean', value : true, description : 'enable Fastboot support') +option('plugin_gpio', type : 'boolean', value : true, description : 'enable GPIO support') option('plugin_logitech_bulkcontroller', type : 'boolean', value : true, description : 'enable Logitech bulk controller support') option('plugin_parade_lspcon', type : 'boolean', value : true, description : 'enable Parade LSPCON support') option('plugin_pixart_rf', type : 'boolean', value : true, description : 'enable PixartRF support') diff --git a/plugins/gpio/README.md b/plugins/gpio/README.md new file mode 100644 index 000000000..e10bdfff6 --- /dev/null +++ b/plugins/gpio/README.md @@ -0,0 +1,34 @@ +# GPIO + +## Introduction + +This plugin sets GPIO outputs either high or low before and/or after an +update has been deployed. + +## GUID Generation + +These device use GPIO `gpiochip_info.label` values, e.g. + +* `GPIO\ID_INT3450:00` + +## Quirk Use + +This plugin uses the following plugin-specific quirks: + +### GpioForUpdate + +The GPIO bit to set before the update is deployed e.g. `INT3450:00,SPI_MUX,high`. +After the update has finished, the bits are returned to the default state. + +For example, to set GPIO pin 2 low for the duration of the ColorHug device update +this could be added to the quirk file: + + [USB\VID_273F&PID_1001] + GpioForUpdate=fake-gpio-chip,2,low + +Since: 1.7.6 + +## External Interface Access + +This plugin requires ioctl `GPIO_GET_CHIPINFO_IOCTL` and `GPIO_V2_GET_LINE_IOCTL` +access on `/dev/gpiochip*` devices. diff --git a/plugins/gpio/fu-gpio-device.c b/plugins/gpio/fu-gpio-device.c new file mode 100644 index 000000000..0aa5dc3e9 --- /dev/null +++ b/plugins/gpio/fu-gpio-device.c @@ -0,0 +1,211 @@ +/* + * Copyright (C) 2022 Richard Hughes + * + * SPDX-License-Identifier: LGPL-2.1+ + */ + +#include "config.h" + +#include +#include + +#include "fu-gpio-device.h" + +struct _FuGpioDevice { + FuUdevDevice parent_instance; + guint num_lines; + gint fd; /* valid when the GPIO bit is assigned */ +}; + +G_DEFINE_TYPE(FuGpioDevice, fu_gpio_device, FU_TYPE_UDEV_DEVICE) + +static void +fu_gpio_device_to_string(FuDevice *device, guint idt, GString *str) +{ + FuGpioDevice *self = FU_GPIO_DEVICE(device); + FU_DEVICE_CLASS(fu_gpio_device_parent_class)->to_string(device, idt, str); + fu_common_string_append_ku(str, idt, "NumLines", self->num_lines); + fu_common_string_append_kb(str, idt, "FdOpen", self->fd > 0); +} + +static gboolean +fu_gpio_device_probe(FuDevice *device, GError **error) +{ + /* FuUdevDevice->probe */ + if (!FU_DEVICE_CLASS(fu_gpio_device_parent_class)->probe(device, error)) + return FALSE; + + /* set the physical ID */ + return fu_udev_device_set_physical_id(FU_UDEV_DEVICE(device), "gpio", error); +} + +static gboolean +fu_gpio_device_setup(FuDevice *device, GError **error) +{ + FuGpioDevice *self = FU_GPIO_DEVICE(device); + struct gpiochip_info info = {0x0}; + + /* get info */ + if (!fu_udev_device_ioctl(FU_UDEV_DEVICE(self), + GPIO_GET_CHIPINFO_IOCTL, + (guint8 *)&info, + NULL, + error)) { + g_prefix_error(error, "failed to get chipinfo: "); + return FALSE; + } + + /* sanity check */ + self->num_lines = info.lines; + if (self->num_lines == 0) { + g_set_error_literal(error, + FWUPD_ERROR, + FWUPD_ERROR_NOT_SUPPORTED, + "0 lines is not supported"); + return FALSE; + } + + /* label is optional, but name is always set */ + if (info.label[0] != '\0') { + g_autofree gchar *logical_id = fu_common_strsafe(info.label, sizeof(info.label)); + g_autofree gchar *instance_id = NULL; + fu_device_set_logical_id(device, logical_id); + instance_id = g_strdup_printf("GPIO\\ID_%s", logical_id); + fu_device_add_instance_id(device, instance_id); + } + + /* success */ + return TRUE; +} + +gboolean +fu_gpio_device_unassign(FuGpioDevice *self, GError **error) +{ + if (self->fd < 0) + return TRUE; + g_debug("unsetting %s", fu_device_get_logical_id(FU_DEVICE(self))); + if (!g_close(self->fd, error)) + return FALSE; + self->fd = -1; + return TRUE; +} + +static gboolean +fu_gpio_device_assign_full(FuGpioDevice *self, guint64 line, gboolean value, GError **error) +{ + const gchar consumer[] = "fwupd"; + struct gpio_v2_line_request req = { + .num_lines = 1, + req.offsets[0] = line, + .config.flags = GPIO_V2_LINE_FLAG_OUTPUT, + .config.num_attrs = 1, + .config.attrs[0].attr.values = value ? 0x1 : 0x0, + .config.attrs[0].mask = 0x1, + }; + + /* this is useful if we have contention with other tools */ + if (!fu_memcpy_safe((guint8 *)req.consumer, + sizeof(req.consumer), + 0x0, /* dst */ + (const guint8 *)consumer, + sizeof(consumer), + 0x0, /* src */ + sizeof(consumer), + error)) + return FALSE; + + /* slightly weird API, but roll with it */ + g_debug("setting %s:0x%02x → %i", + fu_device_get_logical_id(FU_DEVICE(self)), + (guint)line, + value); + if (!fu_udev_device_ioctl(FU_UDEV_DEVICE(self), + GPIO_V2_GET_LINE_IOCTL, + (guint8 *)&req, + NULL, + error)) { + g_prefix_error(error, "failed to assign: "); + return FALSE; + } + + /* success */ + self->fd = req.fd; + return TRUE; +} + +gboolean +fu_gpio_device_assign(FuGpioDevice *self, const gchar *id, gboolean value, GError **error) +{ + guint64 line = G_MAXUINT64; + + /* sanity check */ + if (self->fd > 0) { + g_set_error(error, + FWUPD_ERROR, + FWUPD_ERROR_NOT_FOUND, + "GPIO %s already in use", + id); + return FALSE; + } + + /* specified as a number, or look for @id as named pin */ + if (fu_common_strtoull_full(id, &line, 0, self->num_lines - 1, NULL)) { + struct gpio_v2_line_info info = {.offset = line}; + if (!fu_udev_device_ioctl(FU_UDEV_DEVICE(self), + GPIO_V2_GET_LINEINFO_IOCTL, + (guint8 *)&info, + NULL, + error)) { + g_prefix_error(error, "failed to get lineinfo: "); + return FALSE; + } + } else { + for (guint i = 0; i < self->num_lines; i++) { + struct gpio_v2_line_info info = {.offset = i}; + g_autofree gchar *name = NULL; + if (!fu_udev_device_ioctl(FU_UDEV_DEVICE(self), + GPIO_V2_GET_LINEINFO_IOCTL, + (guint8 *)&info, + NULL, + error)) { + g_prefix_error(error, "failed to get lineinfo: "); + return FALSE; + } + name = fu_common_strsafe(info.name, sizeof(info.name)); + if (g_strcmp0(name, id) == 0) { + line = i; + break; + } + } + } + if (line == G_MAXUINT64) { + g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_FOUND, "failed to find %s", id); + return FALSE; + } + return fu_gpio_device_assign_full(self, line, value, error); +} + +static void +fu_gpio_device_init(FuGpioDevice *self) +{ +} + +static void +fu_gpio_device_finalize(GObject *object) +{ + FuGpioDevice *self = FU_GPIO_DEVICE(object); + if (self->fd > 0) + g_close(self->fd, NULL); + G_OBJECT_CLASS(fu_gpio_device_parent_class)->finalize(object); +} + +static void +fu_gpio_device_class_init(FuGpioDeviceClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS(klass); + FuDeviceClass *klass_device = FU_DEVICE_CLASS(klass); + object_class->finalize = fu_gpio_device_finalize; + klass_device->to_string = fu_gpio_device_to_string; + klass_device->setup = fu_gpio_device_setup; + klass_device->probe = fu_gpio_device_probe; +} diff --git a/plugins/gpio/fu-gpio-device.h b/plugins/gpio/fu-gpio-device.h new file mode 100644 index 000000000..5c05fc947 --- /dev/null +++ b/plugins/gpio/fu-gpio-device.h @@ -0,0 +1,17 @@ +/* + * Copyright (C) 2022 Richard Hughes + * + * SPDX-License-Identifier: LGPL-2.1+ + */ + +#pragma once + +#include + +#define FU_TYPE_GPIO_DEVICE (fu_gpio_device_get_type()) +G_DECLARE_FINAL_TYPE(FuGpioDevice, fu_gpio_device, FU, GPIO_DEVICE, FuUdevDevice) + +gboolean +fu_gpio_device_assign(FuGpioDevice *self, const gchar *id, gboolean value, GError **error); +gboolean +fu_gpio_device_unassign(FuGpioDevice *self, GError **error); diff --git a/plugins/gpio/fu-plugin-gpio.c b/plugins/gpio/fu-plugin-gpio.c new file mode 100644 index 000000000..1f3f9acc2 --- /dev/null +++ b/plugins/gpio/fu-plugin-gpio.c @@ -0,0 +1,164 @@ +/* + * Copyright (C) 2022 Richard Hughes + * + * SPDX-License-Identifier: LGPL-2.1+ + */ + +#include "config.h" + +#include + +#include "fu-gpio-device.h" + +struct FuPluginData { + GPtrArray *current_logical_ids; /* element-type: utf-8 */ +}; + +static void +fu_plugin_gpio_init(FuPlugin *plugin) +{ + FuContext *ctx = fu_plugin_get_context(plugin); + FuPluginData *data = fu_plugin_alloc_data(plugin, sizeof(FuPluginData)); + data->current_logical_ids = g_ptr_array_new_with_free_func(g_free); + fu_context_add_quirk_key(ctx, "GpioForUpdate"); + fu_plugin_add_udev_subsystem(plugin, "gpio"); + fu_plugin_add_device_gtype(plugin, FU_TYPE_GPIO_DEVICE); +} + +static void +fu_plugin_gpio_destroy(FuPlugin *plugin) +{ + FuPluginData *data = fu_plugin_get_data(plugin); + g_ptr_array_unref(data->current_logical_ids); +} + +static gboolean +fu_plugin_gpio_parse_level(const gchar *str, gboolean *ret, GError **error) +{ + if (g_strcmp0(str, "high") == 0) { + *ret = TRUE; + return TRUE; + } + if (g_strcmp0(str, "low") == 0) { + *ret = FALSE; + return TRUE; + } + g_set_error(error, + G_IO_ERROR, + G_IO_ERROR_INVALID_DATA, + "cannot parse level, got %s and expected high|low", + str); + return FALSE; +} + +static gboolean +fu_plugin_gpio_process_quirk(FuPlugin *self, const gchar *str, GError **error) +{ + FuPluginData *data = fu_plugin_get_data(self); + gboolean value = FALSE; + FuDevice *device_tmp; + g_auto(GStrv) split = g_strsplit(str, ",", -1); + g_autoptr(FuDeviceLocker) locker = NULL; + + /* sanity check */ + if (g_strv_length(split) != 3) { + g_set_error(error, + G_IO_ERROR, + G_IO_ERROR_INVALID_DATA, + "invalid format, CHIP_NAME,PIN_NAME,LEVEL, got '%s'", + str); + return FALSE; + } + if (!fu_plugin_gpio_parse_level(split[2], &value, error)) + return FALSE; + device_tmp = fu_plugin_cache_lookup(self, split[0]); + if (device_tmp == NULL) { + g_set_error(error, + FWUPD_ERROR, + FWUPD_ERROR_NOT_FOUND, + "GPIO device %s not found", + split[0]); + return FALSE; + } + locker = fu_device_locker_new(device_tmp, error); + if (locker == NULL) + return FALSE; + if (!fu_gpio_device_assign(FU_GPIO_DEVICE(device_tmp), split[1], value, error)) { + g_prefix_error(error, "failed to assign %s: ", split[0]); + return FALSE; + } + + /* success */ + g_ptr_array_add(data->current_logical_ids, g_strdup(fu_device_get_logical_id(device_tmp))); + return TRUE; +} + +static gboolean +fu_plugin_gpio_prepare(FuPlugin *self, FuDevice *device, FwupdInstallFlags flags, GError **error) +{ + GPtrArray *guids = fu_device_get_guids(device); + for (guint i = 0; i < guids->len; i++) { + const gchar *guid = g_ptr_array_index(guids, i); + const gchar *str; + str = fu_context_lookup_quirk_by_id(fu_plugin_get_context(self), + guid, + "GpioForUpdate"); + if (str == NULL) + continue; + if (!fu_plugin_gpio_process_quirk(self, str, error)) + return FALSE; + } + return TRUE; +} + +static gboolean +fu_plugin_gpio_cleanup(FuPlugin *self, FuDevice *device, FwupdInstallFlags flags, GError **error) +{ + FuPluginData *data = fu_plugin_get_data(self); + g_autoptr(GPtrArray) current_logical_ids = NULL; + + /* deep copy to local to clear transaction array */ + current_logical_ids = + g_ptr_array_copy(data->current_logical_ids, (GCopyFunc)g_strdup, NULL); + g_ptr_array_set_size(data->current_logical_ids, 0); + + /* close the fds we opened during ->prepare */ + for (guint i = 0; i < current_logical_ids->len; i++) { + FuDevice *device_tmp; + const gchar *current_logical_id = g_ptr_array_index(current_logical_ids, i); + + device_tmp = fu_plugin_cache_lookup(self, current_logical_id); + if (device_tmp == NULL) { + g_set_error(error, + FWUPD_ERROR, + FWUPD_ERROR_NOT_FOUND, + "GPIO device %s no longer found", + current_logical_id); + return FALSE; + } + if (!fu_gpio_device_unassign(FU_GPIO_DEVICE(device_tmp), error)) { + g_prefix_error(error, "failed to unassign %s: ", current_logical_id); + return FALSE; + } + } + + /* success */ + return TRUE; +} + +static void +fu_plugin_gpio_device_added(FuPlugin *self, FuDevice *device) +{ + fu_plugin_cache_add(self, fu_device_get_logical_id(device), device); +} + +void +fu_plugin_init_vfuncs(FuPluginVfuncs *vfuncs) +{ + vfuncs->build_hash = FU_BUILD_HASH; + vfuncs->init = fu_plugin_gpio_init; + vfuncs->destroy = fu_plugin_gpio_destroy; + vfuncs->prepare = fu_plugin_gpio_prepare; + vfuncs->cleanup = fu_plugin_gpio_cleanup; + vfuncs->device_added = fu_plugin_gpio_device_added; +} diff --git a/plugins/gpio/gpio.quirk b/plugins/gpio/gpio.quirk new file mode 100644 index 000000000..0400f0533 --- /dev/null +++ b/plugins/gpio/gpio.quirk @@ -0,0 +1,3 @@ +# match all devices with this udev subsystem +[GPIO] +Plugin = gpio diff --git a/plugins/gpio/meson.build b/plugins/gpio/meson.build new file mode 100644 index 000000000..0c7c3b313 --- /dev/null +++ b/plugins/gpio/meson.build @@ -0,0 +1,41 @@ +if get_option('plugin_gpio') +if not get_option('gudev') + error('gudev is required for plugin_gpio') +endif +if not have_linux_gpio + error('linux/gpio.h (with API v2) is required for plugin_gpio') +endif +cargs = ['-DG_LOG_DOMAIN="FuPluginGpio"'] + +install_data([ + 'gpio.quirk', + ], + install_dir: join_paths(datadir, 'fwupd', 'quirks.d') +) + +shared_module('fu_plugin_gpio', + fu_hash, + sources : [ + 'fu-plugin-gpio.c', + 'fu-gpio-device.c', + ], + include_directories : [ + root_incdir, + fwupd_incdir, + fwupdplugin_incdir, + ], + install : true, + install_dir: plugin_dir, + c_args : [ + cargs, + '-DLOCALSTATEDIR="' + localstatedir + '"', + ], + link_with : [ + fwupd, + fwupdplugin, + ], + dependencies : [ + plugin_deps, + ], +) +endif diff --git a/plugins/meson.build b/plugins/meson.build index 3387bb09f..6da3d1e7b 100644 --- a/plugins/meson.build +++ b/plugins/meson.build @@ -25,6 +25,7 @@ subdir('fastboot') subdir('flashrom') subdir('fresco-pd') subdir('goodix-moc') +subdir('gpio') subdir('hailuck') subdir('intel-spi') subdir('iommu') diff --git a/snap/snapcraft.yaml b/snap/snapcraft.yaml index 812459672..11715bb38 100644 --- a/snap/snapcraft.yaml +++ b/snap/snapcraft.yaml @@ -191,6 +191,7 @@ parts: -Dman=false, -Dplugin_modem_manager=true, -Dplugin_powerd=false, + -Dplugin_gpio=false, -Dudevdir=$SNAPCRAFT_STAGE/lib/udev, "-Dgusb:tests=false", "-Dgusb:docs=false",