diff --git a/plugins/steelseries/README.md b/plugins/steelseries/README.md index 976a01abf..32ae12127 100644 --- a/plugins/steelseries/README.md +++ b/plugins/steelseries/README.md @@ -7,6 +7,7 @@ This plugin allows to update firmware on SteelSeries gamepads: * Stratus Duo * Stratus Duo USB wireless adapter * Stratus+ +* Aerox 3 Wireless * Rival 3 Wireless SteelSeries Rival 100 gaming mice support is limited by getting the correct @@ -15,6 +16,7 @@ available from the vendor. This plugin supports the following protocol IDs: +* com.steelseries.fizz * com.steelseries.gamepad * com.steelseries.sonic @@ -40,9 +42,15 @@ The device is not upgradable and thus requires no vendor ID set. ### Wireless Mice +### Rival 3 Wireless + The mouse switch button underneath must be set to 2.4G, and its 2.4G USB Wireless adapter must be connected to host. +### Aerox 3 Wireless + +The mouse must be connected to host via the USB-A to USB-C cable. + ## Vendor ID Security The vendor ID is set from the USB vendor, in this instance set to `USB:0x1038` diff --git a/plugins/steelseries/fu-plugin-steelseries.c b/plugins/steelseries/fu-plugin-steelseries.c index 833b263f7..b751a2f7a 100644 --- a/plugins/steelseries/fu-plugin-steelseries.c +++ b/plugins/steelseries/fu-plugin-steelseries.c @@ -9,6 +9,7 @@ #include #include "fu-steelseries-device.h" +#include "fu-steelseries-fizz.h" #include "fu-steelseries-gamepad.h" #include "fu-steelseries-sonic.h" @@ -16,6 +17,7 @@ static void fu_plugin_steelseries_init(FuPlugin *plugin) { fu_plugin_add_device_gtype(plugin, FU_TYPE_STEELSERIES_DEVICE); + fu_plugin_add_device_gtype(plugin, FU_TYPE_STEELSERIES_FIZZ); fu_plugin_add_device_gtype(plugin, FU_TYPE_STEELSERIES_GAMEPAD); fu_plugin_add_device_gtype(plugin, FU_TYPE_STEELSERIES_SONIC); } diff --git a/plugins/steelseries/fu-steelseries-common.c b/plugins/steelseries/fu-steelseries-common.c index bba2d4748..411031e8b 100644 --- a/plugins/steelseries/fu-steelseries-common.c +++ b/plugins/steelseries/fu-steelseries-common.c @@ -10,6 +10,10 @@ FuSteelseriesDeviceKind fu_steelseries_device_type_from_string(const gchar *name) { + if (g_strcmp0(name, "fizz") == 0) + return FU_STEELSERIES_DEVICE_FIZZ; + if (g_strcmp0(name, "fizz-dongle") == 0) + return FU_STEELSERIES_DEVICE_FIZZ_DONGLE; if (g_strcmp0(name, "gamepad") == 0) return FU_STEELSERIES_DEVICE_GAMEPAD; if (g_strcmp0(name, "gamepad-dongle") == 0) @@ -22,6 +26,10 @@ fu_steelseries_device_type_from_string(const gchar *name) const gchar * fu_steelseries_device_type_to_string(FuSteelseriesDeviceKind type) { + if (type == FU_STEELSERIES_DEVICE_FIZZ) + return "fizz"; + if (type == FU_STEELSERIES_DEVICE_FIZZ_DONGLE) + return "fizz-dongle"; if (type == FU_STEELSERIES_DEVICE_GAMEPAD) return "gamepad"; if (type == FU_STEELSERIES_DEVICE_GAMEPAD_DONGLE) diff --git a/plugins/steelseries/fu-steelseries-common.h b/plugins/steelseries/fu-steelseries-common.h index ba9f2e252..a631a66f7 100644 --- a/plugins/steelseries/fu-steelseries-common.h +++ b/plugins/steelseries/fu-steelseries-common.h @@ -14,6 +14,8 @@ typedef enum { FU_STEELSERIES_DEVICE_GAMEPAD, FU_STEELSERIES_DEVICE_GAMEPAD_DONGLE, FU_STEELSERIES_DEVICE_SONIC, + FU_STEELSERIES_DEVICE_FIZZ, + FU_STEELSERIES_DEVICE_FIZZ_DONGLE, } FuSteelseriesDeviceKind; FuSteelseriesDeviceKind diff --git a/plugins/steelseries/fu-steelseries-firmware.c b/plugins/steelseries/fu-steelseries-firmware.c new file mode 100644 index 000000000..d3605f0c0 --- /dev/null +++ b/plugins/steelseries/fu-steelseries-firmware.c @@ -0,0 +1,90 @@ +/* + * Copyright (C) 2022 Gaël PORTAY + * + * SPDX-License-Identifier: LGPL-2.1+ + */ + +#include "config.h" + +#include + +#include "fu-steelseries-common.h" +#include "fu-steelseries-firmware.h" + +struct _FuSteelseriesFirmware { + FuFirmwareClass parent_instance; + guint32 checksum; +}; + +G_DEFINE_TYPE(FuSteelseriesFirmware, fu_steelseries_firmware, FU_TYPE_FIRMWARE) + +static gboolean +fu_steelseries_firmware_parse(FuFirmware *firmware, + GBytes *fw, + guint64 addr_start, + guint64 addr_end, + FwupdInstallFlags flags, + GError **error) +{ + FuSteelseriesFirmware *self = FU_STEELSERIES_FIRMWARE(firmware); + guint32 checksum_tmp; + guint32 checksum; + + if (!fu_common_read_uint32_safe(g_bytes_get_data(fw, NULL), + g_bytes_get_size(fw), + g_bytes_get_size(fw) - sizeof(checksum), + &checksum, + G_LITTLE_ENDIAN, + error)) + return FALSE; + checksum_tmp = fu_common_crc32(g_bytes_get_data(fw, NULL), + g_bytes_get_size(fw) - sizeof(checksum_tmp)); + if (checksum_tmp != checksum) { + if ((flags & FWUPD_INSTALL_FLAG_IGNORE_CHECKSUM) == 0) { + g_set_error(error, + FWUPD_ERROR, + FWUPD_ERROR_INTERNAL, + "checksum mismatch, got 0x%08x, expected 0x%08x", + checksum_tmp, + checksum); + return FALSE; + } + g_debug("ignoring checksum mismatch, got 0x%08x, expected 0x%08x", + checksum_tmp, + checksum); + } + + self->checksum = checksum; + fu_firmware_add_flag(FU_FIRMWARE(self), FU_FIRMWARE_FLAG_HAS_CHECKSUM); + fu_firmware_set_bytes(firmware, fw); + + /* success */ + return TRUE; +} + +static void +fu_steelseries_firmware_export(FuFirmware *firmware, FuFirmwareExportFlags flags, XbBuilderNode *bn) +{ + FuSteelseriesFirmware *self = FU_STEELSERIES_FIRMWARE(firmware); + + fu_xmlb_builder_insert_kx(bn, "checksum", self->checksum); +} + +static void +fu_steelseries_firmware_init(FuSteelseriesFirmware *self) +{ +} + +static void +fu_steelseries_firmware_class_init(FuSteelseriesFirmwareClass *klass) +{ + FuFirmwareClass *klass_firmware = FU_FIRMWARE_CLASS(klass); + klass_firmware->parse = fu_steelseries_firmware_parse; + klass_firmware->export = fu_steelseries_firmware_export; +} + +FuFirmware * +fu_steelseries_firmware_new(void) +{ + return FU_FIRMWARE(g_object_new(FU_TYPE_STEELSERIES_FIRMWARE, NULL)); +} diff --git a/plugins/steelseries/fu-steelseries-firmware.h b/plugins/steelseries/fu-steelseries-firmware.h new file mode 100644 index 000000000..bb8363a46 --- /dev/null +++ b/plugins/steelseries/fu-steelseries-firmware.h @@ -0,0 +1,19 @@ +/* + * Copyright (C) 2022 Gaël PORTAY + * + * SPDX-License-Identifier: LGPL-2.1+ + */ + +#pragma once + +#include + +#define FU_TYPE_STEELSERIES_FIRMWARE (fu_steelseries_firmware_get_type()) +G_DECLARE_FINAL_TYPE(FuSteelseriesFirmware, + fu_steelseries_firmware, + FU, + STEELSERIES_FIRMWARE, + FuFirmware) + +FuFirmware * +fu_steelseries_firmware_new(void); diff --git a/plugins/steelseries/fu-steelseries-fizz.c b/plugins/steelseries/fu-steelseries-fizz.c new file mode 100644 index 000000000..04ec07513 --- /dev/null +++ b/plugins/steelseries/fu-steelseries-fizz.c @@ -0,0 +1,935 @@ +/* + * Copyright (C) 2022 Gaël PORTAY + * + * SPDX-License-Identifier: LGPL-2.1+ + */ + +#include "config.h" + +#include "fu-steelseries-common.h" +#include "fu-steelseries-firmware.h" +#include "fu-steelseries-fizz.h" + +#define STEELSERIES_TRANSACTION_TIMEOUT 5000 +#define STEELSERIES_BUFFER_CONTROL_SIZE 64 +#define STEELSERIES_BUFFER_TRANSFER_SIZE 52 + +#define STEELSERIES_FIZZ_FILESYSTEM_DONGLE 0x01U +#define STEELSERIES_FIZZ_FILESYSTEM_MOUSE 0x02U + +#define STEELSERIES_FIZZ_DONGLE_FILESYSTEM_MAIN_BOOT_ID 0x01U +#define STEELSERIES_FIZZ_DONGLE_FILESYSTEM_FSDATA_FILE_ID 0x02U +#define STEELSERIES_FIZZ_DONGLE_FILESYSTEM_FACTORY_SETTINGS_ID 0x03U +#define STEELSERIES_FIZZ_DONGLE_FILESYSTEM_MAIN_APP_ID 0x04U +#define STEELSERIES_FIZZ_DONGLE_FILESYSTEM_BACKUP_APP_ID 0x05U +#define STEELSERIES_FIZZ_DONGLE_FILESYSTEM_PROFILES_MOUSE_ID 0x06U +#define STEELSERIES_FIZZ_DONGLE_FILESYSTEM_PROFILES_LIGHTING_ID 0x0fU +#define STEELSERIES_FIZZ_DONGLE_FILESYSTEM_PROFILES_DEVICE_ID 0x10U +#define STEELSERIES_FIZZ_DONGLE_FILESYSTEM_PROFILES_RESERVED_ID 0x11U +#define STEELSERIES_FIZZ_DONGLE_FILESYSTEM_RECOVERY_ID 0x0dU +#define STEELSERIES_FIZZ_DONGLE_FILESYSTEM_FREE_SPACE_ID 0xf1U + +#define STEELSERIES_FIZZ_MOUSE_FILESYSTEM_SOFT_DEVICE_ID 0x00U +#define STEELSERIES_FIZZ_MOUSE_FILESYSTEM_PROFILES_MOUSE_ID 0x06U +#define STEELSERIES_FIZZ_MOUSE_FILESYSTEM_MAIN_APP_ID 0x07U +#define STEELSERIES_FIZZ_MOUSE_FILESYSTEM_BACKUP_APP_ID 0x08U +#define STEELSERIES_FIZZ_MOUSE_FILESYSTEM_MSB_DATA_ID 0x09U +#define STEELSERIES_FIZZ_MOUSE_FILESYSTEM_FACTORY_SETTINGS_ID 0x0aU +#define STEELSERIES_FIZZ_MOUSE_FILESYSTEM_FSDATA_FILE_ID 0x0bU +#define STEELSERIES_FIZZ_MOUSE_FILESYSTEM_MAIN_BOOT_ID 0x0cU +#define STEELSERIES_FIZZ_MOUSE_FILESYSTEM_RECOVERY_ID 0x0eU +#define STEELSERIES_FIZZ_MOUSE_FILESYSTEM_PROFILES_LIGHTING_ID 0x0fU +#define STEELSERIES_FIZZ_MOUSE_FILESYSTEM_PROFILES_DEVICE_ID 0x10U +#define STEELSERIES_FIZZ_MOUSE_FILESYSTEM_FDS_PAGES_ID 0x12U +#define STEELSERIES_FIZZ_MOUSE_FILESYSTEM_PROFILES_BLUETOOTH_ID 0x13U +#define STEELSERIES_FIZZ_MOUSE_FILESYSTEM_FREE_SPACE_ID 0xf0U + +#define STEELSERIES_FIZZ_COMMAND_ERROR_SUCCESS 0 +#define STEELSERIES_FIZZ_COMMAND_ERROR_FILE_NOT_FOUND 1 +#define STEELSERIES_FIZZ_COMMAND_ERROR_FILE_TOO_SHORT 2 +#define STEELSERIES_FIZZ_COMMAND_ERROR_FLASH_FAILED 3 +#define STEELSERIES_FIZZ_COMMAND_ERROR_PERMISSION_DENIED 4 +#define STEELSERIES_FIZZ_COMMAND_ERROR_OPERATION_NO_SUPPORTED 5 + +#define STEELSERIES_FIZZ_COMMAND_OFFSET 0x00U +#define STEELSERIES_FIZZ_ERROR_OFFSET 0x01U + +#define STEELSERIES_FIZZ_VERSION_COMMAND 0x90U +#define STEELSERIES_FIZZ_VERSION_COMMAND_OFFSET 0x00U +#define STEELSERIES_FIZZ_VERSION_MODE_OFFSET 0x01U + +#define STEELSERIES_FIZZ_WRITE_ACCESS_FILE_COMMAND_OFFSET 0x00U +#define STEELSERIES_FIZZ_WRITE_ACCESS_FILE_FILESYSTEM_OFFSET 0x01U +#define STEELSERIES_FIZZ_WRITE_ACCESS_FILE_ID_OFFSET 0x02U +#define STEELSERIES_FIZZ_WRITE_ACCESS_FILE_SIZE_OFFSET 0x03U +#define STEELSERIES_FIZZ_WRITE_ACCESS_FILE_OFFSET_OFFSET 0x05U +#define STEELSERIES_FIZZ_WRITE_ACCESS_FILE_DATA_OFFSET 0x09U + +#define STEELSERIES_FIZZ_READ_ACCESS_FILE_COMMAND_OFFSET 0x00U +#define STEELSERIES_FIZZ_READ_ACCESS_FILE_FILESYSTEM_OFFSET 0x01U +#define STEELSERIES_FIZZ_READ_ACCESS_FILE_ID_OFFSET 0x02U +#define STEELSERIES_FIZZ_READ_ACCESS_FILE_SIZE_OFFSET 0x03U +#define STEELSERIES_FIZZ_READ_ACCESS_FILE_OFFSET_OFFSET 0x05U +#define STEELSERIES_FIZZ_READ_ACCESS_FILE_DATA_OFFSET 0x02U + +#define STEELSERIES_FIZZ_ERASE_FILE_COMMAND_OFFSET 0x0U +#define STEELSERIES_FIZZ_ERASE_FILE_FILESYSTEM_OFFSET 0x1U +#define STEELSERIES_FIZZ_ERASE_FILE_ID_OFFSET 0x2U + +#define STEELSERIES_FIZZ_RESET_COMMAND_OFFSET 0x0U +#define STEELSERIES_FIZZ_RESET_MODE_OFFSET 0x1U + +struct _FuSteelseriesFizz { + FuUsbDevice parent_instance; + FuSteelseriesDeviceKind device_kind; + guint8 iface_idx; + guint8 ep; + gsize in_size; +}; + +G_DEFINE_TYPE(FuSteelseriesFizz, fu_steelseries_fizz, FU_TYPE_USB_DEVICE) + +static gboolean +fu_steelseries_fizz_command_error_to_error(guint8 cmd, guint8 err, GError **error) +{ + /* success */ + if (err == STEELSERIES_FIZZ_COMMAND_ERROR_SUCCESS) + return TRUE; + + if (err == STEELSERIES_FIZZ_COMMAND_ERROR_FILE_NOT_FOUND) { + g_set_error(error, + G_IO_ERROR, + G_FILE_ERROR_NOENT, + "command 0x%02x returned error 0x%02x", + cmd, + err); + return FALSE; + } + + /* targeted offset is past the file end */ + if (err == STEELSERIES_FIZZ_COMMAND_ERROR_FILE_TOO_SHORT) { + g_set_error(error, + G_IO_ERROR, + G_FILE_ERROR_NOSPC, + "command 0x%02x returned error 0x%02x", + cmd, + err); + return FALSE; + } + + /* when internal flash returns error */ + if (err == STEELSERIES_FIZZ_COMMAND_ERROR_FLASH_FAILED) { + g_set_error(error, + G_IO_ERROR, + G_FILE_ERROR_IO, + "command 0x%02x returned error 0x%02x", + cmd, + err); + return FALSE; + } + + /* USB API doesn't have permission to access this file */ + if (err == STEELSERIES_FIZZ_COMMAND_ERROR_PERMISSION_DENIED) { + g_set_error(error, + G_IO_ERROR, + G_FILE_ERROR_ACCES, + "command 0x%02x returned error 0x%02x", + cmd, + err); + return FALSE; + } + + /* USB API doesn't have permission to access this file */ + if (err == STEELSERIES_FIZZ_COMMAND_ERROR_OPERATION_NO_SUPPORTED) { + g_set_error(error, + G_IO_ERROR, + G_FILE_ERROR_PERM, + "command 0x%02x returned error 0x%02x", + cmd, + err); + return FALSE; + } + + /* fallback */ + g_set_error(error, + G_IO_ERROR, + G_IO_ERROR_FAILED, + "command 0x%02x returned error 0x%02x", + cmd, + err); + return FALSE; +} + +static gboolean +fu_steelseries_fizz_command(FuDevice *device, guint8 *data, gboolean answer, GError **error) +{ + FuSteelseriesFizz *self = FU_STEELSERIES_FIZZ(device); + GUsbDevice *usb_device = fu_usb_device_get_dev(FU_USB_DEVICE(device)); + gsize actual_len = 0; + gboolean ret; + + ret = g_usb_device_control_transfer(usb_device, + G_USB_DEVICE_DIRECTION_HOST_TO_DEVICE, + G_USB_DEVICE_REQUEST_TYPE_CLASS, + G_USB_DEVICE_RECIPIENT_INTERFACE, + 0x09, + 0x0200, + self->iface_idx, + data, + STEELSERIES_BUFFER_CONTROL_SIZE, + &actual_len, + STEELSERIES_TRANSACTION_TIMEOUT, + NULL, + error); + if (!ret) { + g_prefix_error(error, "failed to do control transfer: "); + return FALSE; + } + if (actual_len != STEELSERIES_BUFFER_CONTROL_SIZE) { + g_set_error(error, + G_IO_ERROR, + G_IO_ERROR_INVALID_DATA, + "only wrote %" G_GSIZE_FORMAT "bytes", + actual_len); + return FALSE; + } + + /* cleanup the buffer before receiving any data */ + memset(data, 0x00, STEELSERIES_BUFFER_CONTROL_SIZE); + + /* do not expect the answer from device */ + if (answer != TRUE) + return TRUE; + + ret = g_usb_device_interrupt_transfer(usb_device, + self->ep, + data, + self->in_size, + &actual_len, + STEELSERIES_TRANSACTION_TIMEOUT, + NULL, + error); + if (!ret) { + g_prefix_error(error, "failed to do EP transfer: "); + return FALSE; + } + if (actual_len != self->in_size) { + g_set_error(error, + G_IO_ERROR, + G_IO_ERROR_INVALID_DATA, + "only read %" G_GSIZE_FORMAT "bytes", + actual_len); + return FALSE; + } + + /* success */ + return TRUE; +} + +static gboolean +fu_steelseries_fizz_command_and_check_error(FuDevice *device, guint8 *data, GError **error) +{ + FuSteelseriesFizz *self = FU_STEELSERIES_FIZZ(device); + gint gerr = G_FILE_ERROR_FAILED; + const guint8 command = data[0]; + guint8 err; + guint8 cmd; + + if (!fu_steelseries_fizz_command(device, data, TRUE, error)) + return FALSE; + + if (!fu_common_read_uint8_safe(data, + self->in_size, + STEELSERIES_FIZZ_COMMAND_OFFSET, + &cmd, + error)) + return FALSE; + + if (cmd != command) { + g_set_error(error, + G_IO_ERROR, + gerr, + "command invalid, got 0x%02x, expected 0x%02x", + cmd, + command); + return FALSE; + } + + if (!fu_common_read_uint8_safe(data, + self->in_size, + STEELSERIES_FIZZ_ERROR_OFFSET, + &err, + error)) + return FALSE; + + return fu_steelseries_fizz_command_error_to_error(cmd, err, error); +} + +static gchar * +fu_steelseries_fizz_version(FuDevice *device, GError **error) +{ + guint8 data[STEELSERIES_BUFFER_CONTROL_SIZE] = {0}; + const guint16 cmd = 0x90U; + const guint8 mode = 0U; /* string */ + + if (!fu_common_write_uint8_safe(data, + sizeof(data), + STEELSERIES_FIZZ_VERSION_COMMAND_OFFSET, + cmd, + error)) + return NULL; + + if (!fu_common_write_uint8_safe(data, + sizeof(data), + STEELSERIES_FIZZ_VERSION_MODE_OFFSET, + mode, + error)) + return NULL; + + if (g_getenv("FWUPD_STEELSERIES_FIZZ_VERBOSE") != NULL) + fu_common_dump_raw(G_LOG_DOMAIN, "Version", data, sizeof(data)); + if (!fu_steelseries_fizz_command(device, data, TRUE, error)) + return NULL; + if (g_getenv("FWUPD_STEELSERIES_FIZZ_VERBOSE") != NULL) + fu_common_dump_raw(G_LOG_DOMAIN, "Version", data, sizeof(data)); + + /* success */ + return g_strndup((const gchar *)data, sizeof(data)); +} + +static gboolean +fu_steelseries_fizz_write_access_file(FuDevice *device, + guint8 fs, + guint8 id, + const guint8 *buf, + gsize bufsz, + FuProgress *progress, + GError **error) +{ + const guint16 cmd = 0x03U; + guint8 data[STEELSERIES_BUFFER_CONTROL_SIZE] = {0}; + g_autoptr(GPtrArray) chunks = NULL; + + chunks = fu_chunk_array_new(buf, bufsz, 0x0, 0x0, STEELSERIES_BUFFER_TRANSFER_SIZE); + fu_progress_set_id(progress, G_STRLOC); + fu_progress_set_status(progress, FWUPD_STATUS_DEVICE_WRITE); + fu_progress_set_steps(progress, chunks->len); + for (guint i = 0; i < chunks->len; i++) { + FuChunk *chk = g_ptr_array_index(chunks, i); + const guint16 size = fu_chunk_get_data_sz(chk); + const guint32 offset = fu_chunk_get_address(chk); + + if (!fu_common_write_uint8_safe(data, + sizeof(data), + STEELSERIES_FIZZ_WRITE_ACCESS_FILE_COMMAND_OFFSET, + cmd, + error)) + return FALSE; + + if (!fu_common_write_uint8_safe( + data, + sizeof(data), + STEELSERIES_FIZZ_WRITE_ACCESS_FILE_FILESYSTEM_OFFSET, + fs, + error)) + return FALSE; + + if (!fu_common_write_uint8_safe(data, + sizeof(data), + STEELSERIES_FIZZ_WRITE_ACCESS_FILE_ID_OFFSET, + id, + error)) + return FALSE; + + if (!fu_common_write_uint16_safe(data, + sizeof(data), + STEELSERIES_FIZZ_WRITE_ACCESS_FILE_SIZE_OFFSET, + size, + G_LITTLE_ENDIAN, + error)) + return FALSE; + + if (!fu_common_write_uint32_safe(data, + sizeof(data), + STEELSERIES_FIZZ_WRITE_ACCESS_FILE_OFFSET_OFFSET, + offset, + G_LITTLE_ENDIAN, + error)) + return FALSE; + + if (!fu_memcpy_safe(data, + sizeof(data), + STEELSERIES_FIZZ_WRITE_ACCESS_FILE_DATA_OFFSET, + fu_chunk_get_data(chk), + fu_chunk_get_data_sz(chk), + 0x0, + fu_chunk_get_data_sz(chk), + error)) + return FALSE; + + if (g_getenv("FWUPD_STEELSERIES_FIZZ_VERBOSE") != NULL) + fu_common_dump_raw(G_LOG_DOMAIN, "AccessFile", data, sizeof(data)); + if (!fu_steelseries_fizz_command_and_check_error(device, data, error)) + return FALSE; + if (g_getenv("FWUPD_STEELSERIES_FIZZ_VERBOSE") != NULL) + fu_common_dump_raw(G_LOG_DOMAIN, "AccessFile", data, sizeof(data)); + + fu_progress_step_done(progress); + } + + /* success */ + return TRUE; +} + +static gboolean +fu_steelseries_fizz_erase_file(FuDevice *device, guint8 fs, guint8 id, GError **error) +{ + guint8 data[STEELSERIES_BUFFER_CONTROL_SIZE] = {0}; + const guint16 cmd = 0x02U; + + if (!fu_common_write_uint8_safe(data, + sizeof(data), + STEELSERIES_FIZZ_ERASE_FILE_COMMAND_OFFSET, + cmd, + error)) + return FALSE; + + if (!fu_common_write_uint8_safe(data, + sizeof(data), + STEELSERIES_FIZZ_ERASE_FILE_FILESYSTEM_OFFSET, + fs, + error)) + return FALSE; + + if (!fu_common_write_uint8_safe(data, + sizeof(data), + STEELSERIES_FIZZ_ERASE_FILE_ID_OFFSET, + id, + error)) + return FALSE; + + if (g_getenv("FWUPD_STEELSERIES_FIZZ_VERBOSE") != NULL) + fu_common_dump_raw(G_LOG_DOMAIN, "EraseFile", data, sizeof(data)); + if (!fu_steelseries_fizz_command_and_check_error(device, data, error)) + return FALSE; + if (g_getenv("FWUPD_STEELSERIES_FIZZ_VERBOSE") != NULL) + fu_common_dump_raw(G_LOG_DOMAIN, "EraseFile", data, sizeof(data)); + + /* success */ + return TRUE; +} + +static gboolean +fu_steelseries_fizz_reset(FuDevice *device, guint8 mode, GError **error) +{ + guint8 data[STEELSERIES_BUFFER_CONTROL_SIZE] = {0}; + const guint16 cmd = 0x01U; + + if (!fu_common_write_uint8_safe(data, + sizeof(data), + STEELSERIES_FIZZ_RESET_COMMAND_OFFSET, + cmd, + error)) + return FALSE; + + if (!fu_common_write_uint8_safe(data, + sizeof(data), + STEELSERIES_FIZZ_RESET_MODE_OFFSET, + mode, + error)) + return FALSE; + + if (g_getenv("FWUPD_STEELSERIES_FIZZ_VERBOSE") != NULL) + fu_common_dump_raw(G_LOG_DOMAIN, "Reset", data, sizeof(data)); + if (!fu_steelseries_fizz_command(device, data, FALSE, error)) + return FALSE; + + /* success */ + return TRUE; +} + +static gboolean +fu_steelseries_fizz_file_crc32(FuDevice *device, + guint8 fs, + guint8 id, + guint32 *calculated_crc, + guint32 *stored_crc, + GError **error) +{ + guint8 data[STEELSERIES_BUFFER_CONTROL_SIZE] = {0}; + const guint16 cmd = 0x84U; + + if (!fu_common_write_uint8_safe(data, sizeof(data), 0x00U, cmd, error)) + return FALSE; + + if (!fu_common_write_uint8_safe(data, sizeof(data), 0x01U, fs, error)) + return FALSE; + + if (!fu_common_write_uint8_safe(data, sizeof(data), 0x02U, id, error)) + return FALSE; + + if (g_getenv("FWUPD_STEELSERIES_FIZZ_VERBOSE") != NULL) + fu_common_dump_raw(G_LOG_DOMAIN, "FileCRC32", data, sizeof(data)); + if (!fu_steelseries_fizz_command_and_check_error(device, data, error)) + return FALSE; + if (g_getenv("FWUPD_STEELSERIES_FIZZ_VERBOSE") != NULL) + fu_common_dump_raw(G_LOG_DOMAIN, "FileCRC32", data, sizeof(data)); + + if (!fu_common_read_uint32_safe(data, + sizeof(data), + 0x02U, + calculated_crc, + G_LITTLE_ENDIAN, + error)) + return FALSE; + + if (!fu_common_read_uint32_safe(data, + sizeof(data), + 0x06U, + stored_crc, + G_LITTLE_ENDIAN, + error)) + return FALSE; + + /* success */ + return TRUE; +} + +static gboolean +fu_steelseries_fizz_read_access_file(FuDevice *device, + guint8 fs, + guint8 id, + guint8 *buf, + gsize bufsz, + FuProgress *progress, + GError **error) +{ + const guint16 cmd = 0x83U; + guint8 data[STEELSERIES_BUFFER_CONTROL_SIZE] = {0}; + g_autoptr(GPtrArray) chunks = NULL; + + chunks = fu_chunk_array_mutable_new(buf, bufsz, 0x0, 0x0, STEELSERIES_BUFFER_TRANSFER_SIZE); + fu_progress_set_id(progress, G_STRLOC); + fu_progress_set_status(progress, FWUPD_STATUS_DEVICE_WRITE); + fu_progress_set_steps(progress, chunks->len); + for (guint i = 0; i < chunks->len; i++) { + FuChunk *chk = g_ptr_array_index(chunks, i); + const guint16 size = fu_chunk_get_data_sz(chk); + const guint32 offset = fu_chunk_get_address(chk); + + if (!fu_common_write_uint8_safe(data, + sizeof(data), + STEELSERIES_FIZZ_READ_ACCESS_FILE_COMMAND_OFFSET, + cmd, + error)) + return FALSE; + + if (!fu_common_write_uint8_safe(data, + sizeof(data), + STEELSERIES_FIZZ_READ_ACCESS_FILE_FILESYSTEM_OFFSET, + fs, + error)) + return FALSE; + + if (!fu_common_write_uint8_safe(data, + sizeof(data), + STEELSERIES_FIZZ_READ_ACCESS_FILE_ID_OFFSET, + id, + error)) + return FALSE; + + if (!fu_common_write_uint16_safe(data, + sizeof(data), + STEELSERIES_FIZZ_READ_ACCESS_FILE_SIZE_OFFSET, + size, + G_LITTLE_ENDIAN, + error)) + return FALSE; + + if (!fu_common_write_uint32_safe(data, + sizeof(data), + STEELSERIES_FIZZ_READ_ACCESS_FILE_OFFSET_OFFSET, + offset, + G_LITTLE_ENDIAN, + error)) + return FALSE; + + if (g_getenv("FWUPD_STEELSERIES_FIZZ_VERBOSE") != NULL) + fu_common_dump_raw(G_LOG_DOMAIN, "AccessFile", data, sizeof(data)); + if (!fu_steelseries_fizz_command_and_check_error(device, data, error)) + return FALSE; + if (g_getenv("FWUPD_STEELSERIES_FIZZ_VERBOSE") != NULL) + fu_common_dump_raw(G_LOG_DOMAIN, "AccessFile", data, sizeof(data)); + + if (!fu_memcpy_safe(fu_chunk_get_data_out(chk), + fu_chunk_get_data_sz(chk), + 0x00, + data, + sizeof(data), + STEELSERIES_FIZZ_READ_ACCESS_FILE_DATA_OFFSET, + fu_chunk_get_data_sz(chk), + error)) + return FALSE; + + fu_progress_step_done(progress); + } + + /* success */ + return TRUE; +} + +static gboolean +fu_steelseries_fizz_probe(FuDevice *device, GError **error) +{ +#if G_USB_CHECK_VERSION(0, 3, 3) + FuSteelseriesFizz *self = FU_STEELSERIES_FIZZ(device); + GUsbDevice *usb_device = fu_usb_device_get_dev(FU_USB_DEVICE(device)); + GUsbInterface *iface = NULL; + GUsbEndpoint *ep = NULL; + guint8 iface_id = 3; + guint8 ep_id; + guint16 packet_size; + g_autoptr(GPtrArray) ifaces = NULL; + g_autoptr(GPtrArray) endpoints = NULL; + + /* FuUsbDevice->probe */ + if (!FU_DEVICE_CLASS(fu_steelseries_fizz_parent_class)->probe(device, error)) + return FALSE; + + ifaces = g_usb_device_get_interfaces(usb_device, error); + if (ifaces == NULL || ifaces->len < iface_id) { + g_set_error_literal(error, + FWUPD_ERROR, + FWUPD_ERROR_NOT_FOUND, + "update interface not found"); + return FALSE; + } + + /* use the fourth interface for interrupt transfer */ + iface = g_ptr_array_index(ifaces, iface_id); + + endpoints = g_usb_interface_get_endpoints(iface); + /* expecting to have only one endpoint for communication */ + if (endpoints == NULL || endpoints->len != 1) { + g_set_error_literal(error, + FWUPD_ERROR, + FWUPD_ERROR_NOT_FOUND, + "endpoint not found"); + return FALSE; + } + + ep = g_ptr_array_index(endpoints, 0); + ep_id = g_usb_endpoint_get_address(ep); + packet_size = g_usb_endpoint_get_maximum_packet_size(ep); + + self->iface_idx = iface_id; + self->ep = ep_id; + self->in_size = packet_size; + + fu_usb_device_add_interface(FU_USB_DEVICE(self), iface_id); + + /* success */ + return TRUE; +#else + /* failed */ + g_set_error_literal(error, + FWUPD_ERROR, + FWUPD_ERROR_NOT_SUPPORTED, + "this version of GUsb is not supported"); + return FALSE; +#endif +} + +static gboolean +fu_steelseries_fizz_attach(FuDevice *device, FuProgress *progress, GError **error) +{ + g_autoptr(GError) error_local = NULL; + guint8 mode = 0x00U; /* normal */ + + if (!fu_steelseries_fizz_reset(device, mode, &error_local)) + g_warning("failed to reset: %s", error_local->message); + + fu_device_add_flag(device, FWUPD_DEVICE_FLAG_WAIT_FOR_REPLUG); + + /* success */ + return TRUE; +} + +static gboolean +fu_steelseries_fizz_setup(FuDevice *device, GError **error) +{ + FuSteelseriesFizz *self = FU_STEELSERIES_FIZZ(device); + guint32 calculated_crc; + guint32 stored_crc; + guint8 fs = STEELSERIES_FIZZ_FILESYSTEM_MOUSE; + guint8 id = STEELSERIES_FIZZ_MOUSE_FILESYSTEM_BACKUP_APP_ID; + g_autofree gchar *version = NULL; + + /* FuUsbDevice->setup */ + if (!FU_DEVICE_CLASS(fu_steelseries_fizz_parent_class)->setup(device, error)) + return FALSE; + + version = fu_steelseries_fizz_version(device, error); + if (version == NULL) { + g_prefix_error(error, "failed to get version: "); + return FALSE; + } + fu_device_set_version(device, version); + + /* it is a dongle */ + if (self->device_kind == FU_STEELSERIES_DEVICE_FIZZ_DONGLE) { + fs = STEELSERIES_FIZZ_FILESYSTEM_DONGLE; + id = STEELSERIES_FIZZ_DONGLE_FILESYSTEM_BACKUP_APP_ID; + } + + if (!fu_steelseries_fizz_file_crc32(device, fs, id, &calculated_crc, &stored_crc, error)) { + g_prefix_error(error, + "failed to get file CRC32 from FS 0x%02x ID 0x%02x: ", + fs, + id); + return FALSE; + } + + if (calculated_crc != stored_crc) + g_warning("%s: checksum mismatch, got 0x%08x, expected 0x%08x", + fu_device_get_name(device), + calculated_crc, + stored_crc); + + /* success */ + return TRUE; +} + +static gboolean +fu_steelseries_fizz_write_file(FuDevice *device, + guint8 fs, + guint8 id, + FuFirmware *firmware, + FuProgress *progress, + FwupdInstallFlags flags, + GError **error) +{ + guint32 calculated_crc; + guint32 stored_crc; + const guint8 *buf; + gsize bufsz; + g_autoptr(GBytes) blob = NULL; + g_autoptr(GPtrArray) chunks = NULL; + + fu_progress_set_id(progress, G_STRLOC); + fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_ERASE, 38); + fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 60); + fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_VERIFY, 2); + + blob = fu_firmware_get_bytes(firmware, error); + if (blob == NULL) + return FALSE; + buf = fu_bytes_get_data_safe(blob, &bufsz, error); + if (buf == NULL) + return FALSE; + if (g_getenv("FWUPD_STEELSERIES_FIZZ_VERBOSE") != NULL) + fu_common_dump_raw(G_LOG_DOMAIN, "File", buf, bufsz); + if (!fu_steelseries_fizz_erase_file(device, fs, id, error)) { + g_prefix_error(error, "failed to erase file 0x%02x:0x%02x: ", fs, id); + return FALSE; + } + fu_progress_step_done(progress); + if (!fu_steelseries_fizz_write_access_file(device, + fs, + id, + buf, + bufsz, + fu_progress_get_child(progress), + error)) { + g_prefix_error(error, "failed to write file 0x%02x:0x%02x: ", fs, id); + return FALSE; + } + fu_progress_step_done(progress); + + if (!fu_steelseries_fizz_file_crc32(device, fs, id, &calculated_crc, &stored_crc, error)) { + g_prefix_error(error, + "failed to get file CRC32 from FS 0x%02x ID 0x%02x: ", + fs, + id); + return FALSE; + } + if (calculated_crc != stored_crc) + g_warning("%s: checksum mismatch, got 0x%08x, expected 0x%08x", + fu_device_get_name(device), + calculated_crc, + stored_crc); + fu_progress_step_done(progress); + + /* success */ + return TRUE; +} + +static gboolean +fu_steelseries_fizz_write_firmware(FuDevice *device, + FuFirmware *firmware, + FuProgress *progress, + FwupdInstallFlags flags, + GError **error) +{ + FuSteelseriesFizz *self = FU_STEELSERIES_FIZZ(device); + guint8 fs = STEELSERIES_FIZZ_FILESYSTEM_MOUSE; + guint8 id = STEELSERIES_FIZZ_MOUSE_FILESYSTEM_BACKUP_APP_ID; + + /* it is a dongle */ + if (self->device_kind == FU_STEELSERIES_DEVICE_FIZZ_DONGLE) { + fs = STEELSERIES_FIZZ_FILESYSTEM_DONGLE; + id = STEELSERIES_FIZZ_DONGLE_FILESYSTEM_BACKUP_APP_ID; + } + + fu_progress_set_id(progress, G_STRLOC); + fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 100); + + if (!fu_steelseries_fizz_write_file(device, + fs, + id, + firmware, + fu_progress_get_child(progress), + flags, + error)) + return FALSE; + fu_progress_step_done(progress); + + /* success */ + return TRUE; +} + +static FuFirmware * +fu_steelseries_fizz_read_firmware(FuDevice *device, FuProgress *progress, GError **error) +{ + FuSteelseriesFizz *self = FU_STEELSERIES_FIZZ(device); + guint8 fs = STEELSERIES_FIZZ_FILESYSTEM_MOUSE; + guint8 id = STEELSERIES_FIZZ_MOUSE_FILESYSTEM_BACKUP_APP_ID; + gsize bufsz = 0x27000; + g_autoptr(FuFirmware) firmware = fu_steelseries_firmware_new(); + g_autoptr(GBytes) blob = NULL; + g_autofree guint8 *buf = NULL; + + fu_progress_set_id(progress, G_STRLOC); + fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_READ, 100); + + /* it is a dongle */ + if (self->device_kind == FU_STEELSERIES_DEVICE_FIZZ_DONGLE) { + fs = STEELSERIES_FIZZ_FILESYSTEM_DONGLE; + id = STEELSERIES_FIZZ_DONGLE_FILESYSTEM_BACKUP_APP_ID; + bufsz = 0x23000; + } + + buf = g_malloc0(bufsz); + if (!fu_steelseries_fizz_read_access_file(device, + fs, + id, + buf, + bufsz, + fu_progress_get_child(progress), + error)) + return NULL; + fu_progress_step_done(progress); + + if (g_getenv("FWUPD_STEELSERIES_FIZZ_VERBOSE") != NULL) + fu_common_dump_raw(G_LOG_DOMAIN, "Firmware", buf, bufsz); + blob = g_bytes_new_take(g_steal_pointer(&buf), bufsz); + if (!fu_firmware_parse(firmware, blob, FWUPD_INSTALL_FLAG_NONE, error)) + return NULL; + + /* success */ + return g_steal_pointer(&firmware); +} + +static FuFirmware * +fu_steelseries_fizz_prepare_firmware(FuDevice *device, + GBytes *fw, + FwupdInstallFlags flags, + GError **error) +{ + g_autoptr(FuFirmware) firmware = fu_steelseries_firmware_new(); + + if (!fu_firmware_parse(FU_FIRMWARE(firmware), fw, flags, error)) + return NULL; + + /* success */ + return g_steal_pointer(&firmware); +} + +static gboolean +fu_steelseries_fizz_set_quirk_kv(FuDevice *device, + const gchar *key, + const gchar *value, + GError **error) +{ + FuSteelseriesFizz *self = FU_STEELSERIES_FIZZ(device); + + if (g_strcmp0(key, "SteelSeriesDeviceKind") == 0) { + self->device_kind = fu_steelseries_device_type_from_string(value); + if (self->device_kind != FU_STEELSERIES_DEVICE_UNKNOWN) + return TRUE; + + g_set_error_literal(error, + G_IO_ERROR, + G_IO_ERROR_INVALID_DATA, + "unsupported SteelSeriesDeviceKind quirk format"); + return FALSE; + } + + /* failed */ + g_set_error_literal(error, G_IO_ERROR, G_IO_ERROR_NOT_SUPPORTED, "quirk key not supported"); + return FALSE; +} + +static void +fu_steelseries_fizz_to_string(FuDevice *device, guint idt, GString *str) +{ + FuSteelseriesFizz *self = FU_STEELSERIES_FIZZ(device); + + FU_DEVICE_CLASS(fu_steelseries_fizz_parent_class)->to_string(device, idt, str); + + fu_common_string_append_kv(str, + idt, + "DeviceKind", + fu_steelseries_device_type_to_string(self->device_kind)); + fu_common_string_append_kx(str, idt, "Interface", self->iface_idx); + fu_common_string_append_kx(str, idt, "Endpoint", self->ep); +} + +static void +fu_steelseries_fizz_set_progress(FuDevice *self, FuProgress *progress) +{ + fu_progress_set_id(progress, G_STRLOC); + fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 0); /* detach */ + fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 82); /* write */ + fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 0); /* attach */ + fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_BUSY, 18); /* reload */ +} + +static void +fu_steelseries_fizz_class_init(FuSteelseriesFizzClass *klass) +{ + FuDeviceClass *klass_device = FU_DEVICE_CLASS(klass); + + klass_device->probe = fu_steelseries_fizz_probe; + klass_device->attach = fu_steelseries_fizz_attach; + klass_device->setup = fu_steelseries_fizz_setup; + klass_device->write_firmware = fu_steelseries_fizz_write_firmware; + klass_device->read_firmware = fu_steelseries_fizz_read_firmware; + klass_device->prepare_firmware = fu_steelseries_fizz_prepare_firmware; + klass_device->set_quirk_kv = fu_steelseries_fizz_set_quirk_kv; + klass_device->to_string = fu_steelseries_fizz_to_string; + klass_device->set_progress = fu_steelseries_fizz_set_progress; +} + +static void +fu_steelseries_fizz_init(FuSteelseriesFizz *self) +{ + self->device_kind = FU_STEELSERIES_DEVICE_FIZZ; + + fu_device_set_version_format(FU_DEVICE(self), FWUPD_VERSION_FORMAT_TRIPLET); + fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_UPDATABLE); + fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_CAN_VERIFY_IMAGE); + fu_device_add_protocol(FU_DEVICE(self), "com.steelseries.fizz"); + fu_device_set_install_duration(FU_DEVICE(self), 13); /* 13 s */ + fu_device_set_remove_delay(FU_DEVICE(self), FU_DEVICE_REMOVE_DELAY_USER_REPLUG); +} diff --git a/plugins/steelseries/fu-steelseries-fizz.h b/plugins/steelseries/fu-steelseries-fizz.h new file mode 100644 index 000000000..cc7e7a8a6 --- /dev/null +++ b/plugins/steelseries/fu-steelseries-fizz.h @@ -0,0 +1,16 @@ +/* + * Copyright (C) 2022 Gaël PORTAY + * + * SPDX-License-Identifier: LGPL-2.1+ + */ + +#pragma once + +#include + +#define FU_TYPE_STEELSERIES_FIZZ (fu_steelseries_fizz_get_type()) +G_DECLARE_FINAL_TYPE(FuSteelseriesFizz, fu_steelseries_fizz, FU, STEELSERIES_FIZZ, FuUsbDevice) + +struct _FuSteelseriesFizzClass { + FuUsbDeviceClass parent_class; +}; diff --git a/plugins/steelseries/meson.build b/plugins/steelseries/meson.build index df35bd10e..b5fb1c864 100644 --- a/plugins/steelseries/meson.build +++ b/plugins/steelseries/meson.build @@ -11,6 +11,8 @@ shared_module('fu_plugin_steelseries', 'fu-plugin-steelseries.c', 'fu-steelseries-common.c', 'fu-steelseries-device.c', + 'fu-steelseries-firmware.c', + 'fu-steelseries-fizz.c', 'fu-steelseries-gamepad.c', 'fu-steelseries-sonic.c', ], diff --git a/plugins/steelseries/steelseries.quirk b/plugins/steelseries/steelseries.quirk index 7fd10156e..2b09f9200 100644 --- a/plugins/steelseries/steelseries.quirk +++ b/plugins/steelseries/steelseries.quirk @@ -11,6 +11,24 @@ Plugin = steelseries GType = FuSteelseriesSonic Icon = input-mouse +# Aerox 3 Wireless +[USB\VID_1038&PID_1838] +Plugin = steelseries +GType = FuSteelseriesFizz +Name = Aerox 3 Wireless Dongle +Icon = input-mouse +CounterpartGuid = USB\VID_1038&PID_1839 +FirmwareSize = 0x23000 +SteelSeriesDeviceKind = fizz-dongle + +[USB\VID_1038&PID_183A] +Plugin = steelseries +GType = FuSteelseriesFizz +Name = Aerox 3 Wireless Mouse +Icon = input-mouse +CounterpartGuid = USB\VID_1038&PID_183B +FirmwareSize = 0x27000 + # Stratus Duo RX [USB\VID_1038&PID_1430] Plugin = steelseries