diff --git a/contrib/fwupd.spec.in b/contrib/fwupd.spec.in index 0ff08dc98..cf7304eb3 100644 --- a/contrib/fwupd.spec.in +++ b/contrib/fwupd.spec.in @@ -448,6 +448,7 @@ done %endif %{_libdir}/fwupd-plugins-%{fwupdplugin_version}/libfu_plugin_mtd.so %{_libdir}/fwupd-plugins-%{fwupdplugin_version}/libfu_plugin_nitrokey.so +%{_libdir}/fwupd-plugins-%{fwupdplugin_version}/libfu_plugin_nordic_hid.so %{_libdir}/fwupd-plugins-%{fwupdplugin_version}/libfu_plugin_nvme.so %{_libdir}/fwupd-plugins-%{fwupdplugin_version}/libfu_plugin_optionrom.so %{_libdir}/fwupd-plugins-%{fwupdplugin_version}/libfu_plugin_parade_lspcon.so diff --git a/plugins/meson.build b/plugins/meson.build index 5720b6e27..de1c85ca7 100644 --- a/plugins/meson.build +++ b/plugins/meson.build @@ -41,6 +41,7 @@ subdir('modem-manager') subdir('msr') subdir('mtd') subdir('nitrokey') +subdir('nordic-hid') subdir('nvme') subdir('optionrom') subdir('parade-lspcon') diff --git a/plugins/nordic-hid/README.md b/plugins/nordic-hid/README.md new file mode 100644 index 000000000..7cc3ddbca --- /dev/null +++ b/plugins/nordic-hid/README.md @@ -0,0 +1,45 @@ +# Nordic Semiconductor HID + +## Introduction + +This plugin is able flash the firmware on: + +* nRF52-Desktop: nrf52840dk development kit + +The plugin is using Nordic Semiconductor +[HID config channel](https://developer.nordicsemi.com/nRF_Connect_SDK/doc/latest/nrf/applications/nrf_desktop/doc/config_channel.html) +to perform devices update. + +## Firmware Format + +The cabinet file contains ZIP archive prepared by Nordic Semiconductor. +This ZIP archive includes 2 signed image blobs for the target +device, one firmware blob per application slot, and the `manifest.json` file with the metadata description. +At the moment only [nRF Secure Immutable Bootloader](https://developer.nordicsemi.com/nRF_Connect_SDK/doc/latest/nrf/samples/bootloader/README.html#bootloader) +aka "B0" is supported and tested. + +This plugin supports the following protocol ID: + +* "Nordic HID Config Channel" + +## GUID Generation + +For GUID generation the standard HIDRAW DeviceInstanceId values are used +with the addition of the target board and bootloader name: + +* `HIDRAW\VEN_1915&DEV_52DE&BOARD_nrf52840dk&BL_B0` -> 22952036-c346-5755-9646-7bf766b28922 +* `HIDRAW\VEN_1915&DEV_52DE&BOARD_nrf52840dk&BL_MCUBOOT` -> 43b38427-fdf5-5400-a23c-f3eb7ea00e7c + +## Update Behavior + +The firmware is deployed when the device is in normal runtime mode, and the +device will reset when the new firmware has been written. + +## Vendor ID Security + +The vendor ID is set from the HID vendor ID, in this instance set +to `HIDRAW:0x1915`. + +## External Interface Access + +This plugin requires ioctl `HIDIOCSFEATURE` and `HIDIOCGFEATURE` access. diff --git a/plugins/nordic-hid/fu-nordic-hid-archive.c b/plugins/nordic-hid/fu-nordic-hid-archive.c new file mode 100644 index 000000000..56cab26c7 --- /dev/null +++ b/plugins/nordic-hid/fu-nordic-hid-archive.c @@ -0,0 +1,186 @@ +/* + * Copyright (C) 2021 Richard Hughes + * Copyright (C) 2021 Denis Pynkin + * + * SPDX-License-Identifier: LGPL-2.1+ + */ + +#include "config.h" + +#include "fu-nordic-hid-archive.h" +#include "fu-nordic-hid-firmware-b0.h" + +/* current version format is 0 */ +#define MAX_VERSION_FORMAT 0 + +struct _FuNordicHidArchive { + FuFirmwareClass parent_instance; +}; + +G_DEFINE_TYPE(FuNordicHidArchive, fu_nordic_hid_archive, FU_TYPE_FIRMWARE) + +static gboolean +fu_nordic_hid_archive_parse(FuFirmware *firmware, + GBytes *fw, + guint64 addr_start, + guint64 addr_end, + FwupdInstallFlags flags, + GError **error) +{ + JsonNode *json_root_node; + JsonObject *json_obj; + JsonArray *json_files; + guint manifest_ver; + guint files_cnt = 0; + GBytes *manifest = NULL; + g_autoptr(FuArchive) archive = NULL; + g_autoptr(JsonParser) parser = json_parser_new(); + + /* load archive */ + archive = fu_archive_new(fw, FU_ARCHIVE_FLAG_IGNORE_PATH, error); + if (archive == NULL) + return FALSE; + manifest = fu_archive_lookup_by_fn(archive, "manifest.json", error); + if (manifest == NULL) + return FALSE; + + /* parse JSON */ + if (!json_parser_load_from_data(parser, + (const gchar *)g_bytes_get_data(manifest, NULL), + (gssize)g_bytes_get_size(manifest), + error)) { + g_prefix_error(error, "manifest not in JSON format: "); + return FALSE; + } + json_root_node = json_parser_get_root(parser); + if (json_root_node == NULL || !JSON_NODE_HOLDS_OBJECT(json_root_node)) { + g_set_error_literal(error, + FWUPD_ERROR, + FWUPD_ERROR_INVALID_FILE, + "manifest invalid as has no root"); + return FALSE; + } + + json_obj = json_node_get_object(json_root_node); + if (!json_object_has_member(json_obj, "format-version")) { + g_set_error_literal(error, + FWUPD_ERROR, + FWUPD_ERROR_INVALID_FILE, + "manifest has invalid format"); + return FALSE; + } + + manifest_ver = json_object_get_int_member(json_obj, "format-version"); + if (manifest_ver > MAX_VERSION_FORMAT) { + g_set_error_literal(error, + FWUPD_ERROR, + FWUPD_ERROR_INVALID_FILE, + "unsupported manifest version"); + return FALSE; + } + + json_files = json_object_get_array_member(json_obj, "files"); + if (json_files == NULL) { + g_set_error_literal(error, + FWUPD_ERROR, + FWUPD_ERROR_INVALID_FILE, + "manifest invalid as has no 'files' array"); + return FALSE; + } + + files_cnt = json_array_get_length(json_files); + if (files_cnt == 0) { + g_set_error_literal(error, + FWUPD_ERROR, + FWUPD_ERROR_INVALID_FILE, + "manifest invalid as contains no update images"); + return FALSE; + } + + for (guint i = 0; i < files_cnt; i++) { + const gchar *filename = NULL; + const gchar *bootloader_name = NULL; + guint image_addr = 0; + JsonObject *obj = json_array_get_object_element(json_files, i); + GBytes *blob = NULL; + FuFirmware *image = NULL; + g_autofree gchar *image_id = NULL; + g_auto(GStrv) board_split = NULL; + + if (!json_object_has_member(obj, "file")) { + g_set_error_literal(error, + FWUPD_ERROR, + FWUPD_ERROR_INVALID_FILE, + "manifest invalid as has no file name for the image"); + return FALSE; + } + filename = json_object_get_string_member(obj, "file"); + blob = fu_archive_lookup_by_fn(archive, filename, error); + if (blob == NULL) + return FALSE; + + if (json_object_has_member(obj, "version_B0")) { + bootloader_name = "B0"; + image = g_object_new(FU_TYPE_NORDIC_HID_FIRMWARE_B0, NULL); + } else if (json_object_has_member(obj, "version_MCUBOOT")) { + /* TODO: add MCUboot format */ + bootloader_name = "MCUBOOT"; + g_set_error_literal(error, + FWUPD_ERROR, + FWUPD_ERROR_INVALID_FILE, + "MCUboot bootloader is not supported"); + return FALSE; + } else { + g_set_error_literal(error, + FWUPD_ERROR, + FWUPD_ERROR_INVALID_FILE, + "only B0 and MCUboot bootloaders are supported"); + return FALSE; + } + + /* the "board" field contains board name before "_" symbol */ + if (!json_object_has_member(obj, "board")) { + g_set_error_literal(error, + FWUPD_ERROR, + FWUPD_ERROR_INVALID_FILE, + "manifest invalid as has no target board information"); + return FALSE; + } + board_split = g_strsplit(json_object_get_string_member(obj, "board"), "_", -1); + if (board_split[0] == NULL) { + g_set_error_literal(error, + FWUPD_ERROR, + FWUPD_ERROR_INVALID_FILE, + "manifest invalid as has no target board information"); + return FALSE; + } + /* images are listed in strict order: this is guaranteed by producer + * set the id format as __N, i.e "nrf52840dk_B0_bank0" */ + image_id = g_strdup_printf("%s_%s_bank%01u", board_split[0], bootloader_name, i); + if (!fu_firmware_parse(image, blob, flags, error)) + return FALSE; + + fu_firmware_set_id(image, image_id); + fu_firmware_set_idx(image, i); + if (json_object_has_member(obj, "load_address")) { + image_addr = json_object_get_int_member(obj, "load_address"); + fu_firmware_set_addr(image, image_addr); + } + fu_firmware_add_image(firmware, image); + } + + /* success */ + return TRUE; +} + +static void +fu_nordic_hid_archive_init(FuNordicHidArchive *self) +{ +} + +static void +fu_nordic_hid_archive_class_init(FuNordicHidArchiveClass *klass) +{ + FuFirmwareClass *klass_firmware = FU_FIRMWARE_CLASS(klass); + klass_firmware->parse = fu_nordic_hid_archive_parse; +} diff --git a/plugins/nordic-hid/fu-nordic-hid-archive.h b/plugins/nordic-hid/fu-nordic-hid-archive.h new file mode 100644 index 000000000..8c707e13e --- /dev/null +++ b/plugins/nordic-hid/fu-nordic-hid-archive.h @@ -0,0 +1,16 @@ +/* + * Copyright (C) 2021 Richard Hughes + * + * SPDX-License-Identifier: LGPL-2.1+ + */ + +#pragma once + +#include + +#define FU_TYPE_NORDIC_HID_ARCHIVE (fu_nordic_hid_archive_get_type()) +G_DECLARE_FINAL_TYPE(FuNordicHidArchive, + fu_nordic_hid_archive, + FU, + NORDIC_HID_ARCHIVE, + FuArchiveFirmware) diff --git a/plugins/nordic-hid/fu-nordic-hid-cfg-channel.c b/plugins/nordic-hid/fu-nordic-hid-cfg-channel.c new file mode 100644 index 000000000..4f18522e0 --- /dev/null +++ b/plugins/nordic-hid/fu-nordic-hid-cfg-channel.c @@ -0,0 +1,1078 @@ +/* + * Copyright (C) 2021 Ricardo Cañuelo + * Copyright (C) 2021 Denis Pynkin + * + * SPDX-License-Identifier: LGPL-2.1+ + */ + +#include "config.h" + +#ifdef HAVE_HIDRAW_H +#include +#include +#endif +#include + +#include "fu-nordic-hid-archive.h" +#include "fu-nordic-hid-cfg-channel.h" +#include "fu-nordic-hid-firmware-b0.h" + +#define HID_REPORT_ID 6 +#define REPORT_SIZE 30 +#define REPORT_DATA_MAX_LEN (REPORT_SIZE - 5) +#define HWID_LEN 8 +#define END_OF_TRANSFER_CHAR 0x0a +#define INVALID_PEER_ID 0xFF + +#define FU_NORDIC_HID_CFG_CHANNEL_RETRIES 10 +#define FU_NORDIC_HID_CFG_CHANNEL_RETRY_DELAY 100 /* ms */ +#define FU_NORDIC_HID_CFG_CHANNEL_DFU_RETRY_DELAY 500 /* ms */ + +typedef enum { + CONFIG_STATUS_PENDING, + CONFIG_STATUS_GET_MAX_MOD_ID, + CONFIG_STATUS_GET_HWID, + CONFIG_STATUS_GET_BOARD_NAME, + CONFIG_STATUS_INDEX_PEERS, + CONFIG_STATUS_GET_PEER, + CONFIG_STATUS_SET, + CONFIG_STATUS_FETCH, + CONFIG_STATUS_SUCCESS, + CONFIG_STATUS_TIMEOUT, + CONFIG_STATUS_REJECT, + CONFIG_STATUS_WRITE_FAIL, + CONFIG_STATUS_DISCONNECTED, + CONFIG_STATUS_FAULT = 99, +} FuNordicCfgStatus; + +typedef enum { + DFU_STATE_INACTIVE, + DFU_STATE_ACTIVE, + DFU_STATE_STORING, + DFU_STATE_CLEANING, +} FuNordicCfgSyncState; + +typedef struct __attribute__((packed)) { + guint8 report_id; + guint8 recipient; + guint8 event_id; + guint8 status; + guint8 data_len; + guint8 data[REPORT_DATA_MAX_LEN]; +} FuNordicCfgChannelMsg; + +typedef struct { + guint8 idx; + gchar *name; +} FuNordicCfgChannelModuleOption; + +typedef struct { + guint8 idx; + gchar *name; + GPtrArray *options; /* of FuNordicCfgChannelModuleOption */ +} FuNordicCfgChannelModule; + +typedef struct { + guint8 status; + guint8 *buf; + gsize bufsz; +} FuNordicCfgChannelRcvHelper; + +typedef struct { + guint8 dfu_state; + guint32 img_length; + guint32 img_csum; + guint32 offset; + guint16 sync_buffer_size; +} FuNordicCfgChannelDfuInfo; + +G_DEFINE_AUTOPTR_CLEANUP_FUNC(FuNordicCfgChannelMsg, g_free); +G_DEFINE_AUTOPTR_CLEANUP_FUNC(FuNordicCfgChannelDfuInfo, g_free); + +struct _FuNordicDeviceCfgChannel { + FuUdevDevice parent_instance; + gchar *board_name; + gchar *bl_name; + guint8 flash_area_id; + guint32 flashed_image_len; + guint8 peer_id; + GPtrArray *modules; /* of FuNordicCfgChannelModule */ +}; + +G_DEFINE_TYPE(FuNordicDeviceCfgChannel, fu_nordic_hid_cfg_channel, FU_TYPE_UDEV_DEVICE) + +static void +fu_nordic_hid_cfg_channel_module_option_free(FuNordicCfgChannelModuleOption *opt) +{ + g_free(opt->name); + g_free(opt); +} + +static void +fu_nordic_hid_cfg_channel_module_free(FuNordicCfgChannelModule *mod) +{ + if (mod->options != NULL) + g_ptr_array_unref(mod->options); + g_free(mod->name); +} + +static gboolean +fu_nordic_hid_cfg_channel_send(FuNordicDeviceCfgChannel *self, + guint8 *buf, + gsize bufsz, + GError **error) +{ +#ifdef HAVE_HIDRAW_H + if (g_getenv("FWUPD_NORDIC_HID_VERBOSE") != NULL) + fu_common_dump_raw(G_LOG_DOMAIN, "Sent", buf, bufsz); + if (!fu_udev_device_ioctl(FU_UDEV_DEVICE(self), HIDIOCSFEATURE(bufsz), buf, NULL, error)) + return FALSE; + return TRUE; +#else + g_set_error_literal(error, + G_IO_ERROR, + G_IO_ERROR_NOT_SUPPORTED, + " not available"); + return FALSE; +#endif +} + +static gboolean +fu_nordic_hid_cfg_channel_receive(FuNordicDeviceCfgChannel *self, + guint8 *buf, + gsize bufsz, + GError **error) +{ +#ifdef HAVE_HIDRAW_H + if (!fu_udev_device_ioctl(FU_UDEV_DEVICE(self), HIDIOCGFEATURE(bufsz), buf, NULL, error)) + return FALSE; + if (g_getenv("FWUPD_NORDIC_HID_VERBOSE") != NULL) + fu_common_dump_raw(G_LOG_DOMAIN, "Received", buf, bufsz); + /* + * [TODO]: Possibly add the report-id fix for Bluez versions < 5.56: + * https://github.com/bluez/bluez/commit/35a2c50437cca4d26ac6537ce3a964bb509c9b62 + * + * See fu_pxi_ble_device_get_feature() in + * plugins/pixart-rf/fu-pxi-ble-device.c for an example. + */ + return TRUE; +#else + g_set_error_literal(error, + FWUPD_ERROR, + FWUPD_ERROR_NOT_SUPPORTED, + " not available"); + return FALSE; +#endif +} + +static gboolean +fu_nordic_hid_cfg_channel_receive_cb(FuDevice *device, gpointer user_data, GError **error) +{ + FuNordicCfgChannelRcvHelper *args = (FuNordicCfgChannelRcvHelper *)user_data; + FuNordicDeviceCfgChannel *self = FU_NORDIC_HID_CFG_CHANNEL(device); + FuNordicCfgChannelMsg *recv_msg = NULL; + + if (!fu_nordic_hid_cfg_channel_receive(self, args->buf, args->bufsz, error)) + return FALSE; + recv_msg = (FuNordicCfgChannelMsg *)args->buf; + if (recv_msg->status != args->status) { + g_set_error(error, + FWUPD_ERROR, + FWUPD_ERROR_READ, + "received status: 0x%02x, expected: 0x%02x", + recv_msg->status, + args->status); + return FALSE; + } + + /* success */ + return TRUE; +} + +/* + * fu_nordic_hid_cfg_channel_get_event_id: + * @module_name: module name, NULL for generic operations + * @option_name: option name, NULL for generic module operations + * + * Construct Event ID from module and option names. + * + * Returns: %TRUE if module/option pair found + */ +static gboolean +fu_nordic_hid_cfg_channel_get_event_id(FuNordicDeviceCfgChannel *self, + const gchar *module_name, + const gchar *option_name, + guint8 *event_id) +{ + FuNordicCfgChannelModule *mod = NULL; + guint id = 0; + + *event_id = 0; + + /* for generic operations */ + if (module_name == NULL) + return TRUE; + + for (id = 0; id < self->modules->len; id++) { + mod = g_ptr_array_index(self->modules, id); + if (g_strcmp0(module_name, mod->name) == 0) + break; + } + if (mod == NULL || id > 0x0f) + return FALSE; + + *event_id = id << 4; + + /* for generic module operations */ + if (option_name == NULL) + return TRUE; + + for (guint i = 0; i < mod->options->len && i <= 0x0f; i++) { + FuNordicCfgChannelModuleOption *opt = g_ptr_array_index(mod->options, i); + if (g_strcmp0(option_name, opt->name) == 0) { + *event_id = (id << 4) + opt->idx; + return TRUE; + } + } + + /* module have no requested option */ + return FALSE; +} + +static gboolean +fu_nordic_hid_cfg_channel_cmd_send_by_id(FuNordicDeviceCfgChannel *self, + guint8 recipient, + guint8 event_id, + guint8 status, + guint8 *data, + guint8 data_len, + GError **error) +{ + g_autoptr(FuNordicCfgChannelMsg) msg = g_new0(FuNordicCfgChannelMsg, 1); + + msg->report_id = HID_REPORT_ID; + msg->recipient = recipient; + msg->event_id = event_id; + msg->status = status; + msg->data_len = 0; + + if (data != NULL) { + if (data_len > REPORT_DATA_MAX_LEN) { + g_set_error(error, + G_IO_ERROR, + G_IO_ERROR_NOT_SUPPORTED, + "requested to send %d bytes, while maximum is %d", + data_len, + REPORT_DATA_MAX_LEN); + return FALSE; + } + if (!fu_memcpy_safe(msg->data, + REPORT_DATA_MAX_LEN, + 0, + data, + data_len, + 0, + data_len, + error)) + return FALSE; + msg->data_len = data_len; + } + + if (!fu_nordic_hid_cfg_channel_send(self, (guint8 *)msg, sizeof(*msg), error)) { + g_prefix_error(error, "failed to send: "); + return FALSE; + } + + /* success */ + return TRUE; +} + +static gboolean +fu_nordic_hid_cfg_channel_cmd_send(FuNordicDeviceCfgChannel *self, + guint8 recipient, + const gchar *module_name, + const gchar *option_name, + guint8 status, + guint8 *data, + guint8 data_len, + GError **error) +{ + guint8 event_id = 0; + + if (!fu_nordic_hid_cfg_channel_get_event_id(self, module_name, option_name, &event_id)) { + g_set_error(error, + FWUPD_ERROR, + FWUPD_ERROR_NOT_SUPPORTED, + "requested non-existing module %s with option %s", + module_name, + option_name); + return FALSE; + } + + if (!fu_nordic_hid_cfg_channel_cmd_send_by_id(self, + recipient, + event_id, + status, + data, + data_len, + error)) { + g_prefix_error(error, "failed to send: "); + return FALSE; + } + + /* success */ + return TRUE; +} + +static gboolean +fu_nordic_hid_cfg_channel_cmd_receive(FuNordicDeviceCfgChannel *self, + guint8 status, + FuNordicCfgChannelMsg *res, + GError **error) +{ + FuNordicCfgChannelRcvHelper helper; + + res->report_id = HID_REPORT_ID; + helper.status = status; + helper.buf = (guint8 *)res; + helper.bufsz = sizeof(*res); + if (!fu_device_retry(FU_DEVICE(self), + fu_nordic_hid_cfg_channel_receive_cb, + FU_NORDIC_HID_CFG_CHANNEL_RETRIES, + &helper, + error)) { + g_prefix_error(error, "Failed on receive: "); + return FALSE; + } + + /* success */ + return TRUE; +} + +static gboolean +fu_nordic_hid_cfg_channel_index_peers(FuNordicDeviceCfgChannel *self, GError **error) +{ + guint cnt = 0; + g_autoptr(FuNordicCfgChannelMsg) res = g_new0(FuNordicCfgChannelMsg, 1); + g_autoptr(GError) error_local = NULL; + + if (self->peer_id != 0) + return TRUE; + + if (!fu_nordic_hid_cfg_channel_cmd_send(self, + self->peer_id, + NULL, + NULL, + CONFIG_STATUS_INDEX_PEERS, + NULL, + 0, + error)) + return FALSE; + if (fu_nordic_hid_cfg_channel_cmd_receive(self, + CONFIG_STATUS_DISCONNECTED, + res, + &error_local)) { + /* no peers */ + return TRUE; + } + + /* Peers available */ + if (!fu_nordic_hid_cfg_channel_cmd_receive(self, CONFIG_STATUS_SUCCESS, res, error)) + return FALSE; + + while (cnt++ <= 0xFF) { + if (!fu_nordic_hid_cfg_channel_cmd_send(self, + self->peer_id, + NULL, + NULL, + CONFIG_STATUS_GET_PEER, + NULL, + 0, + error)) + return FALSE; + if (!fu_nordic_hid_cfg_channel_cmd_receive(self, CONFIG_STATUS_SUCCESS, res, error)) + return FALSE; + + /* end of the list */ + if (res->data[8] == INVALID_PEER_ID) + return TRUE; + + g_debug("detected peer: 0x%02x", res->data[8]); + /* TODO: add the new child device here */ + } + + g_set_error_literal(error, G_IO_ERROR, G_IO_ERROR_BROKEN_PIPE, "too many peers detected"); + return FALSE; +} + +static gboolean +fu_nordic_hid_cfg_channel_get_board_name(FuNordicDeviceCfgChannel *self, GError **error) +{ + g_autoptr(FuNordicCfgChannelMsg) res = g_new0(FuNordicCfgChannelMsg, 1); + + if (!fu_nordic_hid_cfg_channel_cmd_send(self, + self->peer_id, + NULL, + NULL, + CONFIG_STATUS_GET_BOARD_NAME, + NULL, + 0, + error)) + return FALSE; + if (!fu_nordic_hid_cfg_channel_cmd_receive(self, CONFIG_STATUS_SUCCESS, res, error)) + return FALSE; + self->board_name = fu_common_strsafe((const gchar *)res->data, res->data_len); + + /* success */ + return TRUE; +} + +static gboolean +fu_nordic_hid_cfg_channel_get_bl_name(FuNordicDeviceCfgChannel *self, GError **error) +{ + /* TODO: read the bootloader name from the device + * currently only the B0 bootloader is supported */ + self->bl_name = g_strdup("B0"); + /* success */ + return TRUE; +} + +/* + * NOTE: + * For devices connected directly to the host, + * hw_id = HID_UNIQ = logical_id. + */ +static gboolean +fu_nordic_hid_cfg_channel_get_hwid(FuNordicDeviceCfgChannel *self, GError **error) +{ + guint8 hw_id[HWID_LEN] = {0x0}; + g_autofree gchar *physical_id = NULL; + g_autoptr(FuNordicCfgChannelMsg) res = g_new0(FuNordicCfgChannelMsg, 1); + + if (!fu_nordic_hid_cfg_channel_cmd_send(self, + self->peer_id, + NULL, + NULL, + CONFIG_STATUS_GET_HWID, + NULL, + 0, + error)) + return FALSE; + if (!fu_nordic_hid_cfg_channel_cmd_receive(self, CONFIG_STATUS_SUCCESS, res, error)) + return FALSE; + + if (!fu_memcpy_safe(hw_id, HWID_LEN, 0, res->data, REPORT_DATA_MAX_LEN, 0, HWID_LEN, error)) + return FALSE; + + /* allows to detect the single device connected via several interfaces */ + physical_id = g_strdup_printf("%s-%02x%02x%02x%02x%02x%02x%02x%02x-%01d", + self->board_name, + hw_id[0], + hw_id[1], + hw_id[2], + hw_id[3], + hw_id[4], + hw_id[5], + hw_id[6], + hw_id[7], + self->flash_area_id); + fu_device_set_physical_id(FU_DEVICE(self), physical_id); + + /* success */ + return TRUE; +} + +static gboolean +fu_nordic_hid_cfg_channel_load_module_opts(FuNordicDeviceCfgChannel *self, + FuNordicCfgChannelModule *mod, + GError **error) +{ + for (guint8 i = 0; i < 0xFF; i++) { + FuNordicCfgChannelModuleOption *opt = NULL; + g_autoptr(FuNordicCfgChannelMsg) res = g_new0(FuNordicCfgChannelMsg, 1); + + if (!fu_nordic_hid_cfg_channel_cmd_send_by_id(self, + 0, + mod->idx << 4, + CONFIG_STATUS_FETCH, + NULL, + 0, + error)) + return FALSE; + if (!fu_nordic_hid_cfg_channel_cmd_receive(self, CONFIG_STATUS_SUCCESS, res, error)) + return FALSE; + + /* res->data: option name */ + if (res->data[0] == END_OF_TRANSFER_CHAR) + break; + opt = g_new0(FuNordicCfgChannelModuleOption, 1); + opt->name = fu_common_strsafe((const gchar *)res->data, res->data_len); + opt->idx = i; + g_ptr_array_add(mod->options, opt); + } + + /* success */ + return TRUE; +} + +static gboolean +fu_nordic_hid_cfg_channel_load_module_info(FuNordicDeviceCfgChannel *self, + guint8 module_idx, + GError **error) +{ + FuNordicCfgChannelModule *mod = g_new0(FuNordicCfgChannelModule, 1); + + mod->idx = module_idx; + mod->options = g_ptr_array_new_with_free_func( + (GDestroyNotify)fu_nordic_hid_cfg_channel_module_option_free); + if (!fu_nordic_hid_cfg_channel_load_module_opts(self, mod, error)) + return FALSE; + /* module description is the 1st loaded option */ + if (mod->options->len > 0) { + FuNordicCfgChannelModuleOption *opt = g_ptr_array_index(mod->options, 0); + mod->name = g_strdup(opt->name); + if (!g_ptr_array_remove_index(mod->options, 0)) { + g_set_error_literal(error, + FWUPD_ERROR, + FWUPD_ERROR_INTERNAL, + "cannot remove option"); + return FALSE; + } + } + + /* success */ + g_ptr_array_add(self->modules, mod); + return TRUE; +} + +static gboolean +fu_nordic_hid_cfg_channel_get_modinfo(FuNordicDeviceCfgChannel *self, GError **error) +{ + g_autoptr(FuNordicCfgChannelMsg) res = g_new0(FuNordicCfgChannelMsg, 1); + + if (!fu_nordic_hid_cfg_channel_cmd_send(self, + self->peer_id, + NULL, + NULL, + CONFIG_STATUS_GET_MAX_MOD_ID, + NULL, + 0, + error)) + return FALSE; + if (!fu_nordic_hid_cfg_channel_cmd_receive(self, CONFIG_STATUS_SUCCESS, res, error)) + return FALSE; + + /* res->data[0]: maximum module idx */ + for (guint i = 0; i <= res->data[0]; i++) { + if (!fu_nordic_hid_cfg_channel_load_module_info(self, i, error)) + return FALSE; + } + + /* success */ + return TRUE; +} + +static gboolean +fu_nordic_hid_cfg_channel_dfu_fwinfo(FuNordicDeviceCfgChannel *self, GError **error) +{ + guint16 ver_rev; + guint32 ver_build_nr; + g_autofree gchar *version = NULL; + g_autoptr(FuNordicCfgChannelMsg) res = g_new0(FuNordicCfgChannelMsg, 1); + + if (!fu_nordic_hid_cfg_channel_cmd_send(self, + self->peer_id, + "dfu", + "fwinfo", + CONFIG_STATUS_FETCH, + NULL, + 0, + error)) + return FALSE; + if (!fu_nordic_hid_cfg_channel_cmd_receive(self, CONFIG_STATUS_SUCCESS, res, error)) + return FALSE; + + /* parsing fwinfo answer */ + /* TODO: add banks amount into quirk */ + if (res->data[0] > 1) { + g_set_error_literal(error, + FWUPD_ERROR, + FWUPD_ERROR_NOT_SUPPORTED, + "invalid flash area returned by device"); + return FALSE; + } + /* set the target flash ID area */ + self->flash_area_id = res->data[0] ^ 1; + if (!fu_common_read_uint32_safe(res->data, + REPORT_SIZE, + 0x01, + &self->flashed_image_len, + G_LITTLE_ENDIAN, + error)) + return FALSE; + if (!fu_common_read_uint16_safe(res->data, + REPORT_SIZE, + 0x07, + &ver_rev, + G_LITTLE_ENDIAN, + error)) + return FALSE; + if (!fu_common_read_uint32_safe(res->data, + REPORT_SIZE, + 0x09, + &ver_build_nr, + G_LITTLE_ENDIAN, + error)) + return FALSE; + version = g_strdup_printf("%u.%u.%u.%u", res->data[4], res->data[5], ver_rev, ver_build_nr); + fu_device_set_version(FU_DEVICE(self), version); + + /* success */ + return TRUE; +} + +static gboolean +fu_nordic_hid_cfg_channel_dfu_reboot(FuNordicDeviceCfgChannel *self, GError **error) +{ + g_autoptr(FuNordicCfgChannelMsg) res = g_new0(FuNordicCfgChannelMsg, 1); + + if (!fu_nordic_hid_cfg_channel_cmd_send(self, + self->peer_id, + "dfu", + "reboot", + CONFIG_STATUS_FETCH, + NULL, + 0, + error)) + return FALSE; + if (!fu_nordic_hid_cfg_channel_cmd_receive(self, CONFIG_STATUS_SUCCESS, res, error)) + return FALSE; + if (res->data_len != 1 || res->data[0] != 0x01) { + g_set_error_literal(error, + G_IO_ERROR, + G_IO_ERROR_INVALID_DATA, + "reboot data was invalid"); + return FALSE; + } + + /* success */ + return TRUE; +} + +static gboolean +fu_nordic_hid_cfg_channel_dfu_sync_cb(FuDevice *device, gpointer user_data, GError **error) +{ + FuNordicDeviceCfgChannel *self = FU_NORDIC_HID_CFG_CHANNEL(device); + FuNordicCfgChannelRcvHelper *args = (FuNordicCfgChannelRcvHelper *)user_data; + FuNordicCfgChannelMsg *recv_msg = (FuNordicCfgChannelMsg *)args->buf; + + if (!fu_nordic_hid_cfg_channel_cmd_send(self, + self->peer_id, + "dfu", + "sync", + CONFIG_STATUS_FETCH, + NULL, + 0, + error)) + return FALSE; + if (!fu_nordic_hid_cfg_channel_cmd_receive(self, CONFIG_STATUS_SUCCESS, recv_msg, error)) + return FALSE; + + if (recv_msg->data_len != 0x0F) { + g_set_error_literal(error, + G_IO_ERROR, + G_IO_ERROR_NOT_SUPPORTED, + "incorrect length of reply"); + return FALSE; + } + if (recv_msg->data[0] != args->status) { + g_set_error(error, + FWUPD_ERROR, + FWUPD_ERROR_READ, + "received status: 0x%02x, expected: 0x%02x", + recv_msg->status, + args->status); + return FALSE; + } + + /* success */ + return TRUE; +} + +static gboolean +fu_nordic_hid_cfg_channel_dfu_sync(FuNordicDeviceCfgChannel *self, + FuNordicCfgChannelDfuInfo *dfu_info, + guint8 expecting_state, + GError **error) +{ + FuNordicCfgChannelRcvHelper helper; + g_autoptr(FuNordicCfgChannelMsg) res = g_new0(FuNordicCfgChannelMsg, 1); + + helper.status = expecting_state; + helper.buf = (guint8 *)res; + helper.bufsz = sizeof(*res); + + if (!fu_device_retry_full(FU_DEVICE(self), + fu_nordic_hid_cfg_channel_dfu_sync_cb, + FU_NORDIC_HID_CFG_CHANNEL_RETRIES, + FU_NORDIC_HID_CFG_CHANNEL_DFU_RETRY_DELAY, + &helper, + error)) { + g_prefix_error(error, "failed on dfu sync: "); + return FALSE; + } + dfu_info->dfu_state = res->data[0]; + if (!fu_common_read_uint32_safe(res->data, + REPORT_SIZE, + 0x01, + &dfu_info->img_length, + G_LITTLE_ENDIAN, + error)) + return FALSE; + if (!fu_common_read_uint32_safe(res->data, + REPORT_SIZE, + 0x05, + &dfu_info->img_csum, + G_LITTLE_ENDIAN, + error)) + return FALSE; + if (!fu_common_read_uint32_safe(res->data, + REPORT_SIZE, + 0x09, + &dfu_info->offset, + G_LITTLE_ENDIAN, + error)) + return FALSE; + if (!fu_common_read_uint16_safe(res->data, + REPORT_SIZE, + 0x0D, + &dfu_info->sync_buffer_size, + G_LITTLE_ENDIAN, + error)) + return FALSE; + + return TRUE; +} + +static gboolean +fu_nordic_hid_cfg_channel_dfu_start(FuNordicDeviceCfgChannel *self, + gsize img_length, + guint32 img_crc, + guint32 offset, + GError **error) +{ + guint8 data[REPORT_DATA_MAX_LEN] = {0}; + g_autoptr(FuNordicCfgChannelMsg) res = g_new0(FuNordicCfgChannelMsg, 1); + + /* sanity check */ + if (img_length > G_MAXUINT32) { + g_set_error_literal(error, + G_IO_ERROR, + G_IO_ERROR_INVALID_DATA, + "payload was too large"); + return FALSE; + } + + if (!fu_common_write_uint32_safe(data, + REPORT_DATA_MAX_LEN, + 0x00, + (guint32)img_length, + G_LITTLE_ENDIAN, + error)) + return FALSE; + if (!fu_common_write_uint32_safe(data, + REPORT_DATA_MAX_LEN, + 0x04, + img_crc, + G_LITTLE_ENDIAN, + error)) + return FALSE; + if (!fu_common_write_uint32_safe(data, + REPORT_DATA_MAX_LEN, + 0x08, + offset, + G_LITTLE_ENDIAN, + error)) + return FALSE; + + if (!fu_nordic_hid_cfg_channel_cmd_send(self, + self->peer_id, + "dfu", + "start", + CONFIG_STATUS_SET, + data, + 0x0C, + error)) + return FALSE; + return fu_nordic_hid_cfg_channel_cmd_receive(self, CONFIG_STATUS_SUCCESS, res, error); +} + +static gboolean +fu_nordic_hid_cfg_channel_probe(FuDevice *device, GError **error) +{ + /* FuUdevDevice->probe */ + if (!FU_DEVICE_CLASS(fu_nordic_hid_cfg_channel_parent_class)->probe(device, error)) + return FALSE; + + return fu_udev_device_set_physical_id(FU_UDEV_DEVICE(device), "hid", error); +} + +static gboolean +fu_nordic_hid_cfg_channel_setup(FuDevice *device, GError **error) +{ + FuNordicDeviceCfgChannel *self = FU_NORDIC_HID_CFG_CHANNEL(device); + g_autofree gchar *target_id = NULL; + + if (!fu_nordic_hid_cfg_channel_index_peers(self, error)) + return FALSE; + /* get device info */ + if (!fu_nordic_hid_cfg_channel_get_board_name(self, error)) + return FALSE; + if (!fu_nordic_hid_cfg_channel_get_bl_name(self, error)) + return FALSE; + if (!fu_nordic_hid_cfg_channel_get_hwid(self, error)) + return FALSE; + if (!fu_nordic_hid_cfg_channel_get_modinfo(self, error)) + return FALSE; + if (!fu_nordic_hid_cfg_channel_dfu_fwinfo(self, error)) + return FALSE; + + /* additional GUID based on VID/PID and target area to flash + * needed to distinguish images aimed to different banks*/ + target_id = g_strdup_printf("HIDRAW\\VEN_%04X&DEV_%04X&BOARD_%s&BL_%s", + fu_udev_device_get_vendor(FU_UDEV_DEVICE(device)), + fu_udev_device_get_model(FU_UDEV_DEVICE(device)), + self->board_name, + self->bl_name); + fu_device_add_guid(device, target_id); + target_id = g_strdup_printf("HIDRAW\\VEN_%04X&DEV_%04X&BOARD_%s&BL_%s&BANK_%01X", + fu_udev_device_get_vendor(FU_UDEV_DEVICE(device)), + fu_udev_device_get_model(FU_UDEV_DEVICE(device)), + self->board_name, + self->bl_name, + self->flash_area_id); + fu_device_add_guid(device, target_id); + return TRUE; +} + +static void +fu_nordic_hid_cfg_channel_set_progress(FuDevice *self, FuProgress *progress) +{ + fu_progress_set_id(progress, G_STRLOC); + fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 1); /* detach */ + fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 97); /* write */ + fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 1); /* attach */ + fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_BUSY, 1); /* reload */ +} + +static void +fu_nordic_hid_cfg_channel_module_to_string(FuNordicCfgChannelModule *mod, guint idt, GString *str) +{ + for (guint i = 0; i < mod->options->len; i++) { + FuNordicCfgChannelModuleOption *opt = g_ptr_array_index(mod->options, i); + g_autofree gchar *title = g_strdup_printf("Option%02x", i); + fu_common_string_append_kv(str, idt, title, opt->name); + } +} + +static void +fu_nordic_hid_cfg_channel_to_string(FuDevice *device, guint idt, GString *str) +{ + FuNordicDeviceCfgChannel *self = FU_NORDIC_HID_CFG_CHANNEL(device); + fu_common_string_append_kv(str, idt, "BoardName", self->board_name); + fu_common_string_append_kv(str, idt, "Bootloader", self->bl_name); + fu_common_string_append_kx(str, idt, "FlashAreaId", self->flash_area_id); + fu_common_string_append_kx(str, idt, "FlashedImageLen", self->flashed_image_len); + for (guint i = 0; i < self->modules->len; i++) { + FuNordicCfgChannelModule *mod = g_ptr_array_index(self->modules, i); + g_autofree gchar *title = g_strdup_printf("Module%02x", i); + fu_common_string_append_kv(str, idt, title, mod->name); + fu_nordic_hid_cfg_channel_module_to_string(mod, idt + 1, str); + } +} + +static gboolean +fu_nordic_hid_cfg_channel_write_firmware_chunk(FuNordicDeviceCfgChannel *self, + FuChunk *chk, + gboolean is_last, + GError **error) +{ + guint32 chunk_len; + guint32 offset = 0; + guint8 sync_state = DFU_STATE_ACTIVE; + g_autoptr(FuNordicCfgChannelDfuInfo) dfu_info = g_new0(FuNordicCfgChannelDfuInfo, 1); + + chunk_len = fu_chunk_get_data_sz(chk); + while (offset < chunk_len) { + guint8 data_len; + guint8 data[REPORT_DATA_MAX_LEN] = {0}; + g_autoptr(FuNordicCfgChannelMsg) res = g_new0(FuNordicCfgChannelMsg, 1); + + data_len = ((offset + REPORT_DATA_MAX_LEN) < chunk_len) + ? REPORT_DATA_MAX_LEN + : (guint8)(chunk_len - offset); + + if (!fu_memcpy_safe(data, + REPORT_DATA_MAX_LEN, + 0, + fu_chunk_get_data(chk), + chunk_len, + offset, + data_len, + error)) { + return FALSE; + } + + if (!fu_nordic_hid_cfg_channel_cmd_send(self, + self->peer_id, + "dfu", + "data", + CONFIG_STATUS_SET, + data, + data_len, + error)) + return FALSE; + if (!fu_nordic_hid_cfg_channel_cmd_receive(self, CONFIG_STATUS_SUCCESS, res, error)) + return FALSE; + + offset += data_len; + } + + /* sync should return inactive for the last chunk */ + if (is_last) + sync_state = DFU_STATE_INACTIVE; + return fu_nordic_hid_cfg_channel_dfu_sync(self, dfu_info, sync_state, error); +} + +static gboolean +fu_nordic_hid_cfg_channel_write_firmware_blob(FuNordicDeviceCfgChannel *self, + GBytes *blob, + FuProgress *progress, + GError **error) +{ + g_autoptr(FuNordicCfgChannelDfuInfo) dfu_info = g_new0(FuNordicCfgChannelDfuInfo, 1); + g_autoptr(GPtrArray) chunks = NULL; + + if (!fu_nordic_hid_cfg_channel_dfu_sync(self, dfu_info, DFU_STATE_ACTIVE, error)) + return FALSE; + + chunks = fu_chunk_array_new_from_bytes(blob, 0, 0, dfu_info->sync_buffer_size); + fu_progress_set_id(progress, G_STRLOC); + fu_progress_set_steps(progress, chunks->len); + + for (guint i = 0; i < chunks->len; i++) { + FuChunk *chk = g_ptr_array_index(chunks, i); + gboolean is_last = (i == chunks->len - 1); + if (!fu_nordic_hid_cfg_channel_write_firmware_chunk(self, chk, is_last, error)) { + g_prefix_error(error, "chunk %u: ", fu_chunk_get_idx(chk)); + return FALSE; + } + fu_progress_step_done(progress); + } + + /* success */ + return TRUE; +} + +static gboolean +fu_nordic_hid_cfg_channel_write_firmware(FuDevice *device, + FuFirmware *firmware, + FuProgress *progress, + FwupdInstallFlags flags, + GError **error) +{ + FuNordicDeviceCfgChannel *self = FU_NORDIC_HID_CFG_CHANNEL(device); + guint32 checksum; + const gchar *csum_str = NULL; + g_autofree gchar *image_id = NULL; + g_autoptr(GBytes) blob = NULL; + g_autoptr(FuNordicCfgChannelDfuInfo) dfu_info = g_new0(FuNordicCfgChannelDfuInfo, 1); + + /* select correct firmware per target board, bootloader and bank */ + image_id = + g_strdup_printf("%s_%s_bank%01u", self->board_name, self->bl_name, self->flash_area_id); + firmware = fu_firmware_get_image_by_id(firmware, image_id, error); + if (firmware == NULL) + return FALSE; + + /* explicitly request a custom checksum calculation */ + csum_str = fu_firmware_get_checksum(firmware, -1, error); + if (csum_str == NULL) + return FALSE; + /* expecting checksum string in hex */ + checksum = g_ascii_strtoull(csum_str, NULL, 16); + + /* progress */ + fu_progress_set_id(progress, G_STRLOC); + fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_ERASE, 1); + fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 99); + fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_BUSY, 0); + + /* TODO: check if there is unfinished operation before? */ + blob = fu_firmware_get_bytes(firmware, error); + if (blob == NULL) + return FALSE; + if (!fu_nordic_hid_cfg_channel_dfu_sync(self, dfu_info, DFU_STATE_INACTIVE, error)) + return FALSE; + if (!fu_nordic_hid_cfg_channel_dfu_start(self, + g_bytes_get_size(blob), + checksum, + 0x0 /* offset */, + error)) + return FALSE; + fu_progress_step_done(progress); + + /* write */ + if (!fu_nordic_hid_cfg_channel_write_firmware_blob(self, + blob, + fu_progress_get_child(progress), + error)) + return FALSE; + fu_progress_step_done(progress); + + /* attach */ + if (!fu_nordic_hid_cfg_channel_dfu_reboot(self, error)) + return FALSE; + fu_progress_step_done(progress); + return TRUE; +} + +static void +fu_nordic_hid_cfg_channel_finalize(GObject *object) +{ + FuNordicDeviceCfgChannel *self = FU_NORDIC_HID_CFG_CHANNEL(object); + g_free(self->board_name); + g_free(self->bl_name); + g_ptr_array_unref(self->modules); + G_OBJECT_CLASS(fu_nordic_hid_cfg_channel_parent_class)->finalize(object); +} + +static void +fu_nordic_hid_cfg_channel_class_init(FuNordicDeviceCfgChannelClass *klass) +{ + FuDeviceClass *klass_device = FU_DEVICE_CLASS(klass); + GObjectClass *object_class = G_OBJECT_CLASS(klass); + + klass_device->probe = fu_nordic_hid_cfg_channel_probe; + klass_device->set_progress = fu_nordic_hid_cfg_channel_set_progress; + klass_device->setup = fu_nordic_hid_cfg_channel_setup; + klass_device->to_string = fu_nordic_hid_cfg_channel_to_string; + klass_device->write_firmware = fu_nordic_hid_cfg_channel_write_firmware; + object_class->finalize = fu_nordic_hid_cfg_channel_finalize; +} + +static void +fu_nordic_hid_cfg_channel_init(FuNordicDeviceCfgChannel *self) +{ + /* TODO: change for child devices */ + self->peer_id = 0; + self->modules = + g_ptr_array_new_with_free_func((GDestroyNotify)fu_nordic_hid_cfg_channel_module_free); + + fu_device_set_vendor(FU_DEVICE(self), "Nordic"); + fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_UPDATABLE); + fu_device_set_version_format(FU_DEVICE(self), FWUPD_VERSION_FORMAT_QUAD); + fu_device_add_protocol(FU_DEVICE(self), "com.nordic.hidcfgchannel"); + fu_device_retry_set_delay(FU_DEVICE(self), FU_NORDIC_HID_CFG_CHANNEL_RETRY_DELAY); + fu_device_set_firmware_gtype(FU_DEVICE(self), FU_TYPE_NORDIC_HID_ARCHIVE); +} diff --git a/plugins/nordic-hid/fu-nordic-hid-cfg-channel.h b/plugins/nordic-hid/fu-nordic-hid-cfg-channel.h new file mode 100644 index 000000000..a29487749 --- /dev/null +++ b/plugins/nordic-hid/fu-nordic-hid-cfg-channel.h @@ -0,0 +1,16 @@ +/* + * Copyright (C) 2021 Ricardo Cañuelo + * + * SPDX-License-Identifier: LGPL-2.1+ + */ + +#pragma once + +#include + +#define FU_TYPE_NORDIC_HID_CFG_CHANNEL (fu_nordic_hid_cfg_channel_get_type()) +G_DECLARE_FINAL_TYPE(FuNordicDeviceCfgChannel, + fu_nordic_hid_cfg_channel, + FU, + NORDIC_HID_CFG_CHANNEL, + FuUdevDevice) diff --git a/plugins/nordic-hid/fu-nordic-hid-firmware-b0.c b/plugins/nordic-hid/fu-nordic-hid-firmware-b0.c new file mode 100644 index 000000000..aac81444a --- /dev/null +++ b/plugins/nordic-hid/fu-nordic-hid-firmware-b0.c @@ -0,0 +1,178 @@ +/* + * Copyright (C) 2021 Denis Pynkin + * + * SPDX-License-Identifier: LGPL-2.1+ + */ + +#include "config.h" + +#include + +#include "fu-nordic-hid-firmware-b0.h" + +#define UPDATE_IMAGE_MAGIC_COMMON 0x281ee6de +#define UPDATE_IMAGE_MAGIC_FWINFO 0x8fcebb4c +#define UPDATE_IMAGE_MAGIC_NRF52 0x00003402 +#define UPDATE_IMAGE_MAGIC_NRF53 0x00003502 + +struct _FuNordicHidFirmwareB0 { + FuIhexFirmwareClass parent_instance; + guint32 crc32; +}; + +G_DEFINE_TYPE(FuNordicHidFirmwareB0, fu_nordic_hid_firmware_b0, FU_TYPE_FIRMWARE) + +static void +fu_nordic_hid_firmware_b0_export(FuFirmware *firmware, + FuFirmwareExportFlags flags, + XbBuilderNode *bn) +{ + FuNordicHidFirmwareB0 *self = FU_NORDIC_HID_FIRMWARE_B0(firmware); + fu_xmlb_builder_insert_kx(bn, "crc32", self->crc32); +} + +static GBytes * +fu_nordic_hid_firmware_b0_write(FuFirmware *firmware, GError **error) +{ + g_autoptr(GByteArray) buf = g_byte_array_new(); + g_autoptr(GBytes) blob = NULL; + fu_byte_array_append_uint32(buf, UPDATE_IMAGE_MAGIC_COMMON, G_LITTLE_ENDIAN); + fu_byte_array_append_uint32(buf, UPDATE_IMAGE_MAGIC_FWINFO, G_LITTLE_ENDIAN); + fu_byte_array_append_uint32(buf, UPDATE_IMAGE_MAGIC_NRF52, G_LITTLE_ENDIAN); + blob = fu_firmware_get_bytes(firmware, error); + if (blob == NULL) + return NULL; + fu_byte_array_append_bytes(buf, blob); + return g_byte_array_free_to_bytes(g_steal_pointer(&buf)); +} + +static gboolean +fu_nordic_hid_firmware_b0_read_fwinfo(guint8 const *buf, gsize bufsz, GError **error) +{ + guint32 magic_common; + guint32 magic_fwinfo; + guint32 magic_compat; + guint32 offset; + guint32 hdr_offset[5] = {0x0000, 0x0200, 0x400, 0x800, 0x1000}; + + /* find correct offset to fwinfo */ + for (guint32 i = 0; i < G_N_ELEMENTS(hdr_offset); i++) { + offset = hdr_offset[i]; + if (!fu_common_read_uint32_safe(buf, + bufsz, + offset, + &magic_common, + G_LITTLE_ENDIAN, + error)) + return FALSE; + if (!fu_common_read_uint32_safe(buf, + bufsz, + offset + 0x04, + &magic_fwinfo, + G_LITTLE_ENDIAN, + error)) + return FALSE; + if (!fu_common_read_uint32_safe(buf, + bufsz, + offset + 0x08, + &magic_compat, + G_LITTLE_ENDIAN, + error)) + return FALSE; + if (magic_common != UPDATE_IMAGE_MAGIC_COMMON || + magic_fwinfo != UPDATE_IMAGE_MAGIC_FWINFO) + continue; + switch (magic_compat) { + case UPDATE_IMAGE_MAGIC_NRF52: + case UPDATE_IMAGE_MAGIC_NRF53: + return TRUE; + default: + break; + } + } + + g_set_error_literal(error, + FWUPD_ERROR, + FWUPD_ERROR_INVALID_FILE, + "unable to validate the update binary"); + return FALSE; +} + +static guint32 +fu_nordic_hid_firmware_b0_crc32(const guint8 *buf, gsize bufsz) +{ + guint crc32 = 0x01; + /* maybe skipped "^" step in fu_common_crc32_full()? + * according https://github.com/madler/zlib/blob/master/crc32.c#L225 */ + crc32 ^= 0xFFFFFFFFUL; + return fu_common_crc32_full(buf, bufsz, crc32, 0xEDB88320); +} + +static gchar * +fu_nordic_hid_firmware_b0_get_checksum(FuFirmware *firmware, + GChecksumType csum_kind, + GError **error) +{ + FuNordicHidFirmwareB0 *self = FU_NORDIC_HID_FIRMWARE_B0(firmware); + if (!fu_firmware_has_flag(firmware, FU_FIRMWARE_FLAG_HAS_CHECKSUM)) { + g_set_error_literal(error, + G_IO_ERROR, + G_IO_ERROR_NOT_SUPPORTED, + "unable to calculate the checksum of the update binary"); + return NULL; + } + return g_strdup_printf("%x", self->crc32); +} + +static gboolean +fu_nordic_hid_firmware_b0_parse(FuFirmware *firmware, + GBytes *fw, + guint64 addr_start, + guint64 addr_end, + FwupdInstallFlags flags, + GError **error) +{ + FuNordicHidFirmwareB0 *self = FU_NORDIC_HID_FIRMWARE_B0(firmware); + const guint8 *buf; + gsize bufsz = 0; + + buf = g_bytes_get_data(fw, &bufsz); + if (buf == NULL) { + g_set_error_literal(error, + FWUPD_ERROR, + FWUPD_ERROR_INVALID_FILE, + "unable to get the image binary"); + return FALSE; + } + if (!fu_nordic_hid_firmware_b0_read_fwinfo(buf, bufsz, error)) + return FALSE; + self->crc32 = fu_nordic_hid_firmware_b0_crc32(buf, bufsz); + fu_firmware_add_flag(FU_FIRMWARE(self), FU_FIRMWARE_FLAG_HAS_CHECKSUM); + + /* do not strip the header */ + fu_firmware_set_bytes(firmware, fw); + + /* success */ + return TRUE; +} + +static void +fu_nordic_hid_firmware_b0_init(FuNordicHidFirmwareB0 *self) +{ +} + +static void +fu_nordic_hid_firmware_b0_class_init(FuNordicHidFirmwareB0Class *klass) +{ + FuFirmwareClass *klass_firmware = FU_FIRMWARE_CLASS(klass); + klass_firmware->get_checksum = fu_nordic_hid_firmware_b0_get_checksum; + klass_firmware->export = fu_nordic_hid_firmware_b0_export; + klass_firmware->parse = fu_nordic_hid_firmware_b0_parse; + klass_firmware->write = fu_nordic_hid_firmware_b0_write; +} + +FuFirmware * +fu_nordic_hid_firmware_b0_new(void) +{ + return FU_FIRMWARE(g_object_new(FU_TYPE_NORDIC_HID_FIRMWARE_B0, NULL)); +} diff --git a/plugins/nordic-hid/fu-nordic-hid-firmware-b0.h b/plugins/nordic-hid/fu-nordic-hid-firmware-b0.h new file mode 100644 index 000000000..7c758a744 --- /dev/null +++ b/plugins/nordic-hid/fu-nordic-hid-firmware-b0.h @@ -0,0 +1,19 @@ +/* + * Copyright (C) 2021 Denis Pynkin + * + * SPDX-License-Identifier: LGPL-2.1+ + */ + +#pragma once + +#include + +#define FU_TYPE_NORDIC_HID_FIRMWARE_B0 (fu_nordic_hid_firmware_b0_get_type()) +G_DECLARE_FINAL_TYPE(FuNordicHidFirmwareB0, + fu_nordic_hid_firmware_b0, + FU, + NORDIC_HID_FIRMWARE_B0, + FuFirmware) + +FuFirmware * +fu_nordic_hid_firmware_b0_new(void); diff --git a/plugins/nordic-hid/fu-plugin-nordic-hid.c b/plugins/nordic-hid/fu-plugin-nordic-hid.c new file mode 100644 index 000000000..349beee38 --- /dev/null +++ b/plugins/nordic-hid/fu-plugin-nordic-hid.c @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2021 Ricardo Cañuelo + * + * SPDX-License-Identifier: LGPL-2.1+ + */ + +#include "config.h" + +#include + +#include "fu-nordic-hid-archive.h" +#include "fu-nordic-hid-cfg-channel.h" +#include "fu-nordic-hid-firmware-b0.h" + +static void +fu_plugin_nordic_hid_init(FuPlugin *plugin) +{ + fu_plugin_add_udev_subsystem(plugin, "hidraw"); + fu_plugin_add_device_gtype(plugin, FU_TYPE_NORDIC_HID_CFG_CHANNEL); + fu_plugin_add_firmware_gtype(plugin, NULL, FU_TYPE_NORDIC_HID_ARCHIVE); + fu_plugin_add_firmware_gtype(plugin, NULL, FU_TYPE_NORDIC_HID_FIRMWARE_B0); +} + +void +fu_plugin_init_vfuncs(FuPluginVfuncs *vfuncs) +{ + vfuncs->build_hash = FU_BUILD_HASH; + vfuncs->init = fu_plugin_nordic_hid_init; +} diff --git a/plugins/nordic-hid/meson.build b/plugins/nordic-hid/meson.build new file mode 100644 index 000000000..5eab4bf84 --- /dev/null +++ b/plugins/nordic-hid/meson.build @@ -0,0 +1,34 @@ +if get_option('gudev') and get_option('gusb') +cargs = ['-DG_LOG_DOMAIN="FuPluginNordicHid"'] + +install_data([ + 'nordic-hid.quirk', + ], + install_dir: join_paths(datadir, 'fwupd', 'quirks.d') +) + +shared_module('fu_plugin_nordic_hid', + fu_hash, + sources : [ + 'fu-plugin-nordic-hid.c', + 'fu-nordic-hid-cfg-channel.c', + 'fu-nordic-hid-firmware-b0.c', + 'fu-nordic-hid-archive.c', + ], + include_directories : [ + root_incdir, + fwupd_incdir, + fwupdplugin_incdir, + ], + install : true, + install_dir: plugin_dir, + link_with : [ + fwupd, + fwupdplugin, + ], + c_args : cargs, + dependencies : [ + plugin_deps, + ], +) +endif diff --git a/plugins/nordic-hid/nordic-hid.quirk b/plugins/nordic-hid/nordic-hid.quirk new file mode 100644 index 000000000..b5a33d336 --- /dev/null +++ b/plugins/nordic-hid/nordic-hid.quirk @@ -0,0 +1,9 @@ +# Mouse nRF52 Desktop +[HIDRAW\VEN_1915&DEV_52DE] +Plugin = nordic_hid +GType = FuNordicHidCfgChannel + +# Nordic Semiconductor ASA Dongle nRF52 Desktop +[HIDRAW\VEN_1915&DEV_52DC] +Plugin = nordic_hid +GType = FuNordicHidCfgChannel diff --git a/plugins/nordic-hid/tests/nordic-hid.bin b/plugins/nordic-hid/tests/nordic-hid.bin new file mode 100644 index 000000000..5318f5dd9 Binary files /dev/null and b/plugins/nordic-hid/tests/nordic-hid.bin differ diff --git a/plugins/nordic-hid/tests/nordic-hid.builder.xml b/plugins/nordic-hid/tests/nordic-hid.builder.xml new file mode 100644 index 000000000..c3bebe324 --- /dev/null +++ b/plugins/nordic-hid/tests/nordic-hid.builder.xml @@ -0,0 +1,3 @@ + + aGVsbG8gd29ybGQ= +