fwupd/plugins/bcm57xx/fu-bcm57xx-device.c
Richard Hughes d99f74f0b7 Revert "bcm57xx: Use PCI function 1 as 0 does not work with the Dell KH08P"
This reverts commit b5eddee5f6.

Using pci function 0 works on my Lenovo P50 but not my Lenovo X1. Don't break
machines where we are shipping the chip rather than ones where we probably are
not, especially when it's probably a kernel bug somewhere.

Fixes https://github.com/fwupd/fwupd/issues/2608
2020-11-20 09:54:34 +00:00

645 lines
19 KiB
C

/*
* Copyright (C) 2018-2020 Evan Lojewski
* Copyright (C) 2020 Richard Hughes <richard@hughsie.com>
*
* SPDX-License-Identifier: GPL-2+
*/
#include "config.h"
#ifdef HAVE_ERRNO_H
#include <errno.h>
#endif
#include <glib/gstdio.h>
#ifdef HAVE_ETHTOOL_H
#include <linux/ethtool.h>
#include <linux/sockios.h>
#include <net/if.h>
#endif
#ifdef HAVE_IOCTL_H
#include <sys/ioctl.h>
#endif
#ifdef HAVE_SOCKET_H
#include <sys/socket.h>
#endif
#include "fu-chunk.h"
#include "fu-common.h"
#include "fu-bcm57xx-common.h"
#include "fu-bcm57xx-device.h"
#include "fu-bcm57xx-recovery-device.h"
#include "fu-bcm57xx-firmware.h"
#include "fu-bcm57xx-dict-image.h"
#define FU_BCM57XX_BLOCK_SZ 0x4000 /* 16kb */
struct _FuBcm57xxDevice {
FuUdevDevice parent_instance;
FuBcm57xxRecoveryDevice *recovery;
gchar *ethtool_iface;
int ethtool_fd;
};
G_DEFINE_TYPE (FuBcm57xxDevice, fu_bcm57xx_device, FU_TYPE_UDEV_DEVICE)
static void
fu_bcm57xx_device_to_string (FuUdevDevice *device, guint idt, GString *str)
{
FuBcm57xxDevice *self = FU_BCM57XX_DEVICE (device);
fu_common_string_append_kv (str, idt, "EthtoolIface", self->ethtool_iface);
}
static gboolean
fu_bcm57xx_device_probe (FuUdevDevice *device, GError **error)
{
FuBcm57xxDevice *self = FU_BCM57XX_DEVICE (device);
g_autofree gchar *fn = NULL;
g_autoptr(GPtrArray) ifaces = NULL;
/* only enumerate number 0 */
if (fu_udev_device_get_number (device) != 0) {
g_set_error_literal (error,
FWUPD_ERROR,
FWUPD_ERROR_NOT_SUPPORTED,
"only device 0 supported on multi-device card");
return FALSE;
}
/* we need this even for non-recovery to reset APE */
fu_device_set_quirks (FU_DEVICE (self->recovery),
fu_device_get_quirks (FU_DEVICE (self)));
fu_device_incorporate (FU_DEVICE (self->recovery), FU_DEVICE (self));
if (!fu_device_probe (FU_DEVICE (self->recovery), error))
return FALSE;
/* only if has an interface */
fn = g_build_filename (fu_udev_device_get_sysfs_path (device), "net", NULL);
if (!g_file_test (fn, G_FILE_TEST_EXISTS)) {
g_debug ("waiting for net devices to appear");
g_usleep (50 * 1000);
}
ifaces = fu_common_filename_glob (fn, "en*", NULL);
if (ifaces == NULL || ifaces->len == 0) {
fu_device_add_child (FU_DEVICE (self), FU_DEVICE (self->recovery));
} else {
self->ethtool_iface = g_path_get_basename (g_ptr_array_index (ifaces, 0));
}
/* success */
return fu_udev_device_set_physical_id (device, "pci", error);
}
static gboolean
fu_bcm57xx_device_nvram_write (FuBcm57xxDevice *self,
guint32 address,
const guint8 *buf,
gsize bufsz,
GError **error)
{
#ifdef HAVE_ETHTOOL_H
gsize eepromsz;
gint rc = -1;
struct ifreq ifr = { 0 };
g_autofree struct ethtool_eeprom *eeprom = NULL;
/* failed to load tg3 */
if (self->ethtool_iface == NULL) {
g_set_error_literal (error,
FWUPD_ERROR,
FWUPD_ERROR_NOT_SUPPORTED,
"Not supported as ethtool interface disabled");
return FALSE;
}
/* sanity check */
if (address + bufsz > fu_device_get_firmware_size_max (FU_DEVICE (self))) {
g_set_error (error,
G_IO_ERROR,
G_IO_ERROR_FAILED,
"tried to read outside of EEPROM size [0x%x]",
(guint) fu_device_get_firmware_size_max (FU_DEVICE (self)));
return FALSE;
}
/* write EEPROM (NVRAM) data */
eepromsz = sizeof(struct ethtool_eeprom) + bufsz;
eeprom = (struct ethtool_eeprom *) g_malloc0 (eepromsz);
eeprom->cmd = ETHTOOL_SEEPROM;
eeprom->magic = BCM_NVRAM_MAGIC;
eeprom->len = bufsz;
eeprom->offset = address;
memcpy (eeprom->data, buf, eeprom->len);
strncpy (ifr.ifr_name, self->ethtool_iface, IFNAMSIZ - 1);
ifr.ifr_data = (char *) eeprom;
#ifdef HAVE_IOCTL_H
rc = ioctl (self->ethtool_fd, SIOCETHTOOL, &ifr);
#else
g_set_error_literal (error,
FWUPD_ERROR,
FWUPD_ERROR_NOT_SUPPORTED,
"Not supported as <sys/ioctl.h> not found");
return FALSE;
#endif
if (rc < 0) {
g_set_error (error,
G_IO_ERROR,
G_IO_ERROR_FAILED,
"cannot write eeprom [%i]", rc);
return FALSE;
}
/* success */
return TRUE;
#else
g_set_error_literal (error,
FWUPD_ERROR,
FWUPD_ERROR_NOT_SUPPORTED,
"Not supported as <linux/ethtool.h> not found");
return FALSE;
#endif
}
static gboolean
fu_bcm57xx_device_nvram_read (FuBcm57xxDevice *self,
guint32 address,
guint8 *buf,
gsize bufsz,
GError **error)
{
#ifdef HAVE_ETHTOOL_H
gsize eepromsz;
gint rc = -1;
struct ifreq ifr = { 0 };
g_autofree struct ethtool_eeprom *eeprom = NULL;
/* failed to load tg3 */
if (self->ethtool_iface == NULL) {
g_set_error_literal (error,
FWUPD_ERROR,
FWUPD_ERROR_NOT_SUPPORTED,
"Not supported as ethtool interface disabled");
return FALSE;
}
/* sanity check */
if (address + bufsz > fu_device_get_firmware_size_max (FU_DEVICE (self))) {
g_set_error (error,
G_IO_ERROR,
G_IO_ERROR_FAILED,
"tried to read outside of EEPROM size [0x%x]",
(guint) fu_device_get_firmware_size_max (FU_DEVICE (self)));
return FALSE;
}
/* read EEPROM (NVRAM) data */
eepromsz = sizeof(struct ethtool_eeprom) + bufsz;
eeprom = (struct ethtool_eeprom *) g_malloc0 (eepromsz);
eeprom->cmd = ETHTOOL_GEEPROM;
eeprom->len = bufsz;
eeprom->offset = address;
strncpy (ifr.ifr_name, self->ethtool_iface, IFNAMSIZ - 1);
ifr.ifr_data = (char *) eeprom;
#ifdef HAVE_IOCTL_H
rc = ioctl (self->ethtool_fd, SIOCETHTOOL, &ifr);
#else
g_set_error_literal (error,
FWUPD_ERROR,
FWUPD_ERROR_NOT_SUPPORTED,
"Not supported as <sys/ioctl.h> not found");
return FALSE;
#endif
if (rc < 0) {
g_set_error (error,
G_IO_ERROR,
G_IO_ERROR_FAILED,
"cannot read eeprom [%i]", rc);
return FALSE;
}
/* copy back data */
if (!fu_memcpy_safe (buf, bufsz, 0x0, /* dst */
(guint8 *) eeprom, eepromsz, /* src */
G_STRUCT_OFFSET(struct ethtool_eeprom, data),
bufsz, error))
return FALSE;
/* success */
return TRUE;
#else
g_set_error_literal (error,
FWUPD_ERROR,
FWUPD_ERROR_NOT_SUPPORTED,
"Not supported as <linux/ethtool.h> not found");
return FALSE;
#endif
}
static gboolean
fu_bcm57xx_device_nvram_check (FuBcm57xxDevice *self, GError **error)
{
#ifdef HAVE_ETHTOOL_H
gint rc = -1;
struct ethtool_drvinfo drvinfo = { 0 };
struct ifreq ifr = { 0 };
/* failed to load tg3 */
if (self->ethtool_iface == NULL) {
g_set_error_literal (error,
FWUPD_ERROR,
FWUPD_ERROR_NOT_SUPPORTED,
"Not supported as ethtool interface disabled");
return FALSE;
}
/* get driver info */
drvinfo.cmd = ETHTOOL_GDRVINFO;
strncpy (ifr.ifr_name, self->ethtool_iface, IFNAMSIZ - 1);
ifr.ifr_data = (char *) &drvinfo;
#ifdef HAVE_IOCTL_H
rc = ioctl (self->ethtool_fd, SIOCETHTOOL, &ifr);
#else
g_set_error_literal (error,
FWUPD_ERROR,
FWUPD_ERROR_NOT_SUPPORTED,
"Not supported as <sys/ioctl.h> not found");
return FALSE;
#endif
if (rc < 0) {
g_set_error (error,
G_IO_ERROR,
G_IO_ERROR_FAILED,
"cannot get driver information [%i]", rc);
return FALSE;
}
g_debug ("FW version %s", drvinfo.fw_version);
/* sanity check */
if (drvinfo.eedump_len != fu_device_get_firmware_size_max (FU_DEVICE (self))) {
g_set_error (error,
G_IO_ERROR,
G_IO_ERROR_FAILED,
"EEPROM size invalid, got 0x%x, expected 0x%x",
drvinfo.eedump_len,
(guint) fu_device_get_firmware_size_max (FU_DEVICE (self)));
return FALSE;
}
/* success */
return TRUE;
#else
g_set_error (error,
FWUPD_ERROR,
FWUPD_ERROR_NOT_SUPPORTED,
"Not supported as <linux/ethtool.h> not found");
return FALSE;
#endif
}
static gboolean
fu_bcm57xx_device_activate (FuDevice *device, GError **error)
{
FuBcm57xxDevice *self = FU_BCM57XX_DEVICE (device);
g_autoptr(FuDeviceLocker) locker1 = NULL;
g_autoptr(FuDeviceLocker) locker2 = NULL;
/* the only way to do this is using the mmap method */
locker2 = fu_device_locker_new_full (FU_DEVICE (self->recovery),
(FuDeviceLockerFunc) fu_device_detach,
(FuDeviceLockerFunc) fu_device_attach,
error);
if (locker2 == NULL)
return FALSE;
/* open */
locker1 = fu_device_locker_new (FU_DEVICE (self->recovery), error);
if (locker1 == NULL)
return FALSE;
/* activate, causing APE reset, then close, then attach */
if (!fu_device_activate (FU_DEVICE (self->recovery), error))
return FALSE;
/* ensure we attach before we close */
if (!fu_device_locker_close (locker2, error))
return FALSE;
/* wait for the device to restart before calling reload() */
fu_device_set_status (device, FWUPD_STATUS_DEVICE_BUSY);
fu_device_sleep_with_progress (device, 5); /* seconds */
return TRUE;
}
static GBytes *
fu_bcm57xx_device_dump_firmware (FuDevice *device, GError **error)
{
FuBcm57xxDevice *self = FU_BCM57XX_DEVICE (device);
const gsize bufsz = fu_device_get_firmware_size_max (FU_DEVICE (self));
g_autofree guint8 *buf = g_malloc0 (bufsz);
g_autoptr(GPtrArray) chunks = NULL;
fu_device_set_status (device, FWUPD_STATUS_DEVICE_READ);
chunks = fu_chunk_array_new (buf, bufsz, 0x0, 0x0, FU_BCM57XX_BLOCK_SZ);
for (guint i = 0; i < chunks->len; i++) {
FuChunk *chk = g_ptr_array_index (chunks, i);
if (!fu_bcm57xx_device_nvram_read (self, chk->address,
(guint8 *) chk->data, chk->data_sz,
error))
return NULL;
fu_device_set_progress_full (device, i, chunks->len - 1);
}
/* read from hardware */
return g_bytes_new_take (g_steal_pointer (&buf), bufsz);
}
static FuFirmware *
fu_bcm57xx_device_read_firmware (FuDevice *device, GError **error)
{
g_autoptr(FuFirmware) firmware = fu_bcm57xx_firmware_new ();
g_autoptr(GBytes) fw = NULL;
/* read from hardware */
fw = fu_bcm57xx_device_dump_firmware (device, error);
if (fw == NULL)
return NULL;
if (!fu_firmware_parse (firmware, fw, FWUPD_INSTALL_FLAG_NONE, error))
return NULL;
/* remove images that will contain user-data */
if (!fu_firmware_remove_image_by_id (firmware, "info", error))
return NULL;
if (!fu_firmware_remove_image_by_id (firmware, "info2", error))
return NULL;
if (!fu_firmware_remove_image_by_id (firmware, "vpd", error))
return NULL;
return g_steal_pointer (&firmware);
}
static FuFirmware *
fu_bcm57xx_device_prepare_firmware (FuDevice *device,
GBytes *fw,
FwupdInstallFlags flags,
GError **error)
{
guint dict_cnt = 0;
g_autoptr(GBytes) fw_old = NULL;
g_autoptr(FuFirmware) firmware = fu_bcm57xx_firmware_new ();
g_autoptr(FuFirmware) firmware_tmp = fu_bcm57xx_firmware_new ();
g_autoptr(FuFirmwareImage) img_ape = NULL;
g_autoptr(FuFirmwareImage) img_stage1 = NULL;
g_autoptr(FuFirmwareImage) img_stage2 = NULL;
g_autoptr(GPtrArray) images = NULL;
/* try to parse NVRAM, stage1 or APE */
if (!fu_firmware_parse (firmware_tmp, fw, flags, error)) {
g_prefix_error (error, "failed to parse new firmware: ");
return NULL;
}
/* for full NVRAM image, verify if correct device */
if ((flags & FWUPD_INSTALL_FLAG_IGNORE_VID_PID) == 0) {
guint16 vid = fu_bcm57xx_firmware_get_vendor (FU_BCM57XX_FIRMWARE (firmware_tmp));
guint16 did = fu_bcm57xx_firmware_get_model (FU_BCM57XX_FIRMWARE (firmware_tmp));
if (vid != 0x0 && did != 0x0 &&
(fu_udev_device_get_vendor (FU_UDEV_DEVICE (device)) != vid ||
fu_udev_device_get_model (FU_UDEV_DEVICE (device)) != did)) {
g_set_error (error,
FWUPD_ERROR,
FWUPD_ERROR_NOT_SUPPORTED,
"PCI vendor or model incorrect, "
"got: %04X:%04X expected %04X:%04X",
vid, did,
fu_udev_device_get_vendor (FU_UDEV_DEVICE (device)),
fu_udev_device_get_model (FU_UDEV_DEVICE (device)));
return NULL;
}
}
/* get the existing firmware from the device */
fw_old = fu_bcm57xx_device_dump_firmware (device, error);
if (fw_old == NULL)
return NULL;
if (!fu_firmware_parse (firmware, fw_old, flags, error)) {
g_prefix_error (error, "failed to parse existing firmware: ");
return NULL;
}
if (g_getenv ("FWUPD_BCM57XX_VERBOSE") != NULL) {
g_autofree gchar *str = fu_firmware_to_string (firmware);
g_debug ("existing device firmware: %s", str);
}
/* merge in all the provided images into the existing firmware */
img_stage1 = fu_firmware_get_image_by_id (firmware_tmp, "stage1", NULL);
if (img_stage1 != NULL)
fu_firmware_add_image (firmware, img_stage1);
img_stage2 = fu_firmware_get_image_by_id (firmware_tmp, "stage2", NULL);
if (img_stage2 != NULL)
fu_firmware_add_image (firmware, img_stage2);
img_ape = fu_firmware_get_image_by_id (firmware_tmp, "ape", NULL);
if (img_ape != NULL)
fu_firmware_add_image (firmware, img_ape);
/* the src and dst dictionaries may be in different order */
images = fu_firmware_get_images (firmware);
for (guint i = 0; i < images->len; i++) {
FuFirmwareImage *img = g_ptr_array_index (images, i);
if (FU_IS_BCM57XX_DICT_IMAGE (img)) {
fu_firmware_image_set_idx (img, 0x80 + dict_cnt);
dict_cnt++;
}
}
if (g_getenv ("FWUPD_BCM57XX_VERBOSE") != NULL) {
g_autofree gchar *str = fu_firmware_to_string (firmware);
g_debug ("proposed device firmware: %s", str);
}
/* success */
return g_steal_pointer (&firmware);
}
static gboolean
fu_bcm57xx_device_write_firmware (FuDevice *device,
FuFirmware *firmware,
FwupdInstallFlags flags,
GError **error)
{
FuBcm57xxDevice *self = FU_BCM57XX_DEVICE (device);
g_autoptr(GBytes) blob = NULL;
g_autoptr(GBytes) blob_verify = NULL;
g_autoptr(GPtrArray) chunks = NULL;
/* build the images into one linear blob of the correct size */
fu_device_set_status (device, FWUPD_STATUS_DECOMPRESSING);
blob = fu_firmware_write (firmware, error);
if (blob == NULL)
return FALSE;
/* hit hardware */
fu_device_set_status (device, FWUPD_STATUS_DEVICE_WRITE);
chunks = fu_chunk_array_new_from_bytes (blob, 0x0, 0x0, FU_BCM57XX_BLOCK_SZ);
for (guint i = 0; i < chunks->len; i++) {
FuChunk *chk = g_ptr_array_index (chunks, i);
if (!fu_bcm57xx_device_nvram_write (self, chk->address,
chk->data, chk->data_sz,
error))
return FALSE;
fu_device_set_progress_full (device, i, chunks->len - 1);
}
/* verify */
fu_device_set_status (device, FWUPD_STATUS_DEVICE_VERIFY);
blob_verify = fu_bcm57xx_device_dump_firmware (device, error);
if (blob_verify == NULL)
return FALSE;
if (!fu_common_bytes_compare (blob, blob_verify, error))
return FALSE;
/* reset APE */
return fu_device_activate (device, error);
}
static gboolean
fu_bcm57xx_device_setup (FuDevice *device, GError **error)
{
FuBcm57xxDevice *self = FU_BCM57XX_DEVICE (device);
guint32 fwversion = 0;
/* device is in recovery mode */
if (self->ethtool_iface == NULL) {
g_autoptr(FuDeviceLocker) locker = NULL;
g_debug ("device in recovery mode, use alternate device");
locker = fu_device_locker_new (FU_DEVICE (self->recovery), error);
if (locker == NULL)
return FALSE;
return fu_device_setup (FU_DEVICE (self->recovery), error);
}
/* check the EEPROM size */
if (!fu_bcm57xx_device_nvram_check (self, error))
return FALSE;
/* get NVRAM version */
if (!fu_bcm57xx_device_nvram_read (self, BCM_NVRAM_STAGE1_BASE + BCM_NVRAM_STAGE1_VERSION,
(guint8 *) &fwversion, sizeof(guint32), error))
return FALSE;
if (fwversion != 0x0) {
g_autofree gchar *fwversion_str = NULL;
/* this is only set on the OSS firmware */
fwversion_str = fu_common_version_from_uint32 (GUINT32_FROM_BE(fwversion),
FWUPD_VERSION_FORMAT_TRIPLET);
fu_device_set_version_format (device, FWUPD_VERSION_FORMAT_TRIPLET);
fu_device_set_version (device, fwversion_str);
fu_device_set_version_raw (device, fwversion);
fu_device_set_branch (device, BCM_FW_BRANCH_OSS_FIRMWARE);
} else {
guint8 bufver[16] = { 0x0 };
guint32 veraddr = 0;
g_autoptr(Bcm57xxVeritem) veritem = NULL;
/* fall back to the string, e.g. '5719-v1.43' */
if (!fu_bcm57xx_device_nvram_read (self,
BCM_NVRAM_STAGE1_BASE + BCM_NVRAM_STAGE1_VERADDR,
(guint8 *) &veraddr, sizeof(guint32), error))
return FALSE;
veraddr = GUINT32_FROM_BE(veraddr);
if (veraddr > BCM_PHYS_ADDR_DEFAULT)
veraddr -= BCM_PHYS_ADDR_DEFAULT;
if (!fu_bcm57xx_device_nvram_read (self,
BCM_NVRAM_STAGE1_BASE + veraddr,
bufver, sizeof(bufver), error))
return FALSE;
veritem = fu_bcm57xx_veritem_new (bufver, sizeof(bufver));
if (veritem != NULL) {
fu_device_set_version_format (device, veritem->verfmt);
fu_device_set_version (device, veritem->version);
fu_device_set_branch (device, veritem->branch);
}
}
/* success */
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_flag (FU_DEVICE (self), FWUPD_DEVICE_FLAG_NEEDS_REBOOT);
fu_device_add_flag (FU_DEVICE (self), FWUPD_DEVICE_FLAG_BACKUP_BEFORE_INSTALL);
return TRUE;
}
static gboolean
fu_bcm57xx_device_open (FuDevice *device, GError **error)
{
#ifdef HAVE_SOCKET_H
FuBcm57xxDevice *self = FU_BCM57XX_DEVICE (device);
self->ethtool_fd = socket (AF_INET, SOCK_DGRAM, 0);
if (self->ethtool_fd < 0) {
g_set_error (error,
G_IO_ERROR,
G_IO_ERROR_NOT_SUPPORTED,
"failed to open socket: %s",
#ifdef HAVE_ERRNO_H
strerror (errno));
#else
"unspecified error");
#endif
return FALSE;
}
return TRUE;
#else
g_set_error_literal (error,
G_IO_ERROR,
G_IO_ERROR_NOT_SUPPORTED,
"socket() not supported as sys/socket.h not available");
return FALSE;
#endif
}
static gboolean
fu_bcm57xx_device_close (FuDevice *device, GError **error)
{
FuBcm57xxDevice *self = FU_BCM57XX_DEVICE (device);
return g_close (self->ethtool_fd, error);
}
static void
fu_bcm57xx_device_init (FuBcm57xxDevice *self)
{
fu_device_add_flag (FU_DEVICE (self), FWUPD_DEVICE_FLAG_NO_GUID_MATCHING);
fu_device_set_protocol (FU_DEVICE (self), "com.broadcom.bcm57xx");
fu_device_add_icon (FU_DEVICE (self), "network-wired");
/* other values are set from a quirk */
fu_device_set_firmware_size (FU_DEVICE (self), BCM_FIRMWARE_SIZE);
/* used for recovery in case of ethtool failure and for APE reset */
self->recovery = fu_bcm57xx_recovery_device_new ();
}
static void
fu_bcm57xx_device_finalize (GObject *object)
{
FuBcm57xxDevice *self= FU_BCM57XX_DEVICE (object);
g_free (self->ethtool_iface);
G_OBJECT_CLASS (fu_bcm57xx_device_parent_class)->finalize (object);
}
static void
fu_bcm57xx_device_class_init (FuBcm57xxDeviceClass *klass)
{
GObjectClass *object_class = G_OBJECT_CLASS (klass);
FuDeviceClass *klass_device = FU_DEVICE_CLASS (klass);
FuUdevDeviceClass *klass_udev_device = FU_UDEV_DEVICE_CLASS (klass);
object_class->finalize = fu_bcm57xx_device_finalize;
klass_device->prepare_firmware = fu_bcm57xx_device_prepare_firmware;
klass_device->setup = fu_bcm57xx_device_setup;
klass_device->reload = fu_bcm57xx_device_setup;
klass_device->open = fu_bcm57xx_device_open;
klass_device->close = fu_bcm57xx_device_close;
klass_device->activate = fu_bcm57xx_device_activate;
klass_device->write_firmware = fu_bcm57xx_device_write_firmware;
klass_device->read_firmware = fu_bcm57xx_device_read_firmware;
klass_device->dump_firmware = fu_bcm57xx_device_dump_firmware;
klass_udev_device->probe = fu_bcm57xx_device_probe;
klass_udev_device->to_string = fu_bcm57xx_device_to_string;
}