Contributors: 3
Author Tokens Token Proportion Commits Commit Proportion
Uwe Kleine-König 692 51.49% 2 28.57%
Florian Eckert 631 46.95% 4 57.14%
Marek Behún 21 1.56% 1 14.29%
Total 1344 7


// SPDX-License-Identifier: GPL-2.0

#include <linux/completion.h>
#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 completion sysfs;
	const char *ttyname;
	struct tty_struct *tty;
	int rx, tx;
	bool mode_rx;
	bool mode_tx;
	bool mode_cts;
	bool mode_dsr;
	bool mode_dcd;
	bool mode_rng;
};

/* Indicates which state the LED should now display */
enum led_trigger_tty_state {
	TTY_LED_BLINK,
	TTY_LED_ENABLE,
	TTY_LED_DISABLE,
};

enum led_trigger_tty_modes {
	TRIGGER_TTY_RX = 0,
	TRIGGER_TTY_TX,
	TRIGGER_TTY_CTS,
	TRIGGER_TTY_DSR,
	TRIGGER_TTY_DCD,
	TRIGGER_TTY_RNG,
};

static int ledtrig_tty_wait_for_completion(struct device *dev)
{
	struct ledtrig_tty_data *trigger_data = led_trigger_get_drvdata(dev);
	int ret;

	ret = wait_for_completion_timeout(&trigger_data->sysfs,
					  msecs_to_jiffies(LEDTRIG_TTY_INTERVAL * 20));
	if (ret == 0)
		return -ETIMEDOUT;

	return ret;
}

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;
	int completion;

	reinit_completion(&trigger_data->sysfs);
	completion = ledtrig_tty_wait_for_completion(dev);
	if (completion < 0)
		return completion;

	if (trigger_data->ttyname)
		len = sprintf(buf, "%s\n", trigger_data->ttyname);

	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;
	int completion;

	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;
	}

	reinit_completion(&trigger_data->sysfs);
	completion = ledtrig_tty_wait_for_completion(dev);
	if (completion < 0)
		return completion;

	kfree(trigger_data->ttyname);
	tty_kref_put(trigger_data->tty);
	trigger_data->tty = NULL;

	trigger_data->ttyname = ttyname;

	return ret;
}
static DEVICE_ATTR_RW(ttyname);

static ssize_t ledtrig_tty_attr_show(struct device *dev, char *buf,
				     enum led_trigger_tty_modes attr)
{
	struct ledtrig_tty_data *trigger_data = led_trigger_get_drvdata(dev);
	bool state;

	switch (attr) {
	case TRIGGER_TTY_RX:
		state = trigger_data->mode_rx;
		break;
	case TRIGGER_TTY_TX:
		state = trigger_data->mode_tx;
		break;
	case TRIGGER_TTY_CTS:
		state = trigger_data->mode_cts;
		break;
	case TRIGGER_TTY_DSR:
		state = trigger_data->mode_dsr;
		break;
	case TRIGGER_TTY_DCD:
		state = trigger_data->mode_dcd;
		break;
	case TRIGGER_TTY_RNG:
		state = trigger_data->mode_rng;
		break;
	}

	return sysfs_emit(buf, "%u\n", state);
}

static ssize_t ledtrig_tty_attr_store(struct device *dev, const char *buf,
				      size_t size, enum led_trigger_tty_modes attr)
{
	struct ledtrig_tty_data *trigger_data = led_trigger_get_drvdata(dev);
	bool state;
	int ret;

	ret = kstrtobool(buf, &state);
	if (ret)
		return ret;

	switch (attr) {
	case TRIGGER_TTY_RX:
		trigger_data->mode_rx = state;
		break;
	case TRIGGER_TTY_TX:
		trigger_data->mode_tx = state;
		break;
	case TRIGGER_TTY_CTS:
		trigger_data->mode_cts = state;
		break;
	case TRIGGER_TTY_DSR:
		trigger_data->mode_dsr = state;
		break;
	case TRIGGER_TTY_DCD:
		trigger_data->mode_dcd = state;
		break;
	case TRIGGER_TTY_RNG:
		trigger_data->mode_rng = state;
		break;
	}

	return size;
}

