]> git.proxmox.com Git - mirror_ubuntu-artful-kernel.git/blobdiff - drivers/hid/hid-logitech-hidpp.c
HID: logitech-hidpp: limit visibility of init/deinit functions
[mirror_ubuntu-artful-kernel.git] / drivers / hid / hid-logitech-hidpp.c
index bd2ab476c65ef9f73a04a1f732dc7f75a3b49835..2e2515a4c070eac305a834c9229d0e1fa27f722f 100644 (file)
 #define pr_fmt(fmt) KBUILD_MODNAME ": " fmt
 
 #include <linux/device.h>
+#include <linux/input.h>
+#include <linux/usb.h>
 #include <linux/hid.h>
 #include <linux/module.h>
 #include <linux/slab.h>
 #include <linux/sched.h>
 #include <linux/kfifo.h>
 #include <linux/input/mt.h>
+#include <linux/workqueue.h>
+#include <linux/atomic.h>
+#include <linux/fixp-arith.h>
 #include <asm/unaligned.h>
+#include "usbhid/usbhid.h"
 #include "hid-ids.h"
 
 MODULE_LICENSE("GPL");
@@ -773,6 +779,589 @@ static void hidpp_touchpad_raw_xy_event(struct hidpp_device *hidpp_dev,
        }
 }
 
