[alsa-devel] [PATCH 3/4] hid-lenovo: Add support for X1 Tablet special keys and, LED control
The Lenovo X1 Tablet Cover is connected via USB. It consists of 1 device with 3 usb interfaces. Interface 0 represents keyboard, interface 1 the function / special keys and LED control, interface 2 is the Synaptics touchpad and pointing stick.
This driver will bind to interfaces 0 and 1 and handle function / special keys including LED control.
Signed-off-by: Dennis Wassenberg dennis.wassenberg@secunet.com --- drivers/hid/hid-lenovo.c | 549 +++++++++++++++++++++++++++++++++++++++++++++ include/linux/hid-lenovo.h | 15 ++ 2 files changed, 564 insertions(+) create mode 100644 include/linux/hid-lenovo.h
diff --git a/drivers/hid/hid-lenovo.c b/drivers/hid/hid-lenovo.c index 1ac4ff4..4251aac 100644 --- a/drivers/hid/hid-lenovo.c +++ b/drivers/hid/hid-lenovo.c @@ -3,9 +3,11 @@ * - ThinkPad USB Keyboard with TrackPoint (tpkbd) * - ThinkPad Compact Bluetooth Keyboard with TrackPoint (cptkbd) * - ThinkPad Compact USB Keyboard with TrackPoint (cptkbd) + * - ThinkPad X1 Cover USB Keyboard with TrackPoint and Touchpad (tpx1cover) * * Copyright (c) 2012 Bernhard Seibold * Copyright (c) 2014 Jamie Lentin jm@lentin.co.uk + * Copyright (c) 2016 Dennis Wassenberg dennis.wassenberg@secunet.com */
/* @@ -19,11 +21,19 @@ #include <linux/sysfs.h> #include <linux/device.h> #include <linux/hid.h> +#include <linux/hid-lenovo.h> #include <linux/input.h> #include <linux/leds.h>
#include "hid-ids.h"
+struct led_table_entry { + struct led_classdev *dev; + uint8_t state; +}; + +static struct led_table_entry hid_lenovo_led_table[HID_LENOVO_LED_MAX]; + struct lenovo_drvdata_tpkbd { int led_state; struct led_classdev led_mute; @@ -42,6 +52,37 @@ struct lenovo_drvdata_cptkbd { int sensitivity; };
+struct lenovo_drvdata_tpx1cover { + uint16_t led_state; + uint8_t fnlock_state; + uint8_t led_present; + struct led_classdev led_mute; + struct led_classdev led_micmute; + struct led_classdev led_fnlock; +}; + +int hid_lenovo_led_set(int led_num, bool on) +{ + struct led_classdev *dev; + + if (led_num >= HID_LENOVO_LED_MAX) + return -EINVAL; + + dev = hid_lenovo_led_table[led_num].dev; + hid_lenovo_led_table[led_num].state = on; + + if (!dev) + return -ENODEV; + + if (!dev->brightness_set) + return -ENODEV; + + dev->brightness_set(dev, on ? LED_FULL : LED_OFF); + + return 0; +} +EXPORT_SYMBOL_GPL(hid_lenovo_led_set); + #define map_key_clear(c) hid_map_usage_clear(hi, usage, bit, max, EV_KEY, (c))
static const __u8 lenovo_pro_dock_need_fixup_collection[] = { @@ -86,6 +127,84 @@ static int lenovo_input_mapping_tpkbd(struct hid_device *hdev, return 0; }
+static int lenovo_input_mapping_tpx1cover(struct hid_device *hdev, + struct hid_input *hi, struct hid_field *field, + struct hid_usage *usage, unsigned long **bit, int *max) +{ + if ((usage->hid & HID_USAGE_PAGE) == HID_UP_CONSUMER) { + switch (usage->hid & HID_USAGE) { + case 0x0001: // Unknown keys -> Idenditied by usage index! + map_key_clear(KEY_UNKNOWN); + switch (usage->usage_index) { + case 0x8: + input_set_capability(hi->input, EV_KEY, KEY_FN); + break; + + case 0x9: + input_set_capability(hi->input, EV_KEY, KEY_MICMUTE); + break; + + case 0xa: + input_set_capability(hi->input, EV_KEY, KEY_CONFIG); + break; + + case 0xb: + input_set_capability(hi->input, EV_KEY, KEY_SEARCH); + break; + + case 0xc: + input_set_capability(hi->input, EV_KEY, KEY_SETUP); + break; + + case 0xd: + input_set_capability(hi->input, EV_KEY, KEY_SWITCHVIDEOMODE); + break; + + case 0xe: + input_set_capability(hi->input, EV_KEY, KEY_RFKILL); + break; + + default: + return -1; + } + + return 1; + + case 0x006f: // Consumer.006f ---> Key.BrightnessUp + map_key_clear(KEY_BRIGHTNESSUP); + return 1; + + case 0x0070: // Consumer.0070 ---> Key.BrightnessDown + map_key_clear(KEY_BRIGHTNESSDOWN); + return 1; + + case 0x00b7:// Consumer.00b7 ---> Key.StopCD + map_key_clear(KEY_STOPCD); + return 1; + + case 0x00cd: // Consumer.00cd ---> Key.PlayPause + map_key_clear(KEY_PLAYPAUSE); + return 1; + + case 0x00e0: // Consumer.00e0 ---> Absolute.Volume + return 0; + case 0x00e2: // Consumer.00e2 ---> Key.Mute + map_key_clear(KEY_MUTE); + return 1; + + case 0x00e9: // Consumer.00e9 ---> Key.VolumeUp + map_key_clear(KEY_VOLUMEUP); + return 1; + + case 0x00ea: // Consumer.00ea ---> Key.VolumeDown + map_key_clear(KEY_VOLUMEDOWN); + return 1; + } + } + + return 0; +} + static int lenovo_input_mapping_cptkbd(struct hid_device *hdev, struct hid_input *hi, struct hid_field *field, struct hid_usage *usage, unsigned long **bit, int *max) @@ -172,6 +291,9 @@ static int lenovo_input_mapping(struct hid_device *hdev, case USB_DEVICE_ID_LENOVO_CBTKBD: return lenovo_input_mapping_cptkbd(hdev, hi, field, usage, bit, max); + case USB_DEVICE_ID_LENOVO_X1_COVER: + return lenovo_input_mapping_tpx1cover(hdev, hi, field, + usage, bit, max); default: return 0; } @@ -362,6 +484,143 @@ static int lenovo_event_cptkbd(struct hid_device *hdev, return 0; }
+static enum led_brightness lenovo_led_brightness_get_tpx1cover( + struct led_classdev *led_cdev) +{ + struct device *dev = led_cdev->dev->parent; + struct hid_device *hdev = to_hid_device(dev); + struct lenovo_drvdata_tpx1cover *drv_data = hid_get_drvdata(hdev); + int led_nr = 0; + + if (led_cdev == &drv_data->led_mute) + led_nr = 0; + else if (led_cdev == &drv_data->led_micmute) + led_nr = 1; + else if (led_cdev == &drv_data->led_fnlock) + led_nr = 2; + else + return LED_OFF; + + return drv_data->led_state & (1 << led_nr) + ? LED_FULL + : LED_OFF; +} + +static void lenovo_led_brightness_set_tpx1cover(struct led_classdev *led_cdev, + enum led_brightness value) +{ + struct device *dev = led_cdev->dev->parent; + struct hid_device *hdev = to_hid_device(dev); + struct lenovo_drvdata_tpx1cover *drv_data = hid_get_drvdata(hdev); + struct hid_report *report; + int led_nr = -1; + int led_nr_hw = -1; + + if (led_cdev == &drv_data->led_mute) { + led_nr = 0; + led_nr_hw = 0x64; + } else if (led_cdev == &drv_data->led_micmute) { + led_nr = 1; + led_nr_hw = 0x74; + } else if (led_cdev == &drv_data->led_fnlock) { + led_nr = 2; + led_nr_hw = 0x54; + } else { + hid_warn(hdev, "Invalid LED to set.\n"); + return; + } + + if (value == LED_OFF) + drv_data->led_state &= ~(1 << led_nr); + else + drv_data->led_state |= 1 << led_nr; + + report = hdev->report_enum[HID_OUTPUT_REPORT].report_id_hash[9]; + if (report) { + report->field[0]->value[0] = led_nr_hw; + report->field[0]->value[1] = (drv_data->led_state & (1 << led_nr)) + ? 0x02 : 0x01; + hid_hw_request(hdev, report, HID_REQ_SET_REPORT); + } +} + +static int lenovo_event_tpx1cover(struct hid_device *hdev, + struct hid_field *field, struct hid_usage *usage, __s32 value) +{ + int ret = 0; + + if ((usage->hid & HID_USAGE_PAGE) == HID_UP_CONSUMER + && (usage->hid & HID_USAGE) == 0x0001) { + + if (usage->usage_index == 0x8 && value == 1) { + struct lenovo_drvdata_tpx1cover *drv_data = hid_get_drvdata(hdev); + + if (drv_data && drv_data->led_present) { + drv_data->fnlock_state = lenovo_led_brightness_get_tpx1cover( + &drv_data->led_fnlock) == LED_OFF ? 1 : 0; + lenovo_led_brightness_set_tpx1cover( + &drv_data->led_fnlock, + drv_data->fnlock_state ? LED_FULL : LED_OFF); + } + } + + if (usage->usage_index == 0x9 && value == 1) { + input_event(field->hidinput->input, EV_KEY, KEY_MICMUTE, 1); + input_sync(field->hidinput->input); + input_event(field->hidinput->input, EV_KEY, KEY_MICMUTE, 0); + input_sync(field->hidinput->input); + ret = 1; + } + + if (usage->usage_index == 0xa && value == 1) { + input_event(field->hidinput->input, EV_KEY, KEY_CONFIG, 1); + input_sync(field->hidinput->input); + input_event(field->hidinput->input, EV_KEY, KEY_CONFIG, 0); + input_sync(field->hidinput->input); + + ret = 1; + } + + if (usage->usage_index == 0xb && value == 1) { + input_event(field->hidinput->input, EV_KEY, KEY_SEARCH, 1); + input_sync(field->hidinput->input); + input_event(field->hidinput->input, EV_KEY, KEY_SEARCH, 0); + input_sync(field->hidinput->input); + + ret = 1; + } + + if (usage->usage_index == 0xc && value == 1) { + input_event(field->hidinput->input, EV_KEY, KEY_SETUP, 1); + input_sync(field->hidinput->input); + input_event(field->hidinput->input, EV_KEY, KEY_SETUP, 0); + input_sync(field->hidinput->input); + + ret = 1; + } + + if (usage->usage_index == 0xd && value == 1) { + input_event(field->hidinput->input, EV_KEY, KEY_SWITCHVIDEOMODE, 1); + input_sync(field->hidinput->input); + input_event(field->hidinput->input, EV_KEY, KEY_SWITCHVIDEOMODE, 0); + input_sync(field->hidinput->input); + + ret = 1; + } + + if (usage->usage_index == 0xe && value == 1) { + input_event(field->hidinput->input, EV_KEY, KEY_RFKILL, 1); + input_sync(field->hidinput->input); + input_event(field->hidinput->input, EV_KEY, KEY_RFKILL, 0); + input_sync(field->hidinput->input); + + ret = 1; + } + } + + return ret; +} + static int lenovo_event(struct hid_device *hdev, struct hid_field *field, struct hid_usage *usage, __s32 value) { @@ -369,6 +628,8 @@ static int lenovo_event(struct hid_device *hdev, struct hid_field *field, case USB_DEVICE_ID_LENOVO_CUSBKBD: case USB_DEVICE_ID_LENOVO_CBTKBD: return lenovo_event_cptkbd(hdev, field, usage, value); + case USB_DEVICE_ID_LENOVO_X1_COVER: + return lenovo_event_tpx1cover(hdev, field, usage, value); default: return 0; } @@ -731,6 +992,251 @@ static int lenovo_probe_tpkbd(struct hid_device *hdev) return ret; }
+static int lenovo_probe_tpx1cover_configure(struct hid_device *hdev) +{ + struct hid_report *report = hdev->report_enum[HID_OUTPUT_REPORT].report_id_hash[9]; + struct lenovo_drvdata_tpx1cover *drv_data = hid_get_drvdata(hdev); + + if (!drv_data) + return -ENODEV; + + if (!report) + return -ENOENT; + + report->field[0]->value[0] = 0x54; + report->field[0]->value[1] = 0x20; + hid_hw_request(hdev, report, HID_REQ_SET_REPORT); + hid_hw_wait(hdev); + + report->field[0]->value[0] = 0x54; + report->field[0]->value[1] = 0x08; + hid_hw_request(hdev, report, HID_REQ_SET_REPORT); + hid_hw_wait(hdev); + + report->field[0]->value[0] = 0xA0; + report->field[0]->value[1] = 0x02; + hid_hw_request(hdev, report, HID_REQ_SET_REPORT); + hid_hw_wait(hdev); + + lenovo_led_brightness_set_tpx1cover(&drv_data->led_mute, + hid_lenovo_led_table[HID_LENOVO_LED_MUTE].state ? LED_FULL : LED_OFF); + hid_hw_wait(hdev); + + lenovo_led_brightness_set_tpx1cover(&drv_data->led_micmute, + hid_lenovo_led_table[HID_LENOVO_LED_MICMUTE].state ? LED_FULL : LED_OFF); + hid_hw_wait(hdev); + + lenovo_led_brightness_set_tpx1cover(&drv_data->led_fnlock, LED_FULL); + + return 0; +} + +static int lenovo_probe_tpx1cover_special_functions(struct hid_device *hdev) +{ + struct device *dev = &hdev->dev; + struct lenovo_drvdata_tpx1cover *drv_data = NULL; + + size_t name_sz = strlen(dev_name(dev)) + 16; + char *name_led = NULL; + + struct hid_report *report; + bool report_match = 1; + + int ret = 0; + + report = hid_validate_values(hdev, HID_INPUT_REPORT, 2, 0, 3); + report_match &= report ? 1 : 0; + report = hid_validate_values(hdev, HID_INPUT_REPORT, 3, 0, 16); + report_match &= report ? 1 : 0; + report = hid_validate_values(hdev, HID_OUTPUT_REPORT, 9, 0, 2); + report_match &= report ? 1 : 0; + report = hid_validate_values(hdev, HID_FEATURE_REPORT, 32, 0, 1); + report_match &= report ? 1 : 0; + report = hid_validate_values(hdev, HID_FEATURE_REPORT, 84, 0, 1); + report_match &= report ? 1 : 0; + report = hid_validate_values(hdev, HID_FEATURE_REPORT, 100, 0, 1); + report_match &= report ? 1 : 0; + report = hid_validate_values(hdev, HID_FEATURE_REPORT, 116, 0, 1); + report_match &= report ? 1 : 0; + report = hid_validate_values(hdev, HID_FEATURE_REPORT, 132, 0, 1); + report_match &= report ? 1 : 0; + report = hid_validate_values(hdev, HID_FEATURE_REPORT, 144, 0, 1); + report_match &= report ? 1 : 0; + report = hid_validate_values(hdev, HID_FEATURE_REPORT, 162, 0, 1); + report_match &= report ? 1 : 0; + + if (!report_match) { + ret = -ENODEV; + goto err; + } + + drv_data = devm_kzalloc(&hdev->dev, + sizeof(struct lenovo_drvdata_tpx1cover), + GFP_KERNEL); + + if (!drv_data) { + hid_err(hdev, + "Could not allocate memory for tpx1cover driver data\n"); + ret = -ENOMEM; + goto err; + } + + name_led = devm_kzalloc(&hdev->dev, name_sz, GFP_KERNEL); + if (!name_led) { + hid_err(hdev, "Could not allocate memory for mute led data\n"); + ret = -ENOMEM; + goto err_cleanup; + } + snprintf(name_led, name_sz, "%s:amber:mute", dev_name(dev)); + + drv_data->led_mute.name = name_led; + drv_data->led_mute.brightness_get = lenovo_led_brightness_get_tpx1cover; + drv_data->led_mute.brightness_set = lenovo_led_brightness_set_tpx1cover; + drv_data->led_mute.dev = dev; + hid_lenovo_led_table[HID_LENOVO_LED_MUTE].dev = &drv_data->led_mute; + led_classdev_register(dev, &drv_data->led_mute); + + + name_led = devm_kzalloc(&hdev->dev, name_sz, GFP_KERNEL); + if (!name_led) { + hid_err(hdev, + "Could not allocate memory for mic mute led data\n"); + ret = -ENOMEM; + goto err_cleanup; + } + snprintf(name_led, name_sz, "%s:amber:micmute", dev_name(dev)); + + drv_data->led_micmute.name = name_led; + drv_data->led_micmute.brightness_get = lenovo_led_brightness_get_tpx1cover; + drv_data->led_micmute.brightness_set = lenovo_led_brightness_set_tpx1cover; + drv_data->led_micmute.dev = dev; + hid_lenovo_led_table[HID_LENOVO_LED_MICMUTE].dev = &drv_data->led_micmute; + led_classdev_register(dev, &drv_data->led_micmute); + + + name_led = devm_kzalloc(&hdev->dev, name_sz, GFP_KERNEL); + if (!name_led) { + hid_err(hdev, + "Could not allocate memory for FN lock led data\n"); + ret = -ENOMEM; + goto err_cleanup; + } + + snprintf(name_led, name_sz, "%s:amber:fnlock", dev_name(dev)); + + drv_data->led_fnlock.name = name_led; + drv_data->led_fnlock.brightness_get = lenovo_led_brightness_get_tpx1cover; + drv_data->led_fnlock.brightness_set = lenovo_led_brightness_set_tpx1cover; + drv_data->led_fnlock.dev = dev; + hid_lenovo_led_table[HID_LENOVO_LED_FNLOCK].dev = &drv_data->led_fnlock; + led_classdev_register(dev, &drv_data->led_fnlock); + + drv_data->led_state = 0; + drv_data->fnlock_state = 1; + drv_data->led_present = 1; + + hid_set_drvdata(hdev, drv_data); + + return lenovo_probe_tpx1cover_configure(hdev); + +err_cleanup: + if (drv_data->led_fnlock.name) { + led_classdev_unregister(&drv_data->led_fnlock); + devm_kfree(&hdev->dev, (void *) drv_data->led_fnlock.name); + } + + if (drv_data->led_micmute.name) { + led_classdev_unregister(&drv_data->led_micmute); + devm_kfree(&hdev->dev, (void *) drv_data->led_micmute.name); + } + + if (drv_data->led_mute.name) { + led_classdev_unregister(&drv_data->led_mute); + devm_kfree(&hdev->dev, (void *) drv_data->led_mute.name); + } + + if (drv_data) + kfree(drv_data); + +err: + return ret; +} + +static int lenovo_probe_tpx1cover_touch(struct hid_device *hdev) +{ + struct hid_report *report; + bool report_match = 1; + int ret = 0; + + report = hid_validate_values(hdev, HID_INPUT_REPORT, 2, 0, 2); + report_match &= report ? 1 : 0; + report = hid_validate_values(hdev, HID_INPUT_REPORT, 2, 1, 2); + report_match &= report ? 1 : 0; + report = hid_validate_values(hdev, HID_INPUT_REPORT, 11, 0, 61); + report_match &= report ? 1 : 0; + report = hid_validate_values(hdev, HID_INPUT_REPORT, 12, 0, 61); + report_match &= report ? 1 : 0; + report = hid_validate_values(hdev, HID_INPUT_REPORT, 16, 0, 3); + report_match &= report ? 1 : 0; + report = hid_validate_values(hdev, HID_INPUT_REPORT, 16, 1, 2); + report_match &= report ? 1 : 0; + report = hid_validate_values(hdev, HID_OUTPUT_REPORT, 9, 0, 20); + report_match &= report ? 1 : 0; + report = hid_validate_values(hdev, HID_OUTPUT_REPORT, 10, 0, 20); + report_match &= report ? 1 : 0; + report = hid_validate_values(hdev, HID_FEATURE_REPORT, 14, 0, 1); + report_match &= report ? 1 : 0; + report = hid_validate_values(hdev, HID_FEATURE_REPORT, 15, 0, 3); + report_match &= report ? 1 : 0; + + if (!report_match) + ret = -ENODEV; + + return ret; +} + +static int lenovo_probe_tpx1cover(struct hid_device *hdev) +{ + int ret = 0; + + /* + * Probing for special function keys and LED control -> usb intf 1 + * Probing for touch input -> usb intf 2 (handled by rmi4 driver) + * Other (keyboard) -> usb intf 0 + */ + if (!lenovo_probe_tpx1cover_special_functions(hdev)) { + // special function keys and LED control + ret = 0; + } else if (!lenovo_probe_tpx1cover_touch(hdev)) { + // handled by rmi + ret = -ENODEV; + } else { + // keyboard + struct lenovo_drvdata_tpx1cover *drv_data; + + drv_data = devm_kzalloc(&hdev->dev, + sizeof(struct lenovo_drvdata_tpx1cover), + GFP_KERNEL); + + if (!drv_data) { + hid_err(hdev, + "Could not allocate memory for tpx1cover driver data\n"); + ret = -ENOMEM; + goto out; + } + + drv_data->led_state = 0; + drv_data->led_present = 0; + drv_data->fnlock_state = 0; + hid_set_drvdata(hdev, drv_data); + + ret = 0; + } + +out: + return ret; +} + static int lenovo_probe_cptkbd(struct hid_device *hdev) { int ret; @@ -803,6 +1309,9 @@ static int lenovo_probe(struct hid_device *hdev, case USB_DEVICE_ID_LENOVO_CBTKBD: ret = lenovo_probe_cptkbd(hdev); break; + case USB_DEVICE_ID_LENOVO_X1_COVER: + ret = lenovo_probe_tpx1cover(hdev); + break; default: ret = 0; break; @@ -843,6 +1352,42 @@ static void lenovo_remove_cptkbd(struct hid_device *hdev) &lenovo_attr_group_cptkbd); }
+static void lenovo_remove_tpx1cover(struct hid_device *hdev) +{ + struct lenovo_drvdata_tpx1cover *drv_data = hid_get_drvdata(hdev); + + if (!drv_data) + return; + + if (drv_data->led_present) { + if (drv_data->led_fnlock.name) { + hid_lenovo_led_table[HID_LENOVO_LED_FNLOCK].dev = NULL; + + led_classdev_unregister(&drv_data->led_fnlock); + devm_kfree(&hdev->dev, (void *) drv_data->led_fnlock.name); + } + + if (drv_data->led_micmute.name) { + hid_lenovo_led_table[HID_LENOVO_LED_MICMUTE].dev = NULL; + + led_classdev_unregister(&drv_data->led_micmute); + devm_kfree(&hdev->dev, (void *) drv_data->led_micmute.name); + } + + if (drv_data->led_mute.name) { + hid_lenovo_led_table[HID_LENOVO_LED_MUTE].dev = NULL; + + led_classdev_unregister(&drv_data->led_mute); + devm_kfree(&hdev->dev, (void *) drv_data->led_mute.name); + } + } + + if (drv_data) + devm_kfree(&hdev->dev, drv_data); + + hid_set_drvdata(hdev, NULL); +} + static void lenovo_remove(struct hid_device *hdev) { switch (hdev->product) { @@ -853,6 +1398,9 @@ static void lenovo_remove(struct hid_device *hdev) case USB_DEVICE_ID_LENOVO_CBTKBD: lenovo_remove_cptkbd(hdev); break; + case USB_DEVICE_ID_LENOVO_X1_COVER: + lenovo_remove_tpx1cover(hdev); + break; }
hid_hw_stop(hdev); @@ -883,6 +1431,7 @@ static int lenovo_input_configured(struct hid_device *hdev, { HID_USB_DEVICE(USB_VENDOR_ID_LENOVO, USB_DEVICE_ID_LENOVO_CUSBKBD) }, { HID_BLUETOOTH_DEVICE(USB_VENDOR_ID_LENOVO, USB_DEVICE_ID_LENOVO_CBTKBD) }, { HID_USB_DEVICE(USB_VENDOR_ID_LENOVO, USB_DEVICE_ID_LENOVO_TPPRODOCK) }, + { HID_USB_DEVICE(USB_VENDOR_ID_LENOVO, USB_DEVICE_ID_LENOVO_X1_COVER) }, { } };
diff --git a/include/linux/hid-lenovo.h b/include/linux/hid-lenovo.h new file mode 100644 index 0000000..d0b0145 --- /dev/null +++ b/include/linux/hid-lenovo.h @@ -0,0 +1,15 @@ + +#ifndef __HID_LENOVO_H__ +#define __HID_LENOVO_H__ + + +enum { + HID_LENOVO_LED_MUTE, + HID_LENOVO_LED_MICMUTE, + HID_LENOVO_LED_FNLOCK, + HID_LENOVO_LED_MAX, +}; + +int hid_lenovo_led_set(int led_num, bool on); + +#endif /* __HID_LENOVO_H_ */
participants (1)
-
Dennis Wassenberg