#define DEFINE_TTY_TRIGGER(trigger_name, trigger) \
	static ssize_t trigger_name##_show(struct device *dev, \
		struct device_attribute *attr, char *buf) \
	{ \
		return ledtrig_tty_attr_show(dev, buf, trigger); \
	} \
	static ssize_t trigger_name##_store(struct device *dev, \
		struct device_attribute *attr, const char *buf, size_t size) \
	{ \
		return ledtrig_tty_attr_store(dev, buf, size, trigger); \
	} \
	static DEVICE_ATTR_RW(trigger_name)

DEFINE_TTY_TRIGGER(rx, TRIGGER_TTY_RX);
DEFINE_TTY_TRIGGER(tx, TRIGGER_TTY_TX);
DEFINE_TTY_TRIGGER(cts, TRIGGER_TTY_CTS);
DEFINE_TTY_TRIGGER(dsr, TRIGGER_TTY_DSR);
DEFINE_TTY_TRIGGER(dcd, TRIGGER_TTY_DCD);
DEFINE_TTY_TRIGGER(rng, TRIGGER_TTY_RNG);

static void ledtrig_tty_work(struct work_struct *work)
{
	struct ledtrig_tty_data *trigger_data =
		container_of(work, struct ledtrig_tty_data, dwork.work);
	enum led_trigger_tty_state state = TTY_LED_DISABLE;
	unsigned long interval = LEDTRIG_TTY_INTERVAL;
	bool invert = false;
	int status;
	int ret;

	if (!trigger_data->ttyname)
		goto out;

	/* 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;
	}

	status = tty_get_tiocm(trigger_data->tty);
	if (status > 0) {
		if (trigger_data->mode_cts) {
			if (status & TIOCM_CTS)
				state = TTY_LED_ENABLE;
		}

		if (trigger_data->mode_dsr) {
			if (status & TIOCM_DSR)
				state = TTY_LED_ENABLE;
		}

		if (trigger_data->mode_dcd) {
			if (status & TIOCM_CAR)
				state = TTY_LED_ENABLE;
		}

		if (trigger_data->mode_rng) {
			if (status & TIOCM_RNG)
				state = TTY_LED_ENABLE;
		}
	}

	/*
	 * The evaluation of rx/tx must be done after the evaluation
	 * of TIOCM_*, because rx/tx has priority.
	 */
	if (trigger_data->mode_rx || trigger_data->mode_tx) {
		struct serial_icounter_struct icount;

		ret = tty_get_icount(trigger_data->tty, &icount);
		if (ret)
			goto out;

		if (trigger_data->mode_tx && (icount.tx != trigger_data->tx)) {
			trigger_data->tx = icount.tx;
			invert = state == TTY_LED_ENABLE;
			state = TTY_LED_BLINK;
		}

		if (trigger_data->mode_rx && (icount.rx != trigger_data->rx)) {
			trigger_data->rx = icount.rx;
			invert = state == TTY_LED_ENABLE;
			state = TTY_LED_BLINK;
		}
	}

out:
	switch (state) {
	case TTY_LED_BLINK:
		led_blink_set_oneshot(trigger_data->led_cdev, &interval,
				&interval, invert);
		break;
	case TTY_LED_ENABLE:
		led_set_brightness(trigger_data->led_cdev,
				trigger_data->led_cdev->blink_brightness);
		break;
	case TTY_LED_DISABLE:
		fallthrough;
	default:
		led_set_brightness(trigger_data->led_cdev, LED_OFF);
		break;
	}

	complete_all(&trigger_data->sysfs);
	schedule_delayed_work(&trigger_data->dwork,
			      msecs_to_jiffies(LEDTRIG_TTY_INTERVAL * 2));
}

static struct attribute *ledtrig_tty_attrs[] = {
	&dev_attr_ttyname.attr,
	&dev_attr_rx.attr,
	&dev_attr_tx.attr,
	&dev_attr_cts.attr,
	&dev_attr_dsr.attr,
	&dev_attr_dcd.attr,
	&dev_attr_rng.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;

	/* Enable default rx/tx mode */
	trigger_data->mode_rx = true;
	trigger_data->mode_tx = true;

	led_set_trigger_data(led_cdev, trigger_data);

	INIT_DELAYED_WORK(&trigger_data->dwork, ledtrig_tty_work);
	trigger_data->led_cdev = led_cdev;
	init_completion(&trigger_data->sysfs);

	schedule_delayed_work(&trigger_data->dwork, 0);

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