+/* -------------------------------------------------------------------------- */
+/* 0x8123: Force feedback support                                             */
+/* -------------------------------------------------------------------------- */
+
+#define HIDPP_FF_GET_INFO              0x01
+#define HIDPP_FF_RESET_ALL             0x11
+#define HIDPP_FF_DOWNLOAD_EFFECT       0x21
+#define HIDPP_FF_SET_EFFECT_STATE      0x31
+#define HIDPP_FF_DESTROY_EFFECT                0x41
+#define HIDPP_FF_GET_APERTURE          0x51
+#define HIDPP_FF_SET_APERTURE          0x61
+#define HIDPP_FF_GET_GLOBAL_GAINS      0x71
+#define HIDPP_FF_SET_GLOBAL_GAINS      0x81
+
+#define HIDPP_FF_EFFECT_STATE_GET      0x00
+#define HIDPP_FF_EFFECT_STATE_STOP     0x01
+#define HIDPP_FF_EFFECT_STATE_PLAY     0x02
+#define HIDPP_FF_EFFECT_STATE_PAUSE    0x03
+
+#define HIDPP_FF_EFFECT_CONSTANT       0x00
+#define HIDPP_FF_EFFECT_PERIODIC_SINE          0x01
+#define HIDPP_FF_EFFECT_PERIODIC_SQUARE                0x02
+#define HIDPP_FF_EFFECT_PERIODIC_TRIANGLE      0x03
+#define HIDPP_FF_EFFECT_PERIODIC_SAWTOOTHUP    0x04
+#define HIDPP_FF_EFFECT_PERIODIC_SAWTOOTHDOWN  0x05
+#define HIDPP_FF_EFFECT_SPRING         0x06
+#define HIDPP_FF_EFFECT_DAMPER         0x07
+#define HIDPP_FF_EFFECT_FRICTION       0x08
+#define HIDPP_FF_EFFECT_INERTIA                0x09
+#define HIDPP_FF_EFFECT_RAMP           0x0A
+
+#define HIDPP_FF_EFFECT_AUTOSTART      0x80
+
+#define HIDPP_FF_EFFECTID_NONE         -1
+#define HIDPP_FF_EFFECTID_AUTOCENTER   -2
+
+#define HIDPP_FF_MAX_PARAMS    20
+#define HIDPP_FF_RESERVED_SLOTS        1
+
+struct hidpp_ff_private_data {
+       struct hidpp_device *hidpp;
+       u8 feature_index;
+       u8 version;
+       u16 gain;
+       s16 range;
+       u8 slot_autocenter;
+       u8 num_effects;
+       int *effect_ids;
+       struct workqueue_struct *wq;
+       atomic_t workqueue_size;
+};
+
+struct hidpp_ff_work_data {
+       struct work_struct work;
+       struct hidpp_ff_private_data *data;
+       int effect_id;
+       u8 command;
+       u8 params[HIDPP_FF_MAX_PARAMS];
+       u8 size;
+};
+
+static const signed short hiddpp_ff_effects[] = {
+       FF_CONSTANT,
+       FF_PERIODIC,
+       FF_SINE,
+       FF_SQUARE,
+       FF_SAW_UP,
+       FF_SAW_DOWN,
+       FF_TRIANGLE,
+       FF_SPRING,
+       FF_DAMPER,
+       FF_AUTOCENTER,
+       FF_GAIN,
+       -1
+};
+
+static const signed short hiddpp_ff_effects_v2[] = {
+       FF_RAMP,
+       FF_FRICTION,
+       FF_INERTIA,
+       -1
+};
+
+static const u8 HIDPP_FF_CONDITION_CMDS[] = {
+       HIDPP_FF_EFFECT_SPRING,
+       HIDPP_FF_EFFECT_FRICTION,
+       HIDPP_FF_EFFECT_DAMPER,
+       HIDPP_FF_EFFECT_INERTIA
+};
+
+static const char *HIDPP_FF_CONDITION_NAMES[] = {
+       "spring",
+       "friction",
+       "damper",
+       "inertia"
+};
+
+
+static u8 hidpp_ff_find_effect(struct hidpp_ff_private_data *data, int effect_id)
+{
+       int i;
+
+       for (i = 0; i < data->num_effects; i++)
+               if (data->effect_ids[i] == effect_id)
+                       return i+1;
+
+       return 0;
+}
+
+static void hidpp_ff_work_handler(struct work_struct *w)
+{
+       struct hidpp_ff_work_data *wd = container_of(w, struct hidpp_ff_work_data, work);
+       struct hidpp_ff_private_data *data = wd->data;
+       struct hidpp_report response;
+       u8 slot;
+       int ret;
+
+       /* add slot number if needed */
+       switch (wd->effect_id) {
+       case HIDPP_FF_EFFECTID_AUTOCENTER:
+               wd->params[0] = data->slot_autocenter;
+               break;
+       case HIDPP_FF_EFFECTID_NONE:
+               /* leave slot as zero */
+               break;
+       default:
+               /* find current slot for effect */
+               wd->params[0] = hidpp_ff_find_effect(data, wd->effect_id);
+               break;
+       }
+
+       /* send command and wait for reply */
+       ret = hidpp_send_fap_command_sync(data->hidpp, data->feature_index,
+               wd->command, wd->params, wd->size, &response);
+
+       if (ret) {
+               hid_err(data->hidpp->hid_dev, "Failed to send command to device!\n");
+               goto out;
+       }
+
+       /* parse return data */
+       switch (wd->command) {
+       case HIDPP_FF_DOWNLOAD_EFFECT:
+               slot = response.fap.params[0];
+               if (slot > 0 && slot <= data->num_effects) {
+                       if (wd->effect_id >= 0)
+                               /* regular effect uploaded */
+                               data->effect_ids[slot-1] = wd->effect_id;
+                       else if (wd->effect_id >= HIDPP_FF_EFFECTID_AUTOCENTER)
+                               /* autocenter spring uploaded */
+                               data->slot_autocenter = slot;
+               }
+               break;
+       case HIDPP_FF_DESTROY_EFFECT:
+               if (wd->effect_id >= 0)
+                       /* regular effect destroyed */
+                       data->effect_ids[wd->params[0]-1] = -1;
+               else if (wd->effect_id >= HIDPP_FF_EFFECTID_AUTOCENTER)
+                       /* autocenter spring destoyed */
+                       data->slot_autocenter = 0;
+               break;
+       case HIDPP_FF_SET_GLOBAL_GAINS:
+               data->gain = (wd->params[0] << 8) + wd->params[1];
+               break;
+       case HIDPP_FF_SET_APERTURE:
+               data->range = (wd->params[0] << 8) + wd->params[1];
+               break;
+       default:
+               /* no action needed */
+               break;
+       }
+
+out:
+       atomic_dec(&data->workqueue_size);
+       kfree(wd);
+}
+
+static int hidpp_ff_queue_work(struct hidpp_ff_private_data *data, int effect_id, u8 command, u8 *params, u8 size)
+{
+       struct hidpp_ff_work_data *wd = kzalloc(sizeof(*wd), GFP_KERNEL);
+       int s;
+
+       if (!wd)
+               return -ENOMEM;
+
+       INIT_WORK(&wd->work, hidpp_ff_work_handler);
+
+       wd->data = data;
+       wd->effect_id = effect_id;
+       wd->command = command;
+       wd->size = size;
+       memcpy(wd->params, params, size);
+
+       atomic_inc(&data->workqueue_size);
+       queue_work(data->wq, &wd->work);
+
+       /* warn about excessive queue size */
+       s = atomic_read(&data->workqueue_size);
+       if (s >= 20 && s % 20 == 0)
+               hid_warn(data->hidpp->hid_dev, "Force feedback command queue contains %d commands, causing substantial delays!", s);
+
+       return 0;
+}
+
+static int hidpp_ff_upload_effect(struct input_dev *dev, struct ff_effect *effect, struct ff_effect *old)
+{
+       struct hidpp_ff_private_data *data = dev->ff->private;
+       u8 params[20];
+       u8 size;
+       int force;
+
+       /* set common parameters */
+       params[2] = effect->replay.length >> 8;
+       params[3] = effect->replay.length & 255;
+       params[4] = effect->replay.delay >> 8;
+       params[5] = effect->replay.delay & 255;
+
+       switch (effect->type) {
+       case FF_CONSTANT:
+               force = (effect->u.constant.level * fixp_sin16((effect->direction * 360) >> 16)) >> 15;
+               params[1] = HIDPP_FF_EFFECT_CONSTANT;
+               params[6] = force >> 8;
+               params[7] = force & 255;
+               params[8] = effect->u.constant.envelope.attack_level >> 7;
+               params[9] = effect->u.constant.envelope.attack_length >> 8;
+               params[10] = effect->u.constant.envelope.attack_length & 255;
+               params[11] = effect->u.constant.envelope.fade_level >> 7;
+               params[12] = effect->u.constant.envelope.fade_length >> 8;
+               params[13] = effect->u.constant.envelope.fade_length & 255;
+               size = 14;
+               dbg_hid("Uploading constant force level=%d in dir %d = %d\n",
+                               effect->u.constant.level,
+                               effect->direction, force);
+               dbg_hid("          envelope attack=(%d, %d ms) fade=(%d, %d ms)\n",
+                               effect->u.constant.envelope.attack_level,
+                               effect->u.constant.envelope.attack_length,
+                               effect->u.constant.envelope.fade_level,
+                               effect->u.constant.envelope.fade_length);
+               break;
+       case FF_PERIODIC:
+       {
+               switch (effect->u.periodic.waveform) {
+               case FF_SINE:
+                       params[1] = HIDPP_FF_EFFECT_PERIODIC_SINE;
+                       break;
+               case FF_SQUARE:
+                       params[1] = HIDPP_FF_EFFECT_PERIODIC_SQUARE;
+                       break;
+               case FF_SAW_UP:
+                       params[1] = HIDPP_FF_EFFECT_PERIODIC_SAWTOOTHUP;
+                       break;
+               case FF_SAW_DOWN:
+                       params[1] = HIDPP_FF_EFFECT_PERIODIC_SAWTOOTHDOWN;
+                       break;
+               case FF_TRIANGLE:
+                       params[1] = HIDPP_FF_EFFECT_PERIODIC_TRIANGLE;
+                       break;
+               default:
+                       hid_err(data->hidpp->hid_dev, "Unexpected periodic waveform type %i!\n", effect->u.periodic.waveform);
+                       return -EINVAL;
+               }
+               force = (effect->u.periodic.magnitude * fixp_sin16((effect->direction * 360) >> 16)) >> 15;
+               params[6] = effect->u.periodic.magnitude >> 8;
+               params[7] = effect->u.periodic.magnitude & 255;
+               params[8] = effect->u.periodic.offset >> 8;
+               params[9] = effect->u.periodic.offset & 255;
+               params[10] = effect->u.periodic.period >> 8;
+               params[11] = effect->u.periodic.period & 255;
+               params[12] = effect->u.periodic.phase >> 8;
+               params[13] = effect->u.periodic.phase & 255;
+               params[14] = effect->u.periodic.envelope.attack_level >> 7;
+               params[15] = effect->u.periodic.envelope.attack_length >> 8;
+               params[16] = effect->u.periodic.envelope.attack_length & 255;
+               params[17] = effect->u.periodic.envelope.fade_level >> 7;
+               params[18] = effect->u.periodic.envelope.fade_length >> 8;
+               params[19] = effect->u.periodic.envelope.fade_length & 255;
+               size = 20;
+               dbg_hid("Uploading periodic force mag=%d/dir=%d, offset=%d, period=%d ms, phase=%d\n",
+                               effect->u.periodic.magnitude, effect->direction,
+                               effect->u.periodic.offset,
+                               effect->u.periodic.period,
+                               effect->u.periodic.phase);
+               dbg_hid("          envelope attack=(%d, %d ms) fade=(%d, %d ms)\n",
+                               effect->u.periodic.envelope.attack_level,
+                               effect->u.periodic.envelope.attack_length,
+                               effect->u.periodic.envelope.fade_level,
+                               effect->u.periodic.envelope.fade_length);
+               break;
+       }
+       case FF_RAMP:
+               params[1] = HIDPP_FF_EFFECT_RAMP;
+               force = (effect->u.ramp.start_level * fixp_sin16((effect->direction * 360) >> 16)) >> 15;
+               params[6] = force >> 8;
+               params[7] = force & 255;
+               force = (effect->u.ramp.end_level * fixp_sin16((effect->direction * 360) >> 16)) >> 15;
+               params[8] = force >> 8;
+               params[9] = force & 255;
+               params[10] = effect->u.ramp.envelope.attack_level >> 7;
+               params[11] = effect->u.ramp.envelope.attack_length >> 8;
+               params[12] = effect->u.ramp.envelope.attack_length & 255;
+               params[13] = effect->u.ramp.envelope.fade_level >> 7;
+               params[14] = effect->u.ramp.envelope.fade_length >> 8;
+               params[15] = effect->u.ramp.envelope.fade_length & 255;
+               size = 16;
+               dbg_hid("Uploading ramp force level=%d -> %d in dir %d = %d\n",
+                               effect->u.ramp.start_level,
+                               effect->u.ramp.end_level,
+                               effect->direction, force);
+               dbg_hid("          envelope attack=(%d, %d ms) fade=(%d, %d ms)\n",
+                               effect->u.ramp.envelope.attack_level,
+                               effect->u.ramp.envelope.attack_length,
+                               effect->u.ramp.envelope.fade_level,
+                               effect->u.ramp.envelope.fade_length);
+               break;
+       case FF_FRICTION:
+       case FF_INERTIA:
+       case FF_SPRING:
+       case FF_DAMPER:
+               params[1] = HIDPP_FF_CONDITION_CMDS[effect->type - FF_SPRING];
+               params[6] = effect->u.condition[0].left_saturation >> 9;
+               params[7] = (effect->u.condition[0].left_saturation >> 1) & 255;
+               params[8] = effect->u.condition[0].left_coeff >> 8;
+               params[9] = effect->u.condition[0].left_coeff & 255;
+               params[10] = effect->u.condition[0].deadband >> 9;
+               params[11] = (effect->u.condition[0].deadband >> 1) & 255;
+               params[12] = effect->u.condition[0].center >> 8;
+               params[13] = effect->u.condition[0].center & 255;
+               params[14] = effect->u.condition[0].right_coeff >> 8;
+               params[15] = effect->u.condition[0].right_coeff & 255;
+               params[16] = effect->u.condition[0].right_saturation >> 9;
+               params[17] = (effect->u.condition[0].right_saturation >> 1) & 255;
+               size = 18;
+               dbg_hid("Uploading %s force left coeff=%d, left sat=%d, right coeff=%d, right sat=%d\n",
+                               HIDPP_FF_CONDITION_NAMES[effect->type - FF_SPRING],
+                               effect->u.condition[0].left_coeff,
+                               effect->u.condition[0].left_saturation,
+                               effect->u.condition[0].right_coeff,
+                               effect->u.condition[0].right_saturation);
+               dbg_hid("          deadband=%d, center=%d\n",
+                               effect->u.condition[0].deadband,
+                               effect->u.condition[0].center);
+               break;
+       default:
+               hid_err(data->hidpp->hid_dev, "Unexpected force type %i!\n", effect->type);
+               return -EINVAL;
+       }
+
+       return hidpp_ff_queue_work(data, effect->id, HIDPP_FF_DOWNLOAD_EFFECT, params, size);
+}
+
+static int hidpp_ff_playback(struct input_dev *dev, int effect_id, int value)
+{
+       struct hidpp_ff_private_data *data = dev->ff->private;
+       u8 params[2];
+
+       params[1] = value ? HIDPP_FF_EFFECT_STATE_PLAY : HIDPP_FF_EFFECT_STATE_STOP;
+
+       dbg_hid("St%sing playback of effect %d.\n", value?"art":"opp", effect_id);
+
+       return hidpp_ff_queue_work(data, effect_id, HIDPP_FF_SET_EFFECT_STATE, params, ARRAY_SIZE(params));
+}
+
+static int hidpp_ff_erase_effect(struct input_dev *dev, int effect_id)
+{
+       struct hidpp_ff_private_data *data = dev->ff->private;
+       u8 slot = 0;
+
+       dbg_hid("Erasing effect %d.\n", effect_id);
+
+       return hidpp_ff_queue_work(data, effect_id, HIDPP_FF_DESTROY_EFFECT, &slot, 1);
+}
+
+static void hidpp_ff_set_autocenter(struct input_dev *dev, u16 magnitude)
+{
+       struct hidpp_ff_private_data *data = dev->ff->private;
+       u8 params[18];
+
+       dbg_hid("Setting autocenter to %d.\n", magnitude);
+
+       /* start a standard spring effect */
+       params[1] = HIDPP_FF_EFFECT_SPRING | HIDPP_FF_EFFECT_AUTOSTART;
+       /* zero delay and duration */
+       params[2] = params[3] = params[4] = params[5] = 0;
+       /* set coeff to 25% of saturation */
+       params[8] = params[14] = magnitude >> 11;
+       params[9] = params[15] = (magnitude >> 3) & 255;
+       params[6] = params[16] = magnitude >> 9;
+       params[7] = params[17] = (magnitude >> 1) & 255;
+       /* zero deadband and center */
+       params[10] = params[11] = params[12] = params[13] = 0;
+
+       hidpp_ff_queue_work(data, HIDPP_FF_EFFECTID_AUTOCENTER, HIDPP_FF_DOWNLOAD_EFFECT, params, ARRAY_SIZE(params));
+}
+
+static void hidpp_ff_set_gain(struct input_dev *dev, u16 gain)
+{
+       struct hidpp_ff_private_data *data = dev->ff->private;
+       u8 params[4];
+
+       dbg_hid("Setting gain to %d.\n", gain);
+
+       params[0] = gain >> 8;
+       params[1] = gain & 255;
+       params[2] = 0; /* no boost */
+       params[3] = 0;
+
+       hidpp_ff_queue_work(data, HIDPP_FF_EFFECTID_NONE, HIDPP_FF_SET_GLOBAL_GAINS, params, ARRAY_SIZE(params));
+}
+
+static ssize_t hidpp_ff_range_show(struct device *dev, struct device_attribute *attr, char *buf)
+{
+       struct hid_device *hid = to_hid_device(dev);
+       struct hid_input *hidinput = list_entry(hid->inputs.next, struct hid_input, list);
+       struct input_dev *idev = hidinput->input;
+       struct hidpp_ff_private_data *data = idev->ff->private;
+
+       return scnprintf(buf, PAGE_SIZE, "%u\n", data->range);
+}
+
+static ssize_t hidpp_ff_range_store(struct device *dev, struct device_attribute *attr, const char *buf, size_t count)
+{
+       struct hid_device *hid = to_hid_device(dev);
+       struct hid_input *hidinput = list_entry(hid->inputs.next, struct hid_input, list);
+       struct input_dev *idev = hidinput->input;
+       struct hidpp_ff_private_data *data = idev->ff->private;
+       u8 params[2];
+       int range = simple_strtoul(buf, NULL, 10);
+
+       range = clamp(range, 180, 900);
+
+       params[0] = range >> 8;
+       params[1] = range & 0x00FF;
+
+       hidpp_ff_queue_work(data, -1, HIDPP_FF_SET_APERTURE, params, ARRAY_SIZE(params));
+
+       return count;
+}
+
+static DEVICE_ATTR(range, S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP | S_IROTH, hidpp_ff_range_show, hidpp_ff_range_store);
+
+static void hidpp_ff_destroy(struct ff_device *ff)
+{
+       struct hidpp_ff_private_data *data = ff->private;
+
+       kfree(data->effect_ids);
+}
+
+static int hidpp_ff_init(struct hidpp_device *hidpp, u8 feature_index)
+{
+       struct hid_device *hid = hidpp->hid_dev;
+       struct hid_input *hidinput = list_entry(hid->inputs.next, struct hid_input, list);
+       struct input_dev *dev = hidinput->input;
+       const struct usb_device_descriptor *udesc = &(hid_to_usb_dev(hid)->descriptor);
+       const u16 bcdDevice = le16_to_cpu(udesc->bcdDevice);
+       struct ff_device *ff;
+       struct hidpp_report response;
+       struct hidpp_ff_private_data *data;
+       int error, j, num_slots;
+       u8 version;
+
+       if (!dev) {
+               hid_err(hid, "Struct input_dev not set!\n");
+               return -EINVAL;
+       }
+
+       /* Get firmware release */
+       version = bcdDevice & 255;
+
+       /* Set supported force feedback capabilities */
+       for (j = 0; hiddpp_ff_effects[j] >= 0; j++)
+               set_bit(hiddpp_ff_effects[j], dev->ffbit);
+       if (version > 1)
+               for (j = 0; hiddpp_ff_effects_v2[j] >= 0; j++)
+                       set_bit(hiddpp_ff_effects_v2[j], dev->ffbit);
+
+       /* Read number of slots available in device */
+       error = hidpp_send_fap_command_sync(hidpp, feature_index,
+               HIDPP_FF_GET_INFO, NULL, 0, &response);
+       if (error) {
+               if (error < 0)
+                       return error;
+               hid_err(hidpp->hid_dev, "%s: received protocol error 0x%02x\n",
+                       __func__, error);
+               return -EPROTO;
+       }
+
+       num_slots = response.fap.params[0] - HIDPP_FF_RESERVED_SLOTS;
+
+       error = input_ff_create(dev, num_slots);
+
+       if (error) {
+               hid_err(dev, "Failed to create FF device!\n");
+               return error;
+       }
+
+       data = kzalloc(sizeof(*data), GFP_KERNEL);
+       if (!data)
+               return -ENOMEM;
+       data->effect_ids = kcalloc(num_slots, sizeof(int), GFP_KERNEL);
+       if (!data->effect_ids) {
+               kfree(data);
+               return -ENOMEM;
+       }
+       data->hidpp = hidpp;
+       data->feature_index = feature_index;
+       data->version = version;
+       data->slot_autocenter = 0;
+       data->num_effects = num_slots;
+       for (j = 0; j < num_slots; j++)
+               data->effect_ids[j] = -1;
+
+       ff = dev->ff;
+       ff->private = data;
+
+       ff->upload = hidpp_ff_upload_effect;
+       ff->erase = hidpp_ff_erase_effect;
+       ff->playback = hidpp_ff_playback;
+       ff->set_gain = hidpp_ff_set_gain;
+       ff->set_autocenter = hidpp_ff_set_autocenter;
+       ff->destroy = hidpp_ff_destroy;
+
+
+       /* reset all forces */
+       error = hidpp_send_fap_command_sync(hidpp, feature_index,
+               HIDPP_FF_RESET_ALL, NULL, 0, &response);
+
+       /* Read current Range */
+       error = hidpp_send_fap_command_sync(hidpp, feature_index,
+               HIDPP_FF_GET_APERTURE, NULL, 0, &response);
+       if (error)
+               hid_warn(hidpp->hid_dev, "Failed to read range from device!\n");
+       data->range = error ? 900 : get_unaligned_be16(&response.fap.params[0]);
+
+       /* Create sysfs interface */
+       error = device_create_file(&(hidpp->hid_dev->dev), &dev_attr_range);
+       if (error)
+               hid_warn(hidpp->hid_dev, "Unable to create sysfs interface for \"range\", errno %d!\n", error);
+
+       /* Read the current gain values */
+       error = hidpp_send_fap_command_sync(hidpp, feature_index,
+               HIDPP_FF_GET_GLOBAL_GAINS, NULL, 0, &response);
+       if (error)
+               hid_warn(hidpp->hid_dev, "Failed to read gain values from device!\n");
+       data->gain = error ? 0xffff : get_unaligned_be16(&response.fap.params[0]);
+       /* ignore boost value at response.fap.params[2] */
+
+       /* init the hardware command queue */
+       data->wq = create_singlethread_workqueue("hidpp-ff-sendqueue");
+       atomic_set(&data->workqueue_size, 0);
+
+       /* initialize with zero autocenter to get wheel in usable state */
+       hidpp_ff_set_autocenter(dev, 0);
+
+       hid_info(hid, "Force feeback support loaded (firmware release %d).\n", version);
+
+       return 0;
+}
+
+static int hidpp_ff_deinit(struct hid_device *hid)
+{
+       struct hid_input *hidinput = list_entry(hid->inputs.next, struct hid_input, list);
+       struct input_dev *dev = hidinput->input;
+       struct hidpp_ff_private_data *data;
+
+       if (!dev) {
+               hid_err(hid, "Struct input_dev not found!\n");
+               return -EINVAL;
+       }
+
+       hid_info(hid, "Unloading HID++ force feedback.\n");
+       data = dev->ff->private;
+       if (!data) {
+               hid_err(hid, "Private data not found!\n");
+               return -EINVAL;
+       }
+
+       destroy_workqueue(data->wq);
+       device_remove_file(&hid->dev, &dev_attr_range);
+
+       return 0;
+}
+
+
 /* ************************************************************************** */
 /*                                                                            */
 /* Device Support                                                             */
