/* * Copyright (C) 2020 Richard Hughes * Copyright (c) 2020 Synaptics Incorporated. * Copyright (C) 2012 Andrew Duggan * * SPDX-License-Identifier: LGPL-2.1+ */ #include "config.h" #include #include #include "fu-io-channel.h" #include "fu-synaptics-rmi-hid-device.h" #include "fu-synaptics-rmi-v5-device.h" #include "fu-synaptics-rmi-v7-device.h" struct _FuSynapticsRmiHidDevice { FuSynapticsRmiDevice parent_instance; FuIOChannel *io_channel; }; G_DEFINE_TYPE (FuSynapticsRmiHidDevice, fu_synaptics_rmi_hid_device, FU_TYPE_SYNAPTICS_RMI_DEVICE) #define RMI_WRITE_REPORT_ID 0x9 /* output report */ #define RMI_READ_ADDR_REPORT_ID 0xa /* output report */ #define RMI_READ_DATA_REPORT_ID 0xb /* input report */ #define RMI_ATTN_REPORT_ID 0xc /* input report */ #define RMI_SET_RMI_MODE_REPORT_ID 0xf /* feature report */ #define RMI_DEVICE_DEFAULT_TIMEOUT 2000 #define HID_RMI4_REPORT_ID 0 #define HID_RMI4_READ_INPUT_COUNT 1 #define HID_RMI4_READ_INPUT_DATA 2 #define HID_RMI4_READ_OUTPUT_ADDR 2 #define HID_RMI4_READ_OUTPUT_COUNT 4 #define HID_RMI4_WRITE_OUTPUT_COUNT 1 #define HID_RMI4_WRITE_OUTPUT_ADDR 2 #define HID_RMI4_WRITE_OUTPUT_DATA 4 #define HID_RMI4_FEATURE_MODE 1 #define HID_RMI4_ATTN_INTERUPT_SOURCES 1 #define HID_RMI4_ATTN_DATA 2 /* * This bit disables whatever sleep mode may be selected by the sleep_mode * field and forces the device to run at full power without sleeping. */ #define RMI_F01_CRTL0_NOSLEEP_BIT (1 << 2) /* * msleep mode controls power management on the device and affects all * functions of the device. */ #define RMI_F01_CTRL0_SLEEP_MODE_MASK 0x03 #define RMI_SLEEP_MODE_NORMAL 0x00 #define RMI_SLEEP_MODE_SENSOR_SLEEP 0x01 static GByteArray * fu_synaptics_rmi_hid_device_read (FuSynapticsRmiDevice *rmi_device, guint16 addr, gsize req_sz, GError **error) { FuSynapticsRmiHidDevice *self = FU_SYNAPTICS_RMI_HID_DEVICE (rmi_device); g_autoptr(GByteArray) buf = g_byte_array_new (); g_autoptr(GByteArray) req = g_byte_array_new (); /* maximum size */ if (req_sz > 0xffff) { g_set_error_literal (error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "data to read was too long"); return NULL; } /* report then old 1 byte read count */ fu_byte_array_append_uint8 (req, RMI_READ_ADDR_REPORT_ID); fu_byte_array_append_uint8 (req, 0x0); /* address */ fu_byte_array_append_uint16 (req, addr, G_LITTLE_ENDIAN); /* read output count */ fu_byte_array_append_uint16 (req, req_sz, G_LITTLE_ENDIAN); /* request */ for (guint j = req->len; j < 21; j++) fu_byte_array_append_uint8 (req, 0x0); if (g_getenv ("FWUPD_SYNAPTICS_RMI_VERBOSE") != NULL) { fu_common_dump_full (G_LOG_DOMAIN, "ReportWrite", req->data, req->len, 80, FU_DUMP_FLAGS_NONE); } if (!fu_io_channel_write_byte_array (self->io_channel, req, RMI_DEVICE_DEFAULT_TIMEOUT, FU_IO_CHANNEL_FLAG_SINGLE_SHOT | FU_IO_CHANNEL_FLAG_USE_BLOCKING_IO, error)) return NULL; /* keep reading responses until we get enough data */ while (buf->len < req_sz) { guint8 input_count_sz = 0; g_autoptr(GByteArray) res = NULL; res = fu_io_channel_read_byte_array (self->io_channel, req_sz, RMI_DEVICE_DEFAULT_TIMEOUT, FU_IO_CHANNEL_FLAG_SINGLE_SHOT, error); if (res == NULL) return NULL; if (res->len == 0) { g_set_error_literal (error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "response zero sized"); return NULL; } if (g_getenv ("FWUPD_SYNAPTICS_RMI_VERBOSE") != NULL) { fu_common_dump_full (G_LOG_DOMAIN, "ReportRead", res->data, res->len, 80, FU_DUMP_FLAGS_NONE); } /* ignore non data report events */ if (res->data[HID_RMI4_REPORT_ID] != RMI_READ_DATA_REPORT_ID) { g_debug ("ignoring report with ID 0x%02x", res->data[HID_RMI4_REPORT_ID]); continue; } if (res->len < HID_RMI4_READ_INPUT_DATA) { g_set_error (error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "response too small: 0x%02x", res->len); return NULL; } input_count_sz = res->data[HID_RMI4_READ_INPUT_COUNT]; if (input_count_sz == 0) { g_set_error_literal (error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "input count zero"); return NULL; } if (input_count_sz + (guint) HID_RMI4_READ_INPUT_DATA > res->len) { g_set_error (error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "underflow 0x%02x from expected 0x%02x", res->len, (guint) input_count_sz + HID_RMI4_READ_INPUT_DATA); return NULL; } g_byte_array_append (buf, res->data + HID_RMI4_READ_INPUT_DATA, input_count_sz); } if (g_getenv ("FWUPD_SYNAPTICS_RMI_VERBOSE") != NULL) { fu_common_dump_full (G_LOG_DOMAIN, "DeviceRead", buf->data, buf->len, 80, FU_DUMP_FLAGS_NONE); } return g_steal_pointer (&buf); } static GByteArray * fu_synaptics_rmi_hid_device_read_packet_register (FuSynapticsRmiDevice *rmi_device, guint16 addr, gsize req_sz, GError **error) { return fu_synaptics_rmi_hid_device_read (rmi_device, addr, req_sz, error); } static gboolean fu_synaptics_rmi_hid_device_write (FuSynapticsRmiDevice *rmi_device, guint16 addr, GByteArray *req, FuSynapticsRmiDeviceFlags flags, GError **error) { FuSynapticsRmiHidDevice *self = FU_SYNAPTICS_RMI_HID_DEVICE (rmi_device); guint8 len = 0x0; g_autoptr(GByteArray) buf = g_byte_array_new (); /* check size */ if (req != NULL) { if (req->len > 0xff) { g_set_error_literal (error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "data to write was too long"); return FALSE; } len = req->len; } /* report */ fu_byte_array_append_uint8 (buf, RMI_WRITE_REPORT_ID); /* length */ fu_byte_array_append_uint8 (buf, len); /* address */ fu_byte_array_append_uint16 (buf, addr, G_LITTLE_ENDIAN); /* optional data */ if (req != NULL) g_byte_array_append (buf, req->data, req->len); /* pad out to 21 bytes for some reason */ for (guint i = buf->len; i < 21; i++) fu_byte_array_append_uint8 (buf, 0x0); if (g_getenv ("FWUPD_SYNAPTICS_RMI_VERBOSE") != NULL) { fu_common_dump_full (G_LOG_DOMAIN, "DeviceWrite", buf->data, buf->len, 80, FU_DUMP_FLAGS_NONE); } return fu_io_channel_write_byte_array (self->io_channel, buf, RMI_DEVICE_DEFAULT_TIMEOUT, FU_IO_CHANNEL_FLAG_SINGLE_SHOT | FU_IO_CHANNEL_FLAG_USE_BLOCKING_IO, error); } static gboolean fu_synaptics_rmi_hid_device_wait_for_attr (FuSynapticsRmiDevice *rmi_device, guint8 source_mask, guint timeout_ms, GError **error) { FuSynapticsRmiHidDevice *self = FU_SYNAPTICS_RMI_HID_DEVICE (rmi_device); g_autoptr(GTimer) timer = g_timer_new (); /* wait for event from hardware */ while (g_timer_elapsed (timer, NULL) * 1000.f < timeout_ms) { g_autoptr(GByteArray) res = NULL; g_autoptr(GError) error_local = NULL; /* read from fd */ res = fu_io_channel_read_byte_array (self->io_channel, HID_RMI4_ATTN_INTERUPT_SOURCES + 1, timeout_ms, FU_IO_CHANNEL_FLAG_NONE, &error_local); if (res == NULL) { if (g_error_matches (error_local, G_IO_ERROR, G_IO_ERROR_TIMED_OUT)) break; g_propagate_error (error, g_steal_pointer (&error_local)); return FALSE; } if (g_getenv ("FWUPD_SYNAPTICS_RMI_VERBOSE") != NULL) { fu_common_dump_full (G_LOG_DOMAIN, "ReportRead", res->data, res->len, 80, FU_DUMP_FLAGS_NONE); } if (res->len < HID_RMI4_ATTN_INTERUPT_SOURCES + 1) { g_debug ("attr: ignoring small read of %u", res->len); continue; } if (res->data[HID_RMI4_REPORT_ID] != RMI_ATTN_REPORT_ID) { g_debug ("attr: ignoring invalid report ID 0x%x", res->data[HID_RMI4_REPORT_ID]); continue; } /* success */ if (source_mask & res->data[HID_RMI4_ATTN_INTERUPT_SOURCES]) return TRUE; /* wrong mask */ g_debug ("source mask did not match: 0x%x", res->data[HID_RMI4_ATTN_INTERUPT_SOURCES]); } /* urgh */ g_set_error_literal (error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "no attr report, timed out"); return FALSE; } typedef enum { HID_RMI4_MODE_MOUSE = 0, HID_RMI4_MODE_ATTN_REPORTS = 1, HID_RMI4_MODE_NO_PACKED_ATTN_REPORTS = 2, } FuSynapticsRmiHidMode; static gboolean fu_synaptics_rmi_hid_device_set_mode (FuSynapticsRmiHidDevice *self, FuSynapticsRmiHidMode mode, GError **error) { const guint8 data[] = { 0x0f, mode }; if (g_getenv ("FWUPD_SYNAPTICS_RMI_VERBOSE") != NULL) fu_common_dump_raw (G_LOG_DOMAIN, "SetMode", data, sizeof(data)); return fu_udev_device_ioctl (FU_UDEV_DEVICE (self), HIDIOCSFEATURE(sizeof(data)), (guint8 *) data, NULL, error); } static gboolean fu_synaptics_rmi_hid_device_open (FuDevice *device, GError **error) { FuSynapticsRmiHidDevice *self = FU_SYNAPTICS_RMI_HID_DEVICE (device); /* FuUdevDevice->open */ if (!FU_DEVICE_CLASS (fu_synaptics_rmi_hid_device_parent_class)->open (device, error)) return FALSE; /* set up touchpad so we can query it */ self->io_channel = fu_io_channel_unix_new (fu_udev_device_get_fd (FU_UDEV_DEVICE (device))); if (!fu_synaptics_rmi_hid_device_set_mode (self, HID_RMI4_MODE_ATTN_REPORTS, error)) return FALSE; /* success */ return TRUE; } static gboolean fu_synaptics_rmi_hid_device_close (FuDevice *device, GError **error) { FuSynapticsRmiHidDevice *self = FU_SYNAPTICS_RMI_HID_DEVICE (device); g_autoptr(GError) error_local = NULL; /* turn it back to mouse mode */ if (!fu_synaptics_rmi_hid_device_set_mode (self, HID_RMI4_MODE_MOUSE, &error_local)) { /* if just detached for replug, swallow error */ if (!g_error_matches (error_local, FWUPD_ERROR, FWUPD_ERROR_PERMISSION_DENIED)) { g_propagate_error (error, g_steal_pointer (&error_local)); return FALSE; } g_debug ("ignoring: %s", error_local->message); } fu_udev_device_set_fd (FU_UDEV_DEVICE (device), -1); g_clear_object (&self->io_channel); /* FuUdevDevice->close */ return FU_DEVICE_CLASS (fu_synaptics_rmi_hid_device_parent_class)->close (device, error); } static gboolean fu_synaptics_rmi_hid_device_rebind_driver (FuSynapticsRmiDevice *self, GError **error) { GUdevDevice *udev_device = fu_udev_device_get_dev (FU_UDEV_DEVICE (self)); const gchar *hid_id; const gchar *driver; const gchar *subsystem; g_autofree gchar *fn_rebind = NULL; g_autofree gchar *fn_unbind = NULL; g_autoptr(GUdevDevice) parent_hid = NULL; g_autoptr(GUdevDevice) parent_i2c = NULL; /* get actual HID node */ parent_hid = g_udev_device_get_parent_with_subsystem (udev_device, "hid", NULL); if (parent_hid == NULL) { g_set_error (error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "no HID parent device for %s", g_udev_device_get_sysfs_path (udev_device)); return FALSE; } /* find the physical ID to use for the rebind */ hid_id = g_udev_device_get_property (parent_hid, "HID_PHYS"); if (hid_id == NULL) { g_set_error (error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "no HID_PHYS in %s", g_udev_device_get_sysfs_path (parent_hid)); return FALSE; } g_debug ("HID_PHYS: %s", hid_id); /* build paths */ parent_i2c = g_udev_device_get_parent_with_subsystem (udev_device, "i2c", NULL); if (parent_i2c == NULL) { g_set_error (error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "no I2C parent device for %s", g_udev_device_get_sysfs_path (udev_device)); return FALSE; } driver = g_udev_device_get_driver (parent_i2c); subsystem = g_udev_device_get_subsystem (parent_i2c); fn_rebind = g_build_filename ("/sys/bus/", subsystem, "drivers", driver, "bind", NULL); fn_unbind = g_build_filename ("/sys/bus/", subsystem, "drivers", driver, "unbind", NULL); /* unbind hidraw, then bind it again to get a replug */ fu_device_add_flag (FU_DEVICE (self), FWUPD_DEVICE_FLAG_WAIT_FOR_REPLUG); if (!fu_synaptics_rmi_device_writeln (fn_unbind, hid_id, error)) return FALSE; if (!fu_synaptics_rmi_device_writeln (fn_rebind, hid_id, error)) return FALSE; /* success */ return TRUE; } static gboolean fu_synaptics_rmi_hid_device_detach (FuDevice *device, GError **error) { FuSynapticsRmiDevice *self = FU_SYNAPTICS_RMI_DEVICE (device); FuSynapticsRmiFunction *f34; f34 = fu_synaptics_rmi_device_get_function (self, 0x34, error); if (f34 == NULL) return FALSE; if (f34->function_version == 0x0 || f34->function_version == 0x1) { if (!fu_synaptics_rmi_v5_device_detach (device, error)) return FALSE; } else if (f34->function_version == 0x2) { if (!fu_synaptics_rmi_v7_device_detach (device, error)) return FALSE; } else { g_set_error (error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "f34 function version 0x%02x unsupported", f34->function_version); return FALSE; } return fu_synaptics_rmi_hid_device_rebind_driver (self, error); } static gboolean fu_synaptics_rmi_hid_device_attach (FuDevice *device, GError **error) { FuSynapticsRmiDevice *self = FU_SYNAPTICS_RMI_DEVICE (device); /* sanity check */ if (!fu_device_has_flag (device, FWUPD_DEVICE_FLAG_IS_BOOTLOADER)) { g_debug ("already in runtime mode, skipping"); return TRUE; } /* reset device */ if (!fu_synaptics_rmi_device_reset (self, error)) return FALSE; /* rebind to rescan PDT with new firmware running */ fu_device_set_status (device, FWUPD_STATUS_DEVICE_RESTART); return fu_synaptics_rmi_hid_device_rebind_driver (self, error); } static gboolean fu_synaptics_rmi_hid_device_set_page (FuSynapticsRmiDevice *self, guint8 page, GError **error) { g_autoptr(GByteArray) req = g_byte_array_new (); fu_byte_array_append_uint8 (req, page); if (!fu_synaptics_rmi_device_write (self, RMI_DEVICE_PAGE_SELECT_REGISTER, req, FU_SYNAPTICS_RMI_DEVICE_FLAG_NONE, error)) { g_prefix_error (error, "failed to set RMA page 0x%x: ", page); return FALSE; } return TRUE; } static gboolean fu_synaptics_rmi_hid_device_probe (FuDevice *device, GError **error) { /* FuUdevDevice->probe */ if (!FU_DEVICE_CLASS (fu_synaptics_rmi_hid_device_parent_class)->probe (device, error)) return FALSE; return fu_udev_device_set_physical_id (FU_UDEV_DEVICE (device), "hid", error); } static gboolean fu_synaptics_rmi_hid_device_disable_sleep (FuSynapticsRmiDevice *rmi_device, GError **error) { FuSynapticsRmiFunction *f01; g_autoptr(GByteArray) f01_control0 = NULL; f01 = fu_synaptics_rmi_device_get_function (rmi_device, 0x34, error); if (f01 == NULL) return FALSE; f01_control0 = fu_synaptics_rmi_device_read (rmi_device, f01->control_base, 0x1, error); if (f01_control0 == NULL) { g_prefix_error (error, "failed to write get f01_control0: "); return FALSE; } f01_control0->data[0] |= RMI_F01_CRTL0_NOSLEEP_BIT; f01_control0->data[0] = (f01_control0->data[0] & ~RMI_F01_CTRL0_SLEEP_MODE_MASK) | RMI_SLEEP_MODE_NORMAL; if (!fu_synaptics_rmi_device_write (rmi_device, f01->control_base, f01_control0, FU_SYNAPTICS_RMI_DEVICE_FLAG_NONE, error)) { g_prefix_error (error, "failed to write f01_control0: "); return FALSE; } /* success */ return TRUE; } static gboolean fu_synaptics_rmi_hid_device_query_status (FuSynapticsRmiDevice *rmi_device, GError **error) { FuSynapticsRmiFunction *f34; f34 = fu_synaptics_rmi_device_get_function (rmi_device, 0x34, error); if (f34 == NULL) return FALSE; if (f34->function_version == 0x0 || f34->function_version == 0x1) { return fu_synaptics_rmi_v5_device_query_status (rmi_device, error); } if (f34->function_version == 0x2) { return fu_synaptics_rmi_v7_device_query_status (rmi_device, error); } g_set_error (error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "f34 function version 0x%02x unsupported", f34->function_version); return FALSE; } static void fu_synaptics_rmi_hid_device_init (FuSynapticsRmiHidDevice *self) { fu_device_set_name (FU_DEVICE (self), "Touchpad"); fu_device_set_remove_delay (FU_DEVICE (self), FU_DEVICE_REMOVE_DELAY_RE_ENUMERATE); fu_synaptics_rmi_device_set_max_page (FU_SYNAPTICS_RMI_DEVICE (self), 0xff); } static void fu_synaptics_rmi_hid_device_class_init (FuSynapticsRmiHidDeviceClass *klass) { FuDeviceClass *klass_device = FU_DEVICE_CLASS (klass); FuSynapticsRmiDeviceClass *klass_rmi = FU_SYNAPTICS_RMI_DEVICE_CLASS (klass); klass_device->attach = fu_synaptics_rmi_hid_device_attach; klass_device->detach = fu_synaptics_rmi_hid_device_detach; klass_device->probe = fu_synaptics_rmi_hid_device_probe; klass_device->open = fu_synaptics_rmi_hid_device_open; klass_device->close = fu_synaptics_rmi_hid_device_close; klass_rmi->write = fu_synaptics_rmi_hid_device_write; klass_rmi->read = fu_synaptics_rmi_hid_device_read; klass_rmi->wait_for_attr = fu_synaptics_rmi_hid_device_wait_for_attr; klass_rmi->set_page = fu_synaptics_rmi_hid_device_set_page; klass_rmi->query_status = fu_synaptics_rmi_hid_device_query_status; klass_rmi->read_packet_register = fu_synaptics_rmi_hid_device_read_packet_register; klass_rmi->disable_sleep = fu_synaptics_rmi_hid_device_disable_sleep; }