|  | // SPDX-License-Identifier: GPL-2.0 | 
|  |  | 
|  | #include <linux/delay.h> | 
|  | #include <linux/leds.h> | 
|  | #include <linux/module.h> | 
|  | #include <linux/slab.h> | 
|  | #include <linux/tty.h> | 
|  | #include <uapi/linux/serial.h> | 
|  |  | 
|  | struct ledtrig_tty_data { | 
|  | struct led_classdev *led_cdev; | 
|  | struct delayed_work dwork; | 
|  | struct mutex mutex; | 
|  | const char *ttyname; | 
|  | struct tty_struct *tty; | 
|  | int rx, tx; | 
|  | }; | 
|  |  | 
|  | static void ledtrig_tty_restart(struct ledtrig_tty_data *trigger_data) | 
|  | { | 
|  | schedule_delayed_work(&trigger_data->dwork, 0); | 
|  | } | 
|  |  | 
|  | static ssize_t ttyname_show(struct device *dev, | 
|  | struct device_attribute *attr, char *buf) | 
|  | { | 
|  | struct ledtrig_tty_data *trigger_data = led_trigger_get_drvdata(dev); | 
|  | ssize_t len = 0; | 
|  |  | 
|  | mutex_lock(&trigger_data->mutex); | 
|  |  | 
|  | if (trigger_data->ttyname) | 
|  | len = sprintf(buf, "%s\n", trigger_data->ttyname); | 
|  |  | 
|  | mutex_unlock(&trigger_data->mutex); | 
|  |  | 
|  | return len; | 
|  | } | 
|  |  | 
|  | static ssize_t ttyname_store(struct device *dev, | 
|  | struct device_attribute *attr, const char *buf, | 
|  | size_t size) | 
|  | { | 
|  | struct ledtrig_tty_data *trigger_data = led_trigger_get_drvdata(dev); | 
|  | char *ttyname; | 
|  | ssize_t ret = size; | 
|  | bool running; | 
|  |  | 
|  | if (size > 0 && buf[size - 1] == '\n') | 
|  | size -= 1; | 
|  |  | 
|  | if (size) { | 
|  | ttyname = kmemdup_nul(buf, size, GFP_KERNEL); | 
|  | if (!ttyname) | 
|  | return -ENOMEM; | 
|  | } else { | 
|  | ttyname = NULL; | 
|  | } | 
|  |  | 
|  | mutex_lock(&trigger_data->mutex); | 
|  |  | 
|  | running = trigger_data->ttyname != NULL; | 
|  |  | 
|  | kfree(trigger_data->ttyname); | 
|  | tty_kref_put(trigger_data->tty); | 
|  | trigger_data->tty = NULL; | 
|  |  | 
|  | trigger_data->ttyname = ttyname; | 
|  |  | 
|  | mutex_unlock(&trigger_data->mutex); | 
|  |  | 
|  | if (ttyname && !running) | 
|  | ledtrig_tty_restart(trigger_data); | 
|  |  | 
|  | return ret; | 
|  | } | 
|  | static DEVICE_ATTR_RW(ttyname); | 
|  |  | 
|  | static void ledtrig_tty_work(struct work_struct *work) | 
|  | { | 
|  | struct ledtrig_tty_data *trigger_data = | 
|  | container_of(work, struct ledtrig_tty_data, dwork.work); | 
|  | struct serial_icounter_struct icount; | 
|  | int ret; | 
|  |  | 
|  | mutex_lock(&trigger_data->mutex); | 
|  |  | 
|  | if (!trigger_data->ttyname) { | 
|  | /* exit without rescheduling */ | 
|  | mutex_unlock(&trigger_data->mutex); | 
|  | return; | 
|  | } | 
|  |  | 
|  | /* try to get the tty corresponding to $ttyname */ | 
|  | if (!trigger_data->tty) { | 
|  | dev_t devno; | 
|  | struct tty_struct *tty; | 
|  | int ret; | 
|  |  | 
|  | ret = tty_dev_name_to_number(trigger_data->ttyname, &devno); | 
|  | if (ret < 0) | 
|  | /* | 
|  | * A device with this name might appear later, so keep | 
|  | * retrying. | 
|  | */ | 
|  | goto out; | 
|  |  | 
|  | tty = tty_kopen_shared(devno); | 
|  | if (IS_ERR(tty) || !tty) | 
|  | /* What to do? retry or abort */ | 
|  | goto out; | 
|  |  | 
|  | trigger_data->tty = tty; | 
|  | } | 
|  |  | 
|  | ret = tty_get_icount(trigger_data->tty, &icount); | 
|  | if (ret) { | 
|  | dev_info(trigger_data->tty->dev, "Failed to get icount, stopped polling\n"); | 
|  | mutex_unlock(&trigger_data->mutex); | 
|  | return; | 
|  | } | 
|  |  | 
|  | if (icount.rx != trigger_data->rx || | 
|  | icount.tx != trigger_data->tx) { | 
|  | led_set_brightness_sync(trigger_data->led_cdev, LED_ON); | 
|  |  | 
|  | trigger_data->rx = icount.rx; | 
|  | trigger_data->tx = icount.tx; | 
|  | } else { | 
|  | led_set_brightness_sync(trigger_data->led_cdev, LED_OFF); | 
|  | } | 
|  |  | 
|  | out: | 
|  | mutex_unlock(&trigger_data->mutex); | 
|  | schedule_delayed_work(&trigger_data->dwork, msecs_to_jiffies(100)); | 
|  | } | 
|  |  | 
|  | static struct attribute *ledtrig_tty_attrs[] = { | 
|  | &dev_attr_ttyname.attr, | 
|  | NULL | 
|  | }; | 
|  | ATTRIBUTE_GROUPS(ledtrig_tty); | 
|  |  | 
|  | static int ledtrig_tty_activate(struct led_classdev *led_cdev) | 
|  | { | 
|  | struct ledtrig_tty_data *trigger_data; | 
|  |  | 
|  | trigger_data = kzalloc(sizeof(*trigger_data), GFP_KERNEL); | 
|  | if (!trigger_data) | 
|  | return -ENOMEM; | 
|  |  | 
|  | led_set_trigger_data(led_cdev, trigger_data); | 
|  |  | 
|  | INIT_DELAYED_WORK(&trigger_data->dwork, ledtrig_tty_work); | 
|  | trigger_data->led_cdev = led_cdev; | 
|  | mutex_init(&trigger_data->mutex); | 
|  |  | 
|  | return 0; | 
|  | } | 
|  |  | 
|  | static void ledtrig_tty_deactivate(struct led_classdev *led_cdev) | 
|  | { | 
|  | struct ledtrig_tty_data *trigger_data = led_get_trigger_data(led_cdev); | 
|  |  | 
|  | cancel_delayed_work_sync(&trigger_data->dwork); | 
|  |  | 
|  | kfree(trigger_data); | 
|  | } | 
|  |  | 
|  | static struct led_trigger ledtrig_tty = { | 
|  | .name = "tty", | 
|  | .activate = ledtrig_tty_activate, | 
|  | .deactivate = ledtrig_tty_deactivate, | 
|  | .groups = ledtrig_tty_groups, | 
|  | }; | 
|  | module_led_trigger(ledtrig_tty); | 
|  |  | 
|  | MODULE_AUTHOR("Uwe Kleine-König <u.kleine-koenig@pengutronix.de>"); | 
|  | MODULE_DESCRIPTION("UART LED trigger"); | 
|  | MODULE_LICENSE("GPL v2"); |