@@ -1301,121 +1890,22 @@ static int k400_connect(struct hid_device *hdev, bool connected)
 
 #define HIDPP_PAGE_G920_FORCE_FEEDBACK                 0x8123
 
-/* Using session ID = 1 */
-#define CMD_G920_FORCE_GET_APERTURE                    0x51
-#define CMD_G920_FORCE_SET_APERTURE                    0x61
-
-struct g920_private_data {
-       u8 force_feature;
-       u16 range;
-};
-
-static ssize_t g920_range_show(struct device *dev, struct device_attribute *attr,
-                               char *buf)
-{
-       struct hid_device *hid = to_hid_device(dev);
-       struct hidpp_device *hidpp = hid_get_drvdata(hid);
-       struct g920_private_data *pdata;
-
-       pdata = hidpp->private_data;
-       if (!pdata) {
-               hid_err(hid, "Private driver data not found!\n");
-               return -EINVAL;
-       }
-
-       return scnprintf(buf, PAGE_SIZE, "%u\n", pdata->range);
-}
-
-static ssize_t g920_range_store(struct device *dev, struct device_attribute *attr,
-                                const char *buf, size_t count)
-{
-       struct hid_device *hid = to_hid_device(dev);
-       struct hidpp_device *hidpp = hid_get_drvdata(hid);
-       struct g920_private_data *pdata;
-       struct hidpp_report response;
-       u8 params[2];
-       int ret;
-       u16 range = simple_strtoul(buf, NULL, 10);
-
-       pdata = hidpp->private_data;
-       if (!pdata) {
-               hid_err(hid, "Private driver data not found!\n");
-               return -EINVAL;
-       }
-
-       if (range < 180)
-               range = 180;
-       else if (range > 900)
-               range = 900;
-
-       params[0] = range >> 8;
-       params[1] = range & 0x00FF;
-
-       ret = hidpp_send_fap_command_sync(hidpp, pdata->force_feature,
-               CMD_G920_FORCE_SET_APERTURE, params, 2, &response);
-       if (ret)
-               return ret;
-
-       pdata->range = range;
-       return count;
-}
-
-static DEVICE_ATTR(range, S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP | S_IROTH, g920_range_show, g920_range_store);
-
-static int g920_allocate(struct hid_device *hdev)
-{
-       struct hidpp_device *hidpp = hid_get_drvdata(hdev);
-       struct g920_private_data *pdata;
-
-       pdata = devm_kzalloc(&hdev->dev, sizeof(struct g920_private_data),
-                       GFP_KERNEL);
-       if (!pdata)
-               return -ENOMEM;
-
-       hidpp->private_data = pdata;
-
-       return 0;
-}
-
 static int g920_get_config(struct hidpp_device *hidpp)
 {
-       struct g920_private_data *pdata = hidpp->private_data;
-       struct hidpp_report response;
        u8 feature_type;
        u8 feature_index;
        int ret;
 
-       pdata = hidpp->private_data;
-       if (!pdata) {
-               hid_err(hidpp->hid_dev, "Private driver data not found!\n");
-               return -EINVAL;
-       }
-
        /* Find feature and store for later use */
        ret = hidpp_root_get_feature(hidpp, HIDPP_PAGE_G920_FORCE_FEEDBACK,
                &feature_index, &feature_type);
        if (ret)
                return ret;
 
-       pdata->force_feature = feature_index;
-
-       /* Read current Range */
-       ret = hidpp_send_fap_command_sync(hidpp, feature_index,
-               CMD_G920_FORCE_GET_APERTURE, NULL, 0, &response);
-       if (ret > 0) {
-               hid_err(hidpp->hid_dev, "%s: received protocol error 0x%02x\n",
-                       __func__, ret);
-               return -EPROTO;
-       }
-       if (ret)
-               return ret;
-
-       pdata->range = get_unaligned_be16(&response.fap.params[0]);
-
-       /* Create sysfs interface */
-       ret = device_create_file(&(hidpp->hid_dev->dev), &dev_attr_range);
+       ret = hidpp_ff_init(hidpp, feature_index);
        if (ret)
-               hid_warn(hidpp->hid_dev, "Unable to create sysfs interface for \"range\", errno %d\n", ret);
+               hid_warn(hidpp->hid_dev, "Unable to initialize force feedback support, errno %d\n",
+                               ret);
 
        return 0;
 }
