diff options
Diffstat (limited to 'drivers/leds/trigger/ledtrig-tty.c')
-rw-r--r-- | drivers/leds/trigger/ledtrig-tty.c | 188 |
1 files changed, 188 insertions, 0 deletions
diff --git a/drivers/leds/trigger/ledtrig-tty.c b/drivers/leds/trigger/ledtrig-tty.c new file mode 100644 index 000000000..3e69a7bde --- /dev/null +++ b/drivers/leds/trigger/ledtrig-tty.c @@ -0,0 +1,188 @@ +// 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> + +#define LEDTRIG_TTY_INTERVAL 50 + +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) { + unsigned long interval = LEDTRIG_TTY_INTERVAL; + + led_blink_set_oneshot(trigger_data->led_cdev, &interval, + &interval, 0); + + trigger_data->rx = icount.rx; + trigger_data->tx = icount.tx; + } + +out: + mutex_unlock(&trigger_data->mutex); + schedule_delayed_work(&trigger_data->dwork, + msecs_to_jiffies(LEDTRIG_TTY_INTERVAL * 2)); +} + +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->ttyname); + tty_kref_put(trigger_data->tty); + trigger_data->tty = NULL; + + 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"); |