// 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");