// SPDX-License-Identifier: GPL-2.0 #include #include #include #include #include #include #include #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 "); MODULE_DESCRIPTION("UART LED trigger"); MODULE_LICENSE("GPL v2");