From nobody Fri Apr 3 11:19:20 2026 Received: from mail.tuxedocomputers.com (mail.tuxedocomputers.com [157.90.84.7]) (using TLSv1.2 with cipher ECDHE-RSA-AES128-GCM-SHA256 (128/128 bits)) (No client certificate requested) by smtp.subspace.kernel.org (Postfix) with ESMTPS id A8D532F39B9; Thu, 19 Feb 2026 13:02:46 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=157.90.84.7 ARC-Seal: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1771506172; cv=none; b=EKa0yY5/Whf/l63kbkhRS6ptm89MS3uwqqEF8bOVLFlESSDmYNlmViw1Li42S+G2SLuKdeXXQ+vqS1dgxhWu0EJ8Ca1q80ixiKuKG/Rxl+ee8PxrIzjP/Di8Cnp9fXp5Mt5eHzGUuRba2Cw4h92EhzcTkk/O24bwY9t7c4FGl1w= ARC-Message-Signature: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1771506172; c=relaxed/simple; bh=nU0WGROUUzMh0zS7ya5nhv3/98FjTnKhe10Sc9ySWzw=; h=From:To:Cc:Subject:Date:Message-ID:MIME-Version; b=iYkNmfGoLzEaxY/paOIrvK8Y6zym9n6R5RcdNLWTi1bXle3bpeMsyJEmtK3KyrkBFIjmwsJM4HtjUIHsjyK7auiFODjhTP3A+4dA5wOCbMXL2Dcag1gJKCCOGX+YR4LLxEAvmADe5SGfebMsjNWVMAApBRajFp41z/AwCDgCZq4= ARC-Authentication-Results: i=1; smtp.subspace.kernel.org; dmarc=pass (p=quarantine dis=none) header.from=tuxedocomputers.com; spf=pass smtp.mailfrom=tuxedocomputers.com; dkim=pass (1024-bit key) header.d=tuxedocomputers.com header.i=@tuxedocomputers.com header.b=qbhekwSq; arc=none smtp.client-ip=157.90.84.7 Authentication-Results: smtp.subspace.kernel.org; dmarc=pass (p=quarantine dis=none) header.from=tuxedocomputers.com Authentication-Results: smtp.subspace.kernel.org; spf=pass smtp.mailfrom=tuxedocomputers.com Authentication-Results: smtp.subspace.kernel.org; dkim=pass (1024-bit key) header.d=tuxedocomputers.com header.i=@tuxedocomputers.com header.b="qbhekwSq" Received: from tguttzeit-tuxedo.fritz.box (ip5f5bfe92.dynamic.kabel-deutschland.de [95.91.254.146]) (Authenticated sender: t.guttzeit@tuxedocomputers.com) by mail.tuxedocomputers.com (Postfix) with ESMTPA id 0BAC82FC0050; Thu, 19 Feb 2026 14:02:33 +0100 (CET) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=tuxedocomputers.com; s=default; t=1771506161; h=from:from:reply-to:subject:subject:date:date:message-id:message-id: to:to:cc:cc:mime-version:mime-version: content-transfer-encoding:content-transfer-encoding; bh=BJU1LIzeGSiNVCd+tNfiAQMD27bbOgbU9Zu/sUjAGGE=; b=qbhekwSqo89D+0oLS0VYn0Y+tE2eN0xrkjRx+sNi4GFeO/4l0EMieJshyqELqThDM0dUps JYhlGx32LJ1hY9chDV5UnSnAMMbfJEChVR+z9qngmJVefsGnJA28wg9TveAJhnvvvdKcmi njEcBkUWgMoysIe9TC4iGwc9GZHmhBs= Authentication-Results: mail.tuxedocomputers.com; auth=pass smtp.auth=t.guttzeit@tuxedocomputers.com smtp.mailfrom=tgu@tuxedocomputers.com From: Tim Guttzeit To: Jiri Kosina , Benjamin Tissoires Cc: wse@tuxedocomputers.com, Tim Guttzeit , linux-kernel@vger.kernel.org, linux-input@vger.kernel.org Subject: [PATCH] HID: generic: add LampArray support via hid-lamparray helper Date: Thu, 19 Feb 2026 14:02:10 +0100 Message-ID: <20260219130217.2042972-1-tgu@tuxedocomputers.com> X-Mailer: git-send-email 2.43.0 Precedence: bulk X-Mailing-List: linux-kernel@vger.kernel.org List-Id: List-Subscribe: List-Unsubscribe: MIME-Version: 1.0 Content-Transfer-Encoding: quoted-printable Content-Type: text/plain; charset="utf-8" Add a new hid-lamparray helper module and integrate it with the hid-generic driver. The helper provides basic support for devices exposing a Lighting/LampArray application collection (usage page 0x59) and registers a single-zone RGB LED representation via the LED subsystem. hid-generic now checks for LampArray support after hid_parse() and optionally registers a lamparray instance. Failures in the helper do not abort device probe to keep the device functional. LampArray resources are released on driver remove. Signed-off-by: Tim Guttzeit --- drivers/hid/Makefile | 1 + drivers/hid/hid-generic.c | 41 +- drivers/hid/hid-lamparray.c | 855 ++++++++++++++++++++++++++++++++++++ drivers/hid/hid-lamparray.h | 68 +++ 4 files changed, 963 insertions(+), 2 deletions(-) create mode 100644 drivers/hid/hid-lamparray.c create mode 100644 drivers/hid/hid-lamparray.h diff --git a/drivers/hid/Makefile b/drivers/hid/Makefile index 361a7daedeb8..e6903be9c4df 100644 --- a/drivers/hid/Makefile +++ b/drivers/hid/Makefile @@ -12,6 +12,7 @@ obj-$(CONFIG_HID) +=3D hid.o obj-$(CONFIG_UHID) +=3D uhid.o =20 obj-$(CONFIG_HID_GENERIC) +=3D hid-generic.o +obj-$(CONFIG_HID_GENERIC) +=3D hid-lamparray.o =20 hid-$(CONFIG_HIDRAW) +=3D hidraw.o =20 diff --git a/drivers/hid/hid-generic.c b/drivers/hid/hid-generic.c index c2de916747de..650c2121b403 100644 --- a/drivers/hid/hid-generic.c +++ b/drivers/hid/hid-generic.c @@ -20,6 +20,7 @@ #include =20 #include +#include "hid-lamparray.h" =20 static struct hid_driver hid_generic; =20 @@ -31,7 +32,7 @@ static int __check_hid_generic(struct device_driver *drv,= void *data) if (hdrv =3D=3D &hid_generic) return 0; =20 - return hid_match_device(hdev, hdrv) !=3D NULL; + return !!hid_match_device(hdev, hdrv); } =20 static bool hid_generic_match(struct hid_device *hdev, @@ -60,6 +61,7 @@ static int hid_generic_probe(struct hid_device *hdev, const struct hid_device_id *id) { int ret; + struct lamparray *la =3D NULL; =20 hdev->quirks |=3D HID_QUIRK_INPUT_PER_APP; =20 @@ -67,7 +69,31 @@ static int hid_generic_probe(struct hid_device *hdev, if (ret) return ret; =20 - return hid_hw_start(hdev, HID_CONNECT_DEFAULT); + ret =3D hid_hw_start(hdev, HID_CONNECT_DEFAULT); + if (ret) + return ret; + + /* + * Optional: attach LampArray support if present. + * Never fail probe on LampArray errors; keep device functional. + */ + if (lamparray_is_supported_device(hdev)) { + const struct lamparray_init_state init =3D { + .r =3D 0xff, + .g =3D 0xff, + .b =3D 0xff, + .brightness =3D LED_FULL, + }; + + la =3D lamparray_register(hdev, &init); + if (IS_ERR(la)) { + hid_warn(hdev, "LampArray init failed: %ld\n", PTR_ERR(la)); + la =3D NULL; + } + } + + hid_set_drvdata(hdev, la); + return 0; } =20 static int hid_generic_reset_resume(struct hid_device *hdev) @@ -78,6 +104,16 @@ static int hid_generic_reset_resume(struct hid_device *= hdev) return 0; } =20 +static void hid_generic_remove(struct hid_device *hdev) +{ + struct lamparray *la =3D hid_get_drvdata(hdev); + + if (la) + lamparray_unregister(la); + + hid_hw_stop(hdev); +} + static const struct hid_device_id hid_table[] =3D { { HID_DEVICE(HID_BUS_ANY, HID_GROUP_ANY, HID_ANY_ID, HID_ANY_ID) }, { } @@ -90,6 +126,7 @@ static struct hid_driver hid_generic =3D { .match =3D hid_generic_match, .probe =3D hid_generic_probe, .reset_resume =3D hid_generic_reset_resume, + .remove =3D hid_generic_remove, }; module_hid_driver(hid_generic); =20 diff --git a/drivers/hid/hid-lamparray.c b/drivers/hid/hid-lamparray.c new file mode 100644 index 000000000000..5be46fff0191 --- /dev/null +++ b/drivers/hid/hid-lamparray.c @@ -0,0 +1,855 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * hid-lamparray.c - HID LampArray helper module (single-zone RGB) + * + * Helper module for HID drivers supporting devices that expose a + * Lighting and Illumination (LampArray) application collection + * (usage page 0x59). + * + * The module provides a minimal integration with the LED subsystem + * and treats the device as a single zone: all lamps share one RGB + * value and a global brightness level. It does not implement multi- + * zone layouts or hardware effects. + * + * + * If enabled, one multicolor LED class device is registered under + * /sys/class/leds/ to expose the single-zone RGB control. + * + * The use_leds_uapi sysfs attribute is attached directly to the HID device + * under /sys/bus/hid/devices//use_leds_uapi.Writing 0 to use_leds= _uapi + * unregisters the LED class device. The last state is kept cached. Writin= g 1 + * registers it again and restores the cached state to hardware. + * + * State is cached as last known RGB + brightness. Setting sends a HID + * SET_REPORT. Getting issues a HID GET_REPORT and updates the cache on + * mismatch. Since the device is handled as single-zone, readback only que= ries + * lamp 0 when a lamp range is available. + * + * The module does not bind to devices on its own. Instead, a HID + * driver may query support via lamparray_is_supported_device() after + * hid_parse() and create an instance using lamparray_register(). + * + * Copyright (C) 2026 Tim Guttzeit + */ + +#include "hid-lamparray.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +/* Constants */ + +/* HID usages (LampArray, etc.) */ +#define HID_LIGHTING_ILLUMINATION_USAGE_PAGE 0x0059 + +#define HID_LAIP_LAMP_COUNT 0x0003 +#define HID_LAIP_LAMP_ID 0x0021 +#define HID_LAIP_RED_UPDATE_CHANNEL 0x0051 +#define HID_LAIP_GREEN_UPDATE_CHANNEL 0x0052 +#define HID_LAIP_BLUE_UPDATE_CHANNEL 0x0053 +#define HID_LAIP_INTENSITY_UPDATE_CHANNEL 0x0054 +#define HID_LAIP_LAMP_ID_START 0x0061 +#define HID_LAIP_LAMP_ID_END 0x0062 +#define HID_LAIP_AUTONOMOUS_MODE 0x0071 + +#define HID_APPLICATION_COLLECTION_USAGE_TYPE 0x0001 + +/* Device state */ + +struct lamparray_quirks { + unsigned long flags; + int fixed_lamp_count; +}; + +#define LAMPARRAY_QUIRK_LAMPCOUNT_FIXED BIT(0) + +struct lamparray_quirk_entry { + u16 vendor; + u16 product; + unsigned long flags; + int fixed_lamp_count; +}; + +static const struct lamparray_quirk_entry lamparray_quirk_table[] =3D { + { 0xcafe, 0x4005, LAMPARRAY_QUIRK_LAMPCOUNT_FIXED, 12 }, + {} +}; + +static const struct lamparray_quirk_entry * +lamparray_lookup_quirks(struct hid_device *hdev) +{ + const struct lamparray_quirk_entry *e; + + for (e =3D lamparray_quirk_table; e->vendor; e++) { + if (hdev->vendor =3D=3D e->vendor && hdev->product =3D=3D e->product) + return e; + } + return NULL; +} + +struct lamparray_device { + const struct lamparray_quirk_entry *quirks; + + struct hid_device *hdev; + struct hid_report *update_report; + + struct hid_field *red_field; + int red_index; + struct hid_field *green_field; + int green_index; + struct hid_field *blue_field; + int blue_index; + struct hid_field *intensity_field; + int intensity_index; + + struct hid_report *autonomous_report; + struct hid_field *autonomous_field; + + struct hid_field *range_start_field; + int range_start_index; + + struct hid_field *range_end_field; + int range_end_index; + + struct hid_field *lamp_count_field; + int lamp_count; + int lamp_count_index; + + struct led_classdev_mc mc_cdev; + struct mc_subled subleds[3]; + + struct mutex lock; /* protects cached state and HID access */ + + u8 last_r; + u8 last_g; + u8 last_b; + enum led_brightness last_brightness; + + bool use_leds_uapi; + bool led_registered; +}; + +/* + * Opaque handle exposed to callers via the header. + * Keep the actual state in lamparray_device, but return a stable pointer. + */ +struct lamparray { + struct lamparray_device ldev; +}; + +static DEFINE_XARRAY(lamparray_by_hdev); + +/* HID helper functions */ + +#ifdef DEBUG +static void lamparray_dump_reports(struct hid_device *hdev) +{ + struct hid_report_enum *re; + struct hid_report *report; + int i, j, report_type; + + for (report_type =3D 0; report_type < HID_REPORT_TYPES; report_type++) { + re =3D &hdev->report_enum[report_type]; + hid_dbg(hdev, "Dumping reports for report type %d", + report_type); + list_for_each_entry(report, &re->report_list, list) { + hid_dbg(hdev, + "lamparray: report id=3D%u type=3D%d maxfield=3D%u\n", + report->id, report->type, report->maxfield); + + for (i =3D 0; i < report->maxfield; i++) { + struct hid_field *field =3D report->field[i]; + + for (j =3D 0; j < field->maxusage; j++) { + u32 usage =3D field->usage[j].hid; + u16 page =3D usage >> 16; + u16 id =3D usage & 0xFFFF; + + hid_dbg(hdev, + "lamparray: report %u field %d usage[%d]: page=3D0x%04x id=3D0x%04x\= n", + report->id, i, j, page, id); + } + } + } + } +} +#else +static inline void lamparray_dump_reports(struct hid_device *hdev) +{} +#endif + +static int lamparray_read_lamp_count(struct lamparray_device *ldev) +{ + struct hid_device *hdev =3D ldev->hdev; + struct hid_report *report; + + if (ldev->quirks && + (ldev->quirks->flags & LAMPARRAY_QUIRK_LAMPCOUNT_FIXED)) { + ldev->lamp_count =3D ldev->quirks->fixed_lamp_count; + hid_dbg(hdev, "LampCount from quirk: %d\n", ldev->lamp_count); + return 0; + } + if (!ldev->lamp_count_field) { + hid_warn(hdev, "No LampCount field found\n"); + return -ENODEV; + } + + report =3D ldev->lamp_count_field->report; + + if (!report) { + hid_warn(hdev, "LampCount field has no report\n"); + return -ENODEV; + } + hid_hw_request(hdev, report, HID_REQ_GET_REPORT); + ldev->lamp_count =3D + ldev->lamp_count_field->value[ldev->lamp_count_index]; + + hid_dbg(hdev, "LampCount from device: %d\n", ldev->lamp_count); + + if (ldev->lamp_count <=3D 0) { + hid_warn(hdev, "LampCount is %d (invalid)\n", ldev->lamp_count); + return -EINVAL; + } + + return 0; +} + +static int lamparray_parse_update_report(struct lamparray_device *ldev) +{ + struct hid_device *hdev =3D ldev->hdev; + struct hid_report_enum *re; + struct hid_report *report; + struct hid_field *field; + int i, j; + + lamparray_dump_reports(hdev); + + re =3D &hdev->report_enum[HID_FEATURE_REPORT]; + + list_for_each_entry(report, &re->report_list, list) { + for (i =3D 0; i < report->maxfield; i++) { + field =3D report->field[i]; + if (!field) + continue; + + if (!field->usage || !field->maxusage) + continue; + + for (j =3D 0; j < field->maxusage; j++) { + u32 usage =3D field->usage[j].hid; + u16 page =3D usage >> 16; + u16 id =3D usage & 0xffff; + + if (page !=3D + HID_LIGHTING_ILLUMINATION_USAGE_PAGE) + continue; + switch (id) { + case HID_LAIP_LAMP_COUNT: + ldev->lamp_count_field =3D field; + ldev->lamp_count_index =3D j; + break; + case HID_LAIP_RED_UPDATE_CHANNEL: + ldev->update_report =3D report; + ldev->red_field =3D field; + ldev->red_index =3D j; + break; + case HID_LAIP_GREEN_UPDATE_CHANNEL: + ldev->update_report =3D report; + ldev->green_field =3D field; + ldev->green_index =3D j; + break; + case HID_LAIP_BLUE_UPDATE_CHANNEL: + ldev->update_report =3D report; + ldev->blue_field =3D field; + ldev->blue_index =3D j; + break; + case HID_LAIP_INTENSITY_UPDATE_CHANNEL: + ldev->update_report =3D report; + ldev->intensity_field =3D field; + ldev->intensity_index =3D j; + break; + case HID_LAIP_LAMP_ID_START: + ldev->range_start_field =3D field; + ldev->range_start_index =3D j; + break; + case HID_LAIP_LAMP_ID_END: + ldev->range_end_field =3D field; + ldev->range_end_index =3D j; + break; + case HID_LAIP_AUTONOMOUS_MODE: + ldev->autonomous_field =3D field; + ldev->autonomous_report =3D report; + break; + default: + break; + } + } + } + } + + if (!ldev->update_report || !ldev->red_field || !ldev->green_field || + !ldev->blue_field || !ldev->autonomous_report || !ldev->autonomous_fi= eld) + return -ENODEV; + + return 0; +} + +static int lamparray_hw_set_autonomous(struct lamparray_device *ldev, + bool enable) +{ + struct hid_device *hdev =3D ldev->hdev; + struct hid_field *field =3D ldev->autonomous_field; + struct hid_report *report =3D ldev->autonomous_report; + + if (!field || !report) + return -ENODEV; + + field->value[0] =3D enable ? 1 : 0; + + hid_hw_request(hdev, report, HID_REQ_SET_REPORT); + return 0; +} + +static int lamparray_hw_set_state(struct lamparray_device *ldev, u8 r, u8 = g, + u8 b, u8 intensity) +{ + struct hid_device *hdev =3D ldev->hdev; + struct hid_report *report =3D ldev->update_report; + int i, j; + + if (!report || !ldev->red_field || !ldev->green_field || + !ldev->blue_field || !ldev->intensity_field) + return -ENODEV; + + if (ldev->range_start_field && ldev->range_end_field) { + ldev->range_start_field->value[ldev->range_start_index] =3D 0; + ldev->range_end_field->value[ldev->range_end_index] =3D ldev->lamp_count= - 1; + } + + for (i =3D 0; i < report->maxfield; i++) { + struct hid_field *field =3D report->field[i]; + + if (!field || !field->usage || !field->maxusage) + continue; + + for (j =3D 0; j < field->maxusage; j++) { + u32 usage =3D field->usage[j].hid; + u16 page =3D usage >> 16; + u16 id =3D usage & 0xffff; + + if (page !=3D HID_LIGHTING_ILLUMINATION_USAGE_PAGE) + continue; + + switch (id) { + case HID_LAIP_RED_UPDATE_CHANNEL: + field->value[j] =3D r; + break; + case HID_LAIP_GREEN_UPDATE_CHANNEL: + field->value[j] =3D g; + break; + case HID_LAIP_BLUE_UPDATE_CHANNEL: + field->value[j] =3D b; + break; + case HID_LAIP_INTENSITY_UPDATE_CHANNEL: + field->value[j] =3D intensity; + break; + default: + break; + } + } + } + + hid_hw_request(hdev, report, HID_REQ_SET_REPORT); + return 0; +} + +static int lamparray_hw_get_state(struct lamparray_device *ldev, u8 *r, u8= *g, + u8 *b, enum led_brightness *brightness) +{ + struct hid_device *hdev =3D ldev->hdev; + struct hid_report *report =3D ldev->update_report; + + if (!report || !ldev->red_field || !ldev->green_field || + !ldev->blue_field || !ldev->intensity_field) + return -ENODEV; + + if (!r || !g || !b || !brightness) + return -EINVAL; + + /* Single-zone: Reading lamp 0 only suffices */ + if (ldev->range_start_field && ldev->range_end_field) { + ldev->range_start_field->value[ldev->range_start_index] =3D 0; + ldev->range_end_field->value[ldev->range_end_index] =3D 0; + } + + hid_hw_request(hdev, report, HID_REQ_GET_REPORT); + + *r =3D ldev->red_field->value[ldev->red_index]; + *g =3D ldev->green_field->value[ldev->green_index]; + *b =3D ldev->blue_field->value[ldev->blue_index]; + *brightness =3D ldev->intensity_field->value[ldev->intensity_index]; + + return 0; +} + +/* Helper functions */ + +static int lamparray_restore_state(struct lamparray_device *ldev) +{ + u8 r, g, b; + int ret; + enum led_brightness brightness; + + mutex_lock(&ldev->lock); + + if (!ldev->use_leds_uapi) { + mutex_unlock(&ldev->lock); + return 0; + } + + r =3D ldev->last_r; + g =3D ldev->last_g; + b =3D ldev->last_b; + brightness =3D ldev->last_brightness; + + ldev->mc_cdev.subled_info[0].brightness =3D r; + ldev->mc_cdev.subled_info[1].brightness =3D g; + ldev->mc_cdev.subled_info[2].brightness =3D b; + ldev->mc_cdev.led_cdev.brightness =3D brightness; + + mutex_unlock(&ldev->lock); + + ret =3D lamparray_hw_set_state(ldev, r, g, b, brightness); + return ret; +} + +/* LEDs API */ + +static int lamparray_led_brightness_set(struct led_classdev *cdev, + enum led_brightness brightness) +{ + struct led_classdev_mc *mc =3D lcdev_to_mccdev(cdev); + struct lamparray_device *ldev =3D + container_of(mc, struct lamparray_device, mc_cdev); + struct lamparray *la =3D container_of(ldev, struct lamparray, ldev); + u8 r, g, b; + int ret; + + if (!la) + return -ENODEV; + ldev =3D &la->ldev; + + ret =3D led_mc_calc_color_components(mc, brightness); + if (ret) + return ret; + + r =3D mc->subled_info[0].brightness; + g =3D mc->subled_info[1].brightness; + b =3D mc->subled_info[2].brightness; + + ret =3D lamparray_hw_set_state(ldev, r, g, b, brightness); + if (ret) + hid_err(ldev->hdev, "Failed to send LampArray update: %d\n", + ret); + + mutex_lock(&ldev->lock); + ldev->last_r =3D r; + ldev->last_g =3D g; + ldev->last_b =3D b; + ldev->last_brightness =3D brightness; + mutex_unlock(&ldev->lock); + + return 0; +} + +static enum led_brightness +lamparray_led_brightness_get(struct led_classdev *cdev) +{ + struct led_classdev_mc *mc =3D lcdev_to_mccdev(cdev); + struct lamparray_device *ldev =3D + container_of(mc, struct lamparray_device, mc_cdev); + enum led_brightness brightness; + struct lamparray *la =3D container_of(ldev, struct lamparray, ldev); + u8 rr, gg, bb; + enum led_brightness br; + int ret; + + /* Default: cache (also used while registering LED classdev) */ + mutex_lock(&ldev->lock); + brightness =3D ldev->last_brightness; + mutex_unlock(&ldev->lock); + + /* Only do HID readback after registration completed */ + if (READ_ONCE(ldev->led_registered)) { + if (!la) + return brightness; + ldev =3D &la->ldev; + + ret =3D lamparray_hw_get_state(ldev, &rr, &gg, &bb, &br); + if (ret) { + hid_warn(ldev->hdev, + "Failed to read LampArray state (%d), using cached brightness %u\n", + ret, brightness); + return brightness; + } + + mutex_lock(&ldev->lock); + if (ldev->last_r !=3D rr || ldev->last_g !=3D gg || + ldev->last_b !=3D bb || ldev->last_brightness !=3D br) { + ldev->last_r =3D rr; + ldev->last_g =3D gg; + ldev->last_b =3D bb; + ldev->last_brightness =3D br; + + if (ldev->led_registered && ldev->mc_cdev.subled_info) { + ldev->mc_cdev.subled_info[0].brightness =3D rr; + ldev->mc_cdev.subled_info[1].brightness =3D gg; + ldev->mc_cdev.subled_info[2].brightness =3D bb; + } + } + mutex_unlock(&ldev->lock); + return br; + } + return brightness; +} + +static int lamparray_register_led(struct lamparray_device *ldev) +{ + struct device *dev =3D &ldev->hdev->dev; + struct led_classdev *cdev =3D &ldev->mc_cdev.led_cdev; + u8 r_i, g_i, b_i; + int ret; + + mutex_lock(&ldev->lock); + + if (ldev->led_registered) { + mutex_unlock(&ldev->lock); + return 0; + } + + if (!cdev->name) { + cdev->name =3D + devm_kasprintf(dev, GFP_KERNEL, "%s", dev_name(dev)); + if (!cdev->name) { + mutex_unlock(&ldev->lock); + return -ENOMEM; + } + } + + cdev->max_brightness =3D 255; + cdev->brightness_set_blocking =3D lamparray_led_brightness_set; + cdev->brightness_get =3D lamparray_led_brightness_get; + cdev->brightness =3D ldev->last_brightness; + + ldev->subleds[0].color_index =3D LED_COLOR_ID_RED; + ldev->subleds[1].color_index =3D LED_COLOR_ID_GREEN; + ldev->subleds[2].color_index =3D LED_COLOR_ID_BLUE; + + /* + * Initialize the color mix (multi_intensity) from the last known HW/init + * state so that writing only /brightness scales the expected default col= or + * instead of white. + * + * If last_brightness is non-zero, treat last_r/g/b as per-channel + * brightness and normalize back to intensities (0..255). + * If last_brightness is zero, keep last_r/g/b as the intended mix. + */ + if (ldev->last_brightness) { + r_i =3D (u8)min_t(unsigned int, 255, + (ldev->last_r * 255u) / ldev->last_brightness); + g_i =3D (u8)min_t(unsigned int, 255, + (ldev->last_g * 255u) / ldev->last_brightness); + b_i =3D (u8)min_t(unsigned int, 255, + (ldev->last_b * 255u) / ldev->last_brightness); + } else { + r_i =3D ldev->last_r; + g_i =3D ldev->last_g; + b_i =3D ldev->last_b; + } + + ldev->subleds[0].intensity =3D r_i; + ldev->subleds[1].intensity =3D g_i; + ldev->subleds[2].intensity =3D b_i; + + ldev->mc_cdev.subled_info =3D ldev->subleds; + ldev->mc_cdev.num_colors =3D ARRAY_SIZE(ldev->subleds); + + /* Ensure subled_info[].brightness matches intensity + brightness */ + led_mc_calc_color_components(&ldev->mc_cdev, cdev->brightness); + + ldev->mc_cdev.subled_info =3D ldev->subleds; + ldev->mc_cdev.num_colors =3D ARRAY_SIZE(ldev->subleds); + + mutex_unlock(&ldev->lock); + + ret =3D led_classdev_multicolor_register(dev, &ldev->mc_cdev); + if (ret) + return ret; + + mutex_lock(&ldev->lock); + ldev->led_registered =3D true; + mutex_unlock(&ldev->lock); + + return 0; +} + +static void lamparray_unregister_led(struct lamparray_device *ldev) +{ + bool was_registered; + + mutex_lock(&ldev->lock); + was_registered =3D ldev->led_registered; + ldev->led_registered =3D false; + mutex_unlock(&ldev->lock); + + if (!was_registered) + return; + + led_classdev_multicolor_unregister(&ldev->mc_cdev); +} + +/* Sysfs */ + +static struct lamparray_device * +lamparray_ldev_from_sysfs_dev(struct device *dev) +{ + struct hid_device *hdev =3D to_hid_device(dev); + + return xa_load(&lamparray_by_hdev, (unsigned long)hdev); +} + +static ssize_t use_leds_uapi_show(struct device *dev, + struct device_attribute *attr, char *buf) +{ + struct lamparray_device *ldev =3D lamparray_ldev_from_sysfs_dev(dev); + + if (!ldev) + return -ENODEV; + + return sysfs_emit(buf, "%d\n", ldev->use_leds_uapi); +} + +static ssize_t use_leds_uapi_store(struct device *dev, + struct device_attribute *attr, + const char *buf, size_t count) +{ + struct lamparray_device *ldev =3D lamparray_ldev_from_sysfs_dev(dev); + int val; + int old_val; + int ret; + + if (!ldev) + return -ENODEV; + + ret =3D kstrtoint(buf, 0, &val); + if (ret) + return ret; + + if (val !=3D 0 && val !=3D 1) + return -EINVAL; + + mutex_lock(&ldev->lock); + old_val =3D ldev->use_leds_uapi; + + if (val =3D=3D old_val) { + mutex_unlock(&ldev->lock); + return count; + } + + ldev->use_leds_uapi =3D val; + mutex_unlock(&ldev->lock); + + if (val =3D=3D 1) { + ret =3D lamparray_register_led(ldev); + if (ret) { + mutex_lock(&ldev->lock); + ldev->use_leds_uapi =3D old_val; + mutex_unlock(&ldev->lock); + return ret; + } + ret =3D lamparray_restore_state(ldev); + if (ret) { + hid_err(ldev->hdev, "Could not restore state: %d", ret); + return ret; + } + + } else { + lamparray_unregister_led(ldev); + } + + return count; +} +static DEVICE_ATTR_RW(use_leds_uapi); + +static struct attribute *lamparray_attrs[] =3D { + &dev_attr_use_leds_uapi.attr, + NULL, +}; + +static const struct attribute_group lamparray_attr_group =3D { + .attrs =3D lamparray_attrs, +}; + +static int lamparray_register_sysfs(struct lamparray_device *ldev) +{ + struct device *dev =3D &ldev->hdev->dev; + int ret; + + ret =3D sysfs_create_group(&dev->kobj, &lamparray_attr_group); + if (ret) + hid_err(ldev->hdev, + "Failed to create lamparray sysfs group: %d\n", ret); + + return ret; +} + +static void lamparray_remove_sysfs(struct lamparray_device *ldev) +{ + sysfs_remove_group(&ldev->hdev->dev.kobj, &lamparray_attr_group); +} + +/* Public API */ + +bool lamparray_is_supported_device(struct hid_device *hdev) +{ + unsigned int i; + + hid_dbg(hdev, "lamparray: walking %u collections\n", + hdev->maxcollection); + + for (i =3D 0; i < hdev->maxcollection; i++) { + struct hid_collection *col =3D &hdev->collection[i]; + u16 page =3D (col->usage & HID_USAGE_PAGE) >> 16; + u16 code =3D col->usage & HID_USAGE; + + hid_dbg(hdev, + "lamparray: collection[%u]: type=3D%u level=3D%u usage=3D0x%08x page= =3D0x%04x code=3D0x%04x\n", + i, col->type, col->level, col->usage, page, code); + + if (col->type =3D=3D HID_COLLECTION_APPLICATION && + page =3D=3D HID_LIGHTING_ILLUMINATION_USAGE_PAGE && + code =3D=3D HID_APPLICATION_COLLECTION_USAGE_TYPE) { + return true; + } + } + return false; +} +EXPORT_SYMBOL_GPL(lamparray_is_supported_device); + +struct lamparray * +lamparray_register(struct hid_device *hdev, + const struct lamparray_init_state *led_init_state) +{ + int ret; + struct lamparray *la; + struct lamparray_device *ldev; + + if (!hdev) + return ERR_PTR(-ENODEV); + + la =3D kzalloc(sizeof(*la), GFP_KERNEL); + if (!la) + return ERR_PTR(-ENOMEM); + + ldev =3D &la->ldev; + + mutex_init(&ldev->lock); + ldev->hdev =3D hdev; + ldev->quirks =3D lamparray_lookup_quirks(hdev); + ldev->use_leds_uapi =3D 1; + ldev->led_registered =3D false; + if (!led_init_state) { + ldev->last_r =3D 255; + ldev->last_g =3D 255; + ldev->last_b =3D 255; + ldev->last_brightness =3D LED_OFF; + } else { + ldev->last_r =3D led_init_state->r; + ldev->last_g =3D led_init_state->g; + ldev->last_b =3D led_init_state->b; + ldev->last_brightness =3D led_init_state->brightness; + } + ret =3D lamparray_parse_update_report(ldev); + if (ret) { + hid_err(hdev, "No LampArray update report found: %d\n", ret); + goto err_free; + } + + ret =3D lamparray_read_lamp_count(ldev); + if (ret) { + hid_err(hdev, + "Could not determine LampCount. This device needs a quirk for a fixed L= ampCount: %d\n", + ret); + goto err_unregister_led; + } + + ret =3D lamparray_register_led(ldev); + if (ret) { + hid_warn(hdev, "Failed to register LED UAPI: %d\n", ret); + mutex_lock(&ldev->lock); + ldev->use_leds_uapi =3D 0; + mutex_unlock(&ldev->lock); + } + + ret =3D xa_err(xa_store(&lamparray_by_hdev, (unsigned long)hdev, ldev, + GFP_KERNEL)); + if (ret) + goto err_unregister_led; + + ret =3D lamparray_register_sysfs(ldev); + if (ret) + goto err_xa_erase; + + ret =3D lamparray_hw_set_autonomous(ldev, false); + if (ret) { + hid_err(hdev, "Could not disable autonomous mode: %d", ret); + goto err_remove_sysfs; + } + + hid_info(hdev, "LampArray device registered\n"); + + ret =3D lamparray_restore_state(ldev); + if (ret) { + hid_err(hdev, "Failed to set standard state: %d", ret); + goto err_remove_sysfs; + } + return la; + +err_remove_sysfs: + lamparray_remove_sysfs(ldev); +err_xa_erase: + xa_erase(&lamparray_by_hdev, (unsigned long)hdev); +err_unregister_led: + lamparray_unregister_led(ldev); +err_free: + kfree(la); + return ERR_PTR(ret); +} +EXPORT_SYMBOL_GPL(lamparray_register); + +void lamparray_unregister(struct lamparray *la) +{ + struct lamparray_device *ldev; + + if (!la) + return; + + ldev =3D &la->ldev; + + lamparray_unregister_led(ldev); + lamparray_remove_sysfs(ldev); + xa_erase(&lamparray_by_hdev, (unsigned long)ldev->hdev); + + kfree(la); +} +EXPORT_SYMBOL_GPL(lamparray_unregister); + +MODULE_LICENSE("GPL"); +MODULE_DESCRIPTION("HID LampArray helper module (single-zone RGB)"); diff --git a/drivers/hid/hid-lamparray.h b/drivers/hid/hid-lamparray.h new file mode 100644 index 000000000000..b786ca00c404 --- /dev/null +++ b/drivers/hid/hid-lamparray.h @@ -0,0 +1,68 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ + +#ifndef _HID_LAMPARRAY_H +#define _HID_LAMPARRAY_H + +#include +#include +#include + +struct hid_device; +struct lamparray; + +/* + * Optional initial LED state for lamparray_register(). + * Used to define the initial state of a LampArray's LEDs. + */ +struct lamparray_init_state { + u8 r; + u8 g; + u8 b; + enum led_brightness brightness; +}; + +/** + * lamparray_is_supported_device() - check whether a HID device supports L= ampArray + * @hdev: HID device to inspect + * + * Check whether the given HID device exposes a Lighting/LampArray applica= tion + * collection as defined by the HID Lighting specification. + * + * This helper can be used by HID drivers to determine whether LampArray + * functionality should be enabled for a device. + * + * Return: %true if LampArray support is detected, %false otherwise. + */ +bool lamparray_is_supported_device(struct hid_device *hdev); + +/** + * lamparray_register() - initialize LampArray support for a HID device + * @hdev: HID device + * @led_init_state: Optional LED state at init specification + * + * Allocate and initialize internal LampArray state for the given HID devi= ce. + * The function parses required HID reports and fields and registers the + * associated miscdevice and sysfs attributes. + * + * If enabled, a multicolor LED class device is also registered to expose = the + * LampArray functionality via the LED subsystem. If specified, the desired + * initial LED state is applied. If led_init_state is NULL, a default stat= e is + * applied. + * + * Return: pointer to a LampArray handle on success, or ERR_PTR() on failu= re. + */ +struct lamparray *lamparray_register(struct hid_device *hdev, + const struct lamparray_init_state *led_init_state); + +/** + * lamparray_unregister() - tear down LampArray support + * @la: LampArray handle returned by lamparray_register() + * + * Remove all resources associated with a LampArray instance. + * + * This unregisters the LED class device (if present), removes the miscdev= ice + * and sysfs interfaces and frees all internal state associated with @la. + */ +void lamparray_unregister(struct lamparray *la); + +#endif --=20 2.43.0