162306a36Sopenharmony_ci// SPDX-License-Identifier: GPL-2.0-or-later
262306a36Sopenharmony_ci/*
362306a36Sopenharmony_ci * Character line display core support
462306a36Sopenharmony_ci *
562306a36Sopenharmony_ci * Copyright (C) 2016 Imagination Technologies
662306a36Sopenharmony_ci * Author: Paul Burton <paul.burton@mips.com>
762306a36Sopenharmony_ci *
862306a36Sopenharmony_ci * Copyright (C) 2021 Glider bv
962306a36Sopenharmony_ci */
1062306a36Sopenharmony_ci
1162306a36Sopenharmony_ci#include <generated/utsrelease.h>
1262306a36Sopenharmony_ci
1362306a36Sopenharmony_ci#include <linux/device.h>
1462306a36Sopenharmony_ci#include <linux/module.h>
1562306a36Sopenharmony_ci#include <linux/slab.h>
1662306a36Sopenharmony_ci#include <linux/string.h>
1762306a36Sopenharmony_ci#include <linux/sysfs.h>
1862306a36Sopenharmony_ci#include <linux/timer.h>
1962306a36Sopenharmony_ci
2062306a36Sopenharmony_ci#include "line-display.h"
2162306a36Sopenharmony_ci
2262306a36Sopenharmony_ci#define DEFAULT_SCROLL_RATE	(HZ / 2)
2362306a36Sopenharmony_ci
2462306a36Sopenharmony_ci/**
2562306a36Sopenharmony_ci * linedisp_scroll() - scroll the display by a character
2662306a36Sopenharmony_ci * @t: really a pointer to the private data structure
2762306a36Sopenharmony_ci *
2862306a36Sopenharmony_ci * Scroll the current message along the display by one character, rearming the
2962306a36Sopenharmony_ci * timer if required.
3062306a36Sopenharmony_ci */
3162306a36Sopenharmony_cistatic void linedisp_scroll(struct timer_list *t)
3262306a36Sopenharmony_ci{
3362306a36Sopenharmony_ci	struct linedisp *linedisp = from_timer(linedisp, t, timer);
3462306a36Sopenharmony_ci	unsigned int i, ch = linedisp->scroll_pos;
3562306a36Sopenharmony_ci	unsigned int num_chars = linedisp->num_chars;
3662306a36Sopenharmony_ci
3762306a36Sopenharmony_ci	/* update the current message string */
3862306a36Sopenharmony_ci	for (i = 0; i < num_chars;) {
3962306a36Sopenharmony_ci		/* copy as many characters from the string as possible */
4062306a36Sopenharmony_ci		for (; i < num_chars && ch < linedisp->message_len; i++, ch++)
4162306a36Sopenharmony_ci			linedisp->buf[i] = linedisp->message[ch];
4262306a36Sopenharmony_ci
4362306a36Sopenharmony_ci		/* wrap around to the start of the string */
4462306a36Sopenharmony_ci		ch = 0;
4562306a36Sopenharmony_ci	}
4662306a36Sopenharmony_ci
4762306a36Sopenharmony_ci	/* update the display */
4862306a36Sopenharmony_ci	linedisp->update(linedisp);
4962306a36Sopenharmony_ci
5062306a36Sopenharmony_ci	/* move on to the next character */
5162306a36Sopenharmony_ci	linedisp->scroll_pos++;
5262306a36Sopenharmony_ci	linedisp->scroll_pos %= linedisp->message_len;
5362306a36Sopenharmony_ci
5462306a36Sopenharmony_ci	/* rearm the timer */
5562306a36Sopenharmony_ci	if (linedisp->message_len > num_chars && linedisp->scroll_rate)
5662306a36Sopenharmony_ci		mod_timer(&linedisp->timer, jiffies + linedisp->scroll_rate);
5762306a36Sopenharmony_ci}
5862306a36Sopenharmony_ci
5962306a36Sopenharmony_ci/**
6062306a36Sopenharmony_ci * linedisp_display() - set the message to be displayed
6162306a36Sopenharmony_ci * @linedisp: pointer to the private data structure
6262306a36Sopenharmony_ci * @msg: the message to display
6362306a36Sopenharmony_ci * @count: length of msg, or -1
6462306a36Sopenharmony_ci *
6562306a36Sopenharmony_ci * Display a new message @msg on the display. @msg can be longer than the
6662306a36Sopenharmony_ci * number of characters the display can display, in which case it will begin
6762306a36Sopenharmony_ci * scrolling across the display.
6862306a36Sopenharmony_ci *
6962306a36Sopenharmony_ci * Return: 0 on success, -ENOMEM on memory allocation failure
7062306a36Sopenharmony_ci */
7162306a36Sopenharmony_cistatic int linedisp_display(struct linedisp *linedisp, const char *msg,
7262306a36Sopenharmony_ci			    ssize_t count)
7362306a36Sopenharmony_ci{
7462306a36Sopenharmony_ci	char *new_msg;
7562306a36Sopenharmony_ci
7662306a36Sopenharmony_ci	/* stop the scroll timer */
7762306a36Sopenharmony_ci	del_timer_sync(&linedisp->timer);
7862306a36Sopenharmony_ci
7962306a36Sopenharmony_ci	if (count == -1)
8062306a36Sopenharmony_ci		count = strlen(msg);
8162306a36Sopenharmony_ci
8262306a36Sopenharmony_ci	/* if the string ends with a newline, trim it */
8362306a36Sopenharmony_ci	if (msg[count - 1] == '\n')
8462306a36Sopenharmony_ci		count--;
8562306a36Sopenharmony_ci
8662306a36Sopenharmony_ci	if (!count) {
8762306a36Sopenharmony_ci		/* Clear the display */
8862306a36Sopenharmony_ci		kfree(linedisp->message);
8962306a36Sopenharmony_ci		linedisp->message = NULL;
9062306a36Sopenharmony_ci		linedisp->message_len = 0;
9162306a36Sopenharmony_ci		memset(linedisp->buf, ' ', linedisp->num_chars);
9262306a36Sopenharmony_ci		linedisp->update(linedisp);
9362306a36Sopenharmony_ci		return 0;
9462306a36Sopenharmony_ci	}
9562306a36Sopenharmony_ci
9662306a36Sopenharmony_ci	new_msg = kmemdup_nul(msg, count, GFP_KERNEL);
9762306a36Sopenharmony_ci	if (!new_msg)
9862306a36Sopenharmony_ci		return -ENOMEM;
9962306a36Sopenharmony_ci
10062306a36Sopenharmony_ci	kfree(linedisp->message);
10162306a36Sopenharmony_ci
10262306a36Sopenharmony_ci	linedisp->message = new_msg;
10362306a36Sopenharmony_ci	linedisp->message_len = count;
10462306a36Sopenharmony_ci	linedisp->scroll_pos = 0;
10562306a36Sopenharmony_ci
10662306a36Sopenharmony_ci	/* update the display */
10762306a36Sopenharmony_ci	linedisp_scroll(&linedisp->timer);
10862306a36Sopenharmony_ci
10962306a36Sopenharmony_ci	return 0;
11062306a36Sopenharmony_ci}
11162306a36Sopenharmony_ci
11262306a36Sopenharmony_ci/**
11362306a36Sopenharmony_ci * message_show() - read message via sysfs
11462306a36Sopenharmony_ci * @dev: the display device
11562306a36Sopenharmony_ci * @attr: the display message attribute
11662306a36Sopenharmony_ci * @buf: the buffer to read the message into
11762306a36Sopenharmony_ci *
11862306a36Sopenharmony_ci * Read the current message being displayed or scrolled across the display into
11962306a36Sopenharmony_ci * @buf, for reads from sysfs.
12062306a36Sopenharmony_ci *
12162306a36Sopenharmony_ci * Return: the number of characters written to @buf
12262306a36Sopenharmony_ci */
12362306a36Sopenharmony_cistatic ssize_t message_show(struct device *dev, struct device_attribute *attr,
12462306a36Sopenharmony_ci			    char *buf)
12562306a36Sopenharmony_ci{
12662306a36Sopenharmony_ci	struct linedisp *linedisp = container_of(dev, struct linedisp, dev);
12762306a36Sopenharmony_ci
12862306a36Sopenharmony_ci	return sysfs_emit(buf, "%s\n", linedisp->message);
12962306a36Sopenharmony_ci}
13062306a36Sopenharmony_ci
13162306a36Sopenharmony_ci/**
13262306a36Sopenharmony_ci * message_store() - write a new message via sysfs
13362306a36Sopenharmony_ci * @dev: the display device
13462306a36Sopenharmony_ci * @attr: the display message attribute
13562306a36Sopenharmony_ci * @buf: the buffer containing the new message
13662306a36Sopenharmony_ci * @count: the size of the message in @buf
13762306a36Sopenharmony_ci *
13862306a36Sopenharmony_ci * Write a new message to display or scroll across the display from sysfs.
13962306a36Sopenharmony_ci *
14062306a36Sopenharmony_ci * Return: the size of the message on success, else -ERRNO
14162306a36Sopenharmony_ci */
14262306a36Sopenharmony_cistatic ssize_t message_store(struct device *dev, struct device_attribute *attr,
14362306a36Sopenharmony_ci			     const char *buf, size_t count)
14462306a36Sopenharmony_ci{
14562306a36Sopenharmony_ci	struct linedisp *linedisp = container_of(dev, struct linedisp, dev);
14662306a36Sopenharmony_ci	int err;
14762306a36Sopenharmony_ci
14862306a36Sopenharmony_ci	err = linedisp_display(linedisp, buf, count);
14962306a36Sopenharmony_ci	return err ?: count;
15062306a36Sopenharmony_ci}
15162306a36Sopenharmony_ci
15262306a36Sopenharmony_cistatic DEVICE_ATTR_RW(message);
15362306a36Sopenharmony_ci
15462306a36Sopenharmony_cistatic ssize_t scroll_step_ms_show(struct device *dev,
15562306a36Sopenharmony_ci				   struct device_attribute *attr, char *buf)
15662306a36Sopenharmony_ci{
15762306a36Sopenharmony_ci	struct linedisp *linedisp = container_of(dev, struct linedisp, dev);
15862306a36Sopenharmony_ci
15962306a36Sopenharmony_ci	return sysfs_emit(buf, "%u\n", jiffies_to_msecs(linedisp->scroll_rate));
16062306a36Sopenharmony_ci}
16162306a36Sopenharmony_ci
16262306a36Sopenharmony_cistatic ssize_t scroll_step_ms_store(struct device *dev,
16362306a36Sopenharmony_ci				    struct device_attribute *attr,
16462306a36Sopenharmony_ci				    const char *buf, size_t count)
16562306a36Sopenharmony_ci{
16662306a36Sopenharmony_ci	struct linedisp *linedisp = container_of(dev, struct linedisp, dev);
16762306a36Sopenharmony_ci	unsigned int ms;
16862306a36Sopenharmony_ci
16962306a36Sopenharmony_ci	if (kstrtouint(buf, 10, &ms) != 0)
17062306a36Sopenharmony_ci		return -EINVAL;
17162306a36Sopenharmony_ci
17262306a36Sopenharmony_ci	linedisp->scroll_rate = msecs_to_jiffies(ms);
17362306a36Sopenharmony_ci	if (linedisp->message && linedisp->message_len > linedisp->num_chars) {
17462306a36Sopenharmony_ci		del_timer_sync(&linedisp->timer);
17562306a36Sopenharmony_ci		if (linedisp->scroll_rate)
17662306a36Sopenharmony_ci			linedisp_scroll(&linedisp->timer);
17762306a36Sopenharmony_ci	}
17862306a36Sopenharmony_ci
17962306a36Sopenharmony_ci	return count;
18062306a36Sopenharmony_ci}
18162306a36Sopenharmony_ci
18262306a36Sopenharmony_cistatic DEVICE_ATTR_RW(scroll_step_ms);
18362306a36Sopenharmony_ci
18462306a36Sopenharmony_cistatic struct attribute *linedisp_attrs[] = {
18562306a36Sopenharmony_ci	&dev_attr_message.attr,
18662306a36Sopenharmony_ci	&dev_attr_scroll_step_ms.attr,
18762306a36Sopenharmony_ci	NULL,
18862306a36Sopenharmony_ci};
18962306a36Sopenharmony_ciATTRIBUTE_GROUPS(linedisp);
19062306a36Sopenharmony_ci
19162306a36Sopenharmony_cistatic const struct device_type linedisp_type = {
19262306a36Sopenharmony_ci	.groups	= linedisp_groups,
19362306a36Sopenharmony_ci};
19462306a36Sopenharmony_ci
19562306a36Sopenharmony_ci/**
19662306a36Sopenharmony_ci * linedisp_register - register a character line display
19762306a36Sopenharmony_ci * @linedisp: pointer to character line display structure
19862306a36Sopenharmony_ci * @parent: parent device
19962306a36Sopenharmony_ci * @num_chars: the number of characters that can be displayed
20062306a36Sopenharmony_ci * @buf: pointer to a buffer that can hold @num_chars characters
20162306a36Sopenharmony_ci * @update: Function called to update the display.  This must not sleep!
20262306a36Sopenharmony_ci *
20362306a36Sopenharmony_ci * Return: zero on success, else a negative error code.
20462306a36Sopenharmony_ci */
20562306a36Sopenharmony_ciint linedisp_register(struct linedisp *linedisp, struct device *parent,
20662306a36Sopenharmony_ci		      unsigned int num_chars, char *buf,
20762306a36Sopenharmony_ci		      void (*update)(struct linedisp *linedisp))
20862306a36Sopenharmony_ci{
20962306a36Sopenharmony_ci	static atomic_t linedisp_id = ATOMIC_INIT(-1);
21062306a36Sopenharmony_ci	int err;
21162306a36Sopenharmony_ci
21262306a36Sopenharmony_ci	memset(linedisp, 0, sizeof(*linedisp));
21362306a36Sopenharmony_ci	linedisp->dev.parent = parent;
21462306a36Sopenharmony_ci	linedisp->dev.type = &linedisp_type;
21562306a36Sopenharmony_ci	linedisp->update = update;
21662306a36Sopenharmony_ci	linedisp->buf = buf;
21762306a36Sopenharmony_ci	linedisp->num_chars = num_chars;
21862306a36Sopenharmony_ci	linedisp->scroll_rate = DEFAULT_SCROLL_RATE;
21962306a36Sopenharmony_ci
22062306a36Sopenharmony_ci	device_initialize(&linedisp->dev);
22162306a36Sopenharmony_ci	dev_set_name(&linedisp->dev, "linedisp.%lu",
22262306a36Sopenharmony_ci		     (unsigned long)atomic_inc_return(&linedisp_id));
22362306a36Sopenharmony_ci
22462306a36Sopenharmony_ci	/* initialise a timer for scrolling the message */
22562306a36Sopenharmony_ci	timer_setup(&linedisp->timer, linedisp_scroll, 0);
22662306a36Sopenharmony_ci
22762306a36Sopenharmony_ci	err = device_add(&linedisp->dev);
22862306a36Sopenharmony_ci	if (err)
22962306a36Sopenharmony_ci		goto out_del_timer;
23062306a36Sopenharmony_ci
23162306a36Sopenharmony_ci	/* display a default message */
23262306a36Sopenharmony_ci	err = linedisp_display(linedisp, "Linux " UTS_RELEASE "       ", -1);
23362306a36Sopenharmony_ci	if (err)
23462306a36Sopenharmony_ci		goto out_del_dev;
23562306a36Sopenharmony_ci
23662306a36Sopenharmony_ci	return 0;
23762306a36Sopenharmony_ci
23862306a36Sopenharmony_ciout_del_dev:
23962306a36Sopenharmony_ci	device_del(&linedisp->dev);
24062306a36Sopenharmony_ciout_del_timer:
24162306a36Sopenharmony_ci	del_timer_sync(&linedisp->timer);
24262306a36Sopenharmony_ci	put_device(&linedisp->dev);
24362306a36Sopenharmony_ci	return err;
24462306a36Sopenharmony_ci}
24562306a36Sopenharmony_ciEXPORT_SYMBOL_GPL(linedisp_register);
24662306a36Sopenharmony_ci
24762306a36Sopenharmony_ci/**
24862306a36Sopenharmony_ci * linedisp_unregister - unregister a character line display
24962306a36Sopenharmony_ci * @linedisp: pointer to character line display structure registered previously
25062306a36Sopenharmony_ci *	      with linedisp_register()
25162306a36Sopenharmony_ci */
25262306a36Sopenharmony_civoid linedisp_unregister(struct linedisp *linedisp)
25362306a36Sopenharmony_ci{
25462306a36Sopenharmony_ci	device_del(&linedisp->dev);
25562306a36Sopenharmony_ci	del_timer_sync(&linedisp->timer);
25662306a36Sopenharmony_ci	kfree(linedisp->message);
25762306a36Sopenharmony_ci	put_device(&linedisp->dev);
25862306a36Sopenharmony_ci}
25962306a36Sopenharmony_ciEXPORT_SYMBOL_GPL(linedisp_unregister);
26062306a36Sopenharmony_ci
26162306a36Sopenharmony_ciMODULE_LICENSE("GPL");
262