#ifndef CONFIG_PANEL_BOOT_MESSAGE
#include <generated/utsrelease.h>
#endif
#include <linux/cleanup.h>
#include <linux/device.h>
#include <linux/export.h>
#include <linux/idr.h>
#include <linux/jiffies.h>
#include <linux/kstrtox.h>
#include <linux/list.h>
#include <linux/module.h>
#include <linux/slab.h>
#include <linux/spinlock.h>
#include <linux/string.h>
#include <linux/sysfs.h>
#include <linux/timer.h>
#include <linux/map_to_7segment.h>
#include <linux/map_to_14segment.h>
#include "line-display.h"
#define DEFAULT_SCROLL_RATE (HZ / 2)
struct linedisp_attachment {
struct list_head list;
struct device *device;
struct linedisp *linedisp;
bool direct;
};
static LIST_HEAD(linedisp_attachments);
static DEFINE_SPINLOCK(linedisp_attachments_lock);
static int create_attachment(struct device *dev, struct linedisp *linedisp, bool direct)
{
struct linedisp_attachment *attachment;
attachment = kzalloc(sizeof(*attachment), GFP_KERNEL);
if (!attachment)
return -ENOMEM;
attachment->device = dev;
attachment->linedisp = linedisp;
attachment->direct = direct;
guard(spinlock)(&linedisp_attachments_lock);
list_add(&attachment->list, &linedisp_attachments);
return 0;
}
static struct linedisp *delete_attachment(struct device *dev, bool direct)
{
struct linedisp_attachment *attachment;
struct linedisp *linedisp;
guard(spinlock)(&linedisp_attachments_lock);
list_for_each_entry(attachment, &linedisp_attachments, list) {
if (attachment->device == dev &&
attachment->direct == direct)
break;
}
if (list_entry_is_head(attachment, &linedisp_attachments, list))
return NULL;
linedisp = attachment->linedisp;
list_del(&attachment->list);
kfree(attachment);
return linedisp;
}
static struct linedisp *to_linedisp(struct device *dev)
{
struct linedisp_attachment *attachment;
guard(spinlock)(&linedisp_attachments_lock);
list_for_each_entry(attachment, &linedisp_attachments, list) {
if (attachment->device == dev)
break;
}
if (list_entry_is_head(attachment, &linedisp_attachments, list))
return NULL;
return attachment->linedisp;
}
static inline bool should_scroll(struct linedisp *linedisp)
{
return linedisp->message_len > linedisp->num_chars && linedisp->scroll_rate;
}
static void linedisp_scroll(struct timer_list *t)
{
struct linedisp *linedisp = timer_container_of(linedisp, t, timer);
unsigned int i, ch = linedisp->scroll_pos;
unsigned int num_chars = linedisp->num_chars;
for (i = 0; i < num_chars;) {
for (; i < num_chars && ch < linedisp->message_len; i++, ch++)
linedisp->buf[i] = linedisp->message[ch];
ch = 0;
}
linedisp->ops->update(linedisp);
linedisp->scroll_pos++;
linedisp->scroll_pos %= linedisp->message_len;
mod_timer(&linedisp->timer, jiffies + linedisp->scroll_rate);
}
static int linedisp_display(struct linedisp *linedisp, const char *msg,
ssize_t count)
{
char *new_msg;
timer_delete_sync(&linedisp->timer);
if (count == -1)
count = strlen(msg);
if (msg[count - 1] == '\n')
count--;
if (!count) {
kfree(linedisp->message);
linedisp->message = NULL;
linedisp->message_len = 0;
memset(linedisp->buf, ' ', linedisp->num_chars);
linedisp->ops->update(linedisp);
return 0;
}
new_msg = kmemdup_nul(msg, count, GFP_KERNEL);
if (!new_msg)
return -ENOMEM;
kfree(linedisp->message);
linedisp->message = new_msg;
linedisp->message_len = count;
linedisp->scroll_pos = 0;
if (should_scroll(linedisp)) {
linedisp_scroll(&linedisp->timer);
} else {
memset(linedisp->buf, ' ', linedisp->num_chars);
memcpy(linedisp->buf, linedisp->message,
umin(linedisp->num_chars, linedisp->message_len));
linedisp->ops->update(linedisp);
}
return 0;
}
static ssize_t message_show(struct device *dev, struct device_attribute *attr,
char *buf)
{
struct linedisp *linedisp = to_linedisp(dev);
return sysfs_emit(buf, "%s\n", linedisp->message);
}
static ssize_t message_store(struct device *dev, struct device_attribute *attr,
const char *buf, size_t count)
{
struct linedisp *linedisp = to_linedisp(dev);
int err;
err = linedisp_display(linedisp, buf, count);
return err ?: count;
}
static DEVICE_ATTR_RW(message);
static ssize_t num_chars_show(struct device *dev, struct device_attribute *attr,
char *buf)
{
struct linedisp *linedisp = to_linedisp(dev);
return sysfs_emit(buf, "%u\n", linedisp->num_chars);
}
static DEVICE_ATTR_RO(num_chars);
static ssize_t scroll_step_ms_show(struct device *dev,
struct device_attribute *attr, char *buf)
{
struct linedisp *linedisp = to_linedisp(dev);
return sysfs_emit(buf, "%u\n", jiffies_to_msecs(linedisp->scroll_rate));
}
static ssize_t scroll_step_ms_store(struct device *dev,
struct device_attribute *attr,
const char *buf, size_t count)
{
struct linedisp *linedisp = to_linedisp(dev);
unsigned int ms;
int err;
err = kstrtouint(buf, 10, &ms);
if (err)
return err;
timer_delete_sync(&linedisp->timer);
linedisp->scroll_rate = msecs_to_jiffies(ms);
if (should_scroll(linedisp))
linedisp_scroll(&linedisp->timer);
return count;
}
static DEVICE_ATTR_RW(scroll_step_ms);
static ssize_t map_seg_show(struct device *dev, struct device_attribute *attr, char *buf)
{
struct linedisp *linedisp = to_linedisp(dev);
struct linedisp_map *map = linedisp->map;
memcpy(buf, &map->map, map->size);
return map->size;
}
static ssize_t map_seg_store(struct device *dev, struct device_attribute *attr,
const char *buf, size_t count)
{
struct linedisp *linedisp = to_linedisp(dev);
struct linedisp_map *map = linedisp->map;
if (count != map->size)
return -EINVAL;
memcpy(&map->map, buf, count);
return count;
}
static const SEG7_DEFAULT_MAP(initial_map_seg7);
static DEVICE_ATTR(map_seg7, 0644, map_seg_show, map_seg_store);
static const SEG14_DEFAULT_MAP(initial_map_seg14);
static DEVICE_ATTR(map_seg14, 0644, map_seg_show, map_seg_store);
static struct attribute *linedisp_attrs[] = {
&dev_attr_message.attr,
&dev_attr_num_chars.attr,
&dev_attr_scroll_step_ms.attr,
&dev_attr_map_seg7.attr,
&dev_attr_map_seg14.attr,
NULL
};
static umode_t linedisp_attr_is_visible(struct kobject *kobj, struct attribute *attr, int n)
{
struct device *dev = kobj_to_dev(kobj);
struct linedisp *linedisp = to_linedisp(dev);
struct linedisp_map *map = linedisp->map;
umode_t mode = attr->mode;
if (attr == &dev_attr_map_seg7.attr) {
if (!map)
return 0;
if (map->type != LINEDISP_MAP_SEG7)
return 0;
}
if (attr == &dev_attr_map_seg14.attr) {
if (!map)
return 0;
if (map->type != LINEDISP_MAP_SEG14)
return 0;
}
return mode;
};
static const struct attribute_group linedisp_group = {
.is_visible = linedisp_attr_is_visible,
.attrs = linedisp_attrs,
};
__ATTRIBUTE_GROUPS(linedisp);
static DEFINE_IDA(linedisp_id);
static void linedisp_release(struct device *dev)
{
struct linedisp *linedisp = to_linedisp(dev);
kfree(linedisp->map);
kfree(linedisp->message);
kfree(linedisp->buf);
ida_free(&linedisp_id, linedisp->id);
}
static const struct device_type linedisp_type = {
.groups = linedisp_groups,
.release = linedisp_release,
};
static int linedisp_init_map(struct linedisp *linedisp)
{
struct linedisp_map *map;
int err;
if (!linedisp->ops->get_map_type)
return 0;
err = linedisp->ops->get_map_type(linedisp);
if (err < 0)
return err;
map = kmalloc(sizeof(*map), GFP_KERNEL);
if (!map)
return -ENOMEM;
map->type = err;
switch (map->type) {
case LINEDISP_MAP_SEG7:
map->map.seg7 = initial_map_seg7;
map->size = sizeof(map->map.seg7);
break;
case LINEDISP_MAP_SEG14:
map->map.seg14 = initial_map_seg14;
map->size = sizeof(map->map.seg14);
break;
default:
kfree(map);
return -EINVAL;
}
linedisp->map = map;
return 0;
}
#ifdef CONFIG_PANEL_BOOT_MESSAGE
#define LINEDISP_INIT_TEXT CONFIG_PANEL_BOOT_MESSAGE
#else
#define LINEDISP_INIT_TEXT "Linux " UTS_RELEASE " "
#endif
int linedisp_attach(struct linedisp *linedisp, struct device *dev,
unsigned int num_chars, const struct linedisp_ops *ops)
{
int err;
memset(linedisp, 0, sizeof(*linedisp));
linedisp->ops = ops;
linedisp->num_chars = num_chars;
linedisp->scroll_rate = DEFAULT_SCROLL_RATE;
linedisp->buf = kzalloc(linedisp->num_chars, GFP_KERNEL);
if (!linedisp->buf)
return -ENOMEM;
err = linedisp_init_map(linedisp);
if (err)
goto out_free_buf;
timer_setup(&linedisp->timer, linedisp_scroll, 0);
err = create_attachment(dev, linedisp, true);
if (err)
goto out_del_timer;
err = linedisp_display(linedisp, LINEDISP_INIT_TEXT, -1);
if (err)
goto out_del_attach;
err = device_add_groups(dev, linedisp_groups);
if (err)
goto out_del_attach;
return 0;
out_del_attach:
delete_attachment(dev, true);
out_del_timer:
timer_delete_sync(&linedisp->timer);
out_free_buf:
kfree(linedisp->buf);
return err;
}
EXPORT_SYMBOL_NS_GPL(linedisp_attach, "LINEDISP");
void linedisp_detach(struct device *dev)
{
struct linedisp *linedisp;
linedisp = delete_attachment(dev, true);
if (!linedisp)
return;
timer_delete_sync(&linedisp->timer);
device_remove_groups(dev, linedisp_groups);
kfree(linedisp->map);
kfree(linedisp->message);
kfree(linedisp->buf);
}
EXPORT_SYMBOL_NS_GPL(linedisp_detach, "LINEDISP");
int linedisp_register(struct linedisp *linedisp, struct device *parent,
unsigned int num_chars, const struct linedisp_ops *ops)
{
int err;
memset(linedisp, 0, sizeof(*linedisp));
linedisp->dev.parent = parent;
linedisp->dev.type = &linedisp_type;
linedisp->ops = ops;
linedisp->num_chars = num_chars;
linedisp->scroll_rate = DEFAULT_SCROLL_RATE;
err = ida_alloc(&linedisp_id, GFP_KERNEL);
if (err < 0)
return err;
linedisp->id = err;
device_initialize(&linedisp->dev);
dev_set_name(&linedisp->dev, "linedisp.%u", linedisp->id);
err = -ENOMEM;
linedisp->buf = kzalloc(linedisp->num_chars, GFP_KERNEL);
if (!linedisp->buf)
goto out_put_device;
err = linedisp_init_map(linedisp);
if (err)
goto out_put_device;
timer_setup(&linedisp->timer, linedisp_scroll, 0);
err = create_attachment(&linedisp->dev, linedisp, false);
if (err)
goto out_del_timer;
err = linedisp_display(linedisp, LINEDISP_INIT_TEXT, -1);
if (err)
goto out_del_attach;
err = device_add(&linedisp->dev);
if (err)
goto out_del_attach;
return 0;
out_del_attach:
delete_attachment(&linedisp->dev, false);
out_del_timer:
timer_delete_sync(&linedisp->timer);
out_put_device:
put_device(&linedisp->dev);
return err;
}
EXPORT_SYMBOL_NS_GPL(linedisp_register, "LINEDISP");
void linedisp_unregister(struct linedisp *linedisp)
{
device_del(&linedisp->dev);
delete_attachment(&linedisp->dev, false);
timer_delete_sync(&linedisp->timer);
put_device(&linedisp->dev);
}
EXPORT_SYMBOL_NS_GPL(linedisp_unregister, "LINEDISP");
MODULE_DESCRIPTION("Character line display core support");
MODULE_LICENSE("GPL");