@@ -1739,10 +2229,6 @@ static int hidpp_probe(struct hid_device *hdev, const struct hid_device_id *id)
                ret = k400_allocate(hdev);
                if (ret)
                        goto allocate_fail;
-       } else if (hidpp->quirks & HIDPP_QUIRK_CLASS_G920) {
-               ret = g920_allocate(hdev);
-               if (ret)
-                       goto allocate_fail;
        }
 
        INIT_WORK(&hidpp->work, delayed_work_cb);
@@ -1825,7 +2311,6 @@ static int hidpp_probe(struct hid_device *hdev, const struct hid_device_id *id)
 hid_hw_open_failed:
        hid_device_io_stop(hdev);
        if (hidpp->quirks & HIDPP_QUIRK_CLASS_G920) {
-               device_remove_file(&hdev->dev, &dev_attr_range);
                hid_hw_close(hdev);
                hid_hw_stop(hdev);
        }
@@ -1843,7 +2328,7 @@ static void hidpp_remove(struct hid_device *hdev)
        struct hidpp_device *hidpp = hid_get_drvdata(hdev);
 
        if (hidpp->quirks & HIDPP_QUIRK_CLASS_G920) {
-               device_remove_file(&hdev->dev, &dev_attr_range);
+               hidpp_ff_deinit(hdev);
                hid_hw_close(hdev);
        }
        hid_hw_stop(